diff --git a/.gitignore b/.gitignore index 1cb95a223b..ddac1d2de6 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,7 @@ sphinx/build/ website/build/ website/i18n/ website/node_modules/ +website/.docusaurus/ ## Generated for tutorials website/_tutorials/ @@ -104,6 +105,5 @@ website/pages/tutorials/* ## Generated for Sphinx website/pages/api/ website/static/js/* -!website/static/js/mathjax.js !website/static/js/code_block_buttons.js website/static/_sphinx-sources/ diff --git a/docs/README.md b/docs/README.md index f29e360c28..e0f8404b92 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,2 +1,5 @@ +--- + +--- This directory contains the source files for BoTorch's Docusaurus documentation. See the website's [README](../website/README.md) for additional information. diff --git a/docs/acquisition.md b/docs/acquisition.md index 761a79e096..4444871407 100644 --- a/docs/acquisition.md +++ b/docs/acquisition.md @@ -36,7 +36,9 @@ functions that consider multiple design points jointly (i.e. $q > 1$). An alternative is to use Monte-Carlo (MC) sampling to approximate the integrals. An MC approximation of $\alpha$ at $X$ using $N$ MC samples is -$$ \alpha(X) \approx \frac{1}{N} \sum_{i=1}^N a(\xi_{i}) $$ +$$ +\alpha(X) \approx \frac{1}{N} \sum_{i=1}^N a(\xi_{i}) +$$ where $\xi_i \sim \mathbb{P}(f(X) \mid \mathcal{D})$. @@ -44,17 +46,17 @@ For instance, for q-Expected Improvement (qEI), we have: $$ \text{qEI}(X) \approx \frac{1}{N} \sum_{i=1}^N \max_{j=1,..., q} -\bigl\\{ \max(\xi_{ij} - f^\*, 0) \bigr\\}, +\bigl\{ \max(\xi_{ij} - f^*, 0) \bigr\}, \qquad \xi_{i} \sim \mathbb{P}(f(X) \mid \mathcal{D}) $$ -where $f^\*$ is the best function value observed so far (assuming noiseless +where $f^*$ is the best function value observed so far (assuming noiseless observations). Using the reparameterization trick ([^KingmaWelling2014], [^Rezende2014]), $$ \text{qEI}(X) \approx \frac{1}{N} \sum_{i=1}^N \max_{j=1,..., q} -\bigl\\{ \max\bigl( \mu(X)\_j + (L(X) \epsilon_i)\_j - f^\*, 0 \bigr) \bigr\\}, +\bigl\{ \max\bigl( \mu(X)\_j + (L(X) \epsilon_i)\_j - f^*, 0 \bigr) \bigr\}, \qquad \epsilon_{i} \sim \mathcal{N}(0, I) $$ @@ -65,10 +67,10 @@ All MC-based acquisition functions in BoTorch are derived from [`MCAcquisitionFunction`](../api/acquisition.html#mcacquisitionfunction). Acquisition functions expect input tensors $X$ of shape -$\textit{batch_shape} \times q \times d$, where $d$ is the dimension of the +$\textit{batch\_shape} \times q \times d$, where $d$ is the dimension of the feature space, $q$ is the number of points considered jointly, and -$\textit{batch_shape}$ is the batch-shape of the input tensor. The output -$\alpha(X)$ will have shape $\textit{batch_shape}$, with each element +$\textit{batch\_shape}$ is the batch-shape of the input tensor. The output +$\alpha(X)$ will have shape $\textit{batch\_shape}$, with each element corresponding to the respective $q \times d$ batch tensor in the input $X$. Note that for analytic acquisition functions, it must be that $q=1$. @@ -135,15 +137,19 @@ summary statistics of the posterior distribution at the evaluated point(s). A popular acquisition function is Expected Improvement of a single point for a Gaussian posterior, given by -$$ \text{EI}(x) = \mathbb{E}\bigl[ +$$ +\text{EI}(x) = \mathbb{E}\bigl[ \max(y - f^\*, 0) \mid y\sim \mathcal{N}(\mu(x), \sigma^2(x)) -\bigr] $$ +\bigr] +$$ where $\mu(x)$ and $\sigma(x)$ are the posterior mean and variance of $f$ at the -point $x$, and $f^\*$ is again the best function value observed so far (assuming +point $x$, and $f^*$ is again the best function value observed so far (assuming noiseless observations). It can be shown that -$$ \text{EI}(x) = \sigma(x) \bigl( z \Phi(z) + \varphi(z) \bigr)$$ +$$ +\text{EI}(x) = \sigma(x) \bigl( z \Phi(z) + \varphi(z) \bigr) +$$ where $z = \frac{\mu(x) - f_{\max}}{\sigma(x)}$ and $\Phi$ and $\varphi$ are the cdf and pdf of the standard normal distribution, respectively. diff --git a/docs/getting_started.md b/docs/getting_started.mdx similarity index 92% rename from docs/getting_started.md rename to docs/getting_started.mdx index a17f0f5e02..0b2f9b73f8 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.mdx @@ -3,6 +3,9 @@ id: getting_started title: Getting Started --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + This section shows you how to get your feet wet with BoTorch. Before jumping the gun, we recommend you start with the high-level @@ -17,16 +20,22 @@ BoTorch is easily installed via `pip` (recommended). It is also possible to use the (unofficial) [Anaconda](https://www.anaconda.com/distribution/#download-section) package from the `-c conda-forge` channel. - - + + + ```bash pip install botorch ``` - + + + + ```bash conda install botorch -c gpytorch -c conda-forge ``` - + + + For more installation options and detailed instructions, please see the [Project Readme](https://github.com/pytorch/botorch/blob/main/README.md) diff --git a/docs/objectives.md b/docs/objectives.md index 584a527531..961993eeaf 100644 --- a/docs/objectives.md +++ b/docs/objectives.md @@ -43,7 +43,7 @@ inputs to a `sample_shape x batch_shape x q`-dimensional tensor of sampled objective values. For instance, say you have a multi-output model with $o=2$ outputs, and you want -to optimize a $obj(y) = 1 - \\|y - y_0\\|_2$, where $y_0 \in \mathbb{R}^2$. +to optimize a $obj(y) = 1 - \|y - y_0\|_2$, where $y_0 \in \mathbb{R}^2$. For this you would use the following custom objective (here we can ignore the inputs $X$ as the objective does not depend on it): ```python diff --git a/website/.yarnrc b/website-old/.yarnrc similarity index 100% rename from website/.yarnrc rename to website-old/.yarnrc diff --git a/website/README.md b/website-old/README.md similarity index 100% rename from website/README.md rename to website-old/README.md diff --git a/website-old/_tutorials/GIBBON_for_efficient_batch_entropy_search.html b/website-old/_tutorials/GIBBON_for_efficient_batch_entropy_search.html new file mode 100644 index 0000000000..89d0b43b3d --- /dev/null +++ b/website-old/_tutorials/GIBBON_for_efficient_batch_entropy_search.html @@ -0,0 +1,366 @@ + + + +
+
+
+
+

The GIBBON (General-purpose Information-Based Bayesian OptimisatioN) acquisition function

A particularly intuitive and empirically effective class of acquisition functions has arisen based on information theory. Information-theoretic Bayesian Optimisation (BO) seeks to reduce uncertainty in the location of high-performing areas of the search space, as measured in terms of differential entropy. BoTorch already supports information-theoretic BO through an implementation of the Max-value Entropy Search (MES) acquisition function [1] (see the Max-Value Entropy tutorial for details), which makes evaluations that reduce uncertainty in the maximum value attained by the objective function. However, in order to support batch and multi-fidelity BO, our implementation of MES employs numerical integrations and fantasy observations (i. e., we generate one point each time and when we try to generate the 𝑖-th point of a batch, we condition the models on the 𝑖−1 points generated prior to this). Unfortunately, Each of these calculations can can add significantly to the computational overhead incurred by BO.

+

In this notebook, we provide an information-theoretic acquisition function for tasks where objective function query costs are not large enough to overshadow significant optimisation overheads known as General-purpose Information-Based Bayesian OptimisatioN (GIBBON) [2]. In this tutorial, we present a very high-level overview of GIBBON and demonstrate its use within BoTorch.

+

Calculating GIBBON

Following a principled information-theoretic construction, the GIBBON acquisition function measures the utility of evaluating a candidate batch of $B$ points $\{\textbf{x}\}_{i=1}^B$ as +\begin{align} + \alpha_{\text{GIBBON}}(\{\textbf{x}\}_{i=1}^B) + &= \frac{1}{2}\log |C| + \sum_{i=1}^B \hat{\alpha}_{\text{MES}}(\textbf{x}_i) +\end{align} +where $|C|$ is the determinant of the $B\times B$ correlation matrix between the batch elements and $\hat{\alpha}_{\text{MES}}$ is an analytical approximation of the standard (non-batch) MES acquisition function. The GIBBON acquisition function forms a lower bound on the exact (but intractable) batch MES function and is consequently referred to as the qLowerBoundMaxValueEntropy in BoTorch. Crucially, GIBBON can be computed in closed-form and so incurs substantially lower overheads than batch MES via fantasies.

+

Interpretating GIBBON

Note that the above decomposition of GIBBON has two terms and each has a helpful intuitive justification. In particular, the first term encourages diversity within the batch (achieving high values for points with low predictive correlation), whereas the second term ensures that evaluations are targeted in areas of the search space that provide large amounts of information about the maximum value attained by the objective function.

+
+__References__ + +

[1] Wang, Z., Jegelka, S., Max-value Entropy Search for Efficient Bayesian Optimization. arXiv:1703.01968v3, 2018

+

[2] Moss, M., et al., GIBBON: General-purpose Information-Based Bayesian Optimisation. arXiv:2102.03324, 2020

+
+
+
+
+
+
In [1]:
+
+
+
import os
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

1. Setting up a toy model

We will fit a standard SingleTaskGP model on noisy observations of the synthetic 2D SixHumpCamel function.

+
+
+
+
+
+
In [2]:
+
+
+
import math
+import torch
+
+from botorch.test_functions import SixHumpCamel
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from botorch.utils.transforms import standardize, normalize
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+torch.manual_seed(123456)
+
+bounds = torch.tensor(SixHumpCamel._bounds).T
+bounds_norm = torch.tensor([[0.0, 0.0], [1.0, 1.0]])
+train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(5, 2)
+train_Y = SixHumpCamel(negate=True)(train_X).unsqueeze(-1)
+
+train_X = normalize(train_X, bounds=bounds)
+train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y))
+
+model = SingleTaskGP(train_X, train_Y)
+mll = ExactMarginalLogLikelihood(model.likelihood, model)
+fit_gpytorch_mll(mll, max_attempts=10);
+
+
+
+
+
+
+
+
+

2. Defining the GIBBON acquisition function

GIBBON is implemented in BoTorch as qLowerBoundMaxValueEntropy and supports pending points through its X_pending argument. Required arguments for the constructor are model and candidate_set (the discretized candidate points in the design space that will be used to draw max value samples). There are also other optional parameters, such as number of max value samples. Just like in our implementation of MES, two different sampling algorithms are supported for the max value samples: discretized Thompson sampling and Gumbel sampling (the default choice).

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.acquisition.max_value_entropy_search import qLowerBoundMaxValueEntropy
+
+candidate_set_size = 1000 if not SMOKE_TEST else 5
+candidate_set = torch.rand(
+    candidate_set_size, bounds_norm.size(1), device=bounds.device, dtype=bounds.dtype
+)
+qGIBBON = qLowerBoundMaxValueEntropy(model, candidate_set)
+
+
+
+
+
+
+
+
+

3. Optimizing the GIBBON acquisition function to get the next candidate points

In order to obtain the next candidate point(s) to query, we need to optimize the acquisition function over the design space. For $q=1$ case, we can simply call the optimize_acqf function in the library. For $q>1$, we greedily build batches using sequential optimization.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.optim import optimize_acqf
+
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+
+# for q = 1
+candidates, acq_value = optimize_acqf(
+    acq_function=qGIBBON,
+    bounds=bounds,
+    q=1,
+    num_restarts=NUM_RESTARTS,
+    raw_samples=RAW_SAMPLES,
+)
+candidates, acq_value
+
+
+
+
+
+
+
+
Out[4]:
+
+
(tensor([[ 0.1199, -0.0158]]), tensor(0.0085))
+
+
+
+
+
+
+
+
In [5]:
+
+
+
from botorch.optim import optimize_acqf
+
+# for q = 2, sequential optimsiation
+candidates, acq_value = optimize_acqf(
+    acq_function=qGIBBON,
+    bounds=bounds,
+    q=2,
+    num_restarts=NUM_RESTARTS,
+    raw_samples=RAW_SAMPLES,
+    sequential=True,
+)
+candidates, acq_value
+
+
+
+
+
+
+
+
Out[5]:
+
+
(tensor([[ 0.1194, -0.0160],
+         [ 1.4241,  0.4417]]),
+ tensor([0.0085, 0.0104]))
+
+
+
+
+
+
+
+
+

4. Comparing GIBBON with other acquisition functions

We now perform an illustrative comparison between GIBBON and the other low-cost acquisition functions implemented in BoTorch. We plot points chosen by each of the acquisition functions, each acquisition function's surface.

+
+
+
+
+
+
+

Sequential BO (q=1)

Firstly, we investigate GIBBON in the purely sequential case, comparing agaisnt MES, Expected Improvement (EI) and Probability of Improvement (PI). We see that GIBBON provides a very high-quality approximation of MES, choosing essentially the same location.

+
+
+
+
+
+
In [6]:
+
+
+
from botorch.acquisition import (
+    ExpectedImprovement,
+    ProbabilityOfImprovement,
+    qMaxValueEntropy,
+)
+import matplotlib.pyplot as plt
+
+%matplotlib inline
+
+# prep different acqusition functions
+acqs = {}
+candidate_set = torch.rand(
+    10000, bounds.size(1), device=bounds.device, dtype=bounds.dtype
+)
+acqs["GIBBON"] = qLowerBoundMaxValueEntropy(model, candidate_set)
+acqs["MES"] = qMaxValueEntropy(model, candidate_set)
+acqs["EI"] = ExpectedImprovement(model, best_f=train_Y.max())
+acqs["PI"] = ProbabilityOfImprovement(model, best_f=train_Y.max())
+
+# prep grid to evaluate acq functions
+n = 100 if not SMOKE_TEST else 2
+xv, yv = torch.meshgrid([torch.linspace(0, 1, n), torch.linspace(0, 1, n)])
+test_x = torch.stack([xv.reshape(n * n, 1), yv.reshape(n * n, 1)], -1)
+
+# eval and maximise acq functions
+evals = {}
+candidates = {}
+for acq in acqs.keys():
+    evals[acq] = acqs[acq](test_x).detach().reshape(n, n)
+    candidates[acq], _ = optimize_acqf(
+        acq_function=acqs[acq], bounds=bounds_norm, q=1, num_restarts=5, raw_samples=100
+    )
+
+# plot acqusition function values and chosen points
+fig, (ax1, ax2, ax3, ax4) = plt.subplots(
+    nrows=1, ncols=4, sharex=True, sharey=True, figsize=(10, 5)
+)
+ax1.contourf(xv.numpy(), yv.numpy(), evals["GIBBON"].numpy(), levels=20)
+ax1.scatter(candidates["GIBBON"][:, 0], candidates["GIBBON"][:, 1], marker="X", c="r")
+ax1.set_title("GIBBON")
+ax2.contourf(xv.numpy(), yv.numpy(), evals["MES"].numpy(), levels=20)
+ax2.scatter(candidates["MES"][:, 0], candidates["MES"][:, 1], marker="X", c="r")
+ax2.set_title("MES")
+ax3.contourf(xv.numpy(), yv.numpy(), evals["EI"].numpy(), levels=20)
+ax3.scatter(candidates["EI"][:, 0], candidates["EI"][:, 1], marker="X", c="r")
+ax3.set_title("EI")
+ax4.contourf(xv.numpy(), yv.numpy(), evals["PI"].numpy(), levels=20)
+ax4.scatter(candidates["PI"][:, 0], candidates["PI"][:, 1], marker="X", c="r")
+ax4.set_title("PI")
+fig.text(0.5, -0.1, "x_1", ha="center")
+fig.text(-0.1, 0.5, "x_2", va="center")
+
+
+
+
+
+
+
+
Out[6]:
+
+
Text(-0.1, 0.5, 'x_2')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Batch BO (q=3)

For the batch BO case, GIBBON selects similar points to MES but with an order-of-magnitude lower computational overhead, i.e perfoming information-theoretic BO at the cost of much simpler acqusition functions like EI and PI. We stress that this gap in computational overhead between GIBBON and MES grows substantially as the optimisation progresses (see [2]).

+
+
+
+
+
+
In [7]:
+
+
+
from botorch.acquisition import qNoisyExpectedImprovement, qProbabilityOfImprovement
+from time import time
+
+# prep different acqusition functions
+acqs = {}
+candidate_set = torch.rand(
+    10000, bounds.size(1), device=bounds.device, dtype=bounds.dtype
+)
+acqs["GIBBON"] = qLowerBoundMaxValueEntropy(model, candidate_set)
+acqs["MES"] = qMaxValueEntropy(model, candidate_set)
+acqs["EI"] = qNoisyExpectedImprovement(model, train_X)
+acqs["PI"] = qProbabilityOfImprovement(model, best_f=train_Y.max())
+
+# prep grid to evaluate acq functions
+n = 100 if not SMOKE_TEST else 2
+xv, yv = torch.meshgrid([torch.linspace(0, 1, n), torch.linspace(0, 1, n)])
+test_x = torch.stack([xv.reshape(n * n, 1), yv.reshape(n * n, 1)], -1)
+
+# eval and maximise acq functions
+evals = {}
+candidates = {}
+times = {}
+for acq in acqs.keys():
+    evals[acq] = acqs[acq](test_x).detach().reshape(n, n)
+    t_0 = time()
+    candidates[acq], _ = optimize_acqf(
+        acq_function=acqs[acq],
+        bounds=bounds_norm,
+        q=3,
+        num_restarts=5,
+        raw_samples=100,
+        sequential=True,
+    )
+    times[acq] = time() - t_0
+
+# plot acqusition function values and chosen points
+fig, (ax1, ax2, ax3, ax4) = plt.subplots(
+    nrows=1, ncols=4, sharex=True, sharey=True, figsize=(10, 5)
+)
+ax1.contourf(xv.numpy(), yv.numpy(), evals["GIBBON"].numpy(), levels=20)
+ax1.scatter(candidates["GIBBON"][:, 0], candidates["GIBBON"][:, 1], marker="X", c="r")
+ax1.set_title("GIBBON")
+ax2.contourf(xv.numpy(), yv.numpy(), evals["MES"].numpy(), levels=20)
+ax2.scatter(candidates["MES"][:, 0], candidates["MES"][:, 1], marker="X", c="r")
+ax2.set_title("MES")
+ax3.contourf(xv.numpy(), yv.numpy(), evals["EI"].numpy(), levels=20)
+ax3.scatter(candidates["EI"][:, 0], candidates["EI"][:, 1], marker="X", c="r")
+ax3.set_title("EI")
+ax4.contourf(xv.numpy(), yv.numpy(), evals["PI"].numpy(), levels=20)
+ax4.scatter(candidates["PI"][:, 0], candidates["PI"][:, 1], marker="X", c="r")
+ax4.set_title("PI")
+fig.text(0.5, -0.1, "x_1", ha="center")
+fig.text(-0.1, 0.5, "x_2", va="center")
+
+# plot computational overheads
+plt.figure()
+heights = [times[acq] for acq in acqs.keys()]
+plt.bar(acqs.keys(), heights)
+plt.ylabel("Computation Time")
+plt.xlabel("Acquisition Function")
+
+
+
+
+
+
+
+
Out[7]:
+
+
Text(0.5, 0, 'Acquisition Function')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/Multi_objective_multi_fidelity_BO.html b/website-old/_tutorials/Multi_objective_multi_fidelity_BO.html new file mode 100644 index 0000000000..ba81c5d884 --- /dev/null +++ b/website-old/_tutorials/Multi_objective_multi_fidelity_BO.html @@ -0,0 +1,993 @@ + + + +
+
+
+
+

Multi-fidelity Multi-Objective optimization

+
+
+
+
+
+
+

In this tutorial notebook we demonstrate how to perform multi-objective multi-fidelity optimization in BoTorch using the multi-fidelity Hypervolume Knowledge Gradient (MF-HVKG) [3] and a method called Multi-Objective Multi-Fidelity (MOMF) [1].

+

MF-HVKG performs one-step lookahead: it operates under the assumption that we can make one additional observation, and after receiving that additional observation, we will select the Pareto set of optimal designs. HVKG seeks to select the design x to evaluate that maximizes the value of information about the Pareto set by maximizing the hypervolume under the posterior mean (conditional on receiving on new observation for the design x).

+

MOMF is an alternative approach that introduces an additional "fidelity objective" that is optimized along with the problem objectives. This fidelity objective can be thought of as a trust objective that rewards the optimization when going to higher fidelity. Thus, the MOMF explicitly optimizes for getting more high-fidelity (trustworthy) data while taking into account the higher computational costs associated with it.

+

HVKG is generally more cost efficient [3], since it explicitly targets the goal of MF optimization: select design points and fidelities that enable identifying about the Pareto Frontier at the target fidelity in a cost-aware fashion. MOMF will typically result in faster candidate generation. If the application is high-throughput and requires fast candidate generation, MOMF will be preferable. Otherwise, MF-HVKG will likely give better sample efficiency and performance [3].

+

In this tutorial, we will optimize a synthetic function that is a modified multi-fidelity Branin-Currin [1]. This is a 3-dimesional, bi-objective problem with one of the input dimensions being the fidelity. For the MOMF, this results in a 3-objective problem since it also takes the fidelity objective into account. In this case the fidelity objective is a linear function of fidelity, $ f(s)=s$, where $s$ is the fidelity. The MOMF algorithm can accept any discrete or continuous cost functions as an input. In this example, we choose an exponential dependency of the form $C(s)=\exp(4.8s)$. The goal of the optimization is to find the Pareto front, which is a trade-off solution set for Multi-objective problems, at the highest fidelity.

+

Note: pymoo is an optional dependency that is used for determining the Pareto set of optimal designs under the model posterior mean using NSGA-II (which is not a sample efficient method, but sample efficiency is not critical for this step). If pymoo is not available, the Pareto set of optimal designs is selected from a discrete set. This will work okay for low-dim (e.g. +dimensions) problems, but in general NSGA-II will yield far better results.

+

[1] Irshad, Faran, Stefan Karsch, and Andreas Döpp. "Expected hypervolume improvement for simultaneous multi-objective and multi-fidelity optimization." arXiv preprint arXiv:2112.13901 (2021).

+

[2] S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives. NeurIPS, 2021.

+

[3] S. Daulton, M. Balandat, and E. Bakshy. Hypervolume Knowledge Gradient for Multi-Objective Bayesian Optimization with Partial Information. ICML, 2023.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+from typing import Callable, Dict
+
+import matplotlib.pyplot as plt
+import numpy as np
+import torch
+
+
+
+
+
+
+
+
+

Set dtype and device

Setting up the global variable that determine the device to run the optimization. The optimization is much faster when it runs on GPU.

+
+
+
+
+
+
In [2]:
+
+
+
tkwargs = {  # Tkwargs is a dictionary contaning data about data type and data device
+    "dtype": torch.double,
+    "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"),
+}
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Define the problem and optimization settings

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.test_functions.multi_objective_multi_fidelity import MOMFBraninCurrin
+
+BC = MOMFBraninCurrin(negate=True).to(**tkwargs)
+dim_x = BC.dim
+dim_y = BC.num_objectives
+
+ref_point = torch.zeros(dim_y, **tkwargs)
+
+
+BATCH_SIZE = 1  # For batch optimization, BATCH_SIZE should be greater than 1
+# This evaluation budget is set to be very low to make the notebook run fast. This should be much higher.
+EVAL_BUDGET = 2.05  # in terms of the number of full-fidelity evaluations
+n_INIT = 2  # Initialization budget in terms of the number of full-fidelity evaluations
+# Number of Monte Carlo samples, used to approximate MOMF
+MC_SAMPLES = 2 if SMOKE_TEST else 128
+# Number of restart points for multi-start optimization
+NUM_RESTARTS = 2 if SMOKE_TEST else 10
+# Number of raw samples for initial point selection heuristic
+RAW_SAMPLES = 4 if SMOKE_TEST else 512
+
+standard_bounds = torch.zeros(2, dim_x, **tkwargs)
+standard_bounds[1] = 1
+# mapping from index to target fidelity (highest fidelity)
+target_fidelities = {2: 1.0}
+
+
+
+
+
+
+
+
+
+
[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode
+
+
+
+
+
+
+
+
+
+

Helper functions to define Cost

The cost_func function returns an exponential cost from the fidelity. The cost_callable is a wrapper around it that takes care of the input output shapes. This is provided to the MF algorithms which inversely weight the expected utility by the cost.

+
+
+
+
+
+
In [4]:
+
+
+
from math import exp
+
+
+def cost_func(x):
+    """A simple exponential cost function."""
+    exp_arg = torch.tensor(4.8, **tkwargs)
+    val = torch.exp(exp_arg * x)
+    return val
+
+
+# Displaying the min and max costs for this optimization
+print(f"Min Cost: {cost_func(0)}")
+print(f"Max Cost: {cost_func(1)}")
+
+
+def cost_callable(X: torch.Tensor) -> torch.Tensor:
+    r"""Wrapper for the cost function that takes care of shaping
+    input and output arrays for interfacing with cost_func.
+    This is passed as a callable function to MOMF.
+
+    Args:
+        X: A `batch_shape x q x d`-dim Tensor
+    Returns:
+        Cost `batch_shape x q x m`-dim Tensor of cost generated
+        from fidelity dimension using cost_func.
+    """
+
+    return cost_func(X[..., -1:])
+
+
+
+
+
+
+
+
+
+
Min Cost: 1.0
+Max Cost: 121.51041751873485
+
+
+
+
+
+
+
+
+
+

Model Initialization

We use a multi-output SingleTaskGP to model the problem with a homoskedastic Gaussian likelihood with an inferred noise level. +The model is initialized with random points, where the fidelity is sampled from a probability distribution with a PDF that is inversely proportional to the cost: $p(s)=C(s)^{-1}$. The initialization is given a budget equivalent to 2 full-fidelity evaluations.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.utils.transforms import normalize
+from gpytorch.kernels import MaternKernel, ScaleKernel
+from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood
+from gpytorch.priors import GammaPrior
+
+
+def inv_transform(u):
+    # define inverse transform to sample from the probability distribution with
+    # PDF proportional to 1/(c(x))
+    # u is a uniform(0,1) rv
+    return 5 / 24 * torch.log(-exp(24 / 5) / (exp(24 / 5) * u - u - exp(24 / 5)))
+
+
+def gen_init_data(n: int):
+    r"""
+    Generates the initial data. Sample fidelities inversely proportional to cost.
+    """
+    # total cost budget is n
+    train_x = torch.empty(
+        0, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device
+    )
+    total_cost = 0
+    # assume target fidelity is 1
+    total_cost_limit = (
+        n
+        * cost_callable(
+            torch.ones(
+                1, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device
+            )
+        ).item()
+    )
+    while total_cost < total_cost_limit:
+        new_x = torch.rand(
+            1, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device
+        )
+        new_x[:, -1] = inv_transform(new_x[:, -1])
+        total_cost += cost_callable(new_x)
+        train_x = torch.cat([train_x, new_x], dim=0)
+    train_x = train_x[:-1]
+    train_obj = BC(train_x)
+    return train_x, train_obj
+
+
+def initialize_model(train_x, train_obj, state_dict=None):
+    """Initializes a ModelList with Matern 5/2 Kernel and returns the model and its MLL.
+
+    Note: a batched model could also be used here.
+    """
+    models = []
+    for i in range(train_obj.shape[-1]):
+        m = SingleTaskGP(
+            train_x,
+            train_obj[:, i : i + 1],
+            train_Yvar=torch.full_like(train_obj[:, i : i + 1], 1e-6),
+            outcome_transform=Standardize(m=1),
+            covar_module=ScaleKernel(
+                MaternKernel(
+                    nu=2.5,
+                    ard_num_dims=train_x.shape[-1],
+                    lengthscale_prior=GammaPrior(2.0, 2.0),
+                ),
+                outputscale_prior=GammaPrior(2.0, 0.15),
+            ),
+        )
+        models.append(m)
+    model = ModelListGP(*models)
+    mll = SumMarginalLogLikelihood(model.likelihood, model)
+    if state_dict is not None:
+        model.load_state_dict(state_dict=state_dict)
+    return mll, model
+
+
+
+
+
+
+
+
+

Helper function to optimize acquisition function

This is a helper function that initializes, optimizes the acquisition function MOMF and returns the new_x and new_obj. The problem is called from within this helper function.

+

A simple initialization heuristic is used to select the 20 restart initial locations from a set of 1024 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation.

+
+
+
+
+
+
In [6]:
+
+
+
from botorch.acquisition.multi_objective.multi_fidelity import MOMF
+from botorch.optim.optimize import optimize_acqf
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    FastNondominatedPartitioning,
+)
+from botorch.utils.transforms import unnormalize
+
+
+dim_y_momf = dim_y + 1  # Output Dimesnion for MOMF optimization
+ref_point_momf = torch.zeros(dim_y_momf, **tkwargs)
+
+
+def fid_obj(X: torch.Tensor) -> torch.Tensor:
+    """
+    A Fidelity Objective that can be thought of as a trust objective.
+    Higher Fidelity simulations are rewarded as being more
+    trustworthy. Here we consider just a linear fidelity objective.
+    """
+    fid_obj = 1 * X[..., -1]
+    return fid_obj
+
+
+def get_objective_momf(x: torch.Tensor) -> torch.Tensor:
+    """Wrapper around the Objective function to take care of fid_obj stacking"""
+    y = BC(x)  # The Branin-Currin is called
+    fid = fid_obj(x)  # Getting the fidelity objective values
+    fid_out = fid.unsqueeze(-1)
+    # Concatenating objective values with fid_objective
+    y_out = torch.cat([y, fid_out], -1)
+    return y_out
+
+
+def optimize_MOMF_and_get_obs(
+    model: SingleTaskGP,
+    train_obj: torch.Tensor,
+    sampler: SobolQMCNormalSampler,
+    ref_point: torch.Tensor,
+    standard_bounds: torch.Tensor,
+    BATCH_SIZE: int,
+    cost_call: Callable[[torch.Tensor], torch.Tensor],
+):
+    """
+    Wrapper to call MOMF and optimizes it in a sequential greedy
+    fashion returning a new candidate and evaluation
+    """
+    partitioning = FastNondominatedPartitioning(ref_point=ref_point, Y=train_obj)
+    acq_func = MOMF(
+        model=model,
+        ref_point=ref_point,  # use known reference point
+        partitioning=partitioning,
+        sampler=sampler,
+        cost_call=cost_call,
+    )
+    # Optimization
+    candidates, vals = optimize_acqf(
+        acq_function=acq_func,
+        bounds=standard_bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={
+            "batch_limit": 5,
+            "maxiter": 3 if SMOKE_TEST else 200,
+            "nonnegative": True,
+        },
+        sequential=True,
+    )
+    # if the AF val is 0, set the fidelity parameter to zero
+    if vals.item() == 0.0:
+        candidates[:, -1] = 0.0
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=standard_bounds)
+    new_obj = get_objective_momf(new_x)
+    return new_x, new_obj
+
+
+
+
+
+
+
+
+

Define helper functions for MF-HVKG

get_current_value optimizes the current posterior mean at the full fidelity to determine the hypervolume under the current model.

+

optimize_HVKG_and_get_obs creates the MF-HVKG acquisition function, optimizes it, and returns the new design and corresponding observation.

+
+
+
+
+
+
In [7]:
+
+
+
from botorch.acquisition.cost_aware import InverseCostWeightedUtility
+from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction
+from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import (
+    _get_hv_value_function,
+    qMultiFidelityHypervolumeKnowledgeGradient,
+)
+from botorch.acquisition.utils import project_to_target_fidelity
+from botorch.models.deterministic import GenericDeterministicModel
+from torch import Tensor
+
+NUM_INNER_MC_SAMPLES = 2 if SMOKE_TEST else 32
+NUM_PARETO = 1 if SMOKE_TEST else 10
+NUM_FANTASIES = 2 if SMOKE_TEST else 8
+
+
+def get_current_value(
+    model: SingleTaskGP,
+    ref_point: torch.Tensor,
+    bounds: torch.Tensor,
+    normalized_target_fidelities: Dict[int, float],
+):
+    """Helper to get the hypervolume of the current hypervolume
+    maximizing set.
+    """
+    fidelity_dims, fidelity_targets = zip(*normalized_target_fidelities.items())
+    # optimize
+    non_fidelity_dims = list(set(range(dim_x)) - set(fidelity_dims))
+    curr_val_acqf = FixedFeatureAcquisitionFunction(
+        acq_function=_get_hv_value_function(
+            model=model,
+            ref_point=ref_point,
+            sampler=SobolQMCNormalSampler(
+                sample_shape=torch.Size([NUM_INNER_MC_SAMPLES]),
+            ),
+            use_posterior_mean=True,
+        ),
+        d=dim_x,
+        columns=fidelity_dims,
+        values=fidelity_targets,
+    )
+    # optimize
+    _, current_value = optimize_acqf(
+        acq_function=curr_val_acqf,
+        bounds=bounds[:, non_fidelity_dims],
+        q=NUM_PARETO,
+        num_restarts=1,
+        raw_samples=2 * RAW_SAMPLES,
+        return_best_only=True,
+        options={
+            "nonnegative": True,
+            "maxiter": 3 if SMOKE_TEST else 200,
+        },
+    )
+    return current_value
+
+
+normalized_target_fidelities = {}
+for idx, fidelity in target_fidelities.items():
+    lb = standard_bounds[0, idx].item()
+    ub = standard_bounds[1, idx].item()
+    normalized_target_fidelities[idx] = (fidelity - lb) / (ub - lb)
+project_d = dim_x
+
+
+def project(X: Tensor) -> Tensor:
+
+    return project_to_target_fidelity(
+        X=X,
+        d=project_d,
+        target_fidelities=normalized_target_fidelities,
+    )
+
+
+def optimize_HVKG_and_get_obs(
+    model: SingleTaskGP,
+    ref_point: torch.Tensor,
+    standard_bounds: torch.Tensor,
+    BATCH_SIZE: int,
+    cost_call: Callable[[torch.Tensor], torch.Tensor],
+):
+    """Utility to initialize and optimize HVKG."""
+    cost_model = GenericDeterministicModel(cost_call)
+    cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)
+    current_value = get_current_value(
+        model=model,
+        ref_point=ref_point,
+        bounds=standard_bounds,
+        normalized_target_fidelities=normalized_target_fidelities,
+    )
+
+    acq_func = qMultiFidelityHypervolumeKnowledgeGradient(
+        model=model,
+        ref_point=ref_point,  # use known reference point
+        num_fantasies=NUM_FANTASIES,
+        num_pareto=NUM_PARETO,
+        current_value=current_value,
+        cost_aware_utility=cost_aware_utility,
+        target_fidelities=normalized_target_fidelities,
+        project=project,
+    )
+    # Optimization
+    candidates, vals = optimize_acqf(
+        acq_function=acq_func,
+        bounds=standard_bounds,
+        q=BATCH_SIZE,
+        num_restarts=1,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={
+            "batch_limit": 5,
+            "maxiter": 3 if SMOKE_TEST else 200,
+        },
+    )
+    # if the AF val is 0, set the fidelity parameter to zero
+    if vals.item() == 0.0:
+        candidates[:, -1] = 0.0
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=BC.bounds)
+    new_obj = BC(new_x)
+    return new_x, new_obj
+
+
+
+
+
+
+
+
+

Define helper functions for MF-HVKG

We run MOMF to optimize the multi-fidelity versions of the Branin-Currin functions. The optimization loop works in the following sequence.

+
    +
  1. At the start with an initialization equivalent to 2 full fidelity evaluations.
  2. +
  3. The models are used to generate an acquisition function that is optimized to select new input parameters
  4. +
  5. The objective function is evaluated at the suggested new_x and returns a new_obj.
  6. +
  7. The models are updated with the new points and then are used again to make the next prediction.
  8. +
+

The evaluation budget for the optimization is set to 4 full fidelity evaluations.

+

Note: running this takes some time.

+
+
+
+
+
+
In [8]:
+
+
+
from botorch import fit_gpytorch_mll
+
+
+
+
+
+
+
+
In [9]:
+
+
+
%%time
+
+# Intializing train_x to zero
+verbose = False
+torch.manual_seed(0)
+train_x_momf, _ = gen_init_data(n_INIT)
+train_obj_momf = get_objective_momf(train_x_momf)
+# Generate Sampler
+momf_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+
+# run N_BATCH rounds of BayesOpt after the initial random batch
+iteration = 0
+total_cost = cost_callable(train_x_momf).sum().item()
+while total_cost < EVAL_BUDGET * cost_func(1):
+    if verbose:
+        print(f"cost: {total_cost}")
+
+    # reinitialize the models so they are ready for fitting on next iteration
+    mll, model = initialize_model(normalize(train_x_momf, BC.bounds), train_obj_momf)
+
+    fit_gpytorch_mll(mll=mll)  # Fit the model
+
+    # optimize acquisition functions and get new observations
+    new_x, new_obj = optimize_MOMF_and_get_obs(
+        model=model,
+        train_obj=train_obj_momf,
+        sampler=momf_sampler,
+        ref_point=ref_point_momf,
+        standard_bounds=standard_bounds,
+        BATCH_SIZE=BATCH_SIZE,
+        cost_call=cost_callable,
+    )
+    # Updating train_x and train_obj
+    train_x_momf = torch.cat([train_x_momf, new_x], dim=0)
+    train_obj_momf = torch.cat([train_obj_momf, new_obj], dim=0)
+    iteration += 1
+    total_cost += cost_callable(new_x).sum().item()
+
+
+
+
+
+
+
+
+
+
/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+
+
+
+
+
+
+
CPU times: user 1min 7s, sys: 238 ms, total: 1min 7s
+Wall time: 19.5 s
+
+
+
+
+
+
+
+
+
+

Run MF-HVKG

+
+
+
+
+
+
In [10]:
+
+
+
%%time
+
+torch.manual_seed(0)
+train_x_kg, train_obj_kg = gen_init_data(n_INIT)
+MF_n_INIT = train_x_kg.shape[0]
+iteration = 0
+total_cost = cost_callable(train_x_kg).sum().item()
+while total_cost < EVAL_BUDGET * cost_func(1):
+    if verbose:
+        print(f"cost: {total_cost}")
+
+    # reinitialize the models so they are ready for fitting on next iteration
+    mll, model = initialize_model(normalize(train_x_kg, BC.bounds), train_obj_kg)
+
+    fit_gpytorch_mll(mll=mll)  # Fit the model
+    # optimize acquisition functions and get new observations
+    new_x, new_obj = optimize_HVKG_and_get_obs(
+        model=model,
+        ref_point=ref_point,
+        standard_bounds=standard_bounds,
+        BATCH_SIZE=BATCH_SIZE,
+        cost_call=cost_callable,
+    )
+    # Updating train_x and train_obj
+    train_x_kg = torch.cat([train_x_kg, new_x], dim=0)
+    train_obj_kg = torch.cat([train_obj_kg, new_obj], dim=0)
+    iteration += 1
+    total_cost += cost_callable(new_x).sum().item()
+
+
+
+
+
+
+
+
+
+
/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+
+
+
+
+
+
+
CPU times: user 16min 30s, sys: 3.03 s, total: 16min 33s
+Wall time: 4min 36s
+
+
+
+
+
+
+
+
+
+

Result: Evaluating the Pareto front at the highest fidelity using NSGA-II on the posterior mean

+
+
+
+
+
+
In [11]:
+
+
+
from botorch.utils.multi_objective.pareto import (
+    _is_non_dominated_loop,
+    is_non_dominated,
+)
+from gpytorch import settings
+
+try:
+    # Note: These are the pymoo 0.6+ imports, if you happen to be stuck on
+    # an older pymoo version you need to replace them with the ones below.
+    from pymoo.algorithms.moo.nsga2 import NSGA2
+    from pymoo.core.problem import Problem
+    from pymoo.optimize import minimize
+    from pymoo.termination.max_gen import MaximumGenerationTermination
+
+    # from pymoo.algorithms.nsga2 import NSGA2
+    # from pymoo.model.problem import Problem
+    # from pymoo.util.termination.max_gen import MaximumGenerationTermination
+
+    def get_pareto(
+        model,
+        non_fidelity_indices,
+        project,
+        population_size=20 if SMOKE_TEST else 250,
+        max_gen=10 if SMOKE_TEST else 100,
+        is_mf_model=True,
+    ):
+        """Optimize the posterior mean using NSGA-II."""
+        tkwargs = {
+            "dtype": BC.ref_point.dtype,
+            "device": BC.ref_point.device,
+        }
+        dim = len(non_fidelity_indices)
+
+        class PosteriorMeanPymooProblem(Problem):
+            def __init__(self):
+                super().__init__(
+                    n_var=dim,
+                    n_obj=BC.num_objectives,
+                    type_var=np.double,
+                )
+                self.xl = np.zeros(dim)
+                self.xu = np.ones(dim)
+
+            def _evaluate(self, x, out, *args, **kwargs):
+                X = torch.from_numpy(x).to(**tkwargs)
+                if is_mf_model:
+                    X = project(X)
+                with torch.no_grad():
+                    with settings.cholesky_max_tries(9):
+                        # eval in batch mode
+                        y = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2)
+                out["F"] = -y.cpu().numpy()
+
+        pymoo_problem = PosteriorMeanPymooProblem()
+        algorithm = NSGA2(
+            pop_size=population_size,
+            eliminate_duplicates=True,
+        )
+        res = minimize(
+            pymoo_problem,
+            algorithm,
+            termination=MaximumGenerationTermination(max_gen),
+            seed=0,  # fix seed
+            verbose=False,
+        )
+        X = torch.tensor(
+            res.X,
+            **tkwargs,
+        )
+        # project to full fidelity
+        if is_mf_model:
+            if project is not None:
+                X = project(X)
+        # determine Pareto set of designs under model
+        with torch.no_grad():
+            preds = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2)
+        pareto_mask = is_non_dominated(preds)
+        X = X[pareto_mask]
+        # evaluate Pareto set of designs on true function and compute hypervolume
+        if not is_mf_model:
+            X = project(X)
+        X = unnormalize(X, BC.bounds)
+        Y = BC(X)
+        # compute HV
+        partitioning = FastNondominatedPartitioning(ref_point=BC.ref_point, Y=Y)
+        return partitioning.compute_hypervolume().item()
+
+except ImportError:
+    NUM_DISCRETE_POINTS = 10 if SMOKE_TEST else 100000
+    CHUNK_SIZE = 512
+
+    def get_pareto(
+        model,
+        non_fidelity_indices,
+        project,
+        population_size=20 if SMOKE_TEST else 250,
+        max_gen=10 if SMOKE_TEST else 100,
+        is_mf_model=True,
+    ):
+        """Optimize the posterior mean over a discrete set."""
+        tkwargs = {
+            "dtype": BC.ref_point.dtype,
+            "device": BC.ref_point.device,
+        }
+        dim_x = BC.dim
+
+        discrete_set = torch.rand(NUM_DISCRETE_POINTS, dim_x - 1, **tkwargs)
+        if is_mf_model:
+            discrete_set = project(discrete_set)
+        discrete_set[:, -1] = 1.0  # set to target fidelity
+        with torch.no_grad():
+            preds_list = []
+            for start in range(0, NUM_DISCRETE_POINTS, CHUNK_SIZE):
+                preds = model.posterior(
+                    discrete_set[start : start + CHUNK_SIZE].unsqueeze(-2)
+                ).mean.squeeze(-2)
+                preds_list.append(preds)
+            preds = torch.cat(preds_list, dim=0)
+            pareto_mask = _is_non_dominated_loop(preds)
+            pareto_X = discrete_set[pareto_mask]
+        if not is_mf_model:
+            pareto_X = project(pareto_X)
+        pareto_X = unnormalize(pareto_X, BC.bounds)
+        Y = BC(pareto_X)
+        # compute HV
+        partitioning = FastNondominatedPartitioning(ref_point=BC.ref_point, Y=Y)
+        return partitioning.compute_hypervolume().item()
+
+
+
+
+
+
+
+
+

Evaluate MF-HVKG

We evaluate performance after every 5 evaluations (this is to speed things up, since there are many observations).

+
+
+
+
+
+
In [12]:
+
+
+
%%time
+
+hvs_kg = []
+costs = []
+for i in range(MF_n_INIT, train_x_kg.shape[0] + 1, 5):
+
+    mll, model = initialize_model(
+        normalize(train_x_kg[:i], BC.bounds), train_obj_kg[:i]
+    )
+    fit_gpytorch_mll(mll)
+    hypervolume = get_pareto(model, project=project, non_fidelity_indices=[0, 1])
+    hvs_kg.append(hypervolume)
+    costs.append(cost_callable(train_x_kg[:i]).sum().item())
+
+
+
+
+
+
+
+
+
+
/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+
+
+
+
+
+
+
CPU times: user 1min 20s, sys: 240 ms, total: 1min 21s
+Wall time: 30.6 s
+
+
+
+
+
+
+
+
+
+

Evaluate MOMF

We evaluate performance after every evaluation (there are not as many evaluations since MOMF queries higher fidelities more frequently).

+
+
+
+
+
+
In [13]:
+
+
+
%%time
+
+hvs_momf = []
+costs_momf = []
+for i in range(MF_n_INIT, train_x_momf.shape[0] + 1):
+
+    mll, model = initialize_model(
+        normalize(train_x_momf[:i], BC.bounds), train_obj_momf[:i, :2]
+    )
+    fit_gpytorch_mll(mll)
+    hypervolume = get_pareto(model, project=project, non_fidelity_indices=[0, 1])
+    hvs_momf.append(hypervolume)
+    costs_momf.append(cost_callable(train_x_momf[:i]).sum().item())
+
+
+
+
+
+
+
+
+
+
/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+  warnings.warn(
+
+
+
+
+
+
+
CPU times: user 1min 18s, sys: 260 ms, total: 1min 19s
+Wall time: 28.4 s
+
+
+
+
+
+
+
+
+
+

Plot log inference hypervolume regret (under the model) vs cost

Log inference hypervolume regret, defined as the logarithm of the difference between the maximum hypervolume dominated by the Pareto frontier and the hypervolume corresponding to the Pareto set identified by each algorithm, is a performance evaluation criterion for multi-information source multi-objective optimization [3].

+
+
+
+
+
+
In [14]:
+
+
+
plt.plot(
+    costs_momf,
+    np.log10(BC.max_hv - np.array(hvs_momf)),
+    "--",
+    marker="s",
+    ms=10,
+    label="MOMF",
+)
+plt.plot(
+    costs, np.log10(BC.max_hv - np.array(hvs_kg)), "--", marker="d", ms=10, label="HVKG"
+)
+plt.ylabel("Log Inference Hypervolume Regret")
+plt.xlabel("Cost")
+plt.legend()
+
+
+
+
+
+
+
+
Out[14]:
+
+
<matplotlib.legend.Legend at 0x7f2b5e484550>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/batch_mode_cross_validation.html b/website-old/_tutorials/batch_mode_cross_validation.html new file mode 100644 index 0000000000..a001a7b1b9 --- /dev/null +++ b/website-old/_tutorials/batch_mode_cross_validation.html @@ -0,0 +1,334 @@ + + + +
+
+
+
+

Application of batch-mode regression to cross-validation

botorch provides a helper function gen_loo_cv_folds to easily perform leave-one-out (LOO) cross-validation (CV) by taking advantage of batch-mode regression and evaluation in GPyTorch. This tutorial illustrates the process on a noisy sinusoidal function, similar to the example from the batch-mode GP regression tutorial from GPyTorch:

+

$$y = \sin(2\pi x) + \epsilon, ~\epsilon \sim \mathcal N(0, 0.2).$$

+

Note: this tutorial aims to introduce batch-mode regression and evaluation in GPyTorch with CV as an example application. For alternative, more user-friendly functions to perform CV in Ax, see ax.modelbridge.cross_validation. However, for larger CV tasks, it may be useful to exploit GPyTorch batch-mode, as shown in this tutorial.

+
+
+
+
+
+
In [1]:
+
+
+
import torch
+import math
+
+device = torch.device("cpu")
+dtype = torch.float64
+torch.manual_seed(3);
+
+
+
+
+
+
+
+
+

Initialize the CV dataset

For our training data, we take 20 regularly spaced points on the interval $[0, 1]$ and generate noisy evaluations with an observed noise variance of 0.2. Remember that botorch requires an explicit output dimension.

+
+
+
+
+
+
In [2]:
+
+
+
sigma = math.sqrt(0.2)
+train_X = torch.linspace(0, 1, 20, dtype=dtype, device=device).view(-1, 1)
+train_Y_noiseless = torch.sin(train_X * (2 * math.pi))
+train_Y = train_Y_noiseless + sigma * torch.randn_like(train_Y_noiseless)
+train_Yvar = torch.full_like(train_Y, 0.2)
+
+
+
+
+
+
+
+
+

The botorch function gen_loo_cv_folds takes our observed data train_X, train_Y, train_Yvar as input and returns the LOO CV folds in a CVFolds object.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.cross_validation import gen_loo_cv_folds
+
+cv_folds = gen_loo_cv_folds(train_X=train_X, train_Y=train_Y, train_Yvar=train_Yvar)
+
+
+
+
+
+
+
+
+
+
[KeOps] Warning : omp.h header is not in the path, disabling OpenMP. To fix this, you can set the environment
+                  variable OMP_PATH to the location of the header before importing keopscore or pykeops,
+                  e.g. using os.environ: import os; os.environ['OMP_PATH'] = '/path/to/omp/header'
+[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode
+
+
+
+
+
+
+
+
+
+

The cv_folds object contains the data, stored as tensors of appropriate batch shape, necessary to perform 20 CVs of 19 training points and 1 test point. For example, we can check that the shapes of the training inputs and training targets are b x n x d = 20 x 19 x 1 and b x n x o = 20 x 19 x 1 respectively, where o is the number of outputs.

+
+
+
+
+
+
In [4]:
+
+
+
cv_folds.train_X.shape, cv_folds.train_Y.shape
+
+
+
+
+
+
+
+
Out[4]:
+
+
(torch.Size([20, 19, 1]), torch.Size([20, 19, 1]))
+
+
+
+
+
+
+
+
In [5]:
+
+
+
cv_folds.test_X.shape, cv_folds.test_Y.shape
+
+
+
+
+
+
+
+
Out[5]:
+
+
(torch.Size([20, 1, 1]), torch.Size([20, 1, 1]))
+
+
+
+
+
+
+
+
+

Note that in a situation where the dataset is large, one may not want to perform LOO; in that case, a similar process can be used to perform $k$-fold CV.

+
+
+
+
+
+
+

Perform LOOCV

We can use the batch_cross_validation function to perform LOOCV using batching (meaning that the b = 20 sets of training data can be fit as b = 20 separate GP models with separate hyperparameters in parallel through GPyTorch) and return a CVResult tuple with the batched GPyTorchPosterior object over the LOOCV test points and the observed targets. The batch_cross_validation requires a model class (model_cls) and a marginal log likelihood class (mll_cls). We will use the SingleTaskGP as the model_cls and an ExactMarginalLogLikelihood as the mll_cls.

+
+
+
+
+
+
In [6]:
+
+
+
from botorch.cross_validation import batch_cross_validation
+from botorch.models import SingleTaskGP
+from botorch.models.transforms.input import Normalize
+from botorch.models.transforms.outcome import Standardize
+from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
+
+input_transform = Normalize(d=train_X.shape[-1])
+outcome_transform = Standardize(
+    m=train_Y.shape[-1],
+    batch_shape=cv_folds.train_Y.shape[:-2],
+)
+
+# instantiate and fit model
+cv_results = batch_cross_validation(
+    model_cls=SingleTaskGP,
+    mll_cls=ExactMarginalLogLikelihood,
+    cv_folds=cv_folds,
+    model_init_kwargs={
+        "input_transform": input_transform,
+        "outcome_transform": outcome_transform,
+    },
+)
+
+
+
+
+
+
+
+
+

Compute the cross-validation error and generate plots

To compute the cross-validation error, we first evaluate the test points by computing the posterior in batch mode. Next, we compute the squared errors for each test point from the prediction and take an average across all cross-validation folds.

+
+
+
+
+
+
In [7]:
+
+
+
from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+posterior = cv_results.posterior
+mean = posterior.mean
+cv_error = ((cv_folds.test_Y.squeeze() - mean.squeeze()) ** 2).mean()
+print(f"Cross-validation error: {cv_error : 4.2}")
+
+# get lower and upper confidence bounds
+lower, upper = posterior.mvn.confidence_region()
+
+# scatterplot of predicted versus test
+_, axes = plt.subplots(1, 1, figsize=(6, 4))
+plt.plot([-1.5, 1.5], [-1.5, 1.5], "k", label="true objective", linewidth=2)
+
+axes.set_xlabel("Actual")
+axes.set_ylabel("Predicted")
+
+axes.errorbar(
+    x=cv_folds.test_Y.numpy().flatten(),
+    y=mean.numpy().flatten(),
+    xerr=1.96 * sigma,
+    yerr=((upper - lower) / 2).numpy().flatten(),
+    fmt="*",
+);
+
+
+
+
+
+
+
+
+
+
Cross-validation error:  0.11
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Finally, we can visualize the fitted models. To do this, we again take advantage of batch-mode evaluation to obtain predictions, including lower and upper confidence regions, from each of the 20 models.

+
+
+
+
+
+
In [8]:
+
+
+
model = cv_results.model
+with torch.no_grad():
+    # evaluate the models at a series of points for plotting
+    plot_x = (
+        torch.linspace(0, 1, 101).view(1, -1, 1).repeat(cv_folds.train_X.shape[0], 1, 1)
+    )
+    posterior = model.posterior(plot_x)
+    mean = posterior.mean
+
+    # get lower and upper confidence bounds
+    lower, upper = posterior.mvn.confidence_region()
+    plot_x.squeeze_()
+
+
+
+
+
+
+
+
+

The code snippet below plots the result for the 12th CV fold (by setting num = 12), but note that we have computed the results for all folds above (other plots can be obtained by iterating num from 1 to 20).

+
+
+
+
+
+
In [9]:
+
+
+
_, axes = plt.subplots(1, 1, figsize=(6, 4))
+
+# plot the 12th CV fold
+num = 12
+
+# plot the training data in black
+axes.plot(
+    cv_folds.train_X[num - 1].detach().numpy(),
+    cv_folds.train_Y[num - 1].detach().numpy(),
+    "k*",
+)
+
+# plot the test data in red
+axes.plot(
+    cv_folds.test_X[num - 1].detach().numpy(),
+    cv_folds.test_Y[num - 1].detach().numpy(),
+    "r*",
+)
+
+# plot posterior means as blue line
+axes.plot(plot_x[num - 1].numpy(), mean[num - 1].numpy(), "b")
+
+# shade between the lower and upper confidence bounds
+axes.fill_between(
+    plot_x[num - 1].numpy(), lower[num - 1].numpy(), upper[num - 1].numpy(), alpha=0.5
+);
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/baxus.html b/website-old/_tutorials/baxus.html new file mode 100644 index 0000000000..4fe9767952 --- /dev/null +++ b/website-old/_tutorials/baxus.html @@ -0,0 +1,1505 @@ + + + +
+
+
+
+

BO with BAxUS and TS/EI

In this tutorial, we show how to implement Bayesian optimization with adaptively expanding subspaces (BAxUS) [1] in a closed loop in BoTorch. +The tutorial is purposefully similar to the TuRBO tutorial to highlight the differences in the implementations.

+

This implementation supports either Expected Improvement (EI) or Thompson sampling (TS). We optimize the Branin2 function [2] with 498 dummy dimensions and show that BAxUS outperforms EI as well as Sobol.

+

Since BoTorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_{x\in \mathcal{X}} -f(x)=0$.

+ +
+
+
+
+
+
In [1]:
+
+
+
import math
+import os
+from dataclasses import dataclass
+
+import botorch
+import gpytorch
+import matplotlib.pyplot as plt
+import numpy as np
+import torch
+from gpytorch.constraints import Interval
+from gpytorch.kernels import MaternKernel, ScaleKernel
+from gpytorch.likelihoods import GaussianLikelihood
+from gpytorch.mlls import ExactMarginalLogLikelihood
+from torch.quasirandom import SobolEngine
+
+from botorch.acquisition.analytic import LogExpectedImprovement
+from botorch.exceptions import ModelFittingError
+from botorch.fit import fit_gpytorch_mll
+from botorch.generation import MaxPosteriorSampling
+from botorch.models import SingleTaskGP
+from botorch.optim import optimize_acqf
+from botorch.test_functions import Branin
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+print(f"Running on {device}")
+dtype = torch.double
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+
+
Running on cpu
+
+
+
+
+
+
+
+
+
+

Optimize the augmented Branin function

The goal is to minimize the embedded Branin function

+

$f(x_1, x_2, \ldots, x_{20}) = \left (x_2-\frac{5.1}{4\pi^2}x_1^2+\frac{5}{\pi}x_1-6\right )^2+10\cdot \left (1-\frac{1}{8\pi}\right )\cos(x_1)+10$

+

with bounds [-5, 10] for $x_1$ and [0, 15] for $x_2$ (all other dimensions are ignored). The function has three minima with an optimal value of $0.397887$.

+

As mentioned above, since botorch assumes a maximization problem, we instead maximize $-f(x)$.

+
+
+
+
+
+
+

Define a function with dummy variables

We first define a new function where we only pass the first two input dimensions to the actual Branin function.

+
+
+
+
+
+
In [2]:
+
+
+
branin = Branin(negate=True).to(device=device, dtype=dtype)
+
+
+def branin_emb(x):
+    """x is assumed to be in [-1, 1]^D"""
+    lb, ub = branin.bounds
+    return branin(lb + (ub - lb) * (x[..., :2] + 1) / 2)
+
+
+
+
+
+
+
+
In [3]:
+
+
+
fun = branin_emb
+dim = 500 if not SMOKE_TEST else 50
+
+n_init = 10 if not SMOKE_TEST else 4
+max_cholesky_size = float("inf")  # Always use Cholesky
+
+
+
+
+
+
+
+
+

Maintain the BAxUS state

BAxUS needs to maintain a state, which includes the length of the trust region, success and failure counters, success and failure tolerance, etc. +In contrast to TuRBO, the failure tolerance depends on the target dimensionality.

+

In this tutorial we store the state in a dataclass and update the state of TuRBO after each batch evaluation.

+

Note: These settings assume that the domain has been scaled to $[-1, 1]^d$

+
+
+
+
+
+
In [4]:
+
+
+
@dataclass
+class BaxusState:
+    dim: int
+    eval_budget: int
+    new_bins_on_split: int = 3
+    d_init: int = float("nan")  # Note: post-initialized
+    target_dim: int = float("nan")  # Note: post-initialized
+    n_splits: int = float("nan")  # Note: post-initialized
+    length: float = 0.8
+    length_init: float = 0.8
+    length_min: float = 0.5**7
+    length_max: float = 1.6
+    failure_counter: int = 0
+    success_counter: int = 0
+    success_tolerance: int = 3
+    best_value: float = -float("inf")
+    restart_triggered: bool = False
+
+    def __post_init__(self):
+        n_splits = round(math.log(self.dim, self.new_bins_on_split + 1))
+        self.d_init = 1 + np.argmin(
+            np.abs(
+                (1 + np.arange(self.new_bins_on_split))
+                * (1 + self.new_bins_on_split) ** n_splits
+                - self.dim
+            )
+        )
+        self.target_dim = self.d_init
+        self.n_splits = n_splits
+
+    @property
+    def split_budget(self) -> int:
+        return round(
+            -1
+            * (self.new_bins_on_split * self.eval_budget * self.target_dim)
+            / (self.d_init * (1 - (self.new_bins_on_split + 1) ** (self.n_splits + 1)))
+        )
+
+    @property
+    def failure_tolerance(self) -> int:
+        if self.target_dim == self.dim:
+            return self.target_dim
+        k = math.floor(math.log(self.length_min / self.length_init, 0.5))
+        split_budget = self.split_budget
+        return min(self.target_dim, max(1, math.floor(split_budget / k)))
+
+
+def update_state(state, Y_next):
+    if max(Y_next) > state.best_value + 1e-3 * math.fabs(state.best_value):
+        state.success_counter += 1
+        state.failure_counter = 0
+    else:
+        state.success_counter = 0
+        state.failure_counter += 1
+
+    if state.success_counter == state.success_tolerance:  # Expand trust region
+        state.length = min(2.0 * state.length, state.length_max)
+        state.success_counter = 0
+    elif state.failure_counter == state.failure_tolerance:  # Shrink trust region
+        state.length /= 2.0
+        state.failure_counter = 0
+
+    state.best_value = max(state.best_value, max(Y_next).item())
+    if state.length < state.length_min:
+        state.restart_triggered = True
+    return state
+
+
+
+
+
+
+
+
+

Create a BAxUS embedding

We now show how to create the BAxUS embedding. The essential idea is to assign input dimensions to target dimensions and to assign a sign $\in \pm 1$ to each input dimension, similar to the HeSBO embedding. +We create the embedding matrix that is used to project points from the target to the input space. The matrix is sparse, each column has precisely one non-zero entry that is either 1 or -1.

+
+
+
+
+
+
In [5]:
+
+
+
def embedding_matrix(input_dim: int, target_dim: int) -> torch.Tensor:
+    if (
+        target_dim >= input_dim
+    ):  # return identity matrix if target size greater than input size
+        return torch.eye(input_dim, device=device, dtype=dtype)
+
+    input_dims_perm = (
+        torch.randperm(input_dim, device=device) + 1
+    )  # add 1 to indices for padding column in matrix
+
+    bins = torch.tensor_split(
+        input_dims_perm, target_dim
+    )  # split dims into almost equally-sized bins
+    bins = torch.nn.utils.rnn.pad_sequence(
+        bins, batch_first=True
+    )  # zero pad bins, the index 0 will be cut off later
+
+    mtrx = torch.zeros(
+        (target_dim, input_dim + 1), dtype=dtype, device=device
+    )  # add one extra column for padding
+    mtrx = mtrx.scatter_(
+        1,
+        bins,
+        2 * torch.randint(2, (target_dim, input_dim), dtype=dtype, device=device) - 1,
+    )  # fill mask with random +/- 1 at indices
+
+    return mtrx[:, 1:]  # cut off index zero as this corresponds to zero padding
+
+
+embedding_matrix(10, 3)  # example for an embedding matrix
+
+
+
+
+
+
+
+
Out[5]:
+
+
tensor([[ 1.,  0.,  1.,  1.,  0.,  0.,  0.,  0.,  0., -1.],
+        [ 0.,  0.,  0.,  0.,  1.,  0.,  1.,  0., -1.,  0.],
+        [ 0., -1.,  0.,  0.,  0.,  1.,  0., -1.,  0.,  0.]],
+       dtype=torch.float64)
+
+
+
+
+
+
+
+
+

Function to increase the embedding

Next, we write a helper function to increase the embedding and to bring observations to the increased target space.

+
+
+
+
+
+
In [6]:
+
+
+
def increase_embedding_and_observations(
+    S: torch.Tensor, X: torch.Tensor, n_new_bins: int
+) -> torch.Tensor:
+    assert X.size(1) == S.size(0), "Observations don't lie in row space of S"
+
+    S_update = S.clone()
+    X_update = X.clone()
+
+    for row_idx in range(len(S)):
+        row = S[row_idx]
+        idxs_non_zero = torch.nonzero(row)
+        idxs_non_zero = idxs_non_zero[torch.randperm(len(idxs_non_zero))].reshape(-1)
+
+        if len(idxs_non_zero) <= 1:
+            continue
+
+        non_zero_elements = row[idxs_non_zero].reshape(-1)
+
+        n_row_bins = min(
+            n_new_bins, len(idxs_non_zero)
+        )  # number of new bins is always less or equal than the contributing input dims in the row minus one
+
+        new_bins = torch.tensor_split(idxs_non_zero, n_row_bins)[
+            1:
+        ]  # the dims in the first bin won't be moved
+        elements_to_move = torch.tensor_split(non_zero_elements, n_row_bins)[1:]
+
+        new_bins_padded = torch.nn.utils.rnn.pad_sequence(
+            new_bins, batch_first=True
+        )  # pad the tuples of bins with zeros to apply _scatter
+        els_to_move_padded = torch.nn.utils.rnn.pad_sequence(
+            elements_to_move, batch_first=True
+        )
+
+        S_stack = torch.zeros(
+            (n_row_bins - 1, len(row) + 1), device=device, dtype=dtype
+        )  # submatrix to stack on S_update
+
+        S_stack = S_stack.scatter_(
+            1, new_bins_padded + 1, els_to_move_padded
+        )  # fill with old values (add 1 to indices for padding column)
+
+        S_update[
+            row_idx, torch.hstack(new_bins)
+        ] = 0  # set values that were move to zero in current row
+
+        X_update = torch.hstack(
+            (X_update, X[:, row_idx].reshape(-1, 1).repeat(1, len(new_bins)))
+        )  # repeat observations for row at the end of X (column-wise)
+        S_update = torch.vstack(
+            (S_update, S_stack[:, 1:])
+        )  # stack onto S_update except for padding column
+
+    return S_update, X_update
+
+
+
+
+
+
+
+
In [7]:
+
+
+
S = embedding_matrix(10, 2)
+X = torch.randint(100, (7, 2))
+print(f"S before increase\n{S}")
+print(f"X before increase\n{X}")
+
+S, X = increase_embedding_and_observations(S, X, 3)
+print(f"S after increase\n{S}")
+print(f"X after increase\n{X}")
+
+
+
+
+
+
+
+
+
+
S before increase
+tensor([[ 1.,  0.,  1., -1.,  1.,  0.,  0.,  0.,  0., -1.],
+        [ 0.,  1.,  0.,  0.,  0.,  1., -1.,  1., -1.,  0.]],
+       dtype=torch.float64)
+X before increase
+tensor([[66, 38],
+        [22,  2],
+        [19, 43],
+        [51, 10],
+        [16, 62],
+        [31, 25],
+        [27, 22]])
+S after increase
+tensor([[ 0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0., -1.],
+        [ 0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.,  0.],
+        [ 0.,  0.,  0., -1.,  1.,  0.,  0.,  0.,  0.,  0.],
+        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
+        [ 0.,  1.,  0.,  0.,  0.,  0.,  0.,  0., -1.,  0.],
+        [ 0.,  0.,  0.,  0.,  0.,  0., -1.,  0.,  0.,  0.]],
+       dtype=torch.float64)
+X after increase
+tensor([[66, 38, 66, 66, 38, 38],
+        [22,  2, 22, 22,  2,  2],
+        [19, 43, 19, 19, 43, 43],
+        [51, 10, 51, 51, 10, 10],
+        [16, 62, 16, 16, 62, 62],
+        [31, 25, 31, 31, 25, 25],
+        [27, 22, 27, 27, 22, 22]])
+
+
+
+
+
+
+
+
+
+

Take a look at the state

+
+
+
+
+
+
In [8]:
+
+
+
state = BaxusState(dim=dim, eval_budget=500)
+print(state)
+
+
+
+
+
+
+
+
+
+
BaxusState(dim=500, eval_budget=500, new_bins_on_split=3, d_init=2, target_dim=2, n_splits=4, length=0.8, length_init=0.8, length_min=0.0078125, length_max=1.6, failure_counter=0, success_counter=0, success_tolerance=3, best_value=-inf, restart_triggered=False)
+
+
+
+
+
+
+
+
+
+

Generate initial points

This generates an initial set of Sobol points that we use to start of the BO loop.

+
+
+
+
+
+
In [9]:
+
+
+
def get_initial_points(dim: int, n_pts: int, seed=0):
+    sobol = SobolEngine(dimension=dim, scramble=True, seed=seed)
+    X_init = (
+        2 * sobol.draw(n=n_pts).to(dtype=dtype, device=device) - 1
+    )  # points have to be in [-1, 1]^d
+    return X_init
+
+
+
+
+
+
+
+
+

Generate new batch

Given the current state and a probabilistic (GP) model built from observations X and Y, we generate a new batch of points.

+

This method works on the domain $[-1, +1]^d$, so make sure to not pass in observations from the true domain. unnormalize is called before the true function is evaluated which will first map the points back to the original domain.

+

We support either TS and qEI which can be specified via the acqf argument.

+
+
+
+
+
+
In [10]:
+
+
+
def create_candidate(
+    state,
+    model,  # GP model
+    X,  # Evaluated points on the domain [-1, 1]^d
+    Y,  # Function values
+    n_candidates=None,  # Number of candidates for Thompson sampling
+    num_restarts=10,
+    raw_samples=512,
+    acqf="ts",  # "ei" or "ts"
+):
+    assert acqf in ("ts", "ei")
+    assert X.min() >= -1.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))
+    if n_candidates is None:
+        n_candidates = min(5000, max(2000, 200 * X.shape[-1]))
+
+    # Scale the TR to be proportional to the lengthscales
+    x_center = X[Y.argmax(), :].clone()
+    weights = model.covar_module.lengthscale.detach().view(-1)
+    weights = weights / weights.mean()
+    weights = weights / torch.prod(weights.pow(1.0 / len(weights)))
+    tr_lb = torch.clamp(x_center - weights * state.length, -1.0, 1.0)
+    tr_ub = torch.clamp(x_center + weights * state.length, -1.0, 1.0)
+
+    if acqf == "ts":
+        dim = X.shape[-1]
+        sobol = SobolEngine(dim, scramble=True)
+        pert = sobol.draw(n_candidates).to(dtype=dtype, device=device)
+        pert = tr_lb + (tr_ub - tr_lb) * pert
+
+        # Create a perturbation mask
+        prob_perturb = min(20.0 / dim, 1.0)
+        mask = torch.rand(n_candidates, dim, dtype=dtype, device=device) <= prob_perturb
+        ind = torch.where(mask.sum(dim=1) == 0)[0]
+        mask[ind, torch.randint(0, dim, size=(len(ind),), device=device)] = 1
+
+        # Create candidate points from the perturbations and the mask
+        X_cand = x_center.expand(n_candidates, dim).clone()
+        X_cand[mask] = pert[mask]
+
+        # Sample on the candidate points
+        thompson_sampling = MaxPosteriorSampling(model=model, replacement=False)
+        with torch.no_grad():  # We don't need gradients when using TS
+            X_next = thompson_sampling(X_cand, num_samples=1)
+
+    elif acqf == "ei":
+        ei = LogExpectedImprovement(model, train_Y.max())
+        X_next, acq_value = optimize_acqf(
+            ei,
+            bounds=torch.stack([tr_lb, tr_ub]),
+            q=1,
+            num_restarts=num_restarts,
+            raw_samples=raw_samples,
+        )
+
+    return X_next
+
+
+
+
+
+
+
+
+

Optimization loop

This simple loop runs one instance of BAxUS with Thompson sampling until convergence.

+

BAxUS works on a fixed evaluation budget and shrinks the trust region until the minimal trust region size is reached (state["restart_triggered"] is set to True). +Then, BAxUS increases the target space and carries over the observations to the updated space.

+
+
+
+
+
+
In [11]:
+
+
+
EVALUATION_BUDGET = 100 if not SMOKE_TEST else 10
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+N_CANDIDATES = min(5000, max(2000, 200 * dim)) if not SMOKE_TEST else 4
+
+
+state = BaxusState(dim=dim, eval_budget=EVALUATION_BUDGET - n_init)
+S = embedding_matrix(input_dim=state.dim, target_dim=state.d_init)
+
+X_baxus_target = get_initial_points(state.d_init, n_init)
+X_baxus_input = X_baxus_target @ S
+Y_baxus = torch.tensor(
+    [branin_emb(x) for x in X_baxus_input], dtype=dtype, device=device
+).unsqueeze(-1)
+
+
+# Disable input scaling checks as we normalize to [-1, 1]
+with botorch.settings.validate_input_scaling(False):
+    for _ in range(EVALUATION_BUDGET - n_init):  # Run until evaluation budget depleted
+        # Fit a GP model
+        train_Y = (Y_baxus - Y_baxus.mean()) / Y_baxus.std()
+        likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))
+        model = SingleTaskGP(
+            X_baxus_target, train_Y, likelihood=likelihood
+        )
+        mll = ExactMarginalLogLikelihood(model.likelihood, model)
+
+        # Do the fitting and acquisition function optimization inside the Cholesky context
+        with gpytorch.settings.max_cholesky_size(max_cholesky_size):
+            # Fit the model
+            try:
+                fit_gpytorch_mll(mll)
+            except ModelFittingError:
+                # Right after increasing the target dimensionality, the covariance matrix becomes indefinite
+                # In this case, the Cholesky decomposition might fail due to numerical instabilities
+                # In this case, we revert to Adam-based optimization
+                optimizer = torch.optim.Adam([{"params": model.parameters()}], lr=0.1)
+
+                for _ in range(100):
+                    optimizer.zero_grad()
+                    output = model(X_baxus_target)
+                    loss = -mll(output, train_Y.flatten())
+                    loss.backward()
+                    optimizer.step()
+
+            # Create a batch
+            X_next_target = create_candidate(
+                state=state,
+                model=model,
+                X=X_baxus_target,
+                Y=train_Y,
+                n_candidates=N_CANDIDATES,
+                num_restarts=NUM_RESTARTS,
+                raw_samples=RAW_SAMPLES,
+                acqf="ts",
+            )
+
+        X_next_input = X_next_target @ S
+
+        Y_next = torch.tensor(
+            [branin_emb(x) for x in X_next_input], dtype=dtype, device=device
+        ).unsqueeze(-1)
+
+        # Update state
+        state = update_state(state=state, Y_next=Y_next)
+
+        # Append data
+        X_baxus_input = torch.cat((X_baxus_input, X_next_input), dim=0)
+        X_baxus_target = torch.cat((X_baxus_target, X_next_target), dim=0)
+        Y_baxus = torch.cat((Y_baxus, Y_next), dim=0)
+
+        # Print current status
+        print(
+            f"iteration {len(X_baxus_input)}, d={len(X_baxus_target.T)})  Best value: {state.best_value:.3}, TR length: {state.length:.3}"
+        )
+
+        if state.restart_triggered:
+            state.restart_triggered = False
+            print("increasing target space")
+            S, X_baxus_target = increase_embedding_and_observations(
+                S, X_baxus_target, state.new_bins_on_split
+            )
+            print(f"new dimensionality: {len(S)}")
+            state.target_dim = len(S)
+            state.length = state.length_init
+            state.failure_counter = 0
+            state.success_counter = 0
+
+
+
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 11, d=2)  Best value: -6.04, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 12, d=2)  Best value: -0.951, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 13, d=2)  Best value: -0.926, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 14, d=2)  Best value: -0.925, TR length: 0.8
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 15, d=2)  Best value: -0.925, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 16, d=2)  Best value: -0.925, TR length: 0.2
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 17, d=2)  Best value: -0.925, TR length: 0.1
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 18, d=2)  Best value: -0.925, TR length: 0.05
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 19, d=2)  Best value: -0.925, TR length: 0.025
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 20, d=2)  Best value: -0.925, TR length: 0.0125
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 21, d=2)  Best value: -0.925, TR length: 0.00625
+increasing target space
+new dimensionality: 6
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 22, d=6)  Best value: -0.475, TR length: 0.8
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 23, d=6)  Best value: -0.475, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 24, d=6)  Best value: -0.475, TR length: 0.2
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 25, d=6)  Best value: -0.475, TR length: 0.1
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 26, d=6)  Best value: -0.475, TR length: 0.05
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 27, d=6)  Best value: -0.466, TR length: 0.05
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 28, d=6)  Best value: -0.466, TR length: 0.05
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 29, d=6)  Best value: -0.458, TR length: 0.1
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 30, d=6)  Best value: -0.455, TR length: 0.1
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 31, d=6)  Best value: -0.444, TR length: 0.1
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 32, d=6)  Best value: -0.436, TR length: 0.2
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 33, d=6)  Best value: -0.423, TR length: 0.2
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 34, d=6)  Best value: -0.413, TR length: 0.2
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 35, d=6)  Best value: -0.408, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 36, d=6)  Best value: -0.401, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 37, d=6)  Best value: -0.399, TR length: 0.4
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 38, d=6)  Best value: -0.399, TR length: 0.2
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 39, d=6)  Best value: -0.399, TR length: 0.1
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 40, d=6)  Best value: -0.398, TR length: 0.1
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 41, d=6)  Best value: -0.398, TR length: 0.05
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 42, d=6)  Best value: -0.398, TR length: 0.025
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 43, d=6)  Best value: -0.398, TR length: 0.0125
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 44, d=6)  Best value: -0.398, TR length: 0.00625
+increasing target space
+new dimensionality: 18
+iteration 45, d=18)  Best value: -0.398, TR length: 0.4
+iteration 46, d=18)  Best value: -0.398, TR length: 0.2
+iteration 47, d=18)  Best value: -0.398, TR length: 0.1
+iteration 48, d=18)  Best value: -0.398, TR length: 0.05
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 49, d=18)  Best value: -0.398, TR length: 0.025
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 50, d=18)  Best value: -0.398, TR length: 0.0125
+
+
+
+
+
+
+
/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
iteration 51, d=18)  Best value: -0.398, TR length: 0.00625
+increasing target space
+new dimensionality: 54
+iteration 52, d=54)  Best value: -0.398, TR length: 0.4
+iteration 53, d=54)  Best value: -0.398, TR length: 0.2
+iteration 54, d=54)  Best value: -0.398, TR length: 0.1
+iteration 55, d=54)  Best value: -0.398, TR length: 0.05
+
+
+
+
+
+
+
/Users/balandat/Code/botorch/botorch/optim/fit.py:104: OptimizationWarning: `scipy_minimize` terminated with status OptimizationStatus.FAILURE, displaying original message from `scipy.optimize.minimize`: ABNORMAL_TERMINATION_IN_LNSRCH
+  warn(
+
+
+
+
+
+
+
iteration 56, d=54)  Best value: -0.398, TR length: 0.025
+iteration 57, d=54)  Best value: -0.398, TR length: 0.0125
+iteration 58, d=54)  Best value: -0.398, TR length: 0.00625
+increasing target space
+new dimensionality: 162
+iteration 59, d=162)  Best value: -0.398, TR length: 0.8
+iteration 60, d=162)  Best value: -0.398, TR length: 0.8
+iteration 61, d=162)  Best value: -0.398, TR length: 0.4
+iteration 62, d=162)  Best value: -0.398, TR length: 0.4
+iteration 63, d=162)  Best value: -0.398, TR length: 0.4
+iteration 64, d=162)  Best value: -0.398, TR length: 0.2
+iteration 65, d=162)  Best value: -0.398, TR length: 0.2
+iteration 66, d=162)  Best value: -0.398, TR length: 0.2
+iteration 67, d=162)  Best value: -0.398, TR length: 0.1
+iteration 68, d=162)  Best value: -0.398, TR length: 0.1
+iteration 69, d=162)  Best value: -0.398, TR length: 0.1
+iteration 70, d=162)  Best value: -0.398, TR length: 0.05
+iteration 71, d=162)  Best value: -0.398, TR length: 0.05
+iteration 72, d=162)  Best value: -0.398, TR length: 0.05
+iteration 73, d=162)  Best value: -0.398, TR length: 0.025
+iteration 74, d=162)  Best value: -0.398, TR length: 0.025
+iteration 75, d=162)  Best value: -0.398, TR length: 0.025
+iteration 76, d=162)  Best value: -0.398, TR length: 0.0125
+iteration 77, d=162)  Best value: -0.398, TR length: 0.0125
+iteration 78, d=162)  Best value: -0.398, TR length: 0.0125
+iteration 79, d=162)  Best value: -0.398, TR length: 0.00625
+increasing target space
+new dimensionality: 485
+iteration 80, d=485)  Best value: -0.398, TR length: 0.8
+iteration 81, d=485)  Best value: -0.398, TR length: 0.8
+iteration 82, d=485)  Best value: -0.398, TR length: 0.8
+iteration 83, d=485)  Best value: -0.398, TR length: 0.8
+iteration 84, d=485)  Best value: -0.398, TR length: 0.8
+iteration 85, d=485)  Best value: -0.398, TR length: 0.8
+iteration 86, d=485)  Best value: -0.398, TR length: 0.8
+iteration 87, d=485)  Best value: -0.398, TR length: 0.8
+iteration 88, d=485)  Best value: -0.398, TR length: 0.8
+iteration 89, d=485)  Best value: -0.398, TR length: 0.4
+iteration 90, d=485)  Best value: -0.398, TR length: 0.4
+iteration 91, d=485)  Best value: -0.398, TR length: 0.4
+iteration 92, d=485)  Best value: -0.398, TR length: 0.4
+iteration 93, d=485)  Best value: -0.398, TR length: 0.4
+iteration 94, d=485)  Best value: -0.398, TR length: 0.4
+iteration 95, d=485)  Best value: -0.398, TR length: 0.4
+iteration 96, d=485)  Best value: -0.398, TR length: 0.4
+iteration 97, d=485)  Best value: -0.398, TR length: 0.4
+iteration 98, d=485)  Best value: -0.398, TR length: 0.4
+iteration 99, d=485)  Best value: -0.398, TR length: 0.2
+iteration 100, d=485)  Best value: -0.398, TR length: 0.2
+
+
+
+
+
+
+
+
+
+

GP-LogEI

As a baseline, we compare BAxUS to Log Expected Improvement (LogEI)

+
+
+
+
+
+
In [12]:
+
+
+
X_ei = get_initial_points(dim, n_init)
+Y_ei = torch.tensor(
+    [branin_emb(x) for x in X_ei], dtype=dtype, device=device
+).unsqueeze(-1)
+bounds = torch.stack(
+    [
+        -torch.ones(dim, dtype=dtype, device=device),
+        torch.ones(dim, dtype=dtype, device=device),
+    ]
+)
+
+
+# Disable input scaling checks as we normalize to [-1, 1]
+with botorch.settings.validate_input_scaling(False):
+    while len(Y_ei) < len(Y_baxus):
+        train_Y = (Y_ei - Y_ei.mean()) / Y_ei.std()
+        likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))
+        model = SingleTaskGP(X_ei, train_Y, likelihood=likelihood)
+        mll = ExactMarginalLogLikelihood(model.likelihood, model)
+        optimizer = torch.optim.Adam([{"params": model.parameters()}], lr=0.1)
+        model.train()
+        model.likelihood.train()
+        for _ in range(50):
+            optimizer.zero_grad()
+            output = model(X_ei)
+            loss = -mll(output, train_Y.squeeze())
+            loss.backward()
+            optimizer.step()
+
+        # Create a batch
+        ei = LogExpectedImprovement(model, train_Y.max())
+        candidate, acq_value = optimize_acqf(
+            ei,
+            bounds=bounds,
+            q=1,
+            num_restarts=NUM_RESTARTS,
+            raw_samples=RAW_SAMPLES,
+        )
+        Y_next = torch.tensor(
+            [branin_emb(x) for x in candidate], dtype=dtype, device=device
+        ).unsqueeze(-1)
+
+        # Append data
+        X_ei = torch.cat((X_ei, candidate), axis=0)
+        Y_ei = torch.cat((Y_ei, Y_next), axis=0)
+
+        # Print current status
+        print(f"{len(X_ei)}) Best value: {Y_ei.max().item():.2e}")
+
+
+
+
+
+
+
+
+
+
11) Best value: -4.16e-01
+12) Best value: -4.16e-01
+13) Best value: -4.16e-01
+14) Best value: -4.16e-01
+15) Best value: -4.16e-01
+16) Best value: -4.16e-01
+17) Best value: -4.16e-01
+18) Best value: -4.16e-01
+19) Best value: -4.16e-01
+20) Best value: -4.16e-01
+21) Best value: -4.16e-01
+22) Best value: -4.16e-01
+23) Best value: -4.16e-01
+24) Best value: -4.16e-01
+25) Best value: -4.16e-01
+26) Best value: -4.16e-01
+27) Best value: -4.16e-01
+28) Best value: -4.16e-01
+29) Best value: -4.16e-01
+30) Best value: -4.16e-01
+31) Best value: -4.16e-01
+32) Best value: -4.16e-01
+33) Best value: -4.16e-01
+34) Best value: -4.16e-01
+35) Best value: -4.16e-01
+36) Best value: -4.16e-01
+37) Best value: -4.16e-01
+38) Best value: -4.16e-01
+39) Best value: -4.16e-01
+40) Best value: -4.16e-01
+41) Best value: -4.14e-01
+42) Best value: -4.14e-01
+43) Best value: -4.14e-01
+44) Best value: -4.14e-01
+45) Best value: -4.14e-01
+46) Best value: -4.14e-01
+47) Best value: -4.14e-01
+48) Best value: -4.14e-01
+49) Best value: -4.14e-01
+50) Best value: -4.14e-01
+51) Best value: -4.14e-01
+52) Best value: -4.14e-01
+53) Best value: -4.14e-01
+54) Best value: -4.14e-01
+55) Best value: -4.14e-01
+56) Best value: -4.14e-01
+57) Best value: -4.14e-01
+58) Best value: -4.14e-01
+59) Best value: -4.14e-01
+60) Best value: -4.14e-01
+61) Best value: -4.08e-01
+62) Best value: -4.08e-01
+63) Best value: -4.08e-01
+64) Best value: -4.08e-01
+65) Best value: -4.02e-01
+66) Best value: -4.02e-01
+67) Best value: -4.02e-01
+68) Best value: -4.02e-01
+69) Best value: -4.02e-01
+70) Best value: -4.02e-01
+71) Best value: -4.02e-01
+72) Best value: -4.02e-01
+73) Best value: -4.02e-01
+74) Best value: -4.02e-01
+75) Best value: -4.02e-01
+76) Best value: -4.02e-01
+77) Best value: -4.02e-01
+78) Best value: -4.02e-01
+79) Best value: -4.02e-01
+80) Best value: -4.02e-01
+81) Best value: -4.00e-01
+82) Best value: -4.00e-01
+83) Best value: -4.00e-01
+84) Best value: -4.00e-01
+85) Best value: -4.00e-01
+86) Best value: -4.00e-01
+87) Best value: -4.00e-01
+88) Best value: -4.00e-01
+89) Best value: -4.00e-01
+90) Best value: -4.00e-01
+91) Best value: -4.00e-01
+92) Best value: -4.00e-01
+93) Best value: -4.00e-01
+94) Best value: -4.00e-01
+95) Best value: -4.00e-01
+96) Best value: -4.00e-01
+97) Best value: -4.00e-01
+98) Best value: -4.00e-01
+99) Best value: -4.00e-01
+100) Best value: -4.00e-01
+
+
+
+
+
+
+
+
+
+

Sobol

+
+
+
+
+
+
In [13]:
+
+
+
X_Sobol = (
+    SobolEngine(dim, scramble=True, seed=0)
+    .draw(len(X_baxus_input))
+    .to(dtype=dtype, device=device)
+    * 2
+    - 1
+)
+Y_Sobol = torch.tensor(
+    [branin_emb(x) for x in X_Sobol], dtype=dtype, device=device
+).unsqueeze(-1)
+
+
+
+
+
+
+
+
+

Compare the methods

We show the regret of the different methods.

+
+
+
+
+
+
In [14]:
+
+
+
%matplotlib inline
+
+names = ["BAxUS", "EI", "Sobol"]
+runs = [Y_baxus, Y_ei, Y_Sobol]
+fig, ax = plt.subplots(figsize=(8, 6))
+
+for name, run in zip(names, runs):
+    fx = np.maximum.accumulate(run.cpu())
+    plt.plot(-fx + branin.optimal_value, marker="", lw=3)
+
+plt.ylabel("Regret", fontsize=18)
+plt.xlabel("Number of evaluations", fontsize=18)
+plt.title(f"{dim}D Embedded Branin", fontsize=24)
+plt.xlim([0, len(Y_baxus)])
+plt.yscale("log")
+
+plt.grid(True)
+plt.tight_layout()
+plt.legend(
+    names + ["Global optimal value"],
+    loc="lower center",
+    bbox_to_anchor=(0, -0.08, 1, 1),
+    bbox_transform=plt.gcf().transFigure,
+    ncol=4,
+    fontsize=16,
+)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/bo_with_warped_gp.html b/website-old/_tutorials/bo_with_warped_gp.html new file mode 100644 index 0000000000..635b0d8375 --- /dev/null +++ b/website-old/_tutorials/bo_with_warped_gp.html @@ -0,0 +1,362 @@ + + + +
+
+
+
+

BO with Warped Gaussian Processes

In this tutorial, we illustrate how to use learned input warping functions for robust Bayesian Optimization when the outcome may be non-stationary functions. When the lengthscales are non-stationarity in the raw input space, learning a warping function that maps raw inputs to a warped space where the lengthscales are stationary can be useful, because then standard stationary kernels can be used to effectively model the function.

+

In general, for a relatively simple setup (like this one), we recommend using Ax, since this will simplify your setup (including the amount of code you need to write) considerably. See the Using BoTorch with Ax tutorial. To use input warping with MODULAR_BOTORCH, we can pass the warp_tf, constructed as below, by adding input_transform=warp_tf argument to the Surrogate(...) call.

+

We consider use a Kumaraswamy CDF as the class of input warping function and learn the concentration parameters ($a>0$ and $b>0$). Kumaraswamy CDFs are quite flexible and map inputs in [0, 1] to outputs in [0, 1]. This work follows the Beta CDF input warping proposed by Snoek et al., but replaces the Beta distribution Kumaraswamy distribution, which has a differentiable and closed-form CDF.

+

$$K_\text{cdf}(x) = 1 - (1-x^a)^b$$

+

This enables maximum likelihood (or maximum a posteriori) estimation of the CDF hyperparameters using gradient methods to maximize the likelihood (or posterior probability) jointly with the GP hyperparameters. (Snoek et al. use a fully Bayesian treatment of the CDF parameters). Each input dimension is transformed using a separate warping function.

+

We use the Log Noisy Expected Improvement (qLogNEI) acquisition function to optimize a synthetic Hartmann6 test function. The standard problem is

+

$$f(x) = -\sum_{i=1}^4 \alpha_i \exp \left( -\sum_{j=1}^6 A_{ij} (x_j - P_{ij})^2 \right)$$

+

over $x \in [0,1]^6$ (parameter values can be found in botorch/test_functions/hartmann6.py). For this demonstration, +We first warp each input dimension through a different inverse Kumaraswamy CDF.

+

Since BoTorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_{x} -f(x) = 3.32237$.

+

[1] J. Snoek, K. Swersky, R. S. Zemel, R. P. Adams. Input Warping for Bayesian Optimization of Non-Stationary Functions. Proceedings of the 31st International Conference on Machine Learning, PMLR 32(2):1674-1682, 2014.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import torch
+
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Problem setup

First, we define the sample parameters for the sigmoid functions that transform the respective inputs.

+
+
+
+
+
+
In [2]:
+
+
+
from torch.distributions import Kumaraswamy
+import matplotlib.pyplot as plt
+
+%matplotlib inline
+
+
+fontdict = {"fontsize": 15}
+torch.manual_seed(1234567890)
+c1 = torch.rand(6, dtype=dtype, device=device) * 3 + 0.1
+c0 = torch.rand(6, dtype=dtype, device=device) * 3 + 0.1
+x = torch.linspace(0, 1, 101, dtype=dtype, device=device)
+k = Kumaraswamy(concentration1=c1, concentration0=c0)
+k_icdfs = k.icdf(x.unsqueeze(1).expand(101, 6))
+fig, ax = plt.subplots(1, 1, figsize=(5, 5))
+
+for i in range(6):
+    ax.plot(x.cpu(), k_icdfs[:, i].cpu())
+ax.set_xlabel("Raw Value", **fontdict)
+ax.set_ylabel("Transformed Value", **fontdict)
+
+
+
+
+
+
+
+
Out[2]:
+
+
Text(0, 0.5, 'Transformed Value')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [3]:
+
+
+
from botorch.test_functions import Hartmann
+
+neg_hartmann6 = Hartmann(negate=True)
+
+
+def obj(X):
+    X_warp = k.icdf(X)
+    return neg_hartmann6(X_warp)
+
+
+
+
+
+
+
+
+

Initial design

The models are initialized with 14 points in $[0,1]^6$ drawn from a scrambled sobol sequence.

+

We observe the objectives with additive Gaussian noise with a standard deviation of 0.05.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.models import SingleTaskGP
+from gpytorch.mlls.sum_marginal_log_likelihood import ExactMarginalLogLikelihood
+from botorch.utils.sampling import draw_sobol_samples
+
+NOISE_SE = 0.05
+train_yvar = torch.tensor(NOISE_SE**2, device=device, dtype=dtype)
+
+bounds = torch.tensor([[0.0] * 6, [1.0] * 6], device=device, dtype=dtype)
+
+
+n = 14
+# generate initial training data
+train_x = draw_sobol_samples(
+    bounds=bounds, n=n, q=1, seed=torch.randint(0, 10000, (1,)).item()
+).squeeze(1)
+exact_obj = obj(train_x).unsqueeze(-1)  # add output dimension
+
+best_observed_value = exact_obj.max().item()
+train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)
+
+
+
+
+
+
+
+
+

Input warping and model initialization

We initialize the Warp input transformation and pass it a SingleTaskGP to model the noiseless objective. The Warp object is a torch.nn.Module that contains the concentration parameters and applies the warping function in the Model's forward pass.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.models.transforms.input import Warp
+from gpytorch.priors.torch_priors import LogNormalPrior
+
+
+def initialize_model(train_x, train_obj):
+    # initialize input_warping transformation
+    warp_tf = Warp(
+        indices=list(range(train_x.shape[-1])),
+        # use a prior with median at 1.
+        # when a=1 and b=1, the Kumaraswamy CDF is the identity function
+        concentration1_prior=LogNormalPrior(0.0, 0.75**0.5),
+        concentration0_prior=LogNormalPrior(0.0, 0.75**0.5),
+    )
+    # define the model for objective
+    model = SingleTaskGP(
+        train_X=train_x,
+        train_Y=train_obj,
+        train_Yvar=train_yvar.expand_as(train_obj),
+        input_transform=warp_tf,
+    ).to(train_x)
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    return mll, model
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step

The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. For this example, we'll use sequential $q=1$ optimization. A simple initialization heuristic is used to select the 20 restart initial locations from a set of 512 random points.

+
+
+
+
+
+
In [6]:
+
+
+
from botorch.optim import optimize_acqf
+
+
+num_restarts = 20 if not SMOKE_TEST else 2
+raw_samples = 512 if not SMOKE_TEST else 32
+
+
+def optimize_acqf_and_get_observation(acq_func):
+    """Optimizes the acquisition function, and returns a new candidate and a noisy observation."""
+    # optimize
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        bounds=bounds,
+        q=1,
+        num_restarts=num_restarts,
+        raw_samples=raw_samples,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+    # observe new values
+    new_x = candidates.detach()
+    exact_obj = obj(new_x).unsqueeze(-1)  # add output dimension
+    train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)
+    return new_x, train_obj
+
+
+
+
+
+
+
+
+

Perform Bayesian Optimization

The Bayesian optimization loop iterates the following steps:

+
    +
  1. given a surrogate model, choose a candidate point $x$
  2. +
  3. observe $f(x)$
  4. +
  5. update the surrogate model.
  6. +
+

We do N_BATCH=50 rounds of optimization.

+
+
+
+
+
+
In [7]:
+
+
+
from botorch import fit_gpytorch_mll
+from botorch.acquisition.logei import qLogNoisyExpectedImprovement
+from botorch.exceptions import BadInitialCandidatesWarning
+
+import warnings
+
+warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning)
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+N_BATCH = 50 if not SMOKE_TEST else 5
+
+torch.manual_seed(0)
+
+best_observed = [best_observed_value]
+mll, model = initialize_model(train_x, train_obj)
+
+# run N_BATCH rounds of BayesOpt after the initial random batch
+for iteration in range(1, N_BATCH + 1):
+
+    # fit the models
+    fit_gpytorch_mll(mll)
+    ei = qLogNoisyExpectedImprovement(model=model, X_baseline=train_x)
+
+    # optimize and get new observation
+    new_x, new_obj = optimize_acqf_and_get_observation(ei)
+
+    # update training points
+    train_x = torch.cat([train_x, new_x])
+    train_obj = torch.cat([train_obj, new_obj])
+
+    # update progress
+    best_value = obj(train_x).max().item()
+    best_observed.append(best_value)
+
+    mll, model = initialize_model(train_x, train_obj)
+
+    print(".", end="")
+
+
+
+
+
+
+
+
+
+
..................................................
+
+
+
+
+
+
+
+
+

Plot the results

The plot below shows the log regret at each step of the optimization for each of the algorithms.

+
+
+
+
+
+
In [8]:
+
+
+
import numpy as np
+from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+
+GLOBAL_MAXIMUM = neg_hartmann6.optimal_value
+
+iters = np.arange(N_BATCH + 1)
+y_ei = np.log10(GLOBAL_MAXIMUM - np.asarray(best_observed))
+
+fig, ax = plt.subplots(1, 1, figsize=(8, 6))
+
+ax.plot(
+    iters,
+    y_ei,
+    linewidth=1.5,
+    alpha=0.6,
+)
+
+ax.set_xlabel("number of observations (beyond initial points)")
+ax.set_ylabel("Log10 Regret")
+
+
+
+
+
+
+
+
Out[8]:
+
+
<matplotlib.legend.Legend at 0x7fec08ec4eb0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/bope.html b/website-old/_tutorials/bope.html new file mode 100644 index 0000000000..96db78a013 --- /dev/null +++ b/website-old/_tutorials/bope.html @@ -0,0 +1,534 @@ + + + +
+
+
+
+

Bayesian Optimization with Preference Exploration

In this tutorial, we demonstrate how to implement a closed loop of Bayesian optimization with preference exploration, or BOPE [1]. +BOPE is designed for Bayesian optimization of expensive-to-evaluate experiments, +where the response surface function of the experiment $f_\mathrm{true}$ generates vector-valued outcomes over which a decision-maker (DM) has preferences. +These preferences are encoded by a utility function $g_\mathrm{true}$ that is not known in closed form but can be estimated by +asking the DM to express preferences over pairs of outcome vectors.

+

In other words, with BOPE, we wish to solve the following optimization problem:

+

$$ + \max_{x \in \mathcal{X}} g_\mathrm{true}(f_\mathrm{true}(x)) +$$

+

Unlike many other Bayesian optimization setups where multiple consecutive batches of experiments are performed, +in BOPE, we alternate between two stages: preference exploration and experimentation.

+

In the preference exploration stage, we use an acquisition function (i.e., a preference exploration strategy, or PE strategy) +to adaptively generate pairs of hypothetical outcome and ask the decision-maker’s preference within each pair. +In the experimentation stage, we use a batch version of noisy expected improvement that integrates over our uncertainty in the +utility function called $\text{qNEIUU}$ to generate experimental candidates for evaluation.

+

[1] Z.J. Lin, R. Astudillo, P.I. Frazier, and E. Bakshy, Preference Exploration for Efficient Bayesian Optimization with Multiple Outcomes. AISTATS, 2022.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+from typing import Optional, Tuple
+
+import matplotlib as mpl
+import matplotlib.pylab as plt
+import numpy as np
+import pandas as pd
+import torch
+from botorch.acquisition import (
+    GenericMCObjective,
+    LearnedObjective,
+    MCAcquisitionObjective,
+)
+from botorch.acquisition.logei import qLogNoisyExpectedImprovement
+from botorch.acquisition.monte_carlo import qSimpleRegret
+from botorch.acquisition.preference import AnalyticExpectedUtilityOfBestOption
+from botorch.fit import fit_gpytorch_mll
+from botorch.models.deterministic import FixedSingleSampleModel
+from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.pairwise_gp import PairwiseGP, PairwiseLaplaceMarginalLogLikelihood
+from botorch.models.transforms.input import Normalize
+from botorch.models.transforms.outcome import Standardize
+from botorch.optim.optimize import optimize_acqf
+from botorch.sampling import SobolQMCNormalSampler
+from botorch.test_functions.multi_objective import DTLZ2
+from botorch.utils.sampling import draw_sobol_samples
+from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
+
+
+%matplotlib inline
+
+# Set plotting colors
+colors = ["tab:blue", "tab:orange", "tab:green", "tab:red"]
+mpl.rcParams["axes.prop_cycle"] = mpl.cycler(color=colors)
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+
+
I0214 125107.545 font_manager.py:1550] generated new fontManager
+I0214 125107.837 _utils_internal.py:247] NCCL_DEBUG env var is set to None
+I0214 125107.838 _utils_internal.py:265] NCCL_DEBUG is forced to WARN from None
+
+
+
+
+
+
+
+
+
+

Problem Setup

In this tutorial, we use the DTLZ2 problem with d=5 inputs and k=4 outcomes as our test problem $f_\mathrm{true}$.

+

For the utility function $g_\mathrm{true}$, we use the negative L1 distance from a Pareto-optimal point the outcome space: +$Y^* = f(X^*)$ where $X^* = [0.5, 0.5, 0.5, 0.5, 0.5]$.

+
+
+
+
+
+
In [2]:
+
+
+
def neg_l1_dist(Y: torch.Tensor, X: Optional[torch.Tensor] = None) -> torch.Tensor:
+    """Negative L1 distance from a Pareto optimal points"""
+    if len(Y.shape) == 1:
+        Y = Y.unsqueeze(0)
+    dist = torch.cdist(
+        Y, torch.full(Y.shape[-1:], fill_value=0.5, dtype=Y.dtype).unsqueeze(0), p=1
+    ).squeeze(-1)
+    return -dist
+
+
+if SMOKE_TEST:
+    NUM_RESTARTS = 2
+    NUM_OUTCOME_SAMPLES = 2
+    RAW_SAMPLES = 4
+    BATCH_LIMIT = 2
+else:
+    NUM_RESTARTS = 8
+    # Since BOPE samples from both the outcome and preference models, we take
+    # fewer samples than usual from the outcome model for speed.
+    NUM_OUTCOME_SAMPLES = 32
+    RAW_SAMPLES = 64
+    BATCH_LIMIT = 4
+
+X_dim = 5
+Y_dim = 4
+problem = DTLZ2(dim=X_dim, num_objectives=Y_dim)
+util_func = neg_l1_dist
+
+
+
+
+
+
+
+
+

Here we define a collection of helper functions for BOPE:

+
+
+
+
+
+
In [3]:
+
+
+
def fit_outcome_model(X: torch.Tensor, Y: torch.Tensor) -> SingleTaskGP:
+    """Fit the outcome model f"""
+    outcome_model = SingleTaskGP(
+        train_X=X,
+        train_Y=Y,
+        input_transform=Normalize(d=X.shape[-1]),
+        outcome_transform=Standardize(m=Y.shape[-1]),
+    )
+    mll = ExactMarginalLogLikelihood(outcome_model.likelihood, outcome_model)
+    fit_gpytorch_mll(mll)
+    return outcome_model
+
+
+def fit_pref_model(Y: torch.Tensor, comps: torch.Tensor) -> PairwiseGP:
+    """Fit the preference model g."""
+    model = PairwiseGP(Y, comps, input_transform=Normalize(d=Y.shape[-1]))
+    mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+    return model
+
+
+def gen_rand_X(problem, n: int) -> torch.Tensor:
+    """Generate n quasi-random Sobol points in the design space."""
+    return draw_sobol_samples(bounds=problem.bounds, n=1, q=n).squeeze(0)
+
+
+def generate_random_exp_data(problem, n: int) -> Tuple[torch.Tensor, torch.Tensor]:
+    """Generate n observations of (X, Y) Pairs"""
+    X = gen_rand_X(problem, n)
+    Y = problem(X)
+    return X, Y
+
+
+def generate_random_pref_data(
+    outcome_model: SingleTaskGP, n: int
+) -> Tuple[torch.Tensor, torch.Tensor]:
+    """Generate n pairwise comparison data between 2n points."""
+    X = gen_rand_X(problem, 2 * n)
+    Y = outcome_model.posterior(X).sample().squeeze(0)
+    util = util_func(Y)
+    comps = gen_comps(util)
+    return Y, comps
+
+
+def gen_comps(util: torch.Tensor) -> torch.Tensor:
+    """Given an 1-d tensor of utility, create pairwise comparisons between adjacent items."""
+    util = util.reshape(-1, 2)
+    comps = torch.arange(util.numel()).reshape(-1, 2)
+    flip = util[:, 0] < util[:, 1]
+    comps[flip, [0]], comps[flip, [1]] = comps[flip, [1]], comps[flip, [0]]
+
+    return comps
+
+
+def run_pref_learn(
+    outcome_model: SingleTaskGP,
+    train_Y: torch.Tensor,
+    train_comps: torch.Tensor,
+    n_comps: int,
+    pe_strategy: str,
+    verbose: bool = False,
+) -> Tuple[torch.Tensor, torch.Tensor]:
+    """Perform preference exploration with a given PE strategy for n_comps rounds."""
+    for i in range(n_comps):
+        if verbose:
+            print(f"Running {i+1}/{n_comps} preference learning using {pe_strategy}")
+        pref_model = fit_pref_model(train_Y, train_comps)
+        if pe_strategy == "EUBO-zeta":
+            # EUBO-zeta
+            one_sample_outcome_model = FixedSingleSampleModel(model=outcome_model)
+            acqf = AnalyticExpectedUtilityOfBestOption(
+                pref_model=pref_model, outcome_model=one_sample_outcome_model
+            )
+            cand_X, acqf_val = optimize_acqf(
+                acq_function=acqf,
+                q=2,
+                bounds=problem.bounds,
+                num_restarts=NUM_RESTARTS,
+                raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+                options={"batch_limit": BATCH_LIMIT},
+            )
+            cand_Y = one_sample_outcome_model(cand_X)
+        elif pe_strategy == "Random-f":
+            # Random-f
+            cand_X = gen_rand_X(problem, n=2)
+            cand_Y = outcome_model.posterior(cand_X).sample().squeeze(0)
+        else:
+            raise RuntimeError("Unknown preference exploration strategy!")
+
+        cand_Y = cand_Y.detach().clone()
+        cand_comps = gen_comps(util_func(cand_Y))
+
+        train_comps = torch.cat((train_comps, cand_comps + train_Y.shape[0]))
+        train_Y = torch.cat((train_Y, cand_Y))
+
+    return train_Y, train_comps
+
+
+def gen_exp_cand(
+    outcome_model: SingleTaskGP,
+    objective: MCAcquisitionObjective,
+    q: int,
+    acqf_name: str,
+) -> torch.Tensor:
+    """Given an outcome model and an objective, generate q experimental candidates
+    using specified acquisition function."""
+    sampler = SobolQMCNormalSampler(sample_shape=torch.Size([NUM_OUTCOME_SAMPLES]))
+    if acqf_name == "qNEI":
+        # generate experimental candidates with qNEI/qNEIUU
+        acq_func = qLogNoisyExpectedImprovement(
+            model=outcome_model,
+            objective=objective,
+            X_baseline=X,
+            sampler=sampler,
+            prune_baseline=True,
+        )
+    elif acqf_name == "posterior_mean":
+        # generate experimental candidates with maximum posterior mean
+        acq_func = qSimpleRegret(
+            model=outcome_model,
+            sampler=sampler,
+            objective=objective,
+        )
+    else:
+        raise RuntimeError("Unknown acquisition function name!")
+
+    # optimize the acquisition function
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        q=q,
+        bounds=problem.bounds,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+        options={"batch_limit": BATCH_LIMIT},
+        sequential=True,
+    )
+    return candidates
+
+
+def find_max_posterior_mean(
+    outcome_model: SingleTaskGP,
+    train_Y: torch.Tensor,
+    train_comps: torch.Tensor,
+    verbose: bool = False,
+):
+    """Helper function that find the max posterior mean under current outcome and
+    preference model"""
+    pref_model = fit_pref_model(train_Y, train_comps)
+    pref_obj = LearnedObjective(pref_model=pref_model)
+    post_mean_cand_X = gen_exp_cand(
+        outcome_model, pref_obj, q=1, acqf_name="posterior_mean"
+    )
+
+    post_mean_util = util_func(problem(post_mean_cand_X)).item()
+    if verbose:
+        print(f"Max posterior mean utility: {post_mean_util:.3f}")
+    within_result = {
+        "n_comps": train_comps.shape[0],
+        "util": post_mean_util,
+    }
+    return within_result
+
+
+
+
+
+
+
+
+

Closed Loop BOPE

Setup

The following cell shows the core part of this tutorial. +In BOPE, we use two probablistic models (in this case, two Gaussian processes) $f$ and $g$ +to model $f_\mathrm{true}$ and $g_\mathrm{true}$ respectively.

+

We start by initializing the outcome model $f$ with 8 quasi-random points (the initial experimentation stage).

+

Next, we enter the preference exploration (PE) stage. +A straightforward strategy of performing PE is to present the decision-maker with comparisons using outcomes sampled from the outcome model at random design points. We refer this method as $\text{Random}\mathrm{-}f$.

+

Alternatively, we could initialize the preference model with $\text{Random}\mathrm{-}f$, +then perform PE using the $\text{EUBO}\mathrm{-}\zeta$ acquisition function as proposed in [1].

+ +

In this tutorial, we examine both strategies by starting with initializating the preference model with 3 comparisons using $\text{Random}\mathrm{-}f$. +After that, we perform 3 * 5 = 15 pairwise comparisons using either $\text{EUBO}\mathrm{-}\zeta$ or $\text{Random}\mathrm{-}f$. +Then we move on to the second experimentation stage by generating a candidate using qNEIUU by +leveraging both the outcome model and the learned preference model.

+

We additionally examine two other experimental candidate generation strategies:

+
    +
  • "True utility": We assume the true utility function is known and generate experiment candidate(s) using qLogNEI. This represents the performance upper bound of PE strategies.
  • +
  • "Random experiment": We use random design points to generate new candidates.
  • +
+

In this tutorial, we only run one replication of this benchmark; while in general we would expect "True utility" to outperform $\text{EUBO}\mathrm{-}\zeta$ and $\text{EUBO}\mathrm{-}\zeta$ to outperform +the other strategies, this is not guaranteed for a optimization.

+
+
+
+
+
+
In [4]:
+
+
+
verbose = False
+# Number of pairwise comparisons performed before checking posterior mean
+every_n_comps = 3
+# Total number of checking the maximum posterior mean
+n_check_post_mean = 1 if SMOKE_TEST else 5
+n_outcome_model_initialization_points = 8
+within_session_results = []
+exp_candidate_results = []
+
+# Experimentation stage: initial exploration batch
+torch.manual_seed(0)
+np.random.seed(0)
+X, Y = generate_random_exp_data(problem, n_outcome_model_initialization_points)
+outcome_model = fit_outcome_model(X, Y)
+
+# Preference exploration stage: initialize the preference model with comparsions
+# between pairs of outcomes estimated using random design points
+init_train_Y, init_train_comps = generate_random_pref_data(outcome_model, n=1)
+
+# Perform preference exploration using either Random-f or EUBO-zeta
+for pe_strategy in ["EUBO-zeta", "Random-f"]:
+    train_Y, train_comps = init_train_Y.clone(), init_train_comps.clone()
+    within_result = find_max_posterior_mean(outcome_model, train_Y, train_comps)
+    within_result.update({"pe_strategy": pe_strategy})
+    within_session_results.append(within_result)
+
+    for j in range(n_check_post_mean):
+        train_Y, train_comps = run_pref_learn(
+            outcome_model,
+            train_Y,
+            train_comps,
+            n_comps=every_n_comps,
+            pe_strategy=pe_strategy,
+            verbose=verbose,
+        )
+        if verbose:
+            print(
+                f"Checking posterior mean after {(j+1) * every_n_comps} comps using PE strategy {pe_strategy}"
+            )
+        within_result = find_max_posterior_mean(
+            outcome_model, train_Y, train_comps, verbose=verbose
+        )
+        within_result.update({"pe_strategy": pe_strategy})
+        within_session_results.append(within_result)
+
+    # Going back to the experimentation stage: generate an additional batch of experimental evaluations
+    # with the learned preference model and qNEIUU
+    pref_model = fit_pref_model(train_Y, train_comps)
+    pref_obj = LearnedObjective(pref_model=pref_model)
+    exp_cand_X = gen_exp_cand(outcome_model, pref_obj, q=1, acqf_name="qNEI")
+    qneiuu_util = util_func(problem(exp_cand_X)).item()
+    print(f"{pe_strategy} qNEIUU candidate utility: {qneiuu_util:.3f}")
+    exp_result = {"util": qneiuu_util, "strategy": pe_strategy}
+    exp_candidate_results.append(exp_result)
+
+# Generate a batch of experimental evaluations using oracle and random baselines
+# True utility
+true_obj = GenericMCObjective(util_func)
+true_obj_cand_X = gen_exp_cand(outcome_model, true_obj, q=1, acqf_name="qNEI")
+true_obj_util = util_func(problem(true_obj_cand_X)).item()
+print(f"True objective utility: {true_obj_util:.3f}")
+exp_result = {"util": true_obj_util, "strategy": "True Utility"}
+exp_candidate_results.append(exp_result)
+
+# Random experiment
+_, random_Y = generate_random_exp_data(problem, 1)
+random_util = util_func(random_Y).item()
+print(f"Random experiment utility: {random_util:.3f}")
+exp_result = {"util": random_util, "strategy": "Random Experiment"}
+exp_candidate_results.append(exp_result)
+
+
+
+
+
+
+
+
+
+
EUBO-zeta qNEIUU candidate utility: -1.026
+Random-f qNEIUU candidate utility: -1.226
+True objective utility: -1.004
+Random experiment utility: -1.380
+
+
+
+
+
+
+
+
+
+

Plot the Results

We evaluate our results by creating two plots.

+

In the first plot, we focus on comparing how $\text{EUBO}\mathrm{-}\zeta$ can efficiently identify the maximizer +of $g_\mathrm{true}(f_\mathrm{true}(x))$ within a preference exploration stage. +We examine this by estimating the maximum posterior mean after every 3 pairwise comparisons.

+

Here, we plot the the max utility value identified using $\text{EUBO}\mathrm{-}\zeta$ and $\text{Random}\mathrm{-}f$ +with increasing number of pairwise comparisons. +As we can see in this plot, the preference model learned using $\text{EUBO}\mathrm{-}\zeta$ is able to identify the maximum utility more efficiently.

+
+
+
+
+
+
In [5]:
+
+
+
# Plotting
+plt.figure(figsize=(8, 6))
+for name, group in pd.DataFrame(within_session_results).groupby(
+    "pe_strategy", sort=True
+):
+    plt.plot(
+        group["n_comps"],
+        group["util"],
+        label=name,
+        linewidth=1.5,
+    )
+plt.xlabel("Number of comparisons")
+plt.ylabel("Max value identified")
+plt.legend(title="PE Strategy")
+
+
+
+
+
+
+
+
Out[5]:
+
+
<matplotlib.legend.Legend at 0x7f2fac5f6230>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

In the following cell, we show the utility values achieved using different methods in the 2nd experimentation stage.

+

In repeated iterations -- not done here for speed and succinctness -- we find that +$\text{EUBO}\mathrm{-}\zeta$, as a one-step Bayesian optimal PE strategy, performs very similarly to the true utility strategy on average. +On the other hand, even though $\text{Random}\mathrm{-}f$ is a relatively straightforward PE strategy, +it is still able to suggest experimental candidates with generally higher utility values than the random experiment baseline.

+

For more rigorous performance comparisons, see [1].

+
+
+
+
+
+
In [6]:
+
+
+
# Plotting
+plt.figure(figsize=(8, 6))
+for result in exp_candidate_results:
+    plt.scatter(
+        [result["strategy"]],
+        [result["util"]],
+        s=100,
+        label=result["strategy"],
+    )
+
+plt.xlabel("Experimentation Strategy")
+plt.ylabel("Utility achieved in the 2nd experiment stage")
+plt.legend(title="Experimentation Strategy")
+
+
+
+
+
+
+
+
Out[6]:
+
+
<matplotlib.legend.Legend at 0x7f2f9e963eb0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/closed_loop_botorch_only.html b/website-old/_tutorials/closed_loop_botorch_only.html new file mode 100644 index 0000000000..7cd428de13 --- /dev/null +++ b/website-old/_tutorials/closed_loop_botorch_only.html @@ -0,0 +1,440 @@ + + + +
+
+
+
+

Closed-loop batch, constrained BO in BoTorch with qLogEI and qLogNEI

In this tutorial, we illustrate how to implement a simple Bayesian Optimization (BO) closed loop in BoTorch.

+

In general, we recommend for a relatively simple setup (like this one) to use Ax, since this will simplify your setup (including the amount of code you need to write) considerably. See the Using BoTorch with Ax tutorial.

+

However, you may want to do things that are not easily supported in Ax at this time (like running high-dimensional BO using a VAE+GP model that you jointly train on high-dimensional input data). If you find yourself in such a situation, you will need to write your own optimization loop, as we do in this tutorial.

+

We use the batch Log Expected Improvement (qLogEI) and batch Noisy Expected Improvement (qLogNEI) acquisition functions to optimize a constrained version of the synthetic Hartmann6 test function. The standard problem is

+

$$f(x) = -\sum_{i=1}^4 \alpha_i \exp \left( -\sum_{j=1}^6 A_{ij} (x_j - P_{ij})^2 \right)$$

+

over $x \in [0,1]^6$ (parameter values can be found in botorch/test_functions/hartmann6.py).

+

In real BO applications, the design $x$ can influence multiple metrics in unknown ways, and the decision-maker often wants to optimize one metric without sacrificing another. To illustrate this, we add a synthetic constraint of the form $\|x\|_1 - 3 \le 0$. Both the objective and the constraint are observed with noise.

+

Since botorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_{x} -f(x) = 3.32237$.

+
+
+
+
+
+
In [14]:
+
+
+
import os
+from typing import Optional
+
+import torch
+
+device = torch.device("cuda:3" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Problem setup

First, we define the constraint used in the example in outcome_constraint. The second function weighted_obj is a "feasibility-weighted objective," which returns zero when not feasible.

+
+
+
+
+
+
In [15]:
+
+
+
from botorch.test_functions import Hartmann
+
+
+neg_hartmann6 = Hartmann(negate=True)
+
+
+def outcome_constraint(X):
+    """L1 constraint; feasible if less than or equal to zero."""
+    return X.sum(dim=-1) - 3
+
+
+def weighted_obj(X):
+    """Feasibility weighted objective; zero if not feasible."""
+    return neg_hartmann6(X) * (outcome_constraint(X) <= 0).type_as(X)
+
+
+
+
+
+
+
+
+

Model initialization

We use a MultiOutputGP to model the objective (output 0) and the constraint (output 1). We assume known homoskedastic observation noise on both the objective and constraint with standard error $\sigma = 0.5$.

+

Each component is a SingleTaskGP. The models are initialized with 10 points drawn randomly from $[0,1]^6$.

+
+
+
+
+
+
In [16]:
+
+
+
from botorch.models.transforms.input import Normalize
+from botorch.models import SingleTaskGP, ModelListGP
+from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood
+
+NOISE_SE = 0.25
+train_yvar = torch.tensor(NOISE_SE**2, device=device, dtype=dtype)
+
+
+def generate_initial_data(n=10):
+    # generate training data
+    train_x = torch.rand(10, 6, device=device, dtype=dtype)
+    exact_obj = neg_hartmann6(train_x).unsqueeze(-1)  # add output dimension
+    exact_con = outcome_constraint(train_x).unsqueeze(-1)  # add output dimension
+    train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)
+    train_con = exact_con + NOISE_SE * torch.randn_like(exact_con)
+    best_observed_value = weighted_obj(train_x).max().item()
+    return train_x, train_obj, train_con, best_observed_value
+
+
+def initialize_model(train_x, train_obj, train_con, state_dict=None):
+    # define models for objective and constraint
+    model_obj = SingleTaskGP(
+        train_x,
+        train_obj,
+        train_yvar.expand_as(train_obj),
+        input_transform=Normalize(d=train_x.shape[-1]),
+    ).to(train_x)
+    model_con = SingleTaskGP(
+        train_x,
+        train_con,
+        train_yvar.expand_as(train_con),
+        input_transform=Normalize(d=train_x.shape[-1]),
+    ).to(train_x)
+    # combine into a multi-output GP model
+    model = ModelListGP(model_obj, model_con)
+    mll = SumMarginalLogLikelihood(model.likelihood, model)
+    # load state dict if it is passed
+    if state_dict is not None:
+        model.load_state_dict(state_dict)
+    return mll, model
+
+
+
+
+
+
+
+
+

Define a construct to extract the objective and constraint from the GP

The methods below take the outputs of the GP and return the objective and the constraint. In general, these can be any Callable, but here we simply need to index the correct output.

+
+
+
+
+
+
In [17]:
+
+
+
from botorch.acquisition.objective import GenericMCObjective
+
+def obj_callable(Z: torch.Tensor, X: Optional[torch.Tensor] = None):
+    return Z[..., 0]
+
+
+def constraint_callable(Z):
+    return Z[..., 1]
+
+
+objective = GenericMCObjective(objective=obj_callable)
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step

The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. For this example, we'll use a small batch of $q=3$. The function optimize_acqf optimizes the $q$ points jointly. A simple initialization heuristic is used to select the 10 restart initial locations from a set of 50 random points.

+
+
+
+
+
+
In [18]:
+
+
+
from botorch.optim import optimize_acqf
+
+
+bounds = torch.tensor([[0.0] * 6, [1.0] * 6], device=device, dtype=dtype)
+
+BATCH_SIZE = 3 if not SMOKE_TEST else 2
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 32
+
+
+def optimize_acqf_and_get_observation(acq_func):
+    """Optimizes the acquisition function, and returns a new candidate and a noisy observation."""
+    # optimize
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        bounds=bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+    # observe new values
+    new_x = candidates.detach()
+    exact_obj = neg_hartmann6(new_x).unsqueeze(-1)  # add output dimension
+    exact_con = outcome_constraint(new_x).unsqueeze(-1)  # add output dimension
+    new_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)
+    new_con = exact_con + NOISE_SE * torch.randn_like(exact_con)
+    return new_x, new_obj, new_con
+
+
+def update_random_observations(best_random):
+    """Simulates a random policy by taking a the current list of best values observed randomly,
+    drawing a new random point, observing its value, and updating the list.
+    """
+    rand_x = torch.rand(BATCH_SIZE, 6)
+    next_random_best = weighted_obj(rand_x).max().item()
+    best_random.append(max(best_random[-1], next_random_best))
+    return best_random
+
+
+
+
+
+
+
+
+

Perform Bayesian Optimization loop with qLogNEI

The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps:

+
    +
  1. given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$
  2. +
  3. observe $f(x)$ for each $x$ in the batch
  4. +
  5. update the surrogate model.
  6. +
+

Just for illustration purposes, we run three trials each of which do N_BATCH=20 rounds of optimization. The acquisition function is approximated using MC_SAMPLES=256 samples.

+

Note: Running this may take a little while.

+
+
+
+
+
+
In [19]:
+
+
+
import time
+import warnings
+
+from botorch import fit_gpytorch_mll
+from botorch.acquisition import (
+    qLogExpectedImprovement,
+    qLogNoisyExpectedImprovement,
+)
+from botorch.exceptions import BadInitialCandidatesWarning
+from botorch.sampling.normal import SobolQMCNormalSampler
+
+
+warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning)
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+
+N_TRIALS = 3 if not SMOKE_TEST else 2
+N_BATCH = 20 if not SMOKE_TEST else 2
+MC_SAMPLES = 256 if not SMOKE_TEST else 32
+
+verbose = False
+
+best_observed_all_ei, best_observed_all_nei, best_random_all = [], [], []
+
+# average over multiple trials
+for trial in range(1, N_TRIALS + 1):
+
+    print(f"\nTrial {trial:>2} of {N_TRIALS} ", end="")
+    best_observed_ei, best_observed_nei, best_random = [], [], []
+
+    # call helper functions to generate initial training data and initialize model
+    (
+        train_x_ei,
+        train_obj_ei,
+        train_con_ei,
+        best_observed_value_ei,
+    ) = generate_initial_data(n=10)
+    mll_ei, model_ei = initialize_model(train_x_ei, train_obj_ei, train_con_ei)
+
+    train_x_nei, train_obj_nei, train_con_nei = train_x_ei, train_obj_ei, train_con_ei
+    best_observed_value_nei = best_observed_value_ei
+    mll_nei, model_nei = initialize_model(train_x_nei, train_obj_nei, train_con_nei)
+
+    best_observed_ei.append(best_observed_value_ei)
+    best_observed_nei.append(best_observed_value_nei)
+    best_random.append(best_observed_value_ei)
+
+    # run N_BATCH rounds of BayesOpt after the initial random batch
+    for iteration in range(1, N_BATCH + 1):
+
+        t0 = time.monotonic()
+
+        # fit the models
+        fit_gpytorch_mll(mll_ei)
+        fit_gpytorch_mll(mll_nei)
+
+        # define the qEI and qNEI acquisition modules using a QMC sampler
+        qmc_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+
+        # for best_f, we use the best observed noisy values as an approximation
+        qLogEI = qLogExpectedImprovement(
+            model=model_ei,
+            best_f=(train_obj_ei * (train_con_ei <= 0).to(train_obj_ei)).max(),
+            sampler=qmc_sampler,
+            objective=objective,
+            constraints=[constraint_callable],
+        )
+
+        qLogNEI = qLogNoisyExpectedImprovement(
+            model=model_nei,
+            X_baseline=train_x_nei,
+            sampler=qmc_sampler,
+            objective=objective,
+            constraints=[constraint_callable],
+        )
+
+        # optimize and get new observation
+        new_x_ei, new_obj_ei, new_con_ei = optimize_acqf_and_get_observation(qLogEI)
+        new_x_nei, new_obj_nei, new_con_nei = optimize_acqf_and_get_observation(qLogNEI)
+
+        # update training points
+        train_x_ei = torch.cat([train_x_ei, new_x_ei])
+        train_obj_ei = torch.cat([train_obj_ei, new_obj_ei])
+        train_con_ei = torch.cat([train_con_ei, new_con_ei])
+
+        train_x_nei = torch.cat([train_x_nei, new_x_nei])
+        train_obj_nei = torch.cat([train_obj_nei, new_obj_nei])
+        train_con_nei = torch.cat([train_con_nei, new_con_nei])
+
+        # update progress
+        best_random = update_random_observations(best_random)
+        best_value_ei = weighted_obj(train_x_ei).max().item()
+        best_value_nei = weighted_obj(train_x_nei).max().item()
+        best_observed_ei.append(best_value_ei)
+        best_observed_nei.append(best_value_nei)
+
+        # reinitialize the models so they are ready for fitting on next iteration
+        # use the current state dict to speed up fitting
+        mll_ei, model_ei = initialize_model(
+            train_x_ei,
+            train_obj_ei,
+            train_con_ei,
+            model_ei.state_dict(),
+        )
+        mll_nei, model_nei = initialize_model(
+            train_x_nei,
+            train_obj_nei,
+            train_con_nei,
+            model_nei.state_dict(),
+        )
+
+        t1 = time.monotonic()
+
+        if verbose:
+            print(
+                f"\nBatch {iteration:>2}: best_value (random, qEI, qNEI) = "
+                f"({max(best_random):>4.2f}, {best_value_ei:>4.2f}, {best_value_nei:>4.2f}), "
+                f"time = {t1-t0:>4.2f}.",
+                end="",
+            )
+        else:
+            print(".", end="")
+
+    best_observed_all_ei.append(best_observed_ei)
+    best_observed_all_nei.append(best_observed_nei)
+    best_random_all.append(best_random)
+
+
+
+
+
+
+
+
+
+
+Trial  1 of 3 ....................
+Trial  2 of 3 ....................
+Trial  3 of 3 ....................
+
+
+
+
+
+
+
+
+

Plot the results

The plot below shows the best objective value observed at each step of the optimization for each of the algorithms. The confidence intervals represent the variance at that step in the optimization across the trial runs. The variance across optimization runs is quite high, so in order to get a better estimate of the average performance one would have to run a much larger number of trials N_TRIALS (we avoid this here to limit the runtime of this tutorial).

+
+
+
+
+
+
In [20]:
+
+
+
import numpy as np
+from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+
+def ci(y):
+    return 1.96 * y.std(axis=0) / np.sqrt(N_TRIALS)
+
+
+GLOBAL_MAXIMUM = neg_hartmann6.optimal_value
+
+
+iters = np.arange(N_BATCH + 1) * BATCH_SIZE
+y_ei = np.asarray(best_observed_all_ei)
+y_nei = np.asarray(best_observed_all_nei)
+y_rnd = np.asarray(best_random_all)
+
+fig, ax = plt.subplots(1, 1, figsize=(8, 6))
+ax.errorbar(iters, y_rnd.mean(axis=0), yerr=ci(y_rnd), label="random", linewidth=1.5)
+ax.errorbar(iters, y_ei.mean(axis=0), yerr=ci(y_ei), label="qLogEI", linewidth=1.5)
+ax.errorbar(iters, y_nei.mean(axis=0), yerr=ci(y_nei), label="qLogNEI", linewidth=1.5)
+plt.plot(
+    [0, N_BATCH * BATCH_SIZE],
+    [GLOBAL_MAXIMUM] * 2,
+    "k",
+    label="true best feasible objective",
+    linewidth=2,
+)
+ax.set_ylim(bottom=0.5)
+ax.set(
+    xlabel="number of observations (beyond initial points)",
+    ylabel="best objective value",
+)
+ax.legend(loc="lower right")
+
+
+
+
+
+
+
+
Out[20]:
+
+
<matplotlib.legend.Legend at 0x7fd6c7bbb910>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/compare_mc_analytic_acquisition.html b/website-old/_tutorials/compare_mc_analytic_acquisition.html new file mode 100644 index 0000000000..e116f0e116 --- /dev/null +++ b/website-old/_tutorials/compare_mc_analytic_acquisition.html @@ -0,0 +1,300 @@ + + + +
+
+
+
+

Analytic and MC-based Expected Improvement (EI) acquisition

In this tutorial, we compare the analytic and MC-based EI acquisition functions and show both scipy- and torch-based optimizers for optimizing the acquisition. This tutorial highlights the modularity of botorch and the ability to easily try different acquisition functions and accompanying optimization algorithms on the same fitted model.

+
+
+
+
+
+
+

Comparison of analytic and MC-based EI

Note that we use the analytic and MC variants of the LogEI family of acquisition functions, which remedy numerical issues encountered in the naive implementations. See https://arxiv.org/pdf/2310.20708 for more details.

+
+
+
+
+
+
In [ ]:
+
+
+
import torch
+
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from botorch.test_functions import Hartmann
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+neg_hartmann6 = Hartmann(dim=6, negate=True)
+
+
+
+
+
+
+
+
+

First, we generate some random data and fit a SingleTaskGP for a 6-dimensional synthetic test function 'Hartmann6'.

+
+
+
+
+
+
In [ ]:
+
+
+
torch.manual_seed(seed=12345)  # to keep the data conditions the same
+dtype = torch.float64
+train_x = torch.rand(10, 6, dtype=dtype)
+train_obj = neg_hartmann6(train_x).unsqueeze(-1)
+model = SingleTaskGP(train_X=train_x, train_Y=train_obj)
+mll = ExactMarginalLogLikelihood(model.likelihood, model)
+fit_gpytorch_mll(mll);
+
+
+
+
+
+
+
+
+

Initialize an analytic EI acquisition function on the fitted model.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.acquisition.analytic import LogExpectedImprovement
+
+best_value = train_obj.max()
+LogEI = LogExpectedImprovement(model=model, best_f=best_value)
+
+
+
+
+
+
+
+
+

Next, we optimize the analytic EI acquisition function using 50 random restarts chosen from 100 initial raw samples.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.optim import optimize_acqf
+
+new_point_analytic, _ = optimize_acqf(
+    acq_function=LogEI,
+    bounds=torch.tensor([[0.0] * 6, [1.0] * 6]),
+    q=1,
+    num_restarts=20,
+    raw_samples=100,
+    options={},
+)
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
# NOTE: The acquisition value here is the log of the expected improvement.
+LogEI(new_point_analytic), new_point_analytic
+
+
+
+
+
+
+
+
+

Now, let's swap out the analytic acquisition function and replace it with an MC version. Note that we are in the q = 1 case; for q > 1, an analytic version does not exist.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.acquisition.logei import qLogExpectedImprovement
+from botorch.sampling import SobolQMCNormalSampler
+
+
+sampler = SobolQMCNormalSampler(sample_shape=torch.Size([512]), seed=0)
+MC_LogEI = qLogExpectedImprovement(model, best_f=best_value, sampler=sampler, fat=False)
+torch.manual_seed(seed=0)  # to keep the restart conditions the same
+new_point_mc, _ = optimize_acqf(
+    acq_function=MC_LogEI,
+    bounds=torch.tensor([[0.0] * 6, [1.0] * 6]),
+    q=1,
+    num_restarts=20,
+    raw_samples=100,
+    options={},
+)
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
# NOTE: The acquisition value here is the log of the expected improvement.
+MC_LogEI(new_point_mc), new_point_mc
+
+
+
+
+
+
+
+
+

Check that the two generated points are close.

+
+
+
+
+
+
In [ ]:
+
+
+
torch.linalg.norm(new_point_mc - new_point_analytic)
+
+
+
+
+
+
+
+
+

Using a torch optimizer on a stochastic acquisition function

We could also optimize using a torch optimizer. This is particularly useful for the case of a stochastic acquisition function, which we can obtain by using a StochasticSampler. First, we illustrate the usage of torch.optim.Adam. In the code snippet below, gen_batch_initial_candidates uses a heuristic to select a set of restart locations, gen_candidates_torch is a wrapper to the torch optimizer for maximizing the acquisition value, and get_best_candidates finds the best result amongst the random restarts.

+

Under the hood, gen_candidates_torch uses a convergence criterion based on exponential moving averages of the loss.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.sampling.stochastic_samplers import StochasticSampler
+from botorch.generation import get_best_candidates, gen_candidates_torch
+from botorch.optim import gen_batch_initial_conditions
+
+resampler = StochasticSampler(sample_shape=torch.Size([512]))
+MC_LogEI_resample = qLogExpectedImprovement(model, best_f=best_value, sampler=resampler)
+bounds = torch.tensor([[0.0] * 6, [1.0] * 6])
+
+batch_initial_conditions = gen_batch_initial_conditions(
+    acq_function=MC_LogEI_resample,
+    bounds=bounds,
+    q=1,
+    num_restarts=20,
+    raw_samples=100,
+)
+batch_candidates, batch_acq_values = gen_candidates_torch(
+    initial_conditions=batch_initial_conditions,
+    acquisition_function=MC_LogEI_resample,
+    lower_bounds=bounds[0],
+    upper_bounds=bounds[1],
+    optimizer=torch.optim.Adam,
+    options={"maxiter": 500},
+)
+new_point_torch_Adam = get_best_candidates(
+    batch_candidates=batch_candidates, batch_values=batch_acq_values
+).detach()
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
# NOTE: The acquisition value here is the log of the expected improvement.
+MC_LogEI_resample(new_point_torch_Adam), new_point_torch_Adam
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
torch.linalg.norm(new_point_torch_Adam - new_point_analytic)
+
+
+
+
+
+
+
+
+

By changing the optimizer parameter to gen_candidates_torch, we can also try torch.optim.SGD. Note that without the adaptive step size selection of Adam, basic SGD does worse job at optimizing without further manual tuning of the optimization parameters.

+
+
+
+
+
+
In [ ]:
+
+
+
batch_candidates, batch_acq_values = gen_candidates_torch(
+    initial_conditions=batch_initial_conditions,
+    acquisition_function=MC_LogEI_resample,
+    lower_bounds=bounds[0],
+    upper_bounds=bounds[1],
+    optimizer=torch.optim.SGD,
+    options={"maxiter": 500},
+)
+new_point_torch_SGD = get_best_candidates(
+    batch_candidates=batch_candidates, batch_values=batch_acq_values
+).detach()
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
MC_LogEI_resample(new_point_torch_SGD), new_point_torch_SGD
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
torch.linalg.norm(new_point_torch_SGD - new_point_analytic)
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/composite_bo_with_hogp.html b/website-old/_tutorials/composite_bo_with_hogp.html new file mode 100644 index 0000000000..4de440ddfc --- /dev/null +++ b/website-old/_tutorials/composite_bo_with_hogp.html @@ -0,0 +1,466 @@ + + + +
+
+
+
+

Composite Bayesian Optimization with the High Order Gaussian Process

+
+
+
+
+
+
+

In this tutorial, we're going to explore composite Bayesian optimization Astudillo & Frazier, ICML, '19 with the High Order Gaussian Process (HOGP) model of Zhe et al, AISTATS, '19. The setup for composite Bayesian optimization is that we have an unknown (black box) function mapping input parameters to several outputs, and a second, known function describing the quality of the functional output. We wish to find input parameters that maximize the output metric function. We wish to find input parameters that maximize the output metric function in a black-box manner.

+

Specifically, this can be described as $\max_{x \in \mathcal{X}} g(f(x)),$ where $f$ is unknown and $g$ is known. As in traditional Bayesian optimization, we are going to construct a Gaussian process surrogate model over the expensive to evaluate function $f(.),$ and will use a HOGP to model this function.

+
+
+
+
+
+
+

HOGP model description

The High Order Gaussian Process (HOGP) model is a Gaussian process model designed specifically to operate over tensors or multi-dimensional arrays and exploits structure in the tensor to be able to operate efficiently. Specifically, the HOGP takes as inputs $y \in \mathbb{R}^{N \times d_2 \times \cdots \times d_M}$ and assumes that $\text{vec}(y) \sim \mathcal{N}(0, \otimes_{i=1}^M K_i + \sigma^2 I),$ where $K_1 = K_{XX}.$ Each dimension of the tensor has its own kernel function, $K_i,$ as well as a set of $d_i$ latent parameters that can be optimized over.

+

Recently, Maddox et al, '21 proposed a method for computing posterior samples from the HOGP by exploiting structure in the posterior distribution, thereby enabling its usage in BO settings. While they show that this approach allows to use composite BO on problems with tens or thousands of outputs, for scalability we consider a much smaller example here (that does not require GPU acceleration).

+
+
+
+
+
+
In [1]:
+
+
+
import math
+import os
+import time
+from functools import partial
+
+import gpytorch.settings as gpt_settings
+import matplotlib.pyplot as plt
+import torch
+from botorch.acquisition import qExpectedImprovement
+from botorch.acquisition.objective import GenericMCObjective
+from botorch.models import HigherOrderGP, SingleTaskGP
+from botorch.models.higher_order_gp import FlattenedStandardize
+from botorch.models.transforms import Normalize, Standardize
+from botorch.optim import optimize_acqf
+from botorch.optim.fit import fit_gpytorch_mll_torch
+from botorch.sampling.normal import IIDNormalSampler
+from gpytorch.mlls import ExactMarginalLogLikelihood
+from linear_operator.settings import _fast_solves
+from torch.optim import Adam
+
+%matplotlib inline
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+
+
I0215 081151.431 _utils_internal.py:247] NCCL_DEBUG env var is set to None
+I0215 081151.436 _utils_internal.py:265] NCCL_DEBUG is forced to WARN from None
+
+
+
+
+
+
+
+
+
+

Set Device and dtype

+
+
+
+
+
+
In [2]:
+
+
+
torch.manual_seed(0)
+device = (
+    torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:4")
+)
+dtype = torch.float
+
+print("Using ", device)
+
+
+
+
+
+
+
+
+
+
Using  cpu
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
models_used = (
+    "rnd",
+    "ei",
+    "ei_hogp_cf",
+)
+
+
+
+
+
+
+
+
+

Problem Description

+
+
+
+
+
+
+

We use a simple test problem describing the concentration of pollutants after a chemical spill from Astudillo & Frazier, ICML, '19 defined over a $3 \times 4$ grid of values $s,t$ and we wish to optimize the parameters w.r.t. their true values, to estimate the true value of parameters, $x = [M, D, L, \tau].$ The function is given by +$$ f(s,t | M, D, L, \tau) := \frac{M}{\sqrt{4 \pi D t}} \exp\{-\frac{s^2}{4Dt}\} + \frac{1_{t > \tau} M}{\sqrt{4 \pi D(t - \tau)}} \exp\{- \frac{(s - L)^2}{4 D (t - \tau)}\}, $$ +with the cheap to evaluate, differentiable function given by $g(y):= \sum_{(s,t) \in S \times T} \left(c(s, t|x_{\text{true}}) - y\right)^2.$ As the objective function itself is going to be implemented in Pytorch, we will be able to differentiate through it, enabling the usage of gradient-based optimization to optimize the objectives with respect to the inputs.

+
+
+
+
+
+
In [4]:
+
+
+
def env_cfun(s, t, M, D, L, tau):
+    c1 = M / torch.sqrt(4 * math.pi * D * t)
+    exp1 = torch.exp(-(s**2) / 4 / D / t)
+    term1 = c1 * exp1
+    c2 = M / torch.sqrt(4 * math.pi * D * (t - tau))
+    exp2 = torch.exp(-((s - L) ** 2) / 4 / D / (t - tau))
+    term2 = c2 * exp2
+    term2[torch.isnan(term2)] = 0.0
+    return term1 + term2
+
+
+
+
+
+
+
+
+

Helper Functions

+
+
+
+
+
+
+

These are helper functions for us to maximize the acquisition function and to get random points.

+
+
+
+
+
+
In [5]:
+
+
+
def gen_rand_points(bounds, num_samples):
+    points_nlzd = torch.rand(num_samples, bounds.shape[-1]).to(bounds)
+    return bounds[0] + (bounds[1] - bounds[0]) * points_nlzd
+
+
+def optimize_ei(qEI, bounds, **options):
+    cands_nlzd, _ = optimize_acqf(qEI, bounds, **options)
+    return cands_nlzd
+
+
+
+
+
+
+
+
+

Below is a wrapped function to help us define bounds on the parameter space, we can also vary the size of the grid if we'd like to.

+
+
+
+
+
+
In [6]:
+
+
+
def prepare_data(s_size=3, t_size=4, device=device, dtype=dtype):
+    print("---- Running the environmental problem with ", s_size, t_size, " ----")
+    # X = [M, D, L, tau]
+    bounds = torch.tensor(
+        [[7.0, 0.02, 0.01, 30.010], [13.0, 0.12, 3.00, 30.295]],
+        device=device,
+        dtype=dtype,
+    )
+
+    M0 = torch.tensor(10.0, device=device, dtype=dtype)
+    D0 = torch.tensor(0.07, device=device, dtype=dtype)
+    L0 = torch.tensor(1.505, device=device, dtype=dtype)
+    tau0 = torch.tensor(30.1525, device=device, dtype=dtype)
+
+    # we can vectorize everything, no need for loops
+    if s_size == 3:
+        S = torch.tensor([0.0, 1.0, 2.5], device=device, dtype=dtype)
+    else:
+        S = torch.linspace(0.0, 2.5, s_size, device=device, dtype=dtype)
+    if t_size == 4:
+        T = torch.tensor([15.0, 30.0, 45.0, 60.0], device=device, dtype=dtype)
+    else:
+        T = torch.linspace(15.0, 60.0, t_size, device=device, dtype=dtype)
+
+    Sgrid, Tgrid = torch.meshgrid(S, T)
+
+    # X = [M, D, L, tau]
+    def c_batched(X, k=None):
+        return torch.stack([env_cfun(Sgrid, Tgrid, *x) for x in X])
+
+    c_true = env_cfun(Sgrid, Tgrid, M0, D0, L0, tau0)
+
+    def neq_sum_quared_diff(samples, X=None):
+        # unsqueeze
+        if samples.shape[-1] == (s_size * t_size):
+            samples = samples.unsqueeze(-1).reshape(*samples.shape[:-1], s_size, t_size)
+
+        sq_diffs = (samples - c_true).pow(2)
+        return sq_diffs.sum(dim=(-1, -2)).mul(-1.0)
+
+    objective = GenericMCObjective(neq_sum_quared_diff)
+    num_samples = 32
+
+    return c_batched, objective, bounds, num_samples
+
+
+
+
+
+
+
+
+

In the above, we construct a GenericMCObjective instance to codify the objective function (which is minimizing the MSE of the output tensors and the outputs corresponding to the "true" parameter values). Note that the objective function is encoded in PyTorch and is differentiable (although it technically doesn't have to be). Ultimately, we backpropagate through the objective with respect to the input parameters (and through the HOGP as well).

+
+
+
+
+
+
+

BO Loop

+
+
+
+
+
+
+

Finally, we run the BO loop for 10 iterations, generating 3 candidates in each iteration. This loop might take a while.

+

We will be comparing to both random selection and batch expected improvement on the aggregated metric.

+
+
+
+
+
+
In [7]:
+
+
+
n_init = 20
+
+if SMOKE_TEST:
+    n_batches = 1
+    batch_size = 2
+else:
+    n_batches = 10
+    batch_size = 3
+
+
+
+
+
+
+
+
+

As a word of caution, we've found that when fitting the HOGP model, using first-order optimizers (e.g. Adam) as is used in fit_gpytorch_torch tends to outperform second-order optimizers such as L-BFGS-B due to the large number of free parameters in the HOGP. L-BFGS-B tends to overfit in practice here.

+
+
+
+
+
+
In [8]:
+
+
+
with gpt_settings.cholesky_jitter(1e-4):
+    c_batched, objective, bounds, num_samples = prepare_data(device=device, dtype=dtype)
+
+    train_X_init = gen_rand_points(bounds, n_init)
+    train_Y_init = c_batched(train_X_init)
+
+    # these will keep track of the points explored
+    train_X = {k: train_X_init.clone() for k in models_used}
+    train_Y = {k: train_Y_init.clone() for k in train_X}
+
+    # run the BO loop
+    for i in range(n_batches):
+        tic = time.monotonic()
+
+        # get best observations, log status
+        best_f = {k: objective(v).max().detach() for k, v in train_Y.items()}
+
+        print(
+            f"It {i+1:>2}/{n_batches}, best obs.: "
+            ", ".join([f"{k}: {v:.3f}" for k, v in best_f.items()])
+        )
+
+        # generate random candidates
+        cands = {}
+        cands["rnd"] = gen_rand_points(bounds, batch_size)
+
+        optimize_acqf_kwargs = {
+            "q": batch_size,
+            "num_restarts": 10,
+            "raw_samples": 512,
+        }
+        sampler = IIDNormalSampler(sample_shape=torch.Size([128]))
+
+        train_Y_ei = objective(train_Y["ei"]).unsqueeze(-1)
+        model_ei = SingleTaskGP(
+            train_X["ei"],
+            train_Y_ei,
+            input_transform=Normalize(train_X["ei"].shape[-1]),
+            outcome_transform=Standardize(train_Y_ei.shape[-1]),
+        )
+
+        mll = ExactMarginalLogLikelihood(model_ei.likelihood, model_ei)
+        fit_gpytorch_mll_torch(mll, step_limit=1000, optimizer=partial(Adam, lr=0.01))
+
+        # generate qEI candidate (single output modeling)
+        qEI = qExpectedImprovement(model_ei, best_f=best_f["ei"], sampler=sampler)
+        cands["ei"] = optimize_ei(qEI, bounds, **optimize_acqf_kwargs)
+
+        model_ei_hogp_cf = HigherOrderGP(
+            train_X["ei_hogp_cf"],
+            train_Y["ei_hogp_cf"],
+            outcome_transform=FlattenedStandardize(train_Y["ei_hogp_cf"].shape[1:]),
+            input_transform=Normalize(train_X["ei_hogp_cf"].shape[-1]),
+            latent_init="gp",
+        )
+
+        mll = ExactMarginalLogLikelihood(model_ei_hogp_cf.likelihood, model_ei_hogp_cf)
+        with _fast_solves(True):
+            fit_gpytorch_mll_torch(
+                mll, step_limit=1000, optimizer=partial(Adam, lr=0.01)
+            )
+
+        # generate qEI candidate (multi-output modeling)
+        qEI_hogp_cf = qExpectedImprovement(
+            model_ei_hogp_cf,
+            best_f=best_f["ei_hogp_cf"],
+            sampler=sampler,
+            objective=objective,
+        )
+        cands["ei_hogp_cf"] = optimize_ei(qEI_hogp_cf, bounds, **optimize_acqf_kwargs)
+
+        # make observations and update data
+        for k, Xold in train_X.items():
+            Xnew = cands[k]
+            if Xnew.shape[0] > 0:
+                train_X[k] = torch.cat([Xold, Xnew])
+                train_Y[k] = torch.cat([train_Y[k], c_batched(Xnew)])
+
+        print(f"Wall time: {time.monotonic() - tic:1f}")
+
+    objective_dict = {k: objective(train_Y[k]) for k in train_Y}
+
+
+
+
+
+
+
+
+
+
Wall time: 12.043408
+rnd: -0.071It 10/10, best obs.: , ei: -0.089It 10/10, best obs.: , ei_hogp_cf: -0.000
+Wall time: 12.193747
+
+
+
+
+
+
+
[W 240215 08:15:58 initializers:432] Unable to find non-zero acquisition function values - initial conditions are being selected randomly.
+
+
+
+
+
+
+
+
+
In [9]:
+
+
+
methods_dict = {k: objective_dict[k].cpu().cummax(0)[0] for k in models_used}
+mean_results = {k: -methods_dict[k][n_init:] for k in models_used}
+
+
+
+
+
+
+
+
+

Finally, we plot the results, showing that the HOGP performs well on this task, and converges to a closer parameter value than a batch GP on the composite metric itself.

+
+
+
+
+
+
In [10]:
+
+
+
plt.figure(figsize=(8, 6))
+labels_dict = {"rnd": "Random", "ei": "EI", "ei_hogp_cf": "Composite EI"}
+for k in models_used:
+    plt.plot(
+        torch.arange(n_batches * batch_size),
+        mean_results[k],
+        label=labels_dict[k],
+    )
+plt.legend(fontsize=20)
+plt.semilogy()
+plt.xlabel("Number of Function Queries")
+plt.ylabel("Difference from True Parameter")
+
+
+
+
+
+
+
+
Out[10]:
+
+
Text(0, 0.5, 'Difference from True Parameter')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/composite_mtbo.html b/website-old/_tutorials/composite_mtbo.html new file mode 100644 index 0000000000..a28f3a14bd --- /dev/null +++ b/website-old/_tutorials/composite_mtbo.html @@ -0,0 +1,369 @@ + + + +
+
+
+
+

Composite Bayesian Optimization with Multi-Task Gaussian Processes

In this tutorial, we'll be describing how to perform multi-task Bayesian optimization over composite functions. In these types of problems, there are several related outputs, and an overall easy to evaluate objective function that we wish to maximize.

+

Multi-task Bayesian Optimization was first proposed by Swersky et al, NeurIPS, '13 in the context of fast hyper-parameter tuning for neural network models; however, we demonstrate a more advanced use-case of composite Bayesian optimization where the overall function that we wish to optimize is a cheap-to-evaluate (and known) function of the outputs. In general, we expect that using more information about the function should yield improved performance when attempting to optimize it, particularly if the metric function itself is quickly varying.

+

See the composite BO tutorial w/ HOGP for a more technical introduction. In general, we suggest using MTGPs for unstructured task outputs and the HOGP for matrix / tensor structured outputs.

+

We will use a Multi-Task Gaussian process (MTGP) with an ICM kernel to model all of the outputs in this problem. MTGPs can be easily accessed in Botorch via the botorch.models.KroneckerMultiTaskGP model class (for the "block design" case of fully observed outputs at all inputs). Given $T$ tasks (outputs) and $n$ data points, they assume that the responses, $Y \sim \mathbb{R}^{n \times T},$ are distributed as $\text{vec}(Y) \sim \mathcal{N}(f, D)$ and $f \sim \mathcal{GP}(\mu_{\theta}, K_{XX} \otimes K_{T}),$ where $D$ is a (diagonal) noise term.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import time
+
+import torch
+from botorch.acquisition.logei import qLogExpectedImprovement
+from botorch.acquisition.objective import GenericMCObjective
+from botorch.models import KroneckerMultiTaskGP
+from botorch.optim import optimize_acqf
+from botorch.sampling.normal import IIDNormalSampler
+
+from botorch.test_functions import Hartmann
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+import warnings
+warnings.filterwarnings("ignore")
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Set device, dtype and random seed

+
+
+
+
+
+
In [2]:
+
+
+
torch.random.manual_seed(10)
+
+tkwargs = {
+    "dtype": torch.double,
+    "device": torch.device("cuda:0" if torch.cuda.is_available() else "cpu"),
+}
+
+
+
+
+
+
+
+
+

Problem Definition

The function that we wish to optimize is based off of a contextual version of the Hartmann-6 test function, where following Feng et al, NeurIPS, '20 we convert the sixth task dimension into a task indicator. Here we assume that we evaluate all contexts at once.

+
+
+
+
+
+
In [3]:
+
+
+
from torch import Tensor
+
+
+class ContextualHartmann6(Hartmann):
+    def __init__(self, num_tasks: int = 20, noise_std=None, negate=False):
+        super().__init__(dim=6, noise_std=noise_std, negate=negate)
+        self.task_range = torch.linspace(0, 1, num_tasks).unsqueeze(-1)
+        self._bounds = [(0.0, 1.0) for _ in range(self.dim - 1)]
+        self.bounds = torch.tensor(self._bounds).t()
+
+    def evaluate_true(self, X: Tensor) -> Tensor:
+        batch_X = X.unsqueeze(-2)
+        batch_dims = X.ndim - 1
+
+        expanded_task_range = self.task_range
+        for _ in range(batch_dims):
+            expanded_task_range = expanded_task_range.unsqueeze(0)
+        task_range = expanded_task_range.repeat(*X.shape[:-1], 1, 1).to(X)
+        concatenated_X = torch.cat(
+            (
+                batch_X.repeat(*[1] * batch_dims, self.task_range.shape[0], 1),
+                task_range,
+            ),
+            dim=-1,
+        )
+        return super().evaluate_true(concatenated_X)
+
+
+
+
+
+
+
+
+

We use GenericMCObjective to define the differentiable function that we are optimizing. Here, it is defined as +$$g(f) = \sum_{i=1}^T \cos(f_i^2 + f_i w_i)$$ +where $w$ is a weight vector (drawn randomly once at the start of the optimization). As this function is a non-linear function of the outputs $f,$ we cannot compute acquisition functions via computation of the posterior mean and variance, but rather have to compute posterior samples and evaluate acquisitions with Monte Carlo sampling.

+

For greater than $10$ or so tasks, it is computationally challenging to sample the posterior over all tasks jointly using conventional approaches, except that Maddox et al, '21 have devised an efficient method for exploiting the structure in the posterior distribution of the MTGP, enabling efficient MC based optimization of objectives using MTGPs. In this tutorial, we choose 6 contexts/tasks for demostration.

+
+
+
+
+
+
In [4]:
+
+
+
num_tasks = 6
+problem = ContextualHartmann6(num_tasks=num_tasks, noise_std=0.001, negate=True).to(**tkwargs)
+
+# we choose num_tasks random weights
+weights = torch.randn(num_tasks, **tkwargs)
+
+
+def callable_func(samples, X=None):
+    res = -torch.cos((samples**2) + samples * weights)
+    return res.sum(dim=-1)
+
+
+objective = GenericMCObjective(callable_func)
+
+
+
+
+
+
+
+
In [5]:
+
+
+
bounds = problem.bounds
+
+
+
+
+
+
+
+
+

BO Loop

+
+
+
+
+
+
+

Set environmental parameters, we use 20 initial data points and optimize for 20 steps with a batch size of 3 candidate points at each evaluation.

+
+
+
+
+
+
In [6]:
+
+
+
if SMOKE_TEST:
+    n_init = 5
+    n_steps = 1
+    batch_size = 2
+    num_samples = 4
+    # For L-BFGS inner optimization loop
+    MAXITER = 10
+else:
+    n_init = 10
+    n_steps = 10
+    batch_size = 3
+    num_samples = 64
+    MAXITER = 200
+
+
+
+
+
+
+
+
In [7]:
+
+
+
from botorch.fit import fit_gpytorch_mll
+
+
+
+
+
+
+
+
+

Finally, run the optimization loop.

+

Warning... this optimization loop can take a while, especially on the CPU. We compare to random sampling.

+
+
+
+
+
+
In [8]:
+
+
+
# New version
+torch.manual_seed(0)
+
+init_x = (bounds[1] - bounds[0]) * torch.rand(
+    n_init, bounds.shape[1], **tkwargs
+) + bounds[0]
+
+init_y = problem(init_x)
+
+mtgp_train_x, mtgp_train_y = init_x, init_y
+rand_x, rand_y = init_x, init_y
+
+best_value_mtgp = objective(init_y).max()
+best_random = best_value_mtgp
+
+for iteration in range(n_steps):
+    # we empty the cache to clear memory out
+    torch.cuda.empty_cache()
+
+    # MTGP
+    mtgp_t0 = time.monotonic()
+    mtgp = KroneckerMultiTaskGP(mtgp_train_x, mtgp_train_y)
+    mtgp_mll = ExactMarginalLogLikelihood(mtgp.likelihood, mtgp)
+    fit_gpytorch_mll(mll=mtgp_mll, optimizer_kwargs={"options": {"maxiter": 50}})
+
+    sampler = IIDNormalSampler(sample_shape=torch.Size([num_samples]))
+    mtgp_acqf = qLogExpectedImprovement(
+        model=mtgp,
+        best_f=best_value_mtgp,
+        sampler=sampler,
+        objective=objective,
+    )
+    new_mtgp_x, _ = optimize_acqf(
+        acq_function=mtgp_acqf,
+        bounds=bounds,
+        q=batch_size,
+        num_restarts=10,
+        raw_samples=512,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": MAXITER, "init_batch_limit": 5},
+    )
+    mtgp_train_x = torch.cat((mtgp_train_x, new_mtgp_x), dim=0)
+    mtgp_train_y = torch.cat((mtgp_train_y, problem(new_mtgp_x)), dim=0)
+    best_value_mtgp = objective(mtgp_train_y).max()
+    mtgp_t1 = time.monotonic()
+
+    # rand
+    new_rand_x = (bounds[1] - bounds[0]) * torch.rand(
+        batch_size, bounds.shape[1], **tkwargs
+    ) + bounds[0]
+    rand_x = torch.cat((rand_x, new_rand_x))
+    rand_y = torch.cat((rand_y, problem(new_rand_x)))
+    best_random = objective(rand_y).max()
+
+    print(
+        f"\nBatch {iteration:>2}: best_value (random, mtgp) = "
+        f"({best_random:>4.2f}, {best_value_mtgp:>4.2f}, "
+        f"mtgp time = {mtgp_t1-mtgp_t0:>4.2f}",
+        end="",
+    )
+
+objectives = {
+    "MGTP": objective(mtgp_train_y).detach().cpu(),
+    "Random": objective(rand_y).detach().cpu(),
+}
+
+
+
+
+
+
+
+
+
+
+Batch  0: best_value (random, mtgp) = (-4.76, -4.76, mtgp time = 15.64
+Batch  1: best_value (random, mtgp) = (-4.76, -4.76, mtgp time = 36.96
+Batch  2: best_value (random, mtgp) = (-4.39, -4.76, mtgp time = 23.20
+Batch  3: best_value (random, mtgp) = (-4.39, -4.76, mtgp time = 23.07
+Batch  4: best_value (random, mtgp) = (-4.39, -4.76, mtgp time = 35.79
+Batch  5: best_value (random, mtgp) = (-4.22, -4.76, mtgp time = 50.71
+Batch  6: best_value (random, mtgp) = (-4.22, -2.88, mtgp time = 61.87
+Batch  7: best_value (random, mtgp) = (-4.22, -2.88, mtgp time = 115.77
+Batch  8: best_value (random, mtgp) = (-4.22, -1.91, mtgp time = 67.89
+Batch  9: best_value (random, mtgp) = (-4.22, -1.91, mtgp time = 48.66
+
+
+
+
+
+
+
+
+

Plot Results

+
+
+
+
+
+
In [9]:
+
+
+
import matplotlib.pyplot as plt
+
+
+
+
+
+
+
+
+

Finally, we plot the results. MTGP will outperform the random baseline.

+
+
+
+
+
+
In [10]:
+
+
+
results = {
+    k: t[n_init:].cummax(0).values for k, t in objectives.items()
+}
+
+
+
+
+
+
+
+
In [11]:
+
+
+
for name, vals in results.items():
+    plt.plot(vals, label=name)
+plt.legend()
+
+
+
+
+
+
+
+
Out[11]:
+
+
<matplotlib.legend.Legend at 0x7f26b3d3b010>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/constrained_multi_objective_bo.html b/website-old/_tutorials/constrained_multi_objective_bo.html new file mode 100644 index 0000000000..6c2a83c0c7 --- /dev/null +++ b/website-old/_tutorials/constrained_multi_objective_bo.html @@ -0,0 +1,589 @@ + + + +
+
+
+
+

Constrained, Parallel, Multi-Objective BO in BoTorch with qNEHVI, and qParEGO

In this tutorial, we illustrate how to implement a constrained multi-objective (MO) Bayesian Optimization (BO) closed loop in BoTorch.

+

In general, we recommend using Ax for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. See here for an Ax tutorial on MOBO. If desired, you can use a custom BoTorch model in Ax, following the Using BoTorch with Ax tutorial. Given a MultiObjective, Ax will default to the $q$NEHVI acquisiton function. If desired, this can also be customized by adding "botorch_acqf_class": <desired_botorch_acquisition_function_class>, to the model_kwargs.

+

We use the parallel ParEGO ($q$ParEGO) [1] and parallel Noisy Expected Hypervolume Improvement ($q$NEHVI) [2] acquisition functions to optimize a synthetic C2-DTLZ2 test function with $M=2$ objectives, $V=1$ constraint, and $d=4$ parameters. The two objectives are +$$f_1(\mathbf x) = (1+ g(\mathbf x_M))\cos\big(\frac{\pi}{2}x_1\big)$$ +$$f_2(\mathbf x) = (1+ g(\mathbf x_M))\sin\big(\frac{\pi}{2}x_1\big)$$ +where $g(\mathbf x) = \sum_{x_i \in \mathbf x_M} (x_i - 0.5)^2, \mathbf x \in [0,1]^d,$ and $\mathbf x_M$ represents the last $d - M +1$ elements of $\mathbf x$. Additionally, the C2-DTLZ2 problem uses the following constraint:

+

$$c(\mathbf x) = - \min \bigg[\min_{i=1}^M\bigg((f_i(\mathbf x) -1 )^2 + \sum_{j=1, j=i}^M (f_j^2 - r^2) \bigg), \bigg(\sum_{i=1}^M \big((f_i(\mathbf x) - \frac{1}{\sqrt{M}})^2 - r^2\big)\bigg)\bigg]\geq 0$$

+

where $\mathbf x \in [0,1]^d$ and $r=0.2$.

+

The goal here is to minimize both objectives. Since BoTorch assumes maximization, we maximize the negative of each objective. Since there typically is no single best solution in multi-objective optimization problems, we seek to find the pareto frontier, the set of optimal trade-offs where improving one metric means deteriorating another.

+

[1] S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.

+

[2] S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances in Neural Information Processing Systems 34, 2021.

+

For batch optimization (or in noisy settings), we strongly recommend using $q$NEHVI rather than $q$EHVI [1] because it is far more efficient than $q$EHVI and mathematically equivalent in the noiseless setting.

+
+
+
+
+
+
+

Set dtype and device

Note: $q$EHVI aggressively exploits parallel hardware and is much faster when run on a GPU. See [1] for details.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import torch
+
+
+tkwargs = {
+    "dtype": torch.double,
+    "device": torch.device("cuda:3" if torch.cuda.is_available() else "cpu"),
+}
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Problem setup

+
+
+
+
+
+
In [2]:
+
+
+
from botorch.test_functions.multi_objective import C2DTLZ2
+
+
+d = 4
+M = 2
+problem = C2DTLZ2(dim=d, num_objectives=M, negate=True).to(**tkwargs)
+
+
+
+
+
+
+
+
+

Model initialization

We use a multi-output SingleTaskGP to model the two objectives with a homoskedastic Gaussian likelihood with an inferred noise level.

+

The models are initialized with $2(d+1)=10$ points drawn randomly from $[0,1]^{4}$.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.utils.sampling import draw_sobol_samples
+from botorch.utils.transforms import normalize, unnormalize
+from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood
+
+
+def generate_initial_data(n):
+    # generate training data
+    train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1)
+    train_obj = problem(train_x)
+    # negative values imply feasibility in botorch
+    train_con = -problem.evaluate_slack(train_x)
+    return train_x, train_obj, train_con
+
+
+def initialize_model(train_x, train_obj, train_con):
+    # define models for objective and constraint
+    train_x = normalize(train_x, problem.bounds)
+    train_y = torch.cat([train_obj, train_con], dim=-1)
+    models = []
+    for i in range(train_y.shape[-1]):
+        models.append(
+            SingleTaskGP(
+                train_x, train_y[..., i : i + 1], outcome_transform=Standardize(m=1)
+            )
+        )
+    model = ModelListGP(*models)
+    mll = SumMarginalLogLikelihood(model.likelihood, model)
+    return mll, model
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step for $q$NEHVI

The helper function below initializes the $q$NEHVI acquisition function, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values.

+

For this example, we'll use a small batch of $q=2$. Passing the keyword argument sequential=True to the function optimize_acqfspecifies that candidates should be optimized in a sequential greedy fashion (see [1] for details why this is important). A simple initialization heuristic is used to select the 10 restart initial locations from a set of 512 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation.

+

Reference Point

+

$q$NEHVI requires specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy.

+

Integrating over function values at in-sample designs

+

$q$NEHVI integrates over the unknown function values at the previously evaluated designs (see [2] for details). Therefore, we need to provide the previously evaluated designs (train_x, normalized to be within $[0,1]^d$) to the acquisition function.

+

Pruning baseline designs +To speed up integration over the function values at the previously evaluated designs, we prune the set of previously evaluated designs (by setting prune_baseline=True) to only include those which have positive probability of being on the current in-sample Pareto frontier.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.acquisition.multi_objective.monte_carlo import (
+    qNoisyExpectedHypervolumeImprovement,
+)
+from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective
+from botorch.optim.optimize import optimize_acqf, optimize_acqf_list
+from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization
+from botorch.utils.sampling import sample_simplex
+
+
+BATCH_SIZE = 2
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+
+standard_bounds = torch.zeros(2, problem.dim, **tkwargs)
+standard_bounds[1] = 1
+
+
+def optimize_qnehvi_and_get_observation(model, train_x, train_obj, train_con, sampler):
+    """Optimizes the qNEHVI acquisition function, and returns a new candidate and observation."""
+    train_x = normalize(train_x, problem.bounds)
+    acq_func = qNoisyExpectedHypervolumeImprovement(
+        model=model,
+        ref_point=problem.ref_point.tolist(),  # use known reference point
+        X_baseline=train_x,
+        sampler=sampler,
+        prune_baseline=True,
+        # define an objective that specifies which outcomes are the objectives
+        objective=IdentityMCMultiOutputObjective(outcomes=[0, 1]),
+        # specify that the constraint is on the last outcome
+        constraints=[lambda Z: Z[..., -1]],
+    )
+    # optimize
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        bounds=standard_bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+        sequential=True,
+    )
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=problem.bounds)
+    new_obj = problem(new_x)
+    # negative values imply feasibility in botorch
+    new_con = -problem.evaluate_slack(new_x)
+    return new_x, new_obj, new_con
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step for $q$ParEGO

The helper function below similarly initializes $q$ParEGO, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values.

+

$q$ParEGO uses random augmented chebyshev scalarization with the qExpectedImprovement acquisition function. In the parallel setting ($q>1$), each candidate is optimized in sequential greedy fashion using a different random scalarization (see [1] for details).

+

To do this, we create a list of qExpectedImprovement acquisition functions, each with different random scalarization weights. The optimize_acqf_list method sequentially generates one candidate per acquisition function and conditions the next candidate (and acquisition function) on the previously selected pending candidates.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.acquisition.monte_carlo import qExpectedImprovement
+from botorch.acquisition.objective import GenericMCObjective
+
+
+def optimize_qparego_and_get_observation(model, train_obj, train_con, sampler):
+    """Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization
+    of the qParEGO acquisition function, and returns a new candidate and observation."""
+    acq_func_list = []
+    for _ in range(BATCH_SIZE):
+        # sample random weights
+        weights = sample_simplex(problem.num_objectives, **tkwargs).squeeze()
+        # construct augmented Chebyshev scalarization
+        scalarization = get_chebyshev_scalarization(weights=weights, Y=train_obj)
+        # initialize the scalarized objective (w/o constraints)
+        scalarized_objective = GenericMCObjective(
+            # the last element of the model outputs is the constraint
+            lambda Z, X: scalarization(Z[..., :-1]),
+        )
+        train_y = torch.cat([train_obj, train_con], dim=-1)
+        acq_func = qExpectedImprovement(  # pyre-ignore: [28]
+            model=model,
+            objective=scalarized_objective,
+            best_f=scalarized_objective(train_y).max(),
+            constraints=[lambda Z: Z[..., -1]],
+            sampler=sampler,
+        )
+        acq_func_list.append(acq_func)
+    # optimize
+    candidates, _ = optimize_acqf_list(
+        acq_function_list=acq_func_list,
+        bounds=standard_bounds,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=problem.bounds)
+    new_obj = problem(new_x)
+    # negative values imply feasibility in botorch
+    new_con = -problem.evaluate_slack(new_x)
+    return new_x, new_obj, new_con
+
+
+
+
+
+
+
+
+

Perform Bayesian Optimization loop with $q$EHVI and $q$ParEGO

The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps:

+
    +
  1. given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$
  2. +
  3. observe $f(x)$ for each $x$ in the batch
  4. +
  5. update the surrogate model.
  6. +
+

Just for illustration purposes, we run one trial with N_BATCH=20 rounds of optimization. The acquisition function is approximated using MC_SAMPLES=128 samples.

+

Note: Running this may take a little while.

+
+
+
+
+
+
In [6]:
+
+
+
import time
+import warnings
+
+from botorch import fit_gpytorch_mll
+from botorch.exceptions import BadInitialCandidatesWarning
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.multi_objective.hypervolume import Hypervolume
+from botorch.utils.multi_objective.pareto import is_non_dominated
+
+
+warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning)
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+N_BATCH = 20 if not SMOKE_TEST else 1
+MC_SAMPLES = 128 if not SMOKE_TEST else 16
+verbose = True
+
+hv = Hypervolume(ref_point=problem.ref_point)
+hvs_qparego, hvs_qnehvi, hvs_random = [], [], []
+
+# call helper functions to generate initial training data and initialize model
+train_x_qparego, train_obj_qparego, train_con_qparego = generate_initial_data(
+    n=2 * (d + 1)
+)
+mll_qparego, model_qparego = initialize_model(
+    train_x_qparego, train_obj_qparego, train_con_qparego
+)
+
+train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi = (
+    train_x_qparego,
+    train_obj_qparego,
+    train_con_qparego,
+)
+train_x_random, train_obj_random, train_con_random = (
+    train_x_qparego,
+    train_obj_qparego,
+    train_con_qparego,
+)
+
+mll_qnehvi, model_qnehvi = initialize_model(
+    train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi
+)
+
+# compute pareto front
+is_feas = (train_con_qparego <= 0).all(dim=-1)
+feas_train_obj = train_obj_qparego[is_feas]
+if feas_train_obj.shape[0] > 0:
+    pareto_mask = is_non_dominated(feas_train_obj)
+    pareto_y = feas_train_obj[pareto_mask]
+    # compute hypervolume
+    volume = hv.compute(pareto_y)
+else:
+    volume = 0.0
+
+hvs_qparego.append(volume)
+hvs_qnehvi.append(volume)
+hvs_random.append(volume)
+
+# run N_BATCH rounds of BayesOpt after the initial random batch
+for iteration in range(1, N_BATCH + 1):
+    t0 = time.monotonic()
+
+    # fit the models
+    fit_gpytorch_mll(mll_qparego)
+    fit_gpytorch_mll(mll_qnehvi)
+
+    # define the qParEGO and qNEHVI acquisition modules using a QMC sampler
+    qparego_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+    qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+
+    # optimize acquisition functions and get new observations
+    (
+        new_x_qparego,
+        new_obj_qparego,
+        new_con_qparego,
+    ) = optimize_qparego_and_get_observation(
+        model_qparego, train_obj_qparego, train_con_qparego, qparego_sampler
+    )
+    new_x_qnehvi, new_obj_qnehvi, new_con_qnehvi = optimize_qnehvi_and_get_observation(
+        model_qnehvi, train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi, qnehvi_sampler
+    )
+    new_x_random, new_obj_random, new_con_random = generate_initial_data(n=BATCH_SIZE)
+
+    # update training points
+    train_x_qparego = torch.cat([train_x_qparego, new_x_qparego])
+    train_obj_qparego = torch.cat([train_obj_qparego, new_obj_qparego])
+    train_con_qparego = torch.cat([train_con_qparego, new_con_qparego])
+
+    train_x_qnehvi = torch.cat([train_x_qnehvi, new_x_qnehvi])
+    train_obj_qnehvi = torch.cat([train_obj_qnehvi, new_obj_qnehvi])
+    train_con_qnehvi = torch.cat([train_con_qnehvi, new_con_qnehvi])
+
+    train_x_random = torch.cat([train_x_random, new_x_random])
+    train_obj_random = torch.cat([train_obj_random, new_obj_random])
+    train_con_random = torch.cat([train_con_random, new_con_random])
+
+    # update progress
+    for hvs_list, train_obj, train_con in zip(
+        (hvs_random, hvs_qparego, hvs_qnehvi),
+        (train_obj_random, train_obj_qparego, train_obj_qnehvi),
+        (train_con_random, train_con_qparego, train_con_qnehvi),
+    ):
+        # compute pareto front
+        is_feas = (train_con <= 0).all(dim=-1)
+        feas_train_obj = train_obj[is_feas]
+        if feas_train_obj.shape[0] > 0:
+            pareto_mask = is_non_dominated(feas_train_obj)
+            pareto_y = feas_train_obj[pareto_mask]
+            # compute feasible hypervolume
+            volume = hv.compute(pareto_y)
+        else:
+            volume = 0.0
+        hvs_list.append(volume)
+
+    # reinitialize the models so they are ready for fitting on next iteration
+    # Note: we find improved performance from not warm starting the model hyperparameters
+    # using the hyperparameters from the previous iteration
+    mll_qparego, model_qparego = initialize_model(
+        train_x_qparego, train_obj_qparego, train_con_qparego
+    )
+    mll_qnehvi, model_qnehvi = initialize_model(
+        train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi
+    )
+
+    t1 = time.monotonic()
+
+    if verbose:
+        print(
+            f"\nBatch {iteration:>2}: Hypervolume (random, qParEGO, qNEHVI) = "
+            f"({hvs_random[-1]:>4.2f}, {hvs_qparego[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), "
+            f"time = {t1-t0:>4.2f}.",
+            end="",
+        )
+    else:
+        print(".", end="")
+
+
+
+
+
+
+
+
+
+
+Batch  1: Hypervolume (random, qParEGO, qNEHVI) = (0.00, 0.00, 0.00), time = 4.54.
+Batch  2: Hypervolume (random, qParEGO, qNEHVI) = (0.00, 0.00, 0.00), time = 4.12.
+Batch  3: Hypervolume (random, qParEGO, qNEHVI) = (0.13, 0.00, 0.10), time = 4.10.
+Batch  4: Hypervolume (random, qParEGO, qNEHVI) = (0.13, 0.00, 0.14), time = 4.49.
+Batch  5: Hypervolume (random, qParEGO, qNEHVI) = (0.13, 0.00, 0.17), time = 4.65.
+Batch  6: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.00, 0.23), time = 5.38.
+Batch  7: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.06, 0.25), time = 6.17.
+Batch  8: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.12, 0.27), time = 5.26.
+Batch  9: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.19, 0.28), time = 6.60.
+Batch 10: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.20, 0.28), time = 6.12.
+Batch 11: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.23, 0.32), time = 6.05.
+Batch 12: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.25, 0.34), time = 6.76.
+Batch 13: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.25, 0.35), time = 6.47.
+Batch 14: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.27, 0.36), time = 7.86.
+Batch 15: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.28, 0.36), time = 5.15.
+Batch 16: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.28, 0.36), time = 5.09.
+Batch 17: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.31, 0.37), time = 7.28.
+Batch 18: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.32, 0.37), time = 7.97.
+Batch 19: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.34, 0.37), time = 8.76.
+Batch 20: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.34, 0.38), time = 5.98.
+
+
+
+
+
+
+
+
+

Plot the results

The plot below shows the log feasible hypervolume difference: the log difference between the hypervolume of the true feasible pareto front and the hypervolume of the observed (feasible) pareto front identified by each algorithm. The log feasible hypervolume difference is plotted at each step of the optimization for each of the algorithms.

+

The plot show that $q$NEHVI vastly outperforms the $q$ParEGO and Sobol baselines.

+
+
+
+
+
+
In [7]:
+
+
+
import numpy as np
+from matplotlib import pyplot as plt
+
+
+%matplotlib inline
+
+
+iters = np.arange(N_BATCH + 1) * BATCH_SIZE
+log_hv_difference_qparego = np.log10(problem.max_hv - np.asarray(hvs_qparego))
+log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi))
+log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random))
+
+fig, ax = plt.subplots(1, 1, figsize=(8, 6))
+ax.plot(
+    iters,
+    log_hv_difference_rnd,
+    label="Sobol",
+    linewidth=1.5,
+    color="gray",
+)
+ax.plot(
+    iters,
+    log_hv_difference_qparego,
+    label="qParEGO",
+    linewidth=1.5,
+    color="red",
+)
+ax.plot(
+    iters,
+    log_hv_difference_qnehvi,
+    label="qNEHVI",
+    linewidth=1.5,
+    color="blue",
+)
+ax.set(
+    xlabel="number of observations (beyond initial points)",
+    ylabel="Log Hypervolume Difference",
+)
+ax.legend(loc="lower right")
+
+
+
+
+
+
+
+
Out[7]:
+
+
<matplotlib.legend.Legend at 0x17f7b6380>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Plot the observations colored by iteration

To examine optimization process from another perspective, we plot the collected observations under each algorithm where the color corresponds to the BO iteration at which the point was collected. The plot on the right for $q$NEHVI shows that the $q$NEHVI quickly identifies the pareto front and most of its evaluations are very close to the pareto front. $q$ParEGO also identifies has many observations close to the pareto front, but relies on optimizing random scalarizations, which is a less principled way of optimizing the pareto front compared to $q$NEHVI, which explicitly attempts focuses on improving the pareto front. Sobol generates random points and has few points close to the pareto front

+
+
+
+
+
+
In [8]:
+
+
+
from matplotlib.cm import ScalarMappable
+import matplotlib
+
+
+fig, axes = plt.subplots(1, 3, figsize=(17, 5))
+algos = ["Sobol", "qParEGO", "qNEHVI"]
+cm = plt.get_cmap("viridis")
+
+batch_number = torch.cat(
+    [
+        torch.zeros(2 * (d + 1)),
+        torch.arange(1, N_BATCH + 1).repeat(BATCH_SIZE, 1).t().reshape(-1),
+    ]
+).numpy()
+
+for i, train_obj in enumerate((train_obj_random, train_obj_qparego, train_obj_qnehvi)):
+    sc = axes[i].scatter(
+        train_obj[:, 0].cpu().numpy(),
+        train_obj[:, 1].cpu().numpy(),
+        c=batch_number,
+        alpha=0.8,
+    )
+    axes[i].set_title(algos[i])
+    axes[i].set_xlabel("Objective 1")
+    axes[i].set_xlim(-2.5, 0)
+    axes[i].set_ylim(-2.5, 0)
+axes[0].set_ylabel("Objective 2")
+norm = plt.Normalize(batch_number.min(), batch_number.max())
+sm = ScalarMappable(norm=norm, cmap=cm)
+sm.set_array([])
+fig.subplots_adjust(right=0.9)
+cbar_ax = fig.add_axes([0.93, 0.15, 0.01, 0.7])
+cbar = fig.colorbar(sm, cax=cbar_ax)
+cbar.ax.set_title("Iteration")
+
+
+
+
+
+
+
+
+
+
/var/folders/_j/_hhj7k4913d4jlzgq92bw9b00000gn/T/ipykernel_16702/4269187899.py:7: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed two minor releases later. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap(obj)`` instead.
+  cm = plt.get_cmap("viridis")
+
+
+
+
+
Out[8]:
+
+
Text(0.5, 1.0, 'Iteration')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/constraint_active_search.html b/website-old/_tutorials/constraint_active_search.html new file mode 100644 index 0000000000..c3de96114a --- /dev/null +++ b/website-old/_tutorials/constraint_active_search.html @@ -0,0 +1,590 @@ + + + +
+
+
+
+

Constraint Active Search for Multiobjective Experimental Design

In this tutorial we show how to implement the Expected Coverage Improvement (ECI) [1] acquisition function in BoTorch. For a number of outcome constraints, ECI tries to efficiently discover the feasible region and simultaneously sample diverse feasible configurations. Given a user-specified punchout radius $r$, we center a sphere with that radius around each evaluated configuration. The total coverage is now given by the volume of the union of these sphere intersected with the feasible region; see the paper and, in particular, Figure 2 for a full description of how ECI works.

+

By design, ECI prefers candidates that are in unexplored regions since the candidate's corresponding sphere won't intersect with the spheres around the previously evaluated configurations. On the other hand, ECI also prefers configurations that are likely to satisfy the constraints and to give an improvement in the total coverage. This results in an exploitation-exploration trade-off similar to other acquisition functions.

+

ECI may be estimated using the following equation: +$$ +\text{ECI}(x) = \sum_{x' \in \mathbb{N}(x) \setminus \mathbb{N}_{r}(X)} p(Z(x') = 1 \;|\; \mathcal{D}_t). +$$

+

where $\mathbb{N}(x) \setminus \mathbb{N}_{r}(X)$ a set of points generated via Monte Carlo to be inside a sphere of radius $r$ around $x$, but sufficiently far from the set of known evaluations $X$ (where sufficiently far is defined by the punchout radius $r$). The function $p(Z(x') = 1 \;|\; \mathcal{D}_t)$ is the probability that the GP at $x'$ satisfies a user-specified threshold value, or threshold values in the case of multiple objective functions.

+

[1]: Malkomes et al., Beyond the Pareto Efficient Frontier: Constraint Active Search for Multiobjective Experimental Design, Proceedings of the 38th International Conference on Machine Learning, 2021.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+
+import torch
+from botorch.acquisition.monte_carlo import MCAcquisitionFunction
+from botorch.acquisition.objective import IdentityMCObjective
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import ModelListGP, SingleTaskGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.optim import optimize_acqf
+from botorch.utils.sampling import sample_hypersphere
+from botorch.utils.transforms import t_batch_mode_transform
+from gpytorch.constraints import Interval
+from gpytorch.likelihoods import GaussianLikelihood
+from gpytorch.mlls import ExactMarginalLogLikelihood
+from torch.quasirandom import SobolEngine
+
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
In [2]:
+
+
+
tkwargs = {
+    "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"),
+    "dtype": torch.double,
+}
+
+
+
+
+
+
+
+
+

To start, we need to be able to sample points in $\mathbb{N}(x) \setminus \mathbb{N}_{r}(X)$. We can generate a pool of points and use standard rejection sampling to do so, but this leads to an acquisition function that isn't immediately differentiable; rejection sampling is essentially providing either a binary weight of either 0 or 1 to each point in the sample pool, which is not a differentiable function.

+

In order to make the acquisition function differentiable, we rely on a differentiable approximation of this binary weight function. For example, smooth_box_mask is a continuous differentiable approximation of $a < x < b$ (see the plot below for a visualization). A larger value of eps will make the sigmoid less steep and result in a smoother (and easier to optimize) but less accurate acquisition function.

+
+
+
+
+
+
In [3]:
+
+
+
def smooth_mask(x, a, eps=2e-3):
+    """Returns 0ish for x < a and 1ish for x > a"""
+    return torch.nn.Sigmoid()((x - a) / eps)
+
+
+def smooth_box_mask(x, a, b, eps=2e-3):
+    """Returns 1ish for a < x < b and 0ish otherwise"""
+    return smooth_mask(x, a, eps) - smooth_mask(x, b, eps)
+
+
+
+
+
+
+
+
In [4]:
+
+
+
import matplotlib.pyplot as plt
+
+%matplotlib inline
+
+
+x = torch.linspace(-2, 2, 500, **tkwargs)
+
+fig, ax = plt.subplots(1, 2, figsize=(8, 4))
+ax[0].plot(x.cpu(), smooth_mask(x, -1).cpu(), "b")
+ax[1].plot(x.cpu(), smooth_box_mask(x, -1, 1).cpu(), "b")
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Implementation of ECI

Once we have defined our smooth mask functions, we can compute a differentiable approximation of ECI in a straightforward manner using Monte Carlo (MC). We use the popular variance reduction technique of Common random numbers (CRN).

+

We first use a low discrepancy sequence to generate a set of base samples. We integrate (sum) over these base samples to approximate the ECI acquisition function. Fixing these base samples makes the method deterministic and by using the smooth masks defined earlier, we can filter out infeasible points while still having a differentiable acquisition function.

+

This implementation assumes that the GP models for the different outputs are independent and that each constraints only affects one output (simple box-constraints like f(x) <= 0.5).

+
+
+
+
+
+
In [5]:
+
+
+
class ExpectedCoverageImprovement(MCAcquisitionFunction):
+    def __init__(
+        self,
+        model,
+        constraints,
+        punchout_radius,
+        bounds,
+        num_samples=128,
+        **kwargs,
+    ):
+        """Expected Coverage Improvement (q=1 required, analytic)
+
+        Right now, we assume that all the models in the ModelListGP have
+        the same training inputs.
+
+        Args:
+            model: A ModelListGP object containing models matching the corresponding constraints.
+                All models are assumed to have the same training data.
+            constraints: List containing 2-tuples with (direction, value), e.g.,
+                [('gt', 3), ('lt', 4)]. It is necessary that
+                len(constraints) == model.num_outputs.
+            punchout_radius: Positive value defining the desired minimum distance between points
+            bounds: torch.tensor whose first row is the lower bounds and second row is the upper bounds
+            num_samples: Number of samples for MC integration
+        """
+        super().__init__(model=model, objective=IdentityMCObjective(), **kwargs)
+        assert len(constraints) == model.num_outputs
+        assert all(direction in ("gt", "lt") for direction, _ in constraints)
+        assert punchout_radius > 0
+        self.constraints = constraints
+        self.punchout_radius = punchout_radius
+        self.bounds = bounds
+        self.base_points = self.train_inputs
+        self.ball_of_points = self._generate_ball_of_points(
+            num_samples=num_samples,
+            radius=punchout_radius,
+            device=bounds.device,
+            dtype=bounds.dtype,
+        )
+        self._thresholds = torch.tensor(
+            [threshold for _, threshold in self.constraints]
+        ).to(bounds)
+        assert (
+            all(ub > lb for lb, ub in self.bounds.T) and len(self.bounds.T) == self.dim
+        )
+
+    @property
+    def num_outputs(self):
+        return self.model.num_outputs
+
+    @property
+    def dim(self):
+        return self.train_inputs.shape[-1]
+
+    @property
+    def train_inputs(self):
+        return self.model.models[0].train_inputs[0]
+
+    def _generate_ball_of_points(
+        self, num_samples, radius, device=None, dtype=torch.double
+    ):
+        """Creates a ball of points to be used for MC."""
+        tkwargs = {"device": device, "dtype": dtype}
+        z = sample_hypersphere(d=self.dim, n=num_samples, qmc=True, **tkwargs)
+        r = torch.rand(num_samples, 1, **tkwargs) ** (1 / self.dim)
+        return radius * r * z
+
+    def _get_base_point_mask(self, X):
+        distance_matrix = self.model.models[0].covar_module.covar_dist(
+            X, self.base_points
+        )
+        return smooth_mask(distance_matrix, self.punchout_radius)
+
+    def _estimate_probabilities_of_satisfaction_at_points(self, points):
+        """Estimate the probability of satisfying the given constraints."""
+        posterior = self.model.posterior(X=points)
+        mus, sigma2s = posterior.mean, posterior.variance
+        dist = torch.distributions.normal.Normal(mus, sigma2s.sqrt())
+        norm_cdf = dist.cdf(self._thresholds)
+        probs = torch.ones(points.shape[:-1]).to(points)
+        for i, (direction, _) in enumerate(self.constraints):
+            probs = probs * (
+                norm_cdf[..., i] if direction == "lt" else 1 - norm_cdf[..., i]
+            )
+        return probs
+
+    @t_batch_mode_transform(expected_q=1)
+    def forward(self, X):
+        """Evaluate Expected Improvement on the candidate set X."""
+        ball_around_X = self.ball_of_points + X
+        domain_mask = smooth_box_mask(
+            ball_around_X, self.bounds[0, :], self.bounds[1, :]
+        ).prod(dim=-1)
+        num_points_in_integral = domain_mask.sum(dim=-1)
+        base_point_mask = self._get_base_point_mask(ball_around_X).prod(dim=-1)
+        prob = self._estimate_probabilities_of_satisfaction_at_points(ball_around_X)
+        masked_prob = prob * domain_mask * base_point_mask
+        y = masked_prob.sum(dim=-1) / num_points_in_integral
+        return y
+
+
+
+
+
+
+
+
In [6]:
+
+
+
def get_and_fit_gp(X, Y):
+    """Simple method for creating a GP with one output dimension.
+
+    X is assumed to be in [0, 1]^d.
+    """
+    assert Y.ndim == 2 and Y.shape[-1] == 1
+    likelihood = GaussianLikelihood(noise_constraint=Interval(1e-6, 1e-3))  # Noise-free
+    octf = Standardize(m=1)
+    gp = SingleTaskGP(X, Y, likelihood=likelihood, outcome_transform=octf)
+    mll = ExactMarginalLogLikelihood(model=gp, likelihood=gp.likelihood)
+    fit_gpytorch_mll(mll)
+    return gp
+
+
+
+
+
+
+
+
+

Simple 1D function

To sanity check things, we consider the ECI acquisition function on a one-dimensional toy problem.

+
+
+
+
+
+
In [7]:
+
+
+
def yf(x):
+    return (1 - torch.exp(-4 * (x[:, 0] - 0.4) ** 2)).unsqueeze(-1)
+
+
+x = torch.tensor([0, 0.15, 0.25, 0.4, 0.8, 1.0], **tkwargs).unsqueeze(-1)
+y = yf(x)
+xx = torch.linspace(0, 1, 200, **tkwargs).unsqueeze(-1)
+yy = yf(xx)
+
+
+
+
+
+
+
+
+

Create an ECI acquisition function

Our implementation assumes that the GP is passed in as a ModelListGP and that the GPs match the corresponding constraints. As an example, assume we have two outputs, represented by gp1 and gp2 and two constraints corresponding to output 1 and a third constraint corresponding to output 2. In that case we will create a model list GP as ModelListGP(gp1, gp1, gp2) so they match the constraints.

+
+
+
+
+
+
In [8]:
+
+
+
gp = get_and_fit_gp(x, y)
+model_list_gp = ModelListGP(gp, gp)
+constraints = [("lt", 0.3), ("gt", 0.05)]
+punchout_radius = 0.03
+bounds = torch.tensor([(0, 1)], **tkwargs).T
+eci = ExpectedCoverageImprovement(
+    model=model_list_gp,
+    constraints=constraints,
+    punchout_radius=punchout_radius,
+    bounds=bounds,
+    num_samples=128 if not SMOKE_TEST else 4,
+)
+
+
+
+
+
+
+
+
+

Optimize the acquisition function

+
+
+
+
+
+
In [9]:
+
+
+
best_candidate, best_eci_value = optimize_acqf(
+    acq_function=eci,
+    bounds=torch.tensor([[0.0], [1.0]], **tkwargs),
+    q=1,
+    num_restarts=10,
+    raw_samples=20,  # use a small number here to make sure the optimization works
+)
+print(f"Best candidate: {best_candidate.cpu().item():.3f}")
+
+
+
+
+
+
+
+
+
+
Best candidate: 0.617
+
+
+
+
+
+
+
+
+
+

Plot the GP and the ECI acquisition function

The left plot shows the GP posterior with a 95% confidence interval. The two horizontal lines indicate the feasible region defined by $0.05 \leq f(x) \leq 0.3$. These inequality constraints implicitly define a feasible region, outside which ECI has value zero.

+

We can see in the right plot that ECI indeed has a nonzero value inside the feasible region and a zero value outside. We also optimize the acquisition function and mark its argmax with black star; the argmax is around $x=0.62$. This is reasonable because ECI seeks to select diverse points within the feasible region. $x=0.62$ is far away from other evaluations and thus has the highest diversity.

+
+
+
+
+
+
In [10]:
+
+
+
with torch.no_grad():
+    posterior = gp.posterior(X=xx.unsqueeze(1))
+ymean, yvar = posterior.mean.squeeze(-1), posterior.variance.squeeze(-1)
+eci_vals = eci(xx.unsqueeze(1))
+
+fig, axes = plt.subplots(1, 2, figsize=(12, 5))
+ax = axes[0]
+ax.plot(xx[:, 0].cpu(), ymean[:, 0].cpu(), "b")
+ax.fill_between(
+    xx[:, 0].cpu(),
+    ymean[:, 0].cpu() - 1.96 * yvar[:, 0].sqrt().cpu(),
+    ymean[:, 0].cpu() + 1.96 * yvar[:, 0].sqrt().cpu(),
+    alpha=0.1,
+    color="b",
+)
+ax.plot(x[:, 0].cpu(), y[:, 0].cpu(), "or")
+ax.axhline(0.05, 0, 1)
+ax.axhline(0.3, 0, 1)
+
+ax = axes[1]
+ax.plot(xx[:, 0].cpu(), eci_vals.detach().cpu())
+ax.plot(x[:, 0].cpu(), torch.zeros(len(x), **tkwargs).cpu(), "or")
+ax.plot(best_candidate.cpu(), best_eci_value.cpu(), "*k", ms=10)
+ax.set_title("ECI", fontsize=14)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Full 2D CAS-loop

This creates a simple function with two outputs that we will consider under the two constraints $f_1(x) \leq 0.75$ and $f_2(x) \geq 0.55$. In this particular example, the $f_1(x)$ and $f_2(x)$ are same function for simplicity.

+

The CAS loop follows the prototypical BO loop:

+
    +
  1. Given a surrogate model, maximize ECI to select the next evaluation x.
  2. +
  3. Observe f(x).
  4. +
  5. Update the surrogate model.
  6. +
+
+
+
+
+
+
In [11]:
+
+
+
def yf2d(x):
+    v = torch.exp(-2 * (x[:, 0] - 0.3) ** 2 - 4 * (x[:, 1] - 0.6) ** 2)
+    return torch.stack((v, v), dim=-1)
+
+
+bounds = torch.tensor([[0, 0], [1, 1]], **tkwargs)
+lb, ub = bounds
+dim = len(lb)
+constraints = [("lt", 0.75), ("gt", 0.55)]
+punchout_radius = 0.1
+
+
+
+
+
+
+
+
+

CAS loop using 5 initial Sobol points and 15 ECI iterations

+
+
+
+
+
+
In [12]:
+
+
+
num_init_points = 5
+num_total_points = 15 if not SMOKE_TEST else 5
+
+X = lb + (ub - lb) * SobolEngine(dim, scramble=True).draw(num_init_points).to(**tkwargs)
+Y = yf2d(X)
+
+while len(X) < num_total_points:
+    # We don't have to normalize X since the domain is [0, 1]^2. Make sure to
+    # appropriately adjust the punchout radius if the domain is normalized.
+    gp_models = [get_and_fit_gp(X, Y[:, i : i + 1]) for i in range(Y.shape[-1])]
+    model_list_gp = ModelListGP(gp_models[0], gp_models[1])
+    eci = ExpectedCoverageImprovement(
+        model=model_list_gp,
+        constraints=constraints,
+        punchout_radius=punchout_radius,
+        bounds=bounds,
+        num_samples=128 if not SMOKE_TEST else 4,
+    )
+    x_next, _ = optimize_acqf(
+        acq_function=eci,
+        bounds=bounds,
+        q=1,
+        num_restarts=10 if not SMOKE_TEST else 2,
+        raw_samples=512 if not SMOKE_TEST else 4,
+    )
+    y_next = yf2d(x_next)
+    X = torch.cat((X, x_next))
+    Y = torch.cat((Y, y_next))
+
+
+
+
+
+
+
+
+

Plot the selected points

We plot the feasible region and the points selected by ECI below. The feasible region is outlined with a black ring, and points selected by ECI are marked in green (feasible) and red (infeasible). By design, observe that ECI selects a diverse i.e., well-spaced set of points inside the feasible region.

+
+
+
+
+
+
In [13]:
+
+
+
N1, N2 = 30, 30
+Xplt, Yplt = torch.meshgrid(
+    torch.linspace(0, 1, N1, **tkwargs), torch.linspace(0, 1, N2, **tkwargs)
+)
+xplt = torch.stack(
+    (
+        torch.reshape(Xplt, (Xplt.shape[0] * Xplt.shape[1],)),
+        torch.reshape(Yplt, (Yplt.shape[0] * Yplt.shape[1],)),
+    ),
+    dim=1,
+)
+yplt = yf2d(xplt)
+Zplt = torch.reshape(yplt[:, 0], (N1, N2))  # Since f1(x) = f2(x)
+
+
+
+
+
+
+
+
+
+
/Users/deriksson/opt/anaconda3/lib/python3.9/site-packages/torch/functional.py:504: UserWarning: torch.meshgrid: in an upcoming release, it will be required to pass the indexing argument. (Triggered internally at /Users/runner/work/pytorch/pytorch/pytorch/aten/src/ATen/native/TensorShape.cpp:3191.)
+  return _VF.meshgrid(tensors, **kwargs)  # type: ignore[attr-defined]
+
+
+
+
+
+
+
+
+
In [14]:
+
+
+
def identify_samples_which_satisfy_constraints(X, constraints):
+    """
+    Takes in values (a1, ..., ak, o) and returns (a1, ..., ak, o)
+    True/False values, where o is the number of outputs.
+    """
+    successful = torch.ones(X.shape).to(X)
+    for model_index in range(X.shape[-1]):
+        these_X = X[..., model_index]
+        direction, value = constraints[model_index]
+        successful[..., model_index] = (
+            these_X < value if direction == "lt" else these_X > value
+        )
+    return successful
+
+
+fig, ax = plt.subplots(figsize=(8, 6))
+h1 = ax.contourf(Xplt.cpu().numpy(), Yplt.cpu().numpy(), Zplt.cpu().numpy(), 20, cmap="Blues", alpha=0.6)
+fig.colorbar(h1)
+ax.contour(Xplt.cpu().numpy(), Yplt.cpu().numpy(), Zplt.cpu().numpy(), [0.55, 0.75], colors="k")
+
+feasible_inds = (
+    identify_samples_which_satisfy_constraints(Y, constraints)
+    .prod(dim=-1)
+    .to(torch.bool)
+)
+ax.plot(X[feasible_inds, 0].cpu(), X[feasible_inds, 1].cpu(), "sg", label="Feasible")
+ax.plot(
+    X[~feasible_inds, 0].cpu(), X[~feasible_inds, 1].cpu(), "sr", label="Infeasible"
+)
+
+ax.legend(loc=[0.7, 0.05])
+ax.set_title("$f_1(x)$")  # Recall that f1(x) = f2(x)
+ax.set_xlabel("$x_1$")
+ax.set_ylabel("$x_2$")
+ax.set_aspect("equal", "box")
+ax.set_xlim([-0.05, 1.05])
+ax.set_ylim([-0.05, 1.05])
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/cost_aware_bayesian_optimization.html b/website-old/_tutorials/cost_aware_bayesian_optimization.html new file mode 100644 index 0000000000..df2cbb2502 --- /dev/null +++ b/website-old/_tutorials/cost_aware_bayesian_optimization.html @@ -0,0 +1,613 @@ + + + +
+
+
+
+

Cost-aware Bayesian Optimization

This tutorial covers cost-aware Bayesian optimization, a situation in which the cost of evaluation is unknown but assumed to depend on the set or a subset of the optimization parameters.

+

Note that cost-aware Bayesian optimization is a more general form of multifidelity Bayesian optimization:

+
    +
  • In multi-fidelity Bayesian optimization, the fidelity parameters are typically known ahead of time, an the relationship between cost and performance is typically known e.g., the highest fidelity parameters are the least noisy and the most costly.
  • +
  • In cost-aware Bayesian optimization, we do not know a-priori which parameters dictate cost, nor do we make any assumptions about the relationship between cost and performance.
  • +
+

Cost-aware Bayesian optimization is well suited to any problem for which the user suspects there to be a heterogenous cost of evaluation. It can also be used as a simpler alternative to multifidelity optimization, although we recommend a dedicated multifidelity algorithm for more experienced users. In this tutorial, the acquisition function we use for cost-aware Bayesian optimization is expected improvement per unit (EIpu), which has the following formula:

+

$$ +EIpu(x) = \frac{EI(x)}{c(x)^\alpha} +$$

+

$c(x)$ is a cost model that predicts the evaluation cost and $\alpha \in [0, 1]$ is a decay factor that reduces or increases the cost model's effect to prevent cheap points from dominating the optimization routine. We recommend starting $\alpha$ at 1 and decreasing it to 0 as the optimization budget is exhausted.

+

[1]: Lee, Eric Hans, et al. Cost-aware Bayesian Optimization. International Conference on Machine Learning, AutoML Workshop. 2020.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import time
+import torch
+import warnings
+
+from abc import ABC, abstractmethod
+
+from botorch.acquisition import AnalyticAcquisitionFunction, ExpectedImprovement
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from botorch.models.transforms import Log
+from botorch.optim import optimize_acqf
+from botorch.test_functions import Ackley
+from botorch.utils import standardize
+from botorch.utils.sampling import draw_sobol_samples
+
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+import matplotlib.pyplot as plt
+%matplotlib inline
+
+warnings.filterwarnings("ignore")
+device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")
+tkwargs = {
+    "device": device,
+    "dtype": torch.double,
+}
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Cost Modeling

The first thing we do in this tutorial is define a simple cost model, in which we make no assumptions other than a positive cost. We will use the mean of a GP for the cost model. To enforce positivity, we will model the log cost and then exponentiate when we perform predictions. Users can use more bespoke cost models should they have a better understanding of their problem.

+

Having defined the cost model, we'll also generate some simple plots of a 1D synthetic problem for illustrative purposes, where the objective is the Ackley function and the cost is quadratic.

+
+
+
+
+
+
In [2]:
+
+
+
class CostModel(torch.nn.Module, ABC):
+    """
+    Simple abstract class for a cost model.
+    """    
+    
+    @abstractmethod
+    def forward(self, X):
+        pass
+    
+
+class CostModelGP(CostModel):
+    """
+    A basic cost model that assumes the cost is positive.
+    It models the log cost to guarantee positive cost predictions.
+    """
+
+    def __init__(self, X, Y_cost):
+        assert torch.all(Y_cost > 0)
+        super().__init__()
+        gp = SingleTaskGP(train_X=X, train_Y=Y_cost, outcome_transform=Log())
+        mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)
+        fit_gpytorch_mll(mll)
+        self.gp = gp
+
+    def forward(self, X):
+        return torch.exp(self.gp(X).mean)
+
+
+
+
+
+
+
+
In [3]:
+
+
+
def synthetic_objective_with_cost(x):
+    dim = 1
+    f = Ackley(dim)  # synthetic objective is the Ackley
+    fx = f(x).unsqueeze(1)
+    cx = 200 * (1.1 - x) ** 2  # synthetic cost is quadratric
+    return fx, cx
+
+
+# Generate training data
+dim = 1
+num = 4
+bounds = torch.tensor([[0] * dim, [1] * dim], **tkwargs)
+train_X = draw_sobol_samples(bounds=bounds, n=num, q=1, seed=111).squeeze(1)
+train_Y, cost_Y = synthetic_objective_with_cost(train_X)
+
+# Fit GP to data
+train_Y = standardize(train_Y)
+gp = SingleTaskGP(train_X=train_X, train_Y=train_Y)
+mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)
+fit_gpytorch_mll(mll)
+
+# Fit cost model to data
+cost_model_gp = CostModelGP(train_X, cost_Y)
+
+
+
+
+
+
+
+
+

Plot the GP and the Cost Model

+
+
+
+
+
+
In [4]:
+
+
+
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
+
+# Plot GP
+X_preds = torch.linspace(0, 1, 100, **tkwargs).unsqueeze(1)
+Y_preds = gp.posterior(X_preds)
+Y_mean = Y_preds.mean.squeeze().detach().numpy()
+Y_var = Y_preds.variance.squeeze().detach().numpy()
+axes[0].plot(X_preds, Y_preds.mean.detach().numpy(), "r")
+axes[0].plot(train_X, train_Y, "k^")
+axes[0].fill_between(
+    X_preds.numpy()[:, 0], Y_mean - Y_var, Y_mean + Y_var, color="m", alpha=0.5
+)
+axes[0].set_title("Gaussian Process Model")
+axes[0].set_ylabel("Objective Value")
+
+# Plot Cost Model
+cost_preds = cost_model_gp(X_preds)
+axes[1].plot(X_preds, cost_preds.detach().numpy())
+axes[1].plot(train_X, cost_Y, "kv")
+axes[1].set_title("Cost Model")
+axes[1].set_ylabel("Cost of Evaluation")
+
+
+
+
+
+
+
+
Out[4]:
+
+
Text(0, 0.5, 'Cost of Evaluation')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Expected Improvement Per Unit

Having defined the cost model, we can now define our EIpu acquisition function and plot it for different values of $\alpha$. Note that when $\alpha=0$, EIpu simply reduces to EI.

+
+
+
+
+
+
In [5]:
+
+
+
class ExpectedImprovementWithCost(AnalyticAcquisitionFunction):
+    """
+    This is the acquisition function EI(x) / c(x) ^ alpha, where alpha is a decay
+    factor that reduces or increases the emphasis of the cost model c(x).
+    """
+
+    def __init__(self, model, best_f, cost_model, alpha=1):
+        super().__init__(model=model)
+        self.model = model
+        self.cost_model = cost_model
+        self.ei = ExpectedImprovement(model=model, best_f=best_f)
+        self.alpha = alpha
+
+    def forward(self, X):
+        return self.ei(X) / torch.pow(self.cost_model(X)[:, 0], self.alpha)
+
+
+
+
+
+
+
+
In [6]:
+
+
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
+X_preds = torch.linspace(0, 1, 100, **tkwargs).unsqueeze(1)
+X_batch = X_preds.unsqueeze(1)
+
+
+def normalize_acquisition_values(values):
+    max_value = values.max().item()
+    min_value = values.min().item()
+    return (values - min_value) / (max_value - min_value)
+
+
+# Compute EI
+fmax = torch.max(train_Y)
+ei = ExpectedImprovement(model=gp, best_f=fmax)
+ei_values = normalize_acquisition_values(ei(X_batch))
+
+# Compute and plot EIpu vs EI
+fig.suptitle("EIpu (green) vs EI (blue)")
+for i in range(3):
+    alpha = 1 - i / 2
+    eipu = ExpectedImprovementWithCost(
+        model=gp,
+        best_f=fmax,
+        cost_model=cost_model_gp,
+        alpha=alpha,
+    )
+    eipu_values = normalize_acquisition_values(eipu(X_batch).squeeze())
+    axes[i].plot(X_preds, eipu_values.detach().numpy(), "-g", linewidth=3)
+    axes[i].plot(X_preds, ei_values.detach().numpy(), "--b", alpha=1, linewidth=3)
+    axes[i].set_title(f"alpha={alpha}")
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

A Practial Problem

To make things more interesting, let's look at the classic problem of least squares estimation:

+

$$ +\text{arg} \min_{x \in \mathbb{R}^d} \| Ax - b \|_2 +$$

+

$A$ is a matrix of size $n \times d$ and $b$ is a vector of length $n$. Assuming that $n \geq d$, the solution to this problem is unique and has the following closed form: $(A^T A) ^{-1} (A^T b)$. The problem with explicitly computing this solution is that it will have an $\mathcal{O}(n^3)$ complexity due to the need to compute a Cholesky factorization of the matrix $A^T A$.

+

These difficulties in computing an explicit solution when $n$ is large lead us to a cost-aware twist on the least squares estimation. An alternative solution is to perform batched gradient descent by sampling rows of $A$. Because the batching introduces noise, we'll use Adam to perform the optimization. This introduces hyperparameters such as the learning rate, batch size, and the number of optimization iterations. These hyperparameters influence the cost immensely, as we'll see in a bit.

+
+
+
+
+
+
In [7]:
+
+
+
class NoisyLinearLeastSquares:
+    """
+    The standard linear least squares problem min_x ||Ax - b||_2.
+    We compute the loss via batching that introduces noise.
+    """
+
+    def __init__(self, A, b, batch_size=50):
+        self.A = A
+        self.b = b
+        self.batch_size = min(batch_size, self.A.shape[0])
+
+    def fit(self, lr=1, niters=100):
+        x = torch.zeros(A.shape[1], 1, requires_grad=True, **tkwargs)
+        optimizer = torch.optim.Adam([x], lr=lr)
+        batch_indices = torch.randperm(A.shape[1])[: self.batch_size]
+        for i in range(niters):
+            res = torch.matmul(self.A[batch_indices, :], x) - self.b[batch_indices]
+            loss = torch.norm(res)
+            optimizer.zero_grad()
+            loss.backward()
+            optimizer.step()
+        return x, loss
+
+
+
+
+
+
+
+
+

Cost Analysis

Here, we examine the variation in runtime as we vary both the batch size and the number of Adam iterations. Perhaps unsurpsingly, the runtime varies significantly with these parameters. Though we expect the runtime to be stricly linear in both the batch size and the number of Adam iterations, we can see that in practice the graph is a little variance due to the nuances in which the computer executes the matrix operations.

+
+
+
+
+
+
In [8]:
+
+
+
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
+n = 30000 if not SMOKE_TEST else 300
+d = 3000 if not SMOKE_TEST else 30
+A = torch.rand(n, d, **tkwargs)
+b = torch.rand(n, 1, **tkwargs)
+
+# Timings varying batch size
+batch_sizes = 100 * torch.arange(4, 10, device=device) 
+times_batch = []
+for batch_size in batch_sizes:
+    model = NoisyLinearLeastSquares(A, b, batch_size=batch_size)
+    t_start = time.time()
+    model.fit(lr=0.1, niters=200)
+    times_batch.append(time.time() - t_start)
+
+axes[0].set_title("Time vs Batch Size")
+axes[0].set_xlabel("Batch Size")
+axes[0].set_ylabel("Runtime (s)")
+axes[0].plot(batch_sizes, times_batch, "b")
+
+# Timings varying number of Adam iterations
+iter_count = 10 * torch.arange(1, 20, device=device)
+times_iters = []
+for niters in iter_count:
+    model = NoisyLinearLeastSquares(A, b)
+    t_start = time.time()
+    model.fit(lr=0.1, niters=niters)
+    times_iters.append(time.time() - t_start)
+
+axes[1].set_title("Time vs Iterations")
+axes[1].set_xlabel("Iteration Count")
+axes[1].set_ylabel("Runtime (s)")
+axes[1].plot(iter_count, times_iters, "g")
+
+plt.tight_layout()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Full Optimization Loop

Having defined our problem, let's now run a full optimization loop and see how EIpu does compared to EI. Let's tune three hyperparameters in our least squares estimator: the learning rate, the batch size, and the number of adam iterations.

+
    +
  • $ \textit{learning_rate} \in [0.05, 1.0]$
  • +
  • $ \textit{batch_size} \in [40, 1000] $
  • +
  • $\textit{num_iters} \in [10, 400]$.
  • +
+

Previously, we mentioned that we can use bespoke cost models tailored to the specific problem to increase performance. Let's do this by replacing the generic GP cost model with a custom linear one. Note that we can only do this because we performed some cost analysis above and understand well the relationship between hyperparameters and cost. Our cost model will simply scale linearly with both the batch size and the number of iterations:

+

$$Cost\big(\textit{learning_rate}, \textit{batch_size}, \textit{num_iters}\big) \propto \textit{batch_size} \times \textit{num_iters} $$

+
+
+
+
+
+
In [9]:
+
+
+
# Assume x0 is learning rate, x1 is batch_size, x2 is iterations
+bounds = torch.tensor([[0.05, 40, 10], [1, 1000, 400]], **tkwargs)
+
+
+def objective(x):
+    learning_rate = x[0]
+    batch_size = int(x[1])
+    num_iters = int(x[2])
+    model = NoisyLinearLeastSquares(A, b, batch_size=batch_size)
+    t_start = time.time()
+    x, loss = model.fit(lr=learning_rate, niters=num_iters)
+    cost = time.time() - t_start
+    return loss.item(), cost
+
+
+# Simplified cost model based on analysis above
+class LinearCostModel(CostModel):
+    def __init__(self):
+        super().__init__()
+
+    # Assume x1 is batch_size, x2 is iterations
+    def forward(self, X):
+        return X[:, :, 1] * X[:, :, 2]
+
+
+def generate_initial_data(obj, bounds, num):
+    dim = bounds.shape[1]
+    train_x = draw_sobol_samples(bounds=bounds, n=num, q=1, seed=111).squeeze(1)
+    train_y = []
+    cost_y = []
+    for x in train_x:
+        y, c = obj(x)
+        train_y.append(y)
+        cost_y.append(c)
+    return (
+        train_x,
+        torch.tensor(train_y, **tkwargs).unsqueeze(-1),
+        torch.tensor(cost_y, **tkwargs).unsqueeze(-1),
+    )
+
+
+# Generate initial data
+budget = 25
+num_initial = 5
+init_X, init_Y, init_C = generate_initial_data(objective, bounds, num_initial)
+
+
+
+
+
+
+
+
+

Run Bayesian optimization with EIpu

+
+
+
+
+
+
In [10]:
+
+
+
train_X = init_X
+train_Y = init_Y
+cost_Y = init_C
+
+for i in range(budget):
+    alpha = (budget - i - 1) / (budget - 1)
+
+    # Train GP
+    train_Y_flip = -1 * standardize(train_Y)  # we want to minimize so we negate
+    gp = SingleTaskGP(train_X=train_X, train_Y=train_Y_flip)
+    mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)
+    fit_gpytorch_mll(mll)
+
+    # Train Cost Model
+    cost_model = LinearCostModel()
+    fmax = torch.max(train_Y_flip)
+    eipu = ExpectedImprovementWithCost(
+        model=gp,
+        best_f=fmax,
+        cost_model=cost_model,
+        alpha=alpha,
+    )
+    new_x, acq_value = optimize_acqf(
+        acq_function=eipu,
+        bounds=bounds,
+        q=1,
+        num_restarts=5,
+        raw_samples=1024,
+    )
+
+    # Get objective value and cost
+    new_y, cost_y = objective(new_x.squeeze())
+
+    # update training points
+    train_X = torch.cat([train_X, new_x])
+    train_Y = torch.cat([train_Y, torch.tensor([new_y], **tkwargs).unsqueeze(1)])
+    cost_Y = torch.cat([cost_Y, torch.tensor([cost_y], **tkwargs).unsqueeze(1)])
+
+costs_eipu = cost_Y[:, 0]
+results_ei_cost, _ = torch.cummin(train_Y, dim=0)
+times_ei_cost = torch.cumsum(costs_eipu, dim=0)
+
+
+
+
+
+
+
+
+

Run Bayesian optimization with EI

+
+
+
+
+
+
In [11]:
+
+
+
train_X = init_X
+train_Y = init_Y
+cost_Y = init_C
+
+for i in range(budget):
+    # Train GP
+    train_Y_flip = -1 * standardize(train_Y)  # we want to minimize so we negate
+    gp = SingleTaskGP(train_X=train_X, train_Y=train_Y_flip)
+    mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)
+    fit_gpytorch_mll(mll)
+
+    # Train Cost Model
+    fmax = torch.max(train_Y_flip)
+    ei = ExpectedImprovement(gp, fmax)
+    new_x, acq_value = optimize_acqf(
+        acq_function=ei,
+        bounds=bounds,
+        q=1,
+        num_restarts=5,
+        raw_samples=1024,
+    )
+
+    # Get objective value and cost
+    new_y, cost_y = objective(new_x.squeeze())
+
+    # update training points
+    train_X = torch.cat([train_X, new_x])
+    train_Y = torch.cat([train_Y, torch.tensor([new_y], **tkwargs).unsqueeze(1)])
+    cost_Y = torch.cat([cost_Y, torch.tensor([cost_y], **tkwargs).unsqueeze(1)])
+
+costs_ei = cost_Y[:, 0]
+results_ei, _ = torch.cummin(train_Y, dim=0)
+times_ei = torch.cumsum(costs_ei, dim=0)
+
+
+
+
+
+
+
+
+

Plotting Results

Unlike the usual optimization progress plots, which measure performance by comparing loss to iterations, in the cost aware setting, we measure performance by comparing loss to cumulative training time.

+

EIpu and EI take the same number of iterations, but we can see that EIpu takes less time to execute those iterations (and finds a better result). We've also plotted a histogram of the evaluation times on the right. We can see that because EI is not cost aware, it has a pretty even spread of evaluation costs, whereas EIpu evaluates many more cheap points.

+
+
+
+
+
+
In [16]:
+
+
+
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
+
+axes[0].plot(times_ei_cost, results_ei_cost, "--b", marker="^")
+axes[0].plot(times_ei, results_ei, "--r", marker="v", alpha=0.5)
+axes[0].set_xlabel("Cumulative Training Time (s)")
+axes[0].set_ylabel("Loss")
+axes[0].set_title("Loss over time")
+axes[0].legend(["EIpu", "EI"])
+
+axes[1].hist(costs_eipu, bins=20, color="b")
+axes[1].hist(costs_ei, bins=20, color="r", alpha=0.5)
+axes[1].set_xlabel("Evaluation Time")
+axes[1].set_ylabel("Number of Evaluations")
+axes[1].set_title("Histogram of Evaluation Times")
+axes[1].legend(["EIpu", "EI"])
+
+plt.tight_layout()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/custom_acquisition.html b/website-old/_tutorials/custom_acquisition.html new file mode 100644 index 0000000000..d288f4b980 --- /dev/null +++ b/website-old/_tutorials/custom_acquisition.html @@ -0,0 +1,926 @@ + + + +
+
+
+
+
+
+
+
+
+
+

Upper Confidence Bound (UCB)

The Upper Confidence Bound (UCB) acquisition function balances exploration and exploitation by assigning a score of $\mu + \sqrt{\beta} \cdot \sigma$ if the posterior distribution is normal with mean $\mu$ and variance $\sigma^2$. This "analytic" version is implemented in the UpperConfidenceBound class. The Monte Carlo version of UCB is implemented in the qUpperConfidenceBound class, which also allows for q-batches of size greater than one. (The derivation of q-UCB is given in Appendix A of Wilson et. al., 2017).

+

A scalarized version of q-UCB

Suppose now that we are in a multi-output setting, where, e.g., we model the effects of a design on multiple metrics. We first show a simple extension of the q-UCB acquisition function that accepts a multi-output model and performs q-UCB on a scalarized version of the multiple outputs, achieved via a vector of weights. Implementing a new acquisition function in botorch is easy; one simply needs to implement the constructor and a forward method.

+
+
+
+
+
+
In [1]:
+
+
+
import plotly.io as pio
+
+# Ax uses Plotly to produce interactive plots. These are great for viewing and analysis,
+# though they also lead to large file sizes, which is not ideal for files living in GH.
+# Changing the default to `png` strips the interactive components to get around this.
+pio.renderers.default = "png"
+
+
+
+
+
+
+
+
In [2]:
+
+
+
import math
+from typing import Optional
+
+from botorch.acquisition.monte_carlo import MCAcquisitionFunction
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils import t_batch_mode_transform
+from torch import Tensor
+
+
+class qScalarizedUpperConfidenceBound(MCAcquisitionFunction):
+    def __init__(
+        self,
+        model: Model,
+        beta: Tensor,
+        weights: Tensor,
+        sampler: Optional[MCSampler] = None,
+    ) -> None:
+        # we use the AcquisitionFunction constructor, since that of
+        # MCAcquisitionFunction performs some validity checks that we don't want here
+        super(MCAcquisitionFunction, self).__init__(model=model)
+        if sampler is None:
+            sampler = SobolQMCNormalSampler(sample_shape=torch.Size([512]))
+        self.sampler = sampler
+        self.register_buffer("beta", torch.as_tensor(beta))
+        self.register_buffer("weights", torch.as_tensor(weights))
+
+    @t_batch_mode_transform()
+    def forward(self, X: Tensor) -> Tensor:
+        """Evaluate scalarized qUCB on the candidate set `X`.
+
+        Args:
+            X: A `(b) x q x d`-dim Tensor of `(b)` t-batches with `q` `d`-dim
+                design points each.
+
+        Returns:
+            Tensor: A `(b)`-dim Tensor of Upper Confidence Bound values at the
+                given design points `X`.
+        """
+        posterior = self.model.posterior(X)
+        samples = self.get_posterior_samples(posterior)  # n x b x q x o
+        scalarized_samples = samples.matmul(self.weights)  # n x b x q
+        mean = posterior.mean  # b x q x o
+        scalarized_mean = mean.matmul(self.weights)  # b x q
+        ucb_samples = (
+            scalarized_mean
+            + math.sqrt(self.beta * math.pi / 2)
+            * (scalarized_samples - scalarized_mean).abs()
+        )
+        return ucb_samples.max(dim=-1)[0].mean(dim=0)
+
+
+
+
+
+
+
+
+
+
I1116 181426.999 _utils_internal.py:179] NCCL_DEBUG env var is set to None
+
+
+
+
+
+
+
I1116 181427.000 _utils_internal.py:188] NCCL_DEBUG is INFO from /etc/nccl.conf
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/mpmath/ctx_mp_python.py:892: SyntaxWarning:
+
+"is" with a literal. Did you mean "=="?
+
+/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/mpmath/ctx_mp_python.py:986: SyntaxWarning:
+
+"is" with a literal. Did you mean "=="?
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/solvers/diophantine.py:3188: SyntaxWarning:
+
+"is" with a literal. Did you mean "=="?
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:520: SyntaxWarning:
+
+"is" with a literal. Did you mean "=="?
+
+/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:540: SyntaxWarning:
+
+"is" with a literal. Did you mean "=="?
+
+/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:553: SyntaxWarning:
+
+"is" with a literal. Did you mean "=="?
+
+/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:560: SyntaxWarning:
+
+"is" with a literal. Did you mean "=="?
+
+
+
+
+
+
+
+
+
+
+

Note that qScalarizedUpperConfidenceBound is very similar to qUpperConfidenceBound and only requires a few lines of new code to accomodate scalarization of multiple outputs. The @t_batch_mode_transform decorator ensures that the input X has an explicit t-batch dimension (code comments are added with shapes for clarity).

+

See the end of this tutorial for a quick and easy way of achieving the same scalarization effect using ScalarizedPosteriorTransform.

+
+
+
+
+
+
+

Ad-hoc testing q-Scalarized-UCB

Before hooking the newly defined acquisition function into a Bayesian Optimization loop, we should test it. For this we'll just make sure that it properly evaluates on a compatible multi-output model. Here we just define a basic multi-output SingleTaskGP model trained on synthetic data.

+
+
+
+
+
+
In [3]:
+
+
+
import torch
+
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from botorch.utils import standardize
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+
+# generate synthetic data
+X = torch.rand(20, 2)
+Y = torch.stack([torch.sin(X[:, 0]), torch.cos(X[:, 1])], -1)
+Y = standardize(Y)  # standardize to zero mean unit variance
+
+# construct and fit the multi-output model
+gp = SingleTaskGP(X, Y)
+mll = ExactMarginalLogLikelihood(gp.likelihood, gp)
+fit_gpytorch_mll(mll)
+
+# construct the acquisition function
+qSUCB = qScalarizedUpperConfidenceBound(gp, beta=0.1, weights=torch.tensor([0.1, 0.5]))
+
+
+
+
+
+
+
+
In [4]:
+
+
+
# evaluate on single q-batch with q=3
+qSUCB(torch.rand(3, 2))
+
+
+
+
+
+
+
+
Out[4]:
+
+
tensor([0.4412], grad_fn=<MeanBackward1>)
+
+
+
+
+
+
+
+
In [5]:
+
+
+
# batch-evaluate on two q-batches with q=3
+qSUCB(torch.rand(2, 3, 2))
+
+
+
+
+
+
+
+
Out[5]:
+
+
tensor([0.5129, 0.5216], grad_fn=<MeanBackward1>)
+
+
+
+
+
+
+
+
+

A scalarized version of analytic UCB (q=1 only)

We can also write an analytic version of UCB for a multi-output model, assuming a multivariate normal posterior and q=1. The new class ScalarizedUpperConfidenceBound subclasses AnalyticAcquisitionFunction instead of MCAcquisitionFunction. In contrast to the MC version, instead of using the weights on the MC samples, we directly scalarize the mean vector $\mu$ and covariance matrix $\Sigma$ and apply standard UCB on the univariate normal distribution, which has mean $w^T \mu$ and variance $w^T \Sigma w$. In addition to the @t_batch_transform decorator, here we are also using expected_q=1 to ensure the input X has a q=1.

+

Note: BoTorch also provides a ScalarizedPosteriorTransform abstraction that can be used with any existing analytic acqusition functions and automatically performs the scalarization we implement manually below. See the end of this tutorial for a usage example.

+
+
+
+
+
+
In [6]:
+
+
+
from botorch.acquisition import AnalyticAcquisitionFunction
+
+
+class ScalarizedUpperConfidenceBound(AnalyticAcquisitionFunction):
+    def __init__(
+        self,
+        model: Model,
+        beta: Tensor,
+        weights: Tensor,
+        maximize: bool = True,
+    ) -> None:
+        # we use the AcquisitionFunction constructor, since that of
+        # AnalyticAcquisitionFunction performs some validity checks that we don't want here
+        super(AnalyticAcquisitionFunction, self).__init__(model)
+        self.maximize = maximize
+        self.register_buffer("beta", torch.as_tensor(beta))
+        self.register_buffer("weights", torch.as_tensor(weights))
+
+    @t_batch_mode_transform(expected_q=1)
+    def forward(self, X: Tensor) -> Tensor:
+        """Evaluate the Upper Confidence Bound on the candidate set X using scalarization
+
+        Args:
+            X: A `(b) x d`-dim Tensor of `(b)` t-batches of `d`-dim design
+                points each.
+
+        Returns:
+            A `(b)`-dim Tensor of Upper Confidence Bound values at the given
+                design points `X`.
+        """
+        self.beta = self.beta.to(X)
+        batch_shape = X.shape[:-2]
+        posterior = self.model.posterior(X)
+        means = posterior.mean.squeeze(dim=-2)  # b x o
+        scalarized_mean = means.matmul(self.weights)  # b
+        covs = posterior.mvn.covariance_matrix  # b x o x o
+        weights = self.weights.view(
+            1, -1, 1
+        )  # 1 x o x 1 (assume single batch dimension)
+        weights = weights.expand(batch_shape + weights.shape[1:])  # b x o x 1
+        weights_transpose = weights.permute(0, 2, 1)  # b x 1 x o
+        scalarized_variance = torch.bmm(
+            weights_transpose, torch.bmm(covs, weights)
+        ).view(
+            batch_shape
+        )  # b
+        delta = (self.beta.expand_as(scalarized_mean) * scalarized_variance).sqrt()
+        if self.maximize:
+            return scalarized_mean + delta
+        else:
+            return scalarized_mean - delta
+
+
+
+
+
+
+
+
+

Ad-hoc testing Scalarized-UCB

Notice that we pass in an explicit q-batch dimension for consistency, even though q=1.

+
+
+
+
+
+
In [7]:
+
+
+
# construct the acquisition function
+SUCB = ScalarizedUpperConfidenceBound(gp, beta=0.1, weights=torch.tensor([0.1, 0.5]))
+
+
+
+
+
+
+
+
In [8]:
+
+
+
# evaluate on single point
+SUCB(torch.rand(1, 2))
+
+
+
+
+
+
+
+
Out[8]:
+
+
tensor([0.5031], grad_fn=<AddBackward0>)
+
+
+
+
+
+
+
+
In [9]:
+
+
+
# batch-evaluate on 3 points
+SUCB(torch.rand(3, 1, 2))
+
+
+
+
+
+
+
+
Out[9]:
+
+
tensor([-0.6162, -0.8318, -0.1927], grad_fn=<AddBackward0>)
+
+
+
+
+
+
+
+
+

Using the custom acquisition function with Ax's Service API

+
+
+
+
+
+
+

Registering the new acquisition function

In order to use an acquisition function, Ax needs to know how to generate inputs to construct the acquisition function.

+
+
+
+
+
+
In [10]:
+
+
+
from typing import List
+from typing import Any, Dict
+
+from botorch.acquisition.input_constructors import acqf_input_constructor
+
+
+@acqf_input_constructor(ScalarizedUpperConfidenceBound)
+def construct_inputs_scalarized_ucb(
+    model: Model,
+    beta: float,
+    weights: List[float],
+    posterior_transform: None,
+) -> Dict[str, Any]:
+    return {
+        "model": model,
+        "beta": torch.as_tensor(beta, dtype=torch.double),
+        "weights": torch.as_tensor(weights, dtype=torch.double),
+    }
+
+
+
+
+
+
+
+
+

Setting up a GenerationStrategy using BOTORCH_MODULAR with our custom acquistion function.

BOTORCH_MODULAR is a convenient wrapper implemented in Ax that facilitates the use of custom BoTorch models and acquisition functions in Ax experiments. In order to customize the way the candidates are generated, we need to construct a new GenerationStrategy and pass it into the AxClient.

+
+
+
+
+
+
In [11]:
+
+
+
from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy
+from ax.modelbridge.registry import Models
+
+
+gs = GenerationStrategy(
+    steps=[
+        # Quasi-random initialization step
+        GenerationStep(
+            model=Models.SOBOL,
+            num_trials=5,  # How many trials should be produced from this generation step
+            model_kwargs={"seed": 999},  # Any kwargs you want passed into the model
+        ),
+        # Bayesian optimization step using the custom acquisition function
+        GenerationStep(
+            model=Models.BOTORCH_MODULAR,
+            num_trials=-1,  # No limitation on how many trials should be produced from this step
+            # For `BOTORCH_MODULAR`, we pass in kwargs to specify what surrogate or acquisition function to use.
+            # `acquisition_options` specifies the set of additional arguments to pass into the input constructor.
+            model_kwargs={
+                "botorch_acqf_class": ScalarizedUpperConfidenceBound,
+                "acquisition_options": {"beta": 0.1, "weights": [1.0, 1.0]},
+            },
+        ),
+    ]
+)
+
+
+
+
+
+
+
+
+

Setting up the experiment

We will set up a simple experiment to optimize a simple scalarization of the BraninCurrin function (per the weights above). A detailed tutorial on Service API can be found here.

+

In order to use the GenerationStrategy we just created, we will pass it into the AxClient.

+
+
+
+
+
+
In [12]:
+
+
+
from ax.service.ax_client import AxClient
+from ax.service.utils.instantiation import ObjectiveProperties
+from botorch.test_functions import BraninCurrin
+
+
+# Initialize the client - AxClient offers a convenient API to control the experiment
+ax_client = AxClient(generation_strategy=gs)
+# Setup the experiment
+ax_client.create_experiment(
+    name="branincurrin_test_experiment",
+    parameters=[
+        {
+            "name": f"x{i+1}",
+            "type": "range",
+            # It is crucial to use floats for the bounds, i.e., 0.0 rather than 0.
+            # Otherwise, the parameter would
+            "bounds": [0.0, 1.0],
+        }
+        for i in range(2)
+    ],
+    objectives={
+        "branin": ObjectiveProperties(minimize=True),
+        "currin": ObjectiveProperties(minimize=True),
+    },
+)
+# Setup a function to evaluate the trials
+branincurrin = BraninCurrin()
+
+
+def evaluate(parameters):
+    x = torch.tensor([[parameters.get(f"x{i+1}") for i in range(2)]])
+    bc_eval = branincurrin(x).squeeze().tolist()
+    # In our case, standard error is 0, since we are computing a synthetic function.
+    return {"branin": (bc_eval[0], 0.0), "currin": (bc_eval[1], 0.0)}
+
+
+
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Starting optimization with verbose logging. To disable logging, set the `verbose_logging` argument to `False`. Note that float values in the logs are rounded to 6 decimal points.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.utils.instantiation: Due to non-specification, we will use the heuristic for selecting objective thresholds.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x1. If that is not the expected value type, you can explicity specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x2. If that is not the expected value type, you can explicity specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.utils.instantiation: Created search space: SearchSpace(parameters=[RangeParameter(name='x1', parameter_type=FLOAT, range=[0.0, 1.0]), RangeParameter(name='x2', parameter_type=FLOAT, range=[0.0, 1.0])], parameter_constraints=[]).
+
+
+
+
+
+
+
+
+
+

Running the BO loop

Ax makes this part super simple!

+
+
+
+
+
+
In [13]:
+
+
+
for i in range(10):
+    parameters, trial_index = ax_client.get_next_trial()
+    # Local evaluation here can be replaced with deployment to external system.
+    ax_client.complete_trial(trial_index=trial_index, raw_data=evaluate(parameters))
+
+
+
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 0 with parameters {'x1': 0.62873, 'x2': 0.51481}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 0 with data: {'branin': (46.244598, 0.0), 'currin': (6.842319, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 1 with parameters {'x1': 0.434883, 'x2': 0.396266}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 1 with data: {'branin': (14.735401, 0.0), 'currin': (8.740173, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 2 with parameters {'x1': 0.075645, 'x2': 0.934926}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 2 with data: {'branin': (2.808084, 0.0), 'currin': (4.10731, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 3 with parameters {'x1': 0.863245, 'x2': 0.038764}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 3 with data: {'branin': (9.956846, 0.0), 'currin': (10.342199, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 4 with parameters {'x1': 0.953918, 'x2': 0.808236}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 4 with data: {'branin': (95.420815, 0.0), 'currin': (4.715139, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:44] ax.service.ax_client: Generated new trial 5 with parameters {'x1': 1.0, 'x2': 0.343958}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:45] ax.service.ax_client: Completed trial 5 with data: {'branin': (6.593266, 0.0), 'currin': (7.800417, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:45] ax.service.ax_client: Generated new trial 6 with parameters {'x1': 0.545885, 'x2': 0.0}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:45] ax.service.ax_client: Completed trial 6 with data: {'branin': (5.420934, 0.0), 'currin': (11.428976, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:47] ax.service.ax_client: Generated new trial 7 with parameters {'x1': 0.123588, 'x2': 0.0}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:47] ax.service.ax_client: Completed trial 7 with data: {'branin': (151.344849, 0.0), 'currin': (12.426076, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:48] ax.service.ax_client: Generated new trial 8 with parameters {'x1': 0.045172, 'x2': 0.0}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:48] ax.service.ax_client: Completed trial 8 with data: {'branin': (240.222977, 0.0), 'currin': (7.477064, 0.0)}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:49] ax.service.ax_client: Generated new trial 9 with parameters {'x1': 0.120649, 'x2': 0.032781}.
+
+
+
+
+
+
+
[INFO 11-16 18:14:49] ax.service.ax_client: Completed trial 9 with data: {'branin': (142.032639, 0.0), 'currin': (12.317077, 0.0)}.
+
+
+
+
+
+
+
+
+
+

Viewing trials and plotting the Pareto frontier

View the trials attached to the experiment.

+
+
+
+
+
+
In [14]:
+
+
+
ax_client.generation_strategy.trials_as_df
+
+
+
+
+
+
+
+
+
+
[INFO 11-16 18:14:50] ax.modelbridge.generation_strategy: Note that parameter values in dataframe are rounded to 2 decimal points; the values in the dataframe are thus not the exact ones suggested by Ax in trials.
+
+
+
+
+
Out[14]:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Generation StepGeneration ModelTrial IndexTrial StatusArm Parameterizations
00Sobol0COMPLETED{'0_0': {'x1': 0.63, 'x2': 0.51}}
10Sobol1COMPLETED{'1_0': {'x1': 0.43, 'x2': 0.4}}
20Sobol2COMPLETED{'2_0': {'x1': 0.08, 'x2': 0.93}}
30Sobol3COMPLETED{'3_0': {'x1': 0.86, 'x2': 0.04}}
40Sobol4COMPLETED{'4_0': {'x1': 0.95, 'x2': 0.81}}
51BoTorch5COMPLETED{'5_0': {'x1': 1.0, 'x2': 0.34}}
61BoTorch6COMPLETED{'6_0': {'x1': 0.55, 'x2': 0.0}}
71BoTorch7COMPLETED{'7_0': {'x1': 0.12, 'x2': 0.0}}
81BoTorch8COMPLETED{'8_0': {'x1': 0.05, 'x2': 0.0}}
91BoTorch9COMPLETED{'9_0': {'x1': 0.12, 'x2': 0.03}}
+
+
+
+
+
+
+
+
+

Plot the Pareto frontier.

+

Note that we do not expect a good coverage of the Pareto frontier since we use very small number of evaluations and our acquisition function naively optimizes the sum of the two objectives.

+
+
+
+
+
+
In [15]:
+
+
+
from ax.plot.pareto_frontier import plot_pareto_frontier
+from ax.plot.pareto_utils import compute_posterior_pareto_frontier
+from ax.utils.notebook.plotting import render
+
+
+objectives = ax_client.experiment.optimization_config.objective.objectives
+frontier = compute_posterior_pareto_frontier(
+    experiment=ax_client.experiment,
+    data=ax_client.experiment.fetch_data(),
+    primary_objective=objectives[1].metric,
+    secondary_objective=objectives[0].metric,
+    absolute_metrics=["branin", "currin"],
+)
+render(plot_pareto_frontier(frontier, CI_level=0.90))
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Appendix: Using ScalarizedPosteriorTransform

Using the ScalarizedPosteriorTransform abstraction, the functionality of ScalarizedUpperConfidenceBound implemented above can be easily achieved in just a few lines of code. PosteriorTransforms can be used with both the MC and analytic acquisition functions.

+
+
+
+
+
+
In [16]:
+
+
+
from botorch.acquisition.objective import ScalarizedPosteriorTransform
+from botorch.acquisition.analytic import UpperConfidenceBound
+
+pt = ScalarizedPosteriorTransform(weights=torch.tensor([0.1, 0.5]))
+SUCB = UpperConfidenceBound(gp, beta=0.1, posterior_transform=pt)
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/custom_botorch_model_in_ax.html b/website-old/_tutorials/custom_botorch_model_in_ax.html new file mode 100644 index 0000000000..e0472a818a --- /dev/null +++ b/website-old/_tutorials/custom_botorch_model_in_ax.html @@ -0,0 +1,1879 @@ + + + +
+
+
+
+

Using a custom BoTorch model with Ax

In this tutorial, we illustrate how to use a custom BoTorch model within Ax's botorch_modular API. This allows us to harness the convenience of Ax for running Bayesian Optimization loops while maintaining full flexibility in modeling.

+

Acquisition functions and their optimizers can be swapped out in much the same fashion. See for example the tutorial for Implementing a custom acquisition function.

+

If you want to do something non-standard, or would like to have full insight into every aspect of the implementation, please see this tutorial for how to write your own full optimization loop in BoTorch.

+
+
+
+
+
+
In [ ]:
+
+
+
import os
+from contextlib import contextmanager, nullcontext
+
+import plotly.io as pio
+
+from ax.utils.testing.mock import mock_botorch_optimize_context_manager
+
+# Ax uses Plotly to produce interactive plots. These are great for viewing and analysis,
+# though they also lead to large file sizes, which is not ideal for files living in GH.
+# Changing the default to `png` strips the interactive components to get around this.
+pio.renderers.default = "png"
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+NUM_EVALS = 10 if SMOKE_TEST else 30
+
+
+
+
+
+
+
+
+

Implementing the custom model

For this tutorial, we implement a very simple GPyTorch ExactGP model that uses an RBF kernel (with ARD) and infers a homoskedastic noise level.

+

Model definition is straightforward. Here we implement a GPyTorch ExactGP that inherits from GPyTorchModel; together these two superclasses add all the API calls that BoTorch expects in its various modules.

+

Note: BoTorch allows implementing any custom model that follows the Model API. For more information, please see the Model Documentation.

+
+
+
+
+
+
In [2]:
+
+
+
from typing import Optional
+
+from botorch.models.gpytorch import GPyTorchModel
+from gpytorch.distributions import MultivariateNormal
+from gpytorch.kernels import RBFKernel, ScaleKernel
+from gpytorch.likelihoods import GaussianLikelihood
+from gpytorch.means import ConstantMean
+from gpytorch.models import ExactGP
+from torch import Tensor
+
+
+class SimpleCustomGP(ExactGP, GPyTorchModel):
+
+    _num_outputs = 1  # to inform GPyTorchModel API
+
+    def __init__(self, train_X, train_Y, train_Yvar: Optional[Tensor] = None):
+        # NOTE: This ignores train_Yvar and uses inferred noise instead.
+        # squeeze output dim before passing train_Y to ExactGP
+        super().__init__(train_X, train_Y.squeeze(-1), GaussianLikelihood())
+        self.mean_module = ConstantMean()
+        self.covar_module = ScaleKernel(
+            base_kernel=RBFKernel(ard_num_dims=train_X.shape[-1]),
+        )
+        self.to(train_X)  # make sure we're on the right device/dtype
+
+    def forward(self, x):
+        mean_x = self.mean_module(x)
+        covar_x = self.covar_module(x)
+        return MultivariateNormal(mean_x, covar_x)
+
+
+
+
+
+
+
+
+

Instantiate a BoTorchModel in Ax

A BoTorchModel in Ax encapsulates both the surrogate -- which Ax calls a Surrogate and BoTorch calls a Model -- and an acquisition function. Here, we will only specify the custom surrogate and let Ax choose the default acquisition function.

+

Most models should work with the base Surrogate in Ax, except for BoTorch ModelListGP, which works with ListSurrogate. +Note that the Model (e.g., the SimpleCustomGP) must implement construct_inputs, as this is used to construct the inputs required for instantiating a Model instance from the experiment data.

+
+
+
+
+
+
In [3]:
+
+
+
from ax.models.torch.botorch_modular.model import BoTorchModel
+from ax.models.torch.botorch_modular.surrogate import Surrogate, SurrogateSpec
+from ax.models.torch.botorch_modular.utils import ModelConfig
+
+ax_model = BoTorchModel(
+    surrogate=Surrogate(
+        surrogate_spec=SurrogateSpec(
+            model_configs=[
+                ModelConfig(
+                    # The model class to use
+                    botorch_model_class=SimpleCustomGP,
+                    # Optional, MLL class with which to optimize model parameters
+                    # mll_class=ExactMarginalLogLikelihood,
+                    # Optional, dictionary of keyword arguments to model constructor
+                    # model_options={}
+                )
+            ]
+        )
+    ),
+    # Optional, acquisition function class to use - see custom acquisition tutorial
+    # botorch_acqf_class=qExpectedImprovement,
+)
+
+
+
+
+
+
+
+
+

Combine with a ModelBridge

Models in Ax require a ModelBridge to interface with Experiments. A ModelBridge takes the inputs supplied by the Experiment and converts them to the inputs expected by the Model. For a BoTorchModel, we use TorchModelBridge. The Modular BoTorch interface creates the BoTorchModel and the TorchModelBridge in a single step, as follows:

+
from ax.modelbridge.registry import Models
+model_bridge = Models.BOTORCH_MODULAR(
+    experiment=experiment,
+    data=data,
+    surrogate=Surrogate(SimpleCustomGP),
+    # Optional, will use default if unspecified
+    # botorch_acqf_class=qLogNoisyExpectedImprovement,  
+)
+# To generate a trial
+trial = model_bridge.gen(1)
+
+
+
+
+
+
+
+

Using the custom model in Ax to optimize the Branin function

We will demonstrate this with both the Service API (simpler, easier to use) and the Developer API (advanced, more customizable).

+
+
+
+
+
+
+

Optimization with Ax's Service API

A detailed tutorial on the Service API can be found here.

+

In order to customize the way the candidates are created in the Service API, we need to construct a new GenerationStrategy and pass it into AxClient.

+
+
+
+
+
+
In [4]:
+
+
+
from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy
+from ax.modelbridge.registry import Models
+
+
+gs = GenerationStrategy(
+    steps=[
+        # Quasi-random initialization step
+        GenerationStep(
+            model=Models.SOBOL,
+            num_trials=5,  # How many trials should be produced from this generation step
+        ),
+        # Bayesian optimization step using the custom acquisition function
+        GenerationStep(
+            model=Models.BOTORCH_MODULAR,
+            num_trials=-1,  # No limitation on how many trials should be produced from this step
+            # For `BOTORCH_MODULAR`, we pass in kwargs to specify what surrogate or acquisition function to use.
+            model_kwargs={
+                "surrogate_spec": SurrogateSpec(model_configs=[ModelConfig(botorch_model_class=SimpleCustomGP)]),
+            },
+        ),
+    ]
+)
+
+
+
+
+
+
+
+
+

Setting up the experiment

In order to use the GenerationStrategy we just created, we will pass it into the AxClient.

+
+
+
+
+
+
In [5]:
+
+
+
import torch
+from ax.service.ax_client import AxClient
+from ax.service.utils.instantiation import ObjectiveProperties
+from botorch.test_functions import Branin
+
+
+# Initialize the client - AxClient offers a convenient API to control the experiment
+ax_client = AxClient(generation_strategy=gs)
+# Setup the experiment
+ax_client.create_experiment(
+    name="branin_test_experiment",
+    parameters=[
+        {
+            "name": "x1",
+            "type": "range",
+            # It is crucial to use floats for the bounds, i.e., 0.0 rather than 0.
+            # Otherwise, the parameter would be inferred as an integer range.
+            "bounds": [-5.0, 10.0],
+        },
+        {
+            "name": "x2",
+            "type": "range",
+            "bounds": [0.0, 15.0],
+        },
+    ],
+    objectives={
+        "branin": ObjectiveProperties(minimize=True),
+    },
+)
+# Setup a function to evaluate the trials
+branin = Branin()
+
+
+def evaluate(parameters):
+    x = torch.tensor([[parameters.get(f"x{i+1}") for i in range(2)]])
+    # The GaussianLikelihood used by our model infers an observation noise level,
+    # so we pass an sem value of NaN to indicate that observation noise is unknown
+    return {"branin": (branin(x).item(), float("nan"))}
+
+
+
+
+
+
+
+
+
+
[INFO 11-07 12:52:13] ax.service.ax_client: Starting optimization with verbose logging. To disable logging, set the `verbose_logging` argument to `False`. Note that float values in the logs are rounded to 6 decimal points.
+[INFO 11-07 12:52:13] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x1. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.
+[INFO 11-07 12:52:13] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x2. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.
+[INFO 11-07 12:52:13] ax.service.utils.instantiation: Created search space: SearchSpace(parameters=[RangeParameter(name='x1', parameter_type=FLOAT, range=[-5.0, 10.0]), RangeParameter(name='x2', parameter_type=FLOAT, range=[0.0, 15.0])], parameter_constraints=[]).
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x1. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x2. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.utils.instantiation: Created search space: SearchSpace(parameters=[RangeParameter(name='x1', parameter_type=FLOAT, range=[-5.0, 10.0]), RangeParameter(name='x2', parameter_type=FLOAT, range=[0.0, 15.0])], parameter_constraints=[]).
+
+
+
+
+
+
+
+
+
+

Running the BO loop

+
+
+
+
+
+
+

The next cell sets up a decorator solely to speed up the testing of the notebook in SMOKE_TEST mode. You can safely ignore this cell and the use of the decorator throughout the tutorial.

+
+
+
+
+
+
In [6]:
+
+
+
if SMOKE_TEST:
+    fast_smoke_test = mock_botorch_optimize_context_manager
+else:
+    fast_smoke_test = nullcontext
+
+# Set a seed for reproducible tutorial output
+torch.manual_seed(0)
+
+
+
+
+
+
+
+
Out[6]:
+
+
<torch._C.Generator at 0x11c891530>
+
+
+
+
+
+
+
+
In [7]:
+
+
+
with fast_smoke_test():
+    for i in range(NUM_EVALS):
+        parameters, trial_index = ax_client.get_next_trial()
+        # Local evaluation here can be replaced with deployment to external system.
+        ax_client.complete_trial(trial_index=trial_index, raw_data=evaluate(parameters))
+
+
+
+
+
+
+
+
+
+
/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:
+
+Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.
+
+[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 0 with parameters {'x1': 0.62583, 'x2': 14.359564} using model Sobol.
+[INFO 11-07 12:52:17] ax.service.ax_client: Completed trial 0 with data: {'branin': (104.365417, nan)}.
+/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:
+
+Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.
+
+[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 1 with parameters {'x1': 3.166217, 'x2': 3.867106} using model Sobol.
+[INFO 11-07 12:52:17] ax.service.ax_client: Completed trial 1 with data: {'branin': (2.996862, nan)}.
+/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:
+
+Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.
+
+[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 2 with parameters {'x1': 9.560105, 'x2': 10.718323} using model Sobol.
+[INFO 11-07 12:52:17] ax.service.ax_client: Completed trial 2 with data: {'branin': (66.530632, nan)}.
+/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:
+
+Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.
+
+[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 3 with parameters {'x1': -3.878664, 'x2': 0.117947} using model Sobol.
+[INFO 11-07 12:52:18] ax.service.ax_client: Completed trial 3 with data: {'branin': (198.850861, nan)}.
+/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:
+
+Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.
+
+[INFO 11-07 12:52:18] ax.service.ax_client: Generated new trial 4 with parameters {'x1': -2.362858, 'x2': 8.855021} using model Sobol.
+[INFO 11-07 12:52:18] ax.service.ax_client: Completed trial 4 with data: {'branin': (5.811776, nan)}.
+[INFO 11-07 12:52:21] ax.service.ax_client: Generated new trial 5 with parameters {'x1': 2.562464, 'x2': 4.925756} using model BoTorch.
+[INFO 11-07 12:52:21] ax.service.ax_client: Completed trial 5 with data: {'branin': (6.61104, nan)}.
+[INFO 11-07 12:52:21] ax.service.ax_client: Generated new trial 6 with parameters {'x1': 5.503428, 'x2': 4.951339} using model BoTorch.
+[INFO 11-07 12:52:21] ax.service.ax_client: Completed trial 6 with data: {'branin': (31.249773, nan)}.
+[INFO 11-07 12:52:22] ax.service.ax_client: Generated new trial 7 with parameters {'x1': -2.306809, 'x2': 4.436082} using model BoTorch.
+[INFO 11-07 12:52:22] ax.service.ax_client: Completed trial 7 with data: {'branin': (38.632786, nan)}.
+[INFO 11-07 12:52:22] ax.service.ax_client: Generated new trial 8 with parameters {'x1': -1.582296, 'x2': 7.318848} using model BoTorch.
+[INFO 11-07 12:52:22] ax.service.ax_client: Completed trial 8 with data: {'branin': (12.208769, nan)}.
+[INFO 11-07 12:52:23] ax.service.ax_client: Generated new trial 9 with parameters {'x1': -5.0, 'x2': 9.065641} using model BoTorch.
+[INFO 11-07 12:52:23] ax.service.ax_client: Completed trial 9 with data: {'branin': (78.686066, nan)}.
+[INFO 11-07 12:52:23] ax.service.ax_client: Generated new trial 10 with parameters {'x1': 0.779998, 'x2': 6.842907} using model BoTorch.
+[INFO 11-07 12:52:23] ax.service.ax_client: Completed trial 10 with data: {'branin': (20.849186, nan)}.
+[INFO 11-07 12:52:24] ax.service.ax_client: Generated new trial 11 with parameters {'x1': -0.959171, 'x2': 9.756062} using model BoTorch.
+[INFO 11-07 12:52:24] ax.service.ax_client: Completed trial 11 with data: {'branin': (19.968334, nan)}.
+[INFO 11-07 12:52:25] ax.service.ax_client: Generated new trial 12 with parameters {'x1': 1.759405, 'x2': 0.0} using model BoTorch.
+[INFO 11-07 12:52:25] ax.service.ax_client: Completed trial 12 with data: {'branin': (21.157597, nan)}.
+[INFO 11-07 12:52:25] ax.service.ax_client: Generated new trial 13 with parameters {'x1': -3.67521, 'x2': 15.0} using model BoTorch.
+[INFO 11-07 12:52:25] ax.service.ax_client: Completed trial 13 with data: {'branin': (3.70913, nan)}.
+[INFO 11-07 12:52:26] ax.service.ax_client: Generated new trial 14 with parameters {'x1': 10.0, 'x2': 0.0} using model BoTorch.
+[INFO 11-07 12:52:26] ax.service.ax_client: Completed trial 14 with data: {'branin': (10.960894, nan)}.
+/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning:
+
+A not p.d., added jitter of 1.0e-08 to the diagonal
+
+[INFO 11-07 12:52:26] ax.service.ax_client: Generated new trial 15 with parameters {'x1': 4.693345, 'x2': 0.0} using model BoTorch.
+[INFO 11-07 12:52:26] ax.service.ax_client: Completed trial 15 with data: {'branin': (11.7103, nan)}.
+[INFO 11-07 12:52:27] ax.service.ax_client: Generated new trial 16 with parameters {'x1': -3.16039, 'x2': 12.343285} using model BoTorch.
+[INFO 11-07 12:52:27] ax.service.ax_client: Completed trial 16 with data: {'branin': (0.400116, nan)}.
+[INFO 11-07 12:52:27] ax.service.ax_client: Generated new trial 17 with parameters {'x1': 10.0, 'x2': 3.798226} using model BoTorch.
+[INFO 11-07 12:52:27] ax.service.ax_client: Completed trial 17 with data: {'branin': (2.575594, nan)}.
+[INFO 11-07 12:52:28] ax.service.ax_client: Generated new trial 18 with parameters {'x1': 3.304444, 'x2': 2.327283} using model BoTorch.
+[INFO 11-07 12:52:28] ax.service.ax_client: Completed trial 18 with data: {'branin': (0.555859, nan)}.
+[INFO 11-07 12:52:29] ax.service.ax_client: Generated new trial 19 with parameters {'x1': -3.375582, 'x2': 12.520736} using model BoTorch.
+[INFO 11-07 12:52:29] ax.service.ax_client: Completed trial 19 with data: {'branin': (0.764316, nan)}.
+[INFO 11-07 12:52:30] ax.service.ax_client: Generated new trial 20 with parameters {'x1': 9.267105, 'x2': 2.183014} using model BoTorch.
+[INFO 11-07 12:52:30] ax.service.ax_client: Completed trial 20 with data: {'branin': (0.543305, nan)}.
+[INFO 11-07 12:52:30] ax.service.ax_client: Generated new trial 21 with parameters {'x1': 9.536612, 'x2': 2.744301} using model BoTorch.
+[INFO 11-07 12:52:30] ax.service.ax_client: Completed trial 21 with data: {'branin': (0.487921, nan)}.
+[INFO 11-07 12:52:31] ax.service.ax_client: Generated new trial 22 with parameters {'x1': -3.055135, 'x2': 12.529729} using model BoTorch.
+[INFO 11-07 12:52:31] ax.service.ax_client: Completed trial 22 with data: {'branin': (0.646773, nan)}.
+[INFO 11-07 12:52:32] ax.service.ax_client: Generated new trial 23 with parameters {'x1': 3.099745, 'x2': 2.457142} using model BoTorch.
+[INFO 11-07 12:52:32] ax.service.ax_client: Completed trial 23 with data: {'branin': (0.428578, nan)}.
+[INFO 11-07 12:52:33] ax.service.ax_client: Generated new trial 24 with parameters {'x1': 8.94462, 'x2': 0.943412} using model BoTorch.
+[INFO 11-07 12:52:33] ax.service.ax_client: Completed trial 24 with data: {'branin': (2.820818, nan)}.
+[INFO 11-07 12:52:35] ax.service.ax_client: Generated new trial 25 with parameters {'x1': 9.510065, 'x2': 2.361432} using model BoTorch.
+[INFO 11-07 12:52:35] ax.service.ax_client: Completed trial 25 with data: {'branin': (0.467552, nan)}.
+[INFO 11-07 12:52:36] ax.service.ax_client: Generated new trial 26 with parameters {'x1': 9.425844, 'x2': 2.589096} using model BoTorch.
+[INFO 11-07 12:52:36] ax.service.ax_client: Completed trial 26 with data: {'branin': (0.410706, nan)}.
+[INFO 11-07 12:52:37] ax.service.ax_client: Generated new trial 27 with parameters {'x1': -3.091638, 'x2': 12.315311} using model BoTorch.
+[INFO 11-07 12:52:37] ax.service.ax_client: Completed trial 27 with data: {'branin': (0.435478, nan)}.
+[INFO 11-07 12:52:38] ax.service.ax_client: Generated new trial 28 with parameters {'x1': -3.221389, 'x2': 12.345989} using model BoTorch.
+[INFO 11-07 12:52:38] ax.service.ax_client: Completed trial 28 with data: {'branin': (0.443229, nan)}.
+/Users/sdaulton/botorch_2024_11_07/botorch/botorch/optim/optimize.py:576: 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.'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.'), 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.
+
+/Users/sdaulton/botorch_2024_11_07/botorch/botorch/optim/optimize.py:576: RuntimeWarning:
+
+Optimization failed on the second try, after generating a new set of initial conditions.
+
+[INFO 11-07 12:52:42] ax.service.ax_client: Generated new trial 29 with parameters {'x1': 3.182468, 'x2': 2.521964} using model BoTorch.
+[INFO 11-07 12:52:42] ax.service.ax_client: Completed trial 29 with data: {'branin': (0.48354, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 1 with parameters {'x1': 3.166217, 'x2': 3.867106} using model Sobol.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 1 with data: {'branin': (2.996862, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 2 with parameters {'x1': 9.560105, 'x2': 10.718323} using model Sobol.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 2 with data: {'branin': (66.530624, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 3 with parameters {'x1': -3.878664, 'x2': 0.117947} using model Sobol.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 3 with data: {'branin': (198.850861, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 4 with parameters {'x1': -2.362858, 'x2': 8.855021} using model Sobol.
+
+
+
+
+
+
+
[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 4 with data: {'branin': (5.811776, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:07] ax.service.ax_client: Generated new trial 5 with parameters {'x1': 2.562432, 'x2': 4.925782} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:07] ax.service.ax_client: Completed trial 5 with data: {'branin': (6.611189, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:07] ax.service.ax_client: Generated new trial 6 with parameters {'x1': 5.50005, 'x2': 4.949873} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:07] ax.service.ax_client: Completed trial 6 with data: {'branin': (31.211433, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:08] ax.service.ax_client: Generated new trial 7 with parameters {'x1': -2.300231, 'x2': 4.436402} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:08] ax.service.ax_client: Completed trial 7 with data: {'branin': (38.505764, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:08] ax.service.ax_client: Generated new trial 8 with parameters {'x1': -1.583362, 'x2': 7.318469} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:08] ax.service.ax_client: Completed trial 8 with data: {'branin': (12.206194, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:09] ax.service.ax_client: Generated new trial 9 with parameters {'x1': -5.0, 'x2': 9.066302} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:09] ax.service.ax_client: Completed trial 9 with data: {'branin': (78.675331, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:09] ax.service.ax_client: Generated new trial 10 with parameters {'x1': 0.787884, 'x2': 6.879815} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:09] ax.service.ax_client: Completed trial 10 with data: {'branin': (20.990005, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:10] ax.service.ax_client: Generated new trial 11 with parameters {'x1': 1.60023, 'x2': 0.584966} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:10] ax.service.ax_client: Completed trial 11 with data: {'branin': (19.951, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:10] ax.service.ax_client: Generated new trial 12 with parameters {'x1': 10.0, 'x2': 0.0} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:10] ax.service.ax_client: Completed trial 12 with data: {'branin': (10.960894, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:11] ax.service.ax_client: Generated new trial 13 with parameters {'x1': 7.38266, 'x2': 0.0} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:11] ax.service.ax_client: Completed trial 13 with data: {'branin': (16.027073, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:11] ax.service.ax_client: Generated new trial 14 with parameters {'x1': 4.173322, 'x2': 0.0} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:11] ax.service.ax_client: Completed trial 14 with data: {'branin': (7.656268, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:12] ax.service.ax_client: Generated new trial 15 with parameters {'x1': -3.935855, 'x2': 15.0} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:12] ax.service.ax_client: Completed trial 15 with data: {'branin': (3.810518, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:12] ax.service.ax_client: Generated new trial 16 with parameters {'x1': -3.321259, 'x2': 12.38287} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:12] ax.service.ax_client: Completed trial 16 with data: {'branin': (0.660087, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:13] ax.service.ax_client: Generated new trial 17 with parameters {'x1': 10.0, 'x2': 3.666754} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:13] ax.service.ax_client: Completed trial 17 with data: {'branin': (2.383767, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:14] ax.service.ax_client: Generated new trial 18 with parameters {'x1': 9.34166, 'x2': 2.5446} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:14] ax.service.ax_client: Completed trial 18 with data: {'branin': (0.450308, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:14] ax.service.ax_client: Generated new trial 19 with parameters {'x1': 3.076019, 'x2': 2.418569} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:14] ax.service.ax_client: Completed trial 19 with data: {'branin': (0.426966, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:15] ax.service.ax_client: Generated new trial 20 with parameters {'x1': 9.537424, 'x2': 2.493842} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:15] ax.service.ax_client: Completed trial 20 with data: {'branin': (0.4648, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:16] ax.service.ax_client: Generated new trial 21 with parameters {'x1': -3.360749, 'x2': 15.0} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:16] ax.service.ax_client: Completed trial 21 with data: {'branin': (5.432912, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:17] ax.service.ax_client: Generated new trial 22 with parameters {'x1': 9.516079, 'x2': 2.791557} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:17] ax.service.ax_client: Completed trial 22 with data: {'branin': (0.494746, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:19] ax.service.ax_client: Generated new trial 23 with parameters {'x1': 3.202976, 'x2': 2.439512} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:19] ax.service.ax_client: Completed trial 23 with data: {'branin': (0.460872, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:20] ax.service.ax_client: Generated new trial 24 with parameters {'x1': 9.625609, 'x2': 2.470825} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:20] ax.service.ax_client: Completed trial 24 with data: {'branin': (0.622846, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:21] ax.service.ax_client: Generated new trial 25 with parameters {'x1': -3.235781, 'x2': 12.32664} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:21] ax.service.ax_client: Completed trial 25 with data: {'branin': (0.471375, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:22] ax.service.ax_client: Generated new trial 26 with parameters {'x1': 9.466124, 'x2': 2.301119} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:22] ax.service.ax_client: Completed trial 26 with data: {'branin': (0.449765, nan)}.
+
+
+
+
+
+
+
[W 241107 08:26:24 optimize:576] 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.'), 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.
+
+
+
+
+
+
+
[INFO 11-07 08:26:25] ax.service.ax_client: Generated new trial 27 with parameters {'x1': 2.97826, 'x2': 2.43746} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:25] ax.service.ax_client: Completed trial 27 with data: {'branin': (0.526684, nan)}.
+
+
+
+
+
+
+
[INFO 11-07 08:26:27] ax.service.ax_client: Generated new trial 28 with parameters {'x1': -3.286554, 'x2': 12.040548} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:27] ax.service.ax_client: Completed trial 28 with data: {'branin': (0.84146, nan)}.
+
+
+
+
+
+
+
[W 241107 08:26:28 optimize:576] 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.
+
+
+
+
+
+
+
[INFO 11-07 08:26:29] ax.service.ax_client: Generated new trial 29 with parameters {'x1': 9.459437, 'x2': 2.554713} using model BoTorch.
+
+
+
+
+
+
+
[INFO 11-07 08:26:29] ax.service.ax_client: Completed trial 29 with data: {'branin': (0.406186, nan)}.
+
+
+
+
+
+
+
+
+
+

Viewing the evaluated trials

+
+
+
+
+
+
In [8]:
+
+
+
ax_client.get_trials_data_frame()
+
+
+
+
+
+
+
+
+
+
[WARNING 11-07 12:53:20] ax.service.utils.report_utils: Column reason missing for all trials. Not appending column.
+
+
+
+
+
Out[8]:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
trial_indexarm_nametrial_statusgeneration_methodbraninx1x2
000_0COMPLETEDSobol104.3654170.62583014.359564
111_0COMPLETEDSobol2.9968623.1662173.867106
222_0COMPLETEDSobol66.5306329.56010510.718323
333_0COMPLETEDSobol198.850861-3.8786640.117947
444_0COMPLETEDSobol5.811776-2.3628588.855021
555_0COMPLETEDBoTorch6.6110402.5624644.925756
666_0COMPLETEDBoTorch31.2497735.5034284.951339
777_0COMPLETEDBoTorch38.632786-2.3068094.436082
888_0COMPLETEDBoTorch12.208769-1.5822967.318848
999_0COMPLETEDBoTorch78.686066-5.0000009.065641
101010_0COMPLETEDBoTorch20.8491860.7799986.842907
111111_0COMPLETEDBoTorch19.968334-0.9591719.756062
121212_0COMPLETEDBoTorch21.1575971.7594050.000000
131313_0COMPLETEDBoTorch3.709130-3.67521015.000000
141414_0COMPLETEDBoTorch10.96089410.0000000.000000
151515_0COMPLETEDBoTorch11.7103004.6933450.000000
161616_0COMPLETEDBoTorch0.400116-3.16039012.343285
171717_0COMPLETEDBoTorch2.57559410.0000003.798226
181818_0COMPLETEDBoTorch0.5558593.3044442.327283
191919_0COMPLETEDBoTorch0.764316-3.37558212.520736
202020_0COMPLETEDBoTorch0.5433059.2671052.183014
212121_0COMPLETEDBoTorch0.4879219.5366122.744301
222222_0COMPLETEDBoTorch0.646773-3.05513512.529729
232323_0COMPLETEDBoTorch0.4285783.0997452.457142
242424_0COMPLETEDBoTorch2.8208188.9446200.943412
252525_0COMPLETEDBoTorch0.4675529.5100652.361432
262626_0COMPLETEDBoTorch0.4107069.4258442.589096
272727_0COMPLETEDBoTorch0.435478-3.09163812.315311
282828_0COMPLETEDBoTorch0.443229-3.22138912.345989
292929_0COMPLETEDBoTorch0.4835403.1824682.521964
+
+
+
+
+
+
+
+
In [9]:
+
+
+
parameters, values = ax_client.get_best_parameters()
+print(f"Best parameters: {parameters}")
+print(f"Corresponding mean: {values[0]}, covariance: {values[1]}")
+
+
+
+
+
+
+
+
+
+
Best parameters: {'x1': 9.510065129079985, 'x2': 2.361432108875333}
+Corresponding mean: {'branin': np.float64(0.372037358815291)}, covariance: {'branin': {'branin': np.float64(0.04886421886415146)}}
+
+
+
+
+
+
+
+
+
+

Plotting the response surface and optimization progress

+
+
+
+
+
+
In [10]:
+
+
+
from ax.utils.notebook.plotting import render
+
+render(ax_client.get_contour_plot())
+
+
+
+
+
+
+
+
+
+
[INFO 11-07 12:53:22] ax.service.ax_client: Retrieving contour plot with parameter 'x1' on X-axis and 'x2' on Y-axis, for metric 'branin'. Remaining parameters are affixed to the middle of their range.
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [11]:
+
+
+
best_parameters, values = ax_client.get_best_parameters()
+best_parameters, values[0]
+
+
+
+
+
+
+
+
Out[11]:
+
+
({'x1': 9.510065129079985, 'x2': 2.361432108875333},
+ {'branin': np.float64(0.372037358815291)})
+
+
+
+
+
+
+
+
In [12]:
+
+
+
render(ax_client.get_optimization_trace(objective_optimum=0.397887))
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Optimization with the Developer API

A detailed tutorial on the Service API can be found here.

+

Set up the Experiment in Ax

We need 3 inputs for an Ax Experiment:

+
    +
  • A search space to optimize over;
  • +
  • An optimization config specifiying the objective / metrics to optimize, and optional outcome constraints;
  • +
  • A runner that handles the deployment of trials. For a synthetic optimization problem, such as here, this only returns simple metadata about the trial.
  • +
+
+
+
+
+
+
In [13]:
+
+
+
import pandas as pd
+import torch
+from ax import (
+    Data,
+    Experiment,
+    Metric,
+    Objective,
+    OptimizationConfig,
+    ParameterType,
+    RangeParameter,
+    Runner,
+    SearchSpace,
+)
+from ax.utils.common.result import Ok
+from botorch.test_functions import Branin
+
+
+branin_func = Branin()
+
+# For our purposes, the metric is a wrapper that structures the function output.
+class BraninMetric(Metric):
+    def fetch_trial_data(self, trial):
+        records = []
+        for arm_name, arm in trial.arms_by_name.items():
+            params = arm.parameters
+            tensor_params = torch.tensor([params["x1"], params["x2"]])
+            records.append(
+                {
+                    "arm_name": arm_name,
+                    "metric_name": self.name,
+                    "trial_index": trial.index,
+                    "mean": branin_func(tensor_params),
+                    "sem": float(
+                        "nan"
+                    ),  # SEM (observation noise) - NaN indicates unknown
+                }
+            )
+        return Ok(value=Data(df=pd.DataFrame.from_records(records)))
+
+
+# Search space defines the parameters, their types, and acceptable values.
+search_space = SearchSpace(
+    parameters=[
+        RangeParameter(
+            name="x1", parameter_type=ParameterType.FLOAT, lower=-5, upper=10
+        ),
+        RangeParameter(
+            name="x2", parameter_type=ParameterType.FLOAT, lower=0, upper=15
+        ),
+    ]
+)
+
+optimization_config = OptimizationConfig(
+    objective=Objective(
+        metric=BraninMetric(name="branin_metric", lower_is_better=True),
+        minimize=True,  # This is optional since we specified `lower_is_better=True`
+    )
+)
+
+
+class MyRunner(Runner):
+    def run(self, trial):
+        trial_metadata = {"name": str(trial.index)}
+        return trial_metadata
+
+
+exp = Experiment(
+    name="branin_experiment",
+    search_space=search_space,
+    optimization_config=optimization_config,
+    runner=MyRunner(),
+)
+
+
+
+
+
+
+
+
+

Run the BO loop

First, we use the Sobol generator to create 5 (quasi-) random initial point in the search space. Ax controls objective evaluations via Trials.

+
    +
  • We generate a Trial using a generator run, e.g., Sobol below. A Trial specifies relevant metadata as well as the parameters to be evaluated. At this point, the Trial is at the CANDIDATE stage.
  • +
  • We run the Trial using Trial.run(). In our example, this serves to mark the Trial as RUNNING. In an advanced application, this can be used to dispatch the Trial for evaluation on a remote server.
  • +
  • Once the Trial is done running, we mark it as COMPLETED. This tells the Experiment that it can fetch the Trial data.
  • +
+

A Trial supports evaluation of a single parameterization. For parallel evaluations, see BatchTrial.

+
+
+
+
+
+
In [14]:
+
+
+
from ax.modelbridge.registry import Models
+
+
+sobol = Models.SOBOL(exp.search_space)
+
+for i in range(5):
+    trial = exp.new_trial(generator_run=sobol.gen(1))
+    trial.run()
+    trial.mark_completed()
+
+
+
+
+
+
+
+
+

Once the initial (quasi-) random stage is completed, we can use our SimpleCustomGP with the default acquisition function chosen by Ax to run the BO loop.

+
+
+
+
+
+
In [15]:
+
+
+
with fast_smoke_test():
+    for i in range(NUM_EVALS - 5):
+        model_bridge = Models.BOTORCH_MODULAR(
+            experiment=exp,
+            data=exp.fetch_data(),
+            surrogate_spec=SurrogateSpec(model_configs=[ModelConfig(SimpleCustomGP)]),
+        )
+        trial = exp.new_trial(generator_run=model_bridge.gen(1))
+        trial.run()
+        trial.mark_completed()
+
+
+
+
+
+
+
+
+

View the trials attached to the Experiment.

+
+
+
+
+
+
In [16]:
+
+
+
exp.trials
+
+
+
+
+
+
+
+
Out[16]:
+
+
{0: Trial(experiment_name='branin_experiment', index=0, status=TrialStatus.COMPLETED, arm=Arm(name='0_0', parameters={'x1': 8.268271088600159, 'x2': 13.676363825798035})),
+ 1: Trial(experiment_name='branin_experiment', index=1, status=TrialStatus.COMPLETED, arm=Arm(name='1_0', parameters={'x1': -3.0115276388823986, 'x2': 0.19308556336909533})),
+ 2: Trial(experiment_name='branin_experiment', index=2, status=TrialStatus.COMPLETED, arm=Arm(name='2_0', parameters={'x1': -0.794604872353375, 'x2': 10.19062165170908})),
+ 3: Trial(experiment_name='branin_experiment', index=3, status=TrialStatus.COMPLETED, arm=Arm(name='3_0', parameters={'x1': 2.7553387405350804, 'x2': 4.206141787581146})),
+ 4: Trial(experiment_name='branin_experiment', index=4, status=TrialStatus.COMPLETED, arm=Arm(name='4_0', parameters={'x1': 5.150513867847621, 'x2': 9.072991241700947})),
+ 5: Trial(experiment_name='branin_experiment', index=5, status=TrialStatus.COMPLETED, arm=Arm(name='5_0', parameters={'x1': 1.5497872135088082, 'x2': 9.19017678783918})),
+ 6: Trial(experiment_name='branin_experiment', index=6, status=TrialStatus.COMPLETED, arm=Arm(name='6_0', parameters={'x1': 7.35880350484438, 'x2': 3.182282876550653})),
+ 7: Trial(experiment_name='branin_experiment', index=7, status=TrialStatus.COMPLETED, arm=Arm(name='7_0', parameters={'x1': -5.0, 'x2': 8.206550963671184})),
+ 8: Trial(experiment_name='branin_experiment', index=8, status=TrialStatus.COMPLETED, arm=Arm(name='8_0', parameters={'x1': 4.765065782500189, 'x2': 1.252289390966608})),
+ 9: Trial(experiment_name='branin_experiment', index=9, status=TrialStatus.COMPLETED, arm=Arm(name='9_0', parameters={'x1': 4.505201068669873, 'x2': 3.3644975986881467})),
+ 10: Trial(experiment_name='branin_experiment', index=10, status=TrialStatus.COMPLETED, arm=Arm(name='10_0', parameters={'x1': -3.1164341906014323, 'x2': 15.0})),
+ 11: Trial(experiment_name='branin_experiment', index=11, status=TrialStatus.COMPLETED, arm=Arm(name='11_0', parameters={'x1': 10.0, 'x2': 0.0})),
+ 12: Trial(experiment_name='branin_experiment', index=12, status=TrialStatus.COMPLETED, arm=Arm(name='12_0', parameters={'x1': -5.0, 'x2': 15.0})),
+ 13: Trial(experiment_name='branin_experiment', index=13, status=TrialStatus.COMPLETED, arm=Arm(name='13_0', parameters={'x1': 10.0, 'x2': 2.8078129263430163})),
+ 14: Trial(experiment_name='branin_experiment', index=14, status=TrialStatus.COMPLETED, arm=Arm(name='14_0', parameters={'x1': 2.445554777112278, 'x2': 2.2462261858873593})),
+ 15: Trial(experiment_name='branin_experiment', index=15, status=TrialStatus.COMPLETED, arm=Arm(name='15_0', parameters={'x1': 2.480451132691808, 'x2': 3.056693071733582})),
+ 16: Trial(experiment_name='branin_experiment', index=16, status=TrialStatus.COMPLETED, arm=Arm(name='16_0', parameters={'x1': 10.0, 'x2': 3.9059386481288194})),
+ 17: Trial(experiment_name='branin_experiment', index=17, status=TrialStatus.COMPLETED, arm=Arm(name='17_0', parameters={'x1': 2.0237354666184544, 'x2': 3.511643776100397})),
+ 18: Trial(experiment_name='branin_experiment', index=18, status=TrialStatus.COMPLETED, arm=Arm(name='18_0', parameters={'x1': 2.8769752653087997, 'x2': 2.2483802909633863})),
+ 19: Trial(experiment_name='branin_experiment', index=19, status=TrialStatus.COMPLETED, arm=Arm(name='19_0', parameters={'x1': 3.0536513312697213, 'x2': 2.4346471208663614})),
+ 20: Trial(experiment_name='branin_experiment', index=20, status=TrialStatus.COMPLETED, arm=Arm(name='20_0', parameters={'x1': 9.427576408084287, 'x2': 2.557223349069929})),
+ 21: Trial(experiment_name='branin_experiment', index=21, status=TrialStatus.COMPLETED, arm=Arm(name='21_0', parameters={'x1': 8.84736166287066, 'x2': 0.8696191586858866})),
+ 22: Trial(experiment_name='branin_experiment', index=22, status=TrialStatus.COMPLETED, arm=Arm(name='22_0', parameters={'x1': -1.5039526251440347, 'x2': 15.0})),
+ 23: Trial(experiment_name='branin_experiment', index=23, status=TrialStatus.COMPLETED, arm=Arm(name='23_0', parameters={'x1': -3.335556146334603, 'x2': 12.910932366431291})),
+ 24: Trial(experiment_name='branin_experiment', index=24, status=TrialStatus.COMPLETED, arm=Arm(name='24_0', parameters={'x1': -3.491879380808762, 'x2': 13.514831783855984})),
+ 25: Trial(experiment_name='branin_experiment', index=25, status=TrialStatus.COMPLETED, arm=Arm(name='25_0', parameters={'x1': -3.031782920987203, 'x2': 11.976525526649187})),
+ 26: Trial(experiment_name='branin_experiment', index=26, status=TrialStatus.COMPLETED, arm=Arm(name='26_0', parameters={'x1': -3.1412934283043814, 'x2': 12.616052311698178})),
+ 27: Trial(experiment_name='branin_experiment', index=27, status=TrialStatus.COMPLETED, arm=Arm(name='27_0', parameters={'x1': 9.455852758717114, 'x2': 2.3674336079024183})),
+ 28: Trial(experiment_name='branin_experiment', index=28, status=TrialStatus.COMPLETED, arm=Arm(name='28_0', parameters={'x1': 3.1364699781417986, 'x2': 2.025242611585696})),
+ 29: Trial(experiment_name='branin_experiment', index=29, status=TrialStatus.COMPLETED, arm=Arm(name='29_0', parameters={'x1': -2.936274156572808, 'x2': 11.259953484546505}))}
+
+
+
+
+
+
+
+
+

View the evaluation data about these trials.

+
+
+
+
+
+
In [17]:
+
+
+
exp.fetch_data().df
+
+
+
+
+
+
+
+
Out[17]:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
arm_namemetric_namemeansemtrial_index
00_0branin_metric150.233566NaN0
11_0branin_metric139.047729NaN1
22_0branin_metric24.817539NaN2
33_0branin_metric3.699482NaN3
44_0branin_metric75.591110NaN4
55_0branin_metric38.786335NaN5
66_0branin_metric18.167435NaN6
77_0branin_metric93.378693NaN7
88_0branin_metric10.515005NaN8
99_0branin_metric11.683224NaN9
1010_0branin_metric8.159270NaN10
1111_0branin_metric10.960894NaN11
1212_0branin_metric17.508297NaN12
1313_0branin_metric1.981222NaN13
1414_0branin_metric3.033621NaN14
1515_0branin_metric2.465075NaN15
1616_0branin_metric2.758517NaN16
1717_0branin_metric5.839406NaN17
1818_0branin_metric0.790689NaN18
1919_0branin_metric0.443105NaN19
2020_0branin_metric0.404304NaN20
2121_0branin_metric3.303451NaN21
2222_0branin_metric50.510307NaN22
2323_0branin_metric0.605148NaN23
2424_0branin_metric1.127027NaN24
2525_0branin_metric0.457026NaN25
2626_0branin_metric0.514696NaN26
2727_0branin_metric0.420453NaN27
2828_0branin_metric0.462405NaN28
2929_0branin_metric0.877364NaN29
+
+
+
+
+
+
+
+
+

Plot results

We can use convenient Ax utilities for plotting the results.

+
+
+
+
+
+
In [18]:
+
+
+
import numpy as np
+from ax.plot.trace import optimization_trace_single_method
+
+
+# `plot_single_method` expects a 2-d array of means, because it expects to average means from multiple
+# optimization runs, so we wrap out best objectives array in another array.
+objective_means = np.array([[trial.objective_mean for trial in exp.trials.values()]])
+best_objective_plot = optimization_trace_single_method(
+    y=np.minimum.accumulate(objective_means, axis=1),
+    optimum=0.397887,  # Known minimum objective for Branin function.
+)
+render(best_objective_plot)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/custom_model.html b/website-old/_tutorials/custom_model.html new file mode 100644 index 0000000000..d013486a29 --- /dev/null +++ b/website-old/_tutorials/custom_model.html @@ -0,0 +1,1058 @@ + + + +
+
+
+
+

Custom Models in BoTorch

In this tutorial, we illustrate how to create a custom surrogate model using the Model and Posterior interface. We will cover creating surrogate models from:

+
    +
  • PyTorch distributions
  • +
  • Posterior samples (using Pyro)
  • +
  • Ensemble of ML predictions
  • +
+

This tutorial differs from the Using a custom BoTorch model with Ax tutorial by focusing more on authoring a new model that is compatible with the BoTorch and less on integrating a custom model with Ax's botorch_modular API.

+
+
+
+
+
+
In [1]:
+
+
+
import torch
+
+# Set the seed for reproducibility
+torch.manual_seed(1)
+# Double precision is highly recommended for BoTorch.
+# See https://github.com/pytorch/botorch/discussions/1444
+torch.set_default_dtype(torch.float64)
+
+train_X = torch.rand(20, 2) * 2
+Y = 1 - (train_X - 0.5).norm(dim=-1, keepdim=True)
+Y += 0.1 * torch.rand_like(Y)
+bounds = torch.stack([torch.zeros(2), 2 * torch.ones(2)])
+
+
+
+
+
+
+
+
+

Code to plot our training data.

+
+
+
+
+
+
In [2]:
+
+
+
from matplotlib import pyplot as plt
+from matplotlib.axes import Axes
+from torch import Tensor
+from mpl_toolkits.mplot3d import Axes3D
+
+# Needed for older versions of matplotlib.
+assert Axes3D
+
+
+def plot_toy_data(x: Tensor, y: Tensor) -> Axes:
+    ax = plt.figure().add_subplot(projection="3d")
+    ax.scatter(
+        x[:, 0].detach().numpy().squeeze(),
+        x[:, 1].detach().numpy().squeeze(),
+        zs=y.detach().numpy().squeeze(),
+        label="Observations",
+    )
+    ax.set_xlabel("X1")
+    ax.set_ylabel("X2")
+    ax.set_zlabel("Y")
+    ax.set_title("Toy Data")
+    ax.view_init(elev=15.0, azim=65)
+    ax.legend()
+    return ax
+
+
+plot_toy_data(x=train_X, y=Y)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Probabilistic Linear Regression (w/ Torch Distributions)

BoTorch's Model class only requires you to define a posterior() method that returns a Posterior object, the only requirement of which is to implement an rsample() function for drawing posterior samples. Specifically, we can utilize the subclass TorchPosterior that directly wraps a torch distribution.

+
+
+
+
+
+
In [3]:
+
+
+
from typing import Optional, Union
+from torch import Tensor, distributions, nn
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.model import Model
+from botorch.posteriors.posterior import Posterior
+from botorch.posteriors.torch import TorchPosterior
+
+
+class ProbabilisticRegressionModel(Model):
+    _num_outputs: int
+
+    def __init__(self, train_X: Tensor, train_Y: Tensor):
+        super(ProbabilisticRegressionModel, self).__init__()
+        self._num_outputs = train_Y.shape[-1]
+        # Linear layer that will compute the regression output.
+        self.linear = nn.Linear(train_X.shape[-1], self.num_outputs)
+
+    @property
+    def num_outputs(self) -> int:
+        return self._num_outputs
+
+    def forward(self, x: Tensor) -> distributions.Distribution:
+        n, p = x.squeeze().shape
+        # For now, let's suppose we have known variance 1.
+        return distributions.StudentT(df=n - p, loc=self.linear(x), scale=1)
+
+    def posterior(
+        self,
+        X: Tensor,
+        output_indices: Optional[list[int]] = None,
+        observation_noise: Union[bool, Tensor] = False,
+        posterior_transform: Optional[PosteriorTransform] = None,
+    ) -> Posterior:
+        if output_indices:
+            X = X[..., output_indices]
+        # TorchPosterior directly wraps our torch.distributions.Distribution output.
+        posterior = TorchPosterior(distribution=self(X))
+        if posterior_transform is not None:
+            posterior = posterior_transform(posterior)
+        return posterior
+
+
+
+
+
+
+
+
+
+
[KeOps] Warning : omp.h header is not in the path, disabling OpenMP. To fix this, you can set the environment
+                  variable OMP_PATH to the location of the header before importing keopscore or pykeops,
+                  e.g. using os.environ: import os; os.environ['OMP_PATH'] = '/path/to/omp/header'
+[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
def fit_prob_reg(
+    epochs: int,
+    model: ProbabilisticRegressionModel,
+    optimizer: torch.optim.Optimizer,
+    train_X: Tensor,
+    train_Y: Tensor,
+) -> None:
+    """Optimization loop for linear regression."""
+    train_X = train_X.requires_grad_()
+    for epoch in range(epochs):
+        optimizer.zero_grad()
+        outputs = model(train_X)
+        loss = -outputs.log_prob(train_Y).mean()
+        loss.backward()
+        optimizer.step()
+        if epoch % 10 == 0:
+            print("epoch {}, loss {}".format(epoch, loss.item()))
+
+
+
+
+
+
+
+
In [5]:
+
+
+
prob_regression_model = ProbabilisticRegressionModel(train_X, Y)
+optimizer = torch.optim.Adam(prob_regression_model.parameters(), lr=0.1)
+fit_prob_reg(50, prob_regression_model, optimizer, train_X, Y)
+
+
+
+
+
+
+
+
+
+
epoch 0, loss 1.3283335654335957
+epoch 10, loss 1.0691577720241896
+epoch 20, loss 0.9760611872620313
+epoch 30, loss 0.9548081485136333
+epoch 40, loss 0.9551388835842956
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
ax = plot_toy_data(x=train_X, y=Y)
+ax.scatter(
+    train_X[:, 0].detach().numpy().squeeze(),
+    train_X[:, 1].detach().numpy().squeeze(),
+    zs=prob_regression_model(train_X).mean.detach().squeeze().numpy(),
+)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Finally, since our custom model is based off Model and Posterior, we can use both analytic and MC based acquisition functions for optimization.

+
+
+
+
+
+
In [7]:
+
+
+
from botorch.acquisition.analytic import LogExpectedImprovement
+from botorch.optim.optimize import optimize_acqf
+
+candidate, acq_val = optimize_acqf(
+    LogExpectedImprovement(model=prob_regression_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+)
+candidate, acq_val
+
+
+
+
+
+
+
+
Out[7]:
+
+
(tensor([[0., 0.]]), tensor(-0.1007))
+
+
+
+
+
+
+
+
+

Before using qLogExpectedImprovement we need to register an appropriate sampler for the TorchPosterior. We can use the following code to create a MCSampler for that is specific to torch.distributions.StudentT.

+
+
+
+
+
+
In [8]:
+
+
+
from botorch.sampling.base import MCSampler
+from botorch.sampling.get_sampler import GetSampler
+from botorch.sampling.stochastic_samplers import ForkedRNGSampler
+
+
+@GetSampler.register(distributions.StudentT)
+def _get_sampler_torch(
+    posterior: TorchPosterior,
+    sample_shape: torch.Size,
+    *,
+    seed: Optional[int] = None,
+) -> MCSampler:
+    # Use `ForkedRNGSampler` to ensure determinism in acquisition function evaluations.
+    return ForkedRNGSampler(sample_shape=sample_shape, seed=seed)
+
+
+
+
+
+
+
+
In [9]:
+
+
+
from botorch.acquisition.logei import qLogExpectedImprovement
+
+optimize_acqf(
+    qLogExpectedImprovement(model=prob_regression_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+)
+
+
+
+
+
+
+
+
Out[9]:
+
+
(tensor([[0., 0.]]), tensor(-0.1105))
+
+
+
+
+
+
+
+
+

Supported PyTorch Distributions

Although we chose the StudentT distribution in the above example, any distribution supporting the rsample method will work with BoTorch's automatic differentiation. We can use the has_rsample attribute to see a complete listing of compatible distributions.

+
+
+
+
+
+
In [10]:
+
+
+
print(
+    [
+        j.__name__
+        for j in [getattr(distributions, i) for i in distributions.__all__]
+        if hasattr(j, "has_rsample") and j.has_rsample
+    ]
+)
+
+
+
+
+
+
+
+
+
+
['Beta', 'Cauchy', 'Chi2', 'ContinuousBernoulli', 'Dirichlet', 'Exponential', 'FisherSnedecor', 'Gamma', 'Gumbel', 'HalfCauchy', 'HalfNormal', 'Independent', 'InverseGamma', 'Kumaraswamy', 'Laplace', 'LogNormal', 'LogisticNormal', 'LowRankMultivariateNormal', 'MultivariateNormal', 'Normal', 'OneHotCategoricalStraightThrough', 'Pareto', 'RelaxedBernoulli', 'RelaxedOneHotCategorical', 'StudentT', 'Uniform', 'Weibull', 'Wishart', 'TransformedDistribution']
+
+
+
+
+
+
+
+
+
+

Bayesian Linear Regression

In the previous section, we directly parameterized a "posterior" with a linear layer. In this section, we will follow Chapter 14.2 of Bayesian Data Analysis to implement a proper posterior analytically. This implementation also uses TorchPosterior and the StudentT distribution like before.

+
+
+
+
+
+
In [11]:
+
+
+
from typing import Optional, Union
+from torch import Tensor, distributions, nn
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.model import Model
+from botorch.posteriors.posterior import Posterior
+from botorch.posteriors.torch import TorchPosterior
+
+
+def add_intercept(x: Tensor) -> Tensor:
+    """Adds an intercept column to the design matrix (i.e. tensor)."""
+    return torch.concat([torch.ones_like(x)[..., 0:1], x], dim=-1)
+
+
+class BayesianRegressionModel(Model):
+    _num_outputs: int
+    df: int
+    s_squared: Tensor
+    beta: Tensor
+    L: Tensor
+    add_intercept: bool
+
+    def __init__(self, intercept: bool = True) -> None:
+        super(BayesianRegressionModel, self).__init__()
+        self.add_intercept = intercept
+
+    @property
+    def num_outputs(self) -> int:
+        return self._num_outputs
+
+    def forward(self, x: Tensor) -> Tensor:
+        return x @ self.beta
+
+    def fit(self, x: Tensor, y: Tensor) -> None:
+        self._num_outputs = y.shape[-1]
+        x = add_intercept(x) if self.add_intercept else x
+        n, p = x.shape
+        self.df = n - p
+        # Rather than V = torch.linalg.inv(x.T @ x) as in BDA
+        # instead use L = torch.linalg.cholesky(x.T @ x) for stability.
+        # To use L, we can simply replace operations like:
+        # x = V @ b
+        # with a call to `torch.cholesky_solve`:
+        # x = torch.cholesky_solve(b, L)
+        self.L = torch.linalg.cholesky(x.T @ x)
+        # Least squares estimate
+        # self.beta = torch.cholesky_solve(x.T, self.L) @ y
+        self.beta = torch.cholesky_solve(x.T, self.L) @ y
+        # Model's residuals from the labels.
+        r: Tensor = y - self(x)
+        # Sample variance
+        self.s_squared = (1 / self.df) * r.T @ r
+
+    def posterior(
+        self,
+        X: Tensor,
+        output_indices: Optional[list[int]] = None,
+        observation_noise: Union[bool, Tensor] = False,
+        posterior_transform: Optional[PosteriorTransform] = None,
+    ) -> Posterior:
+        # Squeeze out the q dimension if needed.
+        n, q, _ = X.shape
+        if output_indices:
+            X = X[..., output_indices]
+        if self.add_intercept:
+            X = add_intercept(X)
+        loc = self(X)
+        # Full covariance matrix of all test points.
+        cov = self.s_squared * (
+            torch.eye(n, n) + X.squeeze() @ torch.cholesky_solve(X.squeeze().T, self.L)
+        )
+        # The batch semantics of BoTorch evaluate each data point in their own batch.
+        # So, we extract the diagonal representing Var[\tilde y_i | y_i] of each test point.
+        scale = torch.diag(cov).reshape(n, q, self.num_outputs)
+        # Form the posterior predictive dist according to Sec 14.2, Pg 357 of BDA.
+        posterior_predictive_dist = distributions.StudentT(
+            df=self.df, loc=loc, scale=scale
+        )
+        posterior = TorchPosterior(distribution=posterior_predictive_dist)
+        if posterior_transform is not None:
+            posterior = posterior_transform(posterior)
+        return posterior
+
+
+
+
+
+
+
+
In [12]:
+
+
+
bayesian_regression_model = BayesianRegressionModel(intercept=True)
+bayesian_regression_model.fit(train_X, Y)
+
+
+
+
+
+
+
+
In [13]:
+
+
+
ax = plot_toy_data(x=train_X, y=Y)
+ax.scatter(
+    train_X[:, 0].detach().numpy().squeeze(),
+    train_X[:, 1].detach().numpy().squeeze(),
+    zs=bayesian_regression_model(add_intercept(train_X)).detach().squeeze().numpy(),
+)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [14]:
+
+
+
optimize_acqf(
+    LogExpectedImprovement(model=bayesian_regression_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+)
+
+
+
+
+
+
+
+
Out[14]:
+
+
(tensor([[0., 0.]]), tensor(-1.3847))
+
+
+
+
+
+
+
+
In [15]:
+
+
+
optimize_acqf(
+    qLogExpectedImprovement(model=bayesian_regression_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+)
+
+
+
+
+
+
+
+
Out[15]:
+
+
(tensor([[0., 0.]]), tensor(-1.3684))
+
+
+
+
+
+
+
+
+

Bayesian Linear Regression w/ EnsemblePosterior

The EnsembleModel class provides a default implementation for posterior(). Then the MC acquisition function will be optimized using samples from the posterior predictive distribution (EnsemblePosterior also implements mean and variance properties, so some other analytic acquisition functions will also work). We follow this Pyro tutorial) for a linear regression model fit with Stochastic Variational Inference (SVI).

+
+
+
+
+
+
+

First, we define a Pyro model capable of sampling from a posterior predictive distribution for new observations at test points. Later, when we perform posterior predictive inference, we will use Pyro's Predictive class. By default, Predictive ignores inference gradients with:

+
model = torch.no_grad()(poutine.mask(model, mask=False) if mask else model)
+
+

Since we need to retain the autograd graph to optimize the acquisition function, we can use torch.set_grad_enabled(True) in the forward() method to override this behavior.

+
+
+
+
+
+
In [16]:
+
+
+
import pyro
+import pyro.distributions as dist
+from pyro.infer.autoguide import AutoGuide, AutoDiagonalNormal
+from pyro.nn import PyroSample, PyroModule
+from pyro.infer import SVI, Trace_ELBO
+from pyro.optim import PyroOptim
+
+pyro.set_rng_seed(1)
+
+
+# Bayesian Regression represented as a single hidden layer.
+class BayesianRegression(PyroModule):
+    Y: str = "y"
+
+    def __init__(self, in_features: int, out_features: int):
+        super().__init__()
+        # Linear layer like before, but wrapped with PyroModule.
+        self.linear = PyroModule[nn.Linear](in_features, out_features)
+        # Add priors to the weights & bias of the linear layer.
+        self.linear.weight = PyroSample(
+            dist.Normal(0.0, 1.0)
+            .expand(torch.Size([out_features, in_features]))
+            .to_event(2)
+        )
+        self.linear.bias = PyroSample(
+            dist.Normal(0.0, 10.0).expand(torch.Size([out_features])).to_event(1)
+        )
+
+    def forward(self, x: Tensor, y: Optional[Tensor] = None) -> Tensor:
+        # NOTE: Enable gradient tracking to override behavior of `Predictive`.
+        torch.set_grad_enabled(True)
+        # Prior for the noise level.
+        sigma = pyro.sample("sigma", dist.Uniform(0.0, 10.0))
+        # Linear layer on the inputs.
+        mean = self.linear(x).squeeze(-1)
+        n, p = x.shape[0], x.shape[-1]
+        with pyro.plate("data", x.shape[0]):
+            # Observations will be t distributed.
+            t_dist = dist.StudentT(df=n - p, loc=mean, scale=sigma)
+            _ = pyro.sample(self.Y, t_dist, obs=y)
+        return mean
+
+
+
+
+
+
+
+
In [17]:
+
+
+
def fit_svi(
+    epochs: int,
+    model: PyroModule,
+    guide: AutoGuide,
+    optimizer: PyroOptim,
+    train_X: Tensor,
+    train_Y: Tensor,
+) -> None:
+    svi = SVI(
+        model,
+        guide,
+        optimizer,
+        loss=Trace_ELBO(),
+    )
+    pyro.clear_param_store()
+    for epoch in range(epochs):
+        loss = svi.step(train_X, train_Y.squeeze())
+        if epoch % 10 == 0:
+            print("epoch {}, loss {}".format(epoch, loss))
+
+
+
+
+
+
+
+
+

Now, we incorporate our Pyro model into the Model and Posterior interface like before. EnsemblePosterior expects a (b) x s x q x m tensor where m is the output size of the model and s is the ensemble size.

+
+
+
+
+
+
In [18]:
+
+
+
from botorch.models.ensemble import EnsembleModel
+from pyro.infer import Predictive
+
+class EnsembleBayesianRegressionModel(EnsembleModel):
+    model: BayesianRegression
+    guide: AutoGuide
+    num_samples: int
+    _num_outputs: int
+
+    def __init__(self, train_X: Tensor, train_Y: Tensor, num_samples: int = 100):
+        super(EnsembleBayesianRegressionModel, self).__init__()
+        self._num_outputs = train_Y.shape[-1]
+        self.model = BayesianRegression(train_X.shape[-1], self.num_outputs)
+        self.guide = AutoDiagonalNormal(self.model)
+        self.num_samples = num_samples
+
+    def forward(self, X: Tensor) -> Tensor:
+        predictive = Predictive(
+            self.model,
+            guide=self.guide,
+            num_samples=self.num_samples,
+            # Only return the posterior predictive distribution for y.
+            return_sites=(self.model.Y,),
+        )
+        # `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the
+        # output size of the model and `s` is the ensemble size.
+        samples = (
+            # Retrieve posterior samples from the observation random variable.
+            # This is also known as a posterior predictive distribution.
+            predictive(X.squeeze())[self.model.Y]
+            # Move the ensemble dimension to "s" axis.
+            .transpose(0, 1)
+            # Reshape for `EnsemblePosterior` as mentioned above.
+            .reshape(X.shape[0], -1, 1, self.num_outputs)
+        )
+        return samples
+
+
+
+
+
+
+
+
In [19]:
+
+
+
ensemble_bayesian_regression_model = EnsembleBayesianRegressionModel(
+    train_X=train_X, train_Y=Y
+)
+fit_svi(
+    100,
+    ensemble_bayesian_regression_model.model,
+    ensemble_bayesian_regression_model.guide,
+    pyro.optim.Adam({"lr": 0.1}),
+    train_X,
+    Y,
+)
+
+
+
+
+
+
+
+
+
+
epoch 0, loss 57.859971924474735
+epoch 10, loss 47.17245571053782
+epoch 20, loss 27.547291517941602
+epoch 30, loss 34.39363837327427
+epoch 40, loss 43.94011251783476
+epoch 50, loss 33.11519462561163
+epoch 60, loss 28.7194289840763
+epoch 70, loss 24.450418378181947
+epoch 80, loss 11.057529271793364
+epoch 90, loss 13.638860647173294
+
+
+
+
+
+
+
+
+
In [20]:
+
+
+
ax = plot_toy_data(x=train_X, y=Y)
+ax.scatter(
+    train_X[:, 0].detach().numpy().squeeze(),
+    train_X[:, 1].detach().numpy().squeeze(),
+    zs=ensemble_bayesian_regression_model(train_X)
+    .detach()
+    .squeeze()
+    .mean(dim=-1)
+    .numpy(),
+)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [21]:
+
+
+
optimize_acqf(
+    LogExpectedImprovement(model=ensemble_bayesian_regression_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+)
+
+
+
+
+
+
+
+
Out[21]:
+
+
(tensor([[0., 0.]]), tensor(-1.0121))
+
+
+
+
+
+
+
+
In [22]:
+
+
+
optimize_acqf(
+    qLogExpectedImprovement(model=ensemble_bayesian_regression_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+)
+
+
+
+
+
+
+
+
Out[22]:
+
+
(tensor([[0., 0.]]), tensor(-0.8815))
+
+
+
+
+
+
+
+
+

Random Forest w/ Ensemble Posterior

Finally, we move away from linear models to any ML technique that ensembles many models. Specifically, we can use the RandomForestRegressor from sklearn which is an ensemble method of individual decision trees. These decision trees can be accessed through the object's estimators_ attribute.

+
+
+
+
+
+
In [23]:
+
+
+
import numpy as np
+from sklearn.ensemble import RandomForestRegressor
+from botorch.models.ensemble import EnsembleModel
+
+
+class EnsembleRandomForestModel(EnsembleModel):
+    model: RandomForestRegressor
+    num_samples: int
+    _num_outputs: int
+
+    def __init__(self, num_samples: int = 100):
+        super(EnsembleRandomForestModel, self).__init__()
+        self._num_outputs = 1
+        self.model = RandomForestRegressor(n_estimators=num_samples)
+
+    def fit(self, X: Tensor, y: Tensor) -> None:
+        self.model = self.model.fit(
+            X=X.detach().numpy(), y=y.detach().numpy().squeeze()
+        )
+
+    def forward(self, X: Tensor) -> Tensor:
+        x = X.detach().numpy().squeeze()
+        # Create the ensemble from predictions from each decision tree.
+        y = torch.from_numpy(np.array([i.predict(x) for i in self.model.estimators_]))
+        # `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the
+        # output size of the model and `s` is the ensemble size.
+        samples = y.transpose(0, 1).reshape(X.shape[0], -1, 1, self.num_outputs)
+        return samples
+
+
+
+
+
+
+
+
In [24]:
+
+
+
ensemble_random_forest_model = EnsembleRandomForestModel(num_samples=300)
+ensemble_random_forest_model.fit(X=train_X, y=Y)
+
+
+
+
+
+
+
+
In [25]:
+
+
+
ax = plot_toy_data(x=train_X, y=Y)
+ax.scatter(
+    train_X[:, 0].detach().numpy().squeeze(),
+    train_X[:, 1].detach().numpy().squeeze(),
+    zs=ensemble_random_forest_model(train_X).detach().squeeze().mean(dim=-1).numpy(),
+)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

In order to use gradient-based optimization of the acquisition function (via the standard optimize_acqf() method) we will need to have the samples drawn from the posterior be differentiable w.r.t. to the input to the posterior() method (this is not the case for Random Forest models). Instead, we will perform the acquisition function optimization with gradient-free methods.

+
+
+
+
+
+
In [26]:
+
+
+
optimize_acqf(
+    LogExpectedImprovement(model=ensemble_random_forest_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+    options={"with_grad": False},
+)
+
+
+
+
+
+
+
+
Out[26]:
+
+
(tensor([[0.3959, 1.3023]]), tensor(-4.3914))
+
+
+
+
+
+
+
+
In [27]:
+
+
+
optimize_acqf(
+    qLogExpectedImprovement(model=ensemble_random_forest_model, best_f=Y.max()),
+    bounds=bounds,
+    q=1,
+    num_restarts=5,
+    raw_samples=10,
+    options={"with_grad": False},
+)
+
+
+
+
+
+
+
+
Out[27]:
+
+
(tensor([[0.9057, 0.0959]]), tensor(-15.1323))
+
+
+
+
+
+
+
+
+

CMA-ES

We can also move the optimization loop out of BoTorch entirely and follow the CMA-ES tutorial to optimize with an evolution strategy.

+
+
+
+
+
+
In [28]:
+
+
+
import cma
+import numpy as np
+
+x0 = np.random.rand(2)
+
+es = cma.CMAEvolutionStrategy(
+    x0=x0,
+    sigma0=0.2,
+    inopts={"bounds": [0, 2], "popsize": 50},
+)
+
+log_expected_improvement_ensemble_random_forest_model = LogExpectedImprovement(
+    model=ensemble_random_forest_model, best_f=Y.max()
+)
+
+with torch.no_grad():
+    while not es.stop():
+        xs = es.ask()
+        y = (
+            -log_expected_improvement_ensemble_random_forest_model(
+                torch.from_numpy(np.array(xs)).unsqueeze(-2)
+            )
+            .view(-1)
+            .double()
+            .numpy()
+        )
+        es.tell(xs, y)
+
+torch.from_numpy(es.best.x)
+
+
+
+
+
+
+
+
+
+
(25_w,50)-aCMA-ES (mu_w=14.0,w_1=14%) in dimension 2 (seed=380612, Wed Aug 21 17:25:36 2024)
+
+
+
+
+
Out[28]:
+
+
tensor([0.4497, 0.8411])
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/decoupled_mobo.html b/website-old/_tutorials/decoupled_mobo.html new file mode 100644 index 0000000000..6ad24b84cb --- /dev/null +++ b/website-old/_tutorials/decoupled_mobo.html @@ -0,0 +1,721 @@ + + + +
+
+
+
+

Multi-Objective BO with Decoupled Evaluations using HVKG

In this tutorial, we illustrate how to use the hypervolume knowledge gradient for problems where the objectives can be evaluated independently (decoupled).

+

There are two types of decoupling:

+
    +
  • Competitive decoupling: where the objectives are evaluated using the same evaluation resource. Often the objectives have heterogenous costs and therefore it is prudent to select what design and objective to evaluate in a cost-aware fashion.

    +
  • +
  • Non-competitive decoupling: where the objectives have independent evaluation resources and potentially different numbers of designs can be evaluated in parallel. In this scenario, all available evaluation resources should be exploited and the goal is to optimize the objectives as well as possible within a fixed number of time steps.

    +
  • +
+

In this tutorial, we focus on competitive decoupling and show how HVKG can be used for efficient optimization.

+

[1] S. Daulton, M. Balandat, and E. Bakshy. Hypervolume Knowledge Gradient: A Lookahead Approach for Multi-Objective Bayesian Optimization with Partial Information. ICML, 2023.

+

Note: pymoo is an optional dependency that is used for determining the Pareto set of optimal designs under the model posterior mean using NSGA-II (which is not a sample efficient method, but sample efficiency is not critical for this step). If pymoo is not available, the Pareto set of optimal designs is selected from a discrete set. This will work okay for low-dim (e.g. $\leq2$ dimensions) problems, but in general NSGA-II will yield far better results.

+
+
+
+
+
+
+

Set dtype and device

Note: HVKG aggressively exploits parallel hardware and is much faster when run on a GPU.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+
+import torch
+
+
+tkwargs = {
+    "dtype": torch.double,
+    "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"),
+}
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+
+
I1110 064940.229 _utils_internal.py:230] NCCL_DEBUG env var is set to None
+
+
+
+
+
+
+
I1110 064940.231 _utils_internal.py:239] NCCL_DEBUG is INFO from /etc/nccl.conf
+
+
+
+
+
+
+
+
+
+

Problem setup

In this tutorial, we optimize a bi-objective synthetic function ZDT2 over a 6-dimensional space. The costs of evaluating each objective are 3 and 1, respectively, which we choose to be different to reflect that many multi-objective optimization problems have heterogeneous costs.

+
+
+
+
+
+
In [2]:
+
+
+
from botorch.test_functions.multi_objective import ZDT2
+from botorch.models.cost import FixedCostModel
+
+
+problem = ZDT2(negate=True, dim=6).to(**tkwargs)
+
+# define the cost model
+objective_costs = {0: 3.0, 1: 1.0}
+objective_indices = list(objective_costs.keys())
+objective_costs = {int(k): v for k, v in objective_costs.items()}
+objective_costs_t = torch.tensor(
+    [objective_costs[k] for k in sorted(objective_costs.keys())], **tkwargs
+)
+cost_model = FixedCostModel(fixed_cost=objective_costs_t)
+
+
+
+
+
+
+
+
+

Model initialization

We use a list of SingleTaskGPs to model the two objectives with known noise variances. The models are initialized with $2(d+1)=14$ points drawn randomly from $[0,1]^2$. Since the objectives can be evaluated independently, the number of observations of each objective can be different. Therefore, we must use a ModelListGP.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.utils.sampling import draw_sobol_samples
+from botorch.utils.transforms import normalize, unnormalize
+from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood
+from torch import Tensor
+from gpytorch.priors import GammaPrior
+from gpytorch.kernels import MaternKernel, ScaleKernel
+
+
+def generate_initial_data(n):
+    # generate training data
+    train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1)
+    train_obj_true = problem(train_x)
+    return train_x, train_obj_true
+
+
+def initialize_model(train_x_list, train_obj_list):
+    # define models for objective and constraint
+    train_x_list = [normalize(train_x, problem.bounds) for train_x in train_x_list]
+    models = []
+    for i in range(len(train_obj_list)):
+        train_y = train_obj_list[i]
+        train_yvar = torch.full_like(train_y, 1e-7)  # noiseless
+        models.append(
+            SingleTaskGP(
+                train_X=train_x_list[i],
+                train_Y=train_y,
+                train_Yvar=train_yvar,
+                outcome_transform=Standardize(m=1),
+                covar_module=ScaleKernel(
+                    MaternKernel(
+                        nu=2.5,
+                        ard_num_dims=train_x_list[0].shape[-1],
+                        lengthscale_prior=GammaPrior(2.0, 2.0),
+                    ),
+                    outputscale_prior=GammaPrior(2.0, 0.15),
+                )
+            )
+        )
+    model = ModelListGP(*models)
+    mll = SumMarginalLogLikelihood(model.likelihood, model)
+    return mll, model
+
+
+
+
+
+
+
+
+

Define a helper functions that performs the essential BO step for $q$NEHVI and HVKG

The helper function below initializes the $q$NEHVI acquisition function (a strong baseline, but one that does not support decoupled evaluations), optimizes it, and returns the candidate along with the observed function values.

+

Reference Point

+

$q$NEHVI and HVKG require specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.acquisition.multi_objective.monte_carlo import (
+    qNoisyExpectedHypervolumeImprovement,
+)
+from botorch.optim.optimize import optimize_acqf
+
+
+BATCH_SIZE = 1
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+
+standard_bounds = torch.zeros(2, problem.dim, **tkwargs)
+standard_bounds[1] = 1
+
+
+def optimize_qnehvi_and_get_observation(model, train_x, sampler):
+    """Optimizes the qNEHVI acquisition function, and returns a new candidate and observation."""
+    # partition non-dominated space into disjoint rectangles
+    acq_func = qNoisyExpectedHypervolumeImprovement(
+        model=model,
+        ref_point=problem.ref_point.tolist(),  # use known reference point
+        X_baseline=normalize(train_x, problem.bounds),
+        prune_baseline=True,  # prune baseline points that have estimated zero probability of being Pareto optimal
+        sampler=sampler,
+    )
+    # optimize
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        bounds=standard_bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+        sequential=True,
+    )
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=problem.bounds)
+    new_obj_true = problem(new_x)
+    return new_x, new_obj_true
+
+
+
+
+
+
+
+
+

Helper Function for initializing and optimizing HVKG

Below we define the following helper functions:

+
    +
  1. get_current_value for computing the current hypervolume of the hypervolume maximizing set under the posterior mean.
  2. +
  3. optimize_HVKG_and_get_obs_decoupled to initialize and optimize HVKG to determine which design to evaluate and which objective to evaluate the design on. This method obtains the observation corresponding to that design.
  4. +
+
+
+
+
+
+
In [5]:
+
+
+
from botorch.acquisition.cost_aware import InverseCostWeightedUtility
+from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import (
+    _get_hv_value_function,
+    qHypervolumeKnowledgeGradient,
+)
+from botorch.models.deterministic import GenericDeterministicModel
+from botorch.sampling.list_sampler import ListSampler
+from botorch.sampling.normal import IIDNormalSampler
+
+NUM_PARETO = 2 if SMOKE_TEST else 10
+NUM_FANTASIES = 2 if SMOKE_TEST else 8
+NUM_HVKG_RESTARTS = 1
+
+
+def get_current_value(
+    model,
+    ref_point,
+    bounds,
+):
+    """Helper to get the hypervolume of the current hypervolume
+    maximizing set.
+    """
+    curr_val_acqf = _get_hv_value_function(
+        model=model,
+        ref_point=ref_point,
+        use_posterior_mean=True,
+    )
+    _, current_value = optimize_acqf(
+        acq_function=curr_val_acqf,
+        bounds=bounds,
+        q=NUM_PARETO,
+        num_restarts=20,
+        raw_samples=1024,
+        return_best_only=True,
+        options={"batch_limit": 5},
+    )
+    return current_value
+
+
+def optimize_HVKG_and_get_obs_decoupled(model):
+    """Utility to initialize and optimize HVKG."""
+    cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)
+
+    current_value = get_current_value(
+        model=model,
+        ref_point=problem.ref_point,
+        bounds=standard_bounds,
+    )
+
+    acq_func = qHypervolumeKnowledgeGradient(
+        model=model,
+        ref_point=problem.ref_point,  # use known reference point
+        num_fantasies=NUM_FANTASIES,
+        num_pareto=NUM_PARETO,
+        current_value=current_value,
+        cost_aware_utility=cost_aware_utility,
+    )
+
+    # optimize acquisition functions and get new observations
+    objective_vals = []
+    objective_candidates = []
+    for objective_idx in objective_indices:
+        # set evaluation index to only condition on one objective
+        # this could be multiple objectives
+        X_evaluation_mask = torch.zeros(
+            1,
+            len(objective_indices),
+            dtype=torch.bool,
+            device=standard_bounds.device,
+        )
+        X_evaluation_mask[0, objective_idx] = 1
+        acq_func.X_evaluation_mask = X_evaluation_mask
+        candidates, vals = optimize_acqf(
+            acq_function=acq_func,
+            num_restarts=NUM_HVKG_RESTARTS,
+            raw_samples=RAW_SAMPLES,
+            bounds=standard_bounds,
+            q=BATCH_SIZE,
+            sequential=False,
+            options={"batch_limit": 5},
+        )
+        objective_vals.append(vals.view(-1))
+        objective_candidates.append(candidates)
+    best_objective_index = torch.cat(objective_vals, dim=-1).argmax().item()
+    eval_objective_indices = [best_objective_index]
+    candidates = objective_candidates[best_objective_index]
+    vals = objective_vals[best_objective_index]
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=problem.bounds)
+    new_obj = problem(new_x)
+    new_obj = new_obj[..., eval_objective_indices]
+    return new_x, new_obj, eval_objective_indices
+
+
+
+
+
+
+
+
+

Define function to find model-estimated Pareto set of designs under posterior mean using NSGA-II

+
+
+
+
+
+
In [6]:
+
+
+
import numpy as np
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    FastNondominatedPartitioning,
+)
+from botorch.utils.multi_objective.pareto import _is_non_dominated_loop
+from gpytorch import settings
+
+try:
+    from pymoo.algorithms.nsga2 import NSGA2
+    from pymoo.model.problem import Problem
+    from pymoo.optimize import minimize
+    from pymoo.util.termination.max_gen import MaximumGenerationTermination
+
+    def get_model_identified_hv_maximizing_set(
+        model,
+        population_size=250,
+        max_gen=100,
+    ):
+        """Optimize the posterior mean using NSGA-II."""
+        tkwargs = {
+            "dtype": problem.ref_point.dtype,
+            "device": problem.ref_point.device,
+        }
+        dim = problem.dim
+
+        class PosteriorMeanPymooProblem(Problem):
+            def __init__(self):
+                super().__init__(
+                    n_var=dim,
+                    n_obj=problem.num_objectives,
+                    type_var=np.double,
+                )
+                self.xl = np.zeros(dim)
+                self.xu = np.ones(dim)
+
+            def _evaluate(self, x, out, *args, **kwargs):
+                X = torch.from_numpy(x).to(**tkwargs)
+                is_fantasy_model = (
+                    isinstance(model, ModelListGP)
+                    and model.models[0].train_targets.ndim > 2
+                ) or (
+                    not isinstance(model, ModelListGP) and model.train_targets.ndim > 2
+                )
+                with torch.no_grad():
+                    with settings.cholesky_max_tries(9):
+                        # eval in batch mode
+                        y = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2)
+                    if is_fantasy_model:
+                        y = y.mean(dim=-2)
+                out["F"] = -y.cpu().numpy()
+
+        pymoo_problem = PosteriorMeanPymooProblem()
+        algorithm = NSGA2(
+            pop_size=population_size,
+            eliminate_duplicates=True,
+        )
+        res = minimize(
+            pymoo_problem,
+            algorithm,
+            termination=MaximumGenerationTermination(max_gen),
+            # seed=0,  # fix seed
+            verbose=False,
+        )
+        X = torch.tensor(
+            res.X,
+            **tkwargs,
+        )
+        X = unnormalize(X, problem.bounds)
+        Y = problem(X)
+        # compute HV
+        partitioning = FastNondominatedPartitioning(ref_point=problem.ref_point, Y=Y)
+        return partitioning.compute_hypervolume().item()
+
+except ImportError:
+    NUM_DISCRETE_POINTS = 100 if SMOKE_TEST else 100000
+    CHUNK_SIZE = 512
+
+    def get_model_identified_hv_maximizing_set(
+        model,
+    ):
+        """Optimize the posterior mean over a discrete set."""
+        tkwargs = {
+            "dtype": problem.ref_point.dtype,
+            "device": problem.ref_point.device,
+        }
+        dim = problem.dim
+
+        discrete_set = torch.rand(NUM_DISCRETE_POINTS, dim, **tkwargs)
+        with torch.no_grad():
+            preds_list = []
+            for start in range(0, NUM_DISCRETE_POINTS, CHUNK_SIZE):
+                preds = model.posterior(
+                    discrete_set[start : start + CHUNK_SIZE].unsqueeze(-2)
+                ).mean.squeeze(-2)
+                preds_list.append(preds)
+            preds = torch.cat(preds_list, dim=0)
+            pareto_mask = _is_non_dominated_loop(preds)
+            pareto_X = discrete_set[pareto_mask]
+        pareto_X = unnormalize(pareto_X, problem.bounds)
+        Y = problem(pareto_X)
+        # compute HV
+        partitioning = FastNondominatedPartitioning(ref_point=problem.ref_point, Y=Y)
+        return partitioning.compute_hypervolume().item()
+
+
+
+
+
+
+
+
+

Perform Bayesian Optimization loop with Decoupled HVKG and compared against non-decoupled $q$NEHVI

The Bayesian optimization "loop" for a batch size of 1 simply iterates the following steps:

+
    +
  1. given a surrogate model, choose a candidate design and objective to evaluate (for methods that leverage decoupled evaluations).
  2. +
  3. observe one or more objectives for the candidate design.
  4. +
  5. update the surrogate model.
  6. +
+

The loop will continue to run until a pre-specified evaluation budget (in terms of cost) is exhausted.

+
+
+
+
+
+
In [7]:
+
+
+
import time
+import warnings
+
+from botorch import fit_gpytorch_mll
+from botorch.exceptions import BadInitialCandidatesWarning
+from botorch.sampling.normal import SobolQMCNormalSampler
+
+
+warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning)
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+MC_SAMPLES = 128 if not SMOKE_TEST else 16
+COST_BUDGET = 90 if not SMOKE_TEST else 54
+torch.manual_seed(0)
+verbose = True
+N_INIT = 2 * problem.dim + 1
+
+total_cost = {"hvkg": 0.0, "qnehvi": 0.0, "random": 0.0}
+
+
+# call helper functions to generate initial training data and initialize model
+train_x_hvkg, train_obj_hvkg = generate_initial_data(n=N_INIT)
+train_obj_hvkg_list = list(train_obj_hvkg.split(1, dim=-1))
+train_x_hvkg_list = [train_x_hvkg] * len(train_obj_hvkg_list)
+mll_hvkg, model_hvkg = initialize_model(train_x_hvkg_list, train_obj_hvkg_list)
+train_obj_random_list = train_obj_hvkg_list
+train_x_random_list = train_x_hvkg_list
+train_x_qnehvi_list, train_obj_qnehvi_list = (
+    train_x_hvkg_list,
+    train_obj_hvkg_list,
+)
+cost_hvkg = cost_model(train_x_hvkg).sum(dim=-1)
+total_cost["hvkg"] += cost_hvkg.sum().item()
+cost_qnehvi = cost_hvkg
+cost_random = cost_hvkg
+total_cost["qnehvi"] = total_cost["hvkg"]
+total_cost["random"] = total_cost["hvkg"]
+mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi_list, train_obj_qnehvi_list)
+mll_random, model_random = initialize_model(train_x_random_list, train_obj_random_list)
+# fit the models
+fit_gpytorch_mll(mll_hvkg)
+fit_gpytorch_mll(mll_qnehvi)
+fit_gpytorch_mll(mll_random)
+# compute hypervolume
+hv = get_model_identified_hv_maximizing_set(model=model_qnehvi)
+hvs_hvkg, hvs_qehvi, hvs_qnehvi, hvs_random = [hv], [hv], [hv], [hv]
+if verbose:
+    print(
+        f"\nInitial: Hypervolume (random, qHVKG, qNEHVI) = "
+        f"({hvs_random[-1]:>4.2f}, {hvs_hvkg[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}).",
+        end="",
+    )
+# run N_BATCH rounds of BayesOpt after the initial random batch
+iteration = 0
+active_algos = {k for k, v in total_cost.items() if v < COST_BUDGET}
+while any(v < COST_BUDGET for v in total_cost.values()):
+
+    t0 = time.monotonic()
+    if "hvkg" in active_algos:
+        # generate candidates
+        (
+            new_x_hvkg,
+            new_obj_hvkg,
+            eval_objective_indices_hvkg,
+        ) = optimize_HVKG_and_get_obs_decoupled(
+            model_hvkg,
+        )
+        # update training points
+        for i in eval_objective_indices_hvkg:
+            train_x_hvkg_list[i] = torch.cat([train_x_hvkg_list[i], new_x_hvkg])
+            train_obj_hvkg_list[i] = torch.cat(
+                [train_obj_hvkg_list[i], new_obj_hvkg], dim=0
+            )
+        # update costs
+        all_outcome_cost = cost_model(new_x_hvkg)
+        new_cost_hvkg = all_outcome_cost[..., eval_objective_indices_hvkg].sum(dim=-1)
+        cost_hvkg = torch.cat([cost_hvkg, new_cost_hvkg], dim=0)
+        total_cost["hvkg"] += new_cost_hvkg.sum().item()
+        # fit models
+        mll_hvkg, model_hvkg = initialize_model(train_x_hvkg_list, train_obj_hvkg_list)
+        fit_gpytorch_mll(mll_hvkg)
+
+    if "qnehvi" in active_algos:
+        qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+        # generate candidates
+        new_x_qnehvi, new_obj_qnehvi = optimize_qnehvi_and_get_observation(
+            model_qnehvi, train_x_qnehvi_list[0], qnehvi_sampler
+        )
+        # update training points
+        for i in objective_indices:
+            train_x_qnehvi_list[i] = torch.cat([train_x_qnehvi_list[i], new_x_qnehvi])
+            train_obj_qnehvi_list[i] = torch.cat(
+                [train_obj_qnehvi_list[i], new_obj_qnehvi[..., i : i + 1]]
+            )
+        # update costs
+        new_cost_qnehvi = cost_model(new_x_qnehvi).sum(dim=-1)
+        cost_qnehvi = torch.cat([cost_qnehvi, new_cost_qnehvi], dim=0)
+        total_cost["qnehvi"] += new_cost_qnehvi.sum().item()
+        # fit models
+        mll_qnehvi, model_qnehvi = initialize_model(
+            train_x_qnehvi_list, train_obj_qnehvi_list
+        )
+        fit_gpytorch_mll(mll_qnehvi)
+    if "random" in active_algos:
+        # generate candidates
+        new_x_random, new_obj_random = generate_initial_data(n=BATCH_SIZE)
+        # update training points
+        for i in objective_indices:
+            train_x_random_list[i] = torch.cat([train_x_random_list[i], new_x_random])
+            train_obj_random_list[i] = torch.cat(
+                [train_obj_random_list[i], new_obj_random[..., i : i + 1]]
+            )
+        # update costs
+        new_cost_random = cost_model(new_x_random).sum(dim=-1)
+        cost_random = torch.cat([cost_random, new_cost_random], dim=0)
+        total_cost["random"] += new_cost_random.sum().item()
+        # fit models
+        mll_random, model_random = initialize_model(
+            train_x_random_list, train_obj_random_list
+        )
+        fit_gpytorch_mll(mll_random)
+
+    # compute hypervolume
+    for label, model, hv_list in zip(
+        ["hvkg", "qnehvi", "random"],
+        [model_hvkg, model_qnehvi, model_random],
+        [hvs_hvkg, hvs_qnehvi, hvs_random],
+    ):
+        if label in active_algos:
+            hv = get_model_identified_hv_maximizing_set(model=model)
+            hv_list.append(hv)
+        else:
+            # no update performed
+            hv_list.append(hv_list[-1])
+
+    t1 = time.monotonic()
+    if verbose:
+        print(
+            f"\nBatch {iteration:>2}: Costs (random, qHVKG, qNEHVI) = "
+            f"({total_cost['random']:>4.2f}, {total_cost['hvkg']:>4.2f}, {total_cost['qnehvi']:>4.2f}). "
+        )
+        print(
+            f"\nHypervolume (random, qHVKG, qNEHVI) = "
+            f"({hvs_random[-1]:>4.2f}, {hvs_hvkg[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), "
+            f"time = {t1-t0:>4.2f}.",
+            end="",
+        )
+    else:
+        print(".", end="")
+    iteration += 1
+    active_algos = {k for k, v in total_cost.items() if v < COST_BUDGET}
+
+
+
+
+
+
+
+
+
+
+Initial: Hypervolume (random, qHVKG, qNEHVI) = (89.34, 89.34, 89.34).
+
+
+
+
+
+
+
+
+

Plot the cost vs inference regret

The plot below shows the a common metric of multi-objective optimization performance, the log hypervolume difference: the log difference between the hypervolume of the true pareto front and the hypervolume of the inferred pareto set of designs identified by each algorithm. The log hypervolume difference is plotted cover cost. This is also known as inference regret.

+

The plot shows that HVKG identifies the Pareto optimal designs much faster than $q$NEHVI, and Sobol.

+
+
+
+
+
+
In [21]:
+
+
+
from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+
+log_hv_difference_hvkg = np.log10(problem.max_hv - np.asarray(hvs_hvkg))
+log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi))
+log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random))
+
+fig, ax = plt.subplots(1, 1, figsize=(8, 6))
+running_cost_random = np.cumsum(cost_random.cpu().numpy()[N_INIT-1:])
+running_cost_qnehvi = np.cumsum(cost_qnehvi.cpu().numpy()[N_INIT-1:])
+running_cost_hvkg = np.cumsum(cost_hvkg.cpu().numpy()[N_INIT-1:])
+ax.errorbar(
+    running_cost_random,
+    log_hv_difference_rnd[: len(running_cost_random)],
+    label="Sobol",
+    linewidth=1.5,
+    ls="--",
+    marker="s",
+)
+ax.errorbar(
+    running_cost_qnehvi,
+    log_hv_difference_qnehvi[: len(running_cost_qnehvi)],
+    label="qNEHVI",
+    linewidth=1.5,
+    ls="--",
+    marker="o"
+)
+ax.errorbar(
+    running_cost_hvkg,
+    log_hv_difference_hvkg[: len(running_cost_hvkg)],
+    label="HVKG",
+    linewidth=1.5,
+    ls="--",
+    marker="d"
+)
+ax.set(
+    xlabel="Cost",
+    ylabel="Log Hypervolume Difference",
+)
+ax.legend(loc="upper right")
+
+
+
+
+
+
+
+
Out[21]:
+
+
<matplotlib.legend.Legend at 0x7f0b089d09d0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/discrete_multi_fidelity_bo.html b/website-old/_tutorials/discrete_multi_fidelity_bo.html new file mode 100644 index 0000000000..8691c9b674 --- /dev/null +++ b/website-old/_tutorials/discrete_multi_fidelity_bo.html @@ -0,0 +1,585 @@ + + + +
+
+
+
+

Multi-Fidelity BO with Discrete Fidelities using KG

In this tutorial, we show how to do multi-fidelity BO with discrete fidelities based on [1], where each fidelity is a different "information source." This tutorial uses the same setup as the continuous multi-fidelity BO tutorial, except with discrete fidelity parameters that are interpreted as multiple information sources.

+

We use a GP model with a single task that models the design and fidelity parameters jointly. In some cases, where there is not a natural ordering in the fidelity space, it may be more appropriate to use a multi-task model (with, say, an ICM kernel). We will provide a tutorial once this functionality is in place.

+

[1] M. Poloczek, J. Wang, P.I. Frazier. Multi-Information Source Optimization. NeurIPS, 2017

+

[2] J. Wu, S. Toscano-Palmerin, P.I. Frazier, A.G. Wilson. Practical Multi-fidelity Bayesian Optimization for Hyperparameter Tuning. Conference on Uncertainty in Artificial Intelligence (UAI), 2019

+
+
+
+
+
+
+

Set dtype and device

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import torch
+
+
+tkwargs = {
+    "dtype": torch.double,
+    "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"),
+}
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Problem setup

We'll consider the Augmented Hartmann multi-fidelity synthetic test problem. This function is a version of the Hartmann6 test function with an additional dimension representing the fidelity parameter; details are in [2]. The function takes the form $f(x,s)$ where $x \in [0,1]^6$ and $s \in \{0.5, 0.75, 1\}$. The target fidelity is 1.0, which means that our goal is to solve $\max_x f(x,1.0)$ by making use of cheaper evaluations $f(x,s)$ for $s \in \{0.5, 0.75\}$. In this example, we'll assume that the cost function takes the form $5.0 + s$, illustrating a situation where the fixed cost is $5.0$.

+
+
+
+
+
+
In [2]:
+
+
+
from botorch.test_functions.multi_fidelity import AugmentedHartmann
+
+
+problem = AugmentedHartmann(negate=True).to(**tkwargs)
+fidelities = torch.tensor([0.5, 0.75, 1.0], **tkwargs)
+
+
+
+
+
+
+
+
+

Model initialization

We use a SingleTaskMultiFidelityGP as the surrogate model, which uses a kernel from [2] that is well-suited for multi-fidelity applications. The SingleTaskMultiFidelityGP models the design and fidelity parameters jointly, so its domain is $[0,1]^7$.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP
+from botorch.models.transforms.outcome import Standardize
+from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
+
+
+def generate_initial_data(n=16):
+    # generate training data
+    train_x = torch.rand(n, 6, **tkwargs)
+    train_f = fidelities[torch.randint(3, (n, 1))]
+    train_x_full = torch.cat((train_x, train_f), dim=1)
+    train_obj = problem(train_x_full).unsqueeze(-1)  # add output dimension
+    return train_x_full, train_obj
+
+
+def initialize_model(train_x, train_obj):
+    # define a surrogate model suited for a "training data"-like fidelity parameter
+    # in dimension 6, as in [2]
+    model = SingleTaskMultiFidelityGP(
+        train_x, train_obj, outcome_transform=Standardize(m=1), data_fidelities=[6]
+    )
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    return mll, model
+
+
+
+
+
+
+
+
+

Define a helper function to construct the MFKG acquisition function

The helper function illustrates how one can initialize an $q$MFKG acquisition function. In this example, we assume that the affine cost is known. We then use the notion of a CostAwareUtility in BoTorch to scalarize the "competing objectives" of information gain and cost. The MFKG acquisition function optimizes the ratio of information gain to cost, which is captured by the InverseCostWeightedUtility.

+

In order for MFKG to evaluate the information gain, it uses the model to predict the function value at the highest fidelity after conditioning on the observation. This is handled by the project argument, which specifies how to transform a tensor X to its target fidelity. We use a default helper function called project_to_target_fidelity to achieve this.

+

An important point to keep in mind: in the case of standard KG, one can ignore the current value and simply optimize the expected maximum posterior mean of the next stage. However, for MFKG, since the goal is optimize information gain per cost, it is important to first compute the current value (i.e., maximum of the posterior mean at the target fidelity). To accomplish this, we use a FixedFeatureAcquisitionFunction on top of a PosteriorMean.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch import fit_gpytorch_mll
+from botorch.models.cost import AffineFidelityCostModel
+from botorch.acquisition.cost_aware import InverseCostWeightedUtility
+from botorch.acquisition import PosteriorMean
+from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient
+from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction
+from botorch.optim.optimize import optimize_acqf
+from botorch.acquisition.utils import project_to_target_fidelity
+
+bounds = torch.tensor([[0.0] * problem.dim, [1.0] * problem.dim], **tkwargs)
+target_fidelities = {6: 1.0}
+
+cost_model = AffineFidelityCostModel(fidelity_weights={6: 1.0}, fixed_cost=5.0)
+cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)
+
+
+def project(X):
+    return project_to_target_fidelity(X=X, target_fidelities=target_fidelities)
+
+
+def get_mfkg(model):
+
+    curr_val_acqf = FixedFeatureAcquisitionFunction(
+        acq_function=PosteriorMean(model),
+        d=7,
+        columns=[6],
+        values=[1],
+    )
+
+    _, current_value = optimize_acqf(
+        acq_function=curr_val_acqf,
+        bounds=bounds[:, :-1],
+        q=1,
+        num_restarts=10 if not SMOKE_TEST else 2,
+        raw_samples=1024 if not SMOKE_TEST else 4,
+        options={"batch_limit": 10, "maxiter": 200},
+    )
+
+    return qMultiFidelityKnowledgeGradient(
+        model=model,
+        num_fantasies=128 if not SMOKE_TEST else 2,
+        current_value=current_value,
+        cost_aware_utility=cost_aware_utility,
+        project=project,
+    )
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step

This helper function optimizes the acquisition function and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. The function optimize_acqf_mixed sequentially optimizes the acquisition function over $x$ for each value of the fidelity $s \in \{0, 0.5, 1.0\}$.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.optim.optimize import optimize_acqf_mixed
+
+
+torch.set_printoptions(precision=3, sci_mode=False)
+
+NUM_RESTARTS = 5 if not SMOKE_TEST else 2
+RAW_SAMPLES = 128 if not SMOKE_TEST else 4
+BATCH_SIZE = 4
+
+
+def optimize_mfkg_and_get_observation(mfkg_acqf):
+    """Optimizes MFKG and returns a new candidate, observation, and cost."""
+
+    # generate new candidates
+    candidates, _ = optimize_acqf_mixed(
+        acq_function=mfkg_acqf,
+        bounds=bounds,
+        fixed_features_list=[{6: 0.5}, {6: 0.75}, {6: 1.0}],
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+        # batch_initial_conditions=X_init,
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+
+    # observe new values
+    cost = cost_model(candidates).sum()
+    new_x = candidates.detach()
+    new_obj = problem(new_x).unsqueeze(-1)
+    print(f"candidates:\n{new_x}\n")
+    print(f"observations:\n{new_obj}\n\n")
+    return new_x, new_obj, cost
+
+
+
+
+
+
+
+
+

Perform a few steps of multi-fidelity BO

First, let's generate some initial random data and fit a surrogate model.

+
+
+
+
+
+
In [6]:
+
+
+
train_x, train_obj = generate_initial_data(n=16)
+
+
+
+
+
+
+
+
+

We can now use the helper functions above to run a few iterations of BO.

+
+
+
+
+
+
In [7]:
+
+
+
cumulative_cost = 0.0
+N_ITER = 3 if not SMOKE_TEST else 1
+
+for i in range(N_ITER):
+    mll, model = initialize_model(train_x, train_obj)
+    fit_gpytorch_mll(mll)
+    mfkg_acqf = get_mfkg(model)
+    new_x, new_obj, cost = optimize_mfkg_and_get_observation(mfkg_acqf)
+    train_x = torch.cat([train_x, new_x])
+    train_obj = torch.cat([train_obj, new_obj])
+    cumulative_cost += cost
+
+
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.199, 0.101, 0.436, 0.433, 0.197, 0.421, 0.750],
+        [0.142, 0.274, 0.308, 0.413, 0.298, 0.570, 0.750],
+        [0.097, 0.141, 0.417, 0.453, 0.477, 0.536, 0.500],
+        [0.123, 0.022, 0.328, 0.430, 0.270, 0.689, 0.500]], device='cuda:0',
+       dtype=torch.float64)
+
+observations:
+tensor([[1.369],
+        [2.308],
+        [1.404],
+        [2.297]], device='cuda:0', dtype=torch.float64)
+
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.276, 0.159, 0.231, 0.462, 0.295, 0.633, 1.000],
+        [0.213, 0.163, 0.297, 0.336, 0.276, 0.671, 0.750],
+        [0.029, 0.235, 0.236, 0.405, 0.290, 0.709, 0.500],
+        [0.159, 0.205, 0.360, 0.397, 0.361, 0.717, 1.000]], device='cuda:0',
+       dtype=torch.float64)
+
+observations:
+tensor([[2.170],
+        [2.984],
+        [2.197],
+        [2.588]], device='cuda:0', dtype=torch.float64)
+
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.268, 0.224, 0.340, 0.334, 0.230, 0.751, 0.500],
+        [0.263, 0.181, 0.242, 0.307, 0.335, 0.735, 0.500],
+        [0.166, 0.163, 0.345, 0.260, 0.278, 0.711, 0.500],
+        [0.257, 0.238, 0.337, 0.311, 0.316, 0.639, 0.750]], device='cuda:0',
+       dtype=torch.float64)
+
+observations:
+tensor([[2.565],
+        [2.818],
+        [3.036],
+        [3.036]], device='cuda:0', dtype=torch.float64)
+
+
+
+
+
+
+
+
+
+
+
+

Make a final recommendation

In multi-fidelity BO, there are usually fewer observations of the function at the target fidelity, so it is important to use a recommendation function that uses the correct fidelity. Here, we maximize the posterior mean with the fidelity dimension fixed to the target fidelity of 1.0.

+
+
+
+
+
+
In [8]:
+
+
+
def get_recommendation(model):
+    rec_acqf = FixedFeatureAcquisitionFunction(
+        acq_function=PosteriorMean(model),
+        d=7,
+        columns=[6],
+        values=[1],
+    )
+
+    final_rec, _ = optimize_acqf(
+        acq_function=rec_acqf,
+        bounds=bounds[:, :-1],
+        q=1,
+        num_restarts=10,
+        raw_samples=512,
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+
+    final_rec = rec_acqf._construct_X_full(final_rec)
+
+    objective_value = problem(final_rec)
+    print(f"recommended point:\n{final_rec}\n\nobjective value:\n{objective_value}")
+    return final_rec
+
+
+
+
+
+
+
+
In [9]:
+
+
+
final_rec = get_recommendation(model)
+print(f"\ntotal cost: {cumulative_cost}\n")
+
+
+
+
+
+
+
+
+
+
recommended point:
+tensor([[0.213, 0.164, 0.302, 0.327, 0.283, 0.689, 1.000]], device='cuda:0',
+       dtype=torch.float64)
+
+objective value:
+tensor([3.021], device='cuda:0', dtype=torch.float64)
+
+total cost: 68.0
+
+
+
+
+
+
+
+
+
+
+

Comparison to standard EI (always use target fidelity)

Let's now repeat the same steps using a standard EI acquisition function (note that this is not a rigorous comparison as we are only looking at one trial in order to keep computational requirements low).

+
+
+
+
+
+
In [10]:
+
+
+
from botorch.acquisition import qExpectedImprovement
+
+
+def get_ei(model, best_f):
+
+    return FixedFeatureAcquisitionFunction(
+        acq_function=qExpectedImprovement(model=model, best_f=best_f),
+        d=7,
+        columns=[6],
+        values=[1],
+    )
+
+
+def optimize_ei_and_get_observation(ei_acqf):
+    """Optimizes EI and returns a new candidate, observation, and cost."""
+
+    candidates, _ = optimize_acqf(
+        acq_function=ei_acqf,
+        bounds=bounds[:, :-1],
+        q=BATCH_SIZE,
+        num_restarts=10,
+        raw_samples=512,
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+
+    # add the fidelity parameter
+    candidates = ei_acqf._construct_X_full(candidates)
+
+    # observe new values
+    cost = cost_model(candidates).sum()
+    new_x = candidates.detach()
+    new_obj = problem(new_x).unsqueeze(-1)
+    print(f"candidates:\n{new_x}\n")
+    print(f"observations:\n{new_obj}\n\n")
+    return new_x, new_obj, cost
+
+
+
+
+
+
+
+
In [11]:
+
+
+
cumulative_cost = 0.0
+
+train_x, train_obj = generate_initial_data(n=16)
+
+for _ in range(N_ITER):
+    mll, model = initialize_model(train_x, train_obj)
+    fit_gpytorch_mll(mll)
+    ei_acqf = get_ei(model, best_f=train_obj.max())
+    new_x, new_obj, cost = optimize_ei_and_get_observation(ei_acqf)
+    train_x = torch.cat([train_x, new_x])
+    train_obj = torch.cat([train_obj, new_obj])
+    cumulative_cost += cost
+
+
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.247, 0.687, 0.581, 0.760, 0.093, 0.132, 1.000],
+        [0.319, 0.850, 0.639, 0.865, 0.000, 0.120, 1.000],
+        [0.349, 0.666, 0.555, 0.986, 0.000, 0.126, 1.000],
+        [0.297, 0.792, 0.450, 0.889, 0.034, 0.028, 1.000]], device='cuda:0',
+       dtype=torch.float64)
+
+observations:
+tensor([[0.973],
+        [1.091],
+        [0.340],
+        [0.902]], device='cuda:0', dtype=torch.float64)
+
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.194, 0.858, 0.622, 0.799, 0.000, 0.095, 1.000],
+        [0.341, 0.854, 0.590, 0.767, 0.000, 0.085, 1.000],
+        [0.999, 0.439, 0.828, 0.975, 0.633, 0.176, 1.000],
+        [0.296, 0.859, 0.677, 0.806, 0.119, 0.054, 1.000]], device='cuda:0',
+       dtype=torch.float64)
+
+observations:
+tensor([[    0.862],
+        [    1.975],
+        [    0.000],
+        [    1.514]], device='cuda:0', dtype=torch.float64)
+
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/fbcode/buck-out/opt/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:40: NumericalWarning:
+
+A not p.d., added jitter of 1.0e-08 to the diagonal
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.360, 0.891, 0.588, 0.749, 0.019, 0.036, 1.000],
+        [0.049, 0.894, 0.345, 0.210, 0.482, 0.463, 1.000],
+        [0.398, 0.970, 0.504, 0.213, 0.814, 0.724, 1.000],
+        [0.817, 0.879, 0.691, 0.842, 0.455, 0.937, 1.000]], device='cuda:0',
+       dtype=torch.float64)
+
+observations:
+tensor([[2.271],
+        [0.216],
+        [0.055],
+        [0.036]], device='cuda:0', dtype=torch.float64)
+
+
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
final_rec = get_recommendation(model)
+print(f"\ntotal cost: {cumulative_cost}\n")
+
+
+
+
+
+
+
+
+
+
recommended point:
+tensor([[0.352, 0.874, 0.589, 0.756, 0.008, 0.060, 1.000]], device='cuda:0',
+       dtype=torch.float64)
+
+objective value:
+tensor([2.166], device='cuda:0', dtype=torch.float64)
+
+total cost: 72.0
+
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/fit_model_with_torch_optimizer.html b/website-old/_tutorials/fit_model_with_torch_optimizer.html new file mode 100644 index 0000000000..fb43c9aee5 --- /dev/null +++ b/website-old/_tutorials/fit_model_with_torch_optimizer.html @@ -0,0 +1,306 @@ + + + +
+
+
+
+

Fitting models in BoTorch with a torch.optim.Optimizer

BoTorch provides a convenient botorch.fit.fit_gpytorch_mll function with sensible defaults that work on most basic models, including those that botorch ships with. Internally, this function uses L-BFGS-B to fit the parameters. However, in more advanced use cases you may need or want to implement your own model fitting logic.

+

This tutorial allows you to customize model fitting to your needs using the familiar PyTorch-style model fitting loop.

+

This tutorial is adapted from GPyTorch's Simple GP Regression Tutorial and has very few changes because the out-of-the box models that BoTorch provides are GPyTorch models; in fact, they are proper subclasses that add the botorch.models.Model API functions.

+
+
+
+
+
+
In [1]:
+
+
+
import math
+
+import torch
+
+# use a GPU if available
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+dtype = torch.float
+
+
+
+
+
+
+
+
+

Set up function to model

In this tutorial we will model a simple sinusoidal function with i.i.d. Gaussian noise:

+

$$y = \sin(2\pi x) + \epsilon, ~\epsilon \sim \mathcal N(0, 0.15)$$

+
+
+
+
+
+
+

Initialize training data

+
+
+
+
+
+
In [3]:
+
+
+
# use regular spaced points on the interval [0, 1]
+train_X = torch.linspace(0, 1, 15, dtype=dtype, device=device)
+# training data needs to be explicitly multi-dimensional
+train_X = train_X.unsqueeze(1)
+
+# sample observed values and add some synthetic noise
+train_Y = torch.sin(train_X * (2 * math.pi)) + 0.15 * torch.randn_like(train_X)
+
+
+
+
+
+
+
+
+

Initialize the model

We will model the function using a SingleTaskGP, which by default uses a GaussianLikelihood and infers the unknown noise level.

+

The default optimizer for the SingleTaskGP is L-BFGS-B, which takes as input explicit bounds on the noise parameter. However, the torch optimizers don't support parameter bounds as input. To use the torch optimizers, then, we'll need to manually register a constraint on the noise level. When registering a constraint, the softplus transform is applied by default, enabling us to enforce a lower bound on the noise.

+

Note: Without manual registration, the model itself does not apply any constraints, due to the interaction between constraints and transforms. Although the SingleTaskGP constructor does in fact define a constraint, the constructor sets transform=None, which means that the constraint is not enforced. See the GPyTorch constraints module for additional information.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.models import SingleTaskGP
+from gpytorch.constraints import GreaterThan
+
+
+model = SingleTaskGP(train_X=train_X, train_Y=train_Y)
+model.likelihood.noise_covar.register_constraint("raw_noise", GreaterThan(1e-5))
+
+
+
+
+
+
+
+
+

Define marginal log likelihood

We will jointly optimize the kernel hyperparameters and the likelihood's noise parameter, by minimizing the negative gpytorch.mlls.ExactMarginalLogLikelihood (our loss function).

+
+
+
+
+
+
In [5]:
+
+
+
from gpytorch.mlls import ExactMarginalLogLikelihood
+
+mll = ExactMarginalLogLikelihood(likelihood=model.likelihood, model=model)
+# set mll and all submodules to the specified dtype and device
+mll = mll.to(train_X)
+
+
+
+
+
+
+
+
+

Define optimizer and specify parameters to optimize

We will use stochastic gradient descent (torch.optim.SGD) to optimize the kernel hyperparameters and the noise level. In this example, we will use a simple fixed learning rate of 0.1, but in practice the learning rate may need to be adjusted.

+

Notes:

+
    +
  • As the GaussianLikelihood module is a of child (submodule) of the SingleTaskGP module, model.parameters() will also include the noise level of the GaussianLikelihood.
  • +
  • A subset of the parameters could be passed to the optimizer to tune those parameters, while leaving the other parameters fixed.
  • +
+
+
+
+
+
+
In [6]:
+
+
+
from torch.optim import SGD
+
+optimizer = SGD([{"params": model.parameters()}], lr=0.025)
+
+
+
+
+
+
+
+
+

Fit model hyperparameters and noise level

Now we are ready to write our optimization loop. We will perform 150 epochs of stochastic gradient descent using our entire training set.

+
+
+
+
+
+
In [7]:
+
+
+
NUM_EPOCHS = 150
+
+model.train()
+
+for epoch in range(NUM_EPOCHS):
+    # clear gradients
+    optimizer.zero_grad()
+    # forward pass through the model to obtain the output MultivariateNormal
+    output = model(train_X)
+    # Compute negative marginal log likelihood
+    loss = -mll(output, model.train_targets)
+    # back prop gradients
+    loss.backward()
+    # print every 10 iterations
+    if (epoch + 1) % 10 == 0:
+        print(
+            f"Epoch {epoch+1:>3}/{NUM_EPOCHS} - Loss: {loss.item():>4.3f} "
+            f"lengthscale: {model.covar_module.lengthscale.item():>4.3f} "
+            f"noise: {model.likelihood.noise.item():>4.3f}"
+        )
+    optimizer.step()
+
+
+
+
+
+
+
+
+
+
Epoch  10/150 - Loss: 1.966 lengthscale: 0.645 noise: 2.005
+Epoch  20/150 - Loss: 1.930 lengthscale: 0.599 noise: 1.868
+Epoch  30/150 - Loss: 1.894 lengthscale: 0.560 noise: 1.730
+Epoch  40/150 - Loss: 1.857 lengthscale: 0.527 noise: 1.590
+Epoch  50/150 - Loss: 1.819 lengthscale: 0.497 noise: 1.449
+Epoch  60/150 - Loss: 1.779 lengthscale: 0.471 noise: 1.310
+Epoch  70/150 - Loss: 1.737 lengthscale: 0.448 noise: 1.172
+Epoch  80/150 - Loss: 1.692 lengthscale: 0.427 noise: 1.038
+Epoch  90/150 - Loss: 1.645 lengthscale: 0.407 noise: 0.908
+Epoch 100/150 - Loss: 1.595 lengthscale: 0.389 noise: 0.785
+Epoch 110/150 - Loss: 1.542 lengthscale: 0.372 noise: 0.671
+Epoch 120/150 - Loss: 1.487 lengthscale: 0.355 noise: 0.566
+Epoch 130/150 - Loss: 1.429 lengthscale: 0.341 noise: 0.471
+Epoch 140/150 - Loss: 1.370 lengthscale: 0.328 noise: 0.389
+Epoch 150/150 - Loss: 1.311 lengthscale: 0.317 noise: 0.318
+
+
+
+
+
+
+
+
+
+

Compute posterior over test points and plot fit

We plot the posterior mean and the 2 standard deviations from the mean.

+

Note: The posterior below is the posterior prediction for the underlying sinusoidal function, i.e., it does not include the observation noise. If we wanted to get the posterior prediction for the observations (including the predicted observation noise), we would instead use posterior = posterior = model.posterior(test_X, observation_noise=True).

+
+
+
+
+
+
In [8]:
+
+
+
# set model (and likelihood)
+model.eval()
+
+
+
+
+
+
+
+
In [9]:
+
+
+
from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+# Initialize plot
+f, ax = plt.subplots(1, 1, figsize=(6, 4))
+# test model on 101 regular spaced points on the interval [0, 1]
+test_X = torch.linspace(0, 1, 101, dtype=dtype, device=device)
+# no need for gradients
+with torch.no_grad():
+    # compute posterior
+    posterior = model.posterior(test_X)
+    # Get upper and lower confidence bounds (2 standard deviations from the mean)
+    lower, upper = posterior.mvn.confidence_region()
+    # Plot training points as black stars
+    ax.plot(train_X.cpu().numpy(), train_Y.cpu().numpy(), "k*")
+    # Plot posterior means as blue line
+    ax.plot(test_X.cpu().numpy(), posterior.mean.cpu().numpy(), "b")
+    # Shade between the lower and upper confidence bounds
+    ax.fill_between(
+        test_X.cpu().numpy(), lower.cpu().numpy(), upper.cpu().numpy(), alpha=0.5
+    )
+ax.legend(["Observed Data", "Mean", "Confidence"])
+plt.tight_layout()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Interfacing with Ax

It is simple to package up a custom optimizer loop like the one above and use it within Ax. As described in the Using BoTorch with Ax tutorial, this requires defining a custom model_constructor callable that can then be passed to the get_botorch factory function.

+
+
+
+
+
+
In [11]:
+
+
+
def _get_and_fit_model(Xs, Ys, **kwargs):
+
+    train_X, train_Y = Xs[0], Ys[0]
+    model = SingleTaskGP(train_X=train_X, train_Y=train_Y)
+    mll = ExactMarginalLogLikelihood(model.likelihood, model).to(train_X)
+    model.train()
+
+    optimizer = SGD([{"params": model.parameters()}], lr=kwargs.get("lr"))
+    for epoch in range(kwargs.get("epochs")):
+        optimizer.zero_grad()
+        output = model(train_X)
+        loss = -mll(output, model.train_targets)
+        loss.backward()
+        optimizer.step()
+
+    return model
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/ibnn_bo.html b/website-old/_tutorials/ibnn_bo.html new file mode 100644 index 0000000000..2ab92192cb --- /dev/null +++ b/website-old/_tutorials/ibnn_bo.html @@ -0,0 +1,526 @@ + + + +
+
+
+
+

Infinite-Width Bayesian Neural Networks for Bayesian Optimization

In this tutorial, we present an overview of infinite-width Bayesian neural networks (I-BNNs) [1, 2] and show how to use them as surrogate models for Bayesian optimization (BO).

+

Consider an fully connected neural network with $L$ hidden layers, parameter weights drawn from $\mathcal{N(0, \sigma_w)}$, bias terms drawn from $\mathcal{N(0, \sigma_b)}$, and nonlinearity $\phi$. In the infinite-width limit, the output of this network is exactly equivalent to $\mathcal{GP}(\mu, K^L)$. By the central limit theorem, we find $\mu(x) = 0$, and we can also recursively define the covariance function as +$$K^0(x, x')=\sigma_b^2+\sigma_w^2\frac{x \cdot x'}{d_\text{input}}\qquad K^l(x, x')=\sigma_b^2+\sigma_w^2F_\phi(K^{l-1}(x, x'), K^{l-1}(x, x), K^{l-1}(x', x'))$$ +where $F_\phi$ is a deterministic function based on the activation function $\phi$.

+

We will refer to this kernel as the "I-BNN kernel". Unlike many popular GP kernels, I-BNN covariance function is not based on Euclidean distance, allowing the GP to represent nonstationary functions. This is advantageous for many settings of Bayesian optimization, since the function we want to optimize may not have similar behavior throughout the entire input space. Furthermore, I-BNNs have been shown to work particularly well for BO problems with high-dimensional inputs [3].

+

BoTorch has an implementation of I-BNNs with ReLU activations: InfiniteWidthBNNKernel.

+

[1] Y. Cho, and L. Saul. Kernel Methods for Deep Learning. Advances in Neural Information Processing Systems 22, 2009.
+[2] J. Lee, Y. Bahri, R. Novak, S. Schoenholz, J. Pennington, and J. Dickstein. Deep Neural Networks as Gaussian Processes. International Conference on Learning Representations 2018.
+[3] Y.L. Li, T.G.J. Rudner, A.G. Wilson. A Study of Bayesian Neural Network Surrogates for Bayesian Optimization. International Conference on Learning Representations 2024.

+
+
+
+
+
+
In [13]:
+
+
+
import os
+import warnings
+
+import matplotlib.pyplot as plt
+import torch
+from torch import nn
+
+from gpytorch.kernels import MaternKernel, RBFKernel, ScaleKernel
+from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
+
+from botorch import manual_seed
+from botorch.acquisition import LogExpectedImprovement
+from botorch.fit import fit_gpytorch_mll
+from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.kernels import InfiniteWidthBNNKernel
+from botorch.models.transforms.outcome import Standardize
+from botorch.optim.optimize import optimize_acqf
+from botorch.utils.sampling import draw_sobol_samples
+
+warnings.filterwarnings('ignore')
+
+%matplotlib inline
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+tkwargs = {"device": device, "dtype": dtype}
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

I-BNN Function Draws

We start by visualizing the posteriors of an I-BNN. Here, we define a toy function and draw five initial function evaluations.

+
+
+
+
+
+
In [14]:
+
+
+
torch.manual_seed(1111)
+def f(x):
+    x = -(x - 0.15)
+    return torch.sin(x * (2 * torch.pi)) + torch.sin(x * (2 * torch.pi) * 2)
+
+x = torch.linspace(0, 1, 100).to(dtype).unsqueeze(-1)
+true_y = f(x)
+
+train_x = torch.rand(5, 1).to(**tkwargs)
+train_y = f(train_x)
+
+# visualize the function and the training data
+plt.figure(figsize=(4, 3))
+plt.plot(x.cpu(), true_y.cpu(), linewidth=2)
+plt.scatter(train_x.cpu(), train_y.cpu(), color="black")
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Initializing the Model: We now define two versions of the I-BNN, constructed using a GP with an InfiniteWidthBNNKernel. One version has fixed user-specified values for $\sigma^2_w$ and $\sigma^2_b$, and the other uses the marginal log likelihood to optimize these hyperparameters.

+
+
+
+
+
+
In [15]:
+
+
+
# Function queries are not noisy
+train_Yvar = torch.full_like(train_y, 1e-8)
+
+# I-BNN with fixed hyperparameters
+ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device)
+ibnn_kernel.weight_var = 10.0
+ibnn_kernel.bias_var = 5.0
+model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel)
+model.eval()
+
+# I-BNN with optimized hyperparameters
+model_optimize = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=InfiniteWidthBNNKernel(depth=3))
+mll = ExactMarginalLogLikelihood(model_optimize.likelihood, model_optimize)
+fit_gpytorch_mll(mll)
+model_optimize.eval()
+
+# Default GP with Matern for comparison
+model_matern = SingleTaskGP(train_x, train_y, train_Yvar)
+mll_matern = ExactMarginalLogLikelihood(model_matern.likelihood, model_matern)
+fit_gpytorch_mll(mll_matern)
+model_matern.eval();
+
+
+
+
+
+
+
+
+

Visualizating the Posterior:

+
+
+
+
+
+
In [16]:
+
+
+
def plot_posterior(ax, model, n_draws=5):
+    with torch.no_grad():
+        ax.plot(x.cpu(), true_y.cpu(), linewidth=2, color="black", label="True Objective", linestyle="--")
+        ax.scatter(train_x.cpu(), train_y.cpu(), color="black", s=80, label="Observations")
+
+        test_x = torch.linspace(0, 1, 100).to(**tkwargs)
+        pred_f = model(test_x)
+
+        ax.plot(test_x.cpu(), pred_f.mean.cpu(), linewidth=2, label="Mean")
+        lower, upper = pred_f.confidence_region()
+        ax.fill_between(test_x.cpu(), lower.cpu(), upper.cpu(), alpha=0.2, label=r'$\mu \pm 2\sigma$')
+
+        for i in range(n_draws):
+            if i == 0:
+                ax.plot(test_x.cpu(), pred_f.sample().cpu(), color="green", linewidth=0.5, label="Function Draw")
+            else:
+                ax.plot(test_x.cpu(), pred_f.sample().cpu(), color="green", linewidth=0.5)
+
+
+
+
+
+
+
+
In [17]:
+
+
+
fig, axs = plt.subplots(1, 3, figsize=(18, 5))
+
+plot_posterior(axs[0], model)
+axs[0].set_title("I-BNN (Fixed Hypers)\nWeight Var: %.2f, Bias Var: %.2f" %
+                (model.covar_module.weight_var.item(), model.covar_module.bias_var.item()),
+                fontsize=20)
+axs[0].set_ylim(-7, 8)
+axs[0].legend()
+
+plot_posterior(axs[1], model_optimize)
+axs[1].set_title("I-BNN (Optimized Hypers)\nWeight Var: %.2f, Bias Var: %.2f" %
+                (model_optimize.covar_module.weight_var.item(), model_optimize.covar_module.bias_var.item()),
+                fontsize=20)
+axs[1].set_ylim(-7, 8)
+
+plot_posterior(axs[2], model_matern)
+axs[2].set_title("GP (Matern Kernel)\nLength Scale: %.2f" %
+                model_matern.covar_module.lengthscale.item(),
+                fontsize=20)
+axs[2].set_ylim(-7, 8)
+
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Similar to a Matern kernel, we see that the uncertainty decreases around queried points and increases as we move away. However, we find that the I-BNN function draws are more jagged compared to the Matern draws, and we also note that the uncertainty of an I-BNN towards the edges of the data increases rapidly.

+
+
+
+
+
+
+

Impact of Hyperparameters

The I-BNN has three hyperparameters: the number of hidden layers, the variance of the weights, and the variance of the bias terms. Here, we visualize how modifying these hyperparameters impacts the posterior.

+
+
+
+
+
+
In [18]:
+
+
+
fig, axs = plt.subplots(1, 4, figsize=(20, 4))
+
+for i, ax in enumerate(axs):
+    ibnn_kernel = InfiniteWidthBNNKernel(depth=(i+1), device=device)
+    ibnn_kernel.weight_var = 10.0
+    ibnn_kernel.bias_var = 2.0
+
+    model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval()
+    plot_posterior(ax, model, n_draws=5)
+    ax.set_title(f"Depth: {i+1}")
+    ax.set_ylim(-8, 8)
+    if i == 0:
+        ax.legend()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [19]:
+
+
+
fig, axs = plt.subplots(1, 4, figsize=(20, 4))
+
+for i, ax in enumerate(axs):
+    ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device)
+    ibnn_kernel.weight_var = (i+1) * 5
+    ibnn_kernel.bias_var = 2.0
+
+    model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval()
+    plot_posterior(ax, model, n_draws=5)
+    ax.set_title("Weight Var: %.1f" % ((i+1) * 5))
+    ax.set_ylim(-10, 10)
+    if i == 0:
+        ax.legend()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [20]:
+
+
+
fig, axs = plt.subplots(1, 4, figsize=(20, 4))
+
+for i, ax in enumerate(axs):
+    ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device)
+    ibnn_kernel.weight_var = 10.0
+    ibnn_kernel.bias_var = (i + 1) * 5
+
+    model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval()
+    plot_posterior(ax, model, n_draws=5)
+    ax.set_title("Bias Var: %.1f" % ((i+1) * 5))
+    ax.set_ylim(-5, 6)
+    if i == 0:
+        ax.legend()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

I-BNNs for Bayesian Optimization

We will now use I-BNNs as the surrogate model for a high-dimensional BO problem.

+
+
+
+
+
+
+

Define High-dimensional Function and BO Setup: We will optimize the output of a multilayer perceptron (MLP) with 2 hidden layers, 50 nodes per layer, and ReLU nonlinearities.

+
+
+
+
+
+
In [21]:
+
+
+
class MLP(nn.Module):
+    def __init__(self, input_dims):
+        super().__init__()
+        self.layers = nn.Sequential(
+            nn.Linear(input_dims, 50, dtype=torch.float64),
+            nn.ReLU(),
+            nn.Linear(50, 50, dtype=torch.float64),
+            nn.ReLU(),
+            nn.Linear(50, 1, dtype=torch.float64)
+        )
+
+    def forward(self, x):
+        return self.layers(x)
+
+def create_f(input_dims, seed):
+    # create MLP with weights and biases sampled from N(0, 1)
+    with manual_seed(seed):
+        model = MLP(input_dims).to(**tkwargs)
+        params = torch.nn.utils.parameters_to_vector(model.parameters())
+        params = torch.randn_like(params, dtype=torch.float64)
+        torch.nn.utils.vector_to_parameters(params, model.parameters())
+
+    def f(x):
+        with torch.no_grad():
+            return model(x)
+
+    return f
+
+INPUT_DIMS = 200
+N_ITERATIONS = 100 if not SMOKE_TEST else 5
+N_INIT = 50 if not SMOKE_TEST else 2
+
+f = create_f(INPUT_DIMS, seed=1234)
+bounds = torch.stack([torch.zeros(INPUT_DIMS), torch.ones(INPUT_DIMS)]).to(**tkwargs)
+
+
+
+
+
+
+
+
+

Define BO functions: We use Sobol sampling to initialize the BO problem, and we use the Expected Improvement acquisition function.

+
+
+
+
+
+
In [22]:
+
+
+
from botorch.acquisition.analytic import ExpectedImprovement, LogExpectedImprovement
+
+def generate_initial_data(f, bounds, n, input_dims):
+    train_x = draw_sobol_samples(bounds=bounds, n=n, q=1).to(**tkwargs)
+    train_x = train_x.squeeze(-2) # remove batch dimension
+    train_y = f(train_x)
+    return train_x, train_y
+
+
+def gp_bo_loop(f, bounds, init_x, init_y, kernel, n_iterations, acqf_class, optimize_hypers=False):
+    train_x = init_x.clone()
+    train_y = init_y.clone()
+
+    for iteration in range(n_iterations):
+
+        # fit model to data
+        model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=1), covar_module=kernel)
+        if optimize_hypers:
+            mll = ExactMarginalLogLikelihood(model.likelihood, model)
+            fit_gpytorch_mll(mll)
+        model.eval()
+
+        # optimize acquisition function
+        candidate_x, acq_value = optimize_acqf(
+            acq_function=acqf_class(model, train_y.max()),
+            bounds=bounds,
+            q=1,
+            num_restarts=10,
+            raw_samples=200,
+        )
+        candidate_x = candidate_x.double()
+
+        # update training points
+        train_x = torch.cat([train_x, candidate_x])
+        train_y = torch.cat([train_y, f(candidate_x)])
+
+    return train_x, train_y
+
+
+
+
+
+
+
+
+

Compare I-BNN with GP with Matern kernel and RBF kernel: On this high-dimensional problem, the I-BNN significantly outperforms the standard Matern and RBF kernels and is able to find better rewards.

+
+
+
+
+
+
In [23]:
+
+
+
from functools import partial
+# define kernels
+ibnn_kernel = InfiniteWidthBNNKernel(2, device=device)
+ibnn_kernel.weight_var = 10.0
+ibnn_kernel.bias_var = 5.0
+ibnn_kernel = ScaleKernel(ibnn_kernel, device=device)
+
+matern_kernel = ScaleKernel(MaternKernel(), device=device)
+rbf_kernel = ScaleKernel(RBFKernel(), device=device)
+
+# initialize problem
+train_x, train_y = generate_initial_data(f, bounds, n=N_INIT, input_dims=INPUT_DIMS)
+
+# run BO loop
+acqf_classes = {"LogEI": LogExpectedImprovement}
+results = {}
+for acq_name, acqf_class in acqf_classes.items():
+    run_bo_with_acqf = partial(gp_bo_loop, f=f, bounds=bounds, init_x=train_x, init_y=train_y, acqf_class=acqf_class, n_iterations=N_ITERATIONS)
+    ibnn_x, ibnn_y = run_bo_with_acqf(kernel=ibnn_kernel, optimize_hypers=False)
+    matern_x, matern_y = run_bo_with_acqf(kernel=matern_kernel, optimize_hypers=True)
+    rbf_x, rbf_y = run_bo_with_acqf(kernel=rbf_kernel, optimize_hypers=True)
+    results[acq_name] = {
+        "BNN": (ibnn_x, ibnn_y),
+        "Matern": (matern_x, matern_y),
+        "RBF": (rbf_x, rbf_y),
+    }
+
+
+
+
+
+
+
+
In [24]:
+
+
+
import matplotlib
+def plot_cum_max(y, **kwargs):
+    cum_max = (torch.cummax(y, dim=0)[0]).cpu()
+    plt.plot(range(len(cum_max)), cum_max, **kwargs)
+
+plt.figure(figsize=(8, 6))
+
+colors = matplotlib.cm.get_cmap("tab10").colors
+linestyles = {"LogEI": "-"}
+for acq_name, res in results.items():
+    ls = linestyles[acq_name]
+    ibnn_y = res["BNN"][-1]
+    matern_y = res["Matern"][-1]
+    rbf_y = res["RBF"][-1]
+    plot_cum_max(ibnn_y[N_INIT-1:], label=f"I-BNN ({acq_name})", color=colors[0], ls=ls)
+    plot_cum_max(matern_y[N_INIT-1:], label=f"Matern ({acq_name})", color=colors[1], ls=ls)
+    plot_cum_max(rbf_y[N_INIT-1:], label=f"RBF ({acq_name})", color=colors[2], ls=ls)
+
+plt.xlabel("BO Iterations")
+plt.ylabel("Max Value")
+plt.title(f"{INPUT_DIMS}-d Problem")
+plt.legend()
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/information_theoretic_acquisition_functions.html b/website-old/_tutorials/information_theoretic_acquisition_functions.html new file mode 100644 index 0000000000..31e939e78f --- /dev/null +++ b/website-old/_tutorials/information_theoretic_acquisition_functions.html @@ -0,0 +1,632 @@ + + + +
+
+
+
+

Information-theoretic acquisition functions

+
+
+
+
+
+
+

This notebook illustrates the use of some information-theoretic acquisition functions in BoTorch for single and multi-objective optimization. We present a single-objective example in section 1 and a multi-objective example in section 2. Before introducing these examples, we present an overview on the different approaches and how they are estimated.

+
+
+
+
+
+
+

Notation

We consider the problem of maximizing a function $f: \mathbb{X} \rightarrow \mathbb{R}^M$. In the single-objective setting ($M=1$), the maximum is defined as usual with respect to the total ordering over the real numbers. In the multi-objective setting ($M>1$), the maximum is defined with respect to the Pareto partial ordering over vectors. By an abuse in notation, we denote the optimal set of inputs and outputs by

+

$$\mathbb{X}^* = \text{arg}\max_{\mathbf{x} \in \mathbb{X}} f(\mathbf{x}) \subseteq \mathbb{X} \quad \text{and} \quad \mathbb{Y}^* = f(\mathbb{X}^*) = \max_{\mathbf{x} \in \mathbb{X}} f(\mathbf{x}) \subset \mathbb{R}^M,$$

+

respectively for both the single and multi-objective setting. We denote the collection of optimal input-output pairs by $(\mathbb{X}^*, \mathbb{Y}^*)$.

+
+
+
+
+
+
+

Information-theoretic acquisition functions

Information-theoretic (IT) acquisition functions work by quantifying the utility of an input $\mathbf{x} \in \mathbb{X}$ based on how "informative" the corresponding observation $\mathbf{y} \in \mathbb{R}^M$ will be in learning more about the distribution of some statistic of the function $S(f)$. Here, we define the notion of information via the mutual information ($\text{MI}$):

+

\begin{equation} + \alpha^{\text{IT}}(\mathbf{x}|D_n) + = \text{MI}(\mathbf{y}; S(f)| \mathbf{x}, D_n) + = H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p(S(f)|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, S(f)]], +\end{equation}

+

where $D_n = \{(\mathbf{x}_t, \mathbf{y}_t)\}_{t=1,\dots,n}$ denotes the data set of sampled inputs and observations and the function $H$ denotes the differential entropy $H[p(\mathbf{x})] = - \int p(\mathbf{x}) \log(p(\mathbf{x})) d\mathbf{x}$. The main difference between existing information-theoretic acquisition functions in the literature is the choice of statistic $S$ and the modelling assumptions that are made in order to estimate the resulting acquisition function. In this notebook, we focus on three particular cases of information-theoretic acquisition functions:

+
+
+
+
+
+
+

Predictive Entropy Search (PES)

The PES acquisition function [1] considers the problem of learning more about the distribution of the optimal inputs: $S(f) = \mathbb{X}^*$.

+

\begin{equation} +\alpha^{\text{PES}}(\mathbf{x}|D_n) += \text{MI}(\mathbf{y}; \mathbb{X}^*| \mathbf{x}, D_n) += H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p(\mathbb{X}^*|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{X}^*)]]. +\end{equation}

+
+
+
+
+
+
+

Max-value Entropy Search (MES)

The MES acquisition function [2] considers the problem of learning more about the distribution of the optimal outputs: $S(f) = \mathbb{Y}^*$.

+

\begin{equation} +\alpha^{\text{MES}}(\mathbf{x}|D_n) += \text{MI}(\mathbf{y}; \mathbb{Y}^*| \mathbf{x}, D_n) += H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p(\mathbb{Y}^*|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{Y}^*)]]. +\end{equation}

+
+
+
+
+
+
+

Joint Entropy Search (JES)

The JES acquisition function [3] considers the problem of learning more about the distribution of the optimal inputs and outputs: $S(f) = (\mathbb{X}^*, \mathbb{Y}^*)$.

+

\begin{equation} +\alpha^{\text{JES}}(\mathbf{x}|D_n) += \text{MI}(\mathbf{y}; (\mathbb{X}^*, \mathbb{Y}^*)| \mathbf{x}, D_n) += H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p((\mathbb{X}^*, \mathbb{Y}^*)|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, (\mathbb{X}^*, \mathbb{Y}^*))]]. +\end{equation}

+
+
+
+
+
+
+

Estimation

In order to estimate the three acquistion functions listed above, we make two simplfying assumptions:

+

[Assumption 1] We assume an independent Gaussian process prior on each objective function.

+

[Assumption 2] We assume a Gaussian observation likelihood.

+
+
+
+
+
+
+

First term

Under the modelling assumptions, the first term in each of the acquisition functions is an entropy of a Gaussian random variable, which is analytically tractable.

+

Second term

The second term in each of the acquisition functions is an expectation of an entropy over an intractable distribution. The expectation can be estimated using Monte Carlo, whilst the entropy has to be approximated using different strategies such as moment-matching.

+

Monte Carlo. To sample from the distribution over the optimal points, we can first (approximately) sample a collection of posterior paths $f_j \sim p(f|D_n)$ and then optimize them to obtain the sample of optimal points $(\mathbb{X}^*_j, \mathbb{Y}^*_j)$ for $j=1,\dots,J$.

+

PES entropy estimate. In qPredictiveEntropySearch and qMultiObjectivePredictiveEntropySearch, we approximate the entropy term arising in PES using the expectation propagation strategy described in [4]. In particular, we first relax the global optimality condition:

+

\begin{align} + H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{X}^*)] + &\overset{(1)}{=} H[p(\mathbf{y}| \mathbf{x}, D_n, f(\mathbb{X}) \preceq f(\mathbb{X}^*))] + \\\\ + &\overset{(2)}{\leq} H[p(\mathbf{y}| \mathbf{x}, D_n, f(X_n \cup \{\mathbf{x}\}) \preceq f(\mathbb{X}^*))]. +\end{align}

+

(1) This statement follows from the observation that conditioning on the optimal points $\mathbb{X}^*$ is equivalent to knowing that all points lie below the objective values at the optimal inputs: $f(\mathbb{X}) \preceq f(\mathbb{X}^*)$.

+

(2) We replace the global optimality condition with the local optimality condition: $f(X_n \cup \{\mathbf{x}\}) \preceq f(\mathbb{X}^*)$, where $X_n = \{\mathbf{x}_t\}_{t=1,\dots,n}$. . The upper bound follows from the standard result that conditioning on more information only decreases the entropy: $H(A|B) \leq H(A)$ for any random variables $A$ and $B$.

+

We then estimate the resulting lower bound of the PES acquisition function by approximating the intractable distribution $p(\mathbf{y}| \mathbf{x}, D_n, f(X_n \cup \{\mathbf{x}\}) \preceq f(\mathbb{X}^*))$ with a product of Gaussian random variables, which is fitted via an iterative moment-matching procedure known as expectation propagation. The entropy of this resulting distribution can then be computed analytically.

+

MES and JES entropy estimate. In qLowerBoundMultiObjectiveMaxValueEntropySearch, qLowerBoundJointEntropySearch and qLowerBoundMultiObjectiveJointEntropySearch, we approximate the entropy term arising in MES and JES using the strategies described in [3]. These estimates rely on different upper bounds of the entropy term, which results in different lower bounds for the mutual information. These estimates are motivated by the following chain inequalities for the entropy in the JES expression:

+

\begin{align} + H[p(\mathbf{y}| \mathbf{x}, D_n, (\mathbb{X}^*, \mathbb{Y}^*))] + &\overset{(1)}{=} H[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbb{X}) \preceq \mathbb{Y}^*)] + \\\\ + &\overset{(2)}{\leq} H[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbf{x}) \preceq \mathbb{Y}^*)] + \\\\ + &\overset{(3)}{\leq} H[\mathcal{N}(\mathbf{y}| \mathbf{m}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))}, \mathbf{V}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))})] + \\\\ + &\overset{(4)}{\leq} H[\mathcal{N}(\mathbf{y}| \mathbf{m}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))}, \text{diag}(\mathbf{V}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))}))], +\end{align}

+

where

+

\begin{align} + \mathbf{m}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))} = \mathbb{E}[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbf{x}) \preceq \mathbb{Y}^*)] +\end{align}

+

\begin{align} + \mathbf{V}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))} = \mathbb{C}\text{ov}[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbf{x}) \preceq \mathbb{Y}^*)]. +\end{align}

+

(1) This statement follows from the observation that conditioning on the optimal points $(\mathbb{X}^*, \mathbb{Y}^*)$ is equivalent to knowing that $\mathbb{X}^*$ maps to $\mathbb{Y}^*$ and that all points lie below the Pareto front, $f(\mathbb{X}) \preceq f(\mathbb{X}^*) = \mathbb{Y}^*$.

+

(2) We replace the global optimality condition with the local optimality condition: $f(\mathbf{x}) \preceq \mathbb{Y}^*$. The upper bound follows from the standard result that conditioning on more information only decreases the entropy: $H(A|B) \leq H(A)$ for any random variables $A$ and $B$.

+

(3) We upper bound the entropy using the standard result that the multivariate Gaussian distribution has the maximum entropy over all distributions supported on $\mathbb{R}^M$ with the same first two moments.

+

(4) We upper bound the entropy by again using the standard result that conditioning on more information only decreases the entropy.

+

(Conditioning) A similar chain of inequalities can be obtained for the entropy in the MES term by replacing the augmented data set $D_n \cup (\mathbb{X}^*, \mathbb{Y}^*)$ with the original data set $D_n$. The only real difference between the JES and MES estimate is whether we condition on the extra samples $(\mathbb{X}^*_j, \mathbb{Y}^*_j)$ or not for $j=1,\dots,J$. As a result of this conditioning, the JES estimate can be more expensive than the MES estimate.

+

(Noiseless setting) When the observations are exact, $\mathbf{y} = f(\mathbf{x})$, then the entropy term in (2) can be computed exactly. By setting estimation_type="0", we use this estimate. In the setting where there is observation noise, the estimate also includes an ad-hoc correction which can be useful (more details in the appendix of [3]).

+

(Monte Carlo) The entropy term in (2) can be estimated using Monte Carlo because the distribution has a tractable density under the assumptions. By setting estimation_type="MC", we use this Monte Carlo estimate.

+

(Lower bound) The entropy term in (3) and (4) can be computed exactly. By setting estimation_type="LB", we use this lower bound estimate in (3). By setting estimation_type="LB2", we use lower bound estimate in (4).

+
+
+
+
+
+
+

Batch

For the batch setting, the first term is again analytically tractable. The second term can be estimated using Monte Carlo, whilst the entropy term again has to be estimated.

+

PES entropy estimate. In qPredictiveEntropySearch and qMultiObjectivePredictiveEntropySearch, the entropy term is again approximated using expectation propagation. In particular, we approximate $p(Y| X, D_n, f(X_n \cup X) \preceq f(\mathbb{X}^*))$ with a product of Gaussian random variables.

+

MES and JES entropy estimate In qLowerBoundMultiObjectiveMaxValueEntropySearch, qLowerBoundJointEntropySearch and qLowerBoundMultiObjectiveJointEntropySearch, we approximate a lower bound to the MES and JES acquisition function:

+

\begin{equation} +\alpha^{\text{LB-MES}}(X|D_n) += \text{MI}(Y; \mathbb{Y}^*| X, D_n) += H[p(Y|D_n)] - \sum_{\mathbf{x} \in X} \mathbb{E}_{p(\mathbb{Y}^*|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{Y}^*)]], +\end{equation}

+

\begin{equation} +\alpha^{\text{LB-JES}}(X|D_n) += \text{MI}(Y; (\mathbb{X}^*, \mathbb{Y}^*)| X, D_n) += H[p(Y|D_n)] - \sum_{\mathbf{x} \in X} \mathbb{E}_{p((\mathbb{X}^*, \mathbb{Y}^*)|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, (\mathbb{X}^*, \mathbb{Y}^*))]]. +\end{equation}

+

The advantage of these expressions is that it allows us to take advantage of the existing entropy estimates for the sequential setting.

+
+
+
+
+
+
+

References

+
+
+
+
+
+
+

[1] J.M. Hernández-Lobato, M.W. Hoffman and Z. Ghahramani, Predictive Entropy Search for Efficient Global Optimization of Black-box Functions, NeurIPS, 2014.

+

[2] Z. Wang and S. Jegelka, Max-value Entropy Search for Efficient Bayesian Optimization, ICML, 2017.

+

[3] B. Tu, A. Gandy, N. Kantas and B. Shafei, Joint Entropy Search for Multi-Objective Bayesian Optimization, NeurIPS, 2022.

+

[4] C. Hvarfner, F. Hutter and N. Nardi, Joint Entropy Search for Maximally-Informed Bayesian Optimization, NeurIPS, 2022.

+

[5] E. Garrido-Merchán and D. Hernández-Lobato, Predictive Entropy Search for Multi-objective Bayesian Optimization with Constraints, Neurocomputing, 2019.

+
+
+
+
+
+
+

1. Single-objective example

+
+
+
+
+
+
+

In this section, we present a simple example in one-dimension with one objective to illustrate the use of these acquisition functions. We first define the objective function.

+
+
+
+
+
+
In [ ]:
+
+
+
import os
+
+import matplotlib.pyplot as plt
+import numpy as np
+import torch
+from botorch.fit import fit_gpytorch_mll
+from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.utils.sampling import draw_sobol_samples
+from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+tkwargs = {"dtype": torch.double, "device": "cpu"}
+
+
+def f(x):
+    p1 = torch.cos(torch.pi * x)
+    p2 = 10 * torch.sin(torch.pi * x)
+    p3 = 2 * torch.sin(2 * torch.pi * x)
+    p4 = 2 * torch.sin(6 * torch.pi * x)
+    return p1 + p2 + p3 + p4
+
+
+bounds = torch.tensor([[0.0], [1.0]], **tkwargs)
+
+
+
+
+
+
+
+
+

We now generate some data and then fit the Gaussian process model.

+
+
+
+
+
+
In [ ]:
+
+
+
torch.manual_seed(0)
+np.random.seed(0)
+n = 5
+train_X = draw_sobol_samples(bounds=bounds, n=n, q=1, seed=12345678).squeeze(-2)
+train_Y = f(train_X)
+
+
+def fit_model(train_X, train_Y, num_outputs):
+    model = SingleTaskGP(train_X, train_Y, outcome_transform=Standardize(m=num_outputs))
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+    return model
+
+
+model = fit_model(train_X=train_X, train_Y=train_Y, num_outputs=1)
+
+
+
+
+
+
+
+
+

We now plot the objective function and the model.

+
+
+
+
+
+
In [ ]:
+
+
+
X = torch.linspace(bounds[0, 0], bounds[1, 0], 1000, **tkwargs)
+mean_fX = model.posterior(X).mean.squeeze(-1).detach().numpy()
+std_fX = torch.sqrt(model.posterior(X).variance).squeeze(-1).detach().numpy()
+
+plt.scatter(train_X, train_Y, color="k", label="Observations")
+plt.plot(X, f(X), color="k", linewidth=2, label="Objective function")
+plt.plot(X, mean_fX, color="dodgerblue", linewidth=3, label="Posterior model")
+plt.fill_between(
+    X, (mean_fX + 3 * std_fX), (mean_fX - 3 * std_fX), alpha=0.2, color="dodgerblue"
+)
+plt.xlabel("x", fontsize=15)
+plt.ylabel("y", fontsize=15)
+plt.legend(fontsize=15)
+plt.show()
+
+
+
+
+
+
+
+
+

To compute the information-theoretic acquisition functions, we first need to get some Monte Carlo samples of the optimal inputs and outputs. The method sample_optimal_points generates num_samples approximate samples of the Gaussian process model and optimizes them sequentially using an optimizer. In the single-objective setting, the number of optimal points (num_points) should be set to one. For simplicitly, we consider optimization via random search.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.acquisition.utils import get_optimal_samples
+
+num_samples = 12
+
+optimal_inputs, optimal_outputs = get_optimal_samples(
+    model, bounds=bounds, num_optima=num_samples
+)
+
+
+
+
+
+
+
+
+

We now initialize the information-theoretic acquisition functions. The PES can simply be initialized using just the optimal set of inputs. For the MES and JES acquisition function, we also have to specify the region of integration, which is $\{\mathbf{y}: \mathbf{y} \preceq \mathbb{Y}^*\}$ for a maximization problem. This is done by providing a Tensor of bounds, which is obtained via the method compute_sample_box_decomposition.

+

Note that for the MES algorithm, we use the multi-objective implementation qLowerBoundMultiObjectiveMaxValueEntropySearch, which implements all the estimation types into one acquisition function. BoTorch alreadys supports many other strategies to estimate the single-objective MES algorithms in botorch.acquisition.max_value_entropy, which is described in the other complementary notebooks.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.acquisition.joint_entropy_search import qJointEntropySearch
+from botorch.acquisition.max_value_entropy_search import qLowerBoundMaxValueEntropy
+from botorch.acquisition.predictive_entropy_search import qPredictiveEntropySearch
+
+pes = qPredictiveEntropySearch(model=model, optimal_inputs=optimal_inputs)
+
+# Here we use the lower bound estimates for the MES and JES
+# Note that the single-objective MES interface is slightly different,
+# as it utilizes the Gumbel max-value approximation internally and
+# therefore does not take the max values as input.
+mes_lb = qLowerBoundMaxValueEntropy(
+    model=model,
+    candidate_set=torch.rand(1000, 1),
+)
+jes_lb = qJointEntropySearch(
+    model=model,
+    optimal_inputs=optimal_inputs,
+    optimal_outputs=optimal_outputs,
+    estimation_type="LB",
+)
+
+
+
+
+
+
+
+
+

To illustrate the acquisition functions, we evaluate it over the whole input space and plot it. As described in [3], the JES should be an upper bound to both the PES and MES, although the estimates might not be.

+
+
+
+
+
+
In [ ]:
+
+
+
# the acquisition function call takes a three-dimensional tensor
+fwd_X = X.unsqueeze(-1).unsqueeze(-1)
+
+# make the acquisition functions live on the same scale
+scale_acqvals = True
+
+pes_X = pes(fwd_X).detach().numpy()
+mes_lb_X = mes_lb(fwd_X).detach().numpy()
+jes_lb_X = jes_lb(fwd_X).detach().numpy()
+
+if scale_acqvals:
+    pes_X = pes_X / pes_X.max()
+    mes_lb_X = mes_lb_X / mes_lb_X.max()
+    jes_lb_X = jes_lb_X / jes_lb_X.max()
+
+plt.plot(X, pes_X, color="mediumseagreen", linewidth=3, label="PES")
+plt.plot(X, mes_lb_X, color="crimson", linewidth=3, label="MES-LB")
+plt.plot(X, jes_lb_X, color="dodgerblue", linewidth=3, label="JES-LB")
+
+plt.vlines(
+    X[pes_X.argmax()], 0, 1, color="mediumseagreen", linewidth=1.5, linestyle="--"
+)
+plt.vlines(X[mes_lb_X.argmax()], 0, 1, color="crimson", linewidth=1.5, linestyle=":")
+plt.vlines(
+    X[jes_lb_X.argmax()], 0, 1, color="dodgerblue", linewidth=1.5, linestyle="--"
+)
+plt.legend(fontsize=15)
+plt.xlabel("$x$", fontsize=15)
+plt.ylabel(r"$\alpha(x)$", fontsize=15)
+plt.title("Entropy-based acquisition functions", fontsize=15)
+plt.show()
+
+
+
+
+
+
+
+
+

To maximize the acquisition function in a standard Bayesian optimization loop, we can use the standard optimization routines. Note that the PES acquisition function might not be differentiable since some operations that may arise during expectation propagation are not differentiable. Therefore, we use a finite difference approach to optimize this acquisition function.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.optim import optimize_acqf
+
+# Use finite difference for PES
+candidate, acq_value = optimize_acqf(
+    acq_function=pes,
+    bounds=bounds,
+    q=1,
+    num_restarts=4,
+    raw_samples=256,
+    options={"with_grad": False},
+)
+print("PES: candidate={}, acq_value={}".format(candidate, acq_value))
+
+candidate, acq_value = optimize_acqf(
+    acq_function=mes_lb,
+    bounds=bounds,
+    q=1,
+    num_restarts=4,
+    raw_samples=256,
+)
+print("MES-LB: candidate={}, acq_value={}".format(candidate, acq_value))
+
+candidate, acq_value = optimize_acqf(
+    acq_function=jes_lb,
+    bounds=bounds,
+    q=1,
+    num_restarts=4,
+    raw_samples=256,
+)
+print("JES-LB: candidate={}, acq_value={}".format(candidate, acq_value))
+
+
+
+
+
+
+
+
+

2. Multi-objective batch example

+
+
+
+
+
+
+

In this section, we illustrate a simple multi-objective example. First we generate some data and fit the model.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.acquisition.multi_objective.utils import (
+    compute_sample_box_decomposition,
+    random_search_optimizer,
+    sample_optimal_points,
+)
+from botorch.test_functions.multi_objective import ZDT1
+
+d = 4
+M = 2
+n = 8
+
+if SMOKE_TEST:
+    q = 2
+else:
+    q = 4
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
problem = ZDT1(dim=d, num_objectives=M, noise_std=0, negate=True)
+bounds = problem.bounds.to(**tkwargs)
+
+train_X = draw_sobol_samples(bounds=bounds, n=n, q=1, seed=123).squeeze(-2)
+train_Y = problem(train_X)
+
+model = fit_model(train_X=train_X, train_Y=train_Y, num_outputs=M)
+
+
+
+
+
+
+
+
+

We now obtain Monte Carlo samples of the optimal inputs and outputs.

+
+
+
+
+
+
In [ ]:
+
+
+
num_pareto_samples = 8
+num_pareto_points = 8
+
+# We set the parameters for the random search
+optimizer_kwargs = {
+    "pop_size": 500,
+    "max_tries": 10,
+}
+
+ps, pf = sample_optimal_points(
+    model=model,
+    bounds=bounds,
+    num_samples=num_pareto_samples,
+    num_points=num_pareto_points,
+    optimizer=random_search_optimizer,
+    optimizer_kwargs=optimizer_kwargs,
+)
+
+
+
+
+
+
+
+
+

We initialize the acquisition functions as before.

+
+
+
+
+
+
In [ ]:
+
+
+
from botorch.acquisition.multi_objective.joint_entropy_search import (
+    qLowerBoundMultiObjectiveJointEntropySearch,
+)
+from botorch.acquisition.multi_objective.max_value_entropy_search import (
+    qLowerBoundMultiObjectiveMaxValueEntropySearch,
+)
+from botorch.acquisition.multi_objective.predictive_entropy_search import (
+    qMultiObjectivePredictiveEntropySearch,
+)
+
+pes = qMultiObjectivePredictiveEntropySearch(model=model, pareto_sets=ps)
+
+# Compute the box-decomposition
+hypercell_bounds = compute_sample_box_decomposition(pf)
+
+# # Here we use the lower bound estimates for the MES and JES
+mes_lb = qLowerBoundMultiObjectiveMaxValueEntropySearch(
+    model=model,
+    hypercell_bounds=hypercell_bounds,
+    estimation_type="LB",
+)
+
+jes_lb = qLowerBoundMultiObjectiveJointEntropySearch(
+    model=model,
+    pareto_sets=ps,
+    pareto_fronts=pf,
+    hypercell_bounds=hypercell_bounds,
+    estimation_type="LB",
+)
+
+
+
+
+
+
+
+
+

We now optimize the batch acquistion functions. For the batch PES, we optimize the batch acquisition function directly. Whereas for the MES and JES we use a sequential optimization strategy.

+
+
+
+
+
+
In [ ]:
+
+
+
%%time
+# Use finite difference for PES. This may take some time
+candidates, acq_values = optimize_acqf(
+    acq_function=pes,
+    bounds=bounds,
+    q=q,
+    num_restarts=4,
+    raw_samples=512,
+    options={"with_grad": False},
+)
+print("PES: \ncandidates={}".format(candidates))
+
+# Sequentially greedy optimization
+candidates, acq_values = optimize_acqf(
+    acq_function=mes_lb,
+    bounds=bounds,
+    q=q,
+    num_restarts=4,
+    raw_samples=512,
+    sequential=True,
+)
+print("MES-LB: \ncandidates={}".format(candidates))
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
# Sequentially greedy optimization
+candidates, acq_values = optimize_acqf(
+    acq_function=jes_lb,
+    bounds=bounds,
+    q=q,
+    num_restarts=4,
+    raw_samples=512,
+    sequential=True,
+)
+print("JES-LB: \ncandidates={}".format(candidates))
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/max_value_entropy.html b/website-old/_tutorials/max_value_entropy.html new file mode 100644 index 0000000000..1218728135 --- /dev/null +++ b/website-old/_tutorials/max_value_entropy.html @@ -0,0 +1,285 @@ + + + +
+
+
+
+

The max value entropy search acquisition function

Max-value entropy search (MES) acquisition function quantifies the information gain about the maximum of a black-box function by observing this black-box function $f$ at the candidate set $\{\textbf{x}\}$ (see [1, 2]). BoTorch provides implementations of the MES acquisition function and its multi-fidelity (MF) version with support for trace observations. In this tutorial, we explain at a high level how the MES acquisition function works, its implementation in BoTorch and how to use the MES acquisition function to query the next point in the optimization process.

+

In general, we recommend using Ax for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. You can use a custom BoTorch model and acquisition function in Ax, following the Using BoTorch with Ax tutorial. To use the MES acquisition function, it is sufficient to add "botorch_acqf_class": qMaxValueEntropy, to model_kwargs. The linked tutorial shows how to use a custom BoTorch model. If you'd like to let Ax choose which model to use based on the properties of the search space, you can skip the surrogate argument in model_kwargs.

+

1. MES acquisition function for $q=1$ with noisy observation

For illustrative purposes, we focus in this section on the non-q-batch-mode case ($q=1$). We also assume that the evaluation of the black-box function is noisy. Let us first introduce some notation:

+
    +
  • $f^* = \max_\mathcal{X} (f(\textbf{x}))$, the maximum of the black-box function $f(\textbf{x})$ in the design space $\mathcal{X}$
  • +
  • $y = f(\textbf{x}) + \epsilon, \epsilon \sim N(0, \sigma^2_\epsilon)$, the noisy observation at the design point $\textbf{x}$
  • +
  • $h(Y) = \mathbb{E}_Y[-\log(p(y))] = -\int_\mathcal{Y} p(y)\log p(y) dy$, the differential entropy of random variable $Y$ with support $\mathcal{Y}$: the larger is $h(Y)$, the larger is the uncertainty of $Y$.
  • +
  • $v(\mathcal{D}) = -\mathbb{E}_D[h(F^*\mid\mathcal{D})]$, the value of data set $\mathcal{D}$, where $F^*$ denotes the function maximum (a random variable in our context of our model).
  • +
+

The Max-value Entropy Search (MES) acquisition function at $\textbf{x}$ after observing $\mathcal{D}_t$ can be written as +\begin{align} + \alpha_{\text{MES}}(\textbf{x}) + &= v(\mathcal{D}_t\cup \{(\textbf{x}, y)\}) - v(\mathcal{D}_t) \\ + &= - \mathbb{E}_Y[h(F^* \mid \mathcal{D}_t\cup \{(\textbf{x}, Y)\})] + h(F^*\mid\mathcal{D}_t) \\ + &= - \mathbb{E}_Y[h(F^* \mid Y)] + h(F^*) \\ + &= I(F^*; Y) \\ + &= I(Y; F^*) \quad \text{(symmetry)} \\ + &= - \mathbb{E}_{F^*}[h(Y \mid F^*)] + h(Y) \\ +\end{align} +, which is the mutual information of random variables +$F^*\mid \mathcal{D}_t$ and $Y \mid \textbf{x}, \mathcal{D}_t$. +Here $F^*$ follows the max value distribution conditioned on $\mathcal{D}_t$, and $Y$ follows the GP posterior distribution with noise at $\textbf{x}$ after observing $\mathcal{D}_t$.

+

Rewrite the above formula as +\begin{align} + \alpha_{\text{MES}}(\textbf{x}) &= - H_1 + H_0, \\ + H_0 &= h(Y) = \log \left(\sqrt{2\pi e (\sigma_f^2 + \sigma_\epsilon^2)}\right) \\ + H_1 &= \mathbb{E}_{F^*}[h(Y \mid F^*)] \\ + &\simeq \frac{1}{\left|\mathcal{F}_*\right|} \Sigma_{\mathcal{F}_*} h(Y\mid f^*)) +\end{align} +, where $\mathcal{F}_*$ are the max value samples drawn from the posterior after observing $\mathcal{D}_t$. Without noise, $p(y \mid f^*) = p(f \mid f \leq f^*)$ is a truncated normal distribution with an analytic expression for its entropy. With noise, $Y\mid F\leq f^*$ is not a truncated normal distribution anymore. The question is then how to compute $h(Y\mid f^*)$ or equivalently $p(y\mid f \leq f^*)$?

+

Using Bayes' theorem, +\begin{align} + p(y\mid f \leq f^*) = \frac{P(f \leq f^* \mid y) p(y)}{P(f \leq f^* )} +\end{align} +, where

+
    +
  • $p(y)$ is the posterior probability density function (PDF) with observation noise.
  • +
  • $P(f \leq f^*)$ is the posterior cummulative distribution function (CDF) without observation noise, given any $f^*$.
  • +
+

We also know from the GP predictive distribution +\begin{align} + \begin{bmatrix} + y \\ f + \end{bmatrix} + \sim \mathcal{N} \left( + \begin{bmatrix} + \mu \\ \mu + \end{bmatrix} , + \begin{bmatrix} + \sigma_f^2 + \sigma_\epsilon^2 & \sigma_f^2 \\ + \sigma_f^2 & \sigma_f^2 + \end{bmatrix} + \right). +\end{align} +So +\begin{align} + f \mid y \sim \mathcal{N} (u, s^2) +\end{align} +, where +\begin{align} + u &= \frac{\sigma_f^2(y-\mu)}{\sigma_f^2 + \sigma_\epsilon^2} + \mu \\ + s^2 &= \sigma_f^2 - \frac{(\sigma_f^2)^2}{\sigma_f^2 + \sigma_\epsilon^2} + = \frac{\sigma_f^2\sigma_\epsilon^2}{\sigma_f^2 + \sigma_\epsilon^2} +\end{align} +Thus, $P(f \leq f^* \mid y)$ is the CDF of above Gaussian.

+

Finally, given $f^*$, we have
+\begin{align} + h(Y \mid f^*) + &= -\int_\mathcal{Y} p(y \mid f^*)\log(p(y \mid f^*)) dy\\ + &= -\int_\mathcal{Y} Zp(y)\log(Zp(y)) dy \\ + &\simeq -\frac{1}{\left|\mathcal{Y}\right|} \Sigma_{\mathcal{Y}} Z\log(Zp(y)), \\ + Z &= \frac{P(f \leq f^* \mid y)}{P(f \leq f^* )} +\end{align} +, where $Z$ is the ratio of two CDFs and $\mathcal{Y}$ is the samples drawn from the posterior distribution with noisy observation. The above formulation for noisy MES is inspired from the MF-MES formulation proposed by Takeno et. al [1], which is essentially the same as what is outlined above.

+

Putting all together, +\begin{align} + \alpha_{\text{MES}}(\textbf{x}) + &= H_0 - H_1 \\ + &\simeq H_0 - H_1^{MC}\\ + &= \log \left(\sqrt{2\pi e (\sigma_f^2 + \sigma_\epsilon^2)}\right) + \frac{1}{\left|\mathcal{F}^*\right|} \Sigma_{\mathcal{F}^*} \frac{1}{\left|\mathcal{Y}\right|} \Sigma_{\mathcal{Y}} (Z\log Z + Z\log p(y)) +\end{align}

+

The next design point to query is chosen as the point that maximizes this aquisition function, i. e., +\begin{align} + \textbf{x}_{\text{next}} = \max_{\textbf{x} \in \mathcal{X}} \alpha_{\text{MES}}(\textbf{x}) +\end{align}

+

The implementation in Botorch basically follows the above formulation for both non-MF and MF cases. One difference is that, in order to reduce the variance of the MC estimator for $H_1$, we apply also regression adjustment to get an estimation of $H_1$, +\begin{align} + \widehat{H}_1 &= H_1^{MC} - \beta (H_0^{MC} - H_0) +\end{align} +, where +\begin{align} + H_0^{MC} &= - \frac{1}{\left|\mathcal{Y}\right|} \Sigma_{\mathcal{Y}} \log p(y) \\ + \beta &= \frac{Cov(h_1, h_0)}{\sqrt{Var(h_1)Var(h_0)}} \\ + h_0 &= -\log p(y) \\ + h_1 &= -Z\log(Zp(y)) \\ +\end{align} +This turns out to reduce the variance of the acquisition value by a significant factor, especially when the acquisition value is small, hence making the algorithm numerically more stable.

+

For the case of $q > 1$, joint optimization becomes difficult, since the q-batch-mode MES acquisiton function becomes not tractable due to the multivariate normal CDF functions in $Z$. Instead, the MES acquisition optimization is solved sequentially and using fantasies, i. e., we generate one point each time and when we try to generate the $i$-th point, we condition the models on the $i-1$ points generated prior to this (using the $i-1$ points as fantasies).

+
+__References__ + +

[1] Takeno, S., et al., Multi-fidelity Bayesian Optimization with Max-value Entropy Search. arXiv:1901.08275v1, 2019

+

[2] Wang, Z., Jegelka, S., Max-value Entropy Search for Efficient Bayesian Optimization. arXiv:1703.01968v3, 2018

+
+
+
+
+
+
+

2. Setting up a toy model

We will fit a standard SingleTaskGP model on noisy observations of the synthetic 2D Branin function on the hypercube $[-5,10]\times [0, 15]$.

+
+
+
+
+
+
In [1]:
+
+
+
import math
+import torch
+
+from botorch.test_functions import Branin
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from botorch.utils.transforms import standardize, normalize
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+torch.manual_seed(7)
+
+bounds = torch.tensor(Branin._bounds).T
+train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(10, 2)
+train_Y = Branin(negate=True)(train_X).unsqueeze(-1)
+
+train_X = normalize(train_X, bounds=bounds)
+train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y))
+
+model = SingleTaskGP(train_X, train_Y)
+mll = ExactMarginalLogLikelihood(model.likelihood, model)
+fit_gpytorch_mll(mll);
+
+
+
+
+
+
+
+
+

3. Defining the MES acquisition function

The qMaxValueEntropy acquisition function is a subclass of MCAcquisitionFunction and supports pending points X_pending. Required arguments for the constructor are model and candidate_set (the discretized candidate points in the design space that will be used to draw max value samples). There are also other optional parameters, such as number of max value samples $\mathcal{F^*}$, number of $\mathcal{Y}$ samples and number of fantasies (in case of $q>1$). Two different sampling algorithms are supported for the max value samples: the discretized Thompson sampling and the Gumbel sampling introduced in [2]. Gumbel sampling is the default choice in the acquisition function.

+
+
+
+
+
+
In [2]:
+
+
+
from botorch.acquisition.max_value_entropy_search import qMaxValueEntropy
+
+candidate_set = torch.rand(
+    1000, bounds.size(1), device=bounds.device, dtype=bounds.dtype
+)
+candidate_set = bounds[0] + (bounds[1] - bounds[0]) * candidate_set
+qMES = qMaxValueEntropy(model, candidate_set)
+
+
+
+
+
+
+
+
+

4. Optimizing the MES acquisition function to get the next candidate points

In order to obtain the next candidate point(s) to query, we need to optimize the acquisition function over the design space. For $q=1$ case, we can simply call the optimize_acqf function in the library. At $q>1$, due to the intractability of the aquisition function in this case, we need to use either sequential or cyclic optimization (multiple cycles of sequential optimization).

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.optim import optimize_acqf
+
+# for q = 1
+candidates, acq_value = optimize_acqf(
+    acq_function=qMES,
+    bounds=bounds,
+    q=1,
+    num_restarts=10,
+    raw_samples=512,
+)
+candidates, acq_value
+
+
+
+
+
+
+
+
Out[3]:
+
+
(tensor([[1.5350, 0.0758]]), tensor(0.0121))
+
+
+
+
+
+
+
+
In [4]:
+
+
+
# for q = 2, sequential optimization
+candidates_q2, acq_value_q2 = optimize_acqf(
+    acq_function=qMES,
+    bounds=bounds,
+    q=2,
+    num_restarts=10,
+    raw_samples=512,
+    sequential=True,
+)
+candidates_q2, acq_value_q2
+
+
+
+
+
+
+
+
Out[4]:
+
+
(tensor([[-0.3238,  0.6565],
+         [ 1.5349,  0.0748]]), tensor([0.0135, 0.0065]))
+
+
+
+
+
+
+
+
In [5]:
+
+
+
from botorch.optim import optimize_acqf_cyclic
+
+# for q = 2, cyclic optimization
+candidates_q2_cyclic, acq_value_q2_cyclic = optimize_acqf_cyclic(
+    acq_function=qMES,
+    bounds=bounds,
+    q=2,
+    num_restarts=10,
+    raw_samples=512,
+    cyclic_options={"maxiter": 2},
+)
+candidates_q2_cyclic, acq_value_q2_cyclic
+
+
+
+
+
+
+
+
Out[5]:
+
+
(tensor([[-0.3236,  0.6563],
+         [ 1.5326,  0.0732]]), tensor([0.0101, 0.0064]))
+
+
+
+
+
+
+
+
+

The use of the qMultiFidelityMaxValueEntropy acquisition function is very similar to qMaxValueEntropy, but requires additional optional arguments related to the fidelity and cost models. We will provide more details on the MF-MES acquisition function in a separate tutorial.

+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/meta_learning_with_rgpe.html b/website-old/_tutorials/meta_learning_with_rgpe.html new file mode 100644 index 0000000000..af5744dd4a --- /dev/null +++ b/website-old/_tutorials/meta_learning_with_rgpe.html @@ -0,0 +1,941 @@ + + + +
+
+
+
+

Meta-Learning with the Rank-Weighted GP Ensemble (RGPE)

BoTorch is designed in to be model-agnostic and only requries that a model conform to a minimal interface. This tutorial walks through an example of implementing the rank-weighted Gaussian process ensemble (RGPE) [Feurer, Letham, Bakshy ICML 2018 AutoML Workshop] and using the RGPE in BoTorch to do meta-learning across related optimization tasks.

+ +
+
+
+
+
+
In [1]:
+
+
+
import os
+import torch
+import math
+
+
+torch.manual_seed(29)
+device = torch.device("cuda:2" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+
+
I0829 091817.060 _utils_internal.py:292] NCCL_DEBUG env var is set to None
+
+
+
+
+
+
+
I0829 091817.061 _utils_internal.py:310] NCCL_DEBUG is forced to WARN from None
+
+
+
+
+
+
+
+
+
+

Toy Problem

    +
  • We consider optimizing the following 1-D synthetic function +$$f(x, s_i) = \frac{1}{10}\bigg(x-1\bigg)\bigg(\sin(x+s_i)+\frac{1}{10}\bigg)$$ +where +$$s_i = \frac{(i+9)\pi}{8}$$ +is a task-dependent shift parameter and $i$ is the task index $i \in [1, t]$.

    +
  • +
  • In this tutorial, we will consider the scenario where we have collected data from 5 prior tasks (referred to as base tasks), which with a different task dependent shift parameter $s_i$.

    +
  • +
  • The goal now is use meta-learning to improve sample efficiency when optimizing a 6th task.

    +
  • +
+
+
+
+
+
+
+

Toy Problem Setup

First let's define a function for compute the shift parameter $s_i$ and set the shift amount for the target task.

+
+
+
+
+
+
In [2]:
+
+
+
NUM_BASE_TASKS = 5 if not SMOKE_TEST else 2
+
+
+def task_shift(task):
+    """
+    Fetch shift amount for task.
+    """
+    return math.pi * task / 12.0
+
+
+# set shift for target task
+
+TARGET_SHIFT = 0.0
+
+
+
+
+
+
+
+
+

Then, let's define our function $f(x, s_i)$ and set bounds on $x$.

+
+
+
+
+
+
In [3]:
+
+
+
BOUNDS = torch.tensor([[-10.0], [10.0]], dtype=dtype, device=device)
+
+
+def f(X, shift=TARGET_SHIFT):
+    """
+    Torch-compatible objective function for the target_task
+    """
+    f_X = X * torch.sin(X + math.pi + shift) + X / 10.0
+    return f_X
+
+
+
+
+
+
+
+
+

Sample training data for prior base tasks

+
+
+
+
+
+
+

We sample data from a Sobol sequence to help ensure numerical stability when using a small amount of 1-D data. Sobol sequences help prevent us from sampling a bunch of training points that are close together.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.utils.sampling import draw_sobol_samples
+from botorch.utils.transforms import normalize, unnormalize
+
+
+noise_std = 0.05
+
+# Sample data for each base task
+data_by_task = {}
+for task in range(NUM_BASE_TASKS):
+    num_training_points = 20
+    # draw points from a sobol sequence
+    raw_x = draw_sobol_samples(
+        bounds=BOUNDS,
+        n=num_training_points,
+        q=1,
+        seed=task + 5397923,
+    ).squeeze(1)
+    # get observed values
+    f_x = f(raw_x, task_shift(task + 1))
+    train_y = f_x + noise_std * torch.randn_like(f_x)
+    train_yvar = torch.full_like(train_y, noise_std**2)
+    # store training data
+    data_by_task[task] = {
+        # scale x to [0, 1]
+        "train_x": normalize(raw_x, bounds=BOUNDS),
+        "train_y": train_y,
+        "train_yvar": train_yvar,
+    }
+
+
+
+
+
+
+
+
+

Let's plot the base tasks and the target task function along with the observed points

+
+
+
+
+
+
In [5]:
+
+
+
from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+
+fig, ax = plt.subplots(1, 1, figsize=(12, 8))
+x = torch.linspace(-10, 10, 51)
+for task in data_by_task:
+    # plot true function and observed values for base runs
+    t = ax.plot(
+        unnormalize(data_by_task[task]["train_x"], bounds=BOUNDS).cpu().numpy(),
+        data_by_task[task]["train_y"].cpu().numpy(),
+        ".",
+        markersize=10,
+        label=f"Observed task {task}",
+    )
+    ax.plot(
+        x.detach().numpy(),
+        f(x, task_shift(task + 1)).cpu().numpy(),
+        label=f"Base task {task}",
+        color=t[0].get_color(),
+    )
+# plot true target function
+ax.plot(
+    x.detach().numpy(),
+    f(x, TARGET_SHIFT).detach().numpy(),
+    "--",
+    label="Target task",
+)
+ax.legend(loc="lower right", fontsize=10)
+plt.tight_layout()
+
+
+
+
+
+
+
+
+
+
W0829 091822.520 font_manager.py:1403] findfont: Font family ['Liberation Sans', 'Noto Sans TC', 'Noto Sans SC', 'Noto Sans Thai', 'Noto Naskh Arabic UI', 'Noto Sans UI'] not found. Falling back to DejaVu Sans.
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Fit base task models

+
+
+
+
+
+
+

First, let's define a helper function to fit a SingleTaskGP with an fixed observed noise level.

+
+
+
+
+
+
In [8]:
+
+
+
from gpytorch.mlls import ExactMarginalLogLikelihood
+from botorch.models import SingleTaskGP
+from botorch.fit import fit_gpytorch_mll
+
+
+def get_fitted_model(train_X, train_Y, train_Yvar, state_dict=None):
+    """
+    Get a single task GP. The model will be fit unless a state_dict with model
+        hyperparameters is provided.
+    """
+    model = SingleTaskGP(train_X=train_X, train_Y=train_Y, train_Yvar=train_Yvar)
+    if state_dict is None:
+        mll = ExactMarginalLogLikelihood(model.likelihood, model).to(train_X)
+        fit_gpytorch_mll(mll)
+    else:
+        model.load_state_dict(state_dict)
+    return model
+
+
+
+
+
+
+
+
+

Now let's fit a SingleTaskGP for each base task

+
+
+
+
+
+
In [9]:
+
+
+
# Fit base model
+base_model_list = []
+for task in range(NUM_BASE_TASKS):
+    print(f"Fitting base model {task}")
+    model = get_fitted_model(
+        data_by_task[task]["train_x"],
+        data_by_task[task]["train_y"],
+        data_by_task[task]["train_yvar"],
+    )
+    base_model_list.append(model)
+
+
+
+
+
+
+
+
+
+
Fitting base model 0
+
+
+
+
+
+
+
Fitting base model 1
+Fitting base model 2
+Fitting base model 3
+Fitting base model 4
+
+
+
+
+
+
+
+
+
+

Implement the RGPE

The main idea of the RGPE is to estimate the target function as weighted sum of the target model and the base models: +$$\bar f(\mathbf x | \mathcal D) = +\sum_{i=1}^{t} w_if^i(\mathbf x |\mathcal D_i)$$ +Importantly, the ensemble model is also a GP: +$$\bar f(\mathbf x | \mathcal D) \sim \mathcal N\bigg(\sum_{i=1}^{t} w_i\mu_i(\mathbf x), \sum_{i=1}^{t}w_i^2\sigma_i^2\bigg)$$

+

The weights $w_i$ for model $i$ are based on the the ranking loss between a draw from the model's posterior and the targets. Specifically, the ranking loss for model $i$ is: +$$\mathcal L(f^i, \mathcal D_t) = \sum_{j=1}^{n_t}\sum_{k=1}^{n_t}\mathbb 1\bigg[\bigg(f^i\big(\mathbf x^t_j\big) < f^i\big(\mathbf x_k^t\big)\bigg)\oplus \big(y_j^t < y_k^t\big)\bigg]$$ +where $\oplus$ is exclusive-or.

+

The loss for the target model is computing using leave-one-out cross-validation (LOOCV) and is given by: +$$\mathcal L(f^t, \mathcal D_t) = \sum_{j=1}^{n_t}\sum_{k=1}^{n_t}\mathbb 1\bigg[\bigg(f^t_{-j}\big(\mathbf x^t_j\big) < f^t_{-j}\big(\mathbf x_k^t\big)\bigg)\oplus \big(y_j^t < y_k^t\big)\bigg]$$ +where $f^t_{-j}$ model fitted to all data from the target task except training example $j$.

+

The weights are then computed as: +$$w_i = \frac{1}{S}\sum_{s=1}^S\mathbb 1\big(i = \text{argmin}_{i'}l_{i', s}\big)$$

+
+
+
+
+
+
In [10]:
+
+
+
def roll_col(X, shift):
+    """
+    Rotate columns to right by shift.
+    """
+    return torch.cat((X[..., -shift:], X[..., :-shift]), dim=-1)
+
+
+
+
+
+
+
+
In [11]:
+
+
+
def compute_ranking_loss(f_samps, target_y):
+    """
+    Compute ranking loss for each sample from the posterior over target points.
+
+    Args:
+        f_samps: `n_samples x (n) x n`-dim tensor of samples
+        target_y: `n x 1`-dim tensor of targets
+    Returns:
+        Tensor: `n_samples`-dim tensor containing the ranking loss across each sample
+    """
+    n = target_y.shape[0]
+    if f_samps.ndim == 3:
+        # Compute ranking loss for target model
+        # take cartesian product of target_y
+        cartesian_y = torch.cartesian_prod(
+            target_y.squeeze(-1),
+            target_y.squeeze(-1),
+        ).view(n, n, 2)
+        # the diagonal of f_samps are the out-of-sample predictions
+        # for each LOO model, compare the out of sample predictions to each in-sample prediction
+        rank_loss = (
+            (
+                (f_samps.diagonal(dim1=1, dim2=2).unsqueeze(-1) < f_samps)
+                ^ (cartesian_y[..., 0] < cartesian_y[..., 1])
+            )
+            .sum(dim=-1)
+            .sum(dim=-1)
+        )
+    else:
+        rank_loss = torch.zeros(
+            f_samps.shape[0], dtype=torch.long, device=target_y.device
+        )
+        y_stack = target_y.squeeze(-1).expand(f_samps.shape)
+        for i in range(1, target_y.shape[0]):
+            rank_loss += (
+                (roll_col(f_samps, i) < f_samps) ^ (roll_col(y_stack, i) < y_stack)
+            ).sum(dim=-1)
+    return rank_loss
+
+
+
+
+
+
+
+
+

Define a function to:

+
    +
  1. Create a batch mode-gp LOOCV GP using the hyperparameters from target_model
  2. +
  3. Draw a joint sample across all points from the target task (in-sample and out-of-sample)
  4. +
+
+
+
+
+
+
In [12]:
+
+
+
def get_target_model_loocv_sample_preds(
+    train_x, train_y, train_yvar, target_model, num_samples
+):
+    """
+    Create a batch-mode LOOCV GP and draw a joint sample across all points from the target task.
+
+    Args:
+        train_x: `n x d` tensor of training points
+        train_y: `n x 1` tensor of training targets
+        target_model: fitted target model
+        num_samples: number of mc samples to draw
+
+    Return: `num_samples x n x n`-dim tensor of samples, where dim=1 represents the `n` LOO models,
+        and dim=2 represents the `n` training points.
+    """
+    batch_size = len(train_x)
+    masks = torch.eye(len(train_x), dtype=torch.uint8, device=device).bool()
+    train_x_cv = torch.stack([train_x[~m] for m in masks])
+    train_y_cv = torch.stack([train_y[~m] for m in masks])
+    train_yvar_cv = torch.stack([train_yvar[~m] for m in masks])
+    state_dict = target_model.state_dict()
+    # expand to batch size of batch_mode LOOCV model
+    state_dict_expanded = {
+        name: t.expand(batch_size, *[-1 for _ in range(t.ndim)])
+        for name, t in state_dict.items()
+    }
+    model = get_fitted_model(
+        train_x_cv, train_y_cv, train_yvar_cv, state_dict=state_dict_expanded
+    )
+    with torch.no_grad():
+        posterior = model.posterior(train_x)
+        # Since we have a batch mode gp and model.posterior always returns an output dimension,
+        # the output from `posterior.sample()` here `num_samples x n x n x 1`, so let's squeeze
+        # the last dimension.
+        sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples]))
+        return sampler(posterior).squeeze(-1)
+
+
+
+
+
+
+
+
In [13]:
+
+
+
def compute_rank_weights(train_x, train_y, base_models, target_model, num_samples):
+    """
+    Compute ranking weights for each base model and the target model (using
+        LOOCV for the target model). Note: This implementation does not currently
+        address weight dilution, since we only have a small number of base models.
+
+    Args:
+        train_x: `n x d` tensor of training points (for target task)
+        train_y: `n` tensor of training targets (for target task)
+        base_models: list of base models
+        target_model: target model
+        num_samples: number of mc samples
+
+    Returns:
+        Tensor: `n_t`-dim tensor with the ranking weight for each model
+    """
+    ranking_losses = []
+    # compute ranking loss for each base model
+    for task in range(len(base_models)):
+        model = base_models[task]
+        # compute posterior over training points for target task
+        posterior = model.posterior(train_x)
+        sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples]))
+        base_f_samps = sampler(posterior).squeeze(-1).squeeze(-1)
+        # compute and save ranking loss
+        ranking_losses.append(compute_ranking_loss(base_f_samps, train_y))
+    # compute ranking loss for target model using LOOCV
+    # f_samps
+    target_f_samps = get_target_model_loocv_sample_preds(
+        train_x,
+        train_y,
+        train_yvar,
+        target_model,
+        num_samples,
+    )
+    ranking_losses.append(compute_ranking_loss(target_f_samps, train_y))
+    ranking_loss_tensor = torch.stack(ranking_losses)
+    # compute best model (minimum ranking loss) for each sample
+    best_models = torch.argmin(ranking_loss_tensor, dim=0)
+    # compute proportion of samples for which each model is best
+    rank_weights = (
+        best_models.bincount(minlength=len(ranking_losses)).type_as(train_x)
+        / num_samples
+    )
+    return rank_weights
+
+
+
+
+
+
+
+
In [14]:
+
+
+
from botorch.models.gpytorch import GPyTorchModel
+from gpytorch.models import GP
+from gpytorch.distributions import MultivariateNormal
+from gpytorch.lazy import PsdSumLazyTensor
+from gpytorch.likelihoods import LikelihoodList
+from torch.nn import ModuleList
+
+
+class RGPE(GP, GPyTorchModel):
+    """
+    Rank-weighted GP ensemble. Note: this class inherits from GPyTorchModel which provides an
+        interface for GPyTorch models in botorch.
+    """
+
+    _num_outputs = 1  # metadata for botorch
+
+    def __init__(self, models, weights):
+        super().__init__()
+        self.models = ModuleList(models)
+        for m in models:
+            if not hasattr(m, "likelihood"):
+                raise ValueError(
+                    "RGPE currently only supports models that have a likelihood (e.g. ExactGPs)"
+                )
+        self.likelihood = LikelihoodList(*[m.likelihood for m in models])
+        self.weights = weights
+        self.to(weights)
+
+    def forward(self, x):
+        weighted_means = []
+        weighted_covars = []
+        # filter model with zero weights
+        # weights on covariance matrices are weight**2
+        non_zero_weight_indices = (self.weights**2 > 0).nonzero()
+        non_zero_weights = self.weights[non_zero_weight_indices]
+        # re-normalize
+        non_zero_weights /= non_zero_weights.sum()
+
+        for non_zero_weight_idx in range(non_zero_weight_indices.shape[0]):
+            raw_idx = non_zero_weight_indices[non_zero_weight_idx].item()
+            model = self.models[raw_idx]
+            posterior = model.posterior(x)
+            # unstandardize predictions
+            posterior_mean = posterior.mean.squeeze(-1)
+            posterior_cov = posterior.mvn.lazy_covariance_matrix
+            # apply weight
+            weight = non_zero_weights[non_zero_weight_idx]
+            weighted_means.append(weight * posterior_mean)
+            weighted_covars.append(posterior_cov * weight**2)
+        # set mean and covariance to be the rank-weighted sum the means and covariances of the
+        # base models and target model
+        mean_x = torch.stack(weighted_means).sum(dim=0)
+        covar_x = PsdSumLazyTensor(*weighted_covars)
+        return MultivariateNormal(mean_x, covar_x)
+
+
+
+
+
+
+
+
+

Optimize target function using RGPE + qNEI

+
+
+
+
+
+
In [18]:
+
+
+
# suppress GPyTorch warnings about adding jitter
+import warnings
+
+from botorch.acquisition.logei import qLogNoisyExpectedImprovement
+from botorch.optim.optimize import optimize_acqf
+from botorch.sampling.normal import SobolQMCNormalSampler
+
+
+warnings.filterwarnings("ignore", "^.*jitter.*", category=RuntimeWarning)
+
+
+best_rgpe_all = []
+best_random_all = []
+best_vanilla_nei_all = []
+N_BATCH = 10 if not SMOKE_TEST else 2
+NUM_POSTERIOR_SAMPLES = 256 if not SMOKE_TEST else 16
+RANDOM_INITIALIZATION_SIZE = 3
+N_TRIALS = 10 if not SMOKE_TEST else 2
+MC_SAMPLES = 512 if not SMOKE_TEST else 32
+N_RESTART_CANDIDATES = 512 if not SMOKE_TEST else 8
+N_RESTARTS = 10 if not SMOKE_TEST else 2
+Q_BATCH_SIZE = 1
+
+
+# Average over multiple trials
+for trial in range(N_TRIALS):
+    print(f"Trial {trial + 1} of {N_TRIALS}")
+    best_rgpe = []
+    best_random = []
+    best_vanilla_nei = []
+    # Initial random observations
+    raw_x = draw_sobol_samples(
+        bounds=BOUNDS, n=RANDOM_INITIALIZATION_SIZE, q=1, seed=trial
+    ).squeeze(1)
+    train_x = normalize(raw_x, bounds=BOUNDS)
+    train_y_noiseless = f(raw_x)
+    train_y = train_y_noiseless + noise_std * torch.randn_like(train_y_noiseless)
+    train_yvar = torch.full_like(train_y, noise_std**2)
+    vanilla_nei_train_x = train_x.clone()
+    vanilla_nei_train_y = train_y.clone()
+    vanilla_nei_train_yvar = train_yvar.clone()
+    # keep track of the best observed point at each iteration
+    best_value = train_y.max().item()
+    best_rgpe.append(best_value)
+    best_random.append(best_value)
+    vanilla_nei_best_value = best_value
+    best_vanilla_nei.append(vanilla_nei_best_value)
+
+    # Run N_BATCH rounds of BayesOpt after the initial random batch
+    for iteration in range(N_BATCH):
+        target_model = get_fitted_model(train_x, train_y, train_yvar)
+        model_list = base_model_list + [target_model]
+        rank_weights = compute_rank_weights(
+            train_x,
+            train_y,
+            base_model_list,
+            target_model,
+            NUM_POSTERIOR_SAMPLES,
+        )
+
+        # create model and acquisition function
+        rgpe_model = RGPE(model_list, rank_weights)
+        sampler_qnei = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+        qNEI = qLogNoisyExpectedImprovement(
+            model=rgpe_model,
+            X_baseline=train_x,
+            sampler=sampler_qnei,
+            prune_baseline=False,
+        )
+
+        # optimize
+        candidate, _ = optimize_acqf(
+            acq_function=qNEI,
+            bounds=torch.tensor([[0.0], [1.0]], dtype=dtype, device=device),
+            q=Q_BATCH_SIZE,
+            num_restarts=N_RESTARTS,
+            raw_samples=N_RESTART_CANDIDATES,
+        )
+
+        # fetch the new values
+        new_x = candidate.detach()
+        new_y_noiseless = f(unnormalize(new_x, bounds=BOUNDS))
+        new_y = new_y_noiseless + noise_std * torch.randn_like(new_y_noiseless)
+        new_yvar = torch.full_like(new_y, noise_std**2)
+
+        # update training points
+        train_x = torch.cat((train_x, new_x))
+        train_y = torch.cat((train_y, new_y))
+        train_yvar = torch.cat((train_yvar, new_yvar))
+        random_candidate = torch.rand(1, dtype=dtype, device=device)
+        next_random_noiseless = f(unnormalize(random_candidate, bounds=BOUNDS))
+        next_random = next_random_noiseless + noise_std * torch.randn_like(
+            next_random_noiseless
+        )
+        next_random_best = next_random.max().item()
+        best_random.append(max(best_random[-1], next_random_best))
+
+        # get the new best observed value
+        best_value = train_y.max().item()
+        best_rgpe.append(best_value)
+
+        # Run Vanilla NEI for comparison
+        vanilla_nei_model = get_fitted_model(
+            vanilla_nei_train_x,
+            vanilla_nei_train_y,
+            vanilla_nei_train_yvar,
+        )
+        vanilla_nei_sampler = SobolQMCNormalSampler(
+            sample_shape=torch.Size([MC_SAMPLES])
+        )
+        vanilla_qNEI = qLogNoisyExpectedImprovement(
+            model=vanilla_nei_model,
+            X_baseline=vanilla_nei_train_x,
+            sampler=vanilla_nei_sampler,
+        )
+        vanilla_nei_candidate, _ = optimize_acqf(
+            acq_function=vanilla_qNEI,
+            bounds=torch.tensor([[0.0], [1.0]], dtype=dtype, device=device),
+            q=Q_BATCH_SIZE,
+            num_restarts=N_RESTARTS,
+            raw_samples=N_RESTART_CANDIDATES,
+        )
+        # fetch the new values
+        vanilla_nei_new_x = vanilla_nei_candidate.detach()
+        vanilla_nei_new_y_noiseless = f(unnormalize(vanilla_nei_new_x, bounds=BOUNDS))
+        vanilla_nei_new_y = vanilla_nei_new_y_noiseless + noise_std * torch.randn_like(
+            new_y_noiseless
+        )
+        vanilla_nei_new_yvar = torch.full_like(vanilla_nei_new_y, noise_std**2)
+
+        # update training points
+        vanilla_nei_train_x = torch.cat([vanilla_nei_train_x, vanilla_nei_new_x])
+        vanilla_nei_train_y = torch.cat([vanilla_nei_train_y, vanilla_nei_new_y])
+        vanilla_nei_train_yvar = torch.cat(
+            [vanilla_nei_train_yvar, vanilla_nei_new_yvar]
+        )
+
+        # get the new best observed value
+        vanilla_nei_best_value = vanilla_nei_train_y.max().item()
+        best_vanilla_nei.append(vanilla_nei_best_value)
+
+    best_rgpe_all.append(best_rgpe)
+    best_random_all.append(best_random)
+    best_vanilla_nei_all.append(best_vanilla_nei)
+
+
+
+
+
+
+
+
+
+
Trial 1 of 10
+
+
+
+
+
+
+
Trial 2 of 10
+
+
+
+
+
+
+
Trial 3 of 10
+
+
+
+
+
+
+
Trial 4 of 10
+
+
+
+
+
+
+
Trial 5 of 10
+
+
+
+
+
+
+
[W 240829 09:21:28 optimize:564] 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.
+
+
+
+
+
+
+
[W 240829 09:21:28 optimize:564] Optimization failed on the second try, after generating a new set of initial conditions.
+
+
+
+
+
+
+
Trial 6 of 10
+
+
+
+
+
+
+
Trial 7 of 10
+
+
+
+
+
+
+
Trial 8 of 10
+
+
+
+
+
+
+
Trial 9 of 10
+
+
+
+
+
+
+
[W 240829 09:21:46 optimize:564] 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.
+
+
+
+
+
+
+
[W 240829 09:21:46 optimize:564] Optimization failed on the second try, after generating a new set of initial conditions.
+
+
+
+
+
+
+
Trial 10 of 10
+
+
+
+
+
+
+
+
+
+

Plot best observed value vs iteration

+
+
+
+
+
+
In [19]:
+
+
+
import numpy as np
+
+
+best_rgpe_all = np.array(best_rgpe_all)
+best_random_all = np.array(best_random_all)
+best_vanilla_nei_all = np.array(best_vanilla_nei_all)
+
+x = range(RANDOM_INITIALIZATION_SIZE, RANDOM_INITIALIZATION_SIZE + N_BATCH + 1)
+
+fig, ax = plt.subplots(1, 1, figsize=(10, 6))
+# Plot RGPE - LogNEI
+ax.errorbar(
+    x,
+    best_rgpe_all.mean(axis=0),
+    yerr=1.96 * best_rgpe_all.std(axis=0) / math.sqrt(N_TRIALS),
+    label="RGPE - LogNEI",
+    linewidth=3,
+    capsize=5,
+    capthick=3,
+)
+# Plot SingleTaskGP - LogNEI
+ax.errorbar(
+    x,
+    best_vanilla_nei_all.mean(axis=0),
+    yerr=1.96 * best_vanilla_nei_all.std(axis=0) / math.sqrt(N_TRIALS),
+    label="SingleTaskGP - LogNEI",
+    linewidth=3,
+    capsize=5,
+    capthick=3,
+)
+# Plot Random
+ax.errorbar(
+    x,
+    best_random_all.mean(axis=0),
+    yerr=1.96 * best_random_all.std(axis=0) / math.sqrt(N_TRIALS),
+    label="Random",
+    linewidth=3,
+    capsize=5,
+    capthick=3,
+)
+ax.set_ylim(bottom=0)
+ax.set_xlabel("Iteration", fontsize=12)
+ax.set_ylabel("Best Observed Value", fontsize=12)
+ax.set_title("Best Observed Value by Iteration", fontsize=12)
+ax.legend(loc="lower right", fontsize=10)
+plt.tight_layout()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/multi_fidelity_bo.html b/website-old/_tutorials/multi_fidelity_bo.html new file mode 100644 index 0000000000..b7680c9cd5 --- /dev/null +++ b/website-old/_tutorials/multi_fidelity_bo.html @@ -0,0 +1,626 @@ + + + +
+
+
+
+

Continuous Multi-Fidelity BO in BoTorch with Knowledge Gradient

In this tutorial, we show how to perform continuous multi-fidelity Bayesian optimization (BO) in BoTorch using the multi-fidelity Knowledge Gradient (qMFKG) acquisition function [1, 2].

+

[1] J. Wu, P.I. Frazier. Continuous-Fidelity Bayesian Optimization with Knowledge Gradient. NIPS Workshop on Bayesian Optimization, 2017.

+

[2] J. Wu, S. Toscano-Palmerin, P.I. Frazier, A.G. Wilson. Practical Multi-fidelity Bayesian Optimization for Hyperparameter Tuning. Conference on Uncertainty in Artificial Intelligence (UAI), 2019

+
+
+
+
+
+
+

Set dtype and device

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import torch
+
+
+tkwargs = {
+    "dtype": torch.double,
+    "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"),
+}
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Problem setup

We'll consider the Augmented Hartmann multi-fidelity synthetic test problem. This function is a version of the Hartmann6 test function with an additional dimension representing the fidelity parameter; details are in [2]. The function takes the form $f(x,s)$ where $x \in [0,1]^6$ and $s \in [0,1]$. The target fidelity is 1.0, which means that our goal is to solve $\max_x f(x,1.0)$ by making use of cheaper evaluations $f(x,s)$ for $s < 1.0$. In this example, we'll assume that the cost function takes the form $5.0 + s$, illustrating a situation where the fixed cost is $5.0$.

+
+
+
+
+
+
In [2]:
+
+
+
from botorch.test_functions.multi_fidelity import AugmentedHartmann
+
+
+problem = AugmentedHartmann(negate=True).to(**tkwargs)
+
+
+
+
+
+
+
+
+

Model initialization

We use a SingleTaskMultiFidelityGP as the surrogate model, which uses a kernel from [2] that is well-suited for multi-fidelity applications.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP
+from botorch.models.transforms.outcome import Standardize
+from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
+from botorch.utils.transforms import unnormalize
+from botorch.utils.sampling import draw_sobol_samples
+
+
+def generate_initial_data(n=16):
+    # generate training data
+    train_x = torch.rand(n, 7, **tkwargs)
+    train_obj = problem(train_x).unsqueeze(-1)  # add output dimension
+    return train_x, train_obj
+
+
+def initialize_model(train_x, train_obj):
+    # define a surrogate model suited for a "training data"-like fidelity parameter
+    # in dimension 6, as in [2]
+    model = SingleTaskMultiFidelityGP(
+        train_x, train_obj, outcome_transform=Standardize(m=1), data_fidelities=[6]
+    )
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    return mll, model
+
+
+
+
+
+
+
+
+

Define a helper function to construct the MFKG acquisition function

The helper function illustrates how one can initialize a $q$MFKG acquisition function. In this example, we assume that the affine cost is known. We then use the notion of a CostAwareUtility in BoTorch to scalarize the competing objectives of information gain and cost. The MFKG acquisition function optimizes the ratio of information gain to cost, which is captured by the InverseCostWeightedUtility.

+

In order for MFKG to evaluate the information gain, it uses the model to predict the function value at the highest fidelity after conditioning on the observation. This is handled by the project argument, which specifies how to transform a tensor X to its target fidelity. We use a default helper function called project_to_target_fidelity to achieve this.

+

An important point to keep in mind: in the case of standard KG, one can ignore the current value and simply optimize the expected maximum posterior mean of the next stage. However, for MFKG, since the goal is optimize information gain per cost, it is important to first compute the current value (i.e., maximum of the posterior mean at the target fidelity). To accomplish this, we use a FixedFeatureAcquisitionFunction on top of a PosteriorMean.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch import fit_gpytorch_mll
+from botorch.models.cost import AffineFidelityCostModel
+from botorch.acquisition.cost_aware import InverseCostWeightedUtility
+from botorch.acquisition import PosteriorMean
+from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient
+from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction
+from botorch.optim.optimize import optimize_acqf
+from botorch.acquisition.utils import project_to_target_fidelity
+
+
+bounds = torch.tensor([[0.0] * problem.dim, [1.0] * problem.dim], **tkwargs)
+target_fidelities = {6: 1.0}
+
+cost_model = AffineFidelityCostModel(fidelity_weights={6: 1.0}, fixed_cost=5.0)
+cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)
+
+
+def project(X):
+    return project_to_target_fidelity(X=X, target_fidelities=target_fidelities)
+
+
+def get_mfkg(model):
+
+    curr_val_acqf = FixedFeatureAcquisitionFunction(
+        acq_function=PosteriorMean(model),
+        d=7,
+        columns=[6],
+        values=[1],
+    )
+
+    _, current_value = optimize_acqf(
+        acq_function=curr_val_acqf,
+        bounds=bounds[:, :-1],
+        q=1,
+        num_restarts=10 if not SMOKE_TEST else 2,
+        raw_samples=1024 if not SMOKE_TEST else 4,
+        options={"batch_limit": 10, "maxiter": 200},
+    )
+
+    return qMultiFidelityKnowledgeGradient(
+        model=model,
+        num_fantasies=128 if not SMOKE_TEST else 2,
+        current_value=current_value,
+        cost_aware_utility=cost_aware_utility,
+        project=project,
+    )
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step

This helper function optimizes the acquisition function and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.optim.initializers import gen_one_shot_kg_initial_conditions
+
+torch.set_printoptions(precision=3, sci_mode=False)
+
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+
+
+def optimize_mfkg_and_get_observation(mfkg_acqf):
+    """Optimizes MFKG and returns a new candidate, observation, and cost."""
+
+    X_init = gen_one_shot_kg_initial_conditions(
+        acq_function=mfkg_acqf,
+        bounds=bounds,
+        q=4,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+    candidates, _ = optimize_acqf(
+        acq_function=mfkg_acqf,
+        bounds=bounds,
+        q=4,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+        batch_initial_conditions=X_init,
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+    # observe new values
+    cost = cost_model(candidates).sum()
+    new_x = candidates.detach()
+    new_obj = problem(new_x).unsqueeze(-1)
+    print(f"candidates:\n{new_x}\n")
+    print(f"observations:\n{new_obj}\n\n")
+    return new_x, new_obj, cost
+
+
+
+
+
+
+
+
+

Perform a few steps of multi-fidelity BO

First, let's generate some initial random data and fit a surrogate model.

+
+
+
+
+
+
In [6]:
+
+
+
train_x, train_obj = generate_initial_data(n=16)
+
+
+
+
+
+
+
+
+

We can now use the helper functions above to run a few iterations of BO.

+
+
+
+
+
+
In [7]:
+
+
+
cumulative_cost = 0.0
+N_ITER = 6 if not SMOKE_TEST else 2
+
+
+for _ in range(N_ITER):
+    mll, model = initialize_model(train_x, train_obj)
+    fit_gpytorch_mll(mll)
+    mfkg_acqf = get_mfkg(model)
+    new_x, new_obj, cost = optimize_mfkg_and_get_observation(mfkg_acqf)
+    train_x = torch.cat([train_x, new_x])
+    train_obj = torch.cat([train_obj, new_obj])
+    cumulative_cost += cost
+
+
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.005, 0.185, 0.708, 0.670, 0.472, 0.796, 0.000],
+        [0.000, 0.335, 0.670, 0.584, 0.301, 0.733, 0.000],
+        [0.066, 0.127, 0.583, 0.555, 0.302, 0.734, 0.000],
+        [0.023, 0.210, 0.606, 0.756, 0.236, 0.807, 0.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[0.427],
+        [1.045],
+        [1.396],
+        [0.416]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.024, 0.137, 0.466, 0.545, 0.236, 0.654, 0.000],
+        [0.220, 0.175, 0.597, 0.537, 0.269, 0.681, 0.000],
+        [0.045, 0.088, 0.644, 0.520, 0.234, 0.818, 0.013],
+        [0.024, 0.117, 0.613, 0.496, 0.330, 0.638, 0.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[1.372],
+        [1.640],
+        [1.259],
+        [1.728]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.162, 0.180, 0.608, 0.453, 0.377, 0.667, 0.010],
+        [0.180, 0.138, 0.505, 0.444, 0.293, 0.554, 0.751],
+        [0.185, 0.046, 0.631, 0.491, 0.384, 0.585, 0.002],
+        [0.151, 0.167, 0.698, 0.474, 0.240, 0.580, 0.024]],
+       dtype=torch.float64)
+
+observations:
+tensor([[2.165],
+        [2.315],
+        [1.676],
+        [1.693]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.156, 0.163, 0.527, 0.376, 0.290, 0.618, 0.000],
+        [0.208, 0.148, 0.480, 0.403, 0.399, 0.589, 0.004],
+        [0.131, 0.213, 0.527, 0.401, 0.377, 0.502, 0.009],
+        [0.240, 0.241, 0.519, 0.408, 0.306, 0.564, 0.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[2.882],
+        [2.431],
+        [2.120],
+        [2.504]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.215, 0.081, 0.494, 0.335, 0.243, 0.620, 0.000],
+        [0.198, 0.180, 0.539, 0.310, 0.293, 0.655, 0.016],
+        [0.440, 0.558, 0.028, 0.675, 0.168, 0.008, 0.000],
+        [0.153, 0.201, 0.453, 0.338, 0.252, 0.656, 0.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[2.878],
+        [3.178],
+        [1.162],
+        [2.952]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.232, 0.170, 0.469, 0.256, 0.312, 0.629, 0.037],
+        [0.126, 0.141, 0.519, 0.245, 0.308, 0.671, 0.016],
+        [0.654, 0.372, 0.777, 0.420, 0.574, 0.380, 0.341],
+        [0.218, 0.144, 0.481, 0.280, 0.318, 0.710, 0.031]],
+       dtype=torch.float64)
+
+observations:
+tensor([[3.235],
+        [3.161],
+        [0.170],
+        [3.209]], dtype=torch.float64)
+
+
+
+
+
+
+
+
+
+
+
+

Make a final recommendation

In multi-fidelity BO, there are usually fewer observations of the function at the target fidelity, so it is important to use a recommendation function that uses the correct fidelity. Here, we maximize the posterior mean with the fidelity dimension fixed to the target fidelity of 1.0.

+
+
+
+
+
+
In [8]:
+
+
+
def get_recommendation(model):
+    rec_acqf = FixedFeatureAcquisitionFunction(
+        acq_function=PosteriorMean(model),
+        d=7,
+        columns=[6],
+        values=[1],
+    )
+
+    final_rec, _ = optimize_acqf(
+        acq_function=rec_acqf,
+        bounds=bounds[:, :-1],
+        q=1,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+
+    final_rec = rec_acqf._construct_X_full(final_rec)
+
+    objective_value = problem(final_rec)
+    print(f"recommended point:\n{final_rec}\n\nobjective value:\n{objective_value}")
+    return final_rec
+
+
+
+
+
+
+
+
In [9]:
+
+
+
final_rec = get_recommendation(model)
+print(f"\ntotal cost: {cumulative_cost}\n")
+
+
+
+
+
+
+
+
+
+
recommended point:
+tensor([[0.208, 0.164, 0.514, 0.280, 0.301, 0.664, 1.000]],
+       dtype=torch.float64)
+
+objective value:
+tensor([3.298], dtype=torch.float64)
+
+total cost: 121.25572809899545
+
+
+
+
+
+
+
+
+
+
+

Comparison to standard EI (always use target fidelity)

Let's now repeat the same steps using a standard EI acquisition function (note that this is not a rigorous comparison as we are only looking at one trial in order to keep computational requirements low).

+
+
+
+
+
+
In [10]:
+
+
+
from botorch.acquisition import qExpectedImprovement
+
+
+def get_ei(model, best_f):
+
+    return FixedFeatureAcquisitionFunction(
+        acq_function=qExpectedImprovement(model=model, best_f=best_f),
+        d=7,
+        columns=[6],
+        values=[1],
+    )
+
+
+def optimize_ei_and_get_observation(ei_acqf):
+    """Optimizes EI and returns a new candidate, observation, and cost."""
+
+    candidates, _ = optimize_acqf(
+        acq_function=ei_acqf,
+        bounds=bounds[:, :-1],
+        q=4,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+
+    # add the fidelity parameter
+    candidates = ei_acqf._construct_X_full(candidates)
+
+    # observe new values
+    cost = cost_model(candidates).sum()
+    new_x = candidates.detach()
+    new_obj = problem(new_x).unsqueeze(-1)
+    print(f"candidates:\n{new_x}\n")
+    print(f"observations:\n{new_obj}\n\n")
+    return new_x, new_obj, cost
+
+
+
+
+
+
+
+
In [11]:
+
+
+
cumulative_cost = 0.0
+
+train_x, train_obj = generate_initial_data(n=16)
+
+for _ in range(N_ITER):
+    mll, model = initialize_model(train_x, train_obj)
+    fit_gpytorch_mll(mll)
+    ei_acqf = get_ei(model, best_f=train_obj.max())
+    new_x, new_obj, cost = optimize_ei_and_get_observation(ei_acqf)
+    train_x = torch.cat([train_x, new_x])
+    train_obj = torch.cat([train_obj, new_obj])
+    cumulative_cost += cost
+
+
+
+
+
+
+
+
+
+
candidates:
+tensor([[0.284, 0.692, 0.351, 0.840, 0.487, 0.058, 1.000],
+        [0.571, 0.227, 0.556, 0.254, 0.208, 0.771, 1.000],
+        [0.475, 0.811, 0.448, 0.853, 0.403, 0.000, 1.000],
+        [0.625, 0.141, 0.299, 0.163, 0.171, 0.854, 1.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[0.895],
+        [1.644],
+        [1.248],
+        [0.905]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.580, 0.206, 0.677, 0.320, 0.163, 0.809, 1.000],
+        [0.538, 0.242, 0.613, 0.248, 0.152, 0.667, 1.000],
+        [0.453, 0.231, 0.634, 0.252, 0.290, 0.771, 1.000],
+        [0.619, 0.325, 0.576, 0.301, 0.226, 0.767, 1.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[1.357],
+        [1.445],
+        [2.271],
+        [1.486]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.416, 0.189, 0.617, 0.265, 0.331, 0.728, 1.000],
+        [0.757, 0.521, 0.077, 0.687, 0.779, 0.473, 1.000],
+        [0.416, 0.243, 0.699, 0.191, 0.315, 0.793, 1.000],
+        [0.753, 0.544, 0.275, 0.703, 0.266, 0.637, 1.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[2.547],
+        [0.010],
+        [2.088],
+        [0.134]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.057, 0.684, 1.000, 0.133, 0.647, 0.573, 1.000],
+        [0.339, 0.169, 0.558, 0.284, 0.349, 0.719, 1.000],
+        [0.430, 0.141, 0.663, 0.284, 0.367, 0.703, 1.000],
+        [0.734, 0.006, 0.873, 0.563, 0.275, 0.925, 1.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[0.065],
+        [2.879],
+        [2.321],
+        [0.384]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.286, 0.174, 0.514, 0.281, 0.354, 0.746, 1.000],
+        [0.388, 0.494, 0.511, 0.892, 0.814, 0.650, 1.000],
+        [0.311, 0.700, 0.253, 0.139, 0.203, 0.086, 1.000],
+        [0.323, 0.109, 0.950, 0.702, 0.221, 0.896, 1.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[2.944],
+        [0.026],
+        [0.350],
+        [0.451]], dtype=torch.float64)
+
+
+candidates:
+tensor([[0.694, 0.341, 0.325, 0.928, 0.077, 0.603, 1.000],
+        [0.758, 0.194, 0.803, 0.440, 0.016, 0.814, 1.000],
+        [0.252, 0.168, 0.529, 0.280, 0.329, 0.698, 1.000],
+        [0.438, 0.572, 0.395, 0.611, 0.429, 0.559, 1.000]],
+       dtype=torch.float64)
+
+observations:
+tensor([[0.011],
+        [0.574],
+        [3.203],
+        [0.413]], dtype=torch.float64)
+
+
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
final_rec = get_recommendation(model)
+print(f"\ntotal cost: {cumulative_cost}\n")
+
+
+
+
+
+
+
+
+
+
recommended point:
+tensor([[0.288, 0.175, 0.520, 0.283, 0.351, 0.735, 1.000]],
+       dtype=torch.float64)
+
+objective value:
+tensor([2.990], dtype=torch.float64)
+
+total cost: 144.0
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/multi_objective_bo.html b/website-old/_tutorials/multi_objective_bo.html new file mode 100644 index 0000000000..7f65833114 --- /dev/null +++ b/website-old/_tutorials/multi_objective_bo.html @@ -0,0 +1,636 @@ + + + +
+
+
+
+

Noisy, Parallel, Multi-Objective BO in BoTorch with qEHVI, qNEHVI, and qNParEGO

In this tutorial, we illustrate how to implement a simple multi-objective (MO) Bayesian Optimization (BO) closed loop in BoTorch.

+

In general, we recommend using Ax for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. See here for an Ax tutorial on MOBO. If desired, you can use a custom BoTorch model in Ax, following the Using BoTorch with Ax tutorial. Given a MultiObjective, Ax will default to the $q$NEHVI acquisiton function. If desired, this can also be customized by adding "botorch_acqf_class": <desired_botorch_acquisition_function_class>, to the model_kwargs.

+

We use the parallel ParEGO ($q$ParEGO) [1], parallel Expected Hypervolume Improvement ($q$EHVI) [1], and parallel Noisy Expected Hypervolume Improvement ($q$NEHVI) [2] acquisition functions to optimize a synthetic BraninCurrin problem test function with additive Gaussian observation noise over a 2-parameter search space [0,1]^2. See botorch/test_functions/multi_objective.py for details on BraninCurrin. The noise standard deviations are 15.19 and 0.63 for each objective, respectively.

+

Since botorch assumes a maximization of all objectives, we seek to find the Pareto frontier, the set of optimal trade-offs where improving one metric means deteriorating another.

+

[1] S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.

+

[2] S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances in Neural Information Processing Systems 34, 2021.

+

For batch optimization (or in noisy settings), we strongly recommend using $q$NEHVI rather than $q$EHVI because it is far more efficient than $q$EHVI and mathematically equivalent in the noiseless setting.

+
+
+
+
+
+
+

Set dtype and device

Note: $q$EHVI and $q$NEHVI aggressively exploit parallel hardware and are both much faster when run on a GPU. See [1, 2] for details.

+
+
+
+
+
+
In [8]:
+
+
+
import os
+import torch
+
+
+tkwargs = {
+    "dtype": torch.double,
+    "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"),
+}
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Problem setup

+
+
+
+
+
+
In [9]:
+
+
+
from botorch.test_functions.multi_objective import BraninCurrin
+
+
+problem = BraninCurrin(negate=True).to(**tkwargs)
+
+
+
+
+
+
+
+
+

Model initialization

We use a list of SingleTaskGPs to model the two objectives with known noise variances. If no noise variances were provided, SingleTaskGP would infer (homoskedastic) noise levels instead.

+

The models are initialized with $2(d+1)=6$ points drawn randomly from $[0,1]^2$.

+
+
+
+
+
+
In [10]:
+
+
+
from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.transforms.outcome import Standardize
+from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood
+from botorch.utils.transforms import unnormalize, normalize
+from botorch.utils.sampling import draw_sobol_samples
+
+NOISE_SE = torch.tensor([15.19, 0.63], **tkwargs)
+
+
+def generate_initial_data(n=6):
+    # generate training data
+    train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1)
+    train_obj_true = problem(train_x)
+    train_obj = train_obj_true + torch.randn_like(train_obj_true) * NOISE_SE
+    return train_x, train_obj, train_obj_true
+
+
+def initialize_model(train_x, train_obj):
+    # define models for objective and constraint
+    train_x = normalize(train_x, problem.bounds)
+    models = []
+    for i in range(train_obj.shape[-1]):
+        train_y = train_obj[..., i : i + 1]
+        train_yvar = torch.full_like(train_y, NOISE_SE[i] ** 2)
+        models.append(
+            SingleTaskGP(
+                train_x, train_y, train_yvar, outcome_transform=Standardize(m=1)
+            )
+        )
+    model = ModelListGP(*models)
+    mll = SumMarginalLogLikelihood(model.likelihood, model)
+    return mll, model
+
+
+
+
+
+
+
+
+

Define a helper functions that performs the essential BO step for $q$EHVI and $q$NEHVI

The helper function below initializes the $q$EHVI acquisition function, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values.

+

For this example, we'll use a relatively small batch of optimization ($q=4$). For batch optimization ($q>1$), passing the keyword argument sequential=True to the function optimize_acqfspecifies that candidates should be optimized in a sequential greedy fashion (see [1] for details why this is important). A simple initialization heuristic is used to select the 10 restart initial locations from a set of 512 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation.

+

Reference Point

+

$q$EHVI requires specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy.

+

Partitioning the Non-dominated Space into disjoint rectangles

+

$q$EHVI requires partitioning the non-dominated space into disjoint rectangles (see [1] for details).

+

Note: FastNondominatedPartitioning will be very slow when 1) there are a lot of points on the pareto frontier and 2) there are >5 objectives.

+
+
+
+
+
+
In [11]:
+
+
+
from botorch.optim.optimize import optimize_acqf, optimize_acqf_list
+from botorch.acquisition.objective import GenericMCObjective
+from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    FastNondominatedPartitioning,
+)
+from botorch.acquisition.multi_objective.monte_carlo import (
+    qExpectedHypervolumeImprovement,
+    qNoisyExpectedHypervolumeImprovement,
+)
+from botorch.utils.sampling import sample_simplex
+
+
+BATCH_SIZE = 4
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+
+standard_bounds = torch.zeros(2, problem.dim, **tkwargs)
+standard_bounds[1] = 1
+
+
+def optimize_qehvi_and_get_observation(model, train_x, train_obj, sampler):
+    """Optimizes the qEHVI acquisition function, and returns a new candidate and observation."""
+    # partition non-dominated space into disjoint rectangles
+    with torch.no_grad():
+        pred = model.posterior(normalize(train_x, problem.bounds)).mean
+    partitioning = FastNondominatedPartitioning(
+        ref_point=problem.ref_point,
+        Y=pred,
+    )
+    acq_func = qExpectedHypervolumeImprovement(
+        model=model,
+        ref_point=problem.ref_point,
+        partitioning=partitioning,
+        sampler=sampler,
+    )
+    # optimize
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        bounds=standard_bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+        sequential=True,
+    )
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=problem.bounds)
+    new_obj_true = problem(new_x)
+    new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE
+    return new_x, new_obj, new_obj_true
+
+
+
+
+
+
+
+
+

Integrating over function values at in-sample designs

+

$q$NEHVI integrates over the unknown function values at the previously evaluated designs (see [2] for details). Therefore, we need to provide the previously evaluated designs (train_x, normalized to be within $[0,1]^d$) to the acquisition function.

+

Efficient batch generation with Cached Box Decomposition (CBD)

+

$q$NEHVI leveraged CBD to efficiently generate large batches of candidates. CBD scales polynomially with respect to the batch size where as the inclusion-exclusion principle used by qEHVI scales exponentially with the batch size.

+

Pruning baseline designs +To speed up integration over the function values at the previously evaluated designs, we prune the set of previously evaluated designs (by setting prune_baseline=True) to only include those which have positive probability of being on the current in-sample Pareto frontier.

+
+
+
+
+
+
In [12]:
+
+
+
def optimize_qnehvi_and_get_observation(model, train_x, train_obj, sampler):
+    """Optimizes the qEHVI acquisition function, and returns a new candidate and observation."""
+    # partition non-dominated space into disjoint rectangles
+    acq_func = qNoisyExpectedHypervolumeImprovement(
+        model=model,
+        ref_point=problem.ref_point.tolist(),  # use known reference point
+        X_baseline=normalize(train_x, problem.bounds),
+        prune_baseline=True,  # prune baseline points that have estimated zero probability of being Pareto optimal
+        sampler=sampler,
+    )
+    # optimize
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        bounds=standard_bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+        sequential=True,
+    )
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=problem.bounds)
+    new_obj_true = problem(new_x)
+    new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE
+    return new_x, new_obj, new_obj_true
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step for $q$NParEGO

The helper function below similarly initializes $q$NParEGO, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values.

+

$q$NParEGO uses random augmented chebyshev scalarization with the qNoisyExpectedImprovement acquisition function. In the parallel setting ($q>1$), each candidate is optimized in sequential greedy fashion using a different random scalarization (see [1] for details).

+

To do this, we create a list of qNoisyExpectedImprovement acquisition functions, each with different random scalarization weights. The optimize_acqf_list method sequentially generates one candidate per acquisition function and conditions the next candidate (and acquisition function) on the previously selected pending candidates.

+
+
+
+
+
+
In [13]:
+
+
+
from botorch.acquisition.monte_carlo import qNoisyExpectedImprovement
+
+
+def optimize_qnparego_and_get_observation(model, train_x, train_obj, sampler):
+    """Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization
+    of the qNParEGO acquisition function, and returns a new candidate and observation."""
+    train_x = normalize(train_x, problem.bounds)
+    with torch.no_grad():
+        pred = model.posterior(train_x).mean
+    acq_func_list = []
+    for _ in range(BATCH_SIZE):
+        weights = sample_simplex(problem.num_objectives, **tkwargs).squeeze()
+        objective = GenericMCObjective(
+            get_chebyshev_scalarization(weights=weights, Y=pred)
+        )
+        acq_func = qNoisyExpectedImprovement(  # pyre-ignore: [28]
+            model=model,
+            objective=objective,
+            X_baseline=train_x,
+            sampler=sampler,
+            prune_baseline=True,
+        )
+        acq_func_list.append(acq_func)
+    # optimize
+    candidates, _ = optimize_acqf_list(
+        acq_function_list=acq_func_list,
+        bounds=standard_bounds,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,  # used for intialization heuristic
+        options={"batch_limit": 5, "maxiter": 200},
+    )
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=problem.bounds)
+    new_obj_true = problem(new_x)
+    new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE
+    return new_x, new_obj, new_obj_true
+
+
+
+
+
+
+
+
+

Perform Bayesian Optimization loop with $q$NEHVI, $q$EHVI, and $q$NParEGO

The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps:

+
    +
  1. given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$
  2. +
  3. observe $f(x)$ for each $x$ in the batch
  4. +
  5. update the surrogate model.
  6. +
+

Just for illustration purposes, we run one trial with N_BATCH=20 rounds of optimization. The acquisition function is approximated using MC_SAMPLES=128 samples.

+

Note: Running this may take a little while.

+
+
+
+
+
+
In [14]:
+
+
+
import time
+import warnings
+
+from botorch import fit_gpytorch_mll
+from botorch.exceptions import BadInitialCandidatesWarning
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.multi_objective.box_decompositions.dominated import (
+    DominatedPartitioning,
+)
+from botorch.utils.multi_objective.pareto import is_non_dominated
+
+
+warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning)
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+N_BATCH = 20 if not SMOKE_TEST else 5
+MC_SAMPLES = 128 if not SMOKE_TEST else 16
+
+verbose = True
+
+hvs_qparego, hvs_qehvi, hvs_qnehvi, hvs_random = [], [], [], []
+
+# call helper functions to generate initial training data and initialize model
+train_x_qparego, train_obj_qparego, train_obj_true_qparego = generate_initial_data(
+    n=2 * (problem.dim + 1)
+)
+mll_qparego, model_qparego = initialize_model(train_x_qparego, train_obj_qparego)
+
+train_x_qehvi, train_obj_qehvi, train_obj_true_qehvi = (
+    train_x_qparego,
+    train_obj_qparego,
+    train_obj_true_qparego,
+)
+train_x_qnehvi, train_obj_qnehvi, train_obj_true_qnehvi = (
+    train_x_qparego,
+    train_obj_qparego,
+    train_obj_true_qparego,
+)
+train_x_random, train_obj_random, train_obj_true_random = (
+    train_x_qparego,
+    train_obj_qparego,
+    train_obj_true_qparego,
+)
+mll_qehvi, model_qehvi = initialize_model(train_x_qehvi, train_obj_qehvi)
+mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi, train_obj_qnehvi)
+
+# compute hypervolume
+bd = DominatedPartitioning(ref_point=problem.ref_point, Y=train_obj_true_qparego)
+volume = bd.compute_hypervolume().item()
+
+hvs_qparego.append(volume)
+hvs_qehvi.append(volume)
+hvs_qnehvi.append(volume)
+hvs_random.append(volume)
+
+# run N_BATCH rounds of BayesOpt after the initial random batch
+for iteration in range(1, N_BATCH + 1):
+
+    t0 = time.monotonic()
+
+    # fit the models
+    fit_gpytorch_mll(mll_qparego)
+    fit_gpytorch_mll(mll_qehvi)
+    fit_gpytorch_mll(mll_qnehvi)
+
+    # define the qEI and qNEI acquisition modules using a QMC sampler
+    qparego_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+    qehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+    qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))
+
+    # optimize acquisition functions and get new observations
+    (
+        new_x_qparego,
+        new_obj_qparego,
+        new_obj_true_qparego,
+    ) = optimize_qnparego_and_get_observation(
+        model_qparego, train_x_qparego, train_obj_qparego, qparego_sampler
+    )
+    new_x_qehvi, new_obj_qehvi, new_obj_true_qehvi = optimize_qehvi_and_get_observation(
+        model_qehvi, train_x_qehvi, train_obj_qehvi, qehvi_sampler
+    )
+    (
+        new_x_qnehvi,
+        new_obj_qnehvi,
+        new_obj_true_qnehvi,
+    ) = optimize_qnehvi_and_get_observation(
+        model_qnehvi, train_x_qnehvi, train_obj_qnehvi, qnehvi_sampler
+    )
+    new_x_random, new_obj_random, new_obj_true_random = generate_initial_data(
+        n=BATCH_SIZE
+    )
+
+    # update training points
+    train_x_qparego = torch.cat([train_x_qparego, new_x_qparego])
+    train_obj_qparego = torch.cat([train_obj_qparego, new_obj_qparego])
+    train_obj_true_qparego = torch.cat([train_obj_true_qparego, new_obj_true_qparego])
+
+    train_x_qehvi = torch.cat([train_x_qehvi, new_x_qehvi])
+    train_obj_qehvi = torch.cat([train_obj_qehvi, new_obj_qehvi])
+    train_obj_true_qehvi = torch.cat([train_obj_true_qehvi, new_obj_true_qehvi])
+
+    train_x_qnehvi = torch.cat([train_x_qnehvi, new_x_qnehvi])
+    train_obj_qnehvi = torch.cat([train_obj_qnehvi, new_obj_qnehvi])
+    train_obj_true_qnehvi = torch.cat([train_obj_true_qnehvi, new_obj_true_qnehvi])
+
+    train_x_random = torch.cat([train_x_random, new_x_random])
+    train_obj_random = torch.cat([train_obj_random, new_obj_random])
+    train_obj_true_random = torch.cat([train_obj_true_random, new_obj_true_random])
+
+    # update progress
+    for hvs_list, train_obj in zip(
+        (hvs_random, hvs_qparego, hvs_qehvi, hvs_qnehvi),
+        (
+            train_obj_true_random,
+            train_obj_true_qparego,
+            train_obj_true_qehvi,
+            train_obj_true_qnehvi,
+        ),
+    ):
+        # compute hypervolume
+        bd = DominatedPartitioning(ref_point=problem.ref_point, Y=train_obj)
+        volume = bd.compute_hypervolume().item()
+        hvs_list.append(volume)
+
+    # reinitialize the models so they are ready for fitting on next iteration
+    # Note: we find improved performance from not warm starting the model hyperparameters
+    # using the hyperparameters from the previous iteration
+    mll_qparego, model_qparego = initialize_model(train_x_qparego, train_obj_qparego)
+    mll_qehvi, model_qehvi = initialize_model(train_x_qehvi, train_obj_qehvi)
+    mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi, train_obj_qnehvi)
+
+    t1 = time.monotonic()
+
+    if verbose:
+        print(
+            f"\nBatch {iteration:>2}: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = "
+            f"({hvs_random[-1]:>4.2f}, {hvs_qparego[-1]:>4.2f}, {hvs_qehvi[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), "
+            f"time = {t1-t0:>4.2f}.",
+            end="",
+        )
+    else:
+        print(".", end="")
+
+
+
+
+
+
+
+
+
+
+Batch  1: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 0.00, 0.32, 2.37), time = 29.48.
+Batch  2: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.64, 27.56, 34.87), time = 19.98.
+Batch  3: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.64, 39.05, 48.44), time = 19.73.
+Batch  4: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.77, 47.75, 48.44), time = 17.50.
+Batch  5: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.77, 52.17, 48.44), time = 17.87.
+Batch  6: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 39.70, 53.12, 50.71), time = 13.42.
+Batch  7: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 45.20, 53.12, 53.03), time = 17.19.
+Batch  8: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 45.20, 53.93, 55.20), time = 18.76.
+Batch  9: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 47.48, 54.05, 55.48), time = 18.70.
+Batch 10: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 54.26, 55.61), time = 16.88.
+Batch 11: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 54.39, 56.15), time = 17.31.
+Batch 12: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 54.56, 56.63), time = 17.12.
+Batch 13: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 55.72, 57.04), time = 19.59.
+Batch 14: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 51.15, 55.80, 57.12), time = 16.62.
+Batch 15: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 51.15, 55.86, 57.12), time = 23.50.
+Batch 16: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 51.86, 55.91, 57.12), time = 17.27.
+Batch 17: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 52.02, 55.96, 57.42), time = 20.15.
+Batch 18: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 53.68, 55.98, 57.50), time = 22.07.
+Batch 19: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.64, 53.95, 56.02, 57.57), time = 20.06.
+Batch 20: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.64, 54.32, 56.03, 57.77), time = 26.89.
+
+
+
+
+
+
+
+
+

Plot the results

The plot below shows the a common metric of multi-objective optimization performance, the log hypervolume difference: the log difference between the hypervolume of the true pareto front and the hypervolume of the approximate pareto front identified by each algorithm. The log hypervolume difference is plotted at each step of the optimization for each of the algorithms.

+

The plot shows that $q$NEHVI outperforms $q$EHVI, $q$ParEGO, and Sobol.

+
+
+
+
+
+
In [15]:
+
+
+
import numpy as np
+from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+
+iters = np.arange(N_BATCH + 1) * BATCH_SIZE
+log_hv_difference_qparego = np.log10(problem.max_hv - np.asarray(hvs_qparego))
+log_hv_difference_qehvi = np.log10(problem.max_hv - np.asarray(hvs_qehvi))
+log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi))
+log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random))
+
+fig, ax = plt.subplots(1, 1, figsize=(8, 6))
+ax.errorbar(
+    iters,
+    log_hv_difference_rnd,
+    label="Sobol",
+    linewidth=1.5,
+)
+ax.errorbar(
+    iters,
+    log_hv_difference_qparego,
+    label="qNParEGO",
+    linewidth=1.5,
+)
+ax.errorbar(
+    iters,
+    log_hv_difference_qehvi,
+    label="qEHVI",
+    linewidth=1.5,
+)
+ax.errorbar(
+    iters,
+    log_hv_difference_qnehvi,
+    label="qNEHVI",
+    linewidth=1.5,
+)
+ax.set(
+    xlabel="number of observations (beyond initial points)",
+    ylabel="Log Hypervolume Difference",
+)
+ax.legend(loc="lower left")
+
+
+
+
+
+
+
+
Out[15]:
+
+
<matplotlib.legend.Legend at 0x7fd251ea2370>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Plot the true objectives at the evaluated designs colored by iteration

To examine optimization process from another perspective, we plot the true function values at the designs selected under each algorithm where the color corresponds to the BO iteration at which the point was collected. The plot on the right for $q$NEHVI shows that the $q$NEHVI quickly identifies the pareto front and most of its evaluations are very close to the pareto front. $q$NParEGO also identifies has many observations close to the pareto front, but relies on optimizing random scalarizations, which is a less principled way of optimizing the pareto front compared to $q$NEHVI, which explicitly attempts focuses on improving the pareto front. $q$EHVI uses the posterior mean as a plug-in estimator for the true function values at the in-sample points, whereas $q$NEHVI than integrating over the uncertainty at the in-sample designs Sobol generates random points and has few points close to the Pareto front.

+
+
+
+
+
+
In [16]:
+
+
+
from matplotlib.cm import ScalarMappable
+
+
+fig, axes = plt.subplots(1, 4, figsize=(23, 7), sharex=True, sharey=True)
+algos = ["Sobol", "qNParEGO", "qEHVI", "qNEHVI"]
+cm = plt.get_cmap("viridis")
+
+batch_number = torch.cat(
+    [
+        torch.zeros(2 * (problem.dim + 1)),
+        torch.arange(1, N_BATCH + 1).repeat(BATCH_SIZE, 1).t().reshape(-1),
+    ]
+).numpy()
+for i, train_obj in enumerate(
+    (
+        train_obj_true_random,
+        train_obj_true_qparego,
+        train_obj_true_qehvi,
+        train_obj_true_qnehvi,
+    )
+):
+    sc = axes[i].scatter(
+        train_obj[:, 0].cpu().numpy(),
+        train_obj[:, 1].cpu().numpy(),
+        c=batch_number,
+        alpha=0.8,
+    )
+    axes[i].set_title(algos[i])
+    axes[i].set_xlabel("Objective 1")
+axes[0].set_ylabel("Objective 2")
+norm = plt.Normalize(batch_number.min(), batch_number.max())
+sm = ScalarMappable(norm=norm, cmap=cm)
+sm.set_array([])
+fig.subplots_adjust(right=0.9)
+cbar_ax = fig.add_axes([0.93, 0.15, 0.01, 0.7])
+cbar = fig.colorbar(sm, cax=cbar_ax)
+cbar.ax.set_title("Iteration")
+
+
+
+
+
+
+
+
Out[16]:
+
+
Text(0.5, 1.0, 'Iteration')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/one_shot_kg.html b/website-old/_tutorials/one_shot_kg.html new file mode 100644 index 0000000000..c6d8bc9839 --- /dev/null +++ b/website-old/_tutorials/one_shot_kg.html @@ -0,0 +1,286 @@ + + + +
+
+
+
+

The one-shot Knowledge Gradient acquisition function

The Knowledge Gradient (KG) (see [2, 3]) is a look-ahead acquisition function that quantifies the expected increase in the maximum of the modeled black-box function $f$ from obtaining additional (random) observations collected at the candidate set $\mathbf{x}$. KG often shows improved Bayesian Optimization performance relative to simpler acquisition functions such as Expected Improvement, but in its traditional form it is computationally expensive and hard to implement.

+

BoTorch implements a generalized variant of parallel KG [3] given by +$$ \alpha_{\text{KG}}(\mathbf{x}) = + \mathbb{E}_{\mathcal{D}_{\mathbf{x}}} + \Bigl[\, \max_{x' \in \mathbb{X}} \mathbb{E} \left[ g(\xi)\right] \Bigr] - \mu, +$$ +where $\xi \sim \mathcal{P}(f(x') \mid \mathcal{D} \cup \mathcal{D}_{\mathbf{x}})$ is the posterior at $x'$ conditioned on $\mathcal{D}_{\mathbf{x}}$, the (random) dataset observed at $\mathbf{x}$, and $\mu := \max_{x}\mathbb{E}[g(f(x)) \mid \mathcal{D}]$.

+

In general, we recommend using Ax for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. You can use a custom BoTorch model and acquisition function in Ax, following the Using BoTorch with Ax tutorial. To use the KG acquisition function, it is sufficient to add "botorch_acqf_class": qKnowledgeGradient, to model_kwargs. The linked tutorial shows how to use a custom BoTorch model. If you'd like to let Ax choose which model to use based on the properties of the search space, you can skip the surrogate argument in model_kwargs.

+

Optimizing KG

The conventional approach for optimizing parallel KG (where $g(\xi) = \xi$) is to apply stochastic gradient ascent, with each gradient observation potentially being an average over multiple samples. For each sample $i$, the inner optimization problem $\max_{x_i \in \mathbb{X}} \mathbb{E} \left[ \xi^i \mid \mathcal{D}_{\mathbf{x}}^i \right]$ for the posterior mean is solved numerically. An unbiased stochastic gradient of KG can then be computed by leveraging the envelope theorem and the optimal points $\{x_i^*\}$. In this approach, every iteration requires solving numerous inner optimization problems, one for each outer sample, in order to estimate just one stochastic gradient.

+

The "one-shot" formulation of KG in BoTorch treats optimizing $\alpha_{\text{KG}}(\mathbf{x})$ as an entirely deterministic optimization problem. It involves drawing $N_{\!f} = $ num_fantasies fixed base samples $\mathbf{Z}_f:= \{ \mathbf{Z}^i_f \}_{1\leq i \leq N_{\!f}}$ for the outer expectation, sampling fantasy data $\{\mathcal{D}_{\mathbf{x}}^i(\mathbf{Z}_f^i)\}_{1\leq i \leq N_{\!f}}$, and constructing associated fantasy models $\{\mathcal{M}^i(\mathbf{Z}_f^i)\}_{1 \leq i \leq N_{\!f}}$. The inner maximization can then be moved outside of the sample average, resulting in the following optimization problem: +$$ +\max_{\mathbf{x} \in \mathbb{X}}\alpha_{\text{KG}}(\mathbf{x}) \approx \max_{\mathbf{x}\in \mathbb{X}, \mathbf{X}' \in \mathbb{X}^{N_{\!f}} } %=1}^{\!N_{\!f}}} +\sum_{i=1}^{N_{\!f}} \mathbb{E}\left[g(\xi^i)\right], +$$ +where $\xi^i \sim \mathcal{P}(f(x'^i) \mid \mathcal{D} \cup \mathcal{D}_{\mathbf{x}}^i(\mathbf{Z}_f^i))$ and $\mathbf{X}' := \{x'^i\}_{1 \leq i \leq N_{\!f}}$.

+

If the inner expectation does not have an analytic expression, one can also draw fixed base samples $\mathbf{Z}_I:= \{ \mathbf{Z}^i_I \}_{1\leq i\leq N_{\!I}}$ and use an MC approximation as with the standard MC acquisition functions of type MCAcquisitionFunction. In either case one is left with a deterministic optimization problem.

+

The key difference from the envelope theorem approach is that we do not solve the inner optimization problem to completion for every fantasy point for every gradient step with respect to $\mathbf{x}$. Instead, we solve the nested optimization problem jointly over $\mathbf{x}$ and the fantasy points $\mathbf{X}'$. The resulting optimization problem is of higher dimension, namely $(q + N_{\!f})d$ instead of $qd$, but unlike the envelope theorem formulation it can be solved as a single optimization problem, which can be solved using standard methods for deterministic optimization.

+

[1] M. Balandat, B. Karrer, D. R. Jiang, S. Daulton, B. Letham, A. G. Wilson, and E. Bakshy. BoTorch: A Framework for Efficient Monte-Carlo Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.

+

[2] P. Frazier, W. Powell, and S. Dayanik. A Knowledge-Gradient policy for sequential information collection. SIAM Journal on Control and Optimization, 2008.

+

[3] J. Wu and P. Frazier. The parallel knowledge gradient method for batch bayesian optimization. NIPS 2016.

+
+
+
+
+
+
+

Setting up a toy model

We'll fit a standard SingleTaskGP model on noisy observations of the synthetic function $f(x) = \sin(2 \pi x_1) * \cos(2 \pi x_2)$ in d=2 dimensions on the hypercube $[0, 1]^2$.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import math
+import torch
+
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from botorch.utils import standardize
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
In [2]:
+
+
+
bounds = torch.stack([torch.zeros(2), torch.ones(2)])
+
+train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(20, 2)
+train_Y = torch.sin(2 * math.pi * train_X[:, [0]]) * torch.cos(
+    2 * math.pi * train_X[:, [1]]
+)
+
+train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y))
+
+model = SingleTaskGP(train_X, train_Y)
+mll = ExactMarginalLogLikelihood(model.likelihood, model)
+fit_gpytorch_mll(mll);
+
+
+
+
+
+
+
+
+

Defining the qKnowledgeGradient acquisition function

The qKnowledgeGradient complies with the standard MCAcquisitionFunction API. The only mandatory argument in addition to the model is num_fantasies the number of fantasy samples. More samples result in a better approximation of KG, at the expense of both memory and wall time.

+

qKnowledgeGradient also supports the other parameters of MCAcquisitionFunction, such as a generic objective objective and pending points X_pending. It also accepts a current_value argument that is the maximum posterior mean of the current model (which can be obtained by maximizing PosteriorMean acquisition function). This does not change the optimizer so it is not required, but it means that the acquisition value is some constant shift of the actual "Knowledge Gradient" value.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.acquisition import qKnowledgeGradient
+
+
+NUM_FANTASIES = 128 if not SMOKE_TEST else 4
+qKG = qKnowledgeGradient(model, num_fantasies=NUM_FANTASIES)
+
+
+
+
+
+
+
+
+

Optimizing qKG

qKnowledgeGradient subclasses OneShotAcquisitionFunction, which makes sure that the fantasy parameterization $\mathbf{X}'$ is automatically generated and optimized when calling optimize_acqf on the acquisition function. This means that optimizing one-shot KG in BoTorch is just a easy as optimizing any other acquisition function (from an API perspective, at least). It turns out that a careful initialization of the fantasy points can significantly help with the optimization (see the logic in botorch.optim.initializers.gen_one_shot_kg_initial_conditions for more information).

+

Here we use num_restarts=10 random initial q-batches with q=2 in parallel, with the intialization heuristic starting from raw_samples = 512 raw points (note that since qKnowledgeGradient is significantly more expensive to evaluate than other acquisition functions, large values of num_restarts and raw_samples, which are typically feasible in other settings, can result in long wall times and potential memory issues).

+

Finally, since we do not pass a current_value argument, this value is not actually the KG value, but offset by the constant (w.r.t. the candidates) $\mu := \max_{x}\mathbb{E}[g(f(x)) \mid \mathcal{D}]$.

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.optim import optimize_acqf
+from botorch.utils.sampling import manual_seed
+
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+
+
+with manual_seed(1234):
+    candidates, acq_value = optimize_acqf(
+        acq_function=qKG,
+        bounds=bounds,
+        q=2,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+
+
+
+
+
+
+
+
In [5]:
+
+
+
candidates
+
+
+
+
+
+
+
+
Out[5]:
+
+
tensor([[0.1488, 1.0000],
+        [0.1084, 0.0012]])
+
+
+
+
+
+
+
+
In [6]:
+
+
+
acq_value
+
+
+
+
+
+
+
+
Out[6]:
+
+
tensor(2.4176)
+
+
+
+
+
+
+
+
+

Computing the actual KG value

We first need to find the maximum posterior mean - we can use a large number of random restarts and raw_samples to increase the likelihood that we do indeed find it (this is a non-convex optimization problem, after all).

+
+
+
+
+
+
In [7]:
+
+
+
from botorch.acquisition import PosteriorMean
+
+NUM_RESTARTS = 20 if not SMOKE_TEST else 2
+RAW_SAMPLES = 2048 if not SMOKE_TEST else 4
+
+
+argmax_pmean, max_pmean = optimize_acqf(
+    acq_function=PosteriorMean(model),
+    bounds=bounds,
+    q=1,
+    num_restarts=20 if not SMOKE_TEST else 2,
+    raw_samples=2048 if not SMOKE_TEST else 4,
+)
+
+
+
+
+
+
+
+
+

Now we can optimize KG after passing the current value. We also pass in the sampler from the original qKG above, which containst the fixed base samples $\mathbf{Z}_f$. This is to ensure that we optimize the same approximation and so our values are an apples-to-apples comparison (as num_fantasies increases, the effect of this randomness will get less and less important).

+
+
+
+
+
+
In [8]:
+
+
+
qKG_proper = qKnowledgeGradient(
+    model,
+    num_fantasies=NUM_FANTASIES,
+    sampler=qKG.sampler,
+    current_value=max_pmean,
+)
+
+with manual_seed(1234):
+    candidates_proper, acq_value_proper = optimize_acqf(
+        acq_function=qKG_proper,
+        bounds=bounds,
+        q=2,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+
+
+
+
+
+
+
+
In [9]:
+
+
+
candidates_proper
+
+
+
+
+
+
+
+
Out[9]:
+
+
tensor([[0.0000, 0.1795],
+        [0.1480, 0.0015]])
+
+
+
+
+
+
+
+
In [10]:
+
+
+
acq_value_proper
+
+
+
+
+
+
+
+
Out[10]:
+
+
tensor(0.1131)
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/optimize_stochastic.html b/website-old/_tutorials/optimize_stochastic.html new file mode 100644 index 0000000000..397fcf4e78 --- /dev/null +++ b/website-old/_tutorials/optimize_stochastic.html @@ -0,0 +1,228 @@ + + + +
+
+
+
+

Optimize acquisition functions using torch.optim

In this tutorial, we show how to use PyTorch's optim module for optimizing BoTorch MC acquisition functions. This is useful if the acquisition function is stochastic in nature (caused by re-sampling the base samples when using the reparameterization trick, or if the model posterior itself is stochastic).

+

Note: A pre-packaged, more user-friendly version of the optimization loop we will develop below is contained in the gen_candidates_torch function in the botorch.gen module. This tutorial should be quite useful if you would like to implement custom optimizers beyond what is contained in gen_candidates_torch.

+

As discussed in the CMA-ES tutorial, for deterministic acquisition functions BoTorch uses quasi-second order methods (such as L-BFGS-B or SLSQP) by default, which provide superior convergence speed in this situation.

+
+
+
+
+
+
+

Set up a toy model

We'll fit a SingleTaskGP model on noisy observations of the function $f(x) = 1 - \|x\|_2$ in d=5 dimensions on the hypercube $[-1, 1]^d$.

+
+
+
+
+
+
In [1]:
+
+
+
import torch
+
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+
+
+
+
+
+
+
+
+
I1116 182000.166 _utils_internal.py:179] NCCL_DEBUG env var is set to None
+
+
+
+
+
+
+
I1116 182000.167 _utils_internal.py:188] NCCL_DEBUG is INFO from /etc/nccl.conf
+
+
+
+
+
+
+
+
+
In [2]:
+
+
+
d = 5
+
+bounds = torch.stack([-torch.ones(d), torch.ones(d)])
+
+train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(50, d)
+train_Y = 1 - torch.linalg.norm(train_X, dim=-1, keepdim=True)
+
+model = SingleTaskGP(train_X, train_Y)
+mll = ExactMarginalLogLikelihood(model.likelihood, model)
+fit_gpytorch_mll(mll);
+
+
+
+
+
+
+
+
+

Define acquisition function

We'll use qExpectedImprovement with a StochasticSampler that uses a small number of MC samples. This results in a stochastic acquisition function that one should not attempt to optimize with the quasi-second order methods that are used by default in BoTorch's optimize_acqf function.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.acquisition import qExpectedImprovement
+from botorch.sampling.stochastic_samplers import StochasticSampler
+
+sampler = StochasticSampler(sample_shape=torch.Size([128]))
+qEI = qExpectedImprovement(model, best_f=train_Y.max(), sampler=sampler)
+
+
+
+
+
+
+
+
+

Optimizing the acquisition function

We will perform optimization over N=5 random initial q-batches with q=2 in parallel. We use N random restarts because the acquisition function is non-convex and as a result we may get stuck in local minima.

+
+
+
+
+
+
In [4]:
+
+
+
N = 5
+q = 2
+
+
+
+
+
+
+
+
+

Choosing initial conditions via a heuristic

Using random initial conditions in conjunction with gradient-based optimizers can be problematic because qEI values and their corresponding gradients are often zero in large parts of the feature space. To mitigate this issue, BoTorch provides a heuristic for generating promising initial conditions (this dirty and not-so-little secret of Bayesian Optimization is actually very important for overall closed-loop performance).

+

Given a set of q-batches $X'$ and associated acquisiton function values $Y'$, the initialize_q_batch_nonneg samples promising initial conditions $X$ (without replacement) from the multinomial distribution

+

$$ \mathbb{P}(X = X'_i) \sim \exp (\eta \tilde{Y}_i), \qquad \text{where} \;\; \tilde{Y}_i = \frac{Y'_i - \mu(Y)}{\sigma(Y)} \;\; \text{if} \;\; Y'_i >0 $$

+

and $\mathbb{P}(X = X'_j) = 0$ for all $j$ such that $Y'_j = 0$.

+

Fortunately, thanks to the high degree of parallelism in BoTorch, evaluating the acquisition function at a large number of randomly chosen points is quite cheap.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.optim.initializers import initialize_q_batch_nonneg
+
+# generate a large number of random q-batches
+Xraw = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(100 * N, q, d)
+Yraw = qEI(Xraw)  # evaluate the acquisition function on these q-batches
+
+# apply the heuristic for sampling promising initial conditions
+X, _ = initialize_q_batch_nonneg(Xraw, Yraw, N)
+
+# we'll want gradients for the input
+X.requires_grad_(True);
+
+
+
+
+
+
+
+
+

Optimizing the acquisition function

If you have used PyTorch, the basic optimization loop should be quite familiar. However, it is important to note that there is a key difference here compared to training ML models: When training ML models, one typically computes the gradient of an empirical loss function w.r.t. the model's parameters, while here we take the gradient of the acquisition function w.r.t. to the candidate set.

+

Thus, when setting the optimizer from torch.optim, we do not add the acquisition function's parameters as parameters to optimize (that would be quite bad!).

+

In this example, we use a vanilla Adam optimizer with fixed learning rate for a fixed number of iterations in order to keep things simple. But you can get as fancy as you want with learning rate scheduling, early termination, etc.

+

A couple of things to note:

+
    +
  1. Evaluating the acquisition function on the N x q x d-dim inputs means evaluating N q-batches in t-batch mode. The result of this is an N-dim tensor of acquisition function values, evaluated independently. To compute the gradient of the full input X via back-propagation, we can for convenience just compute the gradient of the sum of the losses.
  2. +
  3. torch.optim does not have good built in support for constraints (general constrained stochastic optimization is hard and still an open research area). Here we do something simple and project the value obtained after taking the gradient step to the feasible set - that is, we perform "projected stochastic gradient descent". Since the feasible set here is a hyperrectangle, this can be done by simple clamping. Another approach would be to transform the feasible interval for each dimension to the real line, e.g. by using a sigmoid function, and then optimizing in the unbounded transformed space.
  4. +
+
+
+
+
+
+
In [6]:
+
+
+
# set up the optimizer, make sure to only pass in the candidate set here
+optimizer = torch.optim.Adam([X], lr=0.01)
+X_traj = []  # we'll store the results
+
+# run a basic optimization loop
+for i in range(75):
+    optimizer.zero_grad()
+    # this performs batch evaluation, so this is an N-dim tensor
+    losses = -qEI(X)  # torch.optim minimizes
+    loss = losses.sum()
+
+    loss.backward()  # perform backward pass
+    optimizer.step()  # take a step
+
+    # clamp values to the feasible set
+    for j, (lb, ub) in enumerate(zip(*bounds)):
+        X.data[..., j].clamp_(lb, ub)  # need to do this on the data not X itself
+
+    # store the optimization trajecatory
+    X_traj.append(X.detach().clone())
+
+    if (i + 1) % 15 == 0:
+        print(f"Iteration {i+1:>3}/75 - Loss: {loss.item():>4.3f}")
+
+    # use your favorite convergence criterion here...
+
+
+
+
+
+
+
+
+
+
Iteration  15/75 - Loss: -0.924
+Iteration  30/75 - Loss: -1.281
+Iteration  45/75 - Loss: -1.374
+Iteration  60/75 - Loss: -1.363
+
+
+
+
+
+
+
Iteration  75/75 - Loss: -1.361
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/optimize_with_cmaes.html b/website-old/_tutorials/optimize_with_cmaes.html new file mode 100644 index 0000000000..ea5981aa1e --- /dev/null +++ b/website-old/_tutorials/optimize_with_cmaes.html @@ -0,0 +1,130 @@ + + + +
+
+
+
+

Optimize acquisition functions using CMA-ES

In this tutorial, we show how to use an external optimizer (in this case CMA-ES) for optimizing BoTorch acquisition functions. CMA-ES is a zero-th order optimizer, meaning that it only uses function evaluations and does not require gradient information. This is of course very useful if gradient information about the function to be optimized is unavailable.

+

In BoTorch, we typically do have gradient information available (thanks, autograd!). One is also generally better off using this information, rather than just ignoring it. However, for certain custom models or acquisition functions, we may not be able to backprop through the acquisition function and/or model. In such instances, using a zero-th order optimizer is appropriate.

+

For this example we use the PyCMA implementation of CMA-ES. PyCMA is easily installed via pip by running pip install cma.

+
+
+
+
+
+
+

Setting up the acquisition function

For the purpose of this tutorial, we'll use a basic UpperConfidenceBound acquisition function on a basic model fit on synthetic data. Please see the documentation for Models and Acquisition Functions for more information.

+
+
+
+
+
+
In [4]:
+
+
+
import math
+import torch
+
+from botorch.fit import fit_gpytorch_mll
+from botorch.models import SingleTaskGP
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+X = torch.rand(20, 2) - 0.5
+Y = (torch.sin(2 * math.pi * X[:, 0]) + torch.cos(2 * math.pi * X[:, 1])).unsqueeze(-1)
+Y += 0.1 * torch.randn_like(Y)
+
+gp = SingleTaskGP(X, Y)
+mll = ExactMarginalLogLikelihood(gp.likelihood, gp)
+fit_gpytorch_mll(mll);
+
+
+
+
+
+
+
+
In [5]:
+
+
+
from botorch.acquisition import UpperConfidenceBound
+
+UCB = UpperConfidenceBound(gp, beta=0.1)
+
+
+
+
+
+
+
+
+

Optimizing the acquisition function using CMA-ES

Note: Relative to sequential evaluations, parallel evaluations of the acqusition function are extremely fast in botorch (due to automatic parallelization across batch dimensions). In order to exploit this, we use the "ask/tell" interface to cma - this way we can batch-evaluate the whole CMA-ES population in parallel.

+

In this examle we use an initial standard deviation $\sigma_0 = 0.2$ and a population size $\lambda = 50$. +We also constrain the input X to the unit cube $[0, 1]^d$. +See cma's API Reference for more information on these options.

+

With this, we can optimize this acquistition function as follows:

+
+
+
+
+
+
In [6]:
+
+
+
import cma
+import numpy as np
+
+# get initial condition for CMAES in numpy form
+# note that CMAES expects a different shape (no explicit q-batch dimension)
+x0 = np.random.rand(2)
+
+# create the CMA-ES optimizer
+es = cma.CMAEvolutionStrategy(
+    x0=x0,
+    sigma0=0.2,
+    inopts={"bounds": [0, 1], "popsize": 50},
+)
+
+# speed up things by telling pytorch not to generate a compute graph in the background
+with torch.no_grad():
+
+    # Run the optimization loop using the ask/tell interface -- this uses
+    # PyCMA's default settings, see the PyCMA documentation for how to modify these
+    while not es.stop():
+        xs = es.ask()  # as for new points to evaluate
+        # convert to Tensor for evaluating the acquisition function
+        X = torch.tensor(xs, device=X.device, dtype=X.dtype)
+        # evaluate the acquisition function (optimizer assumes we're minimizing)
+        Y = -UCB(
+            X.unsqueeze(-2)
+        )  # acquisition functions require an explicit q-batch dimension
+        y = Y.view(-1).double().numpy()  # convert result to numpy array
+        es.tell(xs, y)  # return the result to the optimizer
+
+# convert result back to a torch tensor
+best_x = torch.from_numpy(es.best.x).to(X)
+
+best_x
+
+
+
+
+
+
+
+
+
+
(25_w,50)-aCMA-ES (mu_w=14.0,w_1=14%) in dimension 2 (seed=374178, Thu Aug  8 09:33:08 2019)
+
+
+
+
+
Out[6]:
+
+
tensor([0.2642, 0.0255])
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/preference_bo.html b/website-old/_tutorials/preference_bo.html new file mode 100644 index 0000000000..1f086340c7 --- /dev/null +++ b/website-old/_tutorials/preference_bo.html @@ -0,0 +1,415 @@ + + + +
+
+
+
+

Bayesian optimization with pairwise comparison data

In many real-world problems, people are faced with making multi-objective decisions. While it is often hard write down the exact utility function over those objectives, it is much easier for people to make pairwise comparisons. Drawing from utility theory and discrete choice models in economics, one can assume the user makes comparisons based on some intrinsic utility function and model the latent utility function using only the observed attributes and pairwise comparisons. +In machine learning terms, we are concerned with object ranking here. +This book has some more general discussions on this topic.

+

In this tutorial, we illustrate how to implement a simple Bayesian Optimization (BO) closed loop in BoTorch when we only observe (noisy) pairwise comparisons of the latent function values.

+
+
+
+
+
+
+

Data generation

Let's first generate some data that we are going to model.

+

In this tutorial, the latent function we aim to fit is the weighted sum of the input vector, where for dimension $i$, the weight is $\sqrt{i}$. +The input tensor X is randomly sampled within the d-dimensional unit cube.

+

Specifically, +$$ +y = f(X) = \sum_{i=1}^{d} \sqrt{i} X_i ~~\text{where}~~X \in [0, 1]^d +$$

+

This function is monotonically increasing in each individual dimension and has different weights for each input dimension, which are some properties that many real-world utility functions possess.

+

We generate the data using following code:

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import warnings
+from itertools import combinations
+
+import numpy as np
+import torch
+
+# Suppress potential optimization warnings for cleaner notebook
+warnings.filterwarnings("ignore")
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
In [2]:
+
+
+
# data generating helper functions
+def utility(X):
+    """Given X, output corresponding utility (i.e., the latent function)"""
+    # y is weighted sum of X, with weight sqrt(i) imposed on dimension i
+    weighted_X = X * torch.sqrt(torch.arange(X.size(-1), dtype=torch.float) + 1)
+    y = torch.sum(weighted_X, dim=-1)
+    return y
+
+
+def generate_data(n, dim=2):
+    """Generate data X and y"""
+    # X is randomly sampled from dim-dimentional unit cube
+    # we recommend using double as opposed to float tensor here for
+    # better numerical stability
+    X = torch.rand(n, dim, dtype=torch.float64)
+    y = utility(X)
+    return X, y
+
+
+def generate_comparisons(y, n_comp, noise=0.1, replace=False):
+    """Create pairwise comparisons with noise"""
+    # generate all possible pairs of elements in y
+    all_pairs = np.array(list(combinations(range(y.shape[0]), 2)))
+    # randomly select n_comp pairs from all_pairs
+    comp_pairs = all_pairs[
+        np.random.choice(range(len(all_pairs)), n_comp, replace=replace)
+    ]
+    # add gaussian noise to the latent y values
+    c0 = y[comp_pairs[:, 0]] + np.random.standard_normal(len(comp_pairs)) * noise
+    c1 = y[comp_pairs[:, 1]] + np.random.standard_normal(len(comp_pairs)) * noise
+    reverse_comp = (c0 < c1).numpy()
+    comp_pairs[reverse_comp, :] = np.flip(comp_pairs[reverse_comp, :], 1)
+    comp_pairs = torch.tensor(comp_pairs).long()
+
+    return comp_pairs
+
+
+torch.manual_seed(123)
+n = 50 if not SMOKE_TEST else 5
+m = 100 if not SMOKE_TEST else 10
+dim = 4
+noise = 0.1
+train_X, train_y = generate_data(n, dim=dim)
+train_comp = generate_comparisons(train_y, m, noise=noise)
+
+
+
+
+
+
+
+
+

train_X is a n x dim tensor;

+

train_y is a n-dimensional vector, representing the noise-free latent function value $y$;

+

train_comp is a m x 2 tensor, representing the noisy comparisons based on $\tilde{y} = y + N(0, \sigma^2)$, where train_comp[k, :] = (i, j) indicates $\tilde{y_i} > \tilde{y_j}$.

+

If y is the utility function value for a set of n items for a specific user, $\tilde{y_i} > \tilde{y_j}$ indicates (with some noise) the user prefers item i over item j.

+
+
+
+
+
+
+

PairwiseGP model fitting

In this problem setting, we never observe the actual function value. +Therefore, instead of fitting the model using (train_X, train_y) pair, we will fit the model with (train_X, train_comp).

+

PairwiseGP from BoTorch is designed to work with such pairwise comparison input. +We use PairwiseLaplaceMarginalLogLikelihood as the marginal log likelihood that we aim to maximize for optimizing the hyperparameters.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.fit import fit_gpytorch_mll
+from botorch.models.pairwise_gp import PairwiseGP, PairwiseLaplaceMarginalLogLikelihood
+from botorch.models.transforms.input import Normalize
+
+
+model = PairwiseGP(
+    train_X,
+    train_comp,
+    input_transform=Normalize(d=train_X.shape[-1]),
+)
+mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model)
+mll = fit_gpytorch_mll(mll)
+
+
+
+
+
+
+
+
+

Because the we never observe the latent function value, output values from the model are only meaningful on a relative scale. +Hence, given a test pair (test_X, test_y), we can evaluate the model using Kendall-Tau rank correlation.

+
+
+
+
+
+
In [4]:
+
+
+
from scipy.stats import kendalltau
+
+
+# Kendall-Tau rank correlation
+def eval_kt_cor(model, test_X, test_y):
+    pred_y = model.posterior(test_X).mean.squeeze().detach().numpy()
+    return kendalltau(pred_y, test_y).correlation
+
+
+n_kendall = 1000 if not SMOKE_TEST else 10
+
+test_X, test_y = generate_data(n_kendall, dim=dim)
+kt_correlation = eval_kt_cor(model, test_X, test_y)
+
+print(f"Test Kendall-Tau rank correlation: {kt_correlation:.4f}")
+
+
+
+
+
+
+
+
+
+
Test Kendall-Tau rank correlation: 0.8885
+
+
+
+
+
+
+
+
+
+

Perform Bayesian Optimization loop with EUBO

Now, we demonstrate how to implement a full Bayesian optimization with AnalyticExpectedUtilityOfBestOption (EUBO) acquisition function [4, 5].

+

The Bayesian optimization loop for a batch size of q simply iterates the following steps:

+
    +
  1. given a surrogate model, choose a batch of points $X_{next} = \{x_1, x_2, ..., x_q\}$
  2. +
  3. observe q_comp randomly selected pairs of (noisy) comparisons between elements in $X_{next}$
  4. +
  5. update the surrogate model with $X_{next}$ and the observed pairwise comparisons
  6. +
+

We start off by defining a few helper functions.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.acquisition.preference import AnalyticExpectedUtilityOfBestOption
+from botorch.optim import optimize_acqf
+
+
+def init_and_fit_model(X, comp):
+    """Model fitting helper function"""
+    model = PairwiseGP(
+        X,
+        comp,
+        input_transform=Normalize(d=X.shape[-1]),
+    )
+    mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+    return mll, model
+
+
+def make_new_data(X, next_X, comps, q_comp):
+    """Given X and next_X,
+    generate q_comp new comparisons between next_X
+    and return the concatenated X and comparisons
+    """
+    # next_X is float by default; cast it to the dtype of X (i.e., double)
+    next_X = next_X.to(X)
+    next_y = utility(next_X)
+    next_comps = generate_comparisons(next_y, n_comp=q_comp, noise=noise)
+    comps = torch.cat([comps, next_comps + X.shape[-2]])
+    X = torch.cat([X, next_X])
+    return X, comps
+
+
+
+
+
+
+
+
+

The Bayesian optimization loop is as follows (running the code may take a while).

+
+
+
+
+
+
In [6]:
+
+
+
algos = ["EUBO", "rand"]
+
+NUM_TRIALS = 3 if not SMOKE_TEST else 2
+NUM_BATCHES = 30 if not SMOKE_TEST else 2
+
+dim = 4
+NUM_RESTARTS = 3
+RAW_SAMPLES = 512 if not SMOKE_TEST else 8
+q = 2  # number of points per query
+q_comp = 1  # number of comparisons per query
+
+# initial evals
+best_vals = {}  # best observed values
+for algo in algos:
+    best_vals[algo] = []
+
+# average over multiple trials
+for i in range(NUM_TRIALS):
+    torch.manual_seed(i)
+    np.random.seed(i)
+    data = {}
+    models = {}
+
+    # Create initial data
+    init_X, init_y = generate_data(q, dim=dim)
+    comparisons = generate_comparisons(init_y, q_comp, noise=noise)
+    # X are within the unit cube
+    bounds = torch.stack([torch.zeros(dim), torch.ones(dim)])
+
+    for algo in algos:
+        best_vals[algo].append([])
+        data[algo] = (init_X, comparisons)
+        _, models[algo] = init_and_fit_model(init_X, comparisons)
+
+        best_next_y = utility(init_X).max().item()
+        best_vals[algo][-1].append(best_next_y)
+
+    # we make additional NUM_BATCHES comparison queries after the initial observation
+    for j in range(1, NUM_BATCHES + 1):
+        for algo in algos:
+            model = models[algo]
+            if algo == "EUBO":
+                # create the acquisition function object
+                acq_func = AnalyticExpectedUtilityOfBestOption(pref_model=model)
+                # optimize and get new observation
+                next_X, acq_val = optimize_acqf(
+                    acq_function=acq_func,
+                    bounds=bounds,
+                    q=q,
+                    num_restarts=NUM_RESTARTS,
+                    raw_samples=RAW_SAMPLES,
+                )
+            else:
+                # randomly sample data
+                next_X, _ = generate_data(q, dim=dim)
+
+            # update data
+            X, comps = data[algo]
+            X, comps = make_new_data(X, next_X, comps, q_comp)
+            data[algo] = (X, comps)
+
+            # refit models
+            _, models[algo] = init_and_fit_model(X, comps)
+
+            # record the best observed values so far
+            max_val = utility(X).max().item()
+            best_vals[algo][-1].append(max_val)
+
+
+
+
+
+
+
+
+

Plot the results

The plot below shows the best objective value observed at each step of the optimization for each of the acquisition functions. The error bars represent the 95% confidence intervals for the sample mean at that step in the optimization across the trial runs.

+
+
+
+
+
+
In [7]:
+
+
+
from matplotlib import pyplot as plt
+
+
+%matplotlib inline
+
+plt.rcParams.update({"font.size": 14})
+
+algo_labels = {
+    "rand": "Random Exploration",
+    "EUBO": "EUBO",
+}
+
+
+def ci(y):
+    return 1.96 * y.std(axis=0) / np.sqrt(y.shape[0])
+
+
+# the utility function is maximized at the full vector of 1
+optimal_val = utility(torch.tensor([[1] * dim])).item()
+iters = list(range(NUM_BATCHES + 1))
+
+fig, ax = plt.subplots(1, 1, figsize=(8, 6))
+# plot the optimal value
+ax.plot(
+    iters,
+    [optimal_val] * len(iters),
+    label="Optimal Function Value",
+    color="black",
+    linewidth=1.5,
+)
+
+# plot the the best observed value from each algorithm
+for algo in algos:
+    ys = np.vstack(best_vals[algo])
+    ax.errorbar(
+        iters, ys.mean(axis=0), yerr=ci(ys), label=algo_labels[algo], linewidth=1.5
+    )
+
+ax.set(
+    xlabel=f"Number of queries (q = {q}, num_comparisons = {q_comp})",
+    ylabel="Best observed value",
+    title=f"{dim}-dim weighted vector sum",
+)
+ax.legend(loc="best")
+
+
+
+
+
+
+
+
Out[7]:
+
+
<matplotlib.legend.Legend at 0x28e5ab0a0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

References

[1] Wei Chu, and Zoubin Ghahramani. 2005. “Preference Learning with Gaussian Processes.” In Proceedings of the 22Nd International Conference on Machine Learning, 137–44. ICML ’05. New York, NY, USA: ACM.

+

[2] Eric Brochu, Vlad M. Cora, and Nando de Freitas. 2010. “A Tutorial on Bayesian Optimization of Expensive Cost Functions, with Application to Active User Modeling and Hierarchical Reinforcement Learning.” arXiv [cs.LG]. arXiv.

+

[3] Javier González, Zhenwen Dai, Andreas Damianou, and Neil D. Lawrence. 2017. “Preferential Bayesian Optimization.” In Proceedings of the 34th International Conference on Machine Learning, edited by Doina Precup and Yee Whye Teh, 70:1282–91. Proceedings of Machine Learning Research. International Convention Centre, Sydney, Australia: PMLR.

+

[4] Zhiyuan Jerry Lin, Raul Astudillo, Peter I. Frazier, and Eytan Bakshy, Preference Exploration for Efficient Bayesian Optimization with Multiple Outcomes. AISTATS, 2022. https://arxiv.org/abs/2203.11382

+

[5] Raul Astudillo, Zhiyuan Jerry Lin, Eytan Bakshy, and Peter I. Frazier, qEUBO: A Decision-Theoretic Acquisition Function for Preferential Bayesian Optimization. AISTATS, 2023.

+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/risk_averse_bo_with_environmental_variables.html b/website-old/_tutorials/risk_averse_bo_with_environmental_variables.html new file mode 100644 index 0000000000..d4c91628de --- /dev/null +++ b/website-old/_tutorials/risk_averse_bo_with_environmental_variables.html @@ -0,0 +1,439 @@ + + + +
+
+
+
+

Risk averse Bayesian optimization with environmental variables

This notebook considers risk averse Bayesian optimization of objectives $f(x, w)$, where $x$ denotes the design variable and $w$ denotes the environmental variable. +The design variable $x$ is fully controlled by the practitioner, however, the environmental variable $w$ is only controllable at the experimentation phase and is determined by the environment once the decision $x$ is implemented, according to some probability distribution. +In this setting, with the $W$ denoting the random environmental variable, the objective we want to optimize becomes a random function, written as $f(x, W)$, whose value is determined only once the environmental variable $W$ is realized. +This formulation is relevant whenever we need to make a decision to be implemented in an unknown future environment, and we can simulate the environment during the optimization phase.

+

For this problem setting, [1] proposes to optimize a risk measure of the random function, written as $\rho[f(x, W)]$, where $\rho$ denotes a risk measure, which is a functional that maps a random variable (in this case $f(x, W)$ induced by $W$) to a real number. +They propose the $\rho$KG acquisition function, which extends the well-known knowledge-gradient acquisition function, and requires access to posterior mean of the objective, i.e., $\mathbb{E}_n[\rho[f(x, W)]]$, where the expectation is taken over the sample paths of the GP model. +Unlike the posterior mean of the function $f(x, w)$, the posterior mean of the risk measure is not available in closed-form and needs to be estimated via sampling. +The procedure for estimating $\mathbb{E}_n[\rho[f(x, W)]]$ for a given $x$ is as follows:

+
    +
  • Draw a set of n_w samples of $W$ according to the probability distribution. Let's call this w_set.
  • +
  • Append each $w$ in w_set to the given $x$ to get $(x, w)$ pairs. Note that for a single $x$, we now have n_w pairs of $(x, w)$.
  • +
  • Draw samples from the joint posterior distribution of these n_w pairs of $(x, w)$. Note that the joint distribution here is an n_w-dimensional Gaussian distribution.
  • +
  • Calculate the empirical risk measure corresponding to each sample, converting each n_w-dimensional posterior sample to a scalar sample of the risk measure.
  • +
  • Take the average of these risk measure samples to get the Monte-Carlo estimate of the posterior mean of the risk measure.
  • +
+

Now that the background is established, we are ready to implement a one-shot version of the $\rho$KG acquisition function proposed in [1], in native BoTorch. We will:

+
    +
  • Use AppendFeatures input transform to add the set of $W$ samples to each given $x$;
  • +
  • Calculate the joint posterior over these samples;
  • +
  • Use RiskMeasureMCObjective to convert these joint samples into samples of the risk measure;
  • +
  • And use the samples of the risk measure in qMultiFidelityKnowledgeGradient to define the $\rho$KG acquisition function.
  • +
+

We will use the (negated) Branin function as $f(x, w)$ with the first input dimension denoting $x$ and the second input dimension denoting $w$, and find the $x$ maximizing the CVaR risk measure at risk level $\alpha=0.7$. We will assume that $W$ has a uniform distribution over $[0, 1]$ and approximate the risk measure using $16$ (qMC) samples of $W$ at a given time.

+

CVaR, the Conditional Value-at-Risk, is a risk measure that measures the expectation of the worst outcomes (small rewards or large losses) with a total probability of $1 - \alpha$. +It is commonly defined as the conditional expectation of the reward function, with the condition that the reward is smaller than the corresponding $1 - \alpha$ quantile.

+

Note: Risk measures are typically studied in the context of a minimization problem (including in [1]), since it makes more sense to minimize "risk", and treat the larger values as being undesirable. Since the default behavior in BoTorch is to maximize the objective, the RiskMeasureMCObjective (and its subclasses) is defined w.r.t. the lower tail of the random variable, i.e., by treating the smaller values as undesirable. With this implementation, all that is needed to minimize a risk measure (of the original objective) is to negate the objective, as is done in this notebook.

+

[1] S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of Risk Measures. Advances in Neural Information Processing Systems 33, 2020.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import warnings
+from time import time
+
+import matplotlib.pyplot as plt
+import torch
+from botorch import fit_gpytorch_mll
+from botorch.acquisition import qMultiFidelityKnowledgeGradient, qSimpleRegret
+from botorch.acquisition.risk_measures import CVaR
+from botorch.models import SingleTaskGP
+from botorch.models.transforms import Standardize
+from botorch.models.transforms.input import AppendFeatures
+from botorch.optim import optimize_acqf
+from botorch.utils.sampling import draw_sobol_samples
+from botorch.utils.transforms import unnormalize
+from botorch.test_functions import Branin
+from gpytorch import ExactMarginalLogLikelihood
+from torch import Tensor
+
+%matplotlib inline
+
+warnings.filterwarnings("ignore")
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+BATCH_SIZE = 2 if not SMOKE_TEST else 1
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 128 if not SMOKE_TEST else 4
+N_W = 16 if not SMOKE_TEST else 2
+NUM_ITERATIONS = 20 if not SMOKE_TEST else 2
+NUM_FANTASIES = 16 if not SMOKE_TEST else 2
+
+tkwargs = {"device": "cpu", "dtype": torch.double}
+
+
+
+
+
+
+
+
+

Problem setup

We will initialize the Branin test function and define a wrapper around it to normalize the domain to $[0, 1]^2$.

+
+
+
+
+
+
In [2]:
+
+
+
test_function = Branin(negate=True)
+dim = test_function.dim
+
+
+def evaluate_function(X: Tensor) -> Tensor:
+    return test_function(unnormalize(X, test_function.bounds)).view(*X.shape[:-1], 1)
+
+
+
+
+
+
+
+
+

Model initialization

We will initialize the SingleTaskGP model on $8$ Sobol points drawn from the $(x, w)$ space. +In doing so, we will also pass in the AppendFeatures. We will re-initialize AppendFeatures with a new w_set at every model training to ensure adequate coverage of the $W$ space.

+
+
+
+
+
+
In [3]:
+
+
+
bounds = torch.stack([torch.zeros(dim), torch.ones(dim)]).to(**tkwargs)
+train_X = draw_sobol_samples(bounds=bounds, n=8, q=1).squeeze(-2).to(**tkwargs)
+train_Y = evaluate_function(train_X)
+
+
+def train_model(train_X: Tensor, train_Y: Tensor) -> SingleTaskGP:
+    r"""Returns a `SingleTaskGP` model trained on the inputs"""
+    w_set = (
+        draw_sobol_samples(n=N_W, q=1, bounds=bounds[:, -1:]).squeeze(-2).to(**tkwargs)
+    )
+    model = SingleTaskGP(
+        train_X,
+        train_Y,
+        input_transform=AppendFeatures(feature_set=w_set),
+        outcome_transform=Standardize(m=1),
+    )
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+    return model
+
+
+model = train_model(train_X, train_Y)
+
+
+
+
+
+
+
+
+

Define a helper function that performs the BO step

The helper function will initialize the qMultiFidelityKnowledgeGradient acquisition function with the risk measure objective, and optimize it to find the candidate to evaluate. +We use qMultiFidelityKnowledgeGradient instead of qKnowledgeGraient since it accepts a project callable, which we will use to ignore the $w$ present in the fantasy solutions before adding the w_set via the AppendFeatures input transform.

+
+
+
+
+
+
In [4]:
+
+
+
risk_measure = CVaR(alpha=0.7, n_w=N_W)
+
+
+def ignore_w(X: Tensor) -> Tensor:
+    r"""Remove `w` from the input."""
+    return X[..., :-1]
+
+
+def optimize_rho_kg_and_get_observation():
+    r"""Optimizes the rhoKG acquisition function, and returns a new candidate and observation."""
+    acqf = qMultiFidelityKnowledgeGradient(
+        model=model,
+        num_fantasies=NUM_FANTASIES,
+        objective=risk_measure,
+        project=ignore_w,
+    )
+
+    candidate, _ = optimize_acqf(
+        acq_function=acqf,
+        bounds=bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+
+    new_observations = evaluate_function(candidate)
+    return candidate, new_observations
+
+
+
+
+
+
+
+
+

Perform the Bayesian optimization loop with $\rho$KG

The BO loop iterates the following steps:

+
    +
  • Given the surrogate model, maximize the acquisition function to find the candidate(s) $(x, w)$ to evaluate;
  • +
  • Observe $f(x, w)$ for each candidate;
  • +
  • Update the surrogate model with the new observation.
  • +
+

Note: Running this may take a while.

+
+
+
+
+
+
In [5]:
+
+
+
start_time = time()
+
+for i in range(NUM_ITERATIONS):
+    print(f"Starting iteration {i}, total time: {time() - start_time:.3f} seconds.")
+    # optimize the acquisition function and get the observations
+    candidate, observations = optimize_rho_kg_and_get_observation()
+
+    # update the model with new observations
+    train_X = torch.cat([train_X, candidate], dim=0)
+    train_Y = torch.cat([train_Y, observations], dim=0)
+    model = train_model(train_X, train_Y)
+
+
+
+
+
+
+
+
+
+
Starting iteration 0, total time: 0.000 seconds.
+
+
+
+
+
+
+
Starting iteration 1, total time: 7.142 seconds.
+
+
+
+
+
+
+
Starting iteration 2, total time: 18.505 seconds.
+
+
+
+
+
+
+
Starting iteration 3, total time: 29.673 seconds.
+
+
+
+
+
+
+
Starting iteration 4, total time: 50.260 seconds.
+
+
+
+
+
+
+
Starting iteration 5, total time: 61.387 seconds.
+
+
+
+
+
+
+
Starting iteration 6, total time: 82.665 seconds.
+
+
+
+
+
+
+
Starting iteration 7, total time: 105.007 seconds.
+
+
+
+
+
+
+
Starting iteration 8, total time: 115.363 seconds.
+
+
+
+
+
+
+
Starting iteration 9, total time: 124.725 seconds.
+
+
+
+
+
+
+
Starting iteration 10, total time: 131.432 seconds.
+
+
+
+
+
+
+
Starting iteration 11, total time: 139.990 seconds.
+
+
+
+
+
+
+
Starting iteration 12, total time: 147.682 seconds.
+
+
+
+
+
+
+
Starting iteration 13, total time: 150.100 seconds.
+
+
+
+
+
+
+
Starting iteration 14, total time: 167.178 seconds.
+
+
+
+
+
+
+
Starting iteration 15, total time: 171.254 seconds.
+
+
+
+
+
+
+
Starting iteration 16, total time: 173.408 seconds.
+
+
+
+
+
+
+
Starting iteration 17, total time: 176.923 seconds.
+
+
+
+
+
+
+
Starting iteration 18, total time: 180.522 seconds.
+
+
+
+
+
+
+
Starting iteration 19, total time: 183.080 seconds.
+
+
+
+
+
+
+
+
+
+

Find the solution to implement

We will choose the solution to implement as the point maximizing the posterior expectation of the risk measure. Since this expectation is not available in closed form, we will maximize its qMC estimate as a surrogate. We will use a larger w_set here to get a more precise estimate.

+
+
+
+
+
+
In [6]:
+
+
+
# update the input transform of the already trained model
+w_set = draw_sobol_samples(n=128, q=1, bounds=bounds[:, -1:]).squeeze(-2).to(**tkwargs)
+new_transform = AppendFeatures(feature_set=w_set).eval()
+model.input_transform = new_transform
+
+risk_measure = CVaR(alpha=0.7, n_w=128)
+expected_risk_measure = qSimpleRegret(model=model, objective=risk_measure)
+
+final_candidate, expected_objective = optimize_acqf(
+    acq_function=expected_risk_measure,
+    bounds=bounds[:, :1],
+    q=1,
+    num_restarts=NUM_RESTARTS,
+    raw_samples=RAW_SAMPLES,
+)
+
+
+
+
+
+
+
+
+

Let's plot the true risk measure and see how we did

We can use the input transform and the risk measure we previously defined to make this part easier!

+

The plot shows that we found the global optimal solution and that our estimate of the risk measure at the optimal point is quite accurate.

+
+
+
+
+
+
In [7]:
+
+
+
plot_x = torch.linspace(0, 1, 100, **tkwargs).view(-1, 1)
+eval_X = new_transform(plot_x)
+eval_Y = evaluate_function(eval_X)
+plot_risk_measure = risk_measure(eval_Y)
+
+plt.figure(figsize=(12, 8))
+plt.title("True Risk Measure Objective and Solution Found")
+plt.plot(plot_x, plot_risk_measure)
+plt.scatter(final_candidate, expected_objective, marker="*", color="red", s=500)
+plt.xlabel("x")
+plt.ylabel("$\\rho[f(x, w)]$")
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/risk_averse_bo_with_input_perturbations.html b/website-old/_tutorials/risk_averse_bo_with_input_perturbations.html new file mode 100644 index 0000000000..2b805d97e7 --- /dev/null +++ b/website-old/_tutorials/risk_averse_bo_with_input_perturbations.html @@ -0,0 +1,514 @@ + + + +
+
+
+
+

Risk averse Bayesian optimization with input perturbations

This notebook considers risk averse Bayesian optimization of objectives $f(x + \Delta_x)$, where $x$ denotes the design variable and $\Delta_x$ denotes the perturbations to the inputs that are applied at the implementation phase, such as manufacturing errors. +The design variable $x$ is fully controlled by the practitioner, however, the input perturbation $\Delta_x$ is only controllable at the experimentation phase and is determined by the environment once the decision $x$ is implemented, according to some probability distribution. +This means that while optimizing the design, we can simulate $f(x)$ for any given $x$, however, once the optimization is done, the actual implemented solution becomes $x + \Delta_x$.

+

In this setting, we want to find high-performing designs that are also robust to the effects of the input perturbations. +To do so, we will follow the Bayesian optimization of risk measures framework introduced in [1]. +Please refer to the Risk averse Bayesian optimization with environmental variables notebook for additional background on this.

+

In this notebook, we will use the qNoisyExpectedImprovement acquisition function to optimize the VaR risk measure at risk level $\alpha=0.8$, computed w.r.t. the perturbations in the inputs. To do so, we will:

+
    +
  • Use InputPerturbation input transform to add a set of samples of $\Delta_x$ to each given $x$;
  • +
  • Calculate the joint posterior over these samples;
  • +
  • Use the RiskMeasureMCObjective to convert these joint samples into samples of the risk measure;
  • +
  • And use these risk measure samples to define the improvement in qNoisyExpectedImprovement.
  • +
+

We will use the (negated) SixHumpCamel test function, and assume that the input perturbations follow a Gaussian distribution with standard deviation of 5% of the parameter space (truncated to the parameter bounds). +During optimization, we will use 16 (qMC) samples of $\Delta_x$ to approximate the VaR risk measure.

+

VaR, the Value-at-Risk, is a risk measure that measures the worst possible outcome (small rewards or large losses) after excluding the worst outcomes with a total probability of $1 - \alpha$. +It is commonly used in finance for risk management, and corresponds to the $1 - \alpha$ quantile of the random variable.

+

Note: Risk measures are typically studied in the context of a minimization problem (including in [1]), since it makes more sense to minimize "risk", and treat the larger values as being undesirable. Since the default behavior in BoTorch is to maximize the objective, the RiskMeasureMCObjective (and its subclasses) is defined w.r.t. the lower tail of the random variable, i.e., by treating the smaller values as undesirable. With this implementation, all that is needed to minimize a risk measure (of the original objective) is to negate the objective, as is done in this notebook.

+

[1] S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of Risk Measures. Advances in Neural Information Processing Systems 33, 2020.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import warnings
+from time import time
+
+import matplotlib.pyplot as plt
+import numpy as np
+import torch
+from botorch import fit_gpytorch_mll
+from botorch.acquisition import qNoisyExpectedImprovement, qSimpleRegret
+from botorch.acquisition.risk_measures import VaR
+from botorch.models import SingleTaskGP
+from botorch.models.transforms import Standardize
+from botorch.models.transforms.input import InputPerturbation
+from botorch.sampling import SobolQMCNormalSampler
+from botorch.optim import optimize_acqf
+from botorch.utils.sampling import draw_sobol_samples, draw_sobol_normal_samples
+from botorch.utils.transforms import unnormalize
+from botorch.test_functions import SixHumpCamel
+from gpytorch import ExactMarginalLogLikelihood
+from torch import Tensor
+
+%matplotlib inline
+
+warnings.filterwarnings("ignore")
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+BATCH_SIZE = 2 if not SMOKE_TEST else 1
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 128 if not SMOKE_TEST else 4
+N_W = 16 if not SMOKE_TEST else 2
+NUM_ITERATIONS = 25 if not SMOKE_TEST else 2
+STD_DEV = 0.05
+ALPHA = 0.8
+
+tkwargs = {"device": "cpu", "dtype": torch.double}
+
+
+
+
+
+
+
+
+

Problem setup

We will initialize the SixHumpCamel test function and define a wrapper around it to normalize the domain to $[0, 1]^2$.

+
+
+
+
+
+
In [2]:
+
+
+
test_function = SixHumpCamel(negate=True)
+dim = test_function.dim
+
+
+def evaluate_function(X: Tensor) -> Tensor:
+    return test_function(unnormalize(X, test_function.bounds)).view(*X.shape[:-1], 1)
+
+
+
+
+
+
+
+
+

Model initialization

We will initialize the SingleTaskGP model on $8$ Sobol points. +In doing so, we will also pass in the InputPerturbation. We will re-initialize InputPerturbation with a new set perturbation_set at every model training to ensure adequate coverage of the perturbation space.

+
+
+
+
+
+
In [3]:
+
+
+
bounds = torch.stack([torch.zeros(dim), torch.ones(dim)]).to(**tkwargs)
+train_X = draw_sobol_samples(bounds=bounds, n=8, q=1).squeeze(-2).to(**tkwargs)
+train_Y = evaluate_function(train_X)
+
+
+def train_model(train_X: Tensor, train_Y: Tensor) -> SingleTaskGP:
+    r"""Returns a `SingleTaskGP` model trained on the inputs"""
+    intf = InputPerturbation(
+        perturbation_set=draw_sobol_normal_samples(d=dim, n=N_W, **tkwargs) * STD_DEV,
+        bounds=bounds,
+    )
+    model = SingleTaskGP(
+        train_X, train_Y, input_transform=intf, outcome_transform=Standardize(m=1)
+    )
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+    return model
+
+
+model = train_model(train_X, train_Y)
+
+
+
+
+
+
+
+
+

Define a helper function that performs the BO step

The helper function will initialize the qNoisyExpectedImprovement acquisition function with the risk measure objective, and optimize it to find the candidate to evaluate.

+
+
+
+
+
+
In [4]:
+
+
+
risk_measure = VaR(alpha=ALPHA, n_w=N_W)
+
+
+def optimize_acqf_and_get_observation():
+    r"""Optimizes the acquisition function, and returns a new candidate and observation."""
+    acqf = qNoisyExpectedImprovement(
+        model=model,
+        X_baseline=train_X,
+        sampler=SobolQMCNormalSampler(sample_shape=torch.Size([128])),
+        objective=risk_measure,
+        prune_baseline=True,
+    )
+
+    candidate, _ = optimize_acqf(
+        acq_function=acqf,
+        bounds=bounds,
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+
+    new_observations = evaluate_function(candidate)
+    return candidate, new_observations
+
+
+
+
+
+
+
+
+

Perform the Bayesian optimization loop

The BO loop iterates the following steps:

+
    +
  • Given the surrogate model, maximize the acquisition function to find the candidate(s) to evaluate;
  • +
  • Observe $f(x)$ for each candidate;
  • +
  • Update the surrogate model with the new observation(s).
  • +
+

Note: Running this may take a while.

+
+
+
+
+
+
In [5]:
+
+
+
start_time = time()
+
+for i in range(NUM_ITERATIONS):
+    print(f"Starting iteration {i}, total time: {time() - start_time:.3f} seconds.")
+    # optimize the acquisition function and get the observations
+    candidate, observations = optimize_acqf_and_get_observation()
+
+    # update the model with new observations
+    train_X = torch.cat([train_X, candidate], dim=0)
+    train_Y = torch.cat([train_Y, observations], dim=0)
+    model = train_model(train_X, train_Y)
+
+
+
+
+
+
+
+
+
+
Starting iteration 0, total time: 0.000 seconds.
+
+
+
+
+
+
+
Starting iteration 1, total time: 2.604 seconds.
+
+
+
+
+
+
+
Starting iteration 2, total time: 6.995 seconds.
+
+
+
+
+
+
+
Starting iteration 3, total time: 9.517 seconds.
+
+
+
+
+
+
+
Starting iteration 4, total time: 12.045 seconds.
+
+
+
+
+
+
+
Starting iteration 5, total time: 14.884 seconds.
+
+
+
+
+
+
+
Starting iteration 6, total time: 17.721 seconds.
+
+
+
+
+
+
+
Starting iteration 7, total time: 21.850 seconds.
+
+
+
+
+
+
+
Starting iteration 8, total time: 33.036 seconds.
+
+
+
+
+
+
+
Starting iteration 9, total time: 40.206 seconds.
+
+
+
+
+
+
+
Starting iteration 10, total time: 46.402 seconds.
+
+
+
+
+
+
+
Starting iteration 11, total time: 63.250 seconds.
+
+
+
+
+
+
+
Starting iteration 12, total time: 71.446 seconds.
+
+
+
+
+
+
+
Starting iteration 13, total time: 79.062 seconds.
+
+
+
+
+
+
+
Starting iteration 14, total time: 87.870 seconds.
+
+
+
+
+
+
+
Starting iteration 15, total time: 99.159 seconds.
+
+
+
+
+
+
+
Starting iteration 16, total time: 106.404 seconds.
+
+
+
+
+
+
+
Starting iteration 17, total time: 115.135 seconds.
+
+
+
+
+
+
+
Starting iteration 18, total time: 124.253 seconds.
+
+
+
+
+
+
+
Starting iteration 19, total time: 137.365 seconds.
+
+
+
+
+
+
+
Starting iteration 20, total time: 144.159 seconds.
+
+
+
+
+
+
+
Starting iteration 21, total time: 147.352 seconds.
+
+
+
+
+
+
+
Starting iteration 22, total time: 152.641 seconds.
+
+
+
+
+
+
+
Starting iteration 23, total time: 161.821 seconds.
+
+
+
+
+
+
+
Starting iteration 24, total time: 165.349 seconds.
+
+
+
+
+
+
+
+
+
+

Find the solution to implement

We will choose the solution to implement as the previously evaluated point that maximizes the posterior expectation of the risk measure. Since this expectation is not available in closed form, we will use its qMC estimate as a surrogate. We will use a larger perturbation_set here to get a more precise estimate.

+
+
+
+
+
+
In [6]:
+
+
+
# update the input transform of the already trained model
+new_intf = InputPerturbation(
+    perturbation_set=draw_sobol_normal_samples(d=dim, n=128, **tkwargs) * STD_DEV,
+    bounds=bounds,
+).eval()
+model.input_transform = new_intf
+
+risk_measure = VaR(alpha=ALPHA, n_w=128)
+expected_risk_measure = qSimpleRegret(model=model, objective=risk_measure)
+
+with torch.no_grad():
+    expected_rm_values = expected_risk_measure(train_X.unsqueeze(-2))
+expected_final_rm, max_idx = expected_rm_values.max(dim=0)
+final_candidate = train_X[max_idx]
+
+
+
+
+
+
+
+
+

Plotting the risk measure corresponding to the best observed point over iterations

As before, we define the best observed point as the previously evaluated point that maximizes the posterior expectation of the risk measure.

+
+
+
+
+
+
In [7]:
+
+
+
best_observed = torch.zeros(NUM_ITERATIONS + 1, **tkwargs)
+for i in range(NUM_ITERATIONS + 1):
+    best_observed[i] = expected_rm_values[: 6 + i * BATCH_SIZE].max()
+
+fig, ax = plt.subplots(figsize=(12, 8))
+ax.plot(best_observed)
+ax.set_xlabel("iterations")
+ax.set_ylabel("risk measure")
+ax.set_title("Best Observed Risk Measure")
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Plotting the true risk measure to see how we did

We can use the input transform and the risk measure we previously defined to make this part easier!

+

We plot both the response surface, $f(x)$, and the risk measure surface, $\rho[f(x + \Delta_x)]$, and mark the best risk averse solution found on both plots. +The plots are restricted to $[0.3, 0.7]^2$ to highlight more promising areas of the solution space.

+
+
+
+
+
+
In [8]:
+
+
+
n_plot = 100
+
+fig, axes = plt.subplots(ncols=2, figsize=(24, 10))
+
+for i, ax in enumerate(axes):
+    # generate a grid of `x` points to evaluate for plotting
+    x_ = np.linspace(0.3, 0.7, n_plot)
+    x1, x2 = np.meshgrid(x_, x_)
+    eval_x_grid = torch.cat(
+        [torch.from_numpy(x1).unsqueeze(-1), torch.from_numpy(x2).unsqueeze(-1)], dim=-1
+    )
+    if i == 0:
+        plot_values = evaluate_function(eval_x_grid).view(n_plot, n_plot)
+        ax.set_title("Function $f(x)$ and Solution Found")
+    else:
+        # add `delta_x` to each point, evalute the objective, and calculate the risk measure
+        eval_x_dx = new_intf(eval_x_grid)
+        eval_y = evaluate_function(eval_x_dx)
+        plot_values = risk_measure(eval_y).view(n_plot, n_plot)
+        ax.set_title("Objective $\\rho[f(x + \Delta_x)]$ and Solution Found")
+    contours = ax.contourf(x1, x2, plot_values, levels=40)
+    plt.colorbar(contours, ax=ax)
+    ax.scatter(final_candidate[0], final_candidate[1], marker="*", color="red", s=500)
+    ax.set_xlabel("$x_1$")
+    ax.set_ylabel("$x_2$")
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/robust_multi_objective_bo.html b/website-old/_tutorials/robust_multi_objective_bo.html new file mode 100644 index 0000000000..502a3f8e59 --- /dev/null +++ b/website-old/_tutorials/robust_multi_objective_bo.html @@ -0,0 +1,828 @@ + + + +
+
+
+
+

Robust Multi-Objective Bayesian Optimization Under Input Noise

In this tutorial, we illustrate how to perform robust multi-objective Bayesian optimization (BO) under input noise.

+

This is a simple tutorial; for support for constraints, batch sizes greater than 1, and many alternative methods, please see https://github.com/facebookresearch/robust_mobo.

+

We consider the problem of optimizing (maximizing) a vector-valued objective function $\mathbf f(\mathbf x)$ where at implementation time $\mathbf f(\mathbf x)$ is subject to input noise $\mathbf{f}(\mathbf{x} \diamond \mathbf{\xi})$ where $\mathbf{\xi} \sim P(\mathbf \xi | \mathbf x)$ is the random input noise and $\diamond$ denotes the perturbation function (e.g. addition, multiplication, or any arbitrary function).

+

We consider the scenario where:

+
    +
  1. We have access to a simulator during optimization such that $\mathbf{f}$ can be queried at a given design $\mathbf x$ without input noise.
  2. +
  3. Input noise is only present at implementation time. After optimization, the design that is chosen according to the decision maker's preferences will be subject to input noise.
  4. +
  5. The perturbation function is known.
  6. +
  7. We can sample from the generative process $P(\mathbf \xi | \mathbf x)$.
  8. +
+

Quantifying risk is important to understand how the final selected design will perform under input noise.

+

To quantify risk in the multi-objective setting, the MVaR set is an appealing option. For a given design $\mathbf x$, MVaR is theis the set of points such that for every $\mathbf z$ in the MVaR set, $\mathbf z$ is Pareto dominated by the objectives under input noise $\mathbf f (\mathbf x \diamond \mathbf \xi)$ with probability $\alpha$. In other words, if $\mathbf x$ is the chosen final design, the objectives will be better than $\mathbf z$ with probability $\alpha$ for all $\mathbf z$ in the MVaR set.

+

MVaR

+

However, during optimization we are interested in identifying the global MVaR set that is the optimal set of probabilistic lower bounds across all designs. The global MVaR set is the non-dominated set of points across the union of MVaR sets of all points in the design space. See [1] for a deeper discussion.

+

In this tutorial, we will optimize the 2 1-dimensional functions shown above to identify an approximate global MVaR set. See [1] for a description of these functions.

+

To do so, we will use Bayesian optimization with MARS (MVaR approximated via random scalarizations). MARS exploits the result in [1] that, under limited assumptions, there is a bijection between weights in the $M-1$-dimensional-simplex (where $M$ is the number of objectives) and points $\mathbf z$ in the MVaR set based on the value-at-risk (VaR) of a Chebyshev scalarization.

+

bijection

+

MARS leverages this result to efficiently identify the MVaR set using Bayesian optimization by, at each iteration, sampling a random Chebyshev scalarization and selecting the new design with maximum acquisition value with respect to the value-at-risk +of the sampled scalarization.

+

[1] S. Daulton, S. Cakmak, M. Balandat, M. A. Osborne, E. Zhou, and E. Bakshy. Robust Bayesian Optimziation Under Input Noise. ICML, 2022.

+
+
+
+
+
+
In [1]:
+
+
+
import torch
+import numpy as np
+import os
+
+tkwargs = {
+    "dtype": torch.double,
+    "device": torch.device("cuda:2" if torch.cuda.is_available() else "cpu"),
+}
+seed = 0
+torch.manual_seed(seed)
+np.random.seed(seed)
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Configure the problem and optimization

+
+
+
+
+
+
In [2]:
+
+
+
from botorch.test_functions.multi_objective import ToyRobust
+
+base_function = ToyRobust(negate=True).to(**tkwargs)  # define test function
+bounds = base_function.bounds
+n_w = (
+    2 if SMOKE_TEST else 32
+)  # number of MC samples for approximating input noise distribution
+alpha = 0.9  # probability level
+std_dev = 0.1  # zero-mean quasi-Normal input noise, with a standard deviation of 0.1
+search_space_range = bounds[1] - bounds[0]
+# scale the specified std_dev to a unit cube search space
+scaled_std_dev = (
+    torch.tensor(std_dev, dtype=bounds.dtype, device=bounds.device) / search_space_range
+)
+mc_samples = 2 if SMOKE_TEST else 256  # number of samples for MC acquisition functions
+hv_n_w = (
+    2 if SMOKE_TEST else 512
+)  # number of MC samples for approximating input noise distribution for omniscient evaluation
+mvar_ref_point = torch.tensor(
+    [-14.1951, -3.1887], **tkwargs
+)  # reference point for the MVaR frontier
+# options for acquisition optimization
+options = {
+    "batch_limit": 5,  # number of starting points to jointly optimize in L-BFGS-B
+    "maxiter": 2 if SMOKE_TEST else 200,  # maximum number of L-BFGS-B iterations
+}
+optimization_kwargs = {
+    "num_restarts": 2 if SMOKE_TEST else 20,  # number of random restarts for L-BFGS-B
+    "raw_samples": 10
+    if SMOKE_TEST
+    else 1024,  # number of random samples for initialization heuristic
+    "options": options,
+}
+iterations = 1 if SMOKE_TEST else 5  # number of BO iterations
+verbose = True
+n_initial_points = 4  # number of initial sobol points
+
+
+
+
+
+
+
+
+

Create a function for evaluating the objectives.

We work in a search space that is normalized to the unit cube and only unnormalize the search space to evaluate the objectives.

+
+
+
+
+
+
In [3]:
+
+
+
from botorch.utils.transforms import unnormalize
+
+# define function for evaluation
+def eval_problem(X):
+    X = unnormalize(X, base_function.bounds)
+    return base_function(X)
+
+
+
+
+
+
+
+
+

Create a function for sampling initial quasi-random points from the unit cube

+
+
+
+
+
+
In [4]:
+
+
+
from botorch.utils.sampling import draw_sobol_samples
+
+standard_bounds = torch.ones(2, base_function.dim, **tkwargs)
+standard_bounds[0] = 0
+
+
+def generate_initial_data(
+    n,
+    eval_problem,
+    bounds,
+    tkwargs,
+):
+    r"""
+    Generates the initial data for the experiments.
+    Args:
+        n: Number of training points.
+        eval_problem: The callable used to evaluate the objective function.
+        bounds: The bounds to generate the training points from. `2 x d`-dim tensor.
+        tkwargs: Arguments for tensors, dtype and device.
+    Returns:
+        The train_X and train_Y. `n x d` and `n x m`.
+    """
+    train_x = draw_sobol_samples(bounds=bounds, n=n, q=1).squeeze(-2).to(**tkwargs)
+    train_obj = eval_problem(train_x)
+    return train_x, train_obj
+
+
+
+
+
+
+
+
+

Create a utility module for evaluating the hypervolume of the MVaR frontier

We can evaluate the quality of an MVaR frontier by measuring the hypervolume dominated by the MVaR frontier and bounded from below by a reference point.

+
+
+
+
+
+
In [5]:
+
+
+
from botorch.acquisition.multi_objective.multi_output_risk_measures import MVaR
+from botorch.utils.multi_objective.box_decompositions.dominated import (
+    DominatedPartitioning,
+)
+from botorch.models.transforms.input import InputPerturbation
+
+
+class MVaRHV(torch.nn.Module):
+    r"""A helper class that calculates the HV of the MVaR set."""
+
+    def __init__(
+        self,
+        alpha,
+        eval_problem,
+        ref_point,
+        n_w,
+        perturbation_set,
+    ):
+        super().__init__()
+        self.hv = DominatedPartitioning(ref_point=ref_point)
+        self.mvar = MVaR(n_w=n_w, alpha=alpha)
+        self.perturbation = InputPerturbation(
+            perturbation_set=perturbation_set,
+        ).eval()
+        self.eval_problem = eval_problem
+
+    def forward(self, new_X):
+        r"""Calculate the resulting HV by adding the MVaR corresponding to the new_X
+        to the Pareto set.
+        Args:
+            new_X: `q x dim`-dim tensor of candidate points.
+        Returns:
+            The cumulative MVaR HV of all points evaluated so far.
+        """
+        # Get the corresponding MVaR set.
+        perturbed_X = self.perturbation(new_X)
+        perturbed_Y = self.eval_problem(perturbed_X)
+        new_mvar = self.mvar(perturbed_Y).view(-1, perturbed_Y.shape[-1])
+        # Update and return the new MVaR HV.
+        self.hv.update(new_mvar)
+        return self.hv.compute_hypervolume().item()
+
+
+
+
+
+
+
+
+

Create a method for initializing the surrogate model

+
+
+
+
+
+
In [6]:
+
+
+
from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.model_list_gp_regression import ModelListGP
+from gpytorch.mlls import SumMarginalLogLikelihood
+from botorch.models.transforms.outcome import Standardize
+
+
+def initialize_model(train_x, train_y, perturbation_set):
+    r"""Constructs the model and its MLL.
+    Args:
+        train_x: An `n x d`-dim tensor of training inputs.
+        train_y: An `n x m`-dim tensor of training outcomes.
+        perturbation_set: A `n_w x d`-dim tensor of perturbations
+    Returns:
+        The MLL and the model. Note: the model is not trained!
+    """
+    train_Yvar = torch.full_like(train_y, 1e-7) * train_y.std(dim=0).pow(2)
+    models = []
+    for i in range(train_y.shape[-1]):
+        models.append(
+            SingleTaskGP(
+                train_X=train_x,
+                train_Y=train_y[..., i : i + 1],
+                train_Yvar=train_Yvar[..., i : i + 1],
+                outcome_transform=Standardize(m=1),
+                input_transform=InputPerturbation(perturbation_set=perturbation_set),
+            )
+        )
+    model = ModelListGP(*models)
+    mll = SumMarginalLogLikelihood(model.likelihood, model)
+
+    return mll, model
+
+
+
+
+
+
+
+
+

Create a method for initializing MARS-NEI

We use the MARS approach with the NEI acquisition function as in [1].

+
+
+
+
+
+
In [7]:
+
+
+
from botorch.acquisition.multi_objective.multi_output_risk_measures import MARS
+from botorch.acquisition.monte_carlo import qNoisyExpectedImprovement
+from botorch.utils.sampling import sample_simplex
+
+
+def get_MARS_NEI(
+    model,
+    n_w,
+    X_baseline,
+    sampler,
+    mvar_ref_point,
+):
+    r"""Construct the NEI acquisition function with VaR of Chebyshev scalarizations.
+    Args:
+        model: A fitted multi-output GPyTorchModel.
+        n_w: the number of perturbation samples
+        X_baseline: An `r x d`-dim tensor of points already observed.
+        sampler: The sampler used to draw the base samples.
+        mvar_ref_point: The mvar reference point.
+    Returns:
+        The NEI acquisition function.
+    """
+    # sample weights from the simplex
+    weights = sample_simplex(
+        d=mvar_ref_point.shape[0],
+        n=1,
+        dtype=X_baseline.dtype,
+        device=X_baseline.device,
+    ).squeeze(0)
+    # set up mars objective
+    mars = MARS(
+        alpha=alpha,
+        n_w=n_w,
+        chebyshev_weights=weights,
+        ref_point=mvar_ref_point,
+    )
+    # set normalization bounds for the scalarization
+    mars.set_baseline_Y(model=model, X_baseline=X_baseline)
+    # initial qNEI acquisition function with the MARS objective
+    acq_func = qNoisyExpectedImprovement(
+        model=model,
+        X_baseline=X_baseline,
+        objective=mars,
+        prune_baseline=True,
+        sampler=sampler,
+    )
+    return acq_func
+
+
+
+
+
+
+
+
+

Set up the optimization

+
+
+
+
+
+
In [8]:
+
+
+
# Get the initial data.
+X, Y = generate_initial_data(
+    n=n_initial_points,
+    eval_problem=eval_problem,
+    bounds=standard_bounds,
+    tkwargs=tkwargs,
+)
+
+
+
+
+
+
+
+
In [9]:
+
+
+
from botorch.utils.sampling import draw_sobol_normal_samples
+
+# Ensure consistency of MVaRHV across seeds by using same perturbations.
+# This sets the random seed and generates the perturbations on CPU.
+# MVaR calculations are also moved to CPU.
+old_state = torch.random.get_rng_state()
+torch.manual_seed(0)
+perturbations = (
+    draw_sobol_normal_samples(d=base_function.dim, n=hv_n_w, **tkwargs) * scaled_std_dev
+)
+mvar_hv = MVaRHV(
+    alpha=alpha,
+    eval_problem=eval_problem,
+    ref_point=torch.tensor(mvar_ref_point, **tkwargs),
+    n_w=hv_n_w,
+    perturbation_set=perturbations,
+)
+torch.random.set_rng_state(old_state)
+
+
+
+
+
+
+
+
In [10]:
+
+
+
try:
+    all_mvar_hvs = torch.tensor([mvar_hv(X)], dtype=tkwargs["dtype"])
+except RuntimeError:
+    # Try to feed them one by one. This helps with memory.
+    initial_mvar_hv = 0.0
+    for j in range(X.shape[0]):
+        initial_mvar_hv = mvar_hv(X[j : j + 1])
+    all_mvar_hvs = torch.tensor([initial_mvar_hv], dtype=tkwargs["dtype"])
+
+
+
+
+
+
+
+
+

Run BO with MARS

+
+
+
+
+
+
In [11]:
+
+
+
import gc
+import gpytorch.settings as gpt_settings
+from time import time
+from botorch.fit import fit_gpytorch_mll
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.optim.optimize import optimize_acqf
+
+start = time()
+for i in range(iterations):
+    if verbose:
+        print(
+            f"Starting iteration {i}, "
+            f"time: {time()-start}, current MVaR HV: {all_mvar_hvs[-1]}."
+        )
+
+    # Generate the perturbations for evaluation
+    perturbation_set = (
+        draw_sobol_normal_samples(d=base_function.dim, n=n_w, **tkwargs)
+        * scaled_std_dev
+    )
+    # Fit the model.
+    mll, model = initialize_model(
+        train_x=X, train_y=Y, perturbation_set=perturbation_set
+    )
+    fit_gpytorch_mll(mll)
+
+    with gpt_settings.cholesky_max_tries(6):
+        # Construct the acqf.
+        sampler = SobolQMCNormalSampler(sample_shape=torch.Size([mc_samples]))
+        acq_func = get_MARS_NEI(
+            model=model,
+            n_w=n_w,
+            X_baseline=X,
+            sampler=sampler,
+            mvar_ref_point=mvar_ref_point,
+        )
+
+        # Optimize the acqf.
+        while options["batch_limit"] >= 1:
+            # Try to get around OOM by reducing batch_limit.
+            try:
+                torch.cuda.empty_cache()
+                candidates, _ = optimize_acqf(
+                    acq_function=acq_func,
+                    bounds=standard_bounds,
+                    q=1,
+                    **optimization_kwargs,
+                )
+                torch.cuda.empty_cache()
+                break
+            except RuntimeError as e:
+                if options["batch_limit"] > 1:
+                    print(
+                        "Got a RuntimeError in `optimize_acqf`. "
+                        "Trying with reduced `batch_limit`."
+                    )
+                    options["batch_limit"] //= 2
+                    continue
+                else:
+                    raise e
+    # free memory
+    del acq_func, mll, model
+    gc.collect()
+    torch.cuda.empty_cache()
+
+    # Get the new observations and update the data.
+    new_y = eval_problem(candidates)
+    X = torch.cat([X, candidates], dim=0)
+    Y = torch.cat([Y, new_y], dim=0)
+    new_mvar_hv = mvar_hv(candidates)
+    all_mvar_hvs = torch.cat(
+        [all_mvar_hvs, torch.tensor([new_mvar_hv], dtype=tkwargs["dtype"])], dim=0
+    )
+
+
+
+
+
+
+
+
+
+
Starting iteration 0, time: 0.00027441978454589844, current MVaR HV: 42.430757642706055.
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:
+
+Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+
+
+
+
+
+
+
+
Starting iteration 1, time: 9.476728200912476, current MVaR HV: 85.50895176532245.
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:
+
+Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:306: RuntimeWarning:
+
+Optimization failed in `gen_candidates_scipy` with the following warning(s):
+[NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal')]
+Trying again with a new set of initial conditions.
+
+
+
+
+
+
+
+
Starting iteration 2, time: 24.17291235923767, current MVaR HV: 87.13964153247537.
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:
+
+Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+
+/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/linear_operator/utils/cholesky.py:40: NumericalWarning:
+
+A not p.d., added jitter of 1.0e-08 to the diagonal
+
+
+
+
+
+
+
+
Starting iteration 3, time: 29.630997896194458, current MVaR HV: 87.148383606772.
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:
+
+Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/linear_operator/utils/cholesky.py:40: NumericalWarning:
+
+A not p.d., added jitter of 1.0e-08 to the diagonal
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:306: RuntimeWarning:
+
+Optimization failed in `gen_candidates_scipy` with the following warning(s):
+[NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal')]
+Trying again with a new set of initial conditions.
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:328: RuntimeWarning:
+
+Optimization failed on the second try, after generating a new set of initial conditions.
+
+
+
+
+
+
+
+
Starting iteration 4, time: 43.48030400276184, current MVaR HV: 89.14242777378423.
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:
+
+Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/linear_operator/utils/cholesky.py:40: NumericalWarning:
+
+A not p.d., added jitter of 1.0e-08 to the diagonal
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:306: RuntimeWarning:
+
+Optimization failed in `gen_candidates_scipy` with the following warning(s):
+[NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal')]
+Trying again with a new set of initial conditions.
+
+
+
+
+
+
+
+
/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:328: RuntimeWarning:
+
+Optimization failed on the second try, after generating a new set of initial conditions.
+
+
+
+
+
+
+
+
+
+
+

Evaluate Results

+
+
+
+
+
+
+

First we evaluate the hypervolume dominated by the MvaR frontier and bounded from below by the reference point. A larger hypervolume means a better MVaR frontier.

+
+
+
+
+
+
In [12]:
+
+
+
import matplotlib.pyplot as plt
+
+%matplotlib inline
+plt.plot(torch.arange(all_mvar_hvs.shape[0]), all_mvar_hvs)
+plt.ylabel("MVaR HV")
+plt.xlabel("BO Iterations")
+
+
+
+
+
+
+
+
Out[12]:
+
+
Text(0.5, 0, 'BO Iterations')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Next, we plot the mvar frontier to see the possible probabilistic lower bounds. For each point $\mathbf z$ in the MVaR set, there is a previously evaluated design that will be at least as good as $\mathbf z$ with probability $\alpha$.

+
+
+
+
+
+
In [15]:
+
+
+
from botorch.utils.multi_objective.pareto import is_non_dominated
+
+# Evaluate true MVaR
+# Perturb X
+perturbed_X = mvar_hv.perturbation(X)
+# Compute objectives at perturbed points
+true_Y_under_noise = eval_problem(perturbed_X)
+# calculate the MVaR frontier for each point X
+mvar_points = mvar_hv.mvar(true_Y_under_noise)
+# calculate the pareto frontier over the union of individual MVaR frontiers for each design
+mvar_frontier = mvar_points[is_non_dominated(mvar_points)].cpu()
+
+
+
+
+
+
+
+
In [16]:
+
+
+
plt.plot(
+    mvar_frontier[:, 0], mvar_frontier[:, 1], ".", alpha=0.4, label="MVaR Frontier"
+)
+plt.xlabel("Objective 1")
+plt.ylabel("Objective 2")
+plt.legend()
+
+
+
+
+
+
+
+
Out[16]:
+
+
<matplotlib.legend.Legend at 0x7f242df55a30>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Finally, we can plot the MVaR frontier for each evaluated design. Clearly some designs are far more robust than others under input noise.

+
+
+
+
+
+
In [18]:
+
+
+
for i, y in enumerate(true_Y_under_noise.view(X.shape[0], hv_n_w, -1).cpu()):
+    plt.plot(y[:, 0], y[:, 1], ".", color=f"C{i}", label=f"x_{i}", alpha=0.3)
+plt.xlabel("Objective 1")
+plt.ylabel("Objective 2")
+plt.legend()
+
+
+
+
+
+
+
+
Out[18]:
+
+
<matplotlib.legend.Legend at 0x7f242df52580>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/saasbo.html b/website-old/_tutorials/saasbo.html new file mode 100644 index 0000000000..db454c08a6 --- /dev/null +++ b/website-old/_tutorials/saasbo.html @@ -0,0 +1,569 @@ + + + +
+
+
+
+

High-Dimensional sample-efficient Bayesian Optimization with SAASBO

This tutorial shows how to use the Sparse Axis-Aligned Subspace Bayesian Optimization (SAASBO) +method for high-dimensional Bayesian optimization [1]. SAASBO places strong priors on the +inverse lengthscales to avoid overfitting in high-dimensional spaces. Specifically, SAASBO +uses a hierarchical sparsity prior consisting of a global shrinkage parameter +$\tau \sim \mathcal{HC}(\beta)$ and inverse lengthscales $\rho_d \sim \mathcal{HC}(\tau)$ +for $d=1, \ldots, D$, where $\mathcal{HC}$ is the half-Cauchy distribution. +While half-Cauchy priors favor values near zero they also have heavy tails, which allows the +inverse lengthscales of the most important parameters to escape zero. To perform inference in the +SAAS model we use Hamiltonian Monte Carlo (HMC) as we found that to outperform MAP inference.

+

We find that SAASBO performs well on problems with hundreds of dimensions. As we rely on HMC +and in particular the No-U-Turn-Sampler (NUTS) for inference, the overhead of SAASBO scales +cubically with the number of datapoints. Depending on the problem, using more than a few hundred +evaluations may not be feasible as SAASBO is designed for problems with a limited evaluation budget.

+

In general, we recommend using Ax for a simple BO setup like this one. See here for a SAASBO tutorial in Ax, which uses the Log Noisy Expected Improvement acquisition function. Therefore, this tutorial shows a minimal illustrative example of how to use SAASBO with only BoTorch. To customize the acquisition function used with SAASBO in Ax, see the custom acquisition tutorial, where adding \"surrogate\": Surrogate(SaasFullyBayesianSingleTaskGP), to the model_kwargs of BOTORCH_MODULAR step is sufficient to enable the SAAS model.

+

[1]: D. Eriksson, M. Jankowiak. High-Dimensional Bayesian Optimization with Sparse Axis-Aligned Subspaces. Proceedings of the Thirty-Seventh Conference on Uncertainty in Artificial Intelligence, 2021.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+
+import torch
+from torch.quasirandom import SobolEngine
+
+from botorch import fit_fully_bayesian_model_nuts
+from botorch.acquisition.logei import qLogExpectedImprovement
+from botorch.models.fully_bayesian import SaasFullyBayesianSingleTaskGP
+from botorch.models.transforms import Standardize
+from botorch.optim import optimize_acqf
+from botorch.test_functions import Branin
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
In [2]:
+
+
+
tkwargs = {
+    "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"),
+    "dtype": torch.double,
+}
+
+
+
+
+
+
+
+
+

The time to fit the SAAS model can be decreased by lowering +WARMUP_STEPS and NUM_SAMPLES.

+

We recommend using 512 warmup steps and 256 samples when +possible and to not use fewer than 256 warmup steps and 128 samples. By default, we only +keep each 16th sample which with 256 samples results in 32 hyperparameter samples.

+

To make this tutorial run faster we use 256 warmup steps and 128 samples.

+
+
+
+
+
+
In [3]:
+
+
+
WARMUP_STEPS = 256 if not SMOKE_TEST else 32
+NUM_SAMPLES = 128 if not SMOKE_TEST else 16
+THINNING = 16
+
+
+
+
+
+
+
+
+

Simple model fitting

We generate a simple function that only depends on the first parameter and show that the SAAS +model sets all other lengthscales to large values.

+
+
+
+
+
+
In [4]:
+
+
+
train_X = torch.rand(10, 4, **tkwargs)
+test_X = torch.rand(5, 4, **tkwargs)
+train_Y = torch.sin(train_X[:, :1])
+test_Y = torch.sin(test_X[:, :1])
+
+
+
+
+
+
+
+
+

By default, we infer the unknown noise variance in the data. You can also pass in a known +noise variance (train_Yvar) for each observation, which may be useful in cases where you for example +know that the problem is noise-free and can then set the noise variance to a small value such as 1e-6.

+

In this case you can construct a model as follows:

+
gp = SaasFullyBayesianSingleTaskGP(train_X=train_X, train_Y=train_Y, train_Yvar=torch.full_like(train_Y, 1e-6))
+
+
+
+
+
+
+
In [5]:
+
+
+
gp = SaasFullyBayesianSingleTaskGP(
+    train_X=train_X,
+    train_Y=train_Y,
+    outcome_transform=Standardize(m=1)
+)
+fit_fully_bayesian_model_nuts(
+    gp,
+    warmup_steps=WARMUP_STEPS,
+    num_samples=NUM_SAMPLES,
+    thinning=THINNING,
+    disable_progbar=True,
+)
+with torch.no_grad():
+    posterior = gp.posterior(test_X)
+
+
+
+
+
+
+
+
+

Computing the median lengthscales over the MCMC dimensions makes it clear that the first feature has the smallest lengthscale

+
+
+
+
+
+
In [6]:
+
+
+
print(gp.median_lengthscale.detach())
+
+
+
+
+
+
+
+
+
+
tensor([ 2.6688, 19.3581, 30.6755, 26.3881], dtype=torch.float64)
+
+
+
+
+
+
+
+
+
+

Make predictions with the model

In the next cell we show how to make predictions with the SAAS model. You compute the mean +and variance for test points just like for any other BoTorch posteriors. Note that the mean +and posterior will have an extra batch dimension at -3 that corresponds to the number of MCMC +samples (which is 8 in this tutorial).

+
+
+
+
+
+
In [7]:
+
+
+
print(posterior.mean.shape)
+print(posterior.variance.shape)
+
+
+
+
+
+
+
+
+
+
torch.Size([8, 5, 1])
+torch.Size([8, 5, 1])
+
+
+
+
+
+
+
+
+
+

We also provide several convenience methods for computing different statistics over the MCMC samples:

+
mixture_mean = posterior.mixture_mean
+mixture_variance = posterior.mixture_variance
+mixture_quantile = posterior.quantile(q=0.95)
+
+
+
+
+
+
+
In [8]:
+
+
+
print(f"Ground truth:     {test_Y.squeeze(-1)}")
+print(f"Mixture mean:     {posterior.mixture_mean.squeeze(-1)}")
+
+
+
+
+
+
+
+
+
+
Ground truth:     tensor([0.1842, 0.3531, 0.6900, 0.2710, 0.6056], dtype=torch.float64)
+Mixture mean:     tensor([0.1837, 0.3490, 0.6888, 0.2658, 0.6045], dtype=torch.float64)
+
+
+
+
+
+
+
+
+
+

Optimize Branin embedded in a 30D space

We take the standard 2D Branin problem and embed it in a 30D space. In particular, +we let dimensions 0 and 1 correspond to the true dimensions. We will show that +SAASBO is able to identify the important dimensions and efficiently optimize this function. +We work with the domain $[0, 1]^d$ and unnormalize the inputs to the true domain of Branin +before evaluating the function.

+
+
+
+
+
+
In [9]:
+
+
+
branin = Branin().to(**tkwargs)
+
+
+def branin_emb(x):
+    """x is assumed to be in [0, 1]^d"""
+    lb, ub = branin.bounds
+    return branin(lb + (ub - lb) * x[..., :2])
+
+
+
+
+
+
+
+
In [10]:
+
+
+
DIM = 30 if not SMOKE_TEST else 2
+
+# Evaluation budget
+N_INIT = 10
+N_ITERATIONS = 8 if not SMOKE_TEST else 1
+BATCH_SIZE = 5 if not SMOKE_TEST else 1
+print(f"Using a total of {N_INIT + BATCH_SIZE * N_ITERATIONS} function evaluations")
+
+
+
+
+
+
+
+
+
+
Using a total of 50 function evaluations
+
+
+
+
+
+
+
+
+
+

Run the optimization

We use 10 initial Sobol points followed by 8 iterations of BO using a batch size of 5, +which results in a total of 50 function evaluations. As our goal is to minimize Branin, we flip +the sign of the function values before fitting the SAAS model as the BoTorch acquisition +functions assume maximization.

+
+
+
+
+
+
In [11]:
+
+
+
X = SobolEngine(dimension=DIM, scramble=True, seed=0).draw(N_INIT).to(**tkwargs)
+Y = branin_emb(X).unsqueeze(-1)
+print(f"Best initial point: {Y.min().item():.3f}")
+
+for i in range(N_ITERATIONS):
+    train_Y = -1 * Y  # Flip the sign since we want to minimize f(x)
+    gp = SaasFullyBayesianSingleTaskGP(
+        train_X=X,
+        train_Y=train_Y,
+        train_Yvar=torch.full_like(train_Y, 1e-6),
+        outcome_transform=Standardize(m=1),
+    )
+    fit_fully_bayesian_model_nuts(
+        gp,
+        warmup_steps=WARMUP_STEPS,
+        num_samples=NUM_SAMPLES,
+        thinning=THINNING,
+        disable_progbar=True,
+    )
+
+    EI = qLogExpectedImprovement(model=gp, best_f=train_Y.max())
+    candidates, acq_values = optimize_acqf(
+        EI,
+        bounds=torch.cat((torch.zeros(1, DIM), torch.ones(1, DIM))).to(**tkwargs),
+        q=BATCH_SIZE,
+        num_restarts=10,
+        raw_samples=1024,
+    )
+
+    Y_next = torch.cat([branin_emb(x).unsqueeze(-1) for x in candidates]).unsqueeze(-1)
+    if Y_next.min() < Y.min():
+        ind_best = Y_next.argmin()
+        x0, x1 = candidates[ind_best, :2].tolist()
+        print(
+            f"{i + 1}) New best: {Y_next[ind_best].item():.3f} @ "
+            f"[{x0:.3f}, {x1:.3f}]"
+        )
+    X = torch.cat((X, candidates))
+    Y = torch.cat((Y, Y_next))
+
+
+
+
+
+
+
+
+
+
Best initial point: 5.322
+3) New best: 2.028 @ [1.000, 0.181]
+4) New best: 2.019 @ [1.000, 0.219]
+5) New best: 0.866 @ [0.129, 0.762]
+6) New best: 0.415 @ [0.121, 0.831]
+8) New best: 0.398 @ [0.542, 0.153]
+
+
+
+
+
+
+
+
+
+

Plot the results

We can see that we were able to get close to the global optimium of $\approx 0.398$ after 50 function evaluations.

+
+
+
+
+
+
In [12]:
+
+
+
import matplotlib.pyplot as plt
+import numpy as np
+
+%matplotlib inline
+
+Y_np = Y.cpu().numpy()
+fig, ax = plt.subplots(figsize=(8, 6))
+ax.plot(np.minimum.accumulate(Y_np), color="b", label="SAASBO")
+ax.plot([0, len(Y_np)], [0.398, 0.398], "--", c="g", lw=3, label="Optimal value")
+ax.grid(True)
+ax.set_title(f"Branin, D = {DIM}", fontsize=20)
+ax.set_xlabel("Number of evaluations", fontsize=20)
+ax.set_xlim([0, len(Y_np)])
+ax.set_ylabel("Best value found", fontsize=20)
+ax.set_ylim([0, 8])
+ax.legend(fontsize=18)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Predict on some test points

We fit a model using the 50 datapoints collected by SAASBO and predict on 50 test +points in order to see how well the SAAS model predicts out-of-sample. +The plot shows the mean and a 95% confidence interval for each test point.

+
+
+
+
+
+
In [13]:
+
+
+
train_X = SobolEngine(dimension=DIM, scramble=True, seed=0).draw(50).to(**tkwargs)
+test_X = SobolEngine(dimension=DIM, scramble=True, seed=1).draw(50).to(**tkwargs)
+train_Y = branin_emb(train_X).unsqueeze(-1)
+test_Y = branin_emb(test_X).unsqueeze(-1)
+
+gp = SaasFullyBayesianSingleTaskGP(
+    train_X=train_X,
+    train_Y=train_Y,
+    train_Yvar=torch.full_like(train_Y, 1e-6),
+    outcome_transform=Standardize(m=1),
+)
+fit_fully_bayesian_model_nuts(
+    gp,
+    warmup_steps=WARMUP_STEPS,
+    num_samples=NUM_SAMPLES,
+    thinning=THINNING,
+    disable_progbar=True,
+)
+
+
+
+
+
+
+
+
In [14]:
+
+
+
with torch.no_grad():
+    posterior = gp.posterior(test_X)
+median = posterior.quantile(value=torch.tensor([0.5], **tkwargs))
+q1 = posterior.quantile(value=torch.tensor([0.025], **tkwargs))
+q2 = posterior.quantile(value=torch.tensor([0.975], **tkwargs))
+
+
+
+
+
+
+
+
In [15]:
+
+
+
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
+ax.plot([0, 80], [0, 80], "b--", lw=2)
+
+yerr1, yerr2 = median - q1, q2 - median
+yerr = torch.cat((yerr1.unsqueeze(0), yerr2.unsqueeze(0)), dim=0).squeeze(-1)
+markers, caps, bars = ax.errorbar(
+    test_Y.squeeze(-1).cpu().numpy(),
+    median.squeeze(-1).cpu().numpy(),
+    yerr=yerr.cpu().numpy(),
+    fmt=".",
+    capsize=4,
+    elinewidth=2.0,
+    ms=14,
+    c="k",
+    ecolor="gray",
+)
+ax.set_xlim([0, 80])
+ax.set_ylim([0, 80])
+[bar.set_alpha(0.8) for bar in bars]
+[cap.set_alpha(0.8) for cap in caps]
+ax.set_xlabel("True value", fontsize=20)
+ax.set_ylabel("Predicted value", fontsize=20)
+ax.set_aspect("equal")
+ax.grid(True)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Look a the lengthscales from the final model

As SAASBO places strong priors on the inverse lengthscales, we only expect parameters +0 and 1 to be identified as important by the model since the other parameters have no effect. +We can confirm that this is the case below as the lengthscales of parameters 0 and 1 are +small with all other lengthscales being large.

+
+
+
+
+
+
In [16]:
+
+
+
median_lengthscales = gp.median_lengthscale
+for i in median_lengthscales.argsort()[:10]:
+    print(f"Parameter {i:2}) Median lengthscale = {median_lengthscales[i].item():.2e}")
+
+
+
+
+
+
+
+
+
+
Parameter  0) Median lengthscale = 7.38e-01
+Parameter  1) Median lengthscale = 2.35e+00
+Parameter 12) Median lengthscale = 5.04e+02
+Parameter 29) Median lengthscale = 7.27e+02
+Parameter 27) Median lengthscale = 7.72e+02
+Parameter  7) Median lengthscale = 9.16e+02
+Parameter  3) Median lengthscale = 9.53e+02
+Parameter 16) Median lengthscale = 9.84e+02
+Parameter  8) Median lengthscale = 1.04e+03
+Parameter  9) Median lengthscale = 1.05e+03
+
+
+
+
+
+
+
+
+
In [17]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/scalable_constrained_bo.html b/website-old/_tutorials/scalable_constrained_bo.html new file mode 100644 index 0000000000..9767fafe8a --- /dev/null +++ b/website-old/_tutorials/scalable_constrained_bo.html @@ -0,0 +1,553 @@ + + + +
+
+
+
+

Scalable Constrained Bayesian Optimization (SCBO)

In this tutorial, we show how to implement Scalable Constrained Bayesian Optimization (SCBO) [1] in a closed loop in BoTorch.

+

We optimize the 10𝐷 Ackley function on the domain $[−5,10]^{10}$. This implementation uses two simple constraint functions $c1$ and $c2$. Our goal is to find an $x$ that maximizes the Ackley function subject to the constraints $c1(x) \leq 0$ and $c2(x) \leq 0$.

+

[1]: David Eriksson and Matthias Poloczek. Scalable constrained Bayesian optimization. In International Conference on Artificial Intelligence and Statistics, pages 730–738. PMLR, 2021. +(https://doi.org/10.48550/arxiv.2002.08526)

+

Since SCBO is essentially a constrained version of Trust Region Bayesian Optimization (TuRBO), this tutorial shares much of the same code as the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1) with small modifications made to implement SCBO.

+
+
+
+
+
+
In [ ]:
+
+
+
import math
+import os
+import warnings
+from dataclasses import dataclass
+
+import gpytorch
+import torch
+from gpytorch.constraints import Interval
+from gpytorch.kernels import MaternKernel, ScaleKernel
+from gpytorch.likelihoods import GaussianLikelihood
+from gpytorch.mlls import ExactMarginalLogLikelihood
+from torch import Tensor
+from torch.quasirandom import SobolEngine
+
+from botorch.fit import fit_gpytorch_mll
+# Constrained Max Posterior Sampling s a new sampling class, similar to MaxPosteriorSampling,
+# which implements the constrained version of Thompson Sampling described in [1].
+from botorch.generation.sampling import ConstrainedMaxPosteriorSampling
+from botorch.models import SingleTaskGP
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.test_functions import Ackley
+from botorch.utils.transforms import unnormalize
+
+warnings.filterwarnings("ignore")
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+tkwargs = {"device": device, "dtype": dtype}
+
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

Demonstration with 10-dimensional Ackley function and Two Simple Constraint Functions

+
+
+
+
+
+
In [2]:
+
+
+
# Here we define the example 10D Ackley function
+fun = Ackley(dim=10, negate=True).to(**tkwargs)
+fun.bounds[0, :].fill_(-5)
+fun.bounds[1, :].fill_(10)
+dim = fun.dim
+lb, ub = fun.bounds
+
+batch_size = 4
+n_init = 10
+max_cholesky_size = float("inf")  # Always use Cholesky
+
+# When evaluating the function, we must first unnormalize the inputs since
+# we will use normalized inputs x in the main optimizaiton loop
+def eval_objective(x):
+    """This is a helper function we use to unnormalize and evalaute a point"""
+    return fun(unnormalize(x, fun.bounds))
+
+
+
+
+
+
+
+
+

Defining two simple constraint functions

We'll use two constraints functions: c1 and c2

We want to find solutions which maximize the above Ackley objective subject to the constraint that +c1(x) <= 0 and c2(x) <= 0 +Note that SCBO expects all constraints to be of the for c(x) <= 0, so any other desired constraints must be modified to fit this form.

+

Note also that while the below constraints are very simple functions, the point of this tutorial is to show how to use SCBO, and this same implementation could be applied in the same way if c1, c2 were actually complex black-box functions.

+
+
+
+
+
+
In [3]:
+
+
+
def c1(x):  # Equivalent to enforcing that sum(x) <= 0
+    return x.sum()
+
+
+def c2(x):  # Equivalent to enforcing that ||x||_2 <= 5
+    return torch.norm(x, p=2) - 5
+
+
+# We assume c1, c2 have same bounds as the Ackley function above
+def eval_c1(x):
+    """This is a helper function we use to unnormalize and evalaute a point"""
+    return c1(unnormalize(x, fun.bounds))
+
+
+def eval_c2(x):
+    """This is a helper function we use to unnormalize and evalaute a point"""
+    return c2(unnormalize(x, fun.bounds))
+
+
+
+
+
+
+
+
+

Define TuRBO Class

Just as in the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1), we'll define a class to hold the turst region state and a method update_state() to update the side length of the trust region hyper-cube during optimization. We'll update the side length according to the number of sequential successes or failures as discussed in the original TuRBO paper.

+
+
+
+
+
+
In [4]:
+
+
+
@dataclass
+class ScboState:
+    dim: int
+    batch_size: int
+    length: float = 0.8
+    length_min: float = 0.5**7
+    length_max: float = 1.6
+    failure_counter: int = 0
+    failure_tolerance: int = float("nan")  # Note: Post-initialized
+    success_counter: int = 0
+    success_tolerance: int = 10  # Note: The original paper uses 3
+    best_value: float = -float("inf")
+    best_constraint_values: Tensor = torch.ones(2, **tkwargs) * torch.inf
+    restart_triggered: bool = False
+
+    def __post_init__(self):
+        self.failure_tolerance = math.ceil(max([4.0 / self.batch_size, float(self.dim) / self.batch_size]))
+
+
+def update_tr_length(state: ScboState):
+    # Update the length of the trust region according to
+    # success and failure counters
+    # (Just as in original TuRBO paper)
+    if state.success_counter == state.success_tolerance:  # Expand trust region
+        state.length = min(2.0 * state.length, state.length_max)
+        state.success_counter = 0
+    elif state.failure_counter == state.failure_tolerance:  # Shrink trust region
+        state.length /= 2.0
+        state.failure_counter = 0
+
+    if state.length < state.length_min:  # Restart when trust region becomes too small
+        state.restart_triggered = True
+
+    return state
+
+
+def get_best_index_for_batch(Y: Tensor, C: Tensor):
+    """Return the index for the best point."""
+    is_feas = (C <= 0).all(dim=-1)
+    if is_feas.any():  # Choose best feasible candidate
+        score = Y.clone()
+        score[~is_feas] = -float("inf")
+        return score.argmax()
+    return C.clamp(min=0).sum(dim=-1).argmin()
+
+
+def update_state(state, Y_next, C_next):
+    """Method used to update the TuRBO state after each step of optimization.
+
+    Success and failure counters are updated according to the objective values
+    (Y_next) and constraint values (C_next) of the batch of candidate points
+    evaluated on the optimization step.
+
+    As in the original TuRBO paper, a success is counted whenver any one of the
+    new candidate points improves upon the incumbent best point. The key difference
+    for SCBO is that we only compare points by their objective values when both points
+    are valid (meet all constraints). If exactly one of the two points being compared
+    violates a constraint, the other valid point is automatically considered to be better.
+    If both points violate some constraints, we compare them inated by their constraint values.
+    The better point in this case is the one with minimum total constraint violation
+    (the minimum sum of constraint values)"""
+
+    # Pick the best point from the batch
+    best_ind = get_best_index_for_batch(Y=Y_next, C=C_next)
+    y_next, c_next = Y_next[best_ind], C_next[best_ind]
+
+    if (c_next <= 0).all():
+        # At least one new candidate is feasible
+        improvement_threshold = state.best_value + 1e-3 * math.fabs(state.best_value)
+        if y_next > improvement_threshold or (state.best_constraint_values > 0).any():
+            state.success_counter += 1
+            state.failure_counter = 0
+            state.best_value = y_next.item()
+            state.best_constraint_values = c_next
+        else:
+            state.success_counter = 0
+            state.failure_counter += 1
+    else:
+        # No new candidate is feasible
+        total_violation_next = c_next.clamp(min=0).sum(dim=-1)
+        total_violation_center = state.best_constraint_values.clamp(min=0).sum(dim=-1)
+        if total_violation_next < total_violation_center:
+            state.success_counter += 1
+            state.failure_counter = 0
+            state.best_value = y_next.item()
+            state.best_constraint_values = c_next
+        else:
+            state.success_counter = 0
+            state.failure_counter += 1
+
+    # Update the length of the trust region according to the success and failure counters
+    state = update_tr_length(state)
+    return state
+
+
+# Define example state
+state = ScboState(dim=dim, batch_size=batch_size)
+print(state)
+
+
+
+
+
+
+
+
+
+
ScboState(dim=10, batch_size=4, length=0.8, length_min=0.0078125, length_max=1.6, failure_counter=0, failure_tolerance=3, success_counter=0, success_tolerance=10, best_value=-inf, best_constraint_values=tensor([inf, inf], dtype=torch.float64), restart_triggered=False)
+
+
+
+
+
+
+
+
+
+

Generate Initial Points

Here we define a simple method to generate a set of random initial datapoints that we will use to kick-off optimization.

+
+
+
+
+
+
In [5]:
+
+
+
def get_initial_points(dim, n_pts, seed=0):
+    sobol = SobolEngine(dimension=dim, scramble=True, seed=seed)
+    X_init = sobol.draw(n=n_pts).to(dtype=dtype, device=device)
+    return X_init
+
+
+
+
+
+
+
+
+

Generating a batch of candidates for SCBO

Just as in the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1), we'll define a method generate_batch to generate a new batch of candidate points within the TuRBO trust region using Thompson sampling.

+

The key difference here from TuRBO is that, instead of using MaxPosteriorSampling to simply grab the candidates within the trust region with the maximum posterior values, we use ConstrainedMaxPosteriorSampling to instead grab the candidates within the trust region with the maximum posterior values subject to the constraint that the posteriors for the constraint models for c1(x) and c2(x) must be less than or equal to 0 for both candidates.

+

We use additional GPs ('constraint models') to model each black-box constraint (c1 and c2), and throw out all candidates for which the sampled value for these constraint models is greater than 0. According to [1], in the special case when all of the candidaates are predicted to be constraint violators, we select the candidate with the minimum predicted violation. (See botorch.generation.sampling.ConstrainedMaxPosteriorSampling for implementation details).

+
+
+
+
+
+
In [6]:
+
+
+
def generate_batch(
+    state,
+    model,  # GP model
+    X,  # Evaluated points on the domain [0, 1]^d
+    Y,  # Function values
+    C,  # Constraint values
+    batch_size,
+    n_candidates,  # Number of candidates for Thompson sampling
+    constraint_model,
+    sobol: SobolEngine,
+):
+    assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))
+
+    # Create the TR bounds
+    best_ind = get_best_index_for_batch(Y=Y, C=C)
+    x_center = X[best_ind, :].clone()
+    tr_lb = torch.clamp(x_center - state.length / 2.0, 0.0, 1.0)
+    tr_ub = torch.clamp(x_center + state.length / 2.0, 0.0, 1.0)
+
+    # Thompson Sampling w/ Constraints (SCBO)
+    dim = X.shape[-1]
+    pert = sobol.draw(n_candidates).to(dtype=dtype, device=device)
+    pert = tr_lb + (tr_ub - tr_lb) * pert
+
+    # Create a perturbation mask
+    prob_perturb = min(20.0 / dim, 1.0)
+    mask = torch.rand(n_candidates, dim, **tkwargs) <= prob_perturb
+    ind = torch.where(mask.sum(dim=1) == 0)[0]
+    mask[ind, torch.randint(0, dim - 1, size=(len(ind),), device=device)] = 1
+
+    # Create candidate points from the perturbations and the mask
+    X_cand = x_center.expand(n_candidates, dim).clone()
+    X_cand[mask] = pert[mask]
+
+    # Sample on the candidate points using Constrained Max Posterior Sampling
+    constrained_thompson_sampling = ConstrainedMaxPosteriorSampling(
+        model=model, constraint_model=constraint_model, replacement=False
+    )
+    with torch.no_grad():
+        X_next = constrained_thompson_sampling(X_cand, num_samples=batch_size)
+
+    return X_next
+
+
+
+
+
+
+
+
+

Main Optimization Loop

+
+
+
+
+
+
In [7]:
+
+
+
# Generate initial data
+train_X = get_initial_points(dim, n_init)
+train_Y = torch.tensor([eval_objective(x) for x in train_X], **tkwargs).unsqueeze(-1)
+C1 = torch.tensor([eval_c1(x) for x in train_X], **tkwargs).unsqueeze(-1)
+C2 = torch.tensor([eval_c2(x) for x in train_X], **tkwargs).unsqueeze(-1)
+
+# Initialize TuRBO state
+state = ScboState(dim, batch_size=batch_size)
+
+# Note: We use 2000 candidates here to make the tutorial run faster.
+# SCBO actually uses min(5000, max(2000, 200 * dim)) candidate points by default.
+N_CANDIDATES = 2000 if not SMOKE_TEST else 4
+sobol = SobolEngine(dim, scramble=True, seed=1)
+
+
+def get_fitted_model(X, Y):
+    likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))
+    covar_module = ScaleKernel(  # Use the same lengthscale prior as in the TuRBO paper
+        MaternKernel(nu=2.5, ard_num_dims=dim, lengthscale_constraint=Interval(0.005, 4.0))
+    )
+    model = SingleTaskGP(
+        X,
+        Y,
+        covar_module=covar_module,
+        likelihood=likelihood,
+        outcome_transform=Standardize(m=1),
+    )
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+
+    with gpytorch.settings.max_cholesky_size(max_cholesky_size):
+        fit_gpytorch_mll(mll)
+
+    return model
+
+
+while not state.restart_triggered:  # Run until TuRBO converges
+    # Fit GP models for objective and constraints
+    model = get_fitted_model(train_X, train_Y)
+    c1_model = get_fitted_model(train_X, C1)
+    c2_model = get_fitted_model(train_X, C2)
+
+    # Generate a batch of candidates
+    with gpytorch.settings.max_cholesky_size(max_cholesky_size):
+        X_next = generate_batch(
+            state=state,
+            model=model,
+            X=train_X,
+            Y=train_Y,
+            C=torch.cat((C1, C2), dim=-1),
+            batch_size=batch_size,
+            n_candidates=N_CANDIDATES,
+            constraint_model=ModelListGP(c1_model, c2_model),
+            sobol=sobol,
+        )
+
+    # Evaluate both the objective and constraints for the selected candidaates
+    Y_next = torch.tensor([eval_objective(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1)
+    C1_next = torch.tensor([eval_c1(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1)
+    C2_next = torch.tensor([eval_c2(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1)
+    C_next = torch.cat([C1_next, C2_next], dim=-1)
+
+    # Update TuRBO state
+    state = update_state(state=state, Y_next=Y_next, C_next=C_next)
+
+    # Append data. Note that we append all data, even points that violate
+    # the constraints. This is so our constraint models can learn more
+    # about the constraint functions and gain confidence in where violations occur.
+    train_X = torch.cat((train_X, X_next), dim=0)
+    train_Y = torch.cat((train_Y, Y_next), dim=0)
+    C1 = torch.cat((C1, C1_next), dim=0)
+    C2 = torch.cat((C2, C2_next), dim=0)
+
+    # Print current status. Note that state.best_value is always the best
+    # objective value found so far which meets the constraints, or in the case
+    # that no points have been found yet which meet the constraints, it is the
+    # objective value of the point with the minimum constraint violation.
+    if (state.best_constraint_values <= 0).all():
+        print(f"{len(train_X)}) Best value: {state.best_value:.2e}, TR length: {state.length:.2e}")
+    else:
+        violation = state.best_constraint_values.clamp(min=0).sum()
+        print(
+            f"{len(train_X)}) No feasible point yet! Smallest total violation: "
+            f"{violation:.2e}, TR length: {state.length:.2e}"
+        )
+
+
+
+
+
+
+
+
+
+
14) No feasible point yet! Smallest total violation: 1.61e+01, TR length: 8.00e-01
+18) No feasible point yet! Smallest total violation: 8.45e+00, TR length: 8.00e-01
+22) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 8.00e-01
+26) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 8.00e-01
+30) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 8.00e-01
+34) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 4.00e-01
+38) No feasible point yet! Smallest total violation: 8.84e-01, TR length: 4.00e-01
+42) Best value: -6.17e+00, TR length: 4.00e-01
+46) Best value: -6.17e+00, TR length: 4.00e-01
+50) Best value: -5.94e+00, TR length: 4.00e-01
+54) Best value: -5.94e+00, TR length: 4.00e-01
+58) Best value: -5.81e+00, TR length: 4.00e-01
+62) Best value: -4.58e+00, TR length: 4.00e-01
+66) Best value: -4.58e+00, TR length: 4.00e-01
+70) Best value: -4.58e+00, TR length: 4.00e-01
+74) Best value: -4.58e+00, TR length: 2.00e-01
+78) Best value: -4.19e+00, TR length: 2.00e-01
+82) Best value: -2.97e+00, TR length: 2.00e-01
+86) Best value: -2.97e+00, TR length: 2.00e-01
+90) Best value: -2.97e+00, TR length: 2.00e-01
+94) Best value: -2.97e+00, TR length: 1.00e-01
+98) Best value: -2.41e+00, TR length: 1.00e-01
+102) Best value: -2.41e+00, TR length: 1.00e-01
+106) Best value: -2.41e+00, TR length: 1.00e-01
+110) Best value: -2.36e+00, TR length: 1.00e-01
+114) Best value: -2.36e+00, TR length: 1.00e-01
+118) Best value: -2.36e+00, TR length: 1.00e-01
+122) Best value: -2.36e+00, TR length: 5.00e-02
+126) Best value: -1.57e+00, TR length: 5.00e-02
+130) Best value: -1.57e+00, TR length: 5.00e-02
+134) Best value: -1.16e+00, TR length: 5.00e-02
+138) Best value: -1.16e+00, TR length: 5.00e-02
+142) Best value: -1.16e+00, TR length: 5.00e-02
+146) Best value: -1.05e+00, TR length: 5.00e-02
+150) Best value: -1.05e+00, TR length: 5.00e-02
+154) Best value: -1.05e+00, TR length: 5.00e-02
+158) Best value: -1.05e+00, TR length: 2.50e-02
+162) Best value: -4.22e-01, TR length: 2.50e-02
+166) Best value: -4.22e-01, TR length: 2.50e-02
+170) Best value: -4.22e-01, TR length: 2.50e-02
+174) Best value: -4.22e-01, TR length: 1.25e-02
+178) Best value: -3.24e-01, TR length: 1.25e-02
+182) Best value: -3.24e-01, TR length: 1.25e-02
+186) Best value: -3.24e-01, TR length: 1.25e-02
+190) Best value: -3.24e-01, TR length: 6.25e-03
+
+
+
+
+
+
+
+
+
+

Plot Results

+
+
+
+
+
+
In [8]:
+
+
+
import matplotlib.pyplot as plt
+import numpy as np
+from matplotlib import rc
+
+%matplotlib inline
+
+fig, ax = plt.subplots(figsize=(8, 6))
+
+score = train_Y.clone()
+# Set infeasible to -inf
+score[~(torch.cat((C1, C2), dim=-1) <= 0).all(dim=-1)] = float("-inf")
+fx = np.maximum.accumulate(score.cpu())
+plt.plot(fx, marker="", lw=3)
+
+plt.plot([0, len(train_Y)], [fun.optimal_value, fun.optimal_value], "k--", lw=3)
+plt.ylabel("Function value", fontsize=18)
+plt.xlabel("Number of evaluations", fontsize=18)
+plt.title("10D Ackley with 2 outcome constraints", fontsize=20)
+plt.xlim([0, len(train_Y)])
+plt.ylim([-15, 1])
+
+plt.grid(True)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/thompson_sampling.html b/website-old/_tutorials/thompson_sampling.html new file mode 100644 index 0000000000..ed7fedb8e4 --- /dev/null +++ b/website-old/_tutorials/thompson_sampling.html @@ -0,0 +1,540 @@ + + + +
+
+
+
+

Tutorial on large-scale Thompson sampling

This demo currently considers four approaches to discrete Thompson sampling on m candidates points:

+
    +
  1. Exact sampling with Cholesky: Computing a Cholesky decomposition of the corresponding m x m covariance matrix which reuqires O(m^3) computational cost and O(m^2) space. This is the standard approach to sampling from a Gaussian process, but the quadratic memory usage and cubic compliexity limits the number of candidate points.

    +
  2. +
  3. Contour integral quadrature (CIQ): CIQ [1] is a Krylov subspace method combined with a rational approximation that can be used for computing matrix square roots of covariance matrices, which is the main bottleneck when sampling from a Gaussian process. CIQ relies on computing matrix vector multiplications with the exact kernel matrix which requires O(m^2) computational complexity and space. Note that the space complexity can be further lowered to O(m) by using KeOps, but this is not covered as part of the tutorial.

    +
  4. +
  5. Lanczos: Rather than using CIQ, we can solve the linear systems K^(1/2) v = b using Lanczos and the conjugate gradient (CG) method. This will be faster than CIQ, but will generally produce samples of worse quality. Similarly to CIQ, KeOps can be used to improve space complexity of Lanczos.

    +
  6. +
  7. Random Fourier features (RFFs): The RFF kernel was originally proposed in [2] and we use it as implemented in GPyTorch. RFFs are computationally cheap to work with as the computational cost and space are both O(km) where k is the number of Fourier features. Note that while Cholesky and CIQ are able to generate exact samples from the GP model, RFFs are an unbiased approximation and the resulting samples often aren't perfectly calibrated.

    +
  8. +
+

[1] Pleiss, Geoff, et al. "Fast matrix square roots with applications to Gaussian processes and Bayesian optimization.", Advances in neural information processing systems (2020)

+

[2] Rahimi, Ali, and Benjamin Recht. "Random features for large-scale kernel machines.", Advances in neural information processing systems (2007)

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import time
+from contextlib import ExitStack
+
+import gpytorch
+import gpytorch.settings as gpts
+import torch
+from gpytorch.constraints import Interval
+from gpytorch.distributions import MultivariateNormal
+from gpytorch.kernels import RBFKernel, RFFKernel, ScaleKernel
+from gpytorch.likelihoods import GaussianLikelihood
+from gpytorch.mlls import ExactMarginalLogLikelihood
+from torch.quasirandom import SobolEngine
+from torch import Tensor
+
+from botorch.fit import fit_gpytorch_mll
+from botorch.generation import MaxPosteriorSampling
+from botorch.models import SingleTaskGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.test_functions import Hartmann
+from botorch.utils.sampling import draw_sobol_samples
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+

We will use 6 dimensional Hartmann test function, which is typically evaluated on the unit hypercube.

+
+
+
+
+
+
In [2]:
+
+
+
hart6 = Hartmann(dim=6, negate=True).to(device=device, dtype=dtype)
+dim = hart6.dim
+
+
+
+
+
+
+
+
In [3]:
+
+
+
def generate_batch(
+    X: Tensor,
+    Y: Tensor,
+    batch_size: int,
+    n_candidates: int,
+    sampler: str,  # "cholesky", "ciq", "rff", "lanczos"
+    seed: int,
+) -> Tensor:
+    assert sampler in ("cholesky", "ciq", "rff", "lanczos")
+    assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))
+
+    if sampler == "rff":
+        base_kernel = RFFKernel(ard_num_dims=X.shape[-1], num_samples=1024)
+    else:
+        base_kernel = RBFKernel(ard_num_dims=X.shape[-1])
+    covar_module = ScaleKernel(base_kernel)
+
+    # Fit a GP model
+    model = SingleTaskGP(train_X=X, train_Y=Y, covar_module=covar_module, outcome_transform=Standardize(m=1))
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+
+    # Draw samples on a Sobol sequence
+    X_cand = draw_sobol_samples(bounds=hart6.bounds, n=n_candidates, q=1, seed=seed).squeeze(-2)
+
+    # Thompson sample
+    with ExitStack() as es:
+        if sampler == "cholesky":
+            es.enter_context(gpts.max_cholesky_size(float("inf")))
+        elif sampler == "ciq":
+            es.enter_context(gpts.fast_computations(covar_root_decomposition=True))
+            es.enter_context(gpts.max_cholesky_size(0))
+            es.enter_context(gpts.ciq_samples(True))
+            es.enter_context(
+                gpts.minres_tolerance(2e-3)
+            )  # Controls accuracy and runtime
+            es.enter_context(gpts.num_contour_quadrature(15))
+        elif sampler == "lanczos":
+            es.enter_context(
+                gpts.fast_computations(
+                    covar_root_decomposition=True, log_prob=True, solves=True
+                )
+            )
+            es.enter_context(gpts.max_lanczos_quadrature_iterations(10))
+            es.enter_context(gpts.max_cholesky_size(0))
+            es.enter_context(gpts.ciq_samples(False))
+        elif sampler == "rff":
+            es.enter_context(gpts.fast_computations(covar_root_decomposition=True))
+        es.enter_context(torch.no_grad())
+        
+        thompson_sampling = MaxPosteriorSampling(model=model, replacement=False)
+        X_next = thompson_sampling(X_cand, num_samples=batch_size)
+
+    return X_next
+
+
+
+
+
+
+
+
In [4]:
+
+
+
def run_optimization(
+    sampler: str,
+    n_candidates: int,
+    n_init: int,
+    max_evals: int,
+    batch_size: int,
+    seed: int,
+) -> tuple[Tensor, Tensor]:
+    X = draw_sobol_samples(bounds=hart6.bounds, n=n_init, q=1, seed=seed).squeeze(-2)
+    Y = torch.tensor(
+        [hart6(x) for x in X], dtype=dtype, device=device
+    ).unsqueeze(-1)
+    print(f"{len(X)}) Best value: {Y.max().item():.2e}")
+
+    inner_seed = seed
+    while len(X) < max_evals:
+        # Create a batch
+        start = time.monotonic()
+        inner_seed += 1
+        X_next = generate_batch(
+            X=X,
+            Y=Y,
+            batch_size=min(batch_size, max_evals - len(X)),
+            n_candidates=n_candidates,
+            seed=inner_seed,
+            sampler=sampler,
+        )
+        end = time.monotonic()
+        print(f"Generated batch in {end - start:.1f} seconds")
+        Y_next = torch.tensor(
+            [hart6(x) for x in X_next], dtype=dtype, device=device
+        ).unsqueeze(-1)
+
+        # Append data
+        X = torch.cat((X, X_next), dim=0)
+        Y = torch.cat((Y, Y_next), dim=0)
+
+        print(f"{len(X)}) Best value: {Y.max().item():.2e}")
+    return X, Y
+
+
+
+
+
+
+
+
In [5]:
+
+
+
batch_size = 5
+n_init = 10
+max_evals = 50
+seed = 12345  # To get the same Sobol points
+N_CAND = 10_000 if not SMOKE_TEST else 10
+
+shared_args = {
+    "n_candidates": N_CAND,
+    "n_init": n_init,
+    "max_evals": max_evals,
+    "batch_size": batch_size,
+    "seed": seed,
+}
+
+
+
+
+
+
+
+
+

Track memory footprint

+
+
+
+
+
+
In [6]:
+
+
+
%load_ext memory_profiler
+
+
+
+
+
+
+
+
+

Cholesky

+
+
+
+
+
+
In [7]:
+
+
+
%memit X_chol, Y_chol = run_optimization("cholesky", **shared_args)
+
+
+
+
+
+
+
+
+
+
10) Best value: 6.72e-01
+
+
+
+
+
+
+
/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
Generated batch in 16.0 seconds
+15) Best value: 6.72e-01
+
+
+
+
+
+
+
/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
Generated batch in 14.1 seconds
+20) Best value: 6.72e-01
+
+
+
+
+
+
+
/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
Generated batch in 18.6 seconds
+25) Best value: 1.94e+00
+
+
+
+
+
+
+
/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal
+  warnings.warn(
+
+
+
+
+
+
+
Generated batch in 14.8 seconds
+30) Best value: 1.94e+00
+Generated batch in 13.9 seconds
+35) Best value: 2.11e+00
+Generated batch in 14.0 seconds
+40) Best value: 2.68e+00
+Generated batch in 14.6 seconds
+45) Best value: 2.98e+00
+Generated batch in 14.7 seconds
+50) Best value: 2.98e+00
+peak memory: 7941.41 MiB, increment: 7674.36 MiB
+
+
+
+
+
+
+
+
+
+

RFFs

+
+
+
+
+
+
In [8]:
+
+
+
%memit X_rff, Y_rff = run_optimization("rff", **shared_args)
+
+
+
+
+
+
+
+
+
+
10) Best value: 6.72e-01
+Generated batch in 1.4 seconds
+15) Best value: 6.72e-01
+Generated batch in 2.6 seconds
+20) Best value: 6.72e-01
+Generated batch in 2.0 seconds
+25) Best value: 1.00e+00
+Generated batch in 2.0 seconds
+30) Best value: 1.36e+00
+Generated batch in 2.5 seconds
+35) Best value: 2.00e+00
+Generated batch in 2.2 seconds
+40) Best value: 2.43e+00
+Generated batch in 2.2 seconds
+45) Best value: 2.43e+00
+Generated batch in 2.1 seconds
+50) Best value: 2.65e+00
+peak memory: 1709.20 MiB, increment: 1349.06 MiB
+
+
+
+
+
+
+
+
+
+

Lanczos

+
+
+
+
+
+
In [9]:
+
+
+
%memit X_lanczos, Y_lanczos = run_optimization("lanczos", **shared_args)
+
+
+
+
+
+
+
+
+
+
10) Best value: 6.72e-01
+Generated batch in 1.9 seconds
+15) Best value: 6.72e-01
+Generated batch in 2.5 seconds
+20) Best value: 1.83e+00
+Generated batch in 2.4 seconds
+25) Best value: 1.93e+00
+Generated batch in 2.6 seconds
+30) Best value: 2.39e+00
+Generated batch in 2.5 seconds
+35) Best value: 2.41e+00
+Generated batch in 2.5 seconds
+40) Best value: 2.96e+00
+Generated batch in 2.6 seconds
+45) Best value: 2.98e+00
+Generated batch in 2.5 seconds
+50) Best value: 2.98e+00
+peak memory: 2981.11 MiB, increment: 1271.91 MiB
+
+
+
+
+
+
+
+
+
+

CIQ

+
+
+
+
+
+
In [10]:
+
+
+
%memit X_ciq, Y_ciq = run_optimization("ciq", **shared_args)
+
+
+
+
+
+
+
+
+
+
10) Best value: 6.72e-01
+Generated batch in 9.6 seconds
+15) Best value: 6.72e-01
+Generated batch in 12.5 seconds
+20) Best value: 6.72e-01
+Generated batch in 16.8 seconds
+25) Best value: 6.72e-01
+Generated batch in 18.7 seconds
+30) Best value: 2.19e+00
+Generated batch in 18.2 seconds
+35) Best value: 2.48e+00
+Generated batch in 14.8 seconds
+40) Best value: 2.61e+00
+Generated batch in 15.2 seconds
+45) Best value: 2.98e+00
+Generated batch in 15.9 seconds
+50) Best value: 2.98e+00
+peak memory: 2674.38 MiB, increment: 908.34 MiB
+
+
+
+
+
+
+
+
+
+

Plot

+
+
+
+
+
+
In [12]:
+
+
+
import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+fig = plt.figure(figsize=(10, 8))
+matplotlib.rcParams.update({"font.size": 20})
+
+results = [
+    (Y_chol.cpu(), f"Cholesky-{N_CAND}", "b", "", 14, "--"),
+    (Y_rff.cpu(), f"RFF-{N_CAND}", "r", ".", 16, "-"),
+    (Y_lanczos.cpu(), f"Lanczos-{N_CAND}", "m", "^", 9, "-"),
+    (Y_ciq.cpu(), f"CIQ-{N_CAND}", "g", "*", 12, "-"),
+]
+
+optimum = hart6.optimal_value
+
+ax = fig.add_subplot(1, 1, 1)
+names = []
+for res, name, c, m, ms, ls in results:
+    names.append(name)
+    fx = res.cummax(dim=0)[0]
+    t = 1 + np.arange(len(fx))
+    plt.plot(t[0::2], fx[0::2], c=c, marker=m, linestyle=ls, markersize=ms)
+
+plt.plot([0, max_evals], [hart6.optimal_value, hart6.optimal_value], "k--", lw=3)
+plt.xlabel("Function value", fontsize=18)
+plt.xlabel("Number of evaluations", fontsize=18)
+plt.title("Hartmann6", fontsize=24)
+plt.xlim([0, max_evals])
+plt.ylim([0, 3.5])
+
+plt.grid(True)
+plt.tight_layout()
+plt.legend(
+    names + ["Global optimal value"],
+    loc="lower right",
+    ncol=1,
+    fontsize=18,
+)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/turbo_1.html b/website-old/_tutorials/turbo_1.html new file mode 100644 index 0000000000..7e69774783 --- /dev/null +++ b/website-old/_tutorials/turbo_1.html @@ -0,0 +1,983 @@ + + + +
+
+
+
+

BO with TuRBO-1 and TS/qEI

In this tutorial, we show how to implement Trust Region Bayesian Optimization (TuRBO) [1] in a closed loop in BoTorch.

+

This implementation uses one trust region (TuRBO-1) and supports either parallel expected improvement (qEI) or Thompson sampling (TS). We optimize the $20D$ Ackley function on the domain $[-5, 10]^{20}$ and show that TuRBO-1 outperforms qEI as well as Sobol.

+

Since botorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_x -f(x)=0$.

+

[1]: Eriksson, David, et al. Scalable global optimization via local Bayesian optimization. Advances in Neural Information Processing Systems. 2019

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import math
+import warnings
+from dataclasses import dataclass
+
+import torch
+from botorch.acquisition import qExpectedImprovement, qLogExpectedImprovement
+from botorch.exceptions import BadInitialCandidatesWarning
+from botorch.fit import fit_gpytorch_mll
+from botorch.generation import MaxPosteriorSampling
+from botorch.models import SingleTaskGP
+from botorch.optim import optimize_acqf
+from botorch.test_functions import Ackley
+from botorch.utils.transforms import unnormalize
+from torch.quasirandom import SobolEngine
+
+import gpytorch
+from gpytorch.constraints import Interval
+from gpytorch.kernels import MaternKernel, ScaleKernel
+from gpytorch.likelihoods import GaussianLikelihood
+from gpytorch.mlls import ExactMarginalLogLikelihood
+
+
+warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning)
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+SMOKE_TEST = os.environ.get("SMOKE_TEST")
+
+
+
+
+
+
+
+
+
+
[KeOps] Warning : omp.h header is not in the path, disabling OpenMP. To fix this, you can set the environment
+                  variable OMP_PATH to the location of the header before importing keopscore or pykeops,
+                  e.g. using os.environ: import os; os.environ['OMP_PATH'] = '/path/to/omp/header'
+[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode
+
+
+
+
+
+
+
+
+
+

Optimize the 20-dimensional Ackley function

The goal is to minimize the popular Ackley function:

+

$f(x_1,\ldots,x_d) = -20\exp\left(-0.2 \sqrt{\frac{1}{d} \sum_{j=1}^d x_j^2} \right) -\exp \left( \frac{1}{d} \sum_{j=1}^d \cos(2 \pi x_j) \right) + 20 + e$

+

over the domain $[-5, 10]^{20}$. The global optimal value of $0$ is attained at $x_1 = \ldots = x_d = 0$.

+

As mentioned above, since botorch assumes a maximization problem, we instead maximize $-f(x)$.

+
+
+
+
+
+
In [2]:
+
+
+
fun = Ackley(dim=20, negate=True).to(dtype=dtype, device=device)
+fun.bounds[0, :].fill_(-5)
+fun.bounds[1, :].fill_(10)
+dim = fun.dim
+lb, ub = fun.bounds
+
+batch_size = 4
+n_init = 2 * dim
+max_cholesky_size = float("inf")  # Always use Cholesky
+
+
+def eval_objective(x):
+    """This is a helper function we use to unnormalize and evalaute a point"""
+    return fun(unnormalize(x, fun.bounds))
+
+
+
+
+
+
+
+
+

Maintain the TuRBO state

TuRBO needs to maintain a state, which includes the length of the trust region, success and failure counters, success and failure tolerance, etc.

+

In this tutorial we store the state in a dataclass and update the state of TuRBO after each batch evaluation.

+

Note: These settings assume that the domain has been scaled to $[0, 1]^d$ and that the same batch size is used for each iteration.

+
+
+
+
+
+
In [3]:
+
+
+
@dataclass
+class TurboState:
+    dim: int
+    batch_size: int
+    length: float = 0.8
+    length_min: float = 0.5**7
+    length_max: float = 1.6
+    failure_counter: int = 0
+    failure_tolerance: int = float("nan")  # Note: Post-initialized
+    success_counter: int = 0
+    success_tolerance: int = 10  # Note: The original paper uses 3
+    best_value: float = -float("inf")
+    restart_triggered: bool = False
+
+    def __post_init__(self):
+        self.failure_tolerance = math.ceil(
+            max([4.0 / self.batch_size, float(self.dim) / self.batch_size])
+        )
+
+
+def update_state(state, Y_next):
+    if max(Y_next) > state.best_value + 1e-3 * math.fabs(state.best_value):
+        state.success_counter += 1
+        state.failure_counter = 0
+    else:
+        state.success_counter = 0
+        state.failure_counter += 1
+
+    if state.success_counter == state.success_tolerance:  # Expand trust region
+        state.length = min(2.0 * state.length, state.length_max)
+        state.success_counter = 0
+    elif state.failure_counter == state.failure_tolerance:  # Shrink trust region
+        state.length /= 2.0
+        state.failure_counter = 0
+
+    state.best_value = max(state.best_value, max(Y_next).item())
+    if state.length < state.length_min:
+        state.restart_triggered = True
+    return state
+
+
+
+
+
+
+
+
+

Take a look at the state

+
+
+
+
+
+
In [4]:
+
+
+
state = TurboState(dim=dim, batch_size=batch_size)
+print(state)
+
+
+
+
+
+
+
+
+
+
TurboState(dim=20, batch_size=4, length=0.8, length_min=0.0078125, length_max=1.6, failure_counter=0, failure_tolerance=5, success_counter=0, success_tolerance=10, best_value=-inf, restart_triggered=False)
+
+
+
+
+
+
+
+
+
+

Generate initial points

This generates an initial set of Sobol points that we use to start of the BO loop.

+
+
+
+
+
+
In [5]:
+
+
+
def get_initial_points(dim, n_pts, seed=0):
+    sobol = SobolEngine(dimension=dim, scramble=True, seed=seed)
+    X_init = sobol.draw(n=n_pts).to(dtype=dtype, device=device)
+    return X_init
+
+
+
+
+
+
+
+
+

Generate new batch

Given the current state and a probabilistic (GP) model built from observations X and Y, we generate a new batch of points.

+

This method works on the domain $[0, 1]^d$, so make sure to not pass in observations from the true domain. unnormalize is called before the true function is evaluated which will first map the points back to the original domain.

+

We support either TS and qEI which can be specified via the acqf argument.

+
+
+
+
+
+
In [6]:
+
+
+
def generate_batch(
+    state,
+    model,  # GP model
+    X,  # Evaluated points on the domain [0, 1]^d
+    Y,  # Function values
+    batch_size,
+    n_candidates=None,  # Number of candidates for Thompson sampling
+    num_restarts=10,
+    raw_samples=512,
+    acqf="ts",  # "ei" or "ts"
+):
+    assert acqf in ("ts", "ei")
+    assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))
+    if n_candidates is None:
+        n_candidates = min(5000, max(2000, 200 * X.shape[-1]))
+
+    # Scale the TR to be proportional to the lengthscales
+    x_center = X[Y.argmax(), :].clone()
+    weights = model.covar_module.base_kernel.lengthscale.squeeze().detach()
+    weights = weights / weights.mean()
+    weights = weights / torch.prod(weights.pow(1.0 / len(weights)))
+    tr_lb = torch.clamp(x_center - weights * state.length / 2.0, 0.0, 1.0)
+    tr_ub = torch.clamp(x_center + weights * state.length / 2.0, 0.0, 1.0)
+
+    if acqf == "ts":
+        dim = X.shape[-1]
+        sobol = SobolEngine(dim, scramble=True)
+        pert = sobol.draw(n_candidates).to(dtype=dtype, device=device)
+        pert = tr_lb + (tr_ub - tr_lb) * pert
+
+        # Create a perturbation mask
+        prob_perturb = min(20.0 / dim, 1.0)
+        mask = torch.rand(n_candidates, dim, dtype=dtype, device=device) <= prob_perturb
+        ind = torch.where(mask.sum(dim=1) == 0)[0]
+        mask[ind, torch.randint(0, dim - 1, size=(len(ind),), device=device)] = 1
+
+        # Create candidate points from the perturbations and the mask
+        X_cand = x_center.expand(n_candidates, dim).clone()
+        X_cand[mask] = pert[mask]
+
+        # Sample on the candidate points
+        thompson_sampling = MaxPosteriorSampling(model=model, replacement=False)
+        with torch.no_grad():  # We don't need gradients when using TS
+            X_next = thompson_sampling(X_cand, num_samples=batch_size)
+
+    elif acqf == "ei":
+        ei = qExpectedImprovement(model, train_Y.max())
+        X_next, acq_value = optimize_acqf(
+            ei,
+            bounds=torch.stack([tr_lb, tr_ub]),
+            q=batch_size,
+            num_restarts=num_restarts,
+            raw_samples=raw_samples,
+        )
+
+    return X_next
+
+
+
+
+
+
+
+
+

Optimization loop

This simple loop runs one instance of TuRBO-1 with Thompson sampling until convergence.

+

TuRBO-1 is a local optimizer that can be used for a fixed evaluation budget in a multi-start fashion. Once TuRBO converges, state["restart_triggered"] will be set to true and the run should be aborted. If you want to run more evaluations with TuRBO, you simply generate a new set of initial points and then keep generating batches until convergence or when the evaluation budget has been exceeded. It's important to note that evaluations from previous instances are discarded when TuRBO restarts.

+

NOTE: We use a SingleTaskGP with a noise constraint to keep the noise from getting too large as the problem is noise-free.

+
+
+
+
+
+
In [7]:
+
+
+
X_turbo = get_initial_points(dim, n_init)
+Y_turbo = torch.tensor(
+    [eval_objective(x) for x in X_turbo], dtype=dtype, device=device
+).unsqueeze(-1)
+
+state = TurboState(dim, batch_size=batch_size, best_value=max(Y_turbo).item())
+
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 512 if not SMOKE_TEST else 4
+N_CANDIDATES = min(5000, max(2000, 200 * dim)) if not SMOKE_TEST else 4
+
+torch.manual_seed(0)
+
+while not state.restart_triggered:  # Run until TuRBO converges
+    # Fit a GP model
+    train_Y = (Y_turbo - Y_turbo.mean()) / Y_turbo.std()
+    likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))
+    covar_module = ScaleKernel(  # Use the same lengthscale prior as in the TuRBO paper
+        MaternKernel(
+            nu=2.5, ard_num_dims=dim, lengthscale_constraint=Interval(0.005, 4.0)
+        )
+    )
+    model = SingleTaskGP(
+        X_turbo, train_Y, covar_module=covar_module, likelihood=likelihood
+    )
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+
+    # Do the fitting and acquisition function optimization inside the Cholesky context
+    with gpytorch.settings.max_cholesky_size(max_cholesky_size):
+        # Fit the model
+        fit_gpytorch_mll(mll)
+
+        # Create a batch
+        X_next = generate_batch(
+            state=state,
+            model=model,
+            X=X_turbo,
+            Y=train_Y,
+            batch_size=batch_size,
+            n_candidates=N_CANDIDATES,
+            num_restarts=NUM_RESTARTS,
+            raw_samples=RAW_SAMPLES,
+            acqf="ts",
+        )
+
+    Y_next = torch.tensor(
+        [eval_objective(x) for x in X_next], dtype=dtype, device=device
+    ).unsqueeze(-1)
+
+    # Update state
+    state = update_state(state=state, Y_next=Y_next)
+
+    # Append data
+    X_turbo = torch.cat((X_turbo, X_next), dim=0)
+    Y_turbo = torch.cat((Y_turbo, Y_next), dim=0)
+
+    # Print current status
+    print(
+        f"{len(X_turbo)}) Best value: {state.best_value:.2e}, TR length: {state.length:.2e}"
+    )
+
+
+
+
+
+
+
+
+
+
44) Best value: -1.17e+01, TR length: 8.00e-01
+48) Best value: -1.17e+01, TR length: 8.00e-01
+52) Best value: -1.12e+01, TR length: 8.00e-01
+56) Best value: -1.04e+01, TR length: 8.00e-01
+60) Best value: -1.04e+01, TR length: 8.00e-01
+64) Best value: -9.42e+00, TR length: 8.00e-01
+68) Best value: -9.42e+00, TR length: 8.00e-01
+72) Best value: -9.42e+00, TR length: 8.00e-01
+76) Best value: -9.42e+00, TR length: 8.00e-01
+80) Best value: -8.75e+00, TR length: 8.00e-01
+84) Best value: -8.75e+00, TR length: 8.00e-01
+88) Best value: -8.75e+00, TR length: 8.00e-01
+92) Best value: -8.75e+00, TR length: 8.00e-01
+96) Best value: -8.27e+00, TR length: 8.00e-01
+100) Best value: -8.27e+00, TR length: 8.00e-01
+104) Best value: -8.27e+00, TR length: 8.00e-01
+108) Best value: -8.27e+00, TR length: 8.00e-01
+112) Best value: -8.27e+00, TR length: 8.00e-01
+116) Best value: -8.27e+00, TR length: 4.00e-01
+120) Best value: -6.45e+00, TR length: 4.00e-01
+124) Best value: -6.45e+00, TR length: 4.00e-01
+128) Best value: -6.45e+00, TR length: 4.00e-01
+132) Best value: -6.45e+00, TR length: 4.00e-01
+136) Best value: -5.85e+00, TR length: 4.00e-01
+140) Best value: -5.85e+00, TR length: 4.00e-01
+144) Best value: -5.85e+00, TR length: 4.00e-01
+148) Best value: -5.70e+00, TR length: 4.00e-01
+152) Best value: -5.70e+00, TR length: 4.00e-01
+156) Best value: -5.70e+00, TR length: 4.00e-01
+160) Best value: -5.70e+00, TR length: 4.00e-01
+164) Best value: -5.70e+00, TR length: 4.00e-01
+168) Best value: -5.70e+00, TR length: 2.00e-01
+172) Best value: -4.70e+00, TR length: 2.00e-01
+176) Best value: -4.45e+00, TR length: 2.00e-01
+180) Best value: -4.03e+00, TR length: 2.00e-01
+184) Best value: -4.03e+00, TR length: 2.00e-01
+188) Best value: -4.03e+00, TR length: 2.00e-01
+192) Best value: -4.03e+00, TR length: 2.00e-01
+196) Best value: -4.03e+00, TR length: 2.00e-01
+200) Best value: -3.97e+00, TR length: 2.00e-01
+204) Best value: -3.97e+00, TR length: 2.00e-01
+208) Best value: -3.97e+00, TR length: 2.00e-01
+212) Best value: -3.97e+00, TR length: 2.00e-01
+216) Best value: -3.77e+00, TR length: 2.00e-01
+220) Best value: -3.77e+00, TR length: 2.00e-01
+224) Best value: -3.71e+00, TR length: 2.00e-01
+228) Best value: -3.67e+00, TR length: 2.00e-01
+232) Best value: -3.67e+00, TR length: 2.00e-01
+236) Best value: -3.67e+00, TR length: 2.00e-01
+240) Best value: -3.67e+00, TR length: 2.00e-01
+244) Best value: -3.67e+00, TR length: 2.00e-01
+248) Best value: -3.67e+00, TR length: 1.00e-01
+252) Best value: -3.23e+00, TR length: 1.00e-01
+256) Best value: -3.23e+00, TR length: 1.00e-01
+260) Best value: -3.23e+00, TR length: 1.00e-01
+264) Best value: -2.73e+00, TR length: 1.00e-01
+268) Best value: -2.73e+00, TR length: 1.00e-01
+272) Best value: -2.39e+00, TR length: 1.00e-01
+276) Best value: -2.39e+00, TR length: 1.00e-01
+280) Best value: -2.39e+00, TR length: 1.00e-01
+284) Best value: -2.39e+00, TR length: 1.00e-01
+288) Best value: -2.39e+00, TR length: 1.00e-01
+292) Best value: -2.39e+00, TR length: 5.00e-02
+296) Best value: -2.15e+00, TR length: 5.00e-02
+300) Best value: -2.15e+00, TR length: 5.00e-02
+304) Best value: -1.83e+00, TR length: 5.00e-02
+308) Best value: -1.83e+00, TR length: 5.00e-02
+312) Best value: -1.83e+00, TR length: 5.00e-02
+316) Best value: -1.83e+00, TR length: 5.00e-02
+320) Best value: -1.83e+00, TR length: 5.00e-02
+324) Best value: -1.73e+00, TR length: 5.00e-02
+328) Best value: -1.73e+00, TR length: 5.00e-02
+332) Best value: -1.73e+00, TR length: 5.00e-02
+336) Best value: -1.73e+00, TR length: 5.00e-02
+340) Best value: -1.66e+00, TR length: 5.00e-02
+344) Best value: -1.66e+00, TR length: 5.00e-02
+348) Best value: -1.66e+00, TR length: 5.00e-02
+352) Best value: -1.66e+00, TR length: 5.00e-02
+356) Best value: -1.62e+00, TR length: 5.00e-02
+360) Best value: -1.28e+00, TR length: 5.00e-02
+364) Best value: -1.28e+00, TR length: 5.00e-02
+368) Best value: -1.28e+00, TR length: 5.00e-02
+372) Best value: -1.28e+00, TR length: 5.00e-02
+376) Best value: -1.28e+00, TR length: 5.00e-02
+380) Best value: -1.28e+00, TR length: 2.50e-02
+384) Best value: -1.05e+00, TR length: 2.50e-02
+388) Best value: -1.05e+00, TR length: 2.50e-02
+392) Best value: -1.05e+00, TR length: 2.50e-02
+396) Best value: -1.05e+00, TR length: 2.50e-02
+400) Best value: -1.04e+00, TR length: 2.50e-02
+404) Best value: -1.04e+00, TR length: 2.50e-02
+408) Best value: -1.04e+00, TR length: 2.50e-02
+412) Best value: -1.04e+00, TR length: 2.50e-02
+416) Best value: -9.62e-01, TR length: 2.50e-02
+420) Best value: -9.62e-01, TR length: 2.50e-02
+424) Best value: -9.62e-01, TR length: 2.50e-02
+428) Best value: -9.62e-01, TR length: 2.50e-02
+432) Best value: -9.62e-01, TR length: 2.50e-02
+436) Best value: -8.91e-01, TR length: 2.50e-02
+440) Best value: -8.91e-01, TR length: 2.50e-02
+444) Best value: -7.98e-01, TR length: 2.50e-02
+448) Best value: -7.98e-01, TR length: 2.50e-02
+452) Best value: -7.98e-01, TR length: 2.50e-02
+456) Best value: -7.98e-01, TR length: 2.50e-02
+460) Best value: -7.98e-01, TR length: 2.50e-02
+464) Best value: -6.43e-01, TR length: 2.50e-02
+468) Best value: -6.43e-01, TR length: 2.50e-02
+472) Best value: -6.43e-01, TR length: 2.50e-02
+476) Best value: -6.43e-01, TR length: 2.50e-02
+480) Best value: -6.43e-01, TR length: 2.50e-02
+484) Best value: -6.43e-01, TR length: 1.25e-02
+488) Best value: -6.43e-01, TR length: 1.25e-02
+492) Best value: -6.06e-01, TR length: 1.25e-02
+496) Best value: -5.59e-01, TR length: 1.25e-02
+500) Best value: -3.93e-01, TR length: 1.25e-02
+504) Best value: -3.53e-01, TR length: 1.25e-02
+508) Best value: -3.53e-01, TR length: 1.25e-02
+512) Best value: -3.02e-01, TR length: 1.25e-02
+516) Best value: -2.70e-01, TR length: 1.25e-02
+520) Best value: -2.27e-01, TR length: 1.25e-02
+524) Best value: -1.81e-01, TR length: 1.25e-02
+528) Best value: -1.81e-01, TR length: 1.25e-02
+532) Best value: -1.81e-01, TR length: 1.25e-02
+536) Best value: -1.81e-01, TR length: 1.25e-02
+540) Best value: -1.81e-01, TR length: 1.25e-02
+544) Best value: -1.81e-01, TR length: 6.25e-03
+
+
+
+
+
+
+
+
+
+

GP-LogEI

We compare TuRBO to qLogEI [2], a recent improvement to the expected improvement (EI) acquisition functions.

+

[2]: Ament, Sebastian, et al., Unexpected Improvements to Expected Improvement for Bayesian Optimization. Advances in Neural Information Processing Systems. 2023

+
+
+
+
+
+
In [8]:
+
+
+
torch.manual_seed(0)
+
+X_logei = get_initial_points(dim, n_init)
+Y_logei = torch.tensor(
+    [eval_objective(x) for x in X_logei], dtype=dtype, device=device
+).unsqueeze(-1)
+
+# Cap the number of evals when running smoke test
+max_evals = min(len(Y_turbo), n_init + 2 * batch_size) if SMOKE_TEST else len(Y_turbo)
+while len(Y_logei) < max_evals:
+    train_Y = (Y_logei - Y_logei.mean()) / Y_logei.std()
+    likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))
+    model = SingleTaskGP(X_logei, train_Y, likelihood=likelihood)
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+
+    # Create a batch
+    log_ei = qLogExpectedImprovement(model, train_Y.max())
+    candidate, acq_value = optimize_acqf(
+        log_ei,
+        bounds=torch.stack(
+            [
+                torch.zeros(dim, dtype=dtype, device=device),
+                torch.ones(dim, dtype=dtype, device=device),
+            ]
+        ),
+        q=batch_size,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+    Y_next = torch.tensor(
+        [eval_objective(x) for x in candidate], dtype=dtype, device=device
+    ).unsqueeze(-1)
+
+    # Append data
+    X_logei = torch.cat((X_logei, candidate), axis=0)
+    Y_logei = torch.cat((Y_logei, Y_next), axis=0)
+
+    # Print current status
+    print(f"{len(X_logei)}) Best value: {Y_logei.max().item():.2e}")
+
+
+
+
+
+
+
+
+
+
44) Best value: -1.15e+01
+48) Best value: -1.04e+01
+52) Best value: -1.02e+01
+56) Best value: -9.98e+00
+60) Best value: -9.62e+00
+64) Best value: -9.10e+00
+68) Best value: -9.10e+00
+72) Best value: -8.87e+00
+76) Best value: -8.87e+00
+80) Best value: -8.75e+00
+84) Best value: -8.18e+00
+88) Best value: -7.58e+00
+92) Best value: -7.24e+00
+96) Best value: -6.86e+00
+100) Best value: -6.75e+00
+104) Best value: -6.35e+00
+108) Best value: -5.74e+00
+112) Best value: -5.43e+00
+116) Best value: -5.25e+00
+120) Best value: -4.66e+00
+124) Best value: -4.66e+00
+128) Best value: -4.66e+00
+132) Best value: -4.66e+00
+136) Best value: -4.55e+00
+140) Best value: -4.36e+00
+144) Best value: -4.24e+00
+148) Best value: -4.22e+00
+152) Best value: -4.22e+00
+156) Best value: -3.97e+00
+160) Best value: -3.86e+00
+164) Best value: -3.63e+00
+168) Best value: -3.63e+00
+172) Best value: -3.59e+00
+176) Best value: -3.59e+00
+180) Best value: -3.59e+00
+184) Best value: -3.59e+00
+188) Best value: -3.20e+00
+192) Best value: -3.20e+00
+196) Best value: -3.20e+00
+200) Best value: -3.20e+00
+204) Best value: -3.20e+00
+208) Best value: -3.20e+00
+212) Best value: -2.64e+00
+216) Best value: -2.64e+00
+220) Best value: -2.64e+00
+224) Best value: -2.62e+00
+228) Best value: -2.62e+00
+232) Best value: -2.62e+00
+236) Best value: -2.62e+00
+240) Best value: -2.49e+00
+244) Best value: -2.49e+00
+248) Best value: -2.49e+00
+252) Best value: -2.49e+00
+256) Best value: -2.49e+00
+260) Best value: -2.49e+00
+264) Best value: -2.49e+00
+268) Best value: -2.49e+00
+272) Best value: -2.12e+00
+276) Best value: -2.12e+00
+280) Best value: -2.11e+00
+284) Best value: -2.11e+00
+288) Best value: -2.11e+00
+292) Best value: -2.11e+00
+296) Best value: -2.11e+00
+300) Best value: -2.11e+00
+304) Best value: -2.11e+00
+308) Best value: -2.11e+00
+312) Best value: -2.11e+00
+316) Best value: -2.11e+00
+320) Best value: -2.11e+00
+324) Best value: -2.11e+00
+328) Best value: -2.11e+00
+332) Best value: -2.11e+00
+336) Best value: -2.11e+00
+340) Best value: -2.11e+00
+344) Best value: -2.11e+00
+348) Best value: -2.11e+00
+352) Best value: -2.11e+00
+356) Best value: -2.11e+00
+360) Best value: -2.11e+00
+364) Best value: -2.11e+00
+368) Best value: -2.11e+00
+372) Best value: -2.11e+00
+376) Best value: -2.11e+00
+380) Best value: -2.11e+00
+384) Best value: -2.11e+00
+388) Best value: -2.11e+00
+392) Best value: -2.11e+00
+396) Best value: -2.11e+00
+400) Best value: -2.11e+00
+404) Best value: -2.11e+00
+408) Best value: -2.11e+00
+412) Best value: -2.11e+00
+416) Best value: -2.11e+00
+420) Best value: -2.11e+00
+424) Best value: -2.11e+00
+428) Best value: -2.11e+00
+432) Best value: -2.11e+00
+436) Best value: -2.11e+00
+440) Best value: -2.11e+00
+444) Best value: -2.11e+00
+448) Best value: -2.11e+00
+452) Best value: -2.11e+00
+456) Best value: -2.11e+00
+460) Best value: -2.11e+00
+464) Best value: -2.11e+00
+468) Best value: -2.11e+00
+472) Best value: -2.11e+00
+476) Best value: -2.11e+00
+480) Best value: -2.11e+00
+484) Best value: -2.11e+00
+488) Best value: -2.11e+00
+492) Best value: -2.11e+00
+496) Best value: -2.11e+00
+500) Best value: -2.11e+00
+504) Best value: -2.11e+00
+508) Best value: -2.11e+00
+512) Best value: -2.11e+00
+516) Best value: -2.11e+00
+520) Best value: -2.11e+00
+524) Best value: -2.11e+00
+528) Best value: -2.11e+00
+532) Best value: -2.11e+00
+536) Best value: -2.11e+00
+540) Best value: -2.11e+00
+544) Best value: -2.11e+00
+
+
+
+
+
+
+
+
+
+

GP-EI

+
+
+
+
+
+
In [9]:
+
+
+
torch.manual_seed(0)
+
+X_ei = get_initial_points(dim, n_init)
+Y_ei = torch.tensor(
+    [eval_objective(x) for x in X_ei], dtype=dtype, device=device
+).unsqueeze(-1)
+
+while len(Y_ei) < len(Y_turbo):
+    train_Y = (Y_ei - Y_ei.mean()) / Y_ei.std()
+    likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))
+    model = SingleTaskGP(X_ei, train_Y, likelihood=likelihood)
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    fit_gpytorch_mll(mll)
+
+    # Create a batch
+    ei = qExpectedImprovement(model, train_Y.max())
+    candidate, acq_value = optimize_acqf(
+        ei,
+        bounds=torch.stack(
+            [
+                torch.zeros(dim, dtype=dtype, device=device),
+                torch.ones(dim, dtype=dtype, device=device),
+            ]
+        ),
+        q=batch_size,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+    Y_next = torch.tensor(
+        [eval_objective(x) for x in candidate], dtype=dtype, device=device
+    ).unsqueeze(-1)
+
+    # Append data
+    X_ei = torch.cat((X_ei, candidate), axis=0)
+    Y_ei = torch.cat((Y_ei, Y_next), axis=0)
+
+    # Print current status
+    print(f"{len(X_ei)}) Best value: {Y_ei.max().item():.2e}")
+
+
+
+
+
+
+
+
+
+
44) Best value: -1.13e+01
+48) Best value: -1.04e+01
+52) Best value: -9.96e+00
+56) Best value: -8.97e+00
+60) Best value: -8.73e+00
+64) Best value: -8.73e+00
+68) Best value: -8.73e+00
+72) Best value: -8.68e+00
+76) Best value: -8.68e+00
+80) Best value: -8.68e+00
+84) Best value: -8.68e+00
+88) Best value: -8.68e+00
+92) Best value: -8.68e+00
+96) Best value: -8.68e+00
+100) Best value: -8.68e+00
+104) Best value: -8.68e+00
+108) Best value: -8.68e+00
+112) Best value: -8.68e+00
+116) Best value: -8.68e+00
+120) Best value: -8.68e+00
+124) Best value: -8.68e+00
+128) Best value: -8.68e+00
+132) Best value: -8.68e+00
+136) Best value: -8.68e+00
+140) Best value: -8.68e+00
+144) Best value: -8.68e+00
+148) Best value: -8.68e+00
+152) Best value: -8.68e+00
+156) Best value: -8.68e+00
+160) Best value: -8.68e+00
+164) Best value: -8.68e+00
+168) Best value: -8.68e+00
+172) Best value: -8.68e+00
+176) Best value: -8.68e+00
+180) Best value: -8.68e+00
+184) Best value: -8.68e+00
+188) Best value: -8.68e+00
+192) Best value: -8.68e+00
+196) Best value: -8.68e+00
+200) Best value: -8.68e+00
+204) Best value: -8.68e+00
+208) Best value: -8.68e+00
+212) Best value: -8.68e+00
+216) Best value: -8.68e+00
+220) Best value: -8.68e+00
+224) Best value: -8.68e+00
+228) Best value: -8.68e+00
+232) Best value: -8.68e+00
+236) Best value: -8.68e+00
+240) Best value: -8.68e+00
+244) Best value: -8.68e+00
+248) Best value: -8.68e+00
+252) Best value: -8.68e+00
+256) Best value: -8.68e+00
+260) Best value: -8.68e+00
+264) Best value: -8.68e+00
+268) Best value: -8.68e+00
+272) Best value: -8.68e+00
+276) Best value: -8.68e+00
+280) Best value: -8.68e+00
+284) Best value: -8.68e+00
+288) Best value: -8.68e+00
+292) Best value: -8.68e+00
+296) Best value: -8.68e+00
+300) Best value: -8.68e+00
+304) Best value: -8.68e+00
+308) Best value: -8.68e+00
+312) Best value: -8.68e+00
+316) Best value: -8.68e+00
+320) Best value: -8.68e+00
+324) Best value: -8.68e+00
+328) Best value: -8.68e+00
+332) Best value: -8.68e+00
+336) Best value: -8.68e+00
+340) Best value: -8.68e+00
+344) Best value: -8.68e+00
+348) Best value: -8.68e+00
+352) Best value: -8.68e+00
+356) Best value: -8.68e+00
+360) Best value: -8.68e+00
+364) Best value: -8.68e+00
+368) Best value: -8.68e+00
+372) Best value: -8.68e+00
+376) Best value: -8.68e+00
+380) Best value: -8.68e+00
+384) Best value: -8.68e+00
+388) Best value: -8.68e+00
+392) Best value: -8.68e+00
+396) Best value: -8.68e+00
+400) Best value: -8.68e+00
+404) Best value: -8.68e+00
+408) Best value: -8.68e+00
+412) Best value: -8.68e+00
+416) Best value: -8.68e+00
+420) Best value: -8.68e+00
+424) Best value: -8.68e+00
+428) Best value: -8.68e+00
+432) Best value: -8.68e+00
+436) Best value: -8.68e+00
+440) Best value: -8.68e+00
+444) Best value: -8.68e+00
+448) Best value: -8.68e+00
+452) Best value: -8.68e+00
+456) Best value: -8.68e+00
+460) Best value: -8.68e+00
+464) Best value: -8.68e+00
+468) Best value: -8.68e+00
+472) Best value: -8.68e+00
+476) Best value: -8.68e+00
+480) Best value: -8.68e+00
+484) Best value: -8.68e+00
+488) Best value: -8.68e+00
+492) Best value: -8.68e+00
+496) Best value: -8.68e+00
+500) Best value: -8.68e+00
+504) Best value: -8.68e+00
+508) Best value: -8.68e+00
+512) Best value: -8.68e+00
+516) Best value: -8.68e+00
+520) Best value: -8.68e+00
+524) Best value: -8.68e+00
+528) Best value: -8.68e+00
+532) Best value: -8.68e+00
+536) Best value: -8.68e+00
+540) Best value: -8.68e+00
+544) Best value: -8.68e+00
+
+
+
+
+
+
+
+
+
+

Sobol

+
+
+
+
+
+
In [10]:
+
+
+
X_Sobol = (
+    SobolEngine(dim, scramble=True, seed=0)
+    .draw(len(X_turbo))
+    .to(dtype=dtype, device=device)
+)
+Y_Sobol = torch.tensor(
+    [eval_objective(x) for x in X_Sobol], dtype=dtype, device=device
+).unsqueeze(-1)
+
+
+
+
+
+
+
+
+

Compare the methods

+
+
+
+
+
+
In [11]:
+
+
+
import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+from matplotlib import rc
+
+%matplotlib inline
+
+
+names = ["TuRBO-1", "LogEI", "EI", "Sobol"]
+runs = [Y_turbo, Y_logei, Y_ei, Y_Sobol]
+fig, ax = plt.subplots(figsize=(8, 6))
+
+for name, run in zip(names, runs):
+    fx = np.maximum.accumulate(run.cpu())
+    plt.plot(fx, marker="", lw=3)
+
+plt.plot([0, len(Y_turbo)], [fun.optimal_value, fun.optimal_value], "k--", lw=3)
+plt.xlabel("Function value", fontsize=18)
+plt.xlabel("Number of evaluations", fontsize=18)
+plt.title("20D Ackley", fontsize=24)
+plt.xlim([0, len(Y_turbo)])
+plt.ylim([-15, 1])
+
+plt.grid(True)
+plt.tight_layout()
+plt.legend(
+    names + ["Global optimal value"],
+    loc="lower center",
+    bbox_to_anchor=(0, -0.08, 1, 1),
+    bbox_transform=plt.gcf().transFigure,
+    ncol=5,
+    fontsize=16,
+)
+plt.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+
\ No newline at end of file diff --git a/website-old/_tutorials/vae_mnist.html b/website-old/_tutorials/vae_mnist.html new file mode 100644 index 0000000000..8dc53e105f --- /dev/null +++ b/website-old/_tutorials/vae_mnist.html @@ -0,0 +1,507 @@ + + + +
+
+
+
+

VAE MNIST example: BO in a latent space

+
+
+
+
+
+
+

In this tutorial, we use the MNIST dataset and some standard PyTorch examples to show a synthetic problem where the input to the objective function is a 28 x 28 image. The main idea is to train a variational auto-encoder (VAE) on the MNIST dataset and run Bayesian Optimization in the latent space. We also refer readers to this tutorial, which discusses the method of jointly training a VAE with a predictor (e.g., classifier), and shows a similar tutorial for the MNIST setting.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+import torch
+
+import torch.nn as nn
+import torch.nn.functional as F
+import torch.optim as optim
+from torchvision import datasets  # transforms
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+dtype = torch.double
+SMOKE_TEST = os.environ.get("SMOKE_TEST", False)
+
+
+
+
+
+
+
+
+

Problem setup

Let's first define our synthetic expensive-to-evaluate objective function. We assume that it takes the following form:

+

$$\text{image} \longrightarrow \text{image classifier} \longrightarrow \text{scoring function} +\longrightarrow \text{score}.$$

+

The classifier is a convolutional neural network (CNN) trained using the architecture of the PyTorch CNN example.

+
+
+
+
+
+
In [2]:
+
+
+
class Net(nn.Module):
+    def __init__(self):
+        super(Net, self).__init__()
+        self.conv1 = nn.Conv2d(1, 20, 5, 1)
+        self.conv2 = nn.Conv2d(20, 50, 5, 1)
+        self.fc1 = nn.Linear(4 * 4 * 50, 500)
+        self.fc2 = nn.Linear(500, 10)
+
+    def forward(self, x):
+        x = F.relu(self.conv1(x))
+        x = F.max_pool2d(x, 2, 2)
+        x = F.relu(self.conv2(x))
+        x = F.max_pool2d(x, 2, 2)
+        x = x.view(-1, 4 * 4 * 50)
+        x = F.relu(self.fc1(x))
+        x = self.fc2(x)
+        return F.log_softmax(x, dim=1)
+
+
+
+
+
+
+
+
In [4]:
+
+
+
def get_pretrained_dir() -> str:
+    """
+    Get the directory of pretrained models, which are in the BoTorch repo.
+
+    Returns the location specified by PRETRAINED_LOCATION if that env
+    var is set; otherwise checks if we are in a likely part of the BoTorch
+    repo (botorch/botorch or botorch/tutorials) and returns the right path.
+    """
+    if "PRETRAINED_LOCATION" in os.environ.keys():
+        return os.environ["PRETRAINED_LOCATION"]
+    cwd = os.getcwd()
+    folder = os.path.basename(cwd)
+    # automated tests run from botorch folder
+    if folder == "botorch":  
+        return os.path.join(cwd, "tutorials/pretrained_models/")
+    # typical case (running from tutorial folder)
+    elif folder == "tutorials":
+        return os.path.join(cwd, "pretrained_models/")
+    raise FileNotFoundError("Could not figure out location of pretrained models.")
+
+
+
+
+
+
+
+
In [5]:
+
+
+
cnn_weights_path = os.path.join(get_pretrained_dir(), "mnist_cnn.pt")
+cnn_model = Net().to(dtype=dtype, device=device)
+cnn_state_dict = torch.load(cnn_weights_path, map_location=device, weights_only=True)
+cnn_model.load_state_dict(cnn_state_dict);
+
+
+
+
+
+
+
+
+

Our VAE model follows the PyTorch VAE example, except that we use the same data transform from the CNN tutorial for consistency. We then instantiate the model and again load a pre-trained model. To train these models, we refer readers to the PyTorch Github repository.

+
+
+
+
+
+
In [6]:
+
+
+
class VAE(nn.Module):
+    def __init__(self):
+        super().__init__()
+        self.fc1 = nn.Linear(784, 400)
+        self.fc21 = nn.Linear(400, 20)
+        self.fc22 = nn.Linear(400, 20)
+        self.fc3 = nn.Linear(20, 400)
+        self.fc4 = nn.Linear(400, 784)
+
+    def encode(self, x):
+        h1 = F.relu(self.fc1(x))
+        return self.fc21(h1), self.fc22(h1)
+
+    def reparameterize(self, mu, logvar):
+        std = torch.exp(0.5 * logvar)
+        eps = torch.randn_like(std)
+        return mu + eps * std
+
+    def decode(self, z):
+        h3 = F.relu(self.fc3(z))
+        return torch.sigmoid(self.fc4(h3))
+
+    def forward(self, x):
+        mu, logvar = self.encode(x.view(-1, 784))
+        z = self.reparameterize(mu, logvar)
+        return self.decode(z), mu, logvar
+
+vae_weights_path = os.path.join(get_pretrained_dir(), "mnist_vae.pt")
+vae_model = VAE().to(dtype=dtype, device=device)
+vae_state_dict = torch.load(vae_weights_path, map_location=device, weights_only=True)
+vae_model.load_state_dict(vae_state_dict);
+
+
+
+
+
+
+
+
+

We now define the scoring function that maps digits to scores. The function below prefers the digit '3'.

+
+
+
+
+
+
In [7]:
+
+
+
def score(y):
+    """Returns a 'score' for each digit from 0 to 9. It is modeled as a squared exponential
+    centered at the digit '3'.
+    """
+    return torch.exp(-2 * (y - 3) ** 2)
+
+
+
+
+
+
+
+
+

Given the scoring function, we can now write our overall objective, which as discussed above, starts with an image and outputs a score. Let's say the objective computes the expected score given the probabilities from the classifier.

+
+
+
+
+
+
In [8]:
+
+
+
def score_image(x):
+    """The input x is an image and an expected score 
+    based on the CNN classifier and the scoring 
+    function is returned.
+    """
+    with torch.no_grad():
+        probs = torch.exp(cnn_model(x))  # b x 10
+        scores = score(
+            torch.arange(10, device=device, dtype=dtype)
+        ).expand(probs.shape)
+    return (probs * scores).sum(dim=1)
+
+
+
+
+
+
+
+
+

Finally, we define a helper function decode that takes as input the parameters mu and logvar of the variational distribution and performs reparameterization and the decoding. We use batched Bayesian optimization to search over the parameters mu and logvar

+
+
+
+
+
+
In [9]:
+
+
+
def decode(train_x):
+    with torch.no_grad():
+        decoded = vae_model.decode(train_x)
+    return decoded.view(train_x.shape[0], 1, 28, 28)
+
+
+
+
+
+
+
+
+

Model initialization and initial random batch

We use a SingleTaskGP to model the score of an image generated by a latent representation. The model is initialized with points drawn from $[-6, 6]^{20}$.

+
+
+
+
+
+
In [10]:
+
+
+
from botorch.models import SingleTaskGP
+from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
+from botorch.utils.transforms import normalize, unnormalize
+from botorch.models.transforms import Standardize, Normalize
+
+d = 20
+bounds = torch.tensor([[-6.0] * d, [6.0] * d], device=device, dtype=dtype)
+
+
+def gen_initial_data(n=5):
+    # generate training data
+    train_x = unnormalize(
+        torch.rand(n, d, device=device, dtype=dtype), 
+        bounds=bounds
+    )
+    train_obj = score_image(decode(train_x)).unsqueeze(-1)
+    best_observed_value = train_obj.max().item()
+    return train_x, train_obj, best_observed_value
+
+
+def get_fitted_model(train_x, train_obj, state_dict=None):
+    # initialize and fit model
+    model = SingleTaskGP(
+        train_X=normalize(train_x, bounds), 
+        train_Y=train_obj,
+        outcome_transform=Standardize(m=1)
+    )
+    if state_dict is not None:
+        model.load_state_dict(state_dict)
+    mll = ExactMarginalLogLikelihood(model.likelihood, model)
+    mll.to(train_x)
+    fit_gpytorch_mll(mll)
+    return model
+
+
+
+
+
+
+
+
+
+
<stdin>:1:10: fatal error: 'omp.h' file not found
+#include <omp.h>
+         ^~~~~~~
+1 error generated.
+
+
+
+
+
+
+
[KeOps] Warning : omp.h header is not in the path, disabling OpenMP.
+[KeOps] Warning : Cuda libraries were not detected on the system ; using cpu only mode
+
+
+
+
+
+
+
+
+
+

Define a helper function that performs the essential BO step

The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. For this example, we'll use a small batch of $q=3$.

+
+
+
+
+
+
In [11]:
+
+
+
from botorch.optim import optimize_acqf
+
+
+BATCH_SIZE = 3 if not SMOKE_TEST else 2
+NUM_RESTARTS = 10 if not SMOKE_TEST else 2
+RAW_SAMPLES = 256 if not SMOKE_TEST else 4
+
+
+def optimize_acqf_and_get_observation(acq_func):
+    """Optimizes the acquisition function, and returns a
+    new candidate and a noisy observation"""
+
+    # optimize
+    candidates, _ = optimize_acqf(
+        acq_function=acq_func,
+        bounds=torch.stack(
+            [
+                torch.zeros(d, dtype=dtype, device=device),
+                torch.ones(d, dtype=dtype, device=device),
+            ]
+        ),
+        q=BATCH_SIZE,
+        num_restarts=NUM_RESTARTS,
+        raw_samples=RAW_SAMPLES,
+    )
+
+    # observe new values
+    new_x = unnormalize(candidates.detach(), bounds=bounds)
+    new_obj = score_image(decode(new_x)).unsqueeze(-1)
+    return new_x, new_obj
+
+
+
+
+
+
+
+
+

Perform Bayesian Optimization loop with qEI

The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps: (1) given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$, (2) observe $f(x)$ for each $x$ in the batch, and (3) update the surrogate model. We run N_BATCH=75 iterations. The acquisition function is approximated using MC_SAMPLES=2048 samples. We also initialize the model with 5 randomly drawn points.

+
+
+
+
+
+
In [12]:
+
+
+
from botorch import fit_gpytorch_mll
+from botorch.acquisition.monte_carlo import qExpectedImprovement
+from botorch.sampling.normal import SobolQMCNormalSampler
+
+seed = 1
+torch.manual_seed(seed)
+
+N_BATCH = 25 if not SMOKE_TEST else 3
+best_observed = []
+
+# call helper function to initialize model
+train_x, train_obj, best_value = gen_initial_data(n=5)
+best_observed.append(best_value)
+
+
+
+
+
+
+
+
+
+
[W NNPACK.cpp:64] Could not initialize NNPACK! Reason: Unsupported hardware.
+
+
+
+
+
+
+
+
+
+

We are now ready to run the BO loop (this make take a few minutes, depending on your machine).

+
+
+
+
+
+
In [13]:
+
+
+
import warnings
+from matplotlib import pyplot as plt
+
+warnings.filterwarnings("ignore")
+
+
+print(f"\nRunning BO ", end="")
+
+state_dict = None
+# run N_BATCH rounds of BayesOpt after the initial random batch
+for iteration in range(N_BATCH):
+
+    # fit the model
+    model = get_fitted_model(
+        train_x=train_x,
+        train_obj=train_obj,
+        state_dict=state_dict,
+    )
+
+    # define the qNEI acquisition function
+    qEI = qExpectedImprovement(
+        model=model, best_f=train_obj.max()
+    )
+
+    # optimize and get new observation
+    new_x, new_obj = optimize_acqf_and_get_observation(qEI)
+
+    # update training points
+    train_x = torch.cat((train_x, new_x))
+    train_obj = torch.cat((train_obj, new_obj))
+
+    # update progress
+    best_value = train_obj.max().item()
+    best_observed.append(best_value)
+
+    state_dict = model.state_dict()
+
+    print(".", end="")
+
+
+
+
+
+
+
+
+
+
+Running BO .........................
+
+
+
+
+
+
+
+
+

EI recommends the best point observed so far. We can visualize what the images corresponding to recommended points would have been if the BO process ended at various times. Here, we show the progress of the algorithm by examining the images at 0%, 10%, 25%, 50%, 75%, and 100% completion. The first image is the best image found through the initial random batch.

+
+
+
+
+
+
In [14]:
+
+
+
import numpy as np
+
+from matplotlib import pyplot as plt
+
+%matplotlib inline
+
+
+fig, ax = plt.subplots(1, 6, figsize=(14, 14))
+percentages = np.array([0, 10, 25, 50, 75, 100], dtype=np.float32)
+inds = (N_BATCH * BATCH_SIZE * percentages / 100 + 4).astype(int)
+
+for i, ax in enumerate(ax.flat):
+    b = torch.argmax(score_image(decode(train_x[: inds[i], :])), dim=0)
+    img = decode(train_x[b].view(1, -1)).squeeze().cpu()
+    ax.imshow(img, alpha=0.8, cmap="gray")
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
\ No newline at end of file diff --git a/website/core/Footer.js b/website-old/core/Footer.js similarity index 100% rename from website/core/Footer.js rename to website-old/core/Footer.js diff --git a/website/core/TutorialSidebar.js b/website-old/core/TutorialSidebar.js similarity index 100% rename from website/core/TutorialSidebar.js rename to website-old/core/TutorialSidebar.js diff --git a/website-old/i18n/en.json b/website-old/i18n/en.json new file mode 100644 index 0000000000..b2aa1e82cf --- /dev/null +++ b/website-old/i18n/en.json @@ -0,0 +1,77 @@ +{ + "_comment": "This file is auto-generated by write-translations.js", + "localized-strings": { + "next": "Next", + "previous": "Previous", + "tagline": "Bayesian Optimization in PyTorch", + "docs": { + "acquisition": { + "title": "Acquisition Functions" + }, + "batching": { + "title": "Batching" + }, + "botorch_and_ax": { + "title": "Using BoTorch with Ax" + }, + "constraints": { + "title": "Constraints" + }, + "design_philosophy": { + "title": "Design Philosophy" + }, + "getting_started": { + "title": "Getting Started" + }, + "introduction": { + "title": "Introduction" + }, + "models": { + "title": "Models" + }, + "multi_objective": { + "title": "Multi-Objective Bayesian Optimization" + }, + "objectives": { + "title": "Objectives" + }, + "optimization": { + "title": "Optimization" + }, + "overview": { + "title": "Overview" + }, + "papers": { + "title": "Papers using BoTorch" + }, + "posteriors": { + "title": "Posteriors" + }, + "README": { + "title": "README" + }, + "samplers": { + "title": "Monte Carlo Samplers" + } + }, + "links": { + "Docs": "Docs", + "Tutorials": "Tutorials", + "API Reference": "API Reference", + "Papers": "Papers", + "GitHub": "GitHub" + }, + "categories": { + "About": "About", + "General": "General", + "Basic Concepts": "Basic Concepts", + "Advanced Topics": "Advanced Topics", + "Multi-Objective Optimization": "Multi-Objective Optimization" + } + }, + "pages-strings": { + "Help Translate|recruit community translators for your project": "Help Translate", + "Edit this Doc|recruitment message asking to edit the doc source": "Edit", + "Translate this Doc|recruitment message asking to translate the docs": "Translate" + } +} diff --git a/website-old/package.json b/website-old/package.json new file mode 100644 index 0000000000..162b8d7789 --- /dev/null +++ b/website-old/package.json @@ -0,0 +1,17 @@ +{ + "scripts": { + "examples": "docusaurus-examples", + "start": "docusaurus-start", + "build": "docusaurus-build", + "publish-gh-pages": "docusaurus-publish", + "write-translations": "docusaurus-write-translations", + "version": "docusaurus-version", + "rename-version": "docusaurus-rename-version" + }, + "devDependencies": { + "docusaurus": "^1.14.7" + }, + "dependencies": { + "bl": "^6.0.0" + } +} diff --git a/website-old/pages/api/_modules/botorch/acquisition/acquisition.html b/website-old/pages/api/_modules/botorch/acquisition/acquisition.html new file mode 100644 index 0000000000..c34bc01f19 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/acquisition.html @@ -0,0 +1,260 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.acquisition

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Abstract base module for all botorch acquisition functions."""
+
+from __future__ import annotations
+
+import warnings
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.exceptions import BotorchWarning
+from botorch.models.model import Model, ModelDict
+from botorch.posteriors.posterior import Posterior
+from botorch.sampling.base import MCSampler
+from botorch.sampling.get_sampler import get_sampler
+from torch import Tensor
+from torch.nn import Module
+
+
+
+[docs] +class AcquisitionFunction(Module, ABC): + r"""Abstract base class for acquisition functions. + + Please note that if your acquisition requires a backwards call, + you will need to wrap the backwards call inside of an enable_grad + context to be able to optimize the acquisition. See #1164. + """ + + _log: bool = False # whether the acquisition utilities are in log-space + + def __init__(self, model: Model) -> None: + r"""Constructor for the AcquisitionFunction base class. + + Args: + model: A fitted model. + """ + super().__init__() + self.model: Model = model + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + r"""Informs the acquisition function about pending design points. + + Args: + X_pending: `n x d` Tensor with `n` `d`-dim design points that have + been submitted for evaluation but have not yet been evaluated. + """ + if X_pending is not None: + if X_pending.requires_grad: + warnings.warn( + "Pending points require a gradient but the acquisition function" + " will not provide a gradient to these points.", + BotorchWarning, + stacklevel=2, + ) + self.X_pending = X_pending.detach().clone() + else: + self.X_pending = X_pending
+ + +
+[docs] + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the acquisition function on the candidate set X. + + Args: + X: A `(b) x q x d`-dim Tensor of `(b)` t-batches with `q` `d`-dim + design points each. + + Returns: + A `(b)`-dim Tensor of acquisition function values at the given + design points `X`. + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class OneShotAcquisitionFunction(AcquisitionFunction, ABC): + r""" + Abstract base class for acquisition functions using one-shot optimization + """ + +
+[docs] + @abstractmethod + def get_augmented_q_batch_size(self, q: int) -> int: + r"""Get augmented q batch size for one-shot optimization. + + Args: + q: The number of candidates to consider jointly. + + Returns: + The augmented size for one-shot optimization (including variables + parameterizing the fantasy solutions). + """ + pass # pragma: no cover
+ + +
+[docs] + @abstractmethod + def extract_candidates(self, X_full: Tensor) -> Tensor: + r"""Extract the candidates from a full "one-shot" parameterization. + + Args: + X_full: A `b x q_aug x d`-dim Tensor with `b` t-batches of `q_aug` + design points each. + + Returns: + A `b x q x d`-dim Tensor with `b` t-batches of `q` design points each. + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class MCSamplerMixin(ABC): + r"""A mix-in for adding sampler functionality into an acquisition function class. + + Attributes: + _default_sample_shape: The `sample_shape` for the default sampler. + """ + + _default_sample_shape = torch.Size([512]) + + def __init__(self, sampler: MCSampler | None = None) -> None: + r"""Register the sampler on the acquisition function. + + Args: + sampler: The sampler used to draw base samples for MC-based acquisition + functions. If `None`, a sampler is generated on the fly within + the `get_posterior_samples` method using `get_sampler`. + """ + self.sampler = sampler + +
+[docs] + def get_posterior_samples(self, posterior: Posterior) -> Tensor: + r"""Sample from the posterior using the sampler. + + Args: + posterior: The posterior to sample from. + """ + if self.sampler is None: + self.sampler = get_sampler( + posterior=posterior, sample_shape=self._default_sample_shape + ) + return self.sampler(posterior=posterior)
+ + + @property + def sample_shape(self) -> torch.Size: + return ( + self.sampler.sample_shape + if self.sampler is not None + else self._default_sample_shape + )
+ + + +
+[docs] +class MultiModelAcquisitionFunction(AcquisitionFunction, ABC): + r"""Abstract base class for acquisition functions that require + multiple types of models. + + The intended use case for these acquisition functions are those + where we have multiple models, each serving a distinct purpose. + As an example, we can have a "regression" model that predicts + one or more outcomes, and a "classification" model that predicts + the probabilty that a given parameterization is feasible. The + multi-model acquisition function can then weight the acquisition + value computed with the "regression" model with the feasibility + value predicted by the "classification" model to produce the + composite acquisition value. + + This is currently only a placeholder to help with some development + in Ax. We plan to add some acquisition functions utilizing multiple + models in the future. + """ + + def __init__(self, model_dict: ModelDict) -> None: + r"""Constructor for the MultiModelAcquisitionFunction base class. + + Args: + model_dict: A ModelDict mapping labels to models. + """ + super(AcquisitionFunction, self).__init__() + self.model_dict: ModelDict = model_dict
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/active_learning.html b/website-old/pages/api/_modules/botorch/acquisition/active_learning.html new file mode 100644 index 0000000000..c7dfc5499c --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/active_learning.html @@ -0,0 +1,253 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.active_learning

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Active learning acquisition functions.
+
+.. [Seo2014activedata]
+    S. Seo, M. Wallat, T. Graepel, and K. Obermayer. Gaussian process regression:
+    Active data selection and test point rejection. IJCNN 2000.
+
+.. [Chen2014seqexpdesign]
+    X. Chen and Q. Zhou. Sequential experimental designs for stochastic kriging.
+    Winter Simulation Conference 2014.
+
+.. [Binois2017repexp]
+    M. Binois, J. Huang, R. B. Gramacy, and M. Ludkovski. Replication or
+    exploration? Sequential design for stochastic simulation experiments.
+    ArXiv 2017.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch import settings
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.acquisition.monte_carlo import MCAcquisitionFunction
+from botorch.acquisition.objective import MCAcquisitionObjective, PosteriorTransform
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+
+
+
+[docs] +class qNegIntegratedPosteriorVariance(AcquisitionFunction): + r"""Batch Integrated Negative Posterior Variance for Active Learning. + + This acquisition function quantifies the (negative) integrated posterior variance + (excluding observation noise, computed using MC integration) of the model. + In that, it is a proxy for global model uncertainty, and thus purely focused on + "exploration", rather the "exploitation" of many of the classic Bayesian + Optimization acquisition functions. + + See [Seo2014activedata]_, [Chen2014seqexpdesign]_, and [Binois2017repexp]_. + """ + + def __init__( + self, + model: Model, + mc_points: Tensor, + sampler: MCSampler | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + ) -> None: + r"""q-Integrated Negative Posterior Variance. + + Args: + model: A fitted model. + mc_points: A `batch_shape x N x d` tensor of points to use for + MC-integrating the posterior variance. Usually, these are qMC + samples on the whole design space, but biased sampling directly + allows weighted integration of the posterior variance. + sampler: The sampler used for drawing fantasy samples. In the basic setting + of a standard GP (default) this is a dummy, since the variance of the + model after conditioning does not actually depend on the sampled values. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + X_pending: A `n' x d`-dim Tensor of `n'` design points that have + points that have been submitted for function evaluation but + have not yet been evaluated. + """ + super().__init__(model=model) + self.posterior_transform = posterior_transform + if sampler is None: + # If no sampler is provided, we use the following dummy sampler for the + # fantasize() method in forward. IMPORTANT: This assumes that the posterior + # variance does not depend on the samples y (only on x), which is true for + # standard GP models, but not in general (e.g. for other likelihoods or + # heteroskedastic GPs using a separate noise model fit on data). + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([1])) + self.sampler = sampler + self.X_pending = X_pending + self.register_buffer("mc_points", mc_points) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + # Construct the fantasy model (we actually do not use the full model, + # this is just a convenient way of computing fast posterior covariances + fantasy_model = self.model.fantasize( + X=X, + sampler=self.sampler, + ) + + bdims = tuple(1 for _ in X.shape[:-2]) + if self.model.num_outputs > 1: + # We use q=1 here b/c ScalarizedObjective currently does not fully exploit + # LinearOperator operations and thus may be slow / overly memory-hungry. + # TODO (T52818288): Properly use LinearOperators in scalarize_posterior + mc_points = self.mc_points.view(-1, *bdims, 1, X.size(-1)) + else: + # While we only need marginal variances, we can evaluate for q>1 + # b/c for GPyTorch models lazy evaluation can make this quite a bit + # faster than evaluating in t-batch mode with q-batch size of 1 + mc_points = self.mc_points.view(*bdims, -1, X.size(-1)) + + # evaluate the posterior at the grid points + with settings.propagate_grads(True): + posterior = fantasy_model.posterior( + mc_points, posterior_transform=self.posterior_transform + ) + + neg_variance = posterior.variance.mul(-1.0) + + if self.posterior_transform is None: + # if single-output, shape is 1 x batch_shape x num_grid_points x 1 + return neg_variance.mean(dim=-2).squeeze(-1).squeeze(0) + else: + # if multi-output + obj, shape is num_grid_points x batch_shape x 1 x 1 + return neg_variance.mean(dim=0).squeeze(-1).squeeze(-1)
+
+ + + +
+[docs] +class PairwiseMCPosteriorVariance(MCAcquisitionFunction): + r"""Variance of difference for Active Learning + + Given a model and an objective, calculate the posterior sample variance + of the objective on the difference of pairs of points. See more implementation + details in `forward`. This acquisition function is typically used with a + pairwise model (e.g., PairwiseGP) and a likelihood/link function + on the pair difference (e.g., logistic or probit) for pure exploration + """ + + def __init__( + self, + model: Model, + objective: MCAcquisitionObjective, + sampler: MCSampler | None = None, + ) -> None: + r"""Pairwise Monte Carlo Posterior Variance + + Args: + model: A fitted model. + objective: An MCAcquisitionObjective representing the link function + (e.g., logistic or probit.) applied on the difference of (usually 1-d) + two samples. Can be implemented via GenericMCObjective. + sampler: The sampler used for drawing MC samples. + """ + super().__init__( + model=model, sampler=sampler, objective=objective, X_pending=None + ) + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate PairwiseMCPosteriorVariance on the candidate set `X`. + + Args: + X: A `batch_size x q x d`-dim Tensor. q should be a multiple of 2. + + Returns: + Tensor of shape `batch_size x q` representing the posterior variance + of link function at X that active learning hopes to maximize + """ + if X.shape[-2] == 0 or X.shape[-2] % 2 != 0: + raise RuntimeError( + "q must be a multiple of 2 for PairwiseMCPosteriorVariance" + ) + + # The output is of shape batch_shape x 2 x d + # For PairwiseGP, d = 1 + post = self.model.posterior(X) + samples = self.get_posterior_samples(post) # num_samples x batch_shape x 2 x d + + # The output is of shape num_samples x batch_shape x q/2 x d + # assuming the comparison is made between the 2 * i and 2 * i + 1 elements + samples_diff = samples[..., ::2, :] - samples[..., 1::2, :] + mc_var = self.objective(samples_diff).var(dim=0) + mean_mc_var = mc_var.mean(dim=-1) + + return mean_mc_var
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/analytic.html b/website-old/pages/api/_modules/botorch/acquisition/analytic.html new file mode 100644 index 0000000000..956d3376a0 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/analytic.html @@ -0,0 +1,1354 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.analytic

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Analytic Acquisition Functions that evaluate the posterior without performing
+Monte-Carlo sampling.
+"""
+
+from __future__ import annotations
+
+import math
+
+from abc import ABC
+from contextlib import nullcontext
+from copy import deepcopy
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions import UnsupportedError
+from botorch.exceptions.warnings import legacy_ei_numerics_warning
+from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.gpytorch import GPyTorchModel
+from botorch.models.model import Model
+from botorch.utils.constants import get_constants_like
+from botorch.utils.probability import MVNXPB
+from botorch.utils.probability.utils import (
+    compute_log_prob_feas_from_bounds,
+    log_ndtr as log_Phi,
+    log_phi,
+    ndtr as Phi,
+    phi,
+)
+from botorch.utils.safe_math import log1mexp, logmeanexp
+from botorch.utils.transforms import convert_to_target_pre_hook, t_batch_mode_transform
+from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood
+from torch import Tensor
+from torch.nn.functional import pad
+
+# the following two numbers are needed for _log_ei_helper
+_neg_inv_sqrt2 = -(2**-0.5)
+_log_sqrt_pi_div_2 = math.log(math.pi / 2) / 2
+
+
+
+[docs] +class AnalyticAcquisitionFunction(AcquisitionFunction, ABC): + """Base class for analytic acquisition functions.""" + + def __init__( + self, + model: Model, + posterior_transform: PosteriorTransform | None = None, + ) -> None: + r"""Base constructor for analytic acquisition functions. + + Args: + model: A fitted single-outcome model. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + """ + super().__init__(model=model) + if posterior_transform is None: + if model.num_outputs != 1: + raise UnsupportedError( + "Must specify a posterior transform when using a " + "multi-output model." + ) + else: + if not isinstance(posterior_transform, PosteriorTransform): + raise UnsupportedError( + "AnalyticAcquisitionFunctions only support PosteriorTransforms." + ) + self.posterior_transform = posterior_transform + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + raise UnsupportedError( + "Analytic acquisition functions do not account for X_pending yet." + )
+ + + def _mean_and_sigma( + self, X: Tensor, compute_sigma: bool = True, min_var: float = 1e-12 + ) -> tuple[Tensor, Tensor | None]: + """Computes the first and second moments of the model posterior. + + Args: + X: `batch_shape x q x d`-dim Tensor of model inputs. + compute_sigma: Boolean indicating whether or not to compute the second + moment (default: True). + min_var: The minimum value the variance is clamped too. Should be positive. + + Returns: + A tuple of tensors containing the first and second moments of the model + posterior. Removes the last two dimensions if they have size one. Only + returns a single tensor of means if compute_sigma is True. + """ + self.to(device=X.device) # ensures buffers / parameters are on the same device + posterior = self.model.posterior( + X=X, posterior_transform=self.posterior_transform + ) + mean = posterior.mean.squeeze(-2).squeeze(-1) # removing redundant dimensions + if not compute_sigma: + return mean, None + sigma = posterior.variance.clamp_min(min_var).sqrt().view(mean.shape) + return mean, sigma
+ + + +
+[docs] +class LogProbabilityOfImprovement(AnalyticAcquisitionFunction): + r"""Single-outcome Log Probability of Improvement. + + Logarithm of the probability of improvement over the current best observed value, + computed using the analytic formula under a Normal posterior distribution. Only + supports the case of q=1. Requires the posterior to be Gaussian. The model must be + single-outcome. + + The logarithm of the probability of improvement is numerically better behaved + than the original function, which can lead to significantly improved optimization + of the acquisition function. This is analogous to the common practice of optimizing + the *log* likelihood of a probabilistic model - rather the likelihood - for the + sake of maximium likelihood estimation. + + `logPI(x) = log(P(y >= best_f)), y ~ f(x)` + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> LogPI = LogProbabilityOfImprovement(model, best_f=0.2) + >>> log_pi = LogPI(test_X) + """ + + _log: bool = True + + def __init__( + self, + model: Model, + best_f: float | Tensor, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ): + r"""Single-outcome Probability of Improvement. + + Args: + model: A fitted single-outcome model. + best_f: Either a scalar or a `b`-dim Tensor (batch mode) representing + the best function value observed so far (assumed noiseless). + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.register_buffer("best_f", torch.as_tensor(best_f)) + self.maximize = maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the Log Probability of Improvement on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `(b1 x ... bk)`-dim tensor of Log Probability of Improvement values at + the given design points `X`. + """ + mean, sigma = self._mean_and_sigma(X) + u = _scaled_improvement(mean, sigma, self.best_f, self.maximize) + return log_Phi(u)
+
+ + + +
+[docs] +class ProbabilityOfImprovement(AnalyticAcquisitionFunction): + r"""Single-outcome Probability of Improvement. + + Probability of improvement over the current best observed value, computed + using the analytic formula under a Normal posterior distribution. Only + supports the case of q=1. Requires the posterior to be Gaussian. The model + must be single-outcome. + + `PI(x) = P(y >= best_f), y ~ f(x)` + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> PI = ProbabilityOfImprovement(model, best_f=0.2) + >>> pi = PI(test_X) + """ + + def __init__( + self, + model: Model, + best_f: float | Tensor, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ): + r"""Single-outcome Probability of Improvement. + + Args: + model: A fitted single-outcome model. + best_f: Either a scalar or a `b`-dim Tensor (batch mode) representing + the best function value observed so far (assumed noiseless). + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.register_buffer("best_f", torch.as_tensor(best_f)) + self.maximize = maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the Probability of Improvement on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `(b1 x ... bk)`-dim tensor of Probability of Improvement values at the + given design points `X`. + """ + mean, sigma = self._mean_and_sigma(X) + u = _scaled_improvement(mean, sigma, self.best_f, self.maximize) + return Phi(u)
+
+ + + +
+[docs] +class qAnalyticProbabilityOfImprovement(AnalyticAcquisitionFunction): + r"""Approximate, single-outcome batch Probability of Improvement using MVNXPB. + + This implementation uses MVNXPB, a bivariate conditioning algorithm for + approximating P(a <= Y <= b) for multivariate normal Y. + See [Trinh2015bivariate]_. This (analytic) approximate q-PI is given by + `approx-qPI(X) = P(max Y >= best_f) = 1 - P(Y < best_f), Y ~ f(X), + X = (x_1,...,x_q)`, where `P(Y < best_f)` is estimated using MVNXPB. + """ + + def __init__( + self, + model: Model, + best_f: float | Tensor, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ) -> None: + """qPI using an analytic approximation. + + Args: + model: A fitted single-outcome model. + best_f: Either a scalar or a `b`-dim Tensor (batch mode) representing + the best function value observed so far (assumed noiseless). + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.maximize = maximize + if not torch.is_tensor(best_f): + best_f = torch.tensor(best_f) + self.register_buffer("best_f", best_f) + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + """Evaluate approximate qPI on the candidate set X. + + Args: + X: A `batch_shape x q x d`-dim Tensor of t-batches with `q` `d`-dim design + points each + + Returns: + A `batch_shape`-dim Tensor of approximate Probability of Improvement values + at the given design points `X`, where `batch_shape'` is the broadcasted + batch shape of model and input `X`. + """ + self.best_f = self.best_f.to(X) + posterior = self.model.posterior( + X=X, posterior_transform=self.posterior_transform + ) + + covariance = posterior.distribution.covariance_matrix + bounds = pad( + (self.best_f.unsqueeze(-1) - posterior.distribution.mean).unsqueeze(-1), + pad=(1, 0) if self.maximize else (0, 1), + value=-float("inf") if self.maximize else float("inf"), + ) + # 1 - P(no improvement over best_f) + solver = MVNXPB(covariance_matrix=covariance, bounds=bounds) + return -solver.solve().expm1()
+
+ + + +
+[docs] +class ExpectedImprovement(AnalyticAcquisitionFunction): + r"""Single-outcome Expected Improvement (analytic). + + Computes classic Expected Improvement over the current best observed value, + using the analytic formula for a Normal posterior distribution. Unlike the + MC-based acquisition functions, this relies on the posterior at single test + point being Gaussian (and require the posterior to implement `mean` and + `variance` properties). Only supports the case of `q=1`. The model must be + single-outcome. + + `EI(x) = E(max(f(x) - best_f, 0)),` + + where the expectation is taken over the value of stochastic function `f` at `x`. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> EI = ExpectedImprovement(model, best_f=0.2) + >>> ei = EI(test_X) + + NOTE: It is strongly recommended to use LogExpectedImprovement instead of regular + EI, as it can lead to substantially improved BO performance through improved + numerics. See https://arxiv.org/abs/2310.20708 for details. + """ + + def __init__( + self, + model: Model, + best_f: float | Tensor, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ): + r"""Single-outcome Expected Improvement (analytic). + + Args: + model: A fitted single-outcome model. + best_f: Either a scalar or a `b`-dim Tensor (batch mode) representing + the best function value observed so far (assumed noiseless). + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + """ + legacy_ei_numerics_warning(legacy_name=type(self).__name__) + super().__init__(model=model, posterior_transform=posterior_transform) + self.register_buffer("best_f", torch.as_tensor(best_f)) + self.maximize = maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate Expected Improvement on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + Expected Improvement is computed for each point individually, + i.e., what is considered are the marginal posteriors, not the + joint. + + Returns: + A `(b1 x ... bk)`-dim tensor of Expected Improvement values at the + given design points `X`. + """ + mean, sigma = self._mean_and_sigma(X) + u = _scaled_improvement(mean, sigma, self.best_f, self.maximize) + return sigma * _ei_helper(u)
+
+ + + +
+[docs] +class LogExpectedImprovement(AnalyticAcquisitionFunction): + r"""Single-outcome Log Expected Improvement (analytic). + + Computes the logarithm of the classic Expected Improvement acquisition function, in + a numerically robust manner. In particular, the implementation takes special care + to avoid numerical issues in the computation of the acquisition value and its + gradient in regions where improvement is predicted to be virtually impossible. + + See [Ament2023logei]_ for details. Formally, + + `LogEI(x) = log(E(max(f(x) - best_f, 0))),` + + where the expectation is taken over the value of stochastic function `f` at `x`. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> LogEI = LogExpectedImprovement(model, best_f=0.2) + >>> ei = LogEI(test_X) + """ + + _log: bool = True + + def __init__( + self, + model: Model, + best_f: float | Tensor, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ): + r"""Logarithm of single-outcome Expected Improvement (analytic). + + Args: + model: A fitted single-outcome model. + best_f: Either a scalar or a `b`-dim Tensor (batch mode) representing + the best function value observed so far (assumed noiseless). + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.register_buffer("best_f", torch.as_tensor(best_f)) + self.maximize = maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate logarithm of Expected Improvement on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + Expected Improvement is computed for each point individually, + i.e., what is considered are the marginal posteriors, not the + joint. + + Returns: + A `(b1 x ... bk)`-dim tensor of the logarithm of the Expected Improvement + values at the given design points `X`. + """ + mean, sigma = self._mean_and_sigma(X) + u = _scaled_improvement(mean, sigma, self.best_f, self.maximize) + return _log_ei_helper(u) + sigma.log()
+
+ + + +
+[docs] +class LogConstrainedExpectedImprovement(AnalyticAcquisitionFunction): + r"""Log Constrained Expected Improvement (feasibility-weighted). + + Computes the logarithm of the analytic expected improvement for a Normal posterior + distribution weighted by a probability of feasibility. The objective and + constraints are assumed to be independent and have Gaussian posterior + distributions. Only supports non-batch mode (i.e. `q=1`). The model should be + multi-outcome, with the index of the objective and constraints passed to + the constructor. + + See [Ament2023logei]_ for details. Formally, + + `LogConstrainedEI(x) = log(EI(x)) + Sum_i log(P(y_i \in [lower_i, upper_i]))`, + + where `y_i ~ constraint_i(x)` and `lower_i`, `upper_i` are the lower and + upper bounds for the i-th constraint, respectively. + + Example: + # example where the 0th output has a non-negativity constraint and + # the 1st output is the objective + >>> model = SingleTaskGP(train_X, train_Y) + >>> constraints = {0: (0.0, None)} + >>> LogCEI = LogConstrainedExpectedImprovement(model, 0.2, 1, constraints) + >>> cei = LogCEI(test_X) + """ + + _log: bool = True + + def __init__( + self, + model: Model, + best_f: float | Tensor, + objective_index: int, + constraints: dict[int, tuple[float | None, float | None]], + maximize: bool = True, + ) -> None: + r"""Analytic Log Constrained Expected Improvement. + + Args: + model: A fitted multi-output model. + best_f: Either a scalar or a `b`-dim Tensor (batch mode) representing + the best feasible function value observed so far (assumed noiseless). + objective_index: The index of the objective. + constraints: A dictionary of the form `{i: [lower, upper]}`, where + `i` is the output index, and `lower` and `upper` are lower and upper + bounds on that output (resp. interpreted as -Inf / Inf if None) + maximize: If True, consider the problem a maximization problem. + """ + # Use AcquisitionFunction constructor to avoid check for posterior transform. + super(AnalyticAcquisitionFunction, self).__init__(model=model) + self.posterior_transform = None + self.maximize = maximize + self.objective_index = objective_index + self.constraints = constraints + self.register_buffer("best_f", torch.as_tensor(best_f)) + _preprocess_constraint_bounds(self, constraints=constraints) + self.register_forward_pre_hook(convert_to_target_pre_hook) + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate Constrained Log Expected Improvement on the candidate set X. + + Args: + X: A `(b) x 1 x d`-dim Tensor of `(b)` t-batches of `d`-dim design + points each. + + Returns: + A `(b)`-dim Tensor of Log Expected Improvement values at the given + design points `X`. + """ + means, sigmas = self._mean_and_sigma(X) # (b) x 1 + (m = num constraints) + ind = self.objective_index + mean_obj, sigma_obj = means[..., ind], sigmas[..., ind] + u = _scaled_improvement(mean_obj, sigma_obj, self.best_f, self.maximize) + log_ei = _log_ei_helper(u) + sigma_obj.log() + log_prob_feas = _compute_log_prob_feas(self, means=means, sigmas=sigmas) + return log_ei + log_prob_feas
+
+ + + +
+[docs] +class ConstrainedExpectedImprovement(AnalyticAcquisitionFunction): + r"""Constrained Expected Improvement (feasibility-weighted). + + Computes the analytic expected improvement for a Normal posterior + distribution, weighted by a probability of feasibility. The objective and + constraints are assumed to be independent and have Gaussian posterior + distributions. Only supports non-batch mode (i.e. `q=1`). The model should be + multi-outcome, with the index of the objective and constraints passed to + the constructor. + + `Constrained_EI(x) = EI(x) * Product_i P(y_i \in [lower_i, upper_i])`, + where `y_i ~ constraint_i(x)` and `lower_i`, `upper_i` are the lower and + upper bounds for the i-th constraint, respectively. + + Example: + # example where the 0th output has a non-negativity constraint and + # 1st output is the objective + >>> model = SingleTaskGP(train_X, train_Y) + >>> constraints = {0: (0.0, None)} + >>> cEI = ConstrainedExpectedImprovement(model, 0.2, 1, constraints) + >>> cei = cEI(test_X) + + NOTE: It is strongly recommended to use LogConstrainedExpectedImprovement instead + of regular CEI, as it can lead to substantially improved BO performance through + improved numerics. See https://arxiv.org/abs/2310.20708 for details. + """ + + def __init__( + self, + model: Model, + best_f: float | Tensor, + objective_index: int, + constraints: dict[int, tuple[float | None, float | None]], + maximize: bool = True, + ) -> None: + r"""Analytic Constrained Expected Improvement. + + Args: + model: A fitted multi-output model. + best_f: Either a scalar or a `b`-dim Tensor (batch mode) representing + the best feasible function value observed so far (assumed noiseless). + objective_index: The index of the objective. + constraints: A dictionary of the form `{i: [lower, upper]}`, where + `i` is the output index, and `lower` and `upper` are lower and upper + bounds on that output (resp. interpreted as -Inf / Inf if None) + maximize: If True, consider the problem a maximization problem. + """ + legacy_ei_numerics_warning(legacy_name=type(self).__name__) + # Use AcquisitionFunction constructor to avoid check for posterior transform. + super(AnalyticAcquisitionFunction, self).__init__(model=model) + self.posterior_transform = None + self.maximize = maximize + self.objective_index = objective_index + self.constraints = constraints + self.register_buffer("best_f", torch.as_tensor(best_f)) + _preprocess_constraint_bounds(self, constraints=constraints) + self.register_forward_pre_hook(convert_to_target_pre_hook) + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate Constrained Expected Improvement on the candidate set X. + + Args: + X: A `(b) x 1 x d`-dim Tensor of `(b)` t-batches of `d`-dim design + points each. + + Returns: + A `(b)`-dim Tensor of Expected Improvement values at the given + design points `X`. + """ + means, sigmas = self._mean_and_sigma(X) # (b) x 1 + (m = num constraints) + ind = self.objective_index + mean_obj, sigma_obj = means[..., ind], sigmas[..., ind] + u = _scaled_improvement(mean_obj, sigma_obj, self.best_f, self.maximize) + ei = sigma_obj * _ei_helper(u) + log_prob_feas = _compute_log_prob_feas(self, means=means, sigmas=sigmas) + return ei.mul(log_prob_feas.exp())
+
+ + + +
+[docs] +class LogNoisyExpectedImprovement(AnalyticAcquisitionFunction): + r"""Single-outcome Log Noisy Expected Improvement (via fantasies). + + This computes Log Noisy Expected Improvement by averaging over the Expected + Improvement values of a number of fantasy models. Only supports the case + `q=1`. Assumes that the posterior distribution of the model is Gaussian. + The model must be single-outcome. + + See [Ament2023logei]_ for details. Formally, + + `LogNEI(x) = log(E(max(y - max Y_base), 0))), (y, Y_base) ~ f((x, X_base))`, + + where `X_base` are previously observed points. + + Note: This acquisition function currently relies on using a SingleTaskGP + with known observation noise. In other words, `train_Yvar` must be passed + to the model. (required for noiseless fantasies). + + Example: + >>> model = SingleTaskGP(train_X, train_Y, train_Yvar=train_Yvar) + >>> LogNEI = LogNoisyExpectedImprovement(model, train_X) + >>> nei = LogNEI(test_X) + """ + + _log: bool = True + + def __init__( + self, + model: GPyTorchModel, + X_observed: Tensor, + num_fantasies: int = 20, + maximize: bool = True, + posterior_transform: PosteriorTransform | None = None, + ) -> None: + r"""Single-outcome Noisy Log Expected Improvement (via fantasies). + + Args: + model: A fitted single-outcome model. Only `SingleTaskGP` models with + known observation noise are currently supported. + X_observed: A `n x d` Tensor of observed points that are likely to + be the best observed points so far. + num_fantasies: The number of fantasies to generate. The higher this + number the more accurate the model (at the expense of model + complexity and performance). + maximize: If True, consider the problem a maximization problem. + """ + _check_noisy_ei_model(model=model) + # Sample fantasies. + from botorch.sampling.normal import SobolQMCNormalSampler + + # Drop gradients from model.posterior if X_observed does not require gradients + # as otherwise, gradients of the GP's kernel's hyper-parameters are tracked + # through the rsample_from_base_sample method of GPyTorchPosterior. These + # gradients are usually only required w.r.t. the marginal likelihood. + with nullcontext() if X_observed.requires_grad else torch.no_grad(): + posterior = model.posterior(X=X_observed) + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_fantasies])) + Y_fantasized = sampler(posterior).squeeze(-1) + batch_X_observed = X_observed.expand(num_fantasies, *X_observed.shape) + # The fantasy model will operate in batch mode + fantasy_model = _get_noiseless_fantasy_model( + model=model, batch_X_observed=batch_X_observed, Y_fantasized=Y_fantasized + ) + super().__init__(model=fantasy_model, posterior_transform=posterior_transform) + best_f, _ = Y_fantasized.max(dim=-1) if maximize else Y_fantasized.min(dim=-1) + self.best_f, self.maximize = best_f, maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate logarithm of the mean Expected Improvement on the candidate set X. + + Args: + X: A `b1 x ... bk x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `b1 x ... bk`-dim tensor of Log Noisy Expected Improvement values at + the given design points `X`. + """ + # add batch dimension for broadcasting to fantasy models + mean, sigma = self._mean_and_sigma(X.unsqueeze(-3)) + u = _scaled_improvement(mean, sigma, self.best_f, self.maximize) + log_ei = _log_ei_helper(u) + sigma.log() + # this is mathematically - though not numerically - equivalent to log(mean(ei)) + return logmeanexp(log_ei, dim=-1)
+
+ + + +
+[docs] +class NoisyExpectedImprovement(ExpectedImprovement): + r"""Single-outcome Noisy Expected Improvement (via fantasies). + + This computes Noisy Expected Improvement by averaging over the Expected + Improvement values of a number of fantasy models. Only supports the case + `q=1`. Assumes that the posterior distribution of the model is Gaussian. + The model must be single-outcome. + + `NEI(x) = E(max(y - max Y_baseline), 0)), (y, Y_baseline) ~ f((x, X_baseline))`, + where `X_baseline` are previously observed points. + + Note: This acquisition function currently relies on using a SingleTaskGP + with known observation noise. In other words, `train_Yvar` must be passed + to the model. (required for noiseless fantasies). + + Example: + >>> model = SingleTaskGP(train_X, train_Y, train_Yvar=train_Yvar) + >>> NEI = NoisyExpectedImprovement(model, train_X) + >>> nei = NEI(test_X) + + NOTE: It is strongly recommended to use LogNoisyExpectedImprovement instead + of regular NEI, as it can lead to substantially improved BO performance through + improved numerics. See https://arxiv.org/abs/2310.20708 for details. + """ + + def __init__( + self, + model: GPyTorchModel, + X_observed: Tensor, + num_fantasies: int = 20, + maximize: bool = True, + ) -> None: + r"""Single-outcome Noisy Expected Improvement (via fantasies). + + Args: + model: A fitted single-outcome model. Only `SingleTaskGP` models with + known observation noise are currently supported. + X_observed: A `n x d` Tensor of observed points that are likely to + be the best observed points so far. + num_fantasies: The number of fantasies to generate. The higher this + number the more accurate the model (at the expense of model + complexity and performance). + maximize: If True, consider the problem a maximization problem. + """ + _check_noisy_ei_model(model=model) + legacy_ei_numerics_warning(legacy_name=type(self).__name__) + # Sample fantasies. + from botorch.sampling.normal import SobolQMCNormalSampler + + # Drop gradients from model.posterior if X_observed does not require gradients + # as otherwise, gradients of the GP's kernel's hyper-parameters are tracked + # through the rsample_from_base_sample method of GPyTorchPosterior. These + # gradients are usually only required w.r.t. the marginal likelihood. + with nullcontext() if X_observed.requires_grad else torch.no_grad(): + posterior = model.posterior(X=X_observed) + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_fantasies])) + Y_fantasized = sampler(posterior).squeeze(-1) + batch_X_observed = X_observed.expand(num_fantasies, *X_observed.shape) + # The fantasy model will operate in batch mode + fantasy_model = _get_noiseless_fantasy_model( + model=model, batch_X_observed=batch_X_observed, Y_fantasized=Y_fantasized + ) + best_f, _ = Y_fantasized.max(dim=-1) if maximize else Y_fantasized.min(dim=-1) + super().__init__(model=fantasy_model, best_f=best_f, maximize=maximize) + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate Expected Improvement on the candidate set X. + + Args: + X: A `b1 x ... bk x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `b1 x ... bk`-dim tensor of Noisy Expected Improvement values at + the given design points `X`. + """ + # add batch dimension for broadcasting to fantasy models + mean, sigma = self._mean_and_sigma(X.unsqueeze(-3)) + u = _scaled_improvement(mean, sigma, self.best_f, self.maximize) + return (sigma * _ei_helper(u)).mean(dim=-1)
+
+ + + +
+[docs] +class UpperConfidenceBound(AnalyticAcquisitionFunction): + r"""Single-outcome Upper Confidence Bound (UCB). + + Analytic upper confidence bound that comprises of the posterior mean plus an + additional term: the posterior standard deviation weighted by a trade-off + parameter, `beta`. Only supports the case of `q=1` (i.e. greedy, non-batch + selection of design points). The model must be single-outcome. + + `UCB(x) = mu(x) + sqrt(beta) * sigma(x)`, where `mu` and `sigma` are the + posterior mean and standard deviation, respectively. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> UCB = UpperConfidenceBound(model, beta=0.2) + >>> ucb = UCB(test_X) + """ + + def __init__( + self, + model: Model, + beta: float | Tensor, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ) -> None: + r"""Single-outcome Upper Confidence Bound. + + Args: + model: A fitted single-outcome GP model (must be in batch mode if + candidate sets X will be) + beta: Either a scalar or a one-dim tensor with `b` elements (batch mode) + representing the trade-off parameter between mean and covariance + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.register_buffer("beta", torch.as_tensor(beta)) + self.maximize = maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the Upper Confidence Bound on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `(b1 x ... bk)`-dim tensor of Upper Confidence Bound values at the + given design points `X`. + """ + mean, sigma = self._mean_and_sigma(X) + return (mean if self.maximize else -mean) + self.beta.sqrt() * sigma
+
+ + + +
+[docs] +class PosteriorMean(AnalyticAcquisitionFunction): + r"""Single-outcome Posterior Mean. + + Only supports the case of q=1. Requires the model's posterior to have a + `mean` property. The model must be single-outcome. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> PM = PosteriorMean(model) + >>> pm = PM(test_X) + """ + + def __init__( + self, + model: Model, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ) -> None: + r"""Single-outcome Posterior Mean. + + Args: + model: A fitted single-outcome GP model (must be in batch mode if + candidate sets X will be) + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. Note + that if `maximize=False`, the posterior mean is negated. As a + consequence `optimize_acqf(PosteriorMean(gp, maximize=False))` + actually returns -1 * minimum of the posterior mean. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.maximize = maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the posterior mean on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `(b1 x ... bk)`-dim tensor of Posterior Mean values at the + given design points `X`. + """ + mean, _ = self._mean_and_sigma(X, compute_sigma=False) + return mean if self.maximize else -mean
+
+ + + +
+[docs] +class ScalarizedPosteriorMean(AnalyticAcquisitionFunction): + r"""Scalarized Posterior Mean. + + This acquisition function returns a scalarized (across the q-batch) + posterior mean given a vector of weights. + """ + + def __init__( + self, + model: Model, + weights: Tensor, + posterior_transform: PosteriorTransform | None = None, + ) -> None: + r"""Scalarized Posterior Mean. + + Args: + model: A fitted single-outcome model. + weights: A tensor of shape `q` for scalarization. In order to minimize + the scalarized posterior mean, pass -weights. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.register_buffer("weights", weights) + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the scalarized posterior mean on the candidate set X. + + Args: + X: A `(b) x q x d`-dim Tensor of `(b)` t-batches of `d`-dim design + points each. + + Returns: + A `(b)`-dim Tensor of Posterior Mean values at the given design + points `X`. + """ + return self._mean_and_sigma(X, compute_sigma=False)[0] @ self.weights
+
+ + + +
+[docs] +class PosteriorStandardDeviation(AnalyticAcquisitionFunction): + r"""Single-outcome Posterior Standard Deviation. + + An acquisition function for pure exploration. + Only supports the case of q=1. Requires the model's posterior to have + `mean` and `variance` properties. The model must be either single-outcome + or combined with a `posterior_transform` to produce a single-output posterior. + + Example: + >>> import torch + >>> from botorch.models.gp_regression import SingleTaskGP + >>> from botorch.models.transforms.input import Normalize + >>> from botorch.models.transforms.outcome import Standardize + >>> + >>> # Set up a model + >>> train_X = torch.rand(20, 2, dtype=torch.float64) + >>> train_Y = torch.sin(train_X).sum(dim=1, keepdim=True) + >>> model = SingleTaskGP( + ... train_X, train_Y, outcome_transform=Standardize(m=1), + ... input_transform=Normalize(d=2), + ... ) + >>> # Now set up the acquisition function + >>> PSTD = PosteriorStandardDeviation(model) + >>> test_X = torch.zeros((1, 2), dtype=torch.float64) + >>> std = PSTD(test_X) + >>> std.item() + 0.16341639895667773 + """ + + def __init__( + self, + model: Model, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + ) -> None: + r"""Single-outcome Posterior Mean. + + Args: + model: A fitted single-outcome GP model (must be in batch mode if + candidate sets X will be) + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. Note + that if `maximize=False`, the posterior standard deviation is negated. + As a consequence, + `optimize_acqf(PosteriorStandardDeviation(gp, maximize=False))` + actually returns -1 * minimum of the posterior standard deviation. + """ + super().__init__(model=model, posterior_transform=posterior_transform) + self.maximize = maximize + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the posterior standard deviation on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `(b1 x ... bk)`-dim tensor of Posterior Mean values at the + given design points `X`. + """ + _, std = self._mean_and_sigma(X) + return std if self.maximize else -std
+
+ + + +# --------------- Helper functions for analytic acquisition functions. --------------- + + +def _scaled_improvement( + mean: Tensor, sigma: Tensor, best_f: Tensor, maximize: bool +) -> Tensor: + """Returns `u = (mean - best_f) / sigma`, -u if maximize == True.""" + u = (mean - best_f) / sigma + return u if maximize else -u + + +def _ei_helper(u: Tensor) -> Tensor: + """Computes phi(u) + u * Phi(u), where phi and Phi are the standard normal + pdf and cdf, respectively. This is used to compute Expected Improvement. + """ + return phi(u) + u * Phi(u) + + +def _log_ei_helper(u: Tensor) -> Tensor: + """Accurately computes log(phi(u) + u * Phi(u)) in a differentiable manner for u in + [-10^100, 10^100] in double precision, and [-10^20, 10^20] in single precision. + Beyond these intervals, a basic squaring of u can lead to floating point overflow. + In contrast, the implementation in _ei_helper only yields usable gradients down to + u ~ -10. As a consequence, _log_ei_helper improves the range of inputs for which a + backward pass yields usable gradients by many orders of magnitude. + """ + if not (u.dtype == torch.float32 or u.dtype == torch.float64): + raise TypeError( + f"LogExpectedImprovement only supports torch.float32 and torch.float64 " + f"dtypes, but received {u.dtype=}." + ) + # The function has two branching decisions. The first is u < bound, and in this + # case, just taking the logarithm of the naive _ei_helper implementation works. + bound = -1 + u_upper = u.masked_fill(u < bound, bound) # mask u to avoid NaNs in gradients + log_ei_upper = _ei_helper(u_upper).log() + + # When u <= bound, we need to be more careful and rearrange the EI formula as + # log(phi(u)) + log(1 - exp(w)), where w = log(abs(u) * Phi(u) / phi(u)). + # To this end, a second branch is necessary, depending on whether or not u is + # smaller than approximately the negative inverse square root of the machine + # precision. Below this point, numerical issues in computing log(1 - exp(w)) occur + # as w approaches zero from below, even though the relative contribution to log_ei + # vanishes in machine precision at that point. + neg_inv_sqrt_eps = -1e6 if u.dtype == torch.float64 else -1e3 + + # mask u for to avoid NaNs in gradients in first and second branch + u_lower = u.masked_fill(u > bound, bound) + u_eps = u_lower.masked_fill(u < neg_inv_sqrt_eps, neg_inv_sqrt_eps) + # compute the logarithm of abs(u) * Phi(u) / phi(u) for moderately large negative u + w = _log_abs_u_Phi_div_phi(u_eps) + + # 1) Now, we use a special implementation of log(1 - exp(w)) for moderately + # large negative numbers, and + # 2) capture the leading order of log(1 - exp(w)) for very large negative numbers. + # The second special case is technically only required for single precision numbers + # but does "the right thing" regardless. + log_ei_lower = log_phi(u) + ( + torch.where( + u > neg_inv_sqrt_eps, + log1mexp(w), + # The contribution of the next term relative to log_phi vanishes when + # w_lower << eps but captures the leading order of the log1mexp term. + -2 * u_lower.abs().log(), + ) + ) + return torch.where(u > bound, log_ei_upper, log_ei_lower) + + +def _log_abs_u_Phi_div_phi(u: Tensor) -> Tensor: + """Computes log(abs(u) * Phi(u) / phi(u)), where phi and Phi are the normal pdf + and cdf, respectively. The function is valid for u < 0. + + NOTE: In single precision arithmetic, the function becomes numerically unstable for + u < -1e3. For this reason, a second branch in _log_ei_helper is necessary to handle + this regime, where this function approaches -abs(u)^-2 asymptotically. + + The implementation is based on the following implementation of the logarithm of + the scaled complementary error function (i.e. erfcx). Since we only require the + positive branch for _log_ei_helper, _log_abs_u_Phi_div_phi does not have a branch, + but is only valid for u < 0 (so that _neg_inv_sqrt2 * u > 0). + + def logerfcx(x: Tensor) -> Tensor: + return torch.where( + x < 0, + torch.erfc(x.masked_fill(x > 0, 0)).log() + x**2, + torch.special.erfcx(x.masked_fill(x < 0, 0)).log(), + ) + + Further, it is important for numerical accuracy to move u.abs() into the + logarithm, rather than adding u.abs().log() to logerfcx. This is the reason + for the rather complex name of this function: _log_abs_u_Phi_div_phi. + """ + # get_constants_like allocates tensors with the appropriate dtype and device and + # caches the result, which improves efficiency. + a, b = get_constants_like(values=(_neg_inv_sqrt2, _log_sqrt_pi_div_2), ref=u) + return torch.log(torch.special.erfcx(a * u) * u.abs()) + b + + +def _check_noisy_ei_model(model: GPyTorchModel) -> None: + message = ( + "Only single-output `SingleTaskGP` models with known observation noise " + "are currently supported for fantasy-based NEI & LogNEI." + ) + if not isinstance(model, SingleTaskGP): + raise UnsupportedError(f"{message} Model is not a `SingleTaskGP`.") + if not isinstance(model.likelihood, FixedNoiseGaussianLikelihood): + raise UnsupportedError( + f"{message} Model likelihood is not a `FixedNoiseGaussianLikelihood`." + ) + if model.num_outputs != 1: + raise UnsupportedError(f"{message} Model has {model.num_outputs} outputs.") + + +def _get_noiseless_fantasy_model( + model: SingleTaskGP, batch_X_observed: Tensor, Y_fantasized: Tensor +) -> SingleTaskGP: + r"""Construct a fantasy model from a fitted model and provided fantasies. + + The fantasy model uses the hyperparameters from the original fitted model and + assumes the fantasies are noiseless. + + Args: + model: A fitted SingleTaskGP with known observation noise. + batch_X_observed: A `b x n x d` tensor of inputs where `b` is the number of + fantasies. + Y_fantasized: A `b x n` tensor of fantasized targets where `b` is the number of + fantasies. + + Returns: + The fantasy model. + """ + # initialize a copy of SingleTaskGP on the original training inputs + # this makes SingleTaskGP a non-batch GP, so that the same hyperparameters + # are used across all batches (by default, a GP with batched training data + # uses independent hyperparameters for each batch). + + # We don't want to use the true `outcome_transform` and `input_transform` here + # since the data being passed has already been transformed. We thus pass `None` + # and will instead set them afterwards. + fantasy_model = SingleTaskGP( + train_X=model.train_inputs[0], + train_Y=model.train_targets.unsqueeze(-1), + train_Yvar=model.likelihood.noise_covar.noise.unsqueeze(-1), + covar_module=deepcopy(model.covar_module), + mean_module=deepcopy(model.mean_module), + outcome_transform=None, + input_transform=None, + ) + + Yvar = torch.full_like(Y_fantasized, 1e-7) + + # Set the outcome and input transforms of the fantasy model. + # The transforms should already be in eval mode but just set them to be sure + outcome_transform = getattr(model, "outcome_transform", None) + if outcome_transform is not None: + outcome_transform = deepcopy(outcome_transform).eval() + fantasy_model.outcome_transform = outcome_transform + # Need to transform the outcome just as in the SingleTaskGP constructor. + # Need to unsqueeze for BoTorch and then squeeze again for GPyTorch. + # Not transforming Yvar because 1e-7 is already close to 0 and it is a + # relative, not absolute, value. + Y_fantasized, _ = outcome_transform( + Y_fantasized.unsqueeze(-1), Yvar.unsqueeze(-1) + ) + Y_fantasized = Y_fantasized.squeeze(-1) + input_transform = getattr(model, "input_transform", None) + if input_transform is not None: + fantasy_model.input_transform = deepcopy(input_transform).eval() + + # update training inputs/targets to be batch mode fantasies + fantasy_model.set_train_data( + inputs=batch_X_observed, targets=Y_fantasized, strict=False + ) + # use noiseless fantasies + fantasy_model.likelihood.noise_covar.noise = Yvar + + return fantasy_model + + +def _preprocess_constraint_bounds( + acqf: LogConstrainedExpectedImprovement | ConstrainedExpectedImprovement, + constraints: dict[int, tuple[float | None, float | None]], +) -> None: + r"""Set up constraint bounds. + + Args: + constraints: A dictionary of the form `{i: [lower, upper]}`, where + `i` is the output index, and `lower` and `upper` are lower and upper + bounds on that output (resp. interpreted as -Inf / Inf if None) + """ + con_lower, con_lower_inds = [], [] + con_upper, con_upper_inds = [], [] + con_both, con_both_inds = [], [] + con_indices = list(constraints.keys()) + if len(con_indices) == 0: + raise ValueError("There must be at least one constraint.") + if acqf.objective_index in con_indices: + raise ValueError( + "Output corresponding to objective should not be a constraint." + ) + for k in con_indices: + if constraints[k][0] is not None and constraints[k][1] is not None: + if constraints[k][1] <= constraints[k][0]: + raise ValueError("Upper bound is less than the lower bound.") + con_both_inds.append(k) + con_both.append([constraints[k][0], constraints[k][1]]) + elif constraints[k][0] is not None: + con_lower_inds.append(k) + con_lower.append(constraints[k][0]) + elif constraints[k][1] is not None: + con_upper_inds.append(k) + con_upper.append(constraints[k][1]) + # tensor-based indexing is much faster than list-based advanced indexing + for name, indices in [ + ("con_lower_inds", con_lower_inds), + ("con_upper_inds", con_upper_inds), + ("con_both_inds", con_both_inds), + ("con_both", con_both), + ("con_lower", con_lower), + ("con_upper", con_upper), + ]: + acqf.register_buffer(name, tensor=torch.as_tensor(indices)) + + +def _compute_log_prob_feas( + acqf: LogConstrainedExpectedImprovement | ConstrainedExpectedImprovement, + means: Tensor, + sigmas: Tensor, +) -> Tensor: + r"""Compute logarithm of the feasibility probability for each batch of X. + + Args: + X: A `(b) x 1 x d`-dim Tensor of `(b)` t-batches of `d`-dim design + points each. + means: A `(b) x m`-dim Tensor of means. + sigmas: A `(b) x m`-dim Tensor of standard deviations. + Returns: + A `b`-dim tensor of log feasibility probabilities + + Note: This function does case-work for upper bound, lower bound, and both-sided + bounds. Another way to do it would be to use 'inf' and -'inf' for the + one-sided bounds and use the logic for the both-sided case. But this + causes an issue with autograd since we get 0 * inf. + TODO: Investigate further. + """ + acqf.to(device=means.device) + return compute_log_prob_feas_from_bounds( + acqf.con_lower_inds, + acqf.con_upper_inds, + acqf.con_both_inds, + acqf.con_lower, + acqf.con_upper, + acqf.con_both, + means, + sigmas, + ) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/bayesian_active_learning.html b/website-old/pages/api/_modules/botorch/acquisition/bayesian_active_learning.html new file mode 100644 index 0000000000..c1add0735c --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/bayesian_active_learning.html @@ -0,0 +1,230 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.bayesian_active_learning

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Acquisition functions for Bayesian active learning. This includes:
+BALD [Houlsby2011bald]_ and its batch version [kirsch2019batchbald]_.
+
+References
+
+.. [kirsch2019batchbald]
+    Andreas Kirsch, Joost van Amersfoort, Yarin Gal.
+    BatchBALD: Efficient and Diverse Batch Acquisition for Deep Bayesian
+    Active Learning.
+    In Proceedings of the Annual Conference on Neural Information
+    Processing Systems (NeurIPS), 2019.
+
+"""
+
+from __future__ import annotations
+
+import warnings
+
+from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models import ModelListGP
+from botorch.models.fully_bayesian import MCMC_DIM, SaasFullyBayesianSingleTaskGP
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.transforms import (
+    concatenate_pending_points,
+    is_fully_bayesian,
+    t_batch_mode_transform,
+)
+from gpytorch.distributions.multitask_multivariate_normal import (
+    MultitaskMultivariateNormal,
+)
+from torch import Tensor
+
+
+FULLY_BAYESIAN_ERROR_MSG = (
+    "Fully Bayesian acquisition functions require a SaasFullyBayesianSingleTaskGP "
+    "or of ModelList of SaasFullyBayesianSingleTaskGPs to run."
+)
+
+NEGATIVE_INFOGAIN_WARNING = (
+    "Information gain is negative. This is likely due to a poor Monte Carlo "
+    "estimation of the entropies, extremely high or extremely low correlation "
+    "in the data."  # because both of those cases result in no information gain
+)
+
+
+
+[docs] +def check_negative_info_gain(info_gain: Tensor) -> None: + r"""Check if the (expected) information gain is negative, raise a warning if so.""" + if info_gain.lt(0).any(): + warnings.warn(NEGATIVE_INFOGAIN_WARNING, RuntimeWarning, stacklevel=2)
+ + + +
+[docs] +class FullyBayesianAcquisitionFunction(AcquisitionFunction): + def __init__(self, model: Model): + """Base class for acquisition functions which require a Fully Bayesian + model treatment. + + Args: + model: A fully bayesian single-outcome model. + """ + if is_fully_bayesian(model): + super().__init__(model) + + else: + raise RuntimeError(FULLY_BAYESIAN_ERROR_MSG)
+ + + +
+[docs] +class qBayesianActiveLearningByDisagreement( + FullyBayesianAcquisitionFunction, MCSamplerMixin +): + def __init__( + self, + model: ModelListGP | SaasFullyBayesianSingleTaskGP, + sampler: MCSampler | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + ) -> None: + """ + Batch implementation [kirsch2019batchbald]_ of BALD [Houlsby2011bald]_, + which maximizes the mutual information between the next observation and the + hyperparameters of the model. Computed by Monte Carlo integration. + + Args: + model: A fully bayesian model (SaasFullyBayesianSingleTaskGP). + sampler: The sampler used for drawing samples to approximate the entropy + of the Gaussian Mixture posterior. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points + + """ + super().__init__(model=model) + MCSamplerMixin.__init__(self, sampler=sampler) + self.set_X_pending(X_pending) + self.posterior_transform = posterior_transform + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qBayesianActiveLearningByDisagreement on the candidate set `X`. + A monte carlo-estimated information gain is computed over a Gaussian Mixture + marginal posterior, and the Gaussian conditional posterior to obtain the + qBayesianActiveLearningByDisagreement on the candidate set `X`. + + Args: + X: `batch_shape x q x D`-dim Tensor of input points. + + Returns: + A `batch_shape x num_models`-dim Tensor of BALD values. + """ + posterior = self.model.posterior( + X, observation_noise=True, posterior_transform=self.posterior_transform + ) + if isinstance(posterior.mvn, MultitaskMultivariateNormal): + # The default MultitaskMultivariateNormal conversion for + # GuassianMixturePosteriors does not interleave (and models task and data) + # covariances in the unintended order. This is a inter-task block-diagonal, + # and not inter-data block-diagonal, which is the default for GMMPosteriors + posterior.mvn._interleaved = True + + # draw samples from the mixture posterior. + # samples: num_samples x batch_shape x num_models x q x num_outputs + samples = self.get_posterior_samples(posterior=posterior) + + # Estimate the entropy of 'num_samples' samples from 'num_models' models by + # evaluating the log_prob on each sample on the mixture posterior + # (which constitutes of M models). thus, order N*M^2 computations + + # Make room and move the model dim to the front, squeeze the num_outputs dim. + # prev_samples: num_models x num_samples x batch_shape x 1 x q + prev_samples = samples.unsqueeze(0).transpose(0, MCMC_DIM).squeeze(-1) + + # avg the probs over models in the mixture - dim (-2) will be broadcasted + # with the num_models of the posterior --> querying all samples on all models + # posterior.mvn takes q-dimensional input by default, which removes the q-dim + # component_sample_probs: num_models x num_samples x batch_shape x num_models + component_sample_probs = posterior.mvn.log_prob(prev_samples).exp() + + # average over mixture components + mixture_sample_probs = component_sample_probs.mean(dim=-1, keepdim=True) + + # this is the average over the model and sample dim + prev_entropy = -mixture_sample_probs.log().mean(dim=[0, 1]) + + # the posterior entropy is an average entropy over gaussians, so no mixture + post_entropy = -posterior.mvn.log_prob(samples.squeeze(-1)).mean(0) + + # The BALD acq is defined as an expectation over a fully bayesian model, + # so thus, the mean is computed here and not outside of the forward pass + bald = (prev_entropy - post_entropy).mean(-1, keepdim=True) + check_negative_info_gain(bald) + return bald
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/cached_cholesky.html b/website-old/pages/api/_modules/botorch/acquisition/cached_cholesky.html new file mode 100644 index 0000000000..b72bf56725 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/cached_cholesky.html @@ -0,0 +1,252 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.cached_cholesky

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Abstract class for acquisition functions leveraging a cached Cholesky
+decomposition of the posterior covariance over f(X_baseline).
+"""
+
+from __future__ import annotations
+
+import warnings
+
+import torch
+from botorch.acquisition.acquisition import MCSamplerMixin
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models.gpytorch import GPyTorchModel
+from botorch.models.higher_order_gp import HigherOrderGP
+from botorch.models.model import Model
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.multitask import KroneckerMultiTaskGP, MultiTaskGP
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from botorch.posteriors.posterior import Posterior
+from botorch.sampling.base import MCSampler
+from botorch.utils.low_rank import extract_batch_covar, sample_cached_cholesky
+from gpytorch.distributions.multitask_multivariate_normal import (
+    MultitaskMultivariateNormal,
+)
+from linear_operator.utils.errors import NanError, NotPSDError
+from torch import Tensor
+
+
+
+[docs] +def supports_cache_root(model: Model) -> bool: + r"""Checks if a model supports the cache_root functionality. + The two criteria are that the model is not multi-task and the model + produces a GPyTorchPosterior. + """ + if isinstance(model, ModelListGP): + return all(supports_cache_root(m) for m in model.models) + # Multi task models and non-GPyTorch models are not supported. + if isinstance( + model, (MultiTaskGP, KroneckerMultiTaskGP, HigherOrderGP) + ) or not isinstance(model, GPyTorchModel): + return False + # Models that return a TransformedPosterior are not supported. + if hasattr(model, "outcome_transform") and (not model.outcome_transform._is_linear): + return False + return True
+ + + +def _get_cache_root_not_supported_message(model_cls: type) -> str: + msg = ( + "`cache_root` is only supported for GPyTorchModels that " + "are not MultiTask models and don't produce a " + f"TransformedPosterior. Got a model of type {model_cls}. Setting " + "`cache_root = False`." + ) + return msg + + +
+[docs] +class CachedCholeskyMCSamplerMixin(MCSamplerMixin): + r"""Abstract Mixin class for acquisition functions using a cached Cholesky. + + Specifically, this is for acquisition functions that require sampling from + the posterior P(f(X_baseline, X) | D). The Cholesky of the posterior + covariance over f(X_baseline) is cached. + """ + + def __init__( + self, + model: Model, + cache_root: bool = False, + sampler: MCSampler | None = None, + ) -> None: + r"""Set class attributes and perform compatibility checks. + + Args: + model: A model. + cache_root: A boolean indicating whether to cache the Cholesky. + This might be overridden in the model is not compatible. + sampler: An optional MCSampler object. + """ + MCSamplerMixin.__init__(self, sampler=sampler) + if cache_root and not supports_cache_root(model): + warnings.warn( + _get_cache_root_not_supported_message(type(model)), + RuntimeWarning, + stacklevel=3, + ) + cache_root = False + self._cache_root = cache_root + + def _compute_root_decomposition( + self, + posterior: Posterior, + ) -> Tensor: + r"""Cache Cholesky of the posterior covariance over f(X_baseline). + + Because `LinearOperator.root_decomposition` is decorated with LinearOperator's + @cached decorator, this function is doing a lot implicitly: + + 1) Check if a root decomposition has already been cached to `lazy_covar`. + Note that it will not have been if `posterior.mvn` is a + `MultitaskMultivariateNormal`, since we construct `lazy_covar` in that + case. + 2) If the root decomposition has not been found in the cache, compute it. + 3) Write it to the cache of `lazy_covar`. Note that this will become + inaccessible if `posterior.mvn` is a `MultitaskMultivariateNormal`, + since in that case `lazy_covar`'s scope is only this function. + + Args: + posterior: The posterior over f(X_baseline). + """ + if isinstance(posterior.distribution, MultitaskMultivariateNormal): + lazy_covar = extract_batch_covar(posterior.distribution) + else: + lazy_covar = posterior.distribution.lazy_covariance_matrix + lazy_covar_root = lazy_covar.root_decomposition() + return lazy_covar_root.root.to_dense() + + def _get_f_X_samples(self, posterior: GPyTorchPosterior, q_in: int) -> Tensor: + r"""Get posterior samples at the `q_in` new points from the joint posterior. + + Args: + posterior: The joint posterior is over (X_baseline, X). + q_in: The number of new points in the posterior. See `_set_sampler` for + more information. + + Returns: + A `sample_shape x batch_shape x q x m`-dim tensor of posterior + samples at the new points. + """ + # Technically we should make sure that we add a consistent nugget to the + # cached covariance (and box decompositions) and the new block. + # But recomputing box decompositions every time the jitter changes would + # be quite slow. + if self._cache_root and hasattr(self, "_baseline_L"): + try: + return sample_cached_cholesky( + posterior=posterior, + baseline_L=self._baseline_L, + q=q_in, + base_samples=self.sampler.base_samples, + sample_shape=self.sampler.sample_shape, + ) + except (NanError, NotPSDError): + warnings.warn( + "Low-rank cholesky updates failed due NaNs or due to an " + "ill-conditioned covariance matrix. " + "Falling back to standard sampling.", + BotorchWarning, + stacklevel=3, + ) + + # TODO: improve efficiency for multi-task models + samples = self.get_posterior_samples(posterior) + if isinstance(self.model, HigherOrderGP): + # Select the correct q-batch dimension for HOGP. + q_dim = -self.model._num_dimensions + q_idcs = ( + torch.arange(-q_in, 0, device=samples.device) + samples.shape[q_dim] + ) + return samples.index_select(q_dim, q_idcs) + else: + return samples[..., -q_in:, :] + + def _set_sampler( + self, + q_in: int, + posterior: Posterior, + ) -> None: + r"""Update the sampler to use the original base samples for X_baseline. + + Args: + q_in: The effective input batch size. This is typically equal to the + q-batch size of `X`. However, if using a one-to-many input transform, + e.g., `InputPerturbation` with `n_w` perturbations, the posterior will + have `n_w` points on the q-batch for each point on the q-batch of `X`. + In which case, `q_in = q * n_w` is used. + posterior: The posterior. + """ + if self.q_in != q_in and self.base_sampler is not None: + self.sampler._update_base_samples( + posterior=posterior, base_sampler=self.base_sampler + ) + self.q_in = q_in
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/cost_aware.html b/website-old/pages/api/_modules/botorch/acquisition/cost_aware.html new file mode 100644 index 0000000000..d88296fbce --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/cost_aware.html @@ -0,0 +1,295 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.cost_aware

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Cost functions for cost-aware acquisition functions, e.g. multi-fidelity KG.
+To be used in a context where there is an objective/cost tradeoff.
+"""
+
+from __future__ import annotations
+
+import warnings
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+
+import torch
+from botorch.acquisition.objective import (
+    GenericMCObjective,
+    IdentityMCObjective,
+    MCAcquisitionObjective,
+)
+from botorch.exceptions.warnings import CostAwareWarning
+from botorch.models.deterministic import DeterministicModel
+from botorch.models.gpytorch import GPyTorchModel
+from botorch.sampling.base import MCSampler
+from pyre_extensions import none_throws
+from torch import Tensor
+from torch.nn import Module
+
+
+
+[docs] +class CostAwareUtility(Module, ABC): + """Abstract base class for cost-aware utilities.""" + +
+[docs] + @abstractmethod + def forward( + self, X: Tensor, deltas: Tensor, sampler: MCSampler | None = None + ) -> Tensor: + r"""Evaluate the cost-aware utility on the candidates and improvements. + + Args: + X: A `batch_shape x q x d`-dim Tensor of with `q` `d`-dim design + points each for each t-batch. + deltas: A `num_fantasies x batch_shape`-dim Tensor of `num_fantasy` + samples from the marginal improvement in utility over the + current state at `X` for each t-batch. + sampler: A sampler used for sampling from the posterior of the cost + model. Some subclasses ignore this argument. + + Returns: + A `num_fantasies x batch_shape`-dim Tensor of cost-transformed utilities. + """
+
+ + + +
+[docs] +class GenericCostAwareUtility(CostAwareUtility): + r"""Generic cost-aware utility wrapping a callable.""" + + def __init__(self, cost: Callable[[Tensor, Tensor], Tensor]) -> None: + r"""Generic cost-aware utility wrapping a callable. + + Args: + cost: A callable mapping a `batch_shape x q x d'`-dim candidate set + to a `batch_shape`-dim tensor of costs + """ + super().__init__() + self._cost_callable: Callable[[Tensor, Tensor], Tensor] = cost + +
+[docs] + def forward( + self, X: Tensor, deltas: Tensor, sampler: MCSampler | None = None + ) -> Tensor: + r"""Evaluate the cost function on the candidates and improvements. + + Args: + X: A `batch_shape x q x d'`-dim Tensor of with `q` `d`-dim design + points for each t-batch. + deltas: A `num_fantasies x batch_shape`-dim Tensor of `num_fantasy` + samples from the marginal improvement in utility over the + current state at `X` for each t-batch. + sampler: Ignored. + + Returns: + A `num_fantasies x batch_shape`-dim Tensor of cost-weighted utilities. + """ + return self._cost_callable(X, deltas)
+
+ + + +
+[docs] +class InverseCostWeightedUtility(CostAwareUtility): + r"""A cost-aware utility using inverse cost weighting based on a model. + + Computes the cost-aware utility by inverse-weighting samples + `U = (u_1, ..., u_N)` of the increase in utility. If `use_mean=True`, this + uses the posterior mean `mean_cost` of the cost model, i.e. + `weighted utility = mean(U) / mean_cost`. If `use_mean=False`, it uses + samples `C = (c_1, ..., c_N)` from the posterior of the cost model and + performs the inverse weighting on the sample level: + `weighted utility = mean(u_1 / c_1, ..., u_N / c_N)`. + + Where values in (u_1, ..., u_N) are negative, or for mean(U) < 0, the + weighted utility is instead calculated via scaling by the cost, i.e. if + `use_mean=True`: `weighted_utility = mean(U) * mean_cost` and if + `use_mean=False`: + `weighted utility = mean(u_1 * c_1, u_2 / c_2, u_3 * c_3, ..., u_N / c_N)`, + depending on whether (`u_*` >= 0), as with `u_2` and `u_N` in this case, or + (`u_*` < 0) as with `u_1` and `u_3`. + + The cost is additive across multiple elements of a q-batch. + """ + + def __init__( + self, + cost_model: DeterministicModel | GPyTorchModel, + use_mean: bool = True, + cost_objective: MCAcquisitionObjective | None = None, + min_cost: float = 1e-2, + ) -> None: + r"""Cost-aware utility that weights increase in utility by inverse cost. + For negative increases in utility, the utility is instead scaled by the + cost. See the class description for more information. + + Args: + cost_model: A model of the cost of evaluating a candidate + set `X`, where `X` are the same features as in the model for the + acquisition function this is to be used with. If no cost_objective + is specified, the outputs are required to be non-negative. + use_mean: If True, use the posterior mean, otherwise use posterior + samples from the cost model. + cost_objective: If specified, transform the posterior mean / the + posterior samples from the cost model. This can be used e.g. to + un-transform predictions/samples of a cost model fit on the + log-transformed cost (often done to ensure non-negativity). If the + cost model is multi-output, then by default this will sum the cost + across outputs. + min_cost: A value used to clamp the cost samples so that they are not + too close to zero, which may cause numerical issues. + Returns: + The inverse-cost-weighted utility. + """ + super().__init__() + if cost_objective is None: + if cost_model.num_outputs == 1: + cost_objective = IdentityMCObjective() + else: + # sum over outputs + cost_objective = GenericMCObjective(lambda Y, X: Y.sum(dim=-1)) + + self.cost_model = cost_model + self.cost_objective: MCAcquisitionObjective = cost_objective + self._use_mean = use_mean + self._min_cost = min_cost + +
+[docs] + def forward( + self, + X: Tensor, + deltas: Tensor, + sampler: MCSampler | None = None, + X_evaluation_mask: Tensor | None = None, + ) -> Tensor: + r"""Evaluate the cost function on the candidates and improvements. Note + that negative values of `deltas` are instead scaled by the cost, and not + inverse-weighted. See the class description for more information. + + Args: + X: A `batch_shape x q x d`-dim Tensor of with `q` `d`-dim design + points each for each t-batch. + deltas: A `num_fantasies x batch_shape`-dim Tensor of `num_fantasy` + samples from the marginal improvement in utility over the + current state at `X` for each t-batch. + sampler: A sampler used for sampling from the posterior of the cost + model (required if `use_mean=False`, ignored if `use_mean=True`). + X_evaluation_mask: A `q x m`-dim boolean tensor indicating which + outcomes should be evaluated for each design in the batch. + + Returns: + A `num_fantasies x batch_shape`-dim Tensor of cost-weighted utilities. + """ + if not self._use_mean and sampler is None: + raise RuntimeError("Must provide `sampler` if `use_mean=False`") + if X_evaluation_mask is not None: + # TODO: support different evaluation masks for each X. This requires + # either passing evaluation_mask to `cost_model.posterior` + # or assuming that evaluating `cost_model.posterior(X)` on all + # `q` points and then only selecting the costs for relevant points + # does not change the cost function for each point. This would not be + # true for instance if the incremental cost of evaluating an additional + # point decreased as the number of points increased. + if not all( + torch.equal(X_evaluation_mask[0], X_evaluation_mask[i]) + for i in range(1, X_evaluation_mask.shape[0]) + ): + raise NotImplementedError( + "Currently, all candidates must be evaluated on the same outputs." + ) + output_indices = X_evaluation_mask[0].nonzero().view(-1).tolist() + else: + output_indices = None + cost_posterior = self.cost_model.posterior(X, output_indices=output_indices) + if self._use_mean: + cost = cost_posterior.mean # batch_shape x q x m' + else: + # This will be of shape num_fantasies x batch_shape x q x m' + cost = none_throws(sampler)(cost_posterior) + cost = self.cost_objective(cost) + + # Ensure non-negativity of the cost + if torch.any(cost < -1e-7): + warnings.warn( + "Encountered negative cost values in InverseCostWeightedUtility", + CostAwareWarning, + stacklevel=2, + ) + # clamp (away from zero) and sum cost across elements of the q-batch - + # this will be of shape `num_fantasies x batch_shape` or `batch_shape` + cost = cost.clamp_min(self._min_cost).sum(dim=-1) + + # compute and return the ratio on the sample level - If `use_mean=True` + # this operation involves broadcasting the cost across fantasies. + # We multiply by the cost if the deltas are <= 0, see discussion #2914 + return torch.where(deltas > 0, deltas / cost, deltas * cost)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/decoupled.html b/website-old/pages/api/_modules/botorch/acquisition/decoupled.html new file mode 100644 index 0000000000..a4c2b4d48a --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/decoupled.html @@ -0,0 +1,230 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.decoupled

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Abstract base module for decoupled acquisition functions."""
+
+from __future__ import annotations
+
+import warnings
+from abc import ABC
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.exceptions import BotorchWarning
+from botorch.exceptions.errors import BotorchTensorDimensionError
+from botorch.logging import shape_to_str
+
+from botorch.models.model import ModelList
+from torch import Tensor
+
+
+
+[docs] +class DecoupledAcquisitionFunction(AcquisitionFunction, ABC): + """ + Abstract base class for decoupled acquisition functions. + A decoupled acquisition function where one may intend to + evaluate a design on only a subset of the outcomes. + Typically this would be handled by fantasizing, where one + would fantasize as to what the partial observation would + be if one were to evaluate a design on the subset of + outcomes (e.g. you only fantasize at those outcomes). The + `X_evaluation_mask` specifies which outcomes should be + evaluated for each design. `X_evaluation_mask` is `q x m`, + where there are q design points in the batch and m outcomes. + In the asynchronous case, where there are n' pending points, + we need to track which outcomes each pending point should be + evaluated on. In this case, we concatenate + `X_pending_evaluation_mask` with `X_evaluation_mask` to obtain + the full evaluation_mask. + + + This abstract class handles generating and updating an evaluation mask, + which is a boolean tensor indicating which outcomes a given design is + being evaluated on. The evaluation mask has shape `(n' + q) x m`, where + n' is the number of pending points and the q represents the new + candidates to be generated. + + If `X(_pending)_evaluation_mas`k is None, it is assumed that `X(_pending)` + will be evaluated on all outcomes. + """ + + def __init__( + self, model: ModelList, X_evaluation_mask: Tensor | None = None, **kwargs + ) -> None: + r"""Initialize. + + Args: + model: A model + X_evaluation_mask: A `q x m`-dim boolean tensor + indicating which outcomes the decoupled acquisition + function should generate new candidates for. + """ + if not isinstance(model, ModelList): + raise ValueError(f"{self.__class__.__name__} requires using a ModelList.") + super().__init__(model=model, **kwargs) + self.num_outputs = model.num_outputs + self.X_evaluation_mask = X_evaluation_mask + self.X_pending_evaluation_mask = None + self.X_pending = None + + @property + def X_evaluation_mask(self) -> Tensor | None: + r"""Get the evaluation indices for the new candidate.""" + return self._X_evaluation_mask + + @X_evaluation_mask.setter + def X_evaluation_mask(self, X_evaluation_mask: Tensor | None = None) -> None: + r"""Set the evaluation indices for the new candidate.""" + if X_evaluation_mask is not None: + # TODO: Add batch support + if ( + X_evaluation_mask.ndim != 2 + or X_evaluation_mask.shape[-1] != self.num_outputs + ): + raise BotorchTensorDimensionError( + "Expected X_evaluation_mask to be `q x m`, but got shape" + f" {shape_to_str(X_evaluation_mask.shape)}." + ) + self._X_evaluation_mask = X_evaluation_mask + +
+[docs] + def set_X_pending( + self, + X_pending: Tensor | None = None, + X_pending_evaluation_mask: Tensor | None = None, + ) -> None: + r"""Informs the AF about pending design points for different outcomes. + + Args: + X_pending: A `n' x d` Tensor with `n'` `d`-dim design points that have + been submitted for evaluation but have not yet been evaluated. + X_pending_evaluation_mask: A `n' x m`-dim tensor of booleans indicating + for which outputs the pending point is being evaluated on. If + `X_pending_evaluation_mask` is `None`, it is assumed that + `X_pending` will be evaluated on all outcomes. + """ + if X_pending is not None: + if X_pending.requires_grad: + warnings.warn( + "Pending points require a gradient but the acquisition function" + " will not provide a gradient to these points.", + BotorchWarning, + stacklevel=2, + ) + self.X_pending = X_pending.detach().clone() + if X_pending_evaluation_mask is not None: + if ( + X_pending_evaluation_mask.ndim != 2 + or X_pending_evaluation_mask.shape[0] != X_pending.shape[0] + or X_pending_evaluation_mask.shape[1] != self.num_outputs + ): + raise BotorchTensorDimensionError( + f"Expected `X_pending_evaluation_mask` of shape " + f"`{X_pending.shape[0]} x {self.num_outputs}`, but " + f"got {shape_to_str(X_pending_evaluation_mask.shape)}." + ) + self.X_pending_evaluation_mask = X_pending_evaluation_mask + elif self.X_evaluation_mask is not None: + raise ValueError( + "If `self.X_evaluation_mask` is not None, then " + "`X_pending_evaluation_mask` must be provided." + ) + + else: + self.X_pending = X_pending + self.X_pending_evaluation_mask = X_pending_evaluation_mask
+ + +
+[docs] + def construct_evaluation_mask(self, X: Tensor) -> Tensor | None: + r"""Construct the boolean evaluation mask for X and X_pending + + Args: + X: A `batch_shape x n x d`-dim tensor of designs. + + Returns: + A `n + n' x m`-dim tensor of booleans indicating + which outputs should be evaluated. + """ + if self.X_pending_evaluation_mask is not None: + X_evaluation_mask = self.X_evaluation_mask + if X_evaluation_mask is None: + # evaluate all objectives for X + X_evaluation_mask = torch.ones( + X.shape[-2], self.num_outputs, dtype=torch.bool, device=X.device + ) + elif X_evaluation_mask.shape[0] != X.shape[-2]: + raise BotorchTensorDimensionError( + "Expected the -2 dimension of X and X_evaluation_mask to match." + ) + # construct mask for X + return torch.cat( + [X_evaluation_mask, self.X_pending_evaluation_mask], dim=-2 + ) + return self.X_evaluation_mask
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/factory.html b/website-old/pages/api/_modules/botorch/acquisition/factory.html new file mode 100644 index 0000000000..626b74e73f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/factory.html @@ -0,0 +1,311 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.factory

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Utilities for acquisition functions.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import torch
+
+from botorch.acquisition import logei, monte_carlo
+from botorch.acquisition.multi_objective import (
+    logei as moo_logei,
+    monte_carlo as moo_monte_carlo,
+)
+from botorch.acquisition.objective import MCAcquisitionObjective, PosteriorTransform
+from botorch.acquisition.utils import compute_best_feasible_objective
+from botorch.models.model import Model
+from botorch.sampling.get_sampler import get_sampler
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    FastNondominatedPartitioning,
+    NondominatedPartitioning,
+)
+from torch import Tensor
+
+
+
+[docs] +def get_acquisition_function( + acquisition_function_name: str, + model: Model, + objective: MCAcquisitionObjective, + X_observed: Tensor, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float | None = 1e-3, + mc_samples: int = 512, + seed: int | None = None, + *, + # optional parameters that are only needed for certain acquisition functions + tau: float = 1e-3, + prune_baseline: bool = True, + marginalize_dim: int | None = None, + cache_root: bool = True, + beta: float | None = None, + ref_point: None | list[float] | Tensor = None, + Y: Tensor | None = None, + alpha: float = 0.0, +) -> monte_carlo.MCAcquisitionFunction: + r"""Convenience function for initializing botorch acquisition functions. + + Args: + acquisition_function_name: Name of the acquisition function. + model: A fitted model. + objective: A MCAcquisitionObjective. + X_observed: A `m1 x d`-dim Tensor of `m1` design points that have + already been observed. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `m2 x d`-dim Tensor of `m2` design points whose evaluation + is pending. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. Used for all acquisition functions except qSR and qUCB. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same eta is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + eta value. Used for all acquisition functions except qSR and qUCB. + mc_samples: The number of samples to use for (q)MC evaluation of the + acquisition function. + seed: If provided, perform deterministic optimization (i.e. the + function to optimize is fixed and not stochastic). + + Returns: + The requested acquisition function. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> obj = LinearMCObjective(weights=torch.tensor([1.0, 2.0])) + >>> acqf = get_acquisition_function("qEI", model, obj, train_X) + """ + # initialize the sampler + sampler = get_sampler( + posterior=model.posterior(X_observed[:1]), + sample_shape=torch.Size([mc_samples]), + seed=seed, + ) + if posterior_transform is not None and acquisition_function_name in [ + "qEHVI", + "qNEHVI", + "qLogEHVI", + "qLogNEHVI", + ]: + raise NotImplementedError( + "PosteriorTransforms are not yet implemented for multi-objective " + "acquisition functions." + ) + # instantiate and return the requested acquisition function + if acquisition_function_name in ("qEI", "qLogEI", "qPI"): + # Since these are the non-noisy variants, use the posterior mean at the observed + # inputs directly to compute the best feasible value without sampling. + Y = model.posterior(X_observed, posterior_transform=posterior_transform).mean + obj = objective(samples=Y, X=X_observed) + best_f = compute_best_feasible_objective( + samples=Y, + obj=obj, + constraints=constraints, + model=model, + objective=objective, + posterior_transform=posterior_transform, + X_baseline=X_observed, + ) + if acquisition_function_name in ["qEI", "qLogEI"]: + acqf_class = ( + monte_carlo.qExpectedImprovement + if acquisition_function_name == "qEI" + else logei.qLogExpectedImprovement + ) + return acqf_class( + model=model, + best_f=best_f, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + constraints=constraints, + eta=eta, + ) + elif acquisition_function_name == "qPI": + return monte_carlo.qProbabilityOfImprovement( + model=model, + best_f=best_f, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + tau=tau, + constraints=constraints, + eta=eta, + ) + elif acquisition_function_name in ["qNEI", "qLogNEI"]: + acqf_class = ( + monte_carlo.qNoisyExpectedImprovement + if acquisition_function_name == "qNEI" + else logei.qLogNoisyExpectedImprovement + ) + return acqf_class( + model=model, + X_baseline=X_observed, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + prune_baseline=prune_baseline, + marginalize_dim=marginalize_dim, + cache_root=cache_root, + constraints=constraints, + eta=eta, + ) + elif acquisition_function_name == "qSR": + return monte_carlo.qSimpleRegret( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + ) + elif acquisition_function_name == "qUCB": + if beta is None: + raise ValueError("`beta` must be not be None for qUCB.") + return monte_carlo.qUpperConfidenceBound( + model=model, + beta=beta, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + ) + elif acquisition_function_name in ["qEHVI", "qLogEHVI"]: + if Y is None: + raise ValueError(f"`Y` must not be None for {acquisition_function_name}") + if ref_point is None: + raise ValueError( + f"`ref_point` must not be None for {acquisition_function_name}" + ) + # get feasible points + if constraints is not None: + feas = torch.stack([c(Y) <= 0 for c in constraints], dim=-1).all(dim=-1) + Y = Y[feas] + obj = objective(Y) + if alpha > 0: + partitioning = NondominatedPartitioning( + ref_point=torch.as_tensor(ref_point, dtype=Y.dtype, device=Y.device), + Y=obj, + alpha=alpha, + ) + else: + partitioning = FastNondominatedPartitioning( + ref_point=torch.as_tensor(ref_point, dtype=Y.dtype, device=Y.device), + Y=obj, + ) + acqf_class = ( + moo_monte_carlo.qExpectedHypervolumeImprovement + if acquisition_function_name == "qEHVI" + else moo_logei.qLogExpectedHypervolumeImprovement + ) + return acqf_class( + model=model, + ref_point=ref_point, + partitioning=partitioning, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + X_pending=X_pending, + ) + elif acquisition_function_name in ["qNEHVI", "qLogNEHVI"]: + if ref_point is None: + raise ValueError( + f"`ref_point` must not be None for {acquisition_function_name}" + ) + acqf_class = ( + moo_monte_carlo.qNoisyExpectedHypervolumeImprovement + if acquisition_function_name == "qNEHVI" + else moo_logei.qLogNoisyExpectedHypervolumeImprovement + ) + return acqf_class( + model=model, + ref_point=ref_point, + X_baseline=X_observed, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + prune_baseline=prune_baseline, + alpha=alpha, + X_pending=X_pending, + marginalize_dim=marginalize_dim, + cache_root=cache_root, + ) + raise NotImplementedError( + f"Unknown acquisition function {acquisition_function_name}" + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/fixed_feature.html b/website-old/pages/api/_modules/botorch/acquisition/fixed_feature.html new file mode 100644 index 0000000000..1131943685 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/fixed_feature.html @@ -0,0 +1,268 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.fixed_feature

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+A wrapper around AcquisitionFunctions to fix certain features for optimization.
+This is useful e.g. for performing contextual optimization.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from numbers import Number
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from torch import Tensor
+from torch.nn import Module
+
+
+
+[docs] +def get_dtype_of_sequence(values: Sequence[Tensor | float]) -> torch.dtype: + """ + Return torch.float32 if everything is single-precision and torch.float64 + otherwise. + + Numbers (non-tensors) are double-precision. + """ + + def _is_single(value: Tensor | float) -> bool: + return isinstance(value, Tensor) and value.dtype == torch.float32 + + all_single_precision = all(_is_single(value) for value in values) + return torch.float32 if all_single_precision else torch.float64
+ + + +
+[docs] +def get_device_of_sequence(values: Sequence[Tensor | float]) -> torch.dtype: + """ + CPU if everything is on the CPU; Cuda otherwise. + + Numbers (non-tensors) are considered to be on the CPU. + """ + + def _is_cuda(value: Tensor | float) -> bool: + return hasattr(value, "device") and value.device == torch.device("cuda") + + any_cuda = any(_is_cuda(value) for value in values) + return torch.device("cuda") if any_cuda else torch.device("cpu")
+ + + +
+[docs] +class FixedFeatureAcquisitionFunction(AcquisitionFunction): + """A wrapper around AcquisitionFunctions to fix a subset of features. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) # d = 5 + >>> qEI = qExpectedImprovement(model, best_f=0.0) + >>> columns = [2, 4] + >>> values = X[..., columns] + >>> qEI_FF = FixedFeatureAcquisitionFunction(qEI, 5, columns, values) + >>> qei = qEI_FF(test_X) # d' = 3 + """ + + def __init__( + self, + acq_function: AcquisitionFunction, + d: int, + columns: list[int], + values: Tensor | Sequence[Tensor | float], + ) -> None: + r"""Derived Acquisition Function by fixing a subset of input features. + + Args: + acq_function: The base acquisition function, operating on input + tensors `X_full` of feature dimension `d`. + d: The feature dimension expected by `acq_function`. + columns: `d_f < d` indices of columns in `X_full` that are to be + fixed to the provided values. + values: The values to which to fix the columns in `columns`. Either + a full `batch_shape x q x d_f` tensor of values (if values are + different for each of the `q` input points), or an array-like of + values that is broadcastable to the input across `t`-batch and + `q`-batch dimensions, e.g. a list of length `d_f` if values + are the same across all `t` and `q`-batch dimensions, or a + combination of `Tensor`s and numbers which can be broadcasted + to form a tensor with trailing dimension size of `d_f`. + """ + Module.__init__(self) + self.acq_func = acq_function + self.d = d + + if isinstance(values, Tensor): + new_values = values.detach().clone() + else: + dtype = get_dtype_of_sequence(values) + device = get_device_of_sequence(values) + + new_values = [] + for value in values: + if isinstance(value, Number): + value = torch.tensor([value], dtype=dtype) + else: + if value.ndim == 0: # since we can't broadcast with zero-d tensors + value = value.unsqueeze(0) + value = value.detach().clone() + + new_values.append(value.to(dtype=dtype, device=device)) + + # There are 3 cases for when `values` is a `Sequence`. + # 1) `values` == list of floats as earlier. + # 2) `values` == combination of floats and `Tensor`s. + # 3) `values` == a list of `Tensor`s. + # For 1), the below step creates a vector of length `len(values)` + # For 2), the below step creates a `Tensor` of shape `batch_shape x q x d_f` + # with the broadcasting functionality. + # For 3), this is simply a concatenation, yielding a `Tensor` with the + # same shape as in 2). + # The key difference arises when `_construct_X_full` is invoked. + # In 1), the expansion (`self.values.expand`) will expand the `Tensor` to + # size `batch_shape x q x d_f`. + # In 2) and 3), this expansion is a no-op because they are already of the + # required size. However, 2) and 3) _cannot_ support varying `batch_shape`, + # which means that all calls to `FixedFeatureAcquisitionFunction` have + # to have the same size throughout when `values` contains a `Tensor`. + # This is consistent with the scenario when a singular `Tensor` is passed + # as the `values` argument. + new_values = torch.cat(torch.broadcast_tensors(*new_values), dim=-1) + + self.register_buffer("values", new_values) + # build selector for _construct_X_full + self._selector = [] + idx_X, idx_f = 0, d - new_values.shape[-1] + for i in range(self.d): + if i in columns: + self._selector.append(idx_f) + idx_f += 1 + else: + self._selector.append(idx_X) + idx_X += 1 + +
+[docs] + def forward(self, X: Tensor): + r"""Evaluate base acquisition function under the fixed features. + + Args: + X: Input tensor of feature dimension `d' < d` such that `d' + d_f = d`. + + Returns: + Base acquisition function evaluated on tensor `X_full` constructed + by adding `values` in the appropriate places (see + `_construct_X_full`). + """ + X_full = self._construct_X_full(X) + return self.acq_func(X_full)
+ + + @property + def X_pending(self): + r"""Return the `X_pending` of the base acquisition function.""" + try: + return self.acq_func.X_pending + except (ValueError, AttributeError): + raise ValueError( + f"Base acquisition function {type(self.acq_func).__name__} " + "does not have an `X_pending` attribute." + ) + + @X_pending.setter + def X_pending(self, X_pending: Tensor | None): + r"""Sets the `X_pending` of the base acquisition function.""" + if X_pending is not None: + self.acq_func.X_pending = self._construct_X_full(X_pending) + else: + self.acq_func.X_pending = X_pending + + def _construct_X_full(self, X: Tensor) -> Tensor: + r"""Constructs the full input for the base acquisition function. + + Args: + X: Input tensor with shape `batch_shape x q x d'` such that + `d' + d_f = d`. + + Returns: + Tensor `X_full` of shape `batch_shape x q x d`, where + `X_full[..., i] = values[..., i]` if `i in columns`, + and `X_full[..., i] = X[..., j]`, with + `j = i - sum_{l<=i} 1_{l in fixed_colunns}`. + """ + d_prime, d_f = X.shape[-1], self.values.shape[-1] + if d_prime + d_f != self.d: + raise ValueError( + f"Feature dimension d' ({d_prime}) of input must be " + f"d - d_f ({self.d - d_f})." + ) + # concatenate values to the end + values = self.values.to(X).expand(*X.shape[:-1], d_f) + X_perm = torch.cat([X, values], dim=-1) + # now select the appropriate column order + return X_perm[..., self._selector]
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/input_constructors.html b/website-old/pages/api/_modules/botorch/acquisition/input_constructors.html new file mode 100644 index 0000000000..b05960988f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/input_constructors.html @@ -0,0 +1,2039 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.input_constructors

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+A registry of helpers for generating inputs to acquisition function
+constructors programmatically from a consistent input format.
+"""
+
+from __future__ import annotations
+
+import inspect
+from collections.abc import Callable, Hashable, Iterable, Sequence
+from typing import Any, TypeVar, Union
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.acquisition.active_learning import qNegIntegratedPosteriorVariance
+from botorch.acquisition.analytic import (
+    ExpectedImprovement,
+    LogExpectedImprovement,
+    LogNoisyExpectedImprovement,
+    LogProbabilityOfImprovement,
+    NoisyExpectedImprovement,
+    PosteriorMean,
+    ProbabilityOfImprovement,
+    UpperConfidenceBound,
+)
+from botorch.acquisition.bayesian_active_learning import (
+    qBayesianActiveLearningByDisagreement,
+)
+from botorch.acquisition.cost_aware import InverseCostWeightedUtility
+from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction
+from botorch.acquisition.joint_entropy_search import qJointEntropySearch
+from botorch.acquisition.knowledge_gradient import (
+    qKnowledgeGradient,
+    qMultiFidelityKnowledgeGradient,
+)
+from botorch.acquisition.logei import (
+    qLogExpectedImprovement,
+    qLogNoisyExpectedImprovement,
+    TAU_MAX,
+    TAU_RELU,
+)
+from botorch.acquisition.max_value_entropy_search import (
+    qMaxValueEntropy,
+    qMultiFidelityMaxValueEntropy,
+)
+from botorch.acquisition.monte_carlo import (
+    qExpectedImprovement,
+    qLowerConfidenceBound,
+    qNoisyExpectedImprovement,
+    qProbabilityOfImprovement,
+    qSimpleRegret,
+    qUpperConfidenceBound,
+)
+from botorch.acquisition.multi_objective import (
+    ExpectedHypervolumeImprovement,
+    MCMultiOutputObjective,
+    qExpectedHypervolumeImprovement,
+    qNoisyExpectedHypervolumeImprovement,
+)
+from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import (
+    _get_hv_value_function,
+    qHypervolumeKnowledgeGradient,
+    qMultiFidelityHypervolumeKnowledgeGradient,
+)
+from botorch.acquisition.multi_objective.logei import (
+    qLogExpectedHypervolumeImprovement,
+    qLogNoisyExpectedHypervolumeImprovement,
+)
+from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective
+from botorch.acquisition.multi_objective.parego import qLogNParEGO
+from botorch.acquisition.multi_objective.utils import get_default_partitioning_alpha
+from botorch.acquisition.objective import (
+    ConstrainedMCObjective,
+    IdentityMCObjective,
+    LearnedObjective,
+    MCAcquisitionObjective,
+    PosteriorTransform,
+    ScalarizedPosteriorTransform,
+)
+from botorch.acquisition.preference import (
+    AnalyticExpectedUtilityOfBestOption,
+    qExpectedUtilityOfBestOption,
+)
+from botorch.acquisition.risk_measures import RiskMeasureMCObjective
+from botorch.acquisition.utils import (
+    compute_best_feasible_objective,
+    expand_trace_observations,
+    get_infeasible_cost,
+    get_optimal_samples,
+    project_to_target_fidelity,
+)
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.cost import AffineFidelityCostModel
+from botorch.models.deterministic import FixedSingleSampleModel
+from botorch.models.gpytorch import GPyTorchModel
+from botorch.models.model import Model
+from botorch.optim.optimize import optimize_acqf
+from botorch.sampling.base import MCSampler
+from botorch.sampling.normal import IIDNormalSampler, SobolQMCNormalSampler
+from botorch.utils.containers import BotorchContainer
+from botorch.utils.datasets import SupervisedDataset
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    FastNondominatedPartitioning,
+    NondominatedPartitioning,
+)
+from botorch.utils.sampling import draw_sobol_samples
+from torch import Tensor
+
+
+ACQF_INPUT_CONSTRUCTOR_REGISTRY = {}
+
+T = TypeVar("T")
+MaybeDict = Union[T, dict[Hashable, T]]
+TOptimizeObjectiveKwargs = Union[
+    None,
+    MCAcquisitionObjective,
+    PosteriorTransform,
+    tuple[Tensor, Tensor],
+    dict[int, float],
+    bool,
+    int,
+    dict[str, Any],
+    Callable[[Tensor], Tensor],
+    Tensor,
+]
+
+
+def _field_is_shared(
+    datasets: Iterable[SupervisedDataset] | dict[Hashable, SupervisedDataset],
+    fieldname: str,
+) -> bool:
+    r"""Determines whether or not a given field is shared by all datasets."""
+    if isinstance(datasets, dict):
+        datasets = datasets.values()
+
+    base = None
+    for dataset in datasets:
+        if not hasattr(dataset, fieldname):
+            raise AttributeError(f"{type(dataset)} object has no field `{fieldname}`.")
+
+        obj = getattr(dataset, fieldname)
+        if base is None:
+            base = obj
+        elif isinstance(base, Tensor):
+            if not torch.equal(base, obj):
+                return False
+        elif base != obj:  # pragma: no cover
+            return False
+
+    return True
+
+
+def _get_dataset_field(
+    dataset: MaybeDict[SupervisedDataset],
+    fieldname: str,
+    transform: Callable[[BotorchContainer], Any] | None = None,
+    join_rule: Callable[[Sequence[Any]], Any] | None = None,
+    first_only: bool = False,
+    assert_shared: bool = False,
+) -> Any:
+    r"""Convenience method for extracting a given field from one or more datasets."""
+    if isinstance(dataset, dict):
+        if assert_shared and not _field_is_shared(dataset, fieldname):
+            raise ValueError(f"Field `{fieldname}` must be shared.")
+
+        if not first_only:
+            fields = (
+                _get_dataset_field(d, fieldname, transform) for d in dataset.values()
+            )
+            return join_rule(tuple(fields)) if join_rule else tuple(fields)
+
+        dataset = next(iter(dataset.values()))
+
+    field = getattr(dataset, fieldname)
+    return transform(field) if transform else field
+
+
+
+[docs] +def get_acqf_input_constructor( + acqf_cls: type[AcquisitionFunction], +) -> Callable[..., dict[str, Any]]: + r"""Get acquisition function input constructor from registry. + + Args: + acqf_cls: The AcquisitionFunction class (not instance) for which + to retrieve the input constructor. + + Returns: + The input constructor associated with `acqf_cls`. + + """ + if acqf_cls not in ACQF_INPUT_CONSTRUCTOR_REGISTRY: + raise RuntimeError( + f"Input constructor for acquisition class `{acqf_cls.__name__}` not " + "registered. Use the `@acqf_input_constructor` decorator to register " + "a new method." + ) + return ACQF_INPUT_CONSTRUCTOR_REGISTRY[acqf_cls]
+ + + +
+[docs] +def allow_only_specific_variable_kwargs(f: Callable[..., T]) -> Callable[..., T]: + """ + Decorator for allowing a function to accept keyword arguments that are not + explicitly listed in the function signature, but only specific ones. + + This decorator is applied in `acqf_input_constructor` so that all constructors + obtained with `acqf_input_constructor` allow keyword + arguments such as `training_data` and `objective`, even if they do not appear + in the signature of `f`. Any other keyword arguments will raise an error. + """ + allowed = { + # `training_data` and/or `X_baseline` are needed to compute baselines + # for some EI-type acquisition functions. + "training_data", + "X_baseline", + # Objective thresholds are needed for defining hypervolumes in + # multi-objective optimization. + "objective_thresholds", + # Used in input constructors for some lookahead acquisition functions + # such as qKnowledgeGradient. + "bounds", + } + + def g(*args: Any, **kwargs: Any) -> T: + new_kwargs = {} + accepted_kwargs = inspect.signature(f).parameters.keys() + for k, v in kwargs.items(): + if k in accepted_kwargs: + new_kwargs[k] = v + elif k not in allowed: + raise TypeError( + f"Unexpected keyword argument `{k}` when" + f" constructing input arguments for {f.__name__}." + ) + return f(*args, **new_kwargs) + + return g
+ + + +
+[docs] +def acqf_input_constructor( + *acqf_cls: type[AcquisitionFunction], +) -> Callable[..., AcquisitionFunction]: + r"""Decorator for registering acquisition function input constructors. + + Args: + acqf_cls: The AcquisitionFunction classes (not instances) for which + to register the input constructor. + """ + for acqf_cls_ in acqf_cls: + if acqf_cls_ in ACQF_INPUT_CONSTRUCTOR_REGISTRY: + raise ValueError( + "Cannot register duplicate arg constructor for acquisition " + f"class `{acqf_cls_.__name__}`" + ) + + def decorator(method): + method_kwargs = allow_only_specific_variable_kwargs(method) + for acqf_cls_ in acqf_cls: + ACQF_INPUT_CONSTRUCTOR_REGISTRY[acqf_cls_] = method_kwargs + return method + + return decorator
+ + + +def _register_acqf_input_constructor( + acqf_cls: type[AcquisitionFunction], + input_constructor: Callable[..., dict[str, Any]], +) -> None: + ACQF_INPUT_CONSTRUCTOR_REGISTRY[acqf_cls] = input_constructor + + +# --------------------- Input argument constructors --------------------- # + + +
+[docs] +@acqf_input_constructor(PosteriorMean) +def construct_inputs_posterior_mean( + model: Model, + posterior_transform: PosteriorTransform | None = None, +) -> dict[str, Model | PosteriorTransform | None]: + r"""Construct kwargs for PosteriorMean acquisition function. + + Args: + model: The model to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + return {"model": model, "posterior_transform": posterior_transform}
+ + + +
+[docs] +@acqf_input_constructor( + ExpectedImprovement, + LogExpectedImprovement, + ProbabilityOfImprovement, + LogProbabilityOfImprovement, +) +def construct_inputs_best_f( + model: Model, + training_data: MaybeDict[SupervisedDataset], + posterior_transform: PosteriorTransform | None = None, + best_f: float | Tensor | None = None, + maximize: bool = True, +) -> dict[str, Any]: + r"""Construct kwargs for the acquisition functions requiring `best_f`. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + Used to determine default value for `best_f`. + best_f: Threshold above (or below) which improvement is defined. + posterior_transform: The posterior transform to be used in the + acquisition function. + maximize: If True, consider the problem a maximization problem. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if best_f is None: + best_f = get_best_f_analytic( + training_data=training_data, + posterior_transform=posterior_transform, + ) + + return { + "model": model, + "posterior_transform": posterior_transform, + "best_f": best_f, + "maximize": maximize, + }
+ + + +
+[docs] +@acqf_input_constructor(UpperConfidenceBound) +def construct_inputs_ucb( + model: Model, + posterior_transform: PosteriorTransform | None = None, + beta: float | Tensor = 0.2, + maximize: bool = True, +) -> dict[str, Any]: + r"""Construct kwargs for `UpperConfidenceBound`. + + Args: + model: The model to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + beta: Either a scalar or a one-dim tensor with `b` elements (batch mode) + representing the trade-off parameter between mean and covariance + maximize: If True, consider the problem a maximization problem. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + return { + "model": model, + "posterior_transform": posterior_transform, + "beta": beta, + "maximize": maximize, + }
+ + + +
+[docs] +@acqf_input_constructor(NoisyExpectedImprovement, LogNoisyExpectedImprovement) +def construct_inputs_noisy_ei( + model: Model, + training_data: MaybeDict[SupervisedDataset], + num_fantasies: int = 20, + maximize: bool = True, +) -> dict[str, Any]: + r"""Construct kwargs for `NoisyExpectedImprovement`. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + num_fantasies: The number of fantasies to generate. The higher this + number the more accurate the model (at the expense of model + complexity and performance). + maximize: If True, consider the problem a maximization problem. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + # TODO: Add prune_baseline functionality as for qNEI + X = _get_dataset_field(training_data, "X", first_only=True, assert_shared=True) + return { + "model": model, + "X_observed": X, + "num_fantasies": num_fantasies, + "maximize": maximize, + }
+ + + +
+[docs] +@acqf_input_constructor(qSimpleRegret) +def construct_inputs_qSimpleRegret( + model: Model, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + X_baseline: Tensor | None = None, +) -> dict[str, Any]: + r"""Construct kwargs for qSimpleRegret. + + Args: + model: The model to be used in the acquisition function. + objective: The objective to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + X_pending: A `batch_shape, m x d`-dim Tensor of `m` design points + that have points that have been submitted for function evaluation + but have not yet been evaluated. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are considered as + the potential best design point. If omitted, checks that all + training_data have the same input features and take the first `X`. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if constraints is not None: + if X_baseline is None: + raise ValueError("Constraints require an X_baseline.") + objective = ConstrainedMCObjective( + objective=objective, + constraints=constraints, + infeasible_cost=get_infeasible_cost( + X=X_baseline, model=model, objective=objective + ), + ) + return { + "model": model, + "objective": objective, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + "sampler": sampler, + }
+ + + +
+[docs] +@acqf_input_constructor(qExpectedImprovement) +def construct_inputs_qEI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + best_f: float | Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, +) -> dict[str, Any]: + r"""Construct kwargs for the `qExpectedImprovement` constructor. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + objective: The objective to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + best_f: Threshold above (or below) which improvement is defined. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if best_f is None: + best_f = get_best_f_mc( + training_data=training_data, + objective=objective, + posterior_transform=posterior_transform, + constraints=constraints, + model=model, + ) + + return { + "model": model, + "objective": objective, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + "sampler": sampler, + "best_f": best_f, + "constraints": constraints, + "eta": eta, + }
+ + + +
+[docs] +@acqf_input_constructor(qLogExpectedImprovement) +def construct_inputs_qLogEI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + best_f: float | Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + fat: bool = True, + tau_max: float = TAU_MAX, + tau_relu: float = TAU_RELU, +) -> dict[str, Any]: + r"""Construct kwargs for the `qExpectedImprovement` constructor. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + objective: The objective to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + best_f: Threshold above (or below) which improvement is defined. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + fat: Toggles the logarithmic / linear asymptotic behavior of the smooth + approximation to the ReLU. + tau_max: Temperature parameter controlling the sharpness of the smooth + approximations to max. + tau_relu: Temperature parameter controlling the sharpness of the smooth + approximations to ReLU. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + return { + **construct_inputs_qEI( + model=model, + training_data=training_data, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + sampler=sampler, + best_f=best_f, + constraints=constraints, + eta=eta, + ), + "fat": fat, + "tau_max": tau_max, + "tau_relu": tau_relu, + }
+ + + +
+[docs] +@acqf_input_constructor(qNoisyExpectedImprovement) +def construct_inputs_qNEI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + X_baseline: Tensor | None = None, + prune_baseline: bool | None = True, + cache_root: bool | None = True, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, +) -> dict[str, Any]: + r"""Construct kwargs for the `qNoisyExpectedImprovement` constructor. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + objective: The objective to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are considered as + the potential best design point. If omitted, checks that all + training_data have the same input features and take the first `X`. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the best point. This can significantly + improve performance and is generally recommended. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if X_baseline is None: + X_baseline = _get_dataset_field( + training_data, + fieldname="X", + assert_shared=True, + first_only=True, + ) + return { + "model": model, + "objective": objective, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + "sampler": sampler, + "X_baseline": X_baseline, + "prune_baseline": prune_baseline, + "cache_root": cache_root, + "constraints": constraints, + "eta": eta, + }
+ + + +
+[docs] +@acqf_input_constructor(qLogNoisyExpectedImprovement) +def construct_inputs_qLogNEI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + X_baseline: Tensor | None = None, + prune_baseline: bool | None = True, + cache_root: bool | None = True, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + fat: bool = True, + tau_max: float = TAU_MAX, + tau_relu: float = TAU_RELU, +): + r"""Construct kwargs for the `qLogNoisyExpectedImprovement` constructor. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + objective: The objective to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are considered as + the potential best design point. If omitted, checks that all + training_data have the same input features and take the first `X`. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the best point. This can significantly + improve performance and is generally recommended. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + fat: Toggles the use of the fat-tailed non-linearities to smoothly approximate + the constraints indicator function. + tau_max: Temperature parameter controlling the sharpness of the smooth + approximations to max. + tau_relu: Temperature parameter controlling the sharpness of the smooth + approximations to ReLU. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + return { + **construct_inputs_qNEI( + model=model, + training_data=training_data, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + sampler=sampler, + X_baseline=X_baseline, + prune_baseline=prune_baseline, + cache_root=cache_root, + constraints=constraints, + eta=eta, + ), + "fat": fat, + "tau_max": tau_max, + "tau_relu": tau_relu, + }
+ + + +
+[docs] +@acqf_input_constructor(qProbabilityOfImprovement) +def construct_inputs_qPI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + tau: float = 1e-3, + best_f: float | Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, +) -> dict[str, Any]: + r"""Construct kwargs for the `qProbabilityOfImprovement` constructor. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + objective: The objective to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + tau: The temperature parameter used in the sigmoid approximation + of the step function. Smaller values yield more accurate + approximations of the function, but result in gradients + estimates with higher variance. + best_f: The best objective value observed so far (assumed noiseless). Can + be a `batch_shape`-shaped tensor, which in case of a batched model + specifies potentially different values for each element of the batch. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if best_f is None: + best_f = get_best_f_mc( + training_data=training_data, + objective=objective, + posterior_transform=posterior_transform, + constraints=constraints, + model=model, + ) + + return { + "model": model, + "objective": objective, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + "sampler": sampler, + "tau": tau, + "best_f": best_f, + "constraints": constraints, + "eta": eta, + }
+ + + +
+[docs] +@acqf_input_constructor(qLowerConfidenceBound, qUpperConfidenceBound) +def construct_inputs_qUCB( + model: Model, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + X_baseline: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + beta: float = 0.2, +) -> dict[str, Any]: + r"""Construct kwargs for the `qUpperConfidenceBound` constructor. + + Args: + model: The model to be used in the acquisition function. + objective: The objective to be used in the acquisition function. + posterior_transform: The posterior transform to be used in the + acquisition function. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are used to + compute with infeasible cost when there are constraints. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + beta: Controls tradeoff between mean and standard deviation in UCB. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if constraints is not None: + if X_baseline is None: + raise ValueError("Constraints require an X_baseline.") + if objective is None: + objective = IdentityMCObjective() + objective = ConstrainedMCObjective( + objective=objective, + constraints=constraints, + infeasible_cost=get_infeasible_cost( + X=X_baseline, model=model, objective=objective + ), + ) + return { + "model": model, + "objective": objective, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + "sampler": sampler, + "beta": beta, + }
+ + + +def _get_sampler(mc_samples: int, qmc: bool) -> MCSampler: + """Set up MC sampler for q(N)EHVI.""" + # initialize the sampler + shape = torch.Size([mc_samples]) + if qmc: + return SobolQMCNormalSampler(sample_shape=shape) + return IIDNormalSampler(sample_shape=shape) + + +
+[docs] +@acqf_input_constructor(ExpectedHypervolumeImprovement) +def construct_inputs_EHVI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective_thresholds: Tensor, + posterior_transform: PosteriorTransform | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + alpha: float | None = None, + Y_pmean: Tensor | None = None, +) -> dict[str, Any]: + r"""Construct kwargs for `ExpectedHypervolumeImprovement` constructor.""" + num_objectives = objective_thresholds.shape[0] + if constraints is not None: + raise NotImplementedError("EHVI does not yet support outcome constraints.") + + X = _get_dataset_field( + training_data, + fieldname="X", + first_only=True, + assert_shared=True, + ) + + alpha = ( + get_default_partitioning_alpha(num_objectives=num_objectives) + if alpha is None + else alpha + ) + + # Compute posterior mean (for ref point computation ref pareto frontier) + # if one is not provided among arguments. + if Y_pmean is None: + with torch.no_grad(): + Y_pmean = model.posterior(X).mean + if alpha > 0: + partitioning = NondominatedPartitioning( + ref_point=objective_thresholds, + Y=Y_pmean, + alpha=alpha, + ) + else: + partitioning = FastNondominatedPartitioning( + ref_point=objective_thresholds, + Y=Y_pmean, + ) + + kwargs = { + "model": model, + "ref_point": objective_thresholds, + "partitioning": partitioning, + } + if posterior_transform is not None: + kwargs["posterior_transform"] = posterior_transform + return kwargs
+ + + +
+[docs] +@acqf_input_constructor( + qExpectedHypervolumeImprovement, qLogExpectedHypervolumeImprovement +) +def construct_inputs_qEHVI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective_thresholds: Tensor, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + alpha: float | None = None, + sampler: MCSampler | None = None, + X_pending: Tensor | None = None, + eta: float = 1e-3, + mc_samples: int = 128, + qmc: bool = True, +) -> dict[str, Any]: + r""" + Construct kwargs for `qExpectedHypervolumeImprovement` and + `qLogExpectedHypervolumeImprovement`. + """ + X = _get_dataset_field( + training_data, + fieldname="X", + first_only=True, + assert_shared=True, + ) + + # compute posterior mean (for ref point computation ref pareto frontier) + with torch.no_grad(): + Y_pmean = model.posterior(X).mean + # For HV-based acquisition functions we pass the constraint transform directly + if constraints is not None: + # Adjust `Y_pmean` to contain feasible points only. + feas = torch.stack([c(Y_pmean) <= 0 for c in constraints], dim=-1).all(dim=-1) + Y_pmean = Y_pmean[feas] + + num_objectives = objective_thresholds.shape[0] + + alpha = ( + get_default_partitioning_alpha(num_objectives=num_objectives) + if alpha is None + else alpha + ) + + if objective is None: + ref_point = objective_thresholds + Y = Y_pmean + elif isinstance(objective, RiskMeasureMCObjective): + ref_point = objective.preprocessing_function(objective_thresholds) + Y = objective.preprocessing_function(Y_pmean) + else: + ref_point = objective(objective_thresholds) + Y = objective(Y_pmean) + + if alpha > 0: + partitioning = NondominatedPartitioning( + ref_point=ref_point, + Y=Y, + alpha=alpha, + ) + else: + partitioning = FastNondominatedPartitioning( + ref_point=ref_point, + Y=Y, + ) + + if sampler is None and isinstance(model, GPyTorchModel): + sampler = _get_sampler(mc_samples=mc_samples, qmc=qmc) + + return { + "model": model, + "ref_point": ref_point, + "partitioning": partitioning, + "sampler": sampler, + "X_pending": X_pending, + "constraints": constraints, + "eta": eta, + "objective": objective, + }
+ + + +
+[docs] +@acqf_input_constructor(qNoisyExpectedHypervolumeImprovement) +def construct_inputs_qNEHVI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective_thresholds: Tensor, + objective: MCMultiOutputObjective | None = None, + X_baseline: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + alpha: float | None = None, + sampler: MCSampler | None = None, + X_pending: Tensor | None = None, + eta: float = 1e-3, + fat: bool = False, + mc_samples: int = 128, + qmc: bool = True, + prune_baseline: bool = True, + cache_pending: bool = True, + max_iep: int = 0, + incremental_nehvi: bool = True, + cache_root: bool = True, +) -> dict[str, Any]: + r"""Construct kwargs for `qNoisyExpectedHypervolumeImprovement`'s constructor.""" + if X_baseline is None: + X_baseline = _get_dataset_field( + training_data, + fieldname="X", + first_only=True, + assert_shared=True, + ) + # This selects the objectives (a subset of the outcomes) and set each + # objective threshold to have the proper optimization direction. + if objective is None: + objective = IdentityMCMultiOutputObjective() + + if constraints is not None: + if isinstance(objective, RiskMeasureMCObjective): + raise UnsupportedError( + "Outcome constraints are not supported with risk measures. " + "Use a feasibility-weighted risk measure instead." + ) + + if sampler is None and isinstance(model, GPyTorchModel): + sampler = _get_sampler(mc_samples=mc_samples, qmc=qmc) + + if isinstance(objective, RiskMeasureMCObjective): + ref_point = objective.preprocessing_function(objective_thresholds) + else: + ref_point = objective(objective_thresholds) + + num_objectives = objective_thresholds[~torch.isnan(objective_thresholds)].shape[0] + if alpha is None: + alpha = get_default_partitioning_alpha(num_objectives=num_objectives) + + return { + "model": model, + "ref_point": ref_point, + "X_baseline": X_baseline, + "sampler": sampler, + "objective": objective, + "constraints": constraints, + "X_pending": X_pending, + "eta": eta, + "fat": fat, + "prune_baseline": prune_baseline, + "alpha": alpha, + "cache_pending": cache_pending, + "max_iep": max_iep, + "incremental_nehvi": incremental_nehvi, + "cache_root": cache_root, + }
+ + + +
+[docs] +@acqf_input_constructor(qLogNoisyExpectedHypervolumeImprovement) +def construct_inputs_qLogNEHVI( + model: Model, + training_data: MaybeDict[SupervisedDataset], + objective_thresholds: Tensor, + objective: MCMultiOutputObjective | None = None, + X_baseline: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + alpha: float | None = None, + sampler: MCSampler | None = None, + X_pending: Tensor | None = None, + eta: float = 1e-3, + fat: bool = True, + mc_samples: int = 128, + qmc: bool = True, + prune_baseline: bool = True, + cache_pending: bool = True, + max_iep: int = 0, + incremental_nehvi: bool = True, + cache_root: bool = True, + tau_relu: float = TAU_RELU, + tau_max: float = TAU_MAX, +) -> dict[str, Any]: + """ + Construct kwargs for `qLogNoisyExpectedHypervolumeImprovement`'s constructor." + """ + return { + **construct_inputs_qNEHVI( + model=model, + training_data=training_data, + objective_thresholds=objective_thresholds, + objective=objective, + X_baseline=X_baseline, + constraints=constraints, + alpha=alpha, + sampler=sampler, + X_pending=X_pending, + eta=eta, + fat=fat, + mc_samples=mc_samples, + qmc=qmc, + prune_baseline=prune_baseline, + cache_pending=cache_pending, + max_iep=max_iep, + incremental_nehvi=incremental_nehvi, + cache_root=cache_root, + ), + "tau_relu": tau_relu, + "tau_max": tau_max, + }
+ + + +
+[docs] +@acqf_input_constructor(qLogNParEGO) +def construct_inputs_qLogNParEGO( + model: Model, + training_data: MaybeDict[SupervisedDataset], + scalarization_weights: Tensor | None = None, + objective: MCMultiOutputObjective | None = None, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + X_baseline: Tensor | None = None, + prune_baseline: bool | None = True, + cache_root: bool | None = True, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + fat: bool = True, + tau_max: float = TAU_MAX, + tau_relu: float = TAU_RELU, +): + r"""Construct kwargs for the `qLogNoisyExpectedImprovement` constructor. + + Args: + model: The model to be used in the acquisition function. + training_data: Dataset(s) used to train the model. + scalarization_weights: A `m`-dim Tensor of weights to be used in the + Chebyshev scalarization. If omitted, samples from the unit simplex. + objective: The MultiOutputMCAcquisitionObjective under which the samples are + evaluated before applying Chebyshev scalarization. + Defaults to `IdentityMultiOutputObjective()`. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. + sampler: The sampler used to draw base samples. If omitted, uses + the acquisition functions's default sampler. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are considered as + the potential best design point. If omitted, checks that all + training_data have the same input features and take the first `X`. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the best point. This can significantly + improve performance and is generally recommended. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + fat: Toggles the use of the fat-tailed non-linearities to smoothly approximate + the constraints indicator function. + tau_max: Temperature parameter controlling the sharpness of the smooth + approximations to max. + tau_relu: Temperature parameter controlling the sharpness of the smooth + approximations to ReLU. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + base_inputs = construct_inputs_qLogNEI( + model=model, + training_data=training_data, + objective=objective, + X_pending=X_pending, + sampler=sampler, + X_baseline=X_baseline, + prune_baseline=prune_baseline, + cache_root=cache_root, + constraints=constraints, + eta=eta, + fat=fat, + tau_max=tau_max, + tau_relu=tau_relu, + ) + base_inputs.pop("posterior_transform", None) + return { + **base_inputs, + "scalarization_weights": scalarization_weights, + }
+ + + +
+[docs] +@acqf_input_constructor(qMaxValueEntropy) +def construct_inputs_qMES( + model: Model, + training_data: MaybeDict[SupervisedDataset], + bounds: list[tuple[float, float]], + posterior_transform: PosteriorTransform | None = None, + candidate_size: int = 1000, + maximize: bool = True, + # TODO: qMES also supports other inputs, such as num_fantasies +) -> dict[str, Any]: + r"""Construct kwargs for `qMaxValueEntropy` constructor.""" + + X = _get_dataset_field(training_data, "X", first_only=True) + _kw = {"device": X.device, "dtype": X.dtype} + _rvs = torch.rand(candidate_size, len(bounds), **_kw) + _bounds = torch.as_tensor(bounds, **_kw).transpose(0, 1) + return { + "model": model, + "posterior_transform": posterior_transform, + "candidate_set": _bounds[0] + (_bounds[1] - _bounds[0]) * _rvs, + "maximize": maximize, + }
+ + + +
+[docs] +def construct_inputs_mf_base( + target_fidelities: dict[int, int | float], + fidelity_weights: dict[int, float] | None = None, + cost_intercept: float = 1.0, + num_trace_observations: int = 0, +) -> dict[str, Any]: + r"""Construct kwargs for a multifidelity acquisition function's constructor.""" + if fidelity_weights is None: + fidelity_weights = {f: 1.0 for f in target_fidelities} + + if set(target_fidelities) != set(fidelity_weights): + raise RuntimeError( + "Must provide the same indices for target_fidelities " + f"({set(target_fidelities)}) and fidelity_weights " + f" ({set(fidelity_weights)})." + ) + + cost_aware_utility = InverseCostWeightedUtility( + cost_model=AffineFidelityCostModel( + fidelity_weights=fidelity_weights, fixed_cost=cost_intercept + ) + ) + + return { + "cost_aware_utility": cost_aware_utility, + "expand": lambda X: expand_trace_observations( + X=X, + fidelity_dims=sorted(target_fidelities), + num_trace_obs=num_trace_observations, + ), + "project": lambda X: project_to_target_fidelity( + X=X, target_fidelities=target_fidelities + ), + }
+ + + +
+[docs] +@acqf_input_constructor(qKnowledgeGradient) +def construct_inputs_qKG( + model: Model, + training_data: MaybeDict[SupervisedDataset], + bounds: list[tuple[float, float]], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + num_fantasies: int = 64, + with_current_value: bool = False, + **optimize_objective_kwargs: TOptimizeObjectiveKwargs, +) -> dict[str, Any]: + r"""Construct kwargs for `qKnowledgeGradient` constructor.""" + + inputs_qkg = { + "model": model, + "objective": objective, + "posterior_transform": posterior_transform, + "num_fantasies": num_fantasies, + } + + if with_current_value: + X = _get_dataset_field(training_data, "X", first_only=True) + _bounds = torch.as_tensor(bounds, dtype=X.dtype, device=X.device) + + _, current_value = optimize_objective( + model=model, + bounds=_bounds.t(), + q=1, + objective=objective, + posterior_transform=posterior_transform, + **optimize_objective_kwargs, + ) + inputs_qkg["current_value"] = current_value.detach().cpu().max() + + return inputs_qkg
+ + + +
+[docs] +@acqf_input_constructor(qHypervolumeKnowledgeGradient) +def construct_inputs_qHVKG( + model: Model, + training_data: MaybeDict[SupervisedDataset], + bounds: list[tuple[float, float]], + objective_thresholds: Tensor, + objective: MCMultiOutputObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + num_fantasies: int = 8, + num_pareto: int = 10, + **optimize_objective_kwargs: TOptimizeObjectiveKwargs, +) -> dict[str, Any]: + r"""Construct kwargs for `qKnowledgeGradient` constructor.""" + + X = _get_dataset_field(training_data, "X", first_only=True) + _bounds = torch.as_tensor(bounds, dtype=X.dtype, device=X.device) + + ref_point = _get_ref_point( + objective_thresholds=objective_thresholds, objective=objective + ) + + acq_function = _get_hv_value_function( + model=model, + ref_point=ref_point, + use_posterior_mean=True, + objective=objective, + ) + + _, current_value = optimize_objective( + model=model, + bounds=_bounds.t(), + q=num_pareto, + acq_function=acq_function, + **optimize_objective_kwargs, + ) + + return { + "model": model, + "objective": objective, + "ref_point": ref_point, + "num_fantasies": num_fantasies, + "num_pareto": num_pareto, + "current_value": current_value.detach().cpu().max(), + }
+ + + +
+[docs] +@acqf_input_constructor(qMultiFidelityKnowledgeGradient) +def construct_inputs_qMFKG( + model: Model, + training_data: MaybeDict[SupervisedDataset], + bounds: list[tuple[float, float]], + target_fidelities: dict[int, int | float], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + fidelity_weights: dict[int, float] | None = None, + cost_intercept: float = 1.0, + num_trace_observations: int = 0, + num_fantasies: int = 64, + **optimize_objective_kwargs: TOptimizeObjectiveKwargs, +) -> dict[str, Any]: + r"""Construct kwargs for `qMultiFidelityKnowledgeGradient` constructor.""" + + X = _get_dataset_field(training_data, "X", first_only=True) + _bounds = torch.as_tensor(bounds, dtype=X.dtype, device=X.device) + + inputs_mf = construct_inputs_mf_base( + target_fidelities=target_fidelities, + fidelity_weights=fidelity_weights, + cost_intercept=cost_intercept, + num_trace_observations=num_trace_observations, + ) + + _, current_value = optimize_objective( + model=model, + bounds=_bounds.t(), + q=1, + objective=objective, + posterior_transform=posterior_transform, + fixed_features=target_fidelities, + **optimize_objective_kwargs, + ) + + return { + "model": model, + "objective": objective, + "posterior_transform": posterior_transform, + "num_fantasies": num_fantasies, + "current_value": current_value.detach().cpu().max(), + **inputs_mf, + }
+ + + +
+[docs] +@acqf_input_constructor(qMultiFidelityHypervolumeKnowledgeGradient) +def construct_inputs_qMFHVKG( + model: Model, + training_data: MaybeDict[SupervisedDataset], + bounds: list[tuple[float, float]], + target_fidelities: dict[int, int | float], + objective_thresholds: Tensor, + objective: MCMultiOutputObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + fidelity_weights: dict[int, float] | None = None, + cost_intercept: float = 1.0, + num_trace_observations: int = 0, + num_fantasies: int = 8, + num_pareto: int = 10, + **optimize_objective_kwargs: TOptimizeObjectiveKwargs, +) -> dict[str, Any]: + r""" + Construct kwargs for `qMultiFidelityHypervolumeKnowledgeGradient` constructor. + """ + + inputs_mf = construct_inputs_mf_base( + target_fidelities=target_fidelities, + fidelity_weights=fidelity_weights, + cost_intercept=cost_intercept, + num_trace_observations=num_trace_observations, + ) + + if num_trace_observations > 0: + raise NotImplementedError( + "Trace observations are not currently supported " + "by `qMultiFidelityHypervolumeKnowledgeGradient`." + ) + + del inputs_mf["expand"] + + X = _get_dataset_field(training_data, "X", first_only=True) + _bounds = torch.as_tensor(bounds, dtype=X.dtype, device=X.device) + + ref_point = _get_ref_point( + objective_thresholds=objective_thresholds, objective=objective + ) + + acq_function = _get_hv_value_function( + model=model, + ref_point=ref_point, + use_posterior_mean=True, + objective=objective, + ) + + _, current_value = optimize_objective( + model=model, + bounds=_bounds.t(), + q=num_pareto, + acq_function=acq_function, + fixed_features=target_fidelities, + **optimize_objective_kwargs, + ) + + return { + "model": model, + "objective": objective, + "ref_point": ref_point, + "num_fantasies": num_fantasies, + "num_pareto": num_pareto, + "current_value": current_value.detach().cpu().max(), + "target_fidelities": target_fidelities, + **inputs_mf, + }
+ + + +
+[docs] +@acqf_input_constructor(qMultiFidelityMaxValueEntropy) +def construct_inputs_qMFMES( + model: Model, + training_data: MaybeDict[SupervisedDataset], + bounds: list[tuple[float, float]], + target_fidelities: dict[int, int | float], + num_fantasies: int = 64, + fidelity_weights: dict[int, float] | None = None, + cost_intercept: float = 1.0, + num_trace_observations: int = 0, + candidate_size: int = 1000, + maximize: bool = True, +) -> dict[str, Any]: + r"""Construct kwargs for `qMultiFidelityMaxValueEntropy` constructor.""" + inputs_mf = construct_inputs_mf_base( + target_fidelities=target_fidelities, + fidelity_weights=fidelity_weights, + cost_intercept=cost_intercept, + num_trace_observations=num_trace_observations, + ) + + inputs_qmes = construct_inputs_qMES( + model=model, + training_data=training_data, + bounds=bounds, + candidate_size=candidate_size, + maximize=maximize, + ) + + return {**inputs_mf, **inputs_qmes, "num_fantasies": num_fantasies}
+ + + +
+[docs] +@acqf_input_constructor(AnalyticExpectedUtilityOfBestOption) +def construct_inputs_analytic_eubo( + model: Model, + pref_model: Model | None = None, + previous_winner: Tensor | None = None, + sample_multiplier: float | None = 1.0, + objective: LearnedObjective | None = None, + posterior_transform: PosteriorTransform | None = None, +) -> dict[str, Any]: + r"""Construct kwargs for the `AnalyticExpectedUtilityOfBestOption` constructor. + + `model` is the primary model defined over the parameter space. It can be the + outcome model in BOPE or the preference model in PBO. `pref_model` is the model + defined over the outcome/metric space, which is typically the preference model + in BOPE. + + If both model and pref_model exist, we are performing Bayesian Optimization with + Preference Exploration (BOPE). When only pref_model is None, we are performing + preferential BO (PBO). + + Args: + model: The outcome model to be used in the acquisition function in BOPE + when pref_model exists; otherwise, model is the preference model and + we are doing Preferential BO + pref_model: The preference model to be used in preference exploration as in + BOPE; if None, we are doing PBO and model is the preference model. + previous_winner: The previous winner of the best option. + sample_multiplier: The scale factor for the single-sample model. + objective: Ignored. This argument is allowed to be passed then ignored + because of the way that EUBO is typically used in a BOPE loop. + posterior_transform: Ignored. This argument is allowed to be passed then + ignored because of the way that EUBO is typically used in a BOPE + loop. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if pref_model is None: + return { + "pref_model": model, + "outcome_model": None, + "previous_winner": previous_winner, + } + else: + # construct a deterministic fixed single sample model from `model` + # i.e., performing EUBO-zeta by default as described + # in https://arxiv.org/abs/2203.11382 + # using pref_model.dim instead of model.num_outputs here as MTGP's + # num_outputs could be tied to the number of tasks + w = torch.randn(pref_model.dim) * sample_multiplier + one_sample_outcome_model = FixedSingleSampleModel(model=model, w=w) + + return { + "pref_model": pref_model, + "outcome_model": one_sample_outcome_model, + "previous_winner": previous_winner, + }
+ + + +
+[docs] +@acqf_input_constructor(qExpectedUtilityOfBestOption) +def construct_inputs_qeubo( + model: Model, + pref_model: Model | None = None, + sample_multiplier: float | None = 1.0, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, +) -> dict[str, Any]: + r"""Construct kwargs for the `qExpectedUtilityOfBestOption` (qEUBO) constructor. + + `model` is the primary model defined over the parameter space. It can be the + outcomde model in BOPE or the preference model in PBO. `pref_model` is the model + defined over the outcome/metric space, which is typically the preference model + in BOPE. + + If both model and pref_model exist, we are performing Bayesian Optimization with + Preference Exploration (BOPE). When only pref_model is None, we are performing + preferential BO (PBO). + + Args: + model: The outcome model to be used in the acquisition function in BOPE + when pref_model exists; otherwise, model is the preference model and + we are doing Preferential BO + pref_model: The preference model to be used in preference exploration as in + BOPE; if None, we are doing PBO and model is the preference model. + sample_multiplier: The scale factor for the single-sample model. + + Returns: + A dict mapping kwarg names of the constructor to values. + """ + if pref_model is None: + return { + "pref_model": model, + "outcome_model": None, + "sampler": sampler, + "objective": objective, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + } + else: + # construct a deterministic fixed single sample model from `model` + # i.e., performing EUBO-zeta by default as described + # in https://arxiv.org/abs/2203.11382 + # using pref_model.dim instead of model.num_outputs here as MTGP's + # num_outputs could be tied to the number of tasks + w = torch.randn(pref_model.dim) * sample_multiplier + one_sample_outcome_model = FixedSingleSampleModel(model=model, w=w) + + return { + "pref_model": pref_model, + "outcome_model": one_sample_outcome_model, + "sampler": sampler, + "objective": objective, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + }
+ + + +
+[docs] +def get_best_f_analytic( + training_data: MaybeDict[SupervisedDataset], + posterior_transform: PosteriorTransform | None = None, +) -> Tensor: + if isinstance(training_data, dict) and not _field_is_shared( + training_data, fieldname="X" + ): + raise NotImplementedError("Currently only block designs are supported.") + + Y = _get_dataset_field( + training_data, + fieldname="Y", + join_rule=lambda field_tensors: torch.cat(field_tensors, dim=-1), + ) + + if posterior_transform is not None: + return posterior_transform.evaluate(Y).max(-1).values + if Y.shape[-1] > 1: + raise NotImplementedError( + "Analytic acquisition functions currently only work with " + "multi-output models if provided with a `ScalarizedObjective`." + ) + return Y.max(-2).values.squeeze(-1)
+ + + +
+[docs] +def get_best_f_mc( + training_data: MaybeDict[SupervisedDataset], + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + model: Model | None = None, +) -> Tensor: + """ + Computes the maximum value of the objective over the training data. + + Args: + training_data: Has fields Y, which is evaluated by `objective`, and X, + which is used as `X_baseline`. `Y` is of shape + `batch_shape x q x m`. + objective: The objective under which to evaluate the training data. If + omitted, uses `IdentityMCObjective`. + posterior_transform: An optional PosteriorTransform to apply to `Y` + before computing the objective. + constraints: For assessing feasibility. + model: Used by `compute_best_feasible_objective` when there are no + feasible observations. + + Returns: + A Tensor of shape `batch_shape`. + """ + if isinstance(training_data, dict) and not _field_is_shared( + training_data, fieldname="X" + ): + raise NotImplementedError("Currently only block designs are supported.") + + X_baseline = _get_dataset_field( + training_data, + fieldname="X", + assert_shared=True, + first_only=True, + ) + + Y = _get_dataset_field( + training_data, + fieldname="Y", + join_rule=lambda field_tensors: torch.cat(field_tensors, dim=-1), + ) # batch_shape x q x m + + if posterior_transform is not None: + # retain the original tensor dimension since objective expects explicit + # output dimension. + Y_dim = Y.dim() + Y = posterior_transform.evaluate(Y) + if Y.dim() < Y_dim: + Y = Y.unsqueeze(-1) + if objective is None: + if Y.shape[-1] > 1: + raise UnsupportedError( + "Acquisition functions require an objective when " + "used with multi-output models (except for multi-objective" + "acquisition functions)." + ) + objective = IdentityMCObjective() + # `Y` is of shape `(batch_shape) x q x m`; `MCAcquisitionObjective`s expect + # inputs `sample_shape x (batch_shape) x q x m`. + # For most objectives, `obj` will have shape `1 x (batch_shape) x q`, but + # with a `LearnedObjective` it can be `num_samples x (batch_shape) x q`. + obj = objective(Y.unsqueeze(0), X=X_baseline) + obj = obj.mean(dim=0) # taking mean over monte carlo samples + return compute_best_feasible_objective( + samples=Y, + obj=obj, + constraints=constraints, + model=model, + objective=objective, + posterior_transform=posterior_transform, + X_baseline=X_baseline, + )
+ + + +
+[docs] +def optimize_objective( + model: Model, + bounds: Tensor, + q: int, + acq_function: AcquisitionFunction | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + linear_constraints: tuple[Tensor, Tensor] | None = None, + fixed_features: dict[int, float] | None = None, + qmc: bool = True, + mc_samples: int = 512, + seed_inner: int | None = None, + optimizer_options: dict[str, Any] | None = None, + post_processing_func: Callable[[Tensor], Tensor] | None = None, + batch_initial_conditions: Tensor | None = None, + sequential: bool = False, +) -> tuple[Tensor, Tensor]: + r"""Optimize an objective under the given model. + + Args: + model: The model to be used in the objective. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X`. + q: The cardinality of input sets on which the objective is to be evaluated. + objective: The objective to optimize. + posterior_transform: The posterior transform to be used in the + acquisition function. + linear_constraints: A tuple of (A, b). Given `k` linear constraints on a + `d`-dimensional space, `A` is `k x d` and `b` is `k x 1` such that + `A x <= b`. (Not used by single task models). + fixed_features: A dictionary of feature assignments `{feature_index: value}` to + hold fixed during generation. + qmc: Toggle for enabling (qmc=1) or disabling (qmc=0) use of Quasi Monte Carlo. + mc_samples: Integer number of samples used to estimate Monte Carlo objectives. + seed_inner: Integer seed used to initialize the sampler passed to MCObjective. + optimizer_options: Table used to lookup keyword arguments for the optimizer. + post_processing_func: A function that post-processes an optimization + result appropriately (i.e. according to `round-trip` transformations). + batch_initial_conditions: A Tensor of initial values for the optimizer. + sequential: If False, uses joint optimization, otherwise uses sequential + optimization. + + Returns: + A tuple containing the best input locations and corresponding objective values. + """ + if optimizer_options is None: + optimizer_options = {} + + if acq_function is None: + if objective is None: + acq_function = PosteriorMean( + model=model, posterior_transform=posterior_transform + ) + else: + sampler_cls = SobolQMCNormalSampler if qmc else IIDNormalSampler + acq_function = qSimpleRegret( + model=model, + objective=objective, + posterior_transform=posterior_transform, + sampler=sampler_cls( + sample_shape=torch.Size([mc_samples]), seed=seed_inner + ), + ) + + if fixed_features: + acq_function = FixedFeatureAcquisitionFunction( + acq_function=acq_function, + d=bounds.shape[-1], + columns=list(fixed_features.keys()), + values=list(fixed_features.values()), + ) + free_feature_dims = list(range(len(bounds)) - fixed_features.keys()) + free_feature_bounds = bounds[:, free_feature_dims] # (2, d' <= d) + else: + free_feature_bounds = bounds + + if linear_constraints is None: + inequality_constraints = None + else: + A, b = linear_constraints + inequality_constraints = [] + k, d = A.shape + for i in range(k): + indices = A[i, :].nonzero(as_tuple=False).squeeze() + coefficients = -A[i, indices] + rhs = -b[i, 0] + inequality_constraints.append((indices, coefficients, rhs)) + + return optimize_acqf( + acq_function=acq_function, + bounds=free_feature_bounds, + q=q, + num_restarts=optimizer_options.get("num_restarts", 60), + raw_samples=optimizer_options.get("raw_samples", 1024), + options={ + "batch_limit": optimizer_options.get("batch_limit", 8), + "maxiter": optimizer_options.get("maxiter", 200), + "nonnegative": optimizer_options.get("nonnegative", False), + "method": optimizer_options.get("method", "L-BFGS-B"), + }, + inequality_constraints=inequality_constraints, + fixed_features=None, # handled inside the acquisition function + post_processing_func=post_processing_func, + batch_initial_conditions=batch_initial_conditions, + return_best_only=True, + sequential=sequential, + )
+ + + +
+[docs] +@acqf_input_constructor(qJointEntropySearch) +def construct_inputs_qJES( + model: Model, + bounds: list[tuple[float, float]], + num_optima: int = 64, + condition_noiseless: bool = True, + posterior_transform: ScalarizedPosteriorTransform | None = None, + X_pending: Tensor | None = None, + estimation_type: str = "LB", + num_samples: int = 64, +): + dtype = model.train_targets.dtype + optimal_inputs, optimal_outputs = get_optimal_samples( + model=model, + bounds=torch.as_tensor(bounds, dtype=dtype).T, + num_optima=num_optima, + posterior_transform=posterior_transform, + return_transformed=True, + ) + + inputs = { + "model": model, + "optimal_inputs": optimal_inputs, + "optimal_outputs": optimal_outputs, + "condition_noiseless": condition_noiseless, + "posterior_transform": posterior_transform, + "X_pending": X_pending, + "estimation_type": estimation_type, + "num_samples": num_samples, + } + return inputs
+ + + +
+[docs] +@acqf_input_constructor(qBayesianActiveLearningByDisagreement) +def construct_inputs_BALD( + model: Model, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + posterior_transform: PosteriorTransform | None = None, +): + inputs = { + "model": model, + "X_pending": X_pending, + "sampler": sampler, + "posterior_transform": posterior_transform, + } + return inputs
+ + + +
+[docs] +@acqf_input_constructor(qNegIntegratedPosteriorVariance) +def construct_inputs_NIPV( + model: Model, + bounds: list[tuple[float, float]], + num_mc_points: int = 128, + X_pending: Tensor | None = None, + posterior_transform: PosteriorTransform | None = None, +) -> dict[str, Any]: + """Construct inputs for qNegIntegratedPosteriorVariance.""" + bounds = torch.as_tensor(bounds).to(model.train_targets).T + mc_points = draw_sobol_samples(bounds=bounds, n=num_mc_points, q=1).squeeze(-2) + inputs = { + "model": model, + "mc_points": mc_points, + "X_pending": X_pending, + "posterior_transform": posterior_transform, + } + return inputs
+ + + +def _get_ref_point( + objective_thresholds: Tensor, + objective: MCMultiOutputObjective | None = None, +) -> Tensor: + if objective is None: + ref_point = objective_thresholds + elif isinstance(objective, RiskMeasureMCObjective): + ref_point = objective.preprocessing_function(objective_thresholds) + else: + ref_point = objective(objective_thresholds) + + return ref_point +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/joint_entropy_search.html b/website-old/pages/api/_modules/botorch/acquisition/joint_entropy_search.html new file mode 100644 index 0000000000..50eb14cbd6 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/joint_entropy_search.html @@ -0,0 +1,418 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.joint_entropy_search

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Acquisition function for joint entropy search (JES).
+
+.. [Hvarfner2022joint]
+    C. Hvarfner, F. Hutter, L. Nardi,
+    Joint Entropy Search for Maximally-informed Bayesian Optimization.
+    In Proceedings of the Annual Conference on Neural Information
+    Processing Systems (NeurIPS), 2022.
+
+.. [Tu2022joint]
+    B. Tu, A. Gandy, N. Kantas, B. Shafei,
+    Joint Entropy Search for Multi-objective Bayesian Optimization.
+    In Proceedings of the Annual Conference on Neural Information
+    Processing Systems (NeurIPS), 2022.
+"""
+
+from __future__ import annotations
+
+import warnings
+from math import log, pi
+
+import torch
+from botorch import settings
+from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.fully_bayesian import SaasFullyBayesianSingleTaskGP
+from botorch.models.model import Model
+from botorch.models.utils import check_no_nans, fantasize as fantasize_flag
+from botorch.models.utils.gpytorch_modules import MIN_INFERRED_NOISE_LEVEL
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+
+from torch.distributions import Normal
+
+MCMC_DIM = -3  # Only relevant if you do Fully Bayesian GPs.
+ESTIMATION_TYPES = ["MC", "LB"]
+MC_ADD_TERM = 0.5 * (1 + log(2 * pi))
+
+# The CDF query cannot be strictly zero in the division
+# and this clamping helps assure that it is always positive.
+CLAMP_LB = torch.finfo(torch.float32).eps
+FULLY_BAYESIAN_ERROR_MSG = (
+    "JES is not yet available with Fully Bayesian GPs. Track the issue, "
+    "which regards conditioning on a number of optima on a collection "
+    "of models, in detail at https://github.com/pytorch/botorch/issues/1680"
+)
+
+
+
+[docs] +class qJointEntropySearch(AcquisitionFunction, MCSamplerMixin): + r"""The acquisition function for the Joint Entropy Search, where the batches + `q > 1` are supported through the lower bound formulation. + + This acquisition function computes the mutual information between the observation + at a candidate point `X` and the optimal input-output pair. + + See [Tu2022joint]_ for a discussion on the estimation procedure. + """ + + def __init__( + self, + model: Model, + optimal_inputs: Tensor, + optimal_outputs: Tensor, + condition_noiseless: bool = True, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + estimation_type: str = "LB", + num_samples: int = 64, + ) -> None: + r"""Joint entropy search acquisition function. + + Args: + model: A fitted single-outcome model. + optimal_inputs: A `num_samples x d`-dim tensor containing the sampled + optimal inputs of dimension `d`. We assume for simplicity that each + sample only contains one optimal set of inputs. + optimal_outputs: A `num_samples x 1`-dim Tensor containing the optimal + set of objectives of dimension `1`. + condition_noiseless: Whether to condition on noiseless optimal observations + `f*` [Hvarfner2022joint]_ or noisy optimal observations `y*` + [Tu2022joint]_. These are sampled identically, so this only controls + the fashion in which the GP is reshaped as a result of conditioning + on the optimum. + posterior_transform: PosteriorTransform to negate or scalarize the output. + estimation_type: estimation_type: A string to determine which entropy + estimate is computed: Lower bound" ("LB") or "Monte Carlo" ("MC"). + Lower Bound is recommended due to the relatively high variance + of the MC estimator. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation, but have not yet been evaluated. + num_samples: The number of Monte Carlo samples used for the Monte Carlo + estimate. + """ + super().__init__(model=model) + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples])) + MCSamplerMixin.__init__(self, sampler=sampler) + # To enable fully bayesian GP conditioning, we need to unsqueeze + # to get num_optima x num_gps unique GPs + + # inputs come as num_optima_per_model x (num_models) x d + # but we want it four-dimensional in the Fully bayesian case, + # and three-dimensional otherwise. + self.optimal_inputs = optimal_inputs.unsqueeze(-2) + self.optimal_outputs = optimal_outputs.unsqueeze(-2) + self.optimal_output_values = ( + posterior_transform.evaluate(self.optimal_outputs).unsqueeze(-1) + if posterior_transform + else self.optimal_outputs + ) + self.posterior_transform = posterior_transform + + self.num_samples = optimal_inputs.shape[0] + self.condition_noiseless = condition_noiseless + self.initial_model = model + + # Here, the optimal inputs have shapes num_optima x [num_models if FB] x 1 x D + # and the optimal outputs have shapes num_optima x [num_models if FB] x 1 x 1 + # The third dimension equaling 1 is required to get one optimum per model, + # which raises a BotorchTensorDimensionWarning. + if isinstance(model, SaasFullyBayesianSingleTaskGP): + raise NotImplementedError(FULLY_BAYESIAN_ERROR_MSG) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + with fantasize_flag(): + with settings.propagate_grads(False): + # We must do a forward pass one before conditioning. + self.initial_model.posterior( + self.optimal_inputs[:1], observation_noise=False + ) + + # This equates to the JES version proposed by Hvarfner et. al. + if self.condition_noiseless: + opt_noise = torch.full_like( + self.optimal_outputs, MIN_INFERRED_NOISE_LEVEL + ) + # conditional (batch) model of shape (num_models) + # x num_optima_per_model + self.conditional_model = ( + self.initial_model.condition_on_observations( + X=self.initial_model.transform_inputs(self.optimal_inputs), + Y=self.optimal_outputs, + noise=opt_noise, + ) + ) + else: + self.conditional_model = ( + self.initial_model.condition_on_observations( + X=self.initial_model.transform_inputs(self.optimal_inputs), + Y=self.optimal_outputs, + ) + ) + + self.estimation_type = estimation_type + self.set_X_pending(X_pending) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluates qJointEntropySearch at the design points `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of `batch_shape` t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of acquisition values at the given design + points `X`. + """ + if self.estimation_type == "LB": + res = self._compute_lower_bound_information_gain(X) + elif self.estimation_type == "MC": + res = self._compute_monte_carlo_information_gain(X) + else: + raise ValueError( + f"Estimation type {self.estimation_type} is not valid. " + f"Please specify any of {ESTIMATION_TYPES}" + ) + return res
+ + + def _compute_lower_bound_information_gain( + self, X: Tensor, return_parts: bool = False + ) -> Tensor: + r"""Evaluates the lower bound information gain at the design points `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of `batch_shape` t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of acquisition values at the given design + points `X`. + """ + initial_posterior = self.initial_model.posterior( + X, observation_noise=True, posterior_transform=self.posterior_transform + ) + # need to check if there is a two-dimensional batch shape - + # the sampled optima appear in the dimension right after + batch_shape = X.shape[:-2] + sample_dim = len(batch_shape) + # We DISREGARD the additional constant term. + initial_entropy = 0.5 * torch.logdet( + initial_posterior.mvn.lazy_covariance_matrix + ) + + # initial_entropy of shape batch_size or batch_size x num_models if FBGP + # first need to unsqueeze the sample dim (after batch dim) and then the two last + initial_entropy = ( + initial_entropy.unsqueeze(sample_dim).unsqueeze(-1).unsqueeze(-1) + ) + + # Compute the mixture mean and variance + posterior_m = self.conditional_model.posterior( + X.unsqueeze(MCMC_DIM), + observation_noise=True, + posterior_transform=self.posterior_transform, + ) + noiseless_var = self.conditional_model.posterior( + X.unsqueeze(MCMC_DIM), + observation_noise=False, + posterior_transform=self.posterior_transform, + ).variance + + mean_m = posterior_m.mean + variance_m = posterior_m.variance + + check_no_nans(variance_m) + # get stdv of noiseless variance + stdv = noiseless_var.sqrt() + # batch_shape x 1 + normal = Normal( + torch.zeros(1, device=X.device, dtype=X.dtype), + torch.ones(1, device=X.device, dtype=X.dtype), + ) + normalized_mvs = (self.optimal_output_values - mean_m) / stdv + cdf_mvs = normal.cdf(normalized_mvs).clamp_min(CLAMP_LB) + pdf_mvs = torch.exp(normal.log_prob(normalized_mvs)) + + ratio = pdf_mvs / cdf_mvs + var_truncated = noiseless_var * ( + 1 - (normalized_mvs + ratio) * ratio + ).clamp_min(CLAMP_LB) + + var_truncated = var_truncated + (variance_m - noiseless_var) + conditional_entropy = 0.5 * torch.log(var_truncated) + + # Shape batch_size x num_optima x [num_models if FB] x q x num_outputs + # squeeze the num_outputs dim (since it's 1) + entropy_reduction = ( + initial_entropy - conditional_entropy.sum(dim=-2, keepdim=True) + ).squeeze(-1) + # average over the number of optima and squeeze the q-batch + + entropy_reduction = entropy_reduction.mean(dim=sample_dim).squeeze(-1) + return entropy_reduction + + def _compute_monte_carlo_variables(self, posterior): + """Retrieves monte carlo samples and their log probabilities from the posterior. + + Args: + posterior: The posterior distribution. + + Returns: + A two-element tuple containing: + - samples: a num_optima x batch_shape x num_mc_samples x q x 1 + tensor of samples drawn from the posterior. + - samples_log_prob: a num_optima x batch_shape x num_mc_samples x q x 1 + tensor of associated probabilities. + """ + samples = self.get_posterior_samples(posterior) + samples_log_prob = ( + posterior.mvn.log_prob(samples.squeeze(-1)).unsqueeze(-1).unsqueeze(-1) + ) + return samples, samples_log_prob + + def _compute_monte_carlo_information_gain( + self, X: Tensor, return_parts: bool = False + ) -> Tensor: + r"""Evaluates the lower bound information gain at the design points `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of `batch_shape` t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of acquisition values at the given design + points `X`. + """ + initial_posterior = self.initial_model.posterior( + X, observation_noise=True, posterior_transform=self.posterior_transform + ) + + batch_shape = X.shape[:-2] + sample_dim = len(batch_shape) + # We DISREGARD the additional constant term. + initial_entropy = MC_ADD_TERM + 0.5 * torch.logdet( + initial_posterior.mvn.lazy_covariance_matrix + ) + + # initial_entropy of shape batch_size or batch_size x num_models if FBGP + # first need to unsqueeze the sample dim (after batch dim), then the two last + initial_entropy = ( + initial_entropy.unsqueeze(sample_dim).unsqueeze(-1).unsqueeze(-1) + ) + + # Compute the mixture mean and variance + posterior_m = self.conditional_model.posterior( + X.unsqueeze(MCMC_DIM), + observation_noise=True, + posterior_transform=self.posterior_transform, + ) + noiseless_var = self.conditional_model.posterior( + X.unsqueeze(MCMC_DIM), + observation_noise=False, + posterior_transform=self.posterior_transform, + ).variance + + mean_m = posterior_m.mean + variance_m = posterior_m.variance.clamp_min(CLAMP_LB) + conditional_samples, conditional_logprobs = self._compute_monte_carlo_variables( + posterior_m + ) + + normalized_samples = (conditional_samples - mean_m) / variance_m.sqrt() + # Correlation between noisy observations and noiseless values f + rho = (noiseless_var / variance_m).sqrt() + + normal = Normal( + torch.zeros(1, device=X.device, dtype=X.dtype), + torch.ones(1, device=X.device, dtype=X.dtype), + ) + # prepare max value quantities and re-scale as required + normalized_mvs = (self.optimal_outputs - mean_m) / noiseless_var.sqrt() + mvs_rescaled_mc = (normalized_mvs - rho * normalized_samples) / (1 - rho**2) + cdf_mvs = normal.cdf(normalized_mvs).clamp_min(CLAMP_LB) + cdf_rescaled_mvs = normal.cdf(mvs_rescaled_mc).clamp_min(CLAMP_LB) + mv_ratio = cdf_rescaled_mvs / cdf_mvs + + log_term = torch.log(mv_ratio) + conditional_logprobs + conditional_entropy = -(mv_ratio * log_term).mean(0) + entropy_reduction = ( + initial_entropy - conditional_entropy.sum(dim=-2, keepdim=True) + ).squeeze(-1) + + # average over the number of optima and squeeze the q-batch + entropy_reduction = entropy_reduction.mean(dim=sample_dim).squeeze(-1) + + return entropy_reduction
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/knowledge_gradient.html b/website-old/pages/api/_modules/botorch/acquisition/knowledge_gradient.html new file mode 100644 index 0000000000..8d7b6521d2 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/knowledge_gradient.html @@ -0,0 +1,673 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.knowledge_gradient

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Batch Knowledge Gradient (KG) via one-shot optimization as introduced in
+[Balandat2020botorch]_. For broader discussion of KG see also [Frazier2008knowledge]_
+and [Wu2016parallelkg]_.
+
+.. [Balandat2020botorch]
+    M. Balandat, B. Karrer, D. R. Jiang, S. Daulton, B. Letham, A. G. Wilson, and
+    E. Bakshy. BoTorch: A Framework for Efficient Monte-Carlo Bayesian Optimization.
+    Advances in Neural Information Processing Systems 33, 2020.
+
+.. [Frazier2008knowledge]
+    P. Frazier, W. Powell, and S. Dayanik. A Knowledge-Gradient policy for
+    sequential information collection. SIAM Journal on Control and Optimization,
+    2008.
+
+.. [Wu2016parallelkg]
+    J. Wu and P. Frazier. The parallel knowledge gradient method for batch
+    bayesian optimization. NIPS 2016.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from copy import deepcopy
+from typing import Any
+
+import torch
+from botorch import settings
+from botorch.acquisition.acquisition import (
+    AcquisitionFunction,
+    MCSamplerMixin,
+    OneShotAcquisitionFunction,
+)
+from botorch.acquisition.analytic import PosteriorMean
+from botorch.acquisition.cost_aware import CostAwareUtility
+from botorch.acquisition.monte_carlo import MCAcquisitionFunction, qSimpleRegret
+from botorch.acquisition.objective import MCAcquisitionObjective, PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.transforms import (
+    concatenate_pending_points,
+    match_batch_shape,
+    t_batch_mode_transform,
+)
+from torch import Tensor
+
+
+
+[docs] +class qKnowledgeGradient(MCAcquisitionFunction, OneShotAcquisitionFunction): + r"""Batch Knowledge Gradient using one-shot optimization. + + This computes the batch Knowledge Gradient using fantasies for the outer + expectation and either the model posterior mean or MC-sampling for the inner + expectation. + + In addition to the design variables, the input `X` also includes variables + for the optimal designs for each of the fantasy models. For a fixed number + of fantasies, all parts of `X` can be optimized in a "one-shot" fashion. + """ + + def __init__( + self, + model: Model, + num_fantasies: int | None = 64, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + inner_sampler: MCSampler | None = None, + X_pending: Tensor | None = None, + current_value: Tensor | None = None, + ) -> None: + r"""q-Knowledge Gradient (one-shot optimization). + + Args: + model: A fitted model. Must support fantasizing. + num_fantasies: The number of fantasy points to use. More fantasy + points result in a better approximation, at the expense of + memory and wall time. Unused if `sampler` is specified. + sampler: The sampler used to sample fantasy observations. Optional + if `num_fantasies` is specified. + objective: The objective under which the samples are evaluated. If + `None`, then the analytic posterior mean is used. Otherwise, the + objective is MC-evaluated (using inner_sampler). + posterior_transform: An optional PosteriorTransform. If given, this + transforms the posterior before evaluation. If `objective is None`, + then the analytic posterior mean of the transformed posterior is + used. If `objective` is given, the `inner_sampler` is used to draw + samples from the transformed posterior, which are then evaluated under + the `objective`. + inner_sampler: The sampler used for inner sampling. Ignored if the + objective is `None`. + X_pending: A `m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. + current_value: The current value, i.e. the expected best objective + given the observed points `D`. If omitted, forward will not + return the actual KG value, but the expected best objective + given the data set `D u X`. + """ + if sampler is None: + if num_fantasies is None: + raise ValueError( + "Must specify `num_fantasies` if no `sampler` is provided." + ) + # base samples should be fixed for joint optimization over X, X_fantasies + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_fantasies])) + elif num_fantasies is not None: + if sampler.sample_shape != torch.Size([num_fantasies]): + raise ValueError( + f"The sampler shape must match num_fantasies={num_fantasies}." + ) + else: + num_fantasies = sampler.sample_shape[0] + super(MCAcquisitionFunction, self).__init__(model=model) + MCSamplerMixin.__init__(self, sampler=sampler) + # if not explicitly specified, we use the posterior mean for linear objs + if isinstance(objective, MCAcquisitionObjective) and inner_sampler is None: + inner_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([128])) + elif objective is not None and not isinstance( + objective, MCAcquisitionObjective + ): + raise UnsupportedError( + "Objectives that are not an `MCAcquisitionObjective` are not supported." + ) + + if objective is None and model.num_outputs != 1: + if posterior_transform is None: + raise UnsupportedError( + "Must specify an objective or a posterior transform when using " + "a multi-output model." + ) + elif not posterior_transform.scalarize: + raise UnsupportedError( + "If using a multi-output model without an objective, " + "posterior_transform must scalarize the output." + ) + self.objective = objective + self.posterior_transform = posterior_transform + self.set_X_pending(X_pending) + self.X_pending: Tensor = self.X_pending + self.inner_sampler = inner_sampler + self.num_fantasies: int = num_fantasies + self.current_value = current_value + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qKnowledgeGradient on the candidate set `X`. + + Args: + X: A `b x (q + num_fantasies) x d` Tensor with `b` t-batches of + `q + num_fantasies` design points each. We split this X tensor + into two parts in the `q` dimension (`dim=-2`). The first `q` + are the q-batch of design points and the last num_fantasies are + the current solutions of the inner optimization problem. + + `X_fantasies = X[..., -num_fantasies:, :]` + `X_fantasies.shape = b x num_fantasies x d` + + `X_actual = X[..., :-num_fantasies, :]` + `X_actual.shape = b x q x d` + + Returns: + A Tensor of shape `b`. For t-batch b, the q-KG value of the design + `X_actual[b]` is averaged across the fantasy models, where + `X_fantasies[b, i]` is chosen as the final selection for the + `i`-th fantasy model. + NOTE: If `current_value` is not provided, then this is not the + true KG value of `X_actual[b]`, and `X_fantasies[b, : ]` must be + maximized at fixed `X_actual[b]`. + """ + X_actual, X_fantasies = _split_fantasy_points(X=X, n_f=self.num_fantasies) + + # We only concatenate X_pending into the X part after splitting + if self.X_pending is not None: + X_actual = torch.cat( + [X_actual, match_batch_shape(self.X_pending, X_actual)], dim=-2 + ) + + # construct the fantasy model of shape `num_fantasies x b` + fantasy_model = self.model.fantasize( + X=X_actual, + sampler=self.sampler, + ) + + # get the value function + value_function = _get_value_function( + model=fantasy_model, + objective=self.objective, + posterior_transform=self.posterior_transform, + sampler=self.inner_sampler, + ) + + # make sure to propagate gradients to the fantasy model train inputs + with settings.propagate_grads(True): + values = value_function(X=X_fantasies) # num_fantasies x b + + if self.current_value is not None: + values = values - self.current_value + + # return average over the fantasy samples + return values.mean(dim=0)
+ + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def evaluate(self, X: Tensor, bounds: Tensor, **kwargs: Any) -> Tensor: + r"""Evaluate qKnowledgeGradient on the candidate set `X_actual` by + solving the inner optimization problem. + + Args: + X: A `b x q x d` Tensor with `b` t-batches of `q` design points + each. Unlike `forward()`, this does not include solutions of the + inner optimization problem. + bounds: A `2 x d` tensor of lower and upper bounds for each column of + the solutions to the inner problem. + kwargs: Additional keyword arguments. This includes the options for + optimization of the inner problem, i.e. `num_restarts`, `raw_samples`, + an `options` dictionary to be passed on to the optimization helpers, and + a `scipy_options` dictionary to be passed to `scipy.minimize`. + + Returns: + A Tensor of shape `b`. For t-batch b, the q-KG value of the design + `X[b]` is averaged across the fantasy models. + NOTE: If `current_value` is not provided, then this is not the + true KG value of `X[b]`. + """ + if hasattr(self, "expand"): + X = self.expand(X) + + # construct the fantasy model of shape `num_fantasies x b` + fantasy_model = self.model.fantasize( + X=X, + sampler=self.sampler, + ) + + # get the value function + value_function = _get_value_function( + model=fantasy_model, + objective=self.objective, + posterior_transform=self.posterior_transform, + sampler=self.inner_sampler, + project=getattr(self, "project", None), + ) + + from botorch.generation.gen import gen_candidates_scipy + + # optimize the inner problem + from botorch.optim.initializers import gen_value_function_initial_conditions + + initial_conditions = gen_value_function_initial_conditions( + acq_function=value_function, + bounds=bounds, + num_restarts=kwargs.get("num_restarts", 20), + raw_samples=kwargs.get("raw_samples", 1024), + current_model=self.model, + options={**kwargs.get("options", {}), **kwargs.get("scipy_options", {})}, + ) + + _, values = gen_candidates_scipy( + initial_conditions=initial_conditions, + acquisition_function=value_function, + lower_bounds=bounds[0], + upper_bounds=bounds[1], + options=kwargs.get("scipy_options"), + ) + # get the maximizer for each batch + values, _ = torch.max(values, dim=0) + if self.current_value is not None: + values = values - self.current_value + # NOTE: using getattr to cover both no-attribute with qKG and None with qMFKG + if getattr(self, "cost_aware_utility", None) is not None: + values = self.cost_aware_utility( + X=X, deltas=values, sampler=self.cost_sampler + ) + # return average over the fantasy samples + return values.mean(dim=0)
+ + +
+[docs] + def get_augmented_q_batch_size(self, q: int) -> int: + r"""Get augmented q batch size for one-shot optimization. + + Args: + q: The number of candidates to consider jointly. + + Returns: + The augmented size for one-shot optimization (including variables + parameterizing the fantasy solutions). + """ + return q + self.num_fantasies
+ + +
+[docs] + def extract_candidates(self, X_full: Tensor) -> Tensor: + r"""We only return X as the set of candidates post-optimization. + + Args: + X_full: A `b x (q + num_fantasies) x d`-dim Tensor with `b` + t-batches of `q + num_fantasies` design points each. + + Returns: + A `b x q x d`-dim Tensor with `b` t-batches of `q` design points each. + """ + return X_full[..., : -self.num_fantasies, :]
+
+ + + +
+[docs] +class qMultiFidelityKnowledgeGradient(qKnowledgeGradient): + r"""Batch Knowledge Gradient for multi-fidelity optimization. + + A version of `qKnowledgeGradient` that supports multi-fidelity optimization + via a `CostAwareUtility` and the `project` and `expand` operators. If none + of these are set, this acquisition function reduces to `qKnowledgeGradient`. + Through `valfunc_cls` and `valfunc_argfac`, this can be changed into a custom + multi-fidelity acquisition function (it is only KG if the terminal value is + computed using a posterior mean). + """ + + def __init__( + self, + model: Model, + num_fantasies: int | None = 64, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + inner_sampler: MCSampler | None = None, + X_pending: Tensor | None = None, + current_value: Tensor | None = None, + cost_aware_utility: CostAwareUtility | None = None, + project: Callable[[Tensor], Tensor] = lambda X: X, + expand: Callable[[Tensor], Tensor] = lambda X: X, + valfunc_cls: type[AcquisitionFunction] | None = None, + valfunc_argfac: Callable[[Model], dict[str, Any]] | None = None, + ) -> None: + r"""Multi-Fidelity q-Knowledge Gradient (one-shot optimization). + + Args: + model: A fitted model. Must support fantasizing. + num_fantasies: The number of fantasy points to use. More fantasy + points result in a better approximation, at the expense of + memory and wall time. Unused if `sampler` is specified. + sampler: The sampler used to sample fantasy observations. Optional + if `num_fantasies` is specified. + objective: The objective under which the samples are evaluated. If + `None`, then the analytic posterior mean is used. Otherwise, the + objective is MC-evaluated (using inner_sampler). + posterior_transform: An optional PosteriorTransform. If given, this + transforms the posterior before evaluation. If `objective is None`, + then the analytic posterior mean of the transformed posterior is + used. If `objective` is given, the `inner_sampler` is used to draw + samples from the transformed posterior, which are then evaluated under + the `objective`. + inner_sampler: The sampler used for inner sampling. Ignored if the + objective is `None`. + X_pending: A `m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. + current_value: The current value, i.e. the expected best objective + given the observed points `D`. If omitted, forward will not + return the actual KG value, but the expected best objective + given the data set `D u X`. + cost_aware_utility: A CostAwareUtility computing the cost-transformed + utility from a candidate set and samples of increases in utility. + project: A callable mapping a `batch_shape x q x d` tensor of design + points to a tensor with shape `batch_shape x q_term x d` projected + to the desired target set (e.g. the target fidelities in case of + multi-fidelity optimization). For the basic case, `q_term = q`. + expand: A callable mapping a `batch_shape x q x d` input tensor to + a `batch_shape x (q + q_e)' x d`-dim output tensor, where the + `q_e` additional points in each q-batch correspond to + additional ("trace") observations. + valfunc_cls: An acquisition function class to be used as the terminal + value function. + valfunc_argfac: An argument factory, i.e. callable that maps a `Model` + to a dictionary of kwargs for the terminal value function (e.g. + `best_f` for `ExpectedImprovement`). + """ + if current_value is None and cost_aware_utility is not None: + raise UnsupportedError( + "Cost-aware KG requires current_value to be specified." + ) + super().__init__( + model=model, + num_fantasies=num_fantasies, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + inner_sampler=inner_sampler, + X_pending=X_pending, + current_value=current_value, + ) + self.cost_aware_utility = cost_aware_utility + self.project = project + self.expand = expand + self._cost_sampler = None + self.valfunc_cls = valfunc_cls + self.valfunc_argfac = valfunc_argfac + + @property + def cost_sampler(self): + if self._cost_sampler is None: + # Note: Using the deepcopy here is essential. Removing this poses a + # problem if the base model and the cost model have a different number + # of outputs or test points (this would be caused by expand), as this + # would trigger re-sampling the base samples in the fantasy sampler. + # By cloning the sampler here, the right thing will happen if the + # the sizes are compatible, if they are not this will result in + # samples being drawn using different base samples, but it will at + # least avoid changing state of the fantasy sampler. + self._cost_sampler = deepcopy(self.sampler) + return self._cost_sampler + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qMultiFidelityKnowledgeGradient on the candidate set `X`. + + Args: + X: A `b x (q + num_fantasies) x d` Tensor with `b` t-batches of + `q + num_fantasies` design points each. We split this X tensor + into two parts in the `q` dimension (`dim=-2`). The first `q` + are the q-batch of design points and the last num_fantasies are + the current solutions of the inner optimization problem. + + `X_fantasies = X[..., -num_fantasies:, :]` + `X_fantasies.shape = b x num_fantasies x d` + + `X_actual = X[..., :-num_fantasies, :]` + `X_actual.shape = b x q x d` + + In addition, `X` may be augmented with fidelity parameters as + part of thee `d`-dimension. Projecting fidelities to the target + fidelity is handled by `project`. + + Returns: + A Tensor of shape `b`. For t-batch b, the q-KG value of the design + `X_actual[b]` is averaged across the fantasy models, where + `X_fantasies[b, i]` is chosen as the final selection for the + `i`-th fantasy model. + NOTE: If `current_value` is not provided, then this is not the + true KG value of `X_actual[b]`, and `X_fantasies[b, : ]` must be + maximized at fixed `X_actual[b]`. + """ + X_actual, X_fantasies = _split_fantasy_points(X=X, n_f=self.num_fantasies) + + # We only concatenate X_pending into the X part after splitting + if self.X_pending is not None: + X_eval = torch.cat( + [X_actual, match_batch_shape(self.X_pending, X_actual)], dim=-2 + ) + else: + X_eval = X_actual + + # construct the fantasy model of shape `num_fantasies x b` + # expand X (to potentially add trace observations) + fantasy_model = self.model.fantasize( + X=self.expand(X_eval), + sampler=self.sampler, + ) + # get the value function + value_function = _get_value_function( + model=fantasy_model, + objective=self.objective, + posterior_transform=self.posterior_transform, + sampler=self.inner_sampler, + project=self.project, + valfunc_cls=self.valfunc_cls, + valfunc_argfac=self.valfunc_argfac, + ) + + # make sure to propagate gradients to the fantasy model train inputs + # project the fantasy points + with settings.propagate_grads(True): + values = value_function(X=X_fantasies) # num_fantasies x b + + if self.current_value is not None: + values = values - self.current_value + + if self.cost_aware_utility is not None: + values = self.cost_aware_utility( + X=X_actual, deltas=values, sampler=self.cost_sampler + ) + + # return average over the fantasy samples + return values.mean(dim=0)
+
+ + + +
+[docs] +class ProjectedAcquisitionFunction(AcquisitionFunction): + r""" + Defines a wrapper around an `AcquisitionFunction` that incorporates the project + operator. Typically used to handle value functions in look-ahead methods. + """ + + def __init__( + self, + base_value_function: AcquisitionFunction, + project: Callable[[Tensor], Tensor], + ) -> None: + r""" + Args: + base_value_function: The wrapped `AcquisitionFunction`. + project: A callable mapping a `batch_shape x q x d` tensor of design + points to a tensor with shape `batch_shape x q_term x d` projected + to the desired target set (e.g. the target fidelities in case of + multi-fidelity optimization). For the basic case, `q_term = q`. + """ + super().__init__(base_value_function.model) + self.base_value_function = base_value_function + self.project = project + self.objective = getattr(base_value_function, "objective", None) + self.posterior_transform = base_value_function.posterior_transform + self.sampler = getattr(base_value_function, "sampler", None) + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + return self.base_value_function(self.project(X))
+
+ + + +def _get_value_function( + model: Model, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + sampler: MCSampler | None = None, + project: Callable[[Tensor], Tensor] | None = None, + valfunc_cls: type[AcquisitionFunction] | None = None, + valfunc_argfac: Callable[[Model], dict[str, Any]] | None = None, +) -> AcquisitionFunction: + r"""Construct value function (i.e. inner acquisition function).""" + if valfunc_cls is not None: + common_kwargs: dict[str, Any] = { + "model": model, + "posterior_transform": posterior_transform, + } + if issubclass(valfunc_cls, MCAcquisitionFunction): + common_kwargs["sampler"] = sampler + common_kwargs["objective"] = objective + kwargs = valfunc_argfac(model=model) if valfunc_argfac is not None else {} + base_value_function = valfunc_cls(**common_kwargs, **kwargs) + else: + if objective is not None: + base_value_function = qSimpleRegret( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + ) + else: + base_value_function = PosteriorMean( + model=model, posterior_transform=posterior_transform + ) + + if project is None: + return base_value_function + else: + return ProjectedAcquisitionFunction( + base_value_function=base_value_function, + project=project, + ) + + +def _split_fantasy_points(X: Tensor, n_f: int) -> tuple[Tensor, Tensor]: + r"""Split a one-shot optimization input into actual and fantasy points + + Args: + X: A `batch_shape x (q + n_f) x d`-dim tensor of actual and fantasy + points + + Returns: + 2-element tuple containing + + - A `batch_shape x q x d`-dim tensor `X_actual` of input candidates. + - A `n_f x batch_shape x 1 x d`-dim tensor `X_fantasies` of fantasy + points, where `X_fantasies[i, batch_idx]` is the i-th fantasy point + associated with the batch indexed by `batch_idx`. + """ + if n_f > X.size(-2): + raise ValueError( + f"n_f ({n_f}) must be less than the q-batch dimension of X ({X.size(-2)})" + ) + split_sizes = [X.size(-2) - n_f, n_f] + X_actual, X_fantasies = torch.split(X, split_sizes, dim=-2) + # X_fantasies is b x num_fantasies x d, needs to be num_fantasies x b x 1 x d + # for batch mode evaluation with batch shape num_fantasies x b. + # b x num_fantasies x d --> num_fantasies x b x d + X_fantasies = X_fantasies.permute(-2, *range(X_fantasies.dim() - 2), -1) + # num_fantasies x b x 1 x d + X_fantasies = X_fantasies.unsqueeze(dim=-2) + return X_actual, X_fantasies +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/logei.html b/website-old/pages/api/_modules/botorch/acquisition/logei.html new file mode 100644 index 0000000000..dd663c0f70 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/logei.html @@ -0,0 +1,619 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.logei

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Monte-Carlo variants of the LogEI family of improvements-based acquisition functions,
+see [Ament2023logei]_ for details.
+
+References
+
+.. [Ament2023logei]
+    S. Ament, S. Daulton, D. Eriksson, M. Balandat, and E. Bakshy.
+    Unexpected Improvements to Expected Improvement for Bayesian Optimization. Advances
+    in Neural Information Processing Systems 36, 2023.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from copy import deepcopy
+
+from functools import partial
+
+from typing import TypeVar
+
+import torch
+from botorch.acquisition.cached_cholesky import CachedCholeskyMCSamplerMixin
+from botorch.acquisition.monte_carlo import SampleReducingMCAcquisitionFunction
+from botorch.acquisition.objective import (
+    ConstrainedMCObjective,
+    MCAcquisitionObjective,
+    PosteriorTransform,
+)
+from botorch.acquisition.utils import (
+    compute_best_feasible_objective,
+    prune_inferior_points,
+)
+from botorch.exceptions.errors import BotorchError
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.safe_math import (
+    fatmax,
+    log_fatplus,
+    log_softplus,
+    logmeanexp,
+    smooth_amax,
+)
+from botorch.utils.transforms import match_batch_shape
+from torch import Tensor
+
+"""
+NOTE: On the default temperature parameters:
+
+tau_relu: It is generally important to set `tau_relu` to be very small, in particular,
+smaller than the expected improvement value. Otherwise, the optimization can stagnate.
+By setting `tau_relu=1e-6` by default, stagnation is exceedingly unlikely to occur due
+to the smooth ReLU approximation for practical applications of BO.
+IDEA: We could consider shrinking `tau_relu` with the progression of the optimization.
+
+tau_max: This is only relevant for the batch (`q > 1`) case, and `tau_max=1e-2` is
+sufficient to get a good approximation to the maximum improvement in the batch of
+candidates. If `fat=False`, the smooth approximation to the maximum can saturate
+numerically. It is therefore recommended to use `fat=True` when optimizing batches
+of `q > 1` points.
+"""
+TAU_RELU = 1e-6
+TAU_MAX = 1e-2
+FloatOrTensor = TypeVar("FloatOrTensor", float, Tensor)
+
+
+
+[docs] +class LogImprovementMCAcquisitionFunction(SampleReducingMCAcquisitionFunction): + r""" + Abstract base class for Monte-Carlo-based batch LogEI acquisition functions. + """ + + _log: bool = True + + def __init__( + self, + model: Model, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + fat: bool = True, + tau_max: float = TAU_MAX, + ) -> None: + r""" + Args: + model: A fitted model. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + NOTE: For posteriors that do not support base samples, + a sampler compatible with intended use case must be provided. + See `ForkedRNGSampler` and `StochasticSampler` as examples. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `batch_shape, m x d`-dim Tensor of `m` design points + that have points that have been submitted for function evaluation + but have not yet been evaluated. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are satisfied if `constraint(samples) < 0`. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. See the docs of + `compute_(log_)constraint_indicator` for more details on this parameter. + fat: Toggles the logarithmic / linear asymptotic behavior of the smooth + approximation to the ReLU. + tau_max: Temperature parameter controlling the sharpness of the + approximation to the `max` operator over the `q` candidate points. + """ + if isinstance(objective, ConstrainedMCObjective): + raise BotorchError( + "Log-Improvement should not be used with `ConstrainedMCObjective`." + "Please pass the `constraints` directly to the constructor of the " + "acquisition function." + ) + q_reduction = partial(fatmax if fat else smooth_amax, tau=tau_max) + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + sample_reduction=logmeanexp, + q_reduction=q_reduction, + constraints=constraints, + eta=eta, + fat=fat, + ) + self.tau_max = tau_max
+ + + +
+[docs] +class qLogExpectedImprovement(LogImprovementMCAcquisitionFunction): + r"""MC-based batch Log Expected Improvement. + + This computes qLogEI by + (1) sampling the joint posterior over q points, + (2) evaluating the smoothed log improvement over the current best for each sample, + (3) smoothly maximizing over q, and + (4) averaging over the samples in log space. + + See [Ament2023logei]_ for details. Formally, + + `qLogEI(X) ~ log(qEI(X)) = log(E(max(max Y - best_f, 0)))`. + + where `Y ~ f(X)`, and `X = (x_1,...,x_q)`, . + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> best_f = train_Y.max()[0] + >>> sampler = SobolQMCNormalSampler(1024) + >>> qLogEI = qLogExpectedImprovement(model, best_f, sampler) + >>> qei = qLogEI(test_X) + """ + + def __init__( + self, + model: Model, + best_f: float | Tensor, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + fat: bool = True, + tau_max: float = TAU_MAX, + tau_relu: float = TAU_RELU, + ) -> None: + r"""q-Log Expected Improvement. + + Args: + model: A fitted model. + best_f: The best objective value observed so far (assumed noiseless). Can be + a scalar, or a `batch_shape`-dim tensor. In case of a batched model, the + tensor can specify different values for each element of the batch. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are evaluated. + Defaults to `IdentityMCObjective()`. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into `X` upon forward call. Copied and set to have no + gradient. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are satisfied if `constraint(samples) < 0`. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. See the docs of + `compute_(log_)smoothed_constraint_indicator` for details. + fat: Toggles the logarithmic / linear asymptotic behavior of the smooth + approximation to the ReLU. + tau_max: Temperature parameter controlling the sharpness of the smooth + approximations to max. + tau_relu: Temperature parameter controlling the sharpness of the smooth + approximations to ReLU. + """ + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + constraints=constraints, + eta=eta, + tau_max=check_tau(tau_max, name="tau_max"), + fat=fat, + ) + self.register_buffer("best_f", torch.as_tensor(best_f, dtype=float)) + self.tau_relu = check_tau(tau_relu, name="tau_relu") + + def _sample_forward(self, obj: Tensor) -> Tensor: + r"""Evaluate qLogExpectedImprovement on the candidate set `X`. + + Args: + obj: `mc_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `mc_shape x batch_shape x q`-dim Tensor of expected improvement values. + """ + li = _log_improvement( + Y=obj, + best_f=self.best_f, + tau=self.tau_relu, + fat=self._fat, + ) + return li
+ + + +
+[docs] +class qLogNoisyExpectedImprovement( + LogImprovementMCAcquisitionFunction, CachedCholeskyMCSamplerMixin +): + r"""MC-based batch Log Noisy Expected Improvement. + + This function does not assume a `best_f` is known (which would require + noiseless observations). Instead, it uses samples from the joint posterior + over the `q` test points and previously observed points. A smooth approximation + to the canonical improvement over previously observed points is computed + for each sample and the logarithm of the average is returned. + + See [Ament2023logei]_ for details. Formally, + + `qLogNEI(X) ~ log(qNEI(X)) = Log E(max(max Y - max Y_baseline, 0))`, + + where `(Y, Y_baseline) ~ f((X, X_baseline)), X = (x_1,...,x_q)`. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> sampler = SobolQMCNormalSampler(1024) + >>> qLogNEI = qLogNoisyExpectedImprovement(model, train_X, sampler) + >>> acqval = qLogNEI(test_X) + """ + + def __init__( + self, + model: Model, + X_baseline: Tensor, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + fat: bool = True, + prune_baseline: bool = False, + cache_root: bool = True, + tau_max: float = TAU_MAX, + tau_relu: float = TAU_RELU, + marginalize_dim: int | None = None, + ) -> None: + r"""q-Noisy Expected Improvement. + + Args: + model: A fitted model. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are considered as + the potential best design point. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points + that have points that have been submitted for function evaluation + but have not yet been evaluated. Concatenated into `X` upon + forward call. Copied and set to have no gradient. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are satisfied if `constraint(samples) < 0`. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. See the docs of + `compute_(log_)smoothed_constraint_indicator` for details. + fat: Toggles the logarithmic / linear asymptotic behavior of the smooth + approximation to the ReLU. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the best point. This can significantly + improve performance and is generally recommended. In order to + customize pruning parameters, instead manually call + `botorch.acquisition.utils.prune_inferior_points` on `X_baseline` + before instantiating the acquisition function. + cache_root: A boolean indicating whether to cache the root + decomposition over `X_baseline` and use low-rank updates. + tau_max: Temperature parameter controlling the sharpness of the smooth + approximations to max. + tau_relu: Temperature parameter controlling the sharpness of the smooth + approximations to ReLU. + marginalize_dim: The dimension to marginalize over. + + TODO: similar to qNEHVI, when we are using sequential greedy candidate + selection, we could incorporate pending points X_baseline and compute + the incremental q(Log)NEI from the new point. This would greatly increase + efficiency for large batches. + """ + # TODO: separate out baseline variables initialization and other functions + # in qNEI to avoid duplication of both code and work at runtime. + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + constraints=constraints, + eta=eta, + fat=fat, + tau_max=tau_max, + ) + self.tau_relu = tau_relu + self._init_baseline( + model=model, + X_baseline=X_baseline, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + prune_baseline=prune_baseline, + cache_root=cache_root, + marginalize_dim=marginalize_dim, + ) + + def _sample_forward(self, obj: Tensor) -> Tensor: + r"""Evaluate qLogNoisyExpectedImprovement per sample on the candidate set `X`. + + Args: + obj: `mc_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of log noisy expected smoothed + improvement values. + """ + return _log_improvement( + Y=obj, + best_f=self.compute_best_f(obj), + tau=self.tau_relu, + fat=self._fat, + ) + + def _init_baseline( + self, + model: Model, + X_baseline: Tensor, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + prune_baseline: bool = False, + cache_root: bool = True, + marginalize_dim: int | None = None, + ) -> None: + CachedCholeskyMCSamplerMixin.__init__( + self, model=model, cache_root=cache_root, sampler=sampler + ) + if prune_baseline: + X_baseline = prune_inferior_points( + model=model, + X=X_baseline, + objective=objective, + posterior_transform=posterior_transform, + marginalize_dim=marginalize_dim, + constraints=self._constraints, + ) + self.register_buffer("X_baseline", X_baseline) + # registering buffers for _get_samples_and_objectives in the next `if` block + self.register_buffer("baseline_samples", None) + self.register_buffer("baseline_obj", None) + if self._cache_root: + self.q_in = -1 + # set baseline samples + with torch.no_grad(): # this is _get_samples_and_objectives(X_baseline) + posterior = self.model.posterior( + X_baseline, posterior_transform=self.posterior_transform + ) + # Note: The root decomposition is cached in two different places. It + # may be confusing to have two different caches, but this is not + # trivial to change since each is needed for a different reason: + # - LinearOperator caching to `posterior.mvn` allows for reuse within + # this function, which may be helpful if the same root decomposition + # is produced by the calls to `self.base_sampler` and + # `self._cache_root_decomposition`. + # - self._baseline_L allows a root decomposition to be persisted outside + # this method. + self.baseline_samples = self.get_posterior_samples(posterior) + self.baseline_obj = self.objective(self.baseline_samples, X=X_baseline) + + # We make a copy here because we will write an attribute `base_samples` + # to `self.base_sampler.base_samples`, and we don't want to mutate + # `self.sampler`. + self.base_sampler = deepcopy(self.sampler) + self.register_buffer( + "_baseline_best_f", + self._compute_best_feasible_objective( + samples=self.baseline_samples, obj=self.baseline_obj + ), + ) + self._baseline_L = self._compute_root_decomposition(posterior=posterior) + +
+[docs] + def compute_best_f(self, obj: Tensor) -> Tensor: + """Computes the best (feasible) noisy objective value. + + Args: + obj: `sample_shape x batch_shape x q`-dim Tensor of objectives in forward. + + Returns: + A `sample_shape x batch_shape`-dim Tensor of best feasible objectives. + """ + if self._cache_root: + val = self._baseline_best_f + else: + val = self._compute_best_feasible_objective( + samples=self.baseline_samples, obj=self.baseline_obj + ) + # ensuring shape, dtype, device compatibility with obj + n_sample_dims = len(self.sample_shape) + view_shape = torch.Size( + [ + *val.shape[:n_sample_dims], # sample dimensions + *(1,) * (obj.ndim - val.ndim - 1), # pad to match obj without `q`-dim + *val.shape[n_sample_dims:], # the rest + ] + ) + return val.view(view_shape).to(obj) # obj.shape[:-1], i.e. without `q`-dim`
+ + + def _get_samples_and_objectives(self, X: Tensor) -> tuple[Tensor, Tensor]: + r"""Compute samples at new points, using the cached root decomposition. + + Args: + X: A `batch_shape x q x d`-dim tensor of inputs. + + Returns: + A two-tuple `(samples, obj)`, where `samples` is a tensor of posterior + samples with shape `sample_shape x batch_shape x q x m`, and `obj` is a + tensor of MC objective values with shape `sample_shape x batch_shape x q`. + """ + n_baseline, q = self.X_baseline.shape[-2], X.shape[-2] + X_full = torch.cat([match_batch_shape(self.X_baseline, X), X], dim=-2) + # TODO: Implement more efficient way to compute posterior over both training and + # test points in GPyTorch (https://github.com/cornellius-gp/gpytorch/issues/567) + posterior = self.model.posterior( + X_full, posterior_transform=self.posterior_transform + ) + if not self._cache_root: + samples_full = super().get_posterior_samples(posterior) + obj_full = self.objective(samples_full, X=X_full) + # assigning baseline buffers so `best_f` can be computed in _sample_forward + self.baseline_samples, samples = samples_full.split([n_baseline, q], dim=-2) + self.baseline_obj, obj = obj_full.split([n_baseline, q], dim=-1) + return samples, obj + + # handle one-to-many input transforms + n_plus_q = X_full.shape[-2] + n_w = posterior._extended_shape()[-2] // n_plus_q + q_in = q * n_w + self._set_sampler(q_in=q_in, posterior=posterior) + samples = self._get_f_X_samples(posterior=posterior, q_in=q_in) + obj = self.objective(samples, X=X_full[..., -q:, :]) + return samples, obj + + def _compute_best_feasible_objective(self, samples: Tensor, obj: Tensor) -> Tensor: + r"""Computes best feasible objective value from samples. + + Args: + samples: `sample_shape x batch_shape x q x m`-dim posterior samples. + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape`-dim Tensor of best feasible objectives. + """ + return compute_best_feasible_objective( + samples=samples, + obj=obj, + constraints=self._constraints, + model=self.model, + objective=self.objective, + posterior_transform=self.posterior_transform, + X_baseline=self.X_baseline, + )
+ + + +""" +###################################### utils ########################################## +""" + + +def _log_improvement( + Y: Tensor, + best_f: Tensor, + tau: float | Tensor, + fat: bool, +) -> Tensor: + """Computes the logarithm of the softplus-smoothed improvement, i.e. + `log_softplus(Y - best_f, beta=(1 / tau))`. + Note that softplus is an approximation to the regular ReLU objective whose maximum + pointwise approximation error is linear with respect to tau as tau goes to zero. + + Args: + obj: `mc_samples x batch_shape x q`-dim Tensor of output samples. + best_f: Best previously observed objective value(s), broadcastable with + `mc_samples x batch_shape`-dim Tensor, i.e. `obj`'s dims without `q`. + tau: Temperature parameter for smooth approximation of ReLU. + as `tau -> 0`, maximum pointwise approximation error is linear w.r.t. `tau`. + fat: Toggles the logarithmic / linear asymptotic behavior of the + smooth approximation to ReLU. + + Returns: + A `mc_samples x batch_shape x q`-dim Tensor of improvement values. + """ + log_soft_clamp = log_fatplus if fat else log_softplus + Z = Y - best_f.unsqueeze(-1).to(Y) + return log_soft_clamp(Z, tau=tau) # ~ ((Y - best_f) / Y_std).clamp(0) + + +
+[docs] +def check_tau(tau: FloatOrTensor, name: str) -> FloatOrTensor: + """Checks the validity of the tau arguments of the functions below, and returns + `tau` if it is valid.""" + if isinstance(tau, Tensor) and tau.numel() != 1: + raise ValueError(f"{name} is not a scalar: {tau.numel()=}.") + if not (tau > 0): + raise ValueError(f"{name} is non-positive: {tau=}.") + return tau
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/max_value_entropy_search.html b/website-old/pages/api/_modules/botorch/acquisition/max_value_entropy_search.html new file mode 100644 index 0000000000..35115b91ca --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/max_value_entropy_search.html @@ -0,0 +1,1110 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.max_value_entropy_search

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Acquisition functions for Max-value Entropy Search (MES), General
+Information-Based Bayesian Optimization (GIBBON), and
+multi-fidelity MES with noisy observations and trace observations.
+
+References
+
+.. [Moss2021gibbon]
+    Moss, H. B., et al.,
+    GIBBON: General-purpose Information-Based Bayesian OptimisatioN.
+    Journal of Machine Learning Research, 2021.
+
+.. [Takeno2020mfmves]
+    S. Takeno, H. Fukuoka, Y. Tsukada, T. Koyama, M. Shiga, I. Takeuchi,
+    M. Karasuyama. Multi-fidelity Bayesian Optimization with Max-value Entropy
+    Search and its Parallelization. Proceedings of the 37th International
+    Conference on Machine Learning, 2020.
+
+.. [Wang2017mves]
+    Z. Wang, S. Jegelka, Max-value Entropy Search for Efficient
+    Bayesian Optimization. Proceedings of the 37th International
+    Conference on Machine Learning, 2017.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from copy import deepcopy
+from math import log
+
+import numpy as np
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
+from botorch.acquisition.cost_aware import CostAwareUtility, InverseCostWeightedUtility
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.cost import AffineFidelityCostModel
+from botorch.models.model import Model
+from botorch.models.utils import check_no_nans
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.transforms import match_batch_shape, t_batch_mode_transform
+
+from linear_operator.functions import inv_quad
+from linear_operator.utils.cholesky import psd_safe_cholesky
+from scipy.optimize import brentq
+from scipy.stats import norm
+from torch import Tensor
+
+
+CLAMP_LB = 1.0e-8
+
+
+
+[docs] +class MaxValueBase(AcquisitionFunction, ABC): + r"""Abstract base class for acquisition functions based on Max-value Entropy Search. + + This class provides the basic building blocks for constructing max-value + entropy-based acquisition functions along the lines of [Wang2017mves]_. + + Subclasses need to implement `_sample_max_values` and _compute_information_gain` + methods. + """ + + def __init__( + self, + model: Model, + num_mv_samples: int, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, + X_pending: Tensor | None = None, + ) -> None: + r"""Single-outcome max-value entropy search-based acquisition functions. + + Args: + model: A fitted single-outcome model. + num_mv_samples: Number of max value samples. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + """ + super().__init__(model=model) + + if posterior_transform is None and model.num_outputs != 1: + raise UnsupportedError( + "Must specify a posterior transform when using a multi-output model." + ) + + # Batched GP models are not currently supported + try: + batch_shape = model.batch_shape + except NotImplementedError: + batch_shape = torch.Size() + if len(batch_shape) > 0: + raise NotImplementedError( + "Batched GP models (e.g., fantasized models) are not yet " + f"supported by `{self.__class__.__name__}`." + ) + self.num_mv_samples = num_mv_samples + self.posterior_transform = posterior_transform + self.maximize = maximize + self.weight = 1.0 if maximize else -1.0 + self.set_X_pending(X_pending) + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Compute max-value entropy at the design points `X`. + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of `batch_shape` t-batches + with `1` `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of MVE values at the given design points `X`. + """ + # Compute the posterior, posterior mean, variance and std. + posterior = self.model.posterior( + X.unsqueeze(-3), + observation_noise=False, + posterior_transform=self.posterior_transform, + ) + # batch_shape x num_fantasies x (m) + mean = self.weight * posterior.mean.squeeze(-1).squeeze(-1) + variance = posterior.variance.clamp_min(CLAMP_LB).view_as(mean) + ig = self._compute_information_gain( + X=X, mean_M=mean, variance_M=variance, covar_mM=variance.unsqueeze(-1) + ) + # Average over fantasies, ig is of shape `num_fantasies x batch_shape x (m)`. + return ig.mean(dim=0)
+ + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + r"""Set pending design points. + + Set "pending points" to inform the acquisition function of the candidate + points that have been generated but are pending evaluation. + + Args: + X_pending: `n x d` Tensor with `n` `d`-dim design points that have + been submitted for evaluation but have not yet been evaluated. + """ + if X_pending is not None: + X_pending = X_pending.detach().clone() + self._sample_max_values(num_samples=self.num_mv_samples, X_pending=X_pending) + self.X_pending = X_pending
+ + + # ------- Abstract methods that need to be implemented by subclasses ------- # + + @abstractmethod + def _compute_information_gain(self, X: Tensor) -> Tensor: + r"""Compute the information gain at the design points `X`. + + `num_fantasies = 1` for non-fantasized models. + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of `batch_shape` t-batches + with `1` `d`-dim design point each. + + Returns: + A `num_fantasies x batch_shape`-dim Tensor of information gains at the + given design points `X` (`num_fantasies=1` for non-fantasized models). + """ + pass # pragma: no cover + + @abstractmethod + def _sample_max_values( + self, num_samples: int, X_pending: Tensor | None = None + ) -> None: + r"""Draw samples from the posterior over maximum values. + + These samples are used to compute Monte Carlo approximations of expectations + over the posterior over the function maximum. This function sets + `self.posterior_max_values`. + + Args: + num_samples: The number of samples to draw. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + + Returns: + A `num_samples x num_fantasies` Tensor of posterior max value samples + (`num_fantasies=1` for non-fantasized models). + """ + pass # pragma: no cover
+ + + +
+[docs] +class DiscreteMaxValueBase(MaxValueBase): + r"""Abstract base class for MES-like methods using discrete max posterior sampling. + + This class provides basic functionality for sampling posterior maximum values from + a surrogate Gaussian process model using a discrete set of candidates. It supports + either exact (w.r.t. the candidate set) sampling, or using a Gumbel approximation. + """ + + def __init__( + self, + model: Model, + candidate_set: Tensor, + num_mv_samples: int = 10, + posterior_transform: PosteriorTransform | None = None, + use_gumbel: bool = True, + maximize: bool = True, + X_pending: Tensor | None = None, + train_inputs: Tensor | None = None, + ) -> None: + r"""Single-outcome MES-like acquisition functions based on discrete MV sampling. + + Args: + model: A fitted single-outcome model. + candidate_set: A `n x d` Tensor including `n` candidate points to + discretize the design space. Max values are sampled from the + (joint) model posterior over these points. + num_mv_samples: Number of max value samples. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + use_gumbel: If True, use Gumbel approximation to sample the max values. + maximize: If True, consider the problem a maximization problem. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + train_inputs: A `n_train x d` Tensor that the model has been fitted on. + Not required if the model is an instance of a GPyTorch ExactGP model. + """ + self.use_gumbel = use_gumbel + + if train_inputs is None and hasattr(model, "train_inputs"): + train_inputs = model.train_inputs[0] + if train_inputs is not None: + if train_inputs.ndim > 2: + raise NotImplementedError( + "Batch GP models (e.g. fantasized models) " + "are not yet supported by `MaxValueBase`" + ) + train_inputs = match_batch_shape(train_inputs, candidate_set) + candidate_set = torch.cat([candidate_set, train_inputs], dim=0) + + self.candidate_set = candidate_set + + super().__init__( + model=model, + num_mv_samples=num_mv_samples, + posterior_transform=posterior_transform, + maximize=maximize, + X_pending=X_pending, + ) + + def _sample_max_values( + self, num_samples: int, X_pending: Tensor | None = None + ) -> None: + r"""Draw samples from the posterior over maximum values on a discrete set. + + These samples are used to compute Monte Carlo approximations of expectations + over the posterior over the function maximum. This function sets + `self.posterior_max_values`. + + Args: + num_samples: The number of samples to draw. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + + Returns: + A `num_samples x num_fantasies` Tensor of posterior max value samples + (`num_fantasies=1` for non-fantasized models). + """ + if self.use_gumbel: + sample_max_values = _sample_max_value_Gumbel + else: + sample_max_values = _sample_max_value_Thompson + candidate_set = self.candidate_set + + with torch.no_grad(): + if X_pending is not None: + # Append X_pending to candidate set + X_pending = match_batch_shape(X_pending, self.candidate_set) + candidate_set = torch.cat([self.candidate_set, X_pending], dim=0) + + # project the candidate_set to the highest fidelity, + # which is needed for the multi-fidelity MES + try: + candidate_set = self.project(candidate_set) + except AttributeError: + pass + + self.posterior_max_values = sample_max_values( + model=self.model, + candidate_set=candidate_set, + num_samples=self.num_mv_samples, + posterior_transform=self.posterior_transform, + maximize=self.maximize, + )
+ + + +
+[docs] +class qMaxValueEntropy(DiscreteMaxValueBase, MCSamplerMixin): + r"""The acquisition function for Max-value Entropy Search. + + This acquisition function computes the mutual information of max values and + a candidate point X. See [Wang2017mves]_ for a detailed discussion. + + The model must be single-outcome. The batch case `q > 1` is supported + through cyclic optimization and fantasies. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> candidate_set = torch.rand(1000, bounds.size(1)) + >>> candidate_set = bounds[0] + (bounds[1] - bounds[0]) * candidate_set + >>> MES = qMaxValueEntropy(model, candidate_set) + >>> mes = MES(test_X) + """ + + def __init__( + self, + model: Model, + candidate_set: Tensor, + num_fantasies: int = 16, + num_mv_samples: int = 10, + num_y_samples: int = 128, + posterior_transform: PosteriorTransform | None = None, + use_gumbel: bool = True, + maximize: bool = True, + X_pending: Tensor | None = None, + train_inputs: Tensor | None = None, + ) -> None: + r"""Single-outcome max-value entropy search acquisition function. + + Args: + model: A fitted single-outcome model. + candidate_set: A `n x d` Tensor including `n` candidate points to + discretize the design space. Max values are sampled from the + (joint) model posterior over these points. + num_fantasies: Number of fantasies to generate. The higher this + number the more accurate the model (at the expense of model + complexity, wall time and memory). Ignored if `X_pending` is `None`. + num_mv_samples: Number of max value samples. + num_y_samples: Number of posterior samples at specific design point `X`. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + use_gumbel: If True, use Gumbel approximation to sample the max values. + maximize: If True, consider the problem a maximization problem. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + train_inputs: A `n_train x d` Tensor that the model has been fitted on. + Not required if the model is an instance of a GPyTorch ExactGP model. + """ + super().__init__( + model=model, + candidate_set=candidate_set, + num_mv_samples=num_mv_samples, + posterior_transform=posterior_transform, + use_gumbel=use_gumbel, + maximize=maximize, + X_pending=X_pending, + train_inputs=train_inputs, + ) + MCSamplerMixin.__init__( + self, + sampler=SobolQMCNormalSampler(sample_shape=torch.Size([num_y_samples])), + ) + self._init_model = model # used for `fantasize()` when setting `X_pending` + self.fantasies_sampler = SobolQMCNormalSampler( + sample_shape=torch.Size([num_fantasies]) + ) + self.num_fantasies = num_fantasies + self.set_X_pending(X_pending) # this did not happen in the super constructor + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + r"""Set pending points. + + Informs the acquisition function about pending design points, + fantasizes the model on the pending points and draws max-value samples + from the fantasized model posterior. + + Args: + X_pending: `m x d` Tensor with `m` `d`-dim design points that have + been submitted for evaluation but have not yet been evaluated. + """ + try: + init_model = self._init_model + except AttributeError: + # Short-circuit (this allows calling the super constructor) + return + if X_pending is not None: + # fantasize the model and use this as the new model + self.model = init_model.fantasize( + X=X_pending, + sampler=self.fantasies_sampler, + ) + else: + self.model = init_model + super().set_X_pending(X_pending)
+ + + def _compute_information_gain( + self, X: Tensor, mean_M: Tensor, variance_M: Tensor, covar_mM: Tensor + ) -> Tensor: + r"""Computes the information gain at the design points `X`. + + Approximately computes the information gain at the design points `X`, + for both MES with noisy observations and multi-fidelity MES with noisy + observation and trace observations. + + The implementation is inspired from the papers on multi-fidelity MES by + [Takeno2020mfmves]_. The notation in the comments in this function follows + the Appendix C of [Takeno2020mfmves]_. + + `num_fantasies = 1` for non-fantasized models. + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of `batch_shape` t-batches + with `1` `d`-dim design point each. + mean_M: A `batch_shape x num_fantasies x (m)`-dim Tensor of means. + variance_M: A `batch_shape x num_fantasies x (m)`-dim Tensor of variances. + covar_mM: A + `batch_shape x num_fantasies x (m) x (1 + num_trace_observations)`-dim + Tensor of covariances. + + Returns: + A `num_fantasies x batch_shape`-dim Tensor of information gains at the + given design points `X` (`num_fantasies=1` for non-fantasized models). + """ + # compute the std_m, variance_m with noisy observation + posterior_m = self.model.posterior( + X.unsqueeze(-3), + observation_noise=True, + posterior_transform=self.posterior_transform, + ) + # batch_shape x num_fantasies x (m) x (1 + num_trace_observations) + mean_m = self.weight * posterior_m.mean.squeeze(-1) + # batch_shape x num_fantasies x (m) x (1 + num_trace_observations) + variance_m = posterior_m.distribution.covariance_matrix + check_no_nans(variance_m) + + # compute mean and std for fM|ym, x, Dt ~ N(u, s^2) + samples_m = self.weight * self.get_posterior_samples(posterior_m).squeeze(-1) + # s_m x batch_shape x num_fantasies x (m) (1 + num_trace_observations) + L = psd_safe_cholesky(variance_m) + temp_term = torch.cholesky_solve(covar_mM.unsqueeze(-1), L).transpose(-2, -1) + # equivalent to torch.matmul(covar_mM.unsqueeze(-2), torch.inverse(variance_m)) + # batch_shape x num_fantasies (m) x 1 x (1 + num_trace_observations) + + mean_pt1 = torch.matmul(temp_term, (samples_m - mean_m).unsqueeze(-1)) + mean_new = mean_pt1.squeeze(-1).squeeze(-1) + mean_M + # s_m x batch_shape x num_fantasies x (m) + variance_pt1 = torch.matmul(temp_term, covar_mM.unsqueeze(-1)) + variance_new = variance_M - variance_pt1.squeeze(-1).squeeze(-1) + # batch_shape x num_fantasies x (m) + stdv_new = variance_new.clamp_min(CLAMP_LB).sqrt() + # batch_shape x num_fantasies x (m) + + # define normal distribution to compute cdf and pdf + normal = torch.distributions.Normal( + torch.zeros(1, device=X.device, dtype=X.dtype), + torch.ones(1, device=X.device, dtype=X.dtype), + ) + + # Compute p(fM <= f* | ym, x, Dt) + view_shape = torch.Size( + [ + self.posterior_max_values.shape[0], + # add 1s to broadcast across the batch_shape of X + *[1 for _ in range(X.ndim - self.posterior_max_values.ndim)], + *self.posterior_max_values.shape[1:], + ] + ) # s_M x batch_shape x num_fantasies x (m) + max_vals = self.posterior_max_values.view(view_shape).unsqueeze(1) + # s_M x 1 x batch_shape x num_fantasies x (m) + normalized_mvs_new = (max_vals - mean_new) / stdv_new + # s_M x s_m x batch_shape x num_fantasies x (m) = + # s_M x 1 x batch_shape x num_fantasies x (m) + # - s_m x batch_shape x num_fantasies x (m) + cdf_mvs_new = normal.cdf(normalized_mvs_new).clamp_min(CLAMP_LB) + + # Compute p(fM <= f* | x, Dt) + stdv_M = variance_M.sqrt() + normalized_mvs = (max_vals - mean_M) / stdv_M + # s_M x 1 x batch_shape x num_fantasies x (m) = + # s_M x 1 x 1 x num_fantasies x (m) - batch_shape x num_fantasies x (m) + cdf_mvs = normal.cdf(normalized_mvs).clamp_min(CLAMP_LB) + # s_M x 1 x batch_shape x num_fantasies x (m) + + # Compute log(p(ym | x, Dt)) + log_pdf_fm = posterior_m.distribution.log_prob( + self.weight * samples_m + ).unsqueeze(0) + # 1 x s_m x batch_shape x num_fantasies x (m) + + # H0 = H(ym | x, Dt) + H0 = posterior_m.distribution.entropy() # batch_shape x num_fantasies x (m) + + # regression adjusted H1 estimation, H1_hat = H1_bar - beta * (H0_bar - H0) + # H1 = E_{f*|x, Dt}[H(ym|f*, x, Dt)] + Z = cdf_mvs_new / cdf_mvs # s_M x s_m x batch_shape x num_fantasies x (m) + # s_M x s_m x batch_shape x num_fantasies x (m) + h1 = -Z * Z.log() - Z * log_pdf_fm + check_no_nans(h1) + dim = [0, 1] # dimension of fm samples, fM samples + H1_bar = h1.mean(dim=dim) + h0 = -log_pdf_fm + H0_bar = h0.mean(dim=dim) + cov = ((h1 - H1_bar) * (h0 - H0_bar)).mean(dim=dim) + beta = cov / (h0.var(dim=dim) * h1.var(dim=dim)).sqrt() + H1_hat = H1_bar - beta * (H0_bar - H0) + ig = H0 - H1_hat # batch_shape x num_fantasies x (m) + if self.posterior_max_values.ndim == 2: + permute_idcs = [-1, *range(ig.ndim - 1)] + else: + permute_idcs = [-2, *range(ig.ndim - 2), -1] + ig = ig.permute(*permute_idcs) # num_fantasies x batch_shape x (m) + return ig
+ + + +
+[docs] +class qLowerBoundMaxValueEntropy(DiscreteMaxValueBase): + r"""The acquisition function for General-purpose Information-Based + Bayesian Optimisation (GIBBON). + + This acquisition function provides a computationally cheap approximation of + the mutual information between max values and a batch of candidate points `X`. + See [Moss2021gibbon]_ for a detailed discussion. + + The model must be single-outcome, unless using a PosteriorTransform. + q > 1 is supported through greedy batch filling. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> candidate_set = torch.rand(1000, bounds.size(1)) + >>> candidate_set = bounds[0] + (bounds[1] - bounds[0]) * candidate_set + >>> qGIBBON = qLowerBoundMaxValueEntropy(model, candidate_set) + >>> candidates, _ = optimize_acqf(qGIBBON, bounds, q=5) + """ + + def _compute_information_gain( + self, X: Tensor, mean_M: Tensor, variance_M: Tensor, covar_mM: Tensor + ) -> Tensor: + r"""Compute GIBBON's approximation of information gain at the design points `X`. + + When using GIBBON for batch optimization (i.e `q > 1`), we calculate the + additional information provided by adding a new candidate point to the current + batch of design points (`X_pending`), rather than calculating the information + provided by the whole batch. This allows a modest computational saving. + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of `batch_shape` t-batches + with `1` `d`-dim design point each. + mean_M: A `batch_shape x 1`-dim Tensor of means. + variance_M: A `batch_shape x 1`-dim Tensor of variances + consisting of `batch_shape` t-batches with `num_fantasies` fantasies. + covar_mM: A `batch_shape x num_fantasies x (1 + num_trace_observations)` + -dim Tensor of covariances. + + Returns: + A `num_fantasies x batch_shape`-dim Tensor of information gains at the + given design points `X`. + """ + # TODO: give the Posterior API an add_observation_noise function to avoid + # doing posterior computations twice + + # compute the mean_m, variance_m with noisy observation + posterior_m = self.model.posterior( + X, observation_noise=True, posterior_transform=self.posterior_transform + ) + mean_m = self.weight * posterior_m.mean.squeeze(-1) + # batch_shape x 1 + variance_m = posterior_m.variance.clamp_min(CLAMP_LB).squeeze(-1) + # batch_shape x 1 + check_no_nans(variance_m) + + # get stdv of noiseless variance + stdv = variance_M.sqrt() + # batch_shape x 1 + + # define normal distribution to compute cdf and pdf + normal = torch.distributions.Normal( + torch.zeros(1, device=X.device, dtype=X.dtype), + torch.ones(1, device=X.device, dtype=X.dtype), + ) + + # prepare max value quantities required by GIBBON + mvs = torch.transpose(self.posterior_max_values, 0, 1) + # 1 x s_M + normalized_mvs = (mvs - mean_m) / stdv + # batch_shape x s_M + + cdf_mvs = normal.cdf(normalized_mvs).clamp_min(CLAMP_LB) + pdf_mvs = torch.exp(normal.log_prob(normalized_mvs)) + ratio = pdf_mvs / cdf_mvs + check_no_nans(ratio) + + # prepare squared correlation between current and target fidelity + rhos_squared = torch.pow(covar_mM.squeeze(-1), 2) / (variance_m * variance_M) + # batch_shape x 1 + check_no_nans(rhos_squared) + + # calculate quality contribution to the GIBBON acquisition function + inner_term = 1 - rhos_squared * ratio * (normalized_mvs + ratio) + acq = -0.5 * inner_term.clamp_min(CLAMP_LB).log() + # average over posterior max samples + acq = acq.mean(dim=1).unsqueeze(0) + + if self.X_pending is None: + # for q=1, no repulsion term required + return acq + + # for q>1 GIBBON requires repulsion terms r_i, where + # r_i = log |C_i| for the predictive + # correlation matricies C_i between each candidate point in X and + # the m current batch elements in X_pending. + + # Each predictive covariance matrix can be expressed as + # V_i = [[v_i, A_i], [A_i,B]] for a shared m x m tensor B. + # So we can efficiently calculate |V_i| using the formula for + # determinant of block matricies, i.e. + # |V_i| = (v_i - A_i^T * B^{-1} * A_i) * |B| + # As the |B| term does not depend on X and we later take its log, + # it provides only a translation of the acquisition function surface + # and can thus be ignored. + + if self.posterior_transform is not None: + raise UnsupportedError( + "qLowerBoundMaxValueEntropy does not support PosteriorTransforms" + "when X_pending is not None." + ) + + X_batches = torch.cat( + [X, self.X_pending.unsqueeze(0).repeat(X.shape[0], 1, 1)], 1 + ) + # batch_shape x (1 + m) x d + # NOTE: This is the blocker for supporting posterior transforms. + # We would have to process this MVN, applying whatever operations + # are typically applied for the corresponding posterior, then applying + # the posterior transform onto the resulting object. + V = self.model(X_batches) + # Evaluate terms required for A + A = V.lazy_covariance_matrix[:, 0, 1:].unsqueeze(1) + # batch_shape x 1 x m + # Evaluate terms required for B + B = self.model.posterior( + self.X_pending, + observation_noise=True, + posterior_transform=self.posterior_transform, + ).distribution.covariance_matrix.unsqueeze(0) + # 1 x m x m + + # use determinant of block matrix formula + inv_quad_term = inv_quad(B, A.transpose(1, 2)).unsqueeze(1) + # NOTE: Even when using Cholesky to compute inv_quad, `V_determinant` can be + # negative due to numerical issues. To avoid this, we clamp the variance + # so that `V_determinant` > 0, while still allowing gradients to be + # propagated through `inv_quad_term`, as well as through `variance_m` + # in the expression for `r` below. + # choosing eps to be small while avoiding numerical underflow + eps = 1e-6 if inv_quad_term.dtype == torch.float32 else 1e-12 + V_determinant = variance_m.clamp(inv_quad_term * (1 + eps)) - inv_quad_term + # batch_shape x 1 + + # Take logs and convert covariances to correlations. + r = V_determinant.log() - variance_m.log() # = log(1 - inv_quad / var) + r = 0.5 * r.transpose(0, 1) + return acq + r
+ + + +
+[docs] +class qMultiFidelityMaxValueEntropy(qMaxValueEntropy): + r"""Multi-fidelity max-value entropy. + + The acquisition function for multi-fidelity max-value entropy search + with support for trace observations. See [Takeno2020mfmves]_ + for a detailed discussion of the basic ideas on multi-fidelity MES + (note that this implementation is somewhat different). + + The model must be single-outcome, unless using a PosteriorTransform. + The batch case `q > 1` is supported through cyclic optimization and fantasies. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> candidate_set = torch.rand(1000, bounds.size(1)) + >>> candidate_set = bounds[0] + (bounds[1] - bounds[0]) * candidate_set + >>> MF_MES = qMultiFidelityMaxValueEntropy(model, candidate_set) + >>> mf_mes = MF_MES(test_X) + """ + + def __init__( + self, + model: Model, + candidate_set: Tensor, + num_fantasies: int = 16, + num_mv_samples: int = 10, + num_y_samples: int = 128, + posterior_transform: PosteriorTransform | None = None, + use_gumbel: bool = True, + maximize: bool = True, + X_pending: Tensor | None = None, + cost_aware_utility: CostAwareUtility | None = None, + project: Callable[[Tensor], Tensor] = lambda X: X, + expand: Callable[[Tensor], Tensor] = lambda X: X, + ) -> None: + r"""Single-outcome max-value entropy search acquisition function. + + Args: + model: A fitted single-outcome model. + candidate_set: A `n x d` Tensor including `n` candidate points to + discretize the design space, which will be used to sample the + max values from their posteriors. + cost_aware_utility: A CostAwareUtility computing the cost-transformed + utility from a candidate set and samples of increases in utility. + num_fantasies: Number of fantasies to generate. The higher this + number the more accurate the model (at the expense of model + complexity and performance) and it's only used when `X_pending` + is not `None`. + num_mv_samples: Number of max value samples. + num_y_samples: Number of posterior samples at specific design point `X`. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + use_gumbel: If True, use Gumbel approximation to sample the max values. + maximize: If True, consider the problem a maximization problem. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + cost_aware_utility: A CostAwareUtility computing the cost-transformed + utility from a candidate set and samples of increases in utility. + project: A callable mapping a `batch_shape x q x d` tensor of design + points to a tensor of the same shape projected to the desired + target set (e.g. the target fidelities in case of multi-fidelity + optimization). + expand: A callable mapping a `batch_shape x q x d` input tensor to + a `batch_shape x (q + q_e)' x d`-dim output tensor, where the + `q_e` additional points in each q-batch correspond to + additional ("trace") observations. + """ + super().__init__( + model=model, + candidate_set=candidate_set, + num_fantasies=num_fantasies, + num_mv_samples=num_mv_samples, + num_y_samples=num_y_samples, + posterior_transform=posterior_transform, + use_gumbel=use_gumbel, + maximize=maximize, + X_pending=X_pending, + ) + + if cost_aware_utility is None: + cost_model = AffineFidelityCostModel(fidelity_weights={-1: 1.0}) + cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) + + self.cost_aware_utility = cost_aware_utility + self.expand = expand + self.project = project + self._cost_sampler = None + + # @TODO make sure fidelity_dims align in project, expand & cost_aware_utility + # It seems very difficult due to the current way of handling project/expand + + # resample max values after initializing self.project + # so that the max value samples are at the highest fidelity + self._sample_max_values(self.num_mv_samples) + + @property + def cost_sampler(self): + if self._cost_sampler is None: + # Note: Using the deepcopy here is essential. Removing this poses a + # problem if the base model and the cost model have a different number + # of outputs or test points (this would be caused by expand), as this + # would trigger re-sampling the base samples in the fantasy sampler. + # By cloning the sampler here, the right thing will happen if the + # the sizes are compatible, if they are not this will result in + # samples being drawn using different base samples, but it will at + # least avoid changing state of the fantasy sampler. + self._cost_sampler = deepcopy(self.fantasies_sampler) + return self._cost_sampler + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluates `qMultifidelityMaxValueEntropy` at the design points `X` + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of `batch_shape` t-batches + with `1` `d`-dim design point each. + + Returns: + A `batch_shape`-dim Tensor of MF-MVES values at the design points `X`. + """ + X_expand = self.expand(X) # batch_shape x (1 + num_trace_observations) x d + X_max_fidelity = self.project(X) # batch_shape x 1 x d + X_all = torch.cat((X_expand, X_max_fidelity), dim=-2).unsqueeze(-3) + # batch_shape x num_fantasies x (2 + num_trace_observations) x d + + # Compute the posterior, posterior mean, variance without noise + # `_m` and `_M` in the var names means the current and the max fidelity. + posterior = self.model.posterior( + X_all, observation_noise=False, posterior_transform=self.posterior_transform + ) + mean_M = self.weight * posterior.mean[..., -1, 0] # batch_shape x num_fantasies + variance_M = posterior.variance[..., -1, 0].clamp_min(CLAMP_LB) + # get the covariance between the low fidelities and max fidelity + covar_mM = posterior.distribution.covariance_matrix[..., :-1, -1] + # batch_shape x num_fantasies x (1 + num_trace_observations) + + check_no_nans(mean_M) + check_no_nans(variance_M) + check_no_nans(covar_mM) + + # compute the information gain (IG) + ig = self._compute_information_gain( + X=X_expand, mean_M=mean_M, variance_M=variance_M, covar_mM=covar_mM + ) + ig = self.cost_aware_utility(X=X, deltas=ig, sampler=self.cost_sampler) + return ig.mean(dim=0) # average over the fantasies
+
+ + + +
+[docs] +class qMultiFidelityLowerBoundMaxValueEntropy(qMultiFidelityMaxValueEntropy): + r"""Multi-fidelity acquisition function for General-purpose Information-Based + Bayesian optimization (GIBBON). + + The acquisition function for multi-fidelity max-value entropy search + with support for trace observations. See [Takeno2020mfmves]_ + for a detailed discussion of the basic ideas on multi-fidelity MES + (note that this implementation is somewhat different). This acquisition function + is similar to `qMultiFidelityMaxValueEntropy` but computes the information gain + from the lower bound described in [Moss2021gibbon]. + + The model must be single-outcome, unless using a PosteriorTransform. + The batch case `q > 1` is supported through cyclic optimization and fantasies. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> candidate_set = torch.rand(1000, bounds.size(1)) + >>> candidate_set = bounds[0] + (bounds[1] - bounds[0]) * candidate_set + >>> MF_qGIBBON = qMultiFidelityLowerBoundMaxValueEntropy(model, candidate_set) + >>> mf_gibbon = MF_qGIBBON(test_X) + """ + + def __init__( + self, + model: Model, + candidate_set: Tensor, + num_fantasies: int = 16, + num_mv_samples: int = 10, + num_y_samples: int = 128, + posterior_transform: PosteriorTransform | None = None, + use_gumbel: bool = True, + maximize: bool = True, + cost_aware_utility: CostAwareUtility | None = None, + project: Callable[[Tensor], Tensor] = lambda X: X, + expand: Callable[[Tensor], Tensor] = lambda X: X, + ) -> None: + r"""Single-outcome max-value entropy search acquisition function. + + Args: + model: A fitted single-outcome model. + candidate_set: A `n x d` Tensor including `n` candidate points to + discretize the design space, which will be used to sample the + max values from their posteriors. + cost_aware_utility: A CostAwareUtility computing the cost-transformed + utility from a candidate set and samples of increases in utility. + num_fantasies: Number of fantasies to generate. The higher this + number the more accurate the model (at the expense of model + complexity and performance) and it's only used when `X_pending` + is not `None`. + num_mv_samples: Number of max value samples. + num_y_samples: Number of posterior samples at specific design point `X`. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + use_gumbel: If True, use Gumbel approximation to sample the max values. + maximize: If True, consider the problem a maximization problem. + cost_aware_utility: A CostAwareUtility computing the cost-transformed + utility from a candidate set and samples of increases in utility. + project: A callable mapping a `batch_shape x q x d` tensor of design + points to a tensor of the same shape projected to the desired + target set (e.g. the target fidelities in case of multi-fidelity + optimization). + expand: A callable mapping a `batch_shape x q x d` input tensor to + a `batch_shape x (q + q_e)' x d`-dim output tensor, where the + `q_e` additional points in each q-batch correspond to + additional ("trace") observations. + """ + super().__init__( + model=model, + candidate_set=candidate_set, + num_fantasies=num_fantasies, + num_mv_samples=num_mv_samples, + num_y_samples=num_y_samples, + posterior_transform=posterior_transform, + use_gumbel=use_gumbel, + maximize=maximize, + cost_aware_utility=cost_aware_utility, + project=project, + expand=expand, + ) + + def _compute_information_gain( + self, X: Tensor, mean_M: Tensor, variance_M: Tensor, covar_mM: Tensor + ) -> Tensor: + r"""Compute GIBBON's approximation of information gain at the design points `X`. + + When using GIBBON for batch optimization (i.e `q > 1`), we calculate the + additional information provided by adding a new candidate point to the current + batch of design points (`X_pending`), rather than calculating the information + provided by the whole batch. This allows a modest computational saving. + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of `batch_shape` t-batches + with `1` `d`-dim design point each. + mean_M: A `batch_shape x 1`-dim Tensor of means. + variance_M: A `batch_shape x 1`-dim Tensor of variances + consisting of `batch_shape` t-batches with `num_fantasies` fantasies. + covar_mM: A `batch_shape x num_fantasies x (1 + num_trace_observations)` + -dim Tensor of covariances. + + Returns: + A `num_fantasies x batch_shape`-dim Tensor of information gains at the + given design points `X`. + """ + return qLowerBoundMaxValueEntropy._compute_information_gain( + self, X=X, mean_M=mean_M, variance_M=variance_M, covar_mM=covar_mM + )
+ + + +def _sample_max_value_Thompson( + model: Model, + candidate_set: Tensor, + num_samples: int, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, +) -> Tensor: + """Samples the max values by discrete Thompson sampling. + + Should generally be called within a `with torch.no_grad()` context. + + Args: + model: A fitted single-outcome model. + candidate_set: A `n x d` Tensor including `n` candidate points to + discretize the design space. + num_samples: Number of max value samples. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + + Returns: + A `num_samples x num_fantasies` Tensor of posterior max value samples. + """ + posterior = model.posterior(candidate_set, posterior_transform=posterior_transform) + weight = 1.0 if maximize else -1.0 + samples = weight * posterior.rsample(torch.Size([num_samples])).squeeze(-1) + # samples is num_samples x (num_fantasies) x n + max_values, _ = samples.max(dim=-1) + if len(samples.shape) == 2: + max_values = max_values.unsqueeze(-1) # num_samples x num_fantasies + + return max_values + + +def _sample_max_value_Gumbel( + model: Model, + candidate_set: Tensor, + num_samples: int, + posterior_transform: PosteriorTransform | None = None, + maximize: bool = True, +) -> Tensor: + """Samples the max values by Gumbel approximation. + + Should generally be called within a `with torch.no_grad()` context. + + Args: + model: A fitted single-outcome model. + candidate_set: A `n x d` Tensor including `n` candidate points to + discretize the design space. + num_samples: Number of max value samples. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + maximize: If True, consider the problem a maximization problem. + + Returns: + A `num_samples x num_fantasies` Tensor of posterior max value samples. + """ + # define the approximate CDF for the max value under the independence assumption + posterior = model.posterior(candidate_set, posterior_transform=posterior_transform) + weight = 1.0 if maximize else -1.0 + mu = weight * posterior.mean + sigma = posterior.variance.clamp_min(1e-8).sqrt() + # mu, sigma is (num_fantasies) X n X 1 + if len(mu.shape) == 3 and mu.shape[-1] == 1: + mu = mu.squeeze(-1).T + sigma = sigma.squeeze(-1).T + # mu, sigma is now n X num_fantasies or n X 1 + + # bisect search to find the quantiles 25, 50, 75 + lo = (mu - 3 * sigma).min(dim=0).values + hi = (mu + 5 * sigma).max(dim=0).values + num_fantasies = mu.shape[1] + device = candidate_set.device + dtype = candidate_set.dtype + quantiles = torch.zeros(num_fantasies, 3, device=device, dtype=dtype) + for i in range(num_fantasies): + lo_, hi_ = lo[i], hi[i] + N = norm(mu[:, i].cpu().numpy(), sigma[:, i].cpu().numpy()) + quantiles[i, :] = torch.tensor( + [ + brentq(lambda y: np.exp(np.sum(N.logcdf(y))) - p, lo_, hi_) + for p in [0.25, 0.50, 0.75] + ] + ) + q25, q50, q75 = quantiles[:, 0], quantiles[:, 1], quantiles[:, 2] + # q25, q50, q75 are 1 dimensional tensor with size of either 1 or num_fantasies + + # parameter fitting based on matching percentiles for the Gumbel distribution + b = (q25 - q75) / (log(log(4.0 / 3.0)) - log(log(4.0))) + a = q50 + b * log(log(2.0)) + + # inverse sampling from the fitted Gumbel CDF distribution + sample_shape = (num_samples, num_fantasies) + eps = torch.rand(*sample_shape, device=device, dtype=dtype) + max_values = a - b * eps.log().mul(-1.0).log() + + return max_values # num_samples x num_fantasies +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/monte_carlo.html b/website-old/pages/api/_modules/botorch/acquisition/monte_carlo.html new file mode 100644 index 0000000000..4777d2fe6d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/monte_carlo.html @@ -0,0 +1,983 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.monte_carlo

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Batch acquisition functions using the reparameterization trick in combination
+with (quasi) Monte-Carlo sampling. See [Rezende2014reparam]_, [Wilson2017reparam]_ and
+[Balandat2020botorch]_.
+
+References
+
+.. [Rezende2014reparam]
+    D. J. Rezende, S. Mohamed, and D. Wierstra. Stochastic backpropagation and
+    approximate inference in deep generative models. ICML 2014.
+
+.. [Wilson2017reparam]
+    J. T. Wilson, R. Moriconi, F. Hutter, and M. P. Deisenroth.
+    The reparameterization trick for acquisition functions. ArXiv 2017.
+"""
+
+from __future__ import annotations
+
+import math
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from copy import deepcopy
+from functools import partial
+from typing import Protocol
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
+from botorch.acquisition.cached_cholesky import CachedCholeskyMCSamplerMixin
+from botorch.acquisition.objective import (
+    ConstrainedMCObjective,
+    IdentityMCObjective,
+    MCAcquisitionObjective,
+    PosteriorTransform,
+)
+from botorch.acquisition.utils import (
+    compute_best_feasible_objective,
+    prune_inferior_points,
+    repeat_to_match_aug_dim,
+)
+from botorch.exceptions.errors import UnsupportedError
+from botorch.exceptions.warnings import legacy_ei_numerics_warning
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.objective import compute_smoothed_feasibility_indicator
+from botorch.utils.transforms import (
+    concatenate_pending_points,
+    match_batch_shape,
+    t_batch_mode_transform,
+)
+from torch import Tensor
+
+
+
+[docs] +class MCAcquisitionFunction(AcquisitionFunction, MCSamplerMixin, ABC): + r""" + Abstract base class for Monte-Carlo based batch acquisition functions. + """ + + def __init__( + self, + model: Model, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + ) -> None: + r""" + Args: + model: A fitted model. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated on the fly within the + `get_posterior_samples` method using + `botorch.sampling.get_sampler`. + NOTE: For posteriors that do not support base samples, + a sampler compatible with intended use case must be provided. + See `ForkedRNGSampler` and `StochasticSampler` as examples. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `batch_shape, m x d`-dim Tensor of `m` design points + that have points that have been submitted for function evaluation + but have not yet been evaluated. + """ + super().__init__(model=model) + MCSamplerMixin.__init__(self, sampler=sampler) + if objective is None and model.num_outputs != 1: + if posterior_transform is None: + raise UnsupportedError( + "Must specify an objective or a posterior transform when using " + "a multi-output model." + ) + elif not posterior_transform.scalarize: + raise UnsupportedError( + "If using a multi-output model without an objective, " + "posterior_transform must scalarize the output." + ) + if objective is None: + objective = IdentityMCObjective() + self.posterior_transform = posterior_transform + self.objective: MCAcquisitionObjective = objective + self.set_X_pending(X_pending) + + def _get_samples_and_objectives(self, X: Tensor) -> tuple[Tensor, Tensor]: + """Computes posterior samples and objective values at input X. + + Args: + X: A `batch_shape x q x d`-dim Tensor of model inputs. + + Returns: + A two-tuple `(samples, obj)`, where `samples` is a tensor of posterior + samples with shape `sample_shape x batch_shape x q x m`, and `obj` is a + tensor of MC objective values with shape `sample_shape x batch_shape x q`. + """ + posterior = self.model.posterior( + X=X, posterior_transform=self.posterior_transform + ) + samples = self.get_posterior_samples(posterior) + obj = self.objective(samples=samples, X=X) + return samples, obj + +
+[docs] + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Takes in a `batch_shape x q x d` X Tensor of t-batches with `q` `d`-dim + design points each, and returns a Tensor with shape `batch_shape'`, where + `batch_shape'` is the broadcasted batch shape of model and input `X`. Should + utilize the result of `set_X_pending` as needed to account for pending function + evaluations. + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class SampleReductionProtocol(Protocol): + """For static type check of SampleReducingMCAcquisitionFunction's mc_reduction.""" + + @staticmethod + def __call__(X: Tensor, *, dim: torch.Size) -> Tensor: + pass # pragma: no cover
+ + + +
+[docs] +class SampleReducingMCAcquisitionFunction(MCAcquisitionFunction): + r"""MC-based batch acquisition function that reduces across samples and implements + a general treatment of outcome constraints. + + This class's `forward` computes the - possibly constrained - acquisition value by + (1) computing the unconstrained utility for each MC sample using `_sample_forward`, + (2) weighing the utility values by the constraint indicator per MC sample, and + (3) reducing (e.g. averaging) the weighted utility values over the MC dimension. + + NOTE: Do *NOT* override the `forward` method, unless you have thought about it well. + + `forward` is implemented generically to incorporate constraints in a principled way, + and takes care of reducing over the Monte Carlo and batch dimensions via the + `sample_reduction` and `q_reduction` arguments, which default to `torch.mean` and + `torch.max`, respectively. + + In order to implement a custom SampleReducingMCAcquisitionFunction, we only need to + implement the `_sample_forward(obj: Tensor) -> Tensor` method, which maps objective + samples to acquisition utility values without reducing the Monte Carlo and batch + (i.e. q) dimensions (see details in the docstring of `_sample_forward`). + + A note on design choices: + + The primary purpose of `SampleReducingMCAcquisitionFunction`is to support outcome + constraints. On the surface, designing a wrapper `ConstrainedMCAcquisitionFunction` + could be an elegant solution to this end, but it would still require the acquisition + functions to implement a `_sample_forward` method to weigh acquisition utilities at + the sample level. Further, `qNoisyExpectedImprovement` is a special case that is + hard to encompass in this pattern, since it requires the computation of the best + *feasible* objective, which requires access to the constraint functions. However, + if the constraints are stored in a wrapper class, they will be inaccessible to the + forward pass. These problems are circumvented by the design of this class. + """ + + _log: bool = False # whether the acquisition utilities are in log-space + + def __init__( + self, + model: Model, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + sample_reduction: SampleReductionProtocol = torch.mean, + q_reduction: SampleReductionProtocol = torch.amax, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + fat: bool = False, + ): + r"""Constructor of SampleReducingMCAcquisitionFunction. + + Args: + model: A fitted model. + sampler: The sampler used to draw base samples. If not given, a + sampler is generated on the fly within the + `get_posterior_samples` method using + `botorch.sampling.get_sampler`. + NOTE: For posteriors that do not support base samples, + a sampler compatible with intended use case must be provided. + See `ForkedRNGSampler` and `StochasticSampler` as examples. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + NOTE: `ConstrainedMCObjective` for outcome constraints is deprecated in + favor of passing the `constraints` directly to this constructor. + posterior_transform: A `PosteriorTransform` (optional). + X_pending: A `batch_shape, m x d`-dim Tensor of `m` design points + that have points that have been submitted for function evaluation + but have not yet been evaluated. + sample_reduction: A callable that takes in a `sample_shape x batch_shape` + Tensor of acquisition utility values, a keyword-argument `dim` that + specifies the sample dimensions to reduce over, and returns a + `batch_shape`-dim Tensor of acquisition values. + q_reduction: A callable that takes in a `sample_shape x batch_shape x q` + Tensor of acquisition utility values, a keyword-argument `dim` that + specifies the q dimension to reduce over (i.e. -1), and returns a + `sample_shape x batch_shape`-dim Tensor of acquisition values. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + NOTE: Constraint-weighting is only compatible with non-negative + acquistion utilities, e.g. all improvement-based acquisition functions. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + fat: Wether to apply a fat-tailed smooth approximation to the feasibility + indicator or the canonical sigmoid approximation. + """ + if constraints is not None and isinstance(objective, ConstrainedMCObjective): + raise ValueError( + "ConstrainedMCObjective as well as constraints passed to constructor." + "Choose one or the other, preferably the latter." + ) + # TODO: deprecate ConstrainedMCObjective + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + ) + # Shall the need arise, sample_dim could be exposed in the constructor. + sample_dim = tuple(range(len(self.sample_shape))) + self._sample_reduction = partial(sample_reduction, dim=sample_dim) + self._q_reduction = partial(q_reduction, dim=-1) + self._constraints = constraints + self._eta = eta + self._fat = fat + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Computes the acquisition value associated with the input `X`. Weighs the + acquisition utility values by smoothed constraint indicators if `constraints` + was passed to the constructor of the class. Applies `self.sample_reduction` and + `self.q_reduction` to reduce over the Monte Carlo and batch (q) dimensions. + + NOTE: Do *NOT* override the `forward` method for a custom acquisition function. + Instead, implement the `_sample_forward` method. See the docstring of this class + for details. + + Args: + X: A `batch_shape x q x d` Tensor of t-batches with `q` `d`-dim + design points each. + + Returns: + A Tensor with shape `batch_shape'`, where `batch_shape'` is the broadcasted + batch shape of model and input `X`. + """ + non_reduced_acqval = self._non_reduced_forward(X=X) + return self._sample_reduction(self._q_reduction(non_reduced_acqval))
+ + + def _non_reduced_forward(self, X: Tensor) -> Tensor: + """Compute the constrained acquisition values at the MC-sample, q level. + + Args: + X: A `batch_shape x q x d` Tensor of t-batches with `q` `d`-dim + design points each. + + Returns: + A Tensor with shape `sample_sample x batch_shape x q`. + """ + samples, obj = self._get_samples_and_objectives(X) + samples = repeat_to_match_aug_dim(target_tensor=samples, reference_tensor=obj) + acqval = self._sample_forward(obj) # `sample_sample x batch_shape x q` + return self._apply_constraints(acqval=acqval, samples=samples) + + @abstractmethod + def _sample_forward(self, obj: Tensor) -> Tensor: + """Evaluates the acquisition utility per MC sample based on objective value obj. + Should utilize the result of `set_X_pending` as needed to account for pending + function evaluations. + + Args: + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of acquisition utility values. + """ + pass # pragma: no cover + + def _apply_constraints(self, acqval: Tensor, samples: Tensor) -> Tensor: + """Multiplies the acquisition utility by constraint indicators. + + Args: + acqval: `sample_shape x batch_shape x q`-dim acquisition utility values. + samples: `sample_shape x batch_shape x q x m`-dim posterior samples. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of acquisition utility values + multiplied by a smoothed constraint indicator per sample. + """ + if self._constraints is not None: + if not self._log and (acqval < 0).any(): + raise ValueError( + "Constraint-weighting requires unconstrained " + "acquisition values to be non-negative." + ) + ind = compute_smoothed_feasibility_indicator( + constraints=self._constraints, + samples=samples, + eta=self._eta, + log=self._log, + fat=self._fat, + ) + acqval = acqval.add(ind) if self._log else acqval.mul(ind) + return acqval
+ + + +
+[docs] +class qExpectedImprovement(SampleReducingMCAcquisitionFunction): + r"""MC-based batch Expected Improvement. + + This computes qEI by + (1) sampling the joint posterior over q points + (2) evaluating the improvement over the current best for each sample + (3) maximizing over q + (4) averaging over the samples + + `qEI(X) = E(max(max Y - best_f, 0)), Y ~ f(X), where X = (x_1,...,x_q)` + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> best_f = train_Y.max()[0] + >>> sampler = SobolQMCNormalSampler(1024) + >>> qEI = qExpectedImprovement(model, best_f, sampler) + >>> qei = qEI(test_X) + + NOTE: It is strongly recommended to use qLogExpectedImprovement instead + of regular qEI, as it can lead to substantially improved BO performance through + improved numerics. See https://arxiv.org/abs/2310.20708 for details. + """ + + def __init__( + self, + model: Model, + best_f: float | Tensor, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + ) -> None: + r"""q-Expected Improvement. + + Args: + model: A fitted model. + best_f: The best objective value observed so far (assumed noiseless). Can be + a scalar, or a `batch_shape`-dim tensor. In case of a batched model, the + tensor can specify different values for each element of the batch. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are evaluated. + Defaults to `IdentityMCObjective()`. + NOTE: `ConstrainedMCObjective` for outcome constraints is deprecated in + favor of passing the `constraints` directly to this constructor. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. Copied and set to have no + gradient. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + """ + legacy_ei_numerics_warning(legacy_name=type(self).__name__) + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + constraints=constraints, + eta=eta, + ) + self.register_buffer("best_f", torch.as_tensor(best_f, dtype=float)) + + def _sample_forward(self, obj: Tensor) -> Tensor: + r"""Evaluate qExpectedImprovement per sample on the candidate set `X`. + + Args: + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of improvement utility values. + """ + return (obj - self.best_f.unsqueeze(-1).to(obj)).clamp_min(0)
+ + + +
+[docs] +class qNoisyExpectedImprovement( + SampleReducingMCAcquisitionFunction, CachedCholeskyMCSamplerMixin +): + r"""MC-based batch Noisy Expected Improvement. + + This function does not assume a `best_f` is known (which would require + noiseless observations). Instead, it uses samples from the joint posterior + over the `q` test points and previously observed points. The improvement + over previously observed points is computed for each sample and averaged. + + `qNEI(X) = E(max(max Y - max Y_baseline, 0))`, where + `(Y, Y_baseline) ~ f((X, X_baseline)), X = (x_1,...,x_q)` + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> sampler = SobolQMCNormalSampler(1024) + >>> qNEI = qNoisyExpectedImprovement(model, train_X, sampler) + >>> qnei = qNEI(test_X) + + NOTE: It is strongly recommended to use qLogNoisyExpectedImprovement instead + of regular qNEI, as it can lead to substantially improved BO performance through + improved numerics. See https://arxiv.org/abs/2310.20708 for details. + """ + + def __init__( + self, + model: Model, + X_baseline: Tensor, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + prune_baseline: bool = True, + cache_root: bool = True, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + marginalize_dim: int | None = None, + ) -> None: + r"""q-Noisy Expected Improvement. + + Args: + model: A fitted model. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are considered as + the potential best design point. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + NOTE: `ConstrainedMCObjective` for outcome constraints is deprecated in + favor of passing the `constraints` directly to this constructor. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points + that have points that have been submitted for function evaluation + but have not yet been evaluated. Concatenated into `X` upon + forward call. Copied and set to have no gradient. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the best point. This can significantly + improve performance and is generally recommended. In order to + customize pruning parameters, instead manually call + `botorch.acquisition.utils.prune_inferior_points` on `X_baseline` + before instantiating the acquisition function. + cache_root: A boolean indicating whether to cache the root + decomposition over `X_baseline` and use low-rank updates. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are considered satisfied if the output is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + marginalize_dim: The dimension to marginalize over. + + TODO: similar to qNEHVI, when we are using sequential greedy candidate + selection, we could incorporate pending points X_baseline and compute + the incremental qNEI from the new point. This would greatly increase + efficiency for large batches. + """ + legacy_ei_numerics_warning(legacy_name=type(self).__name__) + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + constraints=constraints, + eta=eta, + ) + CachedCholeskyMCSamplerMixin.__init__( + self, model=model, cache_root=cache_root, sampler=sampler + ) + if prune_baseline: + X_baseline = prune_inferior_points( + model=model, + X=X_baseline, + objective=objective, + posterior_transform=posterior_transform, + constraints=self._constraints, + marginalize_dim=marginalize_dim, + ) + self.register_buffer("X_baseline", X_baseline) + # registering buffers for _get_samples_and_objectives in the next `if` block + self.register_buffer("baseline_samples", None) + self.register_buffer("baseline_obj", None) + if self._cache_root: + self.q_in = -1 + # set baseline samples + with torch.no_grad(): # this is _get_samples_and_objectives(X_baseline) + posterior = self.model.posterior( + X_baseline, posterior_transform=self.posterior_transform + ) + # Note: The root decomposition is cached in two different places. It + # may be confusing to have two different caches, but this is not + # trivial to change since each is needed for a different reason: + # - LinearOperator caching to `posterior.mvn` allows for reuse within + # this function, which may be helpful if the same root decomposition + # is produced by the calls to `self.base_sampler` and + # `self._cache_root_decomposition`. + # - self._baseline_L allows a root decomposition to be persisted outside + # this method. + baseline_samples = self.get_posterior_samples(posterior) + baseline_obj = self.objective(baseline_samples, X=X_baseline) + + # We make a copy here because we will write an attribute `base_samples` + # to `self.base_sampler.base_samples`, and we don't want to mutate + # `self.sampler`. + self.base_sampler = deepcopy(self.sampler) + self.baseline_samples = baseline_samples + self.baseline_obj = baseline_obj + self.register_buffer( + "_baseline_best_f", + self._compute_best_feasible_objective( + samples=baseline_samples, obj=baseline_obj + ), # `sample_shape x batch_shape`-dim + ) + self._baseline_L = self._compute_root_decomposition(posterior=posterior) + +
+[docs] + def compute_best_f(self, obj: Tensor) -> Tensor: + """Computes the best (feasible) noisy objective value. + + Args: + obj: `sample_shape x batch_shape x q`-dim Tensor of objectives in forward. + + Returns: + A `sample_shape x batch_shape`-dim Tensor of best feasible objectives. + """ + if self._cache_root: + val = self._baseline_best_f + else: + val = self._compute_best_feasible_objective( + samples=self.baseline_samples, obj=self.baseline_obj + ) + # ensuring shape, dtype, device compatibility with obj + n_sample_dims = len(self.sample_shape) + view_shape = torch.Size( + [ + *val.shape[:n_sample_dims], # sample dimensions + *(1,) * (obj.ndim - val.ndim - 1), # pad to match obj, without `q`-dim + *val.shape[n_sample_dims:], # the rest + ] + ) + return val.view(view_shape).to(obj) # obj.shape[:-1], i.e. without `q`-dim`
+ + + def _sample_forward(self, obj: Tensor) -> Tensor: + """Evaluate qNoisyExpectedImprovement per objective value in `obj`. + + Args: + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of noisy improvement values. + """ + return (obj - self.compute_best_f(obj).unsqueeze(-1)).clamp_min(0) + + def _get_samples_and_objectives(self, X: Tensor) -> tuple[Tensor, Tensor]: + r"""Compute samples at new points, using the cached root decomposition. + + Args: + X: A `batch_shape x q x d`-dim tensor of inputs. + + Returns: + A two-tuple `(samples, obj)`, where `samples` is a tensor of posterior + samples with shape `sample_shape x batch_shape x q x m`, and `obj` is a + tensor of MC objective values with shape `sample_shape x batch_shape x q`. + """ + q = X.shape[-2] + X_full = torch.cat([match_batch_shape(self.X_baseline, X), X], dim=-2) + # TODO: Implement more efficient way to compute posterior over both training and + # test points in GPyTorch (https://github.com/cornellius-gp/gpytorch/issues/567) + posterior = self.model.posterior( + X_full, posterior_transform=self.posterior_transform + ) + if not self._cache_root: + samples_full = super().get_posterior_samples(posterior) + samples = samples_full[..., -q:, :] + obj_full = self.objective(samples_full, X=X_full) + # assigning baseline buffers so `best_f` can be computed in _sample_forward + self.baseline_obj, obj = obj_full[..., :-q], obj_full[..., -q:] + self.baseline_samples = samples_full[..., :-q, :] + else: + # handle one-to-many input transforms + n_plus_q = X_full.shape[-2] + n_w = posterior._extended_shape()[-2] // n_plus_q + q_in = q * n_w + self._set_sampler(q_in=q_in, posterior=posterior) + samples = self._get_f_X_samples(posterior=posterior, q_in=q_in) + obj = self.objective(samples, X=X_full[..., -q:, :]) + + return samples, obj + + def _compute_best_feasible_objective(self, samples: Tensor, obj: Tensor) -> Tensor: + r"""Computes best feasible objective value from samples. + + Args: + samples: `sample_shape x batch_shape x q x m`-dim posterior samples. + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape`-dim Tensor of best feasible objectives. + """ + return compute_best_feasible_objective( + samples=samples, + obj=obj, + constraints=self._constraints, + model=self.model, + objective=self.objective, + posterior_transform=self.posterior_transform, + X_baseline=self.X_baseline, + )
+ + + +
+[docs] +class qProbabilityOfImprovement(SampleReducingMCAcquisitionFunction): + r"""MC-based batch Probability of Improvement. + + Estimates the probability of improvement over the current best observed + value by sampling from the joint posterior distribution of the q-batch. + MC-based estimates of a probability involves taking expectation of an + indicator function; to support auto-differentiation, the indicator is + replaced with a sigmoid function with temperature parameter `tau`. + + `qPI(X) = P(max Y >= best_f), Y ~ f(X), X = (x_1,...,x_q)` + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> best_f = train_Y.max()[0] + >>> sampler = SobolQMCNormalSampler(1024) + >>> qPI = qProbabilityOfImprovement(model, best_f, sampler) + >>> qpi = qPI(test_X) + """ + + def __init__( + self, + model: Model, + best_f: float | Tensor, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + tau: float = 1e-3, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + ) -> None: + r"""q-Probability of Improvement. + + Args: + model: A fitted model. + best_f: The best objective value observed so far (assumed noiseless). Can + be a `batch_shape`-shaped tensor, which in case of a batched model + specifies potentially different values for each element of the batch. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + NOTE: `ConstrainedMCObjective` for outcome constraints is deprecated in + favor of passing the `constraints` directly to this constructor. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. Concatenated into X upon + forward call. Copied and set to have no gradient. + tau: The temperature parameter used in the sigmoid approximation + of the step function. Smaller values yield more accurate + approximations of the function, but result in gradients + estimates with higher variance. + constraints: A list of constraint callables which map posterior samples to + a scalar. The associated constraint is considered satisfied if this + scalar is less than zero. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. For more details, on this + parameter, see the docs of `compute_smoothed_feasibility_indicator`. + """ + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + constraints=constraints, + eta=eta, + ) + best_f = torch.as_tensor(best_f, dtype=float).unsqueeze(-1) # adding batch dim + self.register_buffer("best_f", best_f) + self.register_buffer("tau", torch.as_tensor(tau, dtype=float)) + + def _sample_forward(self, obj: Tensor) -> Tensor: + r"""Evaluate qProbabilityOfImprovement per sample on the candidate set `X`. + + Args: + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of improvement indicators. + """ + improvement = obj - self.best_f.to(obj) + return torch.sigmoid(improvement / self.tau)
+ + + +
+[docs] +class qSimpleRegret(SampleReducingMCAcquisitionFunction): + r"""MC-based batch Simple Regret. + + Samples from the joint posterior over the q-batch and computes the simple regret. + + `qSR(X) = E(max Y), Y ~ f(X), X = (x_1,...,x_q)` + + Constraints should be provided as a `ConstrainedMCObjective`. + Passing `constraints` as an argument is not supported. This is because + `SampleReducingMCAcquisitionFunction` computes the acquisition values on the sample + level and then weights the sample-level acquisition values by a soft feasibility + indicator. Hence, it expects non-log acquisition function values to be + non-negative. `qSimpleRegret` acquisition values can be negative, so we instead use + a `ConstrainedMCObjective` which applies constraints to the objectives (e.g. before + computing the acquisition function) and shifts negative objective values using + by an infeasible cost to ensure non-negativity (before applying constraints and + shifting them back). + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> sampler = SobolQMCNormalSampler(1024) + >>> qSR = qSimpleRegret(model, sampler) + >>> qsr = qSR(test_X) + """ + + def __init__( + self, + model: Model, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + ) -> None: + r"""q-Simple Regret. + + Args: + model: A fitted model. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. Concatenated into X upon + forward call. Copied and set to have no gradient. + """ + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + ) + + def _sample_forward(self, obj: Tensor) -> Tensor: + r"""Evaluate qSimpleRegret per sample on the candidate set `X`. + + Args: + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of simple regret values. + """ + return obj
+ + + +
+[docs] +class qUpperConfidenceBound(SampleReducingMCAcquisitionFunction): + r"""MC-based batch Upper Confidence Bound. + + Uses a reparameterization to extend UCB to qUCB for q > 1 (See Appendix A + of [Wilson2017reparam].) + + `qUCB = E(max(mu + |Y_tilde - mu|))`, where `Y_tilde ~ N(mu, beta pi/2 Sigma)` + and `f(X)` has distribution `N(mu, Sigma)`. + + Constraints should be provided as a `ConstrainedMCObjective`. + Passing `constraints` as an argument is not supported. This is because + `SampleReducingMCAcquisitionFunction` computes the acquisition values on the sample + level and then weights the sample-level acquisition values by a soft feasibility + indicator. Hence, it expects non-log acquisition function values to be + non-negative. `qSimpleRegret` acquisition values can be negative, so we instead use + a `ConstrainedMCObjective` which applies constraints to the objectives (e.g. before + computing the acquisition function) and shifts negative objective values using + by an infeasible cost to ensure non-negativity (before applying constraints and + shifting them back). + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> sampler = SobolQMCNormalSampler(1024) + >>> qUCB = qUpperConfidenceBound(model, 0.1, sampler) + >>> qucb = qUCB(test_X) + """ + + def __init__( + self, + model: Model, + beta: float, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + ) -> None: + r"""q-Upper Confidence Bound. + + Args: + model: A fitted model. + beta: Controls tradeoff between mean and standard deviation in UCB. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation but have not yet + been evaluated. Concatenated into X upon forward call. Copied and set to + have no gradient. + """ + super().__init__( + model=model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + ) + self.beta_prime = self._get_beta_prime(beta=beta) + + def _get_beta_prime(self, beta: float) -> float: + return math.sqrt(beta * math.pi / 2) + + def _sample_forward(self, obj: Tensor) -> Tensor: + r"""Evaluate qUpperConfidenceBound per sample on the candidate set `X`. + + Args: + obj: A `sample_shape x batch_shape x q`-dim Tensor of MC objective values. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of acquisition values. + """ + mean = obj.mean(dim=0) + return mean + self.beta_prime * (obj - mean).abs()
+ + + +
+[docs] +class qLowerConfidenceBound(qUpperConfidenceBound): + r"""MC-based batched lower confidence bound. + + This acquisition function is useful for confident/risk-averse decision making. + This acquisition function is intended to be maximized as with qUpperConfidenceBound, + but the qLowerConfidenceBound will be pessimistic in the face of uncertainty and + lead to conservative candidates. + """ + + def _get_beta_prime(self, beta: float) -> float: + """Multiply beta prime by -1 to get the lower confidence bound.""" + return -super()._get_beta_prime(beta=beta)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/analytic.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/analytic.html new file mode 100644 index 0000000000..f6fa5fe9b1 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/analytic.html @@ -0,0 +1,282 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.analytic

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Analytic Acquisition Functions for Multi-objective Bayesian optimization.
+
+References
+
+.. [Yang2019]
+    Yang, K., Emmerich, M., Deutz, A. et al. Efficient computation of expected
+    hypervolume improvement using box decomposition algorithms. J Glob Optim 75,
+    3–34 (2019)
+
+"""
+
+from __future__ import annotations
+
+from itertools import product
+
+import torch
+from botorch.acquisition.multi_objective.base import (
+    MultiObjectiveAnalyticAcquisitionFunction,
+)
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.model import Model
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    NondominatedPartitioning,
+)
+from botorch.utils.transforms import t_batch_mode_transform
+from torch import Tensor
+from torch.distributions import Normal
+
+
+
+[docs] +class ExpectedHypervolumeImprovement(MultiObjectiveAnalyticAcquisitionFunction): + def __init__( + self, + model: Model, + ref_point: list[float], + partitioning: NondominatedPartitioning, + posterior_transform: PosteriorTransform | None = None, + ) -> None: + r"""Expected Hypervolume Improvement supporting m>=2 outcomes. + + This implements the computes EHVI using the algorithm from [Yang2019]_, but + additionally computes gradients via auto-differentiation as proposed by + [Daulton2020qehvi]_. + + Note: this is currently inefficient in two ways due to the binary partitioning + algorithm that we use for the box decomposition: + + - We have more boxes in our decomposition + - If we used a box decomposition that used `inf` as the upper bound for + the last dimension *in all hypercells*, then we could reduce the number + of terms we need to compute from 2^m to 2^(m-1). [Yang2019]_ do this + by using DKLV17 and LKF17 for the box decomposition. + + TODO: Use DKLV17 and LKF17 for the box decomposition as in [Yang2019]_ for + greater efficiency. + + TODO: Add support for outcome constraints. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> ref_point = [0.0, 0.0] + >>> EHVI = ExpectedHypervolumeImprovement(model, ref_point, partitioning) + >>> ehvi = EHVI(test_X) + + Args: + model: A fitted model. + ref_point: A list with `m` elements representing the reference point + (in the outcome space) w.r.t. to which compute the hypervolume. + This is a reference point for the outcome values (i.e., after + applying `posterior_transform` if provided). + partitioning: A `NondominatedPartitioning` module that provides the non- + dominated front and a partitioning of the non-dominated space in hyper- + rectangles. + posterior_transform: A `PosteriorTransform`. + """ + # TODO: we could refactor this __init__ logic into a + # HypervolumeAcquisitionFunction Mixin + if len(ref_point) != partitioning.num_outcomes: + raise ValueError( + "The length of the reference point must match the number of outcomes. " + f"Got ref_point with {len(ref_point)} elements, but expected " + f"{partitioning.num_outcomes}." + ) + ref_point = torch.tensor( + ref_point, + dtype=partitioning.pareto_Y.dtype, + device=partitioning.pareto_Y.device, + ) + better_than_ref = (partitioning.pareto_Y > ref_point).all(dim=1) + if not better_than_ref.any() and partitioning.pareto_Y.shape[0] > 0: + raise ValueError( + "At least one pareto point must be better than the reference point." + ) + super().__init__(model=model, posterior_transform=posterior_transform) + self.register_buffer("ref_point", ref_point) + self.partitioning = partitioning + cell_bounds = self.partitioning.get_hypercell_bounds() + self.register_buffer("cell_lower_bounds", cell_bounds[0]) + self.register_buffer("cell_upper_bounds", cell_bounds[1]) + # create indexing tensor of shape `2^m x m` + self._cross_product_indices = torch.tensor( + list(product(*[[0, 1] for _ in range(ref_point.shape[0])])), + dtype=torch.long, + device=ref_point.device, + ) + self.normal = Normal(0, 1) + +
+[docs] + def psi(self, lower: Tensor, upper: Tensor, mu: Tensor, sigma: Tensor) -> Tensor: + r"""Compute Psi function. + + For each cell i and outcome k: + + Psi(lower_{i,k}, upper_{i,k}, mu_k, sigma_k) = ( + sigma_k * PDF((upper_{i,k} - mu_k) / sigma_k) + ( + mu_k - lower_{i,k} + ) * (1 - CDF(upper_{i,k} - mu_k) / sigma_k) + ) + + See Equation 19 in [Yang2019]_ for more details. + + Args: + lower: A `num_cells x m`-dim tensor of lower cell bounds + upper: A `num_cells x m`-dim tensor of upper cell bounds + mu: A `batch_shape x 1 x m`-dim tensor of means + sigma: A `batch_shape x 1 x m`-dim tensor of standard deviations (clamped). + + Returns: + A `batch_shape x num_cells x m`-dim tensor of values. + """ + u = (upper - mu) / sigma + return sigma * self.normal.log_prob(u).exp() + (mu - lower) * ( + 1 - self.normal.cdf(u) + )
+ + +
+[docs] + def nu(self, lower: Tensor, upper: Tensor, mu: Tensor, sigma: Tensor) -> Tensor: + r"""Compute Nu function. + + For each cell i and outcome k: + + nu(lower_{i,k}, upper_{i,k}, mu_k, sigma_k) = ( + upper_{i,k} - lower_{i,k} + ) * (1 - CDF((upper_{i,k} - mu_k) / sigma_k)) + + See Equation 25 in [Yang2019]_ for more details. + + Args: + lower: A `num_cells x m`-dim tensor of lower cell bounds + upper: A `num_cells x m`-dim tensor of upper cell bounds + mu: A `batch_shape x 1 x m`-dim tensor of means + sigma: A `batch_shape x 1 x m`-dim tensor of standard deviations (clamped). + + Returns: + A `batch_shape x num_cells x m`-dim tensor of values. + """ + return (upper - lower) * (1 - self.normal.cdf((upper - mu) / sigma))
+ + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + posterior = self.model.posterior( + X, posterior_transform=self.posterior_transform + ) + mu = posterior.mean + sigma = posterior.variance.clamp_min(1e-9).sqrt() + # clamp here, since upper_bounds will contain `inf`s, which + # are not differentiable + cell_upper_bounds = self.cell_upper_bounds.clamp_max( + 1e10 if X.dtype == torch.double else 1e8 + ) + # Compute psi(lower_i, upper_i, mu_i, sigma_i) for i=0, ... m-2 + psi_lu = self.psi( + lower=self.cell_lower_bounds, upper=cell_upper_bounds, mu=mu, sigma=sigma + ) + # Compute psi(lower_m, lower_m, mu_m, sigma_m) + psi_ll = self.psi( + lower=self.cell_lower_bounds, + upper=self.cell_lower_bounds, + mu=mu, + sigma=sigma, + ) + # Compute nu(lower_m, upper_m, mu_m, sigma_m) + nu = self.nu( + lower=self.cell_lower_bounds, upper=cell_upper_bounds, mu=mu, sigma=sigma + ) + # compute the difference psi_ll - psi_lu + psi_diff = psi_ll - psi_lu + + # this is batch_shape x num_cells x 2 x (m-1) + stacked_factors = torch.stack([psi_diff, nu], dim=-2) + + # Take the cross product of psi_diff and nu across all outcomes + # e.g. for m = 2 + # for each batch and cell, compute + # [psi_diff_0, psi_diff_1] + # [nu_0, psi_diff_1] + # [psi_diff_0, nu_1] + # [nu_0, nu_1] + # this tensor has shape: `batch_shape x num_cells x 2^m x m` + all_factors_up_to_last = stacked_factors.gather( + dim=-2, + index=self._cross_product_indices.expand( + stacked_factors.shape[:-2] + self._cross_product_indices.shape + ), + ) + # compute product for all 2^m terms, + # sum across all terms and hypercells + return all_factors_up_to_last.prod(dim=-1).sum(dim=-1).sum(dim=-1)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/base.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/base.html new file mode 100644 index 0000000000..4bc2e0961e --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/base.html @@ -0,0 +1,225 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.base

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Base classes for multi-objective acquisition functions.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
+from botorch.acquisition.multi_objective.objective import (
+    IdentityMCMultiOutputObjective,
+    MCMultiOutputObjective,
+)
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.model import Model
+from botorch.models.transforms.input import InputPerturbation
+from botorch.sampling.base import MCSampler
+from torch import Tensor
+
+
+
+[docs] +class MultiObjectiveAnalyticAcquisitionFunction(AcquisitionFunction): + r"""Abstract base class for Multi-Objective batch acquisition functions.""" + + def __init__( + self, + model: Model, + posterior_transform: PosteriorTransform | None = None, + ) -> None: + r"""Constructor for the MultiObjectiveAnalyticAcquisitionFunction base class. + + Args: + model: A fitted model. + posterior_transform: A PosteriorTransform (optional). + """ + super().__init__(model=model) + if posterior_transform is None or isinstance( + posterior_transform, PosteriorTransform + ): + self.posterior_transform = posterior_transform + else: + raise UnsupportedError( + "Only a posterior_transform of type PosteriorTransform is " + "supported for Multi-Objective analytic acquisition functions." + ) + +
+[docs] + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Takes in a `batch_shape x 1 x d` X Tensor of t-batches with `1` `d`-dim + design point each, and returns a Tensor with shape `batch_shape'`, where + `batch_shape'` is the broadcasted batch shape of model and input `X`. + """ + pass # pragma: no cover
+ + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + raise UnsupportedError( + "Analytic acquisition functions do not account for X_pending yet." + )
+
+ + + +
+[docs] +class MultiObjectiveMCAcquisitionFunction(AcquisitionFunction, MCSamplerMixin, ABC): + r"""Abstract base class for Multi-Objective batch acquisition functions. + + NOTE: This does not inherit from `MCAcquisitionFunction` to avoid circular imports. + + Args: + _default_sample_shape: The `sample_shape` for the default sampler. + """ + + _default_sample_shape = torch.Size([128]) + + def __init__( + self, + model: Model, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + X_pending: Tensor | None = None, + ) -> None: + r"""Constructor for the `MultiObjectiveMCAcquisitionFunction` base class. + + Args: + model: A fitted model. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + NOTE: For posteriors that do not support base samples, + a sampler compatible with intended use case must be provided. + See `ForkedRNGSampler` and `StochasticSampler` as examples. + objective: The MCMultiOutputObjective under which the samples are + evaluated. Defaults to `IdentityMCMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same eta is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + eta value. + X_pending: A `m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. + """ + super().__init__(model=model) + MCSamplerMixin.__init__(self, sampler=sampler) + if objective is None: + objective = IdentityMCMultiOutputObjective() + elif not isinstance(objective, MCMultiOutputObjective): + raise UnsupportedError( + "Only objectives of type MCMultiOutputObjective are supported for " + "Multi-Objective MC acquisition functions." + ) + if ( + hasattr(model, "input_transform") + and isinstance(model.input_transform, InputPerturbation) + and constraints is not None + ): + raise UnsupportedError( + "Constraints are not supported with input perturbations, due to" + "sample q-batch shape being different than that of the inputs." + "Use a composite objective that applies feasibility weighting to" + "samples before calculating the risk measure." + ) + self.add_module("objective", objective) + self.constraints = constraints + if constraints: + if type(eta) is not Tensor: + eta = torch.full((len(constraints),), eta) + self.register_buffer("eta", eta) + self.X_pending = None + if X_pending is not None: + self.set_X_pending(X_pending) + +
+[docs] + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Takes in a `batch_shape x q x d` X Tensor of t-batches with `q` `d`-dim + design points each, and returns a Tensor with shape `batch_shape'`, where + `batch_shape'` is the broadcasted batch shape of model and input `X`. Should + utilize the result of `set_X_pending` as needed to account for pending function + evaluations. + """ + pass # pragma: no cover
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.html new file mode 100644 index 0000000000..f9270b7ef3 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.html @@ -0,0 +1,648 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.hypervolume_knowledge_gradient

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+"""
+The hypervolume knowledge gradient acquisition function (HVKG).
+
+References:
+
+.. [Daulton2023hvkg]
+    S. Daulton, M. Balandat, E. Bakshy. Hypervolume Knowledge Gradient: A
+    Lookahead Approach for Multi-Objective Bayesian Optimization with Partial
+    Information. Proceedings of the 40th International Conference on Machine
+    Learning, 2023.
+"""
+
+import warnings
+from collections.abc import Callable
+from copy import deepcopy
+from typing import Any
+
+import torch
+from botorch import settings
+from botorch.acquisition.acquisition import (
+    AcquisitionFunction,
+    OneShotAcquisitionFunction,
+)
+
+from botorch.acquisition.cost_aware import CostAwareUtility
+from botorch.acquisition.decoupled import DecoupledAcquisitionFunction
+from botorch.acquisition.knowledge_gradient import ProjectedAcquisitionFunction
+from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction
+from botorch.acquisition.multi_objective.monte_carlo import (
+    qExpectedHypervolumeImprovement,
+)
+from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
+from botorch.exceptions.errors import UnsupportedError
+from botorch.exceptions.warnings import NumericsWarning
+from botorch.models.deterministic import PosteriorMeanModel
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.sampling.list_sampler import ListSampler
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.sampling.stochastic_samplers import StochasticSampler
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    FastNondominatedPartitioning,
+)
+from botorch.utils.transforms import match_batch_shape, t_batch_mode_transform
+from torch import Tensor
+
+
+
+[docs] +class qHypervolumeKnowledgeGradient( + DecoupledAcquisitionFunction, + MultiObjectiveMCAcquisitionFunction, + OneShotAcquisitionFunction, +): + """Batch Hypervolume Knowledge Gradient using one-shot optimization. + + The hypervolume knowledge gradient seeks to maximize the difference in + hypervolume of the hypervolume-maximizing set of a fixed size after + conditioning the unknown observation(s) that would be recevied if X where + evalauted. See [Daulton2023hvkg]_ for details. + + This computes the batch Hypervolume Knowledge Gradient using fantasies for + the outer expectation and MC-sampling for the inner expectation. + + In addition to the design variables, the input `X` also includes variables + for the optimal designs for each of the fantasy models (Note this is + `N x N_pareto` optimal designs). For a fixed number of fantasies, all points + in `X` can be optimized in a "one-shot" fashion. + """ + + def __init__( + self, + model: Model, + ref_point: Tensor, + num_fantasies: int = 8, + num_pareto: int = 10, + sampler: ListSampler | None = None, + objective: MCMultiOutputObjective | None = None, + inner_sampler: MCSampler | None = None, + X_evaluation_mask: list[Tensor] | None = None, + X_pending: Tensor | None = None, + X_pending_evaluation_mask: Tensor | None = None, + current_value: Tensor | None = None, + use_posterior_mean: bool = True, + cost_aware_utility: CostAwareUtility | None = None, + ) -> None: + r"""q-Hypervolume Knowledge Gradient. + + Args: + model: A fitted model. Must support fantasizing. + ref_point: A `m`-dim tensor containing the reference point. + num_fantasies: The number of fantasy points to use. More fantasy + points result in a better approximation, at the expense of + memory and wall time. Unused if `sampler` is specified. + num_pareto: The number of pareto optimal designs to consider. + sampler: The sampler used to sample fantasy observations. Optional + if `num_fantasies` is specified. The optimization performance + does not seem particularly sensitive to the number of fantasies. + As the number of fantasies increases, the estimation of the + expectation over fantasies becomes more accurate, but the one- + shot optimization problem gets harder as there are more "fantasy" + designs that need to be optimized. + objective: The objective under which the samples are evaluated. If + `None`, then the analytic posterior mean is used. Otherwise, the + objective is MC-evaluated (using inner_sampler). + inner_sampler: The sampler used for inner sampling. Ignored if the + objective is `None`. + X_evaluation_mask: A `q x m`-dim tensor of booleans indicating which + objective(s) each of the `q` points should be evaluated on. + X_pending: A `n' x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. + X_pending_evaluation_mask: A `n' x m`-dim tensor of booleans indicating + which objective(s) each of the `n'` pending points are being + evaluated on. + current_value: The current value, i.e. the expected best objective + given the observed points `D`. If omitted, forward will not + return the actual KG value, but the expected best objective + given the data set `D u X`. If pending points are used, + this should be the current value under the fantasy model + conditioned on the pending points so that the incremental KG value + from the new candidates (not pending points) is used. + use_posterior_mean: If true, optimize the hypervolume of the posterior + mean, otherwise optimize the expected hypervolume. See + [Daulton2023hvkg]_ for details. + cost_aware_utility: A CostAwareUtility specifying the cost function for + evaluating the `X` on the objectives indicated by `evaluation_mask`. + """ + if sampler is None: + # base samples should be fixed for joint optimization over X, X_fantasies + samplers = [ + SobolQMCNormalSampler(sample_shape=torch.Size([num_fantasies])) + for _ in range(model.num_outputs) + ] + sampler = ListSampler(*samplers) + else: + sample_shape = sampler.samplers[0].sample_shape + if sample_shape != torch.Size([num_fantasies]): + raise ValueError( + f"The sampler shape must match num_fantasies={num_fantasies}." + ) + super().__init__(model=model, X_evaluation_mask=X_evaluation_mask) + + if inner_sampler is None: + inner_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([32])) + if current_value is None and cost_aware_utility is not None: + raise UnsupportedError( + "Cost-aware HVKG requires current_value to be specified." + ) + self.register_buffer("ref_point", ref_point) + self.sampler = sampler + self.objective = objective + self.set_X_pending( + X_pending=X_pending, X_pending_evaluation_mask=X_pending_evaluation_mask + ) + self.inner_sampler = inner_sampler + self.num_fantasies = num_fantasies + self.num_pareto = num_pareto + self.num_pseudo_points = num_fantasies * num_pareto + self.current_value = current_value + self.use_posterior_mean = use_posterior_mean + self.cost_aware_utility = cost_aware_utility + self._cost_sampler = None + + @property + def cost_sampler(self): + if self._cost_sampler is None: + # Note: Using the deepcopy here is essential. Removing this poses a + # problem if the base model and the cost model have a different number + # of outputs or test points (this would be caused by expand), as this + # would trigger re-sampling the base samples in the fantasy sampler. + # By cloning the sampler here, the right thing will happen if the + # the sizes are compatible, if they are not this will result in + # samples being drawn using different base samples, but it will at + # least avoid changing state of the fantasy sampler. + self._cost_sampler = deepcopy(self.sampler) + return self._cost_sampler + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qKnowledgeGradient on the candidate set `X`. + + Args: + X: A `b x (q + num_fantasies) x d` Tensor with `b` t-batches of + `q + num_fantasies` design points each. We split this X tensor + into two parts in the `q` dimension (`dim=-2`). The first `q` + are the q-batch of design points and the last num_fantasies are + the current solutions of the inner optimization problem. + + `X_fantasies = X[..., -num_fantasies:, :]` + `X_fantasies.shape = b x num_fantasies x d` + + `X_actual = X[..., :-num_fantasies, :]` + `X_actual.shape = b x q x d` + + Returns: + A Tensor of shape `b`. For t-batch b, the q-KG value of the design + `X_actual[b]` is averaged across the fantasy models, where + `X_fantasies[b, i]` is chosen as the final selection for the + `i`-th fantasy model. + NOTE: If `current_value` is not provided, then this is not the + true KG value of `X_actual[b]`, and `X_fantasies[b, : ]` must be + maximized at fixed `X_actual[b]`. + """ + X_actual, X_fantasies = _split_hvkg_fantasy_points( + X=X, n_f=self.num_fantasies, num_pareto=self.num_pareto + ) + q = X_actual.shape[-2] + + # construct evaluation_mask + evaluation_mask = self.construct_evaluation_mask(X=X_actual) + # We only concatenate X_pending into the X part after splitting + if self.X_pending is not None: + X_actual = torch.cat( + [X_actual, match_batch_shape(self.X_pending, X_actual)], dim=-2 + ) + + # Construct the fantasy model of shape `num_fantasies x b` + # Note: For the decoupled, cost-aware (e.g. not async) setting, we + # technically want to make sure to copy the base samples here, so + # that the same fantasies are used for X_pending on the left and + # right of the KG terms. + fantasy_model = self.model.fantasize( + X=X_actual, + sampler=self.sampler, + evaluation_mask=evaluation_mask, + ) + + # get the value function + value_function = _get_hv_value_function( + model=fantasy_model, + ref_point=self.ref_point, + objective=self.objective, + sampler=self.inner_sampler, + use_posterior_mean=self.use_posterior_mean, + ) + + # make sure to propagate gradients to the fantasy model train inputs + with settings.propagate_grads(True): + # X_fantasies is num_pseudo_points x batch_shape x 1 x d + # Reshape it into num_fantasies x batch_shape x num_pareto x d + shape = torch.Size( + [ + self.num_fantasies, + *X_fantasies.shape[1:-2], + self.num_pareto, + X_fantasies.shape[-1], + ] + ) + values = value_function(X=X_fantasies.reshape(shape)) # num_fantasies x b + + if self.current_value is not None: + values = values - self.current_value + + if self.cost_aware_utility is not None: + values = self.cost_aware_utility( + # exclude pending points + X=X_actual[..., :q, :], + deltas=values, + sampler=self.cost_sampler, + X_evaluation_mask=self.X_evaluation_mask, + ) + + # return average over the fantasy samples + return values.mean(dim=0)
+ + +
+[docs] + def get_augmented_q_batch_size(self, q: int) -> int: + r"""Get augmented q batch size for one-shot optimization. + + Args: + q: The number of candidates to consider jointly. + + Returns: + The augmented size for one-shot optimization (including variables + parameterizing the fantasy solutions). + """ + return q + self.num_pseudo_points
+ + +
+[docs] + def extract_candidates(self, X_full: Tensor) -> Tensor: + r"""We only return X as the set of candidates post-optimization. + + Args: + X_full: A `b x (q + num_fantasies) x d`-dim Tensor with `b` + t-batches of `q + num_fantasies` design points each. + + Returns: + A `b x q x d`-dim Tensor with `b` t-batches of `q` design points each. + """ + return X_full[..., : -self.num_pseudo_points, :]
+
+ + + +
+[docs] +class qMultiFidelityHypervolumeKnowledgeGradient(qHypervolumeKnowledgeGradient): + r"""Batch Hypervolume Knowledge Gradient for multi-fidelity optimization. + + See [Daulton2023hvkg]_ for details. + + A version of `qHypervolumeKnowledgeGradient` that supports multi-fidelity + optimization via a `CostAwareUtility` and the `project` and `expand` + operators. If none of these are set, this acquisition function reduces to + `qHypervolumeKnowledgeGradient`. Through `valfunc_cls` and `valfunc_argfac`, + this can be changed into a custom multi-fidelity acquisition function. + """ + + def __init__( + self, + model: Model, + ref_point: Tensor, + target_fidelities: dict[int, float], + num_fantasies: int = 8, + num_pareto: int = 10, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + inner_sampler: MCSampler | None = None, + X_pending: Tensor | None = None, + X_evaluation_mask: Tensor | None = None, + X_pending_evaluation_mask: Tensor | None = None, + current_value: Tensor | None = None, + cost_aware_utility: CostAwareUtility | None = None, + project: Callable[[Tensor], Tensor] = lambda X: X, + valfunc_cls: type[AcquisitionFunction] | None = None, + valfunc_argfac: Callable[[Model], dict[str, Any]] | None = None, + use_posterior_mean: bool = True, + **kwargs: Any, + ) -> None: + r"""Multi-Fidelity q-Knowledge Gradient (one-shot optimization). + + Args: + model: A fitted model. Must support fantasizing. + ref_point: A `m`-dim tensor containing the reference point. + num_fantasies: The number of fantasy points to use. More fantasy + points result in a better approximation, at the expense of + memory and wall time. Unused if `sampler` is specified. + num_pareto: The number of pareto optimal designs to consider. + sampler: The sampler used to sample fantasy observations. Optional + if `num_fantasies` is specified. + objective: The objective under which the samples are evaluated. If + `None`, then the analytic posterior mean is used. Otherwise, the + objective is MC-evaluated (using inner_sampler). + inner_sampler: The sampler used for inner sampling. Ignored if the + objective is `None`. + X_evaluation_mask: A `q x m`-dim tensor of booleans indicating which + objective(s) each of the `q` points should be evaluated on. + X_pending: A `n' x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. + X_pending_evaluation_mask: A `n' x m`-dim tensor of booleans indicating + which objective(s) each of the `n'` pending points are being + evaluated on. + current_value: The current value, i.e. the expected best objective + given the observed points `D`. If omitted, forward will not + return the actual KG value, but the expected best objective + given the data set `D u X`. If pending points are used, + this should be the current value under the fantasy model + conditioned on the pending points so that the incremental KG value + from the new candidates (not pending points) is used. + use_posterior_mean: A boolean indicating whether to use the to optimize + the hypervolume of the posterior mean or whether to optimize the + expected hypervolume. See [Daulton2023hvkg]_ for details. + cost_aware_utility: A CostAwareUtility specifying the cost function for + evaluating the `X` on the objectives indicated by `evaluation_mask`. + project: A callable mapping a `batch_shape x q x d` tensor of design + points to a tensor with shape `batch_shape x q_term x d` projected + to the desired target set (e.g. the target fidelities in case of + multi-fidelity optimization). For the basic case, `q_term = q`. + valfunc_cls: An acquisition function class to be used as the terminal + value function. + valfunc_argfac: An argument factory, i.e. callable that maps a `Model` + to a dictionary of kwargs for the terminal value function (e.g. + `best_f` for `ExpectedImprovement`). + """ + + super().__init__( + model=model, + ref_point=ref_point, + num_fantasies=num_fantasies, + num_pareto=num_pareto, + sampler=sampler, + objective=objective, + inner_sampler=inner_sampler, + X_evaluation_mask=X_evaluation_mask, + X_pending=X_pending, + X_pending_evaluation_mask=X_pending_evaluation_mask, + current_value=current_value, + use_posterior_mean=use_posterior_mean, + cost_aware_utility=cost_aware_utility, + ) + self.project = project + if kwargs.get("expand") is not None: + raise NotImplementedError( + "Trace observations are not currently supported " + "by `qMultiFidelityHypervolumeKnowledgeGradient`." + ) + self.expand = lambda X: X + self.valfunc_cls = valfunc_cls + self.valfunc_argfac = valfunc_argfac + self.target_fidelities = target_fidelities + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qMultiFidelityKnowledgeGradient on the candidate set `X`. + + Args: + X: A `b x (q + num_fantasies) x d` Tensor with `b` t-batches of + `q + num_fantasies` design points each. We split this X tensor + into two parts in the `q` dimension (`dim=-2`). The first `q` + are the q-batch of design points and the last num_fantasies are + the current solutions of the inner optimization problem. + + `X_fantasies = X[..., -num_fantasies:, :]` + `X_fantasies.shape = b x num_fantasies x d` + + `X_actual = X[..., :-num_fantasies, :]` + `X_actual.shape = b x q x d` + + In addition, `X` may be augmented with fidelity parameteres as + part of thee `d`-dimension. Projecting fidelities to the target + fidelity is handled by `project`. + + Returns: + A Tensor of shape `b`. For t-batch b, the q-KG value of the design + `X_actual[b]` is averaged across the fantasy models, where + `X_fantasies[b, i]` is chosen as the final selection for the + `i`-th fantasy model. + NOTE: If `current_value` is not provided, then this is not the + true KG value of `X_actual[b]`, and `X_fantasies[b, : ]` must be + maximized at fixed `X_actual[b]`. + """ + X_actual, X_fantasies = _split_hvkg_fantasy_points( + X=X, n_f=self.num_fantasies, num_pareto=self.num_pareto + ) + q = X_actual.shape[-2] + + # construct evaluation_mask + evaluation_mask = self.construct_evaluation_mask(X=X_actual) + + # We only concatenate X_pending into the X part after splitting + if self.X_pending is not None: + X_actual = torch.cat( + [X_actual, match_batch_shape(self.X_pending, X_actual)], dim=-2 + ) + + # construct the fantasy model of shape `num_fantasies x b` + fantasy_model = self.model.fantasize( + X=X_actual, + sampler=self.sampler, + evaluation_mask=evaluation_mask, + ) + # get the value function + value_function = _get_hv_value_function( + model=fantasy_model, + ref_point=self.ref_point, + objective=self.objective, + sampler=self.inner_sampler, + project=self.project, + valfunc_cls=self.valfunc_cls, + valfunc_argfac=self.valfunc_argfac, + use_posterior_mean=self.use_posterior_mean, + ) + + # make sure to propagate gradients to the fantasy model train inputs + with settings.propagate_grads(True): + # X_fantasies is num_pseudo_points x batch_shape x 1 x d + # Reshape it into num_fantasies x batch_shape x num_pareto x d + shape = torch.Size( + [ + self.num_fantasies, + *X_fantasies.shape[1:-2], + self.num_pareto, + X_fantasies.shape[-1], + ] + ) + values = value_function(X=X_fantasies.reshape(shape)) # num_fantasies x b + if self.current_value is not None: + values = values - self.current_value + + if self.cost_aware_utility is not None: + values = self.cost_aware_utility( + # exclude pending points + X=X_actual[..., :q, :], + deltas=values, + sampler=self.cost_sampler, + X_evaluation_mask=self.X_evaluation_mask, + ) + + # return average over the fantasy samples + return values.mean(dim=0)
+
+ + + +def _get_hv_value_function( + model: Model, + ref_point: Tensor, + objective: MCMultiOutputObjective | None = None, + sampler: MCSampler | None = None, + project: Callable[[Tensor], Tensor] | None = None, + valfunc_cls: type[AcquisitionFunction] | None = None, + valfunc_argfac: Callable[[Model], dict[str, Any]] | None = None, + use_posterior_mean: bool = False, +) -> AcquisitionFunction: + r"""Construct value function (i.e. inner acquisition function). + This is a method for computing hypervolume. + """ + if use_posterior_mean: + model = PosteriorMeanModel(model=model) + sampler = StochasticSampler(sample_shape=torch.Size([1])) # dummy sampler + with warnings.catch_warnings(): + warnings.filterwarnings( + message="qExpectedHypervolumeImprovement has known", + action="ignore", + category=NumericsWarning, + ) + base_value_function = qExpectedHypervolumeImprovement( + model=model, + ref_point=ref_point, + partitioning=FastNondominatedPartitioning( + ref_point=ref_point, + Y=torch.empty( + (0, ref_point.shape[0]), + dtype=ref_point.dtype, + device=ref_point.device, + ), + ), # create empty partitioning + sampler=sampler, + objective=objective, + ) + # ProjectedAcquisitionFunction requires this + base_value_function.posterior_transform = None + + if project is None: + return base_value_function + else: + return ProjectedAcquisitionFunction( + base_value_function=base_value_function, + project=project, + ) + + +def _split_hvkg_fantasy_points( + X: Tensor, n_f: int, num_pareto: int +) -> tuple[Tensor, Tensor]: + r"""Split a one-shot HV-KGoptimization input into actual and fantasy points + + Args: + X: A `batch_shape x (q + n_f*num_pareto) x d`-dim tensor of actual + and fantasy points + + Returns: + 2-element tuple containing + + - A `batch_shape x q x d`-dim tensor `X_actual` of input candidates. + - A `n_f x batch_shape x num_pareto x d`-dim tensor `X_fantasies` of + fantasy points, where `X_fantasies[i, batch_idx]` is the i-th + fantasy point associated with the batch indexed by `batch_idx`. + """ + if n_f * num_pareto > X.size(-2): + raise ValueError( + f"`n_f*num_pareto` ({n_f * num_pareto}) must be less than" + f" the `q`-batch dimension of `X` ({X.size(-2)})." + ) + split_sizes = [X.size(-2) - n_f * num_pareto, n_f * num_pareto] + X_actual, X_fantasies = torch.split(X, split_sizes, dim=-2) + # X_fantasies is b x n_f * num_pareto x d, needs to be n_f x b x num_pareto x d + # reshape into num_fantasies x b x num_pareto x d + new_shape = torch.Size( + [n_f, *X_fantasies.shape[:-2], num_pareto, X_fantasies.shape[-1]] + ) + X_fantasies = X_fantasies.reshape(new_shape) + # n_f x b x num_pareto x d + return X_actual, X_fantasies +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/joint_entropy_search.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/joint_entropy_search.html new file mode 100644 index 0000000000..5317b545bd --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/joint_entropy_search.html @@ -0,0 +1,812 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.joint_entropy_search

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Acquisition functions for joint entropy search for Bayesian optimization (JES).
+
+References:
+
+.. [Tu2022]
+    B. Tu, A. Gandy, N. Kantas and B.Shafei. Joint Entropy Search for Multi-Objective
+    Bayesian Optimization. Advances in Neural Information Processing Systems, 35.
+    2022.
+
+"""
+
+from __future__ import annotations
+
+from abc import abstractmethod
+from math import pi
+
+import torch
+from botorch import settings
+from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.model import Model
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.utils import fantasize as fantasize_flag
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+from torch.distributions import Normal
+
+
+
+[docs] +class LowerBoundMultiObjectiveEntropySearch(AcquisitionFunction, MCSamplerMixin): + r"""Abstract base class for the lower bound multi-objective entropy search + acquisition functions. + """ + + def __init__( + self, + model: Model, + pareto_sets: Tensor, + pareto_fronts: Tensor, + hypercell_bounds: Tensor, + X_pending: Tensor | None = None, + estimation_type: str = "LB", + num_samples: int = 64, + ) -> None: + r"""Lower bound multi-objective entropy search acquisition function. + + Args: + model: A fitted batch model with 'M' number of outputs. + pareto_sets: A `num_pareto_samples x num_pareto_points x d`-dim Tensor + containing the sampled Pareto optimal sets of inputs. + pareto_fronts: A `num_pareto_samples x num_pareto_points x M`-dim Tensor + containing the sampled Pareto optimal sets of outputs. + hypercell_bounds: A `num_pareto_samples x 2 x J x M`-dim Tensor + containing the hyper-rectangle bounds for integration, where `J` is + the number of hyper-rectangles. In the unconstrained case, this gives + the partition of the dominated space. In the constrained case, this + gives the partition of the feasible dominated space union the + infeasible space. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation, but have not yet been evaluated. + estimation_type: A string to determine which entropy estimate is + computed: "0", "LB", "LB2", or "MC". + num_samples: The number of Monte Carlo samples for the Monte Carlo + estimate. + """ + super().__init__(model=model) + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples])) + MCSamplerMixin.__init__(self, sampler=sampler) + # Batch GP models (e.g. fantasized models) are not currently supported + if isinstance(model, ModelListGP): + train_X = model.models[0].train_inputs[0] + else: + train_X = model.train_inputs[0] + if (model.num_outputs > 1 and train_X.ndim > 3) or ( + model.num_outputs == 1 and train_X.ndim > 2 + ): + raise NotImplementedError( + "Batch GP models (e.g. fantasized models) are not supported." + ) + + self.initial_model = model + if (pareto_sets is not None and pareto_sets.ndim != 3) or ( + pareto_fronts is not None and pareto_fronts.ndim != 3 + ): + raise UnsupportedError( + "The Pareto set and front should have a shape of " + "`num_pareto_samples x num_pareto_points x input_dim` and " + "`num_pareto_samples x num_pareto_points x num_objectives`, " + "respectively" + ) + else: + self.pareto_sets = pareto_sets + self.pareto_fronts = pareto_fronts + + if hypercell_bounds.ndim != 4: + raise UnsupportedError( + "The hypercell_bounds should have a shape of " + "`num_pareto_samples x 2 x num_boxes x num_objectives`." + ) + else: + self.hypercell_bounds = hypercell_bounds + self.num_pareto_samples = hypercell_bounds.shape[0] + + self.estimation_type = estimation_type + estimation_types = ["0", "LB", "LB2", "MC"] + + if estimation_type not in estimation_types: + raise NotImplementedError( + "Currently the only supported estimation type are: " + + ", ".join(f'"{h}"' for h in estimation_types) + + "." + ) + + self.set_X_pending(X_pending) + + @abstractmethod + def _compute_posterior_statistics( + self, X: Tensor + ) -> dict[str, GPyTorchPosterior | Tensor]: + r"""Compute the posterior statistics. + + Args: + X: A `batch_shape x q x d`-dim Tensor of inputs. + + Returns: + A dictionary containing the posterior variables used to estimate the + entropy. + + - "initial_entropy": A `batch_shape`-dim Tensor containing the entropy of + the Gaussian random variable `p(Y| X, D_n)`. + - "posterior_mean": A `batch_shape x num_pareto_samples x q x 1 x M`-dim + Tensor containing the posterior mean at the input `X`. + - "posterior_variance": A `batch_shape x num_pareto_samples x q x 1 x M` + -dim Tensor containing the posterior variance at the input `X` + excluding the observation noise. + - "observation_noise": A `batch_shape x num_pareto_samples x q x 1 x M` + -dim Tensor containing the observation noise at the input `X`. + - "posterior_with_noise": The posterior distribution at `X` which + includes the observation noise. This is used to compute the marginal + log-probabilities with respect to `p(y| x, D_n)` for `x` in `X`. + """ + + pass # pragma: no cover + + @abstractmethod + def _compute_monte_carlo_variables( + self, posterior: GPyTorchPosterior + ) -> tuple[Tensor, Tensor]: + r"""Compute the samples and log-probability associated with a posterior + distribution. + + Args: + posterior: A posterior distribution. + + Returns: + A two-element tuple containing: + + - samples: A `num_mc_samples x batch_shape x num_pareto_samples x q x 1 + x M`-dim Tensor containing the Monte Carlo samples. + - samples_log_prob: A `num_mc_samples x batch_shape x num_pareto_samples + x q`-dim Tensor containing the log-probabilities of the Monte Carlo + samples. + """ + + pass # pragma: no cover + + def _compute_lower_bound_information_gain(self, X: Tensor) -> Tensor: + r"""Evaluates the lower bound information gain at the design points `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of `batch_shape` t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of acquisition values at the given design + points `X`. + """ + posterior_statistics = self._compute_posterior_statistics(X) + initial_entropy = posterior_statistics["initial_entropy"] + post_mean = posterior_statistics["posterior_mean"] + post_var = posterior_statistics["posterior_variance"] + obs_noise = posterior_statistics["observation_noise"] + + # Estimate the expected conditional entropy. + # `batch_shape x q` dim Tensor of entropy estimates + if self.estimation_type == "0": + conditional_entropy = _compute_entropy_noiseless( + hypercell_bounds=self.hypercell_bounds, + mean=post_mean, + variance=post_var, + observation_noise=obs_noise, + ) + + elif self.estimation_type == "LB": + conditional_entropy = _compute_entropy_upper_bound( + hypercell_bounds=self.hypercell_bounds, + mean=post_mean, + variance=post_var, + observation_noise=obs_noise, + only_diagonal=False, + ) + + elif self.estimation_type == "LB2": + conditional_entropy = _compute_entropy_upper_bound( + hypercell_bounds=self.hypercell_bounds, + mean=post_mean, + variance=post_var, + observation_noise=obs_noise, + only_diagonal=True, + ) + + elif self.estimation_type == "MC": + posterior_with_noise = posterior_statistics["posterior_with_noise"] + samples, samples_log_prob = self._compute_monte_carlo_variables( + posterior_with_noise + ) + + conditional_entropy = _compute_entropy_monte_carlo( + hypercell_bounds=self.hypercell_bounds, + mean=post_mean, + variance=post_var, + observation_noise=obs_noise, + samples=samples, + samples_log_prob=samples_log_prob, + ) + + # Sum over the batch. + return initial_entropy - conditional_entropy.sum(dim=-1) + +
+[docs] + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Compute lower bound multi-objective entropy search at the design points + `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of `batch_shape` t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of acquisition values at the given design + points `X`. + """ + + pass # pragma: no cover
+
+ + + +
+[docs] +class qLowerBoundMultiObjectiveJointEntropySearch( + LowerBoundMultiObjectiveEntropySearch +): + r"""The acquisition function for the multi-objective joint entropy search, where + the batches `q > 1` are supported through the lower bound formulation. + + This acquisition function computes the mutual information between the observation + at a candidate point `X` and the Pareto optimal input-output pairs. + + See [Tu2022]_ for a discussion on the estimation procedure. + + NOTES: + (i) The estimated acquisition value could be negative. + + (ii) The lower bound batch acquisition function might not be monotone in the + sense that adding more elements to the batch does not necessarily increase the + acquisition value. Specifically, the acquisition value can become smaller when + more inputs are added. + """ + + def __init__( + self, + model: Model, + pareto_sets: Tensor, + pareto_fronts: Tensor, + hypercell_bounds: Tensor, + X_pending: Tensor | None = None, + estimation_type: str = "LB", + num_samples: int = 64, + ) -> None: + r"""Lower bound multi-objective joint entropy search acquisition function. + + Args: + model: A fitted batch model with 'M' number of outputs. + pareto_sets: A `num_pareto_samples x num_pareto_points x d`-dim Tensor + containing the sampled Pareto optimal sets of inputs. + pareto_fronts: A `num_pareto_samples x num_pareto_points x M`-dim Tensor + containing the sampled Pareto optimal sets of outputs. + hypercell_bounds: A `num_pareto_samples x 2 x J x M`-dim Tensor + containing the hyper-rectangle bounds for integration. In the + unconstrained case, this gives the partition of the dominated space. + In the constrained case, this gives the partition of the feasible + dominated space union the infeasible space. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation, but have not yet been evaluated. + estimation_type: A string to determine which entropy estimate is + computed: "0", "LB", "LB2", or "MC". + num_samples: The number of Monte Carlo samples used for the Monte Carlo + estimate. + """ + super().__init__( + model=model, + pareto_sets=pareto_sets, + pareto_fronts=pareto_fronts, + hypercell_bounds=hypercell_bounds, + X_pending=X_pending, + estimation_type=estimation_type, + num_samples=num_samples, + ) + + # Condition the model on the sampled pareto optimal points. + # TODO: Apparently, we need to make a call to the posterior otherwise + # we run into a gpytorch runtime error: + # "Fantasy observations can only be added after making predictions with a + # model so that all test independent caches exist." + + with fantasize_flag(): + with settings.propagate_grads(False): + _ = self.initial_model.posterior( + self.pareto_sets, observation_noise=False + ) + # Condition with observation noise. + self.conditional_model = self.initial_model.condition_on_observations( + X=self.initial_model.transform_inputs(self.pareto_sets), + Y=self.pareto_fronts, + ) + + def _compute_posterior_statistics( + self, X: Tensor + ) -> dict[str, Tensor | GPyTorchPosterior]: + r"""Compute the posterior statistics. + Args: + X: A `batch_shape x q x d`-dim Tensor of inputs. + + Returns: + A dictionary containing the posterior variables used to estimate the + entropy. + + - "initial_entropy": A `batch_shape`-dim Tensor containing the entropy of + the Gaussian random variable `p(Y| X, D_n)`. + - "posterior_mean": A `batch_shape x num_pareto_samples x q x 1 x M`-dim + Tensor containing the posterior mean at the input `X`. + - "posterior_variance": A `batch_shape x num_pareto_samples x q x 1 x M` + -dim Tensor containing the posterior variance at the input `X` + excluding the observation noise. + - "observation_noise": A `batch_shape x num_pareto_samples x q x 1 x M` + -dim Tensor containing the observation noise at the input `X`. + - "posterior_with_noise": The posterior distribution at `X` which + includes the observation noise. This is used to compute the marginal + log-probabilities with respect to `p(y| x, D_n)` for `x` in `X`. + """ + tkwargs = {"dtype": X.dtype, "device": X.device} + CLAMP_LB = torch.finfo(tkwargs["dtype"]).eps + + # Compute the prior entropy term depending on `X`. + initial_posterior_plus_noise = self.initial_model.posterior( + X, observation_noise=True + ) + + # Additional constant term. + add_term = ( + 0.5 + * self.model.num_outputs + * (1 + torch.log(2 * pi * torch.ones(1, **tkwargs))) + ) + # The variance initially has shape `batch_shape x (q*M) x (q*M)` + # prior_entropy has shape `batch_shape`. + initial_entropy = add_term + 0.5 * torch.logdet( + initial_posterior_plus_noise.mvn.covariance_matrix + ) + + posterior_statistics = {"initial_entropy": initial_entropy} + + # Compute the posterior entropy term. + conditional_posterior_with_noise = self.conditional_model.posterior( + X.unsqueeze(-2).unsqueeze(-3), observation_noise=True + ) + + # `batch_shape x num_pareto_samples x q x 1 x M` + post_mean = conditional_posterior_with_noise.mean.swapaxes(-4, -3) + post_var_with_noise = conditional_posterior_with_noise.variance.clamp_min( + CLAMP_LB + ).swapaxes(-4, -3) + + # TODO: This computes the observation noise via a second evaluation of the + # posterior. This step could be done better. + conditional_posterior = self.conditional_model.posterior( + X.unsqueeze(-2).unsqueeze(-3), observation_noise=False + ) + + # `batch_shape x num_pareto_samples x q x 1 x M` + post_var = conditional_posterior.variance.clamp_min(CLAMP_LB).swapaxes(-4, -3) + obs_noise = (post_var_with_noise - post_var).clamp_min(CLAMP_LB) + + posterior_statistics["posterior_mean"] = post_mean + posterior_statistics["posterior_variance"] = post_var + posterior_statistics["observation_noise"] = obs_noise + posterior_statistics["posterior_with_noise"] = conditional_posterior_with_noise + + return posterior_statistics + + def _compute_monte_carlo_variables( + self, posterior: GPyTorchPosterior + ) -> tuple[Tensor, Tensor]: + r"""Compute the samples and log-probability associated with the posterior + distribution that conditions on the Pareto optimal points. + + Args: + posterior: The conditional posterior distribution at an input `X`, where + we have also conditioned over the `num_pareto_samples` of optimal + points. Note that this posterior includes the observation noise. + + Returns: + A two-element tuple containing + + - samples: A `num_mc_samples x batch_shape x num_pareto_samples x q x 1 + x M`-dim Tensor containing the Monte Carlo samples. + - samples_log_probs: A `num_mc_samples x batch_shape x num_pareto_samples + x q`-dim Tensor containing the log-probabilities of the Monte Carlo + samples. + """ + # `num_mc_samples x batch_shape x q x num_pareto_samples x 1 x M` + samples = self.get_posterior_samples(posterior) + + # `num_mc_samples x batch_shape x q x num_pareto_samples` + if self.model.num_outputs == 1: + samples_log_prob = posterior.mvn.log_prob(samples.squeeze(-1)) + else: + samples_log_prob = posterior.mvn.log_prob(samples) + + # Swap axes to get the correct shape: + # samples:`num_mc_samples x batch_shape x num_pareto_samples x q x 1 x M` + # log prob:`num_mc_samples x batch_shape x num_pareto_samples x q` + + return samples.swapaxes(-4, -3), samples_log_prob.swapaxes(-2, -1) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluates qLowerBoundMultiObjectiveJointEntropySearch at the design + points `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of `batch_shape` t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of acquisition values at the given design + points `X`. + """ + return self._compute_lower_bound_information_gain(X)
+
+ + + +def _compute_entropy_noiseless( + hypercell_bounds: Tensor, + mean: Tensor, + variance: Tensor, + observation_noise: Tensor, +) -> Tensor: + r"""Computes the entropy estimate at the design points `X` assuming noiseless + observations. This is used for the JES-0 and MES-0 estimate. + + Args: + hypercell_bounds: A `num_pareto_samples x 2 x J x M` -dim Tensor containing + the box decomposition bounds, where `J = max(num_boxes)`. + mean: A `batch_shape x num_pareto_samples x q x 1 x M`-dim Tensor containing + the posterior mean at X. + variance: A `batch_shape x num_pareto_samples x q x 1 x M`-dim Tensor + containing the posterior variance at X excluding observation noise. + observation_noise: A `batch_shape x num_pareto_samples x q x 1 x M`-dim + Tensor containing the observation noise at X. + + Returns: + A `batch_shape x q`-dim Tensor of entropy estimate at the given design points + `X`. + """ + tkwargs = {"dtype": hypercell_bounds.dtype, "device": hypercell_bounds.device} + CLAMP_LB = torch.finfo(tkwargs["dtype"]).eps + + variance_plus_noise = variance + observation_noise + + # Standardize the box decomposition bounds and compute normal quantities. + # `batch_shape x num_pareto_samples x q x 2 x J x M` + g = (hypercell_bounds.unsqueeze(-4) - mean.unsqueeze(-2)) / torch.sqrt( + variance.unsqueeze(-2) + ) + normal = Normal(torch.zeros_like(g), torch.ones_like(g)) + gcdf = normal.cdf(g) + gpdf = torch.exp(normal.log_prob(g)) + g_times_gpdf = g * gpdf + + # Compute the differences between the upper and lower terms. + Wjm = (gcdf[..., 1, :, :] - gcdf[..., 0, :, :]).clamp_min(CLAMP_LB) + Vjm = g_times_gpdf[..., 1, :, :] - g_times_gpdf[..., 0, :, :] + + # Compute W. + Wj = torch.exp(torch.sum(torch.log(Wjm), dim=-1, keepdims=True)) + W = torch.sum(Wj, dim=-2, keepdims=True).clamp_max(1.0) + + # Compute the sum of ratios. + ratios = 0.5 * (Wj * (Vjm / Wjm)) / W + # `batch_shape x num_pareto_samples x q x 1 x 1` + ratio_term = torch.sum(ratios, dim=(-2, -1), keepdims=True) + + # Compute the logarithm of the variance. + log_term = 0.5 * torch.log(variance_plus_noise).sum(-1, keepdims=True) + + # `batch_shape x num_pareto_samples x q x 1 x 1` + log_term = log_term + torch.log(W) + + # Additional constant term. + M_plus_K = mean.shape[-1] + add_term = 0.5 * M_plus_K * (1 + torch.log(torch.ones(1, **tkwargs) * 2 * pi)) + + # `batch_shape x num_pareto_samples x q` + entropy = add_term + (log_term - ratio_term).squeeze(-1).squeeze(-1) + + return entropy.mean(-2) + + +def _compute_entropy_upper_bound( + hypercell_bounds: Tensor, + mean: Tensor, + variance: Tensor, + observation_noise: Tensor, + only_diagonal: bool = False, +) -> Tensor: + r"""Computes the entropy upper bound at the design points `X`. This is used for + the JES-LB and MES-LB estimate. If `only_diagonal` is True, then this computes + the entropy estimate for the JES-LB2 and MES-LB2. + + Args: + hypercell_bounds: A `num_pareto_samples x 2 x J x M` -dim Tensor containing + the box decomposition bounds, where `J` = max(num_boxes). + mean: A `batch_shape x num_pareto_samples x q x 1 x M`-dim Tensor containing + the posterior mean at X. + variance: A `batch_shape x num_pareto_samples x q x 1 x M`-dim Tensor + containing the posterior variance at X excluding observation noise. + observation_noise: A `batch_shape x num_pareto_samples x q x 1 x M`-dim + Tensor containing the observation noise at X. + only_diagonal: If true, we only compute the diagonal elements of the variance. + + Returns: + A `batch_shape x q`-dim Tensor of entropy estimate at the given design points + `X`. + """ + tkwargs = {"dtype": hypercell_bounds.dtype, "device": hypercell_bounds.device} + CLAMP_LB = torch.finfo(tkwargs["dtype"]).eps + + variance_plus_noise = variance + observation_noise + + # Standardize the box decomposition bounds and compute normal quantities. + # `batch_shape x num_pareto_samples x q x 2 x J x M` + g = (hypercell_bounds.unsqueeze(-4) - mean.unsqueeze(-2)) / torch.sqrt( + variance.unsqueeze(-2) + ) + normal = Normal(torch.zeros_like(g), torch.ones_like(g)) + gcdf = normal.cdf(g) + gpdf = torch.exp(normal.log_prob(g)) + g_times_gpdf = g * gpdf + + # Compute the differences between the upper and lower terms. + Wjm = (gcdf[..., 1, :, :] - gcdf[..., 0, :, :]).clamp_min(CLAMP_LB) + Vjm = g_times_gpdf[..., 1, :, :] - g_times_gpdf[..., 0, :, :] + Gjm = gpdf[..., 1, :, :] - gpdf[..., 0, :, :] + + # Compute W. + Wj = torch.exp(torch.sum(torch.log(Wjm), dim=-1, keepdims=True)) + W = torch.sum(Wj, dim=-2, keepdims=True).clamp_max(1.0) + + Cjm = Gjm / Wjm + + # First moment: + Rjm = Cjm * Wj / W + # `batch_shape x num_pareto_samples x q x 1 x M + mom1 = mean - torch.sqrt(variance) * Rjm.sum(-2, keepdims=True) + # diagonal weighted sum + # `batch_shape x num_pareto_samples x q x 1 x M + diag_weighted_sum = (Wj * variance * Vjm / Wjm / W).sum(-2, keepdims=True) + + if only_diagonal: + # `batch_shape x num_pareto_samples x q x 1 x M` + mean_squared = mean.pow(2) + cross_sum = -2 * (mean * torch.sqrt(variance) * Rjm).sum(-2, keepdims=True) + # `batch_shape x num_pareto_samples x q x 1 x M` + mom2 = variance_plus_noise - diag_weighted_sum + cross_sum + mean_squared + var = (mom2 - mom1.pow(2)).clamp_min(CLAMP_LB) + + # `batch_shape x num_pareto_samples x q + log_det_term = 0.5 * torch.log(var).sum(dim=-1).squeeze(-1) + else: + # First moment x First moment + # `batch_shape x num_pareto_samples x q x 1 x M x M + cross_mom1 = torch.einsum("...i,...j->...ij", mom1, mom1) + + # Second moment: + # `batch_shape x num_pareto_samples x q x 1 x M x M + # firstly compute the general terms + mom2_cross1 = -torch.einsum( + "...i,...j->...ij", mean, torch.sqrt(variance) * Cjm + ) + mom2_cross2 = -torch.einsum( + "...i,...j->...ji", mean, torch.sqrt(variance) * Cjm + ) + mom2_mean_squared = torch.einsum("...i,...j->...ij", mean, mean) + + mom2_weighted_sum = ( + (mom2_cross1 + mom2_cross2) * Wj.unsqueeze(-1) / W.unsqueeze(-1) + ).sum(-3, keepdims=True) + mom2_weighted_sum = mom2_weighted_sum + mom2_mean_squared + + # Compute the additional off-diagonal terms. + mom2_off_diag = torch.einsum( + "...i,...j->...ij", torch.sqrt(variance) * Cjm, torch.sqrt(variance) * Cjm + ) + mom2_off_diag_sum = (mom2_off_diag * Wj.unsqueeze(-1) / W.unsqueeze(-1)).sum( + -3, keepdims=True + ) + + # Compute the diagonal terms and subtract the diagonal computed before. + init_diag = torch.diagonal(mom2_off_diag_sum, dim1=-2, dim2=-1) + diag_weighted_sum = torch.diag_embed( + variance_plus_noise - diag_weighted_sum - init_diag + ) + mom2 = mom2_weighted_sum + mom2_off_diag_sum + diag_weighted_sum + # Compute the variance + var = (mom2 - cross_mom1).squeeze(-3) + + # Jitter the diagonal. + # The jitter is probably not needed here at all. + jitter_diag = 1e-6 * torch.diag_embed(torch.ones(var.shape[:-1], **tkwargs)) + log_det_term = 0.5 * torch.logdet(var + jitter_diag) + + # Additional terms. + M_plus_K = mean.shape[-1] + add_term = 0.5 * M_plus_K * (1 + torch.log(torch.ones(1, **tkwargs) * 2 * pi)) + + # `batch_shape x num_pareto_samples x q + entropy = add_term + log_det_term + return entropy.mean(-2) + + +def _compute_entropy_monte_carlo( + hypercell_bounds: Tensor, + mean: Tensor, + variance: Tensor, + observation_noise: Tensor, + samples: Tensor, + samples_log_prob: Tensor, +) -> Tensor: + r"""Computes the Monte Carlo entropy at the design points `X`. This is used for + the JES-MC and MES-MC estimate. + + Args: + hypercell_bounds: A `num_pareto_samples x 2 x J x M`-dim Tensor containing + the box decomposition bounds, where `J` = max(num_boxes). + mean: A `batch_shape x num_pareto_samples x q x 1 x M`-dim Tensor containing + the posterior mean at X. + variance: A `batch_shape x num_pareto_samples x q x 1 x M`-dim Tensor + containing the posterior variance at X excluding observation noise. + observation_noise: A `batch_shape x num_pareto_samples x q x 1 x M`-dim + Tensor containing the observation noise at X. + samples: A `num_mc_samples x batch_shape x num_pareto_samples x q x 1 x M`-dim + Tensor containing the noisy samples at `X` from the posterior conditioned + on the Pareto optimal points. + samples_log_prob: A `num_mc_samples x batch_shape x num_pareto_samples + x q`-dim Tensor containing the log probability densities of the samples. + + Returns: + A `batch_shape x q`-dim Tensor of entropy estimate at the given design points + `X`. + """ + tkwargs = {"dtype": hypercell_bounds.dtype, "device": hypercell_bounds.device} + CLAMP_LB = torch.finfo(tkwargs["dtype"]).eps + + variance_plus_noise = variance + observation_noise + + #################################################################### + # Standardize the box decomposition bounds and compute normal quantities. + # `batch_shape x num_pareto_samples x q x 2 x J x M` + g = (hypercell_bounds.unsqueeze(-4) - mean.unsqueeze(-2)) / torch.sqrt( + variance.unsqueeze(-2) + ) + # `batch_shape x num_pareto_samples x q x 1 x M` + rho = torch.sqrt(variance / variance_plus_noise) + + # Compute the initial normal quantities. + normal = Normal(torch.zeros_like(g), torch.ones_like(g)) + gcdf = normal.cdf(g) + + # Compute the differences between the upper and lower terms. + Wjm = (gcdf[..., 1, :, :] - gcdf[..., 0, :, :]).clamp_min(CLAMP_LB) + + # Compute W. + Wj = torch.exp(torch.sum(torch.log(Wjm), dim=-1, keepdims=True)) + # `batch_shape x num_pareto_samples x q x 1 x 1` + W = torch.sum(Wj, dim=-2, keepdims=True).clamp_max(1.0) + + #################################################################### + g = g.unsqueeze(0) + rho = rho.unsqueeze(0).unsqueeze(-2) + # `num_mc_samples x batch_shape x num_pareto_samples x q x 1 x 1 x M` + z = ((samples - mean) / torch.sqrt(variance_plus_noise)).unsqueeze(-2) + # `num_mc_samples x batch_shape x num_pareto_samples x q x 2 x J x M` + # Clamping here is important because `1 - rho^2 = 0` at an input where + # observation noise is zero. + g_new = (g - rho * z) / torch.sqrt((1 - rho * rho).clamp_min(CLAMP_LB)) + + # Compute the initial normal quantities. + normal_new = Normal(torch.zeros_like(g_new), torch.ones_like(g_new)) + gcdf_new = normal_new.cdf(g_new) + + # Compute the differences between the upper and lower terms. + Wjm_new = (gcdf_new[..., 1, :, :] - gcdf_new[..., 0, :, :]).clamp_min(CLAMP_LB) + + # Compute W+. + Wj_new = torch.exp(torch.sum(torch.log(Wjm_new), dim=-1, keepdims=True)) + # `num_mc_samples x batch_shape x num_pareto_samples x q x 1 x 1` + W_new = torch.sum(Wj_new, dim=-2, keepdims=True).clamp_max(1.0) + + #################################################################### + # W_ratio = W+ / W + W_ratio = torch.exp(torch.log(W_new) - torch.log(W).unsqueeze(0)) + samples_log_prob = samples_log_prob.unsqueeze(-1).unsqueeze(-1) + + # Compute the Monte Carlo average: - E[W_ratio * log(W+ p(y))] + log(W) + log_term = torch.log(W_new) + samples_log_prob + mc_estimate = -(W_ratio * log_term).mean(0) + # `batch_shape x num_pareto_samples x q + entropy = (mc_estimate + torch.log(W)).squeeze(-1).squeeze(-1) + + # An alternative Monte Carlo estimate: - E[W_ratio * log(W_ratio p(y))] + # log_term = torch.log(W_ratio) + samples_log_prob + # mc_estimate = - (W_ratio * log_term).mean(0) + # # `batch_shape x num_pareto_samples x q + # entropy = mc_estimate.squeeze(-1).squeeze(-1) + + return entropy.mean(-2) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/logei.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/logei.html new file mode 100644 index 0000000000..1cfd2b41b1 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/logei.html @@ -0,0 +1,539 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.logei

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Multi-objective variants of the LogEI family of acquisition functions, see
+[Ament2023logei]_ for details.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import torch
+from botorch.acquisition.logei import TAU_MAX, TAU_RELU
+from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction
+from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    NondominatedPartitioning,
+)
+from botorch.utils.multi_objective.hypervolume import (
+    NoisyExpectedHypervolumeMixin,
+    SubsetIndexCachingMixin,
+)
+from botorch.utils.objective import compute_smoothed_feasibility_indicator
+from botorch.utils.safe_math import (
+    fatmin,
+    log_fatplus,
+    log_softplus,
+    logdiffexp,
+    logmeanexp,
+    logplusexp,
+    logsumexp,
+    smooth_amin,
+)
+from botorch.utils.transforms import (
+    concatenate_pending_points,
+    is_ensemble,
+    match_batch_shape,
+    t_batch_mode_transform,
+)
+from torch import Tensor
+
+
+
+[docs] +class qLogExpectedHypervolumeImprovement( + MultiObjectiveMCAcquisitionFunction, SubsetIndexCachingMixin +): + _log: bool = True + + def __init__( + self, + model: Model, + ref_point: list[float] | Tensor, + partitioning: NondominatedPartitioning, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + X_pending: Tensor | None = None, + eta: Tensor | float = 1e-2, + fat: bool = True, + tau_relu: float = TAU_RELU, + tau_max: float = TAU_MAX, + ) -> None: + r"""Parallel Log Expected Hypervolume Improvement supporting m>=2 outcomes. + + See [Ament2023logei]_ for details and the methodology behind the LogEI family of + acquisition function. Line-by-line differences to the original differentiable + expected hypervolume formulation of [Daulton2020qehvi]_ are described via inline + comments in `forward`. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> ref_point = [0.0, 0.0] + >>> acq = qLogExpectedHypervolumeImprovement(model, ref_point, partitioning) + >>> value = acq(test_X) + + Args: + model: A fitted model. + ref_point: A list or tensor with `m` elements representing the reference + point (in the outcome space) w.r.t. to which compute the hypervolume. + This is a reference point for the objective values (i.e. after + applying`objective` to the samples). + partitioning: A `NondominatedPartitioning` module that provides the non- + dominated front and a partitioning of the non-dominated space in hyper- + rectangles. If constraints are present, this partitioning must only + include feasible points. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + objective: The MCMultiOutputObjective under which the samples are evaluated. + Defaults to `IdentityMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. The acquisition function will compute expected feasible + hypervolume. + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation but have not yet + been evaluated. Concatenated into `X` upon forward call. Copied and set + to have no gradient. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same eta is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + eta value. + fat: Toggles the logarithmic / linear asymptotic behavior of the smooth + approximation to the ReLU and the maximum. + tau_relu: Temperature parameter controlling the sharpness of the + approximation to the ReLU over the `q` candidate points. For further + details, see the comments above the definition of `TAU_RELU`. + tau_max: Temperature parameter controlling the sharpness of the + approximation to the `max` operator over the `q` candidate points. + For further details, see the comments above the definition of `TAU_MAX`. + """ + if len(ref_point) != partitioning.num_outcomes: + raise ValueError( + "The dimensionality of the reference point must match the number of " + f"outcomes. Got ref_point with {len(ref_point)} elements, but expected " + f"{partitioning.num_outcomes}." + ) + ref_point = torch.as_tensor( + ref_point, + dtype=partitioning.pareto_Y.dtype, + device=partitioning.pareto_Y.device, + ) + super().__init__( + model=model, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + X_pending=X_pending, + ) + self.register_buffer("ref_point", ref_point) + cell_bounds = partitioning.get_hypercell_bounds() + self.register_buffer("cell_lower_bounds", cell_bounds[0]) + self.register_buffer("cell_upper_bounds", cell_bounds[1]) + SubsetIndexCachingMixin.__init__(self) + self.tau_relu = tau_relu + self.tau_max = tau_max + self.fat = fat + + def _compute_log_qehvi(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Compute the expected (feasible) hypervolume improvement given MC samples. + + Args: + samples: A `sample_shape x batch_shape x q' x m`-dim tensor of samples. + X: A `batch_shape x q x d`-dim tensor of inputs. + + Returns: + A `batch_shape x (model_batch_shape)`-dim tensor of expected hypervolume + improvement for each batch. + """ + # Note that the objective may subset the outcomes (e.g. this will usually happen + # if there are constraints present). + obj = self.objective(samples, X=X) # mc_samples x batch_shape x q x m + q = obj.shape[-2] + if self.constraints is not None: + log_feas_weights = compute_smoothed_feasibility_indicator( + constraints=self.constraints, + samples=samples, + eta=self.eta, + log=True, + fat=self.fat, + ) + device = self.ref_point.device + q_subset_indices = self.compute_q_subset_indices(q_out=q, device=device) + batch_shape = obj.shape[:-2] # mc_samples x batch_shape + # areas tensor is `mc_samples x batch_shape x num_cells x 2`-dim + log_areas_per_segment = torch.full( + size=( + *batch_shape, + self.cell_lower_bounds.shape[-2], # num_cells + 2, # for even and odd terms + ), + fill_value=-torch.inf, + dtype=obj.dtype, + device=device, + ) + + cell_batch_ndim = self.cell_lower_bounds.ndim - 2 + # conditionally adding mc_samples dim if cell_batch_ndim > 0 + # adding ones to shape equal in number to to batch_shape_ndim - cell_batch_ndim + # adding cell_bounds batch shape w/o 1st dimension + sample_batch_view_shape = torch.Size( + [ + batch_shape[0] if cell_batch_ndim > 0 else 1, + *[1 for _ in range(len(batch_shape) - max(cell_batch_ndim, 1))], + *self.cell_lower_bounds.shape[1:-2], + ] + ) + view_shape = ( + *sample_batch_view_shape, + self.cell_upper_bounds.shape[-2], # num_cells + 1, # adding for q_choose_i dimension + self.cell_upper_bounds.shape[-1], # num_objectives + ) + + for i in range(1, self.q_out + 1): + # TODO: we could use batches to compute (q choose i) and (q choose q-i) + # simultaneously since subsets of size i and q-i have the same number of + # elements. This would decrease the number of iterations, but increase + # memory usage. + q_choose_i = q_subset_indices[f"q_choose_{i}"] # q_choose_i x i + # this tensor is mc_samples x batch_shape x i x q_choose_i x m + obj_subsets = obj.index_select(dim=-2, index=q_choose_i.view(-1)) + obj_subsets = obj_subsets.view( + obj.shape[:-2] + q_choose_i.shape + obj.shape[-1:] + ) # mc_samples x batch_shape x q_choose_i x i x m + + # NOTE: the order of operations in non-log _compute_qehvi is 3), 1), 2). + # since 3) moved above 1), _log_improvement adds another Tensor dimension + # that keeps track of num_cells. + + # 1) computes log smoothed improvement over the cell lower bounds. + # mc_samples x batch_shape x num_cells x q_choose_i x i x m + log_improvement_i = self._log_improvement(obj_subsets, view_shape) + + # 2) take the minimum log improvement over all i subsets. + # since all hyperrectangles share one vertex, the opposite vertex of the + # overlap is given by the component-wise minimum. + # negative of maximum of negative log_improvement is approximation to min. + log_improvement_i = self._smooth_min( + log_improvement_i, + dim=-2, + ) # mc_samples x batch_shape x num_cells x q_choose_i x m + + # 3) compute the log lengths of the cells' sides. + # mc_samples x batch_shape x num_cells x q_choose_i x m + log_lengths_i = self._log_cell_lengths(log_improvement_i, view_shape) + + # 4) take product over hyperrectangle side lengths to compute area (m-dim). + # after, log_areas_i is mc_samples x batch_shape x num_cells x q_choose_i + log_areas_i = log_lengths_i.sum(dim=-1) # areas_i = lengths_i.prod(dim=-1) + + # 5) if constraints are present, apply a differentiable approximation of + # the indicator function. + if self.constraints is not None: + log_feas_subsets = log_feas_weights.index_select( + dim=-1, index=q_choose_i.view(-1) + ).view(log_feas_weights.shape[:-1] + q_choose_i.shape) + log_areas_i = log_areas_i + log_feas_subsets.unsqueeze(-3).sum(dim=-1) + + # 6) sum over all subsets of size i, i.e. reduce over q_choose_i-dim + # after, log_areas_i is mc_samples x batch_shape x num_cells + log_areas_i = logsumexp(log_areas_i, dim=-1) # areas_i.sum(dim=-1) + + # 7) Using the inclusion-exclusion principle, set the sign to be positive + # for subsets of odd sizes and negative for subsets of even size + # in non-log space: areas_per_segment += (-1) ** (i + 1) * areas_i, + # but here in log space, we need to keep track of sign: + log_areas_per_segment[..., i % 2] = logplusexp( + log_areas_per_segment[..., i % 2], + log_areas_i, + ) + + # 8) subtract even from odd log area terms + log_areas_per_segment = logdiffexp( + log_a=log_areas_per_segment[..., 0], log_b=log_areas_per_segment[..., 1] + ) + + # 9) sum over segments (n_cells-dim) and average over MC samples + return logmeanexp(logsumexp(log_areas_per_segment, dim=-1), dim=0) + + def _log_improvement( + self, obj_subsets: Tensor, view_shape: tuple | torch.Size + ) -> Tensor: + # smooth out the clamp and take the log (previous step 3) + # subtract cell lower bounds, clamp min at zero, but first + # make obj_subsets broadcastable with cell bounds: + # mc_samples x batch_shape x (num_cells = 1) x q_choose_i x i x m + obj_subsets = obj_subsets.unsqueeze(-4) + # making cell bounds broadcastable with obj_subsets: + # (mc_samples = 1) x (batch_shape = 1) x num_cells x 1 x (i = 1) x m + cell_lower_bounds = self.cell_lower_bounds.view(view_shape).unsqueeze(-3) + Z = obj_subsets - cell_lower_bounds + log_Zi = self._log_smooth_relu(Z) + return log_Zi # mc_samples x batch_shape x num_cells x q_choose_i x i x m + + def _log_cell_lengths( + self, log_improvement_i: Tensor, view_shape: tuple | torch.Size + ) -> Tensor: + cell_upper_bounds = self.cell_upper_bounds.clamp_max( + 1e10 if log_improvement_i.dtype == torch.double else 1e8 + ) # num_cells x num_objectives + # add batch-dim to compute area for each segment (pseudo-pareto-vertex) + log_cell_lengths = ( + (cell_upper_bounds - self.cell_lower_bounds).log().view(view_shape) + ) # (mc_samples = 1) x (batch_shape = 1) x n_cells x (q_choose_i = 1) x m + # mc_samples x batch_shape x num_cells x q_choose_i x m + return self._smooth_minimum( + log_improvement_i, + log_cell_lengths, + ) + + def _log_smooth_relu(self, X: Tensor) -> Tensor: + f = log_fatplus if self.fat else log_softplus + return f(X, tau=self.tau_relu) + + def _smooth_min(self, X: Tensor, dim: int, keepdim: bool = False) -> Tensor: + f = fatmin if self.fat else smooth_amin + return f(X, tau=self.tau_max, dim=dim) + + def _smooth_minimum(self, X: Tensor, Y: Tensor) -> Tensor: + XY = torch.stack(torch.broadcast_tensors(X, Y), dim=-1) + return self._smooth_min(XY, dim=-1, keepdim=False) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + posterior = self.model.posterior(X) + samples = self.get_posterior_samples(posterior) + return self._compute_log_qehvi(samples=samples, X=X)
+
+ + + +
+[docs] +class qLogNoisyExpectedHypervolumeImprovement( + NoisyExpectedHypervolumeMixin, + qLogExpectedHypervolumeImprovement, +): + _log: bool = True + + def __init__( + self, + model: Model, + ref_point: list[float] | Tensor, + X_baseline: Tensor, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + X_pending: Tensor | None = None, + eta: Tensor | float = 1e-3, + prune_baseline: bool = False, + alpha: float = 0.0, + cache_pending: bool = True, + max_iep: int = 0, + incremental_nehvi: bool = True, + cache_root: bool = True, + tau_relu: float = TAU_RELU, + tau_max: float = 1e-3, # TAU_MAX, + fat: bool = True, + marginalize_dim: int | None = None, + ) -> None: + r""" + q-Log Noisy Expected Hypervolume Improvement supporting m>=2 outcomes. + + Based on the differentiable hypervolume formulation of [Daulton2021nehvi]_. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> ref_point = [0.0, 0.0] + >>> qNEHVI = qNoisyExpectedHypervolumeImprovement(model, ref_point, train_X) + >>> qnehvi = qNEHVI(test_X) + + Args: + model: A fitted model. + ref_point: A list or tensor with `m` elements representing the reference + point (in the outcome space) w.r.t. to which compute the hypervolume. + This is a reference point for the objective values (i.e. after + applying `objective` to the samples). + X_baseline: A `r x d`-dim Tensor of `r` design points that have already + been observed. These points are considered as potential approximate + pareto-optimal design points. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + Note: a pareto front is created for each mc sample, which can be + computationally intensive for `m` > 2. + objective: The MCMultiOutputObjective under which the samples are + evaluated. Defaults to `IdentityMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. The acquisition function will compute expected feasible + hypervolume. + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points that + have points that have been submitted for function evaluation, but + have not yet been evaluated. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same `eta` is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + `eta` value. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the pareto optimal and better than the + reference point. This can significantly improve computation time and + is generally recommended. In order to customize pruning parameters, + instead manually call `prune_inferior_points_multi_objective` on + `X_baseline` before instantiating the acquisition function. + alpha: The hyperparameter controlling the approximate non-dominated + partitioning. The default value of 0.0 means an exact partitioning + is used. As the number of objectives `m` increases, consider increasing + this parameter in order to limit computational complexity. + cache_pending: A boolean indicating whether to use cached box + decompositions (CBD) for handling pending points. This is + generally recommended. + max_iep: The maximum number of pending points before the box + decompositions will be recomputed. + incremental_nehvi: A boolean indicating whether to compute the + incremental NEHVI from the `i`th point where `i=1, ..., q` + under sequential greedy optimization, or the full qNEHVI over + `q` points. + cache_root: A boolean indicating whether to cache the root + decomposition over `X_baseline` and use low-rank updates. + marginalize_dim: A batch dimension that should be marginalized. + """ + MultiObjectiveMCAcquisitionFunction.__init__( + self, + model=model, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + ) + SubsetIndexCachingMixin.__init__(self) + NoisyExpectedHypervolumeMixin.__init__( + self, + model=model, + ref_point=ref_point, + X_baseline=X_baseline, + sampler=sampler, + objective=objective, + constraints=constraints, + X_pending=X_pending, + prune_baseline=prune_baseline, + alpha=alpha, + cache_pending=cache_pending, + max_iep=max_iep, + incremental_nehvi=incremental_nehvi, + cache_root=cache_root, + marginalize_dim=marginalize_dim, + ) + # parameters that are used by qLogEHVI + self.tau_relu = tau_relu + self.tau_max = tau_max + self.fat = fat + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + X_full = torch.cat([match_batch_shape(self.X_baseline, X), X], dim=-2) + # NOTE: To ensure that we correctly sample `f(X)` from the joint distribution + # `f((X_baseline, X)) ~ P(f | D)`, it is critical to compute the joint posterior + # over X *and* X_baseline -- which also contains pending points whenever there + # are any -- since the baseline and pending values `f(X_baseline)` are + # generally pre-computed and cached before the `forward` call, see the docs of + # `cache_pending` for details. + # TODO: Improve the efficiency by not re-computing the X_baseline-X_baseline + # covariance matrix, but only the covariance of + # 1) X and X, and + # 2) X and X_baseline. + posterior = self.model.posterior(X_full) + # Account for possible one-to-many transform and the model batch dimensions in + # ensemble models. + event_shape_lag = 1 if is_ensemble(self.model) else 2 + n_w = ( + posterior._extended_shape()[X_full.dim() - event_shape_lag] + // X_full.shape[-2] + ) + q_in = X.shape[-2] * n_w + self._set_sampler(q_in=q_in, posterior=posterior) + samples = self._get_f_X_samples(posterior=posterior, q_in=q_in) + # Add previous nehvi from pending points. + nehvi = self._compute_log_qehvi(samples=samples, X=X) + if self.incremental_nehvi: + return nehvi + return logplusexp(nehvi, self._prev_nehvi.log())
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/max_value_entropy_search.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/max_value_entropy_search.html new file mode 100644 index 0000000000..3c093eb3f4 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/max_value_entropy_search.html @@ -0,0 +1,456 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.max_value_entropy_search

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Acquisition functions for max-value entropy search for multi-objective
+Bayesian optimization (MESMO).
+
+References
+
+.. [Belakaria2019]
+    S. Belakaria, A. Deshwal, J. R. Doppa. Max-value Entropy Search
+    for Multi-Objective Bayesian Optimization. Advances in Neural
+    Information Processing Systems, 32. 2019.
+
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from math import pi
+
+import torch
+from botorch.acquisition.max_value_entropy_search import qMaxValueEntropy
+from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction
+from botorch.acquisition.multi_objective.joint_entropy_search import (
+    LowerBoundMultiObjectiveEntropySearch,
+)
+from botorch.models.converter import (
+    batched_multi_output_to_single_output,
+    model_list_to_batched,
+)
+from botorch.models.model import Model
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from botorch.sampling.base import MCSampler
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+
+
+
+[docs] +class qMultiObjectiveMaxValueEntropy( + qMaxValueEntropy, MultiObjectiveMCAcquisitionFunction +): + r"""The acquisition function for MESMO. + + This acquisition function computes the mutual information of + Pareto frontier and a candidate point. See [Belakaria2019]_ for + a detailed discussion. + + q > 1 is supported through cyclic optimization and fantasies. + + Noisy observations are support by computing information gain with + observation noise as in Appendix C in [Takeno2020mfmves]_. + + Note: this only supports maximization. + + Attributes: + _default_sample_shape: The `sample_shape` for the default sampler. + + Example: + >>> model = SingleTaskGP(train_X, train_Y, outcome_transform=None) + >>> MESMO = qMultiObjectiveMaxValueEntropy(model, sample_pfs) + >>> mesmo = MESMO(test_X) + """ + + _default_sample_shape = torch.Size([128]) + + def __init__( + self, + model: Model, + sample_pareto_frontiers: Callable[[Model], Tensor], + num_fantasies: int = 16, + X_pending: Tensor | None = None, + sampler: MCSampler | None = None, + ) -> None: + r"""Multi-objective max-value entropy search acquisition function. + + Args: + model: A fitted multi-output model. + sample_pareto_frontiers: A callable that takes a model and returns a + `num_samples x n' x m`-dim tensor of outcomes to use for constructing + `num_samples` sampled Pareto frontiers. + num_fantasies: Number of fantasies to generate. The higher this + number the more accurate the model (at the expense of model + complexity, wall time and memory). Ignored if `X_pending` is `None`. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation, but have not yet been evaluated. + """ + MultiObjectiveMCAcquisitionFunction.__init__(self, model=model, sampler=sampler) + + # Batch GP models (e.g. fantasized models) are not currently supported + if isinstance(model, ModelListGP): + train_X = model.models[0].train_inputs[0] + else: + train_X = model.train_inputs[0] + if train_X.ndim > 3: + raise NotImplementedError( + "Batch GP models (e.g. fantasized models) " + "are not yet supported by qMultiObjectiveMaxValueEntropy" + ) + # convert to batched MO model + batched_mo_model = ( + model_list_to_batched(model) if isinstance(model, ModelListGP) else model + ) + self._init_model = batched_mo_model + self.fantasies_sampler = SobolQMCNormalSampler( + sample_shape=torch.Size([num_fantasies]) + ) + self.num_fantasies = num_fantasies + # weight is used in _compute_information_gain + self.maximize = True + self.weight = 1.0 + self.sample_pareto_frontiers = sample_pareto_frontiers + # Set X_pending, register converted model and sample max values. + self.set_X_pending(X_pending) + # This avoids attribute errors in qMaxValueEntropy code. + self.posterior_transform = None + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + r"""Set pending points. + + Informs the acquisition function about pending design points, + fantasizes the model on the pending points and draws max-value samples + from the fantasized model posterior. + + Args: + X_pending: `m x d` Tensor with `m` `d`-dim design points that have + been submitted for evaluation but have not yet been evaluated. + """ + MultiObjectiveMCAcquisitionFunction.set_X_pending(self, X_pending=X_pending) + if X_pending is not None: + # fantasize the model + fantasy_model = self._init_model.fantasize( + X=X_pending, + sampler=self.fantasies_sampler, + ) + self.mo_model = fantasy_model + # convert model to batched single outcome model. + self.model = batched_multi_output_to_single_output( + batch_mo_model=self.mo_model + ) + self._sample_max_values() + else: + # This is mainly for setting the model to the original model + # after the sequential optimization at q > 1 + self.mo_model = self._init_model + self.model = batched_multi_output_to_single_output( + batch_mo_model=self.mo_model + ) + self._sample_max_values()
+ + + def _sample_max_values(self) -> None: + """Sample max values for MC approximation of the expectation in MES. + + Sets self.posterior_max_values.""" + with torch.no_grad(): + # num_samples x (num_fantasies) x n_pareto_points x m + sampled_pfs = self.sample_pareto_frontiers(self.mo_model) + if sampled_pfs.ndim == 3: + # add fantasy dim + sampled_pfs = sampled_pfs.unsqueeze(-3) + # take component-wise max value + self.posterior_max_values = sampled_pfs.max(dim=-2).values + +
+[docs] + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + r"""Compute max-value entropy at the design points `X`. + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of `batch_shape` t-batches + with `1` `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of MVE values at the given design points `X`. + """ + # `m` dim tensor of information gains + # unsqueeze X to add a batch-dim for the batched model + igs = qMaxValueEntropy.forward(self, X=X.unsqueeze(-3)) + # sum over objectives + return igs.sum(dim=-1)
+
+ + + +
+[docs] +class qLowerBoundMultiObjectiveMaxValueEntropySearch( + LowerBoundMultiObjectiveEntropySearch +): + r"""The acquisition function for the multi-objective Max-value Entropy Search, + where the batches `q > 1` are supported through the lower bound formulation. + + This acquisition function computes the mutual information between the observation + at a candidate point `X` and the Pareto optimal outputs. + + See [Tu2022]_ for a discussion on the estimation procedure. + + NOTES: + (i) The estimated acquisition value could be negative. + + (ii) The lower bound batch acquisition function might not be monotone in the + sense that adding more elements to the batch does not necessarily increase the + acquisition value. Specifically, the acquisition value can become smaller when + more inputs are added. + """ + + def __init__( + self, + model: Model, + hypercell_bounds: Tensor, + X_pending: Tensor | None = None, + estimation_type: str = "LB", + num_samples: int = 64, + ) -> None: + r"""Lower bound multi-objective max-value entropy search acquisition function. + + Args: + model: A fitted batch model with 'M' number of outputs. + hypercell_bounds: A `num_pareto_samples x 2 x J x M`-dim Tensor + containing the hyper-rectangle bounds for integration, where `J` is + the number of hyper-rectangles. In the unconstrained case, this gives + the partition of the dominated space. In the constrained case, this + gives the partition of the feasible dominated space union the + infeasible space. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation, but have not yet been evaluated. + estimation_type: A string to determine which entropy estimate is + computed: "0", "LB", "LB2", or "MC". + num_samples: The number of Monte Carlo samples for the Monte Carlo + estimate. + """ + super().__init__( + model=model, + pareto_sets=None, + pareto_fronts=None, + hypercell_bounds=hypercell_bounds, + X_pending=X_pending, + estimation_type=estimation_type, + num_samples=num_samples, + ) + + def _compute_posterior_statistics( + self, X: Tensor + ) -> dict[str, GPyTorchPosterior | Tensor]: + r"""Compute the posterior statistics. + + Args: + X: A `batch_shape x q x d`-dim Tensor of inputs. + + Returns: + A dictionary containing the posterior variables used to estimate the + entropy. + + - "initial_entropy": A `batch_shape`-dim Tensor containing the entropy of + the Gaussian random variable `p(Y| X, D_n)`. + - "posterior_mean": A `batch_shape x num_pareto_samples x q x 1 x M`-dim + Tensor containing the posterior mean at the input `X`. + - "posterior_variance": A `batch_shape x num_pareto_samples x q x 1 x + M`-dim Tensor containing the posterior variance at the input `X` + excluding the observation noise. + - "observation_noise": A `batch_shape x num_pareto_samples x q x 1 x M` + -dim Tensor containing the observation noise at the input `X`. + - "posterior_with_noise": The posterior distribution at `X` which + includes the observation noise. This is used to compute the marginal + log-probabilities with respect to `p(y| x, D_n)` for `x` in `X`. + """ + tkwargs = {"dtype": X.dtype, "device": X.device} + CLAMP_LB = torch.finfo(tkwargs["dtype"]).eps + + # Compute the initial entropy term depending on `X`. + # TODO: Below we compute posterior_plus_noise twice: + # (1) Firstly, we compute p(Y| X, D_n) when computing the initial entropy + # (2) Secondly, we compute p(y| x, D_n) for x in X in order to compute + # log(p(y|x, D_n)) for x in X in the Monte Carlo estimate.. + # This could be simplified if we could evaluate log(p(y|x, D_n)) using the + # the posterior p(Y| X, D_n) + posterior_plus_noise = self.initial_model.posterior(X, observation_noise=True) + + # Additional constant term. + add_term = ( + 0.5 + * self.model.num_outputs + * (1 + torch.log(2 * pi * torch.ones(1, **tkwargs))) + ) + # The variance initially has shape `batch_shape x (q*M) x (q*M)` + # prior_entropy has shape `batch_shape x num_fantasies` + initial_entropy = add_term + 0.5 * torch.logdet( + posterior_plus_noise.mvn.covariance_matrix + ) + posterior_statistics = {"initial_entropy": initial_entropy} + + # Compute the posterior entropy term. + posterior_plus_noise = self.model.posterior( + X.unsqueeze(-2), observation_noise=True + ) + + # `batch_shape x q x 1 x M` + mean = posterior_plus_noise.mean + var_plus_noise = posterior_plus_noise.variance.clamp_min(CLAMP_LB) + # Expand shapes to `batch_shape x num_pareto_samples x q x 1 x M` + new_shape = ( + mean.shape[:-3] + torch.Size([self.num_pareto_samples]) + mean.shape[-3:] + ) + mean = mean.unsqueeze(-4).expand(new_shape) + var_plus_noise = var_plus_noise.unsqueeze(-4).expand(new_shape) + + # TODO: This computes the observation noise via a second evaluation of the + # posterior. This step could be done better. + posterior = self.model.posterior(X.unsqueeze(-2), observation_noise=False) + var = posterior.variance.clamp_min(CLAMP_LB) + var = var.unsqueeze(-4).expand(new_shape) + obs_noise = var_plus_noise - var + + posterior_statistics["posterior_mean"] = mean + posterior_statistics["posterior_variance"] = var + posterior_statistics["observation_noise"] = obs_noise + posterior_statistics["posterior_with_noise"] = posterior_plus_noise + + return posterior_statistics + + def _compute_monte_carlo_variables( + self, posterior: GPyTorchPosterior + ) -> tuple[Tensor, Tensor]: + r"""Compute the samples and log-probability associated with a posterior + distribution. + + Args: + posterior: The posterior distribution, which includes the observation + noise. + + Returns: + A two-element tuple containing + + - samples: A `num_mc_samples x batch_shape x num_pareto_samples x q x 1 + x M`-dim Tensor containing the Monte Carlo samples. + - samples_log_prob: A `num_mc_samples x batch_shape x num_pareto_samples + x q`-dim Tensor containing the log-probabilities of the Monte Carlo + samples. + """ + + # `num_mc_samples x batch_shape x q x 1 x M` + samples = self.get_posterior_samples(posterior) + + # `num_mc_samples x batch_shape x q` + if self.model.num_outputs == 1: + samples_log_prob = posterior.mvn.log_prob(samples.squeeze(-1)) + else: + samples_log_prob = posterior.mvn.log_prob(samples) + + # Expand shape to `num_mc_samples x batch_shape x num_pareto_samples x + # q x 1 x M` + new_shape = ( + samples.shape[:-3] + + torch.Size([self.num_pareto_samples]) + + samples.shape[-3:] + ) + samples = samples.unsqueeze(-4).expand(new_shape) + + # Expand shape to `num_mc_samples x batch_shape x num_pareto_samples x q` + new_shape = ( + samples_log_prob.shape[:-1] + + torch.Size([self.num_pareto_samples]) + + samples_log_prob.shape[-1:] + ) + samples_log_prob = samples_log_prob.unsqueeze(-2).expand(new_shape) + + return samples, samples_log_prob + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluates qLowerBoundMultiObjectiveMaxValueEntropySearch at the design + points `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of `batch_shape` t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape`-dim Tensor of acquisition values at the given design + points `X`. + """ + return self._compute_lower_bound_information_gain(X)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/monte_carlo.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/monte_carlo.html new file mode 100644 index 0000000000..6b2133000d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/monte_carlo.html @@ -0,0 +1,446 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.monte_carlo

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Monte-Carlo Acquisition Functions for Multi-objective Bayesian optimization.
+In particular, this module contains implementations of
+1) qEHVI [Daulton2020qehvi]_, and
+2) qNEHVI [Daulton2021nehvi]_.
+
+References
+
+.. [Daulton2020qehvi]
+    S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume
+    Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural
+    Information Processing Systems 33, 2020.
+
+.. [Daulton2021nehvi]
+    S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of
+    Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances
+    in Neural Information Processing Systems 34, 2021.
+
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import torch
+from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction
+from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
+from botorch.exceptions.warnings import legacy_ei_numerics_warning
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    NondominatedPartitioning,
+)
+from botorch.utils.multi_objective.hypervolume import (
+    NoisyExpectedHypervolumeMixin,
+    SubsetIndexCachingMixin,
+)
+from botorch.utils.objective import compute_smoothed_feasibility_indicator
+from botorch.utils.transforms import (
+    concatenate_pending_points,
+    is_ensemble,
+    match_batch_shape,
+    t_batch_mode_transform,
+)
+from torch import Tensor
+
+
+
+[docs] +class qExpectedHypervolumeImprovement( + MultiObjectiveMCAcquisitionFunction, SubsetIndexCachingMixin +): + def __init__( + self, + model: Model, + ref_point: list[float] | Tensor, + partitioning: NondominatedPartitioning, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + X_pending: Tensor | None = None, + eta: Tensor | float = 1e-3, + fat: bool = False, + ) -> None: + r"""q-Expected Hypervolume Improvement supporting m>=2 outcomes. + + See [Daulton2020qehvi]_ for details. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> ref_point = [0.0, 0.0] + >>> qEHVI = qExpectedHypervolumeImprovement(model, ref_point, partitioning) + >>> qehvi = qEHVI(test_X) + + Args: + model: A fitted model. + ref_point: A list or tensor with `m` elements representing the reference + point (in the outcome space) w.r.t. to which compute the hypervolume. + This is a reference point for the objective values (i.e. after + applying`objective` to the samples). + partitioning: A `NondominatedPartitioning` module that provides the non- + dominated front and a partitioning of the non-dominated space in hyper- + rectangles. If constraints are present, this partitioning must only + include feasible points. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + objective: The MCMultiOutputObjective under which the samples are evaluated. + Defaults to `IdentityMCMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. The acquisition function will compute expected feasible + hypervolume. + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation but have not yet + been evaluated. Concatenated into `X` upon forward call. Copied and set + to have no gradient. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same eta is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + eta value. + fat: A Boolean flag indicating whether to use the heavy-tailed approximation + of the constraint indicator. + """ + legacy_ei_numerics_warning(legacy_name=type(self).__name__) + if len(ref_point) != partitioning.num_outcomes: + raise ValueError( + "The length of the reference point must match the number of outcomes. " + f"Got ref_point with {len(ref_point)} elements, but expected " + f"{partitioning.num_outcomes}." + ) + ref_point = torch.as_tensor( + ref_point, + dtype=partitioning.pareto_Y.dtype, + device=partitioning.pareto_Y.device, + ) + super().__init__( + model=model, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + X_pending=X_pending, + ) + self.register_buffer("ref_point", ref_point) + cell_bounds = partitioning.get_hypercell_bounds() + self.register_buffer("cell_lower_bounds", cell_bounds[0]) + self.register_buffer("cell_upper_bounds", cell_bounds[1]) + SubsetIndexCachingMixin.__init__(self) + self.fat = fat + + def _compute_qehvi(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Compute the expected (feasible) hypervolume improvement given MC samples. + + Args: + samples: A `n_samples x batch_shape x q' x m`-dim tensor of samples. + X: A `batch_shape x q x d`-dim tensor of inputs. + + Returns: + A `batch_shape x (model_batch_shape)`-dim tensor of expected hypervolume + improvement for each batch. + """ + # Note that the objective may subset the outcomes (e.g. this will usually happen + # if there are constraints present). + obj = self.objective(samples, X=X) + q = obj.shape[-2] + if self.constraints is not None: + feas_weights = compute_smoothed_feasibility_indicator( + constraints=self.constraints, + samples=samples, + eta=self.eta, + fat=self.fat, + ) # `sample_shape x batch-shape x q` + device = self.ref_point.device + q_subset_indices = self.compute_q_subset_indices(q_out=q, device=device) + batch_shape = obj.shape[:-2] + # this is n_samples x input_batch_shape x + areas_per_segment = torch.zeros( + *batch_shape, + self.cell_lower_bounds.shape[-2], + dtype=obj.dtype, + device=device, + ) + cell_batch_ndim = self.cell_lower_bounds.ndim - 2 + sample_batch_view_shape = torch.Size( + [ + batch_shape[0] if cell_batch_ndim > 0 else 1, + *[1 for _ in range(len(batch_shape) - max(cell_batch_ndim, 1))], + *self.cell_lower_bounds.shape[1:-2], + ] + ) + view_shape = ( + *sample_batch_view_shape, + self.cell_upper_bounds.shape[-2], + 1, + self.cell_upper_bounds.shape[-1], + ) + for i in range(1, self.q_out + 1): + # TODO: we could use batches to compute (q choose i) and (q choose q-i) + # simultaneously since subsets of size i and q-i have the same number of + # elements. This would decrease the number of iterations, but increase + # memory usage. + q_choose_i = q_subset_indices[f"q_choose_{i}"] + # this tensor is mc_samples x batch_shape x i x q_choose_i x m + obj_subsets = obj.index_select(dim=-2, index=q_choose_i.view(-1)) + obj_subsets = obj_subsets.view( + obj.shape[:-2] + q_choose_i.shape + obj.shape[-1:] + ) + # since all hyperrectangles share one vertex, the opposite vertex of the + # overlap is given by the component-wise minimum. + # take the minimum in each subset + overlap_vertices = obj_subsets.min(dim=-2).values + # add batch-dim to compute area for each segment (pseudo-pareto-vertex) + # this tensor is mc_samples x batch_shape x num_cells x q_choose_i x m + overlap_vertices = torch.min( + overlap_vertices.unsqueeze(-3), self.cell_upper_bounds.view(view_shape) + ) + # subtract cell lower bounds, clamp min at zero + lengths_i = ( + overlap_vertices - self.cell_lower_bounds.view(view_shape) + ).clamp_min(0.0) + # take product over hyperrectangle side lengths to compute area + # sum over all subsets of size i + areas_i = lengths_i.prod(dim=-1) + # if constraints are present, apply a differentiable approximation of + # the indicator function + if self.constraints is not None: + feas_subsets = feas_weights.index_select( + dim=-1, index=q_choose_i.view(-1) + ).view(feas_weights.shape[:-1] + q_choose_i.shape) + areas_i = areas_i * feas_subsets.unsqueeze(-3).prod(dim=-1) + areas_i = areas_i.sum(dim=-1) + # Using the inclusion-exclusion principle, set the sign to be positive + # for subsets of odd sizes and negative for subsets of even size + areas_per_segment += (-1) ** (i + 1) * areas_i + # sum over segments and average over MC samples + return areas_per_segment.sum(dim=-1).mean(dim=0) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + posterior = self.model.posterior(X) + samples = self.get_posterior_samples(posterior) + return self._compute_qehvi(samples=samples, X=X)
+
+ + + +
+[docs] +class qNoisyExpectedHypervolumeImprovement( + NoisyExpectedHypervolumeMixin, qExpectedHypervolumeImprovement +): + def __init__( + self, + model: Model, + ref_point: list[float] | Tensor, + X_baseline: Tensor, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + X_pending: Tensor | None = None, + eta: Tensor | float = 1e-3, + fat: bool = False, + prune_baseline: bool = False, + alpha: float = 0.0, + cache_pending: bool = True, + max_iep: int = 0, + incremental_nehvi: bool = True, + cache_root: bool = True, + marginalize_dim: int | None = None, + ) -> None: + r"""q-Noisy Expected Hypervolume Improvement supporting m>=2 outcomes. + + See [Daulton2021nehvi]_ for details. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> ref_point = [0.0, 0.0] + >>> qNEHVI = qNoisyExpectedHypervolumeImprovement(model, ref_point, train_X) + >>> qnehvi = qNEHVI(test_X) + + Args: + model: A fitted model. + ref_point: A list or tensor with `m` elements representing the reference + point (in the outcome space) w.r.t. to which compute the hypervolume. + This is a reference point for the objective values (i.e. after + applying `objective` to the samples). + X_baseline: A `r x d`-dim Tensor of `r` design points that have already + been observed. These points are considered as potential approximate + pareto-optimal design points. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + Note: a pareto front is created for each mc sample, which can be + computationally intensive for `m` > 2. + objective: The MCMultiOutputObjective under which the samples are + evaluated. Defaults to `IdentityMCMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. The acquisition function will compute expected feasible + hypervolume. + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points that + have points that have been submitted for function evaluation, but + have not yet been evaluated. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same `eta` is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + `eta` value. For more details, on this parameter, see the docs of + `compute_smoothed_feasibility_indicator`. + fat: A Boolean flag indicating whether to use the heavy-tailed approximation + of the constraint indicator. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the pareto optimal and better than the + reference point. This can significantly improve computation time and + is generally recommended. In order to customize pruning parameters, + instead manually call `prune_inferior_points_multi_objective` on + `X_baseline` before instantiating the acquisition function. + alpha: The hyperparameter controlling the approximate non-dominated + partitioning. The default value of 0.0 means an exact partitioning + is used. As the number of objectives `m` increases, consider increasing + this parameter in order to limit computational complexity. + cache_pending: A boolean indicating whether to use cached box + decompositions (CBD) for handling pending points. This is + generally recommended. + max_iep: The maximum number of pending points before the box + decompositions will be recomputed. + incremental_nehvi: A boolean indicating whether to compute the + incremental NEHVI from the `i`th point where `i=1, ..., q` + under sequential greedy optimization, or the full qNEHVI over + `q` points. + cache_root: A boolean indicating whether to cache the root + decomposition over `X_baseline` and use low-rank updates. + marginalize_dim: A batch dimension that should be marginalized. For example, + this is useful when using a batched fully Bayesian model. + """ + legacy_ei_numerics_warning(legacy_name=type(self).__name__) + MultiObjectiveMCAcquisitionFunction.__init__( + self, + model=model, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + ) + SubsetIndexCachingMixin.__init__(self) + NoisyExpectedHypervolumeMixin.__init__( + self, + model=model, + ref_point=ref_point, + X_baseline=X_baseline, + sampler=self.sampler, + objective=self.objective, + constraints=self.constraints, + X_pending=X_pending, + prune_baseline=prune_baseline, + alpha=alpha, + cache_pending=cache_pending, + max_iep=max_iep, + incremental_nehvi=incremental_nehvi, + cache_root=cache_root, + marginalize_dim=marginalize_dim, + ) + self.fat = fat + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + X_full = torch.cat([match_batch_shape(self.X_baseline, X), X], dim=-2) + # NOTE: To ensure that we correctly sample `f(X)` from the joint distribution + # `f((X_baseline, X)) ~ P(f | D)`, it is critical to compute the joint posterior + # over X *and* X_baseline -- which also contains pending points whenever there + # are any -- since the baseline and pending values `f(X_baseline)` are + # generally pre-computed and cached before the `forward` call, see the docs of + # `cache_pending` for details. + # TODO: Improve the efficiency by not re-computing the X_baseline-X_baseline + # covariance matrix, but only the covariance of + # 1) X and X, and + # 2) X and X_baseline. + posterior = self.model.posterior(X_full) + # Account for possible one-to-many transform and the MCMC batch dimension in + # `SaasFullyBayesianSingleTaskGP` + event_shape_lag = 1 if is_ensemble(self.model) else 2 + n_w = ( + posterior._extended_shape()[X_full.dim() - event_shape_lag] + // X_full.shape[-2] + ) + q_in = X.shape[-2] * n_w + self._set_sampler(q_in=q_in, posterior=posterior) + samples = self._get_f_X_samples(posterior=posterior, q_in=q_in) + # Add previous nehvi from pending points. + return self._compute_qehvi(samples=samples, X=X) + self._prev_nehvi
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/multi_fidelity.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/multi_fidelity.html new file mode 100644 index 0000000000..ea359487a2 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/multi_fidelity.html @@ -0,0 +1,209 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.multi_fidelity

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Multi-Fidelity Acquisition Functions for Multi-objective Bayesian optimization.
+
+References
+
+.. [Irshad2021MOMF]
+    F. Irshad, S. Karsch, and A. Döpp. Expected hypervolume improvement for
+    simultaneous multi-objective and multi-fidelity optimization.
+    arXiv preprint arXiv:2112.13901, 2021.
+
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import torch
+from botorch.acquisition.cost_aware import InverseCostWeightedUtility
+from botorch.acquisition.multi_objective.monte_carlo import (
+    qExpectedHypervolumeImprovement,
+)
+from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
+from botorch.models.cost import AffineFidelityCostModel
+from botorch.models.deterministic import GenericDeterministicModel
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    NondominatedPartitioning,
+)
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+
+
+
+[docs] +class MOMF(qExpectedHypervolumeImprovement): + def __init__( + self, + model: Model, + ref_point: list[float] | Tensor, + partitioning: NondominatedPartitioning, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + eta: Tensor | float = 1e-3, + X_pending: Tensor | None = None, + cost_call: Callable[[Tensor], Tensor] | None = None, + ) -> None: + r"""MOMF acquisition function supporting m>=2 outcomes. + The model needs to have train_obj that has a fidelity + objective appended to its end. + In the following example we consider a 2-D output space + but the ref_point is 3D because of fidelity objective. + + See [Irshad2021MOMF]_ for details. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> ref_point = [0.0, 0.0, 0.0] + >>> cost_func = lambda X: 5 + X[..., -1] + >>> momf = MOMF(model, ref_point, partitioning, cost_func) + >>> momf_val = momf(test_X) + + Args: + model: A fitted model. There are two default assumptions in the training + data. `train_X` should have fidelity parameter `s` as the last dimension + of the input and `train_Y` contains a trust objective as its last + dimension. + ref_point: A list or tensor with `m+1` elements representing the reference + point (in the outcome space) w.r.t. to which compute the hypervolume. + The '+1' takes care of the trust objective appended to `train_Y`. + This is a reference point for the objective values (i.e. after + applying`objective` to the samples). + partitioning: A `NondominatedPartitioning` module that provides the non- + dominated front and a partitioning of the non-dominated space in hyper- + rectangles. If constraints are present, this partitioning must only + include feasible points. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + objective: The MCMultiOutputObjective under which the samples are evaluated. + Defaults to `IdentityMCMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. The acquisition function will compute expected feasible + hypervolume. + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation but have not yet + been evaluated. Concatenated into `X` upon forward call. Copied and set + to have no gradient. + cost_call: A callable cost function mapping a Tensor of dimension + `batch_shape x q x d` to a cost Tensor of dimension + `batch_shape x q x m`. Defaults to an AffineCostModel with + `C(s) = 1 + s`. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same eta is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + eta value. + """ + + if len(ref_point) != partitioning.num_outcomes: + raise ValueError( + "The length of the reference point must match the number of outcomes. " + f"Got ref_point with {len(ref_point)} elements, but expected " + f"{partitioning.num_outcomes}." + ) + ref_point = torch.as_tensor( + ref_point, + dtype=partitioning.pareto_Y.dtype, + device=partitioning.pareto_Y.device, + ) + super().__init__( + model=model, + ref_point=ref_point, + partitioning=partitioning, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + X_pending=X_pending, + ) + + if cost_call is None: + cost_model = AffineFidelityCostModel( + fidelity_weights={-1: 1.0}, fixed_cost=1.0 + ) + else: + cost_model = GenericDeterministicModel(cost_call) + cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) + self.cost_aware_utility = cost_aware_utility + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + posterior = self.model.posterior(X) + samples = self.get_posterior_samples(posterior) + hv_gain = self._compute_qehvi(samples=samples, X=X) + cost_weighted_qehvi = self.cost_aware_utility(X=X, deltas=hv_gain) + return cost_weighted_qehvi
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/multi_output_risk_measures.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/multi_output_risk_measures.html new file mode 100644 index 0000000000..2ad57d9719 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/multi_output_risk_measures.html @@ -0,0 +1,886 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.multi_output_risk_measures

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Multi-output extensions of the risk measures, implemented as Monte-Carlo
+objectives. Except for MVaR, the risk measures are computed over each
+output dimension independently. In contrast, MVaR is computed using the
+joint distribution of the outputs, and provides more accurate risk estimates.
+
+References
+
+.. [Prekopa2012MVaR]
+    A. Prekopa. Multivariate value at risk and related topics.
+    Annals of Operations Research, 2012.
+
+.. [Cousin2013MVaR]
+    A. Cousin and E. Di Bernardino. On multivariate extensions of Value-at-Risk.
+    Journal of Multivariate Analysis, 2013.
+
+.. [Daulton2022MARS]
+    S. Daulton, S, Cakmak, M. Balandat, M. Osborne, E. Zhou, and E. Bakshy.
+    Robust multi-objective Bayesian optimization under input noise.
+    Proceedings of the 39th International Conference on Machine Learning, 2022.
+"""
+
+import warnings
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from math import ceil
+
+import torch
+from botorch.acquisition.multi_objective.objective import (
+    IdentityMCMultiOutputObjective,
+    MCMultiOutputObjective,
+)
+from botorch.acquisition.risk_measures import CVaR, RiskMeasureMCObjective, VaR
+from botorch.exceptions.errors import UnsupportedError
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models.model import Model
+from botorch.utils.multi_objective.pareto import is_non_dominated
+from botorch.utils.transforms import normalize
+from torch import Tensor
+
+
+
+[docs] +class MultiOutputRiskMeasureMCObjective( + RiskMeasureMCObjective, MCMultiOutputObjective, ABC +): + r"""Objective transforming the multi-output posterior samples to samples + of a multi-output risk measure. + + The risk measure is calculated over joint q-batch samples from the posterior. + If the q-batch includes samples corresponding to multiple inputs, it is assumed + that first `n_w` samples correspond to first input, second `n_w` samples + correspond to second input, etc. + """ + + def __init__( + self, + n_w: int, + preprocessing_function: Callable[[Tensor], Tensor] | None = None, + ) -> None: + r"""Transform the posterior samples to samples of a risk measure. + + Args: + n_w: The size of the `w_set` to calculate the risk measure over. + preprocessing_function: A preprocessing function to apply to the + samples before computing the risk measure. This can be used to + remove non-objective outcomes or to align all outcomes for + maximization. For constrained optimization, this should also + apply feasibility-weighting to samples. Given a `batch x m`-dim + tensor of samples, this should return a `batch x m'`-dim tensor. + """ + super().__init__(n_w=n_w, preprocessing_function=preprocessing_function) + + def _prepare_samples(self, samples: Tensor) -> Tensor: + r"""Prepare samples for risk measure calculations by scaling and + separating out the q-batch dimension. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + + Returns: + A `sample_shape x batch_shape x q x n_w x m'`-dim tensor of + prepared samples. + """ + samples = self.preprocessing_function(samples) + return samples.view(*samples.shape[:-2], -1, self.n_w, samples.shape[-1]) + +
+[docs] + @abstractmethod + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the risk measure corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q x m'`-dim tensor of risk measure samples. + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class MultiOutputExpectation(MultiOutputRiskMeasureMCObjective): + r"""A multi-output MC expectation risk measure. + + For unconstrained problems, we recommend using the `ExpectationPosteriorTransform` + instead. `ExpectationPosteriorTransform` directly transforms the posterior + distribution over `q * n_w` to a posterior of `q` expectations, significantly + reducing the cost of posterior sampling as a result. + """ + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the expectation of the given samples. Expectation is + calculated over each `n_w` samples in the q-batch dimension. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q x m'`-dim tensor of expectation samples. + """ + prepared_samples = self._prepare_samples(samples) + return prepared_samples.mean(dim=-2)
+
+ + + +
+[docs] +class IndependentCVaR(CVaR, MultiOutputRiskMeasureMCObjective): + r"""The multi-output Conditional Value-at-Risk risk measure that operates on + each output dimension independently. Since this does not consider the joint + distribution of the outputs (i.e., that the outputs were evaluated on same + perturbed input and are not independent), the risk estimates provided by + `IndependentCVaR` in general are more optimistic than the definition of CVaR + would suggest. + + The Conditional Value-at-Risk measures the expectation of the worst outcomes + (small rewards or large losses) with a total probability of `1 - alpha`. It + is commonly defined as the conditional expectation of the reward function, + with the condition that the reward is smaller than the corresponding + Value-at-Risk (also defined below). + + NOTE: Due to the use of a discrete `w_set` of samples, the VaR and CVaR + calculated here are (possibly biased) Monte-Carlo approximations of the + true risk measures. + """ + + def _get_sorted_prepared_samples(self, samples: Tensor) -> Tensor: + r"""Get the prepared samples that are sorted over the `n_w` dimension. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + + Returns: + A `sample_shape x batch_shape x q x n_w x m'`-dim tensor of sorted samples. + """ + prepared_samples = self._prepare_samples(samples) + return prepared_samples.sort(dim=-2, descending=True).values + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the CVaR corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q x m'`-dim tensor of CVaR samples. + """ + sorted_samples = self._get_sorted_prepared_samples(samples) + return sorted_samples[..., self.alpha_idx :, :].mean(dim=-2)
+
+ + + +
+[docs] +class IndependentVaR(IndependentCVaR): + r"""The multi-output Value-at-Risk risk measure that operates on each output + dimension independently. For the same reasons as `IndependentCVaR`, the risk + estimates provided by this are in general more optimistic than the definition + of VaR would suggest. + + Value-at-Risk measures the smallest possible reward (or largest possible loss) + after excluding the worst outcomes with a total probability of `1 - alpha`. It + is commonly used in financial risk management, and it corresponds to the + `1 - alpha` quantile of a given random variable. + """ + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the VaR corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q x m'`-dim tensor of VaR samples. + """ + sorted_samples = self._get_sorted_prepared_samples(samples) + return sorted_samples[..., self.alpha_idx, :]
+
+ + + +
+[docs] +class MultiOutputWorstCase(MultiOutputRiskMeasureMCObjective): + r"""The multi-output worst-case risk measure.""" + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the worst-case measure corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q x m'`-dim tensor of worst-case samples. + """ + prepared_samples = self._prepare_samples(samples) + return prepared_samples.min(dim=-2).values
+
+ + + +
+[docs] +class MVaR(MultiOutputRiskMeasureMCObjective): + r"""The multivariate Value-at-Risk as introduced in [Prekopa2012MVaR]_. + + MVaR is defined as the non-dominated set of points in the extended domain + of the random variable that have multivariate CDF greater than or equal to + `alpha`. Note that MVaR is set valued and the size of the set depends on the + particular realizations of the random variable. [Cousin2013MVaR]_ instead + propose to use the expectation of the set-valued MVaR as the multivariate + VaR. We support this alternative with an `expectation` flag. + + This supports approximate gradients as discussed in [Daulton2022MARS]_. + """ + + _verify_output_shape = False + + def __init__( + self, + n_w: int, + alpha: float, + expectation: bool = False, + preprocessing_function: Callable[[Tensor], Tensor] | None = None, + *, + pad_to_n_w: bool = False, + filter_dominated: bool = True, + use_counting: bool = False, + ) -> None: + r"""The multivariate Value-at-Risk. + + Args: + n_w: The size of the `w_set` to calculate the risk measure over. + alpha: The risk level of MVaR, float in `(0.0, 1.0]`. Each MVaR value + dominates `alpha` fraction of all observations. + expectation: If True, returns the expectation of the MVaR set as is + done in [Cousin2013MVaR]_. Otherwise, it returns the union of all + values in the MVaR set. Default: False. + preprocessing_function: A preprocessing function to apply to the + samples before computing the risk measure. This can be used to + remove non-objective outcomes or to align all outcomes for + maximization. For constrained optimization, this should also + apply feasibility-weighting to samples. Given a `batch x m`-dim + tensor of samples, this should return a `batch x m'`-dim tensor. + pad_to_n_w: If True, instead of padding up to `k'`, which is the size of + the largest MVaR set across all batches, we pad the MVaR set up to + `n_w`. This produces a return tensor of known size, however, it may + in general be much larger than the alternative. See `forward` for + more details on the return shape. + NOTE: this is only relevant if `expectation=False`. + filter_dominated: If True, returns the non-dominated subset of + alpha level points (this is MVaR as defined by [Prekopa2012MVaR]_). + Disabling this will make it faster, and may be preferable if + the dominated points will be filtered out later, e.g., while + calculating the hypervolume. Disabling this is not recommended + if `expectation=True`. + use_counting: If True, uses `get_mvar_set_via_counting` for finding the + MVaR set. This is method is less memory intensive than the vectorized + implementation, which is beneficial when `n_w` is quite large. + """ + super().__init__(n_w=n_w, preprocessing_function=preprocessing_function) + if not 0 < alpha <= 1: + raise ValueError("`alpha` must be in (0.0, 1.0]") + self.alpha = alpha + self.expectation = expectation + self.pad_to_n_w = pad_to_n_w + self.filter_dominated = filter_dominated + self.use_counting = use_counting + +
+[docs] + def get_mvar_set_via_counting(self, Y: Tensor) -> list[Tensor]: + r"""Find MVaR set based on the definition in [Prekopa2012MVaR]_. + + This first calculates the CDF for each point on the extended domain of the + random variable (the grid defined by the given samples), then takes the + values with CDF equal to (rounded if necessary) `alpha`. The non-dominated + subset of these form the MVaR set. + + This implementation processes each batch of `Y` in a for loop using a counting + based implementation. It requires less memory than the vectorized implementation + and should be used with large (>128) `n_w` values. + + Args: + Y: A `batch x n_w x m`-dim tensor of outcomes. + + Returns: + A `batch` length list of `k x m`-dim tensor of MVaR values, where `k` + depends on the corresponding batch inputs. Note that MVaR values in general + are not in-sample points. + """ + if Y.dim() == 3: + return sum((self.get_mvar_set_via_counting(y_) for y_ in Y), []) + m = Y.shape[-1] + # Generate sets of all unique values in each output dimension. + # Note that points in MVaR are bounded from above by the + # independent VaR of each objective. Hence, we only need to + # consider the unique outcomes that are less than or equal to + # the VaR of the independent objectives + var_alpha_idx = ceil(self.alpha * self.n_w) - 1 + Y_sorted = Y.topk(Y.shape[0] - var_alpha_idx, dim=0, largest=False).values + unique_outcomes_list = [] + for i in range(m): + sorted_i = Y_sorted[:, i].cpu().clone(memory_format=torch.contiguous_format) + unique_outcomes_list.append(sorted_i.unique().tolist()[::-1]) + # Convert this into a list of m dictionaries mapping values to indices. + unique_outcomes = [ + dict(zip(outcomes, range(len(outcomes)))) + for outcomes in unique_outcomes_list + ] + # Initialize a tensor counting the number of points in Y that a given grid point + # is dominated by. This will essentially be a non-normalized CDF. + counter_tensor = torch.zeros( + [len(outcomes) for outcomes in unique_outcomes], + dtype=torch.long, + device=Y.device, + ) + # populate the tensor, counting the dominated points. + # we only need to consider points in Y where at least one + # objective is less than the max objective value in + # unique_outcomes_list + max_vals = torch.tensor( + [o[0] for o in unique_outcomes_list], dtype=Y.dtype, device=Y.device + ) + mask = (Y < max_vals).any(dim=-1) + counter_tensor += self.n_w - mask.sum() + Y_pruned = Y[mask] + for y_ in Y_pruned: + starting_idcs = [unique_outcomes[i].get(y_[i].item(), 0) for i in range(m)] + slices = [slice(s_idx, None) for s_idx in starting_idcs] + counter_tensor[slices] += 1 + + # Get the count alpha-level points should have. + alpha_count = ceil(self.alpha * self.n_w) + # Get the alpha level indices. + alpha_level_indices = (counter_tensor == alpha_count).nonzero(as_tuple=False) + # If there are no exact alpha level points, get the smallest alpha' > alpha + # and find the corresponding alpha level indices. + if alpha_level_indices.numel() == 0: + min_greater_than_alpha = counter_tensor[counter_tensor > alpha_count].min() + alpha_level_indices = (counter_tensor == min_greater_than_alpha).nonzero( + as_tuple=False + ) + unique_outcomes = [ + torch.as_tensor(list(outcomes.keys()), device=Y.device, dtype=Y.dtype) + for outcomes in unique_outcomes + ] + alpha_level_points = torch.stack( + [ + unique_outcomes[i][alpha_level_indices[:, i]] + for i in range(len(unique_outcomes)) + ], + dim=-1, + ) + # MVaR is simply the non-dominated subset of alpha level points. + if self.filter_dominated: + mask = is_non_dominated(alpha_level_points) + mvar = alpha_level_points[mask] + else: + mvar = alpha_level_points + return [mvar]
+ + +
+[docs] + def get_mvar_set_vectorized(self, Y: Tensor) -> list[Tensor]: + r"""Find MVaR set based on the definition in [Prekopa2012MVaR]_. + + This first calculates the CDF for each point on the extended domain of the + random variable (the grid defined by the given samples), then takes the + values with CDF equal to (rounded if necessary) `alpha`. The non-dominated + subset of these form the MVaR set. + + This implementation uses computes the CDF of each point using highly vectorized + operations. As such, it may use large amounts of memory, particularly when the + batch size and/or `n_w` are large. It is typically faster than the alternative + implementation when computing MVaR of a large batch of points with small to + moderate (<128 for m=2, <64 for m=3) `n_w`. + + Args: + Y: A `batch x n_w x m`-dim tensor of observations. + + Returns: + A `batch` length list of `k x m`-dim tensor of MVaR values, where `k` + depends on the corresponding batch inputs. Note that MVaR values in general + are not in-sample points. + """ + if Y.dim() == 2: + Y = Y.unsqueeze(0) + batch, m = Y.shape[0], Y.shape[-1] + # Note that points in MVaR are bounded from above by the + # independent VaR of each objective. Hence, we only need to + # consider the unique outcomes that are less than or equal to + # the VaR of the independent objectives + var_alpha_idx = ceil(self.alpha * self.n_w) - 1 + n_points = Y.shape[-2] - var_alpha_idx + Y_sorted = Y.topk(n_points, dim=-2, largest=False).values + # `y_grid` is the grid formed by all inputs in each batch. + if m == 2: + # This is significantly faster but only works with m=2. + y_grid = torch.stack( + [ + Y_sorted[..., 0].repeat_interleave(repeats=n_points, dim=-1), + Y_sorted[..., 1].repeat(1, n_points), + ], + dim=-1, + ) + else: + y_grid = torch.stack( + [ + torch.stack( + torch.meshgrid( + [Y_sorted[b, :, i] for i in range(m)], indexing="ij" + ), + dim=-1, + ).view(-1, m) + for b in range(batch) + ], + dim=0, + ) + # Get the non-normalized CDF. + cdf = (Y.unsqueeze(-2) >= y_grid.unsqueeze(-3)).all(dim=-1).sum(dim=-2) + # Get the alpha level points + alpha_count = ceil(self.alpha * self.n_w) + # NOTE: Need to loop here since mvar may have different shapes. + mvar = [] + for b in range(batch): + alpha_level_points = y_grid[b][cdf[b] == alpha_count] + # If there are no exact alpha level points, get the smallest alpha' > alpha + # and find the corresponding alpha level indices. + if alpha_level_points.numel() == 0: + min_greater_than_alpha = cdf[b][cdf[b] > alpha_count].min() + alpha_level_points = y_grid[b][cdf[b] == min_greater_than_alpha] + # MVaR is the non-dominated subset of alpha level points. + if self.filter_dominated: + mask = is_non_dominated(alpha_level_points) + mvar.append(alpha_level_points[mask]) + else: + mvar.append(alpha_level_points) + return mvar
+ + +
+[docs] + def make_differentiable(self, prepared_samples: Tensor, mvars: Tensor) -> Tensor: + r"""An experimental approach for obtaining the gradient of the MVaR via + component-wise mapping to original samples. See [Daulton2022MARS]_. + + Args: + prepared_samples: A `(sample_shape * batch_shape * q) x n_w x m`-dim tensor + of posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + mvars: A `(sample_shape * batch_shape * q) x k x m`-dim tensor + of padded MVaR values. + Returns: + The same `mvars` with entries mapped to inputs to produce gradients. + """ + samples = prepared_samples.unsqueeze(-2).repeat(1, 1, mvars.shape[-2], 1) + mask = samples == mvars.unsqueeze(-3) + samples[~mask] = 0 + return samples.sum(dim=-3) / mask.sum(dim=-3)
+ + +
+[docs] + def forward( + self, + samples: Tensor, + X: Tensor | None = None, + ) -> Tensor: + r"""Calculate the MVaR corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q x m'`-dim tensor of MVaR values, + if `self.expectation=True`. + Otherwise, this returns a `sample_shape x batch_shape x (q * k') x m'`-dim + tensor, where `k'` is the maximum `k` across all batches that is returned + by `get_mvar_set_...`. Each `(q * k') x m'` corresponds to the `k` MVaR + values for each `q` batch of `n_w` inputs, padded up to `k'` by repeating + the last element. If `self.pad_to_n_w`, we set `k' = self.n_w`, producing + a deterministic return shape. + """ + batch_shape, m = samples.shape[:-2], samples.shape[-1] + prepared_samples = self._prepare_samples(samples) + # This is -1 x n_w x m. + prepared_samples = prepared_samples.reshape(-1, *prepared_samples.shape[-2:]) + with torch.no_grad(): + if self.use_counting: + mvar_set = self.get_mvar_set_via_counting(prepared_samples) + else: + mvar_set = self.get_mvar_set_vectorized(prepared_samples) + # Set the `pad_size` to either `self.n_w` or the size of the largest MVaR set. + pad_size = self.n_w if self.pad_to_n_w else max([_.shape[0] for _ in mvar_set]) + padded_mvar_list = [] + for mvar_ in mvar_set: + if self.expectation: + padded_mvar_list.append(mvar_.mean(dim=0)) + else: + # Repeat the last entry to make `mvar_set` `pad_size x m`. + repeats_needed = pad_size - mvar_.shape[0] + padded_mvar_list.append( + torch.cat([mvar_, mvar_[-1].expand(repeats_needed, m)], dim=0) + ) + mvars = torch.stack(padded_mvar_list, dim=0) + if samples.requires_grad: + mvars = self.make_differentiable( + prepared_samples=prepared_samples, mvars=mvars + ) + return mvars.view(*batch_shape, -1, m)
+
+ + + +
+[docs] +class MARS(VaR, MultiOutputRiskMeasureMCObjective): + r"""MVaR Approximation based on Random Scalarizations as introduced + in [Daulton2022MARS]_. + + This approximates MVaR via VaR of Chebyshev scalarizations, where each + scalarization corresponds to a point in the MVaR set. As implemented, + this uses one set of scalarization weights to approximate a single MVaR value. + Note that due to the normalization within the Chebyshev scalarization, + the output of this risk measure may not be on the same scale as its inputs. + """ + + _is_mo: bool = False + + def __init__( + self, + alpha: float, + n_w: int, + chebyshev_weights: Tensor | list[float], + baseline_Y: Tensor | None = None, + ref_point: Tensor | list[float] | None = None, + preprocessing_function: Callable[[Tensor], Tensor] | None = None, + ) -> None: + r"""Transform the posterior samples to samples of a risk measure. + + Args: + alpha: The risk level, float in `(0.0, 1.0]`. + n_w: The size of the perturbation set to calculate the risk measure over. + chebyshev_weights: The weights to use in the Chebyshev scalarization. + The Chebyshev scalarization is applied before computing VaR. + The weights must be non-negative. See `preprocessing_function` to + support minimization objectives. + baseline_Y: An `n' x d`-dim tensor of baseline outcomes to use in + determining the normalization bounds for Chebyshev scalarization. + It is recommended to set this via `set_baseline_Y` helper. + ref_point: An optional MVaR reference point to use in determining + the normalization bounds for Chebyshev scalarization. + preprocessing_function: A preprocessing function to apply to the + samples before computing the risk measure. This can be used to + remove non-objective outcomes or to align all outcomes for + maximization. For constrained optimization, this should also + apply feasibility-weighting to samples. + """ + if preprocessing_function is None: + preprocessing_function = IdentityMCMultiOutputObjective() + super().__init__( + alpha=alpha, + n_w=n_w, + preprocessing_function=preprocessing_function, + ) + self.chebyshev_weights = torch.as_tensor(chebyshev_weights) + self.baseline_Y = baseline_Y + self.register_buffer( + "ref_point", torch.as_tensor(ref_point) if ref_point is not None else None + ) + self.mvar = MVaR(n_w=self.n_w, alpha=self.alpha) + self._chebyshev_objective = None + +
+[docs] + def set_baseline_Y( + self, + model: Model | None, + X_baseline: Tensor | None, + Y_samples: Tensor | None = None, + ) -> None: + r"""Set the `baseline_Y` based on the MVaR predictions of the `model` + for `X_baseline`. + + Args: + model: The model being used for MARS optimization. Must have a compatible + `InputPerturbation` transform attached. Ignored if `Y_samples` is given. + X_baseline: An `n x d`-dim tensor of previously evaluated points. + Ignored if `Y_samples` is given. + Y_samples: An optional `(n * n_w) x d`-dim tensor of predictions. If given, + instead of sampling from the model, these are used. + """ + if Y_samples is None: + with torch.no_grad(): + Y = model.posterior(X_baseline.unsqueeze(-2)).mean.squeeze(-2) + else: + if model is not None or X_baseline is not None: + warnings.warn( + "`model` and `X_baseline` are ignored when `Y_samples` is " + "provided to `MARS.set_baseline_Y`.", + BotorchWarning, + stacklevel=2, + ) + Y = Y_samples + Y = self.preprocessing_function(Y) + Y = self.mvar(Y).view(-1, Y.shape[-1]) + Y = Y[is_non_dominated(Y)] + self.baseline_Y = Y
+ + + @property + def chebyshev_weights(self) -> Tensor: + r"""The weights used in Chebyshev scalarization.""" + return self._chebyshev_weights + + @chebyshev_weights.setter + def chebyshev_weights(self, chebyshev_weights: Tensor | list[float]) -> None: + r"""Update the Chebyshev weights. + + Invalidates the cached Chebyshev objective. + + Args: + chebyshev_weights: The weights to use in the Chebyshev scalarization. + The Chebyshev scalarization is applied before computing VaR. + The weights must be non-negative. See `preprocessing_function` to + support minimization objectives. + """ + self._chebyshev_objective = None + chebyshev_weights = torch.as_tensor(chebyshev_weights) + if torch.any(chebyshev_weights < 0): + raise UnsupportedError("Negative weights are not supported in MARS.") + if chebyshev_weights.dim() != 1: + raise UnsupportedError("Batched weights are not supported in MARS.") + self.register_buffer("_chebyshev_weights", chebyshev_weights) + + @property + def baseline_Y(self) -> Tensor | None: + r"""Baseline outcomes used in determining the normalization bounds.""" + return self._baseline_Y + + @baseline_Y.setter + def baseline_Y(self, baseline_Y: Tensor | None) -> None: + r"""Update the baseline outcomes. + + Invalidates the cached Chebyshev objective. + + Args: + baseline_Y: An `n' x d`-dim tensor of baseline outcomes to use in + determining the normalization bounds for Chebyshev scalarization. + It is recommended to set this via `set_baseline_Y` helper. + """ + self._chebyshev_objective = None + self.register_buffer("_baseline_Y", baseline_Y) + + @property + def chebyshev_objective(self) -> Callable[[Tensor, Tensor | None], Tensor]: + r"""The objective for applying the Chebyshev scalarization.""" + if self._chebyshev_objective is None: + self._construct_chebyshev_objective() + return self._chebyshev_objective + + def _construct_chebyshev_objective(self) -> None: + r"""Construct a Chebyshev scalarization. Outcomes are first normalized to [0,1], + then the Chebyshev scalarization is applied. + + NOTE: This is a modified version of the `get_chebyshev_scalarization` helper. + It doesn't support negative weights. All objectives should be aligned for + maximization using `preprocessing_function`. + """ + if self.baseline_Y is None: + raise RuntimeError( + "baseline_Y must be set before constructing the Chebyshev objective." + ) + ref_point = self.ref_point + if ref_point is not None: + ref_point = ref_point.to(self.baseline_Y) + Y_bounds = self._get_Y_normalization_bounds( + Y=self.baseline_Y, ref_point=ref_point + ) + if ref_point is not None: + ref_point = normalize(ref_point.unsqueeze(0), bounds=Y_bounds).squeeze(0) + + def chebyshev_obj(Y: Tensor, X: Tensor | None = None) -> Tensor: + Y = self.preprocessing_function(Y) + Y = normalize(Y, bounds=Y_bounds) + if ref_point is not None: + Y = Y - ref_point + product = torch.einsum("...m,m->...m", Y, self.chebyshev_weights.to(Y)) + return product.min(dim=-1).values + + self._chebyshev_objective = chebyshev_obj + + def _prepare_samples(self, samples: Tensor) -> Tensor: + r"""Prepare samples for VaR computation by applying the Chebyshev scalarization + and separating out the q-batch dimension. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + + Returns: + A `sample_shape x batch_shape x q x n_w`-dim tensor of prepared samples. + """ + samples = self.chebyshev_objective(samples) + return samples.view(*samples.shape[:-1], -1, self.n_w) + + @staticmethod + def _get_Y_normalization_bounds( + Y: Tensor, + ref_point: Tensor | None = None, + ) -> Tensor: + r"""Get normalization bounds for scalarizations. + + Args: + Y: A `n x m`-dim tensor of outcomes. + ref_point: The reference point. + + Returns: + A `2 x m`-dim tensor containing the normalization bounds. + """ + if ref_point is not None: + ref_point = ref_point.to(Y) + + if Y.ndim != 2: + raise UnsupportedError("Batched Y is not supported.") + + if Y.shape[-2] == 0: + # If there are no observations, return standard bounds. + Y_bounds = torch.zeros(2, Y.shape[-1], dtype=Y.dtype, device=Y.device) + Y_bounds[1] = 1.0 + return Y_bounds + + pareto_Y = Y[is_non_dominated(Y)] + if pareto_Y.shape[-2] == 1: + if ref_point is not None and (pareto_Y > ref_point).all(): + Y_bounds = torch.cat([ref_point.unsqueeze(0), pareto_Y], dim=0) + else: + # If there is only one observation, set the bounds to be [Y_m, Y_m + 1] + # for each objective m. This ensures we do not divide by zero. + Y_bounds = torch.cat([pareto_Y, pareto_Y + 1], dim=0) + else: + if ref_point is None: + better_than_ref = torch.ones( + pareto_Y.shape[0], device=pareto_Y.device, dtype=torch.long + ) + else: + better_than_ref = (pareto_Y > ref_point).all(dim=-1) + if ref_point is not None and better_than_ref.any(): + nadir = ref_point + pareto_Y = pareto_Y[better_than_ref] + else: + nadir = pareto_Y.min(dim=-2).values + ideal = pareto_Y.max(dim=-2).values + Y_bounds = torch.stack([nadir, ideal]) + + # If any of the lower bounds is equal to the upper bound, increase the + # upper bound to prevent division by zero. + Y_range = Y_bounds.max(dim=0).values - Y_bounds.min(dim=0).values + mask = Y_range <= 0 + Y_bounds[1, mask] = Y_bounds[1, mask] + 1.0 + return Y_bounds
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/objective.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/objective.html new file mode 100644 index 0000000000..700e07f486 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/objective.html @@ -0,0 +1,291 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.objective

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from abc import abstractmethod
+
+import torch
+from botorch.acquisition.objective import GenericMCObjective, MCAcquisitionObjective
+from botorch.exceptions.errors import BotorchError, BotorchTensorDimensionError
+from botorch.models.model import Model
+from botorch.utils import apply_constraints
+from botorch.utils.transforms import normalize_indices
+from torch import Tensor
+
+
+
+[docs] +class MCMultiOutputObjective(MCAcquisitionObjective): + r"""Abstract base class for MC multi-output objectives. + + Args: + _is_mo: A boolean denoting whether the objectives are multi-output. + """ + + _is_mo: bool = True + +
+[docs] + @abstractmethod + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Evaluate the multi-output objective on the samples. + + Args: + samples: A `sample_shape x batch_shape x q x m`-dim Tensors of samples from + a model posterior. + X: A `batch_shape x q x d`-dim Tensors of inputs. + + Returns: + A `sample_shape x batch_shape x q x m'`-dim Tensor of objective values with + `m'` the output dimension. This assumes maximization in each output + dimension). + + This method is usually not called directly, but via the objectives. + + Example: + >>> # `__call__` method: + >>> samples = sampler(posterior) + >>> outcomes = multi_obj(samples) + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class GenericMCMultiOutputObjective(GenericMCObjective, MCMultiOutputObjective): + r"""Multi-output objective generated from a generic callable. + + Allows to construct arbitrary MC-objective functions from a generic + callable. In order to be able to use gradient-based acquisition function + optimization it should be possible to backpropagate through the callable. + """ + + pass
+ + + +
+[docs] +class IdentityMCMultiOutputObjective(MCMultiOutputObjective): + r"""Trivial objective that returns the unaltered samples. + + Example: + >>> identity_objective = IdentityMCMultiOutputObjective() + >>> samples = sampler(posterior) + >>> objective = identity_objective(samples) + """ + + def __init__( + self, outcomes: list[int] | None = None, num_outcomes: int | None = None + ) -> None: + r"""Initialize Objective. + + Args: + outcomes: A list of the `m'` indices that the weights should be + applied to. + num_outcomes: The total number of outcomes `m` + """ + super().__init__() + if outcomes is not None: + if len(outcomes) < 2: + raise BotorchTensorDimensionError( + "Must specify at least two outcomes for MOO." + ) + if any(i < 0 for i in outcomes): + if num_outcomes is None: + raise BotorchError( + "num_outcomes is required if any outcomes are less than 0." + ) + outcomes = normalize_indices(outcomes, num_outcomes) + self.register_buffer("outcomes", torch.tensor(outcomes, dtype=torch.long)) + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + if hasattr(self, "outcomes"): + return samples.index_select(-1, self.outcomes.to(device=samples.device)) + return samples
+
+ + + +
+[docs] +class WeightedMCMultiOutputObjective(IdentityMCMultiOutputObjective): + r"""Objective that reweights samples by given weights vector. + + Example: + >>> weights = torch.tensor([1.0, -1.0]) + >>> weighted_objective = WeightedMCMultiOutputObjective(weights) + >>> samples = sampler(posterior) + >>> objective = weighted_objective(samples) + """ + + def __init__( + self, + weights: Tensor, + outcomes: list[int] | None = None, + num_outcomes: int | None = None, + ) -> None: + r"""Initialize Objective. + + Args: + weights: `m'`-dim tensor of outcome weights. + outcomes: A list of the `m'` indices that the weights should be + applied to. + num_outcomes: the total number of outcomes `m` + """ + super().__init__(outcomes=outcomes, num_outcomes=num_outcomes) + if weights.ndim != 1: + raise BotorchTensorDimensionError( + f"weights must be an 1-D tensor, but got {weights.shape}." + ) + elif outcomes is not None and weights.shape[0] != len(outcomes): + raise BotorchTensorDimensionError( + "weights must contain the same number of elements as outcomes, " + f"but got {weights.numel()} weights and {len(outcomes)} outcomes." + ) + self.register_buffer("weights", weights) + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + samples = super().forward(samples=samples) + return samples * self.weights.to(samples)
+
+ + + +
+[docs] +class FeasibilityWeightedMCMultiOutputObjective(MCMultiOutputObjective): + def __init__( + self, + model: Model, + X_baseline: Tensor, + constraint_idcs: list[int], + objective: MCMultiOutputObjective | None = None, + ) -> None: + r"""Construct a feasibility-weighted objective. + + This applies feasibility weighting before calculating the objective value. + Defaults to identity if no constraints or objective is present. + + NOTE: By passing in a single-output `MCAcquisitionObjective` as the `objective`, + this can be used as a single-output `MCAcquisitionObjective` as well. + + Args: + model: A fitted Model. + X_baseline: An `n x d`-dim tensor of points already observed. + constraint_idcs: The outcome indices of the constraints. Constraints are + handled by weighting the samples according to a sigmoid approximation + of feasibility. A positive constraint outcome implies feasibility. + objective: An optional objective to apply after feasibility-weighting + the samples. + """ + super().__init__() + num_outputs = model.num_outputs + # Get the non-negative indices. + constraint_idcs = [ + num_outputs + idx if idx < 0 else idx for idx in constraint_idcs + ] + if len(constraint_idcs) != len(set(constraint_idcs)): + raise ValueError("Received duplicate entries for `constraint_idcs`.") + # Extract the indices for objective outcomes. + objective_idcs = [i for i in range(num_outputs) if i not in constraint_idcs] + if len(constraint_idcs) > 0: + # Import locally to avoid circular import. + from botorch.acquisition.utils import get_infeasible_cost + + inf_cost = get_infeasible_cost( + X=X_baseline, model=model, objective=lambda y, X: y + )[objective_idcs] + + def apply_feasibility_weights(Y: Tensor, X: Tensor | None = None) -> Tensor: + return apply_constraints( + obj=Y[..., objective_idcs], + constraints=[lambda Y: -Y[..., i] for i in constraint_idcs], + samples=Y, + # This ensures that the dtype/device is set properly. + infeasible_cost=inf_cost.to(Y), + ) + + self.apply_feasibility_weights = apply_feasibility_weights + else: + self.apply_feasibility_weights = lambda Y: Y + if objective is None: + self.objective = lambda Y, X: Y + else: + self.objective = objective + self._verify_output_shape = objective._verify_output_shape + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + return self.objective(self.apply_feasibility_weights(samples), X=X)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/parego.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/parego.html new file mode 100644 index 0000000000..6ac4268aa1 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/parego.html @@ -0,0 +1,206 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.parego

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from collections.abc import Callable
+
+import torch
+from botorch.acquisition.logei import qLogNoisyExpectedImprovement, TAU_MAX, TAU_RELU
+from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction
+from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
+from botorch.acquisition.objective import GenericMCObjective
+from botorch.models.model import Model
+from botorch.posteriors.fully_bayesian import MCMC_DIM
+from botorch.sampling.base import MCSampler
+from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization
+from botorch.utils.sampling import sample_simplex
+from botorch.utils.transforms import is_ensemble
+from torch import Tensor
+
+
+
+[docs] +class qLogNParEGO(qLogNoisyExpectedImprovement, MultiObjectiveMCAcquisitionFunction): + def __init__( + self, + model: Model, + X_baseline: Tensor, + scalarization_weights: Tensor | None = None, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + X_pending: Tensor | None = None, + eta: Tensor | float = 1e-3, + fat: bool = True, + prune_baseline: bool = False, + cache_root: bool = True, + tau_relu: float = TAU_RELU, + tau_max: float = TAU_MAX, + ) -> None: + r"""q-LogNParEGO supporting m >= 2 outcomes. This acquisition function + utilizes qLogNEI to compute the expected improvement over Chebyshev + scalarization of the objectives. + + This is adapted from qNParEGO proposed in [Daulton2020qehvi]_ to utilize + log-improvement acquisition functions of [Ament2023logei]_. See [Knowles2005]_ + for the original ParEGO algorithm. + + This implementation assumes maximization of all objectives. If any of the model + outputs are to be minimized, either an `objective` should be used to negate the + model outputs or the `scalarization_weights` should be provided with negative + weights for the outputs to be minimized. + + Args: + model: A fitted multi-output model, producing outputs for `m` objectives + and any number of outcome constraints. + NOTE: The model posterior must have a `mean` attribute. + X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points + that have already been observed. These points are considered as + the potential best design point. + scalarization_weights: A `m`-dim Tensor of weights to be used in the + Chebyshev scalarization. If omitted, samples from the unit simplex. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MultiOutputMCAcquisitionObjective under which the samples are + evaluated before applying Chebyshev scalarization. + Defaults to `IdentityMultiOutputObjective()`. + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m'`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are satisfied if `constraint(samples) < 0`. + X_pending: A `batch_shape x q' x d`-dim Tensor of `q'` design points + that have points that have been submitted for function evaluation + but have not yet been evaluated. Concatenated into `X` upon + forward call. Copied and set to have no gradient. + eta: Temperature parameter(s) governing the smoothness of the sigmoid + approximation to the constraint indicators. See the docs of + `compute_(log_)smoothed_constraint_indicator` for details. + fat: Toggles the logarithmic / linear asymptotic behavior of the smooth + approximation to the ReLU. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the best point. This can significantly + improve performance and is generally recommended. In order to + customize pruning parameters, instead manually call + `botorch.acquisition.utils.prune_inferior_points` on `X_baseline` + before instantiating the acquisition function. + cache_root: A boolean indicating whether to cache the root + decomposition over `X_baseline` and use low-rank updates. + tau_max: Temperature parameter controlling the sharpness of the smooth + approximations to max. + tau_relu: Temperature parameter controlling the sharpness of the smooth + approximations to ReLU. + """ + MultiObjectiveMCAcquisitionFunction.__init__( + self, + model=model, + sampler=sampler, + objective=objective, + constraints=constraints, + eta=eta, + ) + org_objective = self.objective + # Create the composite objective. + with torch.no_grad(): + Y_baseline = org_objective(model.posterior(X_baseline).mean) + if is_ensemble(model): + Y_baseline = torch.mean(Y_baseline, dim=MCMC_DIM) + scalarization_weights = ( + scalarization_weights + if scalarization_weights is not None + else sample_simplex( + d=Y_baseline.shape[-1], device=X_baseline.device, dtype=X_baseline.dtype + ).view(-1) + ) + chebyshev_scalarization = get_chebyshev_scalarization( + weights=scalarization_weights, + Y=Y_baseline, + ) + composite_objective = GenericMCObjective( + objective=lambda samples, X=None: chebyshev_scalarization( + org_objective(samples=samples, X=X), X=X + ), + ) + qLogNoisyExpectedImprovement.__init__( + self, + model=model, + X_baseline=X_baseline, + sampler=sampler, + # This overwrites self.objective with the composite objective. + objective=composite_objective, + X_pending=X_pending, + constraints=constraints, + eta=eta, + fat=fat, + prune_baseline=prune_baseline, + cache_root=cache_root, + tau_max=tau_max, + tau_relu=tau_relu, + ) + # Set these after __init__ calls so that they're not overwritten / deleted. + # These are intended mainly for easier debugging & transparency. + self._org_objective: MCMultiOutputObjective = org_objective + self.chebyshev_scalarization: Callable[[Tensor, Tensor | None], Tensor] = ( + chebyshev_scalarization + ) + self.scalarization_weights: Tensor = scalarization_weights + self.Y_baseline: Tensor = Y_baseline
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/predictive_entropy_search.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/predictive_entropy_search.html new file mode 100644 index 0000000000..2f6a1034a0 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/predictive_entropy_search.html @@ -0,0 +1,1247 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.predictive_entropy_search

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Acquisition function for predictive entropy search for multi-objective Bayesian
+optimization (PES). The code does not support constraint handling.
+
+NOTE: The PES acquisition might not be differentiable. As a result, we recommend
+optimizing the acquisition function using finite differences.
+
+References:
+
+.. [Garrido-Merchan2019]
+    E. Garrido-Merchan and D. Hernandez-Lobato. Predictive Entropy Search for
+    Multi-objective Bayesian Optimization with Constraints. Neurocomputing. 2019.
+    The computation follows the procedure described in the supplementary material:
+    https://www.sciencedirect.com/science/article/abs/pii/S0925231219308525
+
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.exceptions import InputDataError
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.model import Model
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.utils import check_no_nans
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+from torch.distributions import Normal
+
+
+
+[docs] +class qMultiObjectivePredictiveEntropySearch(AcquisitionFunction): + r"""The acquisition function for Predictive Entropy Search. The code supports + both single and multiple objectives as well as batching. + + This acquisition function approximates the mutual information between the + observation at a candidate point `X` and the Pareto optimal input using the + moment-matching procedure known as expectation propagation (EP). + + See the Appendix of [Garrido-Merchan2019]_ for the description of the EP + procedure. + + IMPORTANT NOTES: + (i) The PES acquisition function estimated using EP is sometimes not + differentiable, and therefore we advise using a finite-difference estimate of + the gradient as opposed to the gradients identified using automatic + differentiation, which occasionally outputs `nan` values. + + The source of this differentiability is in the `_update_damping` function, which + finds the damping factor `a` that is used to update the EP parameters + `a * param_new + (1 - a) * param_old`. The damping factor has to ensure + that the updated covariance matrices, `a * cov_f_new + (1 - a) cov_f_old`, is + positive semi-definiteness. We follow the original paper, which identifies + `a` via a successive halving scheme i.e. we check `a=1` then `a=0.5` etc. This + procedure means `a` is a function of the test input `X`. This function is not + differentiable in `X`. + + (ii) EP could potentially fail for a number of reasons: + + (a) When the sampled Pareto optimal points `x_p` is poor compared to the + training or testing data `x_n`. + + (b) When the training or testing data `x_n` is close the Pareto optimal + points `x_p`. + + (c) When the convergence threshold is set too small. + + + Problem (a) occurs because we have to compute the variable: + `alpha = (mean(x_n) - mean(x_p)) / std(x_n - x_p)`, which becomes very + large when `x_n` is better than `x_p` with high-probability. This leads to a + log(0) error when we compute `log(1 - cdf(alpha))`. We have preemptively + clamped some values depending on `1`alpha` in order to mitigate this. + + Problem (b) occurs because we have to compute matrix inverses for the + two-dimensional marginals (x_n, x_p). To address this we manually add jitter + to the diagonal of the covariance matrix i.e. `ep_jitter` when training and + `test_jitter` when testing. The default choice is not always appropriate + because the same jitter is used for the inversion of the covariance + and precision matrix, which are on different scales. + + TODO: come up with strategy to adaptively update the jitter. + + Problem (c) occurs because a smaller threshold usually means that more EP + iterations are required. Running too many EP iterations could lead to + invertibility problems such as in problem (b). Setting a larger threshold + or reducing the number of EP iterations could alleviate this. + + (iii) The estimated acquisition value could be negative. + """ + + def __init__( + self, + model: Model, + pareto_sets: Tensor, + maximize: bool = True, + X_pending: Tensor | None = None, + max_ep_iterations: int = 250, + ep_jitter: float = 1e-4, + test_jitter: float = 1e-4, + threshold: float = 1e-2, + ) -> None: + r"""Multi-objective predictive entropy search acquisition function. + + Args: + model: A fitted batched model with `M` number of outputs. + pareto_sets: A `num_pareto_samples x P x d`-dim tensor containing the + Pareto optimal set of inputs, where `P` is the number of pareto + optimal points. The points in each sample have to be discrete + otherwise expectation propagation will fail. + maximize: If true, we consider a maximization problem. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation, but have not yet been evaluated. + max_ep_iterations: The maximum number of expectation propagation + iterations. (The minimum number of iterations is set at 3.) + ep_jitter: The amount of jitter added for the matrix inversion that + occurs during the expectation propagation update during the training + phase. + test_jitter: The amount of jitter added for the matrix inversion that + occurs during the expectation propagation update in the testing + phase. + threshold: The convergence threshold for expectation propagation. This + assesses the relative change in the mean and covariance. We default + to one percent change i.e. `threshold = 1e-2`. + """ + super().__init__(model=model) + + self.model = model + self.maximize = maximize + self.set_X_pending(X_pending) + + if model.num_outputs > 1 or isinstance(model, ModelListGP): + train_X = self.model.train_inputs[0][0] + else: + train_X = self.model.train_inputs[0] + + # Batch GP models (e.g. fantasized models) are not currently supported + if train_X.ndim > 2: + raise NotImplementedError( + "Batch GP models (e.g. fantasized models) are not supported." + ) + + if pareto_sets.ndim != 3 or pareto_sets.shape[-1] != train_X.shape[-1]: + raise UnsupportedError( + "The Pareto set should have a shape of " + "`num_pareto_samples x num_pareto_points x input_dim`." + ) + else: + self.pareto_sets = pareto_sets + + # add the pareto set to the existing training data + self.num_pareto_samples = pareto_sets.shape[0] + + self.augmented_X = torch.cat( + [train_X.repeat(self.num_pareto_samples, 1, 1), self.pareto_sets], dim=-2 + ) + self.max_ep_iterations = max_ep_iterations + self.ep_jitter = ep_jitter + self.test_jitter = test_jitter + self.threshold = threshold + self._expectation_propagation() + + def _expectation_propagation(self) -> None: + r"""Perform expectation propagation to obtain the covariance factors that + depend on the Pareto sets. + + The updates are performed in the natural parameter space. For a multivariate + normal distribution with mean mu and covariance Sigma, we call Sigma^{-1} + the natural covariance and Sigma^{-1} mu the natural mean. + """ + ########################################################################### + # INITIALIZATION + ########################################################################### + M = self.model.num_outputs + + if self.model.num_outputs > 1 or isinstance(self.model, ModelListGP): + train_X = self.model.train_inputs[0][0] + else: + train_X = self.model.train_inputs[0] + + tkwargs = {"dtype": train_X.dtype, "device": train_X.device} + N = len(train_X) + num_pareto_samples = self.num_pareto_samples + P = self.pareto_sets.shape[-2] + + # initialize the predictive natural mean and variances + ( + pred_nat_mean, + pred_nat_cov, + pred_mean, + pred_cov, + ) = _initialize_predictive_matrices( + X=self.augmented_X, + model=self.model, + observation_noise=False, + jitter=self.ep_jitter, + natural=True, + ) + + pred_f_mean = pred_mean[..., 0:M, :] + pred_f_nat_mean = pred_nat_mean[..., 0:M, :] + pred_f_cov = pred_cov[..., 0:M, :, :] + pred_f_nat_cov = pred_nat_cov[..., 0:M, :, :] + + # initialize the marginals + # `num_pareto_samples x M x (N + P)` + mean_f = pred_f_mean.clone() + nat_mean_f = pred_f_nat_mean.clone() + # `num_pareto_samples x M x (N + P) x (N + P)` + cov_f = pred_f_cov.clone() + nat_cov_f = pred_f_nat_cov.clone() + + # initialize omega the function which encodes the fact that the pareto points + # are optimal in the feasible space i.e. any point in the feasible space + # should not dominate the Pareto efficient points. + + # `num_pareto_samples x M x (N + P) x P x 2` + omega_f_nat_mean = torch.zeros((num_pareto_samples, M, N + P, P, 2), **tkwargs) + # `num_pareto_samples x M x (N + P) x P x 2 x 2` + omega_f_nat_cov = torch.zeros( + (num_pareto_samples, M, N + P, P, 2, 2), **tkwargs + ) + + ########################################################################### + # EXPECTATION PROPAGATION + ########################################################################### + damping = torch.ones(num_pareto_samples, M, **tkwargs) + + iteration = 0 + while (torch.sum(damping) > 0) and (iteration < self.max_ep_iterations): + # Compute the new natural mean and covariance + #################################################################### + # OBJECTIVE FUNCTION: OMEGA UPDATE + #################################################################### + omega_f_nat_mean_new, omega_f_nat_cov_new = _safe_update_omega( + mean_f=mean_f, + cov_f=cov_f, + omega_f_nat_mean=omega_f_nat_mean, + omega_f_nat_cov=omega_f_nat_cov, + N=N, + P=P, + M=M, + maximize=self.maximize, + jitter=self.ep_jitter, + ) + + #################################################################### + # OBJECTIVE FUNCTION: MARGINAL UPDATE + #################################################################### + nat_mean_f_new, nat_cov_f_new = _update_marginals( + pred_f_nat_mean=pred_f_nat_mean, + pred_f_nat_cov=pred_f_nat_cov, + omega_f_nat_mean=omega_f_nat_mean_new, + omega_f_nat_cov=omega_f_nat_cov_new, + N=N, + P=P, + ) + ######################################################################## + # OBJECTIVE FUNCTION: DAMPING UPDATE + ######################################################################## + # update damping of objectives + damping, cholesky_nat_cov_f = _update_damping( + nat_cov=nat_cov_f, + nat_cov_new=nat_cov_f_new, + damping_factor=damping, + jitter=self.ep_jitter, + ) + check_no_nans(cholesky_nat_cov_f) + ######################################################################## + # OBJECTIVE FUNCTION: DAMPED UPDATE + ######################################################################## + # Damp update of omega + omega_f_nat_mean = _damped_update( + old_factor=omega_f_nat_mean, + new_factor=omega_f_nat_mean_new, + damping_factor=damping, + ) + + omega_f_nat_cov = _damped_update( + old_factor=omega_f_nat_cov, + new_factor=omega_f_nat_cov_new, + damping_factor=damping, + ) + # update the mean and covariance + nat_mean_f = _damped_update( + old_factor=nat_mean_f, new_factor=nat_mean_f_new, damping_factor=damping + ) + nat_cov_f = _damped_update( + old_factor=nat_cov_f, new_factor=nat_cov_f_new, damping_factor=damping + ) + + # compute cholesky inverse + cov_f_new = torch.cholesky_inverse(cholesky_nat_cov_f) + mean_f_new = torch.einsum("...ij,...j->...i", cov_f_new, nat_mean_f) + check_no_nans(cov_f_new) + ######################################################################## + # OBJECTIVE FUNCTION: CONVERGENCE UPDATE + ######################################################################## + # Set the damping to zero when the change in the mean and + # covariance is less than the threshold + damping, delta_mean_f, delta_cov_f = _update_damping_when_converged( + mean_old=mean_f, + mean_new=mean_f_new, + cov_old=cov_f, + cov_new=cov_f_new, + damping_factor=damping, + threshold=self.threshold, + iteration=iteration, + ) + cov_f = cov_f_new + mean_f = mean_f_new + iteration = iteration + 1 + + ############################################################################ + # SAVE OMEGA AND PHI FACTORS + ############################################################################ + check_no_nans(omega_f_nat_mean) + check_no_nans(omega_f_nat_cov) + # save phi and omega for the forward + self._omega_f_nat_mean = omega_f_nat_mean + self._omega_f_nat_cov = omega_f_nat_cov + + def _compute_information_gain(self, X: Tensor) -> Tensor: + r"""Evaluate qMultiObjectivePredictiveEntropySearch on the candidate set `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of t-batches with `q` `d`-dim + design points each. + + Returns: + A `batch_shape'`-dim Tensor of Predictive Entropy Search values at the + given design points `X`. + """ + tkwargs = {"dtype": X.dtype, "device": X.device} + batch_shape = X.shape[0:-2] + q = X.shape[-2] + M = self.model.num_outputs + + if M > 1 or isinstance(self.model, ModelListGP): + N = len(self.model.train_inputs[0][0]) + else: + N = len(self.model.train_inputs[0]) + P = self.pareto_sets.shape[-2] + num_pareto_samples = self.num_pareto_samples + ########################################################################### + # AUGMENT X WITH THE SAMPLED PARETO SET + ########################################################################### + new_shape = batch_shape + torch.Size([num_pareto_samples]) + X.shape[-2:] + expanded_X = X.unsqueeze(-3).expand(new_shape) + expanded_ps = self.pareto_sets.expand(X.shape[0:-2] + self.pareto_sets.shape) + # `batch_shape x num_pareto_samples x (q + P) x d` + aug_X = torch.cat([expanded_X, expanded_ps], dim=-2) + + ########################################################################### + # COMPUTE THE POSTERIORS AND OBSERVATION NOISE + ########################################################################### + # compute predictive distribution without observation noise + ( + pred_nat_mean, + pred_nat_cov, + pred_mean, + pred_cov, + ) = _initialize_predictive_matrices( + X=aug_X, + model=self.model, + observation_noise=True, + jitter=self.test_jitter, + natural=True, + ) + + pred_f_mean = pred_mean[..., 0:M, :] + pred_f_nat_mean = pred_nat_mean[..., 0:M, :] + pred_f_cov = pred_cov[..., 0:M, :, :] + pred_f_nat_cov = pred_nat_cov[..., 0:M, :, :] + + (_, _, _, pred_cov_noise) = _initialize_predictive_matrices( + X=aug_X, + model=self.model, + observation_noise=True, + jitter=self.test_jitter, + natural=False, + ) + + pred_f_cov_noise = pred_cov_noise[..., 0:M, :, :] + observation_noise = pred_f_cov_noise - pred_f_cov + ########################################################################### + # INITIALIZE THE EP FACTORS + ########################################################################### + # `batch_shape x num_pareto_samples x M x (q + P) x P x 2` + omega_f_nat_mean = torch.zeros( + batch_shape + torch.Size([num_pareto_samples, M, q + P, P, 2]), **tkwargs + ) + # `batch_shape x num_pareto_samples x M x (q + P) x P x 2 x 2` + omega_f_nat_cov = torch.zeros( + batch_shape + torch.Size([num_pareto_samples, M, q + P, P, 2, 2]), **tkwargs + ) + ########################################################################### + # RUN EP ONCE + ########################################################################### + # run update omega once + omega_f_nat_mean, omega_f_nat_cov = _safe_update_omega( + mean_f=pred_f_mean, + cov_f=pred_f_cov, + omega_f_nat_mean=omega_f_nat_mean, + omega_f_nat_cov=omega_f_nat_cov, + N=q, + P=P, + M=M, + maximize=self.maximize, + jitter=self.test_jitter, + ) + ########################################################################### + # ADD THE CACHE FACTORS BACK + ########################################################################### + omega_f_nat_mean, omega_f_nat_cov = _augment_factors_with_cached_factors( + q=q, + N=N, + omega_f_nat_mean=omega_f_nat_mean, + cached_omega_f_nat_mean=self._omega_f_nat_mean, + omega_f_nat_cov=omega_f_nat_cov, + cached_omega_f_nat_cov=self._omega_f_nat_cov, + ) + ########################################################################### + # COMPUTE THE MARGINAL + ########################################################################### + nat_mean_f, nat_cov_f = _update_marginals( + pred_f_nat_mean=pred_f_nat_mean, + pred_f_nat_cov=pred_f_nat_cov, + omega_f_nat_mean=omega_f_nat_mean, + omega_f_nat_cov=omega_f_nat_cov, + N=q, + P=P, + ) + ########################################################################### + # COMPUTE THE DAMPED UPDATE + ########################################################################### + # # update damping of objectives + damping = torch.ones( + batch_shape + torch.Size([num_pareto_samples, M]), **tkwargs + ) + damping, cholesky_nat_cov_f_new = _update_damping( + nat_cov=pred_f_nat_cov, + nat_cov_new=nat_cov_f, + damping_factor=damping, + jitter=self.test_jitter, + ) + + # invert matrix + cov_f_new = torch.cholesky_inverse(cholesky_nat_cov_f_new) + check_no_nans(cov_f_new) + + ########################################################################### + # COMPUTE THE LOG DETERMINANTS + ########################################################################### + # compute the initial log determinant term + log_det_pred_f_cov_noise = _compute_log_determinant(cov=pred_f_cov_noise, q=q) + # compute the post log determinant term + log_det_cov_f = _compute_log_determinant(cov=cov_f_new + observation_noise, q=q) + + ########################################################################### + # COMPUTE THE ACQUISITION FUNCTION + ########################################################################### + q_pes_f = log_det_pred_f_cov_noise - log_det_cov_f + check_no_nans(q_pes_f) + + return 0.5 * q_pes_f + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qMultiObjectivePredictiveEntropySearch on the candidate set `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of t-batches with `q` `d`-dim + design points each. + + Returns: + A `batch_shape'`-dim Tensor of acquisition values at the given design + points `X`. + """ + return self._compute_information_gain(X)
+
+ + + +
+[docs] +def log_cdf_robust(x: Tensor) -> Tensor: + r"""Computes the logarithm of the normal cumulative density robustly. This uses + the approximation log(1-z) ~ -z when z is small: + + if x > 5: + log(cdf(x)) = log(1-cdf(-x)) approx -cdf(-x) + else: + log(cdf(x)). + + Args: + x: a `x_shape`-dim Tensor. + + Returns + A `x_shape`-dim Tensor. + """ + CLAMP_LB = torch.finfo(x.dtype).eps + NEG_INF = torch.finfo(x.dtype).min + normal = Normal(torch.zeros_like(x), torch.ones_like(x)) + cdf_x = normal.cdf(x) + neg_cdf_neg_x = -normal.cdf(-x) + log_cdf_x = torch.where(x < 5, torch.log(cdf_x), neg_cdf_neg_x) + + return log_cdf_x.clamp(NEG_INF, -CLAMP_LB)
+ + + +def _initialize_predictive_matrices( + X: Tensor, + model: Model, + observation_noise: bool = True, + jitter: float = 1e-4, + natural: bool = True, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: + r"""Initializes the natural predictive mean and covariance matrix. For a + multivariate normal distribution with mean mu and covariance Sigma, the natural + mean is Sigma^{-1} mu and the natural covariance is Sigma^{-1}. + + Args: + X: A `batch_shape x R x d`-dim Tensor. + model: The fitted model. + observation_noise: If true, the posterior is computed with observation noise. + jitter: The jitter added to the covariance matrix. + natural: If true, we compute the natural statistics as well. + + Return: + A four-element tuple containing + + - pred_nat_mean: A `batch_shape x num_outputs x R `-dim Tensor containing the + predictive natural mean vectors. + - pred_nat_cov: A `batch_shape x num_outputs x R x R`-dim Tensor containing + the predictive natural covariance matrices. + - pred_mean: A `batch_shape x num_outputs x R`-dim Tensor containing the + predictive mean vectors. + - pred_cov: A `batch_shape x num_outputs x R x R`-dim Tensor containing the + predictive covariance matrices. + """ + tkwargs = {"dtype": X.dtype, "device": X.device} + # compute the predictive mean and covariances at X + posterior = model.posterior(X, observation_noise=observation_noise) + + # `batch_shape x (R * num_outputs) x (R * num_outputs)` + init_pred_cov = posterior.mvn.covariance_matrix + num_outputs = model.num_outputs + R = int(init_pred_cov.shape[-1] / num_outputs) + pred_cov = [ + init_pred_cov[..., (m * R) : ((m + 1) * R), (m * R) : ((m + 1) * R)].unsqueeze( + -1 + ) + for m in range(num_outputs) + ] + # `batch_shape x R x R x num_outputs` (before swap axes) + # `batch_shape x num_outputs x R * R` + pred_cov = torch.cat(pred_cov, axis=-1).swapaxes(-2, -1).swapaxes(-3, -2) + identity = torch.diag_embed(torch.ones(pred_cov.shape[:-1], **tkwargs)) + pred_cov = pred_cov + jitter * identity + + # `batch_shape x num_outputs x R` + pred_mean = posterior.mean.swapaxes(-2, -1) + + ############################################################# + if natural: + # natural parameters + # `batch_shape x num_outputs x R x R` + cholesky_pred_cov, _ = torch.linalg.cholesky_ex(pred_cov) + pred_nat_cov = torch.cholesky_inverse(cholesky_pred_cov) + + # `batch_shape x num_outputs x R` + pred_nat_mean = torch.einsum("...ij,...j->...i", pred_nat_cov, pred_mean) + + return pred_nat_mean, pred_nat_cov, pred_mean, pred_cov + else: + return None, None, pred_mean, pred_cov + + +def _get_omega_f_contribution( + mean: Tensor, cov: Tensor, N: int, P: int, M: int +) -> tuple[Tensor, Tensor]: + r"""Extract the mean vector and covariance matrix corresponding to the `2 x 2` + multivariate normal blocks in the objective model between the points in `X` and + the Pareto optimal set. + + [There is likely a more efficient way to do this.] + + Args: + mean: A `batch_shape x M x (N + P)`-dim Tensor containing the natural + mean matrix for the objectives. + cov: A `batch_shape x M x (N + P) x (N + P)`-dim Tensor containing + the natural mean matrix for the objectives. + N: The number of design points. + P: The number of Pareto optimal points. + M: The number of objectives. + + Return: + A two-element tuple containing + + - mean_fX_fS: A `batch_shape x M x (N + P) x P x 2`-dim Tensor containing the + means of the inputs and Pareto optimal points. + - cov_fX_fS: A `batch_shape x M x (N + P) x P x 2 x 2`-dim Tensor containing + the covariances between the inputs and Pareto optimal points. + """ + tkwargs = {"dtype": mean.dtype, "device": mean.device} + batch_shape = mean.shape[:-2] + # `batch_shape x M x (N + P) x P x 2 x 2` + cov_fX_fS = torch.zeros(batch_shape + torch.Size([M, N + P, P, 2, 2]), **tkwargs) + # `batch_shape x M x (N + P) x P x 2` + mean_fX_fS = torch.zeros(batch_shape + torch.Size([M, N + P, P, 2]), **tkwargs) + + # `batch_shape x M x (N + P) x P` + mean_fX_fS[..., 0] = mean.unsqueeze(-1).expand(mean.shape + torch.Size([P])) + # `batch_shape x M x (N + P) x P` + mean_fX_fS[..., 1] = ( + mean[..., N:].unsqueeze(-2).expand(mean.shape + torch.Size([P])) + ) + # `batch_shape x M x (N + P) x P` + cov_fX_fS[..., 0, 0] = ( + cov[..., range(N + P), range(N + P)] + .unsqueeze(-1) + .expand(batch_shape + torch.Size([M, N + P, P])) + ) + # `batch_shape x M x (N + P) x P` + cov_fX_fS[..., 1, 1] = ( + cov[..., range(N, N + P), range(N, N + P)] + .unsqueeze(-2) + .expand(batch_shape + torch.Size([M, N + P, P])) + ) + + for p in range(P): + # `batch_shape x M x (N + P)` + cov_p = cov[..., range(N + P), N + p] + cov_fX_fS[..., p, 0, 1] = cov_p + cov_fX_fS[..., p, 1, 0] = cov_p + + return mean_fX_fS, cov_fX_fS + + +def _replace_pareto_diagonal(A: Tensor) -> Tensor: + """Replace the pareto diagonal with identity matricx. + + The Pareto diagonal of the omega factor shouldn't be updated because does not + contribute anything: `omega(x_p, x_p) = 1` for any pareto optimal input `x_p`. + + Args: + A: a `batch_shape x M x (N + P) x P x 2 x 2`-dim Tensor. + + Returns: + A `batch_shape x M x (N + P) x P x 2 x 2`-dim Tensor, where the Pareto + diagonal is padded with identity matrices. + """ + tkwargs = {"dtype": A.dtype, "device": A.device} + batch_shape = A.shape[:-5] + P = A.shape[-3] + N = A.shape[-4] - P + M = A.shape[-5] + identity = torch.diag_embed(torch.ones(batch_shape + torch.Size([M, 2]), **tkwargs)) + for p in range(P): + A[..., N + p, p, :, :] = identity + + return A + + +def _update_omega( + mean_f: Tensor, + cov_f: Tensor, + omega_f_nat_mean: Tensor, + omega_f_nat_cov: Tensor, + N: int, + P: int, + M: int, + maximize: bool = True, + jitter: float = 1e-6, +) -> tuple[Tensor, Tensor]: + r"""Computes the new omega factors by matching the moments. + + Args: + mean_f: A `batch_shape x M x (N + P)`-dim Tensor containing the mean vector + for the objectives. + cov_f: A `batch_shape x M x (N + P) x (N + P)`-dim Tensor containing the + covariance matrix for the objectives. + omega_f_nat_mean: A `batch_shape x M x (N + P) x P x 2`-dim Tensor containing + the omega natural mean factors for the objective matrix. + omega_f_nat_cov: A `batch_shape x M x (N + P) x P x 2 x 2`-dim Tensor + containing the omega natural covariance factors for the objective matrix. + N: The number of design points. + M: The number of Pareto optimal points. + M: The number of objectives. + maximize: If true, we consider the Pareto maximum domination relation. + jitter: The jitter for the matrix inverse. + + Return: + A two-element tuple containing + + - omega_f_nat_mean_new: A `batch_shape x M x (N + P) x P x 2` containing the + new omega natural mean factors for the objective matrix. + - omega_f_nat_cov_new: A `batch_shape x M x (N + P) x P x 2 x 2` containing + the new omega natural covariance factors for the objective matrix. + """ + tkwargs = {"dtype": mean_f.dtype, "device": mean_f.device} + CLAMP_LB = torch.finfo(tkwargs["dtype"]).eps + NEG_INF = torch.finfo(tkwargs["dtype"]).min + weight = 1.0 if maximize else -1.0 + ############################################################################### + # EXTRACT THE NECESSARY COMPONENTS + ############################################################################### + # `batch_shape x M x (N + P) x P x 2`-dim mean + # `batch_shape x M x (N + P) x P x 2 x 2`-dim covariance + mean_fX_fS, cov_fX_fS = _get_omega_f_contribution(mean_f, cov_f, N, P, M) + identity = torch.diag_embed(torch.ones(cov_fX_fS.shape[:-1], **tkwargs)) + # remove the Pareto diagonal + cov_fX_fS = _replace_pareto_diagonal(cov_fX_fS + jitter * identity) + nat_cov_fX_fS = torch.inverse(cov_fX_fS) + nat_mean_fX_fS = torch.einsum("...ij,...j->...i", nat_cov_fX_fS, mean_fX_fS) + + ############################################################################### + # COMPUTE THE CAVITIES + ############################################################################### + # cavity distribution + # natural parameters + cav_nat_mean_f = nat_mean_fX_fS - omega_f_nat_mean + cav_nat_cov_f = nat_cov_fX_fS - omega_f_nat_cov + + # transform to standard parameters + # remove the Pareto diagonal + cav_nat_cov_f = _replace_pareto_diagonal(cav_nat_cov_f) + identity = torch.diag_embed(torch.ones(cav_nat_cov_f.shape[:-1], **tkwargs)) + cav_cov_f = torch.inverse(cav_nat_cov_f + jitter * identity) + + cav_mean_f = torch.einsum("...ij,...j->...i", cav_cov_f, cav_nat_mean_f) + + ############################################################################### + # COMPUTE THE NORMALIZATION CONSTANT + ############################################################################### + # `batch_shape x M x (N + P) x P` + # Equation 29 + cav_var_fX_minus_fS = ( + cav_cov_f[..., 0, 0] + cav_cov_f[..., 1, 1] - 2 * cav_cov_f[..., 0, 1] + ).clamp_min(CLAMP_LB) + cav_std_fX_minus_fS = torch.sqrt(cav_var_fX_minus_fS).clamp_min(CLAMP_LB) + + # `batch_shape x M x (N + P) x P` + cav_mean_fX_minus_fS = weight * (cav_mean_f[..., 0] - cav_mean_f[..., 1]) + + # Equation 30 + cav_alpha = cav_mean_fX_minus_fS / cav_std_fX_minus_fS + # compute alpha pdf and cdf + normal_alpha = Normal(torch.zeros_like(cav_alpha), torch.ones_like(cav_alpha)) + # `batch_shape x M x (N + P) x P` + cav_alpha_log_cdf = log_cdf_robust(cav_alpha) + # `batch_shape x M x (N + P) x P` + cav_alpha_log_pdf = normal_alpha.log_prob(cav_alpha).clamp_min(NEG_INF) + # `batch_shape x (N + P) x P` + cav_sum_alpha_log_cdf = torch.sum(cav_alpha_log_cdf, dim=-3).clamp_min(NEG_INF) + + # compute normalization constant Z + # Equation 35 + cav_log_zeta = torch.log1p(-torch.exp(cav_sum_alpha_log_cdf)).clamp_min(NEG_INF) + + # Need to clamp log values to prevent `exp(-inf) = nan` + cav_logZ = cav_log_zeta + + # Equation 40 [first bit] + # `batch_shape x (N + P) x P` + cav_log_rho = -cav_logZ + cav_sum_alpha_log_cdf + + # Equation 40 [second bit] + # `batch_shape x M x (N + P) x P` + cav_log_rho = cav_log_rho.unsqueeze(-3) - cav_alpha_log_cdf + cav_alpha_log_pdf + cav_rho = -torch.exp(cav_log_rho).clamp(NEG_INF, -NEG_INF) + ############################################################################### + # COMPUTE THE PARTIAL DERIVATIVES + ############################################################################### + # `batch_shape x M x (N + P) x P x 2` + # Final vector: `[1, -1]` + ones_mean = torch.ones(cav_mean_f.shape, **tkwargs) + ones_mean[..., 1] = -ones_mean[..., 1] + + # `batch_shape x M x (N + P) x P x 2 x 2` + # Final matrix: `[[1, -1], [-1, 1]]` + ones_cov = torch.ones(cav_cov_f.shape, **tkwargs) + ones_cov[..., 0, 1] = -ones_cov[..., 0, 1] + ones_cov[..., 1, 0] = -ones_cov[..., 1, 0] + + # first partial derivation of the log Z with respect to the mean + # assuming maximization (this is also where the sign will change) + # Equation 41 + cav_dlogZ_dm = cav_rho / cav_std_fX_minus_fS + cav_dlogZ_dm = weight * cav_dlogZ_dm.unsqueeze(-1) * ones_mean + + # second partial derivation of the log Z with respect to the mean + # Equation 42 + cav_d2logZ_dm2 = -cav_rho * (cav_rho + cav_alpha) / cav_var_fX_minus_fS + cav_d2logZ_dm2 = cav_d2logZ_dm2.unsqueeze(-1).unsqueeze(-1) * ones_cov + + ############################################################################### + # COMPUTE THE NEW MEAN AND COVARIANCE + ############################################################################### + # compute the new mean and covariance + cav_updated_mean_f = cav_mean_f + torch.einsum( + "...ij,...j->...i", cav_cov_f, cav_dlogZ_dm + ) + cav_updated_cov_f = cav_cov_f + torch.einsum( + "...ij,...jk,...kl->...il", cav_cov_f, cav_d2logZ_dm2, cav_cov_f + ) + # transform to natural parameters + # remove the Pareto diagonal + cav_updated_cov_f = _replace_pareto_diagonal(cav_updated_cov_f) + + identity = torch.diag_embed(torch.ones(cav_updated_cov_f.shape[:-1], **tkwargs)) + cav_updated_nat_cov_f = torch.inverse(cav_updated_cov_f + jitter * identity) + + cav_updated_nat_mean_f = torch.einsum( + "...ij,...j->...i", cav_updated_nat_cov_f, cav_updated_mean_f + ) + + # match the moments to compute the gain + omega_f_nat_mean_new = cav_updated_nat_mean_f - cav_nat_mean_f + omega_f_nat_cov_new = cav_updated_nat_cov_f - cav_nat_cov_f + + # it is also possible to calculate the update directly as in the original paper: + # identity = torch.diag_embed(torch.ones(cav_d2logZ_dm2.shape[:-1], **tkwargs)) + # denominator = torch.inverse(cav_cov_f @ cav_d2logZ_dm2 + identity) + # omega_f_nat_cov_new = - cav_d2logZ_dm2 @ denominator + # omega_f_nat_mean_new = torch.einsum( + # '...ij,...j->...i', denominator, + # cav_dlogZ_dm - torch.einsum('...ij,...j->...i', cav_d2logZ_dm2, cav_mean_f) + # ) + + return omega_f_nat_mean_new, omega_f_nat_cov_new + + +def _safe_update_omega( + mean_f: Tensor, + cov_f: Tensor, + omega_f_nat_mean: Tensor, + omega_f_nat_cov: Tensor, + N: int, + P: int, + M: int, + maximize: bool = True, + jitter: float = 1e-6, +) -> tuple[Tensor, Tensor]: + r"""Try to update the new omega factors by matching the moments. If the update + is not possible then this returns the initial omega factors. + + Args: + mean_f: A `batch_shape x M x (N + P)`-dim Tensor containing the mean vector + for the objectives. + cov_f: A `batch_shape x M x (N + P) x (N + P)`-dim Tensor containing the + covariance matrix for the objectives. + omega_f_nat_mean: A `batch_shape x M x (N + P) x P x 2`-dim Tensor containing + the omega natural mean factors for the objective matrix. + omega_f_nat_cov: A `batch_shape x M x (N + P) x P x 2 x 2`-dim Tensor + containing the omega natural covariance factors for the objective + matrix. + N: The number of design points. + M: The number of Pareto optimal points. + M: The number of objectives. + maximize: If true, we consider the Pareto maximum domination relation. + jitter: The jitter for the matrix inverse. + + Return: + A two-element tuple containing + + - omega_f_nat_mean_new: A `batch_shape x M x (N + P) x P x 2` containing the + new omega natural mean factors for the objective matrix. + - omega_f_nat_cov_new: A `batch_shape x M x (N + P) x P x 2 x 2` containing + the new omega natural covariance factors for the objective matrix. + """ + try: + omega_f_nat_mean_new, omega_f_nat_cov_new = _update_omega( + mean_f=mean_f, + cov_f=cov_f, + omega_f_nat_mean=omega_f_nat_mean, + omega_f_nat_cov=omega_f_nat_cov, + N=N, + P=P, + M=M, + maximize=maximize, + jitter=jitter, + ) + check_no_nans(omega_f_nat_mean_new) + check_no_nans(omega_f_nat_cov_new) + return omega_f_nat_mean_new, omega_f_nat_cov_new + + except (RuntimeError, InputDataError): + return omega_f_nat_mean, omega_f_nat_cov + + +def _update_marginals( + pred_f_nat_mean: Tensor, + pred_f_nat_cov: Tensor, + omega_f_nat_mean: Tensor, + omega_f_nat_cov: Tensor, + N: int, + P: int, +) -> tuple[Tensor, Tensor]: + r"""Computes the new marginal by summing up all the natural factors. + + Args: + pred_f_nat_mean: A `batch_shape x M x (N + P)`-dim Tensor containing the + natural predictive mean matrix for the objectives. + pred_f_nat_cov: A `batch_shape x M x (N + P) x (N + P)`-dim Tensor containing + the natural predictive covariance matrix for the objectives. + omega_f_nat_mean: A `batch_shape x M x (N + P) x P x 2`-dim Tensor containing + the omega natural mean factors for the objective matrix. + omega_f_nat_cov: A `batch_shape x M x (N + P) x P x 2 x 2`-dim Tensor + containing the omega natural covariance factors for the objective matrix. + N: The number of design points. + P: The number of Pareto optimal points. + + Returns: + A two-element tuple containing + + - nat_mean_f: A `batch_shape x M x (N + P)`-dim Tensor containing the updated + natural mean matrix for the objectives. + - nat_cov_f: A `batch_shape x M x (N + P) x (N + P)`-dim Tensor containing + the updated natural predictive covariance matrix for the objectives. + """ + + # `batch_shape x M x (N + P)` + nat_mean_f = pred_f_nat_mean.clone() + # `batch_shape x M x (N + P) x (N + P) + nat_cov_f = pred_f_nat_cov.clone() + + ################################################################################ + # UPDATE THE OBJECTIVES + ################################################################################ + # remove Pareto diagonal + # zero out the diagonal + omega_f_nat_mean[..., range(N, N + P), range(P), :] = 0 + omega_f_nat_cov[..., range(N, N + P), range(P), :, :] = 0 + + # `batch_shape x M x (N + P)` + # sum over the pareto dim + nat_mean_f = nat_mean_f + omega_f_nat_mean[..., 0].sum(dim=-1) + # `batch_shape x M x P` + # sum over the data dim + nat_mean_f[..., N:] = nat_mean_f[..., N:] + omega_f_nat_mean[..., 1].sum(dim=-2) + + # `batch_shape x M x (N + P)` + nat_cov_f[..., range(N + P), range(N + P)] = nat_cov_f[ + ..., range(N + P), range(N + P) + ] + omega_f_nat_cov[..., 0, 0].sum(dim=-1) + # `batch_shape x M x P` + nat_cov_f[..., range(N, N + P), range(N, N + P)] = nat_cov_f[ + ..., range(N, N + P), range(N, N + P) + ] + omega_f_nat_cov[..., 1, 1].sum(dim=-2) + + for p in range(P): + # `batch_shape x M x (N + P)` + nat_cov_f[..., range(N + P), N + p] = ( + nat_cov_f[..., range(N + P), N + p] + omega_f_nat_cov[..., p, 0, 1] + ) + + # `batch_shape x M x (N + P)` + nat_cov_f[..., N + p, range(N + P)] = ( + nat_cov_f[..., N + p, range(N + P)] + omega_f_nat_cov[..., p, 1, 0] + ) + + return nat_mean_f, nat_cov_f + + +def _damped_update( + old_factor: Tensor, + new_factor: Tensor, + damping_factor: Tensor, +) -> Tensor: + r"""Computes the damped updated for natural factor. + + Args: + old_factor: A `batch_shape x param_shape`-dim Tensor containing the old + natural factor. + new_factor: A `batch_shape x param_shape`-dim Tensor containing the new + natural factor. + damping_factor: A `batch_shape`-dim Tensor containing the damping factor. + + Returns: + A `batch_shape x param_shape`-dim Tensor containing the updated natural + factor. + """ + bs = damping_factor.shape + fs = old_factor.shape + + df = damping_factor + for _ in range(len(fs[len(bs) :])): + df = df.unsqueeze(-1) + + return df * new_factor + (1 - df) * old_factor + + +def _update_damping( + nat_cov: Tensor, + nat_cov_new: Tensor, + damping_factor: Tensor, + jitter: Tensor, +) -> tuple[Tensor, Tensor]: + r"""Updates the damping factor whilst ensuring the covariance matrix is positive + definite by trying a Cholesky decomposition. + + Args: + nat_cov: A `batch_shape x R x R`-dim Tensor containing the old natural + covariance matrix. + nat_cov_new: A `batch_shape x R x R`-dim Tensor containing the new natural + covariance matrix. + damping_factor: A`batch_shape`-dim Tensor containing the damping factor. + jitter: The amount of jitter added before matrix inversion. + + Returns: + A two-element tuple containing + + - A `batch_shape x param_shape`-dim Tensor containing the updated damping + factor. + - A `batch_shape x R x R`-dim Tensor containing the Cholesky factor. + """ + tkwargs = {"dtype": nat_cov.dtype, "device": nat_cov.device} + df = damping_factor + jitter = jitter * torch.diag_embed(torch.ones(nat_cov.shape[:-1], **tkwargs)) + _, info = torch.linalg.cholesky_ex(nat_cov + jitter) + + if torch.sum(info) > 1: + raise ValueError( + "The previous covariance is not positive semi-definite. " + "This usually happens if the predictive covariance is " + "ill-conditioned and the added jitter is insufficient." + ) + + damped_nat_cov = _damped_update( + old_factor=nat_cov, new_factor=nat_cov_new, damping_factor=df + ) + cholesky_factor, info = torch.linalg.cholesky_ex(damped_nat_cov) + contains_nans = torch.any(torch.isnan(cholesky_factor)).item() + + run = 0 + while torch.sum(info) > 1 or contains_nans: + # propose an alternate damping factor which is half the original + df_alt = 0.5 * df + # hard threshold at 1e-3 + df_alt = torch.where( + df_alt > 1e-3, df_alt, torch.zeros(df_alt.shape, **tkwargs) + ) + # only change the damping factor where psd failure occurs + df_new = torch.where(info == 0, df, df_alt) + + # new damped covariance + damped_nat_cov = _damped_update(nat_cov, nat_cov_new, df_new) + + # try cholesky decomposition + cholesky_factor, info = torch.linalg.cholesky_ex(damped_nat_cov + jitter) + contains_nans = torch.any(torch.isnan(cholesky_factor)).item() + df = df_new + run = run + 1 + + return df, cholesky_factor + + +def _update_damping_when_converged( + mean_old: Tensor, + mean_new: Tensor, + cov_old: Tensor, + cov_new: Tensor, + damping_factor: Tensor, + iteration: Tensor, + threshold: float = 1e-3, +) -> tuple[Tensor, Tensor, Tensor]: + r"""Set the damping factor to 0 once converged. Convergence is determined by the + relative change in the entries of the mean and covariance matrix. + + Args: + mean_old: A `batch_shape x R`-dim Tensor containing the old natural mean + matrix for the objective. + mean_new: A `batch_shape x R`-dim Tensor containing the new natural mean + matrix for the objective. + cov_old: A `batch_shape x R x R`-dim Tensor containing the old natural + covariance matrix for the objective. + cov_new: A `batch_shape x R x R`-dim Tensor containing the new natural + covariance matrix for the objective. + iteration: The current iteration number + damping_factor: A `batch_shape`-dim Tensor containing the damping factor. + + Returns: + - A `batch_shape x param_shape`-dim Tensor containing the updated damping + factor. + - Difference between `mean_new` and `mean_old` + - Difference between `cov_new` and `cov_old` + """ + df = damping_factor.clone() + delta_mean = mean_new - mean_old + delta_cov = cov_new - cov_old + am = torch.amax(abs(mean_old), dim=-1) + ac = torch.amax(abs(cov_old), dim=(-2, -1)) + + if iteration > 2: + mask_mean = torch.amax(abs(delta_mean), dim=-1) < threshold * am + mask_cov = torch.amax(abs(delta_cov), dim=(-2, -1)) < threshold * ac + mask = torch.logical_and(mask_mean, mask_cov) + df[mask] = 0 + + return df, delta_mean, delta_cov + + +def _augment_factors_with_cached_factors( + q: int, + N: int, + omega_f_nat_mean: Tensor, + cached_omega_f_nat_mean: Tensor, + omega_f_nat_cov: Tensor, + cached_omega_f_nat_cov: Tensor, +) -> tuple[Tensor, Tensor]: + r"""Incorporate the cached Pareto updated factors in the forward call and + augment them with the previously computed factors. + + Args: + q: The batch size. + N: The number of training points. + omega_f_nat_mean: A `batch_shape x num_pareto_samples x M x (q + P) x P x 2` + -dim Tensor containing the omega natural mean for the objective at `X`. + cached_omega_f_nat_mean: A `num_pareto_samples x M x (N + P) x P x 2`-dim + Tensor containing the omega natural mean for the objective at `X`. + omega_f_nat_cov: A `batch_shape x num_pareto_samples x M x (q + P) x P x 2 + x 2` -dim Tensor containing the omega natural covariance for the + objective at `X`. + cached_omega_f_nat_cov: A `num_pareto_samples x M x (N + P) x P x 2 x 2`-dim + Tensor containing the omega covariance mean for the objective at `X`. + + Returns: + A two-element tuple containing + + - omega_f_nat_mean_new: A `batch_shape x num_pareto_samples x M x (q + P) + x P x 2`-dim Tensor containing the omega natural mean for the objective + at `X`. + - omega_f_nat_cov_new: A `batch_shape x num_pareto_samples x M x (q + P) x + P x 2 x 2`-dim Tensor containing the omega natural covariance for the + objective at `X`. + """ + ############################################################################## + # omega_f_nat_mean + ############################################################################## + # retrieve the natural mean contribution of the Pareto block omega(x_p, x_p) for + # the objective + exp_cached_omega_f_nat_mean = cached_omega_f_nat_mean[..., N:, :, :].expand( + omega_f_nat_mean[..., q:, :, :].shape + ) + omega_f_nat_mean[..., q:, :, :] = exp_cached_omega_f_nat_mean + ############################################################################## + # omega_f_nat_cov + ############################################################################## + # retrieve the natural covariance contribution of the Pareto block + # omega(x_p, x_p) for the objective + exp_omega_f_nat_cov = cached_omega_f_nat_cov[..., N:, :, :, :].expand( + omega_f_nat_cov[..., q:, :, :, :].shape + ) + omega_f_nat_cov[..., q:, :, :, :] = exp_omega_f_nat_cov + + return omega_f_nat_mean, omega_f_nat_cov + + +def _compute_log_determinant(cov: Tensor, q: int) -> Tensor: + r"""Computes the sum of the log determinants of a block diagonal covariance + matrices averaged over the Pareto samples. + + Args: + cov: A `batch_shape x num_pareto_samples x num_outputs x (q + P) x (q + P)` + -dim Tensor containing the covariance matrices. + q: The batch size. + + Return: + log_det_cov: A `batch_shape`-dim Tensor containing the sum of the + determinants for each output. + """ + log_det_cov = torch.logdet(cov[..., 0:q, 0:q]) + check_no_nans(log_det_cov) + + return log_det_cov.sum(dim=-1).mean(dim=-1) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_objective/utils.html b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/utils.html new file mode 100644 index 0000000000..2d558d4fbd --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_objective/utils.html @@ -0,0 +1,431 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_objective.utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Utilities for multi-objective acquisition functions.
+"""
+
+from __future__ import annotations
+
+import warnings
+from collections.abc import Callable
+from math import ceil
+from typing import Any
+
+import torch
+from botorch.acquisition import monte_carlo  # noqa F401
+from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
+from botorch.acquisition.utils import _prune_inferior_shared_processing
+from botorch.exceptions.errors import UnsupportedError
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models.deterministic import GenericDeterministicModel
+from botorch.models.model import Model
+from botorch.sampling.pathwise.posterior_samplers import get_matheron_path_model
+from botorch.utils.multi_objective.box_decompositions.box_decomposition import (
+    BoxDecomposition,
+)
+from botorch.utils.multi_objective.box_decompositions.box_decomposition_list import (
+    BoxDecompositionList,
+)
+from botorch.utils.multi_objective.box_decompositions.dominated import (
+    DominatedPartitioning,
+)
+from botorch.utils.multi_objective.pareto import is_non_dominated
+from botorch.utils.sampling import draw_sobol_samples
+from pyre_extensions import assert_is_instance
+from torch import Tensor
+
+
+
+[docs] +def get_default_partitioning_alpha(num_objectives: int) -> float: + r"""Determines an approximation level based on the number of objectives. + + If `alpha` is 0, FastNondominatedPartitioning should be used. Otherwise, + an approximate NondominatedPartitioning should be used with approximation + level `alpha`. + + Args: + num_objectives: the number of objectives. + + Returns: + The approximation level `alpha`. + """ + if num_objectives <= 4: + return 0.0 + elif num_objectives > 6: + warnings.warn( + "EHVI works best for less than 7 objectives.", + BotorchWarning, + stacklevel=3, + ) + return 10 ** (-8 + num_objectives)
+ + + +
+[docs] +def prune_inferior_points_multi_objective( + model: Model, + X: Tensor, + ref_point: Tensor, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + num_samples: int = 2048, + max_frac: float = 1.0, + marginalize_dim: int | None = None, +) -> Tensor: + r"""Prune points from an input tensor that are unlikely to be pareto optimal. + + Given a model, an objective, and an input tensor `X`, this function returns + the subset of points in `X` that have some probability of being pareto + optimal, better than the reference point, and feasible. This function uses + sampling to estimate the probabilities, the higher the number of points `n` + in `X` the higher the number of samples `num_samples` should be to obtain + accurate estimates. + + Args: + model: A fitted model. Batched models are currently not supported. + X: An input tensor of shape `n x d`. Batched inputs are currently not + supported. + ref_point: The reference point. + objective: The objective under which to evaluate the posterior. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. + num_samples: The number of samples used to compute empirical + probabilities of being the best point. + max_frac: The maximum fraction of points to retain. Must satisfy + `0 < max_frac <= 1`. Ensures that the number of elements in the + returned tensor does not exceed `ceil(max_frac * n)`. + marginalize_dim: A batch dimension that should be marginalized. + For example, this is useful when using a batched fully Bayesian + model. + + Returns: + A `n' x d` with subset of points in `X`, where + + n' = min(N_nz, ceil(max_frac * n)) + + with `N_nz` the number of points in `X` that have non-zero (empirical, + under `num_samples` samples) probability of being pareto optimal. + """ + max_points, obj_vals, infeas = _prune_inferior_shared_processing( + model=model, + X=X, + is_moo=True, + objective=objective, + constraints=constraints, + num_samples=num_samples, + max_frac=max_frac, + marginalize_dim=marginalize_dim, + ) + if infeas.any(): + obj_vals[infeas] = ref_point + pareto_mask = is_non_dominated(obj_vals, deduplicate=False) & ( + obj_vals > ref_point + ).all(dim=-1) + probs = pareto_mask.to(dtype=X.dtype).mean(dim=0) + idcs = probs.nonzero().view(-1) + if idcs.shape[0] > max_points: + counts, order_idcs = torch.sort(probs, stable=True, descending=True) + idcs = order_idcs[:max_points] + effective_n_w = obj_vals.shape[-2] // X.shape[-2] + idcs = (idcs / effective_n_w).long().unique() + return X[idcs]
+ + + +
+[docs] +def compute_sample_box_decomposition( + pareto_fronts: Tensor, + partitioning: type[BoxDecomposition] = DominatedPartitioning, + maximize: bool = True, + num_constraints: int = 0, +) -> Tensor: + r"""Computes the box decomposition associated with some sampled optimal + objectives. This also supports the single-objective and constrained optimization + setting. An objective `y` is feasible if `y <= 0`. + + To take advantage of batch computations, we pad the hypercell bounds with a + `2 x (M + K)`-dim Tensor of zeros `[0, 0]`. + + Args: + pareto_fronts: A `num_pareto_samples x num_pareto_points x M` dim Tensor + containing the sampled optimal set of objectives. + partitioning: A `BoxDecomposition` module that is used to obtain the + hyper-rectangle bounds for integration. In the unconstrained case, this + gives the partition of the dominated space. In the constrained case, this + gives the partition of the feasible dominated space union the infeasible + space. + maximize: If true, the box-decomposition is computed assuming maximization. + num_constraints: The number of constraints `K`. + + Returns: + A `num_pareto_samples x 2 x J x (M + K)`-dim Tensor containing the bounds for + the hyper-rectangles. The number `J` is the smallest number of boxes needed + to partition all the Pareto samples. + """ + tkwargs: dict[str, Any] = { + "dtype": pareto_fronts.dtype, + "device": pareto_fronts.device, + } + # We will later compute `norm.log_prob(NEG_INF)`, this is `-inf` if `NEG_INF` is + # too small. + NEG_INF = -1e10 + + if pareto_fronts.ndim != 3: + raise UnsupportedError( + "Currently this only supports Pareto fronts of the shape " + "`num_pareto_samples x num_pareto_points x num_objectives`." + ) + + num_pareto_samples = pareto_fronts.shape[0] + M = pareto_fronts.shape[-1] + K = num_constraints + ref_point = torch.ones(M, **tkwargs) * NEG_INF + weight = 1.0 if maximize else -1.0 + + if M == 1: + # Only consider a Pareto front with one element. + extreme_values = assert_is_instance( + weight * torch.max(weight * pareto_fronts, dim=-2).values, Tensor + ) + ref_point = weight * ref_point.expand(extreme_values.shape) + + if maximize: + hypercell_bounds = torch.stack( + [ref_point, extreme_values], dim=-2 + ).unsqueeze(-1) + else: + hypercell_bounds = torch.stack( + [extreme_values, ref_point], dim=-2 + ).unsqueeze(-1) + else: + bd_list = [] + for i in range(num_pareto_samples): + bd_list = bd_list + [ + partitioning(ref_point=ref_point, Y=weight * pareto_fronts[i, :, :]) + ] + + # `num_pareto_samples x 2 x J x (M + K)` + hypercell_bounds = ( + BoxDecompositionList(*bd_list).get_hypercell_bounds().movedim(0, 1) + ) + + # If minimizing, then the bounds should be negated and flipped + if not maximize: + hypercell_bounds = weight * torch.flip(hypercell_bounds, dims=[1]) + + # Add an extra box for the inequality constraint. + if K > 0: + # `num_pareto_samples x 2 x (J - 1) x K` + feasible_boxes = torch.zeros(hypercell_bounds.shape[:-1] + (K,), **tkwargs) + + feasible_boxes[..., 0, :, :] = NEG_INF + # `num_pareto_samples x 2 x (J - 1) x (M + K)` + hypercell_bounds = torch.cat([hypercell_bounds, feasible_boxes], dim=-1) + + # `num_pareto_samples x 2 x 1 x (M + K)` + infeasible_box = torch.zeros( + hypercell_bounds.shape[:-2] + (1, M + K), **tkwargs + ) + infeasible_box[..., 1, :, M:] = -NEG_INF + infeasible_box[..., 0, :, 0:M] = NEG_INF + infeasible_box[..., 1, :, 0:M] = -NEG_INF + + # `num_pareto_samples x 2 x J x (M + K)` + hypercell_bounds = torch.cat([hypercell_bounds, infeasible_box], dim=-2) + + # `num_pareto_samples x 2 x J x (M + K)` + return hypercell_bounds
+ + + +
+[docs] +def random_search_optimizer( + model: GenericDeterministicModel, + bounds: Tensor, + num_points: int, + maximize: bool, + pop_size: int = 1024, + max_tries: int = 10, +) -> tuple[Tensor, Tensor]: + r"""Optimize a function via random search. + + Args: + model: The model. + bounds: A `2 x d`-dim Tensor containing the input bounds. + num_points: The number of optimal points to be outputted. + maximize: If true, we consider a maximization problem. + pop_size: The number of function evaluations per try. + max_tries: The maximum number of tries. + + Returns: + A two-element tuple containing + + - A `num_points x d`-dim Tensor containing the collection of optimal inputs. + - A `num_points x M`-dim Tensor containing the collection of optimal + objectives. + """ + tkwargs: dict[str, Any] = {"dtype": bounds.dtype, "device": bounds.device} + weight = 1.0 if maximize else -1.0 + optimal_inputs = torch.tensor([], **tkwargs) + optimal_outputs = torch.tensor([], **tkwargs) + num_tries = 0 + num_found = 0 + ratio = 2 + while ratio > 1 and num_tries < max_tries: + X = draw_sobol_samples(bounds=bounds, n=pop_size, q=1).squeeze(-2) + Y = model.posterior(X).mean + X_aug = torch.cat([optimal_inputs, X], dim=0) + Y_aug = torch.cat([optimal_outputs, Y], dim=0) + pareto_mask = is_non_dominated(weight * Y_aug) + optimal_inputs = X_aug[pareto_mask] + optimal_outputs = Y_aug[pareto_mask] + num_found = len(optimal_inputs) + ratio = ceil(num_points / num_found) + num_tries = num_tries + 1 + # If maximum number of retries exceeded throw out a runtime error. + if ratio > 1: + error_text = f"Only found {num_found} optimal points instead of {num_points}." + raise RuntimeError(error_text) + else: + return optimal_inputs[:num_points], optimal_outputs[:num_points]
+ + + +
+[docs] +def sample_optimal_points( + model: Model, + bounds: Tensor, + num_samples: int, + num_points: int, + optimizer: Callable[ + [GenericDeterministicModel, Tensor, int, bool, Any], tuple[Tensor, Tensor] + ] = random_search_optimizer, + maximize: bool = True, + optimizer_kwargs: dict[str, Any] | None = None, +) -> tuple[Tensor, Tensor]: + r"""Compute a collection of optimal inputs and outputs from samples of a Gaussian + Process (GP). + + Steps: + (1) The samples are generated using random Fourier features (RFFs). + (2) The samples are optimized sequentially using an optimizer. + + TODO: We can generalize the GP sampling step to accommodate for other sampling + strategies rather than restricting to RFFs e.g. decoupled sampling. + + TODO: Currently this defaults to random search optimization, might want to + explore some other alternatives. + + Args: + model: The model. This does not support models which include fantasy + observations. + bounds: A `2 x d`-dim Tensor containing the input bounds. + num_samples: The number of GP samples. + num_points: The number of optimal points to be outputted. + optimizer: A callable that solves the deterministic optimization problem. + maximize: If true, we consider a maximization problem. + optimizer_kwargs: The additional arguments for the optimizer. + + Returns: + A two-element tuple containing + + - A `num_samples x num_points x d`-dim Tensor containing the collection of + optimal inputs. + - A `num_samples x num_points x M`-dim Tensor containing the collection of + optimal objectives. + """ + tkwargs: dict[str, Any] = {"dtype": bounds.dtype, "device": bounds.device} + M = model.num_outputs + d = bounds.shape[-1] + if M == 1: + if num_points > 1: + raise UnsupportedError( + "For single-objective optimization `num_points` should be 1." + ) + if optimizer_kwargs is None: + optimizer_kwargs = {} + pareto_sets = torch.zeros((num_samples, num_points, d), **tkwargs) + pareto_fronts = torch.zeros((num_samples, num_points, M), **tkwargs) + for i in range(num_samples): + sample_i = get_matheron_path_model(model=model) + ps_i, pf_i = optimizer( + model=sample_i, + bounds=bounds, + num_points=num_points, + maximize=maximize, + **optimizer_kwargs, + ) + pareto_sets[i, ...] = ps_i + pareto_fronts[i, ...] = pf_i + + return pareto_sets, pareto_fronts
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/multi_step_lookahead.html b/website-old/pages/api/_modules/botorch/acquisition/multi_step_lookahead.html new file mode 100644 index 0000000000..38fa896bbb --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/multi_step_lookahead.html @@ -0,0 +1,752 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.multi_step_lookahead

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+A general implementation of multi-step look-ahead acquisition function with configurable
+value functions. See [Jiang2020multistep]_.
+
+.. [Jiang2020multistep]
+    S. Jiang, D. R. Jiang, M. Balandat, B. Karrer, J. Gardner, and R. Garnett.
+    Efficient Nonmyopic Bayesian Optimization via One-Shot Multi-Step Trees.
+    In Advances in Neural Information Processing Systems 33, 2020.
+
+"""
+
+from __future__ import annotations
+
+import math
+import warnings
+from collections.abc import Callable
+from typing import Any
+
+import numpy as np
+import torch
+from botorch.acquisition import AcquisitionFunction, OneShotAcquisitionFunction
+from botorch.acquisition.analytic import AnalyticAcquisitionFunction, PosteriorMean
+from botorch.acquisition.monte_carlo import MCAcquisitionFunction
+from botorch.acquisition.objective import MCAcquisitionObjective, PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models.model import Model
+from botorch.optim.initializers import initialize_q_batch
+from botorch.sampling.base import MCSampler
+from botorch.sampling.normal import SobolQMCNormalSampler
+from botorch.utils.transforms import (
+    match_batch_shape,
+    t_batch_mode_transform,
+    unnormalize,
+)
+from torch import Size, Tensor
+from torch.distributions import Beta
+from torch.nn import ModuleList
+
+
+TAcqfArgConstructor = Callable[[Model, Tensor], dict[str, Any]]
+
+
+
+[docs] +class qMultiStepLookahead(MCAcquisitionFunction, OneShotAcquisitionFunction): + r"""MC-based batch Multi-Step Look-Ahead (one-shot optimization).""" + + def __init__( + self, + model: Model, + batch_sizes: list[int], + num_fantasies: list[int] | None = None, + samplers: list[MCSampler] | None = None, + valfunc_cls: list[type[AcquisitionFunction] | None] | None = None, + valfunc_argfacs: list[TAcqfArgConstructor | None] | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + inner_mc_samples: list[int] | None = None, + X_pending: Tensor | None = None, + collapse_fantasy_base_samples: bool = True, + ) -> None: + r"""q-Multi-Step Look-Ahead (one-shot optimization). + + Performs a `k`-step lookahead by means of repeated fantasizing. + + Allows to specify the stage value functions by passing the respective class + objects via the `valfunc_cls` list. Optionally, `valfunc_argfacs` takes a list + of callables that generate additional kwargs for these constructors. By default, + `valfunc_cls` will be chosen as `[None, ..., None, PosteriorMean]`, which + corresponds to the (parallel) multi-step KnowledgeGradient. If, in addition, + `k=1` and `q_1 = 1`, this reduces to the classic Knowledge Gradient. + + WARNING: The complexity of evaluating this function is exponential in the number + of lookahead steps! + + Args: + model: A fitted model. + batch_sizes: A list `[q_1, ..., q_k]` containing the batch sizes for the + `k` look-ahead steps. + num_fantasies: A list `[f_1, ..., f_k]` containing the number of fantasy + points to use for the `k` look-ahead steps. + samplers: A list of MCSampler objects to be used for sampling fantasies in + each stage. + valfunc_cls: A list of `k + 1` acquisition function classes to be used as + the (stage + terminal) value functions. Each element (except for the + last one) can be `None`, in which case a zero stage value is assumed for + the respective stage. If `None`, this defaults to + `[None, ..., None, PosteriorMean]` + valfunc_argfacs: A list of `k + 1` "argument factories", i.e. callables that + map a `Model` and input tensor `X` to a dictionary of kwargs for the + respective stage value function constructor (e.g. `best_f` for + `ExpectedImprovement`). If None, only the standard (`model`, `sampler` + and `objective`) kwargs will be used. + objective: The objective under which the output is evaluated. If `None`, use + the model output (requires a single-output model or a posterior + transform). Otherwise the objective is MC-evaluated + (using `inner_sampler`). + posterior_transform: An optional PosteriorTransform. If given, this + transforms the posterior before evaluation. If `objective is None`, + then the output of the transformed posterior is used. If `objective` is + given, the `inner_sampler` is used to draw samples from the transformed + posterior, which are then evaluated under the `objective`. + inner_mc_samples: A list `[n_0, ..., n_k]` containing the number of MC + samples to be used for evaluating the stage value function. Ignored if + the objective is `None`. + X_pending: A `m x d`-dim Tensor of `m` design points that have points that + have been submitted for function evaluation but have not yet been + evaluated. Concatenated into `X` upon forward call. Copied and set to + have no gradient. + collapse_fantasy_base_samples: If True, collapse_batch_dims of the Samplers + will be applied on fantasy batch dimensions as well, meaning that base + samples are the same in all subtrees starting from the same level. + """ + if objective is not None and not isinstance(objective, MCAcquisitionObjective): + raise UnsupportedError( + "`qMultiStepLookahead` got a non-MC `objective`. This is not supported." + " Use `posterior_transform` and `objective=None` instead." + ) + + super(MCAcquisitionFunction, self).__init__(model=model) + self.batch_sizes = batch_sizes + if not ((num_fantasies is None) ^ (samplers is None)): + raise UnsupportedError( + "qMultiStepLookahead requires exactly one of `num_fantasies` or " + "`samplers` as arguments." + ) + if samplers is None: + # If collapse_fantasy_base_samples is False, the `batch_range_override` + # is set on the samplers during the forward call. + samplers: list[MCSampler] = [ + SobolQMCNormalSampler(sample_shape=torch.Size([nf])) + for nf in num_fantasies + ] + else: + num_fantasies = [sampler.sample_shape[0] for sampler in samplers] + self.num_fantasies = num_fantasies + # By default do not use stage values and use PosteriorMean as terminal value + # function (= multi-step KG) + if valfunc_cls is None: + valfunc_cls = [None for _ in num_fantasies] + [PosteriorMean] + if inner_mc_samples is None: + inner_mc_samples = [None] * (1 + len(num_fantasies)) + # TODO: Allow passing in inner samplers directly + inner_samplers = _construct_inner_samplers( + batch_sizes=batch_sizes, + valfunc_cls=valfunc_cls, + objective=objective, + inner_mc_samples=inner_mc_samples, + ) + if valfunc_argfacs is None: + valfunc_argfacs = [None] * (1 + len(batch_sizes)) + + self.objective = objective + self.posterior_transform = posterior_transform + self.set_X_pending(X_pending) + self.samplers = ModuleList(samplers) + self.inner_samplers = ModuleList(inner_samplers) + self._valfunc_cls = valfunc_cls + self._valfunc_argfacs = valfunc_argfacs + self._collapse_fantasy_base_samples = collapse_fantasy_base_samples + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qMultiStepLookahead on the candidate set X. + + Args: + X: A `batch_shape x q' x d`-dim Tensor with `q'` design points for each + batch, where `q' = q_0 + f_1 q_1 + f_2 f_1 q_2 + ...`. Here `q_i` + is the number of candidates jointly considered in look-ahead step + `i`, and `f_i` is respective number of fantasies. + + Returns: + The acquisition value for each batch as a tensor of shape `batch_shape`. + """ + Xs = self.get_multi_step_tree_input_representation(X) + + # set batch_range on samplers if not collapsing on fantasy dims + if not self._collapse_fantasy_base_samples: + self._set_samplers_batch_range(batch_shape=X.shape[:-2]) + + return _step( + model=self.model, + Xs=Xs, + samplers=self.samplers, + valfunc_cls=self._valfunc_cls, + valfunc_argfacs=self._valfunc_argfacs, + inner_samplers=self.inner_samplers, + objective=self.objective, + posterior_transform=self.posterior_transform, + running_val=None, + )
+ + + @property + def _num_auxiliary(self) -> int: + r"""Number of auxiliary variables in the q-batch dimension. + + Returns: + `q_aux` s.t. `q + q_aux = augmented_q_batch_size` + """ + return np.dot(self.batch_sizes, np.cumprod(self.num_fantasies)).item() + + def _set_samplers_batch_range(self, batch_shape: Size) -> None: + r"""Set batch_range on samplers. + + Args: + batch_shape: The batch shape of the input tensor `X`. + """ + tbatch_dim_start = -2 - len(batch_shape) + for s in self.samplers: + s.batch_range_override = (tbatch_dim_start, -2) + +
+[docs] + def get_augmented_q_batch_size(self, q: int) -> int: + r"""Get augmented q batch size for one-shot optimization. + + Args: + q: The number of candidates to consider jointly. + + Returns: + The augmented size for one-shot optimization (including variables + parameterizing the fantasy solutions): `q_0 + f_1 q_1 + f_2 f_1 q_2 + ...` + """ + return q + self._num_auxiliary
+ + +
+[docs] + def get_split_shapes(self, X: Tensor) -> tuple[Size, list[Size], list[int]]: + r"""Get the split shapes from X. + + Args: + X: A `batch_shape x q_aug x d`-dim tensor including fantasy points. + + Returns: + A 3-tuple `(batch_shape, shapes, sizes)`, where + `shape[i] = f_i x .... x f_1 x batch_shape x q_i x d` and + `size[i] = f_i * ... f_1 * q_i`. + """ + batch_shape, (q_aug, d) = X.shape[:-2], X.shape[-2:] + q = q_aug - self._num_auxiliary + batch_sizes = [q] + self.batch_sizes + # X_i needs to have shape f_i x .... x f_1 x batch_shape x q_i x d + shapes = [ + torch.Size(self.num_fantasies[:i][::-1] + [*batch_shape, q_i, d]) + for i, q_i in enumerate(batch_sizes) + ] + # Each X_i in the split X has shape batch_shape x qtilde x d with + # qtilde = f_i * ... * f_1 * q_i + sizes = [s[: (-2 - len(batch_shape))].numel() * s[-2] for s in shapes] + return batch_shape, shapes, sizes
+ + +
+[docs] + def get_multi_step_tree_input_representation(self, X: Tensor) -> list[Tensor]: + r"""Get the multi-step tree representation of X. + + Args: + X: A `batch_shape x q' x d`-dim Tensor with `q'` design points for each + batch, where `q' = q_0 + f_1 q_1 + f_2 f_1 q_2 + ...`. Here `q_i` + is the number of candidates jointly considered in look-ahead step + `i`, and `f_i` is respective number of fantasies. + + Returns: + A list `[X_j, ..., X_k]` of tensors, where `X_i` has shape + `f_i x .... x f_1 x batch_shape x q_i x d`. + + """ + batch_shape, shapes, sizes = self.get_split_shapes(X=X) + # Each X_i in Xsplit has shape batch_shape x qtilde x d with + # qtilde = f_i * ... * f_1 * q_i + Xsplit = torch.split(X, sizes, dim=-2) + # now reshape (need to permute batch_shape and qtilde dimensions for i > 0) + perm = [-2] + list(range(len(batch_shape))) + [-1] + X0 = Xsplit[0].reshape(shapes[0]) + Xother = [ + X.permute(*perm).reshape(shape) for X, shape in zip(Xsplit[1:], shapes[1:]) + ] + # concatenate in pending points + if self.X_pending is not None: + X0 = torch.cat([X0, match_batch_shape(self.X_pending, X0)], dim=-2) + + return [X0] + Xother
+ + +
+[docs] + def extract_candidates(self, X_full: Tensor) -> Tensor: + r"""We only return X as the set of candidates post-optimization. + + Args: + X_full: A `batch_shape x q' x d`-dim Tensor with `q'` design points for + each batch, where `q' = q + f_1 q_1 + f_2 f_1 q_2 + ...`. + + Returns: + A `batch_shape x q x d`-dim Tensor with `q` design points for each batch. + """ + return X_full[..., : -self._num_auxiliary, :]
+ + +
+[docs] + def get_induced_fantasy_model(self, X: Tensor) -> Model: + r"""Fantasy model induced by X. + + Args: + X: A `batch_shape x q' x d`-dim Tensor with `q'` design points for each + batch, where `q' = q_0 + f_1 q_1 + f_2 f_1 q_2 + ...`. Here `q_i` + is the number of candidates jointly considered in look-ahead step + `i`, and `f_i` is respective number of fantasies. + + Returns: + The fantasy model induced by X. + """ + Xs = self.get_multi_step_tree_input_representation(X) + + # set batch_range on samplers if not collapsing on fantasy dims + if not self._collapse_fantasy_base_samples: + self._set_samplers_batch_range(batch_shape=X.shape[:-2]) + + return _get_induced_fantasy_model( + model=self.model, Xs=Xs, samplers=self.samplers + )
+
+ + + +def _step( + model: Model, + Xs: list[Tensor], + samplers: list[MCSampler | None], + valfunc_cls: list[type[AcquisitionFunction] | None], + valfunc_argfacs: list[TAcqfArgConstructor | None], + inner_samplers: list[MCSampler | None], + objective: MCAcquisitionObjective, + posterior_transform: PosteriorTransform | None, + running_val: Tensor | None = None, + sample_weights: Tensor | None = None, + step_index: int = 0, +) -> Tensor: + r"""Recursive multi-step look-ahead computation. + + Helper function computing the "value-to-go" of a multi-step lookahead scheme. + + Args: + model: A Model of appropriate batch size. Specifically, it must be possible to + evaluate the model's posterior at `Xs[0]`. + Xs: A list `[X_j, ..., X_k]` of tensors, where `X_i` has shape + `f_i x .... x f_1 x batch_shape x q_i x d`. + samplers: A list of `k - j` samplers, such that the number of samples of sampler + `i` is `f_i`. The last element of this list is considered the + "inner sampler", which is used for evaluating the objective in case it is an + MCAcquisitionObjective. + valfunc_cls: A list of acquisition function class to be used as the (stage + + terminal) value functions. Each element (except for the last one) can be + `None`, in which case a zero stage value is assumed for the respective + stage. + valfunc_argfacs: A list of callables that map a `Model` and input tensor `X` to + a dictionary of kwargs for the respective stage value function constructor. + If `None`, only the standard `model`, `sampler` and `objective` kwargs will + be used. + inner_samplers: A list of `MCSampler` objects, each to be used in the stage + value function at the corresponding index. + objective: The MCAcquisitionObjective under which the model output is evaluated. + posterior_transform: A PosteriorTransform. Used to transform the posterior + before sampling / evaluating the model output. + running_val: As `batch_shape`-dim tensor containing the current running value. + sample_weights: A tensor of shape `f_i x .... x f_1 x batch_shape` when called + in the `i`-th step by which to weight the stage value samples. Used in + conjunction with Gauss-Hermite integration or importance sampling. Assumed + to be `None` in the initial step (when `step_index=0`). + step_index: The index of the look-ahead step. `step_index=0` indicates the + initial step. + + Returns: + A `b`-dim tensor containing the multi-step value of the design `X`. + """ + X = Xs[0] + if sample_weights is None: # only happens in the initial step + sample_weights = torch.ones(*X.shape[:-2], device=X.device, dtype=X.dtype) + + # compute stage value + stage_val = _compute_stage_value( + model=model, + valfunc_cls=valfunc_cls[0], + X=X, + objective=objective, + posterior_transform=posterior_transform, + inner_sampler=inner_samplers[0], + arg_fac=valfunc_argfacs[0], + ) + if stage_val is not None: # update running value + # if not None, running_val has shape f_{i-1} x ... x f_1 x batch_shape + # stage_val has shape f_i x ... x f_1 x batch_shape + + # this sum will add a dimension to running_val so that + # updated running_val has shape f_i x ... x f_1 x batch_shape + running_val = stage_val if running_val is None else running_val + stage_val + + # base case: no more fantasizing, return value + if len(Xs) == 1: + # compute weighted average over all leaf nodes of the tree + batch_shape = running_val.shape[step_index:] + # expand sample weights to make sure it is the same shape as running_val, + # because we need to take a sum over sample weights for computing the + # weighted average + sample_weights = sample_weights.expand(running_val.shape) + return (running_val * sample_weights).view(-1, *batch_shape).sum(dim=0) + + # construct fantasy model (with batch shape f_{j+1} x ... x f_1 x batch_shape) + prop_grads = step_index > 0 # need to propagate gradients for steps > 0 + fantasy_model = model.fantasize( + X=X, sampler=samplers[0], propagate_grads=prop_grads + ) + + # augment sample weights appropriately + sample_weights = _construct_sample_weights( + prev_weights=sample_weights, sampler=samplers[0] + ) + + return _step( + model=fantasy_model, + Xs=Xs[1:], + samplers=samplers[1:], + valfunc_cls=valfunc_cls[1:], + valfunc_argfacs=valfunc_argfacs[1:], + inner_samplers=inner_samplers[1:], + objective=objective, + posterior_transform=posterior_transform, + sample_weights=sample_weights, + running_val=running_val, + step_index=step_index + 1, + ) + + +def _compute_stage_value( + model: Model, + valfunc_cls: type[AcquisitionFunction] | None, + X: Tensor, + objective: MCAcquisitionObjective, + posterior_transform: PosteriorTransform | None, + inner_sampler: MCSampler | None = None, + arg_fac: TAcqfArgConstructor | None = None, +) -> Tensor | None: + r"""Compute the stage value of a multi-step look-ahead policy. + + Args: + model: A Model of appropriate batch size. Specifically, it must be possible to + evaluate the model's posterior at `Xs[0]`. + valfunc_cls: The acquisition function class to be used as the stage value + functions. If `None`, a zero stage value is assumed (returns `None`) + X: A tensor with shape `f_i x .... x f_1 x batch_shape x q_i x d` when called in + the `i`-th step. + objective: The MCAcquisitionObjective under which the model output is evaluated. + posterior_transform: A PosteriorTransform. + inner_sampler: An `MCSampler` object to be used in the stage value function. Can + be `None` for analytic acquisition functions or when using the default + sampler of the acquisition function class. + arg_fac: A callable mapping a `Model` and the input tensor `X` to a dictionary + of kwargs for the stage value function constructor. If `None`, only the + standard `model`, `sampler` and `objective` kwargs will be used. + + Returns: + A `f_i x ... x f_1 x batch_shape`-dim tensor of stage values, or `None` + (= zero stage value). + """ + if valfunc_cls is None: + return None + common_kwargs: dict[str, Any] = { + "model": model, + "posterior_transform": posterior_transform, + } + if issubclass(valfunc_cls, MCAcquisitionFunction): + common_kwargs["sampler"] = inner_sampler + common_kwargs["objective"] = objective + kwargs = arg_fac(model=model, X=X) if arg_fac is not None else {} + stage_val_func = valfunc_cls(**common_kwargs, **kwargs) + # shape of stage_val is f_i x ... x f_1 x batch_shape + stage_val = stage_val_func(X=X) + return stage_val + + +def _construct_sample_weights( + prev_weights: Tensor, sampler: MCSampler +) -> Tensor | None: + r"""Iteratively construct tensor of sample weights for multi-step look-ahead. + + Args: + prev_weights: A `f_i x .... x f_1 x batch_shape` tensor of previous sample + weights. + sampler: A `MCSampler` that may have sample weights as the `base_weights` + attribute. If the sampler does not have a `base_weights` attribute, + samples are weighted uniformly. + + Returns: + A `f_{i+1} x .... x f_1 x batch_shape` tensor of sample weights for the next + step. + """ + new_weights = getattr(sampler, "base_weights", None) # TODO: generalize this + if new_weights is None: + # uniform weights + nf = sampler.sample_shape[0] + new_weights = torch.ones( + nf, device=prev_weights.device, dtype=prev_weights.dtype + ) + # reshape new_weights to be f_{i+1} x 1 x ... x 1 + new_weights = new_weights.view(-1, *(1 for _ in prev_weights.shape)) + # normalize new_weights to sum to 1.0 + new_weights = new_weights / new_weights.sum() + return new_weights * prev_weights + + +def _construct_inner_samplers( + batch_sizes: list[int], + valfunc_cls: list[type[AcquisitionFunction] | None], + inner_mc_samples: list[int | None], + objective: MCAcquisitionObjective | None = None, +) -> list[MCSampler | None]: + r"""Check validity of inputs and construct inner samplers. + + Helper function to be used internally for constructing inner samplers. + + Args: + batch_sizes: A list `[q_1, ..., q_k]` containing the batch sizes for the + `k` look-ahead steps. + valfunc_cls: A list of `k + 1` acquisition function classes to be used as the + (stage + terminal) value functions. Each element (except for the last one) + can be `None`, in which case a zero stage value is assumed for the + respective stage. + inner_mc_samples: A list `[n_0, ..., n_k]` containing the number of MC + samples to be used for evaluating the stage value function. Ignored if + the objective is `None`. + objective: The objective under which the output is evaluated. If `None`, use + the model output (requires a single-output model or a posterior transform). + Otherwise the objective is MC-evaluated (using `inner_sampler`). + + Returns: + A list with `k + 1` elements that are either `MCSampler`s or `None. + """ + inner_samplers = [] + for q, vfc, mcs in zip([None] + batch_sizes, valfunc_cls, inner_mc_samples): + if vfc is None: + inner_samplers.append(None) + elif vfc == qMultiStepLookahead: + raise UnsupportedError( + "qMultiStepLookahead not supported as a value function " + "(I see what you did there, nice try...)." + ) + elif issubclass(vfc, AnalyticAcquisitionFunction): + if objective is not None: + raise UnsupportedError( + "Only PosteriorTransforms are supported for analytic value " + f"functions. Received a {objective.__class__.__name__}." + ) + # At this point, we don't know the initial q-batch size here + if q is not None and q > 1: + raise UnsupportedError( + "Only batch sizes of q=1 are supported for analytic value " + "functions." + ) + if q is not None and mcs is not None: + warnings.warn( + "inner_mc_samples is ignored for analytic acquisition functions", + BotorchWarning, + stacklevel=3, + ) + inner_samplers.append(None) + else: + inner_sampler = SobolQMCNormalSampler( + sample_shape=torch.Size([32 if mcs is None else mcs]) + ) + inner_samplers.append(inner_sampler) + return inner_samplers + + +def _get_induced_fantasy_model( + model: Model, Xs: list[Tensor], samplers: list[MCSampler | None] +) -> Model: + r"""Recursive computation of the fantasy model induced by an input tree. + + Args: + model: A Model of appropriate batch size. Specifically, it must be possible to + evaluate the model's posterior at `Xs[0]`. + Xs: A list `[X_j, ..., X_k]` of tensors, where `X_i` has shape + `f_i x .... x f_1 x batch_shape x q_i x d`. + samplers: A list of `k - j` samplers, such that the number of samples of sampler + `i` is `f_i`. The last element of this list is considered the + "inner sampler", which is used for evaluating the objective in case it is an + MCAcquisitionObjective. + + Returns: + A Model obtained by iteratively fantasizing over the input tree `Xs`. + """ + if len(Xs) == 1: + return model + else: + fantasy_model = model.fantasize( + X=Xs[0], + sampler=samplers[0], + ) + + return _get_induced_fantasy_model( + model=fantasy_model, Xs=Xs[1:], samplers=samplers[1:] + ) + + +
+[docs] +def warmstart_multistep( + acq_function: qMultiStepLookahead, + bounds: Tensor, + num_restarts: int, + raw_samples: int, + full_optimizer: Tensor, +) -> Tensor: + r"""Warm-start initialization for multi-step look-ahead acquisition functions. + + For now uses the same q' as in `full_optimizer`. TODO: allow different `q`. + + Args: + acq_function: A qMultiStepLookahead acquisition function. + bounds: A `2 x d` tensor of lower and upper bounds for each column of features. + num_restarts: The number of starting points for multistart acquisition + function optimization. + raw_samples: The number of raw samples to consider in the initialization + heuristic. + full_optimizer: The full tree of optimizers of the previous iteration of shape + `batch_shape x q' x d`. Typically obtained by passing + `return_best_only=False` and `return_full_tree=True` into `optimize_acqf`. + + Returns: + A `num_restarts x q' x d` tensor for initial points for optimization. + + This is a very simple initialization heuristic. + TODO: Use the observed values to identify the fantasy sub-tree that is closest to + the observed value. + """ + batch_shape, shapes, sizes = acq_function.get_split_shapes(full_optimizer) + Xopts = torch.split(full_optimizer, sizes, dim=-2) + tkwargs = {"device": Xopts[0].device, "dtype": Xopts[0].dtype} + + B = Beta(torch.ones(1, **tkwargs), 3 * torch.ones(1, **tkwargs)) + + def mixin_layer(X: Tensor, bounds: Tensor, eta: float) -> Tensor: + perturbations = unnormalize(B.sample(X.shape).squeeze(-1), bounds) + return (1 - eta) * X + eta * perturbations + + def make_init_tree(Xopts: list[Tensor], bounds: Tensor, etas: Tensor) -> Tensor: + Xtrs = [mixin_layer(X=X, bounds=bounds, eta=eta) for eta, X in zip(etas, Xopts)] + return torch.cat(Xtrs, dim=-2) + + def mixin_tree(T: Tensor, bounds: Tensor, alpha: float) -> Tensor: + return (1 - alpha) * T + alpha * unnormalize(torch.rand_like(T), bounds) + + n_repeat = math.ceil(raw_samples / batch_shape[0]) + alphas = torch.linspace(0, 0.75, n_repeat, **tkwargs) + etas = torch.linspace(0.1, 1.0, len(Xopts), **tkwargs) + + X_full = torch.cat( + [ + mixin_tree( + T=make_init_tree(Xopts=Xopts, bounds=bounds, etas=etas), + bounds=bounds, + alpha=alpha, + ) + for alpha in alphas + ], + dim=0, + ) + + with torch.no_grad(): + acq_vals = acq_function(X_full) + X_init, _ = initialize_q_batch(X=X_full, acq_vals=acq_vals, n=num_restarts, eta=1.0) + return X_init[:raw_samples]
+ + + +
+[docs] +def make_best_f(model: Model, X: Tensor) -> dict[str, Any]: + r"""Extract the best observed training input from the model.""" + return {"best_f": model.train_targets.max(dim=-1).values}
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/objective.html b/website-old/pages/api/_modules/botorch/acquisition/objective.html new file mode 100644 index 0000000000..b525e682ee --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/objective.html @@ -0,0 +1,680 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.objective

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Objective Modules to be used with acquisition functions."""
+
+from __future__ import annotations
+
+import warnings
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from typing import TYPE_CHECKING
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.exceptions.warnings import InputDataWarning
+from botorch.models.model import Model
+from botorch.posteriors.gpytorch import GPyTorchPosterior, scalarize_posterior
+from botorch.sampling import IIDNormalSampler
+from botorch.utils import apply_constraints
+from gpytorch.distributions import MultitaskMultivariateNormal, MultivariateNormal
+from linear_operator.operators.dense_linear_operator import to_linear_operator
+from torch import Tensor
+from torch.nn import Module
+
+if TYPE_CHECKING:
+    from botorch.posteriors.posterior import Posterior  # pragma: no cover
+    from botorch.posteriors.posterior_list import PosteriorList  # pragma: no cover
+
+DEFAULT_NUM_PREF_SAMPLES = 16
+
+
+
+[docs] +class PosteriorTransform(Module, ABC): + """Abstract base class for objectives that transform the posterior.""" + +
+[docs] + @abstractmethod + def evaluate(self, Y: Tensor) -> Tensor: + r"""Evaluate the transform on a set of outcomes. + + Args: + Y: A `batch_shape x q x m`-dim tensor of outcomes. + + Returns: + A `batch_shape x q' [x m']`-dim tensor of transformed outcomes. + """ + pass # pragma: no cover
+ + +
+[docs] + @abstractmethod + def forward(self, posterior) -> Posterior: + r"""Compute the transformed posterior. + + Args: + posterior: The posterior to be transformed. + + Returns: + The transformed posterior object. + """ + pass # pragma: no cover
+
+ + + +# import DeterministicModel after PosteriorTransform to avoid circular import +from botorch.models.deterministic import DeterministicModel # noqa + + +
+[docs] +class ScalarizedPosteriorTransform(PosteriorTransform): + r"""An affine posterior transform for scalarizing multi-output posteriors. + + For a Gaussian posterior at a single point (`q=1`) with mean `mu` and + covariance matrix `Sigma`, this yields a single-output posterior with mean + `weights^T * mu` and variance `weights^T Sigma w`. + + Example: + Example for a model with two outcomes: + + >>> weights = torch.tensor([0.5, 0.25]) + >>> posterior_transform = ScalarizedPosteriorTransform(weights) + >>> EI = ExpectedImprovement( + ... model, best_f=0.1, posterior_transform=posterior_transform + ... ) + """ + + scalarize: bool = True + + def __init__(self, weights: Tensor, offset: float = 0.0) -> None: + r""" + Args: + weights: A one-dimensional tensor with `m` elements representing the + linear weights on the outputs. + offset: An offset to be added to posterior mean. + """ + if weights.dim() != 1: + raise ValueError("weights must be a one-dimensional tensor.") + super().__init__() + self.register_buffer("weights", weights) + self.offset = offset + +
+[docs] + def evaluate(self, Y: Tensor) -> Tensor: + r"""Evaluate the transform on a set of outcomes. + + Args: + Y: A `batch_shape x q x m`-dim tensor of outcomes. + + Returns: + A `batch_shape x q`-dim tensor of transformed outcomes. + """ + return self.offset + Y @ self.weights
+ + +
+[docs] + def forward( + self, posterior: GPyTorchPosterior | PosteriorList + ) -> GPyTorchPosterior: + r"""Compute the posterior of the affine transformation. + + Args: + posterior: A posterior with the same number of outputs as the + elements in `self.weights`. + + Returns: + A single-output posterior. + """ + return scalarize_posterior( + posterior=posterior, weights=self.weights, offset=self.offset + )
+
+ + + +
+[docs] +class ExpectationPosteriorTransform(PosteriorTransform): + r"""Transform the `batch x (q * n_w) x m` posterior into a `batch x q x m` + posterior of the expectation. The expectation is calculated over each + consecutive `n_w` block of points in the posterior. + + This is intended for use with `InputPerturbation` or `AppendFeatures` for + optimizing the expectation over `n_w` points. This should not be used when + there are constraints present, since this does not take into account + the feasibility of the objectives. + + Note: This is different than `ScalarizedPosteriorTransform` in that + this operates over the q-batch dimension. + """ + + def __init__(self, n_w: int, weights: Tensor | None = None) -> None: + r"""A posterior transform calculating the expectation over the q-batch + dimension. + + Args: + n_w: The number of points in the q-batch of the posterior to compute + the expectation over. This corresponds to the size of the + `feature_set` of `AppendFeatures` or the size of the `perturbation_set` + of `InputPerturbation`. + weights: An optional `n_w x m`-dim tensor of weights. Can be used to + compute a weighted expectation. Weights are normalized before use. + """ + super().__init__() + if weights is not None: + if weights.dim() != 2 or weights.shape[0] != n_w: + raise ValueError("`weights` must be a tensor of size `n_w x m`.") + if torch.any(weights < 0): + raise ValueError("`weights` must be non-negative.") + else: + weights = torch.ones(n_w, 1) + # Normalize the weights. + weights = weights / weights.sum(dim=0) + self.register_buffer("weights", weights) + self.n_w = n_w + +
+[docs] + def evaluate(self, Y: Tensor) -> Tensor: + r"""Evaluate the expectation of a set of outcomes. + + Args: + Y: A `batch_shape x (q * n_w) x m`-dim tensor of outcomes. + + Returns: + A `batch_shape x q x m`-dim tensor of expectation outcomes. + """ + batch_shape, m = Y.shape[:-2], Y.shape[-1] + weighted_Y = Y.view(*batch_shape, -1, self.n_w, m) * self.weights.to(Y) + return weighted_Y.sum(dim=-2)
+ + +
+[docs] + def forward(self, posterior: GPyTorchPosterior) -> GPyTorchPosterior: + r"""Compute the posterior of the expectation. + + Args: + posterior: An `m`-outcome joint posterior over `q * n_w` points. + + Returns: + An `m`-outcome joint posterior over `q` expectations. + """ + org_mvn = posterior.distribution + if getattr(org_mvn, "_interleaved", False): + raise UnsupportedError( + "`ExpectationPosteriorTransform` does not support " + "interleaved posteriors." + ) + # Initialize the weight matrix of shape compatible with the mvn. + org_event_shape = org_mvn.event_shape + batch_shape = org_mvn.batch_shape + q = org_event_shape[0] // self.n_w + m = 1 if len(org_event_shape) == 1 else org_event_shape[-1] + tkwargs = {"device": org_mvn.loc.device, "dtype": org_mvn.loc.dtype} + weights = torch.zeros(q * m, q * self.n_w * m, **tkwargs) + # Make sure self.weights has the correct dtype/device and shape. + self.weights = self.weights.to(org_mvn.loc).expand(self.n_w, m) + # Fill in the non-zero entries of the weight matrix. + # We want each row to have non-zero weights for the corresponding + # `n_w` sized diagonal. The `m` outcomes are not interleaved. + for i in range(q * m): + weights[i, self.n_w * i : self.n_w * (i + 1)] = self.weights[:, i // q] + # Trasform the mean. + new_loc = ( + (weights @ org_mvn.loc.unsqueeze(-1)) + .view(*batch_shape, m, q) + .transpose(-1, -2) + ) + # Transform the covariance matrix. + org_cov = ( + org_mvn.lazy_covariance_matrix + if org_mvn.islazy + else org_mvn.covariance_matrix + ) + new_cov = weights @ (org_cov @ weights.t()) + if m == 1: + new_mvn = MultivariateNormal( + new_loc.squeeze(-1), to_linear_operator(new_cov) + ) + else: + # Using MTMVN since we pass a single loc and covar for all `m` outputs. + new_mvn = MultitaskMultivariateNormal( + new_loc, to_linear_operator(new_cov), interleaved=False + ) + return GPyTorchPosterior(distribution=new_mvn)
+
+ + + +
+[docs] +class MCAcquisitionObjective(Module, ABC): + r"""Abstract base class for MC-based objectives. + + Args: + _verify_output_shape: If True and `X` is given, check that the q-batch + shape of the objectives agrees with that of X. + _is_mo: A boolean denoting whether the objectives are multi-output. + """ + + _verify_output_shape: bool = True + _is_mo: bool = False + +
+[docs] + @abstractmethod + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Evaluate the objective on the samples. + + Args: + samples: A `sample_shape x batch_shape x q x m`-dim Tensors of + samples from a model posterior. + X: A `batch_shape x q x d`-dim tensor of inputs. Relevant only if + the objective depends on the inputs explicitly. + + Returns: + Tensor: A `sample_shape x batch_shape x q`-dim Tensor of objective + values (assuming maximization). + + This method is usually not called directly, but via the objectives. + + Example: + >>> # `__call__` method: + >>> samples = sampler(posterior) + >>> outcome = mc_obj(samples) + """ + pass # pragma: no cover
+ + + def __call__( + self, samples: Tensor, X: Tensor | None = None, *args, **kwargs + ) -> Tensor: + output = super().__call__(samples=samples, X=X, *args, **kwargs) + # q-batch dimension is at -1 for single-output objectives and at + # -2 for multi-output objectives. + q_batch_idx = -2 if self._is_mo else -1 + if ( + X is not None + and self._verify_output_shape + and output.shape[q_batch_idx] != X.shape[-2] + ): + raise RuntimeError( + "The q-batch shape of the objective values does not agree with " + f"the q-batch shape of X. Got {output.shape[q_batch_idx]} and " + f"{X.shape[-2]}. This may happen if you used a one-to-many input " + "transform but forgot to use a corresponding objective." + ) + return output
+ + + +
+[docs] +class IdentityMCObjective(MCAcquisitionObjective): + r"""Trivial objective extracting the last dimension. + + Example: + >>> identity_objective = IdentityMCObjective() + >>> samples = sampler(posterior) + >>> objective = identity_objective(samples) + """ + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + return samples.squeeze(-1)
+
+ + + +
+[docs] +class LinearMCObjective(MCAcquisitionObjective): + r"""Linear objective constructed from a weight tensor. + + For input `samples` and `mc_obj = LinearMCObjective(weights)`, this produces + `mc_obj(samples) = sum_{i} weights[i] * samples[..., i]` + + Example: + Example for a model with two outcomes: + + >>> weights = torch.tensor([0.75, 0.25]) + >>> linear_objective = LinearMCObjective(weights) + >>> samples = sampler(posterior) + >>> objective = linear_objective(samples) + """ + + def __init__(self, weights: Tensor) -> None: + r""" + Args: + weights: A one-dimensional tensor with `m` elements representing the + linear weights on the outputs. + """ + super().__init__() + if weights.dim() != 1: + raise ValueError("weights must be a one-dimensional tensor.") + self.register_buffer("weights", weights) + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Evaluate the linear objective on the samples. + + Args: + samples: A `sample_shape x batch_shape x q x m`-dim tensors of + samples from a model posterior. + X: A `batch_shape x q x d`-dim tensor of inputs. Relevant only if + the objective depends on the inputs explicitly. + + Returns: + A `sample_shape x batch_shape x q`-dim tensor of objective values. + """ + if samples.shape[-1] != self.weights.shape[-1]: + raise RuntimeError("Output shape of samples not equal to that of weights") + return torch.einsum("...m, m", [samples, self.weights])
+
+ + + +
+[docs] +class GenericMCObjective(MCAcquisitionObjective): + r"""Objective generated from a generic callable. + + Allows to construct arbitrary MC-objective functions from a generic + callable. In order to be able to use gradient-based acquisition function + optimization it should be possible to backpropagate through the callable. + + Example: + >>> generic_objective = GenericMCObjective( + lambda Y, X: torch.sqrt(Y).sum(dim=-1), + ) + >>> samples = sampler(posterior) + >>> objective = generic_objective(samples) + """ + + def __init__(self, objective: Callable[[Tensor, Tensor | None], Tensor]) -> None: + r""" + Args: + objective: A callable `f(samples, X)` mapping a + `sample_shape x batch-shape x q x m`-dim Tensor `samples` and + an optional `batch-shape x q x d`-dim Tensor `X` to a + `sample_shape x batch-shape x q`-dim Tensor of objective values. + """ + super().__init__() + self.objective = objective + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Evaluate the objective on the samples. + + Args: + samples: A `sample_shape x batch_shape x q x m`-dim Tensors of + samples from a model posterior. + X: A `batch_shape x q x d`-dim tensor of inputs. Relevant only if + the objective depends on the inputs explicitly. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of objective values. + """ + return self.objective(samples, X=X)
+
+ + + +
+[docs] +class ConstrainedMCObjective(GenericMCObjective): + r"""Feasibility-weighted objective. + + An Objective allowing to maximize some scalable objective on the model + outputs subject to a number of constraints. Constraint feasibilty is + approximated by a sigmoid function. + + mc_acq(X) = ( + (objective(X) + infeasible_cost) * \prod_i (1 - sigmoid(constraint_i(X))) + ) - infeasible_cost + + See `botorch.utils.objective.apply_constraints` for details on the constraint + handling. + + Example: + >>> bound = 0.0 + >>> objective = lambda Y: Y[..., 0] + >>> # apply non-negativity constraint on f(x)[1] + >>> constraint = lambda Y: bound - Y[..., 1] + >>> constrained_objective = ConstrainedMCObjective(objective, [constraint]) + >>> samples = sampler(posterior) + >>> objective = constrained_objective(samples) + + TODO: Deprecate this as default way to handle constraints with MC acquisition + functions once we have data on how well SampleReducingMCAcquisitionFunction works. + """ + + def __init__( + self, + objective: Callable[[Tensor, Tensor | None], Tensor], + constraints: list[Callable[[Tensor], Tensor]], + infeasible_cost: Tensor | float = 0.0, + eta: Tensor | float = 1e-3, + ) -> None: + r""" + Args: + objective: A callable `f(samples, X)` mapping a + `sample_shape x batch-shape x q x m`-dim Tensor `samples` and + an optional `batch-shape x q x d`-dim Tensor `X` to a + `sample_shape x batch-shape x q`-dim Tensor of objective values. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. + infeasible_cost: The cost of a design if all associated samples are + infeasible. + eta: The temperature parameter of the sigmoid function approximating + the constraint. Can be either a float or a 1-dim tensor. In case + of a float the same eta is used for every constraint in + constraints. In case of a tensor the length of the tensor must + match the number of provided constraints. The i-th constraint is + then estimated with the i-th eta value. + """ + super().__init__(objective=objective) + self.constraints = constraints + if type(eta) is not Tensor: + eta = torch.full((len(constraints),), eta) + self.register_buffer("eta", eta) + self.register_buffer("infeasible_cost", torch.as_tensor(infeasible_cost)) + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Evaluate the feasibility-weighted objective on the samples. + + Args: + samples: A `sample_shape x batch_shape x q x m`-dim Tensors of + samples from a model posterior. + X: A `batch_shape x q x d`-dim tensor of inputs. Relevant only if + the objective depends on the inputs explicitly. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of objective values + weighted by feasibility (assuming maximization). + """ + obj = super().forward(samples=samples) + return apply_constraints( + obj=obj, + constraints=self.constraints, + samples=samples, + infeasible_cost=self.infeasible_cost, + eta=self.eta, + )
+
+ + + +LEARNED_OBJECTIVE_PREF_MODEL_MIXED_DTYPE_WARN = ( + "pref_model has double-precision data, but single-precision data " + "was passed to the LearnedObjective. Upcasting to double." +) + + +
+[docs] +class LearnedObjective(MCAcquisitionObjective): + r"""Learned preference objective constructed from a preference model. + + For input `samples`, it samples each individual sample again from the latent + preference posterior distribution using `pref_model` and return the posterior mean. + + Example: + >>> train_X = torch.rand(2, 2) + >>> train_comps = torch.LongTensor([[0, 1]]) + >>> pref_model = PairwiseGP(train_X, train_comps) + >>> learned_pref_obj = LearnedObjective(pref_model) + >>> samples = sampler(posterior) + >>> objective = learned_pref_obj(samples) + """ + + def __init__( + self, + pref_model: Model, + sample_shape: torch.Size | None = None, + seed: int | None = None, + ): + r""" + Args: + pref_model: A BoTorch model, which models the latent preference/utility + function. Given an input tensor of size + `sample_size x batch_shape x q x d`, its `posterior` method should + return a `Posterior` object with single outcome representing the + utility values of the input. + sample_shape: Determines the number of preference-model samples drawn + *per outcome-model sample* when the `LearnedObjective` is called. + Note that this is an additional layer of sampling relative to what + is needed when evaluating most MC acquisition functions in order to + account for uncertainty in the preference model. If `None`, it will + default to `torch.Size([16])`, so that 16 samples will be drawn + from the preference model at each outcome sample. This number is + relatively high because sampling from the preference model is general + cheap relative to generating the outcome model posterior. + """ + super().__init__() + self.pref_model = pref_model + if isinstance(pref_model, DeterministicModel): + assert sample_shape is None + self.sampler = None + else: + if sample_shape is None: + sample_shape = torch.Size([DEFAULT_NUM_PREF_SAMPLES]) + # using an IIDNormalSampler instead of a SobolQMCNormalSampler by default + # because SobolQMCNormalSampler can support up to 21201 total samples and + # becomes noticeably slower than uniform sampling when the sample size is + # large. + self.sampler = IIDNormalSampler(sample_shape=sample_shape, seed=seed) + self.sampler.batch_range_override = (1, -1) + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Sample each element of samples. + + Args: + samples: A `sample_size x batch_shape x q x d`-dim Tensors of + samples from a model posterior. + + Returns: + A `(sample_size * num_samples) x batch_shape x q`-dim Tensor of + objective values sampled from utility posterior using `pref_model`. + """ + if samples.dtype != torch.float64 and any( + d == torch.float64 for d in self.pref_model.dtypes_of_buffers + ): + warnings.warn( + LEARNED_OBJECTIVE_PREF_MODEL_MIXED_DTYPE_WARN, + InputDataWarning, + stacklevel=2, + ) + samples = samples.to(torch.float64) + + if samples.ndim < 3: + raise ValueError("samples should have at least 3 dimensions.") + + posterior = self.pref_model.posterior(samples) + if isinstance(self.pref_model, DeterministicModel): + # return preference posterior mean + return posterior.mean.squeeze(-1) + else: + # return preference posterior augmented samples + samples = self.sampler(posterior).squeeze(-1) + return samples.reshape(-1, *samples.shape[2:]) # batch_shape x N
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/penalized.html b/website-old/pages/api/_modules/botorch/acquisition/penalized.html new file mode 100644 index 0000000000..9cb8c060c0 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/penalized.html @@ -0,0 +1,523 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.penalized

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Modules to add regularization to acquisition functions.
+"""
+
+from __future__ import annotations
+
+import math
+from collections.abc import Callable
+from typing import Any
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.acquisition.analytic import AnalyticAcquisitionFunction
+from botorch.acquisition.objective import GenericMCObjective
+from botorch.exceptions import UnsupportedError
+from torch import Tensor
+
+
+
+[docs] +class L2Penalty(torch.nn.Module): + r"""L2 penalty class to be added to any arbitrary acquisition function + to construct a PenalizedAcquisitionFunction.""" + + def __init__(self, init_point: Tensor): + r"""Initializing L2 regularization. + + Args: + init_point: The "1 x dim" reference point against which + we want to regularize. + """ + super().__init__() + self.init_point = init_point + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r""" + Args: + X: A "batch_shape x q x dim" representing the points to be evaluated. + + Returns: + A tensor of size "batch_shape" representing the acqfn for each q-batch. + """ + regularization_term = ( + torch.linalg.norm((X - self.init_point), ord=2, dim=-1).max(dim=-1).values + ** 2 + ) + return regularization_term
+
+ + + +
+[docs] +class L1Penalty(torch.nn.Module): + r"""L1 penalty class to be added to any arbitrary acquisition function + to construct a PenalizedAcquisitionFunction.""" + + def __init__(self, init_point: Tensor): + r"""Initializing L1 regularization. + + Args: + init_point: The "1 x dim" reference point against which + we want to regularize. + """ + super().__init__() + self.init_point = init_point + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r""" + Args: + X: A "batch_shape x q x dim" representing the points to be evaluated. + + Returns: + A tensor of size "batch_shape" representing the acqfn for each q-batch. + """ + regularization_term = ( + torch.linalg.norm((X - self.init_point), ord=1, dim=-1).max(dim=-1).values + ) + return regularization_term
+
+ + + +
+[docs] +class GaussianPenalty(torch.nn.Module): + r"""Gaussian penalty class to be added to any arbitrary acquisition function + to construct a PenalizedAcquisitionFunction.""" + + def __init__(self, init_point: Tensor, sigma: float): + r"""Initializing Gaussian regularization. + + Args: + init_point: The "1 x dim" reference point against which + we want to regularize. + sigma: The parameter used in gaussian function. + """ + super().__init__() + self.init_point = init_point + self.sigma = sigma + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r""" + Args: + X: A "batch_shape x q x dim" representing the points to be evaluated. + + Returns: + A tensor of size "batch_shape" representing the acqfn for each q-batch. + """ + sq_diff = torch.linalg.norm((X - self.init_point), ord=2, dim=-1) ** 2 + pdf = torch.exp(sq_diff / 2 / self.sigma**2) + regularization_term = pdf.max(dim=-1).values + return regularization_term
+
+ + + +
+[docs] +class GroupLassoPenalty(torch.nn.Module): + r"""Group lasso penalty class to be added to any arbitrary acquisition function + to construct a PenalizedAcquisitionFunction.""" + + def __init__(self, init_point: Tensor, groups: list[list[int]]): + r"""Initializing Group-Lasso regularization. + + Args: + init_point: The "1 x dim" reference point against which we want + to regularize. + groups: Groups of indices used in group lasso. + """ + super().__init__() + self.init_point = init_point + self.groups = groups + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r""" + X should be batch_shape x 1 x dim tensor. Evaluation for q-batch is not + implemented yet. + """ + if X.shape[-2] != 1: + raise NotImplementedError( + "group-lasso has not been implemented for q>1 yet." + ) + + regularization_term = group_lasso_regularizer( + X=X.squeeze(-2) - self.init_point, groups=self.groups + ) + return regularization_term
+
+ + + +
+[docs] +def narrow_gaussian(X: Tensor, a: Tensor) -> Tensor: + return torch.exp(-0.5 * (X / a) ** 2)
+ + + +
+[docs] +def nnz_approx(X: Tensor, target_point: Tensor, a: Tensor) -> Tensor: + r"""Differentiable relaxation of ||X - target_point||_0 + + Args: + X: An `n x d` tensor of inputs. + target_point: A tensor of size `n` corresponding to the target point. + a: A scalar tensor that controls the differentiable relaxation. + """ + d = X.shape[-1] + if d != target_point.shape[-1]: + raise ValueError("X and target_point have different shapes.") + return d - narrow_gaussian(X - target_point, a).sum(dim=-1, keepdim=True)
+ + + +
+[docs] +class L0Approximation(torch.nn.Module): + r"""Differentiable relaxation of the L0 norm using a Gaussian basis function.""" + + def __init__(self, target_point: Tensor, a: float = 1.0, **tkwargs: Any) -> None: + r"""Initializing L0 penalty with differentiable relaxation. + + Args: + target_point: A tensor corresponding to the target point. + a: A hyperparameter that controls the differentiable relaxation. + """ + super().__init__() + self.target_point = target_point + # hyperparameter to control the differentiable relaxation in L0 norm function. + self.register_buffer("a", torch.tensor(a, **tkwargs)) + + def __call__(self, X: Tensor) -> Tensor: + return nnz_approx(X=X, target_point=self.target_point, a=self.a)
+ + + +
+[docs] +class L0PenaltyApprox(L0Approximation): + r"""Differentiable relaxation of the L0 norm to be added to any arbitrary + acquisition function to construct a PenalizedAcquisitionFunction.""" + + def __init__(self, target_point: Tensor, a: float = 1.0, **tkwargs: Any) -> None: + r"""Initializing L0 penalty with differentiable relaxation. + + Args: + target_point: A tensor corresponding to the target point. + a: A hyperparameter that controls the differentiable relaxation. + """ + super().__init__(target_point=target_point, a=a, **tkwargs) + + def __call__(self, X: Tensor) -> Tensor: + r""" + Args: + X: A "batch_shape x q x dim" representing the points to be evaluated. + Returns: + A tensor of size "batch_shape" representing the acqfn for each q-batch. + """ + return super().__call__(X=X).squeeze(dim=-1).min(dim=-1).values
+ + + +
+[docs] +class PenalizedAcquisitionFunction(AcquisitionFunction): + r"""Single-outcome acquisition function regularized by the given penalty. + + The usage is similar to: + raw_acqf = NoisyExpectedImprovement(...) + penalty = GroupLassoPenalty(...) + acqf = PenalizedAcquisitionFunction(raw_acqf, penalty) + """ + + def __init__( + self, + raw_acqf: AcquisitionFunction, + penalty_func: torch.nn.Module, + regularization_parameter: float, + ) -> None: + r"""Initializing Group-Lasso regularization. + + Args: + raw_acqf: The raw acquisition function that is going to be regularized. + penalty_func: The regularization function. + regularization_parameter: Regularization parameter used in optimization. + """ + super().__init__(model=raw_acqf.model) + self.raw_acqf = raw_acqf + self.penalty_func = penalty_func + self.regularization_parameter = regularization_parameter + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + raw_value = self.raw_acqf(X=X) + penalty_term = self.penalty_func(X) + return raw_value - self.regularization_parameter * penalty_term
+ + + @property + def X_pending(self) -> Tensor | None: + return self.raw_acqf.X_pending + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + if not isinstance(self.raw_acqf, AnalyticAcquisitionFunction): + self.raw_acqf.set_X_pending(X_pending=X_pending) + else: + raise UnsupportedError( + "The raw acquisition function is Analytic and does not account " + "for X_pending yet." + )
+
+ + + +
+[docs] +def group_lasso_regularizer(X: Tensor, groups: list[list[int]]) -> Tensor: + r"""Computes the group lasso regularization function for the given point. + + Args: + X: A bxd tensor representing the points to evaluate the regularization at. + groups: List of indices of different groups. + + Returns: + Computed group lasso norm of at the given points. + """ + return torch.sum( + torch.stack( + [ + math.sqrt(len(g)) * torch.linalg.norm(X[..., g], ord=2, dim=-1) + for g in groups + ], + dim=-1, + ), + dim=-1, + )
+ + + +
+[docs] +class L1PenaltyObjective(torch.nn.Module): + r""" + L1 penalty objective class. An instance of this class can be added to any + arbitrary objective to construct a PenalizedMCObjective. + """ + + def __init__(self, init_point: Tensor): + r"""Initializing L1 penalty objective. + + Args: + init_point: The "1 x dim" reference point against which + we want to regularize. + """ + super().__init__() + self.init_point = init_point + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r""" + Args: + X: A "batch_shape x q x dim" representing the points to be evaluated. + + Returns: + A "1 x batch_shape x q" tensor representing the penalty for each point. + The first dimension corresponds to the dimension of MC samples. + """ + return torch.linalg.norm((X - self.init_point), ord=1, dim=-1).unsqueeze(dim=0)
+
+ + + +
+[docs] +class PenalizedMCObjective(GenericMCObjective): + r"""Penalized MC objective. + + Allows to construct a penalized MC-objective by adding a penalty term to + the original objective. + + mc_acq(X) = objective(X) + penalty_objective(X) + + Note: PenalizedMCObjective allows adding penalty at the MCObjective level, + different from the AcquisitionFunction level in PenalizedAcquisitionFunction. + + Example: + >>> regularization_parameter = 0.01 + >>> init_point = torch.zeros(3) # assume data dim is 3 + >>> objective = lambda Y, X: torch.sqrt(Y).sum(dim=-1) + >>> l1_penalty_objective = L1PenaltyObjective(init_point=init_point) + >>> l1_penalized_objective = PenalizedMCObjective( + objective, l1_penalty_objective, regularization_parameter + ) + >>> samples = sampler(posterior) + objective, l1_penalty_objective, regularization_parameter + """ + + def __init__( + self, + objective: Callable[[Tensor, Tensor | None], Tensor], + penalty_objective: torch.nn.Module, + regularization_parameter: float, + expand_dim: int | None = None, + ) -> None: + r"""Penalized MC objective. + + Args: + objective: A callable `f(samples, X)` mapping a + `sample_shape x batch-shape x q x m`-dim Tensor `samples` and + an optional `batch-shape x q x d`-dim Tensor `X` to a + `sample_shape x batch-shape x q`-dim Tensor of objective values. + penalty_objective: A torch.nn.Module `f(X)` that takes in a + `batch-shape x q x d`-dim Tensor `X` and outputs a + `1 x batch-shape x q`-dim Tensor of penalty objective values. + regularization_parameter: weight of the penalty (regularization) term + expand_dim: dim to expand penalty_objective to match with objective when + fully bayesian model is used. If None, no expansion is performed. + """ + super().__init__(objective=objective) + self.penalty_objective = penalty_objective + self.regularization_parameter = regularization_parameter + self.expand_dim = expand_dim + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Evaluate the penalized objective on the samples. + + Args: + samples: A `sample_shape x batch_shape x q x m`-dim Tensors of + samples from a model posterior. + X: A `batch_shape x q x d`-dim tensor of inputs. Relevant only if + the objective depends on the inputs explicitly. + + Returns: + A `sample_shape x batch_shape x q`-dim Tensor of objective values + with penalty added for each point. + """ + obj = super().forward(samples=samples, X=X) + penalty_obj = self.penalty_objective(X) + # when fully bayesian model is used, we pass unmarginalize_dim to match the + # shape between obj `sample_shape x batch-shape x mcmc_samples x q` and + # penalty_obj `1 x batch-shape x q` + if self.expand_dim is not None: + # reshape penalty_obj to match the dim + penalty_obj = penalty_obj.unsqueeze(self.expand_dim) + # this happens when samples is a `q x m`-dim tensor and X is a `q x d`-dim + # tensor; obj returned from GenericMCObjective is a `q`-dim tensor and + # penalty_obj is a `1 x q`-dim tensor. + if obj.ndim == 1: + assert penalty_obj.shape == torch.Size([1, samples.shape[-2]]) + penalty_obj = penalty_obj.squeeze(dim=0) + return obj - self.regularization_parameter * penalty_obj
+
+ + + +
+[docs] +class L0PenaltyApproxObjective(L0Approximation): + r"""Differentiable relaxation of the L0 norm penalty objective class. + An instance of this class can be added to any arbitrary objective to + construct a PenalizedMCObjective. + """ + + def __init__(self, target_point: Tensor, a: float = 1.0, **tkwargs: Any) -> None: + r"""Initializing L0 penalty with differentiable relaxation. + + Args: + target_point: A tensor corresponding to the target point. + a: A hyperparameter that controls the differentiable relaxation. + """ + super().__init__(target_point=target_point, a=a, **tkwargs) + + def __call__(self, X: Tensor) -> Tensor: + r""" + Args: + X: A "batch_shape x q x dim" representing the points to be evaluated. + Returns: + A "1 x batch_shape x q" tensor representing the penalty for each point. + The first dimension corresponds to the dimension of MC samples. + """ + return super().__call__(X=X).squeeze(dim=-1).unsqueeze(dim=0)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/predictive_entropy_search.html b/website-old/pages/api/_modules/botorch/acquisition/predictive_entropy_search.html new file mode 100644 index 0000000000..d390fbb323 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/predictive_entropy_search.html @@ -0,0 +1,165 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.predictive_entropy_search

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Acquisition function for predictive entropy search (PES). The code utilizes the
+implementation designed for the multi-objective batch setting.
+
+NOTE: The PES acquisition might not be differentiable. As a result, we recommend
+optimizing the acquisition function using finite differences.
+
+"""
+
+from __future__ import annotations
+
+from botorch.acquisition.multi_objective.predictive_entropy_search import (
+    qMultiObjectivePredictiveEntropySearch,
+)
+from botorch.models.model import Model
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+
+
+
+[docs] +class qPredictiveEntropySearch(qMultiObjectivePredictiveEntropySearch): + r"""The acquisition function for Predictive Entropy Search. + + This acquisition function approximates the mutual information between the + observation at a candidate point `X` and the optimal set of inputs using + expectation propagation (EP). + + NOTES: + (i) The expectation propagation procedure can potentially fail due to the unstable + EP updates. This is however unlikely to happen in the single-objective setting + because we have much fewer EP factors. The jitter added in the training phase + (`ep_jitter`) and testing phase (`test_jitter`) can be increased to prevent + these failures from happening. More details in the description of + `qMultiObjectivePredictiveEntropySearch`. + + (ii) The estimated acquisition value could be negative. + """ + + def __init__( + self, + model: Model, + optimal_inputs: Tensor, + maximize: bool = True, + X_pending: Tensor | None = None, + max_ep_iterations: int = 250, + ep_jitter: float = 1e-4, + test_jitter: float = 1e-4, + threshold: float = 1e-2, + ) -> None: + r"""Predictive entropy search acquisition function. + + Args: + model: A fitted single-outcome model. + optimal_inputs: A `num_samples x d`-dim tensor containing the sampled + optimal inputs of dimension `d`. We assume for simplicity that each + sample only contains one optimal set of inputs. + maximize: If true, we consider a maximization problem. + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation, but have not yet been evaluated. + max_ep_iterations: The maximum number of expectation propagation + iterations. (The minimum number of iterations is set at 3.) + ep_jitter: The amount of jitter added for the matrix inversion that + occurs during the expectation propagation update during the training + phase. + test_jitter: The amount of jitter added for the matrix inversion that + occurs during the expectation propagation update in the testing + phase. + threshold: The convergence threshold for expectation propagation. This + assesses the relative change in the mean and covariance. We default + to one percent change i.e. `threshold = 1e-2`. + """ + super().__init__( + model=model, + pareto_sets=optimal_inputs.unsqueeze(-2), + maximize=maximize, + X_pending=X_pending, + max_ep_iterations=max_ep_iterations, + ep_jitter=ep_jitter, + test_jitter=test_jitter, + threshold=threshold, + ) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qPredictiveEntropySearch on the candidate set `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of t-batches with `q` `d`-dim + design points each. + + Returns: + A `batch_shape'`-dim Tensor of Predictive Entropy Search values at the + given design points `X`. + """ + return self._compute_information_gain(X)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/preference.html b/website-old/pages/api/_modules/botorch/acquisition/preference.html new file mode 100644 index 0000000000..955d2be44f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/preference.html @@ -0,0 +1,348 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.preference

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Preference acquisition functions. This includes:
+Analytical EUBO acquisition function as introduced in [Lin2022preference]_
+and its MC-based generalization qEUBO as proposed in [Astudillo2023qeubo]_.
+
+.. [Astudillo2023qeubo]
+    Astudillo, R., Lin, Z.J., Bakshy, E. and Frazier, P.I. qEUBO: A Decision-Theoretic
+    Acquisition Function for Preferential Bayesian Optimization. International
+    Conference on Artificial Intelligence and Statistics (AISTATS), 2023.
+
+.. [Lin2022preference]
+    Lin, Z.J., Astudillo, R., Frazier, P.I. and Bakshy, E. Preference Exploration
+    for Efficient Bayesian Optimization with Multiple Outcomes. International
+    Conference on Artificial Intelligence and Statistics (AISTATS), 2022.
+
+.. [Houlsby2011bald]
+    Houlsby, N., Huszár, F., Ghahramani, Z. and Lengyel, M.
+    Bayesian Active Learning for Gaussian Process Classification.
+    NIPS Workshop on Bayesian optimization, experimental design and bandits:
+    Theory and applications, 2011.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.acquisition import AnalyticAcquisitionFunction
+from botorch.acquisition.monte_carlo import MCAcquisitionFunction
+from botorch.acquisition.objective import MCAcquisitionObjective, PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.deterministic import DeterministicModel
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.transforms import (
+    concatenate_pending_points,
+    match_batch_shape,
+    t_batch_mode_transform,
+)
+from torch import Tensor
+from torch.distributions import Bernoulli, Normal
+
+SIGMA_JITTER = 1e-8
+
+
+
+[docs] +class AnalyticExpectedUtilityOfBestOption(AnalyticAcquisitionFunction): + r"""Analytic Preferential Expected Utility of Best Options, i.e., Analytical EUBO""" + + def __init__( + self, + pref_model: Model, + outcome_model: DeterministicModel | None = None, + previous_winner: Tensor | None = None, + ) -> None: + r"""Analytic implementation of Expected Utility of the Best Option under the + Laplace model (assumes a PairwiseGP is used as the preference model) as + proposed in [Lin2022preference]_. + + Args: + pref_model: The preference model that maps the outcomes (i.e., Y) to + scalar-valued utility. + outcome_model: A deterministic model that maps parameters (i.e., X) to + outcomes (i.e., Y). The outcome model f defines the search space of + Y = f(X). If model is None, we are directly calculating EUBO on + the parameter space. When used with `OneSamplePosteriorDrawModel`, + we are obtaining EUBO-zeta as described in [Lin2022preference]_. + previous_winner: Tensor representing the previous winner in the Y space. + """ + super().__init__(model=pref_model) + # ensure the model is in eval mode + self.add_module("outcome_model", outcome_model) + self.register_buffer("previous_winner", previous_winner) + + tkwargs = { + "dtype": pref_model.datapoints.dtype, + "device": pref_model.datapoints.device, + } + std_norm = torch.distributions.normal.Normal( + torch.zeros(1, **tkwargs), + torch.ones(1, **tkwargs), + ) + self.std_norm = std_norm + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate analytical EUBO on the candidate set X. + + Args: + X: A `batch_shape x q x d`-dim Tensor, where `q = 2` if `previous_winner` + is not `None`, and `q = 1` otherwise. + + Returns: + The acquisition value for each batch as a tensor of shape `batch_shape`. + """ + if not ( + ((X.shape[-2] == 2) and (self.previous_winner is None)) + or ((X.shape[-2] == 1) and (self.previous_winner is not None)) + ): + raise UnsupportedError( + f"{self.__class__.__name__} only support q=2 or q=1" + "with a previous winner specified" + ) + + Y = X if self.outcome_model is None else self.outcome_model(X) + + if self.previous_winner is not None: + Y = torch.cat([Y, match_batch_shape(self.previous_winner, Y)], dim=-2) + + pref_posterior = self.model.posterior(Y) + pref_mean = pref_posterior.mean.squeeze(-1) + pref_cov = pref_posterior.covariance_matrix + delta = pref_mean[..., 0] - pref_mean[..., 1] + + w = torch.tensor([1.0, -1.0], dtype=pref_cov.dtype, device=pref_cov.device) + var = w @ pref_cov @ w + sigma = torch.sqrt(var.clamp(min=SIGMA_JITTER)) + + u = delta / sigma + + ucdf = self.std_norm.cdf(u) + updf = torch.exp(self.std_norm.log_prob(u)) + acqf_val = sigma * (updf + u * ucdf) + if self.previous_winner is None: + acqf_val = acqf_val + pref_mean[..., 1] + return acqf_val
+
+ + + +
+[docs] +class qExpectedUtilityOfBestOption(MCAcquisitionFunction): + r"""MC-based Expected Utility of Best Option (qEUBO) + + This computes qEUBO by + (1) sampling the joint posterior over q points + (2) evaluating the maximum objective value accross the q points + (3) averaging over the samples + + `qEUBO(X) = E[max Y], Y ~ f(X), where X = (x_1,...,x_q)` + """ + + def __init__( + self, + pref_model: Model, + outcome_model: DeterministicModel | None = None, + sampler: MCSampler | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_pending: Tensor | None = None, + ) -> None: + r"""MC-based Expected Utility of Best Option (qEUBO) as proposed + in [Astudillo2023qeubo]_. + + Args: + pref_model: The preference model that maps the outcomes (i.e., Y) to + scalar-valued utility. + outcome_model: A deterministic model that maps parameters (i.e., X) to + outcomes (i.e., Y). The outcome model f defines the search space of + Y = f(X). If model is None, we are directly calculating qEUBO on + the parameter space. + sampler: The sampler used to draw base samples. See `MCAcquisitionFunction` + more details. + objective: The MCAcquisitionObjective under which the samples are evaluated. + Defaults to `IdentityMCObjective()`. + posterior_transform: A PosteriorTransform (optional). + X_pending: A `m x d`-dim Tensor of `m` design points that have been + submitted for function evaluation but have not yet been evaluated. + Concatenated into X upon forward call. Copied and set + to have no gradient. + """ + super().__init__( + model=pref_model, + sampler=sampler, + objective=objective, + posterior_transform=posterior_transform, + X_pending=X_pending, + ) + # ensure the model is in eval mode + self.add_module("outcome_model", outcome_model) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate qEUBO on the candidate set `X`. + + Args: + X: A `batch_shape x q x d`-dim Tensor of t-batches with `q` + `d`-dim design points each. + + Returns: + A `batch_shape'`-dim Tensor of qEUBO values at the given design + points `X`, where `batch_shape'` is the broadcasted batch shape + of model and input `X`. + """ + Y = X if self.outcome_model is None else self.outcome_model(X) + + _, obj = self._get_samples_and_objectives(Y) + obj_best = obj.max(dim=-1).values + return obj_best.mean(dim=0)
+
+ + + +
+[docs] +class PairwiseBayesianActiveLearningByDisagreement(MCAcquisitionFunction): + r"""MC Bayesian Active Learning by Disagreement""" + + def __init__( + self, + pref_model: Model, + outcome_model: DeterministicModel | None = None, + num_samples: int | None = 1024, + std_noise: float | None = 0.0, + ) -> None: + """ + Monte Carlo implementation of Bayesian Active Learning by Disagreement (BALD) + proposed in [Houlsby2011bald]_. + + Args: + pref_model: The preference model that maps the outcomes (i.e., Y) to + scalar-valued utility. + outcome_model: A deterministic model that maps parameters (i.e., X) to + outcomes (i.e., Y). The outcome model f defines the search space of + Y = f(X). If model is None, we are directly calculating BALD on + the parameter space. + num_samples: number of samples to approximate the conditional_entropy. + std_noise: Additional observational noise to include. Defaults to 0. + """ + super().__init__(model=pref_model) + # ensure the model is in eval mode + self.add_module("outcome_model", outcome_model) + + self.num_samples = num_samples + # assuming the relative observation noise is fixed at 1.0 (e.g., in PairwiseGP) + self.std_noise = std_noise + self.std_normal = Normal(0, 1) + +
+[docs] + @t_batch_mode_transform(expected_q=2) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate MC BALD on the candidate set `X`. + + Args: + X: A `batch_shape x 2 x d`-dim Tensor of t-batches with `q=2` + `d`-dim design points each. + + Returns: + A `batch_shape'`-dim Tensor of MC BALD values at the given + design points pair `X`, where `batch_shape'` is the broadcasted + batch shape of model and input `X`. + """ + Y = X if self.outcome_model is None else self.outcome_model(X) + + pref_posterior = self.model.posterior(Y) + pref_mean = pref_posterior.mean.squeeze(-1) + pref_cov = pref_posterior.covariance_matrix + + mu = pref_mean[..., 0] - pref_mean[..., 1] + w = torch.tensor([1.0, -1.0], dtype=pref_cov.dtype, device=pref_cov.device) + var = 2 * self.std_noise + w @ pref_cov @ w + sigma = torch.sqrt(var.clamp(min=SIGMA_JITTER)) + + # eq (3) in Houlsby, et al. (2011) + posterior_entropies = Bernoulli( + self.std_normal.cdf(mu / torch.sqrt(var + 1)) + ).entropy() + + # Sample-based approx to eq (4) in Houlsby, et al. (2011) + obj_samples = self.std_normal.cdf( + Normal(loc=mu, scale=sigma).rsample(torch.Size([self.num_samples])) + ) + sample_entropies = Bernoulli(obj_samples).entropy() + conditional_entropies = sample_entropies.mean(dim=0) + + return posterior_entropies - conditional_entropies
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/prior_guided.html b/website-old/pages/api/_modules/botorch/acquisition/prior_guided.html new file mode 100644 index 0000000000..e0eb36908a --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/prior_guided.html @@ -0,0 +1,171 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.prior_guided

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+"""
+Prior-Guided Acquisition Functions
+
+References
+
+.. [Hvarfner2022]
+    C. Hvarfner, D. Stoll, A. Souza, M. Lindauer, F. Hutter, L. Nardi. PiBO:
+    Augmenting Acquisition Functions with User Beliefs for Bayesian Optimization.
+    ICLR 2022.
+"""
+
+from __future__ import annotations
+
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.acquisition.monte_carlo import SampleReducingMCAcquisitionFunction
+from botorch.exceptions.errors import BotorchError
+from botorch.utils.transforms import concatenate_pending_points, t_batch_mode_transform
+from torch import Tensor
+
+from torch.nn import Module
+
+
+
+[docs] +class PriorGuidedAcquisitionFunction(AcquisitionFunction): + r"""Class for weighting acquisition functions by a prior distribution. + + Supports MC and batch acquisition functions via + SampleReducingAcquisitionFunction. + + See [Hvarfner2022]_ for details. + """ + + def __init__( + self, + acq_function: AcquisitionFunction, + prior_module: Module, + log: bool = False, + prior_exponent: float = 1.0, + X_pending: Tensor | None = None, + ) -> None: + r"""Initialize the prior-guided acquisition function. + + Args: + acq_function: The base acquisition function. + prior_module: A Module that computes the probability + (or log probability) for the provided inputs. + `prior_module.forward` should take a `batch_shape x q`-dim + tensor of inputs and return a `batch_shape x q`-dim tensor + of probabilities. + log: A boolean that should be true if the acquisition function emits a + log-transformed value and the prior module emits a log probability. + prior_exponent: The exponent applied to the prior. This can be used + for example to decay the effect the prior over time as in + [Hvarfner2022]_. + X_pending: `n x d` Tensor with `n` `d`-dim design points that have + been submitted for evaluation but have not yet been evaluated. + Note: X_pending should be provided as an argument to or set on + `PriorGuidedAcquisitionFunction`, but not set on the underlying + acquisition function. + """ + super().__init__(model=acq_function.model) + if getattr(acq_function, "X_pending", None) is not None: + raise BotorchError( + "X_pending is set on acq_function, but should be set on " + "`PriorGuidedAcquisitionFunction`." + ) + self.acq_func = acq_function + self.prior_module = prior_module + self._log = log + self._prior_exponent = prior_exponent + self._is_sample_reducing_af = isinstance( + acq_function, SampleReducingMCAcquisitionFunction + ) + self.set_X_pending(X_pending=X_pending) + +
+[docs] + @concatenate_pending_points + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Compute the acquisition function weighted by the prior.""" + # batch_shape x q + prior = self.prior_module(X) + if self._is_sample_reducing_af: + # sample_shape x batch_shape x q + af_val = self.acq_func._non_reduced_forward(X) + else: + if prior.shape[-1] > 1: + raise NotImplementedError( + "q-batches with q>1 are only supported using " + "SampleReducingMCAcquisitionFunction." + ) + # batch_shape x q + af_val = self.acq_func(X).unsqueeze(-1) + if self._log: + weighted_af_val = af_val + prior * self._prior_exponent + else: + weighted_af_val = af_val * prior.pow(self._prior_exponent) + if self._is_sample_reducing_af: + return self.acq_func._sample_reduction( + self.acq_func._q_reduction(weighted_af_val) + ) + return weighted_af_val.squeeze(-1) # squeeze q-dim
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/proximal.html b/website-old/pages/api/_modules/botorch/acquisition/proximal.html new file mode 100644 index 0000000000..d23c341d24 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/proximal.html @@ -0,0 +1,281 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.proximal

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+A wrapper around AcquisitionFunctions to add proximal weighting of the
+acquisition function.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.acquisition import AcquisitionFunction
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models import ModelListGP
+from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel
+from botorch.models.model import Model
+from botorch.models.transforms.input import InputTransform
+from botorch.utils import t_batch_mode_transform
+from torch import Tensor
+from torch.nn import Module
+
+
+
+[docs] +class ProximalAcquisitionFunction(AcquisitionFunction): + """A wrapper around AcquisitionFunctions to add proximal weighting of the + acquisition function. The acquisition function is + weighted via a squared exponential centered at the last training point, + with varying lengthscales corresponding to `proximal_weights`. Can only be used + with acquisition functions based on single batch models. Acquisition functions + must be positive or `beta` must be specified to apply a SoftPlus transform before + proximal weighting. + + Small values of `proximal_weights` corresponds to strong biasing towards recently + observed points, which smoothes optimization with a small potential decrese in + convergence rate. + + + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> EI = ExpectedImprovement(model, best_f=0.0) + >>> proximal_weights = torch.ones(d) + >>> EI_proximal = ProximalAcquisitionFunction(EI, proximal_weights) + >>> eip = EI_proximal(test_X) + """ + + def __init__( + self, + acq_function: AcquisitionFunction, + proximal_weights: Tensor, + transformed_weighting: bool | None = True, + beta: float | None = None, + ) -> None: + r"""Derived Acquisition Function weighted by proximity to recently + observed point. + + Args: + acq_function: The base acquisition function, operating on input tensors + of feature dimension `d`. + proximal_weights: A `d` dim tensor used to bias locality + along each axis. + transformed_weighting: If True, the proximal weights are applied in + the transformed input space given by + `acq_function.model.input_transform` (if available), otherwise + proximal weights are applied in real input space. + beta: If not None, apply a softplus transform to the base acquisition + function, allows negative base acquisition function values. + """ + Module.__init__(self) + + self.acq_func = acq_function + model = self.acq_func.model + + if hasattr(acq_function, "X_pending"): + if acq_function.X_pending is not None: + raise UnsupportedError( + "Proximal acquisition function requires `X_pending` to be None." + ) + self.X_pending = acq_function.X_pending + + self.register_buffer("proximal_weights", proximal_weights) + self.register_buffer( + "transformed_weighting", torch.tensor(transformed_weighting) + ) + + self.register_buffer("beta", None if beta is None else torch.tensor(beta)) + + _validate_model(model, proximal_weights) + +
+[docs] + @t_batch_mode_transform(expected_q=1, assert_output_shape=False) + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate base acquisition function with proximal weighting. + + Args: + X: Input tensor of feature dimension `d` . + + Returns: + Base acquisition function evaluated on tensor `X` multiplied by proximal + weighting. + """ + model = self.acq_func.model + + train_inputs = model.train_inputs[0] + + # if the model is ModelListGP then get the first model + if isinstance(model, ModelListGP): + train_inputs = train_inputs[0] + model = model.models[0] + + # if the model has more than one output get the first copy of training inputs + if isinstance(model, BatchedMultiOutputGPyTorchModel) and model.num_outputs > 1: + train_inputs = train_inputs[0] + + input_transform = _get_input_transform(model) + + last_X = train_inputs[-1].reshape(1, 1, -1) + + # if transformed_weighting, transform X to calculate diff + # (proximal weighting in transformed space) + # otherwise,un-transform the last observed point to real space + # (proximal weighting in real space) + if input_transform is not None: + if self.transformed_weighting: + # transformed space weighting + diff = input_transform.transform(X) - last_X + else: + # real space weighting + diff = X - input_transform.untransform(last_X) + + else: + # no transformation + diff = X - last_X + + M = torch.linalg.norm(diff / self.proximal_weights, dim=-1) ** 2 + proximal_acq_weight = torch.exp(-0.5 * M) + + base_acqf = self.acq_func(X) + if self.beta is None: + if torch.any(base_acqf < 0): + raise RuntimeError( + "Cannot use proximal biasing for negative " + "acquisition function values, set a value for beta to " + "fix this with a softplus transform" + ) + + else: + base_acqf = torch.nn.functional.softplus(base_acqf, beta=self.beta) + + return base_acqf * proximal_acq_weight.flatten()
+
+ + + +def _validate_model(model: Model, proximal_weights: Tensor) -> None: + r"""Validate model + + Perform vaidation checks on model used in base acquisition function to make sure + it is compatible with proximal weighting. + + Args: + model: Model associated with base acquisition function to be validated. + proximal_weights: A `d` dim tensor used to bias locality + along each axis. + """ + + # check model for train_inputs and single batch + if not hasattr(model, "train_inputs"): + raise UnsupportedError("Acquisition function model must have `train_inputs`.") + + # get train inputs for each type of possible model + if isinstance(model, ModelListGP): + # ModelListGP models + # check to make sure that the training inputs and input transformers for each + # model match and are reversible + train_inputs = model.train_inputs[0][0] + input_transform = _get_input_transform(model.models[0]) + + for i in range(len(model.train_inputs)): + if not torch.equal(train_inputs, model.train_inputs[i][0]): + raise UnsupportedError( + "Proximal acquisition function does not support unequal " + "training inputs" + ) + + if not input_transform == _get_input_transform(model.models[i]): + raise UnsupportedError( + "Proximal acquisition function does not support non-identical " + "input transforms" + ) + + else: + # any non-ModelListGP model + train_inputs = model.train_inputs[0] + + # check to make sure that the model is single t-batch (q-batches are allowed) + if model.batch_shape != torch.Size([]) and train_inputs.shape[1] != 1: + raise UnsupportedError( + "Proximal acquisition function requires a single batch model" + ) + + # check to make sure that weights match the training data shape + if ( + len(proximal_weights.shape) != 1 + or proximal_weights.shape[0] != train_inputs.shape[-1] + ): + raise ValueError( + "`proximal_weights` must be a one dimensional tensor with " + "same feature dimension as model." + ) + + +def _get_input_transform(model: Model) -> InputTransform | None: + """get input transform if defined""" + try: + return model.input_transform + except AttributeError: + return None +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/risk_measures.html b/website-old/pages/api/_modules/botorch/acquisition/risk_measures.html new file mode 100644 index 0000000000..ed2accc08d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/risk_measures.html @@ -0,0 +1,366 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.risk_measures

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Risk Measures implemented as Monte-Carlo objectives, based on Bayesian
+optimization of risk measures as introduced in [Cakmak2020risk]_. For a
+broader discussion of Monte-Carlo methods for VaR and CVaR risk measures,
+see also [Hong2014review]_.
+
+.. [Cakmak2020risk]
+    S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of
+    Risk Measures. Advances in Neural Information Processing Systems 33, 2020.
+
+.. [Hong2014review]
+    L. J. Hong, Z. Hu, and G. Liu. Monte carlo methods for value-at-risk and
+    conditional value-at-risk: a review. ACM Transactions on Modeling and
+    Computer Simulation, 2014.
+"""
+
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from math import ceil
+
+import torch
+from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective
+from botorch.acquisition.objective import IdentityMCObjective, MCAcquisitionObjective
+from torch import Tensor
+
+
+
+[docs] +class RiskMeasureMCObjective(MCAcquisitionObjective, ABC): + r"""Objective transforming the posterior samples to samples of a risk measure. + + The risk measure is calculated over joint q-batch samples from the posterior. + If the q-batch includes samples corresponding to multiple inputs, it is assumed + that first `n_w` samples correspond to first input, second `n_w` samples + correspond to second input etc. + + The risk measures are commonly defined for minimization by considering the + upper tail of the distribution, i.e., treating larger values as being undesirable. + BoTorch by default assumes a maximization objective, so the default behavior here + is to calculate the risk measures w.r.t. the lower tail of the distribution. + This can be changed by passing a preprocessing function with + `weights=torch.tensor([-1.0])`. + """ + + def __init__( + self, + n_w: int, + preprocessing_function: Callable[[Tensor], Tensor] | None = None, + ) -> None: + r"""Transform the posterior samples to samples of a risk measure. + + Args: + n_w: The size of the `w_set` to calculate the risk measure over. + preprocessing_function: A preprocessing function to apply to the samples + before computing the risk measure. This can be used to scalarize + multi-output samples before calculating the risk measure. + For constrained optimization, this should also apply + feasibility-weighting to samples. Given a `batch x m`-dim + tensor of samples, this should return a `batch`-dim tensor. + """ + super().__init__() + self.n_w = n_w + if preprocessing_function is None: + if self._is_mo: + preprocessing_function = IdentityMCMultiOutputObjective() + else: + preprocessing_function = IdentityMCObjective() + self.preprocessing_function = preprocessing_function + + def _prepare_samples(self, samples: Tensor) -> Tensor: + r"""Prepare samples for risk measure calculations by scalarizing and + separating out the q-batch dimension. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + + Returns: + A `sample_shape x batch_shape x q x n_w`-dim tensor of prepared samples. + """ + if samples.shape[-1] > 1 and isinstance( + self.preprocessing_function, IdentityMCObjective + ): + raise RuntimeError( + "Multi-output samples should be scalarized using a " + "`preprocessing_function`." + ) + samples = self.preprocessing_function(samples) + return samples.view(*samples.shape[:-1], -1, self.n_w) + +
+[docs] + @abstractmethod + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the risk measure corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q`-dim tensor of risk measure samples. + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class CVaR(RiskMeasureMCObjective): + r"""The Conditional Value-at-Risk risk measure. + + The Conditional Value-at-Risk measures the expectation of the worst outcomes + (small rewards or large losses) with a total probability of `1 - alpha`. It + is commonly defined as the conditional expectation of the reward function, + with the condition that the reward is smaller than the corresponding + Value-at-Risk (also defined below). + + Note: Due to the use of a discrete `w_set` of samples, the VaR and CVaR + calculated here are (possibly biased) Monte-Carlo approximations of + the true risk measures. + """ + + def __init__( + self, + alpha: float, + n_w: int, + preprocessing_function: Callable[[Tensor], Tensor] | None = None, + ) -> None: + r"""Transform the posterior samples to samples of a risk measure. + + Args: + alpha: The risk level, float in `(0.0, 1.0]`. + n_w: The size of the `w_set` to calculate the risk measure over. + preprocessing_function: A preprocessing function to apply to the samples + before computing the risk measure. This can be used to scalarize + multi-output samples before calculating the risk measure. + For constrained optimization, this should also apply + feasibility-weighting to samples. Given a `batch x m`-dim + tensor of samples, this should return a `batch`-dim tensor. + """ + super().__init__(n_w=n_w, preprocessing_function=preprocessing_function) + if not 0 < alpha <= 1: + raise ValueError("alpha must be in (0.0, 1.0]") + self.alpha = alpha + self.alpha_idx = ceil(n_w * alpha) - 1 + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the CVaR corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q`-dim tensor of CVaR samples. + """ + prepared_samples = self._prepare_samples(samples) + return torch.topk( + prepared_samples, + k=prepared_samples.shape[-1] - self.alpha_idx, + largest=False, + dim=-1, + ).values.mean(dim=-1)
+
+ + + +
+[docs] +class VaR(CVaR): + r"""The Value-at-Risk risk measure. + + Value-at-Risk measures the smallest possible reward (or largest possible loss) + after excluding the worst outcomes with a total probability of `1 - alpha`. It + is commonly used in financial risk management, and it corresponds to the + `1 - alpha` quantile of a given random variable. + """ + + def __init__( + self, + alpha: float, + n_w: int, + preprocessing_function: Callable[[Tensor], Tensor] | None = None, + ) -> None: + r"""Transform the posterior samples to samples of a risk measure. + + Args: + alpha: The risk level, float in `(0.0, 1.0]`. + n_w: The size of the `w_set` to calculate the risk measure over. + preprocessing_function: A preprocessing function to apply to the samples + before computing the risk measure. This can be used to scalarize + multi-output samples before calculating the risk measure. + For constrained optimization, this should also apply + feasibility-weighting to samples. Given a `batch x m`-dim + tensor of samples, this should return a `batch`-dim tensor. + """ + super().__init__( + n_w=n_w, + alpha=alpha, + preprocessing_function=preprocessing_function, + ) + self._q = 1 - self.alpha_idx / n_w + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the VaR corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q`-dim tensor of VaR samples. + """ + prepared_samples = self._prepare_samples(samples) + # this is equivalent to sorting along dim=-1 in descending order + # and taking the values at index self.alpha_idx. E.g. + # >>> sorted_res = prepared_samples.sort(dim=-1, descending=True) + # >>> sorted_res.values[..., self.alpha_idx] + # Using quantile is far more memory efficient since `torch.sort` + # produces values and indices tensors with shape + # `sample_shape x batch_shape x (q * n_w) x m` + return torch.quantile( + input=prepared_samples, + q=self._q, + dim=-1, + keepdim=False, + interpolation="lower", + )
+
+ + + +
+[docs] +class WorstCase(RiskMeasureMCObjective): + r"""The worst-case risk measure.""" + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the worst-case measure corresponding to the given samples. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q`-dim tensor of worst-case samples. + """ + prepared_samples = self._prepare_samples(samples) + return prepared_samples.min(dim=-1).values
+
+ + + +
+[docs] +class Expectation(RiskMeasureMCObjective): + r"""The expectation risk measure. + + For unconstrained problems, we recommend using the `ExpectationPosteriorTransform` + instead. `ExpectationPosteriorTransform` directly transforms the posterior + distribution over `q * n_w` to a posterior of `q` expectations, significantly + reducing the cost of posterior sampling as a result. + """ + +
+[docs] + def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + r"""Calculate the expectation corresponding to the given samples. + This calculates the expectation / mean / average of each `n_w` samples + across the q-batch dimension. If `self.weights` is given, the samples + are scalarized across the output dimension before taking the expectation. + + Args: + samples: A `sample_shape x batch_shape x (q * n_w) x m`-dim tensor of + posterior samples. The q-batches should be ordered so that each + `n_w` block of samples correspond to the same input. + X: A `batch_shape x q x d`-dim tensor of inputs. Ignored. + + Returns: + A `sample_shape x batch_shape x q`-dim tensor of expectation samples. + """ + prepared_samples = self._prepare_samples(samples) + return prepared_samples.mean(dim=-1)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/thompson_sampling.html b/website-old/pages/api/_modules/botorch/acquisition/thompson_sampling.html new file mode 100644 index 0000000000..58eea6563b --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/thompson_sampling.html @@ -0,0 +1,153 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.thompson_sampling

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+import torch
+from botorch.acquisition.analytic import AcquisitionFunction
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.model import Model
+from botorch.sampling.pathwise.posterior_samplers import get_matheron_path_model
+from botorch.utils.transforms import t_batch_mode_transform
+from torch import Tensor
+
+
+BATCH_SIZE_CHANGE_ERROR = """The batch size of PathwiseThompsonSampling should \
+not change during a forward pass - was {}, now {}. Please re-initialize the \
+acquisition if you want to change the batch size."""
+
+
+
+[docs] +class PathwiseThompsonSampling(AcquisitionFunction): + r"""Single-outcome Thompson Sampling packaged as an (analytic) + acquisition function. Querying the acquisition function gives the summed + values of one or more draws from a pathwise drawn posterior sample, and thus + it maximization yields one (or multiple) Thompson sample(s). + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> TS = PathwiseThompsonSampling(model) + """ + + def __init__( + self, + model: Model, + posterior_transform: PosteriorTransform | None = None, + ) -> None: + r"""Single-outcome TS. + + Args: + model: A fitted GP model. + posterior_transform: A PosteriorTransform. If using a multi-output model, + a PosteriorTransform that transforms the multi-output posterior into a + single-output posterior is required. + """ + if model._is_fully_bayesian: + raise NotImplementedError( + "PathwiseThompsonSampling is not supported for fully Bayesian models", + ) + + super().__init__(model=model) + self.batch_size: int | None = None + +
+[docs] + def redraw(self) -> None: + self.samples = get_matheron_path_model( + model=self.model, sample_shape=torch.Size([self.batch_size]) + )
+ + +
+[docs] + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the pathwise posterior sample draws on the candidate set X. + + Args: + X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points. + + Returns: + A `(b1 x ... bk) x [num_models for fully bayesian]`-dim tensor of + evaluations on the posterior sample draws. + """ + batch_size = X.shape[-2] + q_dim = -2 + + # batch_shape x q x 1 x d + X = X.unsqueeze(-2) + if self.batch_size is None: + self.batch_size = batch_size + self.redraw() + elif self.batch_size != batch_size: + raise ValueError( + BATCH_SIZE_CHANGE_ERROR.format(self.batch_size, batch_size) + ) + + # posterior_values.shape post-squeeze: + # batch_shape x q x m + posterior_values = self.samples(X).squeeze(-2) + # sum over batch dim and squeeze num_objectives dim (-1) + return posterior_values.sum(q_dim).squeeze(-1)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/acquisition/utils.html b/website-old/pages/api/_modules/botorch/acquisition/utils.html new file mode 100644 index 0000000000..b863420711 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/acquisition/utils.html @@ -0,0 +1,672 @@ + + + + + + + +
+
+
+
+

Source code for botorch.acquisition.utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Utilities for acquisition functions.
+"""
+
+from __future__ import annotations
+
+import math
+from collections.abc import Callable
+
+import torch
+from botorch.acquisition.objective import (
+    MCAcquisitionObjective,
+    PosteriorTransform,
+    ScalarizedPosteriorTransform,
+)
+from botorch.exceptions.errors import (
+    BotorchTensorDimensionError,
+    DeprecationError,
+    UnsupportedError,
+)
+from botorch.models.fully_bayesian import MCMC_DIM
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.sampling.get_sampler import get_sampler
+from botorch.sampling.pathwise.posterior_samplers import get_matheron_path_model
+from botorch.utils.objective import compute_feasibility_indicator
+from botorch.utils.sampling import optimize_posterior_samples
+from botorch.utils.transforms import is_ensemble, normalize_indices
+from gpytorch.models import GP
+from pyre_extensions import none_throws
+from torch import Tensor
+
+
+
+[docs] +def get_acquisition_function(*args, **kwargs) -> None: + raise DeprecationError( + "`get_acquisition_function` has been moved to `botorch.acquisition.factory`." + )
+ + + +
+[docs] +def repeat_to_match_aug_dim(target_tensor: Tensor, reference_tensor: Tensor) -> Tensor: + """Repeat target_tensor until it has the same first dimension as reference_tensor + This works regardless of the batch shapes and q. + This is useful as we sometimes modify sample shapes such as in LearnedObjective. + + Args: + target_tensor: A `sample_size x batch_shape x q x m`-dim Tensor + reference_tensor: A `(augmented_sample * sample_size) x batch_shape x q`-dim + Tensor. `augmented_sample` could be 1. + + Returns: + The content of `target_tensor` potentially repeated so that its first dimension + matches that of `reference_tensor`. + The shape will be `(augmented_sample * sample_size) x batch_shape x q x m`. + + Examples: + >>> import torch + >>> target_tensor = torch.arange(3).repeat(2, 1).T + >>> target_tensor + tensor([[0, 0], + [1, 1], + [2, 2]]) + >>> repeat_to_match_aug_dim(target_tensor, torch.zeros(6)) + tensor([[0, 0], + [1, 1], + [2, 2], + [0, 0], + [1, 1], + [2, 2]]) + """ + augmented_sample_num, remainder = divmod( + reference_tensor.shape[0], target_tensor.shape[0] + ) + if remainder != 0: + raise ValueError( + "The first dimension of reference_tensor must " + "be a multiple of target_tensor's." + ) + + # using repeat here as obj might be constructed as + # obj.reshape(-1, *samples.shape[2:]) where the first 2 dimensions are + # of shape `augmented_samples x sample_shape`. + repeat_size = (augmented_sample_num,) + (1,) * (target_tensor.ndim - 1) + return target_tensor.repeat(*repeat_size)
+ + + +
+[docs] +def compute_best_feasible_objective( + samples: Tensor, + obj: Tensor, + constraints: list[Callable[[Tensor], Tensor]] | None, + model: Model | None = None, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + X_baseline: Tensor | None = None, + infeasible_obj: Tensor | None = None, +) -> Tensor: + """Computes the largest `obj` value that is feasible under the `constraints`. If + `constraints` is None, returns the best unconstrained objective value. + + When no feasible observations exist and `infeasible_obj` is not `None`, returns + `infeasible_obj` (potentially reshaped). When no feasible observations exist and + `infeasible_obj` is `None`, uses `model`, `objective`, `posterior_transform`, and + `X_baseline` to infer and return an `infeasible_obj` `M` s.t. `M < min_x f(x)`. + + Args: + samples: `(sample_shape) x batch_shape x q x m`-dim posterior samples. + obj: A `(sample_shape) x batch_shape x q`-dim Tensor of MC objective values. + constraints: A list of constraint callables which map posterior samples to + a scalar. The associated constraint is considered satisfied if this + scalar is less than zero. + model: A Model, only required when there are no feasible observations. + objective: An MCAcquisitionObjective, only optionally used when there are no + feasible observations. + posterior_transform: A PosteriorTransform, only optionally used when there are + no feasible observations. + X_baseline: A `batch_shape x d`-dim Tensor of baseline points, only required + when there are no feasible observations. + infeasible_obj: A Tensor to be returned when no feasible points exist. + + Returns: + A `(sample_shape) x batch_shape`-dim Tensor of best feasible objectives. + """ + if constraints is None: # unconstrained case + # we don't need to differentiate through X_baseline for now, so taking + # the regular max over the n points to get best_f is fine + with torch.no_grad(): + return obj.amax(dim=-1, keepdim=False) + + is_feasible = compute_feasibility_indicator( + constraints=constraints, samples=samples + ) # sample_shape x batch_shape x q + + if is_feasible.any(dim=-1).all(): + infeasible_value = -torch.inf + + elif infeasible_obj is not None: + infeasible_value = infeasible_obj.item() + + else: + if model is None: + raise ValueError( + "Must specify `model` when no feasible observation exists." + ) + if X_baseline is None: + raise ValueError( + "Must specify `X_baseline` when no feasible observation exists." + ) + infeasible_value = _estimate_objective_lower_bound( + model=model, + objective=objective, + posterior_transform=posterior_transform, + X=X_baseline, + ).item() + + is_feasible = repeat_to_match_aug_dim( + target_tensor=is_feasible, reference_tensor=obj + ) + obj = torch.where(is_feasible, obj, infeasible_value) + with torch.no_grad(): + return obj.amax(dim=-1, keepdim=False)
+ + + +def _estimate_objective_lower_bound( + model: Model, + objective: MCAcquisitionObjective | None, + posterior_transform: PosteriorTransform | None, + X: Tensor, +) -> Tensor: + """Estimates a lower bound on the objective values by evaluating the model at convex + combinations of `X`, returning the 6-sigma lower bound of the computed statistics. + + Args: + model: A fitted model. + objective: An MCAcquisitionObjective with `m` outputs. + posterior_transform: A PosteriorTransform. + X: A `n x d`-dim Tensor of design points from which to draw convex combinations. + + Returns: + A `m`-dimensional Tensor of lower bounds of the objectives. + """ + convex_weights = torch.rand( + 32, + X.shape[-2], + dtype=X.dtype, + device=X.device, + ) + weights_sum = convex_weights.sum(dim=0, keepdim=True) + convex_weights = convex_weights / weights_sum + # infeasible cost M is such that -M < min_x f(x), thus + # 0 < min_x f(x) - (-M), so we should take -M as a lower + # bound on the best feasible objective + return -get_infeasible_cost( + X=convex_weights @ X, + model=model, + objective=objective, + posterior_transform=posterior_transform, + ) + + +
+[docs] +def get_infeasible_cost( + X: Tensor, + model: Model, + objective: Callable[[Tensor, Tensor | None], Tensor] | None = None, + posterior_transform: PosteriorTransform | None = None, +) -> Tensor: + r"""Get infeasible cost for a model and objective. + + For each outcome, computes an infeasible cost `M` such that + `-M < min_x f(x)` almost always, so that feasible points are preferred. + + Args: + X: A `n x d` Tensor of `n` design points to use in evaluating the + minimum. These points should cover the design space well. The more + points the better the estimate, at the expense of added computation. + model: A fitted botorch model with `m` outcomes. + objective: The objective with which to evaluate the model output. + posterior_transform: A PosteriorTransform (optional). + + Returns: + An `m`-dim tensor of infeasible cost values. + + Example: + >>> model = SingleTaskGP(train_X, train_Y) + >>> objective = lambda Y: Y[..., -1] ** 2 + >>> M = get_infeasible_cost(train_X, model, obj) + """ + if objective is None: + + def objective(Y: Tensor, X: Tensor | None = None): + return Y.squeeze(-1) + + posterior = model.posterior(X, posterior_transform=posterior_transform) + lb = objective(posterior.mean - 6 * posterior.variance.clamp_min(0).sqrt(), X=X) + if lb.ndim < posterior.mean.ndim: + lb = lb.unsqueeze(-1) + # Take outcome-wise min. Looping in to handle batched models. + while lb.dim() > 1: + lb = lb.min(dim=-2).values + return -(lb.clamp_max(0.0))
+ + + +def _prune_inferior_shared_processing( + model: Model, + X: Tensor, + is_moo: bool, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + num_samples: int = 2048, + max_frac: float = 1.0, + sampler: MCSampler | None = None, + marginalize_dim: int | None = None, +) -> tuple[int, Tensor, Tensor]: + r"""Shared data processing for `prune_inferior_points` and + `prune_inferior_points_multi_objective`. + + Returns: + - max_points: The maximum number of points to keep. + - obj_vals: The objective values of the points in `X`. + - infeas: A boolean tensor indicating feasibility of `X`. + """ + func_name = ( + "prune_inferior_points_multi_objective" if is_moo else "prune_inferior_points" + ) + if marginalize_dim is None and is_ensemble(model): + marginalize_dim = MCMC_DIM + + if X.ndim > 2: + raise UnsupportedError( + f"Batched inputs `X` are currently unsupported by `{func_name}`" + ) + if X.size(-2) == 0: + raise ValueError("X must have at least one point.") + if max_frac <= 0 or max_frac > 1.0: + raise ValueError(f"max_frac must take values in (0, 1], is {max_frac}") + max_points = math.ceil(max_frac * X.size(-2)) + with torch.no_grad(): + posterior = model.posterior(X=X, posterior_transform=posterior_transform) + if sampler is None: + sampler = get_sampler( + posterior=posterior, sample_shape=torch.Size([num_samples]) + ) + samples = sampler(posterior) + if objective is not None: + obj_vals = objective(samples=samples, X=X) + elif is_moo: + obj_vals = samples + else: + obj_vals = samples.squeeze(-1) + if obj_vals.ndim > (2 + is_moo): + if obj_vals.ndim == (3 + is_moo) and marginalize_dim is not None: + if marginalize_dim < 0: + # Update `marginalize_dim` to be positive while accounting for + # removal of output dimension in SOO. + marginalize_dim = (not is_moo) + none_throws( + normalize_indices([marginalize_dim], d=obj_vals.ndim) + )[0] + obj_vals = obj_vals.mean(dim=marginalize_dim) + else: + raise UnsupportedError( + "Models with multiple batch dims are currently unsupported by " + f"`{func_name}`." + ) + infeas = ~compute_feasibility_indicator( + constraints=constraints, + samples=samples, + marginalize_dim=marginalize_dim, + ) + return max_points, obj_vals, infeas + + +
+[docs] +def prune_inferior_points( + model: Model, + X: Tensor, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + num_samples: int = 2048, + max_frac: float = 1.0, + sampler: MCSampler | None = None, + marginalize_dim: int | None = None, +) -> Tensor: + r"""Prune points from an input tensor that are unlikely to be the best point. + + Given a model, an objective, and an input tensor `X`, this function returns + the subset of points in `X` that have some probability of being the best + point under the objective. This function uses sampling to estimate the + probabilities, the higher the number of points `n` in `X` the higher the + number of samples `num_samples` should be to obtain accurate estimates. + + Args: + model: A fitted model. Batched models are currently not supported. + X: An input tensor of shape `n x d`. Batched inputs are currently not + supported. + objective: The objective under which to evaluate the posterior. + posterior_transform: A PosteriorTransform (optional). + constraints: A list of constraint callables which map a Tensor of posterior + samples of dimension `sample_shape x batch-shape x q x m`-dim to a + `sample_shape x batch-shape x q`-dim Tensor. The associated constraints + are satisfied if `constraint(samples) < 0`. + num_samples: The number of samples used to compute empirical + probabilities of being the best point. + max_frac: The maximum fraction of points to retain. Must satisfy + `0 < max_frac <= 1`. Ensures that the number of elements in the + returned tensor does not exceed `ceil(max_frac * n)`. + sampler: If provided, will use this customized sampler instead of + automatically constructing one with `num_samples`. + marginalize_dim: A batch dimension that should be marginalized. + For example, this is useful when using a batched fully Bayesian + model. + + Returns: + A `n' x d` with subset of points in `X`, where + + n' = min(N_nz, ceil(max_frac * n)) + + with `N_nz` the number of points in `X` that have non-zero (empirical, + under `num_samples` samples) probability of being the best point. + """ + max_points, obj_vals, infeas = _prune_inferior_shared_processing( + model=model, + X=X, + is_moo=False, + objective=objective, + posterior_transform=posterior_transform, + constraints=constraints, + num_samples=num_samples, + max_frac=max_frac, + sampler=sampler, + marginalize_dim=marginalize_dim, + ) + if infeas.any(): + # set infeasible points to worse than worst objective across all samples + # Use clone() here to avoid deprecated `index_put_` on an expanded tensor + obj_vals = obj_vals.clone() + obj_vals[infeas] = obj_vals.min() - 1 + + is_best = torch.argmax(obj_vals, dim=-1) + idcs, counts = torch.unique(is_best, return_counts=True) + + if len(idcs) > max_points: + counts, order_idcs = torch.sort(counts, stable=True, descending=True) + idcs = order_idcs[:max_points] + + return X[idcs]
+ + + +
+[docs] +def project_to_target_fidelity( + X: Tensor, + target_fidelities: dict[int, float] | None = None, + d: int | None = None, +) -> Tensor: + r"""Project `X` onto the target set of fidelities. + + This function assumes that the set of feasible fidelities is a box, so + projecting here just means setting each fidelity parameter to its target + value. If X does not contain the fidelity dimensions, this will insert + them and set them to their target values. + + Args: + X: A `batch_shape x q x (d or d-d_f)`-dim Tensor of with `q` `d` or + `d-d_f`-dim design points for each t-batch, where d_f is the + number of fidelity dimensions. If the argument `d` is not provided, + `X` must include the fidelity dimensions and have a trailing`X` must + include the fidelity dimensions and have a trailing + target_fidelities: A dictionary mapping a subset of columns of `X` (the + fidelity parameters) to their respective target fidelity value. If + omitted, assumes that the last column of X is the fidelity parameter + with a target value of 1.0. + d: The total dimension `d`. + + Return: + A `batch_shape x q x d`-dim Tensor `X_proj` with fidelity parameters + projected to the provided fidelity values. + """ + if target_fidelities is None: + target_fidelities = {-1: 1.0} + if d is None: + # assume X contains the fidelity dimensions + d = X.shape[-1] + # normalize to positive indices + tfs = {k if k >= 0 else d + k: v for k, v in target_fidelities.items()} + ones = torch.ones(*X.shape[:-1], device=X.device, dtype=X.dtype) + if X.shape[-1] == d: + # X contains fidelity dimensions + # here we're looping through the feature dimension of X - this could be + # slow for large `d`, we should optimize this for that case + X_proj = torch.stack( + [X[..., i] if i not in tfs else tfs[i] * ones for i in range(d)], dim=-1 + ) + elif X.shape[-1] == d - len(target_fidelities): + # need to insert fidelity dimensions + cols = [] + X_idx = 0 + for i in range(d): + if i not in tfs: + cols.append(X[..., X_idx]) + X_idx += 1 + else: + cols.append(tfs[i] * ones) + X_proj = torch.stack(cols, dim=-1) + else: + raise BotorchTensorDimensionError( + "X must have a last dimension with size `d` or `d-d_f`," + f" but got {X.shape[-1]}." + ) + + return X_proj
+ + + +
+[docs] +def expand_trace_observations( + X: Tensor, fidelity_dims: list[int] | None = None, num_trace_obs: int = 0 +) -> Tensor: + r"""Expand `X` with trace observations. + + Expand a tensor of inputs with "trace observations" that are obtained during + the evaluation of the candidate set. This is used in multi-fidelity + optimization. It can be though of as augmenting the `q`-batch with additional + points that are the expected trace observations. + + Let `f_i` be the `i`-th fidelity parameter. Then this functions assumes that + for each element of the q-batch, besides the fidelity `f_i`, we will observe + additonal fidelities `f_i1, ..., f_iK`, where `K = num_trace_obs`, during + evaluation of the candidate set `X`. Specifically, this function assumes + that `f_ij = (K-j) / (num_trace_obs + 1) * f_i` for all `i`. That is, the + expansion is performed in parallel for all fidelities (it does not expand + out all possible combinations). + + Args: + X: A `batch_shape x q x d`-dim Tensor of with `q` `d`-dim design points + (incl. the fidelity parameters) for each t-batch. + fidelity_dims: The indices of the fidelity parameters. If omitted, + assumes that the last column of X contains the fidelity parameters. + num_trace_obs: The number of trace observations to use. + + Return: + A `batch_shape x (q + num_trace_obs x q) x d` Tensor `X_expanded` that + expands `X` with trace observations. + """ + if num_trace_obs == 0: # No need to expand if we don't use trace observations + return X + + if fidelity_dims is None: + fidelity_dims = [-1] + + # The general strategy in the following is to expand `X` to the desired + # shape, and then multiply it (point-wise) with a tensor of scaling factors + reps = [1] * (X.ndim - 2) + [1 + num_trace_obs, 1] + X_expanded = X.repeat(*reps) # batch_shape x (q + num_trace_obs x q) x d + scale_fac = torch.ones_like(X_expanded) + s_pad = 1 / (num_trace_obs + 1) + # tensor of num_trace_obs scaling factors equally space between 1-s_pad and s_pad + sf = torch.linspace(1 - s_pad, s_pad, num_trace_obs, device=X.device, dtype=X.dtype) + # repeat each element q times + q = X.size(-2) + sf = torch.repeat_interleave(sf, q) # num_trace_obs * q + # now expand this to num_trace_obs x q x num_fidelities + sf = sf.unsqueeze(-1).expand(X_expanded.size(-2) - q, len(fidelity_dims)) + # change relevant entries of the scaling tensor + scale_fac[..., q:, fidelity_dims] = sf + return scale_fac * X_expanded
+ + + +
+[docs] +def project_to_sample_points(X: Tensor, sample_points: Tensor) -> Tensor: + r"""Augment `X` with sample points at which to take weighted average. + + Args: + X: A `batch_shape x 1 x d`-dim Tensor of with one d`-dim design points + for each t-batch. + sample_points: `p x d'`-dim Tensor (`d' < d`) of `d'`-dim sample points at + which to compute the expectation. The `d'`-dims refer to the trailing + columns of X. + Returns: + A `batch_shape x p x d` Tensor where the q-batch includes the `p` sample points. + """ + batch_shape = X.shape[:-2] + p, d_prime = sample_points.shape + X_new = X.repeat(*(1 for _ in batch_shape), p, 1) # batch_shape x p x d + X_new[..., -d_prime:] = sample_points + return X_new
+ + + +
+[docs] +def get_optimal_samples( + model: GP, + bounds: Tensor, + num_optima: int, + raw_samples: int = 1024, + num_restarts: int = 20, + posterior_transform: ScalarizedPosteriorTransform | None = None, + objective: MCAcquisitionObjective | None = None, + return_transformed: bool = False, +) -> tuple[Tensor, Tensor]: + """Draws sample paths from the posterior and maximizes the samples using GD. + + Args: + model: The model from which samples are drawn. + bounds: Bounds of the search space. If the model inputs are + normalized, the bounds should be normalized as well. + num_optima: The number of paths to be drawn and optimized. + raw_samples: The number of candidates randomly sample. + Defaults to 1024. + num_restarts: The number of candidates to do gradient-based + optimization on. Defaults to 20. + posterior_transform: A ScalarizedPosteriorTransform (may e.g. be used to + scalarize multi-output models or negate the objective). + objective: An MCAcquisitionObjective, used to negate the objective or otherwise + transform sample outputs. Cannot be combined with `posterior_transform`. + return_transformed: If True, return the transformed samples. + + Returns: + The optimal input locations and corresponding outputs, x* and f*. + + """ + if posterior_transform and not isinstance( + posterior_transform, ScalarizedPosteriorTransform + ): + raise ValueError( + "Only the ScalarizedPosteriorTransform is supported for " + "get_optimal_samples." + ) + if posterior_transform and objective: + raise ValueError( + "Only one of `posterior_transform` and `objective` can be specified." + ) + + if posterior_transform: + sample_transform = posterior_transform.evaluate + elif objective: + sample_transform = objective + else: + sample_transform = None + + paths = get_matheron_path_model(model=model, sample_shape=torch.Size([num_optima])) + optimal_inputs, optimal_outputs = optimize_posterior_samples( + paths=paths, + bounds=bounds, + raw_samples=raw_samples, + num_restarts=num_restarts, + sample_transform=sample_transform, + return_transformed=return_transformed, + ) + return optimal_inputs, optimal_outputs
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/cross_validation.html b/website-old/pages/api/_modules/botorch/cross_validation.html new file mode 100644 index 0000000000..55dac2c697 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/cross_validation.html @@ -0,0 +1,270 @@ + + + + + + + +
+
+
+
+

Source code for botorch.cross_validation

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Cross-validation utilities using batch evaluation mode.
+"""
+
+from __future__ import annotations
+
+from typing import Any, NamedTuple
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.fit import fit_gpytorch_mll
+from botorch.models.gpytorch import GPyTorchModel
+from botorch.models.multitask import MultiTaskGP
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from gpytorch.mlls.marginal_log_likelihood import MarginalLogLikelihood
+from torch import Tensor
+
+
+
+[docs] +class CVFolds(NamedTuple): + train_X: Tensor + test_X: Tensor + train_Y: Tensor + test_Y: Tensor + train_Yvar: Tensor | None = None + test_Yvar: Tensor | None = None
+ + + +
+[docs] +class CVResults(NamedTuple): + model: GPyTorchModel + posterior: GPyTorchPosterior + observed_Y: Tensor + observed_Yvar: Tensor | None = None
+ + + +
+[docs] +def gen_loo_cv_folds( + train_X: Tensor, train_Y: Tensor, train_Yvar: Tensor | None = None +) -> CVFolds: + r"""Generate LOO CV folds w.r.t. to `n`. + + Args: + train_X: A `n x d` or `batch_shape x n x d` (batch mode) tensor of training + features. + train_Y: A `n x (m)` or `batch_shape x n x (m)` (batch mode) tensor of + training observations. + train_Yvar: An `n x (m)` or `batch_shape x n x (m)` (batch mode) tensor + of observed measurement noise. + + Returns: + CVFolds NamedTuple with the following fields: + + - train_X: A `n x (n-1) x d` or `batch_shape x n x (n-1) x d` tensor of + training features. + - test_X: A `n x 1 x d` or `batch_shape x n x 1 x d` tensor of test features. + - train_Y: A `n x (n-1) x m` or `batch_shape x n x (n-1) x m` tensor of + training observations. + - test_Y: A `n x 1 x m` or `batch_shape x n x 1 x m` tensor of test + observations. + - train_Yvar: A `n x (n-1) x m` or `batch_shape x n x (n-1) x m` tensor + of observed measurement noise. + - test_Yvar: A `n x 1 x m` or `batch_shape x n x 1 x m` tensor of observed + measurement noise. + + Example: + >>> train_X = torch.rand(10, 1) + >>> train_Y = torch.rand_like(train_X) + >>> cv_folds = gen_loo_cv_folds(train_X, train_Y) + >>> cv_folds.train_X.shape + torch.Size([10, 9, 1]) + """ + masks = torch.eye(train_X.shape[-2], dtype=torch.uint8, device=train_X.device) + masks = masks.to(dtype=torch.bool) + if train_Y.dim() < train_X.dim(): + # add output dimension + train_Y = train_Y.unsqueeze(-1) + if train_Yvar is not None: + train_Yvar = train_Yvar.unsqueeze(-1) + train_X_cv = torch.cat( + [train_X[..., ~m, :].unsqueeze(dim=-3) for m in masks], dim=-3 + ) + test_X_cv = torch.cat([train_X[..., m, :].unsqueeze(dim=-3) for m in masks], dim=-3) + train_Y_cv = torch.cat( + [train_Y[..., ~m, :].unsqueeze(dim=-3) for m in masks], dim=-3 + ) + test_Y_cv = torch.cat([train_Y[..., m, :].unsqueeze(dim=-3) for m in masks], dim=-3) + if train_Yvar is None: + train_Yvar_cv = None + test_Yvar_cv = None + else: + train_Yvar_cv = torch.cat( + [train_Yvar[..., ~m, :].unsqueeze(dim=-3) for m in masks], dim=-3 + ) + test_Yvar_cv = torch.cat( + [train_Yvar[..., m, :].unsqueeze(dim=-3) for m in masks], dim=-3 + ) + return CVFolds( + train_X=train_X_cv, + test_X=test_X_cv, + train_Y=train_Y_cv, + test_Y=test_Y_cv, + train_Yvar=train_Yvar_cv, + test_Yvar=test_Yvar_cv, + )
+ + + +
+[docs] +def batch_cross_validation( + model_cls: type[GPyTorchModel], + mll_cls: type[MarginalLogLikelihood], + cv_folds: CVFolds, + fit_args: dict[str, Any] | None = None, + observation_noise: bool = False, + model_init_kwargs: dict[str, Any] | None = None, +) -> CVResults: + r"""Perform cross validation by using GPyTorch batch mode. + + WARNING: This function is currently very memory inefficient; use it only + for problems of small size. + + Args: + model_cls: A GPyTorchModel class. This class must initialize the likelihood + internally. Note: Multi-task GPs are not currently supported. + mll_cls: A MarginalLogLikelihood class. + cv_folds: A CVFolds tuple. + fit_args: Arguments passed along to fit_gpytorch_mll. + model_init_kwargs: Keyword arguments passed to the model constructor. + + Returns: + A CVResults tuple with the following fields + + - model: GPyTorchModel for batched cross validation + - posterior: GPyTorchPosterior where the mean has shape `n x 1 x m` or + `batch_shape x n x 1 x m` + - observed_Y: A `n x 1 x m` or `batch_shape x n x 1 x m` tensor of observations. + - observed_Yvar: A `n x 1 x m` or `batch_shape x n x 1 x m` tensor of observed + measurement noise. + + Example: + >>> import torch + >>> from botorch.cross_validation import ( + ... batch_cross_validation, gen_loo_cv_folds + ... ) + >>> + >>> from botorch.models import SingleTaskGP + >>> from botorch.models.transforms.input import Normalize + >>> from botorch.models.transforms.outcome import Standardize + >>> from gpytorch.mlls import ExactMarginalLogLikelihood + + >>> train_X = torch.rand(10, 1) + >>> train_Y = torch.rand_like(train_X) + >>> cv_folds = gen_loo_cv_folds(train_X, train_Y) + >>> input_transform = Normalize(d=train_X.shape[-1]) + >>> outcome_transform = Standardize( + ... m=train_Y.shape[-1], batch_shape=cv_folds.train_Y.shape[:-2] + ... ) + >>> + >>> cv_results = batch_cross_validation( + ... model_cls=SingleTaskGP, + ... mll_cls=ExactMarginalLogLikelihood, + ... cv_folds=cv_folds, + ... model_init_kwargs={ + ... "input_transform": input_transform, + ... "outcome_transform": outcome_transform, + ... }, + ... ) + """ + if issubclass(model_cls, MultiTaskGP): + raise UnsupportedError( + "Multi-task GPs are not currently supported by `batch_cross_validation`." + ) + model_init_kws = model_init_kwargs if model_init_kwargs is not None else {} + if cv_folds.train_Yvar is not None: + model_init_kws["train_Yvar"] = cv_folds.train_Yvar + model_cv = model_cls( + train_X=cv_folds.train_X, + train_Y=cv_folds.train_Y, + **model_init_kws, + ) + mll_cv = mll_cls(model_cv.likelihood, model_cv) + mll_cv.to(cv_folds.train_X) + + fit_args = fit_args or {} + mll_cv = fit_gpytorch_mll(mll_cv, **fit_args) + + # Evaluate on the hold-out set in batch mode + with torch.no_grad(): + posterior = model_cv.posterior( + cv_folds.test_X, observation_noise=observation_noise + ) + + return CVResults( + model=model_cv, + posterior=posterior, + observed_Y=cv_folds.test_Y, + observed_Yvar=cv_folds.test_Yvar, + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/exceptions/errors.html b/website-old/pages/api/_modules/botorch/exceptions/errors.html new file mode 100644 index 0000000000..b56ee42983 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/exceptions/errors.html @@ -0,0 +1,173 @@ + + + + + + + +
+
+
+
+

Source code for botorch.exceptions.errors

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Botorch Errors.
+"""
+
+from typing import Any
+
+import numpy.typing as npt
+
+
+
+[docs] +class BotorchError(Exception): + r"""Base botorch exception.""" + + pass
+ + + +
+[docs] +class CandidateGenerationError(BotorchError): + r"""Exception raised during generating candidates.""" + + pass
+ + + +
+[docs] +class DeprecationError(BotorchError): + r"""Exception raised due to deprecations""" + + pass
+ + + +
+[docs] +class InputDataError(BotorchError): + r"""Exception raised when input data does not comply with conventions.""" + + pass
+ + + +
+[docs] +class UnsupportedError(BotorchError): + r"""Currently unsupported feature.""" + + pass
+ + + +
+[docs] +class BotorchTensorDimensionError(BotorchError): + r"""Exception raised when a tensor violates a botorch convention.""" + + pass
+ + + +
+[docs] +class ModelFittingError(Exception): + r"""Exception raised when attempts to fit a model terminate unsuccessfully.""" + + pass
+ + + +
+[docs] +class OptimizationTimeoutError(BotorchError): + r"""Exception raised when optimization times out.""" + + def __init__( + self, /, *args: Any, current_x: npt.NDArray, runtime: float, **kwargs: Any + ) -> None: + r""" + Args: + *args: Standard args to `BoTorchError`. + current_x: A numpy array representing the current iterate. + runtime: The total runtime in seconds after which the optimization + timed out. + **kwargs: Standard kwargs to `BoTorchError`. + """ + super().__init__(*args, **kwargs) + self.current_x = current_x + self.runtime = runtime
+ + + +
+[docs] +class OptimizationGradientError(BotorchError, RuntimeError): + r"""Exception raised when gradient array `gradf` containts NaNs.""" + + def __init__(self, /, *args: Any, current_x: npt.NDArray, **kwargs: Any) -> None: + r""" + Args: + *args: Standard args to `BoTorchError`. + current_x: A numpy array representing the current iterate. + **kwargs: Standard kwargs to `BoTorchError`. + """ + super().__init__(*args, **kwargs) + self.current_x = current_x
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/exceptions/warnings.html b/website-old/pages/api/_modules/botorch/exceptions/warnings.html new file mode 100644 index 0000000000..14a648199a --- /dev/null +++ b/website-old/pages/api/_modules/botorch/exceptions/warnings.html @@ -0,0 +1,198 @@ + + + + + + + +
+
+
+
+

Source code for botorch.exceptions.warnings

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Botorch Warnings.
+"""
+
+import warnings
+
+
+
+[docs] +class BotorchWarning(Warning): + r"""Base botorch warning.""" + + pass
+ + + +
+[docs] +class BadInitialCandidatesWarning(BotorchWarning): + r"""Warning issued if set of initial candidates for optimziation is bad.""" + + pass
+ + + +
+[docs] +class InputDataWarning(BotorchWarning): + r"""Warning raised when input data does not comply with conventions.""" + + pass
+ + + +
+[docs] +class CostAwareWarning(BotorchWarning): + r"""Warning raised in the context of cost-aware acquisition strategies.""" + + pass
+ + + +
+[docs] +class OptimizationWarning(BotorchWarning): + r"""Optimization-related warnings.""" + + pass
+ + + +
+[docs] +class SamplingWarning(BotorchWarning): + r"""Sampling related warnings.""" + + pass
+ + + +
+[docs] +class BotorchTensorDimensionWarning(BotorchWarning): + r"""Warning raised when a tensor possibly violates a botorch convention.""" + + pass
+ + + +
+[docs] +class UserInputWarning(BotorchWarning): + r"""Warning raised when a potential issue is detected with user provided inputs.""" + + pass
+ + + +
+[docs] +class NumericsWarning(BotorchWarning): + r"""Warning raised when numerical issues are detected.""" + + pass
+ + + +
+[docs] +def legacy_ei_numerics_warning(legacy_name: str) -> None: + """Raises a warning for legacy EI acquisition functions that are known to have + numerical issues and should be replaced with the LogEI version for virtually all + use-cases except for explicit benchmarking of the numerical issues of legacy EI. + + Args: + legacy_name: The name of the legacy EI acquisition function. + logei_name: The name of the associated LogEI acquisition function. + """ + legacy_to_logei = { + "ExpectedImprovement": "LogExpectedImprovement", + "ConstrainedExpectedImprovement": "LogConstrainedExpectedImprovement", + "NoisyExpectedImprovement": "LogNoisyExpectedImprovement", + "qExpectedImprovement": "qLogExpectedImprovement", + "qNoisyExpectedImprovement": "qLogNoisyExpectedImprovement", + "qExpectedHypervolumeImprovement": "qLogExpectedHypervolumeImprovement", + "qNoisyExpectedHypervolumeImprovement": ( + "qLogNoisyExpectedHypervolumeImprovement" + ), + } + # Only raise the warning if the legacy name is in the mapping. It can fail to be in + # the mapping if the legacy acquisition function derives from a legacy EI class, + # e.g. MOMF, which derives from qEHVI, but there is not corresponding LogMOMF yet. + if legacy_name in legacy_to_logei: + logei_name = legacy_to_logei[legacy_name] + msg = ( + f"{legacy_name} has known numerical issues that lead to suboptimal " + "optimization performance. It is strongly recommended to simply replace" + f"\n\n\t {legacy_name} \t --> \t {logei_name} \n\n" + "instead, which fixes the issues and has the same " + "API. See https://arxiv.org/abs/2310.20708 for details." + ) + warnings.warn(msg, NumericsWarning, stacklevel=2)
+ + + +def _get_single_precision_warning(dtype_str: str) -> str: + msg = ( + f"The model inputs are of type {dtype_str}. It is strongly recommended " + "to use double precision in BoTorch, as this improves both " + "precision and stability and can help avoid numerical errors. " + "See https://github.com/pytorch/botorch/discussions/1444" + ) + return msg +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/fit.html b/website-old/pages/api/_modules/botorch/fit.html new file mode 100644 index 0000000000..07863e1b01 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/fit.html @@ -0,0 +1,448 @@ + + + + + + + +
+
+
+
+

Source code for botorch.fit

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Model fitting routines."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Sequence
+from copy import deepcopy
+from functools import partial
+from itertools import filterfalse
+from typing import Any
+from warnings import catch_warnings, simplefilter, warn_explicit, WarningMessage
+
+from botorch.exceptions.errors import ModelFittingError, UnsupportedError
+from botorch.exceptions.warnings import OptimizationWarning
+from botorch.logging import logger
+from botorch.models.approximate_gp import ApproximateGPyTorchModel
+from botorch.models.fully_bayesian import SaasFullyBayesianSingleTaskGP
+from botorch.models.fully_bayesian_multitask import SaasFullyBayesianMultiTaskGP
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.optim.closures import get_loss_closure_with_grads
+from botorch.optim.core import _LBFGSB_MAXITER_MAXFUN_REGEX
+from botorch.optim.fit import fit_gpytorch_mll_scipy, fit_gpytorch_mll_torch
+from botorch.optim.utils import (
+    _warning_handler_template,
+    get_parameters,
+    sample_all_priors,
+)
+from botorch.utils.context_managers import (
+    module_rollback_ctx,
+    parameter_rollback_ctx,
+    TensorCheckpoint,
+)
+from botorch.utils.dispatcher import Dispatcher, type_bypassing_encoder
+from gpytorch.likelihoods import Likelihood
+from gpytorch.mlls._approximate_mll import _ApproximateMarginalLogLikelihood
+from gpytorch.mlls.marginal_log_likelihood import MarginalLogLikelihood
+from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood
+from linear_operator.utils.errors import NotPSDError
+from pyro.infer.mcmc import MCMC, NUTS
+from torch import device, Tensor
+from torch.nn import Parameter
+from torch.utils.data import DataLoader
+
+
+def _debug_warn(w: WarningMessage) -> bool:
+    if _LBFGSB_MAXITER_MAXFUN_REGEX.search(str(w.message)):
+        return True
+    # TODO: Better handle cases where warning handling logic
+    # affects both debug and rethrow functions.
+    return False
+
+
+def _rethrow_warn(w: WarningMessage) -> bool:
+    if not issubclass(w.category, OptimizationWarning):
+        return True
+    if "Optimization timed out after" in str(w.message):
+        return True
+    return False
+
+
+DEFAULT_WARNING_HANDLER = partial(
+    _warning_handler_template,
+    debug=_debug_warn,
+    rethrow=_rethrow_warn,
+)
+FitGPyTorchMLL = Dispatcher("fit_gpytorch_mll", encoder=type_bypassing_encoder)
+
+
+
+[docs] +def fit_gpytorch_mll( + mll: MarginalLogLikelihood, + closure: Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None = None, + optimizer: Callable | None = None, + closure_kwargs: dict[str, Any] | None = None, + optimizer_kwargs: dict[str, Any] | None = None, + **kwargs: Any, +) -> MarginalLogLikelihood: + r"""Clearing house for fitting models passed as GPyTorch MarginalLogLikelihoods. + + Args: + mll: A GPyTorch MarginalLogLikelihood instance. + closure: Forward-backward closure for obtaining objective values and gradients. + Responsible for setting parameters' `grad` attributes. If no closure is + provided, one will be obtained by calling `get_loss_closure_with_grads`. + optimizer: User specified optimization algorithm. When `optimizer is None`, + this keyword argument is omitted when calling the dispatcher. + closure_kwargs: Keyword arguments passed when calling `closure`. + optimizer_kwargs: A dictionary of keyword arguments passed when + calling `optimizer`. + **kwargs: Keyword arguments passed down through the dispatcher to + fit subroutines. Unexpected keywords are ignored. + + Returns: + The `mll` instance. If fitting succeeded, then `mll` will be in evaluation mode, + i.e. `mll.training == False`. Otherwise, `mll` will be in training mode. + """ + if optimizer is not None: # defer to per-method defaults + kwargs["optimizer"] = optimizer + + return FitGPyTorchMLL( + mll, + type(mll.likelihood), + type(mll.model), + closure=closure, + closure_kwargs=closure_kwargs, + optimizer_kwargs=optimizer_kwargs, + **kwargs, + )
+ + + +@FitGPyTorchMLL.register(MarginalLogLikelihood, object, object) +def _fit_fallback( + mll: MarginalLogLikelihood, + _: type[object], + __: type[object], + *, + closure: Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None = None, + optimizer: Callable = fit_gpytorch_mll_scipy, + closure_kwargs: dict[str, Any] | None = None, + optimizer_kwargs: dict[str, Any] | None = None, + max_attempts: int = 5, + pick_best_of_all_attempts: bool = False, + warning_handler: Callable[[WarningMessage], bool] = DEFAULT_WARNING_HANDLER, + caught_exception_types: tuple[type[BaseException], ...] = (NotPSDError,), + **ignore: Any, +) -> MarginalLogLikelihood: + r"""Generic fallback method for fitting Gaussian processes. + + Attempts to fit a model using the provided optimizer, then determines whether or + not to retry by evaluating a given policy on emitted warning messages. The first + attempt is run using the initialized parameter values; subsequent attempts begin + by resampling tunable parameters. + + Args: + closure: Forward-backward closure for obtaining objective values and gradients. + Responsible for setting parameters' `grad` attributes. If no closure is + provided, one will be obtained by calling `get_loss_closure_with_grads`. + optimizer: The underlying optimization algorithm to run. Should return + an `OptimizationResult` object, whose `fval` field records the negative + MLL value. Defaults to `fit_gpytorch_mll_scipy`. + closure_kwargs: Keyword arguments passed to `closure`. + optimizer_kwargs: Keyword arguments passed to `optimizer`. + max_attempts: The maximum number of fit attempts allowed. The attempt budget + is NOT shared between calls to this method. + pick_best_of_all_attempts: If True, the model will be fit `max_attempts` times, + and the attempt that produces largest MLL value will be returned. + First attempt uses the initial hyper parameter values, the subsequent + attempts will call `sample_all_priors` to sample the initial values. + If any attempt produces an error, the resulting parameters are discarded. + If optimizer timeout is used, the `timeout_sec` will be used as is for + each attempt, and it should be manually adjusted accordingly. + warning_handler: A function used to filter warnings produced when calling + `optimizer`. Any unfiltered warnings (those for which `warning_handler` + returns `False`) will be rethrown and trigger a model fitting retry. + caught_exception_types: A tuple of exception types whose instances should + be logged at the `DEBUG` level. + **ignore: This function ignores unrecognized keyword arguments. + + Returns: + The `mll` instance. If fitting succeeded, then `mll` will be in evaluation mode, + i.e. `mll.training == False`. Otherwise, `mll` will be in training mode. + """ + # Setup + optimizer_kwargs = {} if optimizer_kwargs is None else optimizer_kwargs + params_nograd: dict[str, Parameter] = None # pyre-ignore [9] + ckpt_nograd: dict[str, TensorCheckpoint] = None # pyre-ignore [9] + ckpt: dict[str, TensorCheckpoint] = None # pyre-ignore [9] + + # Build closure + mll.train() + if closure is None: + closure = get_loss_closure_with_grads( + mll, parameters=get_parameters(mll, requires_grad=True) + ) + if closure_kwargs is not None: + closure = partial(closure, **closure_kwargs) + + # Record best MLL & corresponding state dict. + best_mll: float = -float("inf") + best_state_dict = None + # Attempt to fit the model + for attempt in range(1, 1 + max_attempts): + # Wrap with rollback contextmanager so that each loop iteration reloads the + # original state_dict upon exiting (unless we clear `ckpt`). + with module_rollback_ctx(mll, checkpoint=ckpt, device=device("cpu")) as ckpt: + if attempt > 1: # resample free parameters + if params_nograd is None: + params_nograd = get_parameters(mll, requires_grad=False) + + if ckpt_nograd is None: # reuse primary checkpoint + ckpt_nograd = {name: ckpt[name] for name in params_nograd} + + with parameter_rollback_ctx(params_nograd, checkpoint=ckpt_nograd): + sample_all_priors(mll.model) + + try: + # Fit the model + with catch_warnings(record=True) as warning_list: + simplefilter("always", category=OptimizationWarning) + result = optimizer(mll, closure=closure, **optimizer_kwargs) + + # Resolve warnings and determine whether or not to retry + success = True + for w in filterfalse(warning_handler, warning_list): + warn_explicit(str(w.message), w.category, w.filename, w.lineno) + success = False + + if success and not pick_best_of_all_attempts: + # If not picking best of all attempts, return the first + # successful attempt. + ckpt.clear() # do not rollback upon exiting + return mll.eval() + elif success: + # Update best MLL and corresponding state dict. + # Optimizers minimize negative MLL, so we negate fval. + current_mll = -result.fval + if current_mll > best_mll: + best_mll = current_mll + # Deepcopy is important here, otherwise they get updated. + best_state_dict = deepcopy(mll.state_dict()) + message = f"Fit attempt #{attempt}: New best MLL: {best_mll}." + else: + message = ( + f"Fit attempt #{attempt}: Current MLL {current_mll} did " + f"not beat best MLL so far {best_mll}." + ) + logger.debug(message) + + # Ensure mll is in the right mode if going for another attempt. + mll = mll if mll.training else mll.train() + if not success: + logger.debug( + f"Fit attempt #{attempt} of {max_attempts} triggered retry " + f"policy {'.' if attempt == max_attempts else '; retrying...'}", + ) + + except caught_exception_types as err: + logger.debug( + f"Fit attempt #{attempt} of {max_attempts} failed with exception:\n" + f"{err}", + ) + + # If picking best of all attempts, return MLL with best state dict. + if best_state_dict is not None: + mll.load_state_dict(best_state_dict) + return mll.eval() + + raise ModelFittingError("All attempts to fit the model have failed.") + + +@FitGPyTorchMLL.register(SumMarginalLogLikelihood, object, ModelListGP) +def _fit_list( + mll: SumMarginalLogLikelihood, + _: type[Likelihood], + __: type[ModelListGP], + **kwargs: Any, +) -> SumMarginalLogLikelihood: + r"""Fitting routine for lists of independent Gaussian processes. + + Args: + **kwargs: Passed to each of `mll.mlls`. + + Returns: + The `mll` instance. If fitting succeeded for all of `mll.mlls`, then `mll` will + be in evaluation mode, i.e. `mll.training == False`. Otherwise, `mll` will be in + training mode. + """ + mll.train() + for sub_mll in mll.mlls: + fit_gpytorch_mll(sub_mll, **kwargs) + + return mll.eval() if not any(sub_mll.training for sub_mll in mll.mlls) else mll + + +@FitGPyTorchMLL.register(_ApproximateMarginalLogLikelihood, object, object) +def _fit_fallback_approximate( + mll: _ApproximateMarginalLogLikelihood, + _: type[Likelihood], + __: type[ApproximateGPyTorchModel], + *, + closure: Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None = None, + data_loader: DataLoader | None = None, + optimizer: Callable | None = None, + full_batch_limit: int = 1024, + **kwargs: Any, +) -> _ApproximateMarginalLogLikelihood: + r"""Fallback method for fitting approximate Gaussian processes. + + Args: + closure: Forward-backward closure for obtaining objective values and gradients. + Responsible for setting parameters' `grad` attributes. If no closure is + provided, one will be obtained by calling `get_loss_closure_with_grads`. + optimizer: The underlying optimization algorithm to run. Default to + `fit_gpytorch_mll_scipy` when `closure=None` and the model's internal + training set has no more than `full_batch_cutoff` observations; otherwise, + defaults to `fit_gpytorch_mll_torch`. + data_loader: An optional DataLoader to pass to `get_loss_closure_with_grads`. + May only be provided when `closure=None`. + full_batch_limit: Threshold for determining the default choice of `optimizer` + when `closure=None`. + **kwargs: Keyword arguments passed to `_fit_fallback`. + """ + if data_loader is not None: + if closure is not None: + raise UnsupportedError( + "Only one of `data_loader` or `closure` may be passed." + ) + closure = get_loss_closure_with_grads( + mll=mll, + data_loader=data_loader, + parameters=get_parameters(mll, requires_grad=True), + ) + + if optimizer is None: + optimizer = ( + fit_gpytorch_mll_scipy + if closure is None and len(mll.model.train_targets) <= full_batch_limit + else fit_gpytorch_mll_torch + ) + + return _fit_fallback(mll, _, __, closure=closure, optimizer=optimizer, **kwargs) + + +
+[docs] +def fit_fully_bayesian_model_nuts( + model: SaasFullyBayesianSingleTaskGP | SaasFullyBayesianMultiTaskGP, + max_tree_depth: int = 6, + warmup_steps: int = 512, + num_samples: int = 256, + thinning: int = 16, + disable_progbar: bool = False, + jit_compile: bool = False, +) -> None: + r"""Fit a fully Bayesian model using the No-U-Turn-Sampler (NUTS) + + + Args: + model: SaasFullyBayesianSingleTaskGP to be fitted. + max_tree_depth: Maximum tree depth for NUTS + warmup_steps: The number of burn-in steps for NUTS. + num_samples: The number of MCMC samples. Note that with thinning, + num_samples / thinning samples are retained. + thinning: The amount of thinning. Every nth sample is retained. + disable_progbar: A boolean indicating whether to print the progress + bar and diagnostics during MCMC. + jit_compile: Whether to use jit. Using jit may be ~2X faster (rough estimate), + but it will also increase the memory usage and sometimes result in runtime + errors, e.g., https://github.com/pyro-ppl/pyro/issues/3136. + + Example: + >>> gp = SaasFullyBayesianSingleTaskGP(train_X, train_Y) + >>> fit_fully_bayesian_model_nuts(gp) + """ + model.train() + + # Do inference with NUTS + nuts = NUTS( + model.pyro_model.sample, + jit_compile=jit_compile, + full_mass=True, + ignore_jit_warnings=True, + max_tree_depth=max_tree_depth, + ) + mcmc = MCMC( + nuts, + warmup_steps=warmup_steps, + num_samples=num_samples, + disable_progbar=disable_progbar, + ) + mcmc.run() + + # Get final MCMC samples from the Pyro model + mcmc_samples = model.pyro_model.postprocess_mcmc_samples( + mcmc_samples=mcmc.get_samples() + ) + for k, v in mcmc_samples.items(): + mcmc_samples[k] = v[::thinning] + + # Load the MCMC samples back into the BoTorch model + model.load_mcmc_samples(mcmc_samples) + model.eval()
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/generation/gen.html b/website-old/pages/api/_modules/botorch/generation/gen.html new file mode 100644 index 0000000000..a2b8726a2c --- /dev/null +++ b/website-old/pages/api/_modules/botorch/generation/gen.html @@ -0,0 +1,548 @@ + + + + + + + +
+
+
+
+

Source code for botorch.generation.gen

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Candidate generation utilities.
+"""
+
+from __future__ import annotations
+
+import time
+import warnings
+from collections.abc import Callable
+from functools import partial
+from typing import Any, NoReturn
+
+import numpy as np
+import numpy.typing as npt
+import torch
+from botorch.acquisition import AcquisitionFunction
+from botorch.exceptions.errors import OptimizationGradientError
+from botorch.exceptions.warnings import OptimizationWarning
+from botorch.generation.utils import _remove_fixed_features_from_optimization
+from botorch.logging import logger
+from botorch.optim.parameter_constraints import (
+    _arrayify,
+    make_scipy_bounds,
+    make_scipy_linear_constraints,
+    make_scipy_nonlinear_inequality_constraints,
+    nonlinear_constraint_is_feasible,
+)
+from botorch.optim.stopping import ExpMAStoppingCriterion
+from botorch.optim.utils import columnwise_clamp, fix_features
+from botorch.optim.utils.timeout import minimize_with_timeout
+from scipy.optimize import OptimizeResult
+from torch import Tensor
+from torch.optim import Optimizer
+
+TGenCandidates = Callable[[Tensor, AcquisitionFunction, Any], tuple[Tensor, Tensor]]
+
+
+
+[docs] +def gen_candidates_scipy( + initial_conditions: Tensor, + acquisition_function: AcquisitionFunction, + lower_bounds: float | Tensor | None = None, + upper_bounds: float | Tensor | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None = None, + options: dict[str, Any] | None = None, + fixed_features: dict[int, float | None] | None = None, + timeout_sec: float | None = None, +) -> tuple[Tensor, Tensor]: + r"""Generate a set of candidates using `scipy.optimize.minimize`. + + Optimizes an acquisition function starting from a set of initial candidates + using `scipy.optimize.minimize` via a numpy converter. + + Args: + initial_conditions: Starting points for optimization, with shape + (b) x q x d. + acquisition_function: Acquisition function to be used. + lower_bounds: Minimum values for each column of initial_conditions. + upper_bounds: Maximum values for each column of initial_conditions. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + nonlinear_inequality_constraints: A list of tuples representing the nonlinear + inequality constraints. The first element in the tuple is a callable + representing a constraint of the form `callable(x) >= 0`. In case of an + intra-point constraint, `callable()`takes in an one-dimensional tensor of + shape `d` and returns a scalar. In case of an inter-point constraint, + `callable()` takes a two dimensional tensor of shape `q x d` and again + returns a scalar. The second element is a boolean, indicating if it is an + intra-point or inter-point constraint (`True` for intra-point. `False` for + inter-point). For more information on intra-point vs inter-point + constraints, see the docstring of the `inequality_constraints` argument to + `optimize_acqf()`. The constraints will later be passed to the scipy + solver. + options: Options used to control the optimization including "method" + and "maxiter". Select method for `scipy.minimize` using the + "method" key. By default uses L-BFGS-B for box-constrained problems + and SLSQP if inequality or equality constraints are present. If + `with_grad=False`, then we use a two-point finite difference estimate + of the gradient. + fixed_features: This is a dictionary of feature indices to values, where + all generated candidates will have features fixed to these values. + If the dictionary value is None, then that feature will just be + fixed to the clamped value and not optimized. Assumes values to be + compatible with lower_bounds and upper_bounds! + timeout_sec: Timeout (in seconds) for `scipy.optimize.minimize` routine - + if provided, optimization will stop after this many seconds and return + the best solution found so far. + + Returns: + 2-element tuple containing + + - The set of generated candidates. + - The acquisition value for each t-batch. + + Example: + >>> qEI = qExpectedImprovement(model, best_f=0.2) + >>> bounds = torch.tensor([[0., 0.], [1., 2.]]) + >>> Xinit = gen_batch_initial_conditions( + >>> qEI, bounds, q=3, num_restarts=25, raw_samples=500 + >>> ) + >>> batch_candidates, batch_acq_values = gen_candidates_scipy( + initial_conditions=Xinit, + acquisition_function=qEI, + lower_bounds=bounds[0], + upper_bounds=bounds[1], + ) + """ + options = options or {} + options = {**options, "maxiter": options.get("maxiter", 2000)} + + # if there are fixed features we may optimize over a domain of lower dimension + reduced_domain = False + if fixed_features: + # if there are no constraints, things are straightforward + if not ( + inequality_constraints + or equality_constraints + or nonlinear_inequality_constraints + ): + reduced_domain = True + # if there are we need to make sure features are fixed to specific values + else: + reduced_domain = None not in fixed_features.values() + + if reduced_domain: + _no_fixed_features = _remove_fixed_features_from_optimization( + fixed_features=fixed_features, + acquisition_function=acquisition_function, + initial_conditions=initial_conditions, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + nonlinear_inequality_constraints=nonlinear_inequality_constraints, + ) + # call the routine with no fixed_features + clamped_candidates, batch_acquisition = gen_candidates_scipy( + initial_conditions=_no_fixed_features.initial_conditions, + acquisition_function=_no_fixed_features.acquisition_function, + lower_bounds=_no_fixed_features.lower_bounds, + upper_bounds=_no_fixed_features.upper_bounds, + inequality_constraints=_no_fixed_features.inequality_constraints, + equality_constraints=_no_fixed_features.equality_constraints, + nonlinear_inequality_constraints=_no_fixed_features.nonlinear_inequality_constraints, # noqa: E501 + options=options, + fixed_features=None, + timeout_sec=timeout_sec, + ) + clamped_candidates = _no_fixed_features.acquisition_function._construct_X_full( + clamped_candidates + ) + return clamped_candidates, batch_acquisition + clamped_candidates = columnwise_clamp( + X=initial_conditions, lower=lower_bounds, upper=upper_bounds + ) + + shapeX = clamped_candidates.shape + x0 = clamped_candidates.view(-1) + bounds = make_scipy_bounds( + X=initial_conditions, lower_bounds=lower_bounds, upper_bounds=upper_bounds + ) + constraints = make_scipy_linear_constraints( + shapeX=shapeX, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + ) + + with_grad = options.get("with_grad", True) + if with_grad: + + def f_np_wrapper(x: npt.NDArray, f: Callable): + """Given a torch callable, compute value + grad given a numpy array.""" + if np.isnan(x).any(): + raise RuntimeError( + f"{np.isnan(x).sum()} elements of the {x.size} element array " + f"`x` are NaN." + ) + X = ( + torch.from_numpy(x) + .to(initial_conditions) + .view(shapeX) + .contiguous() + .requires_grad_(True) + ) + X_fix = fix_features(X, fixed_features=fixed_features) + loss = f(X_fix).sum() + # compute gradient w.r.t. the inputs (does not accumulate in leaves) + gradf = _arrayify(torch.autograd.grad(loss, X)[0].contiguous().view(-1)) + if np.isnan(gradf).any(): + msg = ( + f"{np.isnan(gradf).sum()} elements of the {x.size} element " + "gradient array `gradf` are NaN. " + "This often indicates numerical issues." + ) + if initial_conditions.dtype != torch.double: + msg += " Consider using `dtype=torch.double`." + raise OptimizationGradientError(msg, current_x=x) + fval = loss.item() + return fval, gradf + + else: + + def f_np_wrapper(x: npt.NDArray, f: Callable): + X = torch.from_numpy(x).to(initial_conditions).view(shapeX).contiguous() + with torch.no_grad(): + X_fix = fix_features(X=X, fixed_features=fixed_features) + loss = f(X_fix).sum() + fval = loss.item() + return fval + + if nonlinear_inequality_constraints: + # Make sure `batch_limit` is 1 for now. + if not (len(shapeX) == 3 and shapeX[0] == 1): + raise ValueError( + "`batch_limit` must be 1 when non-linear inequality constraints " + "are given." + ) + constraints += make_scipy_nonlinear_inequality_constraints( + nonlinear_inequality_constraints=nonlinear_inequality_constraints, + f_np_wrapper=f_np_wrapper, + x0=x0, + shapeX=shapeX, + ) + x0 = _arrayify(x0) + + def f(x): + return -acquisition_function(x) + + res = minimize_with_timeout( + fun=f_np_wrapper, + args=(f,), + x0=x0, + method=options.get("method", "SLSQP" if constraints else "L-BFGS-B"), + jac=with_grad, + bounds=bounds, + constraints=constraints, + callback=options.get("callback", None), + options={ + k: v + for k, v in options.items() + if k not in ["method", "callback", "with_grad"] + }, + timeout_sec=timeout_sec, + ) + _process_scipy_result(res=res, options=options) + + candidates = fix_features( + X=torch.from_numpy(res.x).to(initial_conditions).reshape(shapeX), + fixed_features=fixed_features, + ) + + # SLSQP sometimes fails in the line search or may just fail to find a feasible + # candidate in which case we just return the starting point. This happens rarely, + # so it shouldn't be an issue given enough restarts. + if nonlinear_inequality_constraints: + for con, is_intrapoint in nonlinear_inequality_constraints: + if not nonlinear_constraint_is_feasible( + con, is_intrapoint=is_intrapoint, x=candidates + ): + candidates = torch.from_numpy(x0).to(candidates).reshape(shapeX) + warnings.warn( + "SLSQP failed to converge to a solution the satisfies the " + "non-linear constraints. Returning the feasible starting point.", + OptimizationWarning, + stacklevel=2, + ) + break + + clamped_candidates = columnwise_clamp( + X=candidates, lower=lower_bounds, upper=upper_bounds, raise_on_violation=True + ) + with torch.no_grad(): + batch_acquisition = acquisition_function(clamped_candidates) + + return clamped_candidates, batch_acquisition
+ + + +
+[docs] +def gen_candidates_torch( + initial_conditions: Tensor, + acquisition_function: AcquisitionFunction, + lower_bounds: float | Tensor | None = None, + upper_bounds: float | Tensor | None = None, + optimizer: type[Optimizer] = torch.optim.Adam, + options: dict[str, float | str] | None = None, + callback: Callable[[int, Tensor, Tensor], NoReturn] | None = None, + fixed_features: dict[int, float | None] | None = None, + timeout_sec: float | None = None, +) -> tuple[Tensor, Tensor]: + r"""Generate a set of candidates using a `torch.optim` optimizer. + + Optimizes an acquisition function starting from a set of initial candidates + using an optimizer from `torch.optim`. + + Args: + initial_conditions: Starting points for optimization. + acquisition_function: Acquisition function to be used. + lower_bounds: Minimum values for each column of initial_conditions. + upper_bounds: Maximum values for each column of initial_conditions. + optimizer (Optimizer): The pytorch optimizer to use to perform + candidate search. + options: Options used to control the optimization. Includes + maxiter: Maximum number of iterations + callback: A callback function accepting the current iteration, loss, + and gradients as arguments. This function is executed after computing + the loss and gradients, but before calling the optimizer. + fixed_features: This is a dictionary of feature indices to values, where + all generated candidates will have features fixed to these values. + If the dictionary value is None, then that feature will just be + fixed to the clamped value and not optimized. Assumes values to be + compatible with lower_bounds and upper_bounds! + timeout_sec: Timeout (in seconds) for optimization. If provided, + `gen_candidates_torch` will stop after this many seconds and return + the best solution found so far. + + Returns: + 2-element tuple containing + + - The set of generated candidates. + - The acquisition value for each t-batch. + + Example: + >>> qEI = qExpectedImprovement(model, best_f=0.2) + >>> bounds = torch.tensor([[0., 0.], [1., 2.]]) + >>> Xinit = gen_batch_initial_conditions( + >>> qEI, bounds, q=3, num_restarts=25, raw_samples=500 + >>> ) + >>> batch_candidates, batch_acq_values = gen_candidates_torch( + initial_conditions=Xinit, + acquisition_function=qEI, + lower_bounds=bounds[0], + upper_bounds=bounds[1], + ) + """ + start_time = time.monotonic() + options = options or {} + + # if there are fixed features we may optimize over a domain of lower dimension + if fixed_features: + subproblem = _remove_fixed_features_from_optimization( + fixed_features=fixed_features, + acquisition_function=acquisition_function, + initial_conditions=initial_conditions, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + inequality_constraints=None, + equality_constraints=None, + nonlinear_inequality_constraints=None, + ) + + # call the routine with no fixed_features + elapsed = time.monotonic() - start_time + clamped_candidates, batch_acquisition = gen_candidates_torch( + initial_conditions=subproblem.initial_conditions, + acquisition_function=subproblem.acquisition_function, + lower_bounds=subproblem.lower_bounds, + upper_bounds=subproblem.upper_bounds, + optimizer=optimizer, + options=options, + callback=callback, + fixed_features=None, + timeout_sec=timeout_sec - elapsed if timeout_sec else None, + ) + clamped_candidates = subproblem.acquisition_function._construct_X_full( + clamped_candidates + ) + return clamped_candidates, batch_acquisition + _clamp = partial(columnwise_clamp, lower=lower_bounds, upper=upper_bounds) + clamped_candidates = _clamp(initial_conditions).requires_grad_(True) + _optimizer = optimizer(params=[clamped_candidates], lr=options.get("lr", 0.025)) + + i = 0 + stop = False + stopping_criterion = ExpMAStoppingCriterion(**options) + while not stop: + i += 1 + with torch.no_grad(): + X = _clamp(clamped_candidates).requires_grad_(True) + + loss = -acquisition_function(X).sum() + grad = torch.autograd.grad(loss, X)[0] + if callback: + callback(i, loss, grad) + + def assign_grad(): + _optimizer.zero_grad() + clamped_candidates.grad = grad + return loss + + _optimizer.step(assign_grad) + stop = stopping_criterion.evaluate(fvals=loss.detach()) + if timeout_sec is not None: + runtime = time.monotonic() - start_time + if runtime > timeout_sec: + stop = True + logger.info(f"Optimization timed out after {runtime} seconds.") + + clamped_candidates = _clamp(clamped_candidates) + with torch.no_grad(): + batch_acquisition = acquisition_function(clamped_candidates) + + return clamped_candidates, batch_acquisition
+ + + +
+[docs] +def get_best_candidates(batch_candidates: Tensor, batch_values: Tensor) -> Tensor: + r"""Extract best (q-batch) candidate from batch of candidates + + Args: + batch_candidates: A `b x q x d` tensor of `b` q-batch candidates, or a + `b x d` tensor of `b` single-point candidates. + batch_values: A tensor with `b` elements containing the value of the + respective candidate (higher is better). + + Returns: + A tensor of size `q x d` (if q-batch mode) or `d` from batch_candidates + with the highest associated value. + + Example: + >>> qEI = qExpectedImprovement(model, best_f=0.2) + >>> bounds = torch.tensor([[0., 0.], [1., 2.]]) + >>> Xinit = gen_batch_initial_conditions( + >>> qEI, bounds, q=3, num_restarts=25, raw_samples=500 + >>> ) + >>> batch_candidates, batch_acq_values = gen_candidates_scipy( + initial_conditions=Xinit, + acquisition_function=qEI, + lower_bounds=bounds[0], + upper_bounds=bounds[1], + ) + >>> best_candidates = get_best_candidates(batch_candidates, batch_acq_values) + """ + best = torch.argmax(batch_values.view(-1), dim=0) + return batch_candidates[best]
+ + + +def _process_scipy_result(res: OptimizeResult, options: dict[str, Any]) -> None: + r"""Process scipy optimization result to produce relevant logs and warnings.""" + if "success" not in res.keys() or "status" not in res.keys(): + with warnings.catch_warnings(): + warnings.simplefilter("always", category=OptimizationWarning) + warnings.warn( + "Optimization failed within `scipy.optimize.minimize` with no " + "status returned to `res.`", + OptimizationWarning, + stacklevel=3, + ) + elif not res.success: + if ( + "ITERATIONS REACHED LIMIT" in res.message + or "Iteration limit reached" in res.message + ): + logger.info( + "`scipy.minimize` exited by reaching the iteration limit of " + f"`maxiter: {options.get('maxiter')}`." + ) + elif "EVALUATIONS EXCEEDS LIMIT" in res.message: + logger.info( + "`scipy.minimize` exited by reaching the function evaluation limit of " + f"`maxfun: {options.get('maxfun')}`." + ) + elif "Optimization timed out after" in res.message: + logger.info(res.message) + else: + with warnings.catch_warnings(): + warnings.simplefilter("always", category=OptimizationWarning) + warnings.warn( + f"Optimization failed within `scipy.optimize.minimize` with status " + f"{res.status} and message {res.message}.", + OptimizationWarning, + stacklevel=3, + ) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/generation/sampling.html b/website-old/pages/api/_modules/botorch/generation/sampling.html new file mode 100644 index 0000000000..ac91a4e55d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/generation/sampling.html @@ -0,0 +1,440 @@ + + + + + + + +
+
+
+
+

Source code for botorch.generation.sampling

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Sampling-based generation strategies.
+
+A SamplingStrategy returns samples from the input points (i.e. Tensors in feature
+space), rather than the value for a set of tensors, as acquisition functions do.
+The q-batch dimension has similar semantics as for acquisition functions in that the
+points across the q-batch are considered jointly for sampling (where as for
+q-acquisition functions we evaluate the joint value of the q-batch).
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.acquisition.objective import (
+    IdentityMCObjective,
+    MCAcquisitionObjective,
+    PosteriorTransform,
+)
+from botorch.generation.utils import _flip_sub_unique
+from botorch.models.model import Model
+
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.multitask import MultiTaskGP
+from botorch.utils.sampling import batched_multinomial
+from botorch.utils.transforms import standardize
+from torch import Tensor
+from torch.nn import Module
+
+
+
+[docs] +class SamplingStrategy(Module, ABC): + """Abstract base class for sampling-based generation strategies.""" + +
+[docs] + @abstractmethod + def forward(self, X: Tensor, num_samples: int = 1) -> Tensor: + r"""Sample according to the SamplingStrategy. + + Args: + X: A `batch_shape x N x d`-dim Tensor from which to sample (in the `N` + dimension). + num_samples: The number of samples to draw. + + Returns: + A `batch_shape x num_samples x d`-dim Tensor of samples from `X`, where + `X[..., i, :]` is the `i`-th sample. + """ + + pass # pragma: no cover
+
+ + + +
+[docs] +class MaxPosteriorSampling(SamplingStrategy): + r"""Sample from a set of points according to their max posterior value. + + Example: + >>> MPS = MaxPosteriorSampling(model) # model w/ feature dim d=3 + >>> X = torch.rand(2, 100, 3) + >>> sampled_X = MPS(X, num_samples=5) + """ + + def __init__( + self, + model: Model, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + replacement: bool = True, + ) -> None: + r"""Constructor for the SamplingStrategy base class. + + Args: + model: A fitted model. + objective: The MCAcquisitionObjective under which the samples are + evaluated. Defaults to `IdentityMCObjective()`. + posterior_transform: An optional PosteriorTransform. + replacement: If True, sample with replacement. + """ + super().__init__() + self.model = model + self.objective = IdentityMCObjective() if objective is None else objective + self.posterior_transform = posterior_transform + self.replacement = replacement + +
+[docs] + def forward( + self, X: Tensor, num_samples: int = 1, observation_noise: bool = False + ) -> Tensor: + r"""Sample from the model posterior. + + Args: + X: A `batch_shape x N x d`-dim Tensor from which to sample (in the `N` + dimension) according to the maximum posterior value under the objective. + num_samples: The number of samples to draw. + observation_noise: If True, sample with observation noise. + + Returns: + A `batch_shape x num_samples x d`-dim Tensor of samples from `X`, where + `X[..., i, :]` is the `i`-th sample. + """ + posterior = self.model.posterior( + X, + observation_noise=observation_noise, + posterior_transform=self.posterior_transform, + ) + # num_samples x batch_shape x N x m + samples = posterior.rsample(sample_shape=torch.Size([num_samples])) + return self.maximize_samples(X, samples, num_samples)
+ + +
+[docs] + def maximize_samples(self, X: Tensor, samples: Tensor, num_samples: int = 1): + obj = self.objective(samples, X=X) # num_samples x batch_shape x N + if self.replacement: + # if we allow replacement then things are simple(r) + idcs = torch.argmax(obj, dim=-1) + else: + # if we need to deduplicate we have to do some tensor acrobatics + # first we get the indices associated w/ the num_samples top samples + _, idcs_full = torch.topk(obj, num_samples, dim=-1) + # generate some indices to smartly index into the lower triangle of + # idcs_full (broadcasting across batch dimensions) + ridx, cindx = torch.tril_indices(num_samples, num_samples) + # pick the unique indices in order - since we look at the lower triangle + # of the index matrix and we don't sort, this achieves deduplication + sub_idcs = idcs_full[ridx, ..., cindx] + if sub_idcs.ndim == 1: + idcs = _flip_sub_unique(sub_idcs, num_samples) + elif sub_idcs.ndim == 2: + # TODO: Find a better way to do this + n_b = sub_idcs.size(-1) + idcs = torch.stack( + [_flip_sub_unique(sub_idcs[:, i], num_samples) for i in range(n_b)], + dim=-1, + ) + else: + # TODO: Find a general way to do this efficiently. + raise NotImplementedError( + "MaxPosteriorSampling without replacement for more than a single " + "batch dimension is not yet implemented." + ) + # idcs is num_samples x batch_shape, to index into X we need to permute for it + # to have shape batch_shape x num_samples + if idcs.ndim > 1: + idcs = idcs.permute(*range(1, idcs.ndim), 0) + # in order to use gather, we need to repeat the index tensor d times + idcs = idcs.unsqueeze(-1).expand(*idcs.shape, X.size(-1)) + # now if the model is batched batch_shape will not necessarily be the + # batch_shape of X, so we expand X to the proper shape + Xe = X.expand(*obj.shape[1:], X.size(-1)) + # finally we can gather along the N dimension + return torch.gather(Xe, -2, idcs)
+
+ + + +
+[docs] +class BoltzmannSampling(SamplingStrategy): + r"""Sample from a set of points according to a tempered acquisition value. + + Given an acquisition function `acq_func`, this sampling strategies draws + samples from a `batch_shape x N x d`-dim tensor `X` according to a multinomial + distribution over its indices given by + + weight(X[..., i, :]) ~ exp(eta * standardize(acq_func(X[..., i, :]))) + + where `standardize(Y)` standardizes `Y` to zero mean and unit variance. As the + temperature parameter `eta -> 0`, this approaches uniform sampling, while as + `eta -> infty`, this approaches selecting the maximizer(s) of the acquisition + function `acq_func`. + + Example: + >>> UCB = UpperConfidenceBound(model, beta=0.1) + >>> BMUCB = BoltzmannSampling(UCB, eta=0.5) + >>> X = torch.rand(2, 100, 3) + >>> sampled_X = BMUCB(X, num_samples=5) + """ + + def __init__( + self, acq_func: AcquisitionFunction, eta: float = 1.0, replacement: bool = True + ) -> None: + r"""Boltzmann Acquisition Value Sampling. + + Args: + acq_func: The acquisition function; to be evaluated in batch at the + individual points of a q-batch (not jointly, as is the case for + acquisition functions). Can be analytic or Monte-Carlo. + eta: The temperature parameter in the softmax. + replacement: If True, sample with replacement. + """ + super().__init__() + self.acq_func = acq_func + self.eta = eta + self.replacement = replacement + +
+[docs] + def forward(self, X: Tensor, num_samples: int = 1) -> Tensor: + r"""Sample from a tempered value of the acquisition function value. + + Args: + X: A `batch_shape x N x d`-dim Tensor from which to sample (in the `N` + dimension) according to the maximum posterior value under the objective. + Note that if a batched model is used in the underlying acquisition + function, then its batch shape must be broadcastable to `batch_shape`. + num_samples: The number of samples to draw. + + Returns: + A `batch_shape x num_samples x d`-dim Tensor of samples from `X`, where + `X[..., i, :]` is the `i`-th sample. + """ + # TODO: Can we get the model batch shape property from the model? + # we move the `N` dimension to the front for evaluating the acquisition function + # so that X_eval has shape `N x batch_shape x 1 x d` + X_eval = X.permute(-2, *range(X.ndim - 2), -1).unsqueeze(-2) + acqval = self.acq_func(X_eval) # N x batch_shape + # now move the `N` dimension back (this is the number of categories) + acqval = acqval.permute(*range(1, X.ndim - 1), 0) # batch_shape x N + weights = torch.exp(self.eta * standardize(acqval)) # batch_shape x N + idcs = batched_multinomial( + weights=weights, num_samples=num_samples, replacement=self.replacement + ) + # now do some gathering acrobatics to select the right elements from X + return torch.gather(X, -2, idcs.unsqueeze(-1).expand(*idcs.shape, X.size(-1)))
+
+ + + +
+[docs] +class ConstrainedMaxPosteriorSampling(MaxPosteriorSampling): + r"""Constrained max posterior sampling. + + Posterior sampling where we try to maximize an objective function while + simulatenously satisfying a set of constraints c1(x) <= 0, c2(x) <= 0, + ..., cm(x) <= 0 where c1, c2, ..., cm are black-box constraint functions. + Each constraint function is modeled by a seperate GP model. We follow the + procedure as described in https://doi.org/10.48550/arxiv.2002.08526. + + Example: + >>> CMPS = ConstrainedMaxPosteriorSampling( + model, + constraint_model=ModelListGP(cmodel1, cmodel2), + ) + >>> X = torch.rand(2, 100, 3) + >>> sampled_X = CMPS(X, num_samples=5) + """ + + def __init__( + self, + model: Model, + constraint_model: ModelListGP | MultiTaskGP, + objective: MCAcquisitionObjective | None = None, + posterior_transform: PosteriorTransform | None = None, + replacement: bool = True, + ) -> None: + r"""Constructor for the SamplingStrategy base class. + + Args: + model: A fitted model. + objective: The MCAcquisitionObjective under which the samples are evaluated. + Defaults to `IdentityMCObjective()`. + posterior_transform: An optional PosteriorTransform for the objective + function (corresponding to `model`). + replacement: If True, sample with replacement. + constraint_model: either a ModelListGP where each submodel is a GP model for + one constraint function, or a MultiTaskGP model where each task is one + constraint function. All constraints are of the form c(x) <= 0. In the + case when the constraint model predicts that all candidates + violate constraints, we pick the candidates with minimum violation. + """ + if objective is not None: + raise NotImplementedError( + "`objective` is not supported for `ConstrainedMaxPosteriorSampling`." + ) + + super().__init__( + model=model, + objective=objective, + posterior_transform=posterior_transform, + replacement=replacement, + ) + self.constraint_model = constraint_model + + def _convert_samples_to_scores(self, Y_samples, C_samples) -> Tensor: + r"""Convert the objective and constraint samples into a score. + + The logic is as follows: + - If a realization has at least one feasible candidate we use the objective + value as the score and set all infeasible candidates to -inf. + - If a realization doesn't have a feasible candidate we set the score to + the negative total violation of the constraints to incentivize choosing + the candidate with the smallest constraint violation. + + Args: + Y_samples: A `num_samples x batch_shape x num_cand x 1`-dim Tensor of + samples from the objective function. + C_samples: A `num_samples x batch_shape x num_cand x num_constraints`-dim + Tensor of samples from the constraints. + + Returns: + A `num_samples x batch_shape x num_cand x 1`-dim Tensor of scores. + """ + is_feasible = (C_samples <= 0).all( + dim=-1 + ) # num_samples x batch_shape x num_cand + has_feasible_candidate = is_feasible.any(dim=-1) + + scores = Y_samples.clone() + scores[~is_feasible] = -float("inf") + if not has_feasible_candidate.all(): + # Use negative total violation for samples where no candidate is feasible + total_violation = ( + C_samples[~has_feasible_candidate] + .clamp(min=0) + .sum(dim=-1, keepdim=True) + ) + scores[~has_feasible_candidate] = -total_violation + return scores + +
+[docs] + def forward( + self, X: Tensor, num_samples: int = 1, observation_noise: bool = False + ) -> Tensor: + r"""Sample from the model posterior. + + Args: + X: A `batch_shape x N x d`-dim Tensor from which to sample (in the `N` + dimension) according to the maximum posterior value under the objective. + num_samples: The number of samples to draw. + observation_noise: If True, sample with observation noise. + + Returns: + A `batch_shape x num_samples x d`-dim Tensor of samples from `X`, where + `X[..., i, :]` is the `i`-th sample. + """ + posterior = self.model.posterior( + X=X, + observation_noise=observation_noise, + # Note: `posterior_transform` is only used for the objective + posterior_transform=self.posterior_transform, + ) + Y_samples = posterior.rsample(sample_shape=torch.Size([num_samples])) + + # Loop over the constraint models (if possible) to reduce peak memory usage. + constraint_models = ( + self.constraint_model.models + if isinstance(self.constraint_model, ModelListGP) + else [self.constraint_model] + ) + C_samples_list = [] + for c_model in constraint_models: + c_posterior = c_model.posterior(X=X, observation_noise=observation_noise) + C_samples_list.append( + c_posterior.rsample(sample_shape=torch.Size([num_samples])) + ) + C_samples = torch.cat(C_samples_list, dim=-1) + + # Convert the objective and constraint samples into a scalar-valued "score" + scores = self._convert_samples_to_scores( + Y_samples=Y_samples, C_samples=C_samples + ) + return self.maximize_samples(X=X, samples=scores, num_samples=num_samples)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/logging.html b/website-old/pages/api/_modules/botorch/logging.html new file mode 100644 index 0000000000..0e4317242c --- /dev/null +++ b/website-old/pages/api/_modules/botorch/logging.html @@ -0,0 +1,105 @@ + + + + + + + +
+
+
+
+

Source code for botorch.logging

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+import logging
+
+import torch
+
+LOG_LEVEL_DEFAULT = logging.CRITICAL
+
+
+def _get_logger(
+    name: str = "botorch", level: int = LOG_LEVEL_DEFAULT
+) -> logging.Logger:
+    """Gets a default botorch logger
+
+    Logging level can be tuned via botorch.setting.log_level
+
+    Args:
+        name: Name for logger instance
+        level: Logging threshhold for the given logger. Logs of greater or
+            equal severity will be printed to STDERR
+    """
+    logger = logging.getLogger(name)
+    logger.setLevel(level)
+    # Add timestamps to log messages.
+    console = logging.StreamHandler()
+    formatter = logging.Formatter(
+        fmt="[%(levelname)s %(asctime)s] %(name)s: %(message)s",
+        datefmt="%m-%d %H:%M:%S",
+    )
+    console.setFormatter(formatter)
+    logger.addHandler(console)
+    logger.propagate = False
+    return logger
+
+
+
+[docs] +def shape_to_str(shape: torch.Size) -> str: + return f"`{' x '.join(str(i) for i in shape)}`"
+ + + +logger = _get_logger() +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/approximate_gp.html b/website-old/pages/api/_modules/botorch/models/approximate_gp.html new file mode 100644 index 0000000000..555f4db3b0 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/approximate_gp.html @@ -0,0 +1,592 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.approximate_gp

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+References
+
+.. [burt2020svgp]
+    David R. Burt and Carl Edward Rasmussen and Mark van der Wilk,
+    Convergence of Sparse Variational Inference in Gaussian Process Regression,
+    Journal of Machine Learning Research, 2020,
+    http://jmlr.org/papers/v21/19-1015.html.
+
+.. [hensman2013svgp]
+    James Hensman and Nicolo Fusi and Neil D. Lawrence, Gaussian Processes
+    for Big Data, Proceedings of the 29th Conference on Uncertainty in
+    Artificial Intelligence, 2013, https://arxiv.org/abs/1309.6835.
+
+.. [moss2023ipa]
+    Henry B. Moss and Sebastian W. Ober and Victor Picheny,
+    Inducing Point Allocation for Sparse Gaussian Processes
+    in High-Throughput Bayesian Optimization,Proceedings of
+    the 25th International Conference on Artificial Intelligence
+    and Statistics, 2023, https://arxiv.org/pdf/2301.10123.pdf.
+
+"""
+
+from __future__ import annotations
+
+import copy
+import warnings
+
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.warnings import UserInputWarning
+from botorch.models.gpytorch import GPyTorchModel
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from botorch.models.utils import validate_input_scaling
+from botorch.models.utils.gpytorch_modules import (
+    get_covar_module_with_dim_scaled_prior,
+    get_gaussian_likelihood_with_lognormal_prior,
+)
+from botorch.models.utils.inducing_point_allocators import (
+    GreedyVarianceReduction,
+    InducingPointAllocator,
+)
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from gpytorch.distributions import MultivariateNormal
+from gpytorch.kernels import Kernel
+from gpytorch.likelihoods import (
+    GaussianLikelihood,
+    Likelihood,
+    MultitaskGaussianLikelihood,
+)
+from gpytorch.means import ConstantMean, Mean
+from gpytorch.models import ApproximateGP
+from gpytorch.utils.memoize import clear_cache_hook
+from gpytorch.variational import (
+    _VariationalDistribution,
+    _VariationalStrategy,
+    CholeskyVariationalDistribution,
+    IndependentMultitaskVariationalStrategy,
+    VariationalStrategy,
+)
+from torch import Tensor
+from torch.nn import Module
+from typing_extensions import Self
+
+
+TRANSFORM_WARNING = (
+    "Using an {ttype} transform with `SingleTaskVariationalGP`. If this "
+    "model is trained in minibatches, a {ttype} transform with learnable "
+    "parameters would update its parameters for each minibatch, which is "
+    "undesirable. If you do intend to train in minibatches, we recommend "
+    "you not use a {ttype} transform and instead pre-transform your whole "
+    "data set before fitting the model."
+)
+
+
+
+[docs] +class ApproximateGPyTorchModel(GPyTorchModel): + r""" + Botorch wrapper class for various (variational) approximate GP models in + GPyTorch. + + This can either include stochastic variational GPs (SVGPs) or + variational implementations of weight space approximate GPs. + """ + + def __init__( + self, + model: ApproximateGP | None = None, + likelihood: Likelihood | None = None, + num_outputs: int = 1, + *args, + **kwargs, + ) -> None: + r""" + Args: + model: Instance of gpytorch.approximate GP models. If omitted, + constructs a `_SingleTaskVariationalGP`. + likelihood: Instance of a GPyTorch likelihood. If omitted, uses a + either a `GaussianLikelihood` (if `num_outputs=1`) or a + `MultitaskGaussianLikelihood`(if `num_outputs>1`). + num_outputs: Number of outputs expected for the GP model. + args: Optional positional arguments passed to the + `_SingleTaskVariationalGP` constructor if no model is provided. + kwargs: Optional keyword arguments passed to the + `_SingleTaskVariationalGP` constructor if no model is provided. + """ + super().__init__() + + self.model = ( + _SingleTaskVariationalGP(num_outputs=num_outputs, *args, **kwargs) + if model is None + else model + ) + + if likelihood is None: + if num_outputs == 1: + self.likelihood = GaussianLikelihood() + else: + self.likelihood = MultitaskGaussianLikelihood(num_tasks=num_outputs) + else: + self.likelihood = likelihood + self._desired_num_outputs = num_outputs + + @property + def num_outputs(self): + return self._desired_num_outputs + +
+[docs] + def eval(self) -> Self: + r"""Puts the model in `eval` mode.""" + return Module.eval(self)
+ + +
+[docs] + def train(self, mode: bool = True) -> Self: + r"""Put the model in `train` mode. + + Args: + mode: A boolean denoting whether to put in `train` or `eval` mode. + If `False`, model is put in `eval` mode. + """ + return Module.train(self, mode=mode)
+ + +
+[docs] + def posterior( + self, + X, + output_indices: list[int] | None = None, + observation_noise: bool = False, + posterior_transform: PosteriorTransform | None = None, + ) -> GPyTorchPosterior: + if output_indices is not None: + raise NotImplementedError( # pragma: no cover + f"{self.__class__.__name__}.posterior does not support output indices." + ) + self.eval() # make sure model is in eval mode + + # input transforms are applied at `posterior` in `eval` mode, and at + # `model.forward()` at the training time + X = self.transform_inputs(X) + + # check for the multi-batch case for multi-outputs b/c this will throw + # warnings + X_ndim = X.ndim + if self.num_outputs > 1 and X_ndim > 2: + X = X.unsqueeze(-3).repeat(*[1] * (X_ndim - 2), self.num_outputs, 1, 1) + dist = self.model(X) + if observation_noise: + dist = self.likelihood(dist) + + posterior = GPyTorchPosterior(distribution=dist) + if hasattr(self, "outcome_transform"): + posterior = self.outcome_transform.untransform_posterior(posterior) + if posterior_transform is not None: + posterior = posterior_transform(posterior) + return posterior
+ + +
+[docs] + def forward(self, X) -> MultivariateNormal: + if self.training: + X = self.transform_inputs(X) + return self.model(X)
+
+ + + +class _SingleTaskVariationalGP(ApproximateGP): + """ + Base class wrapper for a stochastic variational Gaussian Process (SVGP) + model [hensman2013svgp]_. + + Uses by default pivoted Cholesky initialization for allocating inducing points, + however, custom inducing point allocators can be provided. + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor | None = None, + num_outputs: int = 1, + learn_inducing_points=True, + covar_module: Kernel | None = None, + mean_module: Mean | None = None, + variational_distribution: _VariationalDistribution | None = None, + variational_strategy: type[_VariationalStrategy] = VariationalStrategy, + inducing_points: Tensor | int | None = None, + inducing_point_allocator: InducingPointAllocator | None = None, + ) -> None: + r""" + Args: + train_X: Training inputs (due to the ability of the SVGP to sub-sample + this does not have to be all of the training inputs). + train_Y: Not used. + num_outputs: Number of output responses per input. + covar_module: Kernel function. If omitted, uses an `RBFKernel`. + mean_module: Mean of GP model. If omitted, uses a `ConstantMean`. + variational_distribution: Type of variational distribution to use + (default: CholeskyVariationalDistribution), the properties of the + variational distribution will encourage scalability or ease of + optimization. + variational_strategy: Type of variational strategy to use (default: + VariationalStrategy). The default setting uses "whitening" of the + variational distribution to make training easier. + inducing_points: The number or specific locations of the inducing points. + inducing_point_allocator: The `InducingPointAllocator` used to + initialize the inducing point locations. If omitted, + uses `GreedyVarianceReduction`. + """ + # We use the model subclass wrapper to deal with input / outcome transforms. + # The number of outputs will be correct here due to the check in + # SingleTaskVariationalGP. + input_batch_shape = train_X.shape[:-2] + aug_batch_shape = copy.deepcopy(input_batch_shape) + if num_outputs > 1: + aug_batch_shape += torch.Size((num_outputs,)) + self._aug_batch_shape = aug_batch_shape + + if covar_module is None: + covar_module = get_covar_module_with_dim_scaled_prior( + ard_num_dims=train_X.shape[-1], + batch_shape=self._aug_batch_shape, + ).to(train_X) + + if inducing_point_allocator is None: + inducing_point_allocator = GreedyVarianceReduction() + + # initialize inducing points if they are not given + if not isinstance(inducing_points, Tensor): + if inducing_points is None: + # number of inducing points is 25% the number of data points + # as a heuristic + inducing_points = int(0.25 * train_X.shape[-2]) + + inducing_points = inducing_point_allocator.allocate_inducing_points( + inputs=train_X, + covar_module=covar_module, + num_inducing=inducing_points, + input_batch_shape=input_batch_shape, + ) + + if variational_distribution is None: + variational_distribution = CholeskyVariationalDistribution( + num_inducing_points=inducing_points.shape[-2], + batch_shape=self._aug_batch_shape, + ) + + variational_strategy_instance = variational_strategy( + self, + inducing_points=inducing_points, + variational_distribution=variational_distribution, + learn_inducing_locations=learn_inducing_points, + ) + + # wrap variational models in independent multi-task variational strategy + if num_outputs > 1: + variational_strategy_instance = IndependentMultitaskVariationalStrategy( + base_variational_strategy=variational_strategy_instance, + num_tasks=num_outputs, + task_dim=-1, + ) + super().__init__(variational_strategy=variational_strategy_instance) + + self.mean_module = ( + ConstantMean(batch_shape=self._aug_batch_shape).to(train_X) + if mean_module is None + else mean_module + ) + + self.covar_module = covar_module + + def forward(self, X) -> MultivariateNormal: + mean_x = self.mean_module(X) + covar_x = self.covar_module(X) + latent_dist = MultivariateNormal(mean_x, covar_x) + return latent_dist + + +
+[docs] +class SingleTaskVariationalGP(ApproximateGPyTorchModel): + r"""A single-task variational GP model following [hensman2013svgp]_. + + By default, the inducing points are initialized though the + `GreedyVarianceReduction` of [burt2020svgp]_, which is known to be + effective for building globally accurate models. However, custom + inducing point allocators designed for specific down-stream tasks can also be + provided (see [moss2023ipa]_ for details), e.g. `GreedyImprovementReduction` + when the goal is to build a model suitable for standard BO. + + A single-task variational GP using relatively strong priors on the Kernel + hyperparameters, which work best when covariates are normalized to the unit + cube and outcomes are standardized (zero mean, unit variance). + + This model works in batch mode (each batch having its own hyperparameters). + When the training observations include multiple outputs, this model will use + batching to model outputs independently. However, batches of multi-output models + are not supported at this time, if you need to use those, please use a + ModelListGP. + + Use this model if you have a lot of data or if your responses are non-Gaussian. + + To train this model, you should use gpytorch.mlls.VariationalELBO and not + the exact marginal log likelihood. + + Example: + >>> import torch + >>> from botorch.models import SingleTaskVariationalGP + >>> from gpytorch.mlls import VariationalELBO + >>> + >>> train_X = torch.rand(20, 2) + >>> model = SingleTaskVariationalGP(train_X) + >>> mll = VariationalELBO( + >>> model.likelihood, model.model, num_data=train_X.shape[-2] + >>> ) + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor | None = None, + likelihood: Likelihood | None = None, + num_outputs: int = 1, + learn_inducing_points: bool = True, + covar_module: Kernel | None = None, + mean_module: Mean | None = None, + variational_distribution: _VariationalDistribution | None = None, + variational_strategy: type[_VariationalStrategy] = VariationalStrategy, + inducing_points: Tensor | int | None = None, + inducing_point_allocator: InducingPointAllocator | None = None, + outcome_transform: OutcomeTransform | None = None, + input_transform: InputTransform | None = None, + ) -> None: + r""" + Args: + train_X: Training inputs (due to the ability of the SVGP to sub-sample + this does not have to be all of the training inputs). + train_Y: Training targets (optional). + likelihood: Instance of a GPyTorch likelihood. If omitted, uses a + either a `GaussianLikelihood` (if `num_outputs=1`) or a + `MultitaskGaussianLikelihood`(if `num_outputs>1`). + num_outputs: Number of output responses per input (default: 1). + learn_inducing_points: If True, the inducing point locations are learned + jointly with the other model parameters. + covar_module: Kernel function. If omitted, uses an `RBFKernel`. + mean_module: Mean of GP model. If omitted, uses a `ConstantMean`. + variational_distribution: Type of variational distribution to use + (default: CholeskyVariationalDistribution), the properties of the + variational distribution will encourage scalability or ease of + optimization. + variational_strategy: Type of variational strategy to use (default: + VariationalStrategy). The default setting uses "whitening" of the + variational distribution to make training easier. + inducing_points: The number or specific locations of the inducing points. + inducing_point_allocator: The `InducingPointAllocator` used to + initialize the inducing point locations. If omitted, + uses `GreedyVarianceReduction`. + outcome_transform: An outcome transform that is applied to the training + data during instantiation and to the posterior during inference. + NOTE: If this model is trained in minibatches, an outcome transform + with learnable parameters (such as `Standardize`) would update its + parameters for each minibatch, which is undesirable. If you do intend + to train in minibatches, we recommend you not use an outcome transform + and instead pre-transform your whole data set before fitting the model. + input_transform: An input transform that is applied in the model's + forward pass. + NOTE: If this model is trained in minibatches, an input transform + with learnable parameters (such as `Normalize`) would update its + parameters for each minibatch, which is undesirable. If you do intend + to train in minibatches, we recommend you not use an input transform + and instead pre-transform your whole data set before fitting the model. + """ + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + if train_Y is not None: + if outcome_transform is not None: + warnings.warn( + TRANSFORM_WARNING.format(ttype="outcome"), + UserInputWarning, + stacklevel=3, + ) + train_Y, _ = outcome_transform(train_Y) + self._validate_tensor_args(X=transformed_X, Y=train_Y) + validate_input_scaling(train_X=transformed_X, train_Y=train_Y) + if train_Y.shape[-1] != num_outputs: + num_outputs = train_Y.shape[-1] + + self._num_outputs = num_outputs + self._input_batch_shape = train_X.shape[:-2] + aug_batch_shape = copy.deepcopy(self._input_batch_shape) + if num_outputs > 1: + aug_batch_shape += torch.Size([num_outputs]) + self._aug_batch_shape = aug_batch_shape + + if likelihood is None: + if num_outputs == 1: + likelihood = get_gaussian_likelihood_with_lognormal_prior( + batch_shape=self._aug_batch_shape + ) + else: + likelihood = MultitaskGaussianLikelihood(num_tasks=num_outputs) + else: + self._is_custom_likelihood = True + + if learn_inducing_points and (inducing_point_allocator is not None): + warnings.warn( + "After all the effort of specifying an inducing point allocator, " + "you probably want to stop the inducing point locations " + "being further optimized during the model fit. If so " + "then set `learn_inducing_points` to False.", + UserWarning, + stacklevel=3, + ) + + if inducing_point_allocator is None: + self._inducing_point_allocator = GreedyVarianceReduction() + else: + self._inducing_point_allocator = inducing_point_allocator + + model = _SingleTaskVariationalGP( + train_X=transformed_X, + num_outputs=num_outputs, + learn_inducing_points=learn_inducing_points, + covar_module=covar_module, + mean_module=mean_module, + variational_distribution=variational_distribution, + variational_strategy=variational_strategy, + inducing_points=inducing_points, + inducing_point_allocator=self._inducing_point_allocator, + ) + + super().__init__(model=model, likelihood=likelihood, num_outputs=num_outputs) + + if outcome_transform is not None: + self.outcome_transform = outcome_transform + if input_transform is not None: + warnings.warn( + TRANSFORM_WARNING.format(ttype="input"), + UserInputWarning, + stacklevel=3, + ) + self.input_transform = input_transform + + # for model fitting utilities + # TODO: make this a flag? + self.model.train_inputs = [transformed_X] + if train_Y is not None: + self.model.train_targets = train_Y.squeeze(-1) + + self.to(train_X) + + @property + def batch_shape(self) -> torch.Size: + r"""The batch shape of the model. + + This is a batch shape from an I/O perspective. For a model with `m` + outputs, a `test_batch_shape x q x d`-shaped input `X` to the `posterior` + method returns a Posterior object over an output of shape + `broadcast(test_batch_shape, model.batch_shape) x q x m`. + """ + return self._input_batch_shape + +
+[docs] + def init_inducing_points( + self, + inputs: Tensor, + ) -> Tensor: + r""" + Reinitialize the inducing point locations in-place with the current kernel + applied to `inputs` through the model's inducing point allocation strategy. + The variational distribution and variational strategy caches are reset. + + Args: + inputs: (\*batch_shape, n, d)-dim input data tensor. + + Returns: + (\*batch_shape, m, d)-dim tensor of selected inducing point locations. + """ + var_strat = self.model.variational_strategy + clear_cache_hook(var_strat) + if hasattr(var_strat, "base_variational_strategy"): + var_strat = var_strat.base_variational_strategy + clear_cache_hook(var_strat) + + with torch.no_grad(): + num_inducing = var_strat.inducing_points.size(-2) + inducing_points = self._inducing_point_allocator.allocate_inducing_points( + inputs=inputs, + covar_module=self.model.covar_module, + num_inducing=num_inducing, + input_batch_shape=self._input_batch_shape, + ) + var_strat.inducing_points.copy_(inducing_points) + var_strat.variational_params_initialized.fill_(0) + + return inducing_points
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/contextual.html b/website-old/pages/api/_modules/botorch/models/contextual.html new file mode 100644 index 0000000000..582fe1a4fb --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/contextual.html @@ -0,0 +1,236 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.contextual

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from typing import Any
+
+from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.kernels.contextual_lcea import LCEAKernel
+from botorch.models.kernels.contextual_sac import SACKernel
+from botorch.utils.datasets import SupervisedDataset
+from torch import Tensor
+
+
+
+[docs] +class SACGP(SingleTaskGP): + r"""A GP using a Structural Additive Contextual(SAC) kernel.""" + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + train_Yvar: Tensor | None, + decomposition: dict[str, list[int]], + ) -> None: + r""" + Args: + train_X: (n x d) X training data. + train_Y: (n x 1) Y training data. + train_Yvar: (n x 1) Noise variances of each training Y. If None, + we use an inferred noise likelihood. + decomposition: Keys are context names. Values are the indexes of + parameters belong to the context. The parameter indexes are in + the same order across contexts. + """ + super().__init__(train_X=train_X, train_Y=train_Y, train_Yvar=train_Yvar) + self.covar_module = SACKernel( + decomposition=decomposition, + batch_shape=self._aug_batch_shape, + device=train_X.device, + ) + self.decomposition = decomposition + self.to(train_X) + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset, + decomposition: dict[str, list[int]], + ) -> dict[str, Any]: + r"""Construct `Model` keyword arguments from a dict of `SupervisedDataset`. + + Args: + training_data: A `SupervisedDataset` containing the training data. + decomposition: Dictionary of context names and their indexes of the + corresponding active context parameters. + """ + base_inputs = super().construct_inputs(training_data=training_data) + return { + **base_inputs, + "decomposition": decomposition, + }
+
+ + + +
+[docs] +class LCEAGP(SingleTaskGP): + r"""A GP using a Latent Context Embedding Additive (LCE-A) Kernel. + + Note that the model does not support batch training. Input training + data sets should have dim = 2. + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + train_Yvar: Tensor | None, + decomposition: dict[str, list[int]], + train_embedding: bool = True, + cat_feature_dict: dict | None = None, + embs_feature_dict: dict | None = None, + embs_dim_list: list[int] | None = None, + context_weight_dict: dict | None = None, + ) -> None: + r""" + Args: + train_X: (n x d) X training data. + train_Y: (n x 1) Y training data. + train_Yvar: (n x 1) Noise variance of Y. If None, + we use an inferred noise likelihood. + decomposition: Keys are context names. Values are the indexes of + parameters belong to the context. + train_embedding: Whether to train the embedding layer or not. If False, + the model will use pre-trained embeddings in embs_feature_dict. + cat_feature_dict: Keys are context names and values are list of categorical + features i.e. {"context_name" : [cat_0, ..., cat_k]}, where k is the + number of categorical variables. If None, we use context names in the + decomposition as the only categorical feature, i.e., k = 1. + embs_feature_dict: Pre-trained continuous embedding features of each + context. + embs_dim_list: Embedding dimension for each categorical variable. The length + equals the number of categorical features k. If None, the embedding + dimension is set to 1 for each categorical variable. + context_weight_dict: Known population weights of each context. + """ + super().__init__( + train_X=train_X, + train_Y=train_Y, + train_Yvar=train_Yvar, + outcome_transform=None, + ) + self.covar_module = LCEAKernel( + decomposition=decomposition, + batch_shape=self._aug_batch_shape, + train_embedding=train_embedding, + cat_feature_dict=cat_feature_dict, + embs_feature_dict=embs_feature_dict, + embs_dim_list=embs_dim_list, + context_weight_dict=context_weight_dict, + device=train_X.device, + ) + self.decomposition = decomposition + self.to(train_X) + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset, + decomposition: dict[str, list[str]], + train_embedding: bool = True, + cat_feature_dict: dict | None = None, + embs_feature_dict: dict | None = None, + embs_dim_list: list[int] | None = None, + context_weight_dict: dict | None = None, + ) -> dict[str, Any]: + r"""Construct `Model` keyword arguments from a dict of `SupervisedDataset`. + + Args: + training_data: A `SupervisedDataset` containing the training data. + decomposition: Dictionary of context names and the names of the + corresponding active context parameters. + train_embedding: Whether to train the embedding layer or not. + cat_feature_dict: Keys are context names and values are list of categorical + features i.e. {"context_name" : [cat_0, ..., cat_k]}, where k is the + number of categorical variables. If None, we use context names in the + decomposition as the only categorical feature, i.e., k = 1. + embs_feature_dict: Pre-trained continuous embedding features of each + context. + embs_dim_list: Embedding dimension for each categorical variable. The length + equals the number of categorical features k. If None, the embedding + dimension is set to 1 for each categorical variable. + context_weight_dict: Known population weights of each context. + """ + base_inputs = super().construct_inputs(training_data=training_data) + index_decomp = { + c: [training_data.feature_names.index(i) for i in v] + for c, v in decomposition.items() + } + return { + **base_inputs, + "decomposition": index_decomp, + "train_embedding": train_embedding, + "cat_feature_dict": cat_feature_dict, + "embs_feature_dict": embs_feature_dict, + "embs_dim_list": embs_dim_list, + "context_weight_dict": context_weight_dict, + }
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/contextual_multioutput.html b/website-old/pages/api/_modules/botorch/models/contextual_multioutput.html new file mode 100644 index 0000000000..95e72f9ce2 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/contextual_multioutput.html @@ -0,0 +1,318 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.contextual_multioutput

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+References
+
+.. [Feng2020HDCPS]
+    Q. Feng, B. Latham, H. Mao and E. Backshy. High-Dimensional Contextual Policy
+    Search with Unknown Context Rewards using Bayesian Optimization.
+    Advances in Neural Information Processing Systems 33, NeurIPS 2020.
+"""
+
+from typing import Any
+
+import torch
+from botorch.models.multitask import MultiTaskGP
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from botorch.utils.datasets import MultiTaskDataset, SupervisedDataset
+from botorch.utils.types import _DefaultType, DEFAULT
+from gpytorch.constraints import Interval
+from gpytorch.kernels.rbf_kernel import RBFKernel
+from gpytorch.likelihoods.likelihood import Likelihood
+from gpytorch.module import Module
+from linear_operator.operators import LinearOperator
+from torch import Tensor
+from torch.nn import ModuleList
+
+
+
+[docs] +class LCEMGP(MultiTaskGP): + r"""The Multi-Task GP with the latent context embedding multioutput (LCE-M) + kernel. See [Feng2020HDCPS]_ for a reference on the model and its use in Bayesian + optimization. + + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + task_feature: int, + train_Yvar: Tensor | None = None, + mean_module: Module | None = None, + covar_module: Module | None = None, + likelihood: Likelihood | None = None, + context_cat_feature: Tensor | None = None, + context_emb_feature: Tensor | None = None, + embs_dim_list: list[int] | None = None, + output_tasks: list[int] | None = None, + all_tasks: list[int] | None = None, + outcome_transform: OutcomeTransform | _DefaultType | None = DEFAULT, + input_transform: InputTransform | None = None, + ) -> None: + r""" + Args: + train_X: (n x d) X training data. + train_Y: (n x 1) Y training data. + task_feature: Column index of train_X to get context indices. + train_Yvar: An optional (n x 1) tensor of observed variances of each + training Y. If None, we infer the noise. Note that the inferred noise + is common across all tasks. + mean_module: The mean function to be used. Defaults to `ConstantMean`. + covar_module: The module for computing the covariance matrix between + the non-task features. Defaults to `RBFKernel`. + likelihood: A likelihood. The default is selected based on `train_Yvar`. + If `train_Yvar` is None, a standard `GaussianLikelihood` with inferred + noise level is used. Otherwise, a FixedNoiseGaussianLikelihood is used. + context_cat_feature: (n_contexts x k) one-hot encoded context + features. Rows are ordered by context indices, where k is the + number of categorical variables. If None, task indices will + be used and k = 1. + context_emb_feature: (n_contexts x m) pre-given continuous + embedding features. Rows are ordered by context indices. + embs_dim_list: Embedding dimension for each categorical variable. + The length equals k. If None, the embedding dimension is set to 1 + for each categorical variable. + output_tasks: A list of task indices for which to compute model + outputs for. If omitted, return outputs for all task indices. + all_tasks: By default, multi-task GPs infer the list of all tasks from + the task features in `train_X`. This is an experimental feature that + enables creation of multi-task GPs with tasks that don't appear in the + training data. Note that when a task is not observed, the corresponding + task covariance will heavily depend on random initialization and may + behave unexpectedly. + outcome_transform: An outcome transform that is applied to the + training data during instantiation and to the posterior during + inference (that is, the `Posterior` obtained by calling + `.posterior` on the model will be on the original scale). We use a + `Standardize` transform if no `outcome_transform` is specified. + Pass down `None` to use no outcome transform. + input_transform: An input transform that is applied in the model's + forward pass. + """ + super().__init__( + train_X=train_X, + train_Y=train_Y, + task_feature=task_feature, + train_Yvar=train_Yvar, + mean_module=mean_module, + covar_module=covar_module, + likelihood=likelihood, + output_tasks=output_tasks, + all_tasks=all_tasks, + outcome_transform=outcome_transform, + input_transform=input_transform, + ) + self.device = train_X.device + if all_tasks is None: + all_tasks_tensor = train_X[:, task_feature].unique() + self.all_tasks = all_tasks_tensor.to(dtype=torch.long).tolist() + else: + all_tasks_tensor = torch.tensor(all_tasks, dtype=torch.long) + self.all_tasks = all_tasks + self.all_tasks.sort() # These are the context indices. + + if context_cat_feature is None: + context_cat_feature = all_tasks_tensor.unsqueeze(-1).to(device=self.device) + self.context_cat_feature: Tensor = ( + context_cat_feature # row indices = context indices + ) + self.context_emb_feature = context_emb_feature + + # construct emb_dims based on categorical features + if embs_dim_list is None: + # set embedding_dim = 1 for each categorical variable + embs_dim_list = [1 for _i in range(context_cat_feature.size(1))] + n_embs = sum(embs_dim_list) + self.emb_dims = [ + (len(context_cat_feature[:, i].unique()), embs_dim_list[i]) + for i in range(context_cat_feature.size(1)) + ] + # contruct embedding layer: need to handle multiple categorical features + self.emb_layers = ModuleList( + [ + torch.nn.Embedding(num_embeddings=x, embedding_dim=y, max_norm=1.0) + for x, y in self.emb_dims + ] + ) + self.task_covar_module_base = RBFKernel( + ard_num_dims=n_embs, + lengthscale_constraint=Interval( + 0.0, 2.0, transform=None, initial_value=1.0 + ), + ) + self.to(train_X) + + def _eval_context_covar(self) -> LinearOperator: + """Obtain the context covariance matrix, a linear operator + with shape (num_contexts x num_contexts). + + This first generates the embedding features for all contexts, + then evaluates the task covariance matrix with those embeddings + to get the task covariance matrix. + """ + all_embs = self._task_embeddings() + return self.task_covar_module_base(all_embs) + + def _task_embeddings(self) -> Tensor: + """Generate embedding features for all contexts.""" + embeddings = [ + emb_layer( + self.context_cat_feature[:, i].to( + dtype=torch.long, device=self.device + ) # pyre-ignore + ) + for i, emb_layer in enumerate(self.emb_layers) + ] + embeddings = torch.cat(embeddings, dim=1) + + # add given embeddings if any + if self.context_emb_feature is not None: + embeddings = torch.cat( + [embeddings, self.context_emb_feature.to(self.device)], + dim=1, # pyre-ignore + ) + return embeddings + +
+[docs] + def task_covar_module(self, task_idcs: Tensor) -> Tensor: + r"""Compute the task covariance matrix for a given tensor of + task / context indices. + + Args: + task_idcs: Task index tensor of shape (n x 1) or (b x n x 1). + + Returns: + Task covariance matrix of shape (b x n x n). + """ + # This is a tensor of shape (num_tasks x num_tasks). + covar_matrix = self._eval_context_covar().to_dense() + # Here, we index into the base covar matrix to extract + # the rows & columns corresponding to the task indices. + # First indexing operation picks the rows for each index in + # task indices (results in b x n x num_tasks). We then transpose + # to make the picked rows into columns (b x num_tasks x n), and + # pick the rows again to result in the final covariance matrix. + # The result is a symmetric tensor of shape (b x n x n). + # An alternative implementation could pick the columns directly + # by moving the transpose operation into the index of gather, + # however, this does not seem to make any noticeable difference. + base_idx = task_idcs.squeeze(-1) + expanded_idx = task_idcs.expand( + *([-1] * (task_idcs.dim() - 1)), task_idcs.shape[-2] + ) + return ( + covar_matrix[base_idx].transpose(-1, -2).gather(index=expanded_idx, dim=-2) + )
+ + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset | MultiTaskDataset, + task_feature: int, + output_tasks: list[int] | None = None, + context_cat_feature: Tensor | None = None, + context_emb_feature: Tensor | None = None, + embs_dim_list: list[int] | None = None, + **kwargs, + ) -> dict[str, Any]: + r"""Construct `Model` keyword arguments from a dataset and other args. + + Args: + training_data: A `SupervisedDataset` or a `MultiTaskDataset`. + task_feature: Column index of embedded task indicator features. + output_tasks: A list of task indices for which to compute model + outputs for. If omitted, return outputs for all task indices. + context_cat_feature: (n_contexts x k) one-hot encoded context + features. Rows are ordered by context indices, where k is the + number of categorical variables. If None, task indices will + be used and k = 1. + context_emb_feature: (n_contexts x m) pre-given continuous + embedding features. Rows are ordered by context indices. + embs_dim_list: Embedding dimension for each categorical variable. + The length equals k. If None, the embedding dimension is set to 1 + for each categorical variable. + """ + base_inputs = super().construct_inputs( + training_data=training_data, + task_feature=task_feature, + output_tasks=output_tasks, + **kwargs, + ) + if context_cat_feature is not None: + base_inputs["context_cat_feature"] = context_cat_feature + if context_emb_feature is not None: + base_inputs["context_emb_feature"] = context_emb_feature + if embs_dim_list is not None: + base_inputs["embs_dim_list"] = embs_dim_list + return base_inputs
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/converter.html b/website-old/pages/api/_modules/botorch/models/converter.html new file mode 100644 index 0000000000..f257b03387 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/converter.html @@ -0,0 +1,530 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.converter

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Utilities for converting between different models.
+"""
+
+from __future__ import annotations
+
+import warnings
+from copy import deepcopy
+
+import torch
+from botorch.exceptions import UnsupportedError
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models import SingleTaskGP
+from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP
+from botorch.models.gp_regression_mixed import MixedSingleTaskGP
+from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood
+from torch import Tensor
+from torch.nn import Module, ModuleList
+
+DEPRECATION_MESSAGE = (
+    "Model converter code is deprecated and will be removed in v0.13 release. "
+    "Its correct behavior is dependent on some assumptions about model priors "
+    "that do not always hold. Use it at your own risk! See "
+    "https://github.com/cornellius-gp/gpytorch/issues/2550."
+)
+
+
+def _get_module(module: Module, name: str) -> Module:
+    """Recursively get a sub-module from a module.
+
+    Args:
+        module: A `torch.nn.Module`.
+        name: The name of the submodule to return, in the form of a period-delinated
+            string: `sub_module.subsub_module.[...].leaf_module`.
+
+    Returns:
+        The requested sub-module.
+
+    Example:
+        >>> gp = SingleTaskGP(train_X, train_Y)
+        >>> noise_prior = _get_module(gp, "likelihood.noise_covar.noise_prior")
+    """
+    current = module
+    if name != "":
+        for a in name.split("."):
+            current = getattr(current, a)
+    return current
+
+
+def _check_compatibility(models: ModuleList) -> None:
+    """Check if the submodels of a ModelListGP are compatible with the converter."""
+    # Check that all submodules are of the same type.
+    for modn, mod in models[0].named_modules():
+        mcls = mod.__class__
+        if not all(isinstance(_get_module(m, modn), mcls) for m in models[1:]):
+            raise UnsupportedError(
+                "Sub-modules must be of the same type across models."
+            )
+        if "prior" in modn and len(mod.state_dict()) == 0:
+            warnings.warn(  # pragma no cover -- not tested after GPyTorch 2551.
+                "Model converter cannot verify compatibility of GPyTorch priors "
+                "that do not register their parameters as buffers. If the prior "
+                "is different than the default prior set by the model constructor "
+                "this may not work correctly. Use it at your own risk! See "
+                "https://github.com/cornellius-gp/gpytorch/issues/2550.",
+                BotorchWarning,
+                stacklevel=3,
+            )
+
+    # Check that each model is a BatchedMultiOutputGPyTorchModel.
+    if not all(isinstance(m, BatchedMultiOutputGPyTorchModel) for m in models):
+        raise UnsupportedError(
+            "All models must be of type BatchedMultiOutputGPyTorchModel."
+        )
+
+    # TODO: Add support for custom likelihoods.
+    if any(getattr(m, "_is_custom_likelihood", False) for m in models):
+        raise NotImplementedError(
+            "Conversion of models with custom likelihoods is currently unsupported."
+        )
+
+    # TODO: Add support for outcome transforms.
+    if any(getattr(m, "outcome_transform", None) is not None for m in models):
+        raise UnsupportedError(
+            "Conversion of models with outcome transforms is unsupported. "
+            "To fix this error, explicitly pass `outcome_transform=None`.",
+        )
+
+    # check that each model is single-output
+    if not all(m._num_outputs == 1 for m in models):
+        raise UnsupportedError("All models must be single-output.")
+
+    # check that training inputs are the same
+    if not all(
+        torch.equal(ti, tj)
+        for m in models[1:]
+        for ti, tj in zip(models[0].train_inputs, m.train_inputs)
+    ):
+        raise UnsupportedError("training inputs must agree for all sub-models.")
+
+    # check that there are no batched input transforms
+    default_size = torch.Size([])
+    for m in models:
+        if hasattr(m, "input_transform"):
+            if (
+                m.input_transform is not None
+                and len(getattr(m.input_transform, "batch_shape", default_size)) != 0
+            ):
+                raise UnsupportedError("Batched input_transforms are not supported.")
+
+    # check that all models have the same input transforms
+    if any(hasattr(m, "input_transform") for m in models):
+        if not all(
+            m.input_transform.equals(models[0].input_transform) for m in models[1:]
+        ):
+            raise UnsupportedError("All models must have the same input_transforms.")
+
+
+
+[docs] +def model_list_to_batched(model_list: ModelListGP) -> BatchedMultiOutputGPyTorchModel: + """Convert a ModelListGP to a BatchedMultiOutputGPyTorchModel. + + Args: + model_list: The `ModelListGP` to be converted to the appropriate + `BatchedMultiOutputGPyTorchModel`. All sub-models must be of the same + type and have the shape (batch shape and number of training inputs). + + Returns: + The model converted into a `BatchedMultiOutputGPyTorchModel`. + + Example: + >>> list_gp = ModelListGP(gp1, gp2) + >>> batch_gp = model_list_to_batched(list_gp) + """ + warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2) + was_training = model_list.training + model_list.train() + models = model_list.models + _check_compatibility(models) + + # if the list has only one model, we can just return a copy of that + if len(models) == 1: + return deepcopy(models[0]) + + # construct inputs + train_X = deepcopy(models[0].train_inputs[0]) + train_Y = torch.stack([m.train_targets.clone() for m in models], dim=-1) + kwargs = {"train_X": train_X, "train_Y": train_Y} + if isinstance(models[0].likelihood, FixedNoiseGaussianLikelihood): + kwargs["train_Yvar"] = torch.stack( + [m.likelihood.noise_covar.noise.clone() for m in models], dim=-1 + ) + if isinstance(models[0], SingleTaskMultiFidelityGP): + init_args = models[0]._init_args + if not all( + v == m._init_args[k] for m in models[1:] for k, v in init_args.items() + ): + raise UnsupportedError("All models must have the same fidelity parameters.") + kwargs.update(init_args) + + # add batched kernel, except if the model type is SingleTaskMultiFidelityGP, + # which does not have a `covar_module` + if not isinstance(models[0], SingleTaskMultiFidelityGP): + batch_length = len(models) + covar_module = _batched_kernel(models[0].covar_module, batch_length) + kwargs["covar_module"] = covar_module + # SingleTaskGP uses a default outcome transforms while this converter doesn't + # support outcome transforms. We need to explicitly pass down `None` to make + # sure no outcome transform is being used. + if isinstance(models[0], SingleTaskGP): + kwargs["outcome_transform"] = None + + # construct the batched GP model + input_transform = getattr(models[0], "input_transform", None) + batch_gp = models[0].__class__(input_transform=input_transform, **kwargs) + adjusted_batch_keys, non_adjusted_batch_keys = _get_adjusted_batch_keys( + batch_state_dict=batch_gp.state_dict(), input_transform=input_transform + ) + input_batch_dims = len(models[0]._input_batch_shape) + + # ensure scalars agree (TODO: Allow different priors for different outputs) + for n in non_adjusted_batch_keys: + v0 = _get_module(models[0], n) + if not all(torch.equal(_get_module(m, n), v0) for m in models[1:]): + raise UnsupportedError("All scalars must have the same value.") + + # ensure dimensions of all tensors agree + for n in adjusted_batch_keys: + shape0 = _get_module(models[0], n).shape + if not all(_get_module(m, n).shape == shape0 for m in models[1:]): + raise UnsupportedError("All tensors must have the same shape.") + + # now construct the batched state dict + non_adjusted_batch_state_dict = { + s: p.clone() + for s, p in models[0].state_dict().items() + if s in non_adjusted_batch_keys + } + adjusted_batch_state_dict = { + t: ( + torch.stack( + [m.state_dict()[t].clone() for m in models], dim=input_batch_dims + ) + if "active_dims" not in t + else models[0].state_dict()[t].clone() + ) + for t in adjusted_batch_keys + } + batch_state_dict = {**non_adjusted_batch_state_dict, **adjusted_batch_state_dict} + + # load the state dict into the new model + batch_gp.load_state_dict(batch_state_dict) + + return batch_gp.train(mode=was_training)
+ + + +def _batched_kernel(kernel, batch_length: int): + """Adds a batch dimension of size `batch_length` to all non-scalar + Tensor parameters that govern the kernel function `kernel`. + NOTE: prior or constraint parameters are excluded from batching. + """ + # copy just in case there are non-tensor parameters that are passed by reference + kernel = deepcopy(kernel) + search_str = "raw_outputscale" + for key, attr in kernel.state_dict().items(): + if isinstance(attr, Tensor) and ( + attr.ndim > 0 or (search_str == key.rpartition(".")[-1]) + ): + attr = attr.unsqueeze(0).expand(batch_length, *attr.shape).clone() + set_attribute(kernel, key, torch.nn.Parameter(attr)) + return kernel + + +# two helper functions for `batched_kernel` +# like `setattr` and `getattr` for object hierarchies +
+[docs] +def set_attribute(obj, attr: str, val): + """Like `setattr` but works with hierarchical attribute specification. + E.g. if obj=Zoo(), and attr="tiger.age", set_attribute(obj, attr, 3), + would set the Zoo's tiger's age to three. + """ + path_to_leaf, _, attr_name = attr.rpartition(".") + leaf = get_attribute(obj, path_to_leaf) if path_to_leaf else obj + setattr(leaf, attr_name, val)
+ + + +
+[docs] +def get_attribute(obj, attr: str): + """Like `getattr` but works with hierarchical attribute specification. + E.g. if obj=Zoo(), and attr="tiger.age", get_attribute(obj, attr), + would return the Zoo's tiger's age. + """ + attr_names = attr.split(".") + while attr_names: + obj = getattr(obj, attr_names.pop(0)) + return obj
+ + + +
+[docs] +def batched_to_model_list(batch_model: BatchedMultiOutputGPyTorchModel) -> ModelListGP: + """Convert a BatchedMultiOutputGPyTorchModel to a ModelListGP. + + Args: + batch_model: The `BatchedMultiOutputGPyTorchModel` to be converted to a + `ModelListGP`. + + Returns: + The model converted into a `ModelListGP`. + + Example: + >>> train_X = torch.rand(5, 2) + >>> train_Y = torch.rand(5, 2) + >>> batch_gp = SingleTaskGP(train_X, train_Y) + >>> list_gp = batched_to_model_list(batch_gp) + """ + warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2) + was_training = batch_model.training + batch_model.train() + if isinstance(batch_model, MixedSingleTaskGP): + raise NotImplementedError( + "Conversion of MixedSingleTaskGP is currently not supported." + ) + input_transform = getattr(batch_model, "input_transform", None) + outcome_transform = getattr(batch_model, "outcome_transform", None) + batch_sd = batch_model.state_dict() + + adjusted_batch_keys, non_adjusted_batch_keys = _get_adjusted_batch_keys( + batch_state_dict=batch_sd, + input_transform=input_transform, + outcome_transform=outcome_transform, + ) + input_bdims = len(batch_model._input_batch_shape) + + models = [] + + for i in range(batch_model._num_outputs): + non_adjusted_batch_sd = { + s: batch_sd[s].clone() for s in non_adjusted_batch_keys + } + adjusted_batch_sd = { + t: ( + batch_sd[t].select(input_bdims, i).clone() + if "active_dims" not in t + else batch_sd[t].clone() + ) + for t in adjusted_batch_keys + } + sd = {**non_adjusted_batch_sd, **adjusted_batch_sd} + kwargs = { + "train_X": batch_model.train_inputs[0].select(input_bdims, i).clone(), + "train_Y": batch_model.train_targets.select(input_bdims, i) + .clone() + .unsqueeze(-1), + } + if isinstance(batch_model.likelihood, FixedNoiseGaussianLikelihood): + noise_covar = batch_model.likelihood.noise_covar + kwargs["train_Yvar"] = ( + noise_covar.noise.select(input_bdims, i).clone().unsqueeze(-1) + ) + if isinstance(batch_model, SingleTaskMultiFidelityGP): + kwargs.update(batch_model._init_args) + # NOTE: Adding outcome transform to kwargs to avoid the multiple + # values for same kwarg issue with SingleTaskMultiFidelityGP. + if outcome_transform is not None: + octf = outcome_transform.subset_output(idcs=[i]) + kwargs["outcome_transform"] = octf + # Update the outcome transform state dict entries. + sd = { + **sd, + **{"outcome_transform." + k: v for k, v in octf.state_dict().items()}, + } + else: + kwargs["outcome_transform"] = None + model = batch_model.__class__(input_transform=input_transform, **kwargs) + model.load_state_dict(sd) + models.append(model) + + return ModelListGP(*models).train(mode=was_training)
+ + + +
+[docs] +def batched_multi_output_to_single_output( + batch_mo_model: BatchedMultiOutputGPyTorchModel, +) -> BatchedMultiOutputGPyTorchModel: + """Convert a model from batched multi-output to a batched single-output. + + Note: the underlying GPyTorch GP does not change. The GPyTorch GP's batch_shape + (referred to as `_aug_batch_shape`) is still `_input_batch_shape x num_outputs`. + The only things that change are the attributes of the + BatchedMultiOutputGPyTorchModel that are responsible the internal accounting of + the number of outputs: namely, num_outputs, _input_batch_shape, and + _aug_batch_shape. + Initially for the batched MO models these are: `num_outputs = m`, + `_input_batch_shape = train_X.batch_shape`, and + `_aug_batch_shape = train_X.batch_shape + torch.Size([num_outputs])`. + In the new SO model, these are: `num_outputs = 1`, + `_input_batch_shape = train_X.batch_shape + torch.Size([num_outputs])`, + and `_aug_batch_shape = train_X.batch_shape + torch.Size([num_outputs])`. + + This is a (hopefully) temporary measure until multi-output MVNs with + independent outputs have better support in GPyTorch (see + https://github.com/cornellius-gp/gpytorch/pull/1083). + + Args: + batched_mo_model: The BatchedMultiOutputGPyTorchModel + + Returns: + The model converted into a batch single-output model. + + Example: + >>> train_X = torch.rand(5, 2) + >>> train_Y = torch.rand(5, 2) + >>> batch_mo_gp = SingleTaskGP(train_X, train_Y, outcome_transform=None) + >>> batch_so_gp = batched_multi_output_to_single_output(batch_mo_gp) + """ + warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2) + was_training = batch_mo_model.training + batch_mo_model.train() + if not isinstance(batch_mo_model, BatchedMultiOutputGPyTorchModel): + raise UnsupportedError("Only BatchedMultiOutputGPyTorchModels are supported.") + # TODO: Add support for custom likelihoods. + elif getattr(batch_mo_model, "_is_custom_likelihood", False): + raise NotImplementedError( + "Conversion of models with custom likelihoods is currently unsupported." + ) + input_transform = getattr(batch_mo_model, "input_transform", None) + batch_sd = batch_mo_model.state_dict() + + # TODO: add support for outcome transforms. + if hasattr(batch_mo_model, "outcome_transform"): + raise NotImplementedError( + "Converting batched multi-output models with outcome transforms " + "is not currently supported." + ) + + kwargs = { + "train_X": batch_mo_model.train_inputs[0].clone(), + "train_Y": batch_mo_model.train_targets.clone().unsqueeze(-1), + } + if isinstance(batch_mo_model.likelihood, FixedNoiseGaussianLikelihood): + noise_covar = batch_mo_model.likelihood.noise_covar + kwargs["train_Yvar"] = noise_covar.noise.clone().unsqueeze(-1) + if isinstance(batch_mo_model, SingleTaskMultiFidelityGP): + kwargs.update(batch_mo_model._init_args) + # SingleTaskGP uses a default outcome transforms while this converter doesn't + # support outcome transforms. We need to explicitly pass down `None` to make + # sure no outcome transform is being used. + if isinstance(batch_mo_model, SingleTaskGP): + kwargs["outcome_transform"] = None + + single_outcome_model = batch_mo_model.__class__( + input_transform=input_transform, **kwargs + ) + single_outcome_model.load_state_dict(batch_sd) + return single_outcome_model.train(mode=was_training)
+ + + +def _get_adjusted_batch_keys( + batch_state_dict: dict[str, Tensor], + input_transform: InputTransform | None, + outcome_transform: OutcomeTransform | None = None, +) -> tuple[set[str], set[str]]: + r"""Group the keys based on whether the value requires batch shape changes. + + Args: + batch_state_dict: The state dict of the batch model. + input_transform: The input transform. + outcome_transform: The outcome transform. + + Returns: + A two-element tuple containing: + - The keys of the parameters/buffers that require a batch shape adjustment. + - The keys of the parameters/buffers that do not require a batch shape + adjustment. + """ + # These are the names of the params/buffers that need their batch shape adjusted. + adjusted_batch_keys = {n for n, p in batch_state_dict.items() if len(p.shape) > 0} + # Don't modify transform buffers, so add them to non-adjusted set and remove + # them from tensors. + for transform, transform_type in [ + (input_transform, "input_transform."), + (outcome_transform, "outcome_transform."), + ]: + if transform is not None: + transform_keys = { + transform_type + n for n, p in transform.state_dict().items() + } + adjusted_batch_keys = adjusted_batch_keys - transform_keys + # These are the names of the parameters/buffers that don't need their + # batch shape adjusted. + non_adjusted_batch_keys = set(batch_state_dict) - adjusted_batch_keys + return adjusted_batch_keys, non_adjusted_batch_keys +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/cost.html b/website-old/pages/api/_modules/botorch/models/cost.html new file mode 100644 index 0000000000..f0752cad6d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/cost.html @@ -0,0 +1,193 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.cost

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Cost models to be used with multi-fidelity optimization.
+
+Cost are useful for defining known cost functions when the cost of an evaluation
+is heterogeneous in fidelity. For a full worked example, see the
+`tutorial <https://botorch.org/tutorials/multi_fidelity_bo>`_ on continuous
+multi-fidelity Bayesian Optimization.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.models.deterministic import DeterministicModel
+from torch import Tensor
+
+
+
+[docs] +class AffineFidelityCostModel(DeterministicModel): + r"""Deterministic, affine cost model operating on fidelity parameters. + + For each (q-batch) element of a candidate set `X`, this module computes a + cost of the form + + cost = fixed_cost + sum_j weights[j] * X[fidelity_dims[j]] + + For a full worked example, see the + `tutorial <https://botorch.org/tutorials/multi_fidelity_bo>`_ on continuous + multi-fidelity Bayesian Optimization. + + Example: + >>> from botorch.models import AffineFidelityCostModel + >>> from botorch.acquisition.cost_aware import InverseCostWeightedUtility + >>> cost_model = AffineFidelityCostModel( + >>> fidelity_weights={6: 1.0}, fixed_cost=5.0 + >>> ) + >>> cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) + """ + + def __init__( + self, + fidelity_weights: dict[int, float] | None = None, + fixed_cost: float = 0.01, + ) -> None: + r""" + Args: + fidelity_weights: A dictionary mapping a subset of columns of `X` + (the fidelity parameters) to its associated weight in the + affine cost expression. If omitted, assumes that the last + column of `X` is the fidelity parameter with a weight of 1.0. + fixed_cost: The fixed cost of running a single candidate point (i.e. + an element of a q-batch). + """ + if fidelity_weights is None: + fidelity_weights = {-1: 1.0} + super().__init__() + self.fidelity_dims = sorted(fidelity_weights) + self.fixed_cost = fixed_cost + weights = torch.tensor([fidelity_weights[i] for i in self.fidelity_dims]) + self.register_buffer("weights", weights) + self._num_outputs = 1 + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the cost on a candidate set X. + + Computes a cost of the form + + cost = fixed_cost + sum_j weights[j] * X[fidelity_dims[j]] + + for each element of the q-batch + + Args: + X: A `batch_shape x q x d'`-dim tensor of candidate points. + + Returns: + A `batch_shape x q x 1`-dim tensor of costs. + """ + # TODO: Consider different aggregation (i.e. max) across q-batch + lin_cost = torch.einsum( + "...f,f", X[..., self.fidelity_dims], self.weights.to(X) + ) + return self.fixed_cost + lin_cost.unsqueeze(-1)
+
+ + + +
+[docs] +class FixedCostModel(DeterministicModel): + r"""Deterministic, fixed cost model. + + For each (q-batch) element of a candidate set `X`, this module computes a + fixed cost per objective. + """ + + def __init__( + self, + fixed_cost: Tensor, + ) -> None: + r""" + Args: + fixed_cost: A `m`-dim tensor containing the fixed cost of evaluating each + objective. + """ + super().__init__() + self.register_buffer("fixed_cost", fixed_cost) + self._num_outputs = fixed_cost.shape[-1] + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the cost on a candidate set X. + + Computes the fixed cost of evaluating each objective for each element + of the q-batch. + + Args: + X: A `batch_shape x q x d'`-dim tensor of candidate points. + + Returns: + A `batch_shape x q x m`-dim tensor of costs. + """ + view_shape = [1] * (X.ndim - 1) + [self._num_outputs] + expand_shape = X.shape[:-1] + torch.Size([self._num_outputs]) + return self.fixed_cost.view(view_shape).expand(expand_shape)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/deterministic.html b/website-old/pages/api/_modules/botorch/models/deterministic.html new file mode 100644 index 0000000000..0cbe8aa50c --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/deterministic.html @@ -0,0 +1,306 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.deterministic

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Deterministic Models: Simple wrappers that allow the usage of deterministic
+mappings via the BoTorch Model and Posterior APIs.
+
+Deterministic models are useful for expressing known input-output relationships
+within the BoTorch Model API. This is useful e.g. for multi-objective
+optimization with known objective functions (e.g. the number of parameters of a
+Neural Network in the context of Neural Architecture Search is usually a known
+function of the architecture configuration), or to encode cost functions for
+cost-aware acquisition utilities. Cost-aware optimization is desirable when
+evaluations have a cost that is heterogeneous, either in the inputs `X` or in a
+particular fidelity parameter that directly encodes the fidelity of the
+observation. `GenericDeterministicModel` supports arbitrary deterministic
+functions, while `AffineFidelityCostModel` is a particular cost model for
+multi-fidelity optimization. Other use cases of deterministic models include
+representing approximate GP sample paths, e.g. Matheron paths obtained
+with `get_matheron_path_model`, which allows them to be substituted in acquisition
+functions or in other places where a `Model` is expected.
+"""
+
+from __future__ import annotations
+
+from abc import abstractmethod
+from collections.abc import Callable
+
+import torch
+from botorch.models.ensemble import EnsembleModel
+from botorch.models.model import Model
+from torch import Tensor
+
+
+
+[docs] +class DeterministicModel(EnsembleModel): + """Abstract base class for deterministic models.""" + +
+[docs] + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Compute the (deterministic) model output at X. + + Args: + X: A `batch_shape x n x d`-dim input tensor `X`. + + Returns: + A `batch_shape x n x m`-dimensional output tensor (the outcome + dimension `m` must be explicit if `m=1`). + """ + pass # pragma: no cover
+ + + def _forward(self, X: Tensor) -> Tensor: + r"""Compatibilizes the `DeterministicModel` with `EnsemblePosterior`""" + return self.forward(X=X).unsqueeze(-3)
+ + + +
+[docs] +class GenericDeterministicModel(DeterministicModel): + r"""A generic deterministic model constructed from a callable. + + Example: + >>> f = lambda x: x.sum(dim=-1, keep_dims=True) + >>> model = GenericDeterministicModel(f) + """ + + def __init__(self, f: Callable[[Tensor], Tensor], num_outputs: int = 1) -> None: + r""" + Args: + f: A callable mapping a `batch_shape x n x d`-dim input tensor `X` + to a `batch_shape x n x m`-dimensional output tensor (the + outcome dimension `m` must be explicit, even if `m=1`). + num_outputs: The number of outputs `m`. + """ + super().__init__() + self._f = f + self._num_outputs = num_outputs + +
+[docs] + def subset_output(self, idcs: list[int]) -> GenericDeterministicModel: + r"""Subset the model along the output dimension. + + Args: + idcs: The output indices to subset the model to. + + Returns: + The current model, subset to the specified output indices. + """ + + def f_subset(X: Tensor) -> Tensor: + return self._f(X)[..., idcs] + + return self.__class__(f=f_subset, num_outputs=len(idcs))
+ + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r"""Compute the (deterministic) model output at X. + + Args: + X: A `batch_shape x n x d`-dim input tensor `X`. + + Returns: + A `batch_shape x n x m`-dimensional output tensor. + """ + return self._f(X)
+
+ + + +
+[docs] +class AffineDeterministicModel(DeterministicModel): + r"""An affine deterministic model.""" + + def __init__(self, a: Tensor, b: Tensor | float = 0.01) -> None: + r"""Affine deterministic model from weights and offset terms. + + A simple model of the form + + y[..., m] = b[m] + sum_{i=1}^d a[i, m] * X[..., i] + + Args: + a: A `d x m`-dim tensor of linear weights, where `m` is the number + of outputs (must be explicit if `m=1`) + b: The affine (offset) term. Either a float (for single-output + models or if the offset is shared), or a `m`-dim tensor (with + different offset values for for the `m` different outputs). + """ + if not a.ndim == 2: + raise ValueError("a must be two-dimensional") + if not torch.is_tensor(b): + b = torch.tensor([b]) + if not b.ndim == 1: + raise ValueError("b nust be one-dimensional") + super().__init__() + self.register_buffer("a", a) + self.register_buffer("b", b.expand(a.size(-1))) + self._num_outputs = a.size(-1) + +
+[docs] + def subset_output(self, idcs: list[int]) -> AffineDeterministicModel: + r"""Subset the model along the output dimension. + + Args: + idcs: The output indices to subset the model to. + + Returns: + The current model, subset to the specified output indices. + """ + a_sub = self.a.detach()[..., idcs].clone() + b_sub = self.b.detach()[..., idcs].clone() + return self.__class__(a=a_sub, b=b_sub)
+ + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + return self.b + torch.einsum("...d,dm", X, self.a)
+
+ + + +
+[docs] +class PosteriorMeanModel(DeterministicModel): + """A deterministic model that always returns the posterior mean.""" + + def __init__(self, model: Model) -> None: + r""" + Args: + model: The base model. + """ + super().__init__() + self.model = model + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + return self.model.posterior(X).mean
+
+ + + +
+[docs] +class FixedSingleSampleModel(DeterministicModel): + r""" + A deterministic model defined by a single sample `w`. + + Given a base model `f` and a fixed sample `w`, the model always outputs + + y = f_mean(x) + f_stddev(x) * w + + We assume the outcomes are uncorrelated here. + """ + + def __init__( + self, + model: Model, + w: Tensor | None = None, + dim: int | None = None, + jitter: float | None = 1e-8, + dtype: torch.dtype | None = None, + device: torch.dtype | None = None, + ) -> None: + r""" + Args: + model: The base model. + w: A 1-d tensor with length model.num_outputs. + If None, draw it from a standard normal distribution. + dim: dimensionality of w. + If None and w is not provided, draw w samples of size model.num_outputs. + jitter: jitter value to be added for numerical stability, 1e-8 by default. + dtype: dtype for w if specified + device: device for w if specified + """ + super().__init__() + self.model = model + self._num_outputs = model.num_outputs + self.jitter = jitter + if w is None: + self.w = ( + torch.randn(model.num_outputs, dtype=dtype, device=device) + if dim is None + else torch.randn(dim, dtype=dtype, device=device) + ) + else: + self.w = w + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + post = self.model.posterior(X) + return post.mean + torch.sqrt(post.variance + self.jitter) * self.w.to(X)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/ensemble.html b/website-old/pages/api/_modules/botorch/models/ensemble.html new file mode 100644 index 0000000000..e598739a3f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/ensemble.html @@ -0,0 +1,156 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.ensemble

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Ensemble Models: Simple wrappers that allow the usage of ensembles
+via the BoTorch Model and Posterior APIs.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import Any
+
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.model import Model
+from botorch.posteriors.ensemble import EnsemblePosterior
+from torch import Tensor
+
+
+
+[docs] +class EnsembleModel(Model, ABC): + """Abstract base class for ensemble models.""" + +
+[docs] + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Compute the (ensemble) model output at X. + + Args: + X: A `batch_shape x n x d`-dim input tensor `X`. + + Returns: + A `batch_shape x s x n x m`-dimensional output tensor where + `s` is the size of the ensemble. + """ + pass # pragma: no cover
+ + + def _forward(self, X: Tensor) -> Tensor: + return self.forward(X=X) + + @property + def num_outputs(self) -> int: + r"""The number of outputs of the model.""" + return self._num_outputs + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + posterior_transform: PosteriorTransform | None = None, + **kwargs: Any, + ) -> EnsemblePosterior: + r"""Compute the ensemble posterior at X. + + Args: + X: A `batch_shape x q x d`-dim input tensor `X`. + output_indices: A list of indices, corresponding to the outputs over + which to compute the posterior. If omitted, computes the posterior + over all model outputs. + posterior_transform: An optional PosteriorTransform. + + Returns: + An `EnsemblePosterior` object, representing `batch_shape` joint + posteriors over `n` points and the outputs selected by `output_indices`. + """ + # Apply the input transforms in `eval` mode. + self.eval() + X = self.transform_inputs(X) + # Note: we use a Tensor instance check so that `observation_noise = True` + # just gets ignored. This avoids having to do a bunch of case distinctions + # when using a ModelList. + if isinstance(kwargs.get("observation_noise"), Tensor): + # TODO: Consider returning an MVN here instead + raise UnsupportedError("Ensemble models do not support observation noise.") + values = self._forward(X) + # NOTE: The `outcome_transform` `untransform`s the predictions rather than the + # `posterior` (as is done in GP models). This is more general since it works + # even if the transform doesn't support `untransform_posterior`. + if hasattr(self, "outcome_transform"): + values, _ = self.outcome_transform.untransform(values) + if output_indices is not None: + values = values[..., output_indices] + posterior = EnsemblePosterior(values=values) + if posterior_transform is not None: + return posterior_transform(posterior) + else: + return posterior
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/fully_bayesian.html b/website-old/pages/api/_modules/botorch/models/fully_bayesian.html new file mode 100644 index 0000000000..103f6bad74 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/fully_bayesian.html @@ -0,0 +1,709 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.fully_bayesian

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+r"""Gaussian Process Regression models with fully Bayesian inference.
+
+Fully Bayesian models use Bayesian inference over model hyperparameters, such
+as lengthscales and noise variance, learning a posterior distribution for the
+hyperparameters using the No-U-Turn-Sampler (NUTS). This is followed by
+sampling a small set of hyperparameters (often ~16) from the posterior
+that we will use for model predictions and for computing acquisition function
+values. By contrast, our “standard” models (e.g.
+`SingleTaskGP`) learn only a single best value for each hyperparameter using
+MAP. The fully Bayesian method generally results in a better and more
+well-calibrated model, but is more computationally intensive. For a full
+description, see [Eriksson2021saasbo].
+
+We use a lightweight PyTorch implementation of a Matern-5/2 kernel as there are
+some performance issues with running NUTS on top of standard GPyTorch models.
+The resulting hyperparameter samples are loaded into a batched GPyTorch model
+after fitting.
+
+References:
+
+.. [Eriksson2021saasbo]
+    D. Eriksson, M. Jankowiak. High-Dimensional Bayesian Optimization
+    with Sparse Axis-Aligned Subspaces. Proceedings of the Thirty-
+    Seventh Conference on Uncertainty in Artificial Intelligence, 2021.
+"""
+
+import math
+from abc import abstractmethod
+from collections.abc import Mapping
+from typing import Any
+
+import pyro
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from botorch.models.utils import validate_input_scaling
+from botorch.models.utils.gpytorch_modules import MIN_INFERRED_NOISE_LEVEL
+from botorch.posteriors.fully_bayesian import GaussianMixturePosterior, MCMC_DIM
+from gpytorch.constraints import GreaterThan
+from gpytorch.distributions.multivariate_normal import MultivariateNormal
+from gpytorch.kernels import MaternKernel, ScaleKernel
+from gpytorch.kernels.kernel import dist, Kernel
+from gpytorch.likelihoods.gaussian_likelihood import (
+    FixedNoiseGaussianLikelihood,
+    GaussianLikelihood,
+)
+from gpytorch.likelihoods.likelihood import Likelihood
+from gpytorch.means.constant_mean import ConstantMean
+from gpytorch.means.mean import Mean
+from gpytorch.models.exact_gp import ExactGP
+from pyro.ops.integrator import register_exception_handler
+from torch import Tensor
+
+
+_sqrt5 = math.sqrt(5)
+
+
+def _handle_torch_linalg(exception: Exception) -> bool:
+    return type(exception) is torch.linalg.LinAlgError
+
+
+def _handle_valerr_in_dist_init(exception: Exception) -> bool:
+    if type(exception) is not ValueError:
+        return False
+    return "satisfy the constraint PositiveDefinite()" in str(exception)
+
+
+register_exception_handler("torch_linalg", _handle_torch_linalg)
+register_exception_handler("valerr_in_dist_init", _handle_valerr_in_dist_init)
+
+
+
+[docs] +def matern52_kernel(X: Tensor, lengthscale: Tensor) -> Tensor: + """Matern-5/2 kernel.""" + dist = compute_dists(X=X, lengthscale=lengthscale) + sqrt5_dist = _sqrt5 * dist + return sqrt5_dist.add(1 + 5 / 3 * (dist**2)) * torch.exp(-sqrt5_dist)
+ + + +
+[docs] +def compute_dists(X: Tensor, lengthscale: Tensor) -> Tensor: + """Compute kernel distances.""" + scaled_X = X / lengthscale + return dist(scaled_X, scaled_X, x1_eq_x2=True)
+ + + +
+[docs] +def reshape_and_detach(target: Tensor, new_value: Tensor) -> None: + """Detach and reshape `new_value` to match `target`.""" + return new_value.detach().clone().view(target.shape).to(target)
+ + + +
+[docs] +class PyroModel: + r""" + Base class for a Pyro model; used to assist in learning hyperparameters. + + This class and its subclasses are not a standard BoTorch models; instead + the subclasses are used as inputs to a `SaasFullyBayesianSingleTaskGP`, + which should then have its hyperparameters fit with + `fit_fully_bayesian_model_nuts`. (By default, its subclass `SaasPyroModel` + is used). A `PyroModel`’s `sample` method should specify lightweight + PyTorch functionality, which will be used for fast model fitting with NUTS. + The utility of `PyroModel` is in enabling fast fitting with NUTS, since we + would otherwise need to use GPyTorch, which is computationally infeasible + in combination with Pyro. + """ + +
+[docs] + def set_inputs( + self, train_X: Tensor, train_Y: Tensor, train_Yvar: Tensor | None = None + ) -> None: + """Set the training data. + + Args: + train_X: Training inputs (n x d) + train_Y: Training targets (n x 1) + train_Yvar: Observed noise variance (n x 1). Inferred if None. + """ + self.train_X = train_X + self.train_Y = train_Y + self.train_Yvar = train_Yvar
+ + +
+[docs] + @abstractmethod + def sample(self) -> None: + r"""Sample from the model.""" + pass # pragma: no cover
+ + +
+[docs] + @abstractmethod + def postprocess_mcmc_samples( + self, + mcmc_samples: dict[str, Tensor], + ) -> dict[str, Tensor]: + """Post-process the final MCMC samples.""" + pass # pragma: no cover
+ + +
+[docs] + @abstractmethod + def load_mcmc_samples( + self, mcmc_samples: dict[str, Tensor] + ) -> tuple[Mean, Kernel, Likelihood]: + pass # pragma: no cover
+
+ + + +
+[docs] +class SaasPyroModel(PyroModel): + r"""Implementation of the sparse axis-aligned subspace priors (SAAS) model. + + The SAAS model uses sparsity-inducing priors to identify the most important + parameters. This model is suitable for high-dimensional BO with potentially + hundreds of tunable parameters. See [Eriksson2021saasbo]_ for more details. + + `SaasPyroModel` is not a standard BoTorch model; instead, it is used as + an input to `SaasFullyBayesianSingleTaskGP`. It is used as a default keyword + argument, and end users are not likely to need to instantiate or modify a + `SaasPyroModel` unless they want to customize its attributes (such as + `covar_module`). + """ + +
+[docs] + def set_inputs( + self, train_X: Tensor, train_Y: Tensor, train_Yvar: Tensor | None = None + ) -> None: + super().set_inputs(train_X, train_Y, train_Yvar) + self.ard_num_dims = self.train_X.shape[-1]
+ + +
+[docs] + def sample(self) -> None: + r"""Sample from the SAAS model. + + This samples the mean, noise variance, outputscale, and lengthscales according + to the SAAS prior. + """ + tkwargs = {"dtype": self.train_X.dtype, "device": self.train_X.device} + outputscale = self.sample_outputscale(concentration=2.0, rate=0.15, **tkwargs) + mean = self.sample_mean(**tkwargs) + noise = self.sample_noise(**tkwargs) + lengthscale = self.sample_lengthscale(dim=self.ard_num_dims, **tkwargs) + if self.train_Y.shape[-2] > 0: + # Do not attempt to sample Y if the data is empty. + # This leads to errors with empty data. + K = matern52_kernel(X=self.train_X, lengthscale=lengthscale) + K = outputscale * K + noise * torch.eye(self.train_X.shape[0], **tkwargs) + pyro.sample( + "Y", + pyro.distributions.MultivariateNormal( + loc=mean.view(-1).expand(self.train_X.shape[0]), + covariance_matrix=K, + ), + obs=self.train_Y.squeeze(-1), + )
+ + +
+[docs] + def sample_outputscale( + self, concentration: float = 2.0, rate: float = 0.15, **tkwargs: Any + ) -> Tensor: + r"""Sample the outputscale.""" + return pyro.sample( + "outputscale", + pyro.distributions.Gamma( + torch.tensor(concentration, **tkwargs), + torch.tensor(rate, **tkwargs), + ), + )
+ + +
+[docs] + def sample_mean(self, **tkwargs: Any) -> Tensor: + r"""Sample the mean constant.""" + return pyro.sample( + "mean", + pyro.distributions.Normal( + torch.tensor(0.0, **tkwargs), + torch.tensor(1.0, **tkwargs), + ), + )
+ + +
+[docs] + def sample_noise(self, **tkwargs: Any) -> Tensor: + r"""Sample the noise variance.""" + if self.train_Yvar is None: + return MIN_INFERRED_NOISE_LEVEL + pyro.sample( + "noise", + pyro.distributions.Gamma( + torch.tensor(0.9, **tkwargs), + torch.tensor(10.0, **tkwargs), + ), + ) + else: + return self.train_Yvar
+ + +
+[docs] + def sample_lengthscale( + self, dim: int, alpha: float = 0.1, **tkwargs: Any + ) -> Tensor: + r"""Sample the lengthscale.""" + tausq = pyro.sample( + "kernel_tausq", + pyro.distributions.HalfCauchy(torch.tensor(alpha, **tkwargs)), + ) + inv_length_sq = pyro.sample( + "_kernel_inv_length_sq", + pyro.distributions.HalfCauchy(torch.ones(dim, **tkwargs)), + ) + inv_length_sq = pyro.deterministic( + "kernel_inv_length_sq", tausq * inv_length_sq + ) + lengthscale = pyro.deterministic( + "lengthscale", + inv_length_sq.rsqrt(), + ) + return lengthscale
+ + +
+[docs] + def postprocess_mcmc_samples( + self, mcmc_samples: dict[str, Tensor] + ) -> dict[str, Tensor]: + r"""Post-process the MCMC samples. + + This computes the true lengthscales and removes the inverse lengthscales and + tausq (global shrinkage). + """ + inv_length_sq = ( + mcmc_samples["kernel_tausq"].unsqueeze(-1) + * mcmc_samples["_kernel_inv_length_sq"] + ) + mcmc_samples["lengthscale"] = inv_length_sq.rsqrt() + # Delete `kernel_tausq` and `_kernel_inv_length_sq` since they aren't loaded + # into the final model. + del mcmc_samples["kernel_tausq"], mcmc_samples["_kernel_inv_length_sq"] + return mcmc_samples
+ + +
+[docs] + def load_mcmc_samples( + self, mcmc_samples: dict[str, Tensor] + ) -> tuple[Mean, Kernel, Likelihood]: + r"""Load the MCMC samples into the mean_module, covar_module, and likelihood.""" + tkwargs = {"device": self.train_X.device, "dtype": self.train_X.dtype} + num_mcmc_samples = len(mcmc_samples["mean"]) + batch_shape = torch.Size([num_mcmc_samples]) + + mean_module = ConstantMean(batch_shape=batch_shape).to(**tkwargs) + covar_module = ScaleKernel( + base_kernel=MaternKernel( + ard_num_dims=self.ard_num_dims, + batch_shape=batch_shape, + ), + batch_shape=batch_shape, + ).to(**tkwargs) + if self.train_Yvar is not None: + likelihood = FixedNoiseGaussianLikelihood( + # Reshape to shape `num_mcmc_samples x N` + noise=self.train_Yvar.squeeze(-1).expand( + num_mcmc_samples, len(self.train_Yvar) + ), + batch_shape=batch_shape, + ).to(**tkwargs) + else: + likelihood = GaussianLikelihood( + batch_shape=batch_shape, + noise_constraint=GreaterThan(MIN_INFERRED_NOISE_LEVEL), + ).to(**tkwargs) + likelihood.noise_covar.noise = reshape_and_detach( + target=likelihood.noise_covar.noise, + new_value=mcmc_samples["noise"].clamp_min(MIN_INFERRED_NOISE_LEVEL), + ) + covar_module.base_kernel.lengthscale = reshape_and_detach( + target=covar_module.base_kernel.lengthscale, + new_value=mcmc_samples["lengthscale"], + ) + covar_module.outputscale = reshape_and_detach( + target=covar_module.outputscale, + new_value=mcmc_samples["outputscale"], + ) + mean_module.constant.data = reshape_and_detach( + target=mean_module.constant.data, + new_value=mcmc_samples["mean"], + ) + return mean_module, covar_module, likelihood
+
+ + + +
+[docs] +class SaasFullyBayesianSingleTaskGP(ExactGP, BatchedMultiOutputGPyTorchModel): + r"""A fully Bayesian single-task GP model with the SAAS prior. + + This model assumes that the inputs have been normalized to [0, 1]^d and that + the output has been standardized to have zero mean and unit variance. You can + either normalize and standardize the data before constructing the model or use + an `input_transform` and `outcome_transform`. The SAAS model [Eriksson2021saasbo]_ + with a Matern-5/2 kernel is used by default. + + You are expected to use `fit_fully_bayesian_model_nuts` to fit this model as it + isn't compatible with `fit_gpytorch_mll`. + + Example: + >>> saas_gp = SaasFullyBayesianSingleTaskGP(train_X, train_Y) + >>> fit_fully_bayesian_model_nuts(saas_gp) + >>> posterior = saas_gp.posterior(test_X) + """ + + _is_fully_bayesian = True + _is_ensemble = True + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + train_Yvar: Tensor | None = None, + outcome_transform: OutcomeTransform | None = None, + input_transform: InputTransform | None = None, + pyro_model: PyroModel | None = None, + ) -> None: + r"""Initialize the fully Bayesian single-task GP model. + + Args: + train_X: Training inputs (n x d) + train_Y: Training targets (n x 1) + train_Yvar: Observed noise variance (n x 1). Inferred if None. + outcome_transform: An outcome transform that is applied to the + training data during instantiation and to the posterior during + inference (that is, the `Posterior` obtained by calling + `.posterior` on the model will be on the original scale). + input_transform: An input transform that is applied in the model's + forward pass. + pyro_model: Optional `PyroModel`, defaults to `SaasPyroModel`. + """ + if not ( + train_X.ndim == train_Y.ndim == 2 + and len(train_X) == len(train_Y) + and train_Y.shape[-1] == 1 + ): + raise ValueError( + "Expected train_X to have shape n x d and train_Y to have shape n x 1" + ) + if train_Yvar is not None: + if train_Y.shape != train_Yvar.shape: + raise ValueError( + "Expected train_Yvar to be None or have the same shape as train_Y" + ) + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + if outcome_transform is not None: + train_Y, train_Yvar = outcome_transform(train_Y, train_Yvar) + self._validate_tensor_args(X=transformed_X, Y=train_Y) + validate_input_scaling( + train_X=transformed_X, train_Y=train_Y, train_Yvar=train_Yvar + ) + self._num_outputs = train_Y.shape[-1] + self._input_batch_shape = train_X.shape[:-2] + if train_Yvar is not None: # Clamp after transforming + train_Yvar = train_Yvar.clamp(MIN_INFERRED_NOISE_LEVEL) + + X_tf, Y_tf, _ = self._transform_tensor_args(X=train_X, Y=train_Y) + super().__init__( + train_inputs=X_tf, train_targets=Y_tf, likelihood=GaussianLikelihood() + ) + self.mean_module = None + self.covar_module = None + self.likelihood = None + if pyro_model is None: + pyro_model = SaasPyroModel() + pyro_model.set_inputs( + train_X=transformed_X, train_Y=train_Y, train_Yvar=train_Yvar + ) + self.pyro_model: PyroModel = pyro_model + if outcome_transform is not None: + self.outcome_transform = outcome_transform + if input_transform is not None: + self.input_transform = input_transform + + def _check_if_fitted(self): + r"""Raise an exception if the model hasn't been fitted.""" + if self.covar_module is None: + raise RuntimeError( + "Model has not been fitted. You need to call " + "`fit_fully_bayesian_model_nuts` to fit the model." + ) + + @property + def median_lengthscale(self) -> Tensor: + r"""Median lengthscales across the MCMC samples.""" + self._check_if_fitted() + lengthscale = self.covar_module.base_kernel.lengthscale.clone() + return lengthscale.median(0).values.squeeze(0) + + @property + def num_mcmc_samples(self) -> int: + r"""Number of MCMC samples in the model.""" + self._check_if_fitted() + return len(self.covar_module.outputscale) + + @property + def batch_shape(self) -> torch.Size: + r"""Batch shape of the model, equal to the number of MCMC samples. + Note that `SaasFullyBayesianSingleTaskGP` does not support batching + over input data at this point.""" + return torch.Size([self.num_mcmc_samples]) + + @property + def _aug_batch_shape(self) -> torch.Size: + r"""The batch shape of the model, augmented to include the output dim.""" + aug_batch_shape = self.batch_shape + if self.num_outputs > 1: + aug_batch_shape += torch.Size([self.num_outputs]) + return aug_batch_shape + +
+[docs] + def train(self, mode: bool = True) -> None: + r"""Puts the model in `train` mode.""" + super().train(mode=mode) + if mode: + self.mean_module = None + self.covar_module = None + self.likelihood = None
+ + +
+[docs] + def load_mcmc_samples(self, mcmc_samples: dict[str, Tensor]) -> None: + r"""Load the MCMC hyperparameter samples into the model. + + This method will be called by `fit_fully_bayesian_model_nuts` when the model + has been fitted in order to create a batched SingleTaskGP model. + """ + ( + self.mean_module, + self.covar_module, + self.likelihood, + ) = self.pyro_model.load_mcmc_samples(mcmc_samples=mcmc_samples)
+ + +
+[docs] + def load_state_dict(self, state_dict: Mapping[str, Any], strict: bool = True): + r"""Custom logic for loading the state dict. + + The standard approach of calling `load_state_dict` currently doesn't play well + with the `SaasFullyBayesianSingleTaskGP` since the `mean_module`, `covar_module` + and `likelihood` aren't initialized until the model has been fitted. The reason + for this is that we don't know the number of MCMC samples until NUTS is called. + Given the state dict, we can initialize a new model with some dummy samples and + then load the state dict into this model. This currently only works for a + `SaasPyroModel` and supporting more Pyro models likely requires moving the model + construction logic into the Pyro model itself. + """ + + if not isinstance(self.pyro_model, SaasPyroModel): + raise NotImplementedError("load_state_dict only works for SaasPyroModel") + raw_mean = state_dict["mean_module.raw_constant"] + num_mcmc_samples = len(raw_mean) + dim = self.pyro_model.train_X.shape[-1] + tkwargs = {"device": raw_mean.device, "dtype": raw_mean.dtype} + # Load some dummy samples + mcmc_samples = { + "mean": torch.ones(num_mcmc_samples, **tkwargs), + "lengthscale": torch.ones(num_mcmc_samples, dim, **tkwargs), + "outputscale": torch.ones(num_mcmc_samples, **tkwargs), + } + if self.pyro_model.train_Yvar is None: + mcmc_samples["noise"] = torch.ones(num_mcmc_samples, **tkwargs) + ( + self.mean_module, + self.covar_module, + self.likelihood, + ) = self.pyro_model.load_mcmc_samples(mcmc_samples=mcmc_samples) + # Load the actual samples from the state dict + super().load_state_dict(state_dict=state_dict, strict=strict)
+ + +
+[docs] + def forward(self, X: Tensor) -> MultivariateNormal: + """ + Unlike in other classes' `forward` methods, there is no `if self.training` + block, because it ought to be unreachable: If `self.train()` has been called, + then `self.covar_module` will be None, `check_if_fitted()` will fail, and the + rest of this method will not run. + """ + self._check_if_fitted() + mean_x = self.mean_module(X) + covar_x = self.covar_module(X) + return MultivariateNormal(mean_x, covar_x)
+ + + # pyre-ignore[14]: Inconsistent override +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool = False, + posterior_transform: PosteriorTransform | None = None, + **kwargs: Any, + ) -> GaussianMixturePosterior: + r"""Computes the posterior over model outputs at the provided points. + + Args: + X: A `(batch_shape) x q x d`-dim Tensor, where `d` is the dimension + of the feature space and `q` is the number of points considered + jointly. + output_indices: A list of indices, corresponding to the outputs over + which to compute the posterior (if the model is multi-output). + Can be used to speed up computation if only a subset of the + model's outputs are required for optimization. If omitted, + computes the posterior over all model outputs. + observation_noise: If True, add the observation noise from the + likelihood to the posterior. If a Tensor, use it directly as the + observation noise (must be of shape `(batch_shape) x q x m`). + posterior_transform: An optional PosteriorTransform. + + Returns: + A `GaussianMixturePosterior` object. Includes observation noise + if specified. + """ + self._check_if_fitted() + posterior = super().posterior( + X=X.unsqueeze(MCMC_DIM), + output_indices=output_indices, + observation_noise=observation_noise, + posterior_transform=posterior_transform, + **kwargs, + ) + posterior = GaussianMixturePosterior(distribution=posterior.distribution) + return posterior
+ + +
+[docs] + def condition_on_observations( + self, X: Tensor, Y: Tensor, **kwargs: Any + ) -> BatchedMultiOutputGPyTorchModel: + """Conditions on additional observations for a Fully Bayesian model (either + identical across models or unique per-model). + + Args: + X: A `batch_shape x num_samples x d`-dim Tensor, where `d` is + the dimension of the feature space and `batch_shape` is the number of + sampled models. + Y: A `batch_shape x num_samples x 1`-dim Tensor, where `d` is + the dimension of the feature space and `batch_shape` is the number of + sampled models. + + Returns: + BatchedMultiOutputGPyTorchModel: A fully bayesian model conditioned on + given observations. The returned model has `batch_shape` copies of the + training data in case of identical observations (and `batch_shape` + training datasets otherwise). + """ + if X.ndim == 2 and Y.ndim == 2: + # To avoid an error in GPyTorch when inferring the batch dimension, we add + # the explicit batch shape here. The result is that the conditioned model + # will have 'batch_shape' copies of the training data. + X = X.repeat(self.batch_shape + (1, 1)) + Y = Y.repeat(self.batch_shape + (1, 1)) + + elif X.ndim < Y.ndim: + # We need to duplicate the training data to enable correct batch + # size inference in gpytorch. + X = X.repeat(*(Y.shape[:-2] + (1, 1))) + + return super().condition_on_observations(X, Y, **kwargs)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/fully_bayesian_multitask.html b/website-old/pages/api/_modules/botorch/models/fully_bayesian_multitask.html new file mode 100644 index 0000000000..da1cf605c4 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/fully_bayesian_multitask.html @@ -0,0 +1,538 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.fully_bayesian_multitask

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+r"""Multi-task Gaussian Process Regression models with fully Bayesian inference."""
+
+from collections.abc import Mapping
+from typing import Any, NoReturn
+
+import pyro
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.fully_bayesian import (
+    matern52_kernel,
+    MIN_INFERRED_NOISE_LEVEL,
+    reshape_and_detach,
+    SaasPyroModel,
+)
+from botorch.models.multitask import MultiTaskGP
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from botorch.posteriors.fully_bayesian import GaussianMixturePosterior, MCMC_DIM
+from gpytorch.distributions.multivariate_normal import MultivariateNormal
+from gpytorch.kernels import MaternKernel
+from gpytorch.kernels.kernel import Kernel
+from gpytorch.likelihoods.likelihood import Likelihood
+from gpytorch.means.mean import Mean
+from torch import Tensor
+from torch.nn.parameter import Parameter
+
+
+
+[docs] +class MultitaskSaasPyroModel(SaasPyroModel): + r""" + Implementation of the multi-task sparse axis-aligned subspace priors (SAAS) model. + + The multi-task model uses an ICM kernel. The data kernel is same as the single task + SAAS model in order to handle high-dimensional parameter spaces. The task kernel + is a Matern-5/2 kernel using learned task embeddings as the input. + """ + +
+[docs] + def set_inputs( + self, + train_X: Tensor, + train_Y: Tensor, + train_Yvar: Tensor | None, + task_feature: int, + task_rank: int | None = None, + ) -> None: + """Set the training data. + + Args: + train_X: Training inputs (n x (d + 1)) + train_Y: Training targets (n x 1) + train_Yvar: Observed noise variance (n x 1). If None, we infer the noise. + Note that the inferred noise is common across all tasks. + task_feature: The index of the task feature (`-d <= task_feature <= d`). + task_rank: The num of learned task embeddings to be used in the task kernel. + If omitted, use a full rank (i.e. number of tasks) kernel. + """ + super().set_inputs(train_X, train_Y, train_Yvar) + # obtain a list of task indicies + all_tasks = train_X[:, task_feature].unique().to(dtype=torch.long).tolist() + self.task_feature = task_feature + self.num_tasks = len(all_tasks) + self.task_rank = task_rank or self.num_tasks + # assume there is one column for task feature + self.ard_num_dims = self.train_X.shape[-1] - 1
+ + +
+[docs] + def sample(self) -> None: + r"""Sample from the SAAS model. + + This samples the mean, noise variance, outputscale, and lengthscales according + to the SAAS prior. + """ + tkwargs = {"dtype": self.train_X.dtype, "device": self.train_X.device} + base_idxr = torch.arange(self.ard_num_dims, **{"device": tkwargs["device"]}) + base_idxr[self.task_feature :] += 1 # exclude task feature + task_indices = self.train_X[..., self.task_feature].to( + device=tkwargs["device"], dtype=torch.long + ) + + outputscale = self.sample_outputscale(concentration=2.0, rate=0.15, **tkwargs) + mean = self.sample_mean(**tkwargs) + noise = self.sample_noise(**tkwargs) + + lengthscale = self.sample_lengthscale(dim=self.ard_num_dims, **tkwargs) + K = matern52_kernel(X=self.train_X[..., base_idxr], lengthscale=lengthscale) + + # compute task covar matrix + task_latent_features = self.sample_latent_features(**tkwargs)[task_indices] + task_lengthscale = self.sample_task_lengthscale(**tkwargs) + task_covar = matern52_kernel( + X=task_latent_features, lengthscale=task_lengthscale + ) + K = K.mul(task_covar) + K = outputscale * K + noise * torch.eye(self.train_X.shape[0], **tkwargs) + pyro.sample( + "Y", + pyro.distributions.MultivariateNormal( + loc=mean.view(-1).expand(self.train_X.shape[0]), + covariance_matrix=K, + ), + obs=self.train_Y.squeeze(-1), + )
+ + +
+[docs] + def sample_latent_features(self, **tkwargs: Any): + return pyro.sample( + "latent_features", + pyro.distributions.Normal( + torch.tensor(0.0, **tkwargs), + torch.tensor(1.0, **tkwargs), + ).expand(torch.Size([self.num_tasks, self.task_rank])), + )
+ + +
+[docs] + def sample_task_lengthscale( + self, concentration: float = 6.0, rate: float = 3.0, **tkwargs: Any + ): + return pyro.sample( + "task_lengthscale", + pyro.distributions.Gamma( + torch.tensor(concentration, **tkwargs), + torch.tensor(rate, **tkwargs), + ).expand(torch.Size([self.task_rank])), + )
+ + +
+[docs] + def load_mcmc_samples( + self, mcmc_samples: dict[str, Tensor] + ) -> tuple[Mean, Kernel, Likelihood, Kernel, Parameter]: + r"""Load the MCMC samples into the mean_module, covar_module, and likelihood.""" + tkwargs = {"device": self.train_X.device, "dtype": self.train_X.dtype} + num_mcmc_samples = len(mcmc_samples["mean"]) + batch_shape = torch.Size([num_mcmc_samples]) + + mean_module, covar_module, likelihood = super().load_mcmc_samples( + mcmc_samples=mcmc_samples + ) + + task_covar_module = MaternKernel( + nu=2.5, + ard_num_dims=self.task_rank, + batch_shape=batch_shape, + ).to(**tkwargs) + task_covar_module.lengthscale = reshape_and_detach( + target=task_covar_module.lengthscale, + new_value=mcmc_samples["task_lengthscale"], + ) + latent_features = Parameter( + torch.rand( + batch_shape + torch.Size([self.num_tasks, self.task_rank]), + requires_grad=True, + **tkwargs, + ) + ) + latent_features = reshape_and_detach( + target=latent_features, + new_value=mcmc_samples["latent_features"], + ) + return mean_module, covar_module, likelihood, task_covar_module, latent_features
+
+ + + +
+[docs] +class SaasFullyBayesianMultiTaskGP(MultiTaskGP): + r"""A fully Bayesian multi-task GP model with the SAAS prior. + + This model assumes that the inputs have been normalized to [0, 1]^d and that the + output has been stratified standardized to have zero mean and unit variance for + each task. The SAAS model [Eriksson2021saasbo]_ with a Matern-5/2 is used as data + kernel by default. + + You are expected to use `fit_fully_bayesian_model_nuts` to fit this model as it + isn't compatible with `fit_gpytorch_mll`. + + Example: + >>> X1, X2 = torch.rand(10, 2), torch.rand(20, 2) + >>> i1, i2 = torch.zeros(10, 1), torch.ones(20, 1) + >>> train_X = torch.cat([ + >>> torch.cat([X1, i1], -1), torch.cat([X2, i2], -1), + >>> ]) + >>> train_Y = torch.cat(f1(X1), f2(X2)).unsqueeze(-1) + >>> train_Yvar = 0.01 * torch.ones_like(train_Y) + >>> mtsaas_gp = SaasFullyBayesianMultiTaskGP( + >>> train_X, train_Y, train_Yvar, task_feature=-1, + >>> ) + >>> fit_fully_bayesian_model_nuts(mtsaas_gp) + >>> posterior = mtsaas_gp.posterior(test_X) + """ + + _is_fully_bayesian = True + _is_ensemble = True + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + task_feature: int, + train_Yvar: Tensor | None = None, + output_tasks: list[int] | None = None, + rank: int | None = None, + all_tasks: list[int] | None = None, + outcome_transform: OutcomeTransform | None = None, + input_transform: InputTransform | None = None, + pyro_model: MultitaskSaasPyroModel | None = None, + ) -> None: + r"""Initialize the fully Bayesian multi-task GP model. + + Args: + train_X: Training inputs (n x (d + 1)) + train_Y: Training targets (n x 1) + train_Yvar: Observed noise variance (n x 1). If None, we infer the noise. + Note that the inferred noise is common across all tasks. + task_feature: The index of the task feature (`-d <= task_feature <= d`). + output_tasks: A list of task indices for which to compute model + outputs for. If omitted, return outputs for all task indices. + rank: The num of learned task embeddings to be used in the task kernel. + If omitted, use a full rank (i.e. number of tasks) kernel. + all_tasks: NOT SUPPORTED! + outcome_transform: An outcome transform that is applied to the + training data during instantiation and to the posterior during + inference (that is, the `Posterior` obtained by calling + `.posterior` on the model will be on the original scale). + input_transform: An input transform that is applied to the inputs `X` + in the model's forward pass. + pyro_model: Optional `PyroModel` that has the same signature as + `MultitaskSaasPyroModel`. Defaults to `MultitaskSaasPyroModel`. + """ + if not ( + train_X.ndim == train_Y.ndim == 2 + and len(train_X) == len(train_Y) + and train_Y.shape[-1] == 1 + ): + raise ValueError( + "Expected train_X to have shape n x d and train_Y to have shape n x 1" + ) + if train_Yvar is not None and train_Y.shape != train_Yvar.shape: + raise ValueError( + "Expected train_Yvar to be None or have the same shape as train_Y" + ) + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + if outcome_transform is not None: + outcome_transform.train() # Ensure we learn parameters here on init + train_Y, train_Yvar = outcome_transform(train_Y, train_Yvar) + if train_Yvar is not None: # Clamp after transforming + train_Yvar = train_Yvar.clamp(MIN_INFERRED_NOISE_LEVEL) + + super().__init__( + train_X=train_X, + train_Y=train_Y, + train_Yvar=train_Yvar, + task_feature=task_feature, + output_tasks=output_tasks, + rank=rank, + # We already transformed the data above, this avoids applying the + # default `Standardize` transform twice. As outcome_transform is + # set on `self` below, it will be applied to the posterior in the + # `posterior` method of `MultiTaskGP`. + outcome_transform=None, + ) + if all_tasks is not None and self._expected_task_values != set(all_tasks): + raise NotImplementedError( + "The `all_tasks` argument is not supported by SAAS MTGP. " + f"The training data includes tasks {self._expected_task_values}, " + f"got {all_tasks=}." + ) + self.to(train_X) + + self.mean_module = None + self.covar_module = None + self.likelihood = None + self.task_covar_module = None + self.register_buffer("latent_features", None) + if pyro_model is None: + pyro_model = MultitaskSaasPyroModel() + pyro_model.set_inputs( + train_X=transformed_X, + train_Y=train_Y, + train_Yvar=train_Yvar, + task_feature=task_feature, + task_rank=self._rank, + ) + self.pyro_model: MultitaskSaasPyroModel = pyro_model + if outcome_transform is not None: + self.outcome_transform = outcome_transform + if input_transform is not None: + self.input_transform = input_transform + +
+[docs] + def train(self, mode: bool = True) -> None: + r"""Puts the model in `train` mode.""" + super().train(mode=mode) + if mode: + self.mean_module = None + self.covar_module = None + self.likelihood = None + self.task_covar_module = None
+ + + @property + def median_lengthscale(self) -> Tensor: + r"""Median lengthscales across the MCMC samples.""" + self._check_if_fitted() + lengthscale = self.covar_module.base_kernel.lengthscale.clone() + return lengthscale.median(0).values.squeeze(0) + + @property + def num_mcmc_samples(self) -> int: + r"""Number of MCMC samples in the model.""" + self._check_if_fitted() + return len(self.covar_module.outputscale) + + @property + def batch_shape(self) -> torch.Size: + r"""Batch shape of the model, equal to the number of MCMC samples. + Note that `SaasFullyBayesianMultiTaskGP` does not support batching + over input data at this point. + """ + self._check_if_fitted() + return torch.Size([self.num_mcmc_samples]) + +
+[docs] + def fantasize(self, *args, **kwargs) -> NoReturn: + raise NotImplementedError("Fantasize is not implemented!")
+ + + def _check_if_fitted(self): + r"""Raise an exception if the model hasn't been fitted.""" + if self.covar_module is None: + raise RuntimeError( + "Model has not been fitted. You need to call " + "`fit_fully_bayesian_model_nuts` to fit the model." + ) + +
+[docs] + def load_mcmc_samples(self, mcmc_samples: dict[str, Tensor]) -> None: + r"""Load the MCMC hyperparameter samples into the model. + + This method will be called by `fit_fully_bayesian_model_nuts` when the model + has been fitted in order to create a batched MultiTaskGP model. + """ + ( + self.mean_module, + self.covar_module, + self.likelihood, + self.task_covar_module, + self.latent_features, + ) = self.pyro_model.load_mcmc_samples(mcmc_samples=mcmc_samples)
+ + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool = False, + posterior_transform: PosteriorTransform | None = None, + **kwargs: Any, + ) -> GaussianMixturePosterior: + r"""Computes the posterior over model outputs at the provided points. + + Returns: + A `GaussianMixturePosterior` object. Includes observation noise + if specified. + """ + self._check_if_fitted() + posterior = super().posterior( + X=X, + output_indices=output_indices, + observation_noise=observation_noise, + posterior_transform=posterior_transform, + **kwargs, + ) + posterior = GaussianMixturePosterior(distribution=posterior.distribution) + return posterior
+ + +
+[docs] + def forward(self, X: Tensor) -> MultivariateNormal: + self._check_if_fitted() + X = X.unsqueeze(MCMC_DIM) + + x_basic, task_idcs = self._split_inputs(X) + + mean_x = self.mean_module(x_basic) + covar_x = self.covar_module(x_basic) + + tsub_idcs = task_idcs.squeeze(-3).squeeze(-1) + latent_features = self.latent_features[:, tsub_idcs, :] + + if X.ndim > 3: + # batch eval mode + # for X (batch_shape x num_samples x q x d), task_idcs[:,i,:,] are the same + # reshape X to (batch_shape x num_samples x q x d) + latent_features = latent_features.permute( + [-i for i in range(X.ndim - 1, 2, -1)] + + [0] + + [-i for i in range(2, 0, -1)] + ) + + # Combine the two in an ICM fashion + covar_i = self.task_covar_module(latent_features) + covar = covar_x.mul(covar_i) + return MultivariateNormal(mean_x, covar)
+ + +
+[docs] + def load_state_dict(self, state_dict: Mapping[str, Any], strict: bool = True): + r"""Custom logic for loading the state dict. + + The standard approach of calling `load_state_dict` currently doesn't play well + with the `SaasFullyBayesianMultiTaskGP` since the `mean_module`, `covar_module` + and `likelihood` aren't initialized until the model has been fitted. The reason + for this is that we don't know the number of MCMC samples until NUTS is called. + Given the state dict, we can initialize a new model with some dummy samples and + then load the state dict into this model. This currently only works for a + `MultitaskSaasPyroModel` and supporting more Pyro models likely requires moving + the model construction logic into the Pyro model itself. + + TODO: If this were to inherif from `SaasFullyBayesianSingleTaskGP`, we could + simplify this method and eliminate some others. + """ + if not isinstance(self.pyro_model, MultitaskSaasPyroModel): + raise NotImplementedError( # pragma: no cover + "load_state_dict only works for MultitaskSaasPyroModel" + ) + raw_mean = state_dict["mean_module.raw_constant"] + num_mcmc_samples = len(raw_mean) + dim = self.pyro_model.train_X.shape[-1] - 1 # Removing 1 for the task feature. + tkwargs = {"device": raw_mean.device, "dtype": raw_mean.dtype} + # Load some dummy samples + mcmc_samples = { + "mean": torch.ones(num_mcmc_samples, **tkwargs), + "lengthscale": torch.ones(num_mcmc_samples, dim, **tkwargs), + "outputscale": torch.ones(num_mcmc_samples, **tkwargs), + "task_lengthscale": torch.ones(num_mcmc_samples, self._rank, **tkwargs), + "latent_features": torch.ones( + num_mcmc_samples, self.num_tasks, self._rank, **tkwargs + ), + } + if self.pyro_model.train_Yvar is None: + mcmc_samples["noise"] = torch.ones(num_mcmc_samples, **tkwargs) + ( + self.mean_module, + self.covar_module, + self.likelihood, + self.task_covar_module, + self.latent_features, + ) = self.pyro_model.load_mcmc_samples(mcmc_samples=mcmc_samples) + # Load the actual samples from the state dict + super().load_state_dict(state_dict=state_dict, strict=strict)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/gp_regression.html b/website-old/pages/api/_modules/botorch/models/gp_regression.html new file mode 100644 index 0000000000..a36968192d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/gp_regression.html @@ -0,0 +1,315 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.gp_regression

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Gaussian Process Regression models based on GPyTorch models.
+
+These models are often a good starting point and are further documented in the
+tutorials.
+
+`SingleTaskGP` is a single-task exact GP model that uses relatively strong priors on
+the Kernel hyperparameters, which work best when covariates are normalized to the unit
+cube and outcomes are standardized (zero mean, unit variance). By default, this model
+uses a `Standardize` outcome transform, which applies this standardization. However,
+it does not (yet) use an input transform by default.
+
+`SingleTaskGP` model works in batch mode (each batch having its own hyperparameters).
+When the training observations include multiple outputs, `SingleTaskGP` uses
+batching to model outputs independently.
+
+`SingleTaskGP` supports multiple outputs. However, as a single-task model,
+`SingleTaskGP` should be used only when the outputs are independent and all
+use the same training inputs. If outputs are independent but they have different
+training inputs, use the `ModelListGP`. When modeling correlations between outputs,
+use a multi-task model like `MultiTaskGP`.
+"""
+
+from __future__ import annotations
+
+import warnings
+
+import torch
+from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel
+from botorch.models.model import FantasizeMixin
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform, Standardize
+from botorch.models.utils import validate_input_scaling
+from botorch.models.utils.gpytorch_modules import (
+    get_covar_module_with_dim_scaled_prior,
+    get_gaussian_likelihood_with_lognormal_prior,
+)
+from botorch.utils.containers import BotorchContainer
+from botorch.utils.datasets import SupervisedDataset
+from botorch.utils.types import _DefaultType, DEFAULT
+from gpytorch.distributions.multivariate_normal import MultivariateNormal
+from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood
+from gpytorch.likelihoods.likelihood import Likelihood
+from gpytorch.means.constant_mean import ConstantMean
+from gpytorch.means.mean import Mean
+from gpytorch.models.exact_gp import ExactGP
+from gpytorch.module import Module
+from torch import Tensor
+
+
+
+[docs] +class SingleTaskGP(BatchedMultiOutputGPyTorchModel, ExactGP, FantasizeMixin): + r"""A single-task exact GP model, supporting both known and inferred noise levels. + + A single-task exact GP which, by default, utilizes hyperparameter priors + from [Hvarfner2024vanilla]_. These priors designed to perform well independently of + the dimensionality of the problem. Moreover, they suggest a moderately low level of + noise. Importantly, The model works best when covariates are normalized to the unit + cube and outcomes are standardized (zero mean, unit variance). For a detailed + discussion on the hyperparameter priors, see + https://github.com/pytorch/botorch/discussions/2451. + + This model works in batch mode (each batch having its own hyperparameters). + When the training observations include multiple outputs, this model will use + batching to model outputs independently. + + Use this model when you have independent output(s) and all outputs use the + same training data. If outputs are independent and outputs have different + training data, use the ModelListGP. When modeling correlations between + outputs, use the MultiTaskGP. + + An example of a case in which noise levels are known is online + experimentation, where noise can be measured using the variability of + different observations from the same arm, or provided by outside software. + Another use case is simulation optimization, where the evaluation can + provide variance estimates, perhaps from bootstrapping. In any case, these + noise levels can be provided to `SingleTaskGP` as `train_Yvar`. + + `SingleTaskGP` can also be used when the observations are known to be + noise-free. Noise-free observations can be modeled using arbitrarily small + noise values, such as `train_Yvar=torch.full_like(train_Y, 1e-6)`. + + Example: + Model with inferred noise levels: + + >>> import torch + >>> from botorch.models.gp_regression import SingleTaskGP + >>> from botorch.models.transforms.outcome import Standardize + >>> + >>> train_X = torch.rand(20, 2, dtype=torch.float64) + >>> train_Y = torch.sin(train_X).sum(dim=1, keepdim=True) + >>> outcome_transform = Standardize(m=1) + >>> inferred_noise_model = SingleTaskGP( + ... train_X, train_Y, outcome_transform=outcome_transform, + ... ) + + Model with a known observation variance of 0.2: + + >>> train_Yvar = torch.full_like(train_Y, 0.2) + >>> observed_noise_model = SingleTaskGP( + ... train_X, train_Y, train_Yvar, + ... outcome_transform=outcome_transform, + ... ) + + With noise-free observations: + + >>> train_Yvar = torch.full_like(train_Y, 1e-6) + >>> noise_free_model = SingleTaskGP( + ... train_X, train_Y, train_Yvar, + ... outcome_transform=outcome_transform, + ... ) + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + train_Yvar: Tensor | None = None, + likelihood: Likelihood | None = None, + covar_module: Module | None = None, + mean_module: Mean | None = None, + outcome_transform: OutcomeTransform | _DefaultType | None = DEFAULT, + input_transform: InputTransform | None = None, + ) -> None: + r""" + Args: + train_X: A `batch_shape x n x d` tensor of training features. + train_Y: A `batch_shape x n x m` tensor of training observations. + train_Yvar: An optional `batch_shape x n x m` tensor of observed + measurement noise. + likelihood: A likelihood. If omitted, use a standard + `GaussianLikelihood` with inferred noise level if `train_Yvar` + is None, and a `FixedNoiseGaussianLikelihood` with the given + noise observations if `train_Yvar` is not None. + covar_module: The module computing the covariance (Kernel) matrix. + If omitted, uses an `RBFKernel`. + mean_module: The mean function to be used. If omitted, use a + `ConstantMean`. + outcome_transform: An outcome transform that is applied to the + training data during instantiation and to the posterior during + inference (that is, the `Posterior` obtained by calling + `.posterior` on the model will be on the original scale). We use a + `Standardize` transform if no `outcome_transform` is specified. + Pass down `None` to use no outcome transform. + input_transform: An input transform that is applied in the model's + forward pass. + """ + self._validate_tensor_args(X=train_X, Y=train_Y, Yvar=train_Yvar) + if outcome_transform == DEFAULT: + outcome_transform = Standardize( + m=train_Y.shape[-1], batch_shape=train_X.shape[:-2] + ) + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + if outcome_transform is not None: + train_Y, train_Yvar = outcome_transform(train_Y, train_Yvar) + # Validate again after applying the transforms + self._validate_tensor_args(X=transformed_X, Y=train_Y, Yvar=train_Yvar) + ignore_X_dims = getattr(self, "_ignore_X_dims_scaling_check", None) + validate_input_scaling( + train_X=transformed_X, + train_Y=train_Y, + train_Yvar=train_Yvar, + ignore_X_dims=ignore_X_dims, + ) + self._set_dimensions(train_X=train_X, train_Y=train_Y) + train_X, train_Y, train_Yvar = self._transform_tensor_args( + X=train_X, Y=train_Y, Yvar=train_Yvar + ) + if likelihood is None: + if train_Yvar is None: + likelihood = get_gaussian_likelihood_with_lognormal_prior( + batch_shape=self._aug_batch_shape + ) + else: + likelihood = FixedNoiseGaussianLikelihood( + noise=train_Yvar, batch_shape=self._aug_batch_shape + ) + else: + self._is_custom_likelihood = True + ExactGP.__init__( + self, train_inputs=train_X, train_targets=train_Y, likelihood=likelihood + ) + if mean_module is None: + mean_module = ConstantMean(batch_shape=self._aug_batch_shape) + self.mean_module = mean_module + if covar_module is None: + covar_module = get_covar_module_with_dim_scaled_prior( + ard_num_dims=transformed_X.shape[-1], + batch_shape=self._aug_batch_shape, + ) + # Used for subsetting along the output dimension. See Model.subset_output. + self._subset_batch_dict = { + "mean_module.raw_constant": -1, + "covar_module.raw_lengthscale": -3, + } + if train_Yvar is None: + self._subset_batch_dict["likelihood.noise_covar.raw_noise"] = -2 + self.covar_module: Module = covar_module + # TODO: Allow subsetting of other covar modules + if outcome_transform is not None: + self.outcome_transform = outcome_transform + if input_transform is not None: + self.input_transform = input_transform + self.to(train_X) + +
+[docs] + @classmethod + def construct_inputs( + cls, training_data: SupervisedDataset, *, task_feature: int | None = None + ) -> dict[str, BotorchContainer | Tensor]: + r"""Construct `SingleTaskGP` keyword arguments from a `SupervisedDataset`. + + Args: + training_data: A `SupervisedDataset`, with attributes `train_X`, + `train_Y`, and, optionally, `train_Yvar`. + task_feature: Deprecated and allowed only for backward + compatibility; ignored. + + Returns: + A dict of keyword arguments that can be used to initialize a `SingleTaskGP`, + with keys `train_X`, `train_Y`, and, optionally, `train_Yvar`. + """ + if task_feature is not None: + warnings.warn( + "`task_feature` is deprecated and will be ignored. In the " + "future, this will be an error.", + DeprecationWarning, + stacklevel=2, + ) + return super().construct_inputs(training_data=training_data)
+ + +
+[docs] + def forward(self, x: Tensor) -> MultivariateNormal: + if self.training: + x = self.transform_inputs(x) + mean_x = self.mean_module(x) + covar_x = self.covar_module(x) + return MultivariateNormal(mean_x, covar_x)
+
+ + + +# Note: There used to be `HeteroskedasticSingleTaskGP` here, +# but due to persistent bugs, it was removed in #2616. +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/gp_regression_fidelity.html b/website-old/pages/api/_modules/botorch/models/gp_regression_fidelity.html new file mode 100644 index 0000000000..ced5a6276f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/gp_regression_fidelity.html @@ -0,0 +1,361 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.gp_regression_fidelity

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Multi-Fidelity Gaussian Process Regression models based on GPyTorch models.
+
+For more on Multi-Fidelity BO, see the
+`tutorial <https://botorch.org/tutorials/discrete_multi_fidelity_bo>`__.
+
+A common use case of multi-fidelity regression modeling is optimizing a
+"high-fidelity" function that is expensive to simulate when you have access to
+one or more cheaper "lower-fidelity" versions that are not fully accurate but
+are correlated with the high-fidelity function. The multi-fidelity model models
+both the low- and high-fidelity functions together, including the correlation
+between them, which can help you predict and optimize the high-fidelity function
+without having to do too many expensive high-fidelity evaluations.
+
+.. [Wu2019mf]
+    J. Wu, S. Toscano-Palmerin, P. I. Frazier, and A. G. Wilson. Practical
+    multi-fidelity bayesian optimization for hyperparameter tuning. ArXiv 2019.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from typing import Any
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.kernels.downsampling import DownsamplingKernel
+from botorch.models.kernels.exponential_decay import ExponentialDecayKernel
+from botorch.models.kernels.linear_truncated_fidelity import (
+    LinearTruncatedFidelityKernel,
+)
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from botorch.models.utils.gpytorch_modules import get_covar_module_with_dim_scaled_prior
+from botorch.utils.datasets import SupervisedDataset
+from botorch.utils.types import _DefaultType, DEFAULT
+from gpytorch.kernels.kernel import ProductKernel
+from gpytorch.kernels.scale_kernel import ScaleKernel
+from gpytorch.likelihoods.likelihood import Likelihood
+from gpytorch.priors.torch_priors import GammaPrior
+from torch import Tensor
+
+
+
+[docs] +class SingleTaskMultiFidelityGP(SingleTaskGP): + r"""A single task multi-fidelity GP model. + + A SingleTaskGP model using a DownsamplingKernel for the data fidelity + parameter (if present) and an ExponentialDecayKernel for the iteration + fidelity parameter (if present). + + This kernel is described in [Wu2019mf]_. + + Example: + >>> train_X = torch.rand(20, 4) + >>> train_Y = train_X.pow(2).sum(dim=-1, keepdim=True) + >>> model = SingleTaskMultiFidelityGP(train_X, train_Y, data_fidelities=[3]) + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + train_Yvar: Tensor | None = None, + iteration_fidelity: int | None = None, + data_fidelities: Sequence[int] | None = None, + linear_truncated: bool = True, + nu: float = 2.5, + likelihood: Likelihood | None = None, + outcome_transform: OutcomeTransform | _DefaultType | None = DEFAULT, + input_transform: InputTransform | None = None, + ) -> None: + r""" + Args: + train_X: A `batch_shape x n x (d + s)` tensor of training features, + where `s` is the dimension of the fidelity parameters (either one + or two). + train_Y: A `batch_shape x n x m` tensor of training observations. + train_Yvar: An optional `batch_shape x n x m` tensor of observed + measurement noise. + iteration_fidelity: The column index for the training iteration fidelity + parameter (optional). + data_fidelities: The column indices for the downsampling fidelity parameter. + If a list/tuple of indices is provided, a kernel will be constructed for + each index (optional). + linear_truncated: If True, use a `LinearTruncatedFidelityKernel` instead + of the default kernel. + nu: The smoothness parameter for the Matern kernel: either 1/2, 3/2, or + 5/2. Only used when `linear_truncated=True`. + likelihood: A likelihood. If omitted, use a standard GaussianLikelihood + with inferred noise level. + outcome_transform: An outcome transform that is applied to the + training data during instantiation and to the posterior during + inference (that is, the `Posterior` obtained by calling + `.posterior` on the model will be on the original scale). We use a + `Standardize` transform if no `outcome_transform` is specified. + Pass down `None` to use no outcome transform. + input_transform: An input transform that is applied in the model's + forward pass. + """ + self._init_args = { + "iteration_fidelity": iteration_fidelity, + "data_fidelities": data_fidelities, + "linear_truncated": linear_truncated, + "nu": nu, + "outcome_transform": outcome_transform, + } + if iteration_fidelity is None and ( + data_fidelities is None or len(data_fidelities) == 0 + ): + raise UnsupportedError( + f"{self.__class__.__name__} requires at least one fidelity parameter." + ) + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + + self._set_dimensions(train_X=transformed_X, train_Y=train_Y) + covar_module, subset_batch_dict = _setup_multifidelity_covar_module( + dim=transformed_X.size(-1), + aug_batch_shape=self._aug_batch_shape, + iteration_fidelity=iteration_fidelity, + data_fidelities=data_fidelities, + linear_truncated=linear_truncated, + nu=nu, + ) + super().__init__( + train_X=train_X, + train_Y=train_Y, + train_Yvar=train_Yvar, + likelihood=likelihood, + covar_module=covar_module, + outcome_transform=outcome_transform, + input_transform=input_transform, + ) + # Used for subsetting along the output dimension. See Model.subset_output. + self._subset_batch_dict = { + "mean_module.raw_constant": -1, + "covar_module.raw_outputscale": -1, + **subset_batch_dict, + } + if train_Yvar is None: + self._subset_batch_dict["likelihood.noise_covar.raw_noise"] = -2 + self.to(train_X) + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset, + fidelity_features: list[int], + ) -> dict[str, Any]: + r"""Construct `Model` keyword arguments from a dict of `SupervisedDataset`. + + Args: + training_data: Dictionary of `SupervisedDataset`. + fidelity_features: Index of fidelity parameter as input columns. + """ + inputs = super().construct_inputs(training_data=training_data) + inputs["data_fidelities"] = fidelity_features + return inputs
+
+ + + +def _setup_multifidelity_covar_module( + dim: int, + aug_batch_shape: torch.Size, + iteration_fidelity: int | None, + data_fidelities: Sequence[int] | None, + linear_truncated: bool, + nu: float, +) -> tuple[ScaleKernel, dict]: + """Helper function to get the covariance module and associated subset_batch_dict + for the multifidelity setting. + + Args: + dim: The dimensionality of the training data. + aug_batch_shape: The output-augmented batch shape as defined in + `BatchedMultiOutputGPyTorchModel`. + iteration_fidelity: The column index for the training iteration fidelity + parameter (optional). + data_fidelities: The column indices for the downsampling fidelity parameters + (optional). + linear_truncated: If True, use a `LinearTruncatedFidelityKernel` instead + of the default kernel. + nu: The smoothness parameter for the Matern kernel: either 1/2, 3/2, or + 5/2. Only used when `linear_truncated=True`. + + Returns: + The covariance module and subset_batch_dict. + """ + + if iteration_fidelity is not None and iteration_fidelity < 0: + iteration_fidelity = dim + iteration_fidelity + if data_fidelities is not None: + data_fidelities = list(data_fidelities) + for i in range(len(data_fidelities)): + if data_fidelities[i] < 0: + data_fidelities[i] = dim + data_fidelities[i] + + kernels = [] + + if linear_truncated: + leading_dims = [iteration_fidelity] if iteration_fidelity is not None else [] + trailing_dims = ( + [[i] for i in data_fidelities] if data_fidelities is not None else [[]] + ) + for tdims in trailing_dims: + kernels.append( + LinearTruncatedFidelityKernel( + fidelity_dims=leading_dims + tdims, + dimension=dim, + nu=nu, + batch_shape=aug_batch_shape, + power_prior=GammaPrior(3.0, 3.0), + ) + ) + else: + non_active_dims = set(data_fidelities or []) + if iteration_fidelity is not None: + non_active_dims.add(iteration_fidelity) + active_dimsX = sorted(set(range(dim)) - non_active_dims) + kernels.append( + get_covar_module_with_dim_scaled_prior( + ard_num_dims=len(active_dimsX), + batch_shape=aug_batch_shape, + active_dims=active_dimsX, + ) + ) + if iteration_fidelity is not None: + kernels.append( + ExponentialDecayKernel( + batch_shape=aug_batch_shape, + lengthscale_prior=GammaPrior(3.0, 6.0), + offset_prior=GammaPrior(3.0, 6.0), + power_prior=GammaPrior(3.0, 6.0), + active_dims=[iteration_fidelity], + ) + ) + if data_fidelities is not None: + for data_fidelity in data_fidelities: + kernels.append( + DownsamplingKernel( + batch_shape=aug_batch_shape, + offset_prior=GammaPrior(3.0, 6.0), + power_prior=GammaPrior(3.0, 6.0), + active_dims=[data_fidelity], + ) + ) + + kernel = ProductKernel(*kernels) + + covar_module = ScaleKernel( + kernel, batch_shape=aug_batch_shape, outputscale_prior=GammaPrior(2.0, 0.15) + ) + + key_prefix = "covar_module.base_kernel.kernels" + if linear_truncated: + subset_batch_dict = {} + for i in range(len(kernels)): + subset_batch_dict.update( + { + f"{key_prefix}.{i}.raw_power": -2, + f"{key_prefix}.{i}.covar_module_unbiased.raw_lengthscale": -3, + f"{key_prefix}.{i}.covar_module_biased.raw_lengthscale": -3, + } + ) + else: + subset_batch_dict = { + f"{key_prefix}.0.raw_lengthscale": -3, + } + + if iteration_fidelity is not None: + subset_batch_dict.update( + { + f"{key_prefix}.1.raw_power": -2, + f"{key_prefix}.1.raw_offset": -2, + f"{key_prefix}.1.raw_lengthscale": -3, + } + ) + if data_fidelities is not None: + start_idx = 2 if iteration_fidelity is not None else 1 + for i in range(start_idx, len(data_fidelities) + start_idx): + subset_batch_dict.update( + { + f"{key_prefix}.{i}.raw_power": -2, + f"{key_prefix}.{i}.raw_offset": -2, + } + ) + + return covar_module, subset_batch_dict +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/gp_regression_mixed.html b/website-old/pages/api/_modules/botorch/models/gp_regression_mixed.html new file mode 100644 index 0000000000..c6b7dc8dfd --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/gp_regression_mixed.html @@ -0,0 +1,246 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.gp_regression_mixed

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from typing import Any
+
+import torch
+from botorch.models.gp_regression import SingleTaskGP
+from botorch.models.kernels.categorical import CategoricalKernel
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from botorch.models.utils.gpytorch_modules import get_covar_module_with_dim_scaled_prior
+from botorch.utils.datasets import SupervisedDataset
+from botorch.utils.transforms import normalize_indices
+from botorch.utils.types import _DefaultType, DEFAULT
+from gpytorch.constraints import GreaterThan
+from gpytorch.kernels.kernel import Kernel
+from gpytorch.kernels.scale_kernel import ScaleKernel
+from gpytorch.likelihoods.likelihood import Likelihood
+from torch import Tensor
+
+
+
+[docs] +class MixedSingleTaskGP(SingleTaskGP): + r"""A single-task exact GP model for mixed search spaces. + + This model is similar to `SingleTaskGP`, but supports mixed search spaces, + which combine discrete and continuous features, as well as solely discrete + spaces. It uses a kernel that combines a CategoricalKernel (based on + Hamming distances) and a regular kernel into a kernel of the form + + K((x1, c1), (x2, c2)) = + K_cont_1(x1, x2) + K_cat_1(c1, c2) + + K_cont_2(x1, x2) * K_cat_2(c1, c2) + + where `xi` and `ci` are the continuous and categorical features of the + input, respectively. The suffix `_i` indicates that we fit different + lengthscales for the kernels in the sum and product terms. + + Since this model does not provide gradients for the categorical features, + optimization of the acquisition function will need to be performed in + a mixed fashion, i.e., treating the categorical features properly as + discrete optimization variables. We recommend using `optimize_acqf_mixed.` + + Example: + >>> train_X = torch.cat( + [torch.rand(20, 2), torch.randint(3, (20, 1))], dim=-1) + ) + >>> train_Y = ( + torch.sin(train_X[..., :-1]).sum(dim=1, keepdim=True) + + train_X[..., -1:] + ) + >>> model = MixedSingleTaskGP(train_X, train_Y, cat_dims=[-1]) + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + cat_dims: list[int], + train_Yvar: Tensor | None = None, + cont_kernel_factory: None + | (Callable[[torch.Size, int, list[int]], Kernel]) = None, + likelihood: Likelihood | None = None, + outcome_transform: OutcomeTransform | _DefaultType | None = DEFAULT, + input_transform: InputTransform | None = None, # TODO + ) -> None: + r"""A single-task exact GP model supporting categorical parameters. + + Args: + train_X: A `batch_shape x n x d` tensor of training features. + train_Y: A `batch_shape x n x m` tensor of training observations. + cat_dims: A list of indices corresponding to the columns of + the input `X` that should be considered categorical features. + train_Yvar: An optional `batch_shape x n x m` tensor of observed + measurement noise. + cont_kernel_factory: A method that accepts `batch_shape`, `ard_num_dims`, + and `active_dims` arguments and returns an instantiated GPyTorch + `Kernel` object to be used as the base kernel for the continuous + dimensions. If omitted, this model uses an `RBFKernel` as + the kernel for the ordinal parameters. + likelihood: A likelihood. If omitted, use a standard + GaussianLikelihood with inferred noise level. + outcome_transform: An outcome transform that is applied to the + training data during instantiation and to the posterior during + inference (that is, the `Posterior` obtained by calling + `.posterior` on the model will be on the original scale). We use a + `Standardize` transform if no `outcome_transform` is specified. + Pass down `None` to use no outcome transform. + input_transform: An input transform that is applied in the model's + forward pass. Only input transforms are allowed which do not + transform the categorical dimensions. If you want to use it + for example in combination with a `OneHotToNumeric` input transform + one has to instantiate the transform with `transform_on_train` == False + and pass in the already transformed input. + """ + if len(cat_dims) == 0: + raise ValueError( + "Must specify categorical dimensions for MixedSingleTaskGP" + ) + self._ignore_X_dims_scaling_check = cat_dims + _, aug_batch_shape = self.get_batch_dimensions(train_X=train_X, train_Y=train_Y) + + if cont_kernel_factory is None: + cont_kernel_factory = get_covar_module_with_dim_scaled_prior + + d = train_X.shape[-1] + cat_dims = normalize_indices(indices=cat_dims, d=d) + ord_dims = sorted(set(range(d)) - set(cat_dims)) + if len(ord_dims) == 0: + covar_module = ScaleKernel( + CategoricalKernel( + batch_shape=aug_batch_shape, + ard_num_dims=len(cat_dims), + lengthscale_constraint=GreaterThan(1e-06), + ) + ) + else: + sum_kernel = ScaleKernel( + cont_kernel_factory( + batch_shape=aug_batch_shape, + ard_num_dims=len(ord_dims), + active_dims=ord_dims, + ) + + ScaleKernel( + CategoricalKernel( + batch_shape=aug_batch_shape, + ard_num_dims=len(cat_dims), + active_dims=cat_dims, + lengthscale_constraint=GreaterThan(1e-06), + ) + ) + ) + prod_kernel = ScaleKernel( + cont_kernel_factory( + batch_shape=aug_batch_shape, + ard_num_dims=len(ord_dims), + active_dims=ord_dims, + ) + * CategoricalKernel( + batch_shape=aug_batch_shape, + ard_num_dims=len(cat_dims), + active_dims=cat_dims, + lengthscale_constraint=GreaterThan(1e-06), + ) + ) + covar_module = sum_kernel + prod_kernel + super().__init__( + train_X=train_X, + train_Y=train_Y, + train_Yvar=train_Yvar, + likelihood=likelihood, + covar_module=covar_module, + outcome_transform=outcome_transform, + input_transform=input_transform, + ) + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset, + categorical_features: list[int], + likelihood: Likelihood | None = None, + ) -> dict[str, Any]: + r"""Construct `Model` keyword arguments from a dict of `SupervisedDataset`. + + Args: + training_data: A `SupervisedDataset` containing the training data. + categorical_features: Column indices of categorical features. + likelihood: Optional likelihood used to constuct the model. + """ + base_inputs = super().construct_inputs(training_data=training_data) + return { + **base_inputs, + "cat_dims": categorical_features, + "likelihood": likelihood, + }
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/gpytorch.html b/website-old/pages/api/_modules/botorch/models/gpytorch.html new file mode 100644 index 0000000000..040d722ec5 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/gpytorch.html @@ -0,0 +1,1038 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.gpytorch

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Abstract model class for all GPyTorch-based botorch models.
+
+To implement your own, simply inherit from both the provided classes and a
+GPyTorch Model class such as an ExactGP.
+"""
+
+from __future__ import annotations
+
+import itertools
+import warnings
+from abc import ABC
+from copy import deepcopy
+from typing import Any, TYPE_CHECKING
+
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.errors import (
+    BotorchTensorDimensionError,
+    InputDataError,
+    UnsupportedError,
+)
+from botorch.exceptions.warnings import (
+    _get_single_precision_warning,
+    BotorchTensorDimensionWarning,
+    InputDataWarning,
+)
+from botorch.models.model import Model, ModelList
+from botorch.models.utils import (
+    _make_X_full,
+    add_output_dim,
+    gpt_posterior_settings,
+    mod_batch_shape,
+    multioutput_to_batch_mode_transform,
+)
+from botorch.models.utils.assorted import fantasize as fantasize_flag
+from botorch.posteriors.fully_bayesian import GaussianMixturePosterior
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from botorch.utils.multitask import separate_mtmvn
+from botorch.utils.transforms import is_ensemble
+from gpytorch.distributions import MultitaskMultivariateNormal, MultivariateNormal
+from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood
+from linear_operator.operators import BlockDiagLinearOperator, CatLinearOperator
+from torch import Tensor
+
+if TYPE_CHECKING:
+    from botorch.posteriors.posterior_list import PosteriorList  # pragma: no cover
+    from botorch.posteriors.transformed import TransformedPosterior  # pragma: no cover
+    from gpytorch.likelihoods import Likelihood  # pragma: no cover
+
+
+
+[docs] +class GPyTorchModel(Model, ABC): + r"""Abstract base class for models based on GPyTorch models. + + The easiest way to use this is to subclass a model from a GPyTorch model + class (e.g. an `ExactGP`) and this `GPyTorchModel`. See e.g. `SingleTaskGP`. + """ + + likelihood: Likelihood + + @staticmethod + def _validate_tensor_args( + X: Tensor, Y: Tensor, Yvar: Tensor | None = None, strict: bool = True + ) -> None: + r"""Checks that `Y` and `Yvar` have an explicit output dimension if strict. + Checks that the dtypes of the inputs match, and warns if using float. + + This also checks that `Yvar` has the same trailing dimensions as `Y`. Note + we only infer that an explicit output dimension exists when `X` and `Y` have + the same `batch_shape`. + + Args: + X: A `batch_shape x n x d`-dim Tensor, where `d` is the dimension of + the feature space, `n` is the number of points per batch, and + `batch_shape` is the batch shape (potentially empty). + Y: A `batch_shape' x n x m`-dim Tensor, where `m` is the number of + model outputs, `n'` is the number of points per batch, and + `batch_shape'` is the batch shape of the observations. + Yvar: A `batch_shape' x n x m` tensor of observed measurement noise. + Note: this will be None when using a model that infers the noise + level (e.g. a `SingleTaskGP`). + strict: A boolean indicating whether to check that `Y` and `Yvar` + have an explicit output dimension. + """ + if X.dim() != Y.dim(): + if (X.dim() - Y.dim() == 1) and (X.shape[:-1] == Y.shape): + message = ( + "An explicit output dimension is required for targets." + f" Expected Y with dimension {X.dim()} (got {Y.dim()=})." + ) + else: + message = ( + "Expected X and Y to have the same number of dimensions" + f" (got X with dimension {X.dim()} and Y with dimension" + f" {Y.dim()})." + ) + if strict: + raise BotorchTensorDimensionError(message) + else: + warnings.warn( + "Non-strict enforcement of botorch tensor conventions. The " + "following error would have been raised with strict enforcement: " + f"{message}", + BotorchTensorDimensionWarning, + stacklevel=2, + ) + # Yvar may not have the same batch dimensions, but the trailing dimensions + # of Yvar should be the same as the trailing dimensions of Y. + if Yvar is not None and Y.shape[-(Yvar.dim()) :] != Yvar.shape: + raise BotorchTensorDimensionError( + "An explicit output dimension is required for observation noise." + f" Expected Yvar with shape: {Y.shape[-Yvar.dim() :]} (got" + f" {Yvar.shape})." + ) + # Check the dtypes. + if X.dtype != Y.dtype or (Yvar is not None and Y.dtype != Yvar.dtype): + raise InputDataError( + "Expected all inputs to share the same dtype. Got " + f"{X.dtype} for X, {Y.dtype} for Y, and " + f"{Yvar.dtype if Yvar is not None else None} for Yvar." + ) + if X.dtype != torch.float64: + warnings.warn( + _get_single_precision_warning(str(X.dtype)), + InputDataWarning, + stacklevel=3, # Warn at model constructor call. + ) + + @property + def batch_shape(self) -> torch.Size: + r"""The batch shape of the model. + + This is a batch shape from an I/O perspective, independent of the internal + representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). + For a model with `m` outputs, a `test_batch_shape x q x d`-shaped input `X` + to the `posterior` method returns a Posterior object over an output of + shape `broadcast(test_batch_shape, model.batch_shape) x q x m`. + """ + return self.train_inputs[0].shape[:-2] + + @property + def num_outputs(self) -> int: + r"""The number of outputs of the model.""" + return self._num_outputs + + # pyre-fixme[14]: Inconsistent override. + # `botorch.models.gpytorch.GPyTorchModel.posterior` overrides method defined + # in `Model` inconsistently. Could not find parameter `output_indices` in + # overriding signature. +
+[docs] + def posterior( + self, + X: Tensor, + observation_noise: bool | Tensor = False, + posterior_transform: PosteriorTransform | None = None, + **kwargs: Any, + ) -> GPyTorchPosterior | TransformedPosterior: + r"""Computes the posterior over model outputs at the provided points. + + Args: + X: A `(batch_shape) x q x d`-dim Tensor, where `d` is the dimension + of the feature space and `q` is the number of points considered + jointly. + observation_noise: If True, add the observation noise from the + likelihood to the posterior. If a Tensor, use it directly as the + observation noise (must be of shape `(batch_shape) x q`). It is + assumed to be in the outcome-transformed space if an outcome + transform is used. + posterior_transform: An optional PosteriorTransform. + + Returns: + A `GPyTorchPosterior` object, representing a batch of `b` joint + distributions over `q` points. Includes observation noise if + specified. + """ + self.eval() # make sure model is in eval mode + # input transforms are applied at `posterior` in `eval` mode, and at + # `model.forward()` at the training time + X = self.transform_inputs(X) + with gpt_posterior_settings(): + # NOTE: BoTorch's GPyTorchModels also inherit from GPyTorch's ExactGP, thus + # self(X) calls GPyTorch's ExactGP's __call__, which computes the posterior, + # rather than e.g. SingleTaskGP's forward, which computes the prior. + mvn = self(X) + if observation_noise is not False: + if isinstance(observation_noise, torch.Tensor): + # TODO: Make sure observation noise is transformed correctly + self._validate_tensor_args(X=X, Y=observation_noise) + if observation_noise.size(-1) == 1: + observation_noise = observation_noise.squeeze(-1) + mvn = self.likelihood(mvn, X, noise=observation_noise) + else: + mvn = self.likelihood(mvn, X) + posterior = GPyTorchPosterior(distribution=mvn) + if hasattr(self, "outcome_transform"): + posterior = self.outcome_transform.untransform_posterior(posterior) + if posterior_transform is not None: + return posterior_transform(posterior) + return posterior
+ + +
+[docs] + def condition_on_observations( + self, X: Tensor, Y: Tensor, noise: Tensor | None = None, **kwargs: Any + ) -> Model: + r"""Condition the model on new observations. + + Args: + X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of + the feature space, `n'` is the number of points per batch, and + `batch_shape` is the batch shape (must be compatible with the + batch shape of the model). + Y: A `batch_shape' x n x m`-dim Tensor, where `m` is the number of + model outputs, `n'` is the number of points per batch, and + `batch_shape'` is the batch shape of the observations. + `batch_shape'` must be broadcastable to `batch_shape` using + standard broadcasting semantics. If `Y` has fewer batch dimensions + than `X`, its is assumed that the missing batch dimensions are + the same for all `Y`. + noise: If not `None`, a tensor of the same shape as `Y` representing + the associated noise variance. + kwargs: Passed to `self.get_fantasy_model`. + + Returns: + A `Model` object of the same type, representing the original model + conditioned on the new observations `(X, Y)` (and possibly noise + observations passed in via kwargs). + + Example: + >>> train_X = torch.rand(20, 2) + >>> train_Y = torch.sin(train_X[:, 0]) + torch.cos(train_X[:, 1]) + >>> model = SingleTaskGP(train_X, train_Y) + >>> new_X = torch.rand(5, 2) + >>> new_Y = torch.sin(new_X[:, 0]) + torch.cos(new_X[:, 1]) + >>> model = model.condition_on_observations(X=new_X, Y=new_Y) + """ + Yvar = noise + + if hasattr(self, "outcome_transform"): + # pass the transformed data to get_fantasy_model below + # (unless we've already trasnformed if BatchedMultiOutputGPyTorchModel) + if not isinstance(self, BatchedMultiOutputGPyTorchModel): + # `noise` is assumed to already be outcome-transformed. + Y, _ = self.outcome_transform(Y=Y, Yvar=Yvar) + # Validate using strict=False, since we cannot tell if Y has an explicit + # output dimension. Do not check shapes when fantasizing as they are + # not expected to match. + if fantasize_flag.off(): + self._validate_tensor_args(X=X, Y=Y, Yvar=Yvar, strict=False) + if Y.size(-1) == 1: + Y = Y.squeeze(-1) + if Yvar is not None: + kwargs.update({"noise": Yvar.squeeze(-1)}) + # get_fantasy_model will properly copy any existing outcome transforms + # (since it deepcopies the original model) + + return self.get_fantasy_model(inputs=X, targets=Y, **kwargs)
+
+ + + +# pyre-fixme[13]: uninitialized attributes _num_outputs, _input_batch_shape, +# _aug_batch_shape +
+[docs] +class BatchedMultiOutputGPyTorchModel(GPyTorchModel): + r"""Base class for batched multi-output GPyTorch models with independent outputs. + + This model should be used when the same training data is used for all outputs. + Outputs are modeled independently by using a different batch for each output. + """ + + _num_outputs: int + _input_batch_shape: torch.Size + _aug_batch_shape: torch.Size + +
+[docs] + @staticmethod + def get_batch_dimensions( + train_X: Tensor, train_Y: Tensor + ) -> tuple[torch.Size, torch.Size]: + r"""Get the raw batch shape and output-augmented batch shape of the inputs. + + Args: + train_X: A `n x d` or `batch_shape x n x d` (batch mode) tensor of training + features. + train_Y: A `n x m` or `batch_shape x n x m` (batch mode) tensor of + training observations. + + Returns: + 2-element tuple containing + + - The `input_batch_shape` + - The output-augmented batch shape: `input_batch_shape x (m)` + """ + input_batch_shape = train_X.shape[:-2] + aug_batch_shape = input_batch_shape + num_outputs = train_Y.shape[-1] + if num_outputs > 1: + aug_batch_shape += torch.Size([num_outputs]) + return input_batch_shape, aug_batch_shape
+ + + def _set_dimensions(self, train_X: Tensor, train_Y: Tensor) -> None: + r"""Store the number of outputs and the batch shape. + + Args: + train_X: A `n x d` or `batch_shape x n x d` (batch mode) tensor of training + features. + train_Y: A `n x m` or `batch_shape x n x m` (batch mode) tensor of + training observations. + """ + self._num_outputs = train_Y.shape[-1] + self._input_batch_shape, self._aug_batch_shape = self.get_batch_dimensions( + train_X=train_X, train_Y=train_Y + ) + + @property + def batch_shape(self) -> torch.Size: + r"""The batch shape of the model. + + This is a batch shape from an I/O perspective, independent of the internal + representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). + For a model with `m` outputs, a `test_batch_shape x q x d`-shaped input `X` + to the `posterior` method returns a Posterior object over an output of + shape `broadcast(test_batch_shape, model.batch_shape) x q x m`. + """ + return self._input_batch_shape + + def _transform_tensor_args( + self, X: Tensor, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor, Tensor | None]: + r"""Transforms tensor arguments: for single output models, the output + dimension is squeezed and for multi-output models, the output dimension is + transformed into the left-most batch dimension. + + Args: + X: A `n x d` or `batch_shape x n x d` (batch mode) tensor of training + features. + Y: A `n x m` or `batch_shape x n x m` (batch mode) tensor of + training observations. + Yvar: A `n x m` or `batch_shape x n x m` (batch mode) tensor of + observed measurement noise. Note: this will be None when using a model + that infers the noise level (e.g. a `SingleTaskGP`). + + Returns: + 3-element tuple containing + + - A `input_batch_shape x (m) x n x d` tensor of training features. + - A `target_batch_shape x (m) x n` tensor of training observations. + - A `target_batch_shape x (m) x n` tensor observed measurement noise + (or None). + """ + if self._num_outputs > 1: + return multioutput_to_batch_mode_transform( + train_X=X, train_Y=Y, train_Yvar=Yvar, num_outputs=self._num_outputs + ) + return X, Y.squeeze(-1), None if Yvar is None else Yvar.squeeze(-1) + + def _apply_noise( + self, + X: Tensor, + mvn: MultivariateNormal, + observation_noise: bool | Tensor = False, + ) -> MultivariateNormal: + """Adds the observation noise to the posterior. + + Args: + X: A tensor of shape `batch_shape x q x d`. + mvn: A `MultivariateNormal` object representing the posterior over the true + latent function. + num_outputs: The number of outputs of the model. + observation_noise: If True, add the observation noise from the + likelihood to the posterior. If a Tensor, use it directly as the + observation noise (must be of shape `(batch_shape) x q x m`). + + Returns: + The posterior predictive. + """ + if observation_noise is False: + return mvn + # noise_shape is `broadcast(test_batch_shape, model.batch_shape) x m x q` + noise_shape = mvn.batch_shape + mvn.event_shape + if torch.is_tensor(observation_noise): + # TODO: Validate noise shape + # make observation_noise's shape match noise_shape + if self.num_outputs > 1: + obs_noise = observation_noise.transpose(-1, -2) + else: + obs_noise = observation_noise.squeeze(-1) + mvn = self.likelihood( + mvn, + X, + noise=obs_noise.expand(noise_shape), + ) + elif isinstance(self.likelihood, FixedNoiseGaussianLikelihood): + # Use the mean of the previous noise values (TODO: be smarter here). + observation_noise = self.likelihood.noise.mean(dim=-1, keepdim=True) + mvn = self.likelihood( + mvn, + X, + noise=observation_noise.expand(noise_shape), + ) + else: + mvn = self.likelihood(mvn, X) + return mvn + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool | Tensor = False, + posterior_transform: PosteriorTransform | None = None, + ) -> GPyTorchPosterior | TransformedPosterior: + r"""Computes the posterior over model outputs at the provided points. + + Args: + X: A `(batch_shape) x q x d`-dim Tensor, where `d` is the dimension + of the feature space and `q` is the number of points considered + jointly. + output_indices: A list of indices, corresponding to the outputs over + which to compute the posterior (if the model is multi-output). + Can be used to speed up computation if only a subset of the + model's outputs are required for optimization. If omitted, + computes the posterior over all model outputs. + observation_noise: If True, add the observation noise from the + likelihood to the posterior. If a Tensor, use it directly as the + observation noise (must be of shape `(batch_shape) x q x m`). + posterior_transform: An optional PosteriorTransform. + + Returns: + A `GPyTorchPosterior` object, representing `batch_shape` joint + distributions over `q` points and the outputs selected by + `output_indices` each. Includes observation noise if specified. + """ + self.eval() # make sure model is in eval mode + # input transforms are applied at `posterior` in `eval` mode, and at + # `model.forward()` at the training time + X = self.transform_inputs(X) + with gpt_posterior_settings(): + # insert a dimension for the output dimension + if self._num_outputs > 1: + X, output_dim_idx = add_output_dim( + X=X, original_batch_shape=self._input_batch_shape + ) + # NOTE: BoTorch's GPyTorchModels also inherit from GPyTorch's ExactGP, thus + # self(X) calls GPyTorch's ExactGP's __call__, which computes the posterior, + # rather than e.g. SingleTaskGP's forward, which computes the prior. + mvn = self(X) + mvn = self._apply_noise(X=X, mvn=mvn, observation_noise=observation_noise) + if self._num_outputs > 1: + if torch.jit.is_tracing(): + mvn = MultitaskMultivariateNormal.from_batch_mvn( + mvn, task_dim=output_dim_idx + ) + else: + mean_x = mvn.mean + covar_x = mvn.lazy_covariance_matrix + output_indices = output_indices or range(self._num_outputs) + mvns = [ + MultivariateNormal( + mean_x.select(dim=output_dim_idx, index=t), + covar_x[(slice(None),) * output_dim_idx + (t,)], + ) + for t in output_indices + ] + mvn = MultitaskMultivariateNormal.from_independent_mvns(mvns=mvns) + + posterior = GPyTorchPosterior(distribution=mvn) + if hasattr(self, "outcome_transform"): + posterior = self.outcome_transform.untransform_posterior(posterior) + if posterior_transform is not None: + return posterior_transform(posterior) + return posterior
+ + +
+[docs] + def condition_on_observations( + self, X: Tensor, Y: Tensor, **kwargs: Any + ) -> BatchedMultiOutputGPyTorchModel: + r"""Condition the model on new observations. + + Args: + X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of + the feature space, `m` is the number of points per batch, and + `batch_shape` is the batch shape (must be compatible with the + batch shape of the model). + Y: A `batch_shape' x n' x m`-dim Tensor, where `m` is the number of + model outputs, `n'` is the number of points per batch, and + `batch_shape'` is the batch shape of the observations. + `batch_shape'` must be broadcastable to `batch_shape` using + standard broadcasting semantics. If `Y` has fewer batch dimensions + than `X`, its is assumed that the missing batch dimensions are + the same for all `Y`. + + Returns: + A `BatchedMultiOutputGPyTorchModel` object of the same type with + `n + n'` training examples, representing the original model + conditioned on the new observations `(X, Y)` (and possibly noise + observations passed in via kwargs). + + Example: + >>> train_X = torch.rand(20, 2) + >>> train_Y = torch.cat( + >>> [torch.sin(train_X[:, 0]), torch.cos(train_X[:, 1])], -1 + >>> ) + >>> model = SingleTaskGP(train_X, train_Y) + >>> new_X = torch.rand(5, 2) + >>> new_Y = torch.cat([torch.sin(new_X[:, 0]), torch.cos(new_X[:, 1])], -1) + >>> model = model.condition_on_observations(X=new_X, Y=new_Y) + """ + noise = kwargs.get("noise") + if hasattr(self, "outcome_transform"): + # We need to apply transforms before shifting batch indices around. + # `noise` is assumed to already be outcome-transformed. + Y, _ = self.outcome_transform(Y) + # Do not check shapes when fantasizing as they are not expected to match. + if fantasize_flag.off(): + self._validate_tensor_args(X=X, Y=Y, Yvar=noise, strict=False) + inputs = X + if self._num_outputs > 1: + inputs, targets, noise = multioutput_to_batch_mode_transform( + train_X=X, train_Y=Y, num_outputs=self._num_outputs, train_Yvar=noise + ) + # `multioutput_to_batch_mode_transform` removes the output dimension, + # which is necessary for `condition_on_observations` + targets = targets.unsqueeze(-1) + if noise is not None: + noise = noise.unsqueeze(-1) + else: + inputs = X + targets = Y + if noise is not None: + kwargs.update({"noise": noise}) + fantasy_model = super().condition_on_observations(X=inputs, Y=targets, **kwargs) + fantasy_model._input_batch_shape = fantasy_model.train_targets.shape[ + : (-1 if self._num_outputs == 1 else -2) + ] + if not self._is_fully_bayesian: + fantasy_model._aug_batch_shape = fantasy_model.train_targets.shape[:-1] + return fantasy_model
+ + +
+[docs] + def subset_output(self, idcs: list[int]) -> BatchedMultiOutputGPyTorchModel: + r"""Subset the model along the output dimension. + + Args: + idcs: The output indices to subset the model to. + + Returns: + The current model, subset to the specified output indices. + """ + try: + subset_batch_dict = self._subset_batch_dict + except AttributeError: + raise NotImplementedError( + "`subset_output` requires the model to define a `_subset_batch_dict` " + "attribute that lists the indices of the output dimensions in each " + "model parameter that needs to be subset." + ) + + m = len(idcs) + new_model = deepcopy(self) + + subset_everything = self.num_outputs == m and idcs == list(range(m)) + if subset_everything: + return new_model + + tidxr = torch.tensor(idcs, device=new_model.train_targets.device) + idxr = tidxr if m > 1 else idcs[0] + new_tail_bs = torch.Size([m]) if m > 1 else torch.Size() + + new_model._num_outputs = m + new_model._aug_batch_shape = new_model._aug_batch_shape[:-1] + new_tail_bs + new_model.train_inputs = tuple( + ti[..., idxr, :, :] for ti in new_model.train_inputs + ) + new_model.train_targets = new_model.train_targets[..., idxr, :] + + # adjust batch shapes of parameters/buffers if necessary + for full_name, p in itertools.chain( + new_model.named_parameters(), new_model.named_buffers() + ): + if full_name in subset_batch_dict: + idx = subset_batch_dict[full_name] + new_data = p.index_select(dim=idx, index=tidxr) + if m == 1: + new_data = new_data.squeeze(idx) + p.data = new_data + mod_name = full_name.split(".")[:-1] + mod_batch_shape(new_model, mod_name, m if m > 1 else 0) + + # subset outcome transform if present + try: + subset_octf = new_model.outcome_transform.subset_output(idcs=idcs) + new_model.outcome_transform = subset_octf + except AttributeError: + pass + + # Subset fixed noise likelihood if present. + if isinstance(self.likelihood, FixedNoiseGaussianLikelihood): + full_noise = new_model.likelihood.noise_covar.noise + new_noise = full_noise[..., idcs if len(idcs) > 1 else idcs[0], :] + new_model.likelihood.noise_covar.noise = new_noise + + return new_model
+
+ + + +
+[docs] +class ModelListGPyTorchModel(ModelList, GPyTorchModel, ABC): + r"""Abstract base class for models based on multi-output GPyTorch models. + + This is meant to be used with a gpytorch ModelList wrapper for independent + evaluation of submodels. Those submodels can themselves be multi-output + models, in which case the task covariances will be ignored. + """ + + @property + def batch_shape(self) -> torch.Size: + r"""The batch shape of the model. + + This is a batch shape from an I/O perspective, independent of the internal + representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). + For a model with `m` outputs, a `test_batch_shape x q x d`-shaped input `X` + to the `posterior` method returns a Posterior object over an output of + shape `broadcast(test_batch_shape, model.batch_shape) x q x m`. + """ + batch_shapes = {m.batch_shape for m in self.models} + if len(batch_shapes) > 1: + msg = ( + f"Component models of {self.__class__.__name__} have different " + "batch shapes" + ) + try: + broadcast_shape = torch.broadcast_shapes(*batch_shapes) + warnings.warn(msg + ". Broadcasting batch shapes.", stacklevel=2) + return broadcast_shape + except RuntimeError: + raise NotImplementedError(msg + " that are not broadcastble.") + return next(iter(batch_shapes)) + + # pyre-fixme[15]: Inconsistent override in return types +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool | Tensor = False, + posterior_transform: PosteriorTransform | None = None, + ) -> GPyTorchPosterior | PosteriorList: + r"""Computes the posterior over model outputs at the provided points. + If any model returns a MultitaskMultivariateNormal posterior, then that + will be split into individual MVNs per task, with inter-task covariance + ignored. + + Args: + X: A `b x q x d`-dim Tensor, where `d` is the dimension of the + feature space, `q` is the number of points considered jointly, + and `b` is the batch dimension. + output_indices: A list of indices, corresponding to the outputs over + which to compute the posterior (if the model is multi-output). + Can be used to speed up computation if only a subset of the + model's outputs are required for optimization. If omitted, + computes the posterior over all model outputs. + observation_noise: If True, add the observation noise from the + respective likelihoods to the posterior. If a Tensor of shape + `(batch_shape) x q x m`, use it directly as the observation + noise (with `observation_noise[...,i]` added to the posterior + of the `i`-th model). + posterior_transform: An optional PosteriorTransform. + + Returns: + - If no `posterior_transform` is provided and the component models have no + `outcome_transform`, or if the component models only use linear outcome + transforms like `Standardize` (i.e. not `Log`), returns a + `GPyTorchPosterior` or `GaussianMixturePosterior` object, + representing `batch_shape` joint distributions over `q` points + and the outputs selected by `output_indices` each. Includes + measurement noise if `observation_noise` is specified. + - If no `posterior_transform` is provided and component models have + nonlinear transforms like `Log`, returns a `PosteriorList` with + sub-posteriors of type `TransformedPosterior` + - If `posterior_transform` is provided, that posterior transform will be + applied and will determine the return type. This could potentially be + any subclass of `Posterior`, but common choices give a + `GPyTorchPosterior`. + """ + + # Nonlinear transforms untransform to a `TransformedPosterior`, + # which can't be made into a `GPyTorchPosterior` + returns_untransformed = any( + hasattr(mod, "outcome_transform") and (not mod.outcome_transform._is_linear) + for mod in self.models + ) + # NOTE: We're not passing in the posterior transform here. We'll apply it later. + posterior = ModelList.posterior( + self, + X=X, + output_indices=output_indices, + observation_noise=observation_noise, + ) + if not returns_untransformed: + mvns = [p.distribution for p in posterior.posteriors] + if any(isinstance(m, MultitaskMultivariateNormal) for m in mvns): + mvn_list = [] + for mvn in mvns: + if len(mvn.event_shape) == 2: + # We separate MTMVNs into independent-across-task MVNs for + # the convenience of using BlockDiagLinearOperator below. + # (b x q x m x m) -> list of m (b x q x 1 x 1) + mvn_list.extend(separate_mtmvn(mvn)) + else: + mvn_list.append(mvn) + mean = torch.stack([mvn.mean for mvn in mvn_list], dim=-1) + covars = CatLinearOperator( + *[mvn.lazy_covariance_matrix.unsqueeze(-3) for mvn in mvn_list], + dim=-3, + ) # List of m (b x q x 1 x 1) -> (b x q x m x 1 x 1) + mvn = MultitaskMultivariateNormal( + mean=mean, + covariance_matrix=BlockDiagLinearOperator(covars, block_dim=-3).to( + X + ), # (b x q x m x 1 x 1) -> (b x q x m x m) + interleaved=False, + ) + else: + mvn = ( + mvns[0] + if len(mvns) == 1 + else MultitaskMultivariateNormal.from_independent_mvns(mvns=mvns) + ) + # Return the result as a GPyTorchPosterior/GaussianMixturePosterior. + if any(is_ensemble(m) for m in self.models): + # Mixing fully Bayesian and other GP models is currently + # not supported. + posterior = GaussianMixturePosterior(distribution=mvn) + else: + posterior = GPyTorchPosterior(distribution=mvn) + if posterior_transform is not None: + return posterior_transform(posterior) + return posterior
+ + +
+[docs] + def condition_on_observations(self, X: Tensor, Y: Tensor, **kwargs: Any) -> Model: + raise NotImplementedError()
+
+ + + +
+[docs] +class MultiTaskGPyTorchModel(GPyTorchModel, ABC): + r"""Abstract base class for multi-task models based on GPyTorch models. + + This class provides the `posterior` method to models that implement a + "long-format" multi-task GP in the style of `MultiTaskGP`. + """ + + def _map_tasks(self, task_values: Tensor) -> Tensor: + """Map raw task values to the task indices used by the model. + + Args: + task_values: A tensor of task values. + + Returns: + A tensor of task indices with the same shape as the input + tensor. + """ + if self._task_mapper is None: + if not ( + torch.all(0 <= task_values) and torch.all(task_values < self.num_tasks) + ): + raise ValueError( + "Expected all task features in `X` to be between 0 and " + f"self.num_tasks - 1. Got {task_values}." + ) + else: + task_values = task_values.long() + + unexpected_task_values = set(task_values.unique().tolist()).difference( + self._expected_task_values + ) + if len(unexpected_task_values) > 0: + raise ValueError( + "Received invalid raw task values. Expected raw value to be in" + f" {self._expected_task_values}, but got unexpected task values:" + f" {unexpected_task_values}." + ) + task_values = self._task_mapper[task_values] + return task_values + + def _apply_noise( + self, + X: Tensor, + mvn: MultivariateNormal, + num_outputs: int, + observation_noise: bool | Tensor, + ) -> MultivariateNormal: + """Adds the observation noise to the posterior. + + If the likelihood is a `FixedNoiseGaussianLikelihood`, then + the average noise per task is computed, and a diagonal noise + matrix is added to the posterior covariance matrix, where + the noise per input is the average noise for its respective + task. If the likelihood is a Gaussian likelihood, then + currently there is a shared inferred noise level for all + tasks. + + TODO: implement support for task-specific inferred noise levels. + + Args: + X: A tensor of shape `batch_shape x q x d + 1`, + where `d` is the dimension of the feature space and the `+ 1` + dimension is the task feature / index. + mvn: A `MultivariateNormal` object representing the posterior over the true + latent function. + num_outputs: The number of outputs of the model. + observation_noise: If True, add observation noise from the respective + likelihood. Tensor input is currently not supported. + + Returns: + The posterior predictive. + """ + if torch.is_tensor(observation_noise): + raise NotImplementedError( + "Passing a tensor of observations is not supported by MultiTaskGP." + ) + elif observation_noise is False: + return mvn + elif isinstance(self.likelihood, FixedNoiseGaussianLikelihood): + # get task features for test points + test_task_features = X[..., self._task_feature] + test_task_features = self._map_tasks(test_task_features).long() + unique_test_task_features = test_task_features.unique() + # get task features for training points + train_task_features = self.train_inputs[0][..., self._task_feature] + train_task_features = self._map_tasks(train_task_features).long() + noise_by_task = torch.zeros(self.num_tasks, dtype=X.dtype, device=X.device) + for task_feature in unique_test_task_features: + mask = train_task_features == task_feature + noise_by_task[task_feature] = self.likelihood.noise[mask].mean( + dim=-1, keepdim=True + ) + # noise_shape is `broadcast(test_batch_shape, model.batch_shape) x q` + noise_shape = X.shape[:-1] + observation_noise = noise_by_task[test_task_features].expand(noise_shape) + return self.likelihood( + mvn, + X, + noise=observation_noise, + ) + return self.likelihood(mvn, X) + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool | Tensor = False, + posterior_transform: PosteriorTransform | None = None, + ) -> GPyTorchPosterior | TransformedPosterior: + r"""Computes the posterior over model outputs at the provided points. + + Args: + X: A tensor of shape `batch_shape x q x d` or `batch_shape x q x (d + 1)`, + where `d` is the dimension of the feature space (not including task + indices) and `q` is the number of points considered jointly. The `+ 1` + dimension is the optional task feature / index. If given, the model + produces the outputs for the given task indices. If omitted, the + model produces outputs for tasks in in `self._output_tasks` (specified + as `output_tasks` while constructing the model), which can overwritten + using `output_indices`. + output_indices: A list of task values over which to compute the posterior. + Only used if `X` does not include the task feature. If omitted, + defaults to `self._output_tasks`. + observation_noise: If True, add observation noise from the respective + likelihoods. If a Tensor, specifies the observation noise levels + to add. + posterior_transform: An optional PosteriorTransform. + + Returns: + A `GPyTorchPosterior` object, representing `batch_shape` joint + distributions over `q` points. If the task features are included in `X`, + the posterior will be single output. Otherwise, the posterior will be + single or multi output corresponding to the tasks included in + either the `output_indices` or `self._output_tasks`. + """ + includes_task_feature = X.shape[-1] == self.num_non_task_features + 1 + if includes_task_feature: + if output_indices is not None: + raise ValueError( + "`output_indices` must be None when `X` includes task features." + ) + task_features = X[..., self._task_feature].unique() + num_outputs = 1 + X_full = X + else: + # Add the task features to construct the full X for evaluation. + task_features = torch.tensor( + self._output_tasks if output_indices is None else output_indices, + dtype=torch.long, + device=X.device, + ) + num_outputs = len(task_features) + X_full = _make_X_full( + X=X, output_indices=task_features.tolist(), tf=self._task_feature + ) + # Make sure all task feature values are valid. + task_features = self._map_tasks(task_values=task_features) + self.eval() # make sure model is in eval mode + # input transforms are applied at `posterior` in `eval` mode, and at + # `model.forward()` at the training time + X_full = self.transform_inputs(X_full) + with gpt_posterior_settings(): + mvn = self(X_full) + mvn = self._apply_noise( + X=X_full, + mvn=mvn, + num_outputs=num_outputs, + observation_noise=observation_noise, + ) + # If single-output, return the posterior of a single-output model + if num_outputs == 1: + posterior = GPyTorchPosterior(distribution=mvn) + else: + # Otherwise, make a MultitaskMultivariateNormal out of this + mtmvn = MultitaskMultivariateNormal( + mean=mvn.mean.view(*mvn.mean.shape[:-1], num_outputs, -1).transpose( + -1, -2 + ), + covariance_matrix=mvn.lazy_covariance_matrix, + interleaved=False, + ) + posterior = GPyTorchPosterior(distribution=mtmvn) + if hasattr(self, "outcome_transform"): + posterior = self.outcome_transform.untransform_posterior(posterior) + if posterior_transform is not None: + return posterior_transform(posterior) + return posterior
+ + +
+[docs] + def subset_output(self, idcs: list[int]) -> MultiTaskGPyTorchModel: + r"""Returns a new model that only outputs a subset of the outputs. + + Args: + idcs: A list of output indices, corresponding to the outputs to keep. + + Returns: + A new model that only outputs the requested outputs. + """ + raise UnsupportedError( + "Subsetting outputs is not supported by `MultiTaskGPyTorchModel`." + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/higher_order_gp.html b/website-old/pages/api/_modules/botorch/models/higher_order_gp.html new file mode 100644 index 0000000000..4db2f90d9b --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/higher_order_gp.html @@ -0,0 +1,677 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.higher_order_gp

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+References
+
+.. [Zhe2019hogp]
+    S. Zhe, W. Xing, and R. M. Kirby. Scalable high-order gaussian process regression.
+    Proceedings of Machine Learning Research, volume 89, Apr 2019.
+"""
+
+from __future__ import annotations
+
+import warnings
+from contextlib import ExitStack
+from typing import Any
+
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel
+from botorch.models.model import FantasizeMixin
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform, Standardize
+from botorch.models.utils import gpt_posterior_settings
+from botorch.models.utils.assorted import fantasize as fantasize_flag
+from botorch.models.utils.gpytorch_modules import (
+    get_covar_module_with_dim_scaled_prior,
+    get_gaussian_likelihood_with_lognormal_prior,
+)
+from botorch.posteriors import (
+    GPyTorchPosterior,
+    HigherOrderGPPosterior,
+    TransformedPosterior,
+)
+from botorch.utils.types import _DefaultType, DEFAULT
+from gpytorch.distributions import MultivariateNormal
+from gpytorch.kernels import Kernel
+from gpytorch.likelihoods import Likelihood
+from gpytorch.models import ExactGP
+from gpytorch.priors.torch_priors import MultivariateNormalPrior
+from gpytorch.settings import fast_pred_var, skip_posterior_variances
+from linear_operator.operators import (
+    BatchRepeatLinearOperator,
+    DiagLinearOperator,
+    KroneckerProductLinearOperator,
+    LinearOperator,
+    ZeroLinearOperator,
+)
+from linear_operator.settings import _fast_solves
+from torch import Tensor
+from torch.nn import ModuleList, Parameter, ParameterList
+
+
+
+[docs] +class FlattenedStandardize(Standardize): + r""" + Standardize outcomes in a structured multi-output settings by reshaping the + batched output dimensions to be a vector. Specifically, an output dimension + of [a x b x c] will be squeezed to be a vector of [a * b * c]. + """ + + def __init__( + self, + output_shape: torch.Size, + batch_shape: torch.Size | None = None, + min_stdv: float = 1e-8, + ): + r""" + Args: + output_shape: A `n x output_shape`-dim tensor of training targets. + batch_shape: The batch_shape of the training targets. + min_stddv: The minimum standard deviation for which to perform + standardization (if lower, only de-mean the data). + """ + if batch_shape is None: + batch_shape = torch.Size() + + super().__init__(m=1, outputs=None, batch_shape=batch_shape, min_stdv=min_stdv) + + self.output_shape = output_shape + self.batch_shape = batch_shape + + def _squeeze_to_single_output(self, tsr: Tensor) -> Tensor: + dim_ct = tsr.ndim - len(self.output_shape) - 1 + return tsr.reshape(*tsr.shape[:dim_ct], -1, 1) + + def _return_to_output_shape(self, tsr: Tensor) -> Tensor: + out = tsr.reshape(*tsr.shape[:-2], -1, *self.output_shape) + return out + +
+[docs] + def forward( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + Y = self._squeeze_to_single_output(Y) + if Yvar is not None: + Yvar = self._squeeze_to_single_output(Yvar) + + Y, Yvar = super().forward(Y, Yvar) + Y_out = self._return_to_output_shape(Y) + + if Yvar is not None: + Yvar_out = self._return_to_output_shape(Yvar) + else: + Yvar_out = None + return Y_out, Yvar_out
+ + +
+[docs] + def untransform( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + Y = self._squeeze_to_single_output(Y) + if Yvar is not None: + Yvar = self._squeeze_to_single_output(Yvar) + + Y, Yvar = super().untransform(Y, Yvar) + + Y = self._return_to_output_shape(Y) + if Yvar is not None: + Yvar = self._return_to_output_shape(Yvar) + return Y, Yvar
+ + +
+[docs] + def untransform_posterior( + self, posterior: HigherOrderGPPosterior + ) -> TransformedPosterior: + # TODO: return a HigherOrderGPPosterior once rescaling constant + # muls * LinearOperators won't force a dense decomposition rather than a + # Kronecker structured one. + return TransformedPosterior( + posterior=posterior, + sample_transform=lambda s: self._return_to_output_shape( + self.means + self.stdvs * self._squeeze_to_single_output(s) + ), + mean_transform=lambda m, v: self._return_to_output_shape( + self.means + self.stdvs * self._squeeze_to_single_output(m) + ), + variance_transform=lambda m, v: self._return_to_output_shape( + self._stdvs_sq * self._squeeze_to_single_output(v) + ), + )
+
+ + + +
+[docs] +class HigherOrderGP(BatchedMultiOutputGPyTorchModel, ExactGP, FantasizeMixin): + r""" + A model for high-dimensional output regression. + + As described in [Zhe2019hogp]_. “Higher-order” means that the predictions + are matrices (tensors) with at least two dimensions, such as images or + grids of images, or measurements taken from a region of at least two + dimensions. + The posterior uses Matheron's rule [Doucet2010sampl]_ + as described in [Maddox2021bohdo]_. + + `HigherOrderGP` differs from a "vector” multi-output model in that it uses + Kronecker algebra to obtain parsimonious covariance matrices for these + outputs (see `KroneckerMultiTaskGP` for more information). For example, + imagine a 10 x 20 x 30 grid of images. If we were to vectorize the + resulting 6,000 data points in order to use them in a non-higher-order GP, + they would have a 6,000 x 6,000 covariance matrix, with 36 million entries. + The Kronecker structure allows representing this as a product of 10x10, + 20x20, and 30x30 covariance matrices, with only 1,400 entries. + + NOTE: This model requires the use of specialized Kronecker solves in + linear operator, which are disabled by default in BoTorch. These are enabled + by default in the `HigherOrderGP.posterior` call. However, they need to be + manually enabled by the user during model fitting. + + Example: + >>> from linear_operator.settings import _fast_solves + >>> model = SingleTaskGP(train_X, train_Y) + >>> mll = ExactMarginalLogLikelihood(model.likelihood, model) + >>> with _fast_solves(True): + >>> fit_gpytorch_mll_torch(mll) + >>> samples = model.posterior(test_X).rsample() + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + likelihood: Likelihood | None = None, + covar_modules: list[Kernel] | None = None, + num_latent_dims: list[int] | None = None, + learn_latent_pars: bool = True, + latent_init: str = "default", + outcome_transform: OutcomeTransform | _DefaultType | None = DEFAULT, + input_transform: InputTransform | None = None, + ): + r""" + Args: + train_X: A `batch_shape x n x d`-dim tensor of training inputs. + train_Y: A `batch_shape x n x output_shape`-dim tensor of training targets. + likelihood: Gaussian likelihood for the model. + covar_modules: List of kernels for each output structure. + num_latent_dims: Sizes for the latent dimensions. + learn_latent_pars: If true, learn the latent parameters. + latent_init: [default or gp] how to initialize the latent parameters. + """ + if input_transform is not None: + input_transform.to(train_X) + + # infer the dimension of `output_shape`. + num_output_dims = train_Y.dim() - train_X.dim() + 1 + batch_shape = train_X.shape[:-2] + if len(batch_shape) > 1: + raise NotImplementedError( + "HigherOrderGP currently only supports 1-dim `batch_shape`." + ) + if outcome_transform == DEFAULT: + outcome_transform = FlattenedStandardize( + output_shape=train_Y.shape[-num_output_dims:], + batch_shape=batch_shape, + ) + if outcome_transform is not None: + if isinstance(outcome_transform, Standardize) and not isinstance( + outcome_transform, FlattenedStandardize + ): + warnings.warn( + "HigherOrderGP does not support the outcome_transform " + "`Standardize`! Using `FlattenedStandardize` with `output_shape=" + f"{train_Y.shape[- num_output_dims:]} and batch_shape=" + f"{batch_shape} instead.", + RuntimeWarning, + stacklevel=2, + ) + outcome_transform = FlattenedStandardize( + output_shape=train_Y.shape[-num_output_dims:], + batch_shape=batch_shape, + ) + train_Y, _ = outcome_transform(train_Y) + + self._aug_batch_shape = batch_shape + self._num_dimensions = num_output_dims + 1 + self._num_outputs = train_Y.shape[0] if batch_shape else 1 + self.target_shape = train_Y.shape[-num_output_dims:] + self._input_batch_shape = batch_shape + + if likelihood is None: + likelihood = get_gaussian_likelihood_with_lognormal_prior( + batch_shape=self._aug_batch_shape + ) + else: + self._is_custom_likelihood = True + + super().__init__( + train_X, + train_Y.view(*self._aug_batch_shape, -1), + likelihood=likelihood, + ) + + if covar_modules is not None: + self.covar_modules = ModuleList(covar_modules) + else: + self.covar_modules = ModuleList( + [ + get_covar_module_with_dim_scaled_prior( + ard_num_dims=1 if dim > 0 else train_X.shape[-1], + batch_shape=self._aug_batch_shape, + ) + for dim in range(self._num_dimensions) + ] + ) + + if num_latent_dims is None: + num_latent_dims = [1] * (self._num_dimensions - 1) + + self.to(train_X) + + self._initialize_latents( + latent_init=latent_init, + num_latent_dims=num_latent_dims, + learn_latent_pars=learn_latent_pars, + device=train_Y.device, + dtype=train_Y.dtype, + ) + + if outcome_transform is not None: + self.outcome_transform = outcome_transform + if input_transform is not None: + self.input_transform = input_transform + + def _initialize_latents( + self, + latent_init: str, + num_latent_dims: list[int], + learn_latent_pars: bool, + device: torch.device, + dtype: torch.dtype, + ): + self.latent_parameters = ParameterList() + if latent_init == "default": + for dim_num in range(len(self.covar_modules) - 1): + self.latent_parameters.append( + Parameter( + torch.rand( + *self._aug_batch_shape, + self.target_shape[dim_num], + num_latent_dims[dim_num], + device=device, + dtype=dtype, + ), + requires_grad=learn_latent_pars, + ) + ) + elif latent_init == "gp": + for dim_num, covar in enumerate(self.covar_modules[1:]): + latent_covar = covar( + torch.linspace( + 0.0, + 1.0, + self.target_shape[dim_num], + device=device, + dtype=dtype, + ) + ).add_jitter(1e-4) + latent_dist = MultivariateNormal( + torch.zeros( + *self._aug_batch_shape, + self.target_shape[dim_num], + device=device, + dtype=dtype, + ), + latent_covar, + ) + sample_shape = torch.Size((num_latent_dims[dim_num],)) + latent_sample = latent_dist.sample(sample_shape=sample_shape) + latent_sample = latent_sample.reshape( + *self._aug_batch_shape, + self.target_shape[dim_num], + num_latent_dims[dim_num], + ) + self.latent_parameters.append( + Parameter( + latent_sample, + requires_grad=learn_latent_pars, + ) + ) + self.register_prior( + "latent_parameters_" + str(dim_num), + MultivariateNormalPrior( + latent_dist.loc, + latent_dist.covariance_matrix.detach().clone(), + transform=lambda x: x.squeeze(-1), + ), + lambda module, dim_num=dim_num: self.latent_parameters[dim_num], + ) + +
+[docs] + def forward(self, X: Tensor) -> MultivariateNormal: + if self.training: + X = self.transform_inputs(X) + + covariance_list = [] + covariance_list.append(self.covar_modules[0](X)) + + for cm, param in zip(self.covar_modules[1:], self.latent_parameters): + if not self.training: + with torch.no_grad(): + covariance_list.append(cm(param)) + else: + covariance_list.append(cm(param)) + + # check batch_shapes + if covariance_list[0].batch_shape != covariance_list[1].batch_shape: + for i in range(1, len(covariance_list)): + cm = covariance_list[i] + covariance_list[i] = BatchRepeatLinearOperator( + cm, covariance_list[0].batch_shape + ) + kronecker_covariance = KroneckerProductLinearOperator(*covariance_list) + + # TODO: expand options for the mean module via batch shaping? + mean = torch.zeros( + *covariance_list[0].batch_shape, + kronecker_covariance.shape[-1], + device=kronecker_covariance.device, + dtype=kronecker_covariance.dtype, + ) + return MultivariateNormal(mean, kronecker_covariance)
+ + +
+[docs] + def get_fantasy_model(self, inputs, targets, **kwargs): + # we need to squeeze the targets in order to preserve the shaping + inputs_batch_dims = len(inputs.shape[:-2]) + target_shape = (*inputs.shape[:-2], -1) + if (inputs_batch_dims + self._num_dimensions) < targets.ndim: + target_shape = (targets.shape[0], *target_shape) + reshaped_targets = targets.view(*target_shape) + + return super().get_fantasy_model(inputs, reshaped_targets, **kwargs)
+ + +
+[docs] + def condition_on_observations( + self, X: Tensor, Y: Tensor, noise: torch.Tensor | None = None, **kwargs: Any + ) -> HigherOrderGP: + r"""Condition the model on new observations. + + Args: + X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of + the feature space, `m` is the number of points per batch, and + `batch_shape` is the batch shape (must be compatible with the + batch shape of the model). + Y: A `batch_shape' x n' x m_d`-dim Tensor, where `m_d` is the shaping + of the model outputs, `n'` is the number of points per batch, and + `batch_shape'` is the batch shape of the observations. + `batch_shape'` must be broadcastable to `batch_shape` using + standard broadcasting semantics. If `Y` has fewer batch dimensions + than `X`, its is assumed that the missing batch dimensions are + the same for all `Y`. + noise: If not None, a tensor of the same shape as `Y` representing + the noise variance associated with each observation. + kwargs: Passed to `condition_on_observations`. + + Returns: + A `BatchedMultiOutputGPyTorchModel` object of the same type with + `n + n'` training examples, representing the original model + conditioned on the new observations `(X, Y)` (and possibly noise + observations passed in via kwargs). + """ + if hasattr(self, "outcome_transform"): + # we need to apply transforms before shifting batch indices around + Y, noise = self.outcome_transform(Y=Y, Yvar=noise) + # Do not check shapes when fantasizing as they are not expected to match. + if fantasize_flag.off(): + self._validate_tensor_args(X=X, Y=Y, Yvar=noise, strict=False) + + # we don't need to do un-squeezing because Y already is batched + # we don't support fixed noise here yet + # if noise is not None: + # kwargs.update({"noise": noise}) + fantasy_model = super( + BatchedMultiOutputGPyTorchModel, self + ).condition_on_observations(X=X, Y=Y, noise=noise, **kwargs) + fantasy_model._input_batch_shape = fantasy_model.train_targets.shape[ + : (-1 if self._num_outputs == 1 else -2) + ] + fantasy_model._aug_batch_shape = fantasy_model.train_targets.shape[:-1] + return fantasy_model
+ + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool | Tensor = False, + posterior_transform: PosteriorTransform | None = None, + ) -> GPyTorchPosterior: + self.eval() # make sure we're calling a posterior + + if posterior_transform is not None: + # this could be very costly, disallow for now + raise NotImplementedError( + "Posterior transforms currently not supported for " + f"{self.__class__.__name__}" + ) + + # input transforms are applied at `posterior` in `eval` mode, and at + # `model.forward()` at the training time + X = self.transform_inputs(X) + no_pred_variance = skip_posterior_variances._state + + with ExitStack() as es: + es.enter_context(gpt_posterior_settings()) + es.enter_context(fast_pred_var(True)) + es.enter_context(_fast_solves(True)) + + # we need to skip posterior variances here + es.enter_context(skip_posterior_variances(True)) + mvn = self(X) + if observation_noise is not False: + # TODO: ensure that this still works for structured noise solves. + mvn = self.likelihood(mvn, X) + + # lazy covariance matrix includes the interpolated version of the full + # covariance matrix so we can actually grab that instead. + if X.ndimension() > self.train_inputs[0].ndimension(): + X_batch_shape = X.shape[:-2] + train_inputs = self.train_inputs[0].reshape( + *[1] * len(X_batch_shape), *self.train_inputs[0].shape + ) + train_inputs = train_inputs.repeat( + *X_batch_shape, *[1] * self.train_inputs[0].ndimension() + ) + else: + train_inputs = self.train_inputs[0] + + # we now compute the data covariances for the training data, the testing + # data, the joint covariances, and the test train cross-covariance + train_train_covar = self.prediction_strategy.lik_train_train_covar.detach() + base_train_train_covar = train_train_covar.linear_op + + data_train_covar = base_train_train_covar.linear_ops[0] + data_covar = self.covar_modules[0] + data_train_test_covar = data_covar(X, train_inputs) + data_test_test_covar = data_covar(X) + data_joint_covar = data_train_covar.cat_rows( + cross_mat=data_train_test_covar, + new_mat=data_test_test_covar, + ) + + # we detach the latents so that they don't cause gradient errors + # TODO: Can we enable backprop through the latent covariances? + batch_shape = data_train_test_covar.batch_shape + latent_covar_list = [] + for latent_covar in base_train_train_covar.linear_ops[1:]: + if latent_covar.batch_shape != batch_shape: + latent_covar = BatchRepeatLinearOperator(latent_covar, batch_shape) + latent_covar_list.append(latent_covar.detach()) + + joint_covar = KroneckerProductLinearOperator( + data_joint_covar, *latent_covar_list + ) + test_train_covar = KroneckerProductLinearOperator( + data_train_test_covar, *latent_covar_list + ) + + # compute the posterior variance if necessary + if no_pred_variance: + pred_variance = mvn.variance + else: + pred_variance = self.make_posterior_variances(joint_covar) + + # mean and variance get reshaped into the target shape + new_mean = mvn.mean.reshape(*X.shape[:-1], *self.target_shape) + if not no_pred_variance: + new_variance = pred_variance.reshape(*X.shape[:-1], *self.target_shape) + new_variance = DiagLinearOperator(new_variance) + else: + new_variance = ZeroLinearOperator( + *X.shape[:-1], *self.target_shape, self.target_shape[-1] + ) + + mvn = MultivariateNormal(new_mean, new_variance) + + # return a specialized Posterior to allow for sampling + # cloning the full covar allows backpropagation through it + posterior = HigherOrderGPPosterior( + distribution=mvn, + train_targets=self.train_targets.unsqueeze(-1), + train_train_covar=train_train_covar, + test_train_covar=test_train_covar, + joint_covariance_matrix=joint_covar.clone(), + output_shape=X.shape[:-1] + self.target_shape, + num_outputs=self._num_outputs, + ) + if hasattr(self, "outcome_transform"): + posterior = self.outcome_transform.untransform_posterior(posterior) + return posterior
+ + +
+[docs] + def make_posterior_variances( + self, joint_covariance_matrix: LinearOperator + ) -> Tensor: + r""" + Computes the posterior variances given the data points X. As currently + implemented, it computes another forwards call with the stacked data to get out + the joint covariance across all data points. + """ + # TODO: use the exposed joint covariances from the prediction strategy + data_joint_covariance = joint_covariance_matrix.linear_ops[0].evaluate_kernel() + num_train = self.train_inputs[0].shape[-2] + test_train_covar = data_joint_covariance[..., num_train:, :num_train] + train_train_covar = data_joint_covariance[..., :num_train, :num_train] + test_test_covar = data_joint_covariance[..., num_train:, num_train:] + + jcm_linops = joint_covariance_matrix.linear_ops[1:] + full_train_train_covar = KroneckerProductLinearOperator( + train_train_covar, *jcm_linops + ) + full_test_test_covar = KroneckerProductLinearOperator( + test_test_covar, *jcm_linops + ) + full_test_train_covar_tuple = (test_train_covar,) + jcm_linops + + train_evals, train_evecs = full_train_train_covar.eigh() + # (\kron \Lambda_i + \sigma^2 I)^{-1} + train_inv_evals = DiagLinearOperator( + 1.0 / (train_evals + self.likelihood.noise) + ) + + # compute K_i S_i \hadamard K_i S_i + test_train_hadamard = KroneckerProductLinearOperator( + *[ + lt1.matmul(lt2).to_dense() ** 2 + for lt1, lt2 in zip(full_test_train_covar_tuple, train_evecs.linear_ops) + ] + ) + + # and compute the column sums of + # (\kron K_i S_i * K_i S_i) \tilde{\Lambda}^{-1} + test_train_pred_covar = test_train_hadamard.matmul(train_inv_evals).sum(dim=-1) + + pred_variances = full_test_test_covar.diagonal() - test_train_pred_covar + return pred_variances
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/categorical.html b/website-old/pages/api/_modules/botorch/models/kernels/categorical.html new file mode 100644 index 0000000000..e73e9ee736 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/categorical.html @@ -0,0 +1,102 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.categorical

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+import torch
+from gpytorch.kernels.kernel import Kernel
+from torch import Tensor
+
+
+
+[docs] +class CategoricalKernel(Kernel): + r"""A Kernel for categorical features. + + Computes `exp(-dist(x1, x2) / lengthscale)`, where + `dist(x1, x2)` is zero if `x1 == x2` and one if `x1 != x2`. + If the last dimension is not a batch dimension, then the + mean is considered. + + Note: This kernel is NOT differentiable w.r.t. the inputs. + """ + + has_lengthscale = True + + def forward( + self, + x1: Tensor, + x2: Tensor, + diag: bool = False, + last_dim_is_batch: bool = False, + ) -> Tensor: + delta = x1.unsqueeze(-2) != x2.unsqueeze(-3) + dists = delta / self.lengthscale.unsqueeze(-2) + if last_dim_is_batch: + dists = dists.transpose(-3, -1) + else: + dists = dists.mean(-1) + res = torch.exp(-dists) + if diag: + res = torch.diagonal(res, dim1=-1, dim2=-2) + return res
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/contextual_lcea.html b/website-old/pages/api/_modules/botorch/models/kernels/contextual_lcea.html new file mode 100644 index 0000000000..90513155b7 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/contextual_lcea.html @@ -0,0 +1,497 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.contextual_lcea

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from typing import Any
+
+import torch
+from botorch.models.utils.gpytorch_modules import get_covar_module_with_dim_scaled_prior
+from gpytorch.constraints import Positive
+from gpytorch.kernels.kernel import Kernel
+from gpytorch.priors.torch_priors import GammaPrior
+from linear_operator.operators import DiagLinearOperator
+from linear_operator.operators.dense_linear_operator import DenseLinearOperator
+from torch import Tensor
+from torch.nn import ModuleList
+
+
+def get_order(indices: list[int]) -> list[int]:
+    r"""Get the order indices as integers ranging from 0 to the number of indices.
+
+    Args:
+        indices: A list of parameter indices.
+
+    Returns:
+        A list of integers ranging from 0 to the number of indices.
+    """
+    return [i % len(indices) for i in indices]
+
+
+def is_contiguous(indices: list[int]) -> bool:
+    r"""Check if the list of integers is contiguous.
+
+    Args:
+        indices: A list of parameter indices.
+    Returns:
+        A boolean indicating whether the indices are contiguous.
+    """
+    min_idx = min(indices)
+    return set(indices) == set(range(min_idx, min_idx + len(indices)))
+
+
+def get_permutation(decomposition: dict[str, list[int]]) -> list[int] | None:
+    """Construct permutation to reorder the parameters such that:
+
+    1) the parameters for each context are contiguous.
+    2) The parameters for each context are in the same order
+
+    Args:
+        decomposition: A dictionary mapping context names to a list of
+            parameters.
+    Returns:
+        A permutation to reorder the parameters for (1) and (2).
+        Returning `None` means that ordering specified in `decomposition`
+        satisfies (1) and (2).
+    """
+    permutation = None
+    if not all(
+        is_contiguous(indices=active_parameters)
+        for active_parameters in decomposition.values()
+    ):
+        permutation = _create_new_permutation(decomposition=decomposition)
+    else:
+        same_order = True
+        expected_order = get_order(indices=next(iter(decomposition.values())))
+        for active_parameters in decomposition.values():
+            order = get_order(indices=active_parameters)
+            if order != expected_order:
+                same_order = False
+                break
+        if not same_order:
+            permutation = _create_new_permutation(decomposition=decomposition)
+    return permutation
+
+
+def _create_new_permutation(decomposition: dict[str, list[int]]) -> list[int]:
+    # make contiguous and ordered
+    permutation = []
+    for active_parameters in decomposition.values():
+        sorted_indices = sorted(active_parameters)
+        permutation.extend(sorted_indices)
+    return permutation
+
+
+
+[docs] +class LCEAKernel(Kernel): + r"""The Latent Context Embedding Additive (LCE-A) Kernel. + + This kernel is similar to the SACKernel, and is used when context breakdowns are + unbserverable. It assumes the same additive structure and a spatial kernel shared + across contexts. Rather than assuming independence, LCEAKernel models the + correlation in the latent functions for each context through learning context + embeddings. + """ + + def __init__( + self, + decomposition: dict[str, list[int]], + batch_shape: torch.Size, + train_embedding: bool = True, + cat_feature_dict: dict | None = None, + embs_feature_dict: dict | None = None, + embs_dim_list: list[int] | None = None, + context_weight_dict: dict | None = None, + device: torch.device | None = None, + ) -> None: + r""" + Args: + decomposition: Keys index context names. Values are the indexes of + parameters belong to the context. + batch_shape: Batch shape as usual for gpytorch kernels. Model does not + support batch training. When batch_shape is non-empty, it is used for + loading hyper-parameter values generated from MCMC sampling. + train_embedding: A boolean indictor of whether to learn context embeddings. + cat_feature_dict: Keys are context names and values are list of categorical + features i.e. {"context_name" : [cat_0, ..., cat_k]}. k equals the + number of categorical variables. If None, uses context names in the + decomposition as the only categorical feature, i.e., k = 1. + embs_feature_dict: Pre-trained continuous embedding features of each + context. + embs_dim_list: Embedding dimension for each categorical variable. The length + equals to num of categorical features k. If None, the embedding + dimension is set to 1 for each categorical variable. + context_weight_dict: Known population weights of each context. + """ + super().__init__(batch_shape=batch_shape) + self.batch_shape = batch_shape + self.train_embedding = train_embedding + self._device = device + + self.num_param = len(next(iter(decomposition.values()))) + self.context_list = list(decomposition.keys()) + self.num_contexts = len(self.context_list) + + # get parameter space decomposition + for active_parameters in decomposition.values(): + # check number of parameters are same in each decomp + if len(active_parameters) != self.num_param: + raise ValueError( + "The number of parameters needs to be same across all contexts." + ) + # reorder the parameter list based on decomposition such that + # parameters for each context are contiguous and in the same order for each + # context + self.permutation = get_permutation(decomposition=decomposition) + # get context features and set emb dim + self.context_cat_feature = None + self.context_emb_feature = None + self.n_embs = 0 + self.emb_weight_matrix_list = None + self.emb_dims = None + self._set_context_features( + cat_feature_dict=cat_feature_dict, + embs_feature_dict=embs_feature_dict, + embs_dim_list=embs_dim_list, + ) + # contruct embedding layer + if train_embedding: + self._set_emb_layers() + # task covariance matrix + self.task_covar_module = get_covar_module_with_dim_scaled_prior( + ard_num_dims=self.n_embs, + batch_shape=batch_shape, + ) + # base kernel + self.base_kernel = get_covar_module_with_dim_scaled_prior( + ard_num_dims=self.num_param, + batch_shape=batch_shape, + ) + # outputscales for each context (note this is like sqrt of outputscale) + self.context_weight = None + if context_weight_dict is None: + outputscale_list = torch.zeros( + *batch_shape, self.num_contexts, device=self.device + ) + else: + outputscale_list = torch.zeros(*batch_shape, 1, device=self.device) + self.context_weight = torch.tensor( + [context_weight_dict[c] for c in self.context_list], device=self.device + ) + self.register_parameter( + name="raw_outputscale_list", parameter=torch.nn.Parameter(outputscale_list) + ) + self.register_prior( + "outputscale_list_prior", + GammaPrior(2.0, 15.0), + lambda m: m.outputscale_list, + lambda m, v: m._set_outputscale_list(v), + ) + self.register_constraint("raw_outputscale_list", Positive()) + + @property + def device(self) -> torch.device | None: + return self._device + + @property + def outputscale_list(self) -> Tensor: + return self.raw_outputscale_list_constraint.transform(self.raw_outputscale_list) + + @outputscale_list.setter + def outputscale_list(self, value: Tensor) -> None: + self._set_outputscale_list(value) + + def _set_outputscale_list(self, value: Tensor) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_outputscale_list) + self.initialize( + raw_outputscale_list=self.raw_outputscale_list_constraint.inverse_transform( + value + ) + ) + + def _set_context_features( + self, + cat_feature_dict: dict | None = None, + embs_feature_dict: dict | None = None, + embs_dim_list: list[int] | None = None, + ) -> None: + """Set context categorical features and continuous embedding features. + If cat_feature_dict is None, context indices will be used; If embs_dim_list + is None, we use 1-d embedding for each categorical features. + """ + # get context categorical features + if cat_feature_dict is None: + self.context_cat_feature = torch.arange( + self.num_contexts, device=self.device + ).unsqueeze(-1) + else: + self.context_cat_feature = torch.tensor( + [cat_feature_dict[c] for c in self.context_list] + ) + # construct emb_dims based on categorical features + if embs_dim_list is None: + # set embedding_dim = 1 for each categorical variable + embs_dim_list = [1 for _i in range(self.context_cat_feature.size(1))] + self.emb_dims = [ + (len(self.context_cat_feature[:, i].unique()), embs_dim_list[i]) + for i in range(self.context_cat_feature.size(1)) + ] + if self.train_embedding: + self.n_embs = sum(embs_dim_list) # total num of emb features + # get context embedding features + if embs_feature_dict is not None: + self.context_emb_feature = torch.tensor( + [embs_feature_dict[c] for c in self.context_list], device=self.device + ) + self.n_embs += self.context_emb_feature.size(1) + + def _set_emb_layers(self) -> None: + """Construct embedding layers. + If model is non-batch, we use nn.Embedding to learn emb weights. If model is + batched (sef.batch_shape is non-empty), we load emb weights posterior samples + and construct a parameter list that each parameter is the emb weight of each + layer. The shape of weight matrices are ns x num_contexts x emb_dim. + """ + self.emb_layers = ModuleList( + [ + torch.nn.Embedding(num_embeddings=x, embedding_dim=y, max_norm=1.0) + for x, y in self.emb_dims + ] + ) + # use posterior of emb weights + if len(self.batch_shape) > 0: + self.emb_weight_matrix_list = torch.nn.ParameterList( + [ + torch.nn.Parameter( + torch.zeros( + self.batch_shape + emb_layer.weight.shape, + device=self.device, + ) + ) + for emb_layer in self.emb_layers + ] + ) + + def _eval_context_covar(self) -> Tensor: + """Compute context covariance matrix. + + Returns: + A (ns) x num_contexts x num_contexts tensor. + """ + if len(self.batch_shape) > 0: + # broadcast - (ns x num_contexts x k) + all_embs = self._task_embeddings_batch() + else: + all_embs = self._task_embeddings() # no broadcast - (num_contexts x k) + + context_covar = self.task_covar_module(all_embs).to_dense() + if self.context_weight is None: + context_outputscales = self.outputscale_list + else: + context_outputscales = self.outputscale_list * self.context_weight + context_covar = ( + (context_outputscales.unsqueeze(-2)) + # (ns) x 1 x num_contexts + .mul(context_covar) + .mul(context_outputscales.unsqueeze(-1)) # (ns) x num_contexts x 1 + ) + return context_covar + + def _task_embeddings(self) -> Tensor: + """Generate embedding features of contexts when model is non-batch. + + Returns: + a (num_contexts x n_embs) tensor. n_embs is the sum of embedding + dimensions i.e. sum(embs_dim_list) + """ + if self.train_embedding is False: + return self.context_emb_feature # use pre-trained embedding only + context_features = torch.stack( + [self.context_cat_feature[i, :] for i in range(self.num_contexts)], dim=0 + ) + embeddings = [ + emb_layer(context_features[:, i].to(device=self.device, dtype=torch.long)) + for i, emb_layer in enumerate(self.emb_layers) + ] + embeddings = torch.cat(embeddings, dim=1) + # add given embeddings if any + if self.context_emb_feature is not None: + embeddings = torch.cat([embeddings, self.context_emb_feature], dim=1) + return embeddings + + def _task_embeddings_batch(self) -> Tensor: + """Generate embedding features of contexts when model has multiple batches. + + Returns: + a (ns) x num_contexts x n_embs tensor. ns is the batch size i.e num of + posterior samples; n_embs is the sum of embedding dimensions i.e. + sum(embs_dim_list). + """ + context_features = torch.cat( + [ + self.context_cat_feature[i, :].unsqueeze(0) + for i in range(self.num_contexts) + ] + ) + embeddings = [] + for b in range(self.batch_shape.numel()): # pyre-ignore + for i in range(len(self.emb_weight_matrix_list)): + # loop over emb layer and concat embs from each layer + embeddings.append( + torch.cat( + [ + torch.nn.functional.embedding( + context_features[:, 0].to( + dtype=torch.long, device=self.device + ), + self.emb_weight_matrix_list[i][b, :], + ).unsqueeze(0) + ], + dim=1, + ) + ) + embeddings = torch.cat(embeddings, dim=0) + # add given embeddings if any + if self.context_emb_feature is not None: + embeddings = torch.cat( + [ + embeddings, + self.context_emb_feature.expand( + *self.batch_shape + self.context_emb_feature.shape + ), + ], + dim=-1, + ) + return embeddings + + def train(self, mode: bool = True) -> None: + super().train(mode=mode) + if not mode: + self.register_buffer("_context_covar", self._eval_context_covar()) + + def forward( + self, + x1: Tensor, + x2: Tensor, + diag: bool = False, + last_dim_is_batch: bool = False, + **params: Any, + ) -> Tensor: + """Iterate across each partition of parameter space and sum the + covariance matrices together + """ + # context covar matrix + context_covar = ( + self._eval_context_covar() if self.training else self._context_covar + ) + base_covar_perm = self._eval_base_covar_perm(x1, x2) + # expand context_covar to match base_covar_perm + if base_covar_perm.dim() > context_covar.dim(): + context_covar = context_covar.expand(base_covar_perm.shape) + # then weight by the context kernel + # compute the base kernel on the d parameters + einsum_str = "...nnki, ...nnki -> ...n" if diag else "...ki, ...ki -> ..." + covar_dense = torch.einsum(einsum_str, context_covar, base_covar_perm) + if diag: + return DiagLinearOperator(covar_dense) + return DenseLinearOperator(covar_dense) + + def _eval_base_covar_perm(self, x1: Tensor, x2: Tensor) -> Tensor: + """Computes the base covariance matrix on x1, x2, applying permutations and + reshaping the kernel matrix as required by `forward`. + + NOTE: Using the notation n = num_observations, k = num_contexts, d = input_dim, + the input tensors have to have the following shapes. + + Args: + x1: `batch_shape x n x (k*d)`-dim Tensor of kernel inputs. + x2: `batch_shape x n x (k*d)`-dim Tensor of kernel inputs. + + Returns: + `batch_shape x n x n x k x k`-dim Tensor of base covariance values. + """ + if self.permutation is not None: + x1 = x1[..., self.permutation] + x2 = x2[..., self.permutation] + # turn last two dimensions of n x (k*d) into (n*k) x d. + x1_exp = x1.reshape(*x1.shape[:-2], -1, self.num_param) + x2_exp = x2.reshape(*x2.shape[:-2], -1, self.num_param) + # batch shape x n*k x n*k + base_covar = self.base_kernel(x1_exp, x2_exp) + # batch shape x n x n x k x k + view_shape = x1.shape[:-2] + torch.Size( + [ + x1.shape[-2], + self.num_contexts, + x2.shape[-2], + self.num_contexts, + ] + ) + base_covar_perm = ( + base_covar.to_dense() + .view(view_shape) + .permute(*list(range(x1.ndim - 2)), -4, -2, -3, -1) + ) + return base_covar_perm
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/contextual_sac.html b/website-old/pages/api/_modules/botorch/models/kernels/contextual_sac.html new file mode 100644 index 0000000000..dd1b85c095 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/contextual_sac.html @@ -0,0 +1,178 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.contextual_sac

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from typing import Any
+
+import torch
+from botorch.models.utils.gpytorch_modules import get_covar_module_with_dim_scaled_prior
+from gpytorch.kernels.kernel import Kernel
+from gpytorch.kernels.scale_kernel import ScaleKernel
+from gpytorch.priors.torch_priors import GammaPrior
+from linear_operator.operators.sum_linear_operator import SumLinearOperator
+from torch import Tensor
+from torch.nn import ModuleDict  # pyre-ignore
+
+
+
+[docs] +class SACKernel(Kernel): + r"""The structural additive contextual(SAC) kernel. + + The kernel is used for contextual BO without oberseving context breakdowns. + There are d parameters and M contexts. In total, the dimension of parameter space + is d*M and input x can be written as + x=[x_11, ..., x_1d, x_21, ..., x_2d, ..., x_M1, ..., x_Md]. + + The kernel uses the parameter decomposition and assumes an additive structure + across contexts. Each context compponent is assumed to be independent. + + .. math:: + \begin{equation*} + k(\mathbf{x}, \mathbf{x'}) = k_1(\mathbf{x_(1)}, \mathbf{x'_(1)}) + \cdots + + k_M(\mathbf{x_(M)}, \mathbf{x'_(M)}) + \end{equation*} + + where + * :math: M is the number of partitions of parameter space. Each partition contains + same number of parameters d. Each kernel `k_i` acts only on d parameters of ith + partition i.e. `\mathbf{x}_(i)`. Each kernel `k_i` is a scaled RBF kernel + with same lengthscales but different outputscales. + """ + + def __init__( + self, + decomposition: dict[str, list[int]], + batch_shape: torch.Size, + device: torch.device | None = None, + ) -> None: + r""" + Args: + decomposition: Keys are context names. Values are the indexes of parameters + belong to the context. The parameter indexes are in the same order + across contexts. + batch_shape: Batch shape as usual for gpytorch kernels. + device: The torch device. + """ + + super().__init__(batch_shape=batch_shape) + self.decomposition = decomposition + self._device = device + + num_param = len(next(iter(decomposition.values()))) + for active_parameters in decomposition.values(): + # check number of parameters are same in each decomp + if len(active_parameters) != num_param: + raise ValueError( + "num of parameters needs to be same across all contexts" + ) + + self._indexers = { + context: torch.tensor(active_params, device=self.device) + for context, active_params in self.decomposition.items() + } + + self.base_kernel = get_covar_module_with_dim_scaled_prior( + ard_num_dims=num_param, + batch_shape=batch_shape, + ) + + self.kernel_dict = {} # scaled kernel for each parameter space partition + for context in list(decomposition.keys()): + self.kernel_dict[context] = ScaleKernel( + base_kernel=self.base_kernel, outputscale_prior=GammaPrior(2.0, 15.0) + ) + self.kernel_dict = ModuleDict(self.kernel_dict) + + @property + def device(self) -> torch.device | None: + return self._device + + def forward( + self, + x1: Tensor, + x2: Tensor, + diag: bool = False, + last_dim_is_batch: bool = False, + **params: Any, + ) -> Tensor: + """ + iterate across each partition of parameter space and sum the + covariance matrices together + """ + # same lengthscale for all the components + covars = [ + self.kernel_dict[context]( + x1=x1.index_select(dim=-1, index=active_params), # pyre-ignore + x2=x2.index_select(dim=-1, index=active_params), + diag=diag, + ) + for context, active_params in self._indexers.items() + ] + + if diag: + res = sum(covars) + else: + res = SumLinearOperator(*covars) + return res
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/downsampling.html b/website-old/pages/api/_modules/botorch/models/kernels/downsampling.html new file mode 100644 index 0000000000..d4a39205b6 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/downsampling.html @@ -0,0 +1,189 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.downsampling

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import torch
+from gpytorch.constraints import Interval, Positive
+from gpytorch.kernels import Kernel
+from gpytorch.priors import Prior
+from torch import Tensor
+
+
+
+[docs] +class DownsamplingKernel(Kernel): + r"""GPyTorch Downsampling Kernel. + + Computes a covariance matrix based on the down sampling kernel between + inputs `x_1` and `x_2` (we expect `d = 1`): + + K(\mathbf{x_1}, \mathbf{x_2}) = c + (1 - x_1)^(1 + delta) * + (1 - x_2)^(1 + delta). + + where `c` is an offset parameter, and `delta` is a power parameter. + """ + + def __init__( + self, + power_prior: Prior | None = None, + offset_prior: Prior | None = None, + power_constraint: Interval | None = None, + offset_constraint: Interval | None = None, + **kwargs, + ): + r""" + Args: + power_constraint: Constraint to place on power parameter. Default is + `Positive`. + power_prior: Prior over the power parameter. + offset_constraint: Constraint to place on offset parameter. Default is + `Positive`. + active_dims: List of data dimensions to operate on. `len(active_dims)` + should equal `num_dimensions`. + """ + super().__init__(**kwargs) + + if power_constraint is None: + power_constraint = Positive() + if offset_constraint is None: + offset_constraint = Positive() + + self.register_parameter( + name="raw_power", + parameter=torch.nn.Parameter(torch.zeros(*self.batch_shape, 1)), + ) + + self.register_parameter( + name="raw_offset", + parameter=torch.nn.Parameter(torch.zeros(*self.batch_shape, 1)), + ) + + if power_prior is not None: + self.register_prior( + "power_prior", + power_prior, + lambda m: m.power, + lambda m, v: m._set_power(v), + ) + self.register_constraint("raw_power", power_constraint) + + if offset_prior is not None: + self.register_prior( + "offset_prior", + offset_prior, + lambda m: m.offset, + lambda m, v: m._set_offset(v), + ) + self.register_constraint("raw_offset", offset_constraint) + + @property + def power(self) -> Tensor: + return self.raw_power_constraint.transform(self.raw_power) + + @power.setter + def power(self, value: Tensor) -> None: + self._set_power(value) + + def _set_power(self, value: Tensor) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_power) + self.initialize(raw_power=self.raw_power_constraint.inverse_transform(value)) + + @property + def offset(self) -> Tensor: + return self.raw_offset_constraint.transform(self.raw_offset) + + @offset.setter + def offset(self, value: Tensor) -> None: + self._set_offset(value) + + def _set_offset(self, value: Tensor) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_offset) + self.initialize(raw_offset=self.raw_offset_constraint.inverse_transform(value)) + + def forward( + self, + x1: Tensor, + x2: Tensor, + diag: bool | None = False, + last_dim_is_batch: bool | None = False, + **params, + ) -> Tensor: + offset = self.offset + exponent = 1 + self.power + if last_dim_is_batch: + x1 = x1.transpose(-1, -2).unsqueeze(-1) + x2 = x2.transpose(-1, -2).unsqueeze(-1) + x1_ = 1 - x1 + x2_ = 1 - x2 + + if diag: + return offset + (x1_ * x2_).sum(dim=-1).pow(exponent) + + offset = offset.unsqueeze(-1) # unsqueeze enables batch evaluation + exponent = exponent.unsqueeze(-1) # unsqueeze enables batch evaluation + return offset + x1_.pow(exponent) @ x2_.transpose(-2, -1).pow(exponent)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/exponential_decay.html b/website-old/pages/api/_modules/botorch/models/kernels/exponential_decay.html new file mode 100644 index 0000000000..b22fc31783 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/exponential_decay.html @@ -0,0 +1,184 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.exponential_decay

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import torch
+from gpytorch.constraints import Interval, Positive
+from gpytorch.kernels import Kernel
+from gpytorch.priors import Prior
+from torch import Tensor
+
+
+
+[docs] +class ExponentialDecayKernel(Kernel): + r"""GPyTorch Exponential Decay Kernel. + + Computes a covariance matrix based on the exponential decay kernel + between inputs `x_1` and `x_2` (we expect `d = 1`): + + K(x_1, x_2) = w + beta^alpha / (x_1 + x_2 + beta)^alpha. + + where `w` is an offset parameter, `beta` is a lenthscale parameter, and + `alpha` is a power parameter. + """ + + has_lengthscale = True + + def __init__( + self, + power_prior: Prior | None = None, + offset_prior: Prior | None = None, + power_constraint: Interval | None = None, + offset_constraint: Interval | None = None, + **kwargs, + ): + r""" + Args: + lengthscale_constraint: Constraint to place on lengthscale parameter. + Default is `Positive`. + lengthscale_prior: Prior over the lengthscale parameter. + power_constraint: Constraint to place on power parameter. Default is + `Positive`. + power_prior: Prior over the power parameter. + offset_constraint: Constraint to place on offset parameter. Default is + `Positive`. + active_dims: List of data dimensions to operate on. `len(active_dims)` + should equal `num_dimensions`. + """ + super().__init__(**kwargs) + + if power_constraint is None: + power_constraint = Positive() + if offset_constraint is None: + offset_constraint = Positive() + + self.register_parameter( + name="raw_power", + parameter=torch.nn.Parameter(torch.zeros(*self.batch_shape, 1)), + ) + + self.register_parameter( + name="raw_offset", + parameter=torch.nn.Parameter(torch.zeros(*self.batch_shape, 1)), + ) + + if power_prior is not None: + self.register_prior( + "power_prior", + power_prior, + lambda m: m.power, + lambda m, v: m._set_power(v), + ) + self.register_constraint("raw_power", offset_constraint) + + if offset_prior is not None: + self.register_prior( + "offset_prior", + offset_prior, + lambda m: m.offset, + lambda m, v: m._set_offset(v), + ) + + self.register_constraint("raw_offset", offset_constraint) + + @property + def power(self) -> Tensor: + return self.raw_power_constraint.transform(self.raw_power) + + @power.setter + def power(self, value: Tensor) -> None: + self._set_power(value) + + def _set_power(self, value: Tensor) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_power) + self.initialize(raw_power=self.raw_power_constraint.inverse_transform(value)) + + @property + def offset(self) -> Tensor: + return self.raw_offset_constraint.transform(self.raw_offset) + + @offset.setter + def offset(self, value: Tensor) -> None: + self._set_offset(value) + + def _set_offset(self, value: Tensor) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_offset) + self.initialize(raw_offset=self.raw_offset_constraint.inverse_transform(value)) + + def forward(self, x1: Tensor, x2: Tensor, **params) -> Tensor: + offset = self.offset + power = self.power + if not params.get("diag", False): + offset = offset.unsqueeze(-1) # unsqueeze enables batch evaluation + power = power.unsqueeze(-1) # unsqueeze enables batch evaluation + x1_ = x1.div(self.lengthscale) + x2_ = x2.div(self.lengthscale) + diff = self.covar_dist(x1_, -x2_, **params) + res = offset + (diff + 1).pow(-power) + return res
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/infinite_width_bnn.html b/website-old/pages/api/_modules/botorch/models/kernels/infinite_width_bnn.html new file mode 100644 index 0000000000..b0aa1ab836 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/infinite_width_bnn.html @@ -0,0 +1,239 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.infinite_width_bnn

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import torch
+from gpytorch.constraints import Positive
+from gpytorch.kernels import Kernel
+from torch import Tensor
+
+
+
+[docs] +class InfiniteWidthBNNKernel(Kernel): + r"""Infinite-width BNN kernel. + + Defines the GP kernel which is equivalent to performing exact Bayesian + inference on a fully-connected deep neural network with ReLU activations + and i.i.d. priors in the infinite-width limit. + See [Cho2009kernel]_ and [Lee2018deep]_ for details. + + .. [Cho2009kernel] + Y. Cho, and L. Saul. Kernel methods for deep learning. + Advances in Neural Information Processing Systems 22. 2009. + .. [Lee2018deep] + J. Lee, Y. Bahri, R. Novak, S. Schoenholz, J. Pennington, and J. Dickstein. + Deep Neural Networks as Gaussian Processes. + International Conference on Learning Representations. 2018. + """ + + has_lengthscale = False + + def __init__( + self, + depth: int = 3, + batch_shape: torch.Size | None = None, + active_dims: tuple[int, ...] | None = None, + acos_eps: float = 1e-7, + device: torch.device | None = None, + ) -> None: + r""" + Args: + depth: Depth of neural network. + batch_shape: This will set a separate weight/bias var for each batch. + It should be :math:`B_1 \times \ldots \times B_k` if :math:`\mathbf` is + a :math:`B_1 \times \ldots \times B_k \times N \times D` tensor. + param active_dims: Compute the covariance of only a few input dimensions. + The ints corresponds to the indices of the dimensions. + param acos_eps: A small positive value to restrict acos inputs to + :math`[-1 + \epsilon, 1 - \epsilon]` + param device: Device for parameters. + """ + super().__init__(batch_shape=batch_shape, active_dims=active_dims) + self.depth = depth + self.acos_eps = acos_eps + + self.register_parameter( + "raw_weight_var", + torch.nn.Parameter(torch.zeros(*self.batch_shape, 1, 1, device=device)), + ) + self.register_constraint("raw_weight_var", Positive()) + + self.register_parameter( + "raw_bias_var", + torch.nn.Parameter(torch.zeros(*self.batch_shape, 1, 1, device=device)), + ) + self.register_constraint("raw_bias_var", Positive()) + + @property + def weight_var(self) -> Tensor: + return self.raw_weight_var_constraint.transform(self.raw_weight_var) + + @weight_var.setter + def weight_var(self, value) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_weight_var) + self.initialize( + raw_weight_var=self.raw_weight_var_constraint.inverse_transform(value) + ) + + @property + def bias_var(self) -> Tensor: + return self.raw_bias_var_constraint.transform(self.raw_bias_var) + + @bias_var.setter + def bias_var(self, value) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_bias_var) + self.initialize( + raw_bias_var=self.raw_bias_var_constraint.inverse_transform(value) + ) + + def _initialize_var(self, x: Tensor) -> Tensor: + """Computes the initial variance of x for layer 0""" + return ( + self.weight_var * torch.sum(x * x, dim=-1, keepdim=True) / x.shape[-1] + + self.bias_var + ) + + def _update_var(self, K: Tensor, x: Tensor) -> Tensor: + """Computes the updated variance of x for next layer""" + return self.weight_var * K / 2 + self.bias_var + + def k(self, x1: Tensor, x2: Tensor) -> Tensor: + r""" + For single-layer infinite-width neural networks with i.i.d. priors, + the covariance between outputs can be computed by + :math:`K^0(x, x')=\sigma_b^2+\sigma_w^2\frac{x \cdot x'}{d_\text{input}}`. + + For deeper networks, we can recursively define the covariance as + :math:`K^l(x, x')=\sigma_b^2+\sigma_w^2 + F_\phi(K^{l-1}(x, x'), K^{l-1}(x, x), K^{l-1}(x', x'))` + where :math:`F_\phi` is a deterministic function based on the + activation function :math:`\phi`. + + For ReLU activations, this yields the arc-cosine kernel, which can be computed + analytically. + + Args: + x1: `batch_shape x n1 x d`-dim Tensor + x2: `batch_shape x n2 x d`-dim Tensor + """ + K_12 = ( + self.weight_var * (x1.matmul(x2.transpose(-2, -1)) / x1.shape[-1]) + + self.bias_var + ) + + for layer in range(self.depth): + if layer == 0: + K_11 = self._initialize_var(x1) + K_22 = self._initialize_var(x2) + else: + K_11 = self._update_var(K_11, x1) + K_22 = self._update_var(K_22, x2) + + sqrt_term = torch.sqrt(K_11.matmul(K_22.transpose(-2, -1))) + + fraction = K_12 / sqrt_term + fraction = torch.clamp( + fraction, min=-1 + self.acos_eps, max=1 - self.acos_eps + ) + + theta = torch.acos(fraction) + theta_term = torch.sin(theta) + (torch.pi - theta) * fraction + + K_12 = ( + self.weight_var / (2 * torch.pi) * sqrt_term * theta_term + + self.bias_var + ) + + return K_12 + + def forward( + self, + x1: Tensor, + x2: Tensor, + diag: bool | None = False, + last_dim_is_batch: bool | None = False, + **params, + ) -> Tensor: + """ + Args: + x1: `batch_shape x n1 x d`-dim Tensor + x2: `batch_shape x n2 x d`-dim Tensor + diag: If True, only returns the diagonal of the kernel matrix. + last_dim_is_batch: Not supported by this kernel. + """ + if last_dim_is_batch: + raise RuntimeError("last_dim_is_batch not supported by this kernel.") + + if diag: + K = self._initialize_var(x1) + for _ in range(self.depth): + K = self._update_var(K, x1) + return K.squeeze(-1) + else: + return self.k(x1, x2)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/linear_truncated_fidelity.html b/website-old/pages/api/_modules/botorch/models/kernels/linear_truncated_fidelity.html new file mode 100644 index 0000000000..e7cad77f4f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/linear_truncated_fidelity.html @@ -0,0 +1,299 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.linear_truncated_fidelity

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from typing import Any
+
+import torch
+from botorch.exceptions import UnsupportedError
+from gpytorch.constraints import Interval, Positive
+from gpytorch.kernels import Kernel
+from gpytorch.kernels.matern_kernel import MaternKernel
+from gpytorch.priors import Prior
+from gpytorch.priors.torch_priors import GammaPrior
+from torch import Tensor
+
+
+
+[docs] +class LinearTruncatedFidelityKernel(Kernel): + r"""GPyTorch Linear Truncated Fidelity Kernel. + + Computes a covariance matrix based on the Linear truncated kernel between + inputs `x_1` and `x_2` for up to two fidelity parmeters: + + K(x_1, x_2) = k_0 + c_1(x_1, x_2)k_1 + c_2(x_1,x_2)k_2 + c_3(x_1,x_2)k_3 + + where + + - `k_i(i=0,1,2,3)` are Matern kernels calculated between non-fidelity + parameters of `x_1` and `x_2` with different priors. + - `c_1=(1 - x_1[f_1])(1 - x_2[f_1]))(1 + x_1[f_1] x_2[f_1])^p` is the kernel + of the the bias term, which can be decomposed into a determistic part + and a polynomial kernel. Here `f_1` is the first fidelity dimension and + `p` is the order of the polynomial kernel. + - `c_3` is the same as `c_1` but is calculated for the second fidelity + dimension `f_2`. + - `c_2` is the interaction term with four deterministic terms and the + polynomial kernel between `x_1[..., [f_1, f_2]]` and + `x_2[..., [f_1, f_2]]`. + + Example: + >>> x = torch.randn(10, 5) + >>> # Non-batch: Simple option + >>> covar_module = LinearTruncatedFidelityKernel() + >>> covar = covar_module(x) # Output: LinearOperator of size (10 x 10) + >>> + >>> batch_x = torch.randn(2, 10, 5) + >>> # Batch: Simple option + >>> covar_module = LinearTruncatedFidelityKernel(batch_shape = torch.Size([2])) + >>> covar = covar_module(x) # Output: LinearOperator of size (2 x 10 x 10) + """ + + def __init__( # noqa C901 + self, + fidelity_dims: list[int], + dimension: int | None = None, + power_prior: Prior | None = None, + power_constraint: Interval | None = None, + nu: float = 2.5, + lengthscale_prior_unbiased: Prior | None = None, + lengthscale_prior_biased: Prior | None = None, + lengthscale_constraint_unbiased: Interval | None = None, + lengthscale_constraint_biased: Interval | None = None, + covar_module_unbiased: Kernel | None = None, + covar_module_biased: Kernel | None = None, + **kwargs: Any, + ) -> None: + """ + Args: + fidelity_dims: A list containing either one or two indices specifying + the fidelity parameters of the input. + dimension: The dimension of `x`. Unused if `active_dims` is specified. + power_prior: Prior for the power parameter of the polynomial kernel. + Default is `None`. + power_constraint: Constraint on the power parameter of the polynomial + kernel. Default is `Positive`. + nu: The smoothness parameter for the Matern kernel: either 1/2, 3/2, + or 5/2. Unused if both `covar_module_unbiased` and + `covar_module_biased` are specified. + lengthscale_prior_unbiased: Prior on the lengthscale parameter of Matern + kernel `k_0`. Default is `Gamma(1.1, 1/20)`. + lengthscale_constraint_unbiased: Constraint on the lengthscale parameter + of the Matern kernel `k_0`. Default is `Positive`. + lengthscale_prior_biased: Prior on the lengthscale parameter of Matern + kernels `k_i(i>0)`. Default is `Gamma(5, 1/20)`. + lengthscale_constraint_biased: Constraint on the lengthscale parameter + of the Matern kernels `k_i(i>0)`. Default is `Positive`. + covar_module_unbiased: Specify a custom kernel for `k_0`. If omitted, + use a `MaternKernel`. + covar_module_biased: Specify a custom kernel for the biased parts + `k_i(i>0)`. If omitted, use a `MaternKernel`. + batch_shape: If specified, use a separate lengthscale for each batch of + input data. If `x1` is a `batch_shape x n x d` tensor, this should + be `batch_shape`. + active_dims: Compute the covariance of a subset of input dimensions. The + numbers correspond to the indices of the dimensions. + """ + if dimension is None and kwargs.get("active_dims") is None: + raise UnsupportedError( + "Must specify dimension when not specifying active_dims." + ) + n_fidelity = len(fidelity_dims) + if len(set(fidelity_dims)) != n_fidelity: + raise ValueError("fidelity_dims must not have repeated elements") + if n_fidelity not in {1, 2}: + raise UnsupportedError( + "LinearTruncatedFidelityKernel accepts either one or two" + "fidelity parameters." + ) + if nu not in {0.5, 1.5, 2.5}: + raise ValueError("nu must be one of 0.5, 1.5, or 2.5") + + super().__init__(**kwargs) + self.fidelity_dims = fidelity_dims + if power_constraint is None: + power_constraint = Positive() + + if lengthscale_prior_unbiased is None: + lengthscale_prior_unbiased = GammaPrior(3, 6) + + if lengthscale_prior_biased is None: + lengthscale_prior_biased = GammaPrior(6, 2) + + if lengthscale_constraint_unbiased is None: + lengthscale_constraint_unbiased = Positive() + + if lengthscale_constraint_biased is None: + lengthscale_constraint_biased = Positive() + + self.register_parameter( + name="raw_power", + parameter=torch.nn.Parameter(torch.zeros(*self.batch_shape, 1)), + ) + self.register_constraint("raw_power", power_constraint) + + if power_prior is not None: + self.register_prior( + "power_prior", + power_prior, + lambda m: m.power, + lambda m, v: m._set_power(v), + ) + + if self.active_dims is not None: + dimension = len(self.active_dims) + + if covar_module_unbiased is None: + covar_module_unbiased = MaternKernel( + nu=nu, + batch_shape=self.batch_shape, + lengthscale_prior=lengthscale_prior_unbiased, + ard_num_dims=dimension - n_fidelity, + lengthscale_constraint=lengthscale_constraint_unbiased, + ) + + if covar_module_biased is None: + covar_module_biased = MaternKernel( + nu=nu, + batch_shape=self.batch_shape, + lengthscale_prior=lengthscale_prior_biased, + ard_num_dims=dimension - n_fidelity, + lengthscale_constraint=lengthscale_constraint_biased, + ) + + self.covar_module_unbiased = covar_module_unbiased + self.covar_module_biased = covar_module_biased + + @property + def power(self) -> Tensor: + return self.raw_power_constraint.transform(self.raw_power) + + @power.setter + def power(self, value: Tensor) -> None: + self._set_power(value) + + def _set_power(self, value: Tensor) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.raw_power) + self.initialize(raw_power=self.raw_power_constraint.inverse_transform(value)) + + def forward(self, x1: Tensor, x2: Tensor, diag: bool = False, **params) -> Tensor: + if params.get("last_dim_is_batch", False): + raise NotImplementedError( + "last_dim_is_batch not yet supported by LinearTruncatedFidelityKernel" + ) + + power = self.power.view(*self.batch_shape, 1, 1) + active_dimsM = torch.tensor( + [i for i in range(x1.size(-1)) if i not in self.fidelity_dims], + device=x1.device, + ) + if len(active_dimsM) == 0: + raise RuntimeError( + "Input to LinearTruncatedFidelityKernel must have at least one " + "non-fidelity dimension." + ) + x1_ = x1.index_select(dim=-1, index=active_dimsM) + x2_ = x2.index_select(dim=-1, index=active_dimsM) + covar_unbiased = self.covar_module_unbiased(x1_, x2_, diag=diag) + covar_biased = self.covar_module_biased(x1_, x2_, diag=diag) + + # clamp to avoid numerical issues + fd_idxr0 = torch.full( + (1,), self.fidelity_dims[0], dtype=torch.long, device=x1.device + ) + x11_ = x1.index_select(dim=-1, index=fd_idxr0).clamp(0, 1) + x21t_ = x2.index_select(dim=-1, index=fd_idxr0).clamp(0, 1) + if not diag: + x21t_ = x21t_.transpose(-1, -2) + cross_term_1 = (1 - x11_) * (1 - x21t_) + bias_factor = cross_term_1 * (1 + x11_ * x21t_).pow(power) + + if len(self.fidelity_dims) > 1: + # clamp to avoid numerical issues + fd_idxr1 = torch.full( + (1,), self.fidelity_dims[1], dtype=torch.long, device=x1.device + ) + x12_ = x1.index_select(dim=-1, index=fd_idxr1).clamp(0, 1) + x22t_ = x2.index_select(dim=-1, index=fd_idxr1).clamp(0, 1) + x1b_ = torch.cat([x11_, x12_], dim=-1) + if diag: + x2bt_ = torch.cat([x21t_, x22t_], dim=-1) + k = (1 + (x1b_ * x2bt_).sum(dim=-1, keepdim=True)).pow(power) + else: + x22t_ = x22t_.transpose(-1, -2) + x2bt_ = torch.cat([x21t_, x22t_], dim=-2) + k = (1 + x1b_ @ x2bt_).pow(power) + + cross_term_2 = (1 - x12_) * (1 - x22t_) + bias_factor += cross_term_2 * (1 + x12_ * x22t_).pow(power) + bias_factor += cross_term_2 * cross_term_1 * k + + if diag: + bias_factor = bias_factor.view(covar_biased.shape) + + return covar_unbiased + bias_factor * covar_biased
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/kernels/orthogonal_additive_kernel.html b/website-old/pages/api/_modules/botorch/models/kernels/orthogonal_additive_kernel.html new file mode 100644 index 0000000000..a520747258 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/kernels/orthogonal_additive_kernel.html @@ -0,0 +1,451 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.kernels.orthogonal_additive_kernel

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+import math
+
+import numpy
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from gpytorch.constraints import Interval, Positive
+from gpytorch.kernels import Kernel
+from gpytorch.module import Module
+from gpytorch.priors import Prior
+
+from torch import nn, Tensor
+
+_positivity_constraint = Positive()
+SECOND_ORDER_PRIOR_ERROR_MSG = (
+    "Second order interactions are disabled, but there is a prior on the second order "
+    "coefficients. Please remove the second order prior or enable second order terms."
+)
+
+
+
+[docs] +class OrthogonalAdditiveKernel(Kernel): + r"""Orthogonal Additive Kernels (OAKs) were introduced in [Lu2022additive]_, though + only for the case of Gaussian base kernels with a Gaussian input data distribution. + + The implementation here generalizes OAKs to arbitrary base kernels by using a + Gauss-Legendre quadrature approximation to the required one-dimensional integrals + involving the base kernels. + + .. [Lu2022additive] + X. Lu, A. Boukouvalas, and J. Hensman. Additive Gaussian processes revisited. + Proceedings of the 39th International Conference on Machine Learning. Jul 2022. + """ + + def __init__( + self, + base_kernel: Kernel, + dim: int, + quad_deg: int = 32, + second_order: bool = False, + batch_shape: torch.Size | None = None, + dtype: torch.dtype | None = None, + device: torch.device | None = None, + coeff_constraint: Interval = _positivity_constraint, + offset_prior: Prior | None = None, + coeffs_1_prior: Prior | None = None, + coeffs_2_prior: Prior | None = None, + ): + """ + Args: + base_kernel: The kernel which to orthogonalize and evaluate in `forward`. + dim: Input dimensionality of the kernel. + quad_deg: Number of integration nodes for orthogonalization. + second_order: Toggles second order interactions. If true, both the time and + space complexity of evaluating the kernel are quadratic in `dim`. + batch_shape: Optional batch shape for the kernel and its parameters. + dtype: Initialization dtype for required Tensors. + device: Initialization device for required Tensors. + coeff_constraint: Constraint on the coefficients of the additive kernel. + offset_prior: Prior on the offset coefficient. Should be prior with non- + negative support. + coeffs_1_prior: Prior on the parameter main effects. Should be prior with + non-negative support. + coeffs_2_prior: coeffs_1_prior: Prior on the parameter interactions. Should + be prior with non-negative support. + """ + super().__init__(batch_shape=batch_shape) + self.base_kernel = base_kernel + if not second_order and coeffs_2_prior is not None: + raise AttributeError(SECOND_ORDER_PRIOR_ERROR_MSG) + + # integration nodes, weights for [0, 1] + tkwargs = {"dtype": dtype, "device": device} + z, w = leggauss(deg=quad_deg, a=0, b=1, **tkwargs) + self.z = z.unsqueeze(-1).expand(quad_deg, dim) # deg x dim + self.w = w.unsqueeze(-1) + self.register_parameter( + name="raw_offset", + parameter=nn.Parameter(torch.zeros(self.batch_shape, **tkwargs)), + ) + log_d = math.log(dim) + self.register_parameter( + name="raw_coeffs_1", + parameter=nn.Parameter( + torch.zeros(*self.batch_shape, dim, **tkwargs) - log_d + ), + ) + self.register_parameter( + name="raw_coeffs_2", + parameter=( + nn.Parameter( + torch.zeros(*self.batch_shape, int(dim * (dim - 1) / 2), **tkwargs) + - 2 * log_d + ) + if second_order + else None + ), + ) + if offset_prior is not None: + self.register_prior( + name="offset_prior", + prior=offset_prior, + param_or_closure=_offset_param, + setting_closure=_offset_closure, + ) + if coeffs_1_prior is not None: + self.register_prior( + name="coeffs_1_prior", + prior=coeffs_1_prior, + param_or_closure=_coeffs_1_param, + setting_closure=_coeffs_1_closure, + ) + if coeffs_2_prior is not None: + self.register_prior( + name="coeffs_2_prior", + prior=coeffs_2_prior, + param_or_closure=_coeffs_2_param, + setting_closure=_coeffs_2_closure, + ) + + # for second order interactions, we only + if second_order: + self._rev_triu_indices = torch.tensor( + _reverse_triu_indices(dim), + device=device, + dtype=int, + ) + # zero tensor for construction of upper-triangular coefficient matrix + self._quad_zero = torch.zeros( + tuple(1 for _ in range(len(self.batch_shape) + 1)), **tkwargs + ).expand(*self.batch_shape, 1) + self.coeff_constraint = coeff_constraint + self.dim = dim + + def k(self, x1: Tensor, x2: Tensor) -> Tensor: + """Evaluates the kernel matrix base_kernel(x1, x2) on each input dimension + independently. + + Args: + x1: `batch_shape x n1 x d`-dim Tensor in [0, 1]^dim. + x2: `batch_shape x n2 x d`-dim Tensor in [0, 1]^dim. + + Returns: + A `batch_shape x d x n1 x n2`-dim Tensor of kernel matrices. + """ + return self.base_kernel(x1, x2, last_dim_is_batch=True).to_dense() + + @property + def offset(self) -> Tensor: + """Returns the `batch_shape`-dim Tensor of zeroth-order coefficients.""" + return self.coeff_constraint.transform(self.raw_offset) + + @property + def coeffs_1(self) -> Tensor: + """Returns the `batch_shape x d`-dim Tensor of first-order coefficients.""" + return self.coeff_constraint.transform(self.raw_coeffs_1) + + @property + def coeffs_2(self) -> Tensor | None: + """Returns the upper-triangular tensor of second-order coefficients. + + NOTE: We only keep track of the upper triangular part of raw second order + coefficients since the effect of the lower triangular part is identical and + exclude the diagonal, since it is associated with first-order effects only. + While we could further exploit this structure in the forward pass, the + associated indexing and temporary allocations make it significantly less + efficient than the einsum-based implementation below. + + Returns: + `batch_shape x d x d`-dim Tensor of second-order coefficients. + """ + if self.raw_coeffs_2 is not None: + C2 = self.coeff_constraint.transform(self.raw_coeffs_2) + C2 = torch.cat((C2, self._quad_zero), dim=-1) # batch_shape x (d(d-1)/2+1) + C2 = C2.index_select(-1, self._rev_triu_indices) + return C2.reshape(*self.batch_shape, self.dim, self.dim) + else: + return None + + def _set_coeffs_1(self, value: Tensor) -> None: + value = torch.as_tensor(value).to(self.raw_coeffs_1) + value = value.expand(*self.batch_shape, self.dim) + self.initialize(raw_coeffs_1=self.coeff_constraint.inverse_transform(value)) + + def _set_coeffs_2(self, value: Tensor) -> None: + value = torch.as_tensor(value).to(self.raw_coeffs_1) + value = value.expand(*self.batch_shape, self.dim, self.dim) + row_idcs, col_idcs = torch.triu_indices(self.dim, self.dim, offset=1) + value = value[..., row_idcs, col_idcs].to(self.raw_coeffs_2) + self.initialize(raw_coeffs_2=self.coeff_constraint.inverse_transform(value)) + + def _set_offset(self, value: Tensor) -> None: + value = torch.as_tensor(value).to(self.raw_offset) + self.initialize(raw_offset=self.coeff_constraint.inverse_transform(value)) + + @coeffs_1.setter + def coeffs_1(self, value) -> None: + self._set_coeffs_1(value) + + @coeffs_2.setter + def coeffs_2(self, value) -> None: + self._set_coeffs_2(value) + + @offset.setter + def offset(self, value) -> None: + self._set_offset(value) + + def forward( + self, + x1: Tensor, + x2: Tensor, + diag: bool = False, + last_dim_is_batch: bool = False, + ) -> Tensor: + """Computes the kernel matrix k(x1, x2). + + Args: + x1: `batch_shape x n1 x d`-dim Tensor in [0, 1]^dim. + x2: `batch_shape x n2 x d`-dim Tensor in [0, 1]^dim. + diag: If True, only returns the diagonal of the kernel matrix. + last_dim_is_batch: Not supported by this kernel. + + Returns: + A `batch_shape x n1 x n2`-dim Tensor of kernel matrices. + """ + if last_dim_is_batch: + raise UnsupportedError( + "OrthogonalAdditiveKernel does not support `last_dim_is_batch`." + ) + K_ortho = self._orthogonal_base_kernels(x1, x2) # batch_shape x d x n1 x n2 + + # contracting over d, leading to `batch_shape x n x n`-dim tensor, i.e.: + # K1 = torch.sum(self.coeffs_1[..., None, None] * K_ortho, dim=-3) + K1 = torch.einsum(self.coeffs_1, [..., 0], K_ortho, [..., 0, 1, 2], [..., 1, 2]) + # adding the non-batch dimensions to offset + K = K1 + self.offset[..., None, None] + if self.coeffs_2 is not None: + # Computing the tensor of second order interactions K2. + # NOTE: K2 here is equivalent to: + # K2 = K_ortho.unsqueeze(-4) * K_ortho.unsqueeze(-3) # d x d x n x n + # K2 = (self.coeffs_2[..., None, None] * K2).sum(dim=(-4, -3)) + # but avoids forming the `batch_shape x d x d x n x n`-dim tensor in memory. + # Reducing over the dimensions with the O(d^2) quadratic terms: + K2 = torch.einsum( + K_ortho, + [..., 0, 2, 3], + K_ortho, + [..., 1, 2, 3], + self.coeffs_2, + [..., 0, 1], + [..., 2, 3], # i.e. contracting over the first two non-batch dims + ) + K = K + K2 + + return K if not diag else K.diag() # poor man's diag (TODO) + + def _orthogonal_base_kernels(self, x1: Tensor, x2: Tensor) -> Tensor: + """Evaluates the set of `d` orthogonalized base kernels on (x1, x2). + Note that even if the base kernel is positive, the orthogonalized versions + can - and usually do - take negative values. + + Args: + x1: `batch_shape x n1 x d`-dim inputs to the kernel. + x2: `batch_shape x n2 x d`-dim inputs to the kernel. + + Returns: + A `batch_shape x d x n1 x n2`-dim Tensor. + """ + _check_hypercube(x1, "x1") + if x1 is not x2: + _check_hypercube(x2, "x2") + Kx1x2 = self.k(x1, x2) # d x n x n + # Overwriting allocated quadrature tensors with fitting dtype and device + # self.z, self.w = self.z.to(x1), self.w.to(x1) + # include normalization constant in weights + w = self.w / self.normalizer().sqrt() + Skx1 = self.k(x1, self.z) @ w # batch_shape x d x n + Skx2 = Skx1 if (x1 is x2) else self.k(x2, self.z) @ w # d x n + # this is a tensor of kernel matrices of orthogonal 1d kernels + K_ortho = (Kx1x2 - Skx1 @ Skx2.transpose(-2, -1)).to_dense() # d x n x n + return K_ortho + + def normalizer(self, eps: float = 1e-6) -> Tensor: + """Integrates the `d` orthogonalized base kernels over `[0, 1] x [0, 1]`. + NOTE: If the module is in train mode, this needs to re-compute the normalizer + each time because the underlying parameters might have changed. + + Args: + eps: Minimum value constraint on the normalizers. Avoids division by zero. + + Returns: + A `d`-dim tensor of normalization constants. + """ + if self.train() or getattr(self, "_normalizer", None) is None: + self._normalizer = (self.w.T @ self.k(self.z, self.z) @ self.w).clamp(eps) + return self._normalizer
+ + + +def leggauss( + deg: int, + a: float = -1.0, + b: float = 1.0, + dtype: torch.dtype | None = None, + device: torch.device | None = None, +) -> tuple[Tensor, Tensor]: + """Computes Gauss-Legendre quadrature nodes and weights. Wraps + `numpy.polynomial.legendre.leggauss` and returns Torch Tensors. + + Args: + deg: Number of sample points and weights. Integrates poynomials of degree + `2 * deg + 1` exactly. + a, b: Lower and upper bound of integration domain. + dtype: Desired floating point type of the return Tensors. + device: Desired device type of the return Tensors. + + Returns: + A tuple of Gauss-Legendre quadrature nodes and weights of length deg. + """ + dtype = dtype if dtype is not None else torch.get_default_dtype() + x, w = numpy.polynomial.legendre.leggauss(deg=deg) + x = torch.as_tensor(x, dtype=dtype, device=device) + w = torch.as_tensor(w, dtype=dtype, device=device) + if not (a == -1 and b == 1): # need to normalize for different domain + x = (b - a) * (x + 1) / 2 + a + w = w * ((b - a) / 2) + return x, w + + +def _check_hypercube(x: Tensor, name: str) -> None: + """Raises a `ValueError` if an element `x` is not in [0, 1]. + + Args: + x: Tensor to be checked. + name: Name of the Tensor for the error message. + """ + if (x < 0).any() or (x > 1).any(): + raise ValueError(name + " is not in hypercube [0, 1]^d.") + + +def _reverse_triu_indices(d: int) -> list[int]: + """Computes a list of indices which, upon indexing a `d * (d - 1) / 2 + 1`-dim + Tensor whose last element is zero, will lead to a vectorized representation of + an upper-triangular matrix, whose diagonal is set to zero and whose super-diagonal + elements are set to the `d * (d - 1) / 2` values in the original tensor. + + NOTE: This is a helper function for Orthogonal Additive Kernels, and allows the + implementation to only register `d * (d - 1) / 2` parameters to model the second + order interactions, instead of the full d^2 redundant terms. + + Args: + d: Dimensionality that gives rise to the `d * (d - 1) / 2` quadratic terms. + + Returns: + A list of integer indices in `[0, d * (d - 1) / 2]`. See above for details. + """ + indices = [] + j = 0 + d2 = int(d * (d - 1) / 2) + for i in range(d): + indices.extend(d2 for _ in range(i + 1)) # indexing zero (sub-diagonal) + indices.extend(range(j, j + d - i - 1)) # indexing coeffs (super-diagonal) + j += d - i - 1 + return indices + + +def _coeffs_1_param(m: Module) -> Tensor: + return m.coeffs_1 + + +def _coeffs_2_param(m: Module) -> Tensor: + return m.coeffs_2 + + +def _offset_param(m: Module) -> Tensor: + return m.offset + + +def _coeffs_1_closure(m: Module, v: Tensor) -> Tensor: + return m._set_coeffs_1(v) + + +def _coeffs_2_closure(m: Module, v: Tensor) -> Tensor: + return m._set_coeffs_2(v) + + +def _offset_closure(m: Module, v: Tensor) -> Tensor: + return m._set_offset(v) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/likelihoods/pairwise.html b/website-old/pages/api/_modules/botorch/models/likelihoods/pairwise.html new file mode 100644 index 0000000000..cc1831cf5a --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/likelihoods/pairwise.html @@ -0,0 +1,321 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.likelihoods.pairwise

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Pairwise likelihood for pairwise preference model (e.g., PairwiseGP).
+"""
+
+from __future__ import annotations
+
+import math
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.utils.probability.utils import (
+    log_ndtr,
+    log_phi,
+    standard_normal_log_hazard,
+)
+from gpytorch.likelihoods import Likelihood
+from torch import Tensor
+from torch.distributions import Bernoulli
+
+
+
+[docs] +class PairwiseLikelihood(Likelihood, ABC): + """ + Pairwise likelihood base class for pairwise preference GP (e.g., PairwiseGP). + """ + + def __init__(self, max_plate_nesting: int = 1): + """ + Initialized like a `gpytorch.likelihoods.Likelihood`. + + Args: + max_plate_nesting: Defaults to 1. + """ + super().__init__(max_plate_nesting) + +
+[docs] + def forward(self, utility: Tensor, D: Tensor) -> Bernoulli: + """Given the difference in (estimated) utility util_diff = f(v) - f(u), + return a Bernoulli distribution object representing the likelihood of + the user prefer v over u. + + Note that this is not used by the `PairwiseGP` model, + """ + return Bernoulli(probs=self.p(utility=utility, D=D))
+ + +
+[docs] + @abstractmethod + def p(self, utility: Tensor, D: Tensor) -> Tensor: + """Given the difference in (estimated) utility util_diff = f(v) - f(u), + return the probability of the user prefer v over u. + + Args: + utility: A Tensor of shape `(batch_size) x n`, the utility at MAP point + D: D is `(batch_size x) m x n` matrix with all elements being zero in last + dimension except at two positions D[..., i] = 1 and D[..., j] = -1 + respectively, representing item i is preferred over item j. + log: if true, return log probability + """
+ + +
+[docs] + def log_p(self, utility: Tensor, D: Tensor) -> Tensor: + """return the log of p""" + return torch.log(self.p(utility=utility, D=D))
+ + +
+[docs] + def negative_log_gradient_sum(self, utility: Tensor, D: Tensor) -> Tensor: + """Calculate the sum of negative log gradient with respect to each item's latent + utility values. Useful for models using laplace approximation. + + Args: + utility: A Tensor of shape `(batch_size x) n`, the utility at MAP point + D: D is `(batch_size x) m x n` matrix with all elements being zero in last + dimension except at two positions D[..., i] = 1 and D[..., j] = -1 + respectively, representing item i is preferred over item j. + + Returns: + A `(batch_size x) n` Tensor representing the sum of negative log gradient + values of the likelihood over all comparisons (i.e., the m dimension) + with respect to each item. + """ + raise NotImplementedError
+ + +
+[docs] + def negative_log_hessian_sum(self, utility: Tensor, D: Tensor) -> Tensor: + """Calculate the sum of negative log hessian with respect to each item's latent + utility values. Useful for models using laplace approximation. + + Args: + utility: A Tensor of shape `(batch_size) x n`, the utility at MAP point + D: D is `(batch_size x) m x n` matrix with all elements being zero in last + dimension except at two positions D[..., i] = 1 and D[..., j] = -1 + respectively, representing item i is preferred over item j. + + Returns: + A `(batch_size x) n x n` Tensor representing the sum of negative log hessian + values of the likelihood over all comparisons (i.e., the m dimension) with + respect to each item. + """ + raise NotImplementedError
+
+ + + +
+[docs] +class PairwiseProbitLikelihood(PairwiseLikelihood): + """Pairwise likelihood using probit function + + Given two items v and u with utilities f(v) and f(u), the probability that we + prefer v over u with probability std_normal_cdf((f(v) - f(u))/sqrt(2)). Note + that this formulation implicitly assume the noise term is fixed at 1. + """ + + # Clamping z values for better numerical stability. See self._calc_z for detail + # norm_cdf(z=3) ~= 0.999, top 0.1% percent + _zlim = 3 + + def _calc_z(self, utility: Tensor, D: Tensor) -> Tensor: + """Calculate the z score given estimated utility values and + the comparison matrix D. + """ + scaled_util = (utility / math.sqrt(2)).unsqueeze(-1) + z = D.to(scaled_util) @ scaled_util + z = z.clamp(-self._zlim, self._zlim).squeeze(-1) + return z + + def _calc_z_derived(self, z: Tensor) -> tuple[Tensor, Tensor, Tensor]: + """Calculate auxiliary statistics derived from z, including log pdf, + log cdf, and the hazard function (pdf divided by cdf) + + Args: + z: A Tensor of arbitrary shape. + + Returns: + Tensors with standard normal logpdf(z), logcdf(z), and hazard function + values evaluated at -z. + """ + return log_phi(z), log_ndtr(z), standard_normal_log_hazard(-z).exp() + +
+[docs] + def p(self, utility: Tensor, D: Tensor, log: bool = False) -> Tensor: + z = self._calc_z(utility=utility, D=D) + std_norm = torch.distributions.normal.Normal( + torch.zeros(1, dtype=z.dtype, device=z.device), + torch.ones(1, dtype=z.dtype, device=z.device), + ) + return std_norm.cdf(z)
+ + +
+[docs] + def negative_log_gradient_sum(self, utility: Tensor, D: Tensor) -> Tensor: + # Compute the sum over of grad. of negative Log-LH wrt utility f. + # Original grad should be of dimension m x n, as in (6) from + # [Chu2005preference]_. The sum over the m dimension of grad. of + # negative log likelihood with respect to the utility + z = self._calc_z(utility, D) + _, _, h = self._calc_z_derived(z) + h_factor = h / math.sqrt(2) + grad = (h_factor.unsqueeze(-2) @ (-D)).squeeze(-2) + return grad
+ + +
+[docs] + def negative_log_hessian_sum(self, utility: Tensor, D: Tensor) -> Tensor: + # Original hess should be of dimension m x n x n, as in (7) from + # [Chu2005preference]_ Sum over the first dimension and return a tensor of + # shape n x n. + # The sum over the m dimension of hessian of negative log likelihood + # with respect to the utility + DT = D.transpose(-1, -2) + z = self._calc_z(utility, D) + _, _, h = self._calc_z_derived(z) + mul_factor = h * (h + z) / 2 + mul_factor = mul_factor.unsqueeze(-2).expand(*DT.size()) + # multiply the hessian value by preference signs + # (+1 if preferred or -1 otherwise) and sum over the m dimension + hess = DT * mul_factor @ D + return hess
+
+ + + +
+[docs] +class PairwiseLogitLikelihood(PairwiseLikelihood): + """Pairwise likelihood using logistic (i.e., sigmoid) function + + Given two items v and u with utilities f(v) and f(u), the probability that we + prefer v over u with probability sigmoid(f(v) - f(u)). Note + that this formulation implicitly assume the beta term in logistic function is + fixed at 1. + """ + + # Clamping logit values for better numerical stability. + # See self._calc_logit for detail logistic(8) ~= 0.9997, top 0.03% percent + _logit_lim = 8 + + def _calc_logit(self, utility: Tensor, D: Tensor) -> Tensor: + logit = D.to(utility) @ utility.unsqueeze(-1) + logit = logit.clamp(-self._logit_lim, self._logit_lim).squeeze(-1) + return logit + +
+[docs] + def log_p(self, utility: Tensor, D: Tensor) -> Tensor: + logit = self._calc_logit(utility=utility, D=D) + return torch.nn.functional.logsigmoid(logit)
+ + +
+[docs] + def p(self, utility: Tensor, D: Tensor) -> Tensor: + logit = self._calc_logit(utility=utility, D=D) + return torch.sigmoid(logit)
+ + +
+[docs] + def negative_log_gradient_sum(self, utility: Tensor, D: Tensor) -> Tensor: + indices_shape = utility.shape[:-1] + (-1,) + winner_indices = (D == 1).nonzero(as_tuple=True)[-1].reshape(indices_shape) + loser_indices = (D == -1).nonzero(as_tuple=True)[-1].reshape(indices_shape) + ex = torch.exp(torch.gather(utility, -1, winner_indices)) + ey = torch.exp(torch.gather(utility, -1, loser_indices)) + unsigned_grad = ey / (ex + ey) + grad = (unsigned_grad.unsqueeze(-2) @ (-D)).squeeze(-2) + return grad
+ + +
+[docs] + def negative_log_hessian_sum(self, utility: Tensor, D: Tensor) -> Tensor: + DT = D.transpose(-1, -2) + # calculating f(v) - f(u) given u > v information in D + neg_logit = -(D @ utility.unsqueeze(-1)).squeeze(-1) + term = torch.sigmoid(neg_logit) + mul_factor = term - (term) ** 2 + mul_factor = mul_factor.unsqueeze(-2).expand(*DT.size()) + # multiply the hessian value by preference signs + # (+1 if preferred or -1 otherwise) and sum over the m dimension + hess = DT * mul_factor @ D + return hess
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/model.html b/website-old/pages/api/_modules/botorch/models/model.html new file mode 100644 index 0000000000..9ad9dace77 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/model.html @@ -0,0 +1,807 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.model

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Abstract base module for all BoTorch models.
+
+This module contains `Model`, the abstract base class for all BoTorch models,
+and `ModelList`, a container for a list of Models.
+"""
+
+from __future__ import annotations
+
+import warnings
+from abc import ABC, abstractmethod
+from collections import defaultdict
+from collections.abc import Callable, Mapping
+from typing import Any, TYPE_CHECKING
+
+import numpy as np
+import torch
+from botorch import settings
+from botorch.exceptions.errors import (
+    BotorchTensorDimensionError,
+    DeprecationError,
+    InputDataError,
+)
+from botorch.logging import shape_to_str
+from botorch.models.utils.assorted import fantasize as fantasize_flag
+from botorch.posteriors import Posterior, PosteriorList
+from botorch.sampling.base import MCSampler
+from botorch.sampling.list_sampler import ListSampler
+from botorch.utils.containers import BotorchContainer
+from botorch.utils.datasets import SupervisedDataset
+from botorch.utils.transforms import is_fully_bayesian
+from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood
+from torch import Tensor
+from torch.nn import Module, ModuleDict, ModuleList
+from typing_extensions import Self
+
+if TYPE_CHECKING:
+    from botorch.acquisition.objective import PosteriorTransform  # pragma: no cover
+
+
+
+[docs] +class Model(Module, ABC): + r"""Abstract base class for BoTorch models. + + The `Model` base class cannot be used directly; it only defines an API for other + BoTorch models. + + `Model` subclasses `torch.nn.Module`. While a `Module` is most typically + encountered as a representation of a neural network layer, it can be used more + generally: see + `documentation <https://pytorch.org/tutorials/beginner/examples_nn/polynomial_module.html>`_ + on custom NN Modules. + + `Module` provides several pieces of useful functionality: A `Model`'s attributes of + `Tensor` or `Module` type are automatically registered so they can be moved and/or + cast with the `to` method, automatically differentiated, and used with CUDA. + + Attributes: + _has_transformed_inputs: A boolean denoting whether `train_inputs` are currently + stored as transformed or not. + _original_train_inputs: A Tensor storing the original train inputs for use in + `_revert_to_original_inputs`. Note that this is necessary since + transform / untransform cycle introduces numerical errors which lead + to upstream errors during training. + _is_fully_bayesian: Returns `True` if this is a fully Bayesian model. + _is_ensemble: Returns `True` if this model consists of multiple models + that are stored in an additional batch dimension. This is true for the fully + Bayesian models. + """ # noqa: E501 + + _has_transformed_inputs: bool = False + _original_train_inputs: Tensor | None = None + _is_fully_bayesian = False + _is_ensemble = False + +
+[docs] + @abstractmethod + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool | Tensor = False, + posterior_transform: PosteriorTransform | None = None, + ) -> Posterior: + r"""Computes the posterior over model outputs at the provided points. + + Note: The input transforms should be applied here using + `self.transform_inputs(X)` after the `self.eval()` call and before + any `model.forward` or `model.likelihood` calls. + + Args: + X: A `b x q x d`-dim Tensor, where `d` is the dimension of the + feature space, `q` is the number of points considered jointly, + and `b` is the batch dimension. + output_indices: A list of indices, corresponding to the outputs over + which to compute the posterior (if the model is multi-output). + Can be used to speed up computation if only a subset of the + model's outputs are required for optimization. If omitted, + computes the posterior over all model outputs. + observation_noise: For models with an inferred noise level, if True, + include observation noise. For models with an observed noise level, + this must be a `model_batch_shape x 1 x m`-dim tensor or + a `model_batch_shape x n' x m`-dim tensor containing the average + noise for each batch and output. `noise` must be in the + outcome-transformed space if an outcome transform is used. + posterior_transform: An optional PosteriorTransform. + + Returns: + A `Posterior` object, representing a batch of `b` joint distributions + over `q` points and `m` outputs each. + """ + pass # pragma: no cover
+ + + @property + def batch_shape(self) -> torch.Size: + r"""The batch shape of the model. + + This is a batch shape from an I/O perspective, independent of the internal + representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). + For a model with `m` outputs, a `test_batch_shape x q x d`-shaped input `X` + to the `posterior` method returns a Posterior object over an output of + shape `broadcast(test_batch_shape, model.batch_shape) x q x m`. + """ + cls_name = self.__class__.__name__ + raise NotImplementedError(f"{cls_name} does not define batch_shape property") + + @property + def num_outputs(self) -> int: + r"""The number of outputs of the model.""" + cls_name = self.__class__.__name__ + raise NotImplementedError(f"{cls_name} does not define num_outputs property") + +
+[docs] + def subset_output(self, idcs: list[int]) -> Model: + r"""Subset the model along the output dimension. + + Args: + idcs: The output indices to subset the model to. + + Returns: + A `Model` object of the same type and with the same parameters as + the current model, subset to the specified output indices. + """ + raise NotImplementedError
+ + +
+[docs] + def condition_on_observations(self, X: Tensor, Y: Tensor, **kwargs: Any) -> Model: + r"""Condition the model on new observations. + + Args: + X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of + the feature space, `n'` is the number of points per batch, and + `batch_shape` is the batch shape (must be compatible with the + batch shape of the model). + Y: A `batch_shape' x n' x m`-dim Tensor, where `m` is the number of + model outputs, `n'` is the number of points per batch, and + `batch_shape'` is the batch shape of the observations. + `batch_shape'` must be broadcastable to `batch_shape` using + standard broadcasting semantics. If `Y` has fewer batch dimensions + than `X`, it is assumed that the missing batch dimensions are + the same for all `Y`. + + Returns: + A `Model` object of the same type, representing the original model + conditioned on the new observations `(X, Y)` (and possibly noise + observations passed in via kwargs). + """ + raise NotImplementedError( + f"`condition_on_observations` not implemented for {self.__class__.__name__}" + )
+ + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset, + ) -> dict[str, BotorchContainer | Tensor]: + """ + Construct `Model` keyword arguments from a `SupervisedDataset`. + + Args: + training_data: A `SupervisedDataset`, with attributes `train_X`, + `train_Y`, and, optionally, `train_Yvar`. + + Returns: + A dict of keyword arguments that can be used to initialize a `Model`, + with keys `train_X`, `train_Y`, and, optionally, `train_Yvar`. + """ + if not isinstance(training_data, SupervisedDataset): + raise TypeError( + "Expected `training_data` to be a `SupervisedDataset`, but got " + f"{type(training_data)}." + ) + parsed_data = {"train_X": training_data.X, "train_Y": training_data.Y} + if training_data.Yvar is not None: + parsed_data["train_Yvar"] = training_data.Yvar + return parsed_data
+ + +
+[docs] + def transform_inputs( + self, + X: Tensor, + input_transform: Module | None = None, + ) -> Tensor: + r"""Transform inputs. + + Args: + X: A tensor of inputs + input_transform: A Module that performs the input transformation. + + Returns: + A tensor of transformed inputs + """ + if input_transform is not None: + input_transform.to(X) + return input_transform(X) + try: + return self.input_transform(X) + except AttributeError: + return X
+ + + def _set_transformed_inputs(self) -> None: + r"""Update training inputs with transformed inputs.""" + if hasattr(self, "input_transform") and not self._has_transformed_inputs: + if hasattr(self, "train_inputs"): + self._original_train_inputs = self.train_inputs[0] + with torch.no_grad(): + X_tf = self.input_transform.preprocess_transform( + self.train_inputs[0] + ) + self.set_train_data(X_tf, strict=False) + self._has_transformed_inputs = True + else: + warnings.warn( + "Could not update `train_inputs` with transformed inputs " + f"since {self.__class__.__name__} does not have a `train_inputs` " + "attribute. Make sure that the `input_transform` is applied to " + "both the train inputs and test inputs.", + RuntimeWarning, + stacklevel=3, + ) + + def _revert_to_original_inputs(self) -> None: + r"""Revert training inputs back to original.""" + if hasattr(self, "input_transform") and self._has_transformed_inputs: + self.set_train_data(self._original_train_inputs, strict=False) + self._has_transformed_inputs = False + +
+[docs] + def eval(self) -> Model: + r"""Puts the model in `eval` mode and sets the transformed inputs.""" + self._set_transformed_inputs() + return super().eval()
+ + +
+[docs] + def train(self, mode: bool = True) -> Model: + r"""Put the model in `train` mode. Reverts to the original inputs if in `train` + mode (`mode=True`) or sets transformed inputs if in `eval` mode (`mode=False`). + + Args: + mode: A boolean denoting whether to put in `train` or `eval` mode. + If `False`, model is put in `eval` mode. + """ + if mode: + self._revert_to_original_inputs() + else: + self._set_transformed_inputs() + return super().train(mode=mode)
+ + + @property + def dtypes_of_buffers(self) -> set[torch.dtype]: + return {t.dtype for t in self.buffers() if t is not None}
+ + + +
+[docs] +class FantasizeMixin(ABC): + """ + Mixin to add a `fantasize` method to a `Model`. + + Example: + class BaseModel: + def __init__(self, ...): + def condition_on_observations(self, ...): + def posterior(self, ...): + def transform_inputs(self, ...): + + class ModelThatCanFantasize(BaseModel, FantasizeMixin): + def __init__(self, args): + super().__init__(args) + + model = ModelThatCanFantasize(...) + model.fantasize(X) + """ + +
+[docs] + @abstractmethod + def condition_on_observations(self, X: Tensor, Y: Tensor) -> Self: + """ + Classes that inherit from `FantasizeMixin` must implement + a `condition_on_observations` method. + """
+ + +
+[docs] + @abstractmethod + def posterior( + self, + X: Tensor, + *args, + observation_noise: bool = False, + ) -> Posterior: + """ + Classes that inherit from `FantasizeMixin` must implement + a `posterior` method. + """
+ + +
+[docs] + @abstractmethod + def transform_inputs( + self, + X: Tensor, + input_transform: Module | None = None, + ) -> Tensor: + """ + Classes that inherit from `FantasizeMixin` must implement + a `transform_inputs` method. + """
+ + +
+[docs] + def fantasize( + self, + X: Tensor, + sampler: MCSampler, + observation_noise: Tensor | None = None, + **kwargs: Any, + ) -> Self: + r"""Construct a fantasy model. + + Constructs a fantasy model in the following fashion: + (1) compute the model posterior at `X`, including observation noise. + If `observation_noise` is a Tensor, use it directly as the observation + noise to add. + (2) sample from this posterior (using `sampler`) to generate "fake" + observations. + (3) condition the model on the new fake observations. + + Args: + X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of + the feature space, `n'` is the number of points per batch, and + `batch_shape` is the batch shape (must be compatible with the + batch shape of the model). + sampler: The sampler used for sampling from the posterior at `X`. + observation_noise: A `model_batch_shape x 1 x m`-dim tensor or + a `model_batch_shape x n' x m`-dim tensor containing the average + noise for each batch and output, where `m` is the number of outputs. + `noise` must be in the outcome-transformed space if an outcome + transform is used. + If None and using an inferred noise likelihood, the noise will be the + inferred noise level. If using a fixed noise likelihood, the mean across + the observation noise in the training data is used as observation noise. + kwargs: Will be passed to `model.condition_on_observations` + + Returns: + The constructed fantasy model. + """ + if not isinstance(observation_noise, Tensor) and observation_noise is not None: + raise DeprecationError( + "`fantasize` no longer accepts a boolean for `observation_noise`." + ) + elif observation_noise is None and isinstance( + self.likelihood, FixedNoiseGaussianLikelihood + ): + if self.num_outputs > 1: + # make noise ... x n x m + observation_noise = self.likelihood.noise.transpose(-1, -2) + else: + observation_noise = self.likelihood.noise.unsqueeze(-1) + observation_noise = observation_noise.mean(dim=-2, keepdim=True) + # if the inputs are empty, expand the inputs + if X.shape[-2] == 0: + output_shape = ( + sampler.sample_shape + + X.shape[:-2] + + self.batch_shape + + torch.Size([0, self.num_outputs]) + ) + Y = torch.empty(output_shape, dtype=X.dtype, device=X.device) + if observation_noise is not None: + kwargs["noise"] = observation_noise.expand(Y.shape[1:]) + return self.condition_on_observations( + X=self.transform_inputs(X), + Y=Y, + **kwargs, + ) + propagate_grads = kwargs.pop("propagate_grads", False) + with fantasize_flag(): + with settings.propagate_grads(propagate_grads): + post_X = self.posterior( + X, + observation_noise=( + True if observation_noise is None else observation_noise + ), + ) + Y_fantasized = sampler(post_X) # num_fantasies x batch_shape x n' x m + if observation_noise is not None: + kwargs["noise"] = observation_noise.expand(Y_fantasized.shape[1:]) + return self.condition_on_observations( + X=self.transform_inputs(X), Y=Y_fantasized, **kwargs + )
+
+ + + +
+[docs] +class ModelList(Model): + r"""A multi-output Model represented by a list of independent models. + + All BoTorch models are acceptable as inputs. The cost of this flexibility is + that `ModelList` does not support all methods that may be implemented by its + component models. One use case for `ModelList` is combining a regression + model and a deterministic model in one multi-output container model, e.g. + for cost-aware or multi-objective optimization where one of the outcomes is + a deterministic function of the inputs. + """ + + def __init__(self, *models: Model) -> None: + r""" + Args: + *models: A variable number of models. + + Example: + >>> m_1 = SingleTaskGP(train_X, train_Y) + >>> m_2 = GenericDeterministicModel(lambda x: x.sum(dim=-1)) + >>> m_12 = ModelList(m_1, m_2) + >>> m_12.posterior(test_X) + """ + super().__init__() + self.models = ModuleList(models) + + def _get_group_subset_indices(self, idcs: list[int] | None) -> dict[int, list[int]]: + r"""Convert global subset indices to indices for the individual models. + + Args: + idcs: A list of indices to which the `ModelList` model is to be + subset to. + + Returns: + A dictionary mapping model indices to subset indices of the + respective model in the `ModelList`. + """ + if idcs is None: + return {i: None for i in range(len(self.models))} + output_sizes = [model.num_outputs for model in self.models] + cum_output_sizes = np.cumsum(output_sizes) + idcs = [idx % cum_output_sizes[-1] for idx in idcs] + group_indices: dict[int, list[int]] = defaultdict(list) + for idx in idcs: + grp_idx = np.argwhere(idx < cum_output_sizes)[0].item() + sub_idx = idx - int(np.sum(output_sizes[:grp_idx])) + group_indices[grp_idx].append(sub_idx) + return group_indices + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool | Tensor = False, + posterior_transform: Callable[[PosteriorList], Posterior] | None = None, + ) -> Posterior: + r"""Computes the posterior over model outputs at the provided points. + + Note: The input transforms should be applied here using + `self.transform_inputs(X)` after the `self.eval()` call and before + any `model.forward` or `model.likelihood` calls. + + Args: + X: A `b x q x d`-dim Tensor, where `d` is the dimension of the + feature space, `q` is the number of points considered jointly, + and `b` is the batch dimension. + output_indices: A list of indices, corresponding to the outputs over + which to compute the posterior (if the model is multi-output). + Can be used to speed up computation if only a subset of the + model's outputs are required for optimization. If omitted, + computes the posterior over all model outputs. + observation_noise: If True, add the observation noise from the + respective likelihoods to the posterior. If a Tensor of shape + `(batch_shape) x q x m`, use it directly as the observation + noise (with `observation_noise[...,i]` added to the posterior + of the `i`-th model). `observation_noise` is assumed + to be in the outcome-transformed space, if an outcome transform + is used by the model. + posterior_transform: An optional PosteriorTransform. + + Returns: + A `Posterior` object, representing a batch of `b` joint distributions + over `q` points and `m` outputs each. + """ + group_indices = self._get_group_subset_indices(idcs=output_indices) + posteriors = [] + for i, idcs in group_indices.items(): + if isinstance(observation_noise, Tensor): + if idcs is None: + start_idx = sum(m.num_outputs for m in self.models[:i]) + end_idx = start_idx + self.models[i].num_outputs + idcs = list(range(start_idx, end_idx)) + obs_noise = observation_noise[..., idcs] + else: + obs_noise = observation_noise + posteriors.append( + self.models[i].posterior( + X=X, output_indices=idcs, observation_noise=obs_noise + ) + ) + posterior = PosteriorList(*posteriors) + if posterior_transform is not None: + posterior = posterior_transform(posterior) + return posterior
+ + + @property + def batch_shape(self) -> torch.Size: + r"""The batch shape of the model. + + This is a batch shape from an I/O perspective, independent of the internal + representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). + For a model with `m` outputs, a `test_batch_shape x q x d`-shaped input `X` + to the `posterior` method returns a Posterior object over an output of + shape `broadcast(test_batch_shape, model.batch_shape) x q x m`. + """ + batch_shape = self.models[0].batch_shape + if all(batch_shape == m.batch_shape for m in self.models[1:]): + return batch_shape + # TODO: Allow broadcasting of model batch shapes + raise NotImplementedError( + f"`{self.__class__.__name__}.batch_shape` is only supported if all " + "constituent models have the same `batch_shape`." + ) + + @property + def num_outputs(self) -> int: + r"""The number of outputs of the model. + + Equal to the sum of the number of outputs of the individual models + in the ModelList. + """ + return sum(model.num_outputs for model in self.models) + +
+[docs] + def subset_output(self, idcs: list[int]) -> Model: + r"""Subset the model along the output dimension. + + Args: + idcs: The output indices to subset the model to. Relative to the + overall number of outputs of the model. + + Returns: + A `Model` (either a `ModelList` or one of the submodels) with + the outputs subset to the indices in `idcs`. + + Internally, this drops (if single-output) or subsets (if multi-output) + the constitutent models and returns them as a `ModelList`. If the + result is a single (possibly subset) model from the list, returns this + model (instead of forming a degenerate singe-model `ModelList`). + For instance, if `m = ModelList(m1, m2)` with `m1` a two-output model + and `m2` a single-output model, then `m.subset_output([1]) ` will return + the model `m1` subset to its second output. + """ + group_indices = self._get_group_subset_indices(idcs=idcs) + subset_models = [] + for grp_idx, sub_idcs in group_indices.items(): + subset_model = self.models[grp_idx] + if sub_idcs is not None and subset_model.num_outputs != len(sub_idcs): + subset_model = subset_model.subset_output(idcs=sub_idcs) + subset_models.append(subset_model) + if len(subset_models) == 1: + return subset_models[0] + return self.__class__(*subset_models)
+ + +
+[docs] + def transform_inputs(self, X: Tensor) -> list[Tensor]: + r"""Individually transform the inputs for each model. + + Args: + X: A tensor of inputs. + + Returns: + A list of tensors of transformed inputs. + """ + transformed_X_list = [] + for model in self.models: + try: + transformed_X_list.append(model.input_transform(X)) + except AttributeError: + transformed_X_list.append(X) + return transformed_X_list
+ + +
+[docs] + def load_state_dict( + self, state_dict: Mapping[str, Any], strict: bool = True + ) -> None: + """Initialize the fully Bayesian models before loading the state dict.""" + for i, m in enumerate(self.models): + if is_fully_bayesian(m): + filtered_dict = { + k.replace(f"models.{i}.", ""): v + for k, v in state_dict.items() + if k.startswith(f"models.{i}.") + } + m.load_state_dict(filtered_dict) + super().load_state_dict(state_dict=state_dict, strict=strict)
+ + +
+[docs] + def fantasize( + self, + X: Tensor, + sampler: MCSampler, + observation_noise: Tensor | None = None, + evaluation_mask: Tensor | None = None, + **kwargs: Any, + ) -> Model: + r"""Construct a fantasy model. + + Constructs a fantasy model in the following fashion: + (1) compute the model posterior at `X` (including observation noise if + `observation_noise=True`). + (2) sample from this posterior (using `sampler`) to generate "fake" + observations. + (3) condition the model on the new fake observations. + + Args: + X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of + the feature space, `n'` is the number of points per batch, and + `batch_shape` is the batch shape (must be compatible with the + batch shape of the model). + sampler: The sampler used for sampling from the posterior at `X`. If + evaluation_mask is not None, this must be a `ListSampler`. + observation_noise: A `model_batch_shape x 1 x m`-dim tensor or + a `model_batch_shape x n' x m`-dim tensor containing the average + noise for each batch and output, where `m` is the number of outputs. + `noise` must be in the outcome-transformed space if an outcome + transform is used. If None, then the noise will be the inferred + noise level. + evaluation_mask: A `n' x m`-dim tensor of booleans indicating which + outputs should be fantasized for a given design. This uses the same + evaluation mask for all batches. + + Returns: + The constructed fantasy model. + """ + if evaluation_mask is not None: + if evaluation_mask.ndim != 2 or evaluation_mask.shape != torch.Size( + [X.shape[-2], self.num_outputs] + ): + raise BotorchTensorDimensionError( + f"Expected evaluation_mask of shape `{X.shape[0]} " + f"x {self.num_outputs}`, but got " + f"{shape_to_str(evaluation_mask.shape)}." + ) + if not isinstance(sampler, ListSampler): + raise ValueError("Decoupled fantasization requires a list of samplers.") + + fant_models = [] + X_i = X + if observation_noise is None: + observation_noise_i = observation_noise + for i in range(self.num_outputs): + # get the inputs to fantasize at for output i + if evaluation_mask is not None: + mask_i = evaluation_mask[:, i] + X_i = X[..., mask_i, :] + # TODO (T158701749): implement a QMC DecoupledSampler that draws all + # samples from a single Sobol sequence or consider requiring that the + # sampling is IID to ensure good coverage. + sampler_i = sampler.samplers[i] + if observation_noise is not None: + observation_noise_i = observation_noise[..., mask_i, i : i + 1] + else: + sampler_i = ( + sampler.samplers[i] if isinstance(sampler, ListSampler) else sampler + ) + + fant_model = self.models[i].fantasize( + X=X_i, + sampler=sampler_i, + observation_noise=observation_noise_i, + **kwargs, + ) + fant_models.append(fant_model) + return self.__class__(*fant_models)
+
+ + + +
+[docs] +class ModelDict(ModuleDict): + r"""A lightweight container mapping model names to models.""" + + def __init__(self, **models: Model) -> None: + r"""Initialize a `ModelDict`. + + Args: + models: An arbitrary number of models. Each model can be any type + of BoTorch `Model`, including multi-output models and `ModelList`. + """ + if any(not isinstance(m, Model) for m in models.values()): + raise InputDataError( + f"Expected all models to be a BoTorch `Model`. Got {models}." + ) + super().__init__(modules=models)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/model_list_gp_regression.html b/website-old/pages/api/_modules/botorch/models/model_list_gp_regression.html new file mode 100644 index 0000000000..d93899eca7 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/model_list_gp_regression.html @@ -0,0 +1,203 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.model_list_gp_regression

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Model List GP Regression models.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+import torch
+
+from botorch.exceptions.errors import BotorchTensorDimensionError
+from botorch.models.gpytorch import GPyTorchModel, ModelListGPyTorchModel
+from botorch.models.model import FantasizeMixin
+from gpytorch.models import IndependentModelList
+from torch import Tensor
+
+
+
+[docs] +class ModelListGP(IndependentModelList, ModelListGPyTorchModel, FantasizeMixin): + r"""A multi-output GP model with independent GPs for the outputs. + + This model supports different-shaped training inputs for each of its + sub-models. It can be used with any number of single-output + `GPyTorchModel`\s and the models can be of different types. Use this model + when you have independent outputs with different training data. When + modeling correlations between outputs, use `MultiTaskGP`. + + Internally, this model is just a list of individual models, but it implements + the same input/output interface as all other BoTorch models. This makes it + very flexible and convenient to work with. The sequential evaluation comes + at a performance cost though - if you are using a block design (i.e. the + same number of training example for each output, and a similar model + structure, you should consider using a batched GP model instead, such as + `SingleTaskGP` with batched inputs). + """ + + def __init__(self, *gp_models: GPyTorchModel) -> None: + r""" + Args: + *gp_models: A number of single-output `GPyTorchModel`\s. + If models have input/output transforms, these are honored + individually for each model. + + Example: + >>> model1 = SingleTaskGP(train_X1, train_Y1) + >>> model2 = SingleTaskGP(train_X2, train_Y2) + >>> model = ModelListGP(model1, model2) + """ + super().__init__(*gp_models) + + # pyre-fixme[14]: Inconsistent override. Here `X` is a List[Tensor], but in the + # parent method it's a Tensor. +
+[docs] + def condition_on_observations( + self, X: list[Tensor], Y: Tensor, **kwargs: Any + ) -> ModelListGP: + r"""Condition the model on new observations. + + Args: + X: A `m`-list of `batch_shape x n' x d`-dim Tensors, where `d` is the + dimension of the feature space, `n'` is the number of points + per batch, and `batch_shape` is the batch shape (must be compatible + with the batch shape of the model). + Y: A `batch_shape' x n' x m`-dim Tensor, where `m` is the number of + model outputs, `n'` is the number of points per batch, and + `batch_shape'` is the batch shape of the observations. + `batch_shape'` must be broadcastable to `batch_shape` using + standard broadcasting semantics. If `Y` has fewer batch dimensions + than `X`, its is assumed that the missing batch dimensions are + the same for all `Y`. + kwargs: Keyword arguments passed to + `IndependentModelList.get_fantasy_model`. + + Returns: + A `ModelListGP` representing the original model + conditioned on the new observations `(X, Y)` (and possibly noise + observations passed in via kwargs). Here the `i`-th model has + `n_i + n'` training examples, where the `n'` training examples have + been added and all test-time caches have been updated. + """ + if Y.shape[-1] != self.num_outputs: + raise BotorchTensorDimensionError( + "Incorrect number of outputs for observations. Received " + f"{Y.shape[-1]} observation outputs, but model has " + f"{self.num_outputs} outputs." + ) + if len(X) != self.num_outputs: + raise BotorchTensorDimensionError( + "Incorrect number of inputs for observations. Received " + f"{len(X)} observation inputs, but model has " + f"{self.num_outputs} outputs." + ) + if "noise" in kwargs: + noise = kwargs.pop("noise") + if noise.shape != Y.shape[-noise.dim() :]: + raise BotorchTensorDimensionError( + "The shape of observation noise does not agree with the outcomes. " + f"Received {noise.shape} noise with {Y.shape} outcomes." + ) + + else: + noise = None + targets = [] + inputs = [] + noises = [] + i = 0 + for model in self.models: + j = i + model.num_outputs + y_i = torch.cat([Y[..., k] for k in range(i, j)], dim=-1) + X_i = torch.cat([X[k] for k in range(i, j)], dim=-2) + if noise is None: + noise_i = None + else: + noise_i = torch.cat([noise[..., k] for k in range(i, j)], dim=-1) + if hasattr(model, "outcome_transform"): + y_i, noise_i = model.outcome_transform(y_i, noise_i) + if noise_i is not None: + noise_i = noise_i.squeeze(0) + targets.append(y_i) + inputs.append(X_i) + noises.append(noise_i) + i += model.num_outputs + + kwargs_ = {**kwargs, "noise": noises} if noise is not None else kwargs + return super().get_fantasy_model(inputs, targets, **kwargs_)
+ + + def _set_transformed_inputs(self) -> None: + r"""Update training inputs with transformed inputs.""" + for m in self.models: + m._set_transformed_inputs() + + def _revert_to_original_inputs(self) -> None: + r"""Revert training inputs back to original.""" + for m in self.models: + m._revert_to_original_inputs()
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/multitask.html b/website-old/pages/api/_modules/botorch/models/multitask.html new file mode 100644 index 0000000000..1b2df54735 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/multitask.html @@ -0,0 +1,872 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.multitask

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Multi-Task GP models.
+
+References
+
+.. [Bonilla2007MTGP]
+    E. Bonilla, K. Chai and C. Williams. Multi-task Gaussian Process Prediction.
+    Advances in Neural Information Processing Systems 20, NeurIPS 2007.
+
+.. [Swersky2013MTBO]
+    K. Swersky, J. Snoek and R. Adams. Multi-Task Bayesian Optimization.
+    Advances in Neural Information Processing Systems 26, NeurIPS 2013.
+
+.. [Doucet2010sampl]
+    A. Doucet. A Note on Efficient Conditional Simulation of Gaussian Distributions.
+    http://www.stats.ox.ac.uk/~doucet/doucet_simulationconditionalgaussian.pdf,
+    Apr 2010.
+
+.. [Maddox2021bohdo]
+    W. Maddox, M. Balandat, A. Wilson, and E. Bakshy. Bayesian Optimization with
+    High-Dimensional Outputs. https://arxiv.org/abs/2106.12997, Jun 2021.
+"""
+
+from __future__ import annotations
+
+import math
+from typing import Any
+
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.gpytorch import GPyTorchModel, MultiTaskGPyTorchModel
+from botorch.models.model import FantasizeMixin
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform, Standardize
+from botorch.models.utils.gpytorch_modules import (
+    get_covar_module_with_dim_scaled_prior,
+    get_gaussian_likelihood_with_lognormal_prior,
+    MIN_INFERRED_NOISE_LEVEL,
+)
+from botorch.posteriors.multitask import MultitaskGPPosterior
+from botorch.utils.datasets import MultiTaskDataset, SupervisedDataset
+from botorch.utils.types import _DefaultType, DEFAULT
+from gpytorch.constraints import GreaterThan
+from gpytorch.distributions.multitask_multivariate_normal import (
+    MultitaskMultivariateNormal,
+)
+from gpytorch.distributions.multivariate_normal import MultivariateNormal
+from gpytorch.kernels.index_kernel import IndexKernel
+from gpytorch.kernels.multitask_kernel import MultitaskKernel
+from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood
+from gpytorch.likelihoods.likelihood import Likelihood
+from gpytorch.likelihoods.multitask_gaussian_likelihood import (
+    MultitaskGaussianLikelihood,
+)
+from gpytorch.means import MultitaskMean
+from gpytorch.means.constant_mean import ConstantMean
+from gpytorch.models.exact_gp import ExactGP
+from gpytorch.module import Module
+from gpytorch.priors.lkj_prior import LKJCovariancePrior
+from gpytorch.priors.prior import Prior
+from gpytorch.priors.smoothed_box_prior import SmoothedBoxPrior
+from gpytorch.priors.torch_priors import GammaPrior, LogNormalPrior
+from gpytorch.settings import detach_test_caches
+from gpytorch.utils.errors import CachingError
+from gpytorch.utils.memoize import cached, pop_from_cache
+from linear_operator.operators import (
+    BatchRepeatLinearOperator,
+    CatLinearOperator,
+    DiagLinearOperator,
+    KroneckerProductDiagLinearOperator,
+    KroneckerProductLinearOperator,
+    RootLinearOperator,
+    to_linear_operator,
+)
+from torch import Tensor
+
+
+
+[docs] +def get_task_value_remapping(task_values: Tensor, dtype: torch.dtype) -> Tensor | None: + """Construct an mapping of discrete task values to contiguous int-valued floats. + + Args: + task_values: A sorted long-valued tensor of task values. + dtype: The dtype of the model inputs (e.g. `X`), which the new + task values should have mapped to (e.g. float, double). + + Returns: + A tensor of shape `task_values.max() + 1` that maps task values + to new task values. The indexing operation `mapper[task_value]` + will produce a tensor of new task values, of the same shape as + the original. The elements of the `mapper` tensor that do not + appear in the original `task_values` are mapped to `nan`. The + return value will be `None`, when the task values are contiguous + integers starting from zero. + """ + task_range = torch.arange( + len(task_values), dtype=task_values.dtype, device=task_values.device + ) + mapper = None + if not torch.equal(task_values, task_range): + # Create a tensor that maps task values to new task values. + # The number of tasks should be small, so this should be quite efficient. + mapper = torch.full( + (int(task_values.max().item()) + 1,), + float("nan"), + dtype=dtype, + device=task_values.device, + ) + mapper[task_values] = task_range.to(dtype=dtype) + return mapper
+ + + +
+[docs] +class MultiTaskGP(ExactGP, MultiTaskGPyTorchModel, FantasizeMixin): + r"""Multi-Task exact GP model using an ICM (intrinsic co-regionalization model) + kernel. See [Bonilla2007MTGP]_ and [Swersky2013MTBO]_ for a reference on the + model and its use in Bayesian optimization. + + The model can be single-output or multi-output, determined by the `output_tasks`. + This model uses relatively strong priors on the base Kernel hyperparameters, which + work best when covariates are normalized to the unit cube and outcomes are + standardized (zero mean, unit variance) - this standardization should be applied in + a stratified fashion at the level of the tasks, rather than across all data points. + + If the `train_Yvar` is None, this model infers the noise level. If you have + known observation noise, you can set `train_Yvar` to a tensor containing + the noise variance measurements. WARNING: This currently does not support + different noise levels for the different tasks. + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + task_feature: int, + train_Yvar: Tensor | None = None, + mean_module: Module | None = None, + covar_module: Module | None = None, + likelihood: Likelihood | None = None, + task_covar_prior: Prior | None = None, + output_tasks: list[int] | None = None, + rank: int | None = None, + all_tasks: list[int] | None = None, + outcome_transform: OutcomeTransform | _DefaultType | None = DEFAULT, + input_transform: InputTransform | None = None, + ) -> None: + r"""Multi-Task GP model using an ICM kernel. + + Args: + train_X: A `n x (d + 1)` or `b x n x (d + 1)` (batch mode) tensor + of training data. One of the columns should contain the task + features (see `task_feature` argument). + train_Y: A `n x 1` or `b x n x 1` (batch mode) tensor of training + observations. + task_feature: The index of the task feature (`-d <= task_feature <= d`). + train_Yvar: An optional `n` or `b x n` (batch mode) tensor of observed + measurement noise. If None, we infer the noise. + Note that the inferred noise is common across all tasks. + mean_module: The mean function to be used. Defaults to `ConstantMean`. + covar_module: The module for computing the covariance matrix between + the non-task features. Defaults to `RBFKernel`. + likelihood: A likelihood. The default is selected based on `train_Yvar`. + If `train_Yvar` is None, a standard `GaussianLikelihood` with inferred + noise level is used. Otherwise, a FixedNoiseGaussianLikelihood is used. + output_tasks: A list of task indices for which to compute model + outputs for. If omitted, return outputs for all task indices. + rank: The rank to be used for the index kernel. If omitted, use a + full rank (i.e. number of tasks) kernel. + task_covar_prior : A Prior on the task covariance matrix. Must operate + on p.s.d. matrices. A common prior for this is the `LKJ` prior. + all_tasks: By default, multi-task GPs infer the list of all tasks from + the task features in `train_X`. This is an experimental feature that + enables creation of multi-task GPs with tasks that don't appear in the + training data. Note that when a task is not observed, the corresponding + task covariance will heavily depend on random initialization and may + behave unexpectedly. + outcome_transform: An outcome transform that is applied to the + training data during instantiation and to the posterior during + inference (that is, the `Posterior` obtained by calling + `.posterior` on the model will be on the original scale). We use a + `Standardize` transform if no `outcome_transform` is specified. + Pass down `None` to use no outcome transform. NOTE: Standardization + should be applied in a stratified fashion, separately for each task. + input_transform: An input transform that is applied in the model's + forward pass. + + Example: + >>> X1, X2 = torch.rand(10, 2), torch.rand(20, 2) + >>> i1, i2 = torch.zeros(10, 1), torch.ones(20, 1) + >>> train_X = torch.cat([ + >>> torch.cat([X1, i1], -1), torch.cat([X2, i2], -1), + >>> ]) + >>> train_Y = torch.cat([f1(X1), f2(X2)]).unsqueeze(-1) + >>> model = MultiTaskGP(train_X, train_Y, task_feature=-1) + """ + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + self._validate_tensor_args(X=transformed_X, Y=train_Y, Yvar=train_Yvar) + ( + all_tasks_inferred, + task_feature, + self.num_non_task_features, + ) = self.get_all_tasks(transformed_X, task_feature, output_tasks) + if all_tasks is not None and not set(all_tasks_inferred).issubset(all_tasks): + raise UnsupportedError( + f"The provided {all_tasks=} does not contain all the task features " + f"inferred from the training data {all_tasks_inferred=}. " + "This is not allowed as it will lead to errors during model training." + ) + all_tasks = all_tasks or all_tasks_inferred + self.num_tasks = len(all_tasks) + if outcome_transform == DEFAULT: + outcome_transform = Standardize(m=1, batch_shape=train_X.shape[:-2]) + if outcome_transform is not None: + train_Y, train_Yvar = outcome_transform(Y=train_Y, Yvar=train_Yvar) + + # squeeze output dim + train_Y = train_Y.squeeze(-1) + if output_tasks is None: + output_tasks = all_tasks + else: + if set(output_tasks) - set(all_tasks): + raise RuntimeError("All output tasks must be present in input data.") + self._output_tasks = output_tasks + self._num_outputs = len(output_tasks) + + # TODO (T41270962): Support task-specific noise levels in likelihood + if likelihood is None: + if train_Yvar is None: + likelihood = get_gaussian_likelihood_with_lognormal_prior() + else: + likelihood = FixedNoiseGaussianLikelihood(noise=train_Yvar.squeeze(-1)) + + # construct indexer to be used in forward + self._task_feature = task_feature + self._base_idxr = torch.arange(self.num_non_task_features) + self._base_idxr[task_feature:] += 1 # exclude task feature + + super().__init__( + train_inputs=train_X, train_targets=train_Y, likelihood=likelihood + ) + self.mean_module = mean_module or ConstantMean() + if covar_module is None: + self.covar_module = get_covar_module_with_dim_scaled_prior( + ard_num_dims=self.num_non_task_features + ) + else: + self.covar_module = covar_module + + self._rank = rank if rank is not None else self.num_tasks + self.task_covar_module = IndexKernel( + num_tasks=self.num_tasks, rank=self._rank, prior=task_covar_prior + ) + task_mapper = get_task_value_remapping( + task_values=torch.tensor( + all_tasks, dtype=torch.long, device=train_X.device + ), + dtype=train_X.dtype, + ) + self.register_buffer("_task_mapper", task_mapper) + self._expected_task_values = set(all_tasks) + if input_transform is not None: + self.input_transform = input_transform + if outcome_transform is not None: + self.outcome_transform = outcome_transform + self.to(train_X) + + def _split_inputs(self, x: Tensor) -> tuple[Tensor, Tensor]: + r"""Extracts base features and task indices from input data. + + Args: + x: The full input tensor with trailing dimension of size `d + 1`. + Should be of float/double data type. + + Returns: + 2-element tuple containing + + - A `q x d` or `b x q x d` (batch mode) tensor with trailing + dimension made up of the `d` non-task-index columns of `x`, arranged + in the order as specified by the indexer generated during model + instantiation. + - A `q` or `b x q` (batch mode) tensor of long data type containing + the task indices. + """ + batch_shape, d = x.shape[:-2], x.shape[-1] + x_basic = x[..., self._base_idxr].view(batch_shape + torch.Size([-1, d - 1])) + task_idcs = ( + x[..., self._task_feature] + .view(batch_shape + torch.Size([-1, 1])) + .to(dtype=torch.long) + ) + task_idcs = self._map_tasks(task_values=task_idcs) + return x_basic, task_idcs + +
+[docs] + def forward(self, x: Tensor) -> MultivariateNormal: + if self.training: + x = self.transform_inputs(x) + x_basic, task_idcs = self._split_inputs(x) + # Compute base mean and covariance + mean_x = self.mean_module(x_basic) + covar_x = self.covar_module(x_basic) + # Compute task covariances + covar_i = self.task_covar_module(task_idcs) + # Combine the two in an ICM fashion + covar = covar_x.mul(covar_i) + return MultivariateNormal(mean_x, covar)
+ + +
+[docs] + @classmethod + def get_all_tasks( + cls, + train_X: Tensor, + task_feature: int, + output_tasks: list[int] | None = None, + ) -> tuple[list[int], int, int]: + if train_X.ndim != 2: + # Currently, batch mode MTGPs are blocked upstream in GPyTorch + raise ValueError(f"Unsupported shape {train_X.shape} for train_X.") + + d = train_X.shape[-1] - 1 + if not (-d <= task_feature <= d): + raise ValueError(f"Must have that -{d} <= task_feature <= {d}") + task_feature = task_feature % (d + 1) + all_tasks = ( + train_X[..., task_feature].unique(sorted=True).to(dtype=torch.long).tolist() + ) + return all_tasks, task_feature, d
+ + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset | MultiTaskDataset, + task_feature: int, + output_tasks: list[int] | None = None, + task_covar_prior: Prior | None = None, + prior_config: dict | None = None, + rank: int | None = None, + ) -> dict[str, Any]: + r"""Construct `Model` keyword arguments from a dataset and other args. + + Args: + training_data: A `SupervisedDataset` or a `MultiTaskDataset`. + task_feature: Column index of embedded task indicator features. + output_tasks: A list of task indices for which to compute model + outputs for. If omitted, return outputs for all task indices. + task_covar_prior: A GPyTorch `Prior` object to use as prior on + the cross-task covariance matrix, + prior_config: Configuration for inter-task covariance prior. + Should only be used if `task_covar_prior` is not passed directly. Must + contain `use_LKJ_prior` indicator and should contain float value `eta`. + rank: The rank of the cross-task covariance matrix. + """ + if task_covar_prior is not None and prior_config is not None: + raise ValueError( + "Only one of `task_covar_prior` and `prior_config` arguments expected." + ) + + if prior_config is not None: + if not prior_config.get("use_LKJ_prior"): + raise ValueError("Currently only config for LKJ prior is supported.") + + num_tasks = training_data.X[task_feature].unique().numel() + sd_prior = GammaPrior(1.0, 0.15) + sd_prior._event_shape = torch.Size([num_tasks]) + eta = prior_config.get("eta", 0.5) + if not isinstance(eta, float) and not isinstance(eta, int): + raise ValueError(f"eta must be a real number, your eta was {eta}.") + task_covar_prior = LKJCovariancePrior(num_tasks, eta, sd_prior) + + # Call Model.construct_inputs to parse training data + base_inputs = super().construct_inputs(training_data=training_data) + if ( + isinstance(training_data, MultiTaskDataset) + # If task features are included in the data, all tasks will have + # some observations and they may have different task features. + and training_data.task_feature_index is None + ): + all_tasks = list(range(len(training_data.datasets))) + base_inputs["all_tasks"] = all_tasks + if task_covar_prior is not None: + base_inputs["task_covar_prior"] = task_covar_prior + if rank is not None: + base_inputs["rank"] = rank + base_inputs["task_feature"] = task_feature + base_inputs["output_tasks"] = output_tasks + return base_inputs
+
+ + + +
+[docs] +class KroneckerMultiTaskGP(ExactGP, GPyTorchModel, FantasizeMixin): + """Multi-task GP with Kronecker structure, using an ICM kernel. + + This model assumes the "block design" case, i.e., it requires that all tasks + are observed at all data points. + + For posterior sampling, this model uses Matheron's rule [Doucet2010sampl] to compute + the posterior over all tasks as in [Maddox2021bohdo] by exploiting Kronecker + structure. + + When a multi-fidelity model has Kronecker structure, this means there is one + covariance kernel over the fidelity features (call it `K_f`) and another over + the rest of the input parameters (call it `K_i`), and the resulting covariance + across inputs and fidelities is given by the Kronecker product of the two + covariance matrices. This is equivalent to saying the covariance between + two input and feature pairs is given by + + K((parameter_1, fidelity_1), (parameter_2, fidelity_2)) + = K_f(fidelity_1, fidelity_2) * K_i(parameter_1, parameter_2). + + Then the covariance matrix of `n_i` parameters and `n_f` fidelities can be + codified as a Kronecker product of an `n_i x n_i` matrix and an + `n_f x n_f` matrix, which is far more parsimonious than specifying the + whole `(n_i * n_f) x (n_i * n_f)` covariance matrix. + + Example: + >>> train_X = torch.rand(10, 2) + >>> train_Y = torch.cat([f_1(X), f_2(X)], dim=-1) + >>> model = KroneckerMultiTaskGP(train_X, train_Y) + """ + + def __init__( + self, + train_X: Tensor, + train_Y: Tensor, + likelihood: MultitaskGaussianLikelihood | None = None, + data_covar_module: Module | None = None, + task_covar_prior: Prior | None = None, + rank: int | None = None, + input_transform: InputTransform | None = None, + outcome_transform: OutcomeTransform | None = None, + **kwargs: Any, + ) -> None: + r""" + Args: + train_X: A `batch_shape x n x d` tensor of training features. + train_Y: A `batch_shape x n x m` tensor of training observations. + likelihood: A `MultitaskGaussianLikelihood`. If omitted, uses a + `MultitaskGaussianLikelihood` with a `GammaPrior(1.1, 0.05)` + noise prior. + data_covar_module: The module computing the covariance (Kernel) matrix + in data space. If omitted, uses an `RBFKernel`. + task_covar_prior : A Prior on the task covariance matrix. Must operate + on p.s.d. matrices. A common prior for this is the `LKJ` prior. If + omitted, uses `LKJCovariancePrior` with `eta` parameter as specified + in the keyword arguments (if not specified, use `eta=1.5`). + rank: The rank of the ICM kernel. If omitted, use a full rank kernel. + kwargs: Additional arguments to override default settings of priors, + including: + - eta: The eta parameter on the default LKJ task_covar_prior. + A value of 1.0 is uninformative, values <1.0 favor stronger + correlations (in magnitude), correlations vanish as eta -> inf. + - sd_prior: A scalar prior over nonnegative numbers, which is used + for the default LKJCovariancePrior task_covar_prior. + - likelihood_rank: The rank of the task covariance matrix to fit. + Defaults to 0 (which corresponds to a diagonal covariance matrix). + """ + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + if outcome_transform is not None: + train_Y, _ = outcome_transform(train_Y) + + self._validate_tensor_args(X=transformed_X, Y=train_Y) + self._num_outputs = train_Y.shape[-1] + batch_shape, ard_num_dims = train_X.shape[:-2], train_X.shape[-1] + num_tasks = train_Y.shape[-1] + + if rank is None: + rank = num_tasks + if likelihood is None: + noise_prior = LogNormalPrior(loc=-4.0, scale=1.0) + likelihood = MultitaskGaussianLikelihood( + num_tasks=num_tasks, + batch_shape=batch_shape, + noise_prior=noise_prior, + noise_constraint=GreaterThan( + MIN_INFERRED_NOISE_LEVEL, + transform=None, + initial_value=noise_prior.mode, + ), + rank=kwargs.get("likelihood_rank", 0), + ) + if task_covar_prior is None: + task_covar_prior = LKJCovariancePrior( + n=num_tasks, + eta=torch.tensor(kwargs.get("eta", 1.5)).to(train_X), + sd_prior=kwargs.get( + "sd_prior", + SmoothedBoxPrior(math.exp(-6), math.exp(1.25), 0.05), + ), + ) + super().__init__(train_X, train_Y, likelihood) + self.mean_module = MultitaskMean( + base_means=ConstantMean(batch_shape=batch_shape), num_tasks=num_tasks + ) + if data_covar_module is None: + data_covar_module = get_covar_module_with_dim_scaled_prior( + ard_num_dims=ard_num_dims, + batch_shape=batch_shape, + ) + else: + data_covar_module = data_covar_module + + self.covar_module = MultitaskKernel( + data_covar_module=data_covar_module, + num_tasks=num_tasks, + rank=rank, + batch_shape=batch_shape, + task_covar_prior=task_covar_prior, + ) + + if outcome_transform is not None: + self.outcome_transform = outcome_transform + if input_transform is not None: + self.input_transform = input_transform + self.to(train_X) + +
+[docs] + def forward(self, X: Tensor) -> MultitaskMultivariateNormal: + if self.training: + X = self.transform_inputs(X) + + mean_x = self.mean_module(X) + covar_x = self.covar_module(X) + return MultitaskMultivariateNormal(mean_x, covar_x)
+ + + @property + def _task_covar_matrix(self): + res = self.covar_module.task_covar_module.covar_matrix + if detach_test_caches.on(): + res = res.detach() + return res + + @property + @cached(name="train_full_covar") + def train_full_covar(self): + train_x = self.transform_inputs(self.train_inputs[0]) + + # construct Kxx \otimes Ktt + train_full_covar = self.covar_module(train_x).evaluate_kernel() + if detach_test_caches.on(): + train_full_covar = train_full_covar.detach() + return train_full_covar + + @property + @cached(name="predictive_mean_cache") + def predictive_mean_cache(self): + train_x = self.transform_inputs(self.train_inputs[0]) + train_noise = self.likelihood._shaped_noise_covar(train_x.shape) + if detach_test_caches.on(): + train_noise = train_noise.detach() + + train_diff = self.train_targets - self.mean_module(train_x) + train_solve = (self.train_full_covar + train_noise).solve( + train_diff.reshape(*train_diff.shape[:-2], -1) + ) + if detach_test_caches.on(): + train_solve = train_solve.detach() + + return train_solve + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool | Tensor = False, + posterior_transform: PosteriorTransform | None = None, + ) -> MultitaskGPPosterior: + self.eval() + + if posterior_transform is not None: + # this could be very costly, disallow for now + raise NotImplementedError( + "Posterior transforms currently not supported for " + f"{self.__class__.__name__}" + ) + + X = self.transform_inputs(X) + train_x = self.transform_inputs(self.train_inputs[0]) + + # construct Ktt + task_covar = self._task_covar_matrix + task_rootlt = self._task_covar_matrix.root_decomposition( + method="diagonalization" + ) + task_root = task_rootlt.root + if task_covar.batch_shape != X.shape[:-2]: + task_covar = BatchRepeatLinearOperator( + task_covar, batch_repeat=X.shape[:-2] + ) + task_root = BatchRepeatLinearOperator( + to_linear_operator(task_root), batch_repeat=X.shape[:-2] + ) + + task_covar_rootlt = RootLinearOperator(task_root) + + # construct RR' \approx Kxx + data_data_covar = self.train_full_covar.linear_ops[0] + # populate the diagonalziation caches for the root and inverse root + # decomposition + data_data_evals, data_data_evecs = data_data_covar.diagonalization() + + # pad the eigenvalue and eigenvectors with zeros if we are using lanczos + if data_data_evecs.shape[-1] < data_data_evecs.shape[-2]: + cols_to_add = data_data_evecs.shape[-2] - data_data_evecs.shape[-1] + zero_evecs = torch.zeros( + *data_data_evecs.shape[:-1], + cols_to_add, + dtype=data_data_evals.dtype, + device=data_data_evals.device, + ) + zero_evals = torch.zeros( + *data_data_evecs.shape[:-2], + cols_to_add, + dtype=data_data_evals.dtype, + device=data_data_evals.device, + ) + data_data_evecs = CatLinearOperator( + data_data_evecs, + to_linear_operator(zero_evecs), + dim=-1, + output_device=data_data_evals.device, + ) + data_data_evals = torch.cat((data_data_evals, zero_evals), dim=-1) + + # construct K_{xt, x} + test_data_covar = self.covar_module.data_covar_module(X, train_x) + # construct K_{xt, xt} + test_test_covar = self.covar_module.data_covar_module(X) + + # now update root so that \tilde{R}\tilde{R}' \approx K_{(x,xt), (x,xt)} + # cloning preserves the gradient history + updated_linear_op = data_data_covar.cat_rows( + cross_mat=test_data_covar.clone(), + new_mat=test_test_covar, + method="diagonalization", + ) + updated_root = updated_linear_op.root_decomposition().root + # occasionally, there's device errors so enforce this comes out right + updated_root = updated_root.to(data_data_covar.device) + + # build a root decomposition of the joint train/test covariance matrix + # construct (\tilde{R} \otimes M)(\tilde{R} \otimes M)' \approx + # (K_{(x,xt), (x,xt)} \otimes Ktt) + joint_covar = RootLinearOperator( + KroneckerProductLinearOperator( + updated_root, task_covar_rootlt.root.detach() + ) + ) + + # construct K_{xt, x} \otimes Ktt + test_obs_kernel = KroneckerProductLinearOperator(test_data_covar, task_covar) + + # collect y - \mu(x) and \mu(X) + train_diff = self.train_targets - self.mean_module(train_x) + if detach_test_caches.on(): + train_diff = train_diff.detach() + test_mean = self.mean_module(X) + + train_noise = self.likelihood._shaped_noise_covar(train_x.shape) + diagonal_noise = isinstance(train_noise, DiagLinearOperator) + if detach_test_caches.on(): + train_noise = train_noise.detach() + test_noise = ( + self.likelihood._shaped_noise_covar(X.shape) if observation_noise else None + ) + + # predictive mean and variance for the mvn + # first the predictive mean + pred_mean = ( + test_obs_kernel.matmul(self.predictive_mean_cache).reshape_as(test_mean) + + test_mean + ) + # next the predictive variance, assume diagonal noise + test_var_term = KroneckerProductLinearOperator( + test_test_covar, task_covar + ).diagonal() + + if diagonal_noise: + task_evals, task_evecs = self._task_covar_matrix.diagonalization() + # TODO: make this be the default KPMatmulLT diagonal method in gpytorch + full_data_inv_evals = ( + KroneckerProductDiagLinearOperator( + DiagLinearOperator(data_data_evals), DiagLinearOperator(task_evals) + ) + + train_noise + ).inverse() + test_train_hadamard = KroneckerProductLinearOperator( + test_data_covar.matmul(data_data_evecs).to_dense() ** 2, + task_covar.matmul(task_evecs).to_dense() ** 2, + ) + data_var_term = test_train_hadamard.matmul(full_data_inv_evals).sum(dim=-1) + else: + # if non-diagonal noise (but still kronecker structured), we have to pull + # across the noise because the inverse is not closed form + # should be a kronecker lt, R = \Sigma_X^{-1/2} \kron \Sigma_T^{-1/2} + # TODO: enforce the diagonalization to return a KPLT for all shapes in + # gpytorch or dense linear algebra for small shapes + data_noise, task_noise = train_noise.linear_ops + data_noise_root = data_noise.root_inv_decomposition( + method="diagonalization" + ) + task_noise_root = task_noise.root_inv_decomposition( + method="diagonalization" + ) + + # ultimately we need to compute the diagonal of + # (K_{x* X} \kron K_T)(K_{XX} \kron K_T + \Sigma_X \kron \Sigma_T)^{-1} + # (K_{x* X} \kron K_T)^T + # = (K_{x* X} \Sigma_X^{-1/2} Q_R)(\Lambda_R + I)^{-1} + # (K_{x* X} \Sigma_X^{-1/2} Q_R)^T + # where R = (\Sigma_X^{-1/2T}K_{XX}\Sigma_X^{-1/2} \kron + # \Sigma_T^{-1/2T}K_{T}\Sigma_T^{-1/2}) + # first we construct the components of R's eigen-decomposition + # TODO: make this be the default KPMatmulLT diagonal method in gpytorch + whitened_data_covar = ( + data_noise_root.transpose(-1, -2) + .matmul(data_data_covar) + .matmul(data_noise_root) + ) + w_data_evals, w_data_evecs = whitened_data_covar.diagonalization() + whitened_task_covar = ( + task_noise_root.transpose(-1, -2) + .matmul(self._task_covar_matrix) + .matmul(task_noise_root) + ) + w_task_evals, w_task_evecs = whitened_task_covar.diagonalization() + + # we add one to the eigenvalues as above (not just for stability) + full_data_inv_evals = ( + KroneckerProductDiagLinearOperator( + DiagLinearOperator(w_data_evals), DiagLinearOperator(w_task_evals) + ) + .add_jitter(1.0) + .inverse() + ) + + test_data_comp = ( + test_data_covar.matmul(data_noise_root).matmul(w_data_evecs).to_dense() + ** 2 + ) + task_comp = ( + task_covar.matmul(task_noise_root).matmul(w_task_evecs).to_dense() ** 2 + ) + + test_train_hadamard = KroneckerProductLinearOperator( + test_data_comp, task_comp + ) + data_var_term = test_train_hadamard.matmul(full_data_inv_evals).sum(dim=-1) + + pred_variance = test_var_term - data_var_term + specialized_mvn = MultitaskMultivariateNormal( + pred_mean, DiagLinearOperator(pred_variance) + ) + if observation_noise: + specialized_mvn = self.likelihood(specialized_mvn) + + posterior = MultitaskGPPosterior( + distribution=specialized_mvn, + joint_covariance_matrix=joint_covar, + test_train_covar=test_obs_kernel, + train_diff=train_diff, + test_mean=test_mean, + train_train_covar=self.train_full_covar, + train_noise=train_noise, + test_noise=test_noise, + ) + + if hasattr(self, "outcome_transform"): + posterior = self.outcome_transform.untransform_posterior(posterior) + return posterior
+ + +
+[docs] + def train(self, val=True, *args, **kwargs): + if val: + fixed_cache_names = ["data_data_roots", "train_full_covar", "task_root"] + for name in fixed_cache_names: + try: + pop_from_cache(self, name) + except CachingError: + pass + + return super().train(val, *args, **kwargs)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/pairwise_gp.html b/website-old/pages/api/_modules/botorch/models/pairwise_gp.html new file mode 100644 index 0000000000..dd066b8595 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/pairwise_gp.html @@ -0,0 +1,1303 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.pairwise_gp

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Preference Learning with Gaussian Process
+
+.. [Chu2005preference]
+    Wei Chu, and Zoubin Ghahramani. Preference learning with Gaussian processes.
+    Proceedings of the 22nd international conference on Machine learning. 2005.
+
+.. [Brochu2010tutorial]
+    Eric Brochu, Vlad M. Cora, and Nando De Freitas.
+    A tutorial on Bayesian optimization of expensive cost functions,
+    with application to active user modeling and hierarchical reinforcement learning.
+    arXiv preprint arXiv:1012.2599 (2010).
+"""
+
+from __future__ import annotations
+
+import warnings
+from collections.abc import Iterable
+from copy import deepcopy
+from typing import Any
+
+import numpy as np
+import numpy.typing as npt
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions import UnsupportedError
+from botorch.exceptions.warnings import _get_single_precision_warning, InputDataWarning
+from botorch.models.likelihoods.pairwise import (
+    PairwiseLikelihood,
+    PairwiseProbitLikelihood,
+)
+from botorch.models.model import FantasizeMixin, Model
+from botorch.models.transforms.input import InputTransform
+from botorch.models.utils.assorted import consolidate_duplicates
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from botorch.posteriors.posterior import Posterior
+from botorch.utils.datasets import RankingDataset, SupervisedDataset
+from gpytorch import settings
+from gpytorch.constraints import GreaterThan, Interval
+from gpytorch.distributions.multivariate_normal import MultivariateNormal
+from gpytorch.kernels.rbf_kernel import RBFKernel
+from gpytorch.kernels.scale_kernel import ScaleKernel
+from gpytorch.means.constant_mean import ConstantMean
+from gpytorch.mlls import MarginalLogLikelihood
+from gpytorch.models.gp import GP
+from gpytorch.priors.smoothed_box_prior import SmoothedBoxPrior
+from gpytorch.priors.torch_priors import GammaPrior
+from linear_operator.operators import LinearOperator, RootLinearOperator
+from linear_operator.utils.cholesky import psd_safe_cholesky
+from linear_operator.utils.errors import NotPSDError
+from scipy import optimize
+from torch import float32, float64, Tensor
+from torch.nn.modules.module import _IncompatibleKeys
+
+
+# Helper functions
+def _check_strict_input(
+    inputs: Iterable[Tensor], t_inputs: list[Tensor], target_or_inputs: str
+):
+    for input_, t_input in zip(inputs, t_inputs or (None,)):
+        for attr in {"shape", "dtype", "device"}:
+            expected_attr = getattr(t_input, attr, None)
+            found_attr = getattr(input_, attr, None)
+            if expected_attr != found_attr:
+                raise RuntimeError(
+                    f"Cannot modify {attr} of {target_or_inputs} "
+                    f"(expected {expected_attr}, found {found_attr})."
+                )
+
+
+def _scaled_psd_safe_cholesky(
+    matrix: Tensor, scale: Tensor, jitter: float | None = None
+) -> Tensor:
+    r"""scale matrix by 1/outputscale before cholesky for better numerical stability"""
+    matrix = matrix / scale
+    chol = psd_safe_cholesky(matrix, jitter=jitter)
+    chol = chol * scale.sqrt()
+    return chol
+
+
+def _ensure_psd_with_jitter(
+    matrix: Tensor,
+    scale: float | Tensor = 1.0,
+    jitter: float = 1e-8,
+    max_tries: int = 3,
+) -> Tensor:
+    scaled_matrix = matrix / scale
+    new_jitter = 0
+    for i in range(max_tries):
+        scaled_matrix = scaled_matrix + new_jitter * torch.diag_embed(
+            torch.ones(
+                scaled_matrix.shape[:-1],
+                device=scaled_matrix.device,
+                dtype=scaled_matrix.dtype,
+            )
+        )
+        _, info = torch.linalg.cholesky_ex(scaled_matrix)
+        psd = (info == 0).all()
+        if psd:
+            break
+        else:
+            new_jitter = jitter * (10**i) - new_jitter
+    if not psd:
+        raise NotPSDError(
+            "Matrix not positive definite after repeatedly adding jitter "
+            f"up to {jitter * (10**i):.1e}."
+        )
+    return scaled_matrix * scale
+
+
+# Why we subclass GP even though it provides no functionality:
+# if this subclassing is removed, we get the following GPyTorch error:
+# "RuntimeError: All MarginalLogLikelihood objects must be given a GP object as
+# a model. If you are using a more complicated model involving a GP, pass the
+# underlying GP object as the model, not a full PyTorch module."
+
+[docs] +class PairwiseGP(Model, GP, FantasizeMixin): + r"""Probit GP for preference learning with Laplace approximation + + A probit-likelihood GP that learns via pairwise comparison data, using a + Laplace approximation of the posterior of the estimated utility values. By + default it uses a scaled RBF kernel. + + Implementation is based on [Chu2005preference]_. + Also see [Brochu2010tutorial]_ for additional reference. + + Note that in [Chu2005preference]_ the likelihood of a pairwise comparison + is :math:`\left(\frac{f(x_1) - f(x_2)}{\sqrt{2}\sigma}\right)`, i.e. a scale is + used in the denominator. To maintain consistency with usage of kernels + elsewhere in BoTorch, we instead do not include :math:`\sigma` in the code + (implicitly setting it to 1) and use ScaleKernel to scale the function. + + In the example below, the user/decision maker has stated that they prefer + the first item over the second item and the third item over the second item, + generating comparisons [0, 1] and [2, 1]. + Example: + >>> from botorch.models import PairwiseGP + >>> import torch + >>> datapoints = torch.Tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> comparisons = torch.Tensor([[0, 1], [2, 1]]) + >>> model = PairwiseGP(datapoints, comparisons) + """ + + _buffer_names = [ + "consolidated_datapoints", + "consolidated_comparisons", + "D", + "DT", + "utility", + "covar_chol", + "likelihood_hess", + "hlcov_eye", + "covar", + "covar_inv", + "unconsolidated_datapoints", + "unconsolidated_comparisons", + "consolidated_indices", + ] + + def __init__( + self, + datapoints: Tensor | None, + comparisons: Tensor | None, + likelihood: PairwiseLikelihood | None = None, + covar_module: ScaleKernel | None = None, + input_transform: InputTransform | None = None, + *, + jitter: float = 1e-6, + xtol: float | None = None, + consolidate_rtol: float = 0.0, + consolidate_atol: float = 1e-4, + maxfev: int | None = None, + ) -> None: + r""" + Args: + datapoints: Either `None` or a `batch_shape x n x d` tensor of + training features. If either `datapoints` or `comparisons` is + `None`, construct a prior-only model. + comparisons: Either `None` or a `batch_shape x m x 2` tensor of + training comparisons; comparisons[i] is a noisy indicator + suggesting the utility value of comparisons[i, 0]-th is greater + than comparisons[i, 1]-th. If either `comparisons` or + `datapoints` is `None`, construct a prior-only model. + likelihood: A PairwiseLikelihood. + covar_module: Covariance module. + input_transform: An input transform that is applied in the model's + forward pass. + jitter: Value added to diagonal for numerical stability in + `psd_safe_cholesky`. + xtol: Stopping creteria in scipy.optimize.fsolve used to find f_map + in `PairwiseGP._update`. If None, default behavior is handled by + `PairwiseGP._update`. + consolidate_rtol: `rtol` passed to `consolidate_duplicates`. + consolidate_atol: `atol` passed to `consolidate_duplicates`. + maxfev: The maximum number of calls to the function in + scipy.optimize.fsolve. If None, default behavior is handled by + `PairwiseGP._update`. + """ + super().__init__() + # Input data validation + if datapoints is not None and datapoints.dtype == torch.float32: + warnings.warn( + _get_single_precision_warning(str(datapoints.dtype)), + category=InputDataWarning, + stacklevel=2, + ) + + # Set optional parameters + self._jitter = jitter + self._xtol = xtol + self._consolidate_rtol = consolidate_rtol + self._consolidate_atol = consolidate_atol + self._maxfev = maxfev + + if input_transform is not None: + input_transform.to(datapoints) + # input transformation is applied in set_train_data + self.input_transform = input_transform + + # Compatibility variables with fit_gpytorch_*: Dummy likelihood + # Likelihood is tightly tied with this model and + # it doesn't make much sense to keep it separate + self.likelihood = ( + PairwiseProbitLikelihood() if likelihood is None else likelihood + ) + + for key in self._buffer_names: + self.register_buffer(key, None) + + self.train_inputs = [] + self.train_targets = None + self.utility = None + + self.pred_cov_fac_need_update = True + self.dim = None + self.unconsolidated_datapoints = None + self.unconsolidated_comparisons = None + self.consolidated_datapoints = None + self.consolidated_comparisons = None + self.consolidated_indices = None + + # See set_train_data for additional compatibility variables. + # Not that the datapoints here are not transformed even if input_transform + # is not None to avoid double transformation during model fitting. + # self.transform_inputs is called in `forward` + self.set_train_data(datapoints, comparisons, update_model=False) + + # Set hyperparameters + # Do not set the batch_shape explicitly so mean_module can operate in both mode + # once fsolve used in _update can run in batch mode, we should explicitly set + # the bacth shape here + self.mean_module = ConstantMean() + # Do not optimize constant mean prior + for param in self.mean_module.parameters(): + param.requires_grad = False + + # set covariance module + # the default outputscale here is only a rule of thumb, meant to keep + # estimates away from scale value that would make Phi(f(x)) saturate + # at 0 or 1 + if covar_module is None: + os_lb, os_ub = 1e-2, 1e2 + ls_prior = GammaPrior(concentration=2.4, rate=2.7) + ls_prior_mode = (ls_prior.concentration - 1) / ls_prior.rate + covar_module = ScaleKernel( + RBFKernel( + batch_shape=self.batch_shape, + ard_num_dims=self.dim, + lengthscale_prior=ls_prior, + lengthscale_constraint=GreaterThan( + lower_bound=1e-4, transform=None, initial_value=ls_prior_mode + ), + dtype=torch.float64, + ), + outputscale_prior=SmoothedBoxPrior(a=os_lb, b=os_ub), + # make sure we won't get extreme values for the output scale + outputscale_constraint=Interval( + lower_bound=os_lb * 0.5, + upper_bound=os_ub * 2.0, + initial_value=1.0, + ), + dtype=torch.float64, + ) + if not isinstance(covar_module, ScaleKernel): + raise UnsupportedError("PairwiseGP must be used with a ScaleKernel.") + self.covar_module = covar_module + + self._x0 = None # will store temporary results for warm-starting + if self.datapoints is not None and self.comparisons is not None: + self.to(dtype=self.datapoints.dtype, device=self.datapoints.device) + # Find f_map for initial parameters with transformed datapoints + transformed_dp = self.transform_inputs(self.datapoints) + self._update(transformed_dp) + + self.to(self.datapoints) + + def __deepcopy__(self, memo) -> PairwiseGP: + attrs = ( + "consolidated_datapoints", + "consolidated_comparisons", + "covar", + "covar_inv", + "covar_chol", + "likelihood_hess", + "utility", + "hlcov_eye", + "unconsolidated_datapoints", + "unconsolidated_comparisons", + "consolidated_indices", + ) + + if any(getattr(self, attr) is not None for attr in attrs): + # Temporarily remove non-leaf tensors so that pytorch allows deepcopy + old_attr = {} + for attr in attrs: + old_attr[attr] = getattr(self, attr) + setattr(self, attr, None) + new_model = deepcopy(self, memo) + # now set things back + for attr in attrs: + setattr(self, attr, old_attr[attr]) + return new_model + else: + dcp = self.__deepcopy__ + # make sure we don't fall into the infinite recursive loop + self.__deepcopy__ = None + new_model = deepcopy(self, memo) + self.__deepcopy__ = dcp + return new_model + + def _has_no_data(self): + r"""Return true if the model does not have both datapoints and comparisons""" + return ( + self.datapoints is None + or len(self.datapoints.size()) == 0 + or self.comparisons is None + ) + + def _calc_covar(self, X1: Tensor, X2: Tensor) -> Tensor | LinearOperator: + r"""Calculate the covariance matrix given two sets of datapoints""" + covar = self.covar_module(X1, X2).to_dense() + # making sure covar is PSD when it's a covariance matrix + if X1 is X2: + scale = self.covar_module.outputscale.unsqueeze(-1).unsqueeze(-1).detach() + covar = _ensure_psd_with_jitter( + matrix=covar, + scale=scale, + jitter=self._jitter, + ) + return covar + + def _update_covar(self, datapoints: Tensor) -> None: + r"""Update values derived from the data and hyperparameters + + covar, covar_chol, and covar_inv will be of shape batch_shape x n x n + + Args: + datapoints: (Transformed) datapoints for finding f_max + """ + self.covar = self._calc_covar(datapoints, datapoints) + scale = self.covar_module.outputscale.unsqueeze(-1).unsqueeze(-1).detach() + self.covar_chol = _scaled_psd_safe_cholesky( + matrix=self.covar, + scale=scale, + jitter=self._jitter, + ) + self.covar_inv = torch.cholesky_inverse(self.covar_chol) + + def _prior_mean(self, X: Tensor) -> Tensor | LinearOperator: + r"""Return point prediction using prior only + + Args: + X: A `batch_size x n' x d`-dim Tensor at which to evaluate prior + + Returns: + Prior mean prediction + """ + return self.mean_module(X) + + def _prior_predict(self, X: Tensor) -> tuple[Tensor, Tensor]: + r"""Predict utility based on prior info only + + Args: + X: A `batch_size x n' x d`-dim Tensor at which to evaluate prior + + Returns: + pred_mean: predictive mean + pred_covar: predictive covariance + """ + pred_mean = self._prior_mean(X) + pred_covar = self._calc_covar(X, X) + return pred_mean, pred_covar + + def _grad_posterior_f( + self, + utility: Tensor | npt.NDArray, + datapoints: Tensor, + D: Tensor, + covar_chol: Tensor, + covar_inv: Tensor | None = None, + ret_np: bool = False, + ) -> Tensor | npt.NDArray: + r"""Compute the gradient of S loss wrt to f/utility in [Chu2005preference]_. + + For finding f_map, which is negative of the log posterior, i.e., -log(p(f|D)) + Derivative of (10) in [Chu2005preference]_. + Also see [Brochu2010tutorial]_ page 26. This is needed for estimating f_map. + + Args: + utility: A Tensor of shape `batch_size x n` + datapoints: A Tensor of shape `batch_size x n x d` as in self.datapoints + D: A Tensor of shape `batch_size x m x n` as in self.D + covar_chol: A Tensor of shape `batch_size x n x n`, as in self.covar_chol + covar_inv: `None` or a Tensor of shape `batch_size x n x n`, as in + self.covar_inv. This is not used but is needed so that + PairwiseGP._grad_posterior_f has the same signature as + PairwiseGP._hess_posterior_f. + ret_np: return a numpy array if True, otherwise a Tensor + """ + prior_mean = self._prior_mean(datapoints) + + if ret_np: + utility = torch.tensor(utility, dtype=self.datapoints.dtype) + prior_mean = prior_mean.cpu() + + # NOTE: During the optimization, it can occur that b, p, and g_ are NaNs, though + # in the cases that occured during testing, the optimization routine escaped and + # terminated successfully without NaNs in the result. + b = self.likelihood.negative_log_gradient_sum(utility=utility, D=D) + # g_ = covar_inv x (utility - pred_prior) + p = (utility - prior_mean).unsqueeze(-1).to(covar_chol) + g_ = torch.cholesky_solve(p, covar_chol).squeeze(-1) + g = g_ + b + if ret_np: + return g.cpu().numpy() + return g + + def _hess_posterior_f( + self, + utility: Tensor | npt.NDArray, + datapoints: Tensor, + D: Tensor, + covar_chol: Tensor, + covar_inv: Tensor, + ret_np: bool = False, + ) -> Tensor | npt.NDArray: + r"""Compute the hessian of S loss wrt utility for finding f_map. + + which is negative of the log posterior, i.e., -log(p(f|D)) + Following [Chu2005preference]_ section 2.2.1. + This is needed for estimating f_map + + Args: + utility: A Tensor of shape `batch_size x n` + datapoints: A Tensor of shape `batch_size x n x d`, as in + self.datapoints. This is not used but is needed so that + `_hess_posterior_f` has the same signature as + `_grad_posterior_f`. + D: A Tensor of shape `batch_size x m x n` as in self.D + covar_chol: A Tensor of shape `batch_size x n x n`, as in + self.covar_chol. This is not used but is needed so that + `_hess_posterior_f` has the same signature as + `_grad_posterior_f`. + covar_inv: A Tensor of shape `batch_size x n x n`, as in self.covar_inv + ret_np: return a numpy array if true, otherwise a Tensor + """ + if ret_np: + utility = torch.tensor(utility, dtype=self.datapoints.dtype) + + hl = self.likelihood.negative_log_hessian_sum(utility=utility, D=D) + hess = hl + covar_inv + return hess.numpy() if ret_np else hess + + def _update_utility_derived_values(self) -> None: + r""" + Set self.hlcov_eye to self.likelihood_hess @ self.covar + I. + + `self.hlcov_eye` is a utility-derived value not used during + optimization. This quantity is used so that we will be able to compute + the predictive covariance (in PairwiseGP.forward in posterior mode) with + better numerical stability using the substitution method: + + Let `pred_cov_fac = (covar + hl^-1)`, which is needed for calculating + the predictive covariance = `K - k.T @ pred_cov_fac^-1 @ k`. + Instead of inverting `pred_cov_fac`, let `hlcov_eye = (hl @ covar + I)` + Then we can obtain `pred_cov_fac^-1 @ k` by solving for p in + `(hl @ k) p = hlcov_eye` + `hlcov_eye p = hl @ k` + """ + hl = self.likelihood_hess # "C" from page 27, [Brochu2010tutorial]_ + hlcov = hl @ self.covar + eye = torch.eye( + hlcov.size(-1), dtype=self.datapoints.dtype, device=self.datapoints.device + ).expand(hlcov.shape) + self.hlcov_eye = hlcov + eye + + self.pred_cov_fac_need_update = False + + def _update(self, datapoints: Tensor, **kwargs) -> None: + r"""Update the model by updating the covar matrix and MAP utility values + + Update the model by + 1. Re-evaluating the covar matrix as the data or hyperparams may have changed + 2. Approximating maximum a posteriori of the utility function f using fsolve + + Should be called after data or hyperparameters are changed to update + f_map and related values + + self._xtol and self._maxfev are passed to fsolve as xtol and maxfev + to control stopping criteria + + Args: + datapoints: (transformed) datapoints for finding f_max + """ + + xtol = 1e-6 if self._xtol is None else self._xtol + maxfev = 100 if self._maxfev is None else self._maxfev + + # Using the latest param for covariance before calculating f_map + self._update_covar(datapoints) + + # scipy newton raphson + with torch.no_grad(): + # warm start + init_x0_size = self.batch_shape + torch.Size([self.n]) + if self._x0 is None or torch.Size(self._x0.shape) != init_x0_size: + sqrt_scale = ( + self.covar_module.outputscale.sqrt() + .unsqueeze(-1) + .detach() + .cpu() + .numpy() + ) + # Heuristic intialization using winning count with perturbation + # to avoid extreme or unprobable likelihood values + win_count = self.D.sum(dim=-2).detach().cpu().numpy() + wc_mean, wc_std = ( + win_count.mean(axis=-1, keepdims=True), + win_count.std(axis=-1, keepdims=True).clip(min=1e-6), + ) + x0 = (win_count - wc_mean) / wc_std + # adding random perturbation to in case get stuck at strange init values + x0 = x0 + 0.05 * np.random.standard_normal(init_x0_size) + # scale x0 to be on roughly the right scale + x0 = x0 * sqrt_scale + else: + x0 = self._x0 + + if len(self.batch_shape) > 0: + # batch mode, do optimize.fsolve sequentially on CPU + # TODO: enable vectorization/parallelization here + x0 = x0.reshape(-1, self.n) + dp_v = datapoints.view(-1, self.n, self.dim).cpu() + D_v = self.D.view(-1, self.m, self.n).cpu() + ch_v = self.covar_chol.view(-1, self.n, self.n).cpu() + ci_v = self.covar_inv.view(-1, self.n, self.n).cpu() + x = np.empty(x0.shape) + for i in range(x0.shape[0]): + fsolve_args = (dp_v[i], D_v[i], ch_v[i], ci_v[i], True) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning) + x[i] = optimize.fsolve( + x0=x0[i], + func=self._grad_posterior_f, + fprime=self._hess_posterior_f, + xtol=xtol, + maxfev=maxfev, + args=fsolve_args, + **kwargs, + ) + x = x.reshape(*init_x0_size) + else: + # fsolve only works on CPU + fsolve_args = ( + datapoints.cpu(), + self.D.cpu(), + self.covar_chol.cpu(), + self.covar_inv.cpu(), + True, + ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning) + x = optimize.fsolve( + x0=x0, + func=self._grad_posterior_f, + fprime=self._hess_posterior_f, + xtol=xtol, + maxfev=maxfev, + args=fsolve_args, + **kwargs, + ) + + self._x0 = x.copy() # save for warm-starting + f = torch.tensor(x, dtype=datapoints.dtype, device=datapoints.device) + + # To perform hyperparameter optimization, this needs to be recalculated + # when calling forward() in order to obtain correct gradients + # self.likelihood_hess is updated here is for the rare case where we + # do not want to call forward() + self.likelihood_hess = self.likelihood.negative_log_hessian_sum( + utility=f, D=self.D + ) + + # Lazy update hlcov_eye, which is used in calculating posterior during training + self.pred_cov_fac_need_update = True + # fill in dummy values for hlcov_eye so that load_state_dict can function + hlcov_eye_size = torch.Size((*self.likelihood_hess.shape[:-2], self.n, self.n)) + self.hlcov_eye = torch.empty(hlcov_eye_size) + + # Take two newton step on the posterior MAP point to fill + # in gradients for pytorch. Using 2 instead of 1 since empirically sometimes + # the first step results in gradients in the order of 1e-7 while the 2nd step + # allows it go down further to the order of 1e-12 and stay there. + self.utility = self._util_newton_updates( + dp=datapoints, x0=f.clone().requires_grad_(True), max_iter=2 + ) + + def _transform_batch_shape(self, X: Tensor, X_new: Tensor) -> tuple[Tensor, Tensor]: + r"""Transform X and X_new into the same shape + + Transform the batch shape of X to be compatible + with `X_new` to calculate the posterior. + If X has the same batch size as `X_new`, return it as is. + If one is in batch mode and the other one is not, convert both + into batch mode. + If both are in batch mode, this will only work if X_batch_shape + can propagate to X_new_batch_shape + + Args: + X: A `batch_shape x q x d`-dim or `(1 x) q x d`-dim Tensor + X_new: A `batch_shape x q x d`-dim Tensor + + Returns: + Transformed X and X_new pair + """ + X_bs = X.shape[:-2] # X batch shape + X_new_bs = X_new.shape[:-2] # X_new batch shape + if X_new_bs == X_bs: + # if batch shapes match, there's no need to transform + # X_new may or may not have batch_shape dimensions + return X, X_new + elif len(X_new_bs) < len(X_bs): + # if X_new has fewer dimension, try to expand it to X's shape + return X, X_new.expand(X_bs + X_new.shape[-2:]) + else: + # if X has fewer dimension, try to expand it to X_new's shape + return X.expand(X_new_bs + X.shape[-2:]), X_new + + def _util_newton_updates( + self, dp: Tensor, x0: Tensor, max_iter: int = 1, xtol: float | None = None + ) -> Tensor: + r"""Make `max_iter` newton updates on utility. + + This is used in `forward` to calculate and fill in gradient into tensors. + Instead of doing utility -= H^-1 @ g, use substition method. + See more explanation in _update_utility_derived_values. + By default only need to run one iteration just to fill the the gradients. + + Args: + dp: (Transformed) datapoints. A Tensor of shape `batch_size x n x d` + as in self.datapoints + x0: A `batch_size x n` dimension tensor, initial values. + max_iter: Max number of iterations. + xtol: Stop creteria. If `None`, do not stop until + finishing `max_iter` updates. + """ + xtol = float("-Inf") if xtol is None else xtol + D, ch = self.D, self.covar_chol + covar = self.covar + diff = float("Inf") + i = 0 + x = x0 + eye = None + while i < max_iter and diff > xtol: + hl = self.likelihood.negative_log_hessian_sum(utility=x, D=D) + self.likelihood_hess = hl + cov_hl = covar @ hl + if eye is None: + eye = torch.diag_embed( + torch.ones( + cov_hl.shape[:-1], device=cov_hl.device, dtype=cov_hl.dtype + ) + ) + cov_hl = cov_hl + eye # add 1 to cov_hl + g = self._grad_posterior_f( + utility=x, + datapoints=dp, + D=D, + covar_chol=ch, + ) + cov_g = covar @ g.unsqueeze(-1) + x_update = torch.linalg.solve(cov_hl, cov_g).squeeze(-1) + x_next = x - x_update + diff = torch.linalg.norm(x - x_next) + x = x_next + i += 1 + + return x + + def _consolidate_duplicates( + self, datapoints: Tensor, comparisons: Tensor + ) -> tuple[Tensor, Tensor]: + """Consolidate and cache datapoints and comparisons""" + # check if consolidated datapoints/comparisons are cached + if ( + (datapoints is not self.unconsolidated_datapoints) + or (comparisons is not self.unconsolidated_comparisons) + or (self.consolidated_datapoints is None) + or (self.consolidated_comparisons is None) + ): + self.unconsolidated_datapoints, self.unconsolidated_comparisons = ( + datapoints, + comparisons, + ) + + if len(datapoints.shape) > 2 or len(comparisons.shape) > 2: + # Do not perform consolidation in batch mode as block design + # cannot be guaranteed + self.consolidated_datapoints = datapoints + self.consolidated_comparisons = comparisons + self.consolidated_indices = None + else: + ( + self.consolidated_datapoints, + self.consolidated_comparisons, + self.consolidated_indices, + ) = consolidate_duplicates( + datapoints, + comparisons, + rtol=self._consolidate_rtol, + atol=self._consolidate_atol, + ) + + return self.consolidated_datapoints, self.consolidated_comparisons + + # ============== public APIs ============== + @property + def datapoints(self) -> Tensor: + r"""Alias for consolidated datapoints""" + return self.consolidated_datapoints + + @property + def comparisons(self) -> Tensor: + r"""Alias for consolidated comparisons""" + return self.consolidated_comparisons + + @property + def unconsolidated_utility(self) -> Tensor: + r"""Utility of the unconsolidated datapoints""" + if self.consolidated_indices is None: + # self.consolidated_indices is None in batch mode + return self.utility + else: + return self.utility[self.consolidated_indices] + + @property + def num_outputs(self) -> int: + r"""The number of outputs of the model.""" + return self._num_outputs + + @property + def batch_shape(self) -> torch.Size: + r"""The batch shape of the model. + + This is a batch shape from an I/O perspective, independent of the internal + representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). + For a model with `m` outputs, a `test_batch_shape x q x d`-shaped input `X` + to the `posterior` method returns a Posterior object over an output of + shape `broadcast(test_batch_shape, model.batch_shape) x q x m`. + """ + if self.datapoints is None: + # this could happen in prior mode + return torch.Size() + else: + return self.datapoints.shape[:-2] + +
+[docs] + @classmethod + def construct_inputs( + cls, + training_data: SupervisedDataset, + ) -> dict[str, Tensor]: + r""" + Construct `Model` keyword arguments from a `RankingDataset`. + + Args: + training_data: A `RankingDataset`, with attributes `train_X`, + `train_Y`, and, optionally, `train_Yvar`. + + Returns: + A dict of keyword arguments that can be used to initialize a + `PairwiseGP`, including `datapoints` and `comparisons`. + """ + if not isinstance(training_data, RankingDataset): + raise UnsupportedError( + "Only `RankingDataset` is supported for `PairwiseGP`. Received " + f"{type(training_data)}." + ) + datapoints = training_data._X.values + comparisons = training_data._X.indices + comp_order = training_data.Y + comparisons = torch.gather(input=comparisons, dim=-1, index=comp_order) + + return { + "datapoints": datapoints, + "comparisons": comparisons, + }
+ + +
+[docs] + def set_train_data( + self, + datapoints: Tensor | None = None, + comparisons: Tensor | None = None, + strict: bool = False, + update_model: bool = True, + ) -> None: + r"""Set datapoints and comparisons and update model properties if needed + + Args: + datapoints: Either `None` or a `batch_shape x n x d` dimension + tensor X. If there are input transformations, assume the + datapoints are not transformed. If either `datapoints` or + `comparisons` is `None`, construct a prior-only model. + comparisons: Either `None` or a tensor of size `batch_shape x m x + 2`. (i, j) means f_i is preferred over f_j. If either + `comparisons` or `datapoints` is `None`, construct a prior-only + model. + strict: `strict` argument as in gpytorch.models.exact_gp for compatibility + when using fit_gpytorch_mll with input_transform. + update_model: True if we want to refit the model (see _update) after + re-setting the data. + """ + # When datapoints and/or comparisons are None, we are constructing + # a prior-only model + if datapoints is None or comparisons is None: + return + + # following gpytorch.models.exact_gp.set_train_data + if datapoints is not None: + if torch.is_tensor(datapoints): + inputs = [datapoints] + + inputs = tuple( + input_.unsqueeze(-1) if input_.ndimension() == 1 else input_ + for input_ in inputs + ) + if strict: + _check_strict_input(inputs, self.train_inputs, "inputs") + + datapoints = inputs[0] + # Compatibility variables with fit_gpytorch_* + # alias for datapoints ("train_inputs") + self.train_inputs = inputs + + if comparisons is not None: + if strict: + _check_strict_input([comparisons], [self.train_targets], "targets") + + # convert to long so that it can be used as index and + # compatible with Tensor.scatter_ + comparisons = comparisons.long() + # Compatibility variables with fit_gpytorch_* + # alias for comparisons ("train_targets" here) + self.train_targets = comparisons + + # self.datapoints and self.comparisons are being updated here + self._consolidate_duplicates(datapoints, comparisons) + + # Compatibility variables with optimize_acqf + self._dtype = self.datapoints.dtype + self._num_outputs = 1 # 1 latent value output per observation + + self.dim = self.datapoints.shape[-1] # feature dimensions + self.n = self.datapoints.shape[-2] # num datapoints + self.m = self.comparisons.shape[-2] # num pairwise comparisons + # D is batch_size x m x n or num_comparison x num_datapoints. + # D_k_i is the s_k(x_i) value as in equation (6) in [Chu2005preference]_ + # D will usually be very sparse as well + # TODO swap out scatter_ so that comparisons could be int instead of long + # TODO: make D a sparse matrix once pytorch has better support for + # sparse tensors + D_size = torch.Size((*(self.batch_shape), self.m, self.n)) + self.D = torch.zeros( + D_size, dtype=self.datapoints.dtype, device=self.datapoints.device + ) + comp_view = self.comparisons.view(-1, self.m, 2).long() + for i, sub_D in enumerate(self.D.view(-1, self.m, self.n)): + sub_D.scatter_(1, comp_view[i, :, [0]], 1) + sub_D.scatter_(1, comp_view[i, :, [1]], -1) + self.DT = self.D.transpose(-1, -2) + + if update_model: + transformed_dp = self.transform_inputs(self.datapoints) + self._update(transformed_dp) + + self.to(self.datapoints)
+ + +
+[docs] + def load_state_dict( + self, state_dict: dict[str, Tensor], strict: bool = False + ) -> _IncompatibleKeys: + r"""Removes data related buffers from the `state_dict` and calls + `super().load_state_dict` with `strict=False`. + + Args: + state_dict: The state dict. + strict: Boolean specifying whether or not given and instance-bound + state_dicts should have identical keys. Only implemented for + `strict=False` since buffers will filters out when calling + `_load_from_state_dict`. + + Returns: + A named tuple `_IncompatibleKeys`, containing the `missing_keys` + and `unexpected_keys`. + """ + if strict: + raise UnsupportedError("Passing strict=True is not supported.") + + return super().load_state_dict(state_dict=state_dict, strict=False)
+ + + def _load_from_state_dict( + self, + state_dict: dict[str, Tensor], + prefix: str, + local_metadata: dict[str, Any], + strict: bool, + missing_keys: list[str], + unexpected_keys: list[str], + error_msgs: list[str], + ) -> None: + super()._load_from_state_dict( + state_dict={ + k: v for k, v in state_dict.items() if k not in self._buffer_names + }, + prefix=prefix, + local_metadata=local_metadata, + strict=False, + missing_keys=missing_keys, + unexpected_keys=unexpected_keys, + error_msgs=error_msgs, + ) + +
+[docs] + def forward(self, datapoints: Tensor) -> MultivariateNormal: + r"""Calculate a posterior or prior prediction. + + During training mode, forward implemented solely for gradient-based + hyperparam opt. Essentially what it does is to re-calculate the utility + f using its analytical form at f_map so that we are able to obtain + gradients of the hyperparameters. + + Args: + datapoints: A `batch_shape x n x d` Tensor, + should be the same as self.datapoints during training + + Returns: + A MultivariateNormal object, being one of the followings: + 1. Posterior centered at MAP points for training data (training mode) + 2. Prior predictions (prior mode) + 3. Predictive posterior (eval mode) + """ + # Training mode: optimizing + if self.training: + if self._has_no_data(): + raise RuntimeError( + "datapoints and comparisons cannot be None in training mode. " + "Call .eval() for prior predictions, " + "or call .set_train_data() to add training data." + ) + + if datapoints is not self.unconsolidated_datapoints: + raise RuntimeError("Must train on training data") + + # We pass in the untransformed datapoints into set_train_data + # as we will be setting self.datapoints as the untransformed datapoints + # self.transform_inputs will be called inside before calling _update() + self.set_train_data( + datapoints=datapoints, + comparisons=self.unconsolidated_comparisons, + update_model=True, + ) + + transformed_dp = self.transform_inputs(self.datapoints) + + hl = self.likelihood_hess + covar = self.covar + + # Apply matrix inversion lemma on eq. in page 27 of [Brochu2010tutorial]_ + # (A + B)^-1 = A^-1 - A^-1 @ (I + BA^-1)^-1 @ BA^-1 + # where A = covar_inv, B = hl + hl_cov = hl @ covar + eye = torch.eye( + hl_cov.size(-1), + dtype=self.datapoints.dtype, + device=self.datapoints.device, + ).expand(hl_cov.shape) + hl_cov_I = hl_cov + eye # add I to hl_cov + output_covar = covar - covar @ torch.linalg.solve(hl_cov_I, hl_cov) + output_mean = self.utility + + # Prior mode + elif settings.prior_mode.on() or self._has_no_data(): + transformed_new_dp = self.transform_inputs(datapoints) + # if we don't have any data yet, use prior GP to make predictions + output_mean, output_covar = self._prior_predict(transformed_new_dp) + + # Posterior mode + else: + transformed_dp = self.transform_inputs(self.datapoints) + transformed_new_dp = self.transform_inputs(datapoints).to(transformed_dp) + + # self.utility might be None if exception was raised and _update + # was failed to be called during hyperparameter optimization + # procedures (e.g., fit_gpytorch_mll_scipy) + if self.utility is None: + self._update(transformed_dp) + + if self.pred_cov_fac_need_update: + self._update_utility_derived_values() + + X, X_new = self._transform_batch_shape(transformed_dp, transformed_new_dp) + covar_chol, _ = self._transform_batch_shape(self.covar_chol, X_new) + hl, _ = self._transform_batch_shape(self.likelihood_hess, X_new) + hlcov_eye, _ = self._transform_batch_shape(self.hlcov_eye, X_new) + + # otherwise compute predictive mean and covariance + covar_xnew_x = self._calc_covar(X_new, X) + covar_x_xnew = covar_xnew_x.transpose(-1, -2) + covar_xnew = self._calc_covar(X_new, X_new) + p = self.utility - self._prior_mean(X) + + covar_inv_p = torch.cholesky_solve(p.unsqueeze(-1), covar_chol) + pred_mean = (covar_xnew_x @ covar_inv_p).squeeze(-1) + pred_mean = pred_mean + self._prior_mean(X_new) + + # Using the terminology from [Brochu2010tutorial]_ page 27: + # hl = C; hlcov_eye = CK + I; k = covar_x_xnew + # + # To compute the predictive covariance, one term we need is + # k^T (K + C^{-1})^{-1} k. + # Rather than performing two matrix inversions, we can compute this + # in a more efficient and numerically stable way by using + # fac = hlcov_eye^-1 @ hl @ covar_x_xnew + # = (CK + I)^-1 @ C @ k + # = (K + C^-1)^{-1} + # This is the substitution method. + fac = torch.linalg.solve(hlcov_eye, hl @ covar_x_xnew) + pred_covar = covar_xnew - (covar_xnew_x @ fac) + + output_mean, output_covar = pred_mean, pred_covar + + scale = self.covar_module.outputscale.unsqueeze(-1).unsqueeze(-1).detach() + post = MultivariateNormal( + mean=output_mean, + # output_covar is sometimes non-PSD + # perform a cholesky decomposition to check and amend + covariance_matrix=RootLinearOperator( + _scaled_psd_safe_cholesky( + matrix=output_covar, + scale=scale, + jitter=self._jitter, + ) + ), + ) + return post
+ + + # ============== botorch.models.model.Model interfaces ============== +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + observation_noise: bool = False, + posterior_transform: PosteriorTransform | None = None, + ) -> Posterior: + r"""Computes the posterior over model outputs at the provided points. + + Args: + X: A `batch_shape x q x d`-dim Tensor, where `d` is the dimension + of the feature space and `q` is the number of points considered jointly. + output_indices: As defined in parent Model class, not used for this model. + observation_noise: Ignored (since noise is not identifiable from scale + in probit models). + posterior_transform: An optional PosteriorTransform. + + Returns: + A `Posterior` object, representing joint + distributions over `q` points. + """ + self.eval() # make sure model is in eval mode + + if output_indices is not None: + raise RuntimeError( + "output_indices is not None. PairwiseGP should not be a" + "multi-output model." + ) + + post = self(X) + posterior = GPyTorchPosterior(post) + if posterior_transform is not None: + return posterior_transform(posterior) + return posterior
+ + +
+[docs] + def condition_on_observations(self, X: Tensor, Y: Tensor) -> Model: + r"""Condition the model on new observations. + + Note that unlike other BoTorch models, PairwiseGP requires Y to be + pairwise comparisons. + + Args: + X: A `batch_shape x n x d` dimension tensor X + Y: A tensor of size `batch_shape x m x 2`. (i, j) means + f_i is preferred over f_j + kwargs: Not used. + + Returns: + A (deepcopied) `Model` object of the same type, representing the + original model conditioned on the new observations `(X, Y)`. + """ + new_model = deepcopy(self) + + if self._has_no_data(): + # If the model previously has no data, set X and Y as the data directly + new_model.set_train_data(X, Y, update_model=True) + else: + # Can only condition on pairwise comparisons instead of the directly + # observed values. Raise a RuntimeError if Y is not a tensor presenting + # pairwise comparisons + if Y.dtype in (float32, float64) or Y.shape[-1] != 2: + raise RuntimeError( + "Conditioning on non-pairwise comparison observations." + ) + + # Reshaping datapoints and comparisons by batches + Y_new_batch_shape = Y.shape[:-2] + new_datapoints = self.datapoints.expand( + Y_new_batch_shape + self.datapoints.shape[-2:] + ) + new_comparisons = self.comparisons.expand( + Y_new_batch_shape + self.comparisons.shape[-2:] + ) + # Reshape X since Y may have additional batch dim. from fantasy models + X = X.expand(Y_new_batch_shape + X.shape[-2:]) + + new_datapoints = torch.cat((new_datapoints, X.to(new_datapoints)), dim=-2) + + shifted_comp = Y.to(new_comparisons) + self.n + new_comparisons = torch.cat((new_comparisons, shifted_comp), dim=-2) + + # TODO: be smart about how we can update covar matrix here + new_model.set_train_data(new_datapoints, new_comparisons, update_model=True) + + return new_model
+
+ + + +
+[docs] +class PairwiseLaplaceMarginalLogLikelihood(MarginalLogLikelihood): + r"""Laplace-approximated marginal log likelihood/evidence for PairwiseGP + + See (12) from [Chu2005preference]_. + """ + + def __init__(self, likelihood, model: GP): + """ + Args: + likelihood: Used as in args to GPyTorch MarginalLogLikelihood + model: Used as in args to GPyTorch MarginalLogLikelihood + """ + super().__init__(likelihood, model) + +
+[docs] + def forward(self, post: Posterior, comp: Tensor) -> Tensor: + r"""Calculate approximated log evidence, i.e., log(P(D|theta)) + + Note that post will be based on the consolidated/deduped datapoints for + numerical stability, but comp will still be the unconsolidated comparisons + so that it's still compatible with fit_gpytorch_*. + + Args: + post: training posterior distribution from self.model (after consolidation) + comp: Comparisons pairs (before consolidation) + + Returns: + The approximated evidence, i.e., the marginal log likelihood + """ + + model = self.model + likelihood = self.likelihood + if comp is not model.unconsolidated_comparisons: + raise RuntimeError("Must train on training data") + + f_map = post.mean.squeeze(-1) + + log_likelihood = likelihood.log_p(utility=f_map, D=model.D) + neg_log_likelihood_sum = -(torch.sum(log_likelihood, dim=-1)) + + # 1/2 f_map^T @ covar_inv @ f_map + inv_prod = torch.cholesky_solve(f_map.unsqueeze(-1), model.covar_chol) + log_prior = 0.5 * (f_map.unsqueeze(-2) @ inv_prod).squeeze(-1).squeeze(-1) + log_posterior = neg_log_likelihood_sum + log_prior + # log_posterior is the S loss function in [Chu2005preference]_ + log_posterior = -log_posterior.clamp(min=0) + + mll = model.covar @ model.likelihood_hess + mll = mll + torch.diag_embed( + torch.ones(mll.shape[:-1], device=mll.device, dtype=mll.dtype) + ) + mll = -0.5 * torch.logdet(mll) + + mll = mll + log_posterior + + # Sum up mll first so that when adding parameter prior probs it won't + # propagate and double count + mll = mll.sum() + + # Add log probs of priors on the (functions of) parameters + for _, module, prior, closure, _ in self.named_priors(): + mll = mll.add(prior.log_prob(closure(module)).sum()) + + return mll
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/transforms/factory.html b/website-old/pages/api/_modules/botorch/models/transforms/factory.html new file mode 100644 index 0000000000..999eaabd1f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/transforms/factory.html @@ -0,0 +1,185 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.transforms.factory

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections import OrderedDict
+
+from botorch.models.transforms.input import (
+    ChainedInputTransform,
+    Normalize,
+    OneHotToNumeric,
+    Round,
+)
+from torch import Tensor
+
+
+
+[docs] +def get_rounding_input_transform( + one_hot_bounds: Tensor, + integer_indices: list[int] | None = None, + categorical_features: dict[int, int] | None = None, + initialization: bool = False, + return_numeric: bool = False, + approximate: bool = False, +) -> ChainedInputTransform: + """Get a rounding input transform. + + The rounding function will take inputs from the unit cube, + unnormalize the integers raw search space, round the inputs, + and normalize them back to the unit cube. + + Categoricals are assumed to be one-hot encoded. Integers are + currently assumed to be contiguous ranges (e.g. [1,2,3] and not + [1,5,7]). + + TODO: support non-contiguous sets of integers by modifying + the rounding function. + + Args: + one_hot_bounds: The raw search space bounds where categoricals are + encoded in one-hot representation and the integer parameters + are not normalized. + integer_indices: The indices of the integer parameters. + categorical_features: A dictionary mapping indices to cardinalities + for the categorical features. + initialization: A boolean indicating whether this exact rounding + function is for initialization. For initialization, the bounds + for are expanded such that the end point of a range is selected + with same probability that an interior point is selected, after + rounding. + return_numeric: A boolean indicating whether to return numeric or + one-hot encoded categoricals. Returning a nummeric + representation is helpful if the downstream code (e.g. kernel) + expects a numeric representation of the categoricals. + approximate: A boolean indicating whether to use an approximate + rounding function. + + Returns: + The rounding function ChainedInputTransform. + """ + has_integers = integer_indices is not None and len(integer_indices) > 0 + has_categoricals = ( + categorical_features is not None and len(categorical_features) > 0 + ) + if not (has_integers or has_categoricals): + raise ValueError( + "A rounding function is a no-op " + "if there are no integer or categorical parammeters." + ) + if initialization and has_integers: + # this gives the extreme integer values (end points) + # the same probability as the interior values of the range + init_one_hot_bounds = one_hot_bounds.clone() + init_one_hot_bounds[0, integer_indices] -= 0.4999 + init_one_hot_bounds[1, integer_indices] += 0.4999 + else: + init_one_hot_bounds = one_hot_bounds + + tfs = OrderedDict() + if has_integers: + # unnormalize to integer space + tfs["unnormalize_tf"] = Normalize( + d=init_one_hot_bounds.shape[1], + bounds=init_one_hot_bounds, + indices=integer_indices, + transform_on_train=False, + transform_on_eval=True, + transform_on_fantasize=True, + reverse=True, + ) + # round + tfs["round"] = Round( + approximate=approximate, + transform_on_train=False, + transform_on_fantasize=True, + integer_indices=integer_indices, + categorical_features=categorical_features, + ) + if has_integers: + # renormalize to unit cube + tfs["normalize_tf"] = Normalize( + d=one_hot_bounds.shape[1], + bounds=one_hot_bounds, + indices=integer_indices, + transform_on_train=False, + transform_on_eval=True, + transform_on_fantasize=True, + reverse=False, + ) + if return_numeric and has_categoricals: + tfs["one_hot_to_numeric"] = OneHotToNumeric( + # this is the dimension using one-hot encoded representation + dim=one_hot_bounds.shape[-1], + categorical_features=categorical_features, + transform_on_train=True, + transform_on_eval=True, + transform_on_fantasize=True, + ) + tf = ChainedInputTransform(**tfs) + tf.to(dtype=one_hot_bounds.dtype, device=one_hot_bounds.device) + tf.eval() + return tf
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/transforms/input.html b/website-old/pages/api/_modules/botorch/models/transforms/input.html new file mode 100644 index 0000000000..4bedacfd22 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/transforms/input.html @@ -0,0 +1,1907 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.transforms.input

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Input Transformations.
+
+These classes implement a variety of transformations for
+input parameters including: learned input warping functions,
+rounding functions, and log transformations. The input transformation
+is typically part of a Model and applied within the model.forward()
+method.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections import OrderedDict
+from collections.abc import Callable, Iterable
+from typing import Any
+from warnings import warn
+
+import numpy as np
+import torch
+from botorch.exceptions.errors import BotorchTensorDimensionError
+from botorch.exceptions.warnings import UserInputWarning
+from botorch.models.transforms.utils import interaction_features, subset_transform
+from botorch.models.utils import fantasize
+from botorch.utils.rounding import approximate_round, OneHotArgmaxSTE, RoundSTE
+from gpytorch import Module as GPyTorchModule
+from gpytorch.constraints import GreaterThan
+from gpytorch.priors import Prior
+from torch import LongTensor, nn, Tensor
+from torch.distributions import Kumaraswamy
+from torch.nn import Module, ModuleDict
+from torch.nn.functional import one_hot
+
+
+
+[docs] +class InputTransform(Module, ABC): + r"""Abstract base class for input transforms. + + Properties: + is_one_to_many: A boolean denoting whether the transform produces + multiple values for each input. + transform_on_train: A boolean indicating whether to apply the + transform in train() mode. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. + transform_on_fantasize: A boolean indicating whether to apply + the transform when called from within a `fantasize` call. + """ + + is_one_to_many: bool = False + transform_on_eval: bool + transform_on_train: bool + transform_on_fantasize: bool + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r"""Transform the inputs to a model. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n' x d`-dim tensor of transformed inputs. + """ + if self.training: + if self.transform_on_train: + return self.transform(X) + elif self.transform_on_eval: + if fantasize.off() or self.transform_on_fantasize: + return self.transform(X) + return X
+ + +
+[docs] + @abstractmethod + def transform(self, X: Tensor) -> Tensor: + r"""Transform the inputs to a model. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + pass # pragma: no cover
+ + +
+[docs] + def untransform(self, X: Tensor) -> Tensor: + r"""Un-transform the inputs to a model. + + Args: + X: A `batch_shape x n x d`-dim tensor of transformed inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of un-transformed inputs. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement the `untransform` method." + )
+ + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Note: The reason that a custom equals method is defined rather than + defining an __eq__ method is because defining an __eq__ method sets + the __hash__ method to None. Hashing modules is currently used in + pytorch. See https://github.com/pytorch/pytorch/issues/7733. + + Args: + other: Another input transform. + + Returns: + A boolean indicating if the other transform is equivalent. + """ + other_state_dict = other.state_dict() + return ( + type(self) is type(other) + and (self.transform_on_train == other.transform_on_train) + and (self.transform_on_eval == other.transform_on_eval) + and (self.transform_on_fantasize == other.transform_on_fantasize) + and all( + torch.allclose(v, other_state_dict[k].to(v)) + for k, v in self.state_dict().items() + ) + )
+ + +
+[docs] + def preprocess_transform(self, X: Tensor) -> Tensor: + r"""Apply transforms for preprocessing inputs. + + The main use cases for this method are 1) to preprocess training data + before calling `set_train_data` and 2) preprocess `X_baseline` for noisy + acquisition functions so that `X_baseline` is "preprocessed" with the + same transformations as the cached training inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of (transformed) inputs. + """ + if self.transform_on_train: + # We need to disable learning of bounds / affine coefficients here. + # See why: https://github.com/pytorch/botorch/issues/1078. + if hasattr(self, "learn_coefficients"): + learn_coefficients = self.learn_coefficients + self.learn_coefficients = False + result = self.transform(X) + self.learn_coefficients = learn_coefficients + return result + else: + return self.transform(X) + return X
+
+ + + +
+[docs] +class BatchBroadcastedInputTransform(InputTransform, ModuleDict): + r"""An input transform representing a list of transforms to be broadcasted.""" + + def __init__( + self, + transforms: list[InputTransform], + broadcast_index: int = -3, + ) -> None: + r"""A transform list that is broadcasted across a batch dimension specified by + `broadcast_index`. This is allows using a batched Gaussian process model when + the input transforms are different for different batch dimensions. + + Args: + transforms: The transforms to broadcast across the first batch dimension. + The transform at position i in the list will be applied to `X[i]` for + a given input tensor `X` in the forward pass. + broadcast_index: The tensor index at which the transforms are broadcasted. + + Example: + >>> tf1 = Normalize(d=2) + >>> tf2 = InputStandardize(d=2) + >>> tf = BatchBroadcastedTransformList(transforms=[tf1, tf2]) + """ + super().__init__() + self.transform_on_train = False + self.transform_on_eval = False + self.transform_on_fantasize = False + self.transforms = transforms + if broadcast_index in (-2, -1): + raise ValueError( + "The broadcast index cannot be -2 and -1, as these indices are reserved" + " for non-batch, data and input dimensions." + ) + self.broadcast_index = broadcast_index + self.is_one_to_many = self.transforms[0].is_one_to_many + if not all(tf.is_one_to_many == self.is_one_to_many for tf in self.transforms): + raise ValueError( # output shapes of transforms must be the same + "All transforms must have the same is_one_to_many property." + ) + for tf in self.transforms: + self.transform_on_train |= tf.transform_on_train + self.transform_on_eval |= tf.transform_on_eval + self.transform_on_fantasize |= tf.transform_on_fantasize + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Transform the inputs to a model. + + Individual transforms are applied in sequence and results are returned as + a batched tensor. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + return torch.stack( + [t.forward(Xi) for Xi, t in self._Xs_and_transforms(X)], + dim=self.broadcast_index, + )
+ + +
+[docs] + def untransform(self, X: Tensor) -> Tensor: + r"""Un-transform the inputs to a model. + + Un-transforms of the individual transforms are applied in reverse sequence. + + Args: + X: A `batch_shape x n x d`-dim tensor of transformed inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of un-transformed inputs. + """ + return torch.stack( + [t.untransform(Xi) for Xi, t in self._Xs_and_transforms(X)], + dim=self.broadcast_index, + )
+ + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Args: + other: Another input transform. + + Returns: + A boolean indicating if the other transform is equivalent. + """ + return ( + super().equals(other=other) + and all(t1.equals(t2) for t1, t2 in zip(self.transforms, other.transforms)) + and (self.broadcast_index == other.broadcast_index) + )
+ + +
+[docs] + def preprocess_transform(self, X: Tensor) -> Tensor: + r"""Apply transforms for preprocessing inputs. + + The main use cases for this method are 1) to preprocess training data + before calling `set_train_data` and 2) preprocess `X_baseline` for noisy + acquisition functions so that `X_baseline` is "preprocessed" with the + same transformations as the cached training inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of (transformed) inputs. + """ + return torch.stack( + [t.preprocess_transform(Xi) for Xi, t in self._Xs_and_transforms(X)], + dim=self.broadcast_index, + )
+ + + def _Xs_and_transforms(self, X: Tensor) -> Iterable[tuple[Tensor, InputTransform]]: + r"""Returns an iterable of sub-tensors of X and their associated transforms. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + An iterable containing tuples of sub-tensors of X and their transforms. + """ + Xs = X.unbind(dim=self.broadcast_index) + return zip(Xs, self.transforms)
+ + + +
+[docs] +class ChainedInputTransform(InputTransform, ModuleDict): + r"""An input transform representing the chaining of individual transforms.""" + + def __init__(self, **transforms: InputTransform) -> None: + r"""Chaining of input transforms. + + Args: + transforms: The transforms to chain. Internally, the names of the + kwargs are used as the keys for accessing the individual + transforms on the module. + + Example: + >>> tf1 = Normalize(d=2) + >>> tf2 = Normalize(d=2) + >>> tf = ChainedInputTransform(tf1=tf1, tf2=tf2) + >>> list(tf.keys()) + ['tf1', 'tf2'] + >>> tf["tf1"] + Normalize() + + """ + super().__init__(OrderedDict(transforms)) + self.transform_on_train = False + self.transform_on_eval = False + self.transform_on_fantasize = False + for tf in transforms.values(): + self.is_one_to_many |= tf.is_one_to_many + self.transform_on_train |= tf.transform_on_train + self.transform_on_eval |= tf.transform_on_eval + self.transform_on_fantasize |= tf.transform_on_fantasize + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Transform the inputs to a model. + + Individual transforms are applied in sequence. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + for tf in self.values(): + X = tf.forward(X) + return X
+ + +
+[docs] + def untransform(self, X: Tensor) -> Tensor: + r"""Un-transform the inputs to a model. + + Un-transforms of the individual transforms are applied in reverse sequence. + + Args: + X: A `batch_shape x n x d`-dim tensor of transformed inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of un-transformed inputs. + """ + for tf in reversed(self.values()): + X = tf.untransform(X) + return X
+ + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Args: + other: Another input transform. + + Returns: + A boolean indicating if the other transform is equivalent. + """ + return super().equals(other=other) and all( + t1.equals(t2) for t1, t2 in zip(self.values(), other.values()) + )
+ + +
+[docs] + def preprocess_transform(self, X: Tensor) -> Tensor: + r"""Apply transforms for preprocessing inputs. + + The main use cases for this method are 1) to preprocess training data + before calling `set_train_data` and 2) preprocess `X_baseline` for noisy + acquisition functions so that `X_baseline` is "preprocessed" with the + same transformations as the cached training inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of (transformed) inputs. + """ + for tf in self.values(): + X = tf.preprocess_transform(X) + return X
+
+ + + +
+[docs] +class ReversibleInputTransform(InputTransform, ABC): + r"""An abstract class for a reversible input transform. + + Properties: + reverse: A boolean indicating if the functionality of transform + and untransform methods should be swapped. + """ + + reverse: bool + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Transform the inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + return self._untransform(X) if self.reverse else self._transform(X)
+ + +
+[docs] + def untransform(self, X: Tensor) -> Tensor: + r"""Un-transform the inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of un-transformed inputs. + """ + return self._transform(X) if self.reverse else self._untransform(X)
+ + + @abstractmethod + def _transform(self, X: Tensor) -> Tensor: + r"""Forward transform the inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + pass # pragma: no cover + + @abstractmethod + def _untransform(self, X: Tensor) -> Tensor: + r"""Reverse transform the inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + pass # pragma: no cover + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Args: + other: Another input transform. + + Returns: + A boolean indicating if the other transform is equivalent. + """ + return super().equals(other=other) and (self.reverse == other.reverse)
+
+ + + +
+[docs] +class AffineInputTransform(ReversibleInputTransform): + def __init__( + self, + d: int, + coefficient: Tensor, + offset: Tensor, + indices: list[int] | Tensor | None = None, + batch_shape: torch.Size = torch.Size(), # noqa: B008 + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + reverse: bool = False, + ) -> None: + r"""Apply affine transformation to input: + + `output = (input - offset) / coefficient` + + Args: + d: The dimension of the input space. + coefficient: Tensor of linear coefficients, shape must to be + broadcastable with `(batch_shape x n x d)`-dim input tensors. + offset: Tensor of offset coefficients, shape must to be + broadcastable with `(batch_shape x n x d)`-dim input tensors. + indices: The indices of the inputs to transform. If omitted, + take all dimensions of the inputs into account. Either a list of ints + or a Tensor of type `torch.long`. + batch_shape: The batch shape of the inputs (assuming input tensors + of shape `batch_shape x n x d`). If provided, perform individual + transformation per batch, otherwise uses a single transformation. + transform_on_train: A boolean indicating whether to apply the + transform in train() mode. Default: True. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: True. + reverse: A boolean indicating whether the forward pass should untransform + the inputs. + """ + super().__init__() + if (indices is not None) and (len(indices) == 0): + raise ValueError("`indices` list is empty!") + if (indices is not None) and (len(indices) > 0): + indices = torch.as_tensor( + indices, dtype=torch.long, device=coefficient.device + ) + if len(indices) > d: + raise ValueError("Can provide at most `d` indices!") + if (indices > d - 1).any(): + raise ValueError("Elements of `indices` have to be smaller than `d`!") + if len(indices.unique()) != len(indices): + raise ValueError("Elements of `indices` tensor must be unique!") + self.register_buffer("indices", indices) + torch.broadcast_shapes(coefficient.shape, offset.shape) + + self._d = d + self.register_buffer("_coefficient", coefficient) + self.register_buffer("_offset", offset) + self.batch_shape = batch_shape + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + self.reverse = reverse + + @property + def coefficient(self) -> Tensor: + r"""The tensor of linear coefficients.""" + coeff = self._coefficient + return coeff if self.learn_coefficients and self.training else coeff.detach() + + @property + def offset(self) -> Tensor: + r"""The tensor of offset coefficients.""" + offset = self._offset + return offset if self.learn_coefficients and self.training else offset.detach() + + @property + def learn_coefficients(self) -> bool: + return getattr(self, "_learn_coefficients", False) + + @learn_coefficients.setter + def learn_coefficients(self, value: bool) -> None: + r"""A boolean denoting whether to learn the coefficients + from inputs during model training. + """ + self._learn_coefficients = value + + @subset_transform + def _transform(self, X: Tensor) -> Tensor: + r"""Apply affine transformation to input. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + self._check_shape(X) + if self.learn_coefficients and self.training: + self._update_coefficients(X) + self._to(X) + return (X - self.offset) / self.coefficient + + @subset_transform + def _untransform(self, X: Tensor) -> Tensor: + r"""Apply inverse of affine transformation. + + Args: + X: A `batch_shape x n x d`-dim tensor of transformed inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of un-transformed inputs. + """ + self._to(X) + return self.coefficient * X + self.offset + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Args: + other: Another input transform. + + Returns: + A boolean indicating if the other transform is equivalent. + """ + if hasattr(self, "indices") != hasattr(other, "indices"): + return False + isequal = ( + super().equals(other=other) + and (self._d == other._d) + and torch.allclose(self.coefficient, other.coefficient) + and torch.allclose(self.offset, other.offset) + and self.learn_coefficients == other.learn_coefficients + ) + if hasattr(self, "indices"): + isequal = isequal and bool((self.indices == other.indices).all()) + return isequal
+ + + def _check_shape(self, X: Tensor) -> None: + """Checking input dimensions, included to increase code sharing + among the derived classes Normalize and InputStandardize. + """ + if X.size(-1) != self.offset.size(-1): + raise BotorchTensorDimensionError( + f"Wrong input dimension. Received {X.size(-1)}, " + f"expected {self.offset.size(-1)}." + ) + if X.ndim < 2: + raise BotorchTensorDimensionError( + f"`X` must have at least 2 dimensions, but has {X.ndim}." + ) + + n = len(self.batch_shape) + 2 + if self.training and X.ndim < n: + raise ValueError( + f"`X` must have at least {n} dimensions, {n - 2} batch and 2 innate" + f" , but has {X.ndim}." + ) + + torch.broadcast_shapes(self.coefficient.shape, self.offset.shape, X.shape) + + def _to(self, X: Tensor) -> None: + r"""Makes coefficient and offset have same device and dtype as X.""" + self._coefficient = self.coefficient.to(X) + self._offset = self.offset.to(X) + + def _update_coefficients(self, X: Tensor) -> None: + r"""Updates affine coefficients. Implemented by subclasses, + e.g. Normalize and InputStandardize. + """ + raise NotImplementedError( + "Only subclasses of AffineInputTransform implement " + "_update_coefficients, e.g. Normalize and InputStandardize." + )
+ + + +
+[docs] +class Normalize(AffineInputTransform): + r"""Normalize the inputs to the unit cube. + + If no explicit bounds are provided this module is stateful: If in train mode, + calling `forward` updates the module state (i.e. the normalizing bounds). If + in eval mode, calling `forward` simply applies the normalization using the + current module state. + """ + + def __init__( + self, + d: int, + indices: list[int] | Tensor | None = None, + bounds: Tensor | None = None, + batch_shape: torch.Size = torch.Size(), # noqa: B008 + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + reverse: bool = False, + min_range: float = 1e-8, + learn_bounds: bool | None = None, + almost_zero: float = 1e-12, + ) -> None: + r"""Normalize the inputs to the unit cube. + + Args: + d: The dimension of the input space. + indices: The indices of the inputs to normalize. If omitted, + take all dimensions of the inputs into account. + bounds: If provided, use these bounds to normalize the inputs. If + omitted, learn the bounds in train mode. + batch_shape: The batch shape of the inputs (assuming input tensors + of shape `batch_shape x n x d`). If provided, perform individual + normalization per batch, otherwise uses a single normalization. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: True. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: True. + reverse: A boolean indicating whether the forward pass should untransform + the inputs. + min_range: If the range of an input dimension is smaller than `min_range`, + that input dimension will not be normalized. This is equivalent to + using bounds of `[0, 1]` for this dimension, and helps avoid division + by zero errors and related numerical issues. See the example below. + NOTE: This only applies if `learn_bounds=True`. + learn_bounds: Whether to learn the bounds in train mode. Defaults + to False if bounds are provided, otherwise defaults to True. + + Example: + >>> t = Normalize(d=2) + >>> t(torch.tensor([[3., 2.], [3., 6.]])) + ... tensor([[3., 2.], + ... [3., 6.]]) + >>> t.eval() + ... Normalize() + >>> t(torch.tensor([[3.5, 2.8]])) + ... tensor([[3.5, 0.2]]) + >>> t.bounds + ... tensor([[0., 2.], + ... [1., 6.]]) + >>> t.coefficient + ... tensor([[1., 4.]]) + """ + if learn_bounds is not None: + self.learn_coefficients = learn_bounds + else: + self.learn_coefficients = bounds is None + transform_dimension = d if indices is None else len(indices) + if bounds is not None: + if indices is not None and bounds.size(-1) == d: + bounds = bounds[..., indices] + if bounds.size(-1) != transform_dimension: + raise BotorchTensorDimensionError( + "Dimensions of provided `bounds` are incompatible with " + f"transform_dimension = {transform_dimension}!" + ) + offset = bounds[..., 0:1, :] + coefficient = bounds[..., 1:2, :] - offset + if coefficient.ndim > 2: + batch_shape = coefficient.shape[:-2] + else: + coefficient = torch.ones(*batch_shape, 1, transform_dimension) + offset = torch.zeros(*batch_shape, 1, transform_dimension) + if self.learn_coefficients is False: + warn( + "learn_bounds is False and no bounds were provided. The bounds " + "will not be updated and the transform will be a no-op.", + UserInputWarning, + ) + super().__init__( + d=d, + coefficient=coefficient, + offset=offset, + indices=indices, + batch_shape=batch_shape, + transform_on_train=transform_on_train, + transform_on_eval=transform_on_eval, + transform_on_fantasize=transform_on_fantasize, + reverse=reverse, + ) + self.min_range = min_range + + @property + def ranges(self): + return self.coefficient + + @property + def mins(self): + return self.offset + + @property + def bounds(self) -> Tensor: + r"""The bounds used for normalizing the inputs.""" + return torch.cat([self.offset, self.offset + self.coefficient], dim=-2) + + @property + def learn_bounds(self) -> bool: + return self.learn_coefficients + + def _update_coefficients(self, X) -> None: + """Computes the normalization bounds and updates the affine + coefficients, which determine the base class's behavior. + """ + # Aggregate mins and ranges over extra batch and marginal dims + batch_ndim = min(len(self.batch_shape), X.ndim - 2) # batch rank of `X` + reduce_dims = (*range(X.ndim - batch_ndim - 2), X.ndim - 2) + offset = torch.amin(X, dim=reduce_dims).unsqueeze(-2) + coefficient = torch.amax(X, dim=reduce_dims).unsqueeze(-2) - offset + almost_zero = coefficient < self.min_range + self._coefficient = torch.where(almost_zero, 1.0, coefficient) + self._offset = torch.where(almost_zero, 0.0, offset) + +
+[docs] + def get_init_args(self) -> dict[str, Any]: + r"""Get the arguments necessary to construct an exact copy of the transform.""" + return { + "d": self._d, + "indices": getattr(self, "indices", None), + "bounds": self.bounds, + "batch_shape": self.batch_shape, + "transform_on_train": self.transform_on_train, + "transform_on_eval": self.transform_on_eval, + "transform_on_fantasize": self.transform_on_fantasize, + "reverse": self.reverse, + "min_range": self.min_range, + "learn_bounds": self.learn_bounds, + }
+
+ + + +
+[docs] +class InputStandardize(AffineInputTransform): + r"""Standardize inputs (zero mean, unit variance). + + In train mode, calling `forward` updates the module state + (i.e. the mean/std normalizing constants). If in eval mode, calling `forward` + simply applies the standardization using the current module state. + """ + + def __init__( + self, + d: int, + indices: list[int] | Tensor | None = None, + batch_shape: torch.Size = torch.Size(), # noqa: B008 + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + reverse: bool = False, + min_std: float = 1e-8, + ) -> None: + r"""Standardize inputs (zero mean, unit variance). + + Args: + d: The dimension of the input space. + indices: The indices of the inputs to standardize. If omitted, + take all dimensions of the inputs into account. + batch_shape: The batch shape of the inputs (asssuming input tensors + of shape `batch_shape x n x d`). If provided, perform individual + normalization per batch, otherwise uses a single normalization. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: True + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True + reverse: A boolean indicating whether the forward pass should untransform + the inputs. + min_std: If the standard deviation of an input dimension is smaller than + `min_std`, that input dimension will not be standardized. This is + equivalent to using a standard deviation of 1.0 and a mean of 0.0 for + this dimension, and helps avoid division by zero errors and related + numerical issues. + """ + transform_dimension = d if indices is None else len(indices) + super().__init__( + d=d, + coefficient=torch.ones(*batch_shape, 1, transform_dimension), + offset=torch.zeros(*batch_shape, 1, transform_dimension), + indices=indices, + batch_shape=batch_shape, + transform_on_train=transform_on_train, + transform_on_eval=transform_on_eval, + transform_on_fantasize=transform_on_fantasize, + reverse=reverse, + ) + self.min_std = min_std + self.learn_coefficients = True + + @property + def stds(self): + return self.coefficient + + @property + def means(self): + return self.offset + + def _update_coefficients(self, X: Tensor) -> None: + """Computes the normalization bounds and updates the affine + coefficients, which determine the base class's behavior. + """ + # Aggregate means and standard deviations over extra batch and marginal dims + batch_ndim = min(len(self.batch_shape), X.ndim - 2) # batch rank of `X` + reduce_dims = (*range(X.ndim - batch_ndim - 2), X.ndim - 2) + coefficient, offset = ( + values.unsqueeze(-2) + for values in torch.std_mean(X, dim=reduce_dims, unbiased=True) + ) + almost_zero = coefficient < self.min_std + self._coefficient = torch.where(almost_zero, 1.0, coefficient) + self._offset = torch.where(almost_zero, 0.0, offset)
+ + + +
+[docs] +class Round(InputTransform): + r"""A discretization transformation for discrete inputs. + + If `approximate=False` (the default), uses PyTorch's `round`. + + If `approximate=True`, a differentiable approximate rounding function is + used, with a temperature parameter of `tau`. This method is a piecewise + approximation of a rounding function where each piece is a hyperbolic + tangent function. + + For integers, this will typically be used in conjunction + with normalization as follows: + + In eval() mode (i.e. after training), the inputs pass + would typically be normalized to the unit cube (e.g. during candidate + optimization). 1. These are unnormalized back to the raw input space. + 2. The integers are rounded. 3. All values are normalized to the unit + cube. + + In train() mode, the inputs can either (a) be normalized to the unit + cube or (b) provided using their raw values. In the case of (a) + transform_on_train should be set to True, so that the normalized inputs + are unnormalized before rounding. In the case of (b) transform_on_train + should be set to False, so that the raw inputs are rounded and then + normalized to the unit cube. + + By default, the straight through estimators are used for the gradients as + proposed in [Daulton2022bopr]_. This transformation supports differentiable + approximate rounding (currently only for integers). The rounding function + is approximated with a piece-wise function where each piece is a hyperbolic + tangent function. + + For categorical parameters, the input must be one-hot encoded. + + Example: + >>> bounds = torch.tensor([[0, 5], [0, 1], [0, 1]]).t() + >>> integer_indices = [0] + >>> categorical_features = {1: 2} + >>> unnormalize_tf = Normalize( + >>> d=d, + >>> bounds=bounds, + >>> transform_on_eval=True, + >>> transform_on_train=True, + >>> reverse=True, + >>> ) + >>> round_tf = Round(integer_indices, categorical_features) + >>> normalize_tf = Normalize(d=d, bounds=bounds) + >>> tf = ChainedInputTransform( + >>> tf1=unnormalize_tf, tf2=round_tf, tf3=normalize_tf + >>> ) + """ + + def __init__( + self, + integer_indices: list[int] | LongTensor | None = None, + categorical_features: dict[int, int] | None = None, + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + approximate: bool = False, + tau: float = 1e-3, + ) -> None: + r"""Initialize transform. + + Args: + integer_indices: The indices of the integer inputs. + categorical_features: A dictionary mapping the starting index of each + categorical feature to its cardinality. This assumes that categoricals + are one-hot encoded. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: True. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: True. + approximate: A boolean indicating whether approximate or exact + rounding should be used. Default: False. + tau: The temperature parameter for approximate rounding. + """ + if approximate and categorical_features is not None: + raise NotImplementedError + super().__init__() + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + integer_indices = integer_indices if integer_indices is not None else [] + self.register_buffer( + "integer_indices", torch.as_tensor(integer_indices, dtype=torch.long) + ) + self.categorical_features = categorical_features or {} + self.approximate = approximate + self.tau = tau + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Discretize the inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of discretized inputs. + """ + X_rounded = X.clone() + # round integers + X_int = X_rounded[..., self.integer_indices] + if self.approximate: + X_int = approximate_round(X_int, tau=self.tau) + else: + X_int = RoundSTE.apply(X_int) + X_rounded[..., self.integer_indices] = X_int + # discrete categoricals to the category with the largest value + # in the continuous relaxation of the one-hot encoding + for start, card in self.categorical_features.items(): + end = start + card + X_rounded[..., start:end] = OneHotArgmaxSTE.apply(X[..., start:end]) + return X_rounded
+ + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Args: + other: Another input transform. + + Returns: + A boolean indicating if the other transform is equivalent. + """ + return ( + super().equals(other=other) + and (self.integer_indices == other.integer_indices).all() + and self.categorical_features == other.categorical_features + and self.approximate == other.approximate + and self.tau == other.tau + )
+ + +
+[docs] + def get_init_args(self) -> dict[str, Any]: + r"""Get the arguments necessary to construct an exact copy of the transform.""" + return { + "integer_indices": self.integer_indices, + "categorical_features": self.categorical_features, + "transform_on_train": self.transform_on_train, + "transform_on_eval": self.transform_on_eval, + "transform_on_fantasize": self.transform_on_fantasize, + "approximate": self.approximate, + "tau": self.tau, + }
+
+ + + +
+[docs] +class Log10(ReversibleInputTransform): + r"""A base-10 log transformation.""" + + def __init__( + self, + indices: list[int], + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + reverse: bool = False, + ) -> None: + r"""Initialize transform. + + Args: + indices: The indices of the inputs to log transform. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: True. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: True. + reverse: A boolean indicating whether the forward pass should untransform + the inputs. + """ + super().__init__() + self.register_buffer("indices", torch.tensor(indices, dtype=torch.long)) + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + self.reverse = reverse + + @subset_transform + def _transform(self, X: Tensor) -> Tensor: + r"""Log transform the inputs. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of transformed inputs. + """ + return X.log10() + + @subset_transform + def _untransform(self, X: Tensor) -> Tensor: + r"""Reverse the log transformation. + + Args: + X: A `batch_shape x n x d`-dim tensor of normalized inputs. + + Returns: + A `batch_shape x n x d`-dim tensor of un-normalized inputs. + """ + return 10.0**X
+ + + +
+[docs] +class Warp(ReversibleInputTransform, GPyTorchModule): + r"""A transform that uses learned input warping functions. + + Each specified input dimension is warped using the CDF of a + Kumaraswamy distribution. Typically, MAP estimates of the + parameters of the Kumaraswamy distribution, for each input + dimension, are learned jointly with the GP hyperparameters. + + TODO: implement support using independent warping functions + for each output in batched multi-output and multi-task models. + + For now, ModelListGPs should be used to learn independent warping + functions for each output. + """ + + # TODO: make minimum value dtype-dependent + _min_concentration_level = 1e-4 + + def __init__( + self, + indices: list[int], + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + reverse: bool = False, + eps: float = 1e-7, + concentration1_prior: Prior | None = None, + concentration0_prior: Prior | None = None, + batch_shape: torch.Size | None = None, + ) -> None: + r"""Initialize transform. + + Args: + indices: The indices of the inputs to warp. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: True. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: True. + reverse: A boolean indicating whether the forward pass should untransform + the inputs. + eps: A small value used to clip values to be in the interval (0, 1). + concentration1_prior: A prior distribution on the concentration1 parameter + of the Kumaraswamy distribution. + concentration0_prior: A prior distribution on the concentration0 parameter + of the Kumaraswamy distribution. + batch_shape: An optional batch shape, for learning independent warping + parameters for each batch of inputs. This should match the input batch + shape of the model (i.e., `train_X.shape[:-2]`). + NOTE: This is only supported for single-output models. + """ + super().__init__() + self.register_buffer("indices", torch.tensor(indices, dtype=torch.long)) + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + self.reverse = reverse + self.batch_shape = batch_shape or torch.Size([]) + self._X_min = eps + self._X_range = 1 - 2 * eps + if len(self.batch_shape) > 0: + # Note: this follows the gpytorch shape convention for lengthscales + # There is ongoing discussion about the extra `1`. + # TODO: update to follow new gpytorch convention resulting from + # https://github.com/cornellius-gp/gpytorch/issues/1317 + batch_shape = self.batch_shape + torch.Size([1]) + else: + batch_shape = self.batch_shape + for i in (0, 1): + p_name = f"concentration{i}" + self.register_parameter( + p_name, + nn.Parameter(torch.full(batch_shape + self.indices.shape, 1.0)), + ) + if concentration0_prior is not None: + self.register_prior( + "concentration0_prior", + concentration0_prior, + lambda m: m.concentration0, + lambda m, v: m._set_concentration(i=0, value=v), + ) + if concentration1_prior is not None: + self.register_prior( + "concentration1_prior", + concentration1_prior, + lambda m: m.concentration1, + lambda m, v: m._set_concentration(i=1, value=v), + ) + for i in (0, 1): + p_name = f"concentration{i}" + constraint = GreaterThan( + self._min_concentration_level, + transform=None, + # set the initial value to be the identity transformation + initial_value=1.0, + ) + self.register_constraint(param_name=p_name, constraint=constraint) + + def _set_concentration(self, i: int, value: float | Tensor) -> None: + if not torch.is_tensor(value): + value = torch.as_tensor(value).to(self.concentration0) + self.initialize(**{f"concentration{i}": value}) + + @subset_transform + def _transform(self, X: Tensor) -> Tensor: + r"""Warp the inputs through the Kumaraswamy CDF. + + Args: + X: A `input_batch_shape x (batch_shape) x n x d`-dim tensor of inputs. + batch_shape here can either be self.batch_shape or 1's such that + it is broadcastable with self.batch_shape if self.batch_shape is set. + + Returns: + A `input_batch_shape x (batch_shape) x n x d`-dim tensor of transformed + inputs. + """ + # normalize to [eps, 1-eps], IDEA: could use Normalize and ChainedTransform. + return self._k.cdf( + torch.clamp( + X * self._X_range + self._X_min, + self._X_min, + 1.0 - self._X_min, + ) + ) + + @subset_transform + def _untransform(self, X: Tensor) -> Tensor: + r"""Warp the inputs through the Kumaraswamy inverse CDF. + + Args: + X: A `input_batch_shape x batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `input_batch_shape x batch_shape x n x d`-dim tensor of transformed + inputs. + """ + if len(self.batch_shape) > 0: + if self.batch_shape != X.shape[-2 - len(self.batch_shape) : -2]: + raise BotorchTensorDimensionError( + "The right most batch dims of X must match self.batch_shape: " + f"({self.batch_shape})." + ) + # unnormalize from [eps, 1-eps] to [0,1] + return ((self._k.icdf(X) - self._X_min) / self._X_range).clamp(0.0, 1.0) + + @property + def _k(self) -> Kumaraswamy: + """Returns a Kumaraswamy distribution with the concentration parameters.""" + return Kumaraswamy( + concentration1=self.concentration1, + concentration0=self.concentration0, + )
+ + + +
+[docs] +class AppendFeatures(InputTransform): + r"""A transform that appends the input with a given set of features either + provided beforehand or generated on the fly via a callable. + + As an example, the predefined set of features can be used with + `RiskMeasureMCObjective` to optimize risk measures as described in + [Cakmak2020risk]_. A tutorial notebook implementing the rhoKG acqusition + function introduced in [Cakmak2020risk]_ can be found at + https://botorch.org/tutorials/risk_averse_bo_with_environmental_variables. + + The steps for using this to obtain samples of a risk measure are as follows: + + - Train a model on `(x, w)` inputs and the corresponding observations; + + - Pass in an instance of `AppendFeatures` with the `feature_set` denoting the + samples of `W` as the `input_transform` to the trained model; + + - Call `posterior(...).rsample(...)` on the model with `x` inputs only to + get the joint posterior samples over `(x, w)`s, where the `w`s come + from the `feature_set`; + + - Pass these posterior samples through the `RiskMeasureMCObjective` of choice to + get the samples of the risk measure. + + Note: The samples of the risk measure obtained this way are in general biased + since the `feature_set` does not fully represent the distribution of the + environmental variable. + + Possible examples for using a callable include statistical models that are built on + PyTorch, built-in mathematical operations such as torch.sum, or custom scripted + functions. By this, this input transform allows for advanced feature engineering + and transfer learning models within the optimization loop. + + Example: + >>> # We consider 1D `x` and 1D `w`, with `W` having a + >>> # uniform distribution over [0, 1] + >>> model = SingleTaskGP( + ... train_X=torch.rand(10, 2), + ... train_Y=torch.randn(10, 1), + ... input_transform=AppendFeatures(feature_set=torch.rand(10, 1)) + ... ) + >>> mll = ExactMarginalLogLikelihood(model.likelihood, model) + >>> fit_gpytorch_mll(mll) + >>> test_x = torch.rand(3, 1) + >>> # `posterior_samples` is a `10 x 30 x 1`-dim tensor + >>> posterior_samples = model.posterior(test_x).rsamples(torch.size([10])) + >>> risk_measure = VaR(alpha=0.8, n_w=10) + >>> # `risk_measure_samples` is a `10 x 3`-dim tensor of samples of the + >>> # risk measure VaR + >>> risk_measure_samples = risk_measure(posterior_samples) + """ + + is_one_to_many: bool = True + + def __init__( + self, + feature_set: Tensor | None = None, + f: Callable[[Tensor], Tensor] | None = None, + indices: list[int] | None = None, + fkwargs: dict[str, Any] | None = None, + skip_expand: bool = False, + transform_on_train: bool = False, + transform_on_eval: bool = True, + transform_on_fantasize: bool = False, + ) -> None: + r"""Append `feature_set` to each input or generate a set of features to + append on the fly via a callable. + + Args: + feature_set: An `n_f x d_f`-dim tensor denoting the features to be + appended to the inputs. Default: None. + f: A callable mapping a `batch_shape x q x d`-dim input tensor `X` + to a `batch_shape x q x n_f x d_f`-dimensional output tensor. + Default: None. + indices: List of indices denoting the indices of the features to be + passed into f. Per default all features are passed to `f`. + Default: None. + fkwargs: Dictionary of keyword arguments passed to the callable `f`. + Default: None. + skip_expand: A boolean indicating whether to expand the input tensor + before appending features. This is intended for use with an + `InputPerturbation`. If `True`, the input tensor will be expected + to be of shape `batch_shape x (q * n_f) x d`. Not implemented + in combination with a callable. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: False. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: False. + """ + super().__init__() + if (feature_set is None) and (f is None): + raise ValueError( + "Either a `feature_set` or a callable `f` has to be provided." + ) + if (feature_set is not None) and (f is not None): + raise ValueError( + "Only one can be used: either `feature_set` or callable `f`." + ) + if feature_set is not None: + if feature_set.dim() != 2: + raise ValueError("`feature_set` must be an `n_f x d_f`-dim tensor!") + self.register_buffer("feature_set", feature_set) + self._f = None + if f is not None: + if skip_expand: + raise ValueError( + "`skip_expand` option is not supported in case of using a callable" + ) + if (indices is not None) and (len(indices) == 0): + raise ValueError("`indices` list is empty!") + if indices is not None: + indices = torch.tensor(indices, dtype=torch.long) + if len(indices.unique()) != len(indices): + raise ValueError("Elements of `indices` tensor must be unique!") + self.indices = indices + else: + self.indices = slice(None) + self._f = f + self.fkwargs = fkwargs or {} + + self.skip_expand = skip_expand + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Transform the inputs by appending `feature_set` to each input or + by generating a set of features to be appended on the fly via a callable. + + For each `1 x d`-dim element in the input tensor, this will produce + an `n_f x (d + d_f)`-dim tensor with `feature_set` appended as the last `d_f` + dimensions. For a generic `batch_shape x q x d`-dim `X`, this translates to a + `batch_shape x (q * n_f) x (d + d_f)`-dim output, where the values corresponding + to `X[..., i, :]` are found in `output[..., i * n_f: (i + 1) * n_f, :]`. + + Note: Adding the `feature_set` on the `q-batch` dimension is necessary to avoid + introducing additional bias by evaluating the inputs on independent GP + sample paths. + + Args: + X: A `batch_shape x q x d`-dim tensor of inputs. If `self.skip_expand` is + `True`, then `X` should be of shape `batch_shape x (q * n_f) x d`, + typically obtained by passing a `batch_shape x q x d` shape input + through an `InputPerturbation` with `n_f` perturbation values. + + Returns: + A `batch_shape x (q * n_f) x (d + d_f)`-dim tensor of appended inputs. + """ + if self._f is not None: + expanded_features = self._f(X[..., self.indices], **self.fkwargs) + n_f = expanded_features.shape[-2] + else: + n_f = self.feature_set.shape[-2] + + if self.skip_expand: + expanded_X = X.view(*X.shape[:-2], -1, n_f, X.shape[-1]) + else: + expanded_X = X.unsqueeze(dim=-2).expand(*X.shape[:-1], n_f, -1) + + if self._f is None: + expanded_features = self.feature_set.expand(*expanded_X.shape[:-1], -1) + + appended_X = torch.cat([expanded_X, expanded_features], dim=-1) + return appended_X.view(*X.shape[:-2], -1, appended_X.shape[-1])
+
+ + + +
+[docs] +class InteractionFeatures(AppendFeatures): + r"""A transform that appends the first-order interaction terms $x_i * x_j, i < j$, + for all or a subset of the input variables.""" + + def __init__( + self, + indices: list[int] | None = None, + ) -> None: + r"""Initializes the InteractionFeatures transform. + + Args: + indices: Indices of the subset of dimensions to compute interaction + features on. + """ + + super().__init__( + f=interaction_features, + indices=indices, + transform_on_train=True, + transform_on_eval=True, + transform_on_fantasize=True, + )
+ + + +
+[docs] +class FilterFeatures(InputTransform): + r"""A transform that filters the input with a given set of features indices. + + As an example, this can be used in a multiobjective optimization with `ModelListGP` + in which the specific models only share subsets of features (feature selection). + A reason could be that it is known that specific features do not have any impact on + a specific objective but they need to be included in the model for another one. + """ + + def __init__( + self, + feature_indices: Tensor, + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + ) -> None: + r"""Filter features from a model. + + Args: + feature_set: An one-dim tensor denoting the indices of the features to be + kept and fed to the model. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: True. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: True. + """ + super().__init__() + if feature_indices.dim() != 1: + raise ValueError("`feature_indices` must be a one-dimensional tensor!") + if feature_indices.dtype != torch.int64: + raise ValueError("`feature_indices` tensor must be int64/long!") + if (feature_indices < 0).any(): + raise ValueError( + "Elements of `feature_indices` have to be larger/equal to zero!" + ) + if len(feature_indices.unique()) != len(feature_indices): + raise ValueError("Elements of `feature_indices` tensor must be unique!") + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + self.register_buffer("feature_indices", feature_indices) + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Transform the inputs by keeping only the in `feature_indices` specified + feature indices and filtering out the others. + + Args: + X: A `batch_shape x q x d`-dim tensor of inputs. + + Returns: + A `batch_shape x q x e`-dim tensor of filtered inputs, + where `e` is the length of `feature_indices`. + """ + return X[..., self.feature_indices]
+ + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Args: + other: Another input transform + + Returns: + A boolean indicating if the other transform is equivalent. + """ + if len(self.feature_indices) != len(other.feature_indices): + return False + return super().equals(other=other)
+
+ + + +
+[docs] +class InputPerturbation(InputTransform): + r"""A transform that adds the set of perturbations to the given input. + + Similar to `AppendFeatures`, this can be used with `RiskMeasureMCObjective` + to optimize risk measures. See `AppendFeatures` for additional discussion + on optimizing risk measures. + + A tutorial notebook using this with `qNoisyExpectedImprovement` can be found at + https://botorch.org/tutorials/risk_averse_bo_with_input_perturbations. + """ + + is_one_to_many: bool = True + + def __init__( + self, + perturbation_set: Tensor | Callable[[Tensor], Tensor], + bounds: Tensor | None = None, + indices: list[int] | None = None, + multiplicative: bool = False, + transform_on_train: bool = False, + transform_on_eval: bool = True, + transform_on_fantasize: bool = False, + ) -> None: + r"""Add `perturbation_set` to each input. + + Args: + perturbation_set: An `n_p x d`-dim tensor denoting the perturbations + to be added to the inputs. Alternatively, this can be a callable that + returns `batch x n_p x d`-dim tensor of perturbations for input of + shape `batch x d`. This is useful for heteroscedastic perturbations. + bounds: A `2 x d`-dim tensor of lower and upper bounds for each + column of the input. If given, the perturbed inputs will be + clamped to these bounds. + indices: A list of indices specifying a subset of inputs on which to apply + the transform. Note that `len(indices)` should be equal to the second + dimension of `perturbation_set` and `bounds`. The dimensionality of + the input `X.shape[-1]` can be larger if we only transform a subset. + multiplicative: A boolean indicating whether the input perturbations + are additive or multiplicative. If True, inputs will be multiplied + with the perturbations. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: False. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: False. + """ + super().__init__() + if isinstance(perturbation_set, Tensor): + if perturbation_set.dim() != 2: + raise ValueError("`perturbation_set` must be an `n_p x d`-dim tensor!") + self.register_buffer("perturbation_set", perturbation_set) + else: + self.perturbation_set = perturbation_set + if bounds is not None: + if ( + isinstance(perturbation_set, Tensor) + and bounds.shape[-1] != perturbation_set.shape[-1] + ): + raise ValueError( + "`bounds` must have the same number of columns (last dimension) as " + f"the `perturbation_set`! Got {bounds.shape[-1]} and " + f"{perturbation_set.shape[-1]}." + ) + self.register_buffer("bounds", bounds) + else: + self.bounds = None + self.register_buffer("_perturbations", None) + self.indices = indices + self.multiplicative = multiplicative + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Transform the inputs by adding `perturbation_set` to each input. + + For each `1 x d`-dim element in the input tensor, this will produce + an `n_p x d`-dim tensor with the `perturbation_set` added to the input. + For a generic `batch_shape x q x d`-dim `X`, this translates to a + `batch_shape x (q * n_p) x d`-dim output, where the values corresponding + to `X[..., i, :]` are found in `output[..., i * n_w: (i + 1) * n_w, :]`. + + Note: Adding the `perturbation_set` on the `q-batch` dimension is necessary + to avoid introducing additional bias by evaluating the inputs on independent + GP sample paths. + + Args: + X: A `batch_shape x q x d`-dim tensor of inputs. + + Returns: + A `batch_shape x (q * n_p) x d`-dim tensor of perturbed inputs. + """ + # NOTE: If we had access to n_p without evaluating _perturbations when the + # perturbation_set is a function, we could move this into `_transform`. + # Further, we could remove the two `transpose` calls below if one were + # willing to accept a different ordering of the transformed output. + self._perturbations = self._expanded_perturbations(X) + # make space for n_p dimension, switch n_p with n after transform, and flatten. + return self._transform(X.unsqueeze(-3)).transpose(-3, -2).flatten(-3, -2)
+ + + @subset_transform + def _transform(self, X: Tensor): + p = self._perturbations + Y = X * p if self.multiplicative else X + p + if self.bounds is not None: + return torch.maximum(torch.minimum(Y, self.bounds[1]), self.bounds[0]) + return Y + + @property + def batch_shape(self): + """Returns a shape tuple such that `subset_transform` pre-allocates + a (b x n_p x n x d) - dim tensor, where `b` is the batch shape of the + input `X` of the transform and `n_p` is the number of perturbations. + NOTE: this function is dependent on calling `_expanded_perturbations(X)` + because `n_p` is inaccessible otherwise if `perturbation_set` is a function. + """ + return self._perturbations.shape[:-2] + + def _expanded_perturbations(self, X: Tensor) -> Tensor: + p = self.perturbation_set + if isinstance(p, Tensor): + p = p.expand(X.shape[-2], *p.shape) # p is batch_shape x n x n_p x d + else: + p = p(X) if self.indices is None else p(X[..., self.indices]) + return p.transpose(-3, -2) # p is batch_shape x n_p x n x d
+ + + +
+[docs] +class OneHotToNumeric(InputTransform): + r"""Transform categorical parameters from a one-hot to a numeric representation.""" + + def __init__( + self, + dim: int, + categorical_features: dict[int, int] | None = None, + transform_on_train: bool = True, + transform_on_eval: bool = True, + transform_on_fantasize: bool = True, + ) -> None: + r"""Initialize. + + Args: + dim: The dimension of the one-hot-encoded input. + categorical_features: A dictionary mapping the starting index of each + categorical feature to its cardinality. This assumes that categoricals + are one-hot encoded. + transform_on_train: A boolean indicating whether to apply the + transforms in train() mode. Default: False. + transform_on_eval: A boolean indicating whether to apply the + transform in eval() mode. Default: True. + transform_on_fantasize: A boolean indicating whether to apply the + transform when called from within a `fantasize` call. Default: False. + + Returns: + A `batch_shape x n x d'`-dim tensor of where the one-hot encoded + categoricals are transformed to integer representation. + """ + super().__init__() + self.transform_on_train = transform_on_train + self.transform_on_eval = transform_on_eval + self.transform_on_fantasize = transform_on_fantasize + categorical_features = categorical_features or {} + # sort by starting index + self.categorical_features = OrderedDict( + sorted(categorical_features.items(), key=lambda x: x[0]) + ) + if len(self.categorical_features) > 0: + self.onehot_idx = [ + np.arange(start, start + card) + for start, card in self.categorical_features.items() + ] + idx = np.concatenate(self.onehot_idx) + + if len(idx) != len(set(idx)): + raise ValueError("Categorical features overlap.") + if max(idx) >= dim: + raise ValueError("Categorical features exceed the provided dimension.") + self.numerical_idx = list(set(range(dim)) - set(idx)) + + offset = 0 + self.ordinal_idx = [] + for start, card in self.categorical_features.items(): + self.ordinal_idx.append(start - offset) + offset += card - 1 + + reduced_dim = len(self.ordinal_idx) + len(self.numerical_idx) + self.new_numerical_idx = list( + set(range(reduced_dim)) - set(self.ordinal_idx) + ) + + self.numeric_dim = len(self.new_numerical_idx) + len( + self.categorical_features + ) + +
+[docs] + def transform(self, X: Tensor) -> Tensor: + r"""Transform the categorical inputs into integer representation. + + Args: + X: A `batch_shape x n x d`-dim tensor of inputs. + + Returns: + A `batch_shape x n x d'`-dim tensor of where the one-hot encoded + categoricals are transformed to integer representation. + """ + if len(self.categorical_features) > 0: + X_numeric = X[..., : self.numeric_dim].clone() + # copy the numerical dims over + X_numeric[..., self.new_numerical_idx] = X[..., self.numerical_idx] + for i in range(len(self.categorical_features)): + X_numeric[..., self.ordinal_idx[i]] = X[..., self.onehot_idx[i]].argmax( + dim=-1 + ) + return X_numeric + return X
+ + +
+[docs] + def untransform(self, X: Tensor) -> Tensor: + r"""Transform the categoricals from integer representation to one-hot. + + Args: + X: A `batch_shape x n x d'`-dim tensor of transformed inputs, where + the categoricals are represented as integers. + + Returns: + A `batch_shape x n x d`-dim tensor of inputs, where the categoricals + have been transformed to one-hot representation. + """ + if len(self.categorical_features) > 0: + s = list(X.shape) + s[-1] = len(self.numerical_idx) + len(np.concatenate(self.onehot_idx)) + X_onehot = torch.zeros(size=s).to(X) + X_onehot[..., self.numerical_idx] = X[..., self.new_numerical_idx] + for i in range(len(self.categorical_features)): + X_onehot[..., self.onehot_idx[i]] = one_hot( + X[..., self.ordinal_idx[i]].long(), + num_classes=len(self.onehot_idx[i]), + ).to(X_onehot) + return X_onehot + return X
+ + +
+[docs] + def equals(self, other: InputTransform) -> bool: + r"""Check if another input transform is equivalent. + + Args: + other: Another input transform. + + Returns: + A boolean indicating if the other transform is equivalent. + """ + return ( + type(self) is type(other) + and (self.transform_on_train == other.transform_on_train) + and (self.transform_on_eval == other.transform_on_eval) + and (self.transform_on_fantasize == other.transform_on_fantasize) + and self.categorical_features == other.categorical_features + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/transforms/outcome.html b/website-old/pages/api/_modules/botorch/models/transforms/outcome.html new file mode 100644 index 0000000000..40f440198d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/transforms/outcome.html @@ -0,0 +1,973 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.transforms.outcome

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Outcome transformations for automatically transforming and un-transforming
+model outputs. Outcome transformations are typically part of a Model and
+applied (i) within the model constructor to transform the train observations
+to the model space, and (ii) in the `Model.posterior` call to untransform
+the model posterior back to the original space.
+
+References
+
+.. [eriksson2021scalable]
+    D. Eriksson, M. Poloczek. Scalable Constrained Bayesian Optimization.
+    International Conference on Artificial Intelligence and Statistics. PMLR, 2021,
+    http://proceedings.mlr.press/v130/eriksson21a.html
+
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections import OrderedDict
+
+import torch
+from botorch.models.transforms.utils import (
+    norm_to_lognorm_mean,
+    norm_to_lognorm_variance,
+)
+from botorch.posteriors import GPyTorchPosterior, Posterior, TransformedPosterior
+from botorch.utils.transforms import normalize_indices
+from linear_operator.operators import CholLinearOperator, DiagLinearOperator
+from torch import Tensor
+from torch.nn import Module, ModuleDict
+
+
+
+[docs] +class OutcomeTransform(Module, ABC): + """Abstract base class for outcome transforms.""" + +
+[docs] + @abstractmethod + def forward( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Transform the outcomes in a model's training targets + + Args: + Y: A `batch_shape x n x m`-dim tensor of training targets. + Yvar: A `batch_shape x n x m`-dim tensor of observation noises + associated with the training targets (if applicable). + + Returns: + A two-tuple with the transformed outcomes: + + - The transformed outcome observations. + - The transformed observation noise (if applicable). + """ + pass # pragma: no cover
+ + +
+[docs] + def subset_output(self, idcs: list[int]) -> OutcomeTransform: + r"""Subset the transform along the output dimension. + + This functionality is used to properly treat outcome transformations + in the `subset_model` functionality. + + Args: + idcs: The output indices to subset the transform to. + + Returns: + The current outcome transform, subset to the specified output indices. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement the " + "`subset_output` method" + )
+ + +
+[docs] + def untransform( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Un-transform previously transformed outcomes + + Args: + Y: A `batch_shape x n x m`-dim tensor of transfomred training targets. + Yvar: A `batch_shape x n x m`-dim tensor of transformed observation + noises associated with the training targets (if applicable). + + Returns: + A two-tuple with the un-transformed outcomes: + + - The un-transformed outcome observations. + - The un-transformed observation noise (if applicable). + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement the `untransform` method" + )
+ + + @property + def _is_linear(self) -> bool: + """ + True for transformations such as `Standardize`; these should be able to apply + `untransform_posterior` to a GPyTorchPosterior and return a GPyTorchPosterior, + because a multivariate normal distribution should remain multivariate normal + after applying the transform. + """ + return False + +
+[docs] + def untransform_posterior(self, posterior: Posterior) -> Posterior: + r"""Un-transform a posterior. + + Posteriors with `_is_linear=True` should return a `GPyTorchPosterior` when + `posterior` is a `GPyTorchPosterior`. Posteriors with `_is_linear=False` + likely return a `TransformedPosterior` instead. + + Args: + posterior: A posterior in the transformed space. + + Returns: + The un-transformed posterior. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement the " + "`untransform_posterior` method" + )
+
+ + + +
+[docs] +class ChainedOutcomeTransform(OutcomeTransform, ModuleDict): + r"""An outcome transform representing the chaining of individual transforms""" + + def __init__(self, **transforms: OutcomeTransform) -> None: + r"""Chaining of outcome transforms. + + Args: + transforms: The transforms to chain. Internally, the names of the + kwargs are used as the keys for accessing the individual + transforms on the module. + """ + super().__init__(OrderedDict(transforms)) + +
+[docs] + def forward( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Transform the outcomes in a model's training targets + + Args: + Y: A `batch_shape x n x m`-dim tensor of training targets. + Yvar: A `batch_shape x n x m`-dim tensor of observation noises + associated with the training targets (if applicable). + + Returns: + A two-tuple with the transformed outcomes: + + - The transformed outcome observations. + - The transformed observation noise (if applicable). + """ + for tf in self.values(): + Y, Yvar = tf.forward(Y, Yvar) + return Y, Yvar
+ + +
+[docs] + def subset_output(self, idcs: list[int]) -> OutcomeTransform: + r"""Subset the transform along the output dimension. + + Args: + idcs: The output indices to subset the transform to. + + Returns: + The current outcome transform, subset to the specified output indices. + """ + return self.__class__( + **{name: tf.subset_output(idcs=idcs) for name, tf in self.items()} + )
+ + +
+[docs] + def untransform( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Un-transform previously transformed outcomes + + Args: + Y: A `batch_shape x n x m`-dim tensor of transfomred training targets. + Yvar: A `batch_shape x n x m`-dim tensor of transformed observation + noises associated with the training targets (if applicable). + + Returns: + A two-tuple with the un-transformed outcomes: + + - The un-transformed outcome observations. + - The un-transformed observation noise (if applicable). + """ + for tf in reversed(self.values()): + Y, Yvar = tf.untransform(Y, Yvar) + return Y, Yvar
+ + + @property + def _is_linear(self) -> bool: + """ + A `ChainedOutcomeTransform` is linear only if all of the component transforms + are linear. + """ + return all(octf._is_linear for octf in self.values()) + +
+[docs] + def untransform_posterior(self, posterior: Posterior) -> Posterior: + r"""Un-transform a posterior + + Args: + posterior: A posterior in the transformed space. + + Returns: + The un-transformed posterior. + """ + for tf in reversed(self.values()): + posterior = tf.untransform_posterior(posterior) + return posterior
+
+ + + +
+[docs] +class Standardize(OutcomeTransform): + r"""Standardize outcomes (zero mean, unit variance). + + This module is stateful: If in train mode, calling forward updates the + module state (i.e. the mean/std normalizing constants). If in eval mode, + calling forward simply applies the standardization using the current module + state. + """ + + def __init__( + self, + m: int, + outputs: list[int] | None = None, + batch_shape: torch.Size = torch.Size(), # noqa: B008 + min_stdv: float = 1e-8, + ) -> None: + r"""Standardize outcomes (zero mean, unit variance). + + Args: + m: The output dimension. + outputs: Which of the outputs to standardize. If omitted, all + outputs will be standardized. + batch_shape: The batch_shape of the training targets. + min_stddv: The minimum standard deviation for which to perform + standardization (if lower, only de-mean the data). + """ + super().__init__() + self.register_buffer("means", torch.zeros(*batch_shape, 1, m)) + self.register_buffer("stdvs", torch.ones(*batch_shape, 1, m)) + self.register_buffer("_stdvs_sq", torch.ones(*batch_shape, 1, m)) + self.register_buffer("_is_trained", torch.tensor(False)) + self._outputs = normalize_indices(outputs, d=m) + self._m = m + self._batch_shape = batch_shape + self._min_stdv = min_stdv + +
+[docs] + def forward( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Standardize outcomes. + + If the module is in train mode, this updates the module state (i.e. the + mean/std normalizing constants). If the module is in eval mode, simply + applies the normalization using the module state. + + Args: + Y: A `batch_shape x n x m`-dim tensor of training targets. + Yvar: A `batch_shape x n x m`-dim tensor of observation noises + associated with the training targets (if applicable). + + Returns: + A two-tuple with the transformed outcomes: + + - The transformed outcome observations. + - The transformed observation noise (if applicable). + """ + if self.training: + if Y.shape[:-2] != self._batch_shape: + raise RuntimeError( + f"Expected Y.shape[:-2] to be {self._batch_shape}, matching " + "the `batch_shape` argument to `Standardize`, but got " + f"Y.shape[:-2]={Y.shape[:-2]}." + ) + if Y.size(-1) != self._m: + raise RuntimeError( + f"Wrong output dimension. Y.size(-1) is {Y.size(-1)}; expected " + f"{self._m}." + ) + if Y.shape[-2] < 1: + raise ValueError(f"Can't standardize with no observations. {Y.shape=}.") + + elif Y.shape[-2] == 1: + stdvs = torch.ones( + (*Y.shape[:-2], 1, Y.shape[-1]), dtype=Y.dtype, device=Y.device + ) + else: + stdvs = Y.std(dim=-2, keepdim=True) + stdvs = stdvs.where(stdvs >= self._min_stdv, torch.full_like(stdvs, 1.0)) + means = Y.mean(dim=-2, keepdim=True) + if self._outputs is not None: + unused = [i for i in range(self._m) if i not in self._outputs] + means[..., unused] = 0.0 + stdvs[..., unused] = 1.0 + self.means = means + self.stdvs = stdvs + self._stdvs_sq = stdvs.pow(2) + self._is_trained = torch.tensor(True) + + Y_tf = (Y - self.means) / self.stdvs + Yvar_tf = Yvar / self._stdvs_sq if Yvar is not None else None + return Y_tf, Yvar_tf
+ + +
+[docs] + def subset_output(self, idcs: list[int]) -> OutcomeTransform: + r"""Subset the transform along the output dimension. + + Args: + idcs: The output indices to subset the transform to. + + Returns: + The current outcome transform, subset to the specified output indices. + """ + new_m = len(idcs) + if new_m > self._m: + raise RuntimeError( + "Trying to subset a transform have more outputs than " + " the original transform." + ) + nlzd_idcs = normalize_indices(idcs, d=self._m) + new_outputs = None + if self._outputs is not None: + new_outputs = [i for i in self._outputs if i in nlzd_idcs] + new_tf = self.__class__( + m=new_m, + outputs=new_outputs, + batch_shape=self._batch_shape, + min_stdv=self._min_stdv, + ) + new_tf.means = self.means[..., nlzd_idcs] + new_tf.stdvs = self.stdvs[..., nlzd_idcs] + new_tf._stdvs_sq = self._stdvs_sq[..., nlzd_idcs] + new_tf._is_trained = self._is_trained + if not self.training: + new_tf.eval() + return new_tf
+ + +
+[docs] + def untransform( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Un-standardize outcomes. + + Args: + Y: A `batch_shape x n x m`-dim tensor of standardized targets. + Yvar: A `batch_shape x n x m`-dim tensor of standardized observation + noises associated with the targets (if applicable). + + Returns: + A two-tuple with the un-standardized outcomes: + + - The un-standardized outcome observations. + - The un-standardized observation noise (if applicable). + """ + if not self._is_trained: + raise RuntimeError( + "`Standardize` transforms must be called on outcome data " + "(e.g. `transform(Y)`) before calling `untransform`, since " + "means and standard deviations need to be computed." + ) + + Y_utf = self.means + self.stdvs * Y + Yvar_utf = self._stdvs_sq * Yvar if Yvar is not None else None + return Y_utf, Yvar_utf
+ + + @property + def _is_linear(self) -> bool: + return True + +
+[docs] + def untransform_posterior( + self, posterior: Posterior + ) -> GPyTorchPosterior | TransformedPosterior: + r"""Un-standardize the posterior. + + Args: + posterior: A posterior in the standardized space. + + Returns: + The un-standardized posterior. If the input posterior is a + `GPyTorchPosterior`, return a `GPyTorchPosterior`. Otherwise, return a + `TransformedPosterior`. + """ + if self._outputs is not None: + raise NotImplementedError( + "Standardize does not yet support output selection for " + "untransform_posterior" + ) + if not self._is_trained: + raise RuntimeError( + "`Standardize` transforms must be called on outcome data " + "(e.g. `transform(Y)`) before calling `untransform_posterior`, since " + "means and standard deviations need to be computed." + ) + is_mtgp_posterior = False + if type(posterior) is GPyTorchPosterior: + is_mtgp_posterior = posterior._is_mt + if not self._m == posterior._extended_shape()[-1] and not is_mtgp_posterior: + raise RuntimeError( + "Incompatible output dimensions encountered. Transform has output " + f"dimension {self._m} and posterior has " + f"{posterior._extended_shape()[-1]}." + ) + + if type(posterior) is not GPyTorchPosterior: + # fall back to TransformedPosterior + # this applies to subclasses of GPyTorchPosterior like MultitaskGPPosterior + return TransformedPosterior( + posterior=posterior, + sample_transform=lambda s: self.means + self.stdvs * s, + mean_transform=lambda m, v: self.means + self.stdvs * m, + variance_transform=lambda m, v: self._stdvs_sq * v, + ) + # GPyTorchPosterior (TODO: Should we Lazy-evaluate the mean here as well?) + mvn = posterior.distribution + offset = self.means + scale_fac = self.stdvs + if not posterior._is_mt: + mean_tf = offset.squeeze(-1) + scale_fac.squeeze(-1) * mvn.mean + scale_fac = scale_fac.squeeze(-1).expand_as(mean_tf) + else: + mean_tf = offset + scale_fac * mvn.mean + reps = mean_tf.shape[-2:].numel() // scale_fac.size(-1) + scale_fac = scale_fac.squeeze(-2) + if mvn._interleaved: + scale_fac = scale_fac.repeat(*[1 for _ in scale_fac.shape[:-1]], reps) + else: + scale_fac = torch.repeat_interleave(scale_fac, reps, dim=-1) + + if ( + not mvn.islazy + # TODO: Figure out attribute namming weirdness here + or mvn._MultivariateNormal__unbroadcasted_scale_tril is not None + ): + # if already computed, we can save a lot of time using scale_tril + covar_tf = CholLinearOperator(mvn.scale_tril * scale_fac.unsqueeze(-1)) + else: + lcv = mvn.lazy_covariance_matrix + scale_fac = scale_fac.expand(lcv.shape[:-1]) + scale_mat = DiagLinearOperator(scale_fac) + covar_tf = scale_mat @ lcv @ scale_mat + + kwargs = {"interleaved": mvn._interleaved} if posterior._is_mt else {} + mvn_tf = mvn.__class__(mean=mean_tf, covariance_matrix=covar_tf, **kwargs) + return GPyTorchPosterior(mvn_tf)
+
+ + + +
+[docs] +class Log(OutcomeTransform): + r"""Log-transform outcomes. + + Useful if the targets are modeled using a (multivariate) log-Normal + distribution. This means that we can use a standard GP model on the + log-transformed outcomes and un-transform the model posterior of that GP. + """ + + def __init__(self, outputs: list[int] | None = None) -> None: + r"""Log-transform outcomes. + + Args: + outputs: Which of the outputs to log-transform. If omitted, all + outputs will be standardized. + """ + super().__init__() + self._outputs = outputs + +
+[docs] + def subset_output(self, idcs: list[int]) -> OutcomeTransform: + r"""Subset the transform along the output dimension. + + Args: + idcs: The output indices to subset the transform to. + + Returns: + The current outcome transform, subset to the specified output indices. + """ + new_outputs = None + if self._outputs is not None: + if min(self._outputs + idcs) < 0: + raise NotImplementedError( + f"Negative indexing not supported for {self.__class__.__name__} " + "when subsetting outputs and only transforming some outputs." + ) + new_outputs = [i for i in self._outputs if i in idcs] + new_tf = self.__class__(outputs=new_outputs) + if not self.training: + new_tf.eval() + return new_tf
+ + +
+[docs] + def forward( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Log-transform outcomes. + + Args: + Y: A `batch_shape x n x m`-dim tensor of training targets. + Yvar: A `batch_shape x n x m`-dim tensor of observation noises + associated with the training targets (if applicable). + + Returns: + A two-tuple with the transformed outcomes: + + - The transformed outcome observations. + - The transformed observation noise (if applicable). + """ + Y_tf = torch.log(Y) + outputs = normalize_indices(self._outputs, d=Y.size(-1)) + if outputs is not None: + Y_tf = torch.stack( + [ + Y_tf[..., i] if i in outputs else Y[..., i] + for i in range(Y.size(-1)) + ], + dim=-1, + ) + if Yvar is not None: + # TODO: Delta method, possibly issue warning + raise NotImplementedError( + "Log does not yet support transforming observation noise" + ) + return Y_tf, Yvar
+ + +
+[docs] + def untransform( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Un-transform log-transformed outcomes + + Args: + Y: A `batch_shape x n x m`-dim tensor of log-transfomred targets. + Yvar: A `batch_shape x n x m`-dim tensor of log- transformed + observation noises associated with the training targets + (if applicable). + + Returns: + A two-tuple with the un-transformed outcomes: + + - The exponentiated outcome observations. + - The exponentiated observation noise (if applicable). + """ + Y_utf = torch.exp(Y) + outputs = normalize_indices(self._outputs, d=Y.size(-1)) + if outputs is not None: + Y_utf = torch.stack( + [ + Y_utf[..., i] if i in outputs else Y[..., i] + for i in range(Y.size(-1)) + ], + dim=-1, + ) + if Yvar is not None: + # TODO: Delta method, possibly issue warning + raise NotImplementedError( + "Log does not yet support transforming observation noise" + ) + return Y_utf, Yvar
+ + +
+[docs] + def untransform_posterior(self, posterior: Posterior) -> TransformedPosterior: + r"""Un-transform the log-transformed posterior. + + Args: + posterior: A posterior in the log-transformed space. + + Returns: + The un-transformed posterior. + """ + if self._outputs is not None: + raise NotImplementedError( + "Log does not yet support output selection for untransform_posterior" + ) + return TransformedPosterior( + posterior=posterior, + sample_transform=torch.exp, + mean_transform=norm_to_lognorm_mean, + variance_transform=norm_to_lognorm_variance, + )
+
+ + + +
+[docs] +class Power(OutcomeTransform): + r"""Power-transform outcomes. + + Useful if the targets are modeled using a (multivariate) power transform of + a Normal distribution. This means that we can use a standard GP model on the + power-transformed outcomes and un-transform the model posterior of that GP. + """ + + def __init__(self, power: float, outputs: list[int] | None = None) -> None: + r"""Power-transform outcomes. + + Args: + outputs: Which of the outputs to power-transform. If omitted, all + outputs will be standardized. + """ + super().__init__() + self._outputs = outputs + self.power = power + +
+[docs] + def subset_output(self, idcs: list[int]) -> OutcomeTransform: + r"""Subset the transform along the output dimension. + + Args: + idcs: The output indices to subset the transform to. + + Returns: + The current outcome transform, subset to the specified output indices. + """ + new_outputs = None + if self._outputs is not None: + if min(self._outputs + idcs) < 0: + raise NotImplementedError( + f"Negative indexing not supported for {self.__class__.__name__} " + "when subsetting outputs and only transforming some outputs." + ) + new_outputs = [i for i in self._outputs if i in idcs] + new_tf = self.__class__(power=self.power, outputs=new_outputs) + if not self.training: + new_tf.eval() + return new_tf
+ + +
+[docs] + def forward( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Power-transform outcomes. + + Args: + Y: A `batch_shape x n x m`-dim tensor of training targets. + Yvar: A `batch_shape x n x m`-dim tensor of observation noises + associated with the training targets (if applicable). + + Returns: + A two-tuple with the transformed outcomes: + + - The transformed outcome observations. + - The transformed observation noise (if applicable). + """ + Y_tf = Y.pow(self.power) + outputs = normalize_indices(self._outputs, d=Y.size(-1)) + if outputs is not None: + Y_tf = torch.stack( + [ + Y_tf[..., i] if i in outputs else Y[..., i] + for i in range(Y.size(-1)) + ], + dim=-1, + ) + if Yvar is not None: + # TODO: Delta method, possibly issue warning + raise NotImplementedError( + "Power does not yet support transforming observation noise" + ) + return Y_tf, Yvar
+ + +
+[docs] + def untransform( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Un-transform power-transformed outcomes + + Args: + Y: A `batch_shape x n x m`-dim tensor of power-transfomred targets. + Yvar: A `batch_shape x n x m`-dim tensor of power-transformed + observation noises associated with the training targets + (if applicable). + + Returns: + A two-tuple with the un-transformed outcomes: + + - The un-power transformed outcome observations. + - The un-power transformed observation noise (if applicable). + """ + Y_utf = Y.pow(1.0 / self.power) + outputs = normalize_indices(self._outputs, d=Y.size(-1)) + if outputs is not None: + Y_utf = torch.stack( + [ + Y_utf[..., i] if i in outputs else Y[..., i] + for i in range(Y.size(-1)) + ], + dim=-1, + ) + if Yvar is not None: + # TODO: Delta method, possibly issue warning + raise NotImplementedError( + "Power does not yet support transforming observation noise" + ) + return Y_utf, Yvar
+ + +
+[docs] + def untransform_posterior(self, posterior: Posterior) -> TransformedPosterior: + r"""Un-transform the power-transformed posterior. + + Args: + posterior: A posterior in the power-transformed space. + + Returns: + The un-transformed posterior. + """ + if self._outputs is not None: + raise NotImplementedError( + "Power does not yet support output selection for untransform_posterior" + ) + return TransformedPosterior( + posterior=posterior, + sample_transform=lambda x: x.pow(1.0 / self.power), + )
+
+ + + +
+[docs] +class Bilog(OutcomeTransform): + r"""Bilog-transform outcomes. + + The Bilog transform [eriksson2021scalable]_ is useful for modeling outcome + constraints as it magnifies values near zero and flattens extreme values. + """ + + def __init__(self, outputs: list[int] | None = None) -> None: + r"""Bilog-transform outcomes. + + Args: + outputs: Which of the outputs to Bilog-transform. If omitted, all + outputs will be transformed. + """ + super().__init__() + self._outputs = outputs + +
+[docs] + def subset_output(self, idcs: list[int]) -> OutcomeTransform: + r"""Subset the transform along the output dimension. + + Args: + idcs: The output indices to subset the transform to. + + Returns: + The current outcome transform, subset to the specified output indices. + """ + new_outputs = None + if self._outputs is not None: + if min(self._outputs + idcs) < 0: + raise NotImplementedError( + f"Negative indexing not supported for {self.__class__.__name__} " + "when subsetting outputs and only transforming some outputs." + ) + new_outputs = [i for i in self._outputs if i in idcs] + new_tf = self.__class__(outputs=new_outputs) + if not self.training: + new_tf.eval() + return new_tf
+ + +
+[docs] + def forward( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Bilog-transform outcomes. + + Args: + Y: A `batch_shape x n x m`-dim tensor of training targets. + Yvar: A `batch_shape x n x m`-dim tensor of observation noises + associated with the training targets (if applicable). + + Returns: + A two-tuple with the transformed outcomes: + + - The transformed outcome observations. + - The transformed observation noise (if applicable). + """ + Y_tf = Y.sign() * (Y.abs() + 1.0).log() + outputs = normalize_indices(self._outputs, d=Y.size(-1)) + if outputs is not None: + Y_tf = torch.stack( + [ + Y_tf[..., i] if i in outputs else Y[..., i] + for i in range(Y.size(-1)) + ], + dim=-1, + ) + if Yvar is not None: + raise NotImplementedError( + "Bilog does not yet support transforming observation noise" + ) + return Y_tf, Yvar
+ + +
+[docs] + def untransform( + self, Y: Tensor, Yvar: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + r"""Un-transform bilog-transformed outcomes + + Args: + Y: A `batch_shape x n x m`-dim tensor of bilog-transfomred targets. + Yvar: A `batch_shape x n x m`-dim tensor of bilog-transformed + observation noises associated with the training targets + (if applicable). + + Returns: + A two-tuple with the un-transformed outcomes: + + - The un-transformed outcome observations. + - The un-transformed observation noise (if applicable). + """ + Y_utf = Y.sign() * Y.abs().expm1() + outputs = normalize_indices(self._outputs, d=Y.size(-1)) + if outputs is not None: + Y_utf = torch.stack( + [ + Y_utf[..., i] if i in outputs else Y[..., i] + for i in range(Y.size(-1)) + ], + dim=-1, + ) + if Yvar is not None: + # TODO: Delta method, possibly issue warning + raise NotImplementedError( + "Bilog does not yet support transforming observation noise" + ) + return Y_utf, Yvar
+ + +
+[docs] + def untransform_posterior(self, posterior: Posterior) -> TransformedPosterior: + r"""Un-transform the bilog-transformed posterior. + + Args: + posterior: A posterior in the bilog-transformed space. + + Returns: + The un-transformed posterior. + """ + if self._outputs is not None: + raise NotImplementedError( + "Bilog does not yet support output selection for untransform_posterior" + ) + return TransformedPosterior( + posterior=posterior, + sample_transform=lambda x: x.sign() * x.abs().expm1(), + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/transforms/utils.html b/website-old/pages/api/_modules/botorch/models/transforms/utils.html new file mode 100644 index 0000000000..f6009a4027 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/transforms/utils.html @@ -0,0 +1,222 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.transforms.utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from functools import wraps
+
+import torch
+from torch import Tensor
+
+
+
+[docs] +def lognorm_to_norm(mu: Tensor, Cov: Tensor) -> tuple[Tensor, Tensor]: + """Compute mean and covariance of a MVN from those of the associated log-MVN + + If `Y` is log-normal with mean mu_ln and covariance Cov_ln, then + `X ~ N(mu_n, Cov_n)` with + + Cov_n_{ij} = log(1 + Cov_ln_{ij} / (mu_ln_{i} * mu_n_{j})) + mu_n_{i} = log(mu_ln_{i}) - 0.5 * log(1 + Cov_ln_{ii} / mu_ln_{i}**2) + + Args: + mu: A `batch_shape x n` mean vector of the log-Normal distribution. + Cov: A `batch_shape x n x n` covariance matrix of the log-Normal + distribution. + + Returns: + A two-tuple containing: + + - The `batch_shape x n` mean vector of the Normal distribution + - The `batch_shape x n x n` covariance matrix of the Normal distribution + """ + Cov_n = torch.log1p(Cov / (mu.unsqueeze(-1) * mu.unsqueeze(-2))) + mu_n = torch.log(mu) - 0.5 * torch.diagonal(Cov_n, dim1=-1, dim2=-2) + return mu_n, Cov_n
+ + + +
+[docs] +def norm_to_lognorm(mu: Tensor, Cov: Tensor) -> tuple[Tensor, Tensor]: + """Compute mean and covariance of a log-MVN from its MVN sufficient statistics + + If `X ~ N(mu, Cov)` and `Y = exp(X)`, then `Y` is log-normal with + + mu_ln_{i} = exp(mu_{i} + 0.5 * Cov_{ii}) + Cov_ln_{ij} = exp(mu_{i} + mu_{j} + 0.5 * (Cov_{ii} + Cov_{jj})) * + (exp(Cov_{ij}) - 1) + + Args: + mu: A `batch_shape x n` mean vector of the Normal distribution. + Cov: A `batch_shape x n x n` covariance matrix of the Normal distribution. + + Returns: + A two-tuple containing: + + - The `batch_shape x n` mean vector of the log-Normal distribution. + - The `batch_shape x n x n` covariance matrix of the log-Normal + distribution. + """ + diag = torch.diagonal(Cov, dim1=-1, dim2=-2) + b = mu + 0.5 * diag + mu_ln = torch.exp(b) + Cov_ln = torch.special.expm1(Cov) * torch.exp(b.unsqueeze(-1) + b.unsqueeze(-2)) + return mu_ln, Cov_ln
+ + + +
+[docs] +def norm_to_lognorm_mean(mu: Tensor, var: Tensor) -> Tensor: + """Compute mean of a log-MVN from its MVN marginals + + Args: + mu: A `batch_shape x n` mean vector of the Normal distribution. + var: A `batch_shape x n` variance vectorof the Normal distribution. + + Returns: + The `batch_shape x n` mean vector of the log-Normal distribution. + """ + return torch.exp(mu + 0.5 * var)
+ + + +
+[docs] +def norm_to_lognorm_variance(mu: Tensor, var: Tensor) -> Tensor: + """Compute variance of a log-MVN from its MVN marginals + + Args: + mu: A `batch_shape x n` mean vector of the Normal distribution. + var: A `batch_shape x n` variance vectorof the Normal distribution. + + Returns: + The `batch_shape x n` variance vector of the log-Normal distribution. + """ + b = mu + 0.5 * var + return torch.special.expm1(var) * torch.exp(2 * b)
+ + + +
+[docs] +def expand_and_copy_tensor(X: Tensor, batch_shape: torch.Size) -> Tensor: + r"""Expand and copy X according to batch_shape. + + Args: + X: A `input_batch_shape x n x d`-dim tensor of inputs. + batch_shape: The new batch shape. + + Returns: + A `new_batch_shape x n x d`-dim tensor of inputs, where `new_batch_shape` + is `input_batch_shape` against `batch_shape`. + """ + try: + batch_shape = torch.broadcast_shapes(X.shape[:-2], batch_shape) + except RuntimeError: + raise RuntimeError( + f"Provided batch shape ({batch_shape}) and input batch shape " + f"({X.shape[:-2]}) are not broadcastable." + ) + expand_shape = batch_shape + X.shape[-2:] + return X.expand(expand_shape).clone()
+ + + +
+[docs] +def subset_transform(transform): + r"""Decorator of an input transform function to separate out indexing logic.""" + + @wraps(transform) + def f(self, X: Tensor) -> Tensor: + if not hasattr(self, "indices") or self.indices is None: + return transform(self, X) + has_shape = hasattr(self, "batch_shape") + Y = expand_and_copy_tensor(X, self.batch_shape) if has_shape else X.clone() + Y[..., self.indices] = transform(self, X[..., self.indices]) + return Y + + return f
+ + + +
+[docs] +def interaction_features(X: Tensor) -> Tensor: + """Computes the interaction features between the inputs. + + Args: + X: A `batch_shape x q x d`-dim tensor of inputs. + indices: The input dimensions to generate interaction features for. + + Returns: + A `n x q x 1 x (d * (d-1) / 2))`-dim tensor of interaction features. + """ + dim = X.shape[-1] + row_idcs, col_idcs = torch.triu_indices(dim, dim, offset=1) + return (X.unsqueeze(-1) @ X.unsqueeze(-2))[..., row_idcs, col_idcs].unsqueeze(-2)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/utils/assorted.html b/website-old/pages/api/_modules/botorch/models/utils/assorted.html new file mode 100644 index 0000000000..622b2a21dc --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/utils/assorted.html @@ -0,0 +1,490 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.utils.assorted

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Assorted helper methods and objects for working with BoTorch models."""
+
+from __future__ import annotations
+
+import warnings
+from collections.abc import Iterator
+from contextlib import contextmanager, ExitStack
+
+import torch
+from botorch import settings
+from botorch.exceptions import InputDataError, InputDataWarning
+from botorch.settings import _Flag
+from gpytorch import settings as gpt_settings
+from gpytorch.module import Module
+from torch import Tensor
+
+
+def _make_X_full(X: Tensor, output_indices: list[int], tf: int) -> Tensor:
+    r"""Helper to construct input tensor with task indices.
+
+    Args:
+        X: The raw input tensor (without task information).
+        output_indices: The output indices to generate (passed in via `posterior`).
+        tf: The task feature index.
+
+    Returns:
+        Tensor: The full input tensor for the multi-task model, including task
+            indices.
+    """
+    index_shape = X.shape[:-1] + torch.Size([1])
+    indexers = (
+        torch.full(index_shape, fill_value=i, device=X.device, dtype=X.dtype)
+        for i in output_indices
+    )
+    X_l, X_r = X[..., :tf], X[..., tf:]
+    return torch.cat(
+        [torch.cat([X_l, indexer, X_r], dim=-1) for indexer in indexers], dim=-2
+    )
+
+
+
+[docs] +def multioutput_to_batch_mode_transform( + train_X: Tensor, + train_Y: Tensor, + num_outputs: int, + train_Yvar: Tensor | None = None, +) -> tuple[Tensor, Tensor, Tensor | None]: + r"""Transforms training inputs for a multi-output model. + + Used for multi-output models that internally are represented by a + batched single output model, where each output is modeled as an + independent batch. + + Args: + train_X: A `n x d` or `input_batch_shape x n x d` (batch mode) tensor of + training features. + train_Y: A `n x m` or `target_batch_shape x n x m` (batch mode) tensor of + training observations. + num_outputs: number of outputs + train_Yvar: A `n x m` or `target_batch_shape x n x m` tensor of observed + measurement noise. + + Returns: + 3-element tuple containing + + - A `input_batch_shape x m x n x d` tensor of training features. + - A `target_batch_shape x m x n` tensor of training observations. + - A `target_batch_shape x m x n` tensor observed measurement noise. + """ + # make train_Y `batch_shape x m x n` + train_Y = train_Y.transpose(-1, -2) + # expand train_X to `batch_shape x m x n x d` + train_X = train_X.unsqueeze(-3).expand( + train_X.shape[:-2] + torch.Size([num_outputs]) + train_X.shape[-2:] + ) + if train_Yvar is not None: + # make train_Yvar `batch_shape x m x n` + train_Yvar = train_Yvar.transpose(-1, -2) + return train_X, train_Y, train_Yvar
+ + + +
+[docs] +def add_output_dim(X: Tensor, original_batch_shape: torch.Size) -> tuple[Tensor, int]: + r"""Insert the output dimension at the correct location. + + The trailing batch dimensions of X must match the original batch dimensions + of the training inputs, but can also include extra batch dimensions. + + Args: + X: A `(new_batch_shape) x (original_batch_shape) x n x d` tensor of + features. + original_batch_shape: the batch shape of the model's training inputs. + + Returns: + 2-element tuple containing + + - A `(new_batch_shape) x (original_batch_shape) x m x n x d` tensor of + features. + - The index corresponding to the output dimension. + """ + X_batch_shape = X.shape[:-2] + if len(X_batch_shape) > 0 and len(original_batch_shape) > 0: + # check that X_batch_shape supports broadcasting or augments + # original_batch_shape with extra batch dims + try: + torch.broadcast_shapes(X_batch_shape, original_batch_shape) + except RuntimeError: + raise RuntimeError( + "The trailing batch dimensions of X must match the trailing " + f"batch dimensions of the training inputs. Got {X.shape=} " + f"and {original_batch_shape=}." + ) + # insert `m` dimension + X = X.unsqueeze(-3) + output_dim_idx = max(len(original_batch_shape), len(X_batch_shape)) + return X, output_dim_idx
+ + + +
+[docs] +def check_no_nans(Z: Tensor) -> None: + r"""Check that tensor does not contain NaN values. + + Raises an InputDataError if `Z` contains NaN values. + + Args: + Z: The input tensor. + """ + if torch.any(torch.isnan(Z)).item(): + raise InputDataError("Input data contains NaN values.")
+ + + +
+[docs] +def check_min_max_scaling( + X: Tensor, + strict: bool = False, + atol: float = 1e-2, + raise_on_fail: bool = False, + ignore_dims: list[int] | None = None, +) -> None: + r"""Check that tensor is normalized to the unit cube. + + Args: + X: A `batch_shape x n x d` input tensor. Typically the training inputs + of a model. + strict: If True, require `X` to be scaled to the unit cube (rather than + just to be contained within the unit cube). + atol: The tolerance for the boundary check. Only used if `strict=True`. + raise_on_fail: If True, raise an exception instead of a warning. + ignore_dims: Subset of dimensions where the min-max scaling check is omitted. + """ + ignore_dims = ignore_dims or [] + check_dims = list(set(range(X.shape[-1])) - set(ignore_dims)) + if len(check_dims) == 0: + return None + + with torch.no_grad(): + X_check = X[..., check_dims] + Xmin = torch.min(X_check, dim=-1).values + Xmax = torch.max(X_check, dim=-1).values + msg = None + if strict and max(torch.abs(Xmin).max(), torch.abs(Xmax - 1).max()) > atol: + msg = "scaled" + if torch.any(Xmin < -atol) or torch.any(Xmax > 1 + atol): + msg = "contained" + if msg is not None: + # NOTE: If you update this message, update the warning filters as well. + # See https://github.com/pytorch/botorch/pull/2508. + msg = ( + f"Data (input features) is not {msg} to the unit cube. " + "Please consider min-max scaling the input data." + ) + if raise_on_fail: + raise InputDataError(msg) + warnings.warn(msg, InputDataWarning, stacklevel=2)
+ + + +
+[docs] +def check_standardization( + Y: Tensor, + atol_mean: float = 1e-2, + atol_std: float = 1e-2, + raise_on_fail: bool = False, +) -> None: + r"""Check that tensor is standardized (zero mean, unit variance). + + Args: + Y: The input tensor of shape `batch_shape x n x m`. Typically the + train targets of a model. Standardization is checked across the + `n`-dimension. + atol_mean: The tolerance for the mean check. + atol_std: The tolerance for the std check. + raise_on_fail: If True, raise an exception instead of a warning. + """ + with torch.no_grad(): + Ymean = torch.mean(Y, dim=-2) + mean_not_zero = torch.abs(Ymean).max() > atol_mean + if Y.shape[-2] <= 1: + if mean_not_zero: + # NOTE: If you update this message, update the warning filters as well. + # See https://github.com/pytorch/botorch/pull/2508. + msg = ( + f"Data (outcome observations) is not standardized (mean = {Ymean})." + " Please consider scaling the input to zero mean and unit variance." + ) + if raise_on_fail: + raise InputDataError(msg) + warnings.warn(msg, InputDataWarning, stacklevel=2) + else: + Ystd = torch.std(Y, dim=-2) + std_not_one = torch.abs(Ystd - 1).max() > atol_std + if mean_not_zero or std_not_one: + # NOTE: If you update this message, update the warning filters as well. + # See https://github.com/pytorch/botorch/pull/2508. + msg = ( + "Data (outcome observations) is not standardized " + f"(std = {Ystd}, mean = {Ymean})." + "Please consider scaling the input to zero mean and unit variance." + ) + if raise_on_fail: + raise InputDataError(msg) + warnings.warn(msg, InputDataWarning, stacklevel=2)
+ + + +
+[docs] +def validate_input_scaling( + train_X: Tensor, + train_Y: Tensor, + train_Yvar: Tensor | None = None, + raise_on_fail: bool = False, + ignore_X_dims: list[int] | None = None, +) -> None: + r"""Helper function to validate input data to models. + + Args: + train_X: A `n x d` or `batch_shape x n x d` (batch mode) tensor of + training features. + train_Y: A `n x m` or `batch_shape x n x m` (batch mode) tensor of + training observations. + train_Yvar: A `batch_shape x n x m` or `batch_shape x n x m` (batch mode) + tensor of observed measurement noise. + raise_on_fail: If True, raise an error instead of emitting a warning + (only for normalization/standardization checks, an error is always + raised if NaN values are present). + ignore_X_dims: For this subset of dimensions from `{1, ..., d}`, ignore the + min-max scaling check. + + This function is typically called inside the constructor of standard BoTorch + models. It validates the following: + (i) none of the inputs contain NaN values + (ii) the training data (`train_X`) is normalized to the unit cube for all + dimensions except those in `ignore_X_dims`. + (iii) the training targets (`train_Y`) are standardized (zero mean, unit var) + No checks (other than the NaN check) are performed for observed variances + (`train_Yvar`) at this point. + """ + if settings.validate_input_scaling.off(): + return + check_no_nans(train_X) + check_no_nans(train_Y) + if train_Yvar is not None: + check_no_nans(train_Yvar) + if torch.any(train_Yvar < 0): + raise InputDataError("Input data contains negative variances.") + check_min_max_scaling( + X=train_X, raise_on_fail=raise_on_fail, ignore_dims=ignore_X_dims + ) + check_standardization(Y=train_Y, raise_on_fail=raise_on_fail)
+ + + +
+[docs] +def mod_batch_shape(module: Module, names: list[str], b: int) -> None: + r"""Recursive helper to modify gpytorch modules' batch shape attribute. + + Modifies the module in-place. + + Args: + module: The module to be modified. + names: The list of names to access the attribute. If the full name of + the module is `"module.sub_module.leaf_module"`, this will be + `["sub_module", "leaf_module"]`. + b: The new size of the last element of the module's `batch_shape` + attribute. + """ + if len(names) == 0: + return + m = getattr(module, names[0]) + if len(names) == 1 and hasattr(m, "batch_shape") and len(m.batch_shape) > 0: + m.batch_shape = m.batch_shape[:-1] + torch.Size([b] if b > 0 else []) + else: + mod_batch_shape(module=m, names=names[1:], b=b)
+ + + +
+[docs] +@contextmanager +def gpt_posterior_settings(): + r"""Context manager for settings used for computing model posteriors.""" + with ExitStack() as es: + if gpt_settings.debug.is_default(): + es.enter_context(gpt_settings.debug(False)) + if gpt_settings.fast_pred_var.is_default(): + es.enter_context(gpt_settings.fast_pred_var()) + es.enter_context( + gpt_settings.detach_test_caches(settings.propagate_grads.off()) + ) + yield
+ + + +
+[docs] +def detect_duplicates( + X: Tensor, + rtol: float = 0, + atol: float = 1e-8, +) -> Iterator[tuple[int, int]]: + """Returns an iterator over index pairs `(duplicate index, original index)` for all + duplicate entries of `X`. Supporting 2-d Tensor only. + + Args: + X: the datapoints tensor with potential duplicated entries + rtol: relative tolerance + atol: absolute tolerance + """ + if len(X.shape) != 2: + raise ValueError("X must have 2 dimensions.") + + tols = atol + if rtol: + rval = X.abs().max(dim=-1, keepdim=True).values + tols = tols + rtol * rval.max(rval.transpose(-1, -2)) + + n = X.shape[-2] + dist = torch.full((n, n), float("inf"), device=X.device, dtype=X.dtype) + dist[torch.triu_indices(n, n, offset=1).unbind()] = torch.nn.functional.pdist( + X, p=float("inf") + ) + return ( + (i, int(j)) + # pyre-fixme[19]: Expected 1 positional argument. + for diff, j, i in zip(*(dist - tols).min(dim=-2), range(n)) + if diff < 0 + )
+ + + +
+[docs] +def consolidate_duplicates( + X: Tensor, Y: Tensor, rtol: float = 0.0, atol: float = 1e-8 +) -> tuple[Tensor, Tensor, Tensor]: + """Drop duplicated Xs and update the indices tensor Y accordingly. + Supporting 2d Tensor only as in batch mode block design is not guaranteed. + + Args: + X: the datapoints tensor + Y: the index tensor to be updated (e.g., pairwise comparisons) + rtol: relative tolerance + atol: absolute tolerance + + Returns: + consolidated_X: the consolidated X + consolidated_Y: the consolidated Y (e.g., pairwise comparisons indices) + new_indices: new index of each original item in X, a tensor of size X.shape[-2] + """ + if len(X.shape) != 2: + raise ValueError("X must have 2 dimensions.") + + n = X.shape[-2] + dup_map = dict(detect_duplicates(X=X, rtol=rtol, atol=atol)) + + # Handle edge cases conservatively + # If a item is in both dup set and kept set, do not remove it + common_set = set(dup_map.keys()).intersection(dup_map.values()) + for k in list(dup_map.keys()): + if k in common_set or dup_map[k] in common_set: + del dup_map[k] + + if dup_map: + dup_indices, kept_indices = zip(*dup_map.items()) + + unique_indices = sorted(set(range(n)) - set(dup_indices)) + + # After dropping the duplicates, + # the kept ones' indices may also change by being shifted up + new_idx_map = dict(zip(unique_indices, range(len(unique_indices)))) + new_indices_for_dup = (new_idx_map[idx] for idx in kept_indices) + new_idx_map.update(dict(zip(dup_indices, new_indices_for_dup))) + consolidated_X = X[list(unique_indices), :] + consolidated_Y = torch.tensor( + [[new_idx_map[item.item()] for item in row] for row in Y.unbind()], + dtype=torch.long, + device=Y.device, + ) + new_indices = ( + torch.arange(n, dtype=torch.long) + .apply_(lambda x: new_idx_map[x]) + .to(Y.device) + ) + return consolidated_X, consolidated_Y, new_indices + else: + return X, Y, torch.arange(n, device=Y.device, dtype=Y.dtype)
+ + + +
+[docs] +class fantasize(_Flag): + r"""A flag denoting whether we are currently in a `fantasize` context.""" + + _state: bool = False
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/utils/gpytorch_modules.html b/website-old/pages/api/_modules/botorch/models/utils/gpytorch_modules.html new file mode 100644 index 0000000000..346869fad8 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/utils/gpytorch_modules.html @@ -0,0 +1,203 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.utils.gpytorch_modules

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Pre-packaged kernels for bayesian optimization, including a Scale/Matern
+kernel that is well-suited to low-dimensional high-noise problems, and
+a dimension-agnostic RBF kernel without outputscale.
+
+References:
+
+.. [Hvarfner2024vanilla]
+    C. Hvarfner, E. O. Hellsten, L. Nardi,
+    Vanilla Bayesian Optimization Performs Great in High Dimensions.
+    In International Conference on Machine Learning, 2024.
+"""
+
+from collections.abc import Sequence
+from math import log, sqrt
+
+import torch
+from gpytorch.constraints.constraints import GreaterThan
+from gpytorch.kernels import MaternKernel, RBFKernel, ScaleKernel
+from gpytorch.likelihoods.gaussian_likelihood import GaussianLikelihood
+from gpytorch.priors.torch_priors import GammaPrior, LogNormalPrior
+
+MIN_INFERRED_NOISE_LEVEL = 1e-4
+SQRT2 = sqrt(2)
+SQRT3 = sqrt(3)
+
+
+
+[docs] +def get_matern_kernel_with_gamma_prior( + ard_num_dims: int, batch_shape: torch.Size | None = None +) -> ScaleKernel: + r"""Constructs the Scale-Matern kernel that is used by default by + several models. This uses a Gamma(3.0, 6.0) prior for the lengthscale + and a Gamma(2.0, 0.15) prior for the output scale. + """ + return ScaleKernel( + base_kernel=MaternKernel( + nu=2.5, + ard_num_dims=ard_num_dims, + batch_shape=batch_shape, + lengthscale_prior=GammaPrior(3.0, 6.0), + ), + batch_shape=batch_shape, + outputscale_prior=GammaPrior(2.0, 0.15), + )
+ + + +
+[docs] +def get_gaussian_likelihood_with_gamma_prior( + batch_shape: torch.Size | None = None, +) -> GaussianLikelihood: + r"""Constructs the GaussianLikelihood that is used by default by + several models. This uses a Gamma(1.1, 0.05) prior and constrains the + noise level to be greater than MIN_INFERRED_NOISE_LEVEL (=1e-4). + """ + batch_shape = torch.Size() if batch_shape is None else batch_shape + noise_prior = GammaPrior(1.1, 0.05) + noise_prior_mode = (noise_prior.concentration - 1) / noise_prior.rate + return GaussianLikelihood( + noise_prior=noise_prior, + batch_shape=batch_shape, + noise_constraint=GreaterThan( + MIN_INFERRED_NOISE_LEVEL, + transform=None, + initial_value=noise_prior_mode, + ), + )
+ + + +
+[docs] +def get_gaussian_likelihood_with_lognormal_prior( + batch_shape: torch.Size | None = None, +) -> GaussianLikelihood: + """Return Gaussian likelihood with a LogNormal(-4.0, 1.0) prior. + This prior is based on [Hvarfner2024vanilla]_. + + Args: + batch_shape: Batch shape for the likelihood. + + Returns: + GaussianLikelihood with LogNormal(-4.0, 1.0) prior and constrains the + noise level to be greater than MIN_INFERRED_NOISE_LEVEL (=1e-4). + """ + batch_shape = torch.Size() if batch_shape is None else batch_shape + noise_prior = LogNormalPrior(loc=-4.0, scale=1.0) + return GaussianLikelihood( + noise_prior=noise_prior, + batch_shape=batch_shape, + noise_constraint=GreaterThan( + MIN_INFERRED_NOISE_LEVEL, + transform=None, + initial_value=noise_prior.mode, + ), + )
+ + + +
+[docs] +def get_covar_module_with_dim_scaled_prior( + ard_num_dims: int, + batch_shape: torch.Size | None = None, + use_rbf_kernel: bool = True, + active_dims: Sequence[int] | None = None, +) -> MaternKernel | RBFKernel: + """Returns an RBF or Matern kernel with priors + from [Hvarfner2024vanilla]_. + + Args: + ard_num_dims: Number of feature dimensions for ARD. + batch_shape: Batch shape for the covariance module. + use_rbf_kernel: Whether to use an RBF kernel. If False, uses a Matern kernel. + active_dims: The set of input dimensions to compute the covariances on. + By default, the covariance is computed using the full input tensor. + Set this if you'd like to ignore certain dimensions. + + Returns: + A Kernel constructed according to the given arguments. The prior is constrained + to have lengthscales larger than 0.025 for numerical stability. + """ + base_class = RBFKernel if use_rbf_kernel else MaternKernel + lengthscale_prior = LogNormalPrior(loc=SQRT2 + log(ard_num_dims) * 0.5, scale=SQRT3) + base_kernel = base_class( + ard_num_dims=ard_num_dims, + batch_shape=batch_shape, + lengthscale_prior=lengthscale_prior, + lengthscale_constraint=GreaterThan( + 2.5e-2, transform=None, initial_value=lengthscale_prior.mode + ), + # pyre-ignore[6] GPyTorch type is unnecessarily restrictive. + active_dims=active_dims, + ) + return base_kernel
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/models/utils/inducing_point_allocators.html b/website-old/pages/api/_modules/botorch/models/utils/inducing_point_allocators.html new file mode 100644 index 0000000000..450e1166c0 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/models/utils/inducing_point_allocators.html @@ -0,0 +1,440 @@ + + + + + + + +
+
+
+
+

Source code for botorch.models.utils.inducing_point_allocators

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Functionality for allocating the inducing points of sparse Gaussian
+process models.
+
+References
+
+.. [chen2018dpp]
+    Laming Chen and Guoxin Zhang and Hanning Zhou, Fast greedy MAP inference
+    for determinantal point process to improve recommendation diversity,
+    Proceedings of the 32nd International Conference on Neural Information
+    Processing Systems, 2018, https://arxiv.org/abs/1709.05135.
+
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.model import Model
+
+from botorch.utils.probability.utils import ndtr as Phi, phi
+from gpytorch.module import Module
+from linear_operator.operators import LinearOperator
+from torch import Tensor
+
+NEG_INF = torch.tensor(float("-inf"))
+
+
+
+[docs] +class InducingPointAllocator(ABC): + r""" + This class provides functionality to initialize the inducing point locations + of an inducing point-based model, e.g. a `SingleTaskVariationalGP`. + """ + + @abstractmethod + def _get_quality_function( + self, + ) -> QualityFunction: + """ + Build the quality function required for this inducing point allocation strategy. + + Returns: + A quality function. + """ + +
+[docs] + def allocate_inducing_points( + self, + inputs: Tensor, + covar_module: Module, + num_inducing: int, + input_batch_shape: torch.Size, + ) -> Tensor: + r""" + Initialize the `num_inducing` inducing point locations according to a + specific initialization strategy. todo say something about quality + + Args: + inputs: A (\*batch_shape, n, d)-dim input data tensor. + covar_module: GPyTorch Module returning a LinearOperator kernel matrix. + num_inducing: The maximun number (m) of inducing points (m <= n). + input_batch_shape: The non-task-related batch shape. + + Returns: + A (\*batch_shape, m, d)-dim tensor of inducing point locations. + """ + quality_function = self._get_quality_function() + covar_module = covar_module.to(inputs.device) + + # We use 'no_grad' here because `inducing_points` are not + # auto-differentiable with respect to the kernel hyper-parameters, + # because `_pivoted_cholesky_init` does in-place operations. + with torch.no_grad(): + # Evaluate lazily because this may only be needed to figure out what + # case we are in + possibly_lazy_kernel = covar_module(inputs) + + base_case = possibly_lazy_kernel.ndimension() == 2 + multi_task_case = ( + possibly_lazy_kernel.ndimension() == 3 and len(input_batch_shape) == 0 + ) + + if base_case or multi_task_case: + train_train_kernel = possibly_lazy_kernel.evaluate_kernel() + + if base_case: + quality_scores = quality_function(inputs) + inducing_points = _pivoted_cholesky_init( + train_inputs=inputs, + kernel_matrix=train_train_kernel, + max_length=num_inducing, + quality_scores=quality_scores, + ) + return inducing_points + + if multi_task_case: + input_element = inputs[0] if inputs.ndimension() == 3 else inputs + kernel_element = train_train_kernel[0] + quality_scores = quality_function(input_element) + inducing_points = _pivoted_cholesky_init( + train_inputs=input_element, + kernel_matrix=kernel_element, + max_length=num_inducing, + quality_scores=quality_scores, + ) + return inducing_points + + # batched input cases + batched_inputs = ( + inputs.expand(*input_batch_shape, -1, -1) + if inputs.ndimension() == 2 + else inputs + ) + reshaped_inputs = batched_inputs.flatten(end_dim=-3) + inducing_points = [] + for input_element in reshaped_inputs: + # the extra kernel evals are a little wasteful but make it + # easier to infer the task batch size + # We use 'no_grad' here because `inducing_points` are not + # auto-differentiable with respect to the kernel hyper-parameters, + # because `_pivoted_cholesky_init` does in-place operations. + with torch.no_grad(): + kernel_element = covar_module(input_element).evaluate_kernel() + # handle extra task batch dimension + kernel_element = ( + kernel_element[0] + if kernel_element.ndimension() == 3 + else kernel_element + ) + quality_scores = quality_function(input_element) + inducing_points.append( + _pivoted_cholesky_init( + train_inputs=input_element, + kernel_matrix=kernel_element, + max_length=num_inducing, + quality_scores=quality_scores, + ) + ) + inducing_points = torch.stack(inducing_points).view( + *input_batch_shape, num_inducing, -1 + ) + + return inducing_points
+
+ + + +
+[docs] +class QualityFunction(ABC): + """A function that scores inputs with respect + to a specific criterion.""" + + @abstractmethod + def __call__(self, inputs: Tensor) -> Tensor: # [n, d] -> [n] + """ + Args: + inputs: inputs (of shape n x d) + + Returns: + A tensor of quality scores for each input, of shape [n] + """
+ + + +
+[docs] +class UnitQualityFunction(QualityFunction): + """ + A function returning ones for each element. Using this quality function + for inducing point allocation corresponds to allocating inducing points + with the sole aim of minimizing predictive variance, i.e. the approach + of [burt2020svgp]_. + """ + + @torch.no_grad() + def __call__(self, inputs: Tensor) -> Tensor: # [n, d]-> [n] + """ + Args: + inputs: inputs (of shape n x d) + + Returns: + A tensor of ones for each input, of shape [n] + """ + return torch.ones([inputs.shape[0]], device=inputs.device, dtype=inputs.dtype)
+ + + +
+[docs] +class ExpectedImprovementQualityFunction(QualityFunction): + """ + A function measuring the quality of input points as their expected + improvement with respect to a conservative baseline. Expectations + are according to the model from the previous BO step. See [moss2023ipa]_ + for details and justification. + """ + + def __init__(self, model: Model, maximize: bool): + r""" + Args: + model: The model fitted during the previous BO step. For now, this + must be a single task model (i.e. num_outputs=1). + maximize: Set True if we are performing function maximization, else + set False. + """ + if model.num_outputs != 1: + raise NotImplementedError( + "Multi-output models are currently not supported. " + ) + self._model = model + self._maximize = maximize + + @torch.no_grad() + def __call__(self, inputs: Tensor) -> Tensor: # [n, d] -> [n] + """ + Args: + inputs: inputs (of shape n x d) + + Returns: + A tensor of quality scores for each input, of shape [n] + """ + + posterior = self._model.posterior(inputs) + mean = posterior.mean.squeeze(-2).squeeze(-1) # removing redundant dimensions + sigma = posterior.variance.clamp_min(1e-12).sqrt().view(mean.shape) + + best_f = torch.max(mean) if self._maximize else torch.min(mean) + u = (mean - best_f) / sigma if self._maximize else -(mean - best_f) / sigma + return sigma * (phi(u) + u * Phi(u))
+ + + +
+[docs] +class GreedyVarianceReduction(InducingPointAllocator): + r""" + The inducing point allocator proposed by [burt2020svgp]_, that + greedily chooses inducing point locations with maximal (conditional) + predictive variance. + """ + + def _get_quality_function( + self, + ) -> QualityFunction: + """ + Build the unit quality function required for the greedy variance + reduction inducing point allocation strategy. + + Returns: + A quality function. + """ + + return UnitQualityFunction()
+ + + +
+[docs] +class GreedyImprovementReduction(InducingPointAllocator): + r""" + An inducing point allocator that greedily chooses inducing points with large + predictive variance and that are in promising regions of the search + space (according to the model form the previous BO step), see [moss2023ipa]_. + """ + + def __init__(self, model: Model, maximize: bool): + r""" + + Args: + model: The model fitted during the previous BO step. + maximize: Set True if we are performing function maximization, else + set False. + """ + self._model = model + self._maximize = maximize + + def _get_quality_function( + self, + ) -> QualityFunction: + """ + Build the improvement-based quality function required for the greedy + improvement reduction inducing point allocation strategy. + + Returns: + A quality function. + """ + + return ExpectedImprovementQualityFunction(self._model, self._maximize)
+ + + +
+[docs] +def _pivoted_cholesky_init( + train_inputs: Tensor, + kernel_matrix: Tensor | LinearOperator, + max_length: int, + quality_scores: Tensor, + epsilon: float = 1e-6, +) -> Tensor: + r""" + A pivoted Cholesky initialization method for the inducing points, + originally proposed in [burt2020svgp]_ with the algorithm itself coming from + [chen2018dpp]_. Code is a PyTorch version from [chen2018dpp]_, based on + https://github.com/laming-chen/fast-map-dpp/blob/master/dpp.py but with a small + modification to allow the underlying DPP to be defined through its diversity-quality + decomposition,as discussed by [moss2023ipa]_. This method returns a greedy + approximation of the MAP estimate of the specified DPP, i.e. its returns a + set of points that are highly diverse (according to the provided kernel_matrix) + and have high quality (according to the provided quality_scores). + + Args: + train_inputs: training inputs (of shape n x d) + kernel_matrix: kernel matrix on the training inputs + max_length: number of inducing points to initialize + quality_scores: scores representing the quality of each candidate + input (of shape [n]) + epsilon: numerical jitter for stability. + + Returns: + max_length x d tensor of the training inputs corresponding to the top + max_length pivots of the training kernel matrix + """ + + # this is numerically equivalent to iteratively performing a pivoted cholesky + # while storing the diagonal pivots at each iteration + # TODO: use gpytorch's pivoted cholesky instead once that gets an exposed list + # TODO: ensure this works in batch mode, which it does not currently. + + # todo test for shape of quality function + + if quality_scores.shape[0] != train_inputs.shape[0]: + raise ValueError( + "_pivoted_cholesky_init requires a quality score for each of train_inputs" + ) + + if kernel_matrix.requires_grad: + raise UnsupportedError( + "`_pivoted_cholesky_init` does not support using a `kernel_matrix` " + "with `requires_grad=True`." + ) + + item_size = kernel_matrix.shape[-2] + cis = torch.zeros( + (max_length, item_size), device=kernel_matrix.device, dtype=kernel_matrix.dtype + ) + di2s = kernel_matrix.diagonal() + scores = di2s * torch.square(quality_scores) + selected_item = torch.argmax(scores) + selected_items = [selected_item] + + while len(selected_items) < max_length: + k = len(selected_items) - 1 + ci_optimal = cis[:k, selected_item] + di_optimal = torch.sqrt(di2s[selected_item]) + elements = kernel_matrix[..., selected_item, :] + eis = (elements - torch.matmul(ci_optimal, cis[:k, :])) / di_optimal + cis[k, :] = eis + di2s = di2s - eis.pow(2.0) + di2s[selected_item] = NEG_INF + scores = di2s * torch.square(quality_scores) + selected_item = torch.argmax(scores) + if di2s[selected_item] < epsilon: + break + selected_items.append(selected_item) + + ind_points = train_inputs[torch.stack(selected_items)] + + return ind_points[:max_length, :]
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/closures/core.html b/website-old/pages/api/_modules/botorch/optim/closures/core.html new file mode 100644 index 0000000000..c29c07a2c3 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/closures/core.html @@ -0,0 +1,250 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.closures.core

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+"""Core methods for building closures in torch and interfacing with numpy."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Sequence
+
+from functools import partial
+from typing import Any
+
+import numpy.typing as npt
+
+import torch
+from botorch.optim.utils import (
+    _handle_numerical_errors,
+    get_tensors_as_ndarray_1d,
+    set_tensors_from_ndarray_1d,
+)
+from botorch.optim.utils.numpy_utils import as_ndarray
+from botorch.utils.context_managers import zero_grad_ctx
+from numpy import float64 as np_float64, full as np_full, zeros as np_zeros
+from torch import Tensor
+
+
+
+[docs] +class ForwardBackwardClosure: + r"""Wrapper for fused forward and backward closures.""" + + def __init__( + self, + forward: Callable[[], Tensor], + parameters: dict[str, Tensor], + backward: Callable[[Tensor], None] = Tensor.backward, + reducer: Callable[[Tensor], Tensor] | None = torch.sum, + callback: Callable[[Tensor, Sequence[Tensor | None]], None] | None = None, + context_manager: Callable = None, # pyre-ignore [9] + ) -> None: + r"""Initializes a ForwardBackwardClosure instance. + + Args: + closure: Callable that returns a tensor. + parameters: A dictionary of tensors whose `grad` fields are to be returned. + backward: Callable that takes the (reduced) output of `forward` and sets the + `grad` attributes of tensors in `parameters`. + reducer: Optional callable used to reduce the output of the forward pass. + callback: Optional callable that takes the reduced output of `forward` and + the gradients of `parameters` as positional arguments. + context_manager: A ContextManager used to wrap each forward-backward call. + When passed as `None`, `context_manager` defaults to a `zero_grad_ctx` + that zeroes the gradients of `parameters` upon entry. + """ + if context_manager is None: + context_manager = partial(zero_grad_ctx, parameters) + + self.forward = forward + self.backward = backward + self.parameters = parameters + self.reducer = reducer + self.callback = callback + self.context_manager = context_manager + + def __call__(self, **kwargs: Any) -> tuple[Tensor, tuple[Tensor | None, ...]]: + with self.context_manager(): + values = self.forward(**kwargs) + value = values if self.reducer is None else self.reducer(values) + self.backward(value) + + grads = tuple(param.grad for param in self.parameters.values()) + if self.callback: + self.callback(value, grads) + + return value, grads
+ + + +
+[docs] +class NdarrayOptimizationClosure: + r"""Adds stateful behavior and a numpy.ndarray-typed API to a closure with an + expected return type Tuple[Tensor, Union[Tensor, Sequence[Optional[Tensor]]]].""" + + def __init__( + self, + closure: Callable[[], tuple[Tensor, Sequence[Tensor | None]]], + parameters: dict[str, Tensor], + as_array: Callable[[Tensor], npt.NDArray] = None, # pyre-ignore [9] + get_state: Callable[[], npt.NDArray] = None, # pyre-ignore [9] + set_state: Callable[[npt.NDArray], None] = None, # pyre-ignore [9] + fill_value: float = 0.0, + persistent: bool = True, + ) -> None: + r"""Initializes a NdarrayOptimizationClosure instance. + + Args: + closure: A ForwardBackwardClosure instance. + parameters: A dictionary of tensors representing the closure's state. + Expected to correspond with the first `len(parameters)` optional + gradient tensors returned by `closure`. + as_array: Callable used to convert tensors to ndarrays. + get_state: Callable that returns the closure's state as an ndarray. When + passed as `None`, defaults to calling `get_tensors_as_ndarray_1d` + on `closure.parameters` while passing `as_array` (if given by the user). + set_state: Callable that takes a 1-dimensional ndarray and sets the + closure's state. When passed as `None`, `set_state` defaults to + calling `set_tensors_from_ndarray_1d` with `closure.parameters` and + a given ndarray. + fill_value: Fill value for parameters whose gradients are None. In most + cases, `fill_value` should either be zero or NaN. + persistent: Boolean specifying whether an ndarray should be retained + as a persistent buffer for gradients. + """ + if get_state is None: + # Note: Numpy supports copying data between ndarrays with different dtypes. + # Hence, our default behavior need not coerce the ndarray representations + # of tensors in `parameters` to float64 when copying over data. + _as_array = as_ndarray if as_array is None else as_array + get_state = partial( + get_tensors_as_ndarray_1d, + tensors=parameters, + dtype=np_float64, + as_array=_as_array, + ) + + if as_array is None: # per the note, do this after resolving `get_state` + as_array = partial(as_ndarray, dtype=np_float64) + + if set_state is None: + set_state = partial(set_tensors_from_ndarray_1d, parameters) + + self.closure = closure + self.parameters = parameters + + self.as_array = as_ndarray + self._get_state = get_state + self._set_state = set_state + + self.fill_value = fill_value + self.persistent = persistent + self._gradient_ndarray: npt.NDArray | None = None + + def __call__( + self, state: npt.NDArray | None = None, **kwargs: Any + ) -> tuple[npt.NDArray, npt.NDArray]: + if state is not None: + self.state = state + + try: + value_tensor, grad_tensors = self.closure(**kwargs) + value = self.as_array(value_tensor) + grads = self._get_gradient_ndarray(fill_value=self.fill_value) + index = 0 + for param, grad in zip(self.parameters.values(), grad_tensors): + size = param.numel() + if grad is not None: + grads[index : index + size] = self.as_array(grad.view(-1)) + index += size + except RuntimeError as e: + value, grads = _handle_numerical_errors(e, x=self.state, dtype=np_float64) + + return value, grads + + @property + def state(self) -> npt.NDArray: + return self._get_state() + + @state.setter + def state(self, state: npt.NDArray) -> None: + self._set_state(state) + + def _get_gradient_ndarray(self, fill_value: float | None = None) -> npt.NDArray: + if self.persistent and self._gradient_ndarray is not None: + if fill_value is not None: + self._gradient_ndarray.fill(fill_value) + return self._gradient_ndarray + + size = sum(param.numel() for param in self.parameters.values()) + array = ( + np_zeros(size, dtype=np_float64) + if fill_value is None or fill_value == 0.0 + else np_full(size, fill_value, dtype=np_float64) + ) + if self.persistent: + self._gradient_ndarray = array + + return array
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/closures/model_closures.html b/website-old/pages/api/_modules/botorch/optim/closures/model_closures.html new file mode 100644 index 0000000000..3e639c2490 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/closures/model_closures.html @@ -0,0 +1,281 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.closures.model_closures

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Utilities for building model-based closures."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Sequence
+from itertools import chain, repeat
+from types import NoneType
+from typing import Any
+
+from botorch.optim.closures.core import ForwardBackwardClosure
+from botorch.utils.dispatcher import Dispatcher, type_bypassing_encoder
+from gpytorch.mlls import (
+    ExactMarginalLogLikelihood,
+    MarginalLogLikelihood,
+    SumMarginalLogLikelihood,
+)
+from torch import Tensor
+from torch.utils.data import DataLoader
+
+GetLossClosure = Dispatcher("get_loss_closure", encoder=type_bypassing_encoder)
+GetLossClosureWithGrads = Dispatcher(
+    "get_loss_closure_with_grads", encoder=type_bypassing_encoder
+)
+
+
+
+[docs] +def get_loss_closure( + mll: MarginalLogLikelihood, + data_loader: DataLoader | None = None, + **kwargs: Any, +) -> Callable[[], Tensor]: + r"""Public API for GetLossClosure dispatcher. + + This method, and the dispatcher that powers it, acts as a clearing house + for factory functions that define how `mll` is evaluated. + + Users may specify custom evaluation routines by registering a factory function + with GetLossClosure. These factories should be registered using the type signature + + `Type[MarginalLogLikeLihood], Type[Likelihood], Type[Model], Type[DataLoader]`. + + The final argument, Type[DataLoader], is optional. Evaluation routines that obtain + training data from, e.g., `mll.model` should register this argument as `type(None)`. + + Args: + mll: A MarginalLogLikelihood instance whose negative defines the loss. + data_loader: An optional DataLoader instance for cases where training + data is passed in rather than obtained from `mll.model`. + + Returns: + A closure that takes zero positional arguments and returns the negated + value of `mll`. + """ + return GetLossClosure( + mll, type(mll.likelihood), type(mll.model), data_loader, **kwargs + )
+ + + +
+[docs] +def get_loss_closure_with_grads( + mll: MarginalLogLikelihood, + parameters: dict[str, Tensor], + data_loader: DataLoader | None = None, + backward: Callable[[Tensor], None] = Tensor.backward, + reducer: Callable[[Tensor], Tensor] | None = Tensor.sum, + context_manager: Callable | None = None, + **kwargs: Any, +) -> Callable[[], tuple[Tensor, tuple[Tensor, ...]]]: + r"""Public API for GetLossClosureWithGrads dispatcher. + + In most cases, this method simply adds a backward pass to a loss closure obtained by + calling `get_loss_closure`. For further details, see `get_loss_closure`. + + Args: + mll: A MarginalLogLikelihood instance whose negative defines the loss. + parameters: A dictionary of tensors whose `grad` fields are to be returned. + reducer: Optional callable used to reduce the output of the forward pass. + data_loader: An optional DataLoader instance for cases where training + data is passed in rather than obtained from `mll.model`. + context_manager: An optional ContextManager used to wrap each forward-backward + pass. Defaults to a `zero_grad_ctx` that zeroes the gradients of + `parameters` upon entry. None may be passed as an alias for `nullcontext`. + + Returns: + A closure that takes zero positional arguments and returns the reduced and + negated value of `mll` along with the gradients of `parameters`. + """ + return GetLossClosureWithGrads( + mll, + type(mll.likelihood), + type(mll.model), + data_loader, + parameters=parameters, + reducer=reducer, + backward=backward, + context_manager=context_manager, + **kwargs, + )
+ + + +@GetLossClosureWithGrads.register(object, object, object, object) +def _get_loss_closure_with_grads_fallback( + mll: MarginalLogLikelihood, + _likelihood_type: object, + _model_type: object, + data_loader: DataLoader | None, + parameters: dict[str, Tensor], + reducer: Callable[[Tensor], Tensor] = Tensor.sum, + backward: Callable[[Tensor], None] = Tensor.backward, + context_manager: Callable = None, # pyre-ignore [9] + **kwargs: Any, +) -> ForwardBackwardClosure: + r"""Wraps a `loss_closure` with a ForwardBackwardClosure.""" + loss_closure = get_loss_closure(mll, data_loader=data_loader, **kwargs) + return ForwardBackwardClosure( + forward=loss_closure, + backward=backward, + parameters=parameters, + reducer=reducer, + context_manager=context_manager, + ) + + +@GetLossClosure.register(MarginalLogLikelihood, object, object, DataLoader) +def _get_loss_closure_fallback_external( + mll: MarginalLogLikelihood, + _likelihood_type: object, + _model_type: object, + data_loader: DataLoader, + **ignore: Any, +) -> Callable[[], Tensor]: + r"""Fallback loss closure with externally provided data.""" + batch_generator = chain.from_iterable(iter(data_loader) for _ in repeat(None)) + + def closure(**kwargs: Any) -> Tensor: + batch = next(batch_generator) + if not isinstance(batch, Sequence): + raise TypeError( + "Expected `data_loader` to generate a batch of tensors, " + f"but found {type(batch)}." + ) + + num_inputs = len(mll.model.train_inputs) + model_output = mll.model(*batch[:num_inputs]) + log_likelihood = mll(model_output, *batch[num_inputs:], **kwargs) + return -log_likelihood + + return closure + + +@GetLossClosure.register(MarginalLogLikelihood, object, object, NoneType) +def _get_loss_closure_fallback_internal( + mll: MarginalLogLikelihood, _: object, __: object, ___: None, **ignore: Any +) -> Callable[[], Tensor]: + r"""Fallback loss closure with internally managed data.""" + + def closure(**kwargs: Any) -> Tensor: + model_output = mll.model(*mll.model.train_inputs) + log_likelihood = mll(model_output, mll.model.train_targets, **kwargs) + return -log_likelihood + + return closure + + +@GetLossClosure.register(ExactMarginalLogLikelihood, object, object, NoneType) +def _get_loss_closure_exact_internal( + mll: ExactMarginalLogLikelihood, _: object, __: object, ___: None, **ignore: Any +) -> Callable[[], Tensor]: + r"""ExactMarginalLogLikelihood loss closure with internally managed data.""" + + def closure(**kwargs: Any) -> Tensor: + model = mll.model + # The inputs will get transformed in forward here. + model_output = model(*model.train_inputs) + log_likelihood = mll( + model_output, + model.train_targets, + # During model training, the model inputs get transformed in the forward + # pass. The train_inputs property is not transformed yet, so we need to + # transform it before passing it to the likelihood for consistency. + *(model.transform_inputs(X=t_in) for t_in in model.train_inputs), + **kwargs, + ) + return -log_likelihood + + return closure + + +@GetLossClosure.register(SumMarginalLogLikelihood, object, object, NoneType) +def _get_loss_closure_sum_internal( + mll: SumMarginalLogLikelihood, _: object, __: object, ___: None, **ignore: Any +) -> Callable[[], Tensor]: + r"""SumMarginalLogLikelihood loss closure with internally managed data.""" + + def closure(**kwargs: Any) -> Tensor: + model = mll.model + # The inputs will get transformed in forward here. + model_output = model(*model.train_inputs) + log_likelihood = mll( + model_output, + model.train_targets, + # During model training, the model inputs get transformed in the forward + # pass. The train_inputs property is not transformed yet, so we need to + # transform it before passing it to the likelihood for consistency. + *( + (model.transform_inputs(X=t_in) for t_in in sub_t_in) + for sub_t_in in model.train_inputs + ), + **kwargs, + ) + return -log_likelihood + + return closure +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/core.html b/website-old/pages/api/_modules/botorch/optim/core.html new file mode 100644 index 0000000000..61f114cee5 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/core.html @@ -0,0 +1,310 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.core

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Core abstractions and generic optimizers."""
+
+from __future__ import annotations
+
+import re
+from collections.abc import Callable, Sequence
+from dataclasses import dataclass, replace
+from enum import auto, Enum
+from itertools import count
+from sys import maxsize
+from time import monotonic
+from typing import Any
+
+import numpy.typing as npt
+
+from botorch.optim.closures import NdarrayOptimizationClosure
+from botorch.optim.utils.numpy_utils import get_bounds_as_ndarray
+from botorch.optim.utils.timeout import minimize_with_timeout
+from numpy import asarray, float64 as np_float64
+from torch import Tensor
+from torch.optim.adam import Adam
+from torch.optim.optimizer import Optimizer
+
+try:
+    from torch.optim.lr_scheduler import LRScheduler
+except ImportError:  # pragma: no cover
+    from torch.optim.lr_scheduler import _LRScheduler as LRScheduler  # pragma: no cover
+
+
+_LBFGSB_MAXITER_MAXFUN_REGEX = re.compile(  # regex for maxiter and maxfun messages
+    "TOTAL NO. of (ITERATIONS REACHED LIMIT|f AND g EVALUATIONS EXCEEDS LIMIT)"
+)
+
+
+
+[docs] +class OptimizationStatus(int, Enum): + RUNNING = auto() # incomplete + SUCCESS = auto() # optimizer converged + FAILURE = auto() # terminated abnormally + STOPPED = auto() # stopped due to user provided criterion
+ + + +
+[docs] +@dataclass +class OptimizationResult: + step: int + fval: float | int + status: OptimizationStatus + runtime: float | None = None + message: str | None = None
+ + + +
+[docs] +def scipy_minimize( + closure: ( + Callable[[], tuple[Tensor, Sequence[Tensor | None]]] + | NdarrayOptimizationClosure + ), + parameters: dict[str, Tensor], + bounds: dict[str, tuple[float | None, float | None]] | None = None, + callback: Callable[[dict[str, Tensor], OptimizationResult], None] | None = None, + x0: npt.NDArray | None = None, + method: str = "L-BFGS-B", + options: dict[str, Any] | None = None, + timeout_sec: float | None = None, +) -> OptimizationResult: + r"""Generic scipy.optimize.minimize-based optimization routine. + + Args: + closure: Callable that returns a tensor and an iterable of gradient tensors or + NdarrayOptimizationClosure instance. + parameters: A dictionary of tensors to be optimized. + bounds: A dictionary mapping parameter names to lower and upper bounds. + callback: A callable taking `parameters` and an OptimizationResult as arguments. + x0: An optional initialization vector passed to scipy.optimize.minimize. + method: Solver type, passed along to scipy.minimize. + options: Dictionary of solver options, passed along to scipy.minimize. + timeout_sec: Timeout in seconds to wait before aborting the optimization loop + if not converged (will return the best found solution thus far). + + Returns: + An OptimizationResult summarizing the final state of the run. + """ + start_time = monotonic() + wrapped_closure = ( + closure + if isinstance(closure, NdarrayOptimizationClosure) + else NdarrayOptimizationClosure(closure, parameters) + ) + if bounds is None: + bounds_np = None + else: + bounds_np = get_bounds_as_ndarray(parameters, bounds) + + if callback is None: + wrapped_callback = None + else: + call_counter = count(1) # callbacks are typically made at the end of each iter + + def wrapped_callback(x: npt.NDArray): + result = OptimizationResult( + step=next(call_counter), + fval=float(wrapped_closure(x)[0]), + status=OptimizationStatus.RUNNING, + runtime=monotonic() - start_time, + ) + return callback(parameters, result) # pyre-ignore [29] + + raw = minimize_with_timeout( + wrapped_closure, + wrapped_closure.state if x0 is None else x0.astype(np_float64, copy=False), + jac=True, + bounds=bounds_np, + method=method, + options=options, + callback=wrapped_callback, + timeout_sec=timeout_sec, + ) + + # Post-processing and outcome handling + wrapped_closure.state = asarray(raw.x) # set parameter state to optimal values + msg = raw.message if isinstance(raw.message, str) else raw.message.decode("ascii") + if raw.success: + status = OptimizationStatus.SUCCESS + else: + status = ( # Check whether we stopped due to reaching maxfun or maxiter + OptimizationStatus.STOPPED + if _LBFGSB_MAXITER_MAXFUN_REGEX.search(msg) + or "Optimization timed out after" in msg + else OptimizationStatus.FAILURE + ) + + return OptimizationResult( + fval=raw.fun, + step=raw.nit, + status=status, + message=msg, + runtime=monotonic() - start_time, + )
+ + + +
+[docs] +def torch_minimize( + closure: Callable[[], tuple[Tensor, Sequence[Tensor | None]]], + parameters: dict[str, Tensor], + bounds: dict[str, tuple[float | None, float | None]] | None = None, + callback: Callable[[dict[str, Tensor], OptimizationResult], None] | None = None, + optimizer: Optimizer | Callable[[list[Tensor]], Optimizer] = Adam, + scheduler: LRScheduler | Callable[[Optimizer], LRScheduler] | None = None, + step_limit: int | None = None, + timeout_sec: float | None = None, + stopping_criterion: Callable[[Tensor], bool] | None = None, +) -> OptimizationResult: + r"""Generic torch.optim-based optimization routine. + + Args: + closure: Callable that returns a tensor and an iterable of gradient tensors. + Responsible for setting relevant parameters' `grad` attributes. + parameters: A dictionary of tensors to be optimized. + bounds: An optional dictionary of bounds for elements of `parameters`. + callback: A callable taking `parameters` and an OptimizationResult as arguments. + optimizer: A `torch.optim.Optimizer` instance or a factory that takes + a list of parameters and returns an `Optimizer` instance. + scheduler: A `torch.optim.lr_scheduler._LRScheduler` instance or a factory + that takes a `Optimizer` instance and returns a `_LRSchedule` instance. + step_limit: Integer specifying a maximum number of optimization steps. + One of `step_limit`, `stopping_criterion`, or `timeout_sec` must be passed. + timeout_sec: Timeout in seconds before terminating the optimization loop. + One of `step_limit`, `stopping_criterion`, or `timeout_sec` must be passed. + stopping_criterion: A StoppingCriterion for the optimization loop. + + Returns: + An OptimizationResult summarizing the final state of the run. + """ + result: OptimizationResult + start_time = monotonic() + + if step_limit is None: + if stopping_criterion is None and timeout_sec is None: + raise RuntimeError("No termination conditions were given.") + step_limit = maxsize + + if not isinstance(optimizer, Optimizer): + optimizer = optimizer(list(parameters.values())) + + if not (scheduler is None or isinstance(scheduler, LRScheduler)): + scheduler = scheduler(optimizer) + + _bounds = ( + {} + if bounds is None + else {name: limits for name, limits in bounds.items() if name in parameters} + ) + for step in range(1, step_limit + 1): + fval = closure()[0].detach() + runtime = monotonic() - start_time + result = OptimizationResult( + step=step, + fval=fval.cpu().item(), + status=OptimizationStatus.RUNNING, + runtime=runtime, + ) + + # TODO: Update stopping_criterion API to return a message. + if stopping_criterion and stopping_criterion(fval): + result.status = OptimizationStatus.STOPPED + result.message = "`torch_minimize` stopped due to `stopping_criterion`." + + if timeout_sec is not None and runtime >= timeout_sec: + result.status = OptimizationStatus.STOPPED + result.message = ( + f"`torch_minimize` stopped due to timeout after {runtime} seconds." + ) + + if callback: + callback(parameters, result) + + if result.status != OptimizationStatus.RUNNING: + break + + optimizer.step() + for name, (lower, upper) in _bounds.items(): + parameters[name].data = parameters[name].clamp(min=lower, max=upper) + + if scheduler: + scheduler.step() + + if result.status != OptimizationStatus.RUNNING: + return replace(result, runtime=monotonic() - start_time) + + # Account for final parameter update when stopping due to step_limit + return OptimizationResult( + step=step, + fval=closure()[0].detach().cpu().item(), + status=OptimizationStatus.STOPPED, + runtime=monotonic() - start_time, + message=f"`torch_minimize` stopped after reaching step_limit={step_limit}.", + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/fit.html b/website-old/pages/api/_modules/botorch/optim/fit.html new file mode 100644 index 0000000000..b48cf8b949 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/fit.html @@ -0,0 +1,240 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.fit

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Tools for model fitting."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Sequence
+
+from functools import partial
+from typing import Any, Optional
+from warnings import warn
+
+from botorch.exceptions.warnings import OptimizationWarning
+from botorch.optim.closures import get_loss_closure_with_grads
+from botorch.optim.core import (
+    OptimizationResult,
+    OptimizationStatus,
+    scipy_minimize,
+    torch_minimize,
+)
+from botorch.optim.stopping import ExpMAStoppingCriterion
+from botorch.optim.utils import get_parameters_and_bounds, TorchAttr
+from botorch.utils.types import DEFAULT
+from gpytorch.mlls.marginal_log_likelihood import MarginalLogLikelihood
+from numpy import ndarray
+from torch import Tensor
+from torch.nn import Module
+from torch.optim.adam import Adam
+from torch.optim.lr_scheduler import _LRScheduler
+from torch.optim.optimizer import Optimizer
+
+TBoundsDict = dict[str, tuple[Optional[float], Optional[float]]]
+TScipyObjective = Callable[
+    [ndarray, MarginalLogLikelihood, dict[str, TorchAttr]], tuple[float, ndarray]
+]
+TModToArray = Callable[
+    [Module, Optional[TBoundsDict], Optional[set[str]]],
+    tuple[ndarray, dict[str, TorchAttr], Optional[ndarray]],
+]
+TArrayToMod = Callable[[Module, ndarray, dict[str, TorchAttr]], Module]
+
+
+
+[docs] +def fit_gpytorch_mll_scipy( + mll: MarginalLogLikelihood, + parameters: dict[str, Tensor] | None = None, + bounds: dict[str, tuple[float | None, float | None]] | None = None, + closure: Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None = None, + closure_kwargs: dict[str, Any] | None = None, + method: str = "L-BFGS-B", + options: dict[str, Any] | None = None, + callback: Callable[[dict[str, Tensor], OptimizationResult], None] | None = None, + timeout_sec: float | None = None, +) -> OptimizationResult: + r"""Generic scipy.optimized-based fitting routine for GPyTorch MLLs. + + The model and likelihood in mll must already be in train mode. + + Args: + mll: MarginalLogLikelihood to be maximized. + parameters: Optional dictionary of parameters to be optimized. Defaults + to all parameters of `mll` that require gradients. + bounds: A dictionary of user-specified bounds for `parameters`. Used to update + default parameter bounds obtained from `mll`. + closure: Callable that returns a tensor and an iterable of gradient tensors. + Responsible for setting the `grad` attributes of `parameters`. If no closure + is provided, one will be obtained by calling `get_loss_closure_with_grads`. + closure_kwargs: Keyword arguments passed to `closure`. + method: Solver type, passed along to scipy.minimize. + options: Dictionary of solver options, passed along to scipy.minimize. + callback: Optional callback taking `parameters` and an OptimizationResult as its + sole arguments. + timeout_sec: Timeout in seconds after which to terminate the fitting loop + (note that timing out can result in bad fits!). + + Returns: + The final OptimizationResult. + """ + # Resolve `parameters` and update default bounds + _parameters, _bounds = get_parameters_and_bounds(mll) + bounds = _bounds if bounds is None else {**_bounds, **bounds} + if parameters is None: + parameters = {n: p for n, p in _parameters.items() if p.requires_grad} + + if closure is None: + closure = get_loss_closure_with_grads(mll, parameters=parameters) + + if closure_kwargs is not None: + closure = partial(closure, **closure_kwargs) + + result = scipy_minimize( + closure=closure, + parameters=parameters, + bounds=bounds, + method=method, + options=options, + callback=callback, + timeout_sec=timeout_sec, + ) + if result.status != OptimizationStatus.SUCCESS: + warn( + f"`scipy_minimize` terminated with status {result.status}, displaying" + f" original message from `scipy.optimize.minimize`: {result.message}", + OptimizationWarning, + ) + + return result
+ + + +
+[docs] +def fit_gpytorch_mll_torch( + mll: MarginalLogLikelihood, + parameters: dict[str, Tensor] | None = None, + bounds: dict[str, tuple[float | None, float | None]] | None = None, + closure: Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None = None, + closure_kwargs: dict[str, Any] | None = None, + step_limit: int | None = None, + stopping_criterion: Callable[[Tensor], bool] | None = DEFAULT, # pyre-ignore [9] + optimizer: Optimizer | Callable[..., Optimizer] = Adam, + scheduler: _LRScheduler | Callable[..., _LRScheduler] | None = None, + callback: Callable[[dict[str, Tensor], OptimizationResult], None] | None = None, + timeout_sec: float | None = None, +) -> OptimizationResult: + r"""Generic torch.optim-based fitting routine for GPyTorch MLLs. + + Args: + mll: MarginalLogLikelihood to be maximized. + parameters: Optional dictionary of parameters to be optimized. Defaults + to all parameters of `mll` that require gradients. + bounds: A dictionary of user-specified bounds for `parameters`. Used to update + default parameter bounds obtained from `mll`. + closure: Callable that returns a tensor and an iterable of gradient tensors. + Responsible for setting the `grad` attributes of `parameters`. If no closure + is provided, one will be obtained by calling `get_loss_closure_with_grads`. + closure_kwargs: Keyword arguments passed to `closure`. + step_limit: Optional upper bound on the number of optimization steps. + stopping_criterion: A StoppingCriterion for the optimization loop. + optimizer: A `torch.optim.Optimizer` instance or a factory that takes + a list of parameters and returns an `Optimizer` instance. + scheduler: A `torch.optim.lr_scheduler._LRScheduler` instance or a factory + that takes an `Optimizer` instance and returns an `_LRSchedule`. + callback: Optional callback taking `parameters` and an OptimizationResult as its + sole arguments. + timeout_sec: Timeout in seconds after which to terminate the fitting loop + (note that timing out can result in bad fits!). + + Returns: + The final OptimizationResult. + """ + if stopping_criterion == DEFAULT: + stopping_criterion = ExpMAStoppingCriterion() + + # Resolve `parameters` and update default bounds + param_dict, bounds_dict = get_parameters_and_bounds(mll) + if parameters is None: + parameters = {n: p for n, p in param_dict.items() if p.requires_grad} + + if closure is None: + closure = get_loss_closure_with_grads(mll, parameters) + + if closure_kwargs is not None: + closure = partial(closure, **closure_kwargs) + + return torch_minimize( + closure=closure, + parameters=parameters, + bounds=bounds_dict if bounds is None else {**bounds_dict, **bounds}, + optimizer=optimizer, + scheduler=scheduler, + step_limit=step_limit, + stopping_criterion=stopping_criterion, + callback=callback, + timeout_sec=timeout_sec, + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/homotopy.html b/website-old/pages/api/_modules/botorch/optim/homotopy.html new file mode 100644 index 0000000000..4521a0617c --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/homotopy.html @@ -0,0 +1,248 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.homotopy

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import math
+from collections.abc import Callable
+from dataclasses import dataclass
+
+import torch
+from torch import Tensor
+from torch.nn import Parameter
+
+
+
+[docs] +class FixedHomotopySchedule: + """Homotopy schedule with a fixed list of values.""" + + def __init__(self, values: list[float]) -> None: + r"""Initialize FixedHomotopySchedule. + + Args: + values: A list of values used in homotopy + """ + self._values = values + self.idx = 0 + + @property + def num_steps(self) -> int: + return len(self._values) + + @property + def value(self) -> float: + return self._values[self.idx] + + @property + def should_stop(self) -> bool: + return self.idx == len(self._values) + +
+[docs] + def restart(self) -> None: + self.idx = 0
+ + +
+[docs] + def step(self) -> None: + self.idx += 1
+
+ + + +
+[docs] +class LinearHomotopySchedule(FixedHomotopySchedule): + """Linear homotopy schedule.""" + + def __init__(self, start: float, end: float, num_steps: int) -> None: + r"""Initialize LinearHomotopySchedule. + + Args: + start: start value of homotopy + end: end value of homotopy + num_steps: number of steps in the homotopy schedule. + """ + super().__init__( + values=torch.linspace(start, end, num_steps, dtype=torch.double).tolist() + )
+ + + +
+[docs] +class LogLinearHomotopySchedule(FixedHomotopySchedule): + """Log-linear homotopy schedule.""" + + def __init__(self, start: float, end: float, num_steps: int): + r"""Initialize LogLinearHomotopySchedule. + + Args: + start: start value of homotopy + end: end value of homotopy + num_steps: number of steps in the homotopy schedule. + """ + super().__init__( + values=torch.logspace( + math.log10(start), math.log10(end), num_steps, dtype=torch.double + ).tolist() + )
+ + + +
+[docs] +@dataclass +class HomotopyParameter: + r"""Homotopy parameter. + + The parameter is expected to either be a torch parameter or a torch tensor which may + correspond to a buffer of a module. The parameter has a corresponding schedule. + """ + + parameter: Parameter | Tensor + schedule: FixedHomotopySchedule
+ + + +
+[docs] +class Homotopy: + """Generic homotopy class. + + This class is designed to be used in `optimize_acqf_homotopy`. Given a set of + homotopy parameters and corresponding schedules we step through the homotopies + until we have solved the final problem. We additionally support passing in a list + of callbacks that will be executed each time `step`, `reset`, and `restart` are + called. + """ + + def __init__( + self, + homotopy_parameters: list[HomotopyParameter], + callbacks: list[Callable] | None = None, + ) -> None: + r"""Initialize the homotopy. + + Args: + homotopy_parameters: List of homotopy parameters + callbacks: Optional list of callbacks that are executed each time + `restart`, `reset`, or `step` are called. These may be used to, e.g., + reinitialize the acquisition function which is needed when using qNEHVI. + """ + self._homotopy_parameters = homotopy_parameters + self._callbacks = callbacks or [] + self._original_values = [ + hp.parameter.item() for hp in self._homotopy_parameters + ] + assert all( + isinstance(hp.parameter, Parameter) or isinstance(hp.parameter, Tensor) + for hp in self._homotopy_parameters + ) + # Assume the same number of steps for now + assert len({h.schedule.num_steps for h in self._homotopy_parameters}) == 1 + # Initialize the homotopy parameters + self.restart() + + def _execute_callbacks(self) -> None: + """Execute the callbacks.""" + for callback in self._callbacks: + callback() + + @property + def should_stop(self) -> bool: + """Returns true if all schedules have reached the end.""" + return all(h.schedule.should_stop for h in self._homotopy_parameters) + +
+[docs] + def restart(self) -> None: + """Restart the homotopy to use the initial value in the schedule.""" + for hp in self._homotopy_parameters: + hp.schedule.restart() + hp.parameter.data.fill_(hp.schedule.value) + self._execute_callbacks()
+ + +
+[docs] + def reset(self) -> None: + """Reset the homotopy parameter to their original values.""" + for hp, val in zip(self._homotopy_parameters, self._original_values): + hp.parameter.data.fill_(val) + self._execute_callbacks()
+ + +
+[docs] + def step(self) -> None: + """Take a step according to the schedules.""" + for hp in self._homotopy_parameters: + hp.schedule.step() + if not hp.schedule.should_stop: + hp.parameter.data.fill_(hp.schedule.value) + self._execute_callbacks()
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/initializers.html b/website-old/pages/api/_modules/botorch/optim/initializers.html new file mode 100644 index 0000000000..ccd4da8eef --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/initializers.html @@ -0,0 +1,1421 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.initializers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+References
+
+.. [Regis]
+    R. G. Regis, C. A. Shoemaker. Combining radial basis function
+    surrogates and dynamic coordinate search in high-dimensional
+    expensive black-box optimization, Engineering Optimization, 2013.
+"""
+
+from __future__ import annotations
+
+import warnings
+from collections.abc import Callable
+from math import ceil
+from typing import Optional, Union
+
+import torch
+from botorch.acquisition import analytic, monte_carlo, multi_objective
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction
+from botorch.acquisition.knowledge_gradient import (
+    _get_value_function,
+    qKnowledgeGradient,
+)
+from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import (
+    _get_hv_value_function,
+    qHypervolumeKnowledgeGradient,
+    qMultiFidelityHypervolumeKnowledgeGradient,
+)
+from botorch.exceptions.errors import BotorchTensorDimensionError, UnsupportedError
+from botorch.exceptions.warnings import (
+    BadInitialCandidatesWarning,
+    BotorchWarning,
+    SamplingWarning,
+)
+from botorch.models.model import Model
+from botorch.optim.utils import fix_features, get_X_baseline
+from botorch.utils.multi_objective.pareto import is_non_dominated
+from botorch.utils.sampling import (
+    batched_multinomial,
+    draw_sobol_samples,
+    get_polytope_samples,
+    manual_seed,
+)
+from botorch.utils.transforms import normalize, standardize, unnormalize
+from torch import Tensor
+from torch.distributions import Normal
+from torch.quasirandom import SobolEngine
+
+TGenInitialConditions = Callable[
+    [
+        # reasoning behind this annotation: contravariance
+        qKnowledgeGradient,
+        Tensor,
+        int,
+        int,
+        int,
+        Optional[dict[int, float]],
+        Optional[dict[str, Union[bool, float, int]]],
+        Optional[list[tuple[Tensor, Tensor, float]]],
+        Optional[list[tuple[Tensor, Tensor, float]]],
+    ],
+    Optional[Tensor],
+]
+
+
+
+[docs] +def transform_constraints( + constraints: list[tuple[Tensor, Tensor, float]] | None, q: int, d: int +) -> list[tuple[Tensor, Tensor, float]] | None: + r"""Transform constraints to sample from a d*q-dimensional space instead of a + d-dimensional state. + + This function assumes that constraints are the same for each input batch, + and broadcasts the constraints accordingly to the input batch shape. + + Args: + constraints: A list of tuples (indices, coefficients, rhs), with each tuple + encoding an (in-)equality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) (>)= rhs`. + If `indices` is a 2-d Tensor, this supports specifying constraints across + the points in the `q`-batch (inter-point constraints). If `None`, this + function is a nullop and simply returns `None`. + q: Size of the `q`-batch. + d: Dimensionality of the problem. + + Returns: + List[Tuple[Tensor, Tensor, float]]: List of transformed constraints, if + there are constraints. Returns `None` otherwise. + """ + if constraints is None: + return None + transformed = [] + for constraint in constraints: + if len(constraint[0].shape) == 1: + transformed += transform_intra_point_constraint(constraint, d, q) + else: + transformed.append(transform_inter_point_constraint(constraint, d)) + return transformed
+ + + +
+[docs] +def transform_intra_point_constraint( + constraint: tuple[Tensor, Tensor, float], d: int, q: int +) -> list[tuple[Tensor, Tensor, float]]: + r"""Transforms an intra-point/pointwise constraint from + d-dimensional space to a d*q-dimesional space. + + Args: + constraints: A list of tuples (indices, coefficients, rhs), with each tuple + encoding an (in-)equality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) (>)= rhs`. Here `indices` must + be one-dimensional, and the constraint is applied to all points within the + `q`-batch. + d: Dimensionality of the problem. + + Raises: + ValueError: If indices in the constraints are larger than the + dimensionality d of the problem. + + Returns: + List[Tuple[Tensor, Tensor, float]]: List of transformed constraints. + """ + indices, coefficients, rhs = constraint + if indices.max() >= d: + raise ValueError( + f"Constraint indices cannot exceed the problem dimension {d=}." + ) + return [ + ( + torch.tensor( + [i * d + j for j in indices], dtype=torch.int64, device=indices.device + ), + coefficients, + rhs, + ) + for i in range(q) + ]
+ + + +
+[docs] +def transform_inter_point_constraint( + constraint: tuple[Tensor, Tensor, float], d: int +) -> tuple[Tensor, Tensor, float]: + r"""Transforms an inter-point constraint from + d-dimensional space to a d*q dimesional space. + + Args: + constraints: A list of tuples (indices, coefficients, rhs), with each tuple + encoding an (in-)equality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) (>)= rhs`. `indices` must be a + 2-d Tensor, where in each row `indices[i] = (k_i, l_i)` the first index + `k_i` corresponds to the `k_i`-th element of the `q`-batch and the second + index `l_i` corresponds to the `l_i`-th feature of that element. + + Raises: + ValueError: If indices in the constraints are larger than the + dimensionality d of the problem. + + Returns: + List[Tuple[Tensor, Tensor, float]]: Transformed constraint. + """ + indices, coefficients, rhs = constraint + if indices[:, 1].max() >= d: + raise ValueError( + f"Constraint indices cannot exceed the problem dimension {d=}." + ) + return ( + torch.tensor( + [r[0] * d + r[1] for r in indices], dtype=torch.int64, device=indices.device + ), + coefficients, + rhs, + )
+ + + +
+[docs] +def sample_q_batches_from_polytope( + n: int, + q: int, + bounds: Tensor, + n_burnin: int, + n_thinning: int, + seed: int | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, +) -> Tensor: + r"""Samples `n` q-baches from a polytope of dimension `d`. + + Args: + n: Number of q-batches to sample. + q: Number of samples per q-batch + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X`. + n_burnin: The number of burn-in samples for the Markov chain sampler. + n_thinning: The amount of thinning. The sampler will return every + `n_thinning` sample (after burn-in). + seed: The random seed. + inequality_constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + equality_constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + + Returns: + A `n x q x d`-dim tensor of samples. + """ + + # check if inter-point constraints are present + inter_point = any( + len(indices.shape) > 1 + for constraints in (inequality_constraints or [], equality_constraints or []) + for indices, _, _ in constraints + ) + + if inter_point: + samples = get_polytope_samples( + n=n, + bounds=torch.hstack([bounds for _ in range(q)]), + inequality_constraints=transform_constraints( + constraints=inequality_constraints, q=q, d=bounds.shape[1] + ), + equality_constraints=transform_constraints( + constraints=equality_constraints, q=q, d=bounds.shape[1] + ), + seed=seed, + n_burnin=n_burnin, + n_thinning=n_thinning * q, + ) + else: + samples = get_polytope_samples( + n=n * q, + bounds=bounds, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + seed=seed, + n_burnin=n_burnin, + n_thinning=n_thinning, + ) + return samples.view(n, q, -1).cpu()
+ + + +
+[docs] +def gen_batch_initial_conditions( + acq_function: AcquisitionFunction, + bounds: Tensor, + q: int, + num_restarts: int, + raw_samples: int, + fixed_features: dict[int, float] | None = None, + options: dict[str, bool | float | int] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + generator: Callable[[int, int, int | None], Tensor] | None = None, + fixed_X_fantasies: Tensor | None = None, +) -> Tensor: + r"""Generate a batch of initial conditions for random-restart optimziation. + + TODO: Support t-batches of initial conditions. + + Args: + acq_function: The acquisition function to be optimized. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X`. + q: The number of candidates to consider. + num_restarts: The number of starting points for multistart acquisition + function optimization. + raw_samples: The number of raw samples to consider in the initialization + heuristic. Note: if `sample_around_best` is True (the default is False), + then `2 * raw_samples` samples are used. + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. + options: Options for initial condition generation. For valid options see + `initialize_q_batch` and `initialize_q_batch_nonneg`. If `options` + contains a `nonnegative=True` entry, then `acq_function` is + assumed to be non-negative (useful when using custom acquisition + functions). In addition, an "init_batch_limit" option can be passed + to specify the batch limit for the initialization. This is useful + for avoiding memory limits when computing the batch posterior over + raw samples. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + generator: Callable for generating samples that are then further + processed. It receives `n`, `q` and `seed` as arguments + and returns a tensor of shape `n x q x d`. + fixed_X_fantasies: A fixed set of fantasy points to concatenate to + the `q` candidates being initialized along the `-2` dimension. The + shape should be `num_pseudo_points x d`. E.g., this should be + `num_fantasies x d` for KG and `num_fantasies*num_pareto x d` + for HVKG. + + Returns: + A `num_restarts x q x d` tensor of initial conditions. + + Example: + >>> qEI = qExpectedImprovement(model, best_f=0.2) + >>> bounds = torch.tensor([[0.], [1.]]) + >>> Xinit = gen_batch_initial_conditions( + >>> qEI, bounds, q=3, num_restarts=25, raw_samples=500 + >>> ) + """ + if bounds.isinf().any(): + raise NotImplementedError( + "Currently only finite values in `bounds` are supported " + "for generating initial conditions for optimization." + ) + options = options or {} + sample_around_best = options.get("sample_around_best", False) + if sample_around_best and equality_constraints: + raise UnsupportedError( + "Option 'sample_around_best' is not supported when equality" + "constraints are present." + ) + if sample_around_best and generator: + raise UnsupportedError( + "Option 'sample_around_best' is not supported when custom " + "generator is be used." + ) + seed: int | None = options.get("seed") + batch_limit: int | None = options.get( + "init_batch_limit", options.get("batch_limit") + ) + factor, max_factor = 1, 5 + init_kwargs = {} + device = bounds.device + bounds_cpu = bounds.cpu() + if "eta" in options: + init_kwargs["eta"] = options.get("eta") + if options.get("nonnegative") or is_nonnegative(acq_function): + init_func = initialize_q_batch_nonneg + if "alpha" in options: + init_kwargs["alpha"] = options.get("alpha") + else: + init_func = initialize_q_batch + + q = 1 if q is None else q + # the dimension the samples are drawn from + effective_dim = bounds.shape[-1] * q + if effective_dim > SobolEngine.MAXDIM: + warnings.warn( + f"Sample dimension q*d={effective_dim} exceeding Sobol max dimension " + f"({SobolEngine.MAXDIM}). Using iid samples instead.", + SamplingWarning, + stacklevel=3, + ) + + while factor < max_factor: + with warnings.catch_warnings(record=True) as ws: + n = raw_samples * factor + if generator is not None: + X_rnd = generator(n, q, seed) + # check if no constraints are provided + elif not (inequality_constraints or equality_constraints): + if effective_dim <= SobolEngine.MAXDIM: + X_rnd = draw_sobol_samples(bounds=bounds_cpu, n=n, q=q, seed=seed) + else: + with manual_seed(seed): + # load on cpu + X_rnd_nlzd = torch.rand( + n, q, bounds_cpu.shape[-1], dtype=bounds.dtype + ) + X_rnd = bounds_cpu[0] + (bounds_cpu[1] - bounds_cpu[0]) * X_rnd_nlzd + else: + X_rnd = sample_q_batches_from_polytope( + n=n, + q=q, + bounds=bounds, + n_burnin=options.get("n_burnin", 10000), + n_thinning=options.get("n_thinning", 32), + seed=seed, + equality_constraints=equality_constraints, + inequality_constraints=inequality_constraints, + ) + # sample points around best + if sample_around_best: + X_best_rnd = sample_points_around_best( + acq_function=acq_function, + n_discrete_points=n * q, + sigma=options.get("sample_around_best_sigma", 1e-3), + bounds=bounds, + subset_sigma=options.get("sample_around_best_subset_sigma", 1e-1), + prob_perturb=options.get("sample_around_best_prob_perturb"), + ) + if X_best_rnd is not None: + X_rnd = torch.cat( + [ + X_rnd, + X_best_rnd.view(n, q, bounds.shape[-1]).cpu(), + ], + dim=0, + ) + # Keep X on CPU for consistency & to limit GPU memory usage. + X_rnd = fix_features(X_rnd, fixed_features=fixed_features).cpu() + if fixed_X_fantasies is not None: + if (d_f := fixed_X_fantasies.shape[-1]) != (d_r := X_rnd.shape[-1]): + raise BotorchTensorDimensionError( + "`fixed_X_fantasies` and `bounds` must both have the same " + f"trailing dimension `d`, but have {d_f} and {d_r}, " + "respectively." + ) + X_rnd = torch.cat( + [ + X_rnd, + fixed_X_fantasies.cpu() + .unsqueeze(0) + .expand(X_rnd.shape[0], *fixed_X_fantasies.shape), + ], + dim=-2, + ) + with torch.no_grad(): + if batch_limit is None: + batch_limit = X_rnd.shape[0] + # Evaluate the acquisition function on `X_rnd` using `batch_limit` + # sized chunks. + acq_vals = torch.cat( + [ + acq_function(x_.to(device=device)).cpu() + for x_ in X_rnd.split(split_size=batch_limit, dim=0) + ], + dim=0, + ) + batch_initial_conditions, _ = init_func( + X=X_rnd, acq_vals=acq_vals, n=num_restarts, **init_kwargs + ) + batch_initial_conditions = batch_initial_conditions.to(device=device) + if not any(issubclass(w.category, BadInitialCandidatesWarning) for w in ws): + return batch_initial_conditions + if factor < max_factor: + factor += 1 + if seed is not None: + seed += 1 # make sure to sample different X_rnd + warnings.warn( + "Unable to find non-zero acquisition function values - initial conditions " + "are being selected randomly.", + BadInitialCandidatesWarning, + stacklevel=2, + ) + return batch_initial_conditions
+ + + +
+[docs] +def gen_one_shot_kg_initial_conditions( + acq_function: qKnowledgeGradient, + bounds: Tensor, + q: int, + num_restarts: int, + raw_samples: int, + fixed_features: dict[int, float] | None = None, + options: dict[str, bool | float | int] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, +) -> Tensor | None: + r"""Generate a batch of smart initializations for qKnowledgeGradient. + + This function generates initial conditions for optimizing one-shot KG using + the maximizer of the posterior objective. Intutively, the maximizer of the + fantasized posterior will often be close to a maximizer of the current + posterior. This function uses that fact to generate the initial conditions + for the fantasy points. Specifically, a fraction of `1 - frac_random` (see + options) is generated by sampling from the set of maximizers of the + posterior objective (obtained via random restart optimization) according to + a softmax transformation of their respective values. This means that this + initialization strategy internally solves an acquisition function + maximization problem. The remaining `frac_random` fantasy points as well as + all `q` candidate points are chosen according to the standard initialization + strategy in `gen_batch_initial_conditions`. + + Args: + acq_function: The qHypervolumeKnowledgeGradient instance to be optimized. + bounds: A `2 x d` tensor of lower and upper bounds for each column of + task features. + q: The number of candidates to consider. + num_restarts: The number of starting points for multistart acquisition + function optimization. + raw_samples: The number of raw samples to consider in the initialization + heuristic. + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. + options: Options for initial condition generation. These contain all + settings for the standard heuristic initialization from + `gen_batch_initial_conditions`. In addition, they contain + `frac_random` (the fraction of fully random fantasy points), + `num_inner_restarts` and `raw_inner_samples` (the number of random + restarts and raw samples for solving the posterior objective + maximization problem, respectively) and `eta` (temperature parameter + for sampling heuristic from posterior objective maximizers). + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + + Returns: + A `num_restarts x q' x d` tensor that can be used as initial conditions + for `optimize_acqf()`. Here `q' = q + num_fantasies` is the total number + of points (candidate points plus fantasy points). + + Example: + >>> qHVKG = qHypervolumeKnowledgeGradient(model, ref_point=num_fantasies=64) + >>> bounds = torch.tensor([[0., 0.], [1., 1.]]) + >>> Xinit = gen_one_shot_hvkg_initial_conditions( + >>> qHVKG, bounds, q=3, num_restarts=10, raw_samples=512, + >>> options={"frac_random": 0.25}, + >>> ) + """ + options = options or {} + frac_random: float = options.get("frac_random", 0.1) + if not 0 < frac_random < 1: + raise ValueError( + f"frac_random must take on values in (0,1). Value: {frac_random}" + ) + q_aug = acq_function.get_augmented_q_batch_size(q=q) + + # TODO: Avoid unnecessary computation by not generating all candidates + ics = gen_batch_initial_conditions( + acq_function=acq_function, + bounds=bounds, + q=q_aug, + num_restarts=num_restarts, + raw_samples=raw_samples, + fixed_features=fixed_features, + options=options, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + ) + + # compute maximizer of the value function + value_function = _get_value_function( + model=acq_function.model, + objective=acq_function.objective, + posterior_transform=acq_function.posterior_transform, + sampler=acq_function.inner_sampler, + project=getattr(acq_function, "project", None), + ) + from botorch.optim.optimize import optimize_acqf + + fantasy_cands, fantasy_vals = optimize_acqf( + acq_function=value_function, + bounds=bounds, + q=1, + num_restarts=options.get("num_inner_restarts", 20), + raw_samples=options.get("raw_inner_samples", 1024), + fixed_features=fixed_features, + return_best_only=False, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + ) + + # sampling from the optimizers + n_value = int((1 - frac_random) * (q_aug - q)) # number of non-random ICs + eta = options.get("eta", 2.0) + weights = torch.exp(eta * standardize(fantasy_vals)) + idx = torch.multinomial(weights, num_restarts * n_value, replacement=True) + + # set the respective initial conditions to the sampled optimizers + ics[..., -n_value:, :] = fantasy_cands[idx, 0].view(num_restarts, n_value, -1) + return ics
+ + + +
+[docs] +def gen_one_shot_hvkg_initial_conditions( + acq_function: qHypervolumeKnowledgeGradient, + bounds: Tensor, + q: int, + num_restarts: int, + raw_samples: int, + fixed_features: dict[int, float] | None = None, + options: dict[str, bool | float | int] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, +) -> Tensor | None: + r"""Generate a batch of smart initializations for qHypervolumeKnowledgeGradient. + + This function generates initial conditions for optimizing one-shot HVKG using + the hypervolume maximizing set (of fixed size) under the posterior mean. + Intutively, the hypervolume maximizing set of the fantasized posterior mean + will often be close to a hypervolume maximizing set under the current posterior + mean. This function uses that fact to generate the initial conditions + for the fantasy points. Specifically, a fraction of `1 - frac_random` (see + options) of the restarts are generated by learning the hypervolume maximizing sets + under the current posterior mean, where each hypervolume maximizing set is + obtained from maximizing the hypervolume from a different starting point. Given + a hypervolume maximizing set, the `q` candidate points are selected using to the + standard initialization strategy in `gen_batch_initial_conditions`, with the fixed + hypervolume maximizing set. The remaining `frac_random` restarts fantasy points + as well as all `q` candidate points are chosen according to the standard + initialization strategy in `gen_batch_initial_conditions`. + + Args: + acq_function: The qKnowledgeGradient instance to be optimized. + bounds: A `2 x d` tensor of lower and upper bounds for each column of + task features. + q: The number of candidates to consider. + num_restarts: The number of starting points for multistart acquisition + function optimization. + raw_samples: The number of raw samples to consider in the initialization + heuristic. + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. + options: Options for initial condition generation. These contain all + settings for the standard heuristic initialization from + `gen_batch_initial_conditions`. In addition, they contain + `frac_random` (the fraction of fully random fantasy points), + `num_inner_restarts` and `raw_inner_samples` (the number of random + restarts and raw samples for solving the posterior objective + maximization problem, respectively) and `eta` (temperature parameter + for sampling heuristic from posterior objective maximizers). + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + + Returns: + A `num_restarts x q' x d` tensor that can be used as initial conditions + for `optimize_acqf()`. Here `q' = q + num_fantasies` is the total number + of points (candidate points plus fantasy points). + + Example: + >>> qHVKG = qHypervolumeKnowledgeGradient(model, ref_point) + >>> bounds = torch.tensor([[0., 0.], [1., 1.]]) + >>> Xinit = gen_one_shot_hvkg_initial_conditions( + >>> qHVKG, bounds, q=3, num_restarts=10, raw_samples=512, + >>> options={"frac_random": 0.25}, + >>> ) + """ + from botorch.optim.optimize import optimize_acqf + + options = options or {} + frac_random: float = options.get("frac_random", 0.1) + if not 0 < frac_random < 1: + raise ValueError( + f"frac_random must take on values in (0,1). Value: {frac_random}" + ) + + value_function = _get_hv_value_function( + model=acq_function.model, + ref_point=acq_function.ref_point, + objective=acq_function.objective, + sampler=acq_function.inner_sampler, + use_posterior_mean=acq_function.use_posterior_mean, + ) + + is_mf_hvkg = isinstance(acq_function, qMultiFidelityHypervolumeKnowledgeGradient) + if is_mf_hvkg: + dim = bounds.shape[-1] + fidelity_dims, fidelity_targets = zip(*acq_function.target_fidelities.items()) + value_function = FixedFeatureAcquisitionFunction( + acq_function=value_function, + d=dim, + columns=fidelity_dims, + values=fidelity_targets, + ) + + non_fidelity_dims = list(set(range(dim)) - set(fidelity_dims)) + + num_optim_restarts = int(round(num_restarts * (1 - frac_random))) + fantasy_cands, fantasy_vals = optimize_acqf( + acq_function=value_function, + bounds=bounds[:, non_fidelity_dims] if is_mf_hvkg else bounds, + q=acq_function.num_pareto, + num_restarts=options.get("num_inner_restarts", 20), + raw_samples=options.get("raw_inner_samples", 1024), + fixed_features=fixed_features, + return_best_only=False, + options=options, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + sequential=False, + ) + # sampling from the optimizers + eta = options.get("eta", 2.0) + if num_optim_restarts > 0: + probs = torch.nn.functional.softmax(eta * standardize(fantasy_vals), dim=0) + idx = torch.multinomial( + probs, + num_optim_restarts * acq_function.num_fantasies, + replacement=True, + ) + optim_ics = fantasy_cands[idx] + if is_mf_hvkg: + # add fixed features + optim_ics = value_function._construct_X_full(optim_ics) + optim_ics = optim_ics.reshape( + num_optim_restarts, acq_function.num_pseudo_points, bounds.shape[-1] + ) + + # get random initial conditions + num_random_restarts = num_restarts - num_optim_restarts + if num_random_restarts > 0: + q_aug = acq_function.get_augmented_q_batch_size(q=q) + base_ics = gen_batch_initial_conditions( + acq_function=acq_function, + bounds=bounds, + q=q_aug, + num_restarts=num_restarts, + raw_samples=raw_samples, + fixed_features=fixed_features, + options=options, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + ) + + if num_optim_restarts > 0: + probs = torch.full( + (num_restarts,), + 1.0 / num_restarts, + dtype=optim_ics.dtype, + device=optim_ics.device, + ) + optim_idxr = probs.multinomial( + num_samples=num_optim_restarts, replacement=False + ) + base_ics[optim_idxr, q:] = optim_ics + else: + # optim_ics is num_restarts x num_pseudo_points x d + # add padding so that base_ics is num_restarts x q+num_pseudo_points x d + q_padding = torch.zeros( + optim_ics.shape[0], + q, + optim_ics.shape[-1], + dtype=optim_ics.dtype, + device=optim_ics.device, + ) + base_ics = torch.cat([q_padding, optim_ics], dim=-2) + + if num_optim_restarts > 0: + all_ics = [] + if num_random_restarts > 0: + optim_idcs = optim_idxr.view(-1).tolist() + else: + optim_idcs = list(range(num_restarts)) + for i in list(range(num_restarts)): + if i in optim_idcs: + # optimize the q points, + # given fixed, optimized fantasy designs + ics = gen_batch_initial_conditions( + acq_function=acq_function, + bounds=bounds, + q=q, + num_restarts=1, + raw_samples=raw_samples, + fixed_features=fixed_features, + options=options, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + fixed_X_fantasies=base_ics[i, q:], + ) + else: + # ics are all randomly sampled + ics = base_ics[i : i + 1] + all_ics.append(ics) + return torch.cat(all_ics, dim=0) + + return base_ics
+ + + +
+[docs] +def gen_value_function_initial_conditions( + acq_function: AcquisitionFunction, + bounds: Tensor, + num_restarts: int, + raw_samples: int, + current_model: Model, + fixed_features: dict[int, float] | None = None, + options: dict[str, bool | float | int] | None = None, +) -> Tensor: + r"""Generate a batch of smart initializations for optimizing + the value function of qKnowledgeGradient. + + This function generates initial conditions for optimizing the inner problem of + KG, i.e. its value function, using the maximizer of the posterior objective. + Intutively, the maximizer of the fantasized posterior will often be close to a + maximizer of the current posterior. This function uses that fact to generate the + initital conditions for the fantasy points. Specifically, a fraction of `1 - + frac_random` (see options) of raw samples is generated by sampling from the set of + maximizers of the posterior objective (obtained via random restart optimization) + according to a softmax transformation of their respective values. This means that + this initialization strategy internally solves an acquisition function + maximization problem. The remaining raw samples are generated using + `draw_sobol_samples`. All raw samples are then evaluated, and the initial + conditions are selected according to the standard initialization strategy in + 'initialize_q_batch' individually for each inner problem. + + Args: + acq_function: The value function instance to be optimized. + bounds: A `2 x d` tensor of lower and upper bounds for each column of + task features. + num_restarts: The number of starting points for multistart acquisition + function optimization. + raw_samples: The number of raw samples to consider in the initialization + heuristic. + current_model: The model of the KG acquisition function that was used to + generate the fantasy model of the value function. + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. + options: Options for initial condition generation. These contain all + settings for the standard heuristic initialization from + `gen_batch_initial_conditions`. In addition, they contain + `frac_random` (the fraction of fully random fantasy points), + `num_inner_restarts` and `raw_inner_samples` (the number of random + restarts and raw samples for solving the posterior objective + maximization problem, respectively) and `eta` (temperature parameter + for sampling heuristic from posterior objective maximizers). + + Returns: + A `num_restarts x batch_shape x q x d` tensor that can be used as initial + conditions for `optimize_acqf()`. Here `batch_shape` is the batch shape + of value function model. + + Example: + >>> fant_X = torch.rand(5, 1, 2) + >>> fantasy_model = model.fantasize(fant_X, SobolQMCNormalSampler(16)) + >>> value_function = PosteriorMean(fantasy_model) + >>> bounds = torch.tensor([[0., 0.], [1., 1.]]) + >>> Xinit = gen_value_function_initial_conditions( + >>> value_function, bounds, num_restarts=10, raw_samples=512, + >>> options={"frac_random": 0.25}, + >>> ) + """ + options = options or {} + seed: int | None = options.get("seed") + frac_random: float = options.get("frac_random", 0.6) + if not 0 < frac_random < 1: + raise ValueError( + f"frac_random must take on values in (0,1). Value: {frac_random}" + ) + + # compute maximizer of the current value function + value_function = _get_value_function( + model=current_model, + objective=getattr(acq_function, "objective", None), + posterior_transform=acq_function.posterior_transform, + sampler=getattr(acq_function, "sampler", None), + project=getattr(acq_function, "project", None), + ) + from botorch.optim.optimize import optimize_acqf + + fantasy_cands, fantasy_vals = optimize_acqf( + acq_function=value_function, + bounds=bounds, + q=1, + num_restarts=options.get("num_inner_restarts", 20), + raw_samples=options.get("raw_inner_samples", 1024), + fixed_features=fixed_features, + return_best_only=False, + options={ + k: v + for k, v in options.items() + if k + not in ("frac_random", "num_inner_restarts", "raw_inner_samples", "eta") + }, + ) + + batch_shape = acq_function.model.batch_shape + # sampling from the optimizers + n_value = int((1 - frac_random) * raw_samples) # number of non-random ICs + if n_value > 0: + eta = options.get("eta", 2.0) + weights = torch.exp(eta * standardize(fantasy_vals)) + idx = batched_multinomial( + weights=weights.expand(*batch_shape, -1), + num_samples=n_value, + replacement=True, + ).permute(-1, *range(len(batch_shape))) + resampled = fantasy_cands[idx] + else: + resampled = torch.empty( + 0, + *batch_shape, + 1, + bounds.shape[-1], + dtype=fantasy_cands.dtype, + device=fantasy_cands.device, + ) + # add qMC samples + randomized = draw_sobol_samples( + bounds=bounds, n=raw_samples - n_value, q=1, batch_shape=batch_shape, seed=seed + ).to(resampled) + # full set of raw samples + X_rnd = torch.cat([resampled, randomized], dim=0) + X_rnd = fix_features(X_rnd, fixed_features=fixed_features) + + # evaluate the raw samples + with torch.no_grad(): + acq_vals = acq_function(X_rnd) + + # select the restart points using the heuristic + X_init, _ = initialize_q_batch( + X=X_rnd, acq_vals=acq_vals, n=num_restarts, eta=options.get("eta", 2.0) + ) + return X_init
+ + + +
+[docs] +def initialize_q_batch( + X: Tensor, acq_vals: Tensor, n: int, eta: float = 1.0 +) -> tuple[Tensor, Tensor]: + r"""Heuristic for selecting initial conditions for candidate generation. + + This heuristic selects points from `X` (without replacement) with probability + proportional to `exp(eta * Z)`, where + `Z = (acq_vals - mean(acq_vals)) / std(acq_vals)` + and `eta` is a temperature parameter. + + When using an acquisiton function that is non-negative and possibly zero + over large areas of the feature space (e.g. qEI), you should use + `initialize_q_batch_nonneg` instead. + + Args: + X: A `b x batch_shape x q x d` tensor of `b` - `batch_shape` samples of + `q`-batches from a d`-dim feature space. Typically, these are generated + using qMC sampling. + acq_vals: A tensor of `b x batch_shape` outcomes associated with the samples. + Typically, this is the value of the batch acquisition function to be + maximized. + n: The number of initial condition to be generated. Must be less than `b`. + eta: Temperature parameter for weighting samples. + + Returns: + - An `n x batch_shape x q x d` tensor of `n` - `batch_shape` `q`-batch initial + conditions, where each batch of `n x q x d` samples is selected independently. + - An `n x batch_shape` tensor of the corresponding acquisition values. + + Example: + >>> # To get `n=10` starting points of q-batch size `q=3` + >>> # for model with `d=6`: + >>> qUCB = qUpperConfidenceBound(model, beta=0.1) + >>> X_rnd = torch.rand(500, 3, 6) + >>> X_init, acq_init = initialize_q_batch(X=X_rnd, acq_vals=qUCB(X_rnd), n=10) + """ + n_samples = X.shape[0] + batch_shape = X.shape[1:-2] or torch.Size() + if n > n_samples: + raise RuntimeError( + f"n ({n}) cannot be larger than the number of " + f"provided samples ({n_samples})" + ) + elif n == n_samples: + return X, acq_vals + + Ystd = acq_vals.std(dim=0) + if torch.any(Ystd == 0): + warnings.warn( + "All acquisition values for raw samples points are the same for " + "at least one batch. Choosing initial conditions at random.", + BadInitialCandidatesWarning, + stacklevel=3, + ) + idcs = torch.randperm(n=n_samples, device=X.device)[:n] + return X[idcs], acq_vals[idcs] + + max_val, max_idx = torch.max(acq_vals, dim=0) + Z = (acq_vals - acq_vals.mean(dim=0)) / Ystd + etaZ = eta * Z + weights = torch.exp(etaZ) + while torch.isinf(weights).any(): + etaZ *= 0.5 + weights = torch.exp(etaZ) + if batch_shape == torch.Size(): + idcs = torch.multinomial(weights, n) + else: + idcs = batched_multinomial( + weights=weights.permute(*range(1, len(batch_shape) + 1), 0), num_samples=n + ).permute(-1, *range(len(batch_shape))) + # make sure we get the maximum + if max_idx not in idcs: + idcs[-1] = max_idx + if batch_shape == torch.Size(): + return X[idcs], acq_vals[idcs] + else: + X_select = X.gather( + dim=0, index=idcs.view(*idcs.shape, 1, 1).expand(n, *X.shape[1:]) + ) + acq_select = acq_vals.gather(dim=0, index=idcs) + return X_select, acq_select
+ + + +
+[docs] +def initialize_q_batch_nonneg( + X: Tensor, acq_vals: Tensor, n: int, eta: float = 1.0, alpha: float = 1e-4 +) -> tuple[Tensor, Tensor]: + r"""Heuristic for selecting initial conditions for non-neg. acquisition functions. + + This function is similar to `initialize_q_batch`, but designed specifically + for acquisition functions that are non-negative and possibly zero over + large areas of the feature space (e.g. qEI). All samples for which + `acq_vals < alpha * max(acq_vals)` will be ignored (assuming that `acq_vals` + contains at least one positive value). + + Args: + X: A `b x q x d` tensor of `b` samples of `q`-batches from a `d`-dim. + feature space. Typically, these are generated using qMC. + acq_vals: A tensor of `b` outcomes associated with the samples. Typically, this + is the value of the batch acquisition function to be maximized. + n: The number of initial condition to be generated. Must be less than `b`. + eta: Temperature parameter for weighting samples. + alpha: The threshold (as a fraction of the maximum observed value) under + which to ignore samples. All input samples for which + `Y < alpha * max(Y)` will be ignored. + + Returns: + - An `n x q x d` tensor of `n` `q`-batch initial conditions. + - An `n` tensor of the corresponding acquisition values. + + Example: + >>> # To get `n=10` starting points of q-batch size `q=3` + >>> # for model with `d=6`: + >>> qEI = qExpectedImprovement(model, best_f=0.2) + >>> X_rnd = torch.rand(500, 3, 6) + >>> X_init, acq_init = initialize_q_batch_nonneg( + ... X=X_rnd, acq_vals=qEI(X_rnd), n=10 + ... ) + """ + n_samples = X.shape[0] + if n > n_samples: + raise RuntimeError("n cannot be larger than the number of provided samples") + elif n == n_samples: + return X, acq_vals + + max_val, max_idx = torch.max(acq_vals, dim=0) + if torch.any(max_val <= 0): + warnings.warn( + "All acquisition values for raw sampled points are nonpositive, so " + "initial conditions are being selected randomly.", + BadInitialCandidatesWarning, + stacklevel=3, + ) + idcs = torch.randperm(n=n_samples, device=X.device)[:n] + return X[idcs], acq_vals[idcs] + + # make sure there are at least `n` points with positive acquisition values + pos = acq_vals > 0 + num_pos = pos.sum().item() + if num_pos < n: + # select all positive points and then fill remaining quota with randomly + # selected points + remaining_indices = (~pos).nonzero(as_tuple=False).view(-1) + rand_indices = torch.randperm( + remaining_indices.shape[0], device=acq_vals.device + ) + sampled_remaining_indices = remaining_indices[rand_indices[: n - num_pos]] + pos[sampled_remaining_indices] = 1 + return X[pos], acq_vals[pos] + # select points within alpha of max_val, iteratively decreasing alpha by a + # factor of 10 as necessary + alpha_pos = acq_vals >= alpha * max_val + while alpha_pos.sum() < n: + alpha = 0.1 * alpha + alpha_pos = acq_vals >= alpha * max_val + alpha_pos_idcs = torch.arange(len(acq_vals), device=acq_vals.device)[alpha_pos] + weights = torch.exp(eta * (acq_vals[alpha_pos] / max_val - 1)) + idcs = alpha_pos_idcs[torch.multinomial(weights, n)] + if max_idx not in idcs: + idcs[-1] = max_idx + return X[idcs], acq_vals[idcs]
+ + + +
+[docs] +def sample_points_around_best( + acq_function: AcquisitionFunction, + n_discrete_points: int, + sigma: float, + bounds: Tensor, + best_pct: float = 5.0, + subset_sigma: float = 1e-1, + prob_perturb: float | None = None, +) -> Tensor | None: + r"""Find best points and sample nearby points. + + Args: + acq_function: The acquisition function. + n_discrete_points: The number of points to sample. + sigma: The standard deviation of the additive gaussian noise for + perturbing the best points. + bounds: A `2 x d`-dim tensor containing the bounds. + best_pct: The percentage of best points to perturb. + subset_sigma: The standard deviation of the additive gaussian + noise for perturbing a subset of dimensions of the best points. + prob_perturb: The probability of perturbing each dimension. + + Returns: + An optional `n_discrete_points x d`-dim tensor containing the + sampled points. This is None if no baseline points are found. + """ + X = get_X_baseline(acq_function=acq_function) + if X is None: + return + with torch.no_grad(): + try: + posterior = acq_function.model.posterior(X) + except AttributeError: + warnings.warn( + "Failed to sample around previous best points.", + BotorchWarning, + stacklevel=3, + ) + return + mean = posterior.mean + while mean.ndim > 2: + # take average over batch dims + mean = mean.mean(dim=0) + try: + f_pred = acq_function.objective(mean) + # Some acquisition functions do not have an objective + # and for some acquisition functions the objective is None + except (AttributeError, TypeError): + f_pred = mean + if hasattr(acq_function, "maximize"): + # make sure that the optimiztaion direction is set properly + if not acq_function.maximize: + f_pred = -f_pred + try: + # handle constraints for EHVI-based acquisition functions + constraints = acq_function.constraints + if constraints is not None: + neg_violation = -torch.stack( + [c(mean).clamp_min(0.0) for c in constraints], dim=-1 + ).sum(dim=-1) + feas = neg_violation == 0 + if feas.any(): + f_pred[~feas] = float("-inf") + else: + # set objective equal to negative violation + f_pred = neg_violation + except AttributeError: + pass + if f_pred.ndim == mean.ndim and f_pred.shape[-1] > 1: + # multi-objective + # find pareto set + is_pareto = is_non_dominated(f_pred) + best_X = X[is_pareto] + else: + if f_pred.shape[-1] == 1: + f_pred = f_pred.squeeze(-1) + n_best = max(1, round(X.shape[0] * best_pct / 100)) + # the view() is to ensure that best_idcs is not a scalar tensor + best_idcs = torch.topk(f_pred, n_best).indices.view(-1) + best_X = X[best_idcs] + use_perturbed_sampling = best_X.shape[-1] >= 20 or prob_perturb is not None + n_trunc_normal_points = ( + n_discrete_points // 2 if use_perturbed_sampling else n_discrete_points + ) + perturbed_X = sample_truncated_normal_perturbations( + X=best_X, + n_discrete_points=n_trunc_normal_points, + sigma=sigma, + bounds=bounds, + ) + if use_perturbed_sampling: + perturbed_subset_dims_X = sample_perturbed_subset_dims( + X=best_X, + bounds=bounds, + # ensure that we return n_discrete_points + n_discrete_points=n_discrete_points - n_trunc_normal_points, + sigma=sigma, + prob_perturb=prob_perturb, + ) + perturbed_X = torch.cat([perturbed_X, perturbed_subset_dims_X], dim=0) + # shuffle points + perm = torch.randperm(perturbed_X.shape[0], device=X.device) + perturbed_X = perturbed_X[perm] + return perturbed_X
+ + + +
+[docs] +def sample_truncated_normal_perturbations( + X: Tensor, + n_discrete_points: int, + sigma: float, + bounds: Tensor, + qmc: bool = True, +) -> Tensor: + r"""Sample points around `X`. + + Sample perturbed points around `X` such that the added perturbations + are sampled from N(0, sigma^2 I) and truncated to be within [0,1]^d. + + Args: + X: A `n x d`-dim tensor starting points. + n_discrete_points: The number of points to sample. + sigma: The standard deviation of the additive gaussian noise for + perturbing the points. + bounds: A `2 x d`-dim tensor containing the bounds. + qmc: A boolean indicating whether to use qmc. + + Returns: + A `n_discrete_points x d`-dim tensor containing the sampled points. + """ + X = normalize(X, bounds=bounds) + d = X.shape[1] + # sample points from N(X_center, sigma^2 I), truncated to be within + # [0, 1]^d. + if X.shape[0] > 1: + rand_indices = torch.randint(X.shape[0], (n_discrete_points,), device=X.device) + X = X[rand_indices] + if qmc: + std_bounds = torch.zeros(2, d, dtype=X.dtype, device=X.device) + std_bounds[1] = 1 + u = draw_sobol_samples(bounds=std_bounds, n=n_discrete_points, q=1).squeeze(1) + else: + u = torch.rand((n_discrete_points, d), dtype=X.dtype, device=X.device) + # compute bounds to sample from + a = -X + b = 1 - X + # compute z-score of bounds + alpha = a / sigma + beta = b / sigma + normal = Normal(0, 1) + cdf_alpha = normal.cdf(alpha) + # use inverse transform + perturbation = normal.icdf(cdf_alpha + u * (normal.cdf(beta) - cdf_alpha)) * sigma + # add perturbation and clip points that are still outside + perturbed_X = (X + perturbation).clamp(0.0, 1.0) + return unnormalize(perturbed_X, bounds=bounds)
+ + + +
+[docs] +def sample_perturbed_subset_dims( + X: Tensor, + bounds: Tensor, + n_discrete_points: int, + sigma: float = 1e-1, + qmc: bool = True, + prob_perturb: float | None = None, +) -> Tensor: + r"""Sample around `X` by perturbing a subset of the dimensions. + + By default, dimensions are perturbed with probability equal to + `min(20 / d, 1)`. As shown in [Regis]_, perturbing a small number + of dimensions can be beneificial. The perturbations are sampled + from N(0, sigma^2 I) and truncated to be within [0,1]^d. + + Args: + X: A `n x d`-dim tensor starting points. `X` + must be normalized to be within `[0, 1]^d`. + bounds: The bounds to sample perturbed values from + n_discrete_points: The number of points to sample. + sigma: The standard deviation of the additive gaussian noise for + perturbing the points. + qmc: A boolean indicating whether to use qmc. + prob_perturb: The probability of perturbing each dimension. If omitted, + defaults to `min(20 / d, 1)`. + + Returns: + A `n_discrete_points x d`-dim tensor containing the sampled points. + + """ + if bounds.ndim != 2: + raise BotorchTensorDimensionError("bounds must be a `2 x d`-dim tensor.") + elif X.ndim != 2: + raise BotorchTensorDimensionError("X must be a `n x d`-dim tensor.") + d = bounds.shape[-1] + if prob_perturb is None: + # Only perturb a subset of the features + prob_perturb = min(20.0 / d, 1.0) + + if X.shape[0] == 1: + X_cand = X.repeat(n_discrete_points, 1) + else: + rand_indices = torch.randint(X.shape[0], (n_discrete_points,), device=X.device) + X_cand = X[rand_indices] + pert = sample_truncated_normal_perturbations( + X=X_cand, + n_discrete_points=n_discrete_points, + sigma=sigma, + bounds=bounds, + qmc=qmc, + ) + + # find cases where we are not perturbing any dimensions + mask = ( + torch.rand( + n_discrete_points, + d, + dtype=bounds.dtype, + device=bounds.device, + ) + <= prob_perturb + ) + ind = (~mask).all(dim=-1).nonzero() + # perturb `n_perturb` of the dimensions + n_perturb = ceil(d * prob_perturb) + perturb_mask = torch.zeros(d, dtype=mask.dtype, device=mask.device) + perturb_mask[:n_perturb].fill_(1) + # TODO: use batched `torch.randperm` when available: + # https://github.com/pytorch/pytorch/issues/42502 + for idx in ind: + mask[idx] = perturb_mask[torch.randperm(d, device=bounds.device)] + # Create candidate points + X_cand[mask] = pert[mask] + return X_cand
+ + + +
+[docs] +def is_nonnegative(acq_function: AcquisitionFunction) -> bool: + r"""Determine whether a given acquisition function is non-negative. + + Args: + acq_function: The `AcquisitionFunction` instance. + + Returns: + True if `acq_function` is non-negative, False if not, or if the behavior + is unknown (for custom acquisition functions). + + Example: + >>> qEI = qExpectedImprovement(model, best_f=0.1) + >>> is_nonnegative(qEI) # returns True + """ + return isinstance( + acq_function, + ( + analytic.ExpectedImprovement, + analytic.ConstrainedExpectedImprovement, + analytic.ProbabilityOfImprovement, + analytic.NoisyExpectedImprovement, + monte_carlo.qExpectedImprovement, + monte_carlo.qNoisyExpectedImprovement, + monte_carlo.qProbabilityOfImprovement, + multi_objective.analytic.ExpectedHypervolumeImprovement, + multi_objective.monte_carlo.qExpectedHypervolumeImprovement, + multi_objective.monte_carlo.qNoisyExpectedHypervolumeImprovement, + ), + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/optimize.html b/website-old/pages/api/_modules/botorch/optim/optimize.html new file mode 100644 index 0000000000..4d75a80f43 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/optimize.html @@ -0,0 +1,1417 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.optimize

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Methods for optimizing acquisition functions.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import warnings
+from collections.abc import Callable
+from typing import Any
+
+import torch
+from botorch.acquisition.acquisition import (
+    AcquisitionFunction,
+    OneShotAcquisitionFunction,
+)
+from botorch.acquisition.knowledge_gradient import qKnowledgeGradient
+from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import (
+    qHypervolumeKnowledgeGradient,
+)
+from botorch.exceptions import InputDataError, UnsupportedError
+from botorch.exceptions.errors import CandidateGenerationError
+from botorch.exceptions.warnings import OptimizationWarning
+from botorch.generation.gen import gen_candidates_scipy, TGenCandidates
+from botorch.logging import logger
+from botorch.optim.initializers import (
+    gen_batch_initial_conditions,
+    gen_one_shot_hvkg_initial_conditions,
+    gen_one_shot_kg_initial_conditions,
+    TGenInitialConditions,
+)
+from botorch.optim.stopping import ExpMAStoppingCriterion
+from torch import Tensor
+
+INIT_OPTION_KEYS = {
+    # set of options for initialization that we should
+    # not pass to scipy.optimize.minimize to avoid
+    # warnings
+    "alpha",
+    "batch_limit",
+    "eta",
+    "init_batch_limit",
+    "nonnegative",
+    "n_burnin",
+    "sample_around_best",
+    "sample_around_best_sigma",
+    "sample_around_best_prob_perturb",
+    "seed",
+    "thinning",
+}
+
+
+
+[docs] +@dataclasses.dataclass(frozen=True) +class OptimizeAcqfInputs: + """ + Container for inputs to `optimize_acqf`. + + See docstring for `optimize_acqf` for explanation of parameters. + """ + + acq_function: AcquisitionFunction + bounds: Tensor + q: int + num_restarts: int + raw_samples: int | None + options: dict[str, bool | float | int | str] | None + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None + equality_constraints: list[tuple[Tensor, Tensor, float]] | None + nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None + fixed_features: dict[int, float] | None + post_processing_func: Callable[[Tensor], Tensor] | None + batch_initial_conditions: Tensor | None + return_best_only: bool + gen_candidates: TGenCandidates + sequential: bool + ic_generator: TGenInitialConditions | None = None + timeout_sec: float | None = None + return_full_tree: bool = False + retry_on_optimization_warning: bool = True + ic_gen_kwargs: dict = dataclasses.field(default_factory=dict) + + @property + def full_tree(self) -> bool: + return self.return_full_tree or ( + not isinstance(self.acq_function, OneShotAcquisitionFunction) + ) + + def __post_init__(self) -> None: + if self.inequality_constraints is None and not ( + self.bounds.ndim == 2 and self.bounds.shape[0] == 2 + ): + raise ValueError( + "bounds should be a `2 x d` tensor, current shape: " + f"{list(self.bounds.shape)}." + ) + + d = self.bounds.shape[1] + if self.batch_initial_conditions is not None: + batch_initial_conditions_shape = self.batch_initial_conditions.shape + if len(batch_initial_conditions_shape) not in (2, 3): + raise ValueError( + "batch_initial_conditions must be 2-dimensional or " + "3-dimensional. Its shape is " + f"{batch_initial_conditions_shape}." + ) + if batch_initial_conditions_shape[-1] != d: + raise ValueError( + f"batch_initial_conditions.shape[-1] must be {d}. The " + f"shape is {batch_initial_conditions_shape}." + ) + + elif self.ic_generator is None: + if self.nonlinear_inequality_constraints is not None: + raise RuntimeError( + "`ic_generator` must be given if " + "there are non-linear inequality constraints." + ) + if self.raw_samples is None: + raise ValueError( + "Must specify `raw_samples` when " + "`batch_initial_conditions` is None`." + ) + if self.fixed_features is not None and any( + (k < 0 for k in self.fixed_features) + ): + raise ValueError("All indices (keys) in `fixed_features` must be >= 0.") + +
+[docs] + def get_ic_generator(self) -> TGenInitialConditions: + if self.ic_generator is not None: + return self.ic_generator + elif isinstance(self.acq_function, qKnowledgeGradient): + return gen_one_shot_kg_initial_conditions + elif isinstance(self.acq_function, qHypervolumeKnowledgeGradient): + return gen_one_shot_hvkg_initial_conditions + return gen_batch_initial_conditions
+
+ + + +def _optimize_acqf_all_features_fixed( + *, + bounds: Tensor, + fixed_features: dict[int, float], + q: int, + acq_function: AcquisitionFunction, +) -> tuple[Tensor, Tensor]: + """ + Helper function for `optimize_acqf` for the trivial case where + all features are fixed. + """ + X = torch.tensor( + [fixed_features[i] for i in range(bounds.shape[-1])], + device=bounds.device, + dtype=bounds.dtype, + ) + X = X.expand(q, *X.shape) + with torch.no_grad(): + acq_value = acq_function(X) + return X, acq_value + + +def _validate_sequential_inputs(opt_inputs: OptimizeAcqfInputs) -> None: + # Validate that constraints across the q-dim and + # self.sequential are not present together. + const_err_message = ( + "Inter-point constraints are not supported for sequential optimization. " + "But the {}th {} constraint is defined as inter-point." + ) + if opt_inputs.inequality_constraints is not None: + for i, constraint in enumerate(opt_inputs.inequality_constraints): + if len(constraint[0].shape) > 1: + raise UnsupportedError(const_err_message.format(i, "linear inequality")) + if opt_inputs.equality_constraints is not None: + for i, constraint in enumerate(opt_inputs.equality_constraints): + if len(constraint[0].shape) > 1: + raise UnsupportedError(const_err_message.format(i, "linear equality")) + if opt_inputs.nonlinear_inequality_constraints is not None: + for i, (_, intra_point) in enumerate( + opt_inputs.nonlinear_inequality_constraints + ): + if not intra_point: + raise UnsupportedError( + const_err_message.format(i, "non-linear inequality") + ) + + # TODO: Validate constraints if provided: + # https://github.com/pytorch/botorch/pull/1231 + if opt_inputs.batch_initial_conditions is not None: + raise UnsupportedError( + "`batch_initial_conditions` is not supported for sequential " + "optimization. Either avoid specifying " + "`batch_initial_conditions` to use the custom initializer or " + "use the `ic_generator` kwarg to generate initial conditions " + "for the case of nonlinear inequality constraints." + ) + + if not opt_inputs.return_best_only: + raise NotImplementedError( + "`return_best_only=False` only supported for joint optimization." + ) + if isinstance(opt_inputs.acq_function, OneShotAcquisitionFunction): + raise NotImplementedError( + "sequential optimization currently not supported for one-shot " + "acquisition functions. Must have `sequential=False`." + ) + + +def _optimize_acqf_sequential_q( + opt_inputs: OptimizeAcqfInputs, +) -> tuple[Tensor, Tensor]: + """ + Helper function for `optimize_acqf` when sequential=True and q > 1. + + For each of `q` times, generate a single candidate greedily, then add it to + the list of pending points. + """ + _validate_sequential_inputs(opt_inputs) + # When using sequential optimization, we allocate the total timeout + # evenly across the individual acquisition optimizations. + timeout_sec = ( + opt_inputs.timeout_sec / opt_inputs.q + if opt_inputs.timeout_sec is not None + else None + ) + candidate_list, acq_value_list = [], [] + base_X_pending = opt_inputs.acq_function.X_pending + + new_inputs = dataclasses.replace( + opt_inputs, + q=1, + batch_initial_conditions=None, + return_best_only=True, + sequential=False, + timeout_sec=timeout_sec, + ) + for i in range(opt_inputs.q): + candidate, acq_value = _optimize_acqf_batch(new_inputs) + + candidate_list.append(candidate) + acq_value_list.append(acq_value) + candidates = torch.cat(candidate_list, dim=-2) + new_inputs.acq_function.set_X_pending( + torch.cat([base_X_pending, candidates], dim=-2) + if base_X_pending is not None + else candidates + ) + logger.info(f"Generated sequential candidate {i + 1} of {opt_inputs.q}") + opt_inputs.acq_function.set_X_pending(base_X_pending) + return candidates, torch.stack(acq_value_list) + + +def _optimize_acqf_batch(opt_inputs: OptimizeAcqfInputs) -> tuple[Tensor, Tensor]: + options = opt_inputs.options or {} + + initial_conditions_provided = opt_inputs.batch_initial_conditions is not None + + if initial_conditions_provided: + batch_initial_conditions = opt_inputs.batch_initial_conditions + else: + # pyre-ignore[28]: Unexpected keyword argument `acq_function` to anonymous call. + batch_initial_conditions = opt_inputs.get_ic_generator()( + acq_function=opt_inputs.acq_function, + bounds=opt_inputs.bounds, + q=opt_inputs.q, + num_restarts=opt_inputs.num_restarts, + raw_samples=opt_inputs.raw_samples, + fixed_features=opt_inputs.fixed_features, + options=options, + inequality_constraints=opt_inputs.inequality_constraints, + equality_constraints=opt_inputs.equality_constraints, + **opt_inputs.ic_gen_kwargs, + ) + + batch_limit: int = options.get( + "batch_limit", + ( + opt_inputs.num_restarts + if not opt_inputs.nonlinear_inequality_constraints + else 1 + ), + ) + + def _optimize_batch_candidates() -> tuple[Tensor, Tensor, list[Warning]]: + batch_candidates_list: list[Tensor] = [] + batch_acq_values_list: list[Tensor] = [] + batched_ics = batch_initial_conditions.split(batch_limit) + opt_warnings = [] + timeout_sec = ( + opt_inputs.timeout_sec / len(batched_ics) + if opt_inputs.timeout_sec is not None + else None + ) + + bounds = opt_inputs.bounds + gen_kwargs: dict[str, Any] = { + "lower_bounds": None if bounds[0].isinf().all() else bounds[0], + "upper_bounds": None if bounds[1].isinf().all() else bounds[1], + "options": {k: v for k, v in options.items() if k not in INIT_OPTION_KEYS}, + "fixed_features": opt_inputs.fixed_features, + "timeout_sec": timeout_sec, + } + + for constraint_name in [ + "inequality_constraints", + "equality_constraints", + "nonlinear_inequality_constraints", + ]: + if (constraint := getattr(opt_inputs, constraint_name)) is not None: + gen_kwargs[constraint_name] = constraint + + for i, batched_ics_ in enumerate(batched_ics): + # optimize using random restart optimization + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always", category=OptimizationWarning) + ( + batch_candidates_curr, + batch_acq_values_curr, + ) = opt_inputs.gen_candidates( + batched_ics_, opt_inputs.acq_function, **gen_kwargs + ) + opt_warnings += ws + batch_candidates_list.append(batch_candidates_curr) + batch_acq_values_list.append(batch_acq_values_curr) + logger.info(f"Generated candidate batch {i + 1} of {len(batched_ics)}.") + + batch_candidates = torch.cat(batch_candidates_list) + has_scalars = batch_acq_values_list[0].ndim == 0 + if has_scalars: + batch_acq_values = torch.stack(batch_acq_values_list) + else: + batch_acq_values = torch.cat(batch_acq_values_list).flatten() + return batch_candidates, batch_acq_values, opt_warnings + + batch_candidates, batch_acq_values, ws = _optimize_batch_candidates() + + optimization_warning_raised = any( + issubclass(w.category, OptimizationWarning) for w in ws + ) + if optimization_warning_raised and opt_inputs.retry_on_optimization_warning: + first_warn_msg = ( + "Optimization failed in `gen_candidates_scipy` with the following " + f"warning(s):\n{[w.message for w in ws]}\nBecause you specified " + "`batch_initial_conditions`, optimization will not be retried with " + "new initial conditions and will proceed with the current solution." + " Suggested remediation: Try again with different " + "`batch_initial_conditions`, or don't provide `batch_initial_conditions.`" + if initial_conditions_provided + else "Optimization failed in `gen_candidates_scipy` with the following " + f"warning(s):\n{[w.message for w in ws]}\nTrying again with a new " + "set of initial conditions." + ) + warnings.warn(first_warn_msg, RuntimeWarning, stacklevel=2) + + if not initial_conditions_provided: + batch_initial_conditions = opt_inputs.get_ic_generator()( + acq_function=opt_inputs.acq_function, + bounds=opt_inputs.bounds, + q=opt_inputs.q, + num_restarts=opt_inputs.num_restarts, + raw_samples=opt_inputs.raw_samples, + fixed_features=opt_inputs.fixed_features, + options=options, + inequality_constraints=opt_inputs.inequality_constraints, + equality_constraints=opt_inputs.equality_constraints, + **opt_inputs.ic_gen_kwargs, + ) + + batch_candidates, batch_acq_values, ws = _optimize_batch_candidates() + + optimization_warning_raised = any( + issubclass(w.category, OptimizationWarning) for w in ws + ) + if optimization_warning_raised: + warnings.warn( + "Optimization failed on the second try, after generating a " + "new set of initial conditions.", + RuntimeWarning, + stacklevel=2, + ) + + if opt_inputs.post_processing_func is not None: + batch_candidates = opt_inputs.post_processing_func(batch_candidates) + with torch.no_grad(): + acq_values_list = [ + opt_inputs.acq_function(cand) + for cand in batch_candidates.split(batch_limit, dim=0) + ] + batch_acq_values = torch.cat(acq_values_list, dim=0) + + if opt_inputs.return_best_only: + best = torch.argmax(batch_acq_values.view(-1), dim=0) + batch_candidates = batch_candidates[best] + batch_acq_values = batch_acq_values[best] + + if not opt_inputs.full_tree: + batch_candidates = opt_inputs.acq_function.extract_candidates( + X_full=batch_candidates + ) + + return batch_candidates, batch_acq_values + + +
+[docs] +def optimize_acqf( + acq_function: AcquisitionFunction, + bounds: Tensor, + q: int, + num_restarts: int, + raw_samples: int | None = None, + options: dict[str, bool | float | int | str] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None = None, + fixed_features: dict[int, float] | None = None, + post_processing_func: Callable[[Tensor], Tensor] | None = None, + batch_initial_conditions: Tensor | None = None, + return_best_only: bool = True, + gen_candidates: TGenCandidates | None = None, + sequential: bool = False, + *, + ic_generator: TGenInitialConditions | None = None, + timeout_sec: float | None = None, + return_full_tree: bool = False, + retry_on_optimization_warning: bool = True, + **ic_gen_kwargs: Any, +) -> tuple[Tensor, Tensor]: + r"""Generate a set of candidates via multi-start optimization. + + Args: + acq_function: An AcquisitionFunction. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X` + (if inequality_constraints is provided, these bounds can be -inf and + +inf, respectively). + q: The number of candidates. + num_restarts: The number of starting points for multistart acquisition + function optimization. + raw_samples: The number of samples for initialization. This is required + if `batch_initial_conditions` is not specified. + options: Options for candidate generation. + inequality_constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. `indices` and + `coefficients` should be torch tensors. See the docstring of + `make_scipy_linear_constraints` for an example. When q=1, or when + applying the same constraint to each candidate in the batch + (intra-point constraint), `indices` should be a 1-d tensor. + For inter-point constraints, in which the constraint is applied to the + whole batch of candidates, `indices` must be a 2-d tensor, where + in each row `indices[i] =(k_i, l_i)` the first index `k_i` corresponds + to the `k_i`-th element of the `q`-batch and the second index `l_i` + corresponds to the `l_i`-th feature of that element. + equality_constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an equality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. See the docstring of + `make_scipy_linear_constraints` for an example. + nonlinear_inequality_constraints: A list of tuples representing the nonlinear + inequality constraints. The first element in the tuple is a callable + representing a constraint of the form `callable(x) >= 0`. In case of an + intra-point constraint, `callable()`takes in an one-dimensional tensor of + shape `d` and returns a scalar. In case of an inter-point constraint, + `callable()` takes a two dimensional tensor of shape `q x d` and again + returns a scalar. The second element is a boolean, indicating if it is an + intra-point or inter-point constraint (`True` for intra-point. `False` for + inter-point). For more information on intra-point vs inter-point + constraints, see the docstring of the `inequality_constraints` argument to + `optimize_acqf()`. The constraints will later be passed to the scipy + solver. You need to pass in `batch_initial_conditions` in this case. + Using non-linear inequality constraints also requires that `batch_limit` + is set to 1, which will be done automatically if not specified in + `options`. + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. All indices + should be non-negative. + post_processing_func: A function that post-processes an optimization + result appropriately (i.e., according to `round-trip` + transformations). + batch_initial_conditions: A tensor to specify the initial conditions. Set + this if you do not want to use default initialization strategy. + return_best_only: If False, outputs the solutions corresponding to all + random restart initializations of the optimization. + gen_candidates: A callable for generating candidates (and their associated + acquisition values) given a tensor of initial conditions and an + acquisition function. Other common inputs include lower and upper bounds + and a dictionary of options, but refer to the documentation of specific + generation functions (e.g gen_candidates_scipy and gen_candidates_torch) + for method-specific inputs. Default: `gen_candidates_scipy` + sequential: If False, uses joint optimization, otherwise uses sequential + optimization. + ic_generator: Function for generating initial conditions. Not needed when + `batch_initial_conditions` are provided. Defaults to + `gen_one_shot_kg_initial_conditions` for `qKnowledgeGradient` acquisition + functions and `gen_batch_initial_conditions` otherwise. Must be specified + for nonlinear inequality constraints. + timeout_sec: Max amount of time optimization can run for. + return_full_tree: Return the full tree of optimizers of the previous + iteration. + retry_on_optimization_warning: Whether to retry candidate generation with a new + set of initial conditions when it fails with an `OptimizationWarning`. + ic_gen_kwargs: Additional keyword arguments passed to function specified by + `ic_generator` + + Returns: + A two-element tuple containing + + - A tensor of generated candidates. The shape is + -- `q x d` if `return_best_only` is True (default) + -- `num_restarts x q x d` if `return_best_only` is False + - a tensor of associated acquisition values. If `sequential=False`, + this is a `(num_restarts)`-dim tensor of joint acquisition values + (with explicit restart dimension if `return_best_only=False`). If + `sequential=True`, this is a `q`-dim tensor of expected acquisition + values conditional on having observed candidates `0,1,...,i-1`. + + Example: + >>> # generate `q=2` candidates jointly using 20 random restarts + >>> # and 512 raw samples + >>> candidates, acq_value = optimize_acqf(qEI, bounds, 2, 20, 512) + + >>> generate `q=3` candidates sequentially using 15 random restarts + >>> # and 256 raw samples + >>> qEI = qExpectedImprovement(model, best_f=0.2) + >>> bounds = torch.tensor([[0.], [1.]]) + >>> candidates, acq_value_list = optimize_acqf( + >>> qEI, bounds, 3, 15, 256, sequential=True + >>> ) + """ + # using a default of None simplifies unit testing + if gen_candidates is None: + gen_candidates = gen_candidates_scipy + opt_acqf_inputs = OptimizeAcqfInputs( + acq_function=acq_function, + bounds=bounds, + q=q, + num_restarts=num_restarts, + raw_samples=raw_samples, + options=options, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + nonlinear_inequality_constraints=nonlinear_inequality_constraints, + fixed_features=fixed_features, + post_processing_func=post_processing_func, + batch_initial_conditions=batch_initial_conditions, + return_best_only=return_best_only, + gen_candidates=gen_candidates, + sequential=sequential, + ic_generator=ic_generator, + timeout_sec=timeout_sec, + return_full_tree=return_full_tree, + retry_on_optimization_warning=retry_on_optimization_warning, + ic_gen_kwargs=ic_gen_kwargs, + ) + return _optimize_acqf(opt_acqf_inputs)
+ + + +def _optimize_acqf(opt_inputs: OptimizeAcqfInputs) -> tuple[Tensor, Tensor]: + # Handle the trivial case when all features are fixed + if ( + opt_inputs.fixed_features is not None + and len(opt_inputs.fixed_features) == opt_inputs.bounds.shape[-1] + ): + return _optimize_acqf_all_features_fixed( + bounds=opt_inputs.bounds, + fixed_features=opt_inputs.fixed_features, + q=opt_inputs.q, + acq_function=opt_inputs.acq_function, + ) + + # Perform sequential optimization via successive conditioning on pending points + if opt_inputs.sequential and opt_inputs.q > 1: + return _optimize_acqf_sequential_q(opt_inputs=opt_inputs) + + # Batch optimization (including the case q=1) + return _optimize_acqf_batch(opt_inputs=opt_inputs) + + +
+[docs] +def optimize_acqf_cyclic( + acq_function: AcquisitionFunction, + bounds: Tensor, + q: int, + num_restarts: int, + raw_samples: int | None = None, + options: dict[str, bool | float | int | str] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + fixed_features: dict[int, float] | None = None, + post_processing_func: Callable[[Tensor], Tensor] | None = None, + batch_initial_conditions: Tensor | None = None, + cyclic_options: dict[str, bool | float | int | str] | None = None, + *, + ic_generator: TGenInitialConditions | None = None, + timeout_sec: float | None = None, + return_full_tree: bool = False, + retry_on_optimization_warning: bool = True, + **ic_gen_kwargs: Any, +) -> tuple[Tensor, Tensor]: + r"""Generate a set of `q` candidates via cyclic optimization. + + Args: + acq_function: An AcquisitionFunction + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X` + (if inequality_constraints is provided, these bounds can be -inf and + +inf, respectively). + q: The number of candidates. + num_restarts: Number of starting points for multistart acquisition + function optimization. + raw_samples: Number of samples for initialization. This is required + if `batch_initial_conditions` is not specified. + options: Options for candidate generation. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs` + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs` + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. All indices + should be non-negative. + post_processing_func: A function that post-processes an optimization + result appropriately (i.e., according to `round-trip` + transformations). + batch_initial_conditions: A tensor to specify the initial conditions. + If no initial conditions are provided, the default initialization will + be used. + cyclic_options: Options for stopping criterion for outer cyclic optimization. + ic_generator: Function for generating initial conditions. Not needed when + `batch_initial_conditions` are provided. Defaults to + `gen_one_shot_kg_initial_conditions` for `qKnowledgeGradient` acquisition + functions and `gen_batch_initial_conditions` otherwise. Must be specified + for nonlinear inequality constraints. + timeout_sec: Max amount of time optimization can run for. + return_full_tree: Return the full tree of optimizers of the previous + iteration. + retry_on_optimization_warning: Whether to retry candidate generation with a new + set of initial conditions when it fails with an `OptimizationWarning`. + ic_gen_kwargs: Additional keyword arguments passed to function specified by + `ic_generator` + + Returns: + A two-element tuple containing + + - a `q x d`-dim tensor of generated candidates. + - a `q`-dim tensor of expected acquisition values, where the value at + index `i` is the acquisition value conditional on having observed + all candidates except candidate `i`. + + Example: + >>> # generate `q=3` candidates cyclically using 15 random restarts + >>> # 256 raw samples, and 4 cycles + >>> + >>> qEI = qExpectedImprovement(model, best_f=0.2) + >>> bounds = torch.tensor([[0.], [1.]]) + >>> candidates, acq_value_list = optimize_acqf_cyclic( + >>> qEI, bounds, 3, 15, 256, cyclic_options={"maxiter": 4} + >>> ) + """ + opt_inputs = OptimizeAcqfInputs( + acq_function=acq_function, + bounds=bounds, + q=q, + num_restarts=num_restarts, + raw_samples=raw_samples, + options=options, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + nonlinear_inequality_constraints=None, + fixed_features=fixed_features, + post_processing_func=post_processing_func, + batch_initial_conditions=batch_initial_conditions, + return_best_only=True, + gen_candidates=gen_candidates_scipy, + sequential=True, + ic_generator=ic_generator, + timeout_sec=timeout_sec, + return_full_tree=return_full_tree, + retry_on_optimization_warning=retry_on_optimization_warning, + ic_gen_kwargs=ic_gen_kwargs, + ) + + # for the first cycle, optimize the q candidates sequentially + candidates, acq_vals = _optimize_acqf(opt_inputs) + q = opt_inputs.q + opt_inputs = dataclasses.replace(opt_inputs, q=1) + acq_function = opt_inputs.acq_function + + if q > 1: + cyclic_options = cyclic_options or {} + stopping_criterion = ExpMAStoppingCriterion(**cyclic_options) + stop = stopping_criterion.evaluate(fvals=acq_vals) + base_X_pending = acq_function.X_pending + idxr = torch.ones(q, dtype=torch.bool, device=opt_inputs.bounds.device) + while not stop: + for i in range(q): + # optimize only candidate i + idxr[i] = 0 + acq_function.set_X_pending( + torch.cat([base_X_pending, candidates[idxr]], dim=-2) + if base_X_pending is not None + else candidates[idxr] + ) + opt_inputs = dataclasses.replace( + opt_inputs, + batch_initial_conditions=candidates[i].unsqueeze(0), + sequential=False, + ) + candidate_i, acq_val_i = _optimize_acqf(opt_inputs) + candidates[i] = candidate_i + acq_vals[i] = acq_val_i + idxr[i] = 1 + stop = stopping_criterion.evaluate(fvals=acq_vals) + acq_function.set_X_pending(base_X_pending) + return candidates, acq_vals
+ + + +
+[docs] +def optimize_acqf_list( + acq_function_list: list[AcquisitionFunction], + bounds: Tensor, + num_restarts: int, + raw_samples: int | None = None, + options: dict[str, bool | float | int | str] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None = None, + fixed_features: dict[int, float] | None = None, + fixed_features_list: list[dict[int, float]] | None = None, + post_processing_func: Callable[[Tensor], Tensor] | None = None, + ic_generator: TGenInitialConditions | None = None, + ic_gen_kwargs: dict | None = None, +) -> tuple[Tensor, Tensor]: + r"""Generate a list of candidates from a list of acquisition functions. + + The acquisition functions are optimized in sequence, with previous candidates + set as `X_pending`. This is also known as sequential greedy optimization. + + Args: + acq_function_list: A list of acquisition functions. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X` + (if inequality_constraints is provided, these bounds can be -inf and + +inf, respectively). + num_restarts: Number of starting points for multistart acquisition + function optimization. + raw_samples: Number of samples for initialization. This is required + if `batch_initial_conditions` is not specified. + options: Options for candidate generation. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs` + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs` + nonlinear_inequality_constraints: A list of tuples representing the nonlinear + inequality constraints. The first element in the tuple is a callable + representing a constraint of the form `callable(x) >= 0`. In case of an + intra-point constraint, `callable()`takes in an one-dimensional tensor of + shape `d` and returns a scalar. In case of an inter-point constraint, + `callable()` takes a two dimensional tensor of shape `q x d` and again + returns a scalar. The second element is a boolean, indicating if it is an + intra-point or inter-point constraint (`True` for intra-point. `False` for + inter-point). For more information on intra-point vs inter-point + constraints, see the docstring of the `inequality_constraints` argument to + `optimize_acqf()`. The constraints will later be passed to the scipy + solver. You need to pass in `batch_initial_conditions` in this case. + Using non-linear inequality constraints also requires that `batch_limit` + is set to 1, which will be done automatically if not specified in + `options`. + fixed_features: A map `{feature_index: value}` for features that should + be fixed to a particular value during generation. All indices + (`feature_index`) should be non-negative. + fixed_features_list: A list of maps `{feature_index: value}`. The i-th + item represents the fixed_feature for the i-th optimization. If + `fixed_features_list` is provided, `optimize_acqf_mixed` is invoked. + All indices (`feature_index`) should be non-negative. + post_processing_func: A function that post-processes an optimization + result appropriately (i.e., according to `round-trip` + transformations). + ic_generator: Function for generating initial conditions. Not needed when + `batch_initial_conditions` are provided. Defaults to + `gen_one_shot_kg_initial_conditions` for `qKnowledgeGradient` acquisition + functions and `gen_batch_initial_conditions` otherwise. Must be specified + for nonlinear inequality constraints. + ic_gen_kwargs: Additional keyword arguments passed to function specified by + `ic_generator` + + Returns: + A two-element tuple containing + + - a `q x d`-dim tensor of generated candidates. + - a `q`-dim tensor of expected acquisition values, where the value at + index `i` is the acquisition value conditional on having observed + all candidates except candidate `i`. + """ + if fixed_features and fixed_features_list: + raise ValueError( + "Èither `fixed_feature` or `fixed_features_list` can be provided, not both." + ) + if not acq_function_list: + raise ValueError("acq_function_list must be non-empty.") + candidate_list, acq_value_list = [], [] + candidates = torch.tensor([], device=bounds.device, dtype=bounds.dtype) + base_X_pending = acq_function_list[0].X_pending + for acq_function in acq_function_list: + if candidate_list: + acq_function.set_X_pending( + torch.cat([base_X_pending, candidates], dim=-2) + if base_X_pending is not None + else candidates + ) + if fixed_features_list: + candidate, acq_value = optimize_acqf_mixed( + acq_function=acq_function, + bounds=bounds, + q=1, + num_restarts=num_restarts, + raw_samples=raw_samples, + options=options or {}, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + nonlinear_inequality_constraints=nonlinear_inequality_constraints, + fixed_features_list=fixed_features_list, + post_processing_func=post_processing_func, + ic_generator=ic_generator, + ic_gen_kwargs=ic_gen_kwargs, + ) + else: + ic_gen_kwargs = ic_gen_kwargs or {} + candidate, acq_value = optimize_acqf( + acq_function=acq_function, + bounds=bounds, + q=1, + num_restarts=num_restarts, + raw_samples=raw_samples, + options=options or {}, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + nonlinear_inequality_constraints=nonlinear_inequality_constraints, + fixed_features=fixed_features, + post_processing_func=post_processing_func, + return_best_only=True, + sequential=False, + ic_generator=ic_generator, + **ic_gen_kwargs, + ) + candidate_list.append(candidate) + acq_value_list.append(acq_value) + candidates = torch.cat(candidate_list, dim=-2) + return candidates, torch.stack(acq_value_list)
+ + + +
+[docs] +def optimize_acqf_mixed( + acq_function: AcquisitionFunction, + bounds: Tensor, + q: int, + num_restarts: int, + fixed_features_list: list[dict[int, float]], + raw_samples: int | None = None, + options: dict[str, bool | float | int | str] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None = None, + post_processing_func: Callable[[Tensor], Tensor] | None = None, + batch_initial_conditions: Tensor | None = None, + ic_generator: TGenInitialConditions | None = None, + ic_gen_kwargs: dict | None = None, +) -> tuple[Tensor, Tensor]: + r"""Optimize over a list of fixed_features and returns the best solution. + + This is useful for optimizing over mixed continuous and discrete domains. + For q > 1 this function always performs sequential greedy optimization (with + proper conditioning on generated candidates). + + Args: + acq_function: An AcquisitionFunction + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X` + (if inequality_constraints is provided, these bounds can be -inf and + +inf, respectively). + q: The number of candidates. + num_restarts: Number of starting points for multistart acquisition + function optimization. + raw_samples: Number of samples for initialization. This is required + if `batch_initial_conditions` is not specified. + fixed_features_list: A list of maps `{feature_index: value}`. The i-th + item represents the fixed_feature for the i-th optimization. All + indices (`feature_index`) should be non-negative. + options: Options for candidate generation. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs` + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs` + nonlinear_inequality_constraints: A list of tuples representing the nonlinear + inequality constraints. The first element in the tuple is a callable + representing a constraint of the form `callable(x) >= 0`. In case of an + intra-point constraint, `callable()`takes in an one-dimensional tensor of + shape `d` and returns a scalar. In case of an inter-point constraint, + `callable()` takes a two dimensional tensor of shape `q x d` and again + returns a scalar. The second element is a boolean, indicating if it is an + intra-point or inter-point constraint (`True` for intra-point. `False` for + inter-point). For more information on intra-point vs inter-point + constraints, see the docstring of the `inequality_constraints` argument to + `optimize_acqf()`. The constraints will later be passed to the scipy + solver. You need to pass in `batch_initial_conditions` in this case. + Using non-linear inequality constraints also requires that `batch_limit` + is set to 1, which will be done automatically if not specified in + `options`. + post_processing_func: A function that post-processes an optimization + result appropriately (i.e., according to `round-trip` + transformations). + batch_initial_conditions: A tensor to specify the initial conditions. Set + this if you do not want to use default initialization strategy. + ic_generator: Function for generating initial conditions. Not needed when + `batch_initial_conditions` are provided. Defaults to + `gen_one_shot_kg_initial_conditions` for `qKnowledgeGradient` acquisition + functions and `gen_batch_initial_conditions` otherwise. Must be specified + for nonlinear inequality constraints. + ic_gen_kwargs: Additional keyword arguments passed to function specified by + `ic_generator` + + Returns: + A two-element tuple containing + + - a `q x d`-dim tensor of generated candidates. + - an associated acquisition value. + """ + if not fixed_features_list: + raise ValueError("fixed_features_list must be non-empty.") + + if isinstance(acq_function, OneShotAcquisitionFunction): + if not hasattr(acq_function, "evaluate") and q > 1: + raise ValueError( + "`OneShotAcquisitionFunction`s that do not implement `evaluate` " + "are currently not supported when `q > 1`. This is needed to " + "compute the joint acquisition value." + ) + + ic_gen_kwargs = ic_gen_kwargs or {} + + if q == 1: + ff_candidate_list, ff_acq_value_list = [], [] + num_candidate_generation_failures = 0 + for fixed_features in fixed_features_list: + try: + candidate, acq_value = optimize_acqf( + acq_function=acq_function, + bounds=bounds, + q=q, + num_restarts=num_restarts, + raw_samples=raw_samples, + options=options or {}, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + nonlinear_inequality_constraints=nonlinear_inequality_constraints, + fixed_features=fixed_features, + post_processing_func=post_processing_func, + batch_initial_conditions=batch_initial_conditions, + ic_generator=ic_generator, + return_best_only=True, + **ic_gen_kwargs, + ) + except CandidateGenerationError: + # if candidate generation fails, we skip this candidate + num_candidate_generation_failures += 1 + continue + ff_candidate_list.append(candidate) + ff_acq_value_list.append(acq_value) + + if len(ff_candidate_list) == 0: + raise CandidateGenerationError( + "Candidate generation failed for all `fixed_features`." + ) + elif num_candidate_generation_failures > 0: + warnings.warn( + f"Candidate generation failed for {num_candidate_generation_failures} " + "combinations of `fixed_features`. To suppress this warning, make " + "sure all equality/inequality constraints can be satisfied by all " + "`fixed_features` in `fixed_features_list`.", + OptimizationWarning, + stacklevel=3, + ) + ff_acq_values = torch.stack(ff_acq_value_list) + best = torch.argmax(ff_acq_values) + return ff_candidate_list[best], ff_acq_values[best] + + # For batch optimization with q > 1 we do not want to enumerate all n_combos^n + # possible combinations of discrete choices. Instead, we use sequential greedy + # optimization. + base_X_pending = acq_function.X_pending + candidates = torch.tensor([], device=bounds.device, dtype=bounds.dtype) + + for _ in range(q): + candidate, acq_value = optimize_acqf_mixed( + acq_function=acq_function, + bounds=bounds, + q=1, + num_restarts=num_restarts, + raw_samples=raw_samples, + fixed_features_list=fixed_features_list, + options=options or {}, + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + nonlinear_inequality_constraints=nonlinear_inequality_constraints, + post_processing_func=post_processing_func, + batch_initial_conditions=batch_initial_conditions, + ic_generator=ic_generator, + ic_gen_kwargs=ic_gen_kwargs, + ) + candidates = torch.cat([candidates, candidate], dim=-2) + acq_function.set_X_pending( + torch.cat([base_X_pending, candidates], dim=-2) + if base_X_pending is not None + else candidates + ) + + acq_function.set_X_pending(base_X_pending) + + # compute joint acquisition value + if isinstance(acq_function, OneShotAcquisitionFunction): + acq_value = acq_function.evaluate(X=candidates, bounds=bounds) + else: + acq_value = acq_function(candidates) + return candidates, acq_value
+ + + +
+[docs] +def optimize_acqf_discrete( + acq_function: AcquisitionFunction, + q: int, + choices: Tensor, + max_batch_size: int = 2048, + unique: bool = True, + X_avoid: Tensor | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, +) -> tuple[Tensor, Tensor]: + r"""Optimize over a discrete set of points using batch evaluation. + + For `q > 1` this function generates candidates by means of sequential + conditioning (rather than joint optimization), since for all but the + smalles number of choices the set `choices^q` of discrete points to + evaluate quickly explodes. + + Args: + acq_function: An AcquisitionFunction. + q: The number of candidates. + choices: A `num_choices x d` tensor of possible choices. + max_batch_size: The maximum number of choices to evaluate in batch. + A large limit can cause excessive memory usage if the model has + a large training set. + unique: If True return unique choices, o/w choices may be repeated + (only relevant if `q > 1`). + X_avoid: An `n x d` tensor of candidates that we aren't allowed to pick. + These will be removed from the set of choices. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + Infeasible points will be removed from the set of choices. + + Returns: + A two-element tuple containing + + - a `q x d`-dim tensor of generated candidates. + - an associated acquisition value. + """ + if isinstance(acq_function, OneShotAcquisitionFunction): + raise UnsupportedError( + "Discrete optimization is not supported for" + "one-shot acquisition functions." + ) + if X_avoid is not None and unique: + choices = _filter_invalid(X=choices, X_avoid=X_avoid) + if inequality_constraints is not None: + choices = _filter_infeasible( + X=choices, inequality_constraints=inequality_constraints + ) + len_choices = len(choices) + if len_choices == 0: + message = "`choices` must be non-empty." + if X_avoid is not None or inequality_constraints is not None: + message += ( + " No feasible points remain after removing `X_avoid` and " + "filtering out infeasible points." + ) + raise InputDataError(message) + elif len_choices < q and unique: + warnings.warn( + ( + f"Requested {q=} candidates from fully discrete search " + f"space, but only {len_choices} possible choices remain. " + ), + OptimizationWarning, + stacklevel=2, + ) + q = len_choices + choices_batched = choices.unsqueeze(-2) + if q > 1: + candidate_list, acq_value_list = [], [] + base_X_pending = acq_function.X_pending + for _ in range(q): + with torch.no_grad(): + acq_values = _split_batch_eval_acqf( + acq_function=acq_function, + X=choices_batched, + max_batch_size=max_batch_size, + ) + best_idx = torch.argmax(acq_values) + candidate_list.append(choices_batched[best_idx]) + acq_value_list.append(acq_values[best_idx]) + # set pending points + candidates = torch.cat(candidate_list, dim=-2) + acq_function.set_X_pending( + torch.cat([base_X_pending, candidates], dim=-2) + if base_X_pending is not None + else candidates + ) + # need to remove choice from choice set if enforcing uniqueness + if unique: + choices_batched = torch.cat( + [choices_batched[:best_idx], choices_batched[best_idx + 1 :]] + ) + + # Reset acq_func to previous X_pending state + acq_function.set_X_pending(base_X_pending) + return candidates, torch.stack(acq_value_list) + + with torch.no_grad(): + acq_values = _split_batch_eval_acqf( + acq_function=acq_function, X=choices_batched, max_batch_size=max_batch_size + ) + best_idx = torch.argmax(acq_values) + return choices_batched[best_idx], acq_values[best_idx]
+ + + +def _split_batch_eval_acqf( + acq_function: AcquisitionFunction, X: Tensor, max_batch_size: int +) -> Tensor: + return torch.cat([acq_function(X_) for X_ in X.split(max_batch_size)]) + + +def _generate_neighbors( + x: Tensor, + discrete_choices: list[Tensor], + X_avoid: Tensor, + inequality_constraints: list[tuple[Tensor, Tensor, float]], +) -> Tensor: + # generate all 1D perturbations + npts = sum([len(c) for c in discrete_choices]) + X_loc = x.repeat(npts, 1) + j = 0 + for i, c in enumerate(discrete_choices): + X_loc[j : j + len(c), i] = c + j += len(c) + # remove invalid and infeasible points (also remove x) + X_loc = _filter_invalid(X=X_loc, X_avoid=torch.cat((X_avoid, x))) + X_loc = _filter_infeasible(X=X_loc, inequality_constraints=inequality_constraints) + return X_loc + + +def _filter_infeasible( + X: Tensor, inequality_constraints: list[tuple[Tensor, Tensor, float]] +) -> Tensor: + """Remove all points from `X` that don't satisfy the constraints.""" + is_feasible = torch.ones(X.shape[0], dtype=torch.bool, device=X.device) + for inds, weights, bound in inequality_constraints: + is_feasible &= (X[..., inds] * weights).sum(dim=-1) >= bound + return X[is_feasible] + + +def _filter_invalid(X: Tensor, X_avoid: Tensor) -> Tensor: + """Remove all occurences of `X_avoid` from `X`.""" + return X[~(X == X_avoid.unsqueeze(-2)).all(dim=-1).any(dim=-2)] + + +def _gen_batch_initial_conditions_local_search( + discrete_choices: list[Tensor], + raw_samples: int, + X_avoid: Tensor, + inequality_constraints: list[tuple[Tensor, Tensor, float]], + min_points: int, + max_tries: int = 100, +): + """Generate initial conditions for local search.""" + device = discrete_choices[0].device + dtype = discrete_choices[0].dtype + dim = len(discrete_choices) + X = torch.zeros(0, dim, device=device, dtype=dtype) + for _ in range(max_tries): + X_new = torch.zeros(raw_samples, dim, device=device, dtype=dtype) + for i, c in enumerate(discrete_choices): + X_new[:, i] = c[ + torch.randint(low=0, high=len(c), size=(raw_samples,), device=c.device) + ] + X = torch.unique(torch.cat((X, X_new)), dim=0) + X = _filter_invalid(X=X, X_avoid=X_avoid) + X = _filter_infeasible(X=X, inequality_constraints=inequality_constraints) + if len(X) >= min_points: + return X + raise RuntimeError(f"Failed to generate at least {min_points} initial conditions") + + + + +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/optimize_homotopy.html b/website-old/pages/api/_modules/botorch/optim/optimize_homotopy.html new file mode 100644 index 0000000000..64bd24203e --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/optimize_homotopy.html @@ -0,0 +1,300 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.optimize_homotopy

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from typing import Any
+
+import torch
+from botorch.acquisition import AcquisitionFunction
+
+from botorch.generation.gen import TGenCandidates
+from botorch.optim.homotopy import Homotopy
+from botorch.optim.initializers import TGenInitialConditions
+from botorch.optim.optimize import optimize_acqf
+from torch import Tensor
+
+
+
+[docs] +def prune_candidates( + candidates: Tensor, acq_values: Tensor, prune_tolerance: float +) -> Tensor: + r"""Prune candidates based on their distance to other candidates. + + Args: + candidates: An `n x d` tensor of candidates. + acq_values: An `n` tensor of candidate values. + prune_tolerance: The minimum distance to prune candidates. + + Returns: + An `m x d` tensor of pruned candidates. + """ + if candidates.ndim != 2: + raise ValueError("`candidates` must be of size `n x d`.") + if acq_values.ndim != 1 or len(acq_values) != candidates.shape[0]: + raise ValueError("`acq_values` must be of size `n`.") + if prune_tolerance < 0: + raise ValueError("`prune_tolerance` must be >= 0.") + sorted_inds = acq_values.argsort(descending=True) + candidates = candidates[sorted_inds] + + candidates_new = candidates[:1, :] + for i in range(1, candidates.shape[0]): + if ( + torch.cdist(candidates[i : i + 1, :], candidates_new).min() + > prune_tolerance + ): + candidates_new = torch.cat( + [candidates_new, candidates[i : i + 1, :]], dim=-2 + ) + return candidates_new
+ + + +
+[docs] +def optimize_acqf_homotopy( + acq_function: AcquisitionFunction, + bounds: Tensor, + q: int, + num_restarts: int, + homotopy: Homotopy, + prune_tolerance: float = 1e-4, + raw_samples: int | None = None, + options: dict[str, bool | float | int | str] | None = None, + final_options: dict[str, bool | float | int | str] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None = None, + fixed_features: dict[int, float] | None = None, + post_processing_func: Callable[[Tensor], Tensor] | None = None, + batch_initial_conditions: Tensor | None = None, + gen_candidates: TGenCandidates | None = None, + sequential: bool = False, + *, + ic_generator: TGenInitialConditions | None = None, + timeout_sec: float | None = None, + return_full_tree: bool = False, + retry_on_optimization_warning: bool = True, + **ic_gen_kwargs: Any, +) -> tuple[Tensor, Tensor]: + r"""Generate a set of candidates via multi-start optimization. + + Args: + acq_function: An AcquisitionFunction. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X` + (if inequality_constraints is provided, these bounds can be -inf and + +inf, respectively). + q: The number of candidates. + homotopy: Homotopy object that will make the necessary modifications to the + problem when calling `step()`. + prune_tolerance: The minimum distance to prune candidates. + num_restarts: The number of starting points for multistart acquisition + function optimization. + raw_samples: The number of samples for initialization. This is required + if `batch_initial_conditions` is not specified. + options: Options for candidate generation in the initial step of the homotopy. + final_options: Options for candidate generation in the final step of + the homotopy. + inequality_constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. `indices` and + `coefficients` should be torch tensors. See the docstring of + `make_scipy_linear_constraints` for an example. When q=1, or when + applying the same constraint to each candidate in the batch + (intra-point constraint), `indices` should be a 1-d tensor. + For inter-point constraints, in which the constraint is applied to the + whole batch of candidates, `indices` must be a 2-d tensor, where + in each row `indices[i] =(k_i, l_i)` the first index `k_i` corresponds + to the `k_i`-th element of the `q`-batch and the second index `l_i` + corresponds to the `l_i`-th feature of that element. + equality_constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an equality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. See the docstring of + `make_scipy_linear_constraints` for an example. + nonlinear_inequality_constraints: A list of tuples representing the nonlinear + inequality constraints. The first element in the tuple is a callable + representing a constraint of the form `callable(x) >= 0`. In case of an + intra-point constraint, `callable()`takes in an one-dimensional tensor of + shape `d` and returns a scalar. In case of an inter-point constraint, + `callable()` takes a two dimensional tensor of shape `q x d` and again + returns a scalar. The second element is a boolean, indicating if it is an + intra-point or inter-point constraint (`True` for intra-point. `False` for + inter-point). For more information on intra-point vs inter-point + constraints, see the docstring of the `inequality_constraints` argument to + `optimize_acqf()`. The constraints will later be passed to the scipy + solver. You need to pass in `batch_initial_conditions` in this case. + Using non-linear inequality constraints also requires that `batch_limit` + is set to 1, which will be done automatically if not specified in + `options`. + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. + post_processing_func: A function that post-processes an optimization + result appropriately (i.e., according to `round-trip` + transformations). + batch_initial_conditions: A tensor to specify the initial conditions. Set + this if you do not want to use default initialization strategy. + gen_candidates: A callable for generating candidates (and their associated + acquisition values) given a tensor of initial conditions and an + acquisition function. Other common inputs include lower and upper bounds + and a dictionary of options, but refer to the documentation of specific + generation functions (e.g gen_candidates_scipy and gen_candidates_torch) + for method-specific inputs. Default: `gen_candidates_scipy` + sequential: If False, uses joint optimization, otherwise uses sequential + optimization. + ic_generator: Function for generating initial conditions. Not needed when + `batch_initial_conditions` are provided. Defaults to + `gen_one_shot_kg_initial_conditions` for `qKnowledgeGradient` acquisition + functions and `gen_batch_initial_conditions` otherwise. Must be specified + for nonlinear inequality constraints. + timeout_sec: Max amount of time optimization can run for. + return_full_tree: Return the full tree of optimizers of the previous + iteration. + retry_on_optimization_warning: Whether to retry candidate generation with a new + set of initial conditions when it fails with an `OptimizationWarning`. + ic_gen_kwargs: Additional keyword arguments passed to function specified by + `ic_generator` + """ + shared_optimize_acqf_kwargs = { + "num_restarts": num_restarts, + "raw_samples": raw_samples, + "inequality_constraints": inequality_constraints, + "equality_constraints": equality_constraints, + "nonlinear_inequality_constraints": nonlinear_inequality_constraints, + "fixed_features": fixed_features, + "return_best_only": False, # False to make n_restarts persist through homotopy. + "gen_candidates": gen_candidates, + "sequential": sequential, + "ic_generator": ic_generator, + "timeout_sec": timeout_sec, + "return_full_tree": return_full_tree, + "retry_on_optimization_warning": retry_on_optimization_warning, + **ic_gen_kwargs, + } + + candidate_list, acq_value_list = [], [] + if q > 1: + base_X_pending = acq_function.X_pending + + for _ in range(q): + candidates = batch_initial_conditions + homotopy.restart() + + while not homotopy.should_stop: + candidates, acq_values = optimize_acqf( + acq_function=acq_function, + bounds=bounds, + q=1, + options=options, + batch_initial_conditions=candidates, + **shared_optimize_acqf_kwargs, + ) + homotopy.step() + + # Prune candidates + candidates = prune_candidates( + candidates=candidates.squeeze(1), + acq_values=acq_values, + prune_tolerance=prune_tolerance, + ).unsqueeze(1) + + # Optimize one more time with the final options + candidates, acq_values = optimize_acqf( + acq_function=acq_function, + bounds=bounds, + q=1, + options=final_options, + batch_initial_conditions=candidates, + **shared_optimize_acqf_kwargs, + ) + + # Post-process the candidates and grab the best candidate + if post_processing_func is not None: + candidates = post_processing_func(candidates) + acq_values = acq_function(candidates) + + best = torch.argmax(acq_values.view(-1), dim=0) + candidate, acq_value = candidates[best], acq_values[best] + + # Keep the new candidate and update the pending points + candidate_list.append(candidate) + acq_value_list.append(acq_value) + selected_candidates = torch.cat(candidate_list, dim=-2) + + if q > 1: + acq_function.set_X_pending( + torch.cat([base_X_pending, selected_candidates], dim=-2) + if base_X_pending is not None + else selected_candidates + ) + + if q > 1: # Reset acq_function to previous X_pending state + acq_function.set_X_pending(base_X_pending) + + homotopy.reset() # Reset the homotopy parameters + + return selected_candidates, torch.stack(acq_value_list)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/optimize_mixed.html b/website-old/pages/api/_modules/botorch/optim/optimize_mixed.html new file mode 100644 index 0000000000..32a48986d7 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/optimize_mixed.html @@ -0,0 +1,857 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.optimize_mixed

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+import dataclasses
+import warnings
+from typing import Any, Callable
+
+import torch
+from botorch.acquisition import AcquisitionFunction
+from botorch.exceptions.errors import CandidateGenerationError, UnsupportedError
+from botorch.exceptions.warnings import OptimizationWarning
+from botorch.generation.gen import gen_candidates_scipy
+from botorch.optim.initializers import initialize_q_batch
+from botorch.optim.optimize import (
+    _optimize_acqf,
+    _validate_sequential_inputs,
+    OptimizeAcqfInputs,
+)
+from botorch.optim.utils.acquisition_utils import fix_features, get_X_baseline
+from botorch.utils.sampling import (
+    draw_sobol_samples,
+    HitAndRunPolytopeSampler,
+    sparse_to_dense_constraints,
+)
+from botorch.utils.transforms import unnormalize
+from pyre_extensions import assert_is_instance, none_throws
+from torch import Tensor
+from torch.quasirandom import SobolEngine
+
+# Default values.
+# NOTE: When changing a default, update the corresponding value in the docstrings.
+STD_CONT_PERTURBATION = 0.1
+RAW_SAMPLES = 1024  # Number of candidates from which to select starting points.
+NUM_RESTARTS = 20  # Number of restarts of optimizer with different starting points.
+MAX_BATCH_SIZE = 2048  # Maximum batch size.
+MAX_ITER_ALTER = 64  # Maximum number of alternating iterations.
+MAX_ITER_DISCRETE = 4  # Maximum number of discrete iterations.
+MAX_ITER_CONT = 8  # Maximum number of continuous iterations.
+# Maximum number of discrete values for a discrete dimension.
+# If there are more values for a dimension, we will use continuous
+# relaxation to optimize it.
+MAX_DISCRETE_VALUES = 20
+# Maximum number of iterations for optimizing the continuous relaxation
+# during initialization
+MAX_ITER_INIT = 100
+CONVERGENCE_TOL = 1e-8  # Optimizer convergence tolerance.
+DUPLICATE_TOL = 1e-6  # Tolerance for deduplicating initial candidates.
+
+SUPPORTED_OPTIONS = {
+    "initialization_strategy",
+    "tol",
+    "maxiter_alternating",
+    "maxiter_discrete",
+    "maxiter_continuous",
+    "maxiter_init",
+    "max_discrete_values",
+    "num_spray_points",
+    "std_cont_perturbation",
+    "batch_limit",
+    "init_batch_limit",
+}
+SUPPORTED_INITIALIZATION = {"continuous_relaxation", "equally_spaced", "random"}
+
+
+def _setup_continuous_relaxation(
+    discrete_dims: list[int],
+    bounds: Tensor,
+    max_discrete_values: int,
+    post_processing_func: Callable[[Tensor], Tensor] | None,
+) -> tuple[list[int], Callable[[Tensor], Tensor] | None]:
+    r"""Update `discrete_dims` and `post_processing_func` to use
+    continuous relaxation for discrete dimensions that have more than
+    `max_discrete_values` values. These dimensions are removed from
+    `discrete_dims` and `post_processing_func` is updated to round
+    them to the nearest integer.
+    """
+    discrete_dims_t = torch.tensor(discrete_dims, dtype=torch.long)
+    num_discrete_values = (
+        bounds[1, discrete_dims_t] - bounds[0, discrete_dims_t]
+    ).cpu()
+    dims_to_relax = discrete_dims_t[num_discrete_values > max_discrete_values]
+    if dims_to_relax.numel() == 0:
+        # No dimension needs continuous relaxation.
+        return discrete_dims, post_processing_func
+    # Remove relaxed dims from `discrete_dims`.
+    discrete_dims = list(set(discrete_dims).difference(dims_to_relax.tolist()))
+
+    def new_post_processing_func(X: Tensor) -> Tensor:
+        r"""Round the relaxed dimensions to the nearest integer and apply the original
+        `post_processing_func`."""
+        X[..., dims_to_relax] = X[..., dims_to_relax].round()
+        if post_processing_func is not None:
+            X = post_processing_func(X)
+        return X
+
+    return discrete_dims, new_post_processing_func
+
+
+def _filter_infeasible(
+    X: Tensor, inequality_constraints: list[tuple[Tensor, Tensor, float]] | None
+) -> Tensor:
+    r"""Filters infeasible points from a set of points.
+
+    NOTE: This function only supports intra-point constraints. This is validated
+        in `optimize_acqf_mixed_alternating`, so we do not repeat the
+        validation in here.
+
+    Args:
+        X: A tensor of points of shape `n x d`.
+        inequality_constraints: A list of tuples (indices, coefficients, rhs),
+            with each tuple encoding an inequality constraint of the form
+            `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. `indices` and
+            `coefficients` should be torch tensors. See the docstring of
+            `make_scipy_linear_constraints` for an example.
+
+    Returns:
+        The tensor `X` with infeasible points removed.
+    """
+    if inequality_constraints is None:
+        return X
+    is_feasible = torch.ones(X.shape[:-1], device=X.device, dtype=torch.bool)
+    for idx, coef, rhs in inequality_constraints:
+        is_feasible &= (X[..., idx] * coef).sum(dim=-1) >= rhs
+    return X[is_feasible]
+
+
+
+[docs] +def get_nearest_neighbors( + current_x: Tensor, + bounds: Tensor, + discrete_dims: Tensor, +) -> Tensor: + r"""Generate all 1-Manhattan distance neighbors of a given input. The neighbors + are generated for the discrete dimensions only. + + NOTE: This assumes that `current_x` is detached and uses in-place operations, + which are known to be incompatible with autograd. + + Args: + current_x: The design to find the neighbors of. A tensor of shape `d`. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X`. + discrete_dims: A tensor of indices corresponding to binary and + integer parameters. + + Returns: + A tensor of shape `num_neighbors x d`, denoting all unique 1-Manhattan + distance neighbors. + """ + num_discrete = len(discrete_dims) + diag_ones = torch.eye(num_discrete, dtype=current_x.dtype, device=current_x.device) + # Neighbors obtained by increasing a discrete dimension by one. + plus_neighbors = current_x.repeat(num_discrete, 1) + plus_neighbors[:, discrete_dims] += diag_ones + plus_neighbors.clamp_(max=bounds[1]) + # Neighbors obtained by decreasing a discrete dimension by one. + minus_neighbors = current_x.repeat(num_discrete, 1) + minus_neighbors[:, discrete_dims] -= diag_ones + minus_neighbors.clamp_(min=bounds[0]) + unique_neighbors = torch.cat([minus_neighbors, plus_neighbors], dim=0).unique(dim=0) + # Also remove current_x if it is in unique_neighbors. + unique_neighbors = unique_neighbors[~(unique_neighbors == current_x).all(dim=-1)] + return unique_neighbors
+ + + +
+[docs] +def get_spray_points( + X_baseline: Tensor, + cont_dims: Tensor, + discrete_dims: Tensor, + bounds: Tensor, + num_spray_points: int, + std_cont_perturbation: float = STD_CONT_PERTURBATION, +) -> Tensor: + r"""Generate spray points by perturbing the Pareto optimal points. + + Given the points on the Pareto frontier, we create perturbations (spray points) + by adding Gaussian perturbation to the continuous parameters and 1-Manhattan + distance neighbors of the discrete (binary and integer) parameters. + + Args: + X_baseline: Tensor of best acquired points across BO run. + cont_dims: Indices of continuous parameters/input dimensions. + discrete_dims: Indices of binary/integer parameters/input dimensions. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X`. + num_spray_points: Number of spray points to return. + std_cont_perturbation: standard deviation of Normal perturbations of + continuous dimensions. Default is STD_CONT_PERTURBATION = 0.2. + + Returns: + A (num_spray_points x d)-dim tensor of perturbed points. + """ + dim = bounds.shape[-1] + device, dtype = X_baseline.device, X_baseline.dtype + perturb_nbors = torch.zeros(0, dim, device=device, dtype=dtype) + for x in X_baseline: + discrete_perturbs = get_nearest_neighbors( + current_x=x, bounds=bounds, discrete_dims=discrete_dims + ) + discrete_perturbs = discrete_perturbs[ + torch.randint(len(discrete_perturbs), (num_spray_points,), device=device) + ] + cont_perturbs = x[cont_dims] + std_cont_perturbation * torch.randn( + num_spray_points, len(cont_dims), device=device, dtype=dtype + ) + cont_perturbs = cont_perturbs.clamp_( + min=bounds[0, cont_dims], max=bounds[1, cont_dims] + ) + nbds = torch.zeros(num_spray_points, dim, device=device, dtype=dtype) + nbds[..., discrete_dims] = discrete_perturbs[..., discrete_dims] + nbds[..., cont_dims] = cont_perturbs + perturb_nbors = torch.cat([perturb_nbors, nbds], dim=0) + return perturb_nbors
+ + + +
+[docs] +def sample_feasible_points( + opt_inputs: OptimizeAcqfInputs, + discrete_dims: Tensor, + num_points: int, +) -> Tensor: + r"""Sample feasible points from the optimization domain. + + Feasibility is determined according to the discrete dimensions taking + integer values and the inequality constraints being satisfied. + + If there are no inequality constraints, Sobol is used to generate the base points. + Otherwise, we use the polytope sampler to generate the base points. The base points + are then rounded to the nearest integer values for the discrete dimensions, and + the infeasible points are filtered out (in case rounding leads to infeasibility). + + This method will do 10 attempts to generate `num_points` feasible points, and + return the points generated so far. If no points are generated, it will error out. + + Args: + opt_inputs: Common set of arguments for acquisition optimization. + discrete_dims: A tensor of indices corresponding to binary and + integer parameters. + num_points: The number of points to sample. + + Returns: + A tensor of shape `num_points x d` containing the sampled points. + """ + bounds = opt_inputs.bounds + all_points = torch.empty( + 0, bounds.shape[-1], device=bounds.device, dtype=bounds.dtype + ) + constraints = opt_inputs.inequality_constraints + if constraints is None: + # Generate base points using Sobol. + sobol_engine = SobolEngine(dimension=bounds.shape[-1], scramble=True) + + def generator(n: int) -> Tensor: + samples = sobol_engine.draw(n=n, dtype=bounds.dtype).to(bounds.device) + return unnormalize(X=samples, bounds=bounds) + + else: + # Generate base points using polytope sampler. + # Since we may generate many times, we initialize the sampler with burn-in + # to reduce the start-up cost for subsequent calls. + A, b = sparse_to_dense_constraints(d=bounds.shape[-1], constraints=constraints) + polytope_sampler = HitAndRunPolytopeSampler( + bounds=bounds, inequality_constraints=(-A, -b) + ) + + def generator(n: int) -> Tensor: + return polytope_sampler.draw(n=n) + + for _ in range(10): + num_remaining = num_points - len(all_points) + if num_remaining <= 0: + break + # Generate twice as many, since we're likely to filter out some points. + base_points = generator(n=num_remaining * 2) + # Round the discrete dimensions to the nearest integer. + base_points[:, discrete_dims] = base_points[:, discrete_dims].round() + # Fix the fixed features. + base_points = fix_features( + X=base_points, fixed_features=opt_inputs.fixed_features + ) + # Filter out infeasible points. + feasible_points = _filter_infeasible( + X=base_points, inequality_constraints=constraints + ) + all_points = torch.cat([all_points, feasible_points], dim=0) + + if len(all_points) == 0: + raise CandidateGenerationError( + "Could not generate any feasible starting points for mixed optimizer." + ) + return all_points[:num_points]
+ + + +
+[docs] +def generate_starting_points( + opt_inputs: OptimizeAcqfInputs, + discrete_dims: Tensor, + cont_dims: Tensor, +) -> tuple[Tensor, Tensor]: + """Generate initial starting points for the alternating optimization. + + This method attempts to generate the initial points using the specified + options and completes any missing points using `sample_feasible_points`. + + Args: + opt_inputs: Common set of arguments for acquisition optimization. + This function utilizes `acq_function`, `bounds`, `num_restarts`, + `raw_samples`, `options`, `fixed_features` and constraints + from `opt_inputs`. + discrete_dims: A tensor of indices corresponding to integer and + binary parameters. + cont_dims: A tensor of indices corresponding to continuous parameters. + + Returns: + A tuple of two tensors: a (num_restarts x d)-dim tensor of starting points + and a (num_restarts)-dim tensor of their respective acquisition values. + In rare cases, this method may return fewer than `num_restarts` points. + """ + bounds = opt_inputs.bounds + binary_dims = [] + for dim in discrete_dims: + if bounds[0, dim] == 0 and bounds[1, dim] == 1: + binary_dims.append(dim) + num_binary = len(binary_dims) + num_integer = len(discrete_dims) - num_binary + num_restarts = opt_inputs.num_restarts + raw_samples = none_throws(opt_inputs.raw_samples) + + options = opt_inputs.options or {} + initialization_strategy = options.get( + "initialization_strategy", + ( + "equally_spaced" + if num_integer == 0 and num_binary >= 2 + else "continuous_relaxation" + ), + ) + if initialization_strategy not in SUPPORTED_INITIALIZATION: + raise UnsupportedError( # pragma: no cover + f"Unsupported initialization strategy: {initialization_strategy}." + f"Supported strategies are: {SUPPORTED_INITIALIZATION}." + ) + + # Initialize `x_init_candts` here so that it's always defined as a tensor. + x_init_candts = torch.empty( + 0, bounds.shape[-1], device=bounds.device, dtype=bounds.dtype + ) + if initialization_strategy == "continuous_relaxation": + try: + # Optimize the acquisition function with continuous relaxation. + updated_opt_inputs = dataclasses.replace( + opt_inputs, + q=1, + return_best_only=False, + options={ + "maxiter": options.get("maxiter_init", MAX_ITER_INIT), + "batch_limit": options.get("batch_limit", MAX_BATCH_SIZE), + "init_batch_limit": options.get("init_batch_limit", MAX_BATCH_SIZE), + }, + ) + x_init_candts, _ = _optimize_acqf(opt_inputs=updated_opt_inputs) + x_init_candts = x_init_candts.squeeze(-2).detach() + except Exception as e: + warnings.warn( + "Failed to initialize using continuous relaxation. Using " + "`sample_feasible_points` for initialization. Original error " + f"message: {e}", + OptimizationWarning, + stacklevel=2, + ) + + if len(x_init_candts) == 0: + # Generate Sobol points as a fallback for `continuous_relaxation` and for + # further refinement in `equally_spaced` strategy. + x_init_candts = draw_sobol_samples(bounds=bounds, n=raw_samples, q=1) + x_init_candts = x_init_candts.squeeze(-2) + + if initialization_strategy == "equally_spaced": + if num_integer > 0: + raise ValueError( # pragma: no cover + "Equally spaced initialization is not supported with non-binary " + "discrete variables." + ) + # Picking initial points by equally spaced number of features/binary inputs. + k = torch.randint( + low=0, + high=num_binary, + size=(raw_samples,), + dtype=torch.int64, + device=bounds.device, + ) + x_init_candts[:, binary_dims] = 0 + binary_dims_t = torch.as_tensor(binary_dims, device=bounds.device) + for i, xi in enumerate(x_init_candts): + rand_binary_dims = binary_dims_t[ + torch.randperm(num_binary, device=xi.device)[: k[i]] + ] + x_init_candts[i, rand_binary_dims] = 1 + + num_spray_points = assert_is_instance( + options.get("num_spray_points", 20 if num_integer == 0 else 0), int + ) + if ( + num_spray_points > 0 + and (X_baseline := get_X_baseline(acq_function=opt_inputs.acq_function)) + is not None + ): + perturb_nbors = get_spray_points( + X_baseline=X_baseline, + cont_dims=cont_dims, + discrete_dims=discrete_dims, + bounds=bounds, + num_spray_points=num_spray_points, + std_cont_perturbation=assert_is_instance( + options.get("std_cont_perturbation", STD_CONT_PERTURBATION), float + ), + ) + x_init_candts = torch.cat([x_init_candts, perturb_nbors], dim=0) + + # Process the candidates to make sure they are all feasible. + x_init_candts[..., discrete_dims] = x_init_candts[..., discrete_dims].round() + x_init_candts = fix_features( + X=x_init_candts, fixed_features=opt_inputs.fixed_features + ) + x_init_candts = _filter_infeasible( + X=x_init_candts, inequality_constraints=opt_inputs.inequality_constraints + ) + + # If there are fewer than `num_restarts` feasible points, attempt to generate more. + if len(x_init_candts) < num_restarts: + new_x_init = sample_feasible_points( + opt_inputs=opt_inputs, + discrete_dims=discrete_dims, + num_points=num_restarts - len(x_init_candts), + ) + x_init_candts = torch.cat([x_init_candts, new_x_init], dim=0) + + with torch.no_grad(): + acq_vals = torch.cat( + [ + opt_inputs.acq_function(X_.unsqueeze(-2)) + for X_ in x_init_candts.split( + options.get("init_batch_limit", MAX_BATCH_SIZE) + ) + ] + ) + if len(x_init_candts) > num_restarts: + # If there are more than `num_restarts` feasible points, select a diverse + # set of initializers using Boltzmann sampling. + x_init_candts, acq_vals = initialize_q_batch( + X=x_init_candts, acq_vals=acq_vals, n=num_restarts + ) + return x_init_candts, acq_vals
+ + + +
+[docs] +def discrete_step( + opt_inputs: OptimizeAcqfInputs, + discrete_dims: Tensor, + current_x: Tensor, +) -> tuple[Tensor, Tensor]: + """Discrete nearest neighbour search. + + Args: + opt_inputs: Common set of arguments for acquisition optimization. + This function utilizes `acq_function`, `bounds`, `options` + and constraints from `opt_inputs`. + discrete_dims: A tensor of indices corresponding to binary and + integer parameters. + current_x: Starting point. A tensor of shape `d`. + + Returns: + A tuple of two tensors: a (d)-dim tensor of optimized point + and a scalar tensor of correspondins acquisition value. + """ + with torch.no_grad(): + current_acqval = opt_inputs.acq_function(current_x.unsqueeze(0)) + options = opt_inputs.options or {} + for _ in range( + assert_is_instance(options.get("maxiter_discrete", MAX_ITER_DISCRETE), int) + ): + x_neighbors = get_nearest_neighbors( + current_x=current_x.detach(), + bounds=opt_inputs.bounds, + discrete_dims=discrete_dims, + ) + x_neighbors = _filter_infeasible( + X=x_neighbors, inequality_constraints=opt_inputs.inequality_constraints + ) + if x_neighbors.numel() == 0: + # Exit gracefully with last point if there are no feasible neighbors. + break + with torch.no_grad(): + acq_vals = torch.cat( + [ + opt_inputs.acq_function(X_.unsqueeze(-2)) + for X_ in x_neighbors.split( + options.get("init_batch_limit", MAX_BATCH_SIZE) + ) + ] + ) + argmax = acq_vals.argmax() + improvement = acq_vals[argmax] - current_acqval + if improvement > 0: + current_acqval, current_x = acq_vals[argmax], x_neighbors[argmax] + if improvement <= options.get("tol", CONVERGENCE_TOL): + break + return current_x, current_acqval
+ + + +
+[docs] +def continuous_step( + opt_inputs: OptimizeAcqfInputs, + discrete_dims: Tensor, + current_x: Tensor, +) -> tuple[Tensor, Tensor]: + """Continuous search using L-BFGS-B through optimize_acqf. + + Args: + opt_inputs: Common set of arguments for acquisition optimization. + This function utilizes `acq_function`, `bounds`, `options`, + `fixed_features` and constraints from `opt_inputs`. + discrete_dims: A tensor of indices corresponding to binary and + integer parameters. + current_x: Starting point. A tensor of shape `d`. + + Returns: + A tuple of two tensors: a (1 x d)-dim tensor of optimized points + and a (1)-dim tensor of acquisition values. + """ + bounds = opt_inputs.bounds + options = opt_inputs.options or {} + if (current_x < bounds[0]).any() or (current_x > bounds[1]).any(): + raise ValueError("continuous_step requires current_x to be within bounds.") + if len(discrete_dims) == len(current_x): # nothing continuous to optimize + with torch.no_grad(): + return current_x, opt_inputs.acq_function(current_x.unsqueeze(0)) + + updated_opt_inputs = dataclasses.replace( + opt_inputs, + q=1, + num_restarts=1, + batch_initial_conditions=current_x.unsqueeze(0), + fixed_features={ + **dict(zip(discrete_dims.tolist(), current_x[discrete_dims])), + **(opt_inputs.fixed_features or {}), + }, + options={ + "maxiter": options.get("maxiter_continuous", MAX_ITER_CONT), + "tol": options.get("tol", CONVERGENCE_TOL), + "batch_limit": options.get("batch_limit", MAX_BATCH_SIZE), + }, + ) + return _optimize_acqf(opt_inputs=updated_opt_inputs)
+ + + +
+[docs] +def optimize_acqf_mixed_alternating( + acq_function: AcquisitionFunction, + bounds: Tensor, + discrete_dims: list[int], + options: dict[str, Any] | None = None, + q: int = 1, + raw_samples: int = RAW_SAMPLES, + num_restarts: int = NUM_RESTARTS, + post_processing_func: Callable[[Tensor], Tensor] | None = None, + sequential: bool = True, + fixed_features: dict[int, float] | None = None, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, +) -> tuple[Tensor, Tensor]: + r""" + Optimizes acquisition function over mixed binary and continuous input spaces. + Multiple random restarting starting points are picked by evaluating a large set + of initial candidates. From each starting point, alternating discrete local search + and continuous optimization via (L-BFGS) is performed for a fixed number of + iterations. + + NOTE: This method assumes that all discrete variables are integer valued. + The discrete dimensions that have more than + `options.get("max_discrete_values", MAX_DISCRETE_VALUES)` values will + be optimized using continuous relaxation. + + # TODO: Support categorical variables. + + Args: + acq_function: BoTorch Acquisition function. + bounds: A `2 x d` tensor of lower and upper bounds for each column of `X`. + discrete_dims: A list of indices corresponding to integer and binary parameters. + options: Dictionary specifying optimization options. Supports the following: + - "initialization_strategy": Strategy used to generate the initial candidates. + "random", "continuous_relaxation" or "equally_spaced" (linspace style). + - "tol": The algorithm terminates if the absolute improvement in acquisition + value of one iteration is smaller than this number. + - "maxiter_alternating": Number of alternating steps. Defaults to 64. + - "maxiter_discrete": Maximum number of iterations in each discrete step. + Defaults to 4. + - "maxiter_continuous": Maximum number of iterations in each continuous step. + Defaults to 8. + - "max_discrete_values": Maximum number of values for a discrete dimension + to be optimized using discrete step / local search. The discrete dimensions + with more values will be optimized using continuous relaxation. + - "num_spray_points": Number of spray points (around `X_baseline`) to add to + the points generated by the initialization strategy. Defaults to 20 if + all discrete variables are binary and to 0 otherwise. + - "std_cont_perturbation": Standard deviation of the normal perturbations of + the continuous variables used to generate the spray points. + Defaults to 0.1. + - "batch_limit": The maximum batch size for jointly evaluating candidates + during optimization. + - "init_batch_limit": The maximum batch size for jointly evaluating candidates + during initialization. During initialization, candidates are evaluated + in a `no_grad` context, which reduces memory usage. As a result, + `init_batch_limit` can be set to a larger value than `batch_limit`. + Defaults to `batch_limit`, if given. + q: Number of candidates. + raw_samples: Number of initial candidates used to select starting points from. + Defaults to 1024. + num_restarts: Number of random restarts. Defaults to 20. + post_processing_func: A function that post-processes an optimization result + appropriately (i.e., according to `round-trip` transformations). + sequential: Whether to use joint or sequential optimization across q-batch. + This currently only supports sequential optimization. + fixed_features: A map `{feature_index: value}` for features that + should be fixed to a particular value during generation. + inequality_constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. `indices` and + `coefficients` should be torch tensors. See the docstring of + `make_scipy_linear_constraints` for an example. + + Returns: + A tuple of two tensors: a (q x d)-dim tensor of optimized points + and a (q)-dim tensor of their respective acquisition values. + """ + if sequential is False: # pragma: no cover + raise NotImplementedError( + "`optimize_acqf_mixed_alternating` only supports " + "sequential optimization." + ) + + fixed_features = fixed_features or {} + options = options or {} + options.setdefault("batch_limit", MAX_BATCH_SIZE) + options.setdefault("init_batch_limit", options["batch_limit"]) + if not (keys := set(options.keys())).issubset(SUPPORTED_OPTIONS): + unsupported_keys = keys.difference(SUPPORTED_OPTIONS) + raise UnsupportedError( + f"Received an unsupported option {unsupported_keys}. {SUPPORTED_OPTIONS=}." + ) + + # Update discrete dims and post processing functions to account for any + # dimensions that should be using continuous relaxation. + discrete_dims, post_processing_func = _setup_continuous_relaxation( + discrete_dims=discrete_dims, + bounds=bounds, + max_discrete_values=assert_is_instance( + options.get("max_discrete_values", MAX_DISCRETE_VALUES), int + ), + post_processing_func=post_processing_func, + ) + + opt_inputs = OptimizeAcqfInputs( + acq_function=acq_function, + bounds=bounds, + q=q, + num_restarts=num_restarts, + raw_samples=raw_samples, + options=options, + inequality_constraints=inequality_constraints, + equality_constraints=None, + nonlinear_inequality_constraints=None, + fixed_features=fixed_features, + post_processing_func=post_processing_func, + batch_initial_conditions=None, + return_best_only=True, + gen_candidates=gen_candidates_scipy, + sequential=sequential, + ) + _validate_sequential_inputs(opt_inputs=opt_inputs) + + base_X_pending = acq_function.X_pending if q > 1 else None + dim = bounds.shape[-1] + tkwargs: dict[str, Any] = {"device": bounds.device, "dtype": bounds.dtype} + # Remove fixed features from dims, so they don't get optimized. + discrete_dims = [dim for dim in discrete_dims if dim not in fixed_features] + if len(discrete_dims) == 0: + return _optimize_acqf(opt_inputs=opt_inputs) + if not ( + isinstance(discrete_dims, list) + and len(set(discrete_dims)) == len(discrete_dims) + and min(discrete_dims) >= 0 + and max(discrete_dims) <= dim - 1 + ): + raise ValueError( + "`discrete_dims` must be a list with unique integers " + "between 0 and num_dims - 1." + ) + discrete_dims_t = torch.tensor( + discrete_dims, dtype=torch.long, device=tkwargs["device"] + ) + cont_dims = complement_indices_like(indices=discrete_dims_t, d=dim) + # Fixed features are all in cont_dims. Remove them, so they don't get optimized. + ff_idcs = torch.tensor( + list(fixed_features.keys()), dtype=torch.long, device=tkwargs["device"] + ) + cont_dims = cont_dims[(cont_dims.unsqueeze(-1) != ff_idcs).all(dim=-1)] + candidates = torch.empty(0, dim, **tkwargs) + for _q in range(q): + # Generate starting points. + best_X, best_acq_val = generate_starting_points( + opt_inputs=opt_inputs, + discrete_dims=discrete_dims_t, + cont_dims=cont_dims, + ) + + # TODO: Eliminate this for loop. Tensors being unequal sizes could potentially + # be handled by concatenating them rather than stacking, and keeping a list + # of indices. + for i in range(num_restarts): + alternate_steps = 0 + while alternate_steps < options.get("maxiter_alternating", MAX_ITER_ALTER): + starting_acq_val = best_acq_val[i].clone() + alternate_steps += 1 + for step in (discrete_step, continuous_step): + best_X[i], best_acq_val[i] = step( + opt_inputs=opt_inputs, + discrete_dims=discrete_dims_t, + current_x=best_X[i], + ) + + improvement = best_acq_val[i] - starting_acq_val + if improvement < options.get("tol", CONVERGENCE_TOL): + # Check for convergence + break + + new_candidate = best_X[torch.argmax(best_acq_val)].unsqueeze(0) + candidates = torch.cat([candidates, new_candidate], dim=-2) + # Update pending points to include the new candidate. + if q > 1: + acq_function.set_X_pending( + torch.cat([base_X_pending, candidates], dim=-2) + if base_X_pending is not None + else candidates + ) + if q > 1: + acq_function.set_X_pending(base_X_pending) + + if post_processing_func is not None: + candidates = post_processing_func(candidates) + + with torch.no_grad(): + acq_value = acq_function(candidates) # compute joint acquisition value + return candidates, acq_value
+ + + +
+[docs] +def complement_indices_like(indices: Tensor, d: int) -> Tensor: + r"""Computes a tensor of complement indices: {range(d) \\ indices}. + Same as complement_indices but returns an integer tensor like indices. + """ + return torch.tensor( + complement_indices(indices.tolist(), d), + device=indices.device, + dtype=indices.dtype, + )
+ + + +
+[docs] +def complement_indices(indices: list[int], d: int) -> list[int]: + r"""Computes a list of complement indices: {range(d) \\ indices}. + + Args: + indices: a list of integers. + d: an integer dimension in which to compute the complement. + + Returns: + A list of integer indices. + """ + return sorted(set(range(d)).difference(indices))
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/parameter_constraints.html b/website-old/pages/api/_modules/botorch/optim/parameter_constraints.html new file mode 100644 index 0000000000..8949b3e3a7 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/parameter_constraints.html @@ -0,0 +1,680 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.parameter_constraints

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Utility functions for constrained optimization.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from functools import partial
+from typing import Union
+
+import numpy as np
+import numpy.typing as npt
+import torch
+from botorch.exceptions.errors import CandidateGenerationError, UnsupportedError
+from scipy.optimize import Bounds
+from torch import Tensor
+
+
+ScipyConstraintDict = dict[
+    str, Union[str, Callable[[np.ndarray], float], Callable[[np.ndarray], np.ndarray]]
+]
+NLC_TOL = -1e-6
+
+
+
+[docs] +def make_scipy_bounds( + X: Tensor, + lower_bounds: float | Tensor | None = None, + upper_bounds: float | Tensor | None = None, +) -> Bounds | None: + r"""Creates a scipy Bounds object for optimziation + + Args: + X: `... x d` tensor + lower_bounds: Lower bounds on each column (last dimension) of `X`. If + this is a single float, then all columns have the same bound. + upper_bounds: Lower bounds on each column (last dimension) of `X`. If + this is a single float, then all columns have the same bound. + + Returns: + A scipy `Bounds` object if either lower_bounds or upper_bounds is not + None, and None otherwise. + + Example: + >>> X = torch.rand(5, 2) + >>> scipy_bounds = make_scipy_bounds(X, 0.1, 0.8) + """ + if lower_bounds is None and upper_bounds is None: + return None + + def _expand(bounds: float | Tensor, X: Tensor, lower: bool) -> Tensor: + if bounds is None: + ebounds = torch.full_like(X, float("-inf" if lower else "inf")) + else: + if not torch.is_tensor(bounds): + bounds = torch.tensor(bounds) + ebounds = bounds.expand_as(X) + return _arrayify(ebounds).flatten() + + lb = _expand(bounds=lower_bounds, X=X, lower=True) + ub = _expand(bounds=upper_bounds, X=X, lower=False) + return Bounds(lb=lb, ub=ub, keep_feasible=True)
+ + + +
+[docs] +def make_scipy_linear_constraints( + shapeX: torch.Size, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, +) -> list[ScipyConstraintDict]: + r"""Generate scipy constraints from torch representation. + + Args: + shapeX: The shape of the torch.Tensor to optimize over (i.e. `(b) x q x d`) + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`, where + `indices` is a single-dimensional index tensor (long dtype) containing + indices into the last dimension of `X`, `coefficients` is a + single-dimensional tensor of coefficients of the same length, and + rhs is a scalar. + equality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) == rhs` (with `indices` + and `coefficients` of the same form as in `inequality_constraints`). + + Returns: + A list of dictionaries containing callables for constraint function + values and Jacobians and a string indicating the associated constraint + type ("eq", "ineq"), as expected by `scipy.minimize`. + + This function assumes that constraints are the same for each input batch, + and broadcasts the constraints accordingly to the input batch shape. This + function does support constraints across elements of a q-batch if the + indices are a 2-d Tensor. + + Example: + The following will enforce that `x[1] + 0.5 x[3] >= -0.1` for each `x` + in both elements of the q-batch, and each of the 3 t-batches: + + >>> constraints = make_scipy_linear_constraints( + >>> torch.Size([3, 2, 4]), + >>> [(torch.tensor([1, 3]), torch.tensor([1.0, 0.5]), -0.1)], + >>> ) + + The following will enforce that `x[0, 1] + 0.5 x[1, 3] >= -0.1` where + x[0, :] is the first element of the q-batch and x[1, :] is the second + element of the q-batch, for each of the 3 t-batches: + + >>> constraints = make_scipy_linear_constraints( + >>> torch.size([3, 2, 4]) + >>> [(torch.tensor([[0, 1], [1, 3]), torch.tensor([1.0, 0.5]), -0.1)], + >>> ) + """ + constraints = [] + if inequality_constraints is not None: + for indcs, coeffs, rhs in inequality_constraints: + constraints += _make_linear_constraints( + indices=indcs, coefficients=coeffs, rhs=rhs, shapeX=shapeX, eq=False + ) + if equality_constraints is not None: + for indcs, coeffs, rhs in equality_constraints: + constraints += _make_linear_constraints( + indices=indcs, coefficients=coeffs, rhs=rhs, shapeX=shapeX, eq=True + ) + return constraints
+ + + +
+[docs] +def eval_lin_constraint( + x: npt.NDArray, flat_idxr: list[int], coeffs: npt.NDArray, rhs: float +) -> np.float64: + r"""Evaluate a single linear constraint. + + Args: + x: The input array. + flat_idxr: The indices in `x` to consider. + coeffs: The coefficients corresponding to the indices. + rhs: The right-hand-side of the constraint. + + Returns: + The evaluted constraint: `\sum_i (coeffs[i] * x[i]) - rhs` + """ + return np.sum(x[flat_idxr] * coeffs, -1) - rhs
+ + + +
+[docs] +def lin_constraint_jac( + x: npt.NDArray, flat_idxr: list[int], coeffs: npt.NDArray, n: int +) -> npt.NDArray: + r"""Return the Jacobian associated with a linear constraint. + + Args: + x: The input array. + flat_idxr: The indices for the elements of x that appear in the constraint. + coeffs: The coefficients corresponding to the indices. + n: number of elements + + Returns: + The Jacobian. + """ + # TODO: Use sparse representation (not sure if scipy optim supports that) + jac = np.zeros(n) + jac[flat_idxr] = coeffs + return jac
+ + + +def _arrayify(X: Tensor) -> npt.NDArray: + r"""Convert a torch.Tensor (any dtype or device) to a numpy (double) array. + + Args: + X: The input tensor. + + Returns: + A numpy array of double dtype with the same shape and data as `X`. + """ + return X.cpu().detach().contiguous().double().clone().numpy() + + +def _validate_linear_constraints_shape_input(shapeX: torch.Size) -> torch.Size: + """ + Validate `shapeX` input to `_make_linear_constraints`. + + Check that it has either 2 or 3 dimensions, and add a scalar batch + dimension if it is only 2d. + """ + + if len(shapeX) not in (2, 3): + raise UnsupportedError( + f"`shapeX` must be `(b) x q x d` (at least two-dimensional). It is " + f"{shapeX}." + ) + if len(shapeX) == 2: + shapeX = torch.Size([1, *shapeX]) + return shapeX + + +def _validate_linear_constraints_indices_input(indices: Tensor, q: int, d: int) -> None: + if indices.dim() > 2: + raise UnsupportedError( + "Linear constraints supported only on individual candidates and " + "across q-batches, not across general batch shapes." + ) + elif indices.dim() == 2: + if indices[:, 0].max() > q - 1: + raise RuntimeError(f"Index out of bounds for {q}-batch") + if indices[:, 1].max() > d - 1: + raise RuntimeError(f"Index out of bounds for {d}-dim parameter tensor") + elif indices.dim() == 1: + if indices.max() > d - 1: + raise RuntimeError(f"Index out of bounds for {d}-dim parameter tensor") + else: + raise ValueError("`indices` must be at least one-dimensional") + + +def _make_linear_constraints( + indices: Tensor, + coefficients: Tensor, + rhs: float, + shapeX: torch.Size, + eq: bool = False, +) -> list[ScipyConstraintDict]: + r"""Create linear constraints to be used by `scipy.minimize`. + + Encodes constraints of the form + `\sum_i (coefficients[i] * X[..., indices[i]]) ? rhs` + where `?` can be designated either as `>=` by setting `eq=False`, or as + `=` by setting `eq=True`. + + If indices is one-dimensional, the constraints are broadcasted across + all elements of the q-batch. If indices is two-dimensional, then + constraints are applied across elements of a q-batch. In either case, + constraints are created for all t-batches. + + Args: + indices: A tensor of shape `c` or `c x 2`, where c is the number of terms + in the constraint. If single-dimensional, contains the indices of + the dimensions of the feature space that occur in the linear + constraint. If two-dimensional, contains pairs of indices of the + q-batch (0) and the feature space (1) that occur in the linear + constraint. + coefficients: A single-dimensional tensor of coefficients with the same + number of elements as `indices`. + rhs: The right hand side of the constraint. + shapeX: The shape of the torch tensor to construct the constraints for + (i.e. `(b) x q x d`). Must have two or three dimensions. + eq: If True, return an equality constraint, o/w return an inequality + constraint (indicated by "eq" / "ineq" value of the `type` key). + + Returns: + A list of constraint dictionaries with the following keys + + - "type": Indicates the type of the constraint ("eq" if `eq=True`, "ineq" o/w) + - "fun": A callable evaluating the constraint value on `x`, a flattened + version of the input tensor `X`, returning a scalar. + - "jac": A callable evaluating the constraint's Jacobian on `x`, a flattened + version of the input tensor `X`, returning a numpy array. + + >>> shapeX = torch.Size([3, 5, 4]) + >>> constraints = _make_linear_constraints( + ... indices=torch.tensor([1., 2.]), + ... coefficients=torch.tensor([-0.5, 1.3]), + ... rhs=0.49, + ... shapeX=shapeX, + ... eq=True + ... ) + >>> len(constraints) + 15 + >>> constraints[0].keys() + dict_keys(['type', 'fun', 'jac']) + >>> x = np.arange(60).reshape(shapeX) + >>> constraints[0]["fun"](x) + 1.61 # 1 * -0.5 + 2 * 1.3 - 0.49 + >>> constraints[0]["jac"](x) + [0., -0.5, 1.3, 0., 0., ...] + >>> constraints[1]["fun"](x) # + 4.81 + """ + + shapeX = _validate_linear_constraints_shape_input(shapeX) + + b, q, d = shapeX + _validate_linear_constraints_indices_input(indices, q, d) + n = shapeX.numel() + constraints: list[ScipyConstraintDict] = [] + coeffs = _arrayify(coefficients) + ctype = "eq" if eq else "ineq" + + offsets = [q * d, d] + if indices.dim() == 2: + # indices has two dimensions (potential constraints across q-batch elements) + # rule is [i, j, k] is at + # i * offsets[0] + j * offsets[1] + k + for i in range(b): + list_ind = (idx.tolist() for idx in indices) + idxr = [i * offsets[0] + idx[0] * offsets[1] + idx[1] for idx in list_ind] + fun = partial( + eval_lin_constraint, flat_idxr=idxr, coeffs=coeffs, rhs=float(rhs) + ) + jac = partial(lin_constraint_jac, flat_idxr=idxr, coeffs=coeffs, n=n) + constraints.append({"type": ctype, "fun": fun, "jac": jac}) + elif indices.dim() == 1: + # indices is one-dim - broadcast constraints across q-batches and t-batches + for i in range(b): + for j in range(q): + idxr = (i * offsets[0] + j * offsets[1] + indices).tolist() + fun = partial( + eval_lin_constraint, flat_idxr=idxr, coeffs=coeffs, rhs=float(rhs) + ) + jac = partial(lin_constraint_jac, flat_idxr=idxr, coeffs=coeffs, n=n) + constraints.append({"type": ctype, "fun": fun, "jac": jac}) + return constraints + + +def _make_nonlinear_constraints( + f_np_wrapper: Callable, nlc: Callable, is_intrapoint: bool, shapeX: torch.Size +) -> list[ScipyConstraintDict]: + """Create nonlinear constraints to be used by `scipy.minimize`. + + Args: + f_np_wrapper: A wrapper function that given a constraint evaluates + the value and gradient (using autograd) of a numpy input and returns both + the objective and the gradient. + nlc: Callable representing a constraint of the form `callable(x) >= 0`. In case + of an intra-point constraint, `callable()`takes in an one-dimensional tensor + of shape `d` and returns a scalar. In case of an inter-point constraint, + `callable()` takes a two dimensional tensor of shape `q x d` and again + returns a scalar. + is_intrapoint: A Boolean indicating if a constraint is an intra-point or + inter-point constraint (see the docstring of the `inequality_constraints` + argument to `optimize_acqf()`). + shapeX: Shape of the three-dimensional batch X, that should be optimized. + + Returns: + A list of constraint dictionaries with the following keys + + - "type": Indicates the type of the constraint, here always "ineq". + - "fun": A callable evaluating the constraint value on `x`, a flattened + version of the input tensor `X`, returning a scalar. + - "jac": A callable evaluating the constraint's Jacobian on `x`, a flattened + version of the input tensor `X`, returning a numpy array. + """ + shapeX = _validate_linear_constraints_shape_input(shapeX) + b, q, _ = shapeX + constraints = [] + + def get_intrapoint_constraint(b: int, q: int, nlc: Callable) -> Callable: + return lambda x: nlc(x[b, q]) + + def get_interpoint_constraint(b: int, nlc: Callable) -> Callable: + return lambda x: nlc(x[b]) + + if is_intrapoint: + for i in range(b): + for j in range(q): + f_obj, f_grad = _make_f_and_grad_nonlinear_inequality_constraints( + f_np_wrapper=f_np_wrapper, + nlc=get_intrapoint_constraint(b=i, q=j, nlc=nlc), + ) + constraints.append({"type": "ineq", "fun": f_obj, "jac": f_grad}) + else: + for i in range(b): + f_obj, f_grad = _make_f_and_grad_nonlinear_inequality_constraints( + f_np_wrapper=f_np_wrapper, + nlc=get_interpoint_constraint(b=i, nlc=nlc), + ) + constraints.append({"type": "ineq", "fun": f_obj, "jac": f_grad}) + + return constraints + + +def _generate_unfixed_nonlin_constraints( + constraints: list[tuple[Callable[[Tensor], Tensor], bool]] | None, + fixed_features: dict[int, float], + dimension: int, +) -> list[Callable[[Tensor], Tensor]] | None: + """Given a dictionary of fixed features, returns a list of callables for + nonlinear inequality constraints expecting only a tensor with the non-fixed + features as input. + """ + if not constraints: + return constraints + + selector = [] + idx_X, idx_f = 0, dimension - len(fixed_features) + for i in range(dimension): + if i in fixed_features.keys(): + selector.append(idx_f) + idx_f += 1 + else: + selector.append(idx_X) + idx_X += 1 + + values = torch.tensor(list(fixed_features.values()), dtype=torch.double) + + def _wrap_nonlin_constraint( + constraint: Callable[[Tensor], Tensor], + ) -> Callable[[Tensor], Tensor]: + def new_nonlin_constraint(X: Tensor) -> Tensor: + ivalues = values.to(X).expand(*X.shape[:-1], len(fixed_features)) + X_perm = torch.cat([X, ivalues], dim=-1) + return constraint(X_perm[..., selector]) + + return new_nonlin_constraint + + return [ + (_wrap_nonlin_constraint(constraint=nlc), is_intrapoint) + for nlc, is_intrapoint in constraints + ] + + +def _generate_unfixed_lin_constraints( + constraints: list[tuple[Tensor, Tensor, float]] | None, + fixed_features: dict[int, float], + dimension: int, + eq: bool, +) -> list[tuple[Tensor, Tensor, float]] | None: + # If constraints is None or an empty list, then return itself + if not constraints: + return constraints + + # replace_index generates the new indices for the unfixed dimensions + # after eliminating the fixed dimensions. + # Example: dimension = 5, ff.keys() = [1, 3], replace_index = {0: 0, 2: 1, 4: 2} + unfixed_keys = sorted(set(range(dimension)) - set(fixed_features)) + unfixed_keys = torch.tensor(unfixed_keys).to(constraints[0][0]) + replace_index = torch.arange(dimension - len(fixed_features)).to(constraints[0][0]) + + new_constraints = [] + # parse constraints one-by-one + for constraint_id, (indices, coefficients, rhs) in enumerate(constraints): + new_rhs = rhs + new_indices = [] + new_coefficients = [] + # the following unsqueeze is done to facilitate a simpler for-loop. + indices_2dim = indices if indices.ndim == 2 else indices.unsqueeze(-1) + for coefficient, index in zip(coefficients, indices_2dim): + ffval_or_None = fixed_features.get(index[-1].item()) + # if ffval_or_None is None, then the index is not fixed + if ffval_or_None is None: + new_indices.append(index) + new_coefficients.append(coefficient) + # otherwise, we "remove" the constraints corresponding to that index + else: + new_rhs = new_rhs - coefficient.item() * ffval_or_None + + # all indices were fixed, so the constraint is gone. + if len(new_indices) == 0: + if (eq and new_rhs != 0) or (not eq and new_rhs > 0): + prefix = "Eq" if eq else "Ineq" + raise CandidateGenerationError( + f"{prefix}uality constraint {constraint_id} not met " + "with fixed_features." + ) + else: + # However, one key transformation has to be noted. + # new_indices is with respect to the older (fuller) domain, and so it will + # have to be converted using replace_index. + new_indices = torch.stack(new_indices, dim=0) + # generate new index location after the removal of fixed_features indices + new_indices_dim_d = new_indices[:, -1].unsqueeze(-1) + new_indices_dim_d = replace_index[ + torch.nonzero(new_indices_dim_d == unfixed_keys, as_tuple=True)[1] + ] + new_indices[:, -1] = new_indices_dim_d + # squeeze(-1) is a no-op if dim -1 is not singleton + new_indices.squeeze_(-1) + # convert new_coefficients to Tensor + new_coefficients = torch.stack(new_coefficients) + new_constraints.append((new_indices, new_coefficients, new_rhs)) + return new_constraints + + +def _make_f_and_grad_nonlinear_inequality_constraints( + f_np_wrapper: Callable, nlc: Callable +) -> tuple[Callable[[Tensor], Tensor], Callable[[Tensor], Tensor]]: + """ + Create callables for objective + grad for the nonlinear inequality constraints. + The Scipy interface requires specifying separate callables and we use caching to + avoid evaluating the same input twice. This caching only works if + the returned functions are evaluated on the same input in immediate + sequence (i.e., calling `f_obj(X_1)`, `f_grad(X_1)` will result in a + single forward pass, while `f_obj(X_1)`, `f_grad(X_2)`, `f_obj(X_1)` + will result in three forward passes). + """ + + def f_obj_and_grad(x): + obj, grad = f_np_wrapper(x, f=nlc) + return obj, grad + + cache = {"X": None, "obj": None, "grad": None} + + def f_obj(X): + X_c = cache["X"] + if X_c is None or not np.array_equal(X_c, X): + cache["X"] = X.copy() + cache["obj"], cache["grad"] = f_obj_and_grad(X) + return cache["obj"] + + def f_grad(X): + X_c = cache["X"] + if X_c is None or not np.array_equal(X_c, X): + cache["X"] = X.copy() + cache["obj"], cache["grad"] = f_obj_and_grad(X) + return cache["grad"] + + return f_obj, f_grad + + +
+[docs] +def nonlinear_constraint_is_feasible( + nonlinear_inequality_constraint: Callable, is_intrapoint: bool, x: Tensor +) -> bool: + """Checks if a nonlinear inequality constraint is fulfilled. + + Args: + nonlinear_inequality_constraint: Callable to evaluate the + constraint. + intra: If True, the constraint is an intra-point constraint that + is applied pointwise and is broadcasted over the q-batch. Else, the + constraint has to evaluated over the whole q-batch and is a an + inter-point constraint. + x: Tensor of shape (b x q x d). + + Returns: + bool: True if the constraint is fulfilled, else False. + """ + + def check_x(x: Tensor) -> bool: + return _arrayify(nonlinear_inequality_constraint(x)).item() >= NLC_TOL + + for x_ in x: + if is_intrapoint: + if not all(check_x(x__) for x__ in x_): + return False + else: + if not check_x(x_): + return False + return True
+ + + +
+[docs] +def make_scipy_nonlinear_inequality_constraints( + nonlinear_inequality_constraints: list[tuple[Callable, bool]], + f_np_wrapper: Callable, + x0: Tensor, + shapeX: torch.Size, +) -> list[dict]: + r"""Generate Scipy nonlinear inequality constraints from callables. + + Args: + nonlinear_inequality_constraints: A list of tuples representing the nonlinear + inequality constraints. The first element in the tuple is a callable + representing a constraint of the form `callable(x) >= 0`. In case of an + intra-point constraint, `callable()`takes in an one-dimensional tensor of + shape `d` and returns a scalar. In case of an inter-point constraint, + `callable()` takes a two dimensional tensor of shape `q x d` and again + returns a scalar. The second element is a boolean, indicating if it is an + intra-point or inter-point constraint (`True` for intra-point. `False` for + inter-point). For more information on intra-point vs inter-point + constraints, see the docstring of the `inequality_constraints` argument to + `optimize_acqf()`. The constraints will later be passed to the scipy + solver. + f_np_wrapper: A wrapper function that given a constraint evaluates the value + and gradient (using autograd) of a numpy input and returns both the + objective and the gradient. + x0: The starting point for SLSQP. We return this starting point in (rare) + cases where SLSQP fails and thus require it to be feasible. + shapeX: Shape of the three-dimensional batch X, that should be optimized. + + Returns: + A list of dictionaries containing callables for constraint function + values and Jacobians and a string indicating the associated constraint + type ("eq", "ineq"), as expected by `scipy.minimize`. + """ + + scipy_nonlinear_inequality_constraints = [] + for constraint in nonlinear_inequality_constraints: + if not isinstance(constraint, tuple): + raise ValueError( + f"A nonlinear constraint has to be a tuple, got {type(constraint)}." + ) + if len(constraint) != 2: + raise ValueError( + "A nonlinear constraint has to be a tuple of length 2, " + f"got length {len(constraint)}." + ) + nlc, is_intrapoint = constraint + if not nonlinear_constraint_is_feasible( + nlc, is_intrapoint=is_intrapoint, x=x0.reshape(shapeX) + ): + raise ValueError( + "`batch_initial_conditions` must satisfy the non-linear inequality " + "constraints." + ) + + scipy_nonlinear_inequality_constraints += _make_nonlinear_constraints( + f_np_wrapper=f_np_wrapper, + nlc=nlc, + is_intrapoint=is_intrapoint, + shapeX=shapeX, + ) + return scipy_nonlinear_inequality_constraints
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/stopping.html b/website-old/pages/api/_modules/botorch/optim/stopping.html new file mode 100644 index 0000000000..fefe500f32 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/stopping.html @@ -0,0 +1,197 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.stopping

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from torch import Tensor
+
+
+
+[docs] +class StoppingCriterion(ABC): + r"""Base class for evaluating optimization convergence. + + Stopping criteria are implemented as a objects rather than a function, so that they + can keep track of past function values between optimization steps. + """ + +
+[docs] + @abstractmethod + def evaluate(self, fvals: Tensor) -> bool: + r"""Evaluate the stopping criterion. + + Args: + fvals: tensor containing function values for the current iteration. If + `fvals` contains more than one element, then the stopping criterion is + evaluated element-wise and True is returned if the stopping criterion is + true for all elements. + + Returns: + Stopping indicator (if True, stop the optimziation). + """ + pass # pragma: no cover
+ + + def __call__(self, fvals: Tensor) -> bool: + return self.evaluate(fvals)
+ + + +
+[docs] +class ExpMAStoppingCriterion(StoppingCriterion): + r"""Exponential moving average stopping criterion. + + Computes an exponentially weighted moving average over window length `n_window` + and checks whether the relative decrease in this moving average between steps + is less than a provided tolerance level. That is, in iteration `i`, it computes + + v[i,j] := fvals[i - n_window + j] * w[j] + + for all `j = 0, ..., n_window`, where `w[j] = exp(-eta * (1 - j / n_window))`. + Letting `ma[i] := sum_j(v[i,j])`, the criterion evaluates to `True` whenever + + (ma[i-1] - ma[i]) / abs(ma[i-1]) < rel_tol (if minimize=True) + (ma[i] - ma[i-1]) / abs(ma[i-1]) < rel_tol (if minimize=False) + """ + + def __init__( + self, + maxiter: int = 10000, + minimize: bool = True, + n_window: int = 10, + eta: float = 1.0, + rel_tol: float = 1e-5, + ) -> None: + r"""Exponential moving average stopping criterion. + + Args: + maxiter: Maximum number of iterations. + minimize: If True, assume minimization. + n_window: The size of the exponential moving average window. + eta: The exponential decay factor in the weights. + rel_tol: Relative tolerance for termination. + """ + self.maxiter = maxiter + self.minimize = minimize + self.n_window = n_window + self.rel_tol = rel_tol + self.iter = 0 + weights = torch.exp(torch.linspace(-eta, 0, self.n_window)) + self.weights = weights / weights.sum() + self._prev_fvals = None + +
+[docs] + def evaluate(self, fvals: Tensor) -> bool: + r"""Evaluate the stopping criterion. + + Args: + fvals: tensor containing function values for the current iteration. If + `fvals` contains more than one element, then the stopping criterion is + evaluated element-wise and True is returned if the stopping criterion is + true for all elements. + + TODO: add support for utilizing gradient information + + Returns: + Stopping indicator (if True, stop the optimziation). + """ + self.iter += 1 + if self.iter == self.maxiter: + return True + + if self._prev_fvals is None: + self._prev_fvals = fvals.unsqueeze(0) + else: + self._prev_fvals = torch.cat( + [self._prev_fvals[-self.n_window :], fvals.unsqueeze(0)] + ) + + if self._prev_fvals.size(0) < self.n_window + 1: + return False + + weights = self.weights + weights = weights.to(fvals) + if self._prev_fvals.ndim > 1: + weights = weights.unsqueeze(-1) + + # TODO: Update the exp moving average efficiently + prev_ma = (self._prev_fvals[:-1] * weights).sum(dim=0) + ma = (self._prev_fvals[1:] * weights).sum(dim=0) + # TODO: Handle approx. zero losses (normalize by min/max loss range) + rel_delta = (prev_ma - ma) / prev_ma.abs() + + if not self.minimize: + rel_delta = -rel_delta + if torch.max(rel_delta) < self.rel_tol: + return True + + return False
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/utils/acquisition_utils.html b/website-old/pages/api/_modules/botorch/optim/utils/acquisition_utils.html new file mode 100644 index 0000000000..4f602a9369 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/utils/acquisition_utils.html @@ -0,0 +1,207 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.utils.acquisition_utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Utilities for maximizing acquisition functions."""
+
+from __future__ import annotations
+
+from typing import Mapping
+from warnings import warn
+
+import torch
+from botorch.acquisition.acquisition import AcquisitionFunction
+from botorch.exceptions.errors import BotorchError
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models.gpytorch import ModelListGPyTorchModel
+from torch import Tensor
+
+
+
+[docs] +def columnwise_clamp( + X: Tensor, + lower: float | Tensor | None = None, + upper: float | Tensor | None = None, + raise_on_violation: bool = False, +) -> Tensor: + r"""Clamp values of a Tensor in column-wise fashion (with support for t-batches). + + This function is useful in conjunction with optimizers from the torch.optim + package, which don't natively handle constraints. If you apply this after + a gradient step you can be fancy and call it "projected gradient descent". + This funtion is also useful for post-processing candidates generated by the + scipy optimizer that satisfy bounds only up to numerical accuracy. + + Args: + X: The `b x n x d` input tensor. If 2-dimensional, `b` is assumed to be 1. + lower: The column-wise lower bounds. If scalar, apply bound to all columns. + upper: The column-wise upper bounds. If scalar, apply bound to all columns. + raise_on_violation: If `True`, raise an exception when the elments in `X` + are out of the specified bounds (up to numerical accuracy). This is + useful for post-processing candidates generated by optimizers that + satisfy imposed bounds only up to numerical accuracy. + + Returns: + The clamped tensor. + """ + if lower is None and upper is None: + return X + + if lower is not None: + lower = torch.as_tensor(lower).expand_as(X).to(X) + + if upper is not None: + upper = torch.as_tensor(upper).expand_as(X).to(X) + if lower is not None and (lower > upper).any(): + raise ValueError("Lower bounds cannot exceed upper bounds.") + + out = X.clamp(lower, upper) + if raise_on_violation and not X.allclose(out): + raise BotorchError("Original value(s) are out of bounds.") + + return out
+ + + +
+[docs] +def fix_features( + X: Tensor, fixed_features: Mapping[int, float | None] | None = None +) -> Tensor: + r"""Fix feature values in a Tensor. + + The fixed features will have zero gradient in downstream calculations. + + Args: + X: input Tensor with shape `... x p`, where `p` is the number of features + fixed_features: A mapping with keys as column indices and values + equal to what the feature should be set to in `X`. If the value is + None, that column is just considered fixed. Keys should be in the + range `[0, p - 1]`. + + Returns: + The tensor X with fixed features. + """ + if fixed_features is None: + return X + + columns = list(X.unbind(dim=-1)) + for index, value in fixed_features.items(): + if value is None: + columns[index] = columns[index].detach() + else: + columns[index] = torch.full_like(columns[index], value) + + return torch.stack(columns, dim=-1)
+ + + +
+[docs] +def get_X_baseline(acq_function: AcquisitionFunction) -> Tensor | None: + r"""Extract X_baseline from an acquisition function. + + This tries to find the baseline set of points. First, this checks if the + acquisition function has an `X_baseline` attribute. If it does not, + then this method attempts to use the model's `train_inputs` as `X_baseline`. + + Args: + acq_function: The acquisition function. + + Returns + An optional `n x d`-dim tensor of baseline points. This is None if no + baseline points are found. + """ + try: + X = acq_function.X_baseline + # if there are no baseline points, use training points + if X.shape[0] == 0: + raise BotorchError + except (BotorchError, AttributeError): + try: + # for entropy MOO methods + model = acq_function.mo_model + except AttributeError: + try: + # some acquisition functions do not have a model attribute + # e.g. FixedFeatureAcquisitionFunction + model = acq_function.model + except AttributeError: + warn("Failed to extract X_baseline.", BotorchWarning) + return + try: + # Make sure we get the original train inputs. + m = model.models[0] if isinstance(model, ModelListGPyTorchModel) else model + if m._has_transformed_inputs: + X = m._original_train_inputs + else: + X = m.train_inputs[0] + except (BotorchError, AttributeError): + warn("Failed to extract X_baseline.", BotorchWarning) + return + # just use one batch + while X.ndim > 2: + X = X[0] + return X
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/utils/model_utils.html b/website-old/pages/api/_modules/botorch/optim/utils/model_utils.html new file mode 100644 index 0000000000..dd273340fd --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/utils/model_utils.html @@ -0,0 +1,269 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.utils.model_utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Utilities for fitting and manipulating models."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterator
+
+from re import Pattern
+from typing import Any, NamedTuple
+from warnings import warn
+
+import torch
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models.gpytorch import GPyTorchModel
+from torch import Tensor
+from torch.nn import Module
+from torch.utils.data import DataLoader, TensorDataset
+
+
+
+[docs] +class TorchAttr(NamedTuple): + shape: torch.Size + dtype: torch.dtype + device: torch.device
+ + + +
+[docs] +def get_data_loader( + model: GPyTorchModel, batch_size: int = 1024, **kwargs: Any +) -> DataLoader: + dataset = TensorDataset(*model.train_inputs, model.train_targets) + return DataLoader( + dataset=dataset, batch_size=min(batch_size, len(model.train_targets)), **kwargs + )
+ + + +
+[docs] +def get_parameters( + module: Module, + requires_grad: bool | None = None, + name_filter: Callable[[str], bool] | None = None, +) -> dict[str, Tensor]: + r"""Helper method for obtaining a module's parameters and their respective ranges. + + Args: + module: The target module from which parameters are to be extracted. + requires_grad: Optional Boolean used to filter parameters based on whether + or not their require_grad attribute matches the user provided value. + name_filter: Optional Boolean function used to filter parameters by name. + + Returns: + A dictionary of parameters. + """ + parameters = {} + for name, param in module.named_parameters(): + if requires_grad is not None and param.requires_grad != requires_grad: + continue + + if name_filter and not name_filter(name): + continue + + parameters[name] = param + + return parameters
+ + + +
+[docs] +def get_parameters_and_bounds( + module: Module, + requires_grad: bool | None = None, + name_filter: Callable[[str], bool] | None = None, + default_bounds: tuple[float, float] = (-float("inf"), float("inf")), +) -> tuple[dict[str, Tensor], dict[str, tuple[float | None, float | None]]]: + r"""Helper method for obtaining a module's parameters and their respective ranges. + + Args: + module: The target module from which parameters are to be extracted. + name_filter: Optional Boolean function used to filter parameters by name. + requires_grad: Optional Boolean used to filter parameters based on whether + or not their require_grad attribute matches the user provided value. + default_bounds: Default lower and upper bounds for constrained parameters + with `None` typed bounds. + + Returns: + A dictionary of parameters and a dictionary of parameter bounds. + """ + if hasattr(module, "named_parameters_and_constraints"): + bounds = {} + params = {} + for name, param, constraint in module.named_parameters_and_constraints(): + if (requires_grad is None or (param.requires_grad == requires_grad)) and ( + name_filter is None or name_filter(name) + ): + params[name] = param + if constraint is None: + continue + + bounds[name] = tuple( + default if bound is None else constraint.inverse_transform(bound) + for (bound, default) in zip(constraint, default_bounds) + ) + + return params, bounds + + params = get_parameters( + module, requires_grad=requires_grad, name_filter=name_filter + ) + return params, {}
+ + + +
+[docs] +def get_name_filter( + patterns: Iterator[Pattern | str], +) -> Callable[[str | tuple[str, Any, ...]], bool]: + r"""Returns a binary function that filters strings (or iterables whose first + element is a string) according to a bank of excluded patterns. Typically, used + in conjunction with generators such as `module.named_parameters()`. + + Args: + patterns: A collection of regular expressions or strings that + define the set of names to be excluded. + + Returns: + A binary function indicating whether or not an item should be filtered. + """ + names = set() + _patterns = set() + for pattern in patterns: + if isinstance(pattern, str): + names.add(pattern) + elif isinstance(pattern, Pattern): + _patterns.add(pattern) + else: + raise TypeError( + "Expected `patterns` to contain `str` or `re.Pattern` typed elements, " + f"but found {type(pattern)}." + ) + + def name_filter(item: str | tuple[str, Any, ...]) -> bool: + name = item if isinstance(item, str) else next(iter(item)) + if name in names: + return False + + for pattern in _patterns: + if pattern.search(name): + return False + + return True + + return name_filter
+ + + +
+[docs] +def sample_all_priors(model: GPyTorchModel, max_retries: int = 100) -> None: + r"""Sample from hyperparameter priors (in-place). + + Args: + model: A GPyTorchModel. + """ + for _, module, prior, closure, setting_closure in model.named_priors(): + if setting_closure is None: + raise RuntimeError( + "Must provide inverse transform to be able to sample from prior." + ) + for i in range(max_retries): + try: + # Set sample shape, so that the prior samples have the same shape + # as `closure(module)` without having to be repeated. + prior_shape = prior._extended_shape() + if prior_shape.numel() == 1: + # For a univariate prior we can sample the size of the closure. + # Otherwise we will sample exactly the same value for all + # lengthscales where we commonly specify a univariate prior. + setting_closure(module, prior.sample(closure(module).shape)) + else: + closure_shape = closure(module).shape + sample_shape = closure_shape[: -len(prior_shape)] + setting_closure(module, prior.sample(sample_shape=sample_shape)) + break + except NotImplementedError: + warn( + f"`rsample` not implemented for {type(prior)}. Skipping.", + BotorchWarning, + ) + break + except RuntimeError as e: + if "out of bounds of its current constraints" in str(e): + if i == max_retries - 1: + raise RuntimeError( + "Failed to sample a feasible parameter value " + f"from the prior after {max_retries} attempts." + ) + else: + raise e
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/utils/numpy_utils.html b/website-old/pages/api/_modules/botorch/optim/utils/numpy_utils.html new file mode 100644 index 0000000000..fe372d89fe --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/utils/numpy_utils.html @@ -0,0 +1,242 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.utils.numpy_utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Utilities for interfacing Numpy and Torch."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterator
+from itertools import tee
+
+import numpy as np
+import numpy.typing as npt
+import torch
+from torch import Tensor
+
+
+torch_to_numpy_dtype_dict = {
+    torch.bool: bool,
+    torch.uint8: np.uint8,
+    torch.int8: np.int8,
+    torch.int16: np.int16,
+    torch.int32: np.int32,
+    torch.int64: np.int64,
+    torch.float16: np.float16,
+    torch.float32: np.float32,
+    torch.float64: np.float64,
+    torch.complex64: np.complex64,
+    torch.complex128: np.complex128,
+}
+
+
+
+[docs] +def as_ndarray( + values: Tensor, dtype: np.dtype | None = None, inplace: bool = True +) -> npt.NDArray: + r"""Helper for going from torch.Tensor to numpy.ndarray. + + Args: + values: Tensor to be converted to ndarray. + dtype: Optional numpy.dtype for the converted tensor. + inplace: Boolean indicating whether memory should be shared if possible. + + Returns: + An ndarray with the same data as `values`. + """ + with torch.no_grad(): + out = values.cpu() # maybe transfer to cpu + + # Determine whether or not to `clone` + if ( + # cond 1: are we not in `inplace` mode? + not inplace + # cond 2: did we already copy when calling `cpu` above? + and out.device == values.device + # cond 3: will we copy when calling `astype` below? + and (dtype is None or out.dtype == torch_to_numpy_dtype_dict[dtype]) + ): + out = out.clone() + + # Convert to ndarray and maybe cast to `dtype` + out = out.numpy() + return out.astype(dtype, copy=False)
+ + + +
+[docs] +def get_tensors_as_ndarray_1d( + tensors: Iterator[Tensor] | dict[str, Tensor], + out: npt.NDArray | None = None, + dtype: np.dtype | str | None = None, + as_array: Callable[[Tensor], npt.NDArray] = as_ndarray, +) -> npt.NDArray: + # Create a pair of iterators, one for setup and one for data transfer + named_tensors_iter, named_tensors_iter2 = tee( + iter(tensors.items()) if isinstance(tensors, dict) else enumerate(tensors), 2 + ) + + # Use `named_tensors_iter` to get size of `out` and `dtype` when None + try: + name, tnsr = next(named_tensors_iter) + except StopIteration: + raise RuntimeError(f"Argument `tensors` with type {type(tensors)} is empty.") + size = tnsr.numel() + sum(tnsr.numel() for _, tnsr in named_tensors_iter) + dtype = torch_to_numpy_dtype_dict[tnsr.dtype] if dtype is None else dtype + + # Preallocate or validate `out` + if out is None: # use first tensor as a reference when `dtype` is None + out = np.empty([size], dtype=dtype) + elif out.ndim != 1: + raise ValueError(f"Expected a vector for `out`, but out.shape={out.shape}.") + elif out.size != size: + raise ValueError( + f"Size of `parameters` ({size}) does not match size of `out` ({out.size})." + ) + + # Use `named_tensors_iter2` to transfer data from `tensors` to `out` + index = 0 + for name, tnsr in named_tensors_iter2: + try: + size = tnsr.numel() + out[index : index + size] = as_array(tnsr.view(-1)) + index += size + except Exception as e: + raise RuntimeError( + "`get_tensors_as_ndarray_1d` failed while copying values from " + f"tensor {name}; rethrowing original exception." + ) from e + + return out
+ + + +
+[docs] +def set_tensors_from_ndarray_1d( + tensors: Iterator[Tensor] | dict[str, Tensor], + array: npt.NDArray, +) -> None: + r"""Sets the values of one more tensors based off of a vector of assignments.""" + named_tensors_iter = ( + iter(tensors.items()) if isinstance(tensors, dict) else enumerate(tensors) + ) + with torch.no_grad(): + index = 0 + for name, tnsr in named_tensors_iter: + try: + size = tnsr.numel() + vals = array[index : index + size] if tnsr.ndim else array[index] + tnsr.copy_( + torch.as_tensor(vals, device=tnsr.device, dtype=tnsr.dtype).view( + tnsr.shape + ) + ) + index += size + except Exception as e: + raise RuntimeError( + "`set_tensors_from_ndarray_1d` failed while copying values to " + f"tensor {name}; rethrowing original exception." + ) from e
+ + + +
+[docs] +def get_bounds_as_ndarray( + parameters: dict[str, Tensor], + bounds: dict[str, tuple[float | Tensor | None, float | Tensor | None]], +) -> npt.NDArray | None: + r"""Helper method for converting bounds into an ndarray. + + Args: + parameters: A dictionary of parameters. + bounds: A dictionary of (optional) lower and upper bounds. + + Returns: + An ndarray of bounds. + """ + inf = float("inf") + full_size = sum(param.numel() for param in parameters.values()) + out = np.full((full_size, 2), (-inf, inf)) + index = 0 + for name, param in parameters.items(): + size = param.numel() + if name in bounds: + lower, upper = bounds[name] + lower = -inf if lower is None else lower + upper = inf if upper is None else upper + if isinstance(lower, Tensor): + lower = lower.cpu() + if isinstance(upper, Tensor): + upper = upper.cpu() + out[index : index + size, 0] = lower + out[index : index + size, 1] = upper + index = index + size + # If all bounds are +/- inf, return None. + if np.isinf(out).all(): + out = None + return out
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/optim/utils/timeout.html b/website-old/pages/api/_modules/botorch/optim/utils/timeout.html new file mode 100644 index 0000000000..a0ce6bf61f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/optim/utils/timeout.html @@ -0,0 +1,168 @@ + + + + + + + +
+
+
+
+

Source code for botorch.optim.utils.timeout

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import time
+import warnings
+from collections.abc import Callable, Sequence
+from typing import Any
+
+import numpy.typing as npt
+from botorch.exceptions.errors import OptimizationTimeoutError
+from scipy import optimize
+
+
+
+[docs] +def minimize_with_timeout( + fun: Callable[[npt.NDArray, ...], float], + x0: npt.NDArray, + args: tuple[Any, ...] = (), + method: str | None = None, + jac: str | Callable | bool | None = None, + hess: str | Callable | optimize.HessianUpdateStrategy | None = None, + hessp: Callable | None = None, + bounds: Sequence[tuple[float, float]] | optimize.Bounds | None = None, + constraints=(), # Typing this properly is a s**t job + tol: float | None = None, + callback: Callable | None = None, + options: dict[str, Any] | None = None, + timeout_sec: float | None = None, +) -> optimize.OptimizeResult: + r"""Wrapper around scipy.optimize.minimize to support timeout. + + This method calls scipy.optimize.minimize with all arguments forwarded + verbatim. The only difference is that if provided a `timeout_sec` argument, + it will automatically stop the optimziation after the timeout is reached. + + Internally, this is achieved by automatically constructing a wrapper callback + method that is injected to the scipy.optimize.minimize call and that keeps + track of the runtime and the optimization variables at the current iteration. + """ + if timeout_sec is not None: + start_time = time.monotonic() + callback_data = {"num_iterations": 0} # update from withing callback below + + def timeout_callback(xk: npt.NDArray) -> bool: + runtime = time.monotonic() - start_time + callback_data["num_iterations"] += 1 + if runtime > timeout_sec: + raise OptimizationTimeoutError(current_x=xk, runtime=runtime) + return False + + if callback is None: + wrapped_callback = timeout_callback + + elif callable(method): + raise NotImplementedError( + "Custom callable not supported for `method` argument." + ) + + elif method == "trust-constr": # special signature + + def wrapped_callback( + xk: npt.NDArray, state: optimize.OptimizeResult + ) -> bool: + # order here is important to make sure base callback gets executed + return callback(xk, state) or timeout_callback(xk=xk) + + else: + + def wrapped_callback(xk: npt.NDArray) -> None: + timeout_callback(xk=xk) + callback(xk) + + else: + wrapped_callback = callback + + try: + warnings.filterwarnings("error", message="Method .* cannot handle") + return optimize.minimize( + fun=fun, + x0=x0, + args=args, + method=method, + jac=jac, + hess=hess, + hessp=hessp, + bounds=bounds, + constraints=constraints, + tol=tol, + callback=wrapped_callback, + options=options, + ) + except OptimizationTimeoutError as e: + msg = f"Optimization timed out after {e.runtime} seconds." + current_fun, *_ = fun(e.current_x, *args) + + return optimize.OptimizeResult( + fun=current_fun, + x=e.current_x, + nit=callback_data["num_iterations"], + success=False, # same as when maxiter is reached + status=1, # same as when L-BFGS-B reaches maxiter + message=msg, + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/ensemble.html b/website-old/pages/api/_modules/botorch/posteriors/ensemble.html new file mode 100644 index 0000000000..7c85d441ca --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/ensemble.html @@ -0,0 +1,207 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.ensemble

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Ensemble posteriors. Used in conjunction with ensemble models.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.posteriors.posterior import Posterior
+from torch import Tensor
+
+
+
+[docs] +class EnsemblePosterior(Posterior): + r"""Ensemble posterior, that should be used for ensemble models that compute + eagerly a finite number of samples per X value as for example a deep ensemble + or a random forest.""" + + def __init__(self, values: Tensor) -> None: + r""" + Args: + values: Values of the samples produced by this posterior as + a `(b) x s x q x m` tensor where `m` is the output size of the + model and `s` is the ensemble size. + """ + if values.ndim < 3: + raise ValueError("Values has to be at least three-dimensional.") + self.values = values + + @property + def ensemble_size(self) -> int: + r"""The size of the ensemble""" + return self.values.shape[-3] + + @property + def weights(self) -> Tensor: + r"""The weights of the individual models in the ensemble. + Equally weighted by default.""" + return torch.ones(self.ensemble_size) / self.ensemble_size + + @property + def device(self) -> torch.device: + r"""The torch device of the posterior.""" + return self.values.device + + @property + def dtype(self) -> torch.dtype: + r"""The torch dtype of the posterior.""" + return self.values.dtype + + @property + def mean(self) -> Tensor: + r"""The mean of the posterior as a `(b) x n x m`-dim Tensor.""" + return self.values.mean(dim=-3) + + @property + def variance(self) -> Tensor: + r"""The variance of the posterior as a `(b) x n x m`-dim Tensor. + + Computed as the sample variance across the ensemble outputs. + """ + if self.ensemble_size == 1: + return torch.zeros_like(self.values.squeeze(-3)) + return self.values.var(dim=-3) + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + r"""Returns the shape of the samples produced by the posterior with + the given `sample_shape`. + """ + return sample_shape + self.values.shape[:-3] + self.values.shape[-2:] + +
+[docs] + def rsample( + self, + sample_shape: torch.Size | None = None, + ) -> Tensor: + r"""Sample from the posterior (with gradients). + + Based on the sample shape, base samples are generated and passed to + `rsample_from_base_samples`. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if sample_shape is None: + sample_shape = torch.Size([1]) + # get indices as base_samples + base_samples = ( + torch.multinomial( + self.weights, + num_samples=sample_shape.numel(), + replacement=True, + ) + .reshape(sample_shape) + .to(device=self.device) + ) + return self.rsample_from_base_samples( + sample_shape=sample_shape, base_samples=base_samples + )
+ + +
+[docs] + def rsample_from_base_samples( + self, sample_shape: torch.Size, base_samples: Tensor + ) -> Tensor: + r"""Sample from the posterior (with gradients) using base samples. + + This is intended to be used with a sampler that produces the corresponding base + samples, and enables acquisition optimization via Sample Average Approximation. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + base_samples: A Tensor of indices as base samples of shape + `sample_shape`, typically obtained from `IndexSampler`. + This is used for deterministic optimization. The predictions of + the ensemble corresponding to the indices are then sampled. + + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if base_samples.shape != sample_shape: + raise ValueError("Base samples do not match sample shape.") + # move sample axis to front + values = self.values.movedim(-3, 0) + # sample from the first dimension of values + return values[base_samples, ...]
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/fully_bayesian.html b/website-old/pages/api/_modules/botorch/posteriors/fully_bayesian.html new file mode 100644 index 0000000000..fe9899cd39 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/fully_bayesian.html @@ -0,0 +1,242 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.fully_bayesian

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from warnings import warn
+
+import torch
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from gpytorch.distributions import MultivariateNormal
+from torch import Tensor
+
+
+MCMC_DIM = -3  # Location of the MCMC batch dimension
+TOL = 1e-6  # Bisection tolerance
+
+
+
+[docs] +def batched_bisect( + f: Callable, target: float, bounds: Tensor, tol: float = TOL, max_steps: int = 32 +): + r"""Batched bisection with a fixed number of steps. + + Args: + f: Target function that takes a `(b1 x ... x bk)`-dim tensor and returns a + `(b1 x ... x bk)`-dim tensor. + target: Scalar target value of type float. + bounds: Lower and upper bounds, of size `2 x b1 x ... x bk`. + tol: We termniate if all elements satisfy are within `tol` of the `target`. + max_steps: Maximum number of bisection steps. + + Returns: + Tensor X of size `b1 x ... x bk` such that `f(X) = target`. + """ + # Make sure target is actually contained in the interval + f1, f2 = f(bounds[0]), f(bounds[1]) + if not ((f1 <= target) & (target <= f2)).all(): + raise ValueError( + "The target is not contained in the interval specified by the bounds" + ) + bounds = bounds.clone() # Will be modified in-place + center = bounds.mean(dim=0) + f_center = f(center) + for _ in range(max_steps): + go_left = f_center > target + bounds[1, go_left] = center[go_left] + bounds[0, ~go_left] = center[~go_left] + center = bounds.mean(dim=0) + f_center = f(center) + # Check convergence + if (f_center - target).abs().max() <= tol: + return center + return center
+ + + +def _quantile(posterior: GaussianMixturePosterior, value: Tensor) -> Tensor: + r"""Compute the posterior quantiles for the mixture of models.""" + if value.numel() > 1: + return torch.stack( + [_quantile(posterior=posterior, value=v) for v in value], dim=0 + ) + if value <= 0 or value >= 1: + raise ValueError("value is expected to be in the range (0, 1).") + dist = torch.distributions.Normal( + loc=posterior.mean, scale=posterior.variance.sqrt() + ) + if posterior.mean.shape[MCMC_DIM] == 1: # Analytical solution + return dist.icdf(value).squeeze(MCMC_DIM) + icdf_val = dist.icdf(value) + low = icdf_val.min(dim=MCMC_DIM).values - TOL + high = icdf_val.max(dim=MCMC_DIM).values + TOL + bounds = torch.cat((low.unsqueeze(0), high.unsqueeze(0)), dim=0) + return batched_bisect( + f=lambda x: dist.cdf(x.unsqueeze(MCMC_DIM)).mean(dim=MCMC_DIM), + target=value.item(), + bounds=bounds, + ) + + +
+[docs] +class GaussianMixturePosterior(GPyTorchPosterior): + r"""A Gaussian mixture posterior. + + The MCMC batch dimension that corresponds to the models in the mixture is located + at `MCMC_DIM` (defined at the top of this file). Note that while each MCMC sample + corresponds to a Gaussian posterior, the posterior is rather a mixture of Gaussian + distributions. + """ + + def __init__(self, distribution: MultivariateNormal) -> None: + r"""A posterior for a fully Bayesian model. + + Args: + distribution: A GPyTorch MultivariateNormal (single-output case) + """ + super().__init__(distribution=distribution) + self._mean = ( + distribution.mean if self._is_mt else distribution.mean.unsqueeze(-1) + ) + self._variance = ( + distribution.variance + if self._is_mt + else distribution.variance.unsqueeze(-1) + ) + self._covariance_matrix = distribution.lazy_covariance_matrix + + self._mixture_mean: Tensor | None = None + self._mixture_variance: Tensor | None = None + self._mixture_covariance_matrix: Tensor | None = None + + @property + def mixture_mean(self) -> Tensor: + r"""The posterior mean for the mixture of models.""" + if self._mixture_mean is None: + self._mixture_mean = self._mean.mean(dim=MCMC_DIM) + return self._mixture_mean + + @property + def mixture_variance(self) -> Tensor: + r"""The posterior variance for the mixture of models.""" + if self._mixture_variance is None: + num_mcmc_samples = self.mean.shape[MCMC_DIM] + t1 = self._variance.sum(dim=MCMC_DIM) / num_mcmc_samples + t2 = self._mean.pow(2).sum(dim=MCMC_DIM) / num_mcmc_samples + t3 = -(self._mean.sum(dim=MCMC_DIM) / num_mcmc_samples).pow(2) + self._mixture_variance = t1 + t2 + t3 + return self._mixture_variance + + @property + def mixture_covariance_matrix(self) -> Tensor: + r"""The posterior covariance matrix for the mixture of models.""" + if self._mixture_covariance_matrix is None: + num_mcmc_samples = self.mean.shape[MCMC_DIM] + t1 = self._covariance_matrix.sum(dim=MCMC_DIM) / num_mcmc_samples + mean_diff = self._mean - self.mixture_mean.unsqueeze(MCMC_DIM) + t2 = ( + torch.matmul(mean_diff, mean_diff.transpose(-1, -2)).sum(dim=MCMC_DIM) + / num_mcmc_samples + ) + self._mixture_covariance_matrix = t1 + t2 + + return self._mixture_covariance_matrix + +
+[docs] + def quantile(self, value: Tensor) -> Tensor: + r"""Compute the posterior quantiles for the mixture of models.""" + return _quantile(posterior=self, value=value)
+ + + @property + def batch_range(self) -> tuple[int, int]: + r"""The t-batch range. + + This is used in samplers to identify the t-batch component of the + `base_sample_shape`. The base samples are expanded over the t-batches to + provide consistency in the acquisition values, i.e., to ensure that a + candidate produces same value regardless of its position on the t-batch. + """ + return (0, -2) if self._is_mt else (0, -1)
+ + + +
+[docs] +class FullyBayesianPosterior(GaussianMixturePosterior): + """For backwards compatibility.""" + + def __init__(self, distribution: MultivariateNormal) -> None: + """DEPRECATED.""" + warn( + "`FullyBayesianPosterior` is marked for deprecation, consider using " + "`GaussianMixturePosterior` instead.", + DeprecationWarning, + ) + super().__init__(distribution=distribution)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/gpytorch.html b/website-old/pages/api/_modules/botorch/posteriors/gpytorch.html new file mode 100644 index 0000000000..7fd620fbfc --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/gpytorch.html @@ -0,0 +1,416 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.gpytorch

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Posterior module to be used with GPyTorch models.
+"""
+
+from __future__ import annotations
+
+from contextlib import ExitStack
+from typing import TYPE_CHECKING
+
+import torch
+from botorch.exceptions.errors import BotorchTensorDimensionError
+from botorch.posteriors.base_samples import _reshape_base_samples_non_interleaved
+from botorch.posteriors.torch import TorchPosterior
+from gpytorch.distributions import MultitaskMultivariateNormal, MultivariateNormal
+from linear_operator import settings as linop_settings
+from linear_operator.operators import (
+    BlockDiagLinearOperator,
+    DenseLinearOperator,
+    LinearOperator,
+    SumLinearOperator,
+)
+from torch import Tensor
+from torch.distributions import Normal
+
+if TYPE_CHECKING:
+    from botorch.posteriors.posterior_list import PosteriorList  # pragma: no cover
+
+
+
+[docs] +class GPyTorchPosterior(TorchPosterior): + r"""A posterior based on GPyTorch's multi-variate Normal distributions.""" + + distribution: MultivariateNormal + + def __init__(self, distribution: MultivariateNormal) -> None: + r"""A posterior based on GPyTorch's multi-variate Normal distributions. + + Args: + distribution: A GPyTorch MultivariateNormal (single-output case) or + MultitaskMultivariateNormal (multi-output case). + """ + super().__init__(distribution=distribution) + self._is_mt = isinstance(distribution, MultitaskMultivariateNormal) + + @property + def mvn(self) -> MultivariateNormal: + r"""Expose the distribution as a backwards-compatible attribute.""" + return self.distribution + + @property + def base_sample_shape(self) -> torch.Size: + r"""The shape of a base sample used for constructing posterior samples.""" + return self.distribution.batch_shape + self.distribution.base_sample_shape + + @property + def batch_range(self) -> tuple[int, int]: + r"""The t-batch range. + + This is used in samplers to identify the t-batch component of the + `base_sample_shape`. The base samples are expanded over the t-batches to + provide consistency in the acquisition values, i.e., to ensure that a + candidate produces same value regardless of its position on the t-batch. + """ + if self._is_mt: + return (0, -2) + else: + return (0, -1) + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + r"""Returns the shape of the samples produced by the posterior with + the given `sample_shape`. + """ + base_shape = self.distribution.batch_shape + self.distribution.event_shape + if not self._is_mt: + base_shape += torch.Size([1]) + return sample_shape + base_shape + +
+[docs] + def rsample_from_base_samples( + self, + sample_shape: torch.Size, + base_samples: Tensor, + ) -> Tensor: + r"""Sample from the posterior (with gradients) using base samples. + + This is intended to be used with a sampler that produces the corresponding base + samples, and enables acquisition optimization via Sample Average Approximation. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + base_samples: A Tensor of `N(0, I)` base samples of shape + `sample_shape x base_sample_shape`, typically obtained from + a `Sampler`. This is used for deterministic optimization. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if base_samples.shape[: len(sample_shape)] != sample_shape: + raise RuntimeError( + "`sample_shape` disagrees with shape of `base_samples`. " + f"Got {sample_shape=} and {base_samples.shape=}." + ) + if self._is_mt: + base_samples = _reshape_base_samples_non_interleaved( + mvn=self.distribution, + base_samples=base_samples, + sample_shape=sample_shape, + ) + with ExitStack() as es: + if linop_settings._fast_covar_root_decomposition.is_default(): + es.enter_context(linop_settings._fast_covar_root_decomposition(False)) + samples = self.distribution.rsample( + sample_shape=sample_shape, base_samples=base_samples + ) + if not self._is_mt: + samples = samples.unsqueeze(-1) + return samples
+ + +
+[docs] + def rsample(self, sample_shape: torch.Size | None = None) -> Tensor: + r"""Sample from the posterior (with gradients). + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if sample_shape is None: + sample_shape = torch.Size([1]) + with ExitStack() as es: + if linop_settings._fast_covar_root_decomposition.is_default(): + es.enter_context(linop_settings._fast_covar_root_decomposition(False)) + samples = self.distribution.rsample(sample_shape=sample_shape) + # make sure there always is an output dimension + if not self._is_mt: + samples = samples.unsqueeze(-1) + return samples
+ + + @property + def mean(self) -> Tensor: + r"""The posterior mean.""" + mean = self.distribution.mean + if not self._is_mt: + mean = mean.unsqueeze(-1) + return mean + + @property + def variance(self) -> Tensor: + r"""The posterior variance.""" + variance = self.distribution.variance + if not self._is_mt: + variance = variance.unsqueeze(-1) + return variance + +
+[docs] + def quantile(self, value: Tensor) -> Tensor: + r"""Compute the quantiles of the marginal distributions.""" + if value.numel() > 1: + return torch.stack([self.quantile(v) for v in value], dim=0) + marginal = Normal(loc=self.mean, scale=self.variance.sqrt()) + return marginal.icdf(value)
+ + +
+[docs] + def density(self, value: Tensor) -> Tensor: + r"""The probability density of the marginal distributions.""" + if value.numel() > 1: + return torch.stack([self.density(v) for v in value], dim=0) + marginal = Normal(loc=self.mean, scale=self.variance.sqrt()) + return marginal.log_prob(value).exp()
+
+ + + +def _validate_scalarize_inputs(weights: Tensor, m: int) -> None: + if weights.ndim > 1: + raise BotorchTensorDimensionError("`weights` must be one-dimensional") + if m != weights.size(0): + raise RuntimeError( + f"Output shape not equal to that of weights. Output shape is {m} and " + f"weights are {weights.shape}" + ) + + +
+[docs] +def scalarize_posterior_gpytorch( + posterior: GPyTorchPosterior, + weights: Tensor, + offset: float = 0.0, +) -> tuple[Tensor, Tensor | LinearOperator]: + r"""Helper function for `scalarize_posterior`, producing a mean and + variance. + + This mean and variance are consumed by `scalarize_posterior` to produce + a `GPyTorchPosterior`. + + Args: + posterior: The posterior over `m` outcomes to be scalarized. + Supports `t`-batching. + weights: A tensor of weights of size `m`. + offset: The offset of the affine transformation. + + Returns: + The transformed (single-output) posterior. If the input posterior has + mean `mu` and covariance matrix `Sigma`, this posterior has mean + `weights^T * mu` and variance `weights^T Sigma w`. + + Example: + Example for a model with two outcomes: + + >>> X = torch.rand(1, 2) + >>> posterior = model.posterior(X) + >>> weights = torch.tensor([0.5, 0.25]) + >>> mean, cov = scalarize_posterior_gpytorch(posterior, weights=weights) + >>> mvn = MultivariateNormal(mean, cov) + >>> new_posterior = GPyTorchPosterior + """ + mean = posterior.mean + q, m = mean.shape[-2:] + _validate_scalarize_inputs(weights, m) + batch_shape = mean.shape[:-2] + mvn = posterior.distribution + cov = mvn.lazy_covariance_matrix if mvn.islazy else mvn.covariance_matrix + + if m == 1: # just scaling, no scalarization necessary + new_mean = offset + (weights[0] * mean).view(*batch_shape, q) + new_cov = weights[0] ** 2 * cov + return new_mean, new_cov + + new_mean = offset + (mean @ weights).view(*batch_shape, q) + + if q == 1: + new_cov = weights.unsqueeze(-2) @ (cov @ weights.unsqueeze(-1)) + else: + # we need to handle potentially different representations of the multi-task mvn + if mvn._interleaved: + w_cov = weights.repeat(q).unsqueeze(0) + sum_shape = batch_shape + torch.Size([q, m, q, m]) + sum_dims = (-1, -2) + else: + # special-case the independent setting + if isinstance(cov, BlockDiagLinearOperator): + new_cov = SumLinearOperator( + *[ + cov.base_linear_op[..., i, :, :] * weights[i].pow(2) + for i in range(cov.base_linear_op.size(-3)) + ] + ) + return new_mean, new_cov + + w_cov = torch.repeat_interleave(weights, q).unsqueeze(0) + sum_shape = batch_shape + torch.Size([m, q, m, q]) + sum_dims = (-2, -3) + + cov_scaled = w_cov * cov * w_cov.transpose(-1, -2) + # TODO: Do not instantiate full covariance for LinearOperators + # (ideally we simplify this in GPyTorch: + # https://github.com/cornellius-gp/gpytorch/issues/1055) + if isinstance(cov_scaled, LinearOperator): + cov_scaled = cov_scaled.to_dense() + new_cov = cov_scaled.view(sum_shape).sum(dim=sum_dims[0]).sum(dim=sum_dims[1]) + new_cov = DenseLinearOperator(new_cov) + + return new_mean, new_cov
+ + + +
+[docs] +def scalarize_posterior( + posterior: GPyTorchPosterior | PosteriorList, + weights: Tensor, + offset: float = 0.0, +) -> GPyTorchPosterior: + r"""Affine transformation of a multi-output posterior. + + Args: + posterior: The posterior over `m` outcomes to be scalarized. + Supports `t`-batching. Can be either a `GPyTorchPosterior`, + or a `PosteriorList` that contains GPyTorchPosteriors all with q=1. + weights: A tensor of weights of size `m`. + offset: The offset of the affine transformation. + + Returns: + The transformed (single-output) posterior. If the input posterior has + mean `mu` and covariance matrix `Sigma`, this posterior has mean + `weights^T * mu` and variance `weights^T Sigma w`. + + Example: + Example for a model with two outcomes: + + >>> X = torch.rand(1, 2) + >>> posterior = model.posterior(X) + >>> weights = torch.tensor([0.5, 0.25]) + >>> new_posterior = scalarize_posterior(posterior, weights=weights) + """ + # GPyTorchPosterior case + if hasattr(posterior, "distribution"): + mean, cov = scalarize_posterior_gpytorch(posterior, weights, offset) + mvn = MultivariateNormal(mean, cov) + return GPyTorchPosterior(mvn) + + # PosteriorList case + if not hasattr(posterior, "posteriors"): + raise NotImplementedError( + "scalarize_posterior only works with a posterior that has an attribute " + "`distribution`, such as a GPyTorchPosterior, or a posterior that contains " + "sub-posteriors in an attribute `posteriors`, as in a PosteriorList." + ) + + mean = posterior.mean + q, m = mean.shape[-2:] + + _validate_scalarize_inputs(weights, m) + batch_shape = mean.shape[:-2] + + if q != 1: + raise NotImplementedError( + "scalarize_posterior only works with a PosteriorList if each sub-posterior " + "has q=1." + ) + + means = [post.mean for post in posterior.posteriors] + if {mean.shape[-1] for mean in means} != {1}: + raise NotImplementedError( + "scalarize_posterior only works with a PosteriorList if each sub-posterior " + "has one outcome." + ) + + new_mean = offset + (mean @ weights).view(*batch_shape, q) + new_cov = (posterior.variance @ (weights**2))[:, None] + mvn = MultivariateNormal(new_mean, new_cov) + return GPyTorchPosterior(mvn)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/higher_order.html b/website-old/pages/api/_modules/botorch/posteriors/higher_order.html new file mode 100644 index 0000000000..a30f360bdd --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/higher_order.html @@ -0,0 +1,331 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.higher_order

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+import torch
+from botorch.exceptions.errors import BotorchTensorDimensionError
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from gpytorch.distributions import MultivariateNormal
+from linear_operator.operators import LinearOperator
+from torch import Tensor
+
+
+
+[docs] +class HigherOrderGPPosterior(GPyTorchPosterior): + r""" + Posterior class for a Higher order Gaussian process model [Zhe2019hogp]_. Extends + the standard GPyTorch posterior class by overwriting the rsample method. + The posterior variance is handled internally by the HigherOrderGP model. + HOGP is a tensorized GP model so the posterior covariance grows to be extremely + large, but is highly structured, which means that we can exploit Kronecker + identities to sample from the posterior using Matheron's rule as described in + [Doucet2010sampl]_. + + In general, this posterior should ONLY be used for HOGP models + that have highly structured covariances. It should also only be used internally when + called from the HigherOrderGP.posterior(...) method. At this time, the posterior + does not support gradients with respect to the training data. + """ + + def __init__( + self, + distribution: MultivariateNormal, + joint_covariance_matrix: LinearOperator, + train_train_covar: LinearOperator, + test_train_covar: LinearOperator, + train_targets: Tensor, + output_shape: torch.Size, + num_outputs: int, + ) -> None: + r"""A Posterior for HigherOrderGP models. + + Args: + distribution: Posterior multivariate normal distribution. + joint_covariance_matrix: Joint test train covariance matrix over the entire + tensor. + train_train_covar: Covariance matrix of train points in the data space. + test_train_covar: Covariance matrix of test x train points + in the data space. + train_targets: Training responses vectorized. + output_shape: Shape output training responses. + num_outputs: Batch shaping of model. + """ + super().__init__(distribution=distribution) + self.joint_covariance_matrix = joint_covariance_matrix + self.train_train_covar = train_train_covar + self.test_train_covar = test_train_covar + self.train_targets = train_targets + self.output_shape = output_shape + self._is_mt = True + self.num_outputs = num_outputs + + @property + def base_sample_shape(self): + r"""The shape of a base sample used for constructing posterior samples. + + Overwrites the standard `base_sample_shape` call to inform samplers that + `n + 2 n_train` samples need to be drawn rather than n samples. + """ + joint_covar = self.joint_covariance_matrix + batch_shape = joint_covar.shape[:-2] + sampling_shape = torch.Size( + [joint_covar.shape[-2] + self.train_train_covar.shape[-2]] + ) + return batch_shape + sampling_shape + + @property + def batch_range(self) -> tuple[int, int]: + r"""The t-batch range. + + This is used in samplers to identify the t-batch component of the + `base_sample_shape`. The base samples are expanded over the t-batches to + provide consistency in the acquisition values, i.e., to ensure that a + candidate produces same value regardless of its position on the t-batch. + """ + return (0, -1) + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + r"""Returns the shape of the samples produced by the posterior with + the given `sample_shape`. + """ + return sample_shape + self.output_shape + + def _prepare_base_samples( + self, sample_shape: torch.Size, base_samples: Tensor = None + ) -> Tensor: + covariance_matrix = self.joint_covariance_matrix + joint_size = covariance_matrix.shape[-1] + batch_shape = covariance_matrix.batch_shape + + if base_samples is not None: + if base_samples.shape[: len(sample_shape)] != sample_shape: + raise RuntimeError("sample_shape disagrees with shape of base_samples.") + + appended_shape = joint_size + self.train_train_covar.shape[-1] + if appended_shape != base_samples.shape[-1]: + # get base_samples to the correct shape by expanding as sample shape, + # batch shape, then rest of dimensions. We have to add first the sample + # shape, then the batch shape of the model, and then finally the shape + # of the test data points squeezed into a single dimension, accessed + # from the test_train_covar. + base_sample_shapes = ( + sample_shape + batch_shape + self.test_train_covar.shape[-2:-1] + ) + if base_samples.nelement() == base_sample_shapes.numel(): + base_samples = base_samples.reshape(base_sample_shapes) + + new_base_samples = torch.randn( + *sample_shape, + *batch_shape, + appended_shape - base_samples.shape[-1], + device=base_samples.device, + dtype=base_samples.dtype, + ) + base_samples = torch.cat((base_samples, new_base_samples), dim=-1) + else: + raise BotorchTensorDimensionError( + "The base samples are not compatible with base sample shape. " + f"Received base samples of shape {base_samples.shape}, " + f"expected {base_sample_shapes}." + ) + + if base_samples is None: + # TODO: Allow qMC sampling + base_samples = torch.randn( + *sample_shape, + *batch_shape, + joint_size, + device=covariance_matrix.device, + dtype=covariance_matrix.dtype, + ) + + noise_base_samples = torch.randn( + *sample_shape, + *batch_shape, + self.train_train_covar.shape[-1], + device=covariance_matrix.device, + dtype=covariance_matrix.dtype, + ) + else: + # finally split up the base samples + noise_base_samples = base_samples[..., joint_size:] + base_samples = base_samples[..., :joint_size] + + perm_list = [*range(1, base_samples.ndim), 0] + return base_samples.permute(*perm_list), noise_base_samples.permute(*perm_list) + +
+[docs] + def rsample_from_base_samples( + self, + sample_shape: torch.Size, + base_samples: Tensor | None, + ) -> Tensor: + r"""Sample from the posterior (with gradients) using base samples. + + As the posterior covariance is difficult to draw from in this model, + we implement Matheron's rule as described in [Doucet2010sampl]-. This may not + work entirely correctly for deterministic base samples unless base samples + are provided that are of shape `n + 2 * n_train` because the sampling method + draws `2 * n_train` samples as well as the standard `n`. + samples. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + base_samples: An (optional) Tensor of `N(0, I)` base samples of + appropriate dimension, typically obtained from a `Sampler`. + This is used for deterministic optimization. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + base_samples, noise_base_samples = self._prepare_base_samples( + sample_shape, base_samples + ) + + # base samples now have trailing sample dimension + covariance_matrix = self.joint_covariance_matrix + covar_root = covariance_matrix.root_decomposition().root + + samples = covar_root.matmul(base_samples[..., : covar_root.shape[-1], :]) + + # now pluck out Y_x and X_x + noiseless_train_marginal_samples = samples[ + ..., : self.train_train_covar.shape[-1], : + ] + test_marginal_samples = samples[..., self.train_train_covar.shape[-1] :, :] + # we need to add noise to the train_joint_samples + # THIS ASSUMES CONSTANT NOISE + # The following assumes test_train_covar is a SumLinearOperator. TODO: Improve + noise_std = self.train_train_covar.linear_ops[1]._diag[..., 0] ** 0.5 + # TODO: cleanup the reshaping here + # expands the noise to allow broadcasting against the noise base samples + # reshape_as or view_as don't work here because we need to expand to + # broadcast against `samples x batch_shape x output_shape` while noise_std + # is `batch_shape x 1`. + if self.num_outputs > 1 or noise_std.ndim > 1: + ntms_dims = [ + i == noise_std.shape[0] for i in noiseless_train_marginal_samples.shape + ] + for matched in ntms_dims: + if not matched: + noise_std = noise_std.unsqueeze(-1) + + # we need to add noise into the noiseless samples + noise_marginal_samples = noise_std * noise_base_samples + + train_marginal_samples = ( + noiseless_train_marginal_samples + noise_marginal_samples + ) + + # compute y - Y_x + train_rhs = self.train_targets - train_marginal_samples + + # K_{train, train}^{-1} (y - Y_x) + # internally, this solve is done using Kronecker algebra and is fast. + kinv_rhs = self.train_train_covar.solve(train_rhs) + # multiply by cross-covariance + test_updated_samples = self.test_train_covar.matmul(kinv_rhs) + + # add samples + test_cond_samples = test_marginal_samples + test_updated_samples + test_cond_samples = test_cond_samples.permute( + test_cond_samples.ndim - 1, *range(0, test_cond_samples.ndim - 1) + ) + + # reshape samples to be the actual size of the train targets + return test_cond_samples.reshape(*sample_shape, *self.output_shape)
+ + +
+[docs] + def rsample( + self, + sample_shape: torch.Size | None = None, + ) -> Tensor: + r"""Sample from the posterior (with gradients). + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if sample_shape is None: + sample_shape = torch.Size([1]) + return self.rsample_from_base_samples( + sample_shape=sample_shape, base_samples=None + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/multitask.html b/website-old/pages/api/_modules/botorch/posteriors/multitask.html new file mode 100644 index 0000000000..1a08b084be --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/multitask.html @@ -0,0 +1,392 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.multitask

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+import torch
+from botorch.exceptions.errors import BotorchTensorDimensionError
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from gpytorch.distributions import MultivariateNormal
+from linear_operator.operators import LinearOperator, to_linear_operator
+from torch import Tensor
+
+
+
+[docs] +class MultitaskGPPosterior(GPyTorchPosterior): + def __init__( + self, + distribution: MultivariateNormal, + joint_covariance_matrix: LinearOperator, + test_train_covar: LinearOperator, + train_diff: Tensor, + test_mean: Tensor, + train_train_covar: LinearOperator, + train_noise: LinearOperator | Tensor, + test_noise: LinearOperator | Tensor | None = None, + ): + r""" + Posterior class for a Kronecker Multi-task GP model using with ICM kernel. + Extends the standard GPyTorch posterior class by overwriting the rsample + method. In general, this posterior should ONLY be used for MTGP models + that have structured covariances. It should also only be used internally when + called from the `KroneckerMultiTaskGP.posterior(...)` method. + + Args: + distribution: Posterior multivariate normal distribution. + joint_covariance_matrix: Joint test train covariance matrix over the entire + tensor. + test_train_covar: Covariance matrix of test x train points in the data + space. + train_diff: Difference between train mean and train responses. + test_mean: Test mean response. + train_train_covar: Covariance matrix of train points in the data space. + train_noise: Training noise covariance. + test_noise: Only used if posterior should contain observation noise. + Testing noise covariance. + """ + super().__init__(distribution=distribution) + self._is_mt = True + + self.joint_covariance_matrix = joint_covariance_matrix + self.test_train_covar = test_train_covar + self.train_diff = train_diff + self.test_mean = test_mean + self.train_train_covar = train_train_covar + self.train_noise = train_noise + self.test_noise = test_noise + self.observation_noise = self.test_noise is not None + + self.num_train = self.train_diff.shape[-2] + # The following assumes test_train_covar is a SumLinearOperator. TODO: Improve + self.num_tasks = self.test_train_covar.linear_ops[-1].shape[-1] + + @property + def base_sample_shape(self) -> torch.Size: + r"""The shape of a base sample used for constructing posterior samples. + + Overwrites the standard `base_sample_shape` call to inform samplers that + `n + 2 n_train` samples need to be drawn rather than n samples. + """ + batch_shape = self.joint_covariance_matrix.shape[:-2] + sampling_shape = ( + self.joint_covariance_matrix.shape[-2] + self.train_noise.shape[-2] + ) + if self.observation_noise: + sampling_shape = sampling_shape + self.test_noise.shape[-2] + return batch_shape + torch.Size((sampling_shape,)) + + @property + def batch_range(self) -> tuple[int, int]: + r"""The t-batch range. + + This is used in samplers to identify the t-batch component of the + `base_sample_shape`. The base samples are expanded over the t-batches to + provide consistency in the acquisition values, i.e., to ensure that a + candidate produces same value regardless of its position on the t-batch. + """ + return (0, -1) + + def _prepare_base_samples( + self, sample_shape: torch.Size, base_samples: Tensor = None + ) -> tuple[Tensor, Tensor]: + covariance_matrix = self.joint_covariance_matrix + joint_size = covariance_matrix.shape[-1] + batch_shape = covariance_matrix.batch_shape + + # pre-allocated this as None + test_noise_base_samples = None + if base_samples is not None: + if base_samples.shape[: len(sample_shape)] != sample_shape: + raise RuntimeError( + "sample_shape disagrees with shape of base_samples." + f"provided base sample shape is {base_samples.shape} while" + f"the expected shape is {sample_shape}." + ) + + if base_samples.shape[-1] != 1: + base_samples = base_samples.unsqueeze(-1) + unsqueezed_dim = -2 + + appended_shape = joint_size + self.train_train_covar.shape[-1] + if self.observation_noise: + appended_shape = appended_shape + self.test_noise.shape[-1] + + if appended_shape != base_samples.shape[unsqueezed_dim]: + # get base_samples to the correct shape by expanding as sample shape, + # batch shape, then rest of dimensions. We have to add first the sample + # shape, then the batch shape of the model, and then finally the shape + # of the test data points squeezed into a single dimension, accessed + # from the test_train_covar. + base_sample_shapes = ( + sample_shape + batch_shape + self.test_train_covar.shape[-2:-1] + ) + if base_samples.nelement() == base_sample_shapes.numel(): + base_samples = base_samples.reshape(base_sample_shapes) + + new_base_samples = torch.randn( + *sample_shape, + *batch_shape, + appended_shape - base_samples.shape[-1], + dtype=base_samples.dtype, + device=base_samples.device, + ) + base_samples = torch.cat((base_samples, new_base_samples), dim=-1) + base_samples = base_samples.unsqueeze(-1) + else: + raise BotorchTensorDimensionError( + "The base samples are not compatible with base sample shape. " + f"Received base samples of shape {base_samples.shape}, " + f"expected {base_sample_shapes}." + ) + + if base_samples is None: + # TODO: Allow qMC sampling + base_samples = torch.randn( + *sample_shape, + *batch_shape, + joint_size, + 1, + device=covariance_matrix.device, + dtype=covariance_matrix.dtype, + ) + + noise_base_samples = torch.randn( + *sample_shape, + *batch_shape, + self.train_train_covar.shape[-1], + 1, + device=covariance_matrix.device, + dtype=covariance_matrix.dtype, + ) + if self.observation_noise: + test_noise_base_samples = torch.randn( + *sample_shape, + *self.test_noise.shape[:-1], + 1, + device=covariance_matrix.device, + dtype=covariance_matrix.dtype, + ) + else: + # finally split up the base samples + noise_base_samples = base_samples[..., joint_size:, :] + base_samples = base_samples[..., :joint_size, :] + if self.observation_noise: + test_noise_base_samples = noise_base_samples[ + ..., -self.test_noise.shape[-1] :, : + ] + noise_base_samples = noise_base_samples[ + ..., : -self.test_noise.shape[-1], : + ] + + return base_samples, noise_base_samples, test_noise_base_samples + +
+[docs] + def rsample_from_base_samples( + self, + sample_shape: torch.Size, + base_samples: Tensor | None, + train_diff: Tensor | None = None, + ) -> Tensor: + r"""Sample from the posterior (with gradients) using base samples. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + base_samples: An (optional) Tensor of `N(0, I)` base samples of + appropriate dimension, typically obtained from a `Sampler`. + This is used for deterministic optimization. + train_diff: Difference between train mean and train responses to assume + during sampling. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if train_diff is None: + train_diff = self.train_diff + + ( + base_samples, + noise_base_samples, + test_noise_base_samples, + ) = self._prepare_base_samples( + sample_shape=sample_shape, base_samples=base_samples + ) + joint_samples = self._draw_from_base_covar( + self.joint_covariance_matrix, base_samples + ) + noise_samples = self._draw_from_base_covar(self.train_noise, noise_base_samples) + + # pluck out the train + test samples and add the likelihood's noise to the + # train side. This should be fine for higher rank likelihoods. + n_obs = self.num_tasks * self.num_train + n_test = joint_samples.shape[-1] - n_obs + obs_samples, test_samples = torch.split(joint_samples, [n_obs, n_test], dim=-1) + updated_obs_samples = obs_samples + noise_samples + obs_minus_samples = ( + train_diff.reshape(*train_diff.shape[:-2], -1) - updated_obs_samples + ) + train_covar_plus_noise = self.train_train_covar + self.train_noise + obs_solve = _permute_solve( + train_covar_plus_noise, obs_minus_samples.unsqueeze(-1) + ) + + # and multiply the test-observed matrix against the result of the solve + updated_samples = self.test_train_covar.matmul(obs_solve).squeeze(-1) + + # finally, we add the conditioned samples to the prior samples + final_samples = test_samples + updated_samples + + # add in likelihood noise if necessary + if self.observation_noise: + test_noise_samples = self._draw_from_base_covar( + self.test_noise, test_noise_base_samples + ) + final_samples = final_samples + test_noise_samples + + # and reshape + final_samples = final_samples.reshape( + *final_samples.shape[:-1], self.test_mean.shape[-2], self.num_tasks + ) + final_samples = final_samples + self.test_mean + + return final_samples
+ + +
+[docs] + def rsample( + self, + sample_shape: torch.Size | None = None, + ) -> Tensor: + r"""Sample from the posterior (with gradients). + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if sample_shape is None: + sample_shape = torch.Size([1]) + return self.rsample_from_base_samples( + sample_shape=sample_shape, base_samples=None + )
+ + + def _draw_from_base_covar( + self, covar: Tensor | LinearOperator, base_samples: Tensor + ) -> Tensor: + # Now reparameterize those base samples + if not isinstance(covar, LinearOperator): + covar = to_linear_operator(covar) + covar_root = covar.root_decomposition().root + # If necessary, adjust base_samples for rank of root decomposition + if covar_root.shape[-1] < base_samples.shape[-2]: + base_samples = base_samples[..., : covar_root.shape[-1], :] + elif covar_root.shape[-1] > base_samples.shape[-2]: + raise RuntimeError("Incompatible dimension of `base_samples`") + # the mean is included in the posterior forwards so is not included here + res = covar_root.matmul(base_samples) + + return res.squeeze(-1)
+ + + +def _permute_solve(A: LinearOperator, b: Tensor) -> LinearOperator: + r"""Solve the batched linear system AX = b, where b is a batched column + vector. The solve is carried out after permuting the largest batch + dimension of b to the final position, which results in a more efficient + matrix-matrix solve. + + This ideally should be handled upstream (in GPyTorch, linear_operator or + PyTorch), after which any uses of this method can be replaced with + `A.solve(b)`. + + Args: + A: LinearOperator of shape (n, n) + b: Tensor of shape (..., n, 1) + + Returns: + LinearOperator of shape (..., n, 1) + """ + # permute dimensions to move largest batch dimension to the end (more efficient + # than unsqueezing) + perm = list(range(b.ndim)) + if b.ndim > 2: + largest_batch_dim, _ = max(enumerate(b.shape[:-2]), key=lambda t: t[1]) + perm[-1], perm[largest_batch_dim] = perm[largest_batch_dim], perm[-1] + b_p = b.permute(*perm) + + x_p = A.solve(b_p) + + # Undo permutation + inverse_perm = torch.argsort(torch.tensor(perm)) + x = x_p.permute(*inverse_perm) + + return x +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/posterior.html b/website-old/pages/api/_modules/botorch/posteriors/posterior.html new file mode 100644 index 0000000000..368b2db184 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/posterior.html @@ -0,0 +1,221 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.posterior

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Abstract base module for all botorch posteriors.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from torch import Tensor
+
+
+
+[docs] +class Posterior(ABC): + """Abstract base class for botorch posteriors.""" + +
+[docs] + def rsample_from_base_samples( + self, + sample_shape: torch.Size, + base_samples: Tensor, + ) -> Tensor: + r"""Sample from the posterior (with gradients) using base samples. + + This is intended to be used with a sampler that produces the corresponding base + samples, and enables acquisition optimization via Sample Average Approximation. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + base_samples: The base samples, obtained from the appropriate sampler. + This is a tensor of shape `sample_shape x base_sample_shape`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement `rsample_from_base_samples`." + ) # pragma: no cover
+ + +
+[docs] + @abstractmethod + def rsample( + self, + sample_shape: torch.Size | None = None, + ) -> Tensor: + r"""Sample from the posterior (with gradients). + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + pass # pragma: no cover
+ + +
+[docs] + def sample(self, sample_shape: torch.Size | None = None) -> Tensor: + r"""Sample from the posterior without gradients. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + with torch.no_grad(): + return self.rsample(sample_shape=sample_shape)
+ + + @property + @abstractmethod + def device(self) -> torch.device: + r"""The torch device of the distribution.""" + pass # pragma: no cover + + @property + @abstractmethod + def dtype(self) -> torch.dtype: + r"""The torch dtype of the distribution.""" + pass # pragma: no cover + +
+[docs] + def quantile(self, value: Tensor) -> Tensor: + r"""Compute quantiles of the distribution. + + For multi-variate distributions, this may return the quantiles of + the marginal distributions. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement a `quantile` method." + ) # pragma: no cover
+ + +
+[docs] + def density(self, value: Tensor) -> Tensor: + r"""The probability density (or mass) of the distribution. + + For multi-variate distributions, this may return the density of + the marginal distributions. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement a `density` method." + ) # pragma: no cover
+ + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + r"""Returns the shape of the samples produced by the posterior with + the given `sample_shape`. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement `_extended_shape`." + ) + + @property + def base_sample_shape(self) -> torch.Size: + r"""The base shape of the base samples expected in `rsample`. + + Informs the sampler to produce base samples of shape + `sample_shape x base_sample_shape`. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement `base_sample_shape`." + ) + + @property + def batch_range(self) -> tuple[int, int]: + r"""The t-batch range. + + This is used in samplers to identify the t-batch component of the + `base_sample_shape`. The base samples are expanded over the t-batches to + provide consistency in the acquisition values, i.e., to ensure that a + candidate produces same value regardless of its position on the t-batch. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement `batch_range`." + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/posterior_list.html b/website-old/pages/api/_modules/botorch/posteriors/posterior_list.html new file mode 100644 index 0000000000..15cd1803e7 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/posterior_list.html @@ -0,0 +1,245 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.posterior_list

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Abstract base module for all botorch posteriors.
+"""
+
+from __future__ import annotations
+
+from functools import cached_property
+from typing import Any
+
+import torch
+from botorch.posteriors.fully_bayesian import GaussianMixturePosterior, MCMC_DIM
+from botorch.posteriors.posterior import Posterior
+from torch import Tensor
+
+
+
+[docs] +class PosteriorList(Posterior): + r"""A Posterior represented by a list of independent Posteriors. + + When at least one of the posteriors is a `GaussianMixturePosterior`, the other + posteriors are expanded to match the size of the `GaussianMixturePosterior`. + """ + + def __init__(self, *posteriors: Posterior) -> None: + r"""A Posterior represented by a list of independent Posteriors. + + Args: + *posteriors: A variable number of single-outcome posteriors. + + Example: + >>> p_1 = model_1.posterior(test_X) + >>> p_2 = model_2.posterior(test_X) + >>> p_12 = PosteriorList(p_1, p_2) + + Note: This is typically produced automatically in `ModelList`; it should + generally not be necessary for the end user to invoke it manually. + """ + self.posteriors = list(posteriors) + + @cached_property + def _is_gaussian_mixture(self) -> bool: + r"""Check if any of the posteriors is a `GaussianMixturePosterior`.""" + return any(isinstance(p, GaussianMixturePosterior) for p in self.posteriors) + + def _get_mcmc_batch_dimension(self) -> int: + """Return the number of MCMC samples in the corresponding batch dimension.""" + mcmc_samples = [ + p.mean.shape[MCMC_DIM] + for p in self.posteriors + if isinstance(p, GaussianMixturePosterior) + ] + if len(set(mcmc_samples)) > 1: + raise NotImplementedError( + "All MCMC batch dimensions must have the same size, got shapes: " + f"{mcmc_samples}." + ) + return mcmc_samples[0] + + @staticmethod + def _reshape_tensor(X: Tensor, mcmc_samples: int) -> Tensor: + """Reshape a tensor without an MCMC batch dimension to match the shape.""" + X = X.unsqueeze(MCMC_DIM) + return X.expand(*X.shape[:MCMC_DIM], mcmc_samples, *X.shape[MCMC_DIM + 1 :]) + + def _reshape_and_cat(self, tensors: list[Tensor]): + r"""Reshape, if needed, and concatenate (across dim=-1) a list of tensors.""" + if self._is_gaussian_mixture: + mcmc_samples = self._get_mcmc_batch_dimension() + return torch.cat( + [ + ( + x + if isinstance(p, GaussianMixturePosterior) + else self._reshape_tensor(x, mcmc_samples=mcmc_samples) + ) + for x, p in zip(tensors, self.posteriors) + ], + dim=-1, + ) + else: + return torch.cat(tensors, dim=-1) + + @property + def device(self) -> torch.device: + r"""The torch device of the posterior.""" + devices = {p.device for p in self.posteriors} + if len(devices) > 1: + raise NotImplementedError( # pragma: no cover + "Multi-device posteriors are currently not supported. " + "The devices of the constituent posteriors are: {devices}." + ) + return next(iter(devices)) + + @property + def dtype(self) -> torch.dtype: + r"""The torch dtype of the posterior.""" + dtypes = {p.dtype for p in self.posteriors} + if len(dtypes) > 1: + raise NotImplementedError( + "Multi-dtype posteriors are currently not supported. " + "The dtypes of the constituent posteriors are: {dtypes}." + ) + return next(iter(dtypes)) + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + r"""Returns the shape of the samples produced by the posterior with + the given `sample_shape`. + + If there's at least one `GaussianMixturePosterior`, the MCMC dimension + is included the `_extended_shape`. + """ + if self._is_gaussian_mixture: + mcmc_shape = torch.Size([self._get_mcmc_batch_dimension()]) + extend_dim = MCMC_DIM + 1 # The dimension to inject MCMC shape. + extended_shapes = [] + for p in self.posteriors: + es = p._extended_shape(sample_shape=sample_shape) + if self._is_gaussian_mixture and not isinstance( + p, GaussianMixturePosterior + ): + # Extend the shapes of non-fully Bayesian ones to match. + extended_shapes.append(es[:extend_dim] + mcmc_shape + es[extend_dim:]) + else: + extended_shapes.append(es) + batch_shapes = [es[:-1] for es in extended_shapes] + if len(set(batch_shapes)) > 1: + raise NotImplementedError( + "`PosteriorList` is only supported if the constituent posteriors " + f"all have the same `batch_shape`. Batch shapes: {batch_shapes}." + ) + # Last dimension is the output dimension (concatenation dimension). + return batch_shapes[0] + torch.Size([sum(es[-1] for es in extended_shapes)]) + + @property + def mean(self) -> Tensor: + r"""The mean of the posterior as a `(b) x n x m`-dim Tensor. + + This is only supported if all posteriors provide a mean. + """ + return self._reshape_and_cat(tensors=[p.mean for p in self.posteriors]) + + @property + def variance(self) -> Tensor: + r"""The variance of the posterior as a `(b) x n x m`-dim Tensor. + + This is only supported if all posteriors provide a variance. + """ + return self._reshape_and_cat(tensors=[p.variance for p in self.posteriors]) + +
+[docs] + def rsample(self, sample_shape: torch.Size | None = None) -> Tensor: + r"""Sample from the posterior (with gradients). + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + samples = [p.rsample(sample_shape=sample_shape) for p in self.posteriors] + return self._reshape_and_cat(tensors=samples)
+ + + def __getattr__(self, name: str) -> Any: + r"""A catch-all for attributes not defined on the posterior level. + + Raises an attribute error. + """ + raise AttributeError( + f"`PosteriorList` does not define the attribute {name}. " + "Consider accessing the attributes of the individual posteriors instead." + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/torch.html b/website-old/pages/api/_modules/botorch/posteriors/torch.html new file mode 100644 index 0000000000..c2d4fe85db --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/torch.html @@ -0,0 +1,192 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.torch

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Posterior module to be used with PyTorch distributions.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+import torch
+from botorch.posteriors.posterior import Posterior
+from torch import Tensor
+from torch.distributions.distribution import Distribution
+
+
+
+[docs] +class TorchPosterior(Posterior): + r"""A posterior based on a PyTorch Distribution. + + NOTE: For any attribute that is not explicitly defined on the Posterior level, this + returns the corresponding attribute of the distribution. This allows easy access + to the distribution attributes, without having to expose them on the Posterior. + """ + + def __init__(self, distribution: Distribution) -> None: + r"""A posterior based on a PyTorch Distribution. + + Args: + distribution: A PyTorch Distribution object. + """ + self.distribution = distribution + # Get the device and dtype from distribution attributes. + for attr in vars(distribution).values(): + if isinstance(attr, Tensor): + self._device = attr.device + self._dtype = attr.dtype + break + +
+[docs] + def rsample( + self, + sample_shape: torch.Size | None = None, + ) -> Tensor: + r"""Sample from the posterior (with gradients). + + This is generally used with a sampler that produces the base samples. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + if sample_shape is None: + sample_shape = torch.Size() + return self.distribution.rsample(sample_shape=sample_shape)
+ + + @property + def device(self) -> torch.device: + r"""The torch device of the distribution.""" + return self._device + + @property + def dtype(self) -> torch.dtype: + r"""The torch dtype of the distribution.""" + return self._dtype + + def __getattr__(self, name: str) -> Any: + r"""A catch-all for attributes not defined on the posterior level. + + Returns the attributes of the distribution instead. + """ + return getattr(self.distribution, name) + + def __getstate__(self) -> dict[str, Any]: + r"""A minimal utility to support pickle protocol. + + Pickle uses `__get/setstate__` to serialize / deserialize the objects. + Since we define `__getattr__` above, it takes precedence over these + methods, and we end up in an infinite loop unless we also define + `__getstate__` and `__setstate__`. + """ + return self.__dict__ + + def __setstate__(self, d: dict[str, Any]) -> None: + r"""A minimal utility to support pickle protocol.""" + self.__dict__ = d + +
+[docs] + def quantile(self, value: Tensor) -> Tensor: + r"""Compute quantiles of the distribution. + + For multi-variate distributions, this may return the quantiles of + the marginal distributions. + """ + if value.numel() > 1: + return torch.stack([self.quantile(v) for v in value], dim=0) + return self.icdf(value)
+ + +
+[docs] + def density(self, value: Tensor) -> Tensor: + r"""The probability density (or mass if discrete) of the distribution. + + For multi-variate distributions, this may return the density of + the marginal distributions. + """ + if value.numel() > 1: + return torch.stack([self.density(v) for v in value], dim=0) + return self.log_prob(value).exp()
+ + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + r"""Returns the shape of the samples produced by the distribution with + the given `sample_shape`. + """ + return self.distribution._extended_shape(sample_shape=sample_shape)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/posteriors/transformed.html b/website-old/pages/api/_modules/botorch/posteriors/transformed.html new file mode 100644 index 0000000000..af6dc03cc0 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/posteriors/transformed.html @@ -0,0 +1,211 @@ + + + + + + + +
+
+
+
+

Source code for botorch.posteriors.transformed

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import torch
+from botorch.posteriors.posterior import Posterior
+from torch import Tensor
+
+
+
+[docs] +class TransformedPosterior(Posterior): + r"""A generic transformation of a posterior (implicitly represented).""" + + def __init__( + self, + posterior: Posterior, + sample_transform: Callable[[Tensor], Tensor], + mean_transform: Callable[[Tensor, Tensor], Tensor] | None = None, + variance_transform: Callable[[Tensor, Tensor], Tensor] | None = None, + ) -> None: + r"""An implicitly represented transformed posterior. + + Args: + posterior: The posterior object to be transformed. + sample_transform: A callable applying a sample-level transform to a + `sample_shape x batch_shape x q x m`-dim tensor of samples from + the original posterior, returning a tensor of samples of the + same shape. + mean_transform: A callable transforming a 2-tuple of mean and + variance (both of shape `batch_shape x m x o`) of the original + posterior to the mean of the transformed posterior. + variance_transform: A callable transforming a 2-tuple of mean and + variance (both of shape `batch_shape x m x o`) of the original + posterior to a variance of the transformed posterior. + """ + self._posterior = posterior + self._sample_transform = sample_transform + self._mean_transform = mean_transform + self._variance_transform = variance_transform + + @property + def base_sample_shape(self) -> torch.Size: + r"""The shape of a base sample used for constructing posterior samples.""" + return self._posterior.base_sample_shape + + @property + def batch_range(self) -> tuple[int, int]: + r"""The t-batch range. + + This is used in samplers to identify the t-batch component of the + `base_sample_shape`. The base samples are expanded over the t-batches to + provide consistency in the acquisition values, i.e., to ensure that a + candidate produces same value regardless of its position on the t-batch. + """ + return self._posterior.batch_range + + @property + def device(self) -> torch.device: + r"""The torch device of the posterior.""" + return self._posterior.device + + @property + def dtype(self) -> torch.dtype: + r"""The torch dtype of the posterior.""" + return self._posterior.dtype + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + r"""Returns the shape of the samples produced by the posterior with + the given `sample_shape`. + + NOTE: This assumes that the `sample_transform` does not change the + shape of the samples. + """ + return self._posterior._extended_shape(sample_shape=sample_shape) + + @property + def mean(self) -> Tensor: + r"""The mean of the posterior as a `batch_shape x n x m`-dim Tensor.""" + if self._mean_transform is None: + raise NotImplementedError("No mean transform provided.") + try: + variance = self._posterior.variance + except (NotImplementedError, AttributeError): + variance = None + return self._mean_transform(self._posterior.mean, variance) + + @property + def variance(self) -> Tensor: + r"""The variance of the posterior as a `batch_shape x n x m`-dim Tensor.""" + if self._variance_transform is None: + raise NotImplementedError("No variance transform provided.") + return self._variance_transform(self._posterior.mean, self._posterior.variance) + +
+[docs] + def rsample_from_base_samples( + self, + sample_shape: torch.Size, + base_samples: Tensor, + ) -> Tensor: + r"""Sample from the posterior (with gradients) using base samples. + + This is intended to be used with a sampler that produces the corresponding base + samples, and enables acquisition optimization via Sample Average Approximation. + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + base_samples: The base samples, obtained from the appropriate sampler. + This is a tensor of shape `sample_shape x base_sample_shape`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + samples = self._posterior.rsample_from_base_samples( + sample_shape=sample_shape, base_samples=base_samples + ) + return self._sample_transform(samples)
+ + +
+[docs] + def rsample( + self, + sample_shape: torch.Size | None = None, + ) -> Tensor: + r"""Sample from the posterior (with gradients). + + Args: + sample_shape: A `torch.Size` object specifying the sample shape. To + draw `n` samples, set to `torch.Size([n])`. To draw `b` batches + of `n` samples each, set to `torch.Size([b, n])`. + + Returns: + Samples from the posterior, a tensor of shape + `self._extended_shape(sample_shape=sample_shape)`. + """ + samples = self._posterior.rsample(sample_shape=sample_shape) + return self._sample_transform(samples)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/base.html b/website-old/pages/api/_modules/botorch/sampling/base.html new file mode 100644 index 0000000000..5fc178c930 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/base.html @@ -0,0 +1,211 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.base

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+The base class for sampler modules to be used with MC-evaluated acquisition functions.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.exceptions.errors import InputDataError
+from botorch.posteriors import Posterior
+from torch import Tensor
+from torch.nn import Module
+
+
+KWARGS_DEPRECATED_MSG = (
+    "The {} argument of `MCSampler`s has been deprecated and will raise an "
+    "error in a future version."
+)
+KWARG_ERR_MSG = (
+    "`MCSampler`s no longer support the `{}` argument. "
+    "Consider using `{}` for similar functionality."
+)
+
+
+
+[docs] +class MCSampler(Module, ABC): + r"""Abstract base class for Samplers. + + Subclasses must implement the `forward` method. + + Example: + This method is usually not called directly, but via the sampler's + `__call__` method: + >>> posterior = model.posterior(test_X) + >>> samples = sampler(posterior) + """ + + def __init__( + self, + sample_shape: torch.Size, + seed: int | None = None, + ) -> None: + r"""Abstract base class for samplers. + + Args: + sample_shape: The `sample_shape` of the samples to generate. The full shape + of the samples is given by `posterior._extended_shape(sample_shape)`. + seed: An optional seed to use for sampling. + """ + super().__init__() + if not isinstance(sample_shape, torch.Size): + raise InputDataError( + "Expected `sample_shape` to be a `torch.Size` object, " + f"got {sample_shape}." + ) + self.sample_shape = sample_shape + self.seed = seed if seed is not None else torch.randint(0, 1000000, (1,)).item() + self.register_buffer("base_samples", None) + +
+[docs] + @abstractmethod + def forward(self, posterior: Posterior) -> Tensor: + r"""Draws MC samples from the posterior. + + Args: + posterior: The posterior to sample from. + + Returns: + The samples drawn from the posterior. + """ + pass # pragma no cover
+ + + def _get_batch_range(self, posterior: Posterior) -> tuple[int, int]: + r"""Get the t-batch range of the posterior with an optional override. + + In rare cases, e.g., in `qMultiStepLookahead`, we may want to override the + `batch_range` of the posterior. If this behavior is desired, one can set + `batch_range_override` attribute on the samplers. + + Args: + posterior: The posterior to sample from. + + Returns: + The t-batch range to use for collapsing the base samples. + """ + if hasattr(self, "batch_range_override"): + return self.batch_range_override + return posterior.batch_range + + def _get_collapsed_shape(self, posterior: Posterior) -> torch.Size: + r"""Get the shape of the base samples with the t-batches collapsed. + + Args: + posterior: The posterior to sample from. + + Returns: + The collapsed shape of the base samples expected by the posterior. The + t-batch dimensions of the base samples are collapsed to size 1. This is + useful to prevent sampling variance across t-batches. + """ + base_sample_shape = posterior.base_sample_shape + batch_start, batch_end = self._get_batch_range(posterior) + base_sample_shape = ( + base_sample_shape[:batch_start] + + torch.Size([1 for _ in base_sample_shape[batch_start:batch_end]]) + + base_sample_shape[batch_end:] + ) + return self.sample_shape + base_sample_shape + + def _get_extended_base_sample_shape(self, posterior: Posterior) -> torch.Size: + r"""Get the shape of the base samples expected by the posterior. + + Args: + posterior: The posterior to sample from. + + Returns: + The extended shape of the base samples expected by the posterior. + """ + return self.sample_shape + posterior.base_sample_shape + + def _update_base_samples( + self, posterior: Posterior, base_sampler: MCSampler + ) -> None: + r"""Update the sampler to use the original base samples for X_baseline. + + This is used in CachedCholeskyAcquisitionFunctions to ensure consistency. + + Args: + posterior: The posterior for which the base samples are constructed. + base_sampler: The base sampler to retrieve the base samples from. + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement `_update_base_samples`." + ) + + def _instance_check(self, base_sampler): + r"""Check that `base_sampler` is an instance of `self.__class__`.""" + if not isinstance(base_sampler, self.__class__): + raise RuntimeError( + "Expected `base_sampler` to be an instance of " + f"{self.__class__.__name__}. Got {base_sampler}." + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/get_sampler.html b/website-old/pages/api/_modules/botorch/sampling/get_sampler.html new file mode 100644 index 0000000000..c01b231d60 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/get_sampler.html @@ -0,0 +1,198 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.get_sampler

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+import torch
+from botorch.logging import logger
+from botorch.posteriors.ensemble import EnsemblePosterior
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from botorch.posteriors.posterior import Posterior
+from botorch.posteriors.posterior_list import PosteriorList
+from botorch.posteriors.torch import TorchPosterior
+from botorch.posteriors.transformed import TransformedPosterior
+from botorch.sampling.base import MCSampler
+from botorch.sampling.index_sampler import IndexSampler
+from botorch.sampling.list_sampler import ListSampler
+from botorch.sampling.normal import (
+    IIDNormalSampler,
+    NormalMCSampler,
+    SobolQMCNormalSampler,
+)
+from botorch.utils.dispatcher import Dispatcher
+from gpytorch.distributions import MultivariateNormal
+from torch.distributions import Distribution
+from torch.quasirandom import SobolEngine
+
+
+def _posterior_to_distribution_encoder(
+    posterior: Posterior,
+) -> type[Distribution] | type[Posterior]:
+    r"""An encoder returning the type of the distribution for `TorchPosterior`
+    and the type of the posterior for the rest.
+    """
+    if isinstance(posterior, TorchPosterior):
+        return type(posterior.distribution)
+    return type(posterior)
+
+
+GetSampler = Dispatcher("get_sampler", encoder=_posterior_to_distribution_encoder)
+
+
+
+[docs] +def get_sampler( + posterior: TorchPosterior, + sample_shape: torch.Size, + *, + seed: int | None = None, +) -> MCSampler: + r"""Get the sampler for the given posterior. + + The sampler can be used as `sampler(posterior)` to produce samples + suitable for use in acquisition function optimization via SAA. + + Args: + posterior: A `Posterior` to get the sampler for. + sample_shape: The sample shape of the samples produced by the + given sampler. The full shape of the resulting samples is + given by `posterior._extended_shape(sample_shape)`. + seed: Seed used to initialize sampler. + + Returns: + The `MCSampler` object for the given posterior. + """ + return GetSampler(posterior, sample_shape=sample_shape, seed=seed)
+ + + +@GetSampler.register(MultivariateNormal) +def _get_sampler_mvn( + posterior: GPyTorchPosterior, + sample_shape: torch.Size, + *, + seed: int | None = None, +) -> NormalMCSampler: + r"""The Sobol normal sampler for the `MultivariateNormal` posterior. + + If the output dim is too large, falls back to `IIDNormalSampler`. + """ + sampler = SobolQMCNormalSampler(sample_shape=sample_shape, seed=seed) + collapsed_shape = sampler._get_collapsed_shape(posterior=posterior) + base_collapsed_shape = collapsed_shape[len(sample_shape) :] + if base_collapsed_shape.numel() > SobolEngine.MAXDIM: + logger.warning( + f"Output dim {base_collapsed_shape.numel()} is too large for the " + "Sobol engine. Using IIDNormalSampler instead." + ) + sampler = IIDNormalSampler(sample_shape=sample_shape, seed=seed) + return sampler + + +@GetSampler.register(TransformedPosterior) +def _get_sampler_derived( + posterior: TransformedPosterior, + sample_shape: torch.Size, + *, + seed: int | None = None, +) -> MCSampler: + r"""Get the sampler for the underlying posterior.""" + return get_sampler( + posterior=posterior._posterior, + sample_shape=sample_shape, + seed=seed, + ) + + +@GetSampler.register(PosteriorList) +def _get_sampler_list( + posterior: PosteriorList, sample_shape: torch.Size, *, seed: int | None = None +) -> MCSampler: + r"""Get the `ListSampler` with the appropriate list of samplers.""" + samplers = [ + get_sampler(posterior=p, sample_shape=sample_shape, seed=seed) + for p in posterior.posteriors + ] + return ListSampler(*samplers) + + +@GetSampler.register(EnsemblePosterior) +def _get_sampler_ensemble( + posterior: EnsemblePosterior, + sample_shape: torch.Size, + seed: int | None = None, +) -> MCSampler: + r"""Get the `IndexSampler` for the `EnsemblePosterior`.""" + return IndexSampler(sample_shape=sample_shape, seed=seed) + + +@GetSampler.register(object) +def _not_found_error( + posterior: Posterior, + sample_shape: torch.Size, + seed: int | None = None, +) -> None: + raise NotImplementedError( + f"A registered `MCSampler` for posterior {posterior} is not found. You can " + "implement and register one using `@GetSampler.register`." + ) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/index_sampler.html b/website-old/pages/api/_modules/botorch/sampling/index_sampler.html new file mode 100644 index 0000000000..6e870d3819 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/index_sampler.html @@ -0,0 +1,128 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.index_sampler

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Sampler to be used with `EnsemblePosteriors` to enable
+deterministic optimization of acquisition functions with ensemble models.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.posteriors.ensemble import EnsemblePosterior
+from botorch.sampling.base import MCSampler
+from torch import Tensor
+
+
+
+[docs] +class IndexSampler(MCSampler): + r"""A sampler that calls `posterior.rsample_from_base_samples` to + generate the samples via index base samples.""" + +
+[docs] + def forward(self, posterior: EnsemblePosterior) -> Tensor: + r"""Draws MC samples from the posterior. + + Args: + posterior: The ensemble posterior to sample from. + + Returns: + The samples drawn from the posterior. + """ + self._construct_base_samples(posterior=posterior) + samples = posterior.rsample_from_base_samples( + sample_shape=self.sample_shape, base_samples=self.base_samples + ) + return samples
+ + + def _construct_base_samples(self, posterior: EnsemblePosterior) -> None: + r"""Constructs base samples as indices to sample with them from + the Posterior. + + Args: + posterior: The ensemble posterior to construct the base samples + for. + """ + if self.base_samples is None or self.base_samples.shape != self.sample_shape: + with torch.random.fork_rng(): + torch.manual_seed(self.seed) + base_samples = torch.multinomial( + posterior.weights, + num_samples=self.sample_shape.numel(), + replacement=True, + ).reshape(self.sample_shape) + self.register_buffer("base_samples", base_samples) + if self.base_samples.device != posterior.device: + self.to(device=posterior.device) # pragma: nocover + + def _update_base_samples( + self, posterior: EnsemblePosterior, base_sampler: IndexSampler + ) -> None: + r"""Null operation just needed for compatibility with + `CachedCholeskyAcquisitionFunction`.""" + pass
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/list_sampler.html b/website-old/pages/api/_modules/botorch/sampling/list_sampler.html new file mode 100644 index 0000000000..f3125ed142 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/list_sampler.html @@ -0,0 +1,138 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.list_sampler

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+A `SamplerList` for sampling from a `PosteriorList`.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.posteriors.posterior_list import PosteriorList
+from botorch.sampling.base import MCSampler
+from torch import Tensor
+from torch.nn import ModuleList
+
+
+
+[docs] +class ListSampler(MCSampler): + def __init__(self, *samplers: MCSampler) -> None: + r"""A list of samplers for sampling from a `PosteriorList`. + + Args: + samplers: A variable number of samplers. This should include + a sampler for each posterior. + """ + super(MCSampler, self).__init__() + self.samplers = ModuleList(samplers) + self._validate_samplers() + + def _validate_samplers(self) -> None: + r"""Checks that the samplers share the same sample shape.""" + sample_shapes = [s.sample_shape for s in self.samplers] + if not all(sample_shapes[0] == ss for ss in sample_shapes): + raise UnsupportedError( + "ListSampler requires all samplers to have the same sample shape." + ) + + @property + def sample_shape(self) -> torch.Size: + r"""The sample shape of the underlying samplers.""" + self._validate_samplers() + return self.samplers[0].sample_shape + +
+[docs] + def forward(self, posterior: PosteriorList) -> Tensor: + r"""Samples from the posteriors and concatenates the samples. + + Args: + posterior: A `PosteriorList` to sample from. + + Returns: + The samples drawn from the posterior. + """ + samples_list = [ + s(posterior=p) for s, p in zip(self.samplers, posterior.posteriors) + ] + return posterior._reshape_and_cat(tensors=samples_list)
+ + + def _update_base_samples( + self, posterior: PosteriorList, base_sampler: ListSampler + ) -> None: + r"""Update the sampler to use the original base samples for X_baseline. + + This is used in CachedCholeskyAcquisitionFunctions to ensure consistency. + + Args: + posterior: The posterior for which the base samples are constructed. + base_sampler: The base sampler to retrieve the base samples from. + """ + self._instance_check(base_sampler=base_sampler) + for s, p, bs in zip(self.samplers, posterior.posteriors, base_sampler.samplers): + s._update_base_samples(posterior=p, base_sampler=bs)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/normal.html b/website-old/pages/api/_modules/botorch/sampling/normal.html new file mode 100644 index 0000000000..2645fcee9b --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/normal.html @@ -0,0 +1,283 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.normal

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Sampler modules producing N(0,1) samples, to be used with MC-evaluated
+acquisition functions and Gaussian posteriors.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.exceptions import UnsupportedError
+from botorch.posteriors import Posterior
+from botorch.posteriors.higher_order import HigherOrderGPPosterior
+from botorch.posteriors.multitask import MultitaskGPPosterior
+from botorch.posteriors.transformed import TransformedPosterior
+from botorch.sampling.base import MCSampler
+from botorch.utils.sampling import draw_sobol_normal_samples, manual_seed
+from torch import Tensor
+from torch.quasirandom import SobolEngine
+
+
+
+[docs] +class NormalMCSampler(MCSampler, ABC): + r"""Base class for samplers producing (possibly QMC) N(0,1) samples. + + Subclasses must implement the `_construct_base_samples` method. + """ + +
+[docs] + def forward(self, posterior: Posterior) -> Tensor: + r"""Draws MC samples from the posterior. + + Args: + posterior: The posterior to sample from. + + Returns: + The samples drawn from the posterior. + """ + self._construct_base_samples(posterior=posterior) + samples = posterior.rsample_from_base_samples( + sample_shape=self.sample_shape, + base_samples=self.base_samples.expand( + self._get_extended_base_sample_shape(posterior=posterior) + ), + ) + return samples
+ + + @abstractmethod + def _construct_base_samples(self, posterior: Posterior) -> None: + r"""Generate base samples (if necessary). + + This function will generate a new set of base samples and register the + `base_samples` buffer if one of the following is true: + + - the MCSampler has no `base_samples` attribute. + - the output of `_get_collapsed_shape` does not agree with the shape of + `self.base_samples`. + + Args: + posterior: The Posterior for which to generate base samples. + """ + pass # pragma: no cover + + def _update_base_samples( + self, posterior: Posterior, base_sampler: MCSampler + ) -> None: + r"""Update the sampler to use the original base samples for X_baseline. + + This is used in CachedCholeskyAcquisitionFunctions to ensure consistency. + + Args: + posterior: The posterior for which the base samples are constructed. + base_sampler: The base sampler to retrieve the base samples from. + """ + self._instance_check(base_sampler=base_sampler) + self._construct_base_samples(posterior=posterior) + if base_sampler.base_samples is not None: + current_base_samples = base_sampler.base_samples.detach().clone() + # This is the # of non-`sample_shape` dimensions. + base_ndims = current_base_samples.dim() - 1 + # Unsqueeze as many dimensions as needed to match target_shape. + target_shape = self._get_collapsed_shape(posterior=posterior) + view_shape = ( + self.sample_shape + + torch.Size([1] * (len(target_shape) - current_base_samples.dim())) + + current_base_samples.shape[-base_ndims:] + ) + expanded_shape = ( + target_shape[:-base_ndims] + current_base_samples.shape[-base_ndims:] + ) + # Use stored base samples: + # Use all base_samples from the current sampler + # this includes the base_samples from the base_sampler + # and any base_samples for the new points in the sampler. + # For example, when using sequential greedy candidate generation + # then generate the new candidate point using last (-1) base_sample + # in sampler. This copies that base sample. + expanded_samples = current_base_samples.view(view_shape).expand( + expanded_shape + ) + non_transformed_posterior = ( + posterior._posterior + if isinstance(posterior, TransformedPosterior) + else posterior + ) + if isinstance( + non_transformed_posterior, + (HigherOrderGPPosterior, MultitaskGPPosterior), + ): + n_train_samples = current_base_samples.shape[-1] // 2 + # The train base samples. + self.base_samples[..., :n_train_samples] = expanded_samples[ + ..., :n_train_samples + ] + # The train noise base samples. + self.base_samples[..., -n_train_samples:] = expanded_samples[ + ..., -n_train_samples: + ] + else: + batch_shape = non_transformed_posterior.batch_shape + single_output = ( + len(posterior.base_sample_shape) - len(batch_shape) + ) == 1 + if single_output: + self.base_samples[..., : current_base_samples.shape[-1]] = ( + expanded_samples + ) + else: + self.base_samples[..., : current_base_samples.shape[-2], :] = ( + expanded_samples + )
+ + + +
+[docs] +class IIDNormalSampler(NormalMCSampler): + r"""Sampler for MC base samples using iid N(0,1) samples. + + Example: + >>> sampler = IIDNormalSampler(1000, seed=1234) + >>> posterior = model.posterior(test_X) + >>> samples = sampler(posterior) + """ + + def _construct_base_samples(self, posterior: Posterior) -> None: + r"""Generate iid `N(0,1)` base samples (if necessary). + + This function will generate a new set of base samples and set the + `base_samples` buffer if one of the following is true: + + - the MCSampler has no `base_samples` attribute. + - the output of `_get_collapsed_shape` does not agree with the shape of + `self.base_samples`. + + Args: + posterior: The Posterior for which to generate base samples. + """ + target_shape = self._get_collapsed_shape(posterior=posterior) + if self.base_samples is None or self.base_samples.shape != target_shape: + with manual_seed(seed=self.seed): + base_samples = torch.randn( + target_shape, device=posterior.device, dtype=posterior.dtype + ) + self.register_buffer("base_samples", base_samples) + if self.base_samples.device != posterior.device: + self.to(device=posterior.device) # pragma: nocover + if self.base_samples.dtype != posterior.dtype: + self.to(dtype=posterior.dtype)
+ + + +
+[docs] +class SobolQMCNormalSampler(NormalMCSampler): + r"""Sampler for quasi-MC N(0,1) base samples using Sobol sequences. + + Example: + >>> sampler = SobolQMCNormalSampler(torch.Size([1024]), seed=1234) + >>> posterior = model.posterior(test_X) + >>> samples = sampler(posterior) + """ + + def _construct_base_samples(self, posterior: Posterior) -> None: + r"""Generate quasi-random Normal base samples (if necessary). + + This function will generate a new set of base samples and set the + `base_samples` buffer if one of the following is true: + + - the MCSampler has no `base_samples` attribute. + - the output of `_get_collapsed_shape` does not agree with the shape of + `self.base_samples`. + + Args: + posterior: The Posterior for which to generate base samples. + """ + target_shape = self._get_collapsed_shape(posterior=posterior) + if self.base_samples is None or self.base_samples.shape != target_shape: + base_collapsed_shape = target_shape[len(self.sample_shape) :] + output_dim = base_collapsed_shape.numel() + if output_dim > SobolEngine.MAXDIM: + raise UnsupportedError( + "SobolQMCSampler only supports dimensions " + f"`q * o <= {SobolEngine.MAXDIM}`. Requested: {output_dim}" + ) + base_samples = draw_sobol_normal_samples( + d=output_dim, + n=self.sample_shape.numel(), + device=posterior.device, + dtype=posterior.dtype, + seed=self.seed, + ) + base_samples = base_samples.view(target_shape) + self.register_buffer("base_samples", base_samples) + self.to(device=posterior.device, dtype=posterior.dtype)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pairwise_samplers.html b/website-old/pages/api/_modules/botorch/sampling/pairwise_samplers.html new file mode 100644 index 0000000000..86defe240d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pairwise_samplers.html @@ -0,0 +1,193 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pairwise_samplers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from itertools import combinations
+from typing import Any
+
+import numpy as np
+import torch
+from botorch.posteriors.posterior import Posterior
+from botorch.sampling.base import MCSampler
+from botorch.sampling.normal import IIDNormalSampler, SobolQMCNormalSampler
+from torch import Tensor
+
+
+
+[docs] +class PairwiseMCSampler(MCSampler): + r""" + Abstract class for Pairwise MC Sampler. + + This sampler will sample pairwise comparisons. It is to be used together + with PairwiseGP and BoTorch acquisition functions (e.g., qKnowledgeGradient) + + """ + + def __init__(self, max_num_comparisons: int = None, seed: int = None) -> None: + r""" + Args: + max_num_comparisons: Max number of comparisons drawn within samples. + If None, use all possible pairwise comparisons + seed: The seed for np.random.seed. If omitted, use a random seed. + May be overwritten by sibling classes or subclasses. + """ + self.max_num_comparisons = max_num_comparisons + self.seed = seed if seed is not None else torch.randint(0, 1000000, (1,)).item() + +
+[docs] + def forward(self, posterior: Posterior) -> Tensor: + r"""Draws MC samples from the posterior and make comparisons + + Args: + posterior: The Posterior to sample from. + The returned samples are expected to have output dimension of 1. + + Returns: + Posterior sample pairwise comparisons. + """ + samples = super().forward(posterior) + np.random.seed(self.seed) + + s_n = samples.shape[-2] # candidate number per batch + if s_n < 2: + raise RuntimeError("Number of samples < 2, cannot make comparisons") + + # TODO: Don't instantiate a generator + all_pairs = np.array(list(combinations(range(s_n), 2))) + if self.max_num_comparisons is None: + comp_n = len(all_pairs) + else: + comp_n = min(self.max_num_comparisons, len(all_pairs)) + + comp_pairs = all_pairs[ + np.random.choice(range(len(all_pairs)), comp_n, replace=False) + ] + s_comps_size = torch.Size((*samples.shape[:-2], comp_n, 2)) + s_v = samples.view(-1, s_n) + + idx1, idx2 = comp_pairs[:, 0], comp_pairs[:, 1] + prefs = (s_v[:, idx1] > s_v[:, idx2]).long().cpu() + cpt = comp_pairs.T + c1 = np.choose(prefs, cpt) + c2 = np.choose(1 - prefs, cpt) + s_comps = torch.stack([c1, c2], dim=-1).reshape(s_comps_size) + + return s_comps
+
+ + + +
+[docs] +class PairwiseIIDNormalSampler(PairwiseMCSampler, IIDNormalSampler): + def __init__( + self, + sample_shape: torch.Size, + seed: int | None = None, + max_num_comparisons: int = None, + **kwargs: Any, + ) -> None: + r""" + Args: + sample_shape: The `sample_shape` of the samples to generate. + seed: The seed for the RNG. If omitted, use a random seed. + max_num_comparisons: Max number of comparisons drawn within samples. + If None, use all possible pairwise comparisons. + kwargs: Catch-all for deprecated arguments. + """ + PairwiseMCSampler.__init__( + self, max_num_comparisons=max_num_comparisons, seed=seed + ) + IIDNormalSampler.__init__(self, sample_shape=sample_shape, seed=seed, **kwargs)
+ + + +
+[docs] +class PairwiseSobolQMCNormalSampler(PairwiseMCSampler, SobolQMCNormalSampler): + def __init__( + self, + sample_shape: torch.Size, + seed: int | None = None, + max_num_comparisons: int = None, + **kwargs: Any, + ) -> None: + r""" + Args: + sample_shape: The `sample_shape` of the samples to generate. + seed: The seed for the RNG. If omitted, use a random seed. + max_num_comparisons: Max number of comparisons drawn within samples. + If None, use all possible pairwise comparisons. + kwargs: Catch-all for deprecated arguments. + """ + PairwiseMCSampler.__init__( + self, max_num_comparisons=max_num_comparisons, seed=seed + ) + SobolQMCNormalSampler.__init__( + self, sample_shape=sample_shape, seed=seed, **kwargs + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pathwise/features/generators.html b/website-old/pages/api/_modules/botorch/sampling/pathwise/features/generators.html new file mode 100644 index 0000000000..93d035bbf2 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pathwise/features/generators.html @@ -0,0 +1,256 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pathwise.features.generators

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+.. [rahimi2007random]
+    A. Rahimi and B. Recht. Random features for large-scale kernel machines.
+    Advances in Neural Information Processing Systems 20 (2007).
+
+.. [sutherland2015error]
+    D. J. Sutherland and J. Schneider. On the error of random Fourier features.
+    arXiv preprint arXiv:1506.02785 (2015).
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from typing import Any
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.sampling.pathwise.features.maps import KernelFeatureMap
+from botorch.sampling.pathwise.utils import (
+    ChainedTransform,
+    FeatureSelector,
+    InverseLengthscaleTransform,
+    OutputscaleTransform,
+    SineCosineTransform,
+)
+from botorch.utils.dispatcher import Dispatcher
+from botorch.utils.sampling import draw_sobol_normal_samples
+from gpytorch import kernels
+from gpytorch.kernels.kernel import Kernel
+from torch import Size, Tensor
+from torch.distributions import Gamma
+
+TKernelFeatureMapGenerator = Callable[[Kernel, int, int], KernelFeatureMap]
+GenKernelFeatures = Dispatcher("gen_kernel_features")
+
+
+
+[docs] +def gen_kernel_features( + kernel: kernels.Kernel, + num_inputs: int, + num_outputs: int, + **kwargs: Any, +) -> KernelFeatureMap: + r"""Generates a feature map :math:`\phi: \mathcal{X} \to \mathbb{R}^{n}` such that + :math:`k(x, x') ≈ \phi(x)^{T} \phi(x')`. For stationary kernels :math:`k`, defaults + to the method of random Fourier features. For more details, see [rahimi2007random]_ + and [sutherland2015error]_. + + Args: + kernel: The kernel :math:`k` to be represented via a finite-dim basis. + num_inputs: The number of input features. + num_outputs: The number of kernel features. + """ + return GenKernelFeatures( + kernel, + num_inputs=num_inputs, + num_outputs=num_outputs, + **kwargs, + )
+ + + +def _gen_fourier_features( + kernel: kernels.Kernel, + weight_generator: Callable[[Size], Tensor], + num_inputs: int, + num_outputs: int, +) -> KernelFeatureMap: + r"""Generate a feature map :math:`\phi: \mathcal{X} \to \mathbb{R}^{2l}` that + approximates a stationary kernel so that :math:`k(x, x') ≈ \phi(x)^\top \phi(x')`. + + Following [sutherland2015error]_, we represent complex exponentials by pairs of + basis functions :math:`\phi_{i}(x) = \sin(x^\top w_{i})` and + :math:`\phi_{i + l} = \cos(x^\top w_{i}). + + Args: + kernel: A stationary kernel :math:`k(x, x') = k(x - x')`. + weight_generator: A callable used to generate weight vectors :math:`w`. + num_inputs: The number of input features. + num_outputs: The number of Fourier features. + """ + if num_outputs % 2: + raise UnsupportedError( + f"Expected an even number of output features, but received {num_outputs=}." + ) + + input_transform = InverseLengthscaleTransform(kernel) + if kernel.active_dims is not None: + num_inputs = len(kernel.active_dims) + input_transform = ChainedTransform( + input_transform, FeatureSelector(indices=kernel.active_dims) + ) + + weight = weight_generator( + Size([kernel.batch_shape.numel() * num_outputs // 2, num_inputs]) + ).reshape(*kernel.batch_shape, num_outputs // 2, num_inputs) + + output_transform = SineCosineTransform( + torch.tensor((2 / num_outputs) ** 0.5, device=kernel.device, dtype=kernel.dtype) + ) + return KernelFeatureMap( + kernel=kernel, + weight=weight, + input_transform=input_transform, + output_transform=output_transform, + ) + + +@GenKernelFeatures.register(kernels.RBFKernel) +def _gen_kernel_features_rbf( + kernel: kernels.RBFKernel, + *, + num_inputs: int, + num_outputs: int, +) -> KernelFeatureMap: + def _weight_generator(shape: Size) -> Tensor: + try: + n, d = shape + except ValueError: + raise UnsupportedError( + f"Expected `shape` to be 2-dimensional, but {len(shape)=}." + ) + + return draw_sobol_normal_samples( + n=n, + d=d, + device=kernel.lengthscale.device, + dtype=kernel.lengthscale.dtype, + ) + + return _gen_fourier_features( + kernel=kernel, + weight_generator=_weight_generator, + num_inputs=num_inputs, + num_outputs=num_outputs, + ) + + +@GenKernelFeatures.register(kernels.MaternKernel) +def _gen_kernel_features_matern( + kernel: kernels.MaternKernel, + *, + num_inputs: int, + num_outputs: int, +) -> KernelFeatureMap: + def _weight_generator(shape: Size) -> Tensor: + try: + n, d = shape + except ValueError: + raise UnsupportedError( + f"Expected `shape` to be 2-dimensional, but {len(shape)=}." + ) + + dtype = kernel.lengthscale.dtype + device = kernel.lengthscale.device + nu = torch.tensor(kernel.nu, device=device, dtype=dtype) + normals = draw_sobol_normal_samples(n=n, d=d, device=device, dtype=dtype) + return Gamma(nu, nu).rsample((n, 1)).rsqrt() * normals + + return _gen_fourier_features( + kernel=kernel, + weight_generator=_weight_generator, + num_inputs=num_inputs, + num_outputs=num_outputs, + ) + + +@GenKernelFeatures.register(kernels.ScaleKernel) +def _gen_kernel_features_scale( + kernel: kernels.ScaleKernel, + *, + num_inputs: int, + num_outputs: int, +) -> KernelFeatureMap: + active_dims = kernel.active_dims + feature_map = gen_kernel_features( + kernel.base_kernel, + num_inputs=num_inputs if active_dims is None else len(active_dims), + num_outputs=num_outputs, + ) + + if active_dims is not None and active_dims is not kernel.base_kernel.active_dims: + feature_map.input_transform = ChainedTransform( + feature_map.input_transform, FeatureSelector(indices=active_dims) + ) + + feature_map.output_transform = ChainedTransform( + OutputscaleTransform(kernel), feature_map.output_transform + ) + return feature_map +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pathwise/features/maps.html b/website-old/pages/api/_modules/botorch/sampling/pathwise/features/maps.html new file mode 100644 index 0000000000..1e50478aa6 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pathwise/features/maps.html @@ -0,0 +1,206 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pathwise.features.maps

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import torch
+from botorch.sampling.pathwise.utils import (
+    TInputTransform,
+    TOutputTransform,
+    TransformedModuleMixin,
+)
+from gpytorch.kernels import Kernel
+from linear_operator.operators import LinearOperator
+from torch import Size, Tensor
+from torch.nn import Module
+
+
+
+[docs] +class FeatureMap(TransformedModuleMixin, Module): + num_outputs: int + batch_shape: Size + input_transform: TInputTransform | None + output_transform: TOutputTransform | None
+ + + +
+[docs] +class KernelEvaluationMap(FeatureMap): + r"""A feature map defined by centering a kernel at a set of points.""" + + def __init__( + self, + kernel: Kernel, + points: Tensor, + input_transform: TInputTransform | None = None, + output_transform: TOutputTransform | None = None, + ) -> None: + r"""Initializes a KernelEvaluationMap instance: + + .. code-block:: text + + feature_map(x) = output_transform(kernel(input_transform(x), points)). + + Args: + kernel: The kernel :math:`k` used to define the feature map. + points: A tensor passed as the kernel's second argument. + input_transform: An optional input transform for the module. + output_transform: An optional output transform for the module. + """ + try: + torch.broadcast_shapes(points.shape[:-2], kernel.batch_shape) + except RuntimeError: + raise RuntimeError( + f"Shape mismatch: {points.shape=}, but {kernel.batch_shape=}." + ) + + super().__init__() + self.kernel = kernel + self.points = points + self.input_transform = input_transform + self.output_transform = output_transform + +
+[docs] + def forward(self, x: Tensor) -> Tensor | LinearOperator: + return self.kernel(x, self.points)
+ + + @property + def num_outputs(self) -> int: + if self.output_transform is None: + return self.points.shape[-1] + + canary = torch.empty( + 1, self.points.shape[-1], device=self.points.device, dtype=self.points.dtype + ) + return self.output_transform(canary).shape[-1] + + @property + def batch_shape(self) -> Size: + return self.kernel.batch_shape
+ + + +
+[docs] +class KernelFeatureMap(FeatureMap): + r"""Representation of a kernel :math:`k: \mathcal{X}^2 \to \mathbb{R}` as an + n-dimensional feature map :math:`\phi: \mathcal{X} \to \mathbb{R}^n` satisfying: + :math:`k(x, x') ≈ \phi(x)^\top \phi(x')`. + """ + + def __init__( + self, + kernel: Kernel, + weight: Tensor, + bias: Tensor | None = None, + input_transform: TInputTransform | None = None, + output_transform: TOutputTransform | None = None, + ) -> None: + r"""Initializes a KernelFeatureMap instance: + + .. code-block:: text + + feature_map(x) = output_transform(input_transform(x)^{T} weight + bias). + + Args: + kernel: The kernel :math:`k` used to define the feature map. + weight: A tensor of weights used to linearly combine the module's inputs. + bias: A tensor of biases to be added to the linearly combined inputs. + input_transform: An optional input transform for the module. + output_transform: An optional output transform for the module. + """ + super().__init__() + self.kernel = kernel + self.register_buffer("weight", weight) + self.register_buffer("bias", bias) + self.weight = weight + self.bias = bias + self.input_transform = input_transform + self.output_transform = output_transform + +
+[docs] + def forward(self, x: Tensor) -> Tensor: + out = x @ self.weight.transpose(-2, -1) + return out if self.bias is None else out + self.bias
+ + + @property + def num_outputs(self) -> int: + if self.output_transform is None: + return self.weight.shape[-2] + + canary = torch.empty( + self.weight.shape[-2], device=self.weight.device, dtype=self.weight.dtype + ) + return self.output_transform(canary).shape[-1] + + @property + def batch_shape(self) -> Size: + return self.kernel.batch_shape
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pathwise/paths.html b/website-old/pages/api/_modules/botorch/sampling/pathwise/paths.html new file mode 100644 index 0000000000..b55f75d7da --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pathwise/paths.html @@ -0,0 +1,265 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pathwise.paths

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from abc import ABC
+from collections.abc import Callable, Iterable, Iterator, Mapping
+from typing import Any
+
+from botorch.exceptions.errors import UnsupportedError
+from botorch.sampling.pathwise.features import FeatureMap
+from botorch.sampling.pathwise.utils import (
+    TInputTransform,
+    TOutputTransform,
+    TransformedModuleMixin,
+)
+from torch import Tensor
+from torch.nn import Module, ModuleDict, ModuleList, Parameter
+
+
+
+[docs] +class SamplePath(ABC, TransformedModuleMixin, Module): + r"""Abstract base class for Botorch sample paths."""
+ + + +
+[docs] +class PathDict(SamplePath): + r"""A dictionary of SamplePaths.""" + + def __init__( + self, + paths: Mapping[str, SamplePath] | None = None, + join: Callable[[list[Tensor]], Tensor] | None = None, + input_transform: TInputTransform | None = None, + output_transform: TOutputTransform | None = None, + ) -> None: + r"""Initializes a PathDict instance. + + Args: + paths: An optional mapping of strings to sample paths. + join: An optional callable used to combine each path's outputs. + input_transform: An optional input transform for the module. + output_transform: An optional output transform for the module. + """ + if join is None and output_transform is not None: + raise UnsupportedError("Output transforms must be preceded by a join rule.") + + super().__init__() + self.join = join + self.input_transform = input_transform + self.output_transform = output_transform + self.paths = ( + paths + if isinstance(paths, ModuleDict) + else ModuleDict({} if paths is None else paths) + ) + +
+[docs] + def forward(self, x: Tensor, **kwargs: Any) -> Tensor | dict[str, Tensor]: + out = [path(x, **kwargs) for path in self.paths.values()] + return dict(zip(self.paths, out)) if self.join is None else self.join(out)
+ + +
+[docs] + def items(self) -> Iterable[tuple[str, SamplePath]]: + return self.paths.items()
+ + +
+[docs] + def keys(self) -> Iterable[str]: + return self.paths.keys()
+ + +
+[docs] + def values(self) -> Iterable[SamplePath]: + return self.paths.values()
+ + + def __len__(self) -> int: + return len(self.paths) + + def __iter__(self) -> Iterator[SamplePath]: + yield from self.paths + + def __delitem__(self, key: str) -> None: + del self.paths[key] + + def __getitem__(self, key: str) -> SamplePath: + return self.paths[key] + + def __setitem__(self, key: str, val: SamplePath) -> None: + self.paths[key] = val
+ + + +
+[docs] +class PathList(SamplePath): + r"""A list of SamplePaths.""" + + def __init__( + self, + paths: Iterable[SamplePath] | None = None, + join: Callable[[list[Tensor]], Tensor] | None = None, + input_transform: TInputTransform | None = None, + output_transform: TOutputTransform | None = None, + ) -> None: + r"""Initializes a PathList instance. + + Args: + paths: An optional iterable of sample paths. + join: An optional callable used to combine each path's outputs. + input_transform: An optional input transform for the module. + output_transform: An optional output transform for the module. + """ + + if join is None and output_transform is not None: + raise UnsupportedError("Output transforms must be preceded by a join rule.") + + super().__init__() + self.join = join + self.input_transform = input_transform + self.output_transform = output_transform + self.paths = ( + paths + if isinstance(paths, ModuleList) + else ModuleList({} if paths is None else paths) + ) + +
+[docs] + def forward(self, x: Tensor, **kwargs: Any) -> Tensor | list[Tensor]: + out = [path(x, **kwargs) for path in self.paths] + return out if self.join is None else self.join(out)
+ + + def __len__(self) -> int: + return len(self.paths) + + def __iter__(self) -> Iterator[SamplePath]: + yield from self.paths + + def __delitem__(self, key: int) -> None: + del self.paths[key] + + def __getitem__(self, key: int) -> SamplePath: + return self.paths[key] + + def __setitem__(self, key: int, val: SamplePath) -> None: + self.paths[key] = val
+ + + +
+[docs] +class GeneralizedLinearPath(SamplePath): + r"""A sample path in the form of a generalized linear model.""" + + def __init__( + self, + feature_map: FeatureMap, + weight: Parameter | Tensor, + bias_module: Module | None = None, + input_transform: TInputTransform | None = None, + output_transform: TOutputTransform | None = None, + ): + r"""Initializes a GeneralizedLinearPath instance. + + .. code-block:: text + + path(x) = output_transform(bias_module(z) + feature_map(z)^T weight), + where z = input_transform(x). + + Args: + feature_map: A map used to featurize the module's inputs. + weight: A tensor of weights used to combine input features. + bias_module: An optional module used to define additive offsets. + input_transform: An optional input transform for the module. + output_transform: An optional output transform for the module. + """ + super().__init__() + self.feature_map = feature_map + if not isinstance(weight, Parameter): + self.register_buffer("weight", weight) + self.weight = weight + self.bias_module = bias_module + self.input_transform = input_transform + self.output_transform = output_transform + +
+[docs] + def forward(self, x: Tensor, **kwargs) -> Tensor: + feat = self.feature_map(x, **kwargs) + out = (feat @ self.weight.unsqueeze(-1)).squeeze(-1) + return out if self.bias_module is None else out + self.bias_module(x)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pathwise/posterior_samplers.html b/website-old/pages/api/_modules/botorch/sampling/pathwise/posterior_samplers.html new file mode 100644 index 0000000000..1fb622b588 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pathwise/posterior_samplers.html @@ -0,0 +1,318 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pathwise.posterior_samplers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+.. [wilson2020sampling]
+    J. Wilson, V. Borovitskiy, A. Terenin, P. Mostowsky, and M. Deisenroth. Efficiently
+    sampling functions from Gaussian process posteriors. International Conference on
+    Machine Learning (2020).
+
+.. [wilson2021pathwise]
+    J. Wilson, V. Borovitskiy, A. Terenin, P. Mostowsky, and M. Deisenroth. Pathwise
+    Conditioning of Gaussian Processes. Journal of Machine Learning Research (2021).
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models.approximate_gp import ApproximateGPyTorchModel
+from botorch.models.deterministic import GenericDeterministicModel
+from botorch.models.model import ModelList
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.sampling.pathwise.paths import PathDict, PathList, SamplePath
+from botorch.sampling.pathwise.prior_samplers import (
+    draw_kernel_feature_paths,
+    TPathwisePriorSampler,
+)
+from botorch.sampling.pathwise.update_strategies import gaussian_update, TPathwiseUpdate
+from botorch.sampling.pathwise.utils import (
+    get_output_transform,
+    get_train_inputs,
+    get_train_targets,
+    TInputTransform,
+    TOutputTransform,
+)
+from botorch.utils.context_managers import delattr_ctx
+from botorch.utils.dispatcher import Dispatcher
+from botorch.utils.transforms import is_ensemble
+from gpytorch.models import ApproximateGP, ExactGP, GP
+from torch import Size, Tensor
+
+DrawMatheronPaths = Dispatcher("draw_matheron_paths")
+
+
+
+[docs] +class MatheronPath(PathDict): + r"""Represents function draws from a GP posterior via Matheron's rule: + + .. code-block:: text + + "Prior path" + v + (f | y)(·) = f(·) + Cov(f(·), y) Cov(y, y)^{-1} (y - f(X) - ε), + \_______________________________________/ + v + "Update path" + + where `=` denotes equality in distribution, :math:`f \sim GP(0, k)`, + :math:`y \sim N(f(X), \Sigma)`, and :math:`\epsilon \sim N(0, \Sigma)`. + For more information, see [wilson2020sampling]_ and [wilson2021pathwise]_. + """ + + def __init__( + self, + prior_paths: SamplePath, + update_paths: SamplePath, + input_transform: TInputTransform | None = None, + output_transform: TOutputTransform | None = None, + ) -> None: + r"""Initializes a MatheronPath instance. + + Args: + prior_paths: Sample paths used to represent the prior. + update_paths: Sample paths used to represent the data. + input_transform: An optional input transform for the module. + output_transform: An optional output transform for the module. + """ + + super().__init__( + join=sum, + paths={"prior_paths": prior_paths, "update_paths": update_paths}, + input_transform=input_transform, + output_transform=output_transform, + )
+ + + +
+[docs] +def get_matheron_path_model( + model: GP, sample_shape: Size | None = None +) -> GenericDeterministicModel: + r"""Generates a deterministic model using a single Matheron path drawn + from the model's posterior. + + The deterministic model evalutes the output of `draw_matheron_paths`, + and reshapes it to mimic the output behavior of the model's posterior. + + Args: + model: The model whose posterior is to be sampled. + sample_shape: The shape of the sample paths to be drawn, if an ensemble + of sample paths is desired. If this is specified, the resulting + deterministic model will behave as if the `sample_shape` is prepended + to the `batch_shape` of the model. The inputs used to evaluate the model + must be adjusted to match. + + Returns: + A deterministic model that evaluates the Matheron path. + """ + sample_shape = Size() if sample_shape is None else sample_shape + path = draw_matheron_paths(model, sample_shape=sample_shape) + num_outputs = model.num_outputs + if isinstance(model, ModelList) and len(model.models) != num_outputs: + raise UnsupportedError("A model-list of multi-output models is not supported.") + + def f(X: Tensor) -> Tensor: + r"""Reshapes the path evaluations to bring the output dimension to the end. + + Args: + X: The input tensor of shape `batch_shape x q x d`. + If the model is batched, `batch_shape` must be broadcastable to + the model batch shape. + + Returns: + The output tensor of shape `batch_shape x q x m`. + """ + if num_outputs == 1: + # For single-output, we lack the output dimension. Add one. + res = path(X).unsqueeze(-1) + elif isinstance(model, ModelList): + # For model list, path evaluates to a list of tensors. Stack them. + res = torch.stack(path(X), dim=-1) + else: + # For multi-output, path expects inputs broadcastable to + # `model._aug_batch_shape x q x d` and returns outputs of shape + # `model._aug_batch_shape x q`. Augmented batch shape includes the + # `m` dimension, so we will unsqueeze that and transpose after. + res = path(X.unsqueeze(-3)).transpose(-1, -2) + return res + + path_model = GenericDeterministicModel(f=f, num_outputs=num_outputs) + path_model._is_ensemble = is_ensemble(model) or len(sample_shape) > 0 + return path_model
+ + + +
+[docs] +def draw_matheron_paths( + model: GP, + sample_shape: Size, + prior_sampler: TPathwisePriorSampler = draw_kernel_feature_paths, + update_strategy: TPathwiseUpdate = gaussian_update, +) -> MatheronPath: + r"""Generates function draws from (an approximate) Gaussian process posterior. + + When evaluted, sample paths produced by this method return Tensors with dimensions + `sample_dims x batch_dims x [joint_dim]`, where `joint_dim` denotes the penultimate + dimension of the input tensor. For multioutput models, outputs are returned as the + final batch dimension. + + Args: + model: Gaussian process whose posterior is to be sampled. + sample_shape: Sizes of sample dimensions. + prior_sample: A callable that takes a model and a sample shape and returns + a set of sample paths representing the prior. + update_strategy: A callable that takes a model and a tensor of prior process + values and returns a set of sample paths representing the data. + """ + + return DrawMatheronPaths( + model, + sample_shape=sample_shape, + prior_sampler=prior_sampler, + update_strategy=update_strategy, + )
+ + + +@DrawMatheronPaths.register(ModelListGP) +def _draw_matheron_paths_ModelListGP( + model: ModelListGP, + sample_shape: Size, + *, + prior_sampler: TPathwisePriorSampler = draw_kernel_feature_paths, + update_strategy: TPathwiseUpdate = gaussian_update, +): + return PathList( + [ + draw_matheron_paths( + model=m, + sample_shape=sample_shape, + prior_sampler=prior_sampler, + update_strategy=update_strategy, + ) + for m in model.models + ] + ) + + +@DrawMatheronPaths.register(ExactGP) +def _draw_matheron_paths_ExactGP( + model: ExactGP, + *, + sample_shape: Size, + prior_sampler: TPathwisePriorSampler, + update_strategy: TPathwiseUpdate, +) -> MatheronPath: + (train_X,) = get_train_inputs(model, transformed=True) + train_Y = get_train_targets(model, transformed=True) + with delattr_ctx(model, "outcome_transform"): + # Generate draws from the prior + prior_paths = prior_sampler(model=model, sample_shape=sample_shape) + sample_values = prior_paths.forward(train_X) + + # Compute pathwise updates + update_paths = update_strategy( + model=model, + sample_values=sample_values, + target_values=train_Y, + ) + + return MatheronPath( + prior_paths=prior_paths, + update_paths=update_paths, + output_transform=get_output_transform(model), + ) + + +@DrawMatheronPaths.register((ApproximateGP, ApproximateGPyTorchModel)) +def _draw_matheron_paths_ApproximateGP( + model: ApproximateGP | ApproximateGPyTorchModel, + *, + sample_shape: Size, + prior_sampler: TPathwisePriorSampler, + update_strategy: TPathwiseUpdate, +) -> MatheronPath: + # Note: Inducing points are assumed to be pre-transformed + Z = ( + model.model.variational_strategy.inducing_points + if isinstance(model, ApproximateGPyTorchModel) + else model.variational_strategy.inducing_points + ) + with delattr_ctx(model, "outcome_transform"): + # Generate draws from the prior + prior_paths = prior_sampler(model=model, sample_shape=sample_shape) + sample_values = prior_paths.forward(Z) # `forward` bypasses transforms + + # Compute pathwise updates + update_paths = update_strategy(model=model, sample_values=sample_values) + + return MatheronPath( + prior_paths=prior_paths, + update_paths=update_paths, + output_transform=get_output_transform(model), + ) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pathwise/prior_samplers.html b/website-old/pages/api/_modules/botorch/sampling/pathwise/prior_samplers.html new file mode 100644 index 0000000000..da6e282e0c --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pathwise/prior_samplers.html @@ -0,0 +1,215 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pathwise.prior_samplers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from typing import Any
+
+from botorch.models.approximate_gp import ApproximateGPyTorchModel
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.sampling.pathwise.features import gen_kernel_features
+from botorch.sampling.pathwise.features.generators import TKernelFeatureMapGenerator
+from botorch.sampling.pathwise.paths import GeneralizedLinearPath, PathList, SamplePath
+from botorch.sampling.pathwise.utils import (
+    get_input_transform,
+    get_output_transform,
+    get_train_inputs,
+    TInputTransform,
+    TOutputTransform,
+)
+from botorch.utils.dispatcher import Dispatcher
+from botorch.utils.sampling import draw_sobol_normal_samples
+from gpytorch.kernels import Kernel
+from gpytorch.models import ApproximateGP, ExactGP, GP
+from gpytorch.variational import _VariationalStrategy
+from torch import Size, Tensor
+from torch.nn import Module
+
+TPathwisePriorSampler = Callable[[GP, Size], SamplePath]
+DrawKernelFeaturePaths = Dispatcher("draw_kernel_feature_paths")
+
+
+
+[docs] +def draw_kernel_feature_paths( + model: GP, sample_shape: Size, **kwargs: Any +) -> GeneralizedLinearPath: + r"""Draws functions from a Bayesian-linear-model-based approximation to a GP prior. + + When evaluted, sample paths produced by this method return Tensors with dimensions + `sample_dims x batch_dims x [joint_dim]`, where `joint_dim` denotes the penultimate + dimension of the input tensor. For multioutput models, outputs are returned as the + final batch dimension. + + Args: + model: The prior over functions. + sample_shape: The shape of the sample paths to be drawn. + """ + return DrawKernelFeaturePaths(model, sample_shape=sample_shape, **kwargs)
+ + + +def _draw_kernel_feature_paths_fallback( + num_inputs: int, + mean_module: Module | None, + covar_module: Kernel, + sample_shape: Size, + num_features: int = 1024, + map_generator: TKernelFeatureMapGenerator = gen_kernel_features, + input_transform: TInputTransform | None = None, + output_transform: TOutputTransform | None = None, + weight_generator: Callable[[Size], Tensor] | None = None, +) -> GeneralizedLinearPath: + # Generate a kernel feature map + feature_map = map_generator( + kernel=covar_module, + num_inputs=num_inputs, + num_outputs=num_features, + ) + + # Sample random weights with which to combine kernel features + if weight_generator is None: + weight = draw_sobol_normal_samples( + n=sample_shape.numel() * covar_module.batch_shape.numel(), + d=feature_map.num_outputs, + device=covar_module.device, + dtype=covar_module.dtype, + ).reshape(sample_shape + covar_module.batch_shape + (feature_map.num_outputs,)) + else: + weight = weight_generator( + sample_shape + covar_module.batch_shape + (feature_map.num_outputs,) + ).to(device=covar_module.device, dtype=covar_module.dtype) + + # Return the sample paths + return GeneralizedLinearPath( + feature_map=feature_map, + weight=weight, + bias_module=mean_module, + input_transform=input_transform, + output_transform=output_transform, + ) + + +@DrawKernelFeaturePaths.register(ExactGP) +def _draw_kernel_feature_paths_ExactGP( + model: ExactGP, **kwargs: Any +) -> GeneralizedLinearPath: + (train_X,) = get_train_inputs(model, transformed=False) + return _draw_kernel_feature_paths_fallback( + num_inputs=train_X.shape[-1], + mean_module=model.mean_module, + covar_module=model.covar_module, + input_transform=get_input_transform(model), + output_transform=get_output_transform(model), + **kwargs, + ) + + +@DrawKernelFeaturePaths.register(ModelListGP) +def _draw_kernel_feature_paths_list( + model: ModelListGP, + join: Callable[[list[Tensor]], Tensor] | None = None, + **kwargs: Any, +) -> PathList: + paths = [draw_kernel_feature_paths(m, **kwargs) for m in model.models] + return PathList(paths=paths, join=join) + + +@DrawKernelFeaturePaths.register(ApproximateGPyTorchModel) +def _draw_kernel_feature_paths_ApproximateGPyTorchModel( + model: ApproximateGPyTorchModel, **kwargs: Any +) -> GeneralizedLinearPath: + (train_X,) = get_train_inputs(model, transformed=False) + return DrawKernelFeaturePaths( + model.model, + num_inputs=train_X.shape[-1], + input_transform=get_input_transform(model), + output_transform=get_output_transform(model), + **kwargs, + ) + + +@DrawKernelFeaturePaths.register(ApproximateGP) +def _draw_kernel_feature_paths_ApproximateGP( + model: ApproximateGP, **kwargs: Any +) -> GeneralizedLinearPath: + return DrawKernelFeaturePaths(model, model.variational_strategy, **kwargs) + + +@DrawKernelFeaturePaths.register(ApproximateGP, _VariationalStrategy) +def _draw_kernel_feature_paths_ApproximateGP_fallback( + model: ApproximateGP, + _: _VariationalStrategy, + *, + num_inputs: int, + **kwargs: Any, +) -> GeneralizedLinearPath: + return _draw_kernel_feature_paths_fallback( + num_inputs=num_inputs, + mean_module=model.mean_module, + covar_module=model.covar_module, + **kwargs, + ) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pathwise/update_strategies.html b/website-old/pages/api/_modules/botorch/sampling/pathwise/update_strategies.html new file mode 100644 index 0000000000..1f7c016475 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pathwise/update_strategies.html @@ -0,0 +1,258 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pathwise.update_strategies

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from types import NoneType
+
+from typing import Any
+
+import torch
+from botorch.models.approximate_gp import ApproximateGPyTorchModel
+from botorch.models.transforms.input import InputTransform
+from botorch.sampling.pathwise.features import KernelEvaluationMap
+from botorch.sampling.pathwise.paths import GeneralizedLinearPath, SamplePath
+from botorch.sampling.pathwise.utils import (
+    get_input_transform,
+    get_train_inputs,
+    get_train_targets,
+    TInputTransform,
+)
+from botorch.utils.dispatcher import Dispatcher
+from botorch.utils.types import DEFAULT
+from gpytorch.kernels.kernel import Kernel
+from gpytorch.likelihoods import _GaussianLikelihoodBase, Likelihood
+from gpytorch.models import ApproximateGP, ExactGP, GP
+from gpytorch.variational import VariationalStrategy
+from linear_operator.operators import (
+    LinearOperator,
+    SumLinearOperator,
+    ZeroLinearOperator,
+)
+from torch import Tensor
+
+TPathwiseUpdate = Callable[[GP, Tensor], SamplePath]
+GaussianUpdate = Dispatcher("gaussian_update")
+
+
+
+[docs] +def gaussian_update( + model: GP, + sample_values: Tensor, + likelihood: Likelihood | None = DEFAULT, + **kwargs: Any, +) -> GeneralizedLinearPath: + r"""Computes a Gaussian pathwise update in exact arithmetic: + + .. code-block:: text + + (f | y)(·) = f(·) + Cov(f(·), y) Cov(y, y)^{-1} (y - f(X) - ε), + \_______________________________________/ + V + "Gaussian pathwise update" + + where `=` denotes equality in distribution, :math:`f \sim GP(0, k)`, + :math:`y \sim N(f(X), \Sigma)`, and :math:`\epsilon \sim N(0, \Sigma)`. + For more information, see [wilson2020sampling]_ and [wilson2021pathwise]_. + + Args: + model: A Gaussian process prior together with a likelihood. + sample_values: Assumed values for :math:`f(X)`. + likelihood: An optional likelihood used to help define the desired + update. Defaults to `model.likelihood` if it exists else None. + """ + if likelihood is DEFAULT: + likelihood = getattr(model, "likelihood", None) + + return GaussianUpdate(model, likelihood, sample_values=sample_values, **kwargs)
+ + + +def _gaussian_update_exact( + kernel: Kernel, + points: Tensor, + target_values: Tensor, + sample_values: Tensor, + noise_covariance: Tensor | LinearOperator | None = None, + scale_tril: Tensor | LinearOperator | None = None, + input_transform: TInputTransform | None = None, +) -> GeneralizedLinearPath: + # Prepare Cholesky factor of `Cov(y, y)` and noise sample values as needed + if isinstance(noise_covariance, (NoneType, ZeroLinearOperator)): + scale_tril = kernel(points).cholesky() if scale_tril is None else scale_tril + else: + noise_values = torch.randn_like(sample_values).unsqueeze(-1) + noise_values = noise_covariance.cholesky() @ noise_values + sample_values = sample_values + noise_values.squeeze(-1) + scale_tril = ( + SumLinearOperator(kernel(points), noise_covariance).cholesky() + if scale_tril is None + else scale_tril + ) + + # Solve for `Cov(y, y)^{-1}(Y - f(X) - ε)` + errors = target_values - sample_values + weight = torch.cholesky_solve(errors.unsqueeze(-1), scale_tril.to_dense()) + + # Define update feature map and paths + feature_map = KernelEvaluationMap( + kernel=kernel, + points=points, + input_transform=input_transform, + ) + return GeneralizedLinearPath(feature_map=feature_map, weight=weight.squeeze(-1)) + + +@GaussianUpdate.register(ExactGP, _GaussianLikelihoodBase) +def _gaussian_update_ExactGP( + model: ExactGP, + likelihood: _GaussianLikelihoodBase, + *, + sample_values: Tensor, + target_values: Tensor | None = None, + points: Tensor | None = None, + noise_covariance: Tensor | LinearOperator | None = None, + scale_tril: Tensor | LinearOperator | None = None, +) -> GeneralizedLinearPath: + if points is None: + (points,) = get_train_inputs(model, transformed=True) + + if target_values is None: + target_values = get_train_targets(model, transformed=True) + + if noise_covariance is None: + noise_covariance = likelihood.noise_covar(shape=points.shape[:-1]) + + return _gaussian_update_exact( + kernel=model.covar_module, + points=points, + target_values=target_values, + sample_values=sample_values, + noise_covariance=noise_covariance, + scale_tril=scale_tril, + input_transform=get_input_transform(model), + ) + + +@GaussianUpdate.register(ApproximateGPyTorchModel, (Likelihood, NoneType)) +def _gaussian_update_ApproximateGPyTorchModel( + model: ApproximateGPyTorchModel, + likelihood: Likelihood | None, + **kwargs: Any, +) -> GeneralizedLinearPath: + return GaussianUpdate( + model.model, likelihood, input_transform=get_input_transform(model), **kwargs + ) + + +@GaussianUpdate.register(ApproximateGP, (Likelihood, NoneType)) +def _gaussian_update_ApproximateGP( + model: ApproximateGP, likelihood: Likelihood | None, **kwargs: Any +) -> GeneralizedLinearPath: + return GaussianUpdate(model, model.variational_strategy, **kwargs) + + +@GaussianUpdate.register(ApproximateGP, VariationalStrategy) +def _gaussian_update_ApproximateGP_VariationalStrategy( + model: ApproximateGP, + _: VariationalStrategy, + *, + sample_values: Tensor, + target_values: Tensor | None = None, + noise_covariance: Tensor | LinearOperator | None = None, + input_transform: InputTransform | None = None, + **ignore: Any, +) -> GeneralizedLinearPath: + # TODO: Account for jitter added by `psd_safe_cholesky` + if not isinstance(noise_covariance, (NoneType, ZeroLinearOperator)): + raise NotImplementedError( + f"`noise_covariance` argument not yet supported for {type(model)}." + ) + + # Inducing points `Z` are assumed to live in transformed space + batch_shape = model.covar_module.batch_shape + v = model.variational_strategy + Z = v.inducing_points + L = v._cholesky_factor(v(Z, prior=True).lazy_covariance_matrix).to( + dtype=sample_values.dtype + ) + + # Generate whitened inducing variables `u`, then location-scale transform + if target_values is None: + u = v.variational_distribution.rsample( + sample_values.shape[: sample_values.ndim - len(batch_shape) - 1], + ) + target_values = model.mean_module(Z) + (u @ L.transpose(-1, -2)) + + return _gaussian_update_exact( + kernel=model.covar_module, + points=Z, + target_values=target_values, + sample_values=sample_values, + scale_tril=L, + input_transform=input_transform, + ) +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/pathwise/utils.html b/website-old/pages/api/_modules/botorch/sampling/pathwise/utils.html new file mode 100644 index 0000000000..def7095bbc --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/pathwise/utils.html @@ -0,0 +1,426 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.pathwise.utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Callable, Iterable
+from typing import Any, overload, Union
+
+import torch
+from botorch.models.approximate_gp import SingleTaskVariationalGP
+from botorch.models.gpytorch import GPyTorchModel
+from botorch.models.model import Model, ModelList
+from botorch.models.transforms.input import InputTransform
+from botorch.models.transforms.outcome import OutcomeTransform
+from botorch.utils.dispatcher import Dispatcher
+from gpytorch.kernels import ScaleKernel
+from gpytorch.kernels.kernel import Kernel
+from torch import LongTensor, Tensor
+from torch.nn import Module, ModuleList
+
+TInputTransform = Union[InputTransform, Callable[[Tensor], Tensor]]
+TOutputTransform = Union[OutcomeTransform, Callable[[Tensor], Tensor]]
+GetTrainInputs = Dispatcher("get_train_inputs")
+GetTrainTargets = Dispatcher("get_train_targets")
+
+
+
+[docs] +class TransformedModuleMixin: + r"""Mixin that wraps a module's __call__ method with optional transforms.""" + + input_transform: TInputTransform | None + output_transform: TOutputTransform | None + + def __call__(self, values: Tensor, *args: Any, **kwargs: Any) -> Tensor: + input_transform = getattr(self, "input_transform", None) + if input_transform is not None: + values = ( + input_transform.forward(values) + if isinstance(input_transform, InputTransform) + else input_transform(values) + ) + + output = super().__call__(values, *args, **kwargs) + output_transform = getattr(self, "output_transform", None) + if output_transform is None: + return output + + return ( + output_transform.untransform(output)[0] + if isinstance(output_transform, OutcomeTransform) + else output_transform(output) + )
+ + + +
+[docs] +class TensorTransform(ABC, Module): + r"""Abstract base class for transforms that map tensor to tensor.""" + +
+[docs] + @abstractmethod + def forward(self, values: Tensor, **kwargs: Any) -> Tensor: + pass # pragma: no cover
+
+ + + +
+[docs] +class ChainedTransform(TensorTransform): + r"""A composition of TensorTransforms.""" + + def __init__(self, *transforms: TensorTransform): + r"""Initializes a ChainedTransform instance. + + Args: + transforms: A set of transforms to be applied from right to left. + """ + super().__init__() + self.transforms = ModuleList(transforms) + +
+[docs] + def forward(self, values: Tensor) -> Tensor: + for transform in reversed(self.transforms): + values = transform(values) + return values
+
+ + + +
+[docs] +class SineCosineTransform(TensorTransform): + r"""A transform that returns concatenated sine and cosine features.""" + + def __init__(self, scale: Tensor | None = None): + r"""Initializes a SineCosineTransform instance. + + Args: + scale: An optional tensor used to rescale the module's outputs. + """ + super().__init__() + self.scale = scale + +
+[docs] + def forward(self, values: Tensor) -> Tensor: + sincos = torch.concat([values.sin(), values.cos()], dim=-1) + return sincos if self.scale is None else self.scale * sincos
+
+ + + +
+[docs] +class InverseLengthscaleTransform(TensorTransform): + r"""A transform that divides its inputs by a kernels lengthscales.""" + + def __init__(self, kernel: Kernel): + r"""Initializes an InverseLengthscaleTransform instance. + + Args: + kernel: The kernel whose lengthscales are to be used. + """ + if not kernel.has_lengthscale: + raise RuntimeError(f"{type(kernel)} does not implement `lengthscale`.") + + super().__init__() + self.kernel = kernel + +
+[docs] + def forward(self, values: Tensor) -> Tensor: + return self.kernel.lengthscale.reciprocal() * values
+
+ + + +
+[docs] +class OutputscaleTransform(TensorTransform): + r"""A transform that multiplies its inputs by the square root of a + kernel's outputscale.""" + + def __init__(self, kernel: ScaleKernel): + r"""Initializes an OutputscaleTransform instance. + + Args: + kernel: A ScaleKernel whose `outputscale` is to be used. + """ + super().__init__() + self.kernel = kernel + +
+[docs] + def forward(self, values: Tensor) -> Tensor: + outputscale = ( + self.kernel.outputscale[..., None, None] + if self.kernel.batch_shape + else self.kernel.outputscale + ) + return outputscale.sqrt() * values
+
+ + + +
+[docs] +class FeatureSelector(TensorTransform): + r"""A transform that returns a subset of its input's features. + along a given tensor dimension.""" + + def __init__(self, indices: Iterable[int], dim: int | LongTensor = -1): + r"""Initializes a FeatureSelector instance. + + Args: + indices: A LongTensor of feature indices. + dim: The dimensional along which to index features. + """ + super().__init__() + self.register_buffer("dim", dim if torch.is_tensor(dim) else torch.tensor(dim)) + self.register_buffer( + "indices", indices if torch.is_tensor(indices) else torch.tensor(indices) + ) + +
+[docs] + def forward(self, values: Tensor) -> Tensor: + return values.index_select(dim=self.dim, index=self.indices)
+
+ + + +
+[docs] +class OutcomeUntransformer(TensorTransform): + r"""Module acting as a bridge for `OutcomeTransform.untransform`.""" + + def __init__( + self, + transform: OutcomeTransform, + num_outputs: int | LongTensor, + ): + r"""Initializes an OutcomeUntransformer instance. + + Args: + transform: The wrapped OutcomeTransform instance. + num_outputs: The number of outcome features that the + OutcomeTransform transforms. + """ + super().__init__() + self.transform = transform + self.register_buffer( + "num_outputs", + num_outputs if torch.is_tensor(num_outputs) else torch.tensor(num_outputs), + ) + +
+[docs] + def forward(self, values: Tensor) -> Tensor: + # OutcomeTransforms expect an explicit output dimension in the final position. + if self.num_outputs == 1: # BoTorch has suppressed the output dimension + output_values, _ = self.transform.untransform(values.unsqueeze(-1)) + return output_values.squeeze(-1) + + # BoTorch has moved the output dimension inside as the final batch dimension. + output_values, _ = self.transform.untransform(values.transpose(-2, -1)) + return output_values.transpose(-2, -1)
+
+ + + +
+[docs] +def get_input_transform(model: GPyTorchModel) -> InputTransform | None: + r"""Returns a model's input_transform or None.""" + return getattr(model, "input_transform", None)
+ + + +
+[docs] +def get_output_transform(model: GPyTorchModel) -> OutcomeUntransformer | None: + r"""Returns a wrapped version of a model's outcome_transform or None.""" + transform = getattr(model, "outcome_transform", None) + if transform is None: + return None + + return OutcomeUntransformer(transform=transform, num_outputs=model.num_outputs)
+ + + +@overload +def get_train_inputs(model: Model, transformed: bool = False) -> tuple[Tensor, ...]: + pass # pragma: no cover + + +@overload +def get_train_inputs(model: ModelList, transformed: bool = False) -> list[...]: + pass # pragma: no cover + + +
+[docs] +def get_train_inputs(model: Model, transformed: bool = False): + return GetTrainInputs(model, transformed=transformed)
+ + + +@GetTrainInputs.register(Model) +def _get_train_inputs_Model(model: Model, transformed: bool = False) -> tuple[Tensor]: + if not transformed: + original_train_input = getattr(model, "_original_train_inputs", None) + if torch.is_tensor(original_train_input): + return (original_train_input,) + + (X,) = model.train_inputs + transform = get_input_transform(model) + if transform is None: + return (X,) + + if model.training: + return (transform.forward(X) if transformed else X,) + return (X if transformed else transform.untransform(X),) + + +@GetTrainInputs.register(SingleTaskVariationalGP) +def _get_train_inputs_SingleTaskVariationalGP( + model: SingleTaskVariationalGP, transformed: bool = False +) -> tuple[Tensor]: + (X,) = model.model.train_inputs + if model.training != transformed: + return (X,) + + transform = get_input_transform(model) + if transform is None: + return (X,) + + return (transform.forward(X) if model.training else transform.untransform(X),) + + +@GetTrainInputs.register(ModelList) +def _get_train_inputs_ModelList( + model: ModelList, transformed: bool = False +) -> list[...]: + return [get_train_inputs(m, transformed=transformed) for m in model.models] + + +@overload +def get_train_targets(model: Model, transformed: bool = False) -> Tensor: + pass # pragma: no cover + + +@overload +def get_train_targets(model: ModelList, transformed: bool = False) -> list[...]: + pass # pragma: no cover + + +
+[docs] +def get_train_targets(model: Model, transformed: bool = False): + return GetTrainTargets(model, transformed=transformed)
+ + + +@GetTrainTargets.register(Model) +def _get_train_targets_Model(model: Model, transformed: bool = False) -> Tensor: + Y = model.train_targets + + # Note: Avoid using `get_output_transform` here since it creates a Module + transform = getattr(model, "outcome_transform", None) + if transformed or transform is None: + return Y + + if model.num_outputs == 1: + return transform.untransform(Y.unsqueeze(-1))[0].squeeze(-1) + return transform.untransform(Y.transpose(-2, -1))[0].transpose(-2, -1) + + +@GetTrainTargets.register(SingleTaskVariationalGP) +def _get_train_targets_SingleTaskVariationalGP( + model: Model, transformed: bool = False +) -> Tensor: + Y = model.model.train_targets + transform = getattr(model, "outcome_transform", None) + if transformed or transform is None: + return Y + + if model.num_outputs == 1: + return transform.untransform(Y.unsqueeze(-1))[0].squeeze(-1) + + # SingleTaskVariationalGP.__init__ doesn't bring the multitoutpout dimension inside + return transform.untransform(Y)[0] + + +@GetTrainTargets.register(ModelList) +def _get_train_targets_ModelList( + model: ModelList, transformed: bool = False +) -> list[...]: + return [get_train_targets(m, transformed=transformed) for m in model.models] +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/qmc.html b/website-old/pages/api/_modules/botorch/sampling/qmc.html new file mode 100644 index 0000000000..be8e53ae37 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/qmc.html @@ -0,0 +1,242 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.qmc

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Quasi Monte-Carlo sampling from Normal distributions.
+
+References:
+
+.. [Pages2018numprob]
+    G. Pages. Numerical Probability: An Introduction with Applications to
+    Finance. Universitext. Springer International Publishing, 2018.
+"""
+
+from __future__ import annotations
+
+import math
+
+import torch
+from torch import Tensor
+from torch.quasirandom import SobolEngine
+
+
+
+[docs] +class NormalQMCEngine: + r"""Engine for qMC sampling from a Multivariate Normal `N(0, I_d)`. + + By default, this implementation uses Box-Muller transformed Sobol samples + following pg. 123 in [Pages2018numprob]_. To use the inverse transform + instead, set `inv_transform=True`. + + Example: + >>> engine = NormalQMCEngine(3) + >>> samples = engine.draw(16) + """ + + def __init__( + self, d: int, seed: int | None = None, inv_transform: bool = False + ) -> None: + r"""Engine for drawing qMC samples from a multivariate normal `N(0, I_d)`. + + Args: + d: The dimension of the samples. + seed: The seed with which to seed the random number generator of the + underlying SobolEngine. + inv_transform: If True, use inverse transform instead of Box-Muller. + """ + self._d = d + self._seed = seed + self._inv_transform = inv_transform + if inv_transform: + sobol_dim = d + else: + # to apply Box-Muller, we need an even number of dimensions + sobol_dim = 2 * math.ceil(d / 2) + self._sobol_engine = SobolEngine(dimension=sobol_dim, scramble=True, seed=seed) + +
+[docs] + def draw( + self, + n: int = 1, + out: Tensor | None = None, + dtype: torch.dtype | None = None, + ) -> Tensor | None: + r"""Draw `n` qMC samples from the standard Normal. + + Args: + n: The number of samples to draw. As a best practice, use powers of 2. + out: An option output tensor. If provided, draws are put into this + tensor, and the function returns None. + dtype: The desired torch data type (ignored if `out` is provided). + If None, uses `torch.get_default_dtype()`. + + Returns: + A `n x d` tensor of samples if `out=None` and `None` otherwise. + """ + dtype = torch.get_default_dtype() if dtype is None else dtype + # get base samples + samples = self._sobol_engine.draw(n, dtype=dtype) + if self._inv_transform: + # apply inverse transform (values to close to 0/1 result in inf values) + v = 0.5 + (1 - torch.finfo(samples.dtype).eps) * (samples - 0.5) + samples_tf = torch.erfinv(2 * v - 1) * math.sqrt(2) + else: + # apply Box-Muller transform (note: [1] indexes starting from 1) + even = torch.arange(0, samples.shape[-1], 2) + Rs = (-2 * torch.log(samples[:, even])).sqrt() + thetas = 2 * math.pi * samples[:, 1 + even] + cos = torch.cos(thetas) + sin = torch.sin(thetas) + samples_tf = torch.stack([Rs * cos, Rs * sin], -1).reshape(n, -1) + # make sure we only return the number of dimension requested + samples_tf = samples_tf[:, : self._d] + if out is None: + return samples_tf + else: + out.copy_(samples_tf)
+
+ + + +
+[docs] +class MultivariateNormalQMCEngine: + r"""Engine for qMC sampling from a multivariate Normal `N(\mu, \Sigma)`. + + By default, this implementation uses Box-Muller transformed Sobol samples + following pg. 123 in [Pages2018numprob]_. To use the inverse transform + instead, set `inv_transform=True`. + + Example: + >>> mean = torch.tensor([1.0, 2.0]) + >>> cov = torch.tensor([[1.0, 0.25], [0.25, 2.0]]) + >>> engine = MultivariateNormalQMCEngine(mean, cov) + >>> samples = engine.draw(16) + """ + + def __init__( + self, + mean: Tensor, + cov: Tensor, + seed: int | None = None, + inv_transform: bool = False, + ) -> None: + r"""Engine for qMC sampling from a multivariate Normal `N(\mu, \Sigma)`. + + Args: + mean: The mean vector. + cov: The covariance matrix. + seed: The seed with which to seed the random number generator of the + underlying SobolEngine. + inv_transform: If True, use inverse transform instead of Box-Muller. + """ + # validate inputs + if not cov.shape[0] == cov.shape[1]: + raise ValueError("Covariance matrix is not square.") + if not mean.shape[0] == cov.shape[0]: + raise ValueError("Dimension mismatch between mean and covariance.") + if not torch.allclose(cov, cov.transpose(-1, -2)): + raise ValueError("Covariance matrix is not symmetric.") + self._mean = mean + self._normal_engine = NormalQMCEngine( + d=mean.shape[0], seed=seed, inv_transform=inv_transform + ) + # compute Cholesky decomp; if it fails, do the eigendecomposition + try: + self._corr_matrix = torch.linalg.cholesky(cov).transpose(-1, -2) + except RuntimeError: + eigval, eigvec = torch.linalg.eigh(cov) + tol = 1e-8 if eigval.dtype == torch.double else 1e-6 + if torch.any(eigval < -tol): + raise ValueError("Covariance matrix not PSD.") + eigval_root = eigval.clamp_min(0.0).sqrt() + self._corr_matrix = (eigvec * eigval_root).transpose(-1, -2) + +
+[docs] + def draw(self, n: int = 1, out: Tensor | None = None) -> Tensor | None: + r"""Draw `n` qMC samples from the multivariate Normal. + + Args: + n: The number of samples to draw. As a best practice, use powers of 2. + out: An option output tensor. If provided, draws are put into this + tensor, and the function returns None. + + Returns: + A `n x d` tensor of samples if `out=None` and `None` otherwise. + """ + dtype = out.dtype if out is not None else self._mean.dtype + device = out.device if out is not None else self._mean.device + base_samples = self._normal_engine.draw(n, dtype=dtype).to(device=device) + corr_mat = self._corr_matrix.to(dtype=dtype, device=device) + mean = self._mean.to(dtype=dtype, device=device) + qmc_samples = base_samples @ corr_mat + mean + if out is None: + return qmc_samples + else: + out.copy_(qmc_samples)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/sampling/stochastic_samplers.html b/website-old/pages/api/_modules/botorch/sampling/stochastic_samplers.html new file mode 100644 index 0000000000..f46544065f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/sampling/stochastic_samplers.html @@ -0,0 +1,135 @@ + + + + + + + +
+
+
+
+

Source code for botorch.sampling.stochastic_samplers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Samplers to enable use cases that are not base sample driven, such as
+stochastic optimization of acquisition functions.
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.posteriors import Posterior
+from botorch.sampling.base import MCSampler
+from torch import Tensor
+
+
+
+[docs] +class ForkedRNGSampler(MCSampler): + r"""A sampler using `torch.fork_rng` to enable replicable sampling + from a posterior that does not support base samples. + + NOTE: This approach is not a one-to-one replacement for base sample + driven sampling. The main missing piece in this approach is that its + outputs are not replicable across the batch dimensions. As a result, + when an acquisition function is batch evaluated with repeated candidates, + each candidate will produce a different acquisition value, which is not + compatible with Sample Average Approximation. + """ + +
+[docs] + def forward(self, posterior: Posterior) -> Tensor: + r"""Draws MC samples from the posterior in a `fork_rng` context. + + Args: + posterior: The posterior to sample from. + + Returns: + The samples drawn from the posterior. + """ + with torch.random.fork_rng(): + torch.manual_seed(self.seed) + return posterior.rsample(sample_shape=self.sample_shape)
+
+ + + +
+[docs] +class StochasticSampler(MCSampler): + r"""A sampler that simply calls `posterior.rsample` to generate the + samples. This should only be used for stochastic optimization of the + acquisition functions, e.g., via `gen_candidates_torch`. This should + not be used with `optimize_acqf`, which uses deterministic optimizers + under the hood. + + NOTE: This ignores the `seed` option. + """ + +
+[docs] + def forward(self, posterior: Posterior) -> Tensor: + r"""Draws MC samples from the posterior. + + Args: + posterior: The posterior to sample from. + + Returns: + The samples drawn from the posterior. + """ + return posterior.rsample(sample_shape=self.sample_shape)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/settings.html b/website-old/pages/api/_modules/botorch/settings.html new file mode 100644 index 0000000000..f29b56dacf --- /dev/null +++ b/website-old/pages/api/_modules/botorch/settings.html @@ -0,0 +1,163 @@ + + + + + + + +
+
+
+
+

Source code for botorch.settings

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+BoTorch settings.
+"""
+
+from __future__ import annotations
+
+from botorch.logging import LOG_LEVEL_DEFAULT, logger
+
+
+class _Flag:
+    r"""Base class for context managers for a binary setting."""
+
+    _state: bool = False
+
+    @classmethod
+    def on(cls) -> bool:
+        return cls._state
+
+    @classmethod
+    def off(cls) -> bool:
+        return not cls._state
+
+    @classmethod
+    def _set_state(cls, state: bool) -> None:
+        cls._state = state
+
+    def __init__(self, state: bool = True) -> None:
+        self.prev = self.__class__.on()
+        self.state = state
+
+    def __enter__(self) -> None:
+        self.__class__._set_state(self.state)
+
+    def __exit__(self, *args) -> None:
+        self.__class__._set_state(self.prev)
+
+
+
+[docs] +class propagate_grads(_Flag): + r"""Flag for propagating gradients to model training inputs / training data. + + When set to `True`, gradients will be propagated to the training inputs. + This is useful in particular for propating gradients through fantasy models. + """ + + _state: bool = False
+ + + +
+[docs] +class validate_input_scaling(_Flag): + r"""Flag for validating input normalization/standardization. + + When set to `True`, standard botorch models will validate (up to reasonable + tolerance) that + (i) none of the inputs contain NaN values + (ii) the training data (`train_X`) is normalized to the unit cube + (iii) the training targets (`train_Y`) are standardized (zero mean, unit var) + No checks (other than the NaN check) are performed for observed variances + (`train_Y_var`) at this point. + """ + + _state: bool = True
+ + + +
+[docs] +class log_level: + r"""Flag for printing verbose logging statements. + + Applies the given level to logging.getLogger('botorch') calls. For + instance, when set to logging.INFO, all logger calls of level INFO or + above will be printed to STDERR + """ + + level: int = LOG_LEVEL_DEFAULT + + @classmethod + def _set_level(cls, level: int) -> None: + cls.level = level + logger.setLevel(level) + + def __init__(self, level: int = LOG_LEVEL_DEFAULT) -> None: + r""" + Args: + level: The log level. Defaults to LOG_LEVEL_DEFAULT. + """ + self.prev = self.__class__.level + self.level = level + + def __enter__(self) -> None: + self.__class__._set_level(self.level) + + def __exit__(self, *args) -> None: + self.__class__._set_level(self.prev)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_functions/base.html b/website-old/pages/api/_modules/botorch/test_functions/base.html new file mode 100644 index 0000000000..5069042bad --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_functions/base.html @@ -0,0 +1,290 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_functions.base

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Base class for test functions for optimization benchmarks.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.exceptions.errors import InputDataError
+from torch import Tensor
+from torch.nn import Module
+
+
+
+[docs] +class BaseTestProblem(Module, ABC): + r"""Base class for test functions.""" + + dim: int + _bounds: list[tuple[float, float]] + _check_grad_at_opt: bool = True + + def __init__( + self, + noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r"""Base constructor for test functions. + + Args: + noise_std: Standard deviation of the observation noise. If a list is + provided, specifies separate noise standard deviations for each + objective in a multiobjective problem. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + super().__init__() + self.noise_std = noise_std + self.negate = negate + if len(self._bounds) != self.dim: + raise InputDataError( + "Expected the bounds to match the dimensionality of the domain. " + f"Got {self.dim=} and {len(self._bounds)=}." + ) + self.register_buffer( + "bounds", + torch.tensor(self._bounds, dtype=dtype).transpose(-1, -2), + ) + +
+[docs] + def forward(self, X: Tensor, noise: bool = True) -> Tensor: + r"""Evaluate the function on a set of points. + + Args: + X: A `(batch_shape) x d`-dim tensor of point(s) at which to evaluate + the function. + noise: If `True`, add observation noise as specified by `noise_std`. + + Returns: + A `batch_shape`-dim tensor ouf function evaluations. + """ + f = self.evaluate_true(X=X) + if noise and self.noise_std is not None: + _noise = torch.tensor(self.noise_std, device=X.device, dtype=X.dtype) + f += _noise * torch.randn_like(f) + if self.negate: + f = -f + return f
+ + +
+[docs] + @abstractmethod + def evaluate_true(self, X: Tensor) -> Tensor: + r""" + Evaluate the function (w/o observation noise) on a set of points. + + Args: + X: A `(batch_shape) x d`-dim tensor of point(s) at which to + evaluate. + + Returns: + A `batch_shape`-dim tensor. + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class ConstrainedBaseTestProblem(BaseTestProblem, ABC): + r"""Base class for test functions with constraints. + + In addition to one or more objectives, a problem may have a number of outcome + constraints of the form `c_i(x) >= 0` for `i=1, ..., n_c`. + + This base class provides common functionality for such problems. + """ + + num_constraints: int + _check_grad_at_opt: bool = False + constraint_noise_std: None | float | list[float] = None + +
+[docs] + def evaluate_slack(self, X: Tensor, noise: bool = True) -> Tensor: + r"""Evaluate the constraint slack on a set of points. + + Constraints `i` is assumed to be feasible at `x` if the associated slack + `c_i(x)` is positive. Zero slack means that the constraint is active. Negative + slack means that the constraint is violated. + + Args: + X: A `batch_shape x d`-dim tensor of point(s) at which to evaluate the + constraint slacks: `c_1(X), ...., c_{n_c}(X)`. + noise: If `True`, add observation noise to the slack as specified by + `noise_std`. + + Returns: + A `batch_shape x n_c`-dim tensor of constraint slack (where positive slack + corresponds to the constraint being feasible). + """ + cons = self.evaluate_slack_true(X=X) + if noise and self.constraint_noise_std is not None: + _constraint_noise = torch.tensor( + self.constraint_noise_std, device=X.device, dtype=X.dtype + ) + cons += _constraint_noise * torch.randn_like(cons) + return cons
+ + +
+[docs] + def is_feasible(self, X: Tensor, noise: bool = True) -> Tensor: + r"""Evaluate whether the constraints are feasible on a set of points. + + Args: + X: A `batch_shape x d`-dim tensor of point(s) at which to evaluate the + constraints. + noise: If `True`, add observation noise as specified by `noise_std`. + + Returns: + A `batch_shape`-dim boolean tensor that is `True` iff all constraint + slacks (potentially including observation noise) are positive. + """ + return (self.evaluate_slack(X=X, noise=noise) >= 0.0).all(dim=-1)
+ + +
+[docs] + @abstractmethod + def evaluate_slack_true(self, X: Tensor) -> Tensor: + r"""Evaluate the constraint slack (w/o observation noise) on a set of points. + + Args: + X: A `batch_shape x d`-dim tensor of point(s) at which to evaluate the + constraint slacks: `c_1(X), ...., c_{n_c}(X)`. + + Returns: + A `batch_shape x n_c`-dim tensor of constraint slack (where positive slack + corresponds to the constraint being feasible). + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class MultiObjectiveTestProblem(BaseTestProblem, ABC): + r"""Base class for multi-objective test functions. + + TODO: add a pareto distance function that returns the distance + between a provided point and the closest point on the true pareto front. + """ + + num_objectives: int + _ref_point: list[float] + _max_hv: float | None = None + + def __init__( + self, + noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r"""Base constructor for multi-objective test functions. + + Args: + noise_std: Standard deviation of the observation noise. If a list is + provided, specifies separate noise standard deviations for each + objective. + negate: If True, negate the objectives. + """ + if isinstance(noise_std, list) and len(noise_std) != len(self._ref_point): + raise InputDataError( + f"If specified as a list, length of noise_std ({len(noise_std)}) " + f"must match the number of objectives ({len(self._ref_point)})" + ) + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + ref_point = torch.tensor(self._ref_point, dtype=dtype) + if negate: + ref_point *= -1 + self.register_buffer("ref_point", ref_point) + + @property + def max_hv(self) -> float: + if self._max_hv is not None: + return self._max_hv + else: + raise NotImplementedError( + f"Problem {self.__class__.__name__} does not specify maximal " + "hypervolume." + ) + +
+[docs] + def gen_pareto_front(self, n: int) -> Tensor: + r"""Generate `n` pareto optimal points.""" + raise NotImplementedError
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_functions/multi_fidelity.html b/website-old/pages/api/_modules/botorch/test_functions/multi_fidelity.html new file mode 100644 index 0000000000..4f8179b80e --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_functions/multi_fidelity.html @@ -0,0 +1,238 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_functions.multi_fidelity

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Synthetic functions for multi-fidelity optimization benchmarks.
+"""
+
+from __future__ import annotations
+
+import math
+
+import torch
+from botorch.test_functions.synthetic import SyntheticTestFunction
+from torch import Tensor
+
+
+
+[docs] +class AugmentedBranin(SyntheticTestFunction): + r"""Augmented Branin test function for multi-fidelity optimization. + + 3-dimensional function with domain `[-5, 10] x [0, 15] * [0,1]`, where + the last dimension of is the fidelity parameter: + + B(x) = (x_2 - (b - 0.1 * (1 - x_3))x_1^2 + c x_1 - r)^2 + + 10 (1-t) cos(x_1) + 10 + + Here `b`, `c`, `r` and `t` are constants where `b = 5.1 / (4 * math.pi ** 2)` + `c = 5 / math.pi`, `r = 6`, `t = 1 / (8 * math.pi)`. + B has infinitely many minimizers with `x_1 = -pi, pi, 3pi` + and `B_min = 0.397887` + """ + + dim = 3 + _bounds = [(-5.0, 10.0), (0.0, 15.0), (0.0, 1.0)] + _optimal_value = 0.397887 + _optimizers = [ # this is a subset, ther are infinitely many optimizers + (-math.pi, 12.275, 1), + (math.pi, 1.3867356039019576, 0.1), + (math.pi, 1.781519779945532, 0.5), + (math.pi, 2.1763039559891064, 0.9), + ] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + t1 = ( + X[..., 1] + - (5.1 / (4 * math.pi**2) - 0.1 * (1 - X[..., 2])) * X[..., 0].pow(2) + + 5 / math.pi * X[..., 0] + - 6 + ) + t2 = 10 * (1 - 1 / (8 * math.pi)) * torch.cos(X[..., 0]) + return t1.pow(2) + t2 + 10
+
+ + + +
+[docs] +class AugmentedHartmann(SyntheticTestFunction): + r"""Augmented Hartmann synthetic test function. + + 7-dimensional function (typically evaluated on `[0, 1]^7`), where the last + dimension is the fidelity parameter. + + H(x) = -(ALPHA_1 - 0.1 * (1-x_7)) * exp(- sum_{j=1}^6 A_1j (x_j - P_1j) ** 2) - + sum_{i=2}^4 ALPHA_i exp( - sum_{j=1}^6 A_ij (x_j - P_ij) ** 2) + + H has a unique global minimizer + `x = [0.20169, 0.150011, 0.476874, 0.275332, 0.311652, 0.6573, 1.0]` + + with `H_min = -3.32237` + """ + + dim = 7 + _bounds = [(0.0, 1.0) for _ in range(7)] + _optimal_value = -3.32237 + _optimizers = [(0.20169, 0.150011, 0.476874, 0.275332, 0.311652, 0.6573, 1.0)] + _check_grad_at_opt = False + + def __init__( + self, + noise_std: float | None = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + self.register_buffer("ALPHA", torch.tensor([1.0, 1.2, 3.0, 3.2])) + A = [ + [10, 3, 17, 3.5, 1.7, 8], + [0.05, 10, 17, 0.1, 8, 14], + [3, 3.5, 1.7, 10, 17, 8], + [17, 8, 0.05, 10, 0.1, 14], + ] + P = [ + [1312, 1696, 5569, 124, 8283, 5886], + [2329, 4135, 8307, 3736, 1004, 9991], + [2348, 1451, 3522, 2883, 3047, 6650], + [4047, 8828, 8732, 5743, 1091, 381.0], + ] + self.register_buffer("A", torch.tensor(A)) + self.register_buffer("P", torch.tensor(P)) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + self.to(device=X.device, dtype=X.dtype) + inner_sum = torch.sum( + self.A * (X[..., :6].unsqueeze(-2) - 0.0001 * self.P).pow(2), dim=-1 + ) + alpha1 = self.ALPHA[0] - 0.1 * (1 - X[..., 6]) + H = ( + -(torch.sum(self.ALPHA[1:] * torch.exp(-inner_sum)[..., 1:], dim=-1)) + - alpha1 * torch.exp(-inner_sum)[..., 0] + ) + return H
+
+ + + +
+[docs] +class AugmentedRosenbrock(SyntheticTestFunction): + r"""Augmented Rosenbrock synthetic test function for multi-fidelity optimization. + + d-dimensional function (usually evaluated on `[-5, 10]^(d-2) * [0, 1]^2`), + where the last two dimensions are the fidelity parameters: + + f(x) = sum_{i=1}^{d-1} (100 (x_{i+1} - x_i^2 + 0.1 * (1-x_{d-1}))^2 + + (x_i - 1 + 0.1 * (1 - x_d)^2)^2) + + f has one minimizer for its global minimum at `z_1 = (1, 1, ..., 1)` with + `f(z_i) = 0.0`. + """ + + _optimal_value = 0.0 + + def __init__( + self, + dim=3, + noise_std: float | None = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. Must be at least 3. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + if dim < 3: + raise ValueError( + "AugmentedRosenbrock must be defined it at least 3 dimensions" + ) + self.dim = dim + self._bounds = [(-5.0, 10.0) for _ in range(self.dim)] + self._optimizers = [tuple(1.0 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X_curr = X[..., :-3] + X_next = X[..., 1:-2] + t1 = 100 * (X_next - X_curr.pow(2) + 0.1 * (1 - X[..., -2:-1])).pow(2) + t2 = (X_curr - 1 + 0.1 * (1 - X[..., -1:]).pow(2)).pow(2) + return -((t1 + t2).sum(dim=-1))
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_functions/multi_objective.html b/website-old/pages/api/_modules/botorch/test_functions/multi_objective.html new file mode 100644 index 0000000000..4cd28f0d64 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_functions/multi_objective.html @@ -0,0 +1,1817 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_functions.multi_objective

+#! /usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Multi-objective optimization benchmark problems.
+
+References
+
+.. [Daulton2022]
+    S. Daulton, S. Cakmak, M. Balandat, M. A. Osborne, E. Zhou, and E. Bakshy.
+    Robust Multi-Objective Bayesian Optimization Under Input Noise.
+    Proceedings of the 39th International Conference on Machine Learning, 2022.
+
+.. [Deb2005dtlz]
+    K. Deb, L. Thiele, M. Laumanns, E. Zitzler, A. Abraham, L. Jain, and
+    R. Goldberg. Scalable test problems for evolutionary multi-objective
+    optimization. Evolutionary Multiobjective Optimization, Springer-Verlag,
+    pp. 105-145, 2005.
+
+.. [Deb2005robust]
+    K. Deb and H. Gupta. Searching for Robust Pareto-Optimal Solutions in
+    Multi-objective Optimization. Evolutionary Multi-Criterion Optimization,
+    Springer-Berlin, pp. 150-164, 2005.
+
+.. [Frohlich2020]
+    L. Frohlich, E. Klenske, J. Vinogradska, C. Daniel, and M. Zeilinger.
+    Noisy-Input Entropy Search for Efficient Robust Bayesian Optimization.
+    Proceedings of the Twenty Third International Conference on Artificial
+    Intelligence and Statistics, PMLR 108:2262-2272, 2020.
+
+.. [GarridoMerchan2020]
+    E. C. Garrido-Merch ́an and D. Hern ́andez-Lobato. Parallel Predictive Entropy
+    Search for Multi-objective Bayesian Optimization with Constraints.
+    arXiv e-prints, arXiv:2004.00601, Apr. 2020.
+
+.. [Gelbart2014]
+    Michael A. Gelbart, Jasper Snoek, and Ryan P. Adams. 2014. Bayesian
+    optimization with unknown constraints. In Proceedings of the Thirtieth
+    Conference on Uncertainty in Artificial Intelligence (UAI’14).
+    AUAI Press, Arlington, Virginia, USA, 250–259.
+
+.. [Liang2021]
+    Q. Liang and L. Lai, Scalable Bayesian Optimization Accelerates Process
+    Optimization of Penicillin Production. NeurIPS 2021 AI for Science Workshop, 2021.
+
+.. [Ma2019]
+    Z. Ma and Y. Wang. Evolutionary Constrained Multiobjective Optimization:
+    Test Suite Construction and Performance Comparisons. IEEE Transactions
+    on Evolutionary Computation, 23(6):972–986, December 2019.
+
+.. [Oszycka1995]
+    A. Osyczka and S. Kundu. A new method to solve generalized
+    multicriteria optimization problems using the simple genetic algorithm.
+    In Structural Optimization 10. 94–99, 1995.
+
+.. [Tanabe2020]
+    Ryoji Tanabe and Hisao Ishibuchi. An easy-to-use real-world multi-objective
+    optimization problem suite, Applied Soft Computing,Volume 89, 2020.
+
+.. [Yang2019a]
+    K. Yang, M. Emmerich, A. Deutz, and T. Bäck. Multi-Objective Bayesian
+    Global Optimization using expected hypervolume improvement gradient.
+    Swarm and evolutionary computation 44, pp. 945--956, 2019.
+
+.. [Zitzler2000]
+    E. Zitzler, K. Deb, and L. Thiele. Comparison of multiobjective
+    evolutionary algorithms: Empirical results. Evolutionary Computation, vol.
+    8, no. 2,pp. 173–195, 2000.
+"""
+
+from __future__ import annotations
+
+import math
+from abc import ABC, abstractmethod
+from math import pi
+
+import torch
+from botorch.exceptions.errors import UnsupportedError
+from botorch.test_functions.base import (
+    ConstrainedBaseTestProblem,
+    MultiObjectiveTestProblem,
+)
+from botorch.test_functions.synthetic import Branin, Levy
+from botorch.utils.sampling import sample_hypersphere, sample_simplex
+from botorch.utils.transforms import unnormalize
+from scipy.special import gamma
+from torch import Tensor
+from torch.distributions import MultivariateNormal
+
+
+
+[docs] +class BraninCurrin(MultiObjectiveTestProblem): + r"""Two objective problem composed of the Branin and Currin functions. + + Branin (rescaled): + + f(x) = ( + 15*x_1 - 5.1 * (15 * x_0 - 5) ** 2 / (4 * pi ** 2) + 5 * (15 * x_0 - 5) + / pi - 5 + ) ** 2 + (10 - 10 / (8 * pi)) * cos(15 * x_0 - 5)) + + Currin: + + f(x) = (1 - exp(-1 / (2 * x_1))) * ( + 2300 * x_0 ** 3 + 1900 * x_0 ** 2 + 2092 * x_0 + 60 + ) / 100 * x_0 ** 3 + 500 * x_0 ** 2 + 4 * x_0 + 20 + + """ + + dim = 2 + num_objectives = 2 + _bounds = [(0.0, 1.0), (0.0, 1.0)] + _ref_point = [18.0, 6.0] + _max_hv = 59.36011874867746 # this is approximated using NSGA-II + + def __init__( + self, + noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + noise_std: Standard deviation of the observation noise. + negate: If True, negate the objectives. + dtype: The dtype that is used for the bounds of the function. + """ + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + self._branin = Branin() + + def _rescaled_branin(self, X: Tensor) -> Tensor: + # return to Branin bounds + x_0 = 15 * X[..., 0] - 5 + x_1 = 15 * X[..., 1] + return self._branin(torch.stack([x_0, x_1], dim=-1)) + + @staticmethod + def _currin(X: Tensor) -> Tensor: + x_0 = X[..., 0] + x_1 = X[..., 1] + factor1 = 1 - torch.exp(-1 / (2 * x_1)) + numer = 2300 * x_0.pow(3) + 1900 * x_0.pow(2) + 2092 * x_0 + 60 + denom = 100 * x_0.pow(3) + 500 * x_0.pow(2) + 4 * x_0 + 20 + return factor1 * numer / denom + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + # branin rescaled with inputsto [0,1]^2 + branin = self._rescaled_branin(X=X) + currin = self._currin(X=X) + return torch.stack([branin, currin], dim=-1)
+
+ + + +
+[docs] +class DH(MultiObjectiveTestProblem, ABC): + r"""Base class for DH problems for robust multi-objective optimization. + + In their paper, [Deb2005robust]_ consider these problems under a mean-robustness + setting, and use uniformly distributed input perturbations from the box with + edge lengths `delta_0 = delta`, `delta_i = 2 * delta, i > 0`, with `delta` ranging + up to `0.01` for DH1 and DH2, and `delta = 0.03` for DH3 and DH4. + + These are d-dimensional problems with two objectives: + + f_0(x) = x_0 + f_1(x) = h(x) + g(x) * S(x) for DH1 and DH2 + f_1(x) = h(x) * (g(x) + S(x)) for DH3 and DH4 + + The goal is to minimize both objectives. See [Deb2005robust]_ for more details + on DH. The reference points were set using `infer_reference_point`. + """ + + num_objectives = 2 + _ref_point: list[float] = [1.1, 1.1] + _x_1_lb: float + _area_under_curve: float + _min_dim: int + + def __init__( + self, + dim: int, + noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + if dim < self._min_dim: + raise ValueError(f"dim must be >= {self._min_dim}, but got dim={dim}!") + self.dim = dim + self._bounds = [(0.0, 1.0), (self._x_1_lb, 1.0)] + [ + (-1.0, 1.0) for _ in range(dim - 2) + ] + # max_hv is the area of the box minus the area of the curve formed by the PF. + self._max_hv = self._ref_point[0] * self._ref_point[1] - self._area_under_curve + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + + @abstractmethod + def _h(self, X: Tensor) -> Tensor: + pass # pragma: no cover + + @abstractmethod + def _g(self, X: Tensor) -> Tensor: + pass # pragma: no cover + + @abstractmethod + def _S(self, X: Tensor) -> Tensor: + pass # pragma: no cover
+ + + +
+[docs] +class DH1(DH): + r"""DH1 test problem. + + d-dimensional problem evaluated on `[0, 1] x [-1, 1]^{d-1}`: + + f_0(x) = x_0 + f_1(x) = h(x_0) + g(x) * S(x_0) + h(x_0) = 1 - x_0^2 + g(x) = \sum_{i=1}^{d-1} (10 + x_i^2 - 10 * cos(4 * pi * x_i)) + S(x_0) = alpha / (0.2 + x_0) + beta * x_0^2 + + where alpha = 1 and beta = 1. + + The Pareto front corresponds to the equation `f_1 = 1 - f_0^2`, and it is found at + `x_i = 0` for `i > 0` and any value of `x_0` in `(0, 1]`. + """ + + alpha = 1.0 + beta = 1.0 + _x_1_lb = -1.0 + _area_under_curve = 2.0 / 3.0 + _min_dim = 2 + + def _h(self, X: Tensor) -> Tensor: + return 1 - X[..., 0].pow(2) + + def _g(self, X: Tensor) -> Tensor: + x_1_to = X[..., 1:] + return torch.sum( + 10 + x_1_to.pow(2) - 10 * torch.cos(4 * math.pi * x_1_to), + dim=-1, + ) + + def _S(self, X: Tensor) -> Tensor: + x_0 = X[..., 0] + return self.alpha / (0.2 + x_0) + self.beta * x_0.pow(2) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + f_0 = X[..., 0] + # This may encounter 0 / 0, which we set to 0. + f_1 = self._h(X) + torch.nan_to_num(self._g(X) * self._S(X)) + return torch.stack([f_0, f_1], dim=-1)
+
+ + + +
+[docs] +class DH2(DH1): + r"""DH2 test problem. + + This is identical to DH1 except for having `beta = 10.0`. + """ + + beta = 10.0
+ + + +
+[docs] +class DH3(DH): + r"""DH3 test problem. + + d-dimensional problem evaluated on `[0, 1]^2 x [-1, 1]^{d-2}`: + + f_0(x) = x_0 + f_1(x) = h(x_1) * (g(x) + S(x_0)) + h(x_1) = 2 - 0.8 * exp(-((x_1 - 0.35) / 0.25)^2) - exp(-((x_1 - 0.85) / 0.03)^2) + g(x) = \sum_{i=2}^{d-1} (50 * x_i^2) + S(x_0) = 1 - sqrt(x_0) + + The Pareto front is found at `x_i = 0` for `i > 1`. There's a local and a global + Pareto front, which are found at `x_1 = 0.35` and `x_1 = 0.85`, respectively. + The approximate relationships between the objectives at local and global Pareto + fronts are given by `f_1 = 1.2 (1 - sqrt(f_0))` and `f_1 = 1 - f_0`, respectively. + The specific values on the Pareto fronts can be found by varying `x_0`. + """ + + _x_1_lb = 0.0 + _area_under_curve = 0.328449169794718 + _min_dim = 3 + + @staticmethod + def _exp_args(x: Tensor) -> tuple[Tensor, Tensor]: + exp_arg_1 = -((x - 0.35) / 0.25).pow(2) + exp_arg_2 = -((x - 0.85) / 0.03).pow(2) + return exp_arg_1, exp_arg_2 + + def _h(self, X: Tensor) -> Tensor: + exp_arg_1, exp_arg_2 = self._exp_args(X[..., 1]) + return 2 - 0.8 * torch.exp(exp_arg_1) - torch.exp(exp_arg_2) + + def _g(self, X: Tensor) -> Tensor: + return 50 * X[..., 2:].pow(2).sum(dim=-1) + + def _S(self, X: Tensor) -> Tensor: + return 1 - X[..., 0].sqrt() + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + f_0 = X[..., 0] + f_1 = self._h(X) * (self._g(X) + self._S(X)) + return torch.stack([f_0, f_1], dim=-1)
+
+ + + +
+[docs] +class DH4(DH3): + r"""DH4 test problem. + + This is similar to DH3 except that it is evaluated on + `[0, 1] x [-0.15, 1] x [-1, 1]^{d-2}` and: + + h(x_0, x_1) = 2 - x_0 - 0.8 * exp(-((x_0 + x_1 - 0.35) / 0.25)^2) + - exp(-((x_0 + x_1 - 0.85) / 0.03)^2) + + The Pareto front is found at `x_i = 0` for `i > 2`, with the local one being + near `x_0 + x_1 = 0.35` and the global one near `x_0 + x_1 = 0.85`. + """ + + _x_1_lb = -0.15 + _area_under_curve = 0.22845 + + def _h(self, X: Tensor) -> Tensor: + exp_arg_1, exp_arg_2 = self._exp_args(X[..., :2].sum(dim=-1)) + return 2 - X[..., 0] - 0.8 * torch.exp(exp_arg_1) - torch.exp(exp_arg_2)
+ + + +
+[docs] +class DTLZ(MultiObjectiveTestProblem): + r"""Base class for DTLZ problems. + + See [Deb2005dtlz]_ for more details on DTLZ. + """ + + def __init__( + self, + dim: int, + num_objectives: int = 2, + noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension of the function. + num_objectives: Must be less than dim. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + if dim <= num_objectives: + raise ValueError( + f"dim must be > num_objectives, but got {dim} and {num_objectives}." + ) + self.num_objectives = num_objectives + self.dim = dim + self.k = self.dim - self.num_objectives + 1 + self._bounds = [(0.0, 1.0) for _ in range(self.dim)] + self._ref_point = [self._ref_val for _ in range(num_objectives)] + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype)
+ + + +
+[docs] +class DTLZ1(DTLZ): + r"""DLTZ1 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + + f_0(x) = 0.5 * x_0 * (1 + g(x)) + f_1(x) = 0.5 * (1 - x_0) * (1 + g(x)) + g(x) = 100 * \sum_{i=m}^{d-1} ( + k + (x_i - 0.5)^2 - cos(20 * pi * (x_i - 0.5)) + ) + + where k = d - m + 1. + + The pareto front is given by the line (or hyperplane) \sum_i f_i(x) = 0.5. + The goal is to minimize both objectives. The reference point comes from [Yang2019]_. + """ + + _ref_val = 400.0 + + @property + def _max_hv(self) -> float: + return self._ref_val**self.num_objectives - 1 / 2**self.num_objectives + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X_m = X[..., -self.k :] + X_m_minus_half = X_m - 0.5 + sum_term = ( + X_m_minus_half.pow(2) - torch.cos(20 * math.pi * X_m_minus_half) + ).sum(dim=-1) + g_X_m = 100 * (self.k + sum_term) + g_X_m_term = 0.5 * (1 + g_X_m) + fs = [] + for i in range(self.num_objectives): + idx = self.num_objectives - 1 - i + f_i = g_X_m_term * X[..., :idx].prod(dim=-1) + if i > 0: + f_i *= 1 - X[..., idx] + fs.append(f_i) + return torch.stack(fs, dim=-1)
+ + +
+[docs] + def gen_pareto_front(self, n: int) -> Tensor: + r"""Generate `n` pareto optimal points. + + The pareto points randomly sampled from the hyperplane sum_i f(x_i) = 0.5. + """ + f_X = 0.5 * sample_simplex( + n=n, + d=self.num_objectives, + qmc=True, + dtype=self.ref_point.dtype, + device=self.ref_point.device, + ) + if self.negate: + f_X *= -1 + return f_X
+
+ + + +
+[docs] +class DTLZ2(DTLZ): + r"""DLTZ2 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + + f_0(x) = (1 + g(x)) * cos(x_0 * pi / 2) + f_1(x) = (1 + g(x)) * sin(x_0 * pi / 2) + g(x) = \sum_{i=m}^{d-1} (x_i - 0.5)^2 + + The pareto front is given by the unit hypersphere \sum{i} f_i^2 = 1. + Note: the pareto front is completely concave. The goal is to minimize + both objectives. + """ + + _ref_val = 1.1 + + @property + def _max_hv(self) -> float: + # hypercube - volume of hypersphere in R^d such that all coordinates are + # positive + hypercube_vol = self._ref_val**self.num_objectives + pos_hypersphere_vol = ( + math.pi ** (self.num_objectives / 2) + / gamma(self.num_objectives / 2 + 1) + / 2**self.num_objectives + ) + return hypercube_vol - pos_hypersphere_vol + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X_m = X[..., -self.k :] + g_X = (X_m - 0.5).pow(2).sum(dim=-1) + g_X_plus1 = 1 + g_X + fs = [] + pi_over_2 = math.pi / 2 + for i in range(self.num_objectives): + idx = self.num_objectives - 1 - i + f_i = g_X_plus1.clone() + f_i *= torch.cos(X[..., :idx] * pi_over_2).prod(dim=-1) + if i > 0: + f_i *= torch.sin(X[..., idx] * pi_over_2) + fs.append(f_i) + return torch.stack(fs, dim=-1)
+ + +
+[docs] + def gen_pareto_front(self, n: int) -> Tensor: + r"""Generate `n` pareto optimal points. + + The pareto points are randomly sampled from the hypersphere's + positive section. + """ + f_X = sample_hypersphere( + n=n, + d=self.num_objectives, + dtype=self.ref_point.dtype, + device=self.ref_point.device, + qmc=True, + ).abs() + if self.negate: + f_X *= -1 + return f_X
+
+ + + +
+[docs] +class DTLZ3(DTLZ2): + r"""DTLZ3 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + + f_0(x) = (1 + g(x)) * cos(x_0 * pi / 2) + f_1(x) = (1 + g(x)) * sin(x_0 * pi / 2) + g(x) = 100 * [k + \sum_{i=m}^{n-1} (x_i - 0.5)^2 - cos(20 * pi * (x_i - 0.5))] + + `g(x)` introduces (`3k−1`) local Pareto fronts that are parallel to + the one global Pareto-optimal front. + + The global Pareto-optimal front corresponds to x_i = 0.5 for x_i in X_m. + """ + + _ref_val = 10000.0 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X_m = X[..., -self.k :] + g_X = 100 * ( + X_m.shape[-1] + + ((X_m - 0.5).pow(2) - torch.cos(20 * math.pi * (X_m - 0.5))).sum(dim=-1) + ) + g_X_plus1 = 1 + g_X + fs = [] + pi_over_2 = math.pi / 2 + for i in range(self.num_objectives): + idx = self.num_objectives - 1 - i + f_i = g_X_plus1.clone() + f_i *= torch.cos(X[..., :idx] * pi_over_2).prod(dim=-1) + if i > 0: + f_i *= torch.sin(X[..., idx] * pi_over_2) + fs.append(f_i) + return torch.stack(fs, dim=-1)
+
+ + + +
+[docs] +class DTLZ4(DTLZ2): + r"""DTLZ4 test problem. + + This is the same as DTLZ2, but with alpha=100 as the exponent, + resulting in dense solutions near the f_M-f_1 plane. + + The global Pareto-optimal front corresponds to x_i = 0.5 for x_i in X_m. + """ + + _alpha = 100.0
+ + + +
+[docs] +class DTLZ5(DTLZ): + r"""DTLZ5 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + + f_0(x) = (1 + g(x)) * cos(theta_0 * pi / 2) + f_1(x) = (1 + g(x)) * sin(theta_0 * pi / 2) + theta_i = pi / (4 * (1 + g(X_m)) * (1 + 2 * g(X_m) * x_i)) for i = 1, ... , M-2 + g(x) = \sum_{i=m}^{d-1} (x_i - 0.5)^2 + + The global Pareto-optimal front corresponds to x_i = 0.5 for x_i in X_m. + """ + + _ref_val = 10.0 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X_m = X[..., -self.k :] + X_ = X[..., : -self.k] + g_X = (X_m - 0.5).pow(2).sum(dim=-1) + theta = 1 / (2 * (1 + g_X.unsqueeze(-1))) * (1 + 2 * g_X.unsqueeze(-1) * X_) + theta = torch.cat([X[..., :1], theta[..., 1:]], dim=-1) + fs = [] + pi_over_2 = math.pi / 2 + g_X_plus1 = g_X + 1 + for i in range(self.num_objectives): + f_i = g_X_plus1.clone() + f_i *= torch.cos(theta[..., : theta.shape[-1] - i] * pi_over_2).prod(dim=-1) + if i > 0: + f_i *= torch.sin(theta[..., theta.shape[-1] - i] * pi_over_2) + fs.append(f_i) + return torch.stack(fs, dim=-1)
+
+ + + +
+[docs] +class DTLZ7(DTLZ): + r"""DTLZ7 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + f_0(x) = x_0 + f_1(x) = x_1 + ... + f_{M-1}(x) = (1 + g(X_m)) * h(f_0, f_1, ..., f_{M-2}, g, x) + h(f_0, f_1, ..., f_{M-2}, g, x) = + M - sum_{i=0}^{M-2} f_i(x)/(1+g(x)) * (1 + sin(3 * pi * f_i(x))) + + This test problem has 2M-1 disconnected Pareto-optimal regions in the search space. + + The pareto frontier corresponds to X_m = 0. + """ + + _ref_val = 15.0 + +
+[docs] + def evaluate_true(self, X): + f = [] + for i in range(0, self.num_objectives - 1): + f.append(X[..., i]) + f = torch.stack(f, dim=-1) + + g_X = 1 + 9 / self.k * torch.sum(X[..., -self.k :], dim=-1) + h = self.num_objectives - torch.sum( + f / (1 + g_X.unsqueeze(-1)) * (1 + torch.sin(3 * math.pi * f)), dim=-1 + ) + return torch.cat([f, ((1 + g_X) * h).unsqueeze(-1)], dim=-1)
+
+ + + +
+[docs] +class GMM(MultiObjectiveTestProblem): + r"""A test problem where each objective is a Gaussian mixture model. + + This implementation is adapted from the single objective version (proposed by + [Frohlich2020]_) at + https://github.com/boschresearch/NoisyInputEntropySearch/blob/master/ + core/util/objectives.py. + + See [Daulton2022]_ for details on this multi-objective problem. + """ + + dim = 2 + _bounds = [(0.0, 1.0), (0.0, 1.0)] + + def __init__( + self, + noise_std: None | float | list[float] = None, + negate: bool = False, + num_objectives: int = 2, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + noise_std: Standard deviation of the observation noise. + negate: If True, negate the objectives. + num_objectives: The number of objectives. + dtype: The dtype that is used for the bounds of the function. + """ + if num_objectives not in (2, 3, 4): + raise UnsupportedError("GMM only currently supports 2 to 4 objectives.") + self._ref_point = [-0.2338, -0.2211] + if num_objectives > 2: + self._ref_point.append(-0.5180) + if num_objectives > 3: + self._ref_point.append(-0.1866) + self.num_objectives = num_objectives + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + gmm_pos = torch.tensor( + [ + [[0.2, 0.2], [0.8, 0.2], [0.5, 0.7]], + [[0.07, 0.2], [0.4, 0.8], [0.85, 0.1]], + ] + ) + gmm_var = torch.tensor([[0.20, 0.10, 0.10], [0.2, 0.1, 0.05]]).pow(2) + gmm_norm = 2 * pi * gmm_var * torch.tensor([0.5, 0.7, 0.7]) + if num_objectives > 2: + gmm_pos = torch.cat( + [gmm_pos, torch.tensor([[[0.08, 0.21], [0.45, 0.75], [0.86, 0.11]]])], + dim=0, + ) + gmm_var = torch.cat( + [gmm_var, torch.tensor([[0.2, 0.1, 0.07]]).pow(2)], dim=0 + ) + gmm_norm = torch.cat( + [ + gmm_norm, + 2 * pi * gmm_var[2] * torch.tensor([[0.5, 0.7, 0.9]]), + ], + dim=0, + ) + if num_objectives > 3: + gmm_pos = torch.cat( + [gmm_pos, torch.tensor([[[0.09, 0.19], [0.44, 0.72], [0.89, 0.13]]])], + dim=0, + ) + gmm_var = torch.cat( + [gmm_var, torch.tensor([[0.15, 0.07, 0.09]]).pow(2)], dim=0 + ) + gmm_norm = torch.cat( + [ + gmm_norm, + 2 * pi * gmm_var[3] * torch.tensor([[0.5, 0.7, 0.9]]), + ], + dim=0, + ) + gmm_covar = gmm_var.view(*gmm_var.shape, 1, 1) * torch.eye( + 2, dtype=gmm_var.dtype, device=gmm_var.device + ) + self.register_buffer("gmm_pos", gmm_pos) + self.register_buffer("gmm_covar", gmm_covar) + self.register_buffer("gmm_norm", gmm_norm) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + r"""Evaluate the GMMs.""" + # This needs to be reinstantiated because MVN apparently does not + # have a `to` method to make it device/dtype agnostic. + mvn = MultivariateNormal(loc=self.gmm_pos, covariance_matrix=self.gmm_covar) + view_shape = ( + X.shape[:-1] + + torch.Size([1] * (self.gmm_pos.ndim - 1)) + + self.gmm_pos.shape[-1:] + ) + expand_shape = X.shape[:-1] + self.gmm_pos.shape + pdf_X = mvn.log_prob(X.view(view_shape).expand(expand_shape)).exp() + # Multiply by -1 to make this a minimization problem by default + return -(self.gmm_norm * pdf_X).sum(dim=-1)
+
+ + + +
+[docs] +class Penicillin(MultiObjectiveTestProblem): + r"""A penicillin production simulator from [Liang2021]_. + + This implementation is adapted from + https://github.com/HarryQL/TuRBO-Penicillin. + + The goal is to maximize the penicillin yield while minimizing + time to ferment and the CO2 byproduct. + + The function is defined for minimization of all objectives. + + The reference point was set using the `infer_reference_point` heuristic + on the Pareto frontier over a large discrete set of random designs. + """ + + dim = 7 + num_objectives = 3 + _bounds = [ + (60.0, 120.0), + (0.05, 18.0), + (293.0, 303.0), + (0.05, 18.0), + (0.01, 0.5), + (500.0, 700.0), + (5.0, 6.5), + ] + _ref_point = [1.85, 86.93, 514.70] + + Y_xs = 0.45 + Y_ps = 0.90 + K_1 = 10 ** (-10) + K_2 = 7 * 10 ** (-5) + m_X = 0.014 + alpha_1 = 0.143 + alpha_2 = 4 * 10 ** (-7) + alpha_3 = 10 ** (-4) + mu_X = 0.092 + K_X = 0.15 + mu_p = 0.005 + K_p = 0.0002 + K_I = 0.10 + K = 0.04 + k_g = 7.0 * 10**3 + E_g = 5100.0 + k_d = 10.0**33 + E_d = 50000.0 + lambd = 2.5 * 10 ** (-4) + T_v = 273.0 # Kelvin + T_o = 373.0 + R = 1.9872 # CAL/(MOL K) + V_max = 180.0 + +
+[docs] + @classmethod + def penicillin_vectorized(cls, X_input: Tensor) -> Tensor: + r"""Penicillin simulator, simplified and vectorized. + + The 7 input parameters are (in order): culture volume, biomass + concentration, temperature, glucose concentration, substrate feed + rate, substrate feed concentration, and H+ concentration. + + Args: + X_input: A `n x 7`-dim tensor of inputs. + + Returns: + An `n x 3`-dim tensor of (negative) penicillin yield, CO2 and time. + """ + V, X, T, S, F, s_f, H_ = torch.split(X_input, 1, -1) + P, CO2 = torch.zeros_like(V), torch.zeros_like(V) + H = torch.full_like(H_, 10.0).pow(-H_) + + active = torch.ones_like(V).bool() + t_tensor = torch.full_like(V, 2500) + + for t in range(1, 2501): + if active.sum() == 0: + break + F_loss = ( + V[active] + * cls.lambd + * (torch.exp(5 * ((T[active] - cls.T_o) / (cls.T_v - cls.T_o))) - 1) + ) + dV_dt = F[active] - F_loss + mu = ( + (cls.mu_X / (1 + cls.K_1 / H[active] + H[active] / cls.K_2)) + * (S[active] / (cls.K_X * X[active] + S[active])) + * ( + (cls.k_g * torch.exp(-cls.E_g / (cls.R * T[active]))) + - (cls.k_d * torch.exp(-cls.E_d / (cls.R * T[active]))) + ) + ) + dX_dt = mu * X[active] - (X[active] / V[active]) * dV_dt + mu_pp = cls.mu_p * ( + S[active] / (cls.K_p + S[active] + S[active].pow(2) / cls.K_I) + ) + dS_dt = ( + -(mu / cls.Y_xs) * X[active] + - (mu_pp / cls.Y_ps) * X[active] + - cls.m_X * X[active] + + F[active] * s_f[active] / V[active] + - (S[active] / V[active]) * dV_dt + ) + dP_dt = ( + (mu_pp * X[active]) + - cls.K * P[active] + - (P[active] / V[active]) * dV_dt + ) + dCO2_dt = cls.alpha_1 * dX_dt + cls.alpha_2 * X[active] + cls.alpha_3 + + # UPDATE + P[active] = P[active] + dP_dt # Penicillin concentration + V[active] = V[active] + dV_dt # Culture medium volume + X[active] = X[active] + dX_dt # Biomass concentration + S[active] = S[active] + dS_dt # Glucose concentration + CO2[active] = CO2[active] + dCO2_dt # CO2 concentration + + # Update active indices + full_dpdt = torch.ones_like(P) + full_dpdt[active] = dP_dt + inactive = (V > cls.V_max) + (S < 0) + (full_dpdt < 10e-12) + t_tensor[inactive] = torch.minimum( + t_tensor[inactive], torch.full_like(t_tensor[inactive], t) + ) + active[inactive] = 0 + + return torch.stack([-P, CO2, t_tensor], dim=-1)
+ + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + # This uses in-place operations. Hence, the clone is to avoid modifying + # the original X in-place. + return self.penicillin_vectorized(X.view(-1, self.dim).clone()).view( + *X.shape[:-1], self.num_objectives + )
+
+ + + +
+[docs] +class ToyRobust(MultiObjectiveTestProblem): + r"""A 1D problem where the Pareto frontier is sensitive to input noise. + + Specifically, the pareto frontier over the nominal objectives is + sensitive to input noise. The first objective is a mixture of a linear + function and a sinusoidal function, and the second objective is a modified + Levy function, where the second parameter is fixed. + + This function comes from [Daulton2022]_. + + The reference point was set using the `infer_reference_point` + heuristic on the Pareto frontier over a large discrete set of + random designs. + """ + + dim = 1 + _bounds = [(0.0, 0.7)] + _ref_point = [-6.1397, -8.1942] + num_objectives = 2 + levy = Levy() + +
+[docs] + def f_1(self, X: Tensor) -> Tensor: + p1 = 2.4 - 10 * X - 0.1 * X.pow(2) + p2 = 2 * X - 0.1 * X.pow(2) + smoother = (X - 0.5).pow(2) + torch.sin(30 * X) * 0.1 + x_mask = torch.sigmoid((0.2 - X) / 0.005) + return -(p1 * x_mask + p2 * (1 - x_mask) + smoother) * 30 + 30
+ + +
+[docs] + def f_2(self, X: Tensor) -> Tensor: + X = torch.cat( + [X, torch.zeros_like(X)], + dim=-1, + ) + # Cut out the first part of the function. + X = X * 0.95 + 0.03 + X = unnormalize(X, self.levy.bounds.to(X)) + Y = self.levy(X).unsqueeze(-1) + Y -= X[..., :1].pow(2) * 0.75 + return Y
+ + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + return -torch.cat([self.f_1(X), self.f_2(X)], dim=-1)
+
+ + + +
+[docs] +class VehicleSafety(MultiObjectiveTestProblem): + r"""Optimize Vehicle crash-worthiness. + + See [Tanabe2020]_ for details. + + The reference point is 1.1 * the nadir point from + approximate front provided by [Tanabe2020]_. + + The maximum hypervolume is computed using the approximate + pareto front from [Tanabe2020]_. + """ + + _ref_point = [1864.72022, 11.81993945, 0.2903999384] + _max_hv = 246.81607081187002 + _bounds = [(1.0, 3.0)] * 5 + dim = 5 + num_objectives = 3 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X1, X2, X3, X4, X5 = torch.split(X, 1, -1) + f1 = ( + 1640.2823 + + 2.3573285 * X1 + + 2.3220035 * X2 + + 4.5688768 * X3 + + 7.7213633 * X4 + + 4.4559504 * X5 + ) + f2 = ( + 6.5856 + + 1.15 * X1 + - 1.0427 * X2 + + 0.9738 * X3 + + 0.8364 * X4 + - 0.3695 * X1 * X4 + + 0.0861 * X1 * X5 + + 0.3628 * X2 * X4 + - 0.1106 * X1.pow(2) + - 0.3437 * X3.pow(2) + + 0.1764 * X4.pow(2) + ) + f3 = ( + -0.0551 + + 0.0181 * X1 + + 0.1024 * X2 + + 0.0421 * X3 + - 0.0073 * X1 * X2 + + 0.024 * X2 * X3 + - 0.0118 * X2 * X4 + - 0.0204 * X3 * X4 + - 0.008 * X3 * X5 + - 0.0241 * X2.pow(2) + + 0.0109 * X4.pow(2) + ) + f_X = torch.cat([f1, f2, f3], dim=-1) + return f_X
+
+ + + +
+[docs] +class ZDT(MultiObjectiveTestProblem): + r"""Base class for ZDT problems. + + See [Zitzler2000]_ for more details on ZDT. + """ + + _ref_point = [11.0, 11.0] + + def __init__( + self, + dim: int, + num_objectives: int = 2, + noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension of the function. + num_objectives: Number of objectives. Must not be larger than dim. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + if num_objectives != 2: + raise NotImplementedError( + f"{type(self).__name__} currently only supports 2 objectives." + ) + if dim < num_objectives: + raise ValueError( + f"dim must be >= num_objectives, but got {dim} and {num_objectives}" + ) + self.num_objectives = num_objectives + self.dim = dim + self._bounds = [(0.0, 1.0) for _ in range(self.dim)] + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + + @staticmethod + def _g(X: Tensor) -> Tensor: + return 1 + 9 * X[..., 1:].mean(dim=-1)
+ + + +
+[docs] +class ZDT1(ZDT): + r"""ZDT1 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + + f_0(x) = x_0 + f_1(x) = g(x) * (1 - sqrt(x_0 / g(x)) + g(x) = 1 + 9 / (d - 1) * \sum_{i=1}^{d-1} x_i + + The reference point comes from [Yang2019a]_. + + The pareto front is convex. + """ + + _max_hv = 120 + 2 / 3 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + f_0 = X[..., 0] + g = self._g(X=X) + f_1 = g * (1 - (f_0 / g).sqrt()) + return torch.stack([f_0, f_1], dim=-1)
+ + +
+[docs] + def gen_pareto_front(self, n: int) -> Tensor: + f_0 = torch.linspace( + 0, 1, n, dtype=self.bounds.dtype, device=self.bounds.device + ) + f_1 = 1 - f_0.sqrt() + f_X = torch.stack([f_0, f_1], dim=-1) + if self.negate: + f_X *= -1 + return f_X
+
+ + + +
+[docs] +class ZDT2(ZDT): + r"""ZDT2 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + + f_0(x) = x_0 + f_1(x) = g(x) * (1 - (x_0 / g(x))^2) + g(x) = 1 + 9 / (d - 1) * \sum_{i=1}^{d-1} x_i + + The reference point comes from [Yang2019a]_. + + The pareto front is concave. + """ + + _max_hv = 120 + 1 / 3 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + f_0 = X[..., 0] + g = self._g(X=X) + f_1 = g * (1 - (f_0 / g).pow(2)) + return torch.stack([f_0, f_1], dim=-1)
+ + +
+[docs] + def gen_pareto_front(self, n: int) -> Tensor: + f_0 = torch.linspace( + 0, 1, n, dtype=self.bounds.dtype, device=self.bounds.device + ) + f_1 = 1 - f_0.pow(2) + f_X = torch.stack([f_0, f_1], dim=-1) + if self.negate: + f_X *= -1 + return f_X
+
+ + + +
+[docs] +class ZDT3(ZDT): + r"""ZDT3 test problem. + + d-dimensional problem evaluated on `[0, 1]^d`: + + f_0(x) = x_0 + f_1(x) = 1 - sqrt(x_0 / g(x)) - x_0 / g * sin(10 * pi * x_0) + g(x) = 1 + 9 / (d - 1) * \sum_{i=1}^{d-1} x_i + + The reference point comes from [Yang2019a]_. + + The pareto front consists of several discontinuous convex parts. + """ + + _max_hv = 128.77811613069076060 + _parts = [ + # this interval includes both end points + [0, 0.0830015349], + # this interval includes only the right end points + [0.1822287280, 0.2577623634], + [0.4093136748, 0.4538821041], + [0.6183967944, 0.6525117038], + [0.8233317983, 0.8518328654], + ] + # nugget to make sure linspace returns elements within the specified range + _eps = 1e-6 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + f_0 = X[..., 0] + g = self._g(X=X) + f_1 = 1 - (f_0 / g).sqrt() - f_0 / g * torch.sin(10 * math.pi * f_0) + return torch.stack([f_0, f_1], dim=-1)
+ + +
+[docs] + def gen_pareto_front(self, n: int) -> Tensor: + n_parts = len(self._parts) + n_per_part = torch.full( + torch.Size([n_parts]), + n // n_parts, + dtype=torch.long, + device=self.bounds.device, + ) + left_over = n % n_parts + n_per_part[:left_over] += 1 + f_0s = [] + for i, p in enumerate(self._parts): + left, right = p + f_0s.append( + torch.linspace( + left + self._eps, + right - self._eps, + int(n_per_part[i]), + dtype=self.bounds.dtype, + device=self.bounds.device, + ) + ) + f_0 = torch.cat(f_0s, dim=0) + f_1 = 1 - f_0.sqrt() - f_0 * torch.sin(10 * math.pi * f_0) + f_X = torch.stack([f_0, f_1], dim=-1) + if self.negate: + f_X *= -1 + return f_X
+
+ + + +
+[docs] +class CarSideImpact(MultiObjectiveTestProblem): + r"""Car side impact problem. + + See [Tanabe2020]_ for details. + + The reference point is `nadir + 0.1 * (ideal - nadir)` + where the ideal and nadir points come from the approximate + Pareto frontier from [Tanabe2020]_. The max_hv was computed + based on the approximate Pareto frontier from [Tanabe2020]_. + """ + + num_objectives: int = 4 + dim: int = 7 + _bounds = [ + (0.5, 1.5), + (0.45, 1.35), + (0.5, 1.5), + (0.5, 1.5), + (0.875, 2.625), + (0.4, 1.2), + (0.4, 1.2), + ] + _ref_point = [45.4872, 4.5114, 13.3394, 10.3942] + _max_hv = 484.72654347642793 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X1, X2, X3, X4, X5, X6, X7 = torch.split(X, 1, -1) + f1 = ( + 1.98 + + 4.9 * X1 + + 6.67 * X2 + + 6.98 * X3 + + 4.01 * X4 + + 1.78 * X5 + + 10**-5 * X6 + + 2.73 * X7 + ) + f2 = 4.72 - 0.5 * X4 - 0.19 * X2 * X3 + V_MBP = 10.58 - 0.674 * X1 * X2 - 0.67275 * X2 + V_FD = 16.45 - 0.489 * X3 * X7 - 0.843 * X5 * X6 + f3 = 0.5 * (V_MBP + V_FD) + g1 = 1 - 1.16 + 0.3717 * X2 * X4 + 0.0092928 * X3 + g2 = ( + 0.32 + - 0.261 + + 0.0159 * X1 * X2 + + 0.06486 * X1 + + 0.019 * X2 * X7 + - 0.0144 * X3 * X5 + - 0.0154464 * X6 + ) + g3 = ( + 0.32 + - 0.214 + - 0.00817 * X5 + + 0.045195 * X1 + + 0.0135168 * X1 + - 0.03099 * X2 * X6 + + 0.018 * X2 * X7 + - 0.007176 * X3 + - 0.023232 * X3 + + 0.00364 * X5 * X6 + + 0.018 * X2.pow(2) + ) + g4 = 0.32 - 0.74 + 0.61 * X2 + 0.031296 * X3 + 0.031872 * X7 - 0.227 * X2.pow(2) + g5 = 32 - 28.98 - 3.818 * X3 + 4.2 * X1 * X2 - 1.27296 * X6 + 2.68065 * X7 + g6 = ( + 32 + - 33.86 + - 2.95 * X3 + + 5.057 * X1 * X2 + + 3.795 * X2 + + 3.4431 * X7 + - 1.45728 + ) + g7 = 32 - 46.36 + 9.9 * X2 + 4.4505 * X1 + g8 = 4 - f2 + g9 = 9.9 - V_MBP + g10 = 15.7 - V_FD + g = torch.cat([g1, g2, g3, g4, g5, g6, g7, g8, g9, g10], dim=-1) + zero = torch.tensor(0.0, dtype=X.dtype, device=X.device) + g = torch.where(g < 0, -g, zero) + f4 = g.sum(dim=-1, keepdim=True) + return torch.cat([f1, f2, f3, f4], dim=-1)
+
+ + + +# ------ Constrained Multi-Objective Test Problems ----- # + + +
+[docs] +class BNH(MultiObjectiveTestProblem, ConstrainedBaseTestProblem): + r"""The constrained BNH problem. + + See [GarridoMerchan2020]_ for more details on this problem. Note that this is a + minimization problem. + """ + + dim = 2 + num_objectives = 2 + num_constraints = 2 + _bounds = [(0.0, 5.0), (0.0, 3.0)] + _ref_point = [0.0, 0.0] # TODO: Determine proper reference point + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + return torch.stack( + [4.0 * X.pow(2).sum(dim=-1), (X - 5.0).pow(2).sum(dim=-1)], dim=-1 + )
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + c1 = 25.0 - (X[..., 0] - 5.0).pow(2) - X[..., 1].pow(2) + c2 = (X[..., 0] - 8.0).pow(2) + (X[..., 1] + 3.0).pow(2) - 7.7 + return torch.stack([c1, c2], dim=-1)
+
+ + + +
+[docs] +class CONSTR(MultiObjectiveTestProblem, ConstrainedBaseTestProblem): + r"""The constrained CONSTR problem. + + See [GarridoMerchan2020]_ for more details on this problem. Note that this is a + minimization problem. + """ + + dim = 2 + num_objectives = 2 + num_constraints = 2 + _bounds = [(0.1, 10.0), (0.0, 5.0)] + _ref_point = [10.0, 10.0] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + obj1 = X[..., 0] + obj2 = (1.0 + X[..., 1]) / X[..., 0] + return torch.stack([obj1, obj2], dim=-1)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + c1 = 9.0 * X[..., 0] + X[..., 1] - 6.0 + c2 = 9.0 * X[..., 0] - X[..., 1] - 1.0 + return torch.stack([c1, c2], dim=-1)
+
+ + + +
+[docs] +class ConstrainedBraninCurrin(BraninCurrin, ConstrainedBaseTestProblem): + r"""Constrained Branin Currin Function. + + This uses the disk constraint from [Gelbart2014]_. + """ + + dim = 2 + num_objectives = 2 + num_constraints = 1 + _bounds = [(0.0, 1.0), (0.0, 1.0)] + _con_bounds = [(-5.0, 10.0), (0.0, 15.0)] + _ref_point = [80.0, 12.0] + _max_hv = 608.4004237022673 # from NSGA-II with 90k evaluations + + def __init__( + self, + noise_std: None | float | list[float] = None, + constraint_noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + noise_std: Standard deviation of the observation noise of the objectives. + constraint_noise_std: Standard deviation of the observation noise of the + constraint. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + con_bounds = torch.tensor(self._con_bounds, dtype=self.bounds.dtype).transpose( + -1, -2 + ) + self.register_buffer("con_bounds", con_bounds) + self.constraint_noise_std = constraint_noise_std + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + X_tf = unnormalize(X, self.con_bounds) + return 50 - (X_tf[..., 0:1] - 2.5).pow(2) - (X_tf[..., 1:2] - 7.5).pow(2)
+
+ + + +
+[docs] +class C2DTLZ2(DTLZ2, ConstrainedBaseTestProblem): + num_constraints = 1 + _r = 0.2 + # approximate from nsga-ii, TODO: replace with analytic + _max_hv = 0.3996406303723544 + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + if X.ndim > 2: + raise NotImplementedError("Batch X is not supported.") + f_X = self.evaluate_true(X) + term1 = (f_X - 1).pow(2) + mask = ~(torch.eye(f_X.shape[-1], device=f_X.device).bool()) + indices = torch.arange(f_X.shape[1], device=f_X.device).repeat(f_X.shape[1], 1) + indexer = indices[mask].view(f_X.shape[1], f_X.shape[-1] - 1) + term2_inner = ( + f_X.unsqueeze(1) + .expand(f_X.shape[0], f_X.shape[-1], f_X.shape[-1]) + .gather(dim=-1, index=indexer.repeat(f_X.shape[0], 1, 1)) + ) + term2 = (term2_inner.pow(2) - self._r**2).sum(dim=-1) + min1 = (term1 + term2).min(dim=-1).values + min2 = ((f_X - 1 / math.sqrt(f_X.shape[-1])).pow(2) - self._r**2).sum(dim=-1) + return -torch.min(min1, min2).unsqueeze(-1)
+
+ + + +
+[docs] +class DiscBrake(MultiObjectiveTestProblem, ConstrainedBaseTestProblem): + r"""The Disc Brake problem. + + There are 2 objectives and 4 constraints. + + Both objectives should be minimized. + + See [Tanabe2020]_ for details. + + The reference point was set using the `infer_reference_point` + heuristic on the Pareto frontier over a large discrete set of + random designs. + """ + + dim = 4 + num_objectives = 2 + num_constraints = 4 + _bounds = [(55.0, 80.0), (75.0, 110.0), (1000.0, 3000.0), (11.0, 20.0)] + _ref_point = [5.7771, 3.9651] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + f = torch.zeros( + *X.shape[:-1], self.num_objectives, dtype=X.dtype, device=X.device + ) + + X1, X2, X3, X4 = torch.split(X, 1, -1) + sq_diff = X2.pow(2) - X1.pow(2) + f[..., :1] = 4.9 * 1e-5 * sq_diff * (X4 - 1.0) + f[..., 1:] = (9.82 * 1e6) * sq_diff / (X3 * X4 * (X2.pow(3) - X1.pow(3))) + + return f
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + g = torch.zeros( + *X.shape[:-1], self.num_constraints, dtype=X.dtype, device=X.device + ) + X1, X2, X3, X4 = torch.split(X, 1, -1) + sq_diff = X2.pow(2) - X1.pow(2) + cub_diff = X2.pow(3) - X1.pow(3) + g[..., :1] = X2 - X1 - 20.0 + g[..., 1:2] = 0.4 - X3 / (3.14 * sq_diff) + g[..., 2:3] = 1.0 - (2.22 * 1e-3 * X3 * cub_diff) / sq_diff.pow(2) + g[..., 3:] = (2.66 * 1e-2 * X3 * X4 * cub_diff) / sq_diff - 900.0 + return g
+
+ + + +
+[docs] +class MW7(MultiObjectiveTestProblem, ConstrainedBaseTestProblem): + r"""The MW7 problem. + + This problem has 2 objectives, 2 constraints, and a disconnected Pareto + frontier. It supports arbitrary input dimension > 1. See [Ma2019]_ for details. + + This implementation is adapted from: + https://github.com/anyoptimization/pymoo/blob/master/pymoo/problems/multi/mw.py + """ + + num_constraints = 2 + num_objectives = 2 + _ref_point = [1.2, 1.2] + + def __init__( + self, + dim: int, + noise_std: None | float | list[float] = None, + constraint_noise_std: None | float | list[float] = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension of the function. Must be at least 2. + noise_std: Standard deviation of the observation noise of the objectives. + constraint_noise_std: Standard deviation of the observation noise of the + constraints. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + if dim < 2: + raise ValueError("dim must be greater than or equal to 2.") + self.dim = dim + self._bounds = [(0.0, 1.0) for _ in range(self.dim)] + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + self.constraint_noise_std = constraint_noise_std + +
+[docs] + def LA2(self, A, B, C, D, theta) -> Tensor: + return A * torch.sin(B * theta.pow(C)).pow(D)
+ + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + a = X[..., :-1] - 0.5 + contrib = 2 * (X[..., 1:] + a.pow(2) - 1).pow(2) + g = 1 + contrib.sum(dim=-1) + f0 = g * X[..., 0] + f1 = g * torch.sqrt(1 - (f0 / g).pow(2)) + return torch.stack([f0, f1], dim=-1)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + ff = self.evaluate_true(X) + f0, f1 = ff[..., 0], ff[..., 1] + atan = torch.arctan(f1 / f0) + g0 = ( + f0.pow(2) + + f1.pow(2) + - (1.2 + (self.LA2(0.4, 4.0, 1.0, 16.0, atan)).abs()).pow(2) + ) + g1 = (1.15 - self.LA2(0.2, 4.0, 1.0, 8.0, atan)).pow(2) - f0.pow(2) - f1.pow(2) + return -torch.stack([g0, g1], dim=-1)
+
+ + + +
+[docs] +class OSY(MultiObjectiveTestProblem, ConstrainedBaseTestProblem): + r""" + The OSY test problem from [Oszycka1995]_. + Implementation from + https://github.com/msu-coinlab/pymoo/blob/master/pymoo/problems/multi/osy.py + Note that this implementation assumes minimization, so please choose negate=True. + """ + + dim = 6 + num_constraints = 6 + num_objectives = 2 + _bounds = [ + (0.0, 10.0), + (0.0, 10.0), + (1.0, 5.0), + (0.0, 6.0), + (1.0, 5.0), + (0.0, 10.0), + ] + _ref_point = [-75.0, 75.0] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + f1 = -( + 25 * (X[..., 0] - 2).pow(2) + + (X[..., 1] - 2).pow(2) + + (X[..., 2] - 1).pow(2) + + (X[..., 3] - 4).pow(2) + + (X[..., 4] - 1).pow(2) + ) + f2 = X.pow(2).sum(-1) + return torch.stack([f1, f2], dim=-1)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + g1 = X[..., 0] + X[..., 1] - 2.0 + g2 = 6.0 - X[..., 0] - X[..., 1] + g3 = 2.0 - X[..., 1] + X[..., 0] + g4 = 2.0 - X[..., 0] + 3.0 * X[..., 1] + g5 = 4.0 - (X[..., 2] - 3.0).pow(2) - X[..., 3] + g6 = (X[..., 4] - 3.0).pow(2) + X[..., 5] - 4.0 + return torch.stack([g1, g2, g3, g4, g5, g6], dim=-1)
+
+ + + +
+[docs] +class SRN(MultiObjectiveTestProblem, ConstrainedBaseTestProblem): + r"""The constrained SRN problem. + + See [GarridoMerchan2020]_ for more details on this problem. Note that this is a + minimization problem. + """ + + dim = 2 + num_objectives = 2 + num_constraints = 2 + _bounds = [(-20.0, 20.0), (-20.0, 20.0)] + _ref_point = [0.0, 0.0] # TODO: Determine proper reference point + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + obj1 = 2.0 + (X - 2.0).pow(2).sum(dim=-1) + obj2 = 9.0 * X[..., 0] - (X[..., 1] - 1.0).pow(2) + return torch.stack([obj1, obj2], dim=-1)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + c1 = 225.0 - (X.pow(2)).pow(2).sum(dim=-1) + c2 = -10.0 - X[..., 0] + 3 * X[..., 1] + return torch.stack([c1, c2], dim=-1)
+
+ + + +
+[docs] +class WeldedBeam(MultiObjectiveTestProblem, ConstrainedBaseTestProblem): + r""" + The Welded Beam multi-objective test problem. Similar to `WeldedBeamSO` in + `botorch.test_function.synthetic`, but with an additional output, somewhat + modified constraints, and a different domain. + + Implementation from + https://github.com/msu-coinlab/pymoo/blob/master/pymoo/problems/multi/welded_beam.py + Note that this implementation assumes minimization, so please choose negate=True. + """ + + dim = 4 + num_constraints = 4 + num_objectives = 2 + _bounds = [ + (0.125, 5.0), + (0.1, 10.0), + (0.1, 10.0), + (0.125, 5.0), + ] + _ref_point = [40, 0.015] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + # We could do the following, but the constraints are using somewhat + # different numbers (see below). + # f1 = WeldedBeam.evaluate_true(self, X) + x1, x2, x3, x4 = X.unbind(-1) + f1 = 1.10471 * x1.pow(2) * x2 + 0.04811 * x3 * x4 * (14.0 + x2) + f2 = 2.1952 / (x4 * x3.pow(3)) + return torch.stack([f1, f2], dim=-1)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + x1, x2, x3, x4 = X.unbind(-1) + P = 6000.0 + L = 14.0 + t_max = 13600.0 + s_max = 30000.0 + + # Ideally, we could just do the following, but the numbers in the + # single-outcome WeldedBeam are different (see below) + # g1_, g2_, g3_, _, _, g6_ = WeldedBeam.evaluate_slack_true(self, X) + # g1 = g1_ / t_max + # g2 = g2_ / s_max + # g3 = 1 / (5 - 0.125) * g3_ + # g4 = 1 / P * g6_ + + R = torch.sqrt(0.25 * (x2.pow(2) + (x1 + x3).pow(2))) + M = P * (L + x2 / 2) + # This `J` is different than the one in [CoelloCoello2002constraint]_ + # by a factor of 2 (sqrt(2) instead of sqrt(0.5)) + J = 2 * math.sqrt(0.5) * x1 * x2 * (x2.pow(2) / 12 + 0.25 * (x1 + x3).pow(2)) + t1 = P / (math.sqrt(2) * x1 * x2) + t2 = M * R / J + t = torch.sqrt(t1.pow(2) + t1 * t2 * x2 / R + t2.pow(2)) + s = 6 * P * L / (x4 * x3.pow(2)) + # These numbers are also different from [CoelloCoello2002constraint]_ + P_c = 64746.022 * (1 - 0.0282346 * x3) * x3 * x4.pow(3) + + g1 = (t - t_max) / t_max + g2 = (s - s_max) / s_max + g3 = 1 / (5 - 0.125) * (x1 - x4) + g4 = (P - P_c) / P + + return torch.stack([g1, g2, g3, g4], dim=-1)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_functions/multi_objective_multi_fidelity.html b/website-old/pages/api/_modules/botorch/test_functions/multi_objective_multi_fidelity.html new file mode 100644 index 0000000000..663ef56c15 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_functions/multi_objective_multi_fidelity.html @@ -0,0 +1,233 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_functions.multi_objective_multi_fidelity

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Multi-objective multi-fidelity optimization benchmark problems.
+
+References
+
+.. [Irshad2021]
+    F. Irshad, S. Karsch, and A. Döpp. Expected hypervolume improvement for
+    simultaneous multi-objective and multi-fidelity optimization.
+    arXiv preprint arXiv:2112.13901, 2021.
+"""
+
+import math
+
+import torch
+from botorch.test_functions.base import MultiObjectiveTestProblem
+from torch import Tensor
+
+
+
+[docs] +class MOMFBraninCurrin(MultiObjectiveTestProblem): + r"""Branin-Currin problem for multi-objective-multi-fidelity optimization. + + (2+1)-dimensional function with domain `[0,1]^3` where the last dimension + is the fidelity parameter `s`. + Both functions assume minimization. See [Irshad2021]_ for more details. + + Modified Branin function: + + B(x,s) = 21-(( + 15*x_2 - b(s) * (15 * x_1 - 5) ** 2 + c(s) * (15 * x_1 - 5) - 6 ) ** 2 + + 10 * (1 - t(s)) * cos(15 * x_1 - 5)+10)/22 + + Here `b`, `c`, `r` and `t` are constants and `s` is the fidelity parameter: + where `b = 5.1 / (4 * math.pi ** 2) - 0.01(1-s)`, + `c = 5 / math.pi - 0.1*(1 - s)`, + `r = 6`, + `t = 1 / (8 * math.pi) + 0.05*(1-s)` + + Modified Currin function: + + C(x) = 14-((1 - 0.1(1-s)exp(-1 / (2 * x_2))) * ( + 2300 * x_1 ** 3 + 1900 * x_1 ** 2 + 2092 * x_1 + 60 + ) / 100 * x_1 ** 3 + 500 * x_1 ** 2 + 4 * x_2 + 20)/15 + + """ + + dim = 3 + num_objectives = 2 + _bounds = [(0.0, 1.0) for _ in range(dim)] + _ref_point = [0, 0] + _max_hv = 0.5235514158034145 + + def _branin(self, X: Tensor) -> Tensor: + x1 = X[..., 0] + x2 = X[..., 1] + s = X[..., 2] + + x11 = 15 * x1 - 5 + x22 = 15 * x2 + b = 5.1 / (4 * math.pi**2) - 0.01 * (1 - s) + c = 5 / math.pi - 0.1 * (1 - s) + r = 6 + t = 1 / (8 * math.pi) + 0.05 * (1 - s) + y = (x22 - b * x11**2 + c * x11 - r) ** 2 + 10 * (1 - t) * torch.cos(x11) + 10 + B = 21 - y + return B / 22 + + def _currin(self, X: Tensor) -> Tensor: + x1 = X[..., 0] + x2 = X[..., 1] + s = X[..., 2] + A = 2300 * x1**3 + 1900 * x1**2 + 2092 * x1 + 60 + B = 100 * x1**3 + 500 * x1**2 + 4 * x1 + 20 + y = (1 - 0.1 * (1 - s) * torch.exp(-1 / (2 * x2))) * A / B + C = -y + 14 + return C / 15 + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + branin = self._branin(X) + currin = self._currin(X) + return torch.stack([-branin, -currin], dim=-1)
+
+ + + +
+[docs] +class MOMFPark(MultiObjectiveTestProblem): + r"""Modified Park test functions for multi-objective multi-fidelity optimization. + + (4+1)-dimensional function with domain `[0,1]^5` where the last dimension + is the fidelity parameter `s`. See [Irshad2021]_ for more details. + + The first modified Park function is + + P1(x, s)=A*(T1(x,s)+T2(x,s)-B)/22-0.8 + + The second modified Park function is + + P2(x,s)=A*(5-2/3*exp(x1+x2)-x4*sin(x3)*A+x3-B)/4 - 0.7 + + Here + + T_1(x,s) = (x1+0.001*(1-s))/2*sqrt(1+(x2+x3**2)*x4/(x1**2)) + + T_2(x, s) = (x1+3*x4)*exp(1+sin(x3)) + + and `A(s)=(0.9+0.1*s)`, `B(s)=0.1*(1-s)`. + """ + + dim = 5 + num_objectives = 2 + _bounds = [(0.0, 1.0) for _ in range(dim)] + _ref_point = [0, 0] + _max_hv = 0.08551927363087991 + + def _transform(self, X: Tensor) -> Tensor: + x1 = X[..., 0] + x2 = X[..., 1] + x3 = X[..., 2] + x4 = X[..., 3] + s = X[..., 4] + _x1 = 1 - 2 * (x1 - 0.6) ** 2 + _x2 = x2 + _x3 = 1 - 3 * (x3 - 0.5) ** 2 + _x4 = 1 - (x4 - 0.8) ** 2 + return torch.stack([_x1, _x2, _x3, _x4, s], dim=-1) + + def _park1(self, X: Tensor) -> Tensor: + x1 = X[..., 0] + x2 = X[..., 1] + x3 = X[..., 2] + x4 = X[..., 3] + s = X[..., 4] + T1 = ( + (x1 + 1e-3 * (1 - s)) + / 2 + * torch.sqrt(1 + (x2 + x3**2) * x4 / (x1**2 + 1e-4)) + ) + T2 = (x1 + 3 * x4) * torch.exp(1 + torch.sin(x3)) + A = 0.9 + 0.1 * s + B = 0.1 * (1 - s) + return A * (T1 + T2 - B) / 22 - 0.8 + + def _park2(self, X: Tensor) -> Tensor: + x1 = X[..., 0] + x2 = X[..., 1] + x3 = X[..., 2] + x4 = X[..., 3] + s = X[..., 4] + A = 0.9 + 0.1 * s + B = 0.1 * (1 - s) + return ( + A * (5 - 2 / 3 * torch.exp(x1 + x2) + x4 * torch.sin(x3) * A - x3 + B) / 4 + - 0.7 + ) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + X = self._transform(X) + park1 = self._park1(X) + park2 = self._park2(X) + return torch.stack([-park1, -park2], dim=-1)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_functions/sensitivity_analysis.html b/website-old/pages/api/_modules/botorch/test_functions/sensitivity_analysis.html new file mode 100644 index 0000000000..179f2b03a2 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_functions/sensitivity_analysis.html @@ -0,0 +1,373 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_functions.sensitivity_analysis

+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+import math
+
+import torch
+
+from botorch.test_functions.synthetic import SyntheticTestFunction
+from torch import Tensor
+
+
+
+[docs] +class Ishigami(SyntheticTestFunction): + r"""Ishigami test function. + + three-dimensional function (usually evaluated on `[-pi, pi]^3`): + + f(x) = sin(x_1) + a sin(x_2)^2 + b x_3^4 sin(x_1) + + Here `a` and `b` are constants where a=7 and b=0.1 or b=0.05 + Proposed to test sensitivity analysis methods because it exhibits strong + nonlinearity and nonmonotonicity and a peculiar dependence on x_3. + """ + + def __init__( + self, + b: float = 0.1, + noise_std: float | None = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + b: the b constant, should be 0.1 or 0.05. + noise_std: Standard deviation of the observation noise. + negative: If True, negative the objective. + dtype: The dtype that is used for the bounds of the function. + """ + self._optimizers = None + if b not in (0.1, 0.05): + raise ValueError("b parameter should be 0.1 or 0.05") + self.dim = 3 + if b == 0.1: + self.si = [0.3138, 0.4424, 0] + self.si_t = [0.558, 0.442, 0.244] + self.s_ij = [0, 0.244, 0] + self.dgsm_gradient = [-0.0004, -0.0004, -0.0004] + self.dgsm_gradient_abs = [1.9, 4.45, 1.97] + self.dgsm_gradient_square = [7.7, 24.5, 11] + elif b == 0.05: + self.si = [0.218, 0.687, 0] + self.si_t = [0.3131, 0.6868, 0.095] + self.s_ij = [0, 0.094, 0] + self.dgsm_gradient = [-0.0002, -0.0002, -0.0002] + self.dgsm_gradient_abs = [1.26, 4.45, 1.97] + self.dgsm_gradient_square = [2.8, 24.5, 11] + self._bounds = [(-math.pi, math.pi) for _ in range(self.dim)] + self.b = b + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + + @property + def _optimal_value(self) -> float: + raise NotImplementedError + +
+[docs] + def compute_dgsm(self, X: Tensor) -> tuple[list[float], list[float], list[float]]: + r"""Compute derivative global sensitivity measures. + + This function can be called separately to estimate the dgsm measure + The exact global integrals of these values are already added under + as attributes dgsm_gradient, dgsm_gradient_bas, and dgsm_gradient_square. + + Args: + X: Set of points at which to compute derivative measures. + + Returns: The average gradient, absolute gradient, and square gradients. + """ + dx_1 = torch.cos(X[..., 0]) * (1 + self.b * (X[..., 2] ** 4)) + dx_2 = 14 * torch.cos(X[..., 1]) * torch.sin(X[..., 1]) + dx_3 = 0.4 * (X[..., 2] ** 3) * torch.sin(X[..., 0]) + gradient_measure = [ + torch.mean(dx_1).item(), + torch.mean(dx_1).item(), + torch.mean(dx_1).item(), + ] + gradient_absolute_measure = [ + torch.mean(torch.abs(dx_1)).item(), + torch.mean(torch.abs(dx_2)).item(), + torch.mean(torch.abs(dx_3)).item(), + ] + gradient_square_measure = [ + torch.mean(torch.pow(dx_1, 2)).item(), + torch.mean(torch.pow(dx_2, 2)).item(), + torch.mean(torch.pow(dx_3, 2)).item(), + ] + return gradient_measure, gradient_absolute_measure, gradient_square_measure
+ + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + self.to(device=X.device, dtype=X.dtype) + t = ( + torch.sin(X[..., 0]) + + 7 * (torch.sin(X[..., 1]) ** 2) + + self.b * (X[..., 2] ** 4) * torch.sin(X[..., 0]) + ) + return t
+
+ + + +
+[docs] +class Gsobol(SyntheticTestFunction): + r"""Gsobol test function. + + d-dimensional function (usually evaluated on `[0, 1]^d`): + + f(x) = Prod_{i=1}\^{d} ((\|4x_i-2\|+a_i)/(1+a_i)), a_i >=0 + + common combinations of dimension and a vector: + + dim=8, a= [0, 1, 4.5, 9, 99, 99, 99, 99] + dim=6, a=[0, 0.5, 3, 9, 99, 99] + dim = 15, a= [1, 2, 5, 10, 20, 50, 100, 500, 1000, ..., 1000] + + Proposed to test sensitivity analysis methods + First order Sobol indices have closed form expression S_i=V_i/V with : + + V_i= 1/(3(1+a_i)\^2) + V= Prod_{i=1}\^{d} (1+V_i) - 1 + + """ + + def __init__( + self, + dim: int, + a: list = None, + noise_std: float | None = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: Dimensionality of the problem. If 6, 8, or 15, will use standard a. + a: a parameter, unless dim is 6, 8, or 15. + noise_std: Standard deviation of observation noise. + negate: Return negative of function. + dtype: The dtype that is used for the bounds of the function. + """ + self._optimizers = None + self.dim = dim + self._bounds = [(0, 1) for _ in range(self.dim)] + if self.dim == 6: + self.a = [0, 0.5, 3, 9, 99, 99] + elif self.dim == 8: + self.a = [0, 1, 4.5, 9, 99, 99, 99, 99] + elif self.dim == 15: + self.a = [ + 1, + 2, + 5, + 10, + 20, + 50, + 100, + 500, + 1000, + 1000, + 1000, + 1000, + 1000, + 1000, + 1000, + ] + else: + self.a = a + self.optimal_sobol_indicies() + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + + @property + def _optimal_value(self) -> float: + raise NotImplementedError + +
+[docs] + def optimal_sobol_indicies(self): + vi = [] + for i in range(self.dim): + vi.append(1 / (3 * ((1 + self.a[i]) ** 2))) + self.vi = Tensor(vi) + self.V = torch.prod(1 + self.vi) - 1 + self.si = self.vi / self.V + si_t = [] + for i in range(self.dim): + si_t.append( + ( + self.vi[i] + * torch.prod(self.vi[:i] + 1) + * torch.prod(self.vi[i + 1 :] + 1) + ) + / self.V + ) + self.si_t = Tensor(si_t)
+ + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + self.to(device=X.device, dtype=X.dtype) + t = 1 + for i in range(self.dim): + t = t * (torch.abs(4 * X[..., i] - 2) + self.a[i]) / (1 + self.a[i]) + return t
+
+ + + +
+[docs] +class Morris(SyntheticTestFunction): + r"""Morris test function. + + 20-dimensional function (usually evaluated on `[0, 1]^20`): + + f(x) = sum_{i=1}\^20 beta_i w_i + sum_{i<j}\^20 beta_ij w_i w_j + + sum_{i<j<l}\^20 beta_ijl w_i w_j w_l + 5w_1 w_2 w_3 w_4 + + Proposed to test sensitivity analysis methods + """ + + def __init__( + self, + noise_std: float | None = None, + negate: bool = False, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + noise_std: Standard deviation of observation noise. + negate: Return negative of function. + dtype: The dtype that is used for the bounds of the function. + """ + self._optimizers = None + self.dim = 20 + self._bounds = [(0, 1) for _ in range(self.dim)] + self.si = [ + 0.005, + 0.008, + 0.017, + 0.009, + 0.016, + 0, + 0.069, + 0.1, + 0.15, + 0.1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + + @property + def _optimal_value(self) -> float: + raise NotImplementedError + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + self.to(device=X.device, dtype=X.dtype) + W = [] + t1 = 0 + t2 = 0 + t3 = 0 + for i in range(self.dim): + if i in [2, 4, 6]: + wi = 2 * (1.1 * X[..., i] / (X[..., i] + 0.1) - 0.5) + else: + wi = 2 * (X[..., i] - 0.5) + W.append(wi) + if i < 10: + betai = 20 + else: + betai = (-1) ** (i + 1) + t1 = t1 + betai * wi + for i in range(self.dim): + for j in range(i + 1, self.dim): + if i < 6 or j < 6: + beta_ij = -15 + else: + beta_ij = (-1) ** (i + j + 2) + t2 = t2 + beta_ij * W[i] * W[j] + for k in range(j + 1, self.dim): + if i < 5 or j < 5 or k < 5: + beta_ijk = -10 + else: + beta_ijk = 0 + t3 = t3 + beta_ijk * W[i] * W[j] * W[k] + t4 = 5 * W[0] * W[1] * W[2] * W[3] + return t1 + t2 + t3 + t4
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_functions/synthetic.html b/website-old/pages/api/_modules/botorch/test_functions/synthetic.html new file mode 100644 index 0000000000..8c6eaf157f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_functions/synthetic.html @@ -0,0 +1,1442 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_functions.synthetic

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Synthetic functions for optimization benchmarks.
+
+Most test functions (if not indicated otherwise) are taken from
+[Bingham2013virtual]_.
+
+
+References:
+
+.. [Bingham2013virtual]
+    D. Bingham, S. Surjanovic. Virtual Library of Simulation Experiments.
+    https://www.sfu.ca/~ssurjano/optimization.html
+
+.. [CoelloCoello2002constraint]
+    C. A. Coello Coello and E. Mezura Montes. Constraint-handling in genetic
+    algorithms through the use of dominance-based tournament selection.
+    Advanced Engineering Informatics, 16(3):193–203, 2002.
+
+.. [Hedar2006derivfree]
+    A.-R. Hedar and M. Fukushima. Derivative-free filter simulated annealing
+    method for constrained continuous global optimization. Journal of Global
+    Optimization, 35(4):521–549, 2006.
+
+.. [Lemonge2010constrained]
+    A. C. C. Lemonge, H. J. C. Barbosa, C. C. H. Borges, and F. B. dos Santos
+    Silva. Constrained optimization problems in mechanical engineering design
+    using a real-coded steady-state genetic algorithm. Mecánica Computacional,
+    XXIX:9287–9303, 2010.
+
+.. [Letham2019]
+    B. Letham, B. Karrer, G. Ottoni, and E. Bakshy. Constrained Bayesian
+    Optimization with Noisy Experiments. Bayesian Analysis, Bayesian Anal.
+    14(2), 495-519, 2019.
+
+.. [Gramacy2016]
+    R. Gramacy, G. Gray, S. Le Digabel, H. Lee, P. Ranjan, G. Wells & S. Wild.
+    Modeling an Augmented Lagrangian for Blackbox Constrained Optimization,
+    Technometrics, 2016.
+"""
+
+from __future__ import annotations
+
+import math
+from abc import ABC
+
+import torch
+from botorch.exceptions.errors import InputDataError
+from botorch.test_functions.base import BaseTestProblem, ConstrainedBaseTestProblem
+from botorch.test_functions.utils import round_nearest
+from torch import Tensor
+
+
+
+[docs] +class SyntheticTestFunction(BaseTestProblem, ABC): + r"""Base class for synthetic test functions.""" + + _optimal_value: float | None = None + _optimizers: list[tuple[float, ...]] | None = None + num_objectives: int = 1 + + def __init__( + self, + noise_std: None | float | list[float] = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + noise_std: Standard deviation of the observation noise. If a list is + provided, specifies separate noise standard deviations for each + objective in a multiobjective problem. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + if bounds is not None: + self._bounds = bounds + super().__init__(noise_std=noise_std, negate=negate, dtype=dtype) + if self._optimizers is not None: + if bounds is not None: + # Ensure at least one optimizer lies within the custom bounds + def in_bounds( + optimizer: tuple[float, ...], bounds: list[tuple[float, float]] + ) -> bool: + for i, xopt in enumerate(optimizer): + lower, upper = bounds[i] + if xopt < lower or xopt > upper: + return False + + return True + + if not any( + in_bounds(optimizer=optimizer, bounds=bounds) + for optimizer in self._optimizers + ): + raise ValueError( + "No global optimum found within custom bounds. Please specify " + "bounds which include at least one point in " + f"`{self.__class__.__name__}._optimizers`." + ) + self.register_buffer( + "optimizers", torch.tensor(self._optimizers, dtype=self.bounds.dtype) + ) + + @property + def optimal_value(self) -> float: + r"""The global minimum (maximum if negate=True) of the function.""" + if self._optimal_value is not None: + return -self._optimal_value if self.negate else self._optimal_value + else: + raise NotImplementedError( + f"Problem {self.__class__.__name__} does not specify an optimal value." + )
+ + + +
+[docs] +class Ackley(SyntheticTestFunction): + r"""Ackley test function. + + d-dimensional function (usually evaluated on `[-32.768, 32.768]^d`): + + f(x) = -A exp(-B sqrt(1/d sum_{i=1}^d x_i^2)) - + exp(1/d sum_{i=1}^d cos(c x_i)) + A + exp(1) + + f has one minimizer for its global minimum at `z_1 = (0, 0, ..., 0)` with + `f(z_1) = 0`. + """ + + _optimal_value = 0.0 + _check_grad_at_opt: bool = False + + def __init__( + self, + dim: int = 2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + """ + self.dim = dim + if bounds is None: + bounds = [(-32.768, 32.768) for _ in range(self.dim)] + self._optimizers = [tuple(0.0 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + self.a = 20 + self.b = 0.2 + self.c = 2 * math.pi + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + a, b, c = self.a, self.b, self.c + part1 = -a * torch.exp(-b / math.sqrt(self.dim) * torch.linalg.norm(X, dim=-1)) + part2 = -(torch.exp(torch.mean(torch.cos(c * X), dim=-1))) + return part1 + part2 + a + math.e
+
+ + + +
+[docs] +class Beale(SyntheticTestFunction): + dim = 2 + _optimal_value = 0.0 + _bounds = [(-4.5, 4.5), (-4.5, 4.5)] + _optimizers = [(3.0, 0.5)] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2 = X[..., 0], X[..., 1] + part1 = (1.5 - x1 + x1 * x2).pow(2) + part2 = (2.25 - x1 + x1 * x2.pow(2)).pow(2) + part3 = (2.625 - x1 + x1 * x2.pow(3)).pow(2) + return part1 + part2 + part3
+
+ + + +
+[docs] +class Branin(SyntheticTestFunction): + r"""Branin test function. + + Two-dimensional function (usually evaluated on `[-5, 10] x [0, 15]`): + + B(x) = (x_2 - b x_1^2 + c x_1 - r)^2 + 10 (1-t) cos(x_1) + 10 + + Here `b`, `c`, `r` and `t` are constants where `b = 5.1 / (4 * math.pi ** 2)` + `c = 5 / math.pi`, `r = 6`, `t = 1 / (8 * math.pi)` + B has 3 minimizers for its global minimum at `z_1 = (-pi, 12.275)`, + `z_2 = (pi, 2.275)`, `z_3 = (9.42478, 2.475)` with `B(z_i) = 0.397887`. + """ + + dim = 2 + _bounds = [(-5.0, 10.0), (0.0, 15.0)] + _optimal_value = 0.397887 + _optimizers = [(-math.pi, 12.275), (math.pi, 2.275), (9.42478, 2.475)] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + t1 = ( + X[..., 1] + - 5.1 / (4 * math.pi**2) * X[..., 0].pow(2) + + 5 / math.pi * X[..., 0] + - 6 + ) + t2 = 10 * (1 - 1 / (8 * math.pi)) * torch.cos(X[..., 0]) + return t1.pow(2) + t2 + 10
+
+ + + +
+[docs] +class Bukin(SyntheticTestFunction): + dim = 2 + _bounds = [(-15.0, -5.0), (-3.0, 3.0)] + _optimal_value = 0.0 + _optimizers = [(-10.0, 1.0)] + _check_grad_at_opt: bool = False + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + part1 = 100.0 * torch.sqrt(torch.abs(X[..., 1] - 0.01 * X[..., 0].pow(2))) + part2 = 0.01 * torch.abs(X[..., 0] + 10.0) + return part1 + part2
+
+ + + +
+[docs] +class Cosine8(SyntheticTestFunction): + r"""Cosine Mixture test function. + + 8-dimensional function (usually evaluated on `[-1, 1]^8`): + + f(x) = 0.1 sum_{i=1}^8 cos(5 pi x_i) - sum_{i=1}^8 x_i^2 + + f has one maximizer for its global maximum at `z_1 = (0, 0, ..., 0)` with + `f(z_1) = 0.8` + """ + + dim = 8 + _bounds = [(-1.0, 1.0) for _ in range(8)] + _optimal_value = 0.8 + _optimizers = [tuple(0.0 for _ in range(8))] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + return torch.sum(0.1 * torch.cos(5 * math.pi * X) - X.pow(2), dim=-1)
+
+ + + +
+[docs] +class DropWave(SyntheticTestFunction): + dim = 2 + _bounds = [(-5.12, 5.12), (-5.12, 5.12)] + _optimal_value = -1.0 + _optimizers = [(0.0, 0.0)] + _check_grad_at_opt = False + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + norm = torch.linalg.norm(X, dim=-1) + part1 = 1.0 + torch.cos(12.0 * norm) + part2 = 0.5 * norm.pow(2) + 2.0 + return -part1 / part2
+
+ + + +
+[docs] +class DixonPrice(SyntheticTestFunction): + _optimal_value = 0.0 + + def __init__( + self, + dim=2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + dtype: The dtype that is used for the bounds of the function. + """ + self.dim = dim + if bounds is None: + bounds = [(-10.0, 10.0) for _ in range(self.dim)] + self._optimizers = [ + tuple( + math.pow(2.0, -(1.0 - 2.0 ** (-(i - 1)))) + for i in range(1, self.dim + 1) + ) + ] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + d = self.dim + part1 = (X[..., 0] - 1).pow(2) + i = X.new(range(2, d + 1)) + part2 = torch.sum(i * (2.0 * X[..., 1:].pow(2) - X[..., :-1]).pow(2), dim=-1) + return part1 + part2
+
+ + + +
+[docs] +class EggHolder(SyntheticTestFunction): + r"""Eggholder test function. + + Two-dimensional function (usually evaluated on `[-512, 512]^2`): + + E(x) = (x_2 + 47) sin(R1(x)) - x_1 * sin(R2(x)) + + where `R1(x) = sqrt(|x_2 + x_1 / 2 + 47|)`, `R2(x) = sqrt|x_1 - (x_2 + 47)|)`. + """ + + dim = 2 + _bounds = [(-512.0, 512.0), (-512.0, 512.0)] + _optimal_value = -959.6407 + _optimizers = [(512.0, 404.2319)] + _check_grad_at_opt: bool = False + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2 = X[..., 0], X[..., 1] + part1 = -(x2 + 47.0) * torch.sin(torch.sqrt(torch.abs(x2 + x1 / 2.0 + 47.0))) + part2 = -x1 * torch.sin(torch.sqrt(torch.abs(x1 - (x2 + 47.0)))) + return part1 + part2
+
+ + + +
+[docs] +class Griewank(SyntheticTestFunction): + r"""Griewank synthetic test function. + + The Griewank function is defined for any `d`, is typically evaluated on + `[-600, 600]^d`, and given by: + + G(x) = sum_{i=1}^d x_i**2 / 4000 - prod_{i=1}^d cos(x_i / sqrt(i)) + 1 + + G has many widespread local minima, which are regularly distributed. + The global minimum is at `z = (0, ..., 0)` with `G(z) = 0`. + """ + + _optimal_value = 0.0 + + def __init__( + self, + dim=2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + self.dim = dim + if bounds is None: + bounds = [(-600.0, 600.0) for _ in range(self.dim)] + self._optimizers = [tuple(0.0 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + part1 = torch.sum(X.pow(2) / 4000.0, dim=-1) + d = X.shape[-1] + part2 = -(torch.prod(torch.cos(X / torch.sqrt(X.new(range(1, d + 1)))), dim=-1)) + return part1 + part2 + 1.0
+
+ + + +
+[docs] +class Hartmann(SyntheticTestFunction): + r"""Hartmann synthetic test function. + + Most commonly used is the six-dimensional version (typically evaluated on + `[0, 1]^6`): + + H(x) = - sum_{i=1}^4 ALPHA_i exp( - sum_{j=1}^6 A_ij (x_j - P_ij)**2 ) + + H has a 6 local minima and a global minimum at + + z = (0.20169, 0.150011, 0.476874, 0.275332, 0.311652, 0.6573) + + with `H(z) = -3.32237`. + """ + + def __init__( + self, + dim=6, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + if dim not in (3, 4, 6): + raise ValueError(f"Hartmann with dim {dim} not defined") + self.dim = dim + if bounds is None: + bounds = [(0.0, 1.0) for _ in range(self.dim)] + # optimizers and optimal values for dim=4 not implemented + optvals = {3: -3.86278, 6: -3.32237} + optimizers = { + 3: [(0.114614, 0.555649, 0.852547)], + 6: [(0.20169, 0.150011, 0.476874, 0.275332, 0.311652, 0.6573)], + } + self._optimal_value = optvals.get(self.dim) + self._optimizers = optimizers.get(self.dim) + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + self.register_buffer("ALPHA", torch.tensor([1.0, 1.2, 3.0, 3.2])) + if dim == 3: + A = [[3.0, 10, 30], [0.1, 10, 35], [3.0, 10, 30], [0.1, 10, 35]] + P = [ + [3689, 1170, 2673], + [4699, 4387, 7470], + [1091, 8732, 5547], + [381, 5743, 8828.0], + ] + elif dim == 4: + A = [ + [10, 3, 17, 3.5], + [0.05, 10, 17, 0.1], + [3, 3.5, 1.7, 10], + [17, 8, 0.05, 10], + ] + P = [ + [1312, 1696, 5569, 124.0], + [2329, 4135, 8307, 3736], + [2348, 1451, 3522, 2883], + [4047, 8828, 8732, 5743], + ] + elif dim == 6: + A = [ + [10, 3, 17, 3.5, 1.7, 8], + [0.05, 10, 17, 0.1, 8, 14], + [3, 3.5, 1.7, 10, 17, 8], + [17, 8, 0.05, 10, 0.1, 14], + ] + P = [ + [1312, 1696, 5569, 124, 8283, 5886], + [2329, 4135, 8307, 3736, 1004, 9991], + [2348, 1451, 3522, 2883, 3047, 6650], + [4047, 8828, 8732, 5743, 1091, 381.0], + ] + else: # pragma: no cover -- unreacheable code for pyre. + raise NotImplementedError + self.register_buffer("A", torch.tensor(A)) + self.register_buffer("P", torch.tensor(P)) + + @property + def optimizers(self) -> Tensor: + if self.dim == 4: + raise NotImplementedError() + return super().optimizers + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + self.to(device=X.device, dtype=X.dtype) + inner_sum = torch.sum( + self.A * (X.unsqueeze(-2) - 0.0001 * self.P).pow(2), dim=-1 + ) + H = -(torch.sum(self.ALPHA * torch.exp(-inner_sum), dim=-1)) + if self.dim == 4: + H = (1.1 + H) / 0.839 + return H
+
+ + + +
+[docs] +class HolderTable(SyntheticTestFunction): + r"""Holder Table synthetic test function. + + Two-dimensional function (typically evaluated on `[0, 10] x [0, 10]`): + + `H(x) = - | sin(x_1) * cos(x_2) * exp(| 1 - ||x|| / pi | ) |` + + H has 4 global minima with `H(z_i) = -19.2085` at + + z_1 = ( 8.05502, 9.66459) + z_2 = (-8.05502, -9.66459) + z_3 = (-8.05502, 9.66459) + z_4 = ( 8.05502, -9.66459) + """ + + dim = 2 + _bounds = [(-10.0, 10.0), (-10.0, 10.0)] + _optimal_value = -19.2085 + _optimizers = [ + (8.05502, 9.66459), + (-8.05502, -9.66459), + (-8.05502, 9.66459), + (8.05502, -9.66459), + ] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + term = torch.abs(1 - torch.linalg.norm(X, dim=-1) / math.pi) + return -( + torch.abs(torch.sin(X[..., 0]) * torch.cos(X[..., 1]) * torch.exp(term)) + )
+
+ + + +
+[docs] +class Levy(SyntheticTestFunction): + r"""Levy synthetic test function. + + d-dimensional function (usually evaluated on `[-10, 10]^d`): + + f(x) = sin^2(pi w_1) + + sum_{i=1}^{d-1} (w_i-1)^2 (1 + 10 sin^2(pi w_i + 1)) + + (w_d - 1)^2 (1 + sin^2(2 pi w_d)) + + where `w_i = 1 + (x_i - 1) / 4` for all `i`. + + f has one minimizer for its global minimum at `z_1 = (1, 1, ..., 1)` with + `f(z_1) = 0`. + """ + + _optimal_value = 0.0 + + def __init__( + self, + dim=2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + self.dim = dim + if bounds is None: + bounds = [(-10.0, 10.0) for _ in range(self.dim)] + self._optimizers = [tuple(1.0 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + w = 1.0 + (X - 1.0) / 4.0 + part1 = torch.sin(math.pi * w[..., 0]).pow(2) + part2 = torch.sum( + (w[..., :-1] - 1.0).pow(2) + * (1.0 + 10.0 * torch.sin(math.pi * w[..., :-1] + 1.0).pow(2)), + dim=-1, + ) + part3 = (w[..., -1] - 1.0).pow(2) * ( + 1.0 + torch.sin(2.0 * math.pi * w[..., -1]).pow(2) + ) + return part1 + part2 + part3
+
+ + + +
+[docs] +class Michalewicz(SyntheticTestFunction): + r"""Michalewicz synthetic test function. + + d-dim function (usually evaluated on hypercube [0, pi]^d): + + M(x) = sum_{i=1}^d sin(x_i) (sin(i x_i^2 / pi)^20) + """ + + def __init__( + self, + dim=2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + """ + self.dim = dim + if bounds is None: + bounds = [(0.0, math.pi) for _ in range(self.dim)] + optvals = {2: -1.80130341, 5: -4.687658, 10: -9.66015} + optimizers = {2: [(2.20290552, 1.57079633)]} + self._optimal_value = optvals.get(self.dim) + self._optimizers = optimizers.get(self.dim) + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + self.register_buffer( + "i", torch.tensor(tuple(range(1, self.dim + 1)), dtype=self.bounds.dtype) + ) + + @property + def optimizers(self) -> Tensor: + if self.dim in (5, 10): + raise NotImplementedError() + return super().optimizers + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + self.to(device=X.device, dtype=X.dtype) + m = 10 + return -( + torch.sum( + torch.sin(X) * torch.sin(self.i * X.pow(2) / math.pi).pow(2 * m), dim=-1 + ) + )
+
+ + + +
+[docs] +class Powell(SyntheticTestFunction): + r"""Powell synthetic test function. + + `d`-dim function (usually evaluated on the hypercube `[-4, 5]^d`): + + P(x) = sum_{i=1}^d/4 ( + (x_{4i-3} + 10 x_{4i-2})**2 + + 5 (x_{4i-1} - x_{4i})**2 + + (x_{4i-2} - 2 x_{4i-1})**4 + + 10 (x_{4i-3} - x_{4i})**4 + ) + + + P has a global minimizer at `z = (0, ..., 0)` with `P(z) = 0`. + """ + + _optimal_value = 0.0 + + def __init__( + self, + dim=4, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + self.dim = dim + if bounds is None: + bounds = [(-4.0, 5.0) for _ in range(self.dim)] + self._optimizers = [tuple(0.0 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + result = torch.zeros_like(X[..., 0]) + for i in range(self.dim // 4): + i_ = i + 1 + part1 = (X[..., 4 * i_ - 4] + 10.0 * X[..., 4 * i_ - 3]).pow(2) + part2 = 5.0 * (X[..., 4 * i_ - 2] - X[..., 4 * i_ - 1]).pow(2) + part3 = (X[..., 4 * i_ - 3] - 2.0 * X[..., 4 * i_ - 2]).pow(4) + part4 = 10.0 * (X[..., 4 * i_ - 4] - X[..., 4 * i_ - 1]).pow(4) + result += part1 + part2 + part3 + part4 + return result
+
+ + + +
+[docs] +class Rastrigin(SyntheticTestFunction): + _optimal_value = 0.0 + + def __init__( + self, + dim=2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + self.dim = dim + if bounds is None: + bounds = [(-5.12, 5.12) for _ in range(self.dim)] + self._optimizers = [tuple(0.0 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + return 10.0 * self.dim + torch.sum( + X.pow(2) - 10.0 * torch.cos(2.0 * math.pi * X), dim=-1 + )
+
+ + + +
+[docs] +class Rosenbrock(SyntheticTestFunction): + r"""Rosenbrock synthetic test function. + + d-dimensional function (usually evaluated on `[-5, 10]^d`): + + f(x) = sum_{i=1}^{d-1} (100 (x_{i+1} - x_i^2)^2 + (x_i - 1)^2) + + f has one minimizer for its global minimum at `z_1 = (1, 1, ..., 1)` with + `f(z_i) = 0.0`. + """ + + _optimal_value = 0.0 + + def __init__( + self, + dim=2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + self.dim = dim + if bounds is None: + bounds = [(-5.0, 10.0) for _ in range(self.dim)] + self._optimizers = [tuple(1.0 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + return torch.sum( + 100.0 * (X[..., 1:] - X[..., :-1].pow(2)).pow(2) + (X[..., :-1] - 1).pow(2), + dim=-1, + )
+
+ + + +
+[docs] +class Shekel(SyntheticTestFunction): + r"""Shekel synthtetic test function. + + 4-dimensional function (usually evaluated on `[0, 10]^4`): + + f(x) = -sum_{i=1}^10 (sum_{j=1}^4 (x_j - A_{ji})^2 + C_i)^{-1} + + f has one minimizer for its global minimum at `z_1 = (4, 4, 4, 4)` with + `f(z_1) = -10.5363`. + """ + + dim = 4 + _bounds = [(0.0, 10.0), (0.0, 10.0), (0.0, 10.0), (0.0, 10.0)] + _optimizers = [(4.000747, 3.99951, 4.00075, 3.99951)] + + def __init__( + self, + m: int = 10, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + m: Defaults to 10. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + self.m = m + optvals = {5: -10.1532, 7: -10.4029, 10: -10.536443} + self._optimal_value = optvals[self.m] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + + self.register_buffer("beta", torch.tensor([1, 2, 2, 4, 4, 6, 3, 7, 5, 5.0])) + C_t = torch.tensor( + [ + [4, 1, 8, 6, 3, 2, 5, 8, 6, 7], + [4, 1, 8, 6, 7, 9, 3, 1, 2, 3.6], + [4, 1, 8, 6, 3, 2, 5, 8, 6, 7], + [4, 1, 8, 6, 7, 9, 3, 1, 2, 3.6], + ], + ) + self.register_buffer("C", C_t.transpose(-1, -2)) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + self.to(device=X.device, dtype=X.dtype) + beta = self.beta / 10.0 + result = -sum( + 1 / (torch.sum((X - self.C[i]).pow(2), dim=-1) + beta[i]) + for i in range(self.m) + ) + return result
+
+ + + +
+[docs] +class SixHumpCamel(SyntheticTestFunction): + dim = 2 + _bounds = [(-3.0, 3.0), (-2.0, 2.0)] + _optimal_value = -1.0316 + _optimizers = [(0.0898, -0.7126), (-0.0898, 0.7126)] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2 = X[..., 0], X[..., 1] + return ( + (4 - 2.1 * x1.pow(2) + x1.pow(4) / 3) * x1.pow(2) + + x1 * x2 + + (4 * x2.pow(2) - 4) * x2.pow(2) + )
+
+ + + +
+[docs] +class StyblinskiTang(SyntheticTestFunction): + r"""Styblinski-Tang synthtetic test function. + + d-dimensional function (usually evaluated on the hypercube `[-5, 5]^d`): + + H(x) = 0.5 * sum_{i=1}^d (x_i^4 - 16 * x_i^2 + 5 * x_i) + + H has a single global mininimum `H(z) = -39.166166 * d` at `z = [-2.903534]^d` + """ + + def __init__( + self, + dim=2, + noise_std: float | None = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + self.dim = dim + if bounds is None: + bounds = [(-5.0, 5.0) for _ in range(self.dim)] + self._optimal_value = -39.166166 * self.dim + self._optimizers = [tuple(-2.903534 for _ in range(self.dim))] + super().__init__(noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype) + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + return 0.5 * (X.pow(4) - 16 * X.pow(2) + 5 * X).sum(dim=-1)
+
+ + + +
+[docs] +class ThreeHumpCamel(SyntheticTestFunction): + dim = 2 + _bounds = [(-5.0, 5.0), (-5.0, 5.0)] + _optimal_value = 0.0 + _optimizers = [(0.0, 0.0)] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2 = X[..., 0], X[..., 1] + return ( + 2.0 * x1.pow(2) - 1.05 * x1.pow(4) + x1.pow(6) / 6.0 + x1 * x2 + x2.pow(2) + )
+
+ + + +# ------------ Constrained synthetic test functions ----------- # + + +
+[docs] +class ConstrainedSyntheticTestFunction( + ConstrainedBaseTestProblem, SyntheticTestFunction, ABC +): + r"""Base class for constrained synthetic test functions.""" + + def __init__( + self, + noise_std: None | float | list[float] = None, + constraint_noise_std: None | float | list[float] = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + noise_std: Standard deviation of the observation noise. If a list is + provided, specifies separate noise standard deviations for each + objective in a multiobjective problem. + constraint_noise_std: Standard deviation of the constraint noise. + If a list is provided, specifies separate noise standard + deviations for each constraint. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + SyntheticTestFunction.__init__( + self, noise_std=noise_std, negate=negate, bounds=bounds, dtype=dtype + ) + self.constraint_noise_std = self._validate_constraint_noise( + constraint_noise_std + ) + + def _validate_constraint_noise( + self, constraint_noise_std + ) -> None | float | list[float]: + """ + Validates that constraint_noise_std has length equal to + the number of constraints, if given as a list + + Args: + constraint_noise_std: Standard deviation of the constraint noise. + If a list is provided, specifies separate noise standard + deviations for each constraint. + """ + if ( + isinstance(constraint_noise_std, list) + and len(constraint_noise_std) != self.num_constraints + ): + raise InputDataError( + "If specified as a list, length of constraint_noise_std " + f"({len(constraint_noise_std)}) must match the " + f"number of constraints ({self.num_constraints})" + ) + return constraint_noise_std
+ + + +
+[docs] +class ConstrainedGramacy(ConstrainedSyntheticTestFunction): + r"""Constrained Gramacy test function. + + This problem comes from [Gramacy2016]_. The problem is defined + over the unit cube and the goal is to minimize x1+x2 subject to + 1.5 - x1 - 2 * x2 - 0.5 * sin(2*pi*(x1^2 - 2 * x2)) <= 0 + and x1^2 + x2^2 - 1.5 <= 0. + """ + + num_objectives = 1 + num_constraints = 2 + dim = 2 + _bounds = [(0.0, 1.0), (0.0, 1.0)] + _optimizers = [(0.1954, 0.4044)] + _optimal_value = 0.5998 # approximate from [Gramacy2016]_ + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + """ + Evaluate the function (w/o observation noise) on a set of points. + + Args: + X: A `batch_shape x d`-dim tensor of point(s) at which to evaluate the + function. + """ + return X.sum(dim=-1)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + x1, x2 = X.split(1, dim=-1) + c1 = 1.5 - x1 - 2 * x2 - 0.5 * torch.sin(2 * math.pi * (x1.pow(2) - 2 * x2)) + c2 = x1.pow(2) + x2.pow(2) - 1.5 + return torch.cat([-c1, -c2], dim=-1)
+
+ + + +
+[docs] +class ConstrainedHartmann(Hartmann, ConstrainedSyntheticTestFunction): + r"""Constrained Hartmann test function. + + This is a constrained version of the standard Hartmann test function that + uses `||x||_2 <= 1` as the constraint. This problem comes from [Letham2019]_. + """ + + num_constraints = 1 + + def __init__( + self, + dim: int = 6, + noise_std: None | float = None, + constraint_noise_std: None | float | list[float] = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + constraint_noise_std: Standard deviation of the constraint noise. + If a list is provided, specifies separate noise standard + deviations for each constraint. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + Hartmann.__init__( + self, + dim=dim, + noise_std=noise_std, + negate=negate, + bounds=bounds, + dtype=dtype, + ) + self.constraint_noise_std = self._validate_constraint_noise( + constraint_noise_std + ) + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + return -X.norm(dim=-1, keepdim=True) + 1
+
+ + + +
+[docs] +class ConstrainedHartmannSmooth(Hartmann, ConstrainedSyntheticTestFunction): + r"""Smooth constrained Hartmann test function. + + This is a constrained version of the standard Hartmann test function that + uses `||x||_2^2 <= 1` as the constraint to obtain smoother constraint slack. + """ + + num_constraints = 1 + + def __init__( + self, + dim: int = 6, + noise_std: None | float = None, + constraint_noise_std: None | float | list[float] = None, + negate: bool = False, + bounds: list[tuple[float, float]] | None = None, + dtype: torch.dtype = torch.double, + ) -> None: + r""" + Args: + dim: The (input) dimension. + noise_std: Standard deviation of the observation noise. + constraint_noise_std: Standard deviation of the constraint noise. + If a list is provided, specifies separate noise standard + deviations for each constraint. + negate: If True, negate the function. + bounds: Custom bounds for the function specified as (lower, upper) pairs. + dtype: The dtype that is used for the bounds of the function. + """ + Hartmann.__init__( + self, + dim=dim, + noise_std=noise_std, + negate=negate, + bounds=bounds, + dtype=dtype, + ) + self.constraint_noise_std = self._validate_constraint_noise( + constraint_noise_std + ) + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + return -X.pow(2).sum(dim=-1, keepdim=True) + 1
+
+ + + +
+[docs] +class PressureVessel(ConstrainedSyntheticTestFunction): + r"""Pressure vessel design problem with constraints. + + The four-dimensional pressure vessel design problem with four black-box + constraints from [CoelloCoello2002constraint]_. + """ + + dim = 4 + num_constraints = 4 + _bounds = [(0.0, 10.0), (0.0, 10.0), (10.0, 50.0), (150.0, 200.0)] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2, x3, x4 = X.unbind(-1) + x1 = round_nearest(x1, increment=0.0625, bounds=self._bounds[0]) + x2 = round_nearest(x2, increment=0.0625, bounds=self._bounds[1]) + return ( + 0.6224 * x1 * x3 * x4 + + 1.7781 * x2 * x3.pow(2) + + 3.1661 * x1.pow(2) * x4 + + 19.84 * x1.pow(2) * x3 + )
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + x1, x2, x3, x4 = X.unbind(-1) + return -torch.stack( + [ + -x1 + 0.0193 * x3, + -x2 + 0.00954 * x3, + -math.pi * x3.pow(2) * x4 - (4 / 3) * math.pi * x3.pow(3) + 1296000.0, + x4 - 240.0, + ], + dim=-1, + )
+
+ + + +
+[docs] +class WeldedBeamSO(ConstrainedSyntheticTestFunction): + r"""Welded beam design problem with constraints (single-outcome). + + The four-dimensional welded beam design proble problem with six + black-box constraints from [CoelloCoello2002constraint]_. + + For a (somewhat modified) multi-objective version, see + `botorch.test_functions.multi_objective.WeldedBeam`. + """ + + dim = 4 + num_constraints = 6 + _bounds = [(0.125, 10.0), (0.1, 10.0), (0.1, 10.0), (0.1, 10.0)] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2, x3, x4 = X.unbind(-1) + return 1.10471 * x1.pow(2) * x2 + 0.04811 * x3 * x4 * (14.0 + x2)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + x1, x2, x3, x4 = X.unbind(-1) + P = 6000.0 + L = 14.0 + E = 30e6 + G = 12e6 + t_max = 13600.0 + s_max = 30000.0 + d_max = 0.25 + + M = P * (L + x2 / 2) + R = torch.sqrt(0.25 * (x2.pow(2) + (x1 + x3).pow(2))) + J = 2 * math.sqrt(2) * x1 * x2 * (x2.pow(2) / 12 + 0.25 * (x1 + x3).pow(2)) + P_c = ( + 4.013 + * E + * x3 + * x4.pow(3) + * 6 + / (L**2) + * (1 - 0.25 * x3 * math.sqrt(E / G) / L) + ) + t1 = P / (math.sqrt(2) * x1 * x2) + t2 = M * R / J + t = torch.sqrt(t1.pow(2) + t1 * t2 * x2 / R + t2.pow(2)) + s = 6 * P * L / (x4 * x3.pow(2)) + d = 4 * P * L**3 / (E * x3.pow(3) * x4) + + g1 = t - t_max + g2 = s - s_max + g3 = x1 - x4 + g4 = 0.10471 * x1.pow(2) + 0.04811 * x3 * x4 * (14.0 + x2) - 5.0 + g5 = d - d_max + g6 = P - P_c + + return -torch.stack([g1, g2, g3, g4, g5, g6], dim=-1)
+
+ + + +
+[docs] +class TensionCompressionString(ConstrainedSyntheticTestFunction): + r"""Tension compression string optimization problem with constraints. + + The three-dimensional tension compression string optimization problem with + four black-box constraints from [Hedar2006derivfree]_. + """ + + dim = 3 + num_constraints = 4 + _bounds = [(0.01, 1.0), (0.01, 1.0), (0.01, 20.0)] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2, x3 = X.unbind(-1) + return x1.pow(2) * x2 * (x3 + 2)
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + x1, x2, x3 = X.unbind(-1) + constraints = torch.stack( + [ + 1 - x2.pow(3) * x3 / (71785 * x1.pow(4)), + (4 * x2.pow(2) - x1 * x2) / (12566 * x1.pow(3) * (x2 - x1)) + + 1 / (5108 * x1.pow(2)) + - 1, + 1 - 140.45 * x1 / (x3 * x2.pow(2)), + (x1 + x2) / 1.5 - 1, + ], + dim=-1, + ) + return -constraints.clamp_max(100)
+
+ + + +
+[docs] +class SpeedReducer(ConstrainedSyntheticTestFunction): + r"""Speed Reducer design problem with constraints. + + The seven-dimensional speed reducer design problem with eleven black-box + constraints from [Lemonge2010constrained]_. + """ + + dim = 7 + num_constraints = 11 + _bounds = [ + (2.6, 3.6), + (0.7, 0.8), + (17.0, 28.0), + (7.3, 8.3), + (7.8, 8.3), + (2.9, 3.9), + (5.0, 5.5), + ] + +
+[docs] + def evaluate_true(self, X: Tensor) -> Tensor: + x1, x2, x3, x4, x5, x6, x7 = X.unbind(-1) + return ( + 0.7854 * x1 * x2.pow(2) * (3.3333 * x3.pow(2) + 14.9334 * x3 - 43.0934) + + -1.508 * x1 * (x6.pow(2) + x7.pow(2)) + + 7.4777 * (x6.pow(3) + x7.pow(3)) + + 0.7854 * (x4 * x6.pow(2) + x5 * x7.pow(2)) + )
+ + +
+[docs] + def evaluate_slack_true(self, X: Tensor) -> Tensor: + x1, x2, x3, x4, x5, x6, x7 = X.unbind(-1) + return -torch.stack( + [ + 27.0 * (1 / x1) * (1 / x2.pow(2)) * (1 / x3) - 1, + 397.5 * (1 / x1) * (1 / x2.pow(2)) * (1 / x3.pow(2)) - 1, + 1.93 * (1 / x2) * (1 / x3) * x4.pow(3) * (1 / x6.pow(4)) - 1, + 1.93 * (1 / x2) * (1 / x3) * x5.pow(3) * (1 / x7.pow(4)) - 1, + 1 + / (0.1 * x6.pow(3)) + * torch.sqrt((745 * x4 / (x2 * x3)).pow(2) + 16.9 * 1e6) + - 1100, + 1 + / (0.1 * x7.pow(3)) + * torch.sqrt((745 * x5 / (x2 * x3)).pow(2) + 157.5 * 1e6) + - 850, + x2 * x3 - 40, + 5 - x1 / x2, + x1 / x2 - 12, + (1.5 * x6 + 1.9) / x4 - 1, + (1.1 * x7 + 1.9) / x5 - 1, + ], + dim=-1, + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_functions/utils.html b/website-old/pages/api/_modules/botorch/test_functions/utils.html new file mode 100644 index 0000000000..5aba136222 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_functions/utils.html @@ -0,0 +1,95 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_functions.utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+from __future__ import annotations
+
+import torch
+
+from torch import Tensor
+
+
+
+[docs] +def round_nearest( + X: Tensor, increment: float, bounds: tuple[float, float] | None +) -> Tensor: + r"""Rounds the input tensor to the nearest multiple of `increment`. + + Args: + X: The input to be rounded. + increment: The increment to round to. + bounds: An optional tuple of two floats representing the lower and upper + bounds on `X`. If provided, this will round to the nearest multiple + of `increment` that lies within the bounds. + + Returns: + The rounded input. + """ + X_round = torch.round(X / increment) * increment + if bounds is not None: + X_round = torch.where(X_round < bounds[0], X_round + increment, X_round) + X_round = torch.where(X_round > bounds[1], X_round - increment, X_round) + return X_round
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/test_utils/mock.html b/website-old/pages/api/_modules/botorch/test_utils/mock.html new file mode 100644 index 0000000000..1572bb9678 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/test_utils/mock.html @@ -0,0 +1,196 @@ + + + + + + + +
+
+
+
+

Source code for botorch.test_utils.mock

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+"""
+Utilities for speeding up optimization in tests.
+
+"""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+from contextlib import contextmanager, ExitStack
+from functools import wraps
+from typing import Any, Callable
+from unittest import mock
+
+from botorch.optim.initializers import (
+    gen_batch_initial_conditions,
+    gen_one_shot_kg_initial_conditions,
+)
+from botorch.optim.utils.timeout import minimize_with_timeout
+from scipy.optimize import OptimizeResult
+from torch import Tensor
+
+
+
+[docs] +@contextmanager +def mock_optimize_context_manager( + force: bool = False, +) -> Generator[None, None, None]: + """A context manager that uses mocks to speed up optimization for testing. + Currently, the primary tactic is to force the underlying scipy methods to stop + after just one iteration. + + force: If True will not raise an AssertionError if no mocks are called. + USE RESPONSIBLY. + """ + + def one_iteration_minimize(*args: Any, **kwargs: Any) -> OptimizeResult: + if kwargs["options"] is None: + kwargs["options"] = {} + + kwargs["options"]["maxiter"] = 1 + return minimize_with_timeout(*args, **kwargs) + + def minimal_gen_ics(*args: Any, **kwargs: Any) -> Tensor: + kwargs["num_restarts"] = 2 + kwargs["raw_samples"] = 4 + + return gen_batch_initial_conditions(*args, **kwargs) + + def minimal_gen_os_ics(*args: Any, **kwargs: Any) -> Tensor | None: + kwargs["num_restarts"] = 2 + kwargs["raw_samples"] = 4 + + return gen_one_shot_kg_initial_conditions(*args, **kwargs) + + with ExitStack() as es: + # Note this `minimize_with_timeout` is defined in optim.utils.timeout; + # this mock only has an effect when calling a function used in + # `botorch.generation.gen`, such as `gen_candidates_scipy`. + mock_generation = es.enter_context( + mock.patch( + "botorch.generation.gen.minimize_with_timeout", + wraps=one_iteration_minimize, + ) + ) + + # Similarly, works when using calling a function defined in + # `optim.core`, such as `scipy_minimize` and `torch_minimize`. + mock_fit = es.enter_context( + mock.patch( + "botorch.optim.core.minimize_with_timeout", + wraps=one_iteration_minimize, + ) + ) + + # Works when calling a function in `optim.optimize` such as + # `optimize_acqf` + mock_gen_ics = es.enter_context( + mock.patch( + "botorch.optim.optimize.gen_batch_initial_conditions", + wraps=minimal_gen_ics, + ) + ) + + # Works when calling a function in `optim.optimize` such as + # `optimize_acqf` + mock_gen_os_ics = es.enter_context( + mock.patch( + "botorch.optim.optimize.gen_one_shot_kg_initial_conditions", + wraps=minimal_gen_os_ics, + ) + ) + + # Reduce default number of iterations in `optimize_acqf_mixed_alternating`. + for name in [ + "MAX_ITER_ALTER", + "MAX_ITER_DISCRETE", + "MAX_ITER_CONT", + ]: + es.enter_context(mock.patch(f"botorch.optim.optimize_mixed.{name}", new=1)) + + yield + + if (not force) and all( + mock_.call_count < 1 + for mock_ in [ + mock_generation, + mock_fit, + mock_gen_ics, + mock_gen_os_ics, + ] + ): + raise AssertionError( + "No mocks were called in the context manager. Please remove unused " + "mock_optimize_context_manager()." + )
+ + + +
+[docs] +def mock_optimize(f: Callable) -> Callable: + """Wraps `f` in `mock_optimize_context_manager` for use as a decorator.""" + + @wraps(f) + # pyre-fixme[3]: Return type must be annotated. + def inner(*args: Any, **kwargs: Any): + with mock_optimize_context_manager(): + return f(*args, **kwargs) + + return inner
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/constants.html b/website-old/pages/api/_modules/botorch/utils/constants.html new file mode 100644 index 0000000000..6f613f16dc --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/constants.html @@ -0,0 +1,101 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.constants

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Iterator
+
+from functools import lru_cache
+from numbers import Number
+
+import torch
+from torch import Tensor
+
+
+
+[docs] +@lru_cache(maxsize=None) +def get_constants( + values: Number | Iterator[Number], + device: torch.device | None = None, + dtype: torch.dtype | None = None, +) -> Tensor | tuple[Tensor, ...]: + r"""Returns scalar-valued Tensors containing each of the given constants. + Used to expedite tensor operations involving scalar arithmetic. Note that + the returned Tensors should not be modified in-place.""" + if isinstance(values, Number): + return torch.full((), values, dtype=dtype, device=device) + + return tuple(torch.full((), val, dtype=dtype, device=device) for val in values)
+ + + +
+[docs] +def get_constants_like( + values: Number | Iterator[Number], + ref: Tensor, +) -> Tensor | Iterator[Tensor]: + return get_constants(values, device=ref.device, dtype=ref.dtype)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/constraints.html b/website-old/pages/api/_modules/botorch/utils/constraints.html new file mode 100644 index 0000000000..b62619d9d5 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/constraints.html @@ -0,0 +1,162 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.constraints

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Helpers for handling input or outcome constraints.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from functools import partial
+
+import torch
+from torch import Tensor
+
+
+
+[docs] +def get_outcome_constraint_transforms( + outcome_constraints: tuple[Tensor, Tensor] | None, +) -> list[Callable[[Tensor], Tensor]] | None: + r"""Create outcome constraint callables from outcome constraint tensors. + + Args: + outcome_constraints: A tuple of `(A, b)`. For `k` outcome constraints + and `m` outputs at `f(x)``, `A` is `k x m` and `b` is `k x 1` such + that `A f(x) <= b`. + + Returns: + A list of callables, each mapping a Tensor of size `b x q x m` to a + tensor of size `b x q`, where `m` is the number of outputs of the model. + Negative values imply feasibility. The callables support broadcasting + (e.g. for calling on a tensor of shape `mc_samples x b x q x m`). + + Example: + >>> # constrain `f(x)[0] <= 0` + >>> A = torch.tensor([[1., 0.]]) + >>> b = torch.tensor([[0.]]) + >>> outcome_constraints = get_outcome_constraint_transforms((A, b)) + """ + if outcome_constraints is None: + return None + A, b = outcome_constraints + + def _oc(a: Tensor, rhs: Tensor, Y: Tensor) -> Tensor: + r"""Evaluate constraints. + + Note: einsum multiples Y by a and sums over the `m`-dimension. Einsum + is ~2x faster than using `(Y * a.view(1, 1, -1)).sum(dim-1)`. + + Args: + a: `m`-dim tensor of weights for the outcomes + rhs: Singleton tensor containing the outcome constraint value + Y: `... x b x q x m` tensor of function values + + Returns: + A `... x b x q`-dim tensor where negative values imply feasibility + """ + lhs = torch.einsum("...m, m", [Y, a]) + return lhs - rhs + + return [partial(_oc, a, rhs) for a, rhs in zip(A, b)]
+ + + +
+[docs] +def get_monotonicity_constraints( + d: int, + descending: bool = False, + dtype: torch.dtype | None = None, + device: torch.device | None = None, +) -> tuple[Tensor, Tensor]: + """Returns a system of linear inequalities `(A, b)` that generically encodes order + constraints on the elements of a `d`-dimsensional space, i.e. `A @ x < b` implies + `x[i] < x[i + 1]` for a `d`-dimensional vector `x`. + + Idea: Could encode `A` as sparse matrix, if it is supported well. + + Args: + d: Dimensionality of the constraint space, i.e. number of monotonic parameters. + descending: If True, forces the elements of a vector to be monotonically de- + creasing and be monotonically increasing otherwise. + dtype: The dtype of the returned Tensors. + device: The device of the returned Tensors. + + Returns: + A tuple of Tensors `(A, b)` representing the monotonicity constraint as a system + of linear inequalities `A @ x < b`. `A` is `(d - 1) x d`-dimensional and `b` is + `(d - 1) x 1`-dimensional. + """ + A = torch.zeros(d - 1, d, dtype=dtype, device=device) + idx = torch.arange(d - 1) + A[idx, idx] = 1 + A[idx, idx + 1] = -1 + b = torch.zeros(d - 1, 1, dtype=dtype, device=device) + if descending: + A = -A + return A, b
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/containers.html b/website-old/pages/api/_modules/botorch/utils/containers.html new file mode 100644 index 0000000000..f5aa5030a1 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/containers.html @@ -0,0 +1,238 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.containers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Representations for different kinds of data."""
+
+from __future__ import annotations
+
+import dataclasses
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, fields
+from typing import Any
+
+import torch
+
+from torch import device as Device, dtype as Dtype, LongTensor, Size, Tensor
+
+
+
+[docs] +class BotorchContainer(ABC): + r"""Abstract base class for BoTorch's data containers. + + A BotorchContainer represents a tensor, which should be the sole object + returned by its `__call__` method. Said tensor is expected to consist of + one or more "events" (e.g. data points or feature vectors), whose shape is + given by the required `event_shape` field. + + Notice: Once version 3.10 becomes standard, this class should + be reworked to take advantage of dataclasses' `kw_only` flag. + """ + + event_shape: Size + + def __post_init__(self, validate_init: bool = True) -> None: + if validate_init: + self._validate() + + @abstractmethod + def __call__(self) -> Tensor: + raise NotImplementedError + + @abstractmethod + def __eq__(self, other: Any) -> bool: + raise NotImplementedError + + @property + @abstractmethod + def shape(self) -> Size: + raise NotImplementedError + + @property + @abstractmethod + def device(self) -> Device: + raise NotImplementedError + + @property + @abstractmethod + def dtype(self) -> Dtype: + raise NotImplementedError + + def _validate(self) -> None: + for field in fields(self): + if field.name == "event_shape": + return + raise AttributeError("Missing required field `event_shape`.")
+ + + +
+[docs] +@dataclass(eq=False) +class DenseContainer(BotorchContainer): + r"""Basic representation of data stored as a dense Tensor.""" + + values: Tensor + event_shape: Size + + def __call__(self) -> Tensor: + """Returns a dense tensor representation of the container's contents.""" + return self.values + + def __eq__(self, other: Any) -> bool: + return ( + type(other) is type(self) + and self.shape == other.shape + and self.values.equal(other.values) + ) + + @property + def shape(self) -> Size: + return self.values.shape + + @property + def device(self) -> Device: + return self.values.device + + @property + def dtype(self) -> Dtype: + return self.values.dtype + + def _validate(self) -> None: + super()._validate() + for a, b in zip(reversed(self.event_shape), reversed(self.values.shape)): + if a != b: + raise ValueError( + f"Shape of `values` {self.values.shape} incompatible with " + f"`event shape` {self.event_shape}." + ) + +
+[docs] + def clone(self) -> DenseContainer: + return dataclasses.replace(self)
+
+ + + +
+[docs] +@dataclass(eq=False) +class SliceContainer(BotorchContainer): + r"""Represent data points formed by concatenating (n-1)-dimensional slices + taken from the leading dimension of an n-dimensional source tensor.""" + + values: Tensor + indices: LongTensor + event_shape: Size + + def __call__(self) -> Tensor: + flat = self.values.index_select(dim=0, index=self.indices.view(-1)) + return flat.view(*self.indices.shape[:-1], -1, *self.values.shape[2:]) + + def __eq__(self, other: Any) -> bool: + return ( + type(other) is type(self) + and self.values.equal(other.values) + and self.indices.equal(other.indices) + ) + + @property + def shape(self) -> Size: + return self.indices.shape[:-1] + self.event_shape + + @property + def device(self) -> Device: + return self.values.device + + @property + def dtype(self) -> Dtype: + return self.values.dtype + + def _validate(self) -> None: + super()._validate() + values = self.values + indices = self.indices + assert indices.ndim > 1 + assert (-1 < indices.min()) & (indices.max() < len(values)) + + event_shape = self.event_shape + _event_shape = (indices.shape[-1] * values.shape[1],) + values.shape[2:] + if event_shape != _event_shape: + raise ValueError( + f"Shapes of `values` {values.shape} and `indices` " + f"{indices.shape} incompatible with `event_shape` {event_shape}." + ) + +
+[docs] + def clone(self) -> SliceContainer: + return type(self)( + values=self.values.clone(), + indices=self.indices.clone(), + event_shape=torch.Size(self.event_shape), + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/context_managers.html b/website-old/pages/api/_modules/botorch/utils/context_managers.html new file mode 100644 index 0000000000..c55488909d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/context_managers.html @@ -0,0 +1,235 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.context_managers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Utilities for optimization.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Generator, Iterable
+
+from contextlib import contextmanager
+from typing import Any, NamedTuple
+
+from torch import device as Device, dtype as Dtype, Tensor
+from torch.nn import Module
+
+
+
+[docs] +class TensorCheckpoint(NamedTuple): + values: Tensor + device: Device | None = None + dtype: Dtype | None = None
+ + + +
+[docs] +@contextmanager +def delattr_ctx( + instance: object, *attrs: str, enforce_hasattr: bool = False +) -> Generator[None, None, None]: + r"""Contextmanager for temporarily deleting attributes.""" + try: + cache = {} + for key in attrs: + if hasattr(instance, key): + cache[key] = getattr(instance, key) + delattr(instance, key) + elif enforce_hasattr: + raise ValueError( + f"Attribute {key} missing from {type(instance)} instance." + ) + yield + finally: + for key, cached_val in cache.items(): + setattr(instance, key, cached_val)
+ + + +
+[docs] +@contextmanager +def parameter_rollback_ctx( + parameters: dict[str, Tensor], + checkpoint: dict[str, TensorCheckpoint] | None = None, + **tkwargs: Any, +) -> Generator[dict[str, TensorCheckpoint], None, None]: + r"""Contextmanager that exits by rolling back a module's state_dict. + + Args: + module: Module instance. + name_filter: Optional Boolean function used to filter items by name. + checkpoint: Optional cache of values and tensor metadata specifying the rollback + state for the module (or some subset thereof). + **tkwargs: Keyword arguments passed to `torch.Tensor.to` when copying data from + each tensor in `module.state_dict()` to the internally created checkpoint. + Only adhered to when the `checkpoint` argument is None. + + Yields: + A dictionary of TensorCheckpoints for the module's state_dict. Any in-places + changes to the checkpoint will be observed at rollback time. If the checkpoint + is cleared, no rollback will occur. + """ + # Create copies of the orginal values + if checkpoint is None: + checkpoint = { + name: TensorCheckpoint( + values=param.detach().to(**tkwargs).clone(), + device=param.device, + dtype=param.dtype, + ) + for name, param in parameters.items() + } + + try: # yield the checkpoint dictionary to the user + yield checkpoint + finally: # restore original values of tracked parameters + if checkpoint: + for name, param in parameters.items(): + if name in checkpoint: + values, device, dtype = checkpoint[name] + param.data.copy_(values.to(device=device, dtype=dtype))
+ + + +
+[docs] +@contextmanager +def module_rollback_ctx( + module: Module, + name_filter: Callable[[str], bool] | None = None, + checkpoint: dict[str, TensorCheckpoint] | None = None, + **tkwargs: Any, +) -> Generator[dict[str, TensorCheckpoint], None, None]: + r"""Contextmanager that exits by rolling back a module's state_dict. + + Args: + module: Module instance. + name_filter: Optional Boolean function used to filter items by name. + checkpoint: Optional cache of values and tensor metadata specifying the rollback + state for the module (or some subset thereof). + **tkwargs: Keyword arguments passed to `torch.Tensor.to` when copying data from + each tensor in `module.state_dict()` to the internally created checkpoint. + Only adhered to when the `checkpoint` argument is None. + + Yields: + A dictionary of TensorCheckpoints for the module's state_dict. Any in-places + changes to the checkpoint will be observed at rollback time. If the checkpoint + is cleared, no rollback will occur. + """ + # Create copies of the orginal values + if checkpoint is None: + checkpoint = { + name: TensorCheckpoint( + values=values.detach().to(**tkwargs).clone(), + device=values.device, + dtype=values.dtype, + ) + for name, values in module.state_dict().items() + if name_filter is None or name_filter(name) + } + + try: # yield the checkpoint dictionary to the user + yield checkpoint + finally: # restore original values of tracked parameters + if checkpoint: + state_dict = module.state_dict() + for key, (values, device, dtype) in checkpoint.items(): + tnsr = state_dict.get(key) + if tnsr is None: + state_dict[key] = values.to(device=device, dtype=dtype) + else: + tnsr[...] = values.to(device=device, dtype=dtype) + + module.load_state_dict(state_dict)
+ + + +
+[docs] +@contextmanager +def zero_grad_ctx( + parameters: dict[str, Tensor] | Iterable[Tensor], + zero_on_enter: bool = True, + zero_on_exit: bool = False, +) -> Generator[None, None, None]: + def zero_() -> None: + for param in ( + parameters.values() if isinstance(parameters, dict) else parameters + ): + if param.grad is not None: + param.grad.zero_() + + if zero_on_enter: + zero_() + + try: + yield + finally: + if zero_on_exit: + zero_()
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/datasets.html b/website-old/pages/api/_modules/botorch/utils/datasets.html new file mode 100644 index 0000000000..5998da9479 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/datasets.html @@ -0,0 +1,824 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.datasets

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Representations for different kinds of datasets."""
+
+from __future__ import annotations
+
+import copy
+
+from typing import Any
+
+import torch
+from botorch.exceptions.errors import InputDataError, UnsupportedError
+from botorch.utils.containers import BotorchContainer, SliceContainer
+from torch import long, ones, Tensor
+
+
+
+[docs] +class SupervisedDataset: + r"""Base class for datasets consisting of labelled pairs `(X, Y)` + and an optional `Yvar` that stipulates observations variances so + that `Y[i] ~ N(f(X[i]), Yvar[i])`. + + Example: + + .. code-block:: python + + X = torch.rand(16, 2) + Y = torch.rand(16, 1) + feature_names = ["learning_rate", "embedding_dim"] + outcome_names = ["neg training loss"] + A = SupervisedDataset( + X=X, + Y=Y, + feature_names=feature_names, + outcome_names=outcome_names, + ) + B = SupervisedDataset( + X=DenseContainer(X, event_shape=X.shape[-1:]), + Y=DenseContainer(Y, event_shape=Y.shape[-1:]), + feature_names=feature_names, + outcome_names=outcome_names, + ) + assert A == B + """ + + def __init__( + self, + X: BotorchContainer | Tensor, + Y: BotorchContainer | Tensor, + *, + feature_names: list[str], + outcome_names: list[str], + Yvar: BotorchContainer | Tensor | None = None, + validate_init: bool = True, + ) -> None: + r"""Constructs a `SupervisedDataset`. + + Args: + X: A `Tensor` or `BotorchContainer` representing the input features. + Y: A `Tensor` or `BotorchContainer` representing the outcomes. + feature_names: A list of names of the features in `X`. + outcome_names: A list of names of the outcomes in `Y`. + Yvar: An optional `Tensor` or `BotorchContainer` representing + the observation noise. + validate_init: If `True`, validates the input shapes. + """ + self._X = X + self._Y = Y + self._Yvar = Yvar + self.feature_names = feature_names + self.outcome_names = outcome_names + self.validate_init = validate_init + if validate_init: + self._validate() + + @property + def X(self) -> Tensor: + if isinstance(self._X, Tensor): + return self._X + return self._X() + + @property + def Y(self) -> Tensor: + if isinstance(self._Y, Tensor): + return self._Y + return self._Y() + + @property + def Yvar(self) -> Tensor | None: + if self._Yvar is None or isinstance(self._Yvar, Tensor): + return self._Yvar + return self._Yvar() + + def _validate( + self, + validate_feature_names: bool = True, + validate_outcome_names: bool = True, + ) -> None: + r"""Checks that the shapes of the inputs are compatible with each other. + + Args: + validate_feature_names: By default, we validate that the length of + `feature_names` matches the # of columns of `self.X`. If a + particular dataset, e.g., `RankingDataset`, is known to violate + this assumption, this can be set to `False`. + validate_outcome_names: By default, we validate that the length of + `outcomes_names` matches the # of columns of `self.Y`. If a + particular dataset, e.g., `RankingDataset`, is known to violate + this assumption, this can be set to `False`. + """ + shape_X = self.X.shape + if isinstance(self._X, BotorchContainer): + shape_X = shape_X[: len(shape_X) - len(self._X.event_shape)] + else: + shape_X = shape_X[:-1] + shape_Y = self.Y.shape + if isinstance(self._Y, BotorchContainer): + shape_Y = shape_Y[: len(shape_Y) - len(self._Y.event_shape)] + else: + shape_Y = shape_Y[:-1] + if shape_X != shape_Y: + raise ValueError("Batch dimensions of `X` and `Y` are incompatible.") + if self.Yvar is not None and self.Yvar.shape != self.Y.shape: + raise ValueError("Shapes of `Y` and `Yvar` are incompatible.") + if validate_feature_names and len(self.feature_names) != self.X.shape[-1]: + raise ValueError( + "`X` must have the same number of columns as the number of " + "features in `feature_names`." + ) + if validate_outcome_names and len(self.outcome_names) != self.Y.shape[-1]: + raise ValueError( + "`Y` must have the same number of columns as the number of " + "outcomes in `outcome_names`." + ) + + def __eq__(self, other: Any) -> bool: + return ( + type(other) is type(self) + and torch.equal(self.X, other.X) + and torch.equal(self.Y, other.Y) + and ( + other.Yvar is None + if self.Yvar is None + else other.Yvar is not None and torch.equal(self.Yvar, other.Yvar) + ) + and self.feature_names == other.feature_names + and self.outcome_names == other.outcome_names + ) + +
+[docs] + def clone( + self, deepcopy: bool = False, mask: Tensor | None = None + ) -> SupervisedDataset: + """Return a copy of the dataset. + + Args: + deepcopy: If True, perform a deep copy. Otherwise, use the same + tensors/lists. + mask: A `n`-dim boolean mask indicating which rows to keep. This is used + along the -2 dimension. + + Returns: + The new dataset. + """ + new_X = self._X + new_Y = self._Y + new_Yvar = self._Yvar + feature_names = self.feature_names + outcome_names = self.outcome_names + if mask is not None: + if any(isinstance(x, BotorchContainer) for x in [new_X, new_Y, new_Yvar]): + raise NotImplementedError( + "Masking is not supported for BotorchContainers." + ) + new_X = new_X[..., mask, :] + new_Y = new_Y[..., mask, :] + if new_Yvar is not None: + new_Yvar = new_Yvar[..., mask, :] + if deepcopy: + new_X = new_X.clone() + new_Y = new_Y.clone() + new_Yvar = new_Yvar.clone() if new_Yvar is not None else None + feature_names = copy.copy(self.feature_names) + outcome_names = copy.copy(self.outcome_names) + kwargs = {} + if new_Yvar is not None: + kwargs = {"Yvar": new_Yvar} + return type(self)( + X=new_X, + Y=new_Y, + feature_names=feature_names, + outcome_names=outcome_names, + validate_init=self.validate_init, + **kwargs, + )
+
+ + + +
+[docs] +class RankingDataset(SupervisedDataset): + r"""A SupervisedDataset whose labelled pairs `(x, y)` consist of m-ary combinations + `x ∈ Z^{m}` of elements from a ground set `Z = (z_1, ...)` and ranking vectors + `y {0, ..., m - 1}^{m}` with properties: + + a) Ranks start at zero, i.e. min(y) = 0. + b) Sorted ranks are contiguous unless one or more ties are present. + c) `k` ranks are skipped after a `k`-way tie. + + Example: + + .. code-block:: python + + X = SliceContainer( + values=torch.rand(16, 2), + indices=torch.stack([torch.randperm(16)[:3] for _ in range(8)]), + event_shape=torch.Size([3 * 2]), + ) + Y = DenseContainer( + torch.stack([torch.randperm(3) for _ in range(8)]), + event_shape=torch.Size([3]) + ) + feature_names = ["item_0", "item_1"] + outcome_names = ["ranking outcome"] + dataset = RankingDataset( + X=X, + Y=Y, + feature_names=feature_names, + outcome_names=outcome_names, + ) + """ + + def __init__( + self, + X: SliceContainer, + Y: BotorchContainer | Tensor, + feature_names: list[str], + outcome_names: list[str], + validate_init: bool = True, + ) -> None: + r"""Construct a `RankingDataset`. + + Args: + X: A `SliceContainer` representing the input features being ranked. + Y: A `Tensor` or `BotorchContainer` representing the rankings. + feature_names: A list of names of the features in X. + outcome_names: A list of names of the outcomes in Y. + validate_init: If `True`, validates the input shapes. + """ + super().__init__( + X=X, + Y=Y, + feature_names=feature_names, + outcome_names=outcome_names, + Yvar=None, + validate_init=validate_init, + ) + + def _validate(self) -> None: + super()._validate(validate_feature_names=False, validate_outcome_names=False) + if len(self.feature_names) != self._X.values.shape[-1]: + raise ValueError( + "The `values` field of `X` must have the same number of columns as " + "the number of features in `feature_names`." + ) + + Y = self.Y + arity = self._X.indices.shape[-1] + if Y.min() < 0 or Y.max() >= arity: + raise ValueError("Invalid ranking(s): out-of-bounds ranks detected.") + + # Ensure that rankings are well-defined + Y_sort = Y.sort(descending=False, dim=-1).values + y_incr = ones([], dtype=long) + y_prev = None + for i, y in enumerate(Y_sort.unbind(dim=-1)): + if i == 0: + if (y != 0).any(): + raise ValueError("Invalid ranking(s): missing zero-th rank.") + y_prev = y + continue + + y_diff = y - y_prev + y_prev = y + + # Either a tie or next ranking when accounting for previous ties + if not ((y_diff == 0) | (y_diff == y_incr)).all(): + raise ValueError("Invalid ranking(s): ranks not skipped after ties.") + + # Same as: torch.where(y_diff == 0, y_incr + 1, 1) + y_incr = y_incr - y_diff + 1
+ + + +
+[docs] +class MultiTaskDataset(SupervisedDataset): + """This is a multi-task dataset that is constructed from the datasets of + individual tasks. It offers functionality to combine parts of individual + datasets to construct the inputs necessary for the `MultiTaskGP` models. + + The datasets of individual tasks are allowed to represent different sets + of features. When there are heterogeneous feature sets, calling + `MultiTaskDataset.X` will result in an error. + """ + + def __init__( + self, + datasets: list[SupervisedDataset], + target_outcome_name: str, + task_feature_index: int | None = None, + ): + """Construct a `MultiTaskDataset`. + + Args: + datasets: A list of the datasets of individual tasks. Each dataset + is expected to contain data for only one outcome. + target_outcome_name: Name of the target outcome to be modeled. + task_feature_index: If the task feature is included in the Xs of the + individual datasets, this should be used to specify its index. + If omitted, the task feature will be appended while concatenating Xs. + If given, we sanity-check that the names of the task features + match between all datasets. + """ + self.datasets: dict[str, SupervisedDataset] = { + ds.outcome_names[0]: ds for ds in datasets + } + self.target_outcome_name = target_outcome_name + self.task_feature_index = task_feature_index + self._validate_datasets(datasets=datasets) + self.feature_names = self.datasets[target_outcome_name].feature_names + self.outcome_names = [target_outcome_name] + + # Check if the datasets have identical feature sets. + self.has_heterogeneous_features = any( + datasets[0].feature_names != ds.feature_names for ds in datasets[1:] + ) + +
+[docs] + @classmethod + def from_joint_dataset( + cls, + dataset: SupervisedDataset, + task_feature_index: int, + target_task_value: int, + outcome_names_per_task: dict[int, str] | None = None, + ) -> MultiTaskDataset: + r"""Construct a `MultiTaskDataset` from a joint dataset that includes the + data for all tasks with the task feature index. + + This will break down the joint dataset into individual datasets by the value + of the task feature. Each resulting dataset will have its outcome name set + based on `outcome_names_per_task`, with the missing values defaulting to + `task_<task_feature>` (except for the target task, which will retain the + original outcome name from the dataset). + + Args: + dataset: The joint dataset. + task_feature_index: The column index of the task feature in `dataset.X`. + target_task_value: The value of the task feature for the target task + in the dataset. The data for the target task is filtered according to + `dataset.X[task_feature_index] == target_task_value`. + outcome_names_per_task: Optional dictionary mapping task feature values + to the outcome names for each task. If not provided, the auxiliary + tasks will be named `task_<task_feature>` and the target task will + retain the outcome name from the dataset. + + Returns: + A `MultiTaskDataset` instance. + """ + if len(dataset.outcome_names) > 1: + raise UnsupportedError( + "Dataset containing more than one outcome is not supported. " + f"Got {dataset.outcome_names=}." + ) + outcome_names_per_task = outcome_names_per_task or {} + # Split datasets by task feature. + datasets = [] + all_task_features = dataset.X[:, task_feature_index] + for task_value in all_task_features.unique().long().tolist(): + default_name = ( + dataset.outcome_names[0] + if task_value == target_task_value + else f"task_{task_value}" + ) + outcome_name = outcome_names_per_task.get(task_value, default_name) + filter_mask = all_task_features == task_value + new_dataset = SupervisedDataset( + X=dataset.X[filter_mask], + Y=dataset.Y[filter_mask], + Yvar=dataset.Yvar[filter_mask] if dataset.Yvar is not None else None, + feature_names=dataset.feature_names, + outcome_names=[outcome_name], + ) + datasets.append(new_dataset) + # Return the new dataset + return cls( + datasets=datasets, + target_outcome_name=outcome_names_per_task.get( + target_task_value, dataset.outcome_names[0] + ), + task_feature_index=task_feature_index, + )
+ + + def _validate_datasets(self, datasets: list[SupervisedDataset]) -> None: + """Validates that: + * Each dataset models only one outcome; + * Each outcome is modeled by only one dataset; + * The target outcome is included in the datasets; + * The datasets do not model batched inputs; + * The task feature names of the datasets all match; + * Either all or none of the datasets specify Yvar. + """ + if any(len(ds.outcome_names) > 1 for ds in datasets): + raise UnsupportedError( + "Datasets containing more than one outcome are not supported." + ) + if len(self.datasets) != len(datasets): + raise UnsupportedError( + "Received multiple datasets for the same outcome. Each dataset " + "must contain data for a unique outcome. Got datasets with " + f"outcome names: {(ds.outcome_names for ds in datasets)}." + ) + if self.target_outcome_name not in self.datasets: + raise InputDataError( + "Target outcome is not present in the datasets. " + f"Got {self.target_outcome_name=} and datasets for " + f"outcomes {list(self.datasets.keys())}." + ) + if any(len(ds.X.shape) > 2 for ds in datasets): + raise UnsupportedError( + "Datasets modeling batched inputs are not supported." + ) + if self.task_feature_index is not None: + tf_names = [ds.feature_names[self.task_feature_index] for ds in datasets] + if any(name != tf_names[0] for name in tf_names[1:]): + raise InputDataError( + "Expected the names of the task features to match across all " + f"datasets. Got {tf_names}." + ) + all_Yvars = [ds.Yvar for ds in datasets] + is_none = [yvar is None for yvar in all_Yvars] + # Check that either all or None of the Yvars exist. + if not all(is_none) and any(is_none): + raise UnsupportedError( + "Expected either all or none of the datasets to have a Yvar. " + "Only subset of datasets define Yvar, which is unsupported." + ) + + @property + def X(self) -> Tensor: + """Appends task features, if needed, and concatenates the Xs of datasets to + produce the `train_X` expected by `MultiTaskGP` and subclasses. + + If appending the task features, 0 is reserved for the target task and the + remaining tasks are populated with 1, 2, ..., len(datasets) - 1. + """ + if self.has_heterogeneous_features: + raise UnsupportedError( + "Concatenating `X`s from datasets with heterogeneous feature sets " + "is not supported." + ) + all_Xs = [] + next_task = 1 + for outcome, ds in self.datasets.items(): + if self.task_feature_index is None: + # Append the task feature index. + if outcome == self.target_outcome_name: + task_feature = 0 + else: + task_feature = next_task + next_task = next_task + 1 + all_Xs.append(torch.nn.functional.pad(ds.X, (0, 1), value=task_feature)) + else: + all_Xs.append(ds.X) + return torch.cat(all_Xs, dim=0) + + @property + def Y(self) -> Tensor: + """Concatenates Ys of the datasets.""" + return torch.cat([ds.Y for ds in self.datasets.values()], dim=0) + + @property + def Yvar(self) -> Tensor | None: + """Concatenates Yvars of the datasets if they exist.""" + all_Yvars = [ds.Yvar for ds in self.datasets.values()] + return None if all_Yvars[0] is None else torch.cat(all_Yvars, dim=0) + +
+[docs] + def get_dataset_without_task_feature(self, outcome_name: str) -> SupervisedDataset: + """A helper for extracting the child datasets with their task features removed. + + If the task feature index is `None`, the dataset will be returned as is. + + Args: + outcome_name: The outcome name for the dataset to extract. + + Returns: + The dataset without the task feature. + """ + dataset = self.datasets[outcome_name] + if self.task_feature_index is None: + return dataset + indices = list(range(len(self.feature_names))) + indices.pop(self.task_feature_index) + return SupervisedDataset( + X=dataset.X[..., indices], + Y=dataset.Y, + Yvar=dataset.Yvar, + feature_names=[ + fn for i, fn in enumerate(dataset.feature_names) if i in indices + ], + outcome_names=[outcome_name], + )
+ + + def __eq__(self, other: Any) -> bool: + return ( + type(other) is type(self) + and self.datasets == other.datasets + and self.target_outcome_name == other.target_outcome_name + and self.task_feature_index == other.task_feature_index + ) + +
+[docs] + def clone( + self, deepcopy: bool = False, mask: Tensor | None = None + ) -> MultiTaskDataset: + """Return a copy of the dataset. + + Args: + deepcopy: If True, perform a deep copy. Otherwise, use the same + tensors/lists/datasets. + mask: A `n`-dim boolean mask indicating which rows to keep from the target + dataset. This is used along the -2 dimension. + + Returns: + The new dataset. + """ + datasets = list(self.datasets.values()) + if mask is not None or deepcopy: + new_datasets = [] + for outcome, ds in self.datasets.items(): + new_datasets.append( + ds.clone( + deepcopy=deepcopy, + mask=mask if outcome == self.target_outcome_name else None, + ) + ) + datasets = new_datasets + return MultiTaskDataset( + datasets=datasets, + target_outcome_name=self.target_outcome_name, + task_feature_index=self.task_feature_index, + )
+
+ + + +
+[docs] +class ContextualDataset(SupervisedDataset): + """This is a contextual dataset that is constructed from either a single + dateset containing overall outcome or a list of datasets that each corresponds + to a context breakdown. + """ + + def __init__( + self, + datasets: list[SupervisedDataset], + parameter_decomposition: dict[str, list[str]], + metric_decomposition: dict[str, list[str]] | None = None, + ): + """Construct a `ContextualDataset`. + + Args: + datasets: A list of the datasets of individual tasks. Each dataset + is expected to contain data for only one outcome. + parameter_decomposition: Dict from context name to list of feature + names corresponding to that context. + metric_decomposition: Context breakdown metrics. Keys are context names. + Values are the lists of metric names belonging to the context: + {'context1': ['m1_c1'], 'context2': ['m1_c2'],}. + """ + self.datasets: dict[str, SupervisedDataset] = { + ds.outcome_names[0]: ds for ds in datasets + } + self.feature_names = datasets[0].feature_names + self.outcome_names = list(self.datasets.keys()) + self.parameter_decomposition = parameter_decomposition + self.metric_decomposition = metric_decomposition + self._validate_datasets() + self._validate_decompositions() + self.context_buckets = self._extract_context_buckets() + self.parameter_index_decomp = { + c: [self.feature_names.index(i) for i in parameter_decomposition[c]] + for c in self.context_buckets + } + + @property + def X(self) -> Tensor: + return self.datasets[self.outcome_names[0]].X + + @property + def Y(self) -> Tensor: + """Concatenates the Ys from the child datasets to create the Y expected + by LCEM model if there are multiple datasets; Or return the Y expected + by LCEA model if there is only one dataset. + """ + Ys = [ds.Y for ds in self.datasets.values()] + if len(Ys) == 1: + return Ys[0] + else: + return torch.cat(Ys, dim=-1) + + @property + def Yvar(self) -> Tensor | None: + """Concatenates the Yvars from the child datasets to create the Y expected + by LCEM model if there are multiple datasets; Or return the Yvar expected + by LCEA model if there is only one dataset. + """ + Yvars = [ds.Yvar for ds in self.datasets.values()] + if Yvars[0] is None: + return None + elif len(Yvars) == 1: + return Yvars[0] + else: + return torch.cat(Yvars, dim=-1) + + def _extract_context_buckets(self) -> list[str]: + """Determines the context buckets from the data, and sets the + context_buckets attribute. + + If we have an outcome for each context, we will lists the contexts + in the same order as the outcomes (i.e., the order of datasets). + + If there is a single outcome (aggregated across contexts), the context + buckets are taken from the parameter decomposition. + """ + if len(self.outcome_names) > 1: + assert len(self.outcome_names) == len( + self.metric_decomposition + ), "Expected a single dataset, or one for each context bucket." + context_buckets = [] + for outcome_name in self.outcome_names: + for k, v in self.metric_decomposition.items(): + if outcome_name in v: + context_buckets.append(k) + break + else: + context_buckets = list(self.parameter_decomposition.keys()) + return context_buckets + + def _validate_datasets(self) -> None: + """Validation of given datasets. + 1. each dataset has same X. + 2. metric_decomposition is not None if there are multiple datasets. + 3. metric_decomposition contains all the outcomes in datasets. + 4. value keys of parameter decomposition and the keys of + metric_decomposition match context buckets. + 5. Yvar is None for all, or not for all. + """ + datasets = list(self.datasets.values()) + X = datasets[0].X + Yvar_is_none = datasets[0].Yvar is None + for dataset in datasets: + if torch.equal(X, dataset.X) is not True: + raise InputDataError("Require same X for context buckets") + if (dataset.Yvar is None) != Yvar_is_none: + raise InputDataError( + "Require Yvar to be specified for all buckets, or for none" + ) + + if len(datasets) > 1: + if self.metric_decomposition is None: + raise InputDataError( + "metric_decomposition must be provided when there are" + + " multiple datasets." + ) + else: + if self.metric_decomposition is not None: + raise InputDataError( + "metric_decomposition is redundant when there is one " + + "dataset for overall outcome." + ) + + def _validate_decompositions(self) -> None: + """Checks that the decompositions are valid. + + Raises: + InputDataError: If any of the decompositions are invalid. + """ + if self.metric_decomposition is not None: + m = len(list(self.metric_decomposition.values())[0]) + existing_metrics = set() + for v in self.metric_decomposition.values(): + if existing_metrics.intersection(list(v)): + raise InputDataError( + "metric_decomposition has same metric for multiple contexts." + ) + if len(v) != m or len(set(v)) != m: + raise InputDataError( + "All values in metric_decomposition must have the same length." + ) + existing_metrics.update(list(v)) + + if set(self.metric_decomposition.keys()) != set( + self.parameter_decomposition.keys() + ): + raise InputDataError( + "Keys of metric and parameter decompositions do not match." + ) + + all_metrics = [] + for m in self.metric_decomposition.values(): + all_metrics.extend(m) + for outcome in self.outcome_names: + if outcome not in all_metrics: + raise InputDataError( + f"{outcome} is missing in metric_decomposition." + ) + +
+[docs] + def clone( + self, deepcopy: bool = False, mask: Tensor | None = None + ) -> ContextualDataset: + """Return a copy of the dataset. + + Args: + deepcopy: If True, perform a deep copy. Otherwise, use the same + tensors/lists/datasets. + mask: A `n`-dim boolean mask indicating which rows to keep. This is used + along the -2 dimension. `n` here corresponds to the number of rows in + an individual dataset. + + Returns: + The new dataset. + """ + datasets = list(self.datasets.values()) + if mask is not None or deepcopy: + datasets = [ds.clone(deepcopy=deepcopy, mask=mask) for ds in datasets] + if deepcopy: + parameter_decomposition = copy.deepcopy(self.parameter_decomposition) + metric_decomposition = copy.deepcopy(self.metric_decomposition) + else: + parameter_decomposition = self.parameter_decomposition + metric_decomposition = self.metric_decomposition + return ContextualDataset( + datasets=datasets, + parameter_decomposition=parameter_decomposition, + metric_decomposition=metric_decomposition, + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/dispatcher.html b/website-old/pages/api/_modules/botorch/utils/dispatcher.html new file mode 100644 index 0000000000..69415e98ca --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/dispatcher.html @@ -0,0 +1,230 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.dispatcher

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from inspect import getsource, getsourcefile
+from typing import Any
+
+from multipledispatch.dispatcher import (
+    Dispatcher as MDDispatcher,
+    MDNotImplementedError,  # trivial subclass of NotImplementedError
+    str_signature,
+)
+
+
+
+[docs] +def type_bypassing_encoder(arg: Any) -> type: + # Allow type variables to be passed as pre-encoded arguments + return arg if isinstance(arg, type) else type(arg)
+ + + +
+[docs] +class Dispatcher(MDDispatcher): + r"""Clearing house for multiple dispatch functionality. This class extends + `<multipledispatch.Dispatcher>` by: (i) generalizing the argument encoding + convention during method lookup, (ii) implementing `__getitem__` as a dedicated + method lookup function. + """ + + def __init__( + self, + name: str, + doc: str | None = None, + encoder: Callable[Any, type] = type, + ) -> None: + """ + Args: + name: A string identifier for the `Dispatcher` instance. + doc: A docstring for the multiply dispatched method(s). + encoder: A callable that individually transforms the arguments passed + at runtime in order to construct the key used for method lookup as + `tuple(map(encoder, args))`. Defaults to `type`. + """ + super().__init__(name=name, doc=doc) + self._encoder = encoder + + def __getitem__( + self, + args: Any | None = None, + types: tuple[type] | None = None, + ) -> Callable: + r"""Method lookup. + + Args: + args: A set of arguments that act as identifiers for a stored method. + types: A tuple of types that encodes `args`. + + Returns: + A callable corresponding to the given `args` or `types`. + """ + if types is None: + if args is None: + raise RuntimeError("One of `args` or `types` must be provided.") + types = self.encode_args(args) + elif args is not None: + raise RuntimeError("Only one of `args` or `types` may be provided.") + + try: + func = self._cache[types] + except KeyError: + func = self.dispatch(*types) + if not func: + msg = f"{self.name}: <{', '.join(cls.__name__ for cls in types)}" + raise NotImplementedError(f"Could not find signature for {msg}") + self._cache[types] = func + return func + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + r"""Multiply dispatches a call to a collection of methods. + + Args: + args: A set of arguments that act as identifiers for a stored method. + kwargs: Optional keyword arguments passed to the retrieved method. + + Returns: + The result of evaluating `func(*args, **kwargs)`, where `func` is + the function obtained via method lookup. + """ + types = self.encode_args(args) + func = self.__getitem__(types=types) + try: + return func(*args, **kwargs) + except MDNotImplementedError: + # Traverses registered methods in order, yields whenever a match is found + funcs = self.dispatch_iter(*types) + next(funcs) # burn first, same as self.__getitem__(types=types) + for func in funcs: + try: + return func(*args, **kwargs) + except MDNotImplementedError: + pass + + raise NotImplementedError( + f"Matching functions for {self.name:s}: {str_signature(types):s} " + "found, but none completed successfully" + ) + +
+[docs] + def dispatch(self, *types: type) -> Callable: + r"""Method lookup strategy. Checks for an exact match before traversing + the set of registered methods according to the current ordering. + + Args: + types: A tuple of types that gets compared with the signatures + of registered methods to determine compatibility. + + Returns: + The first method encountered with a matching signature. + """ + if types in self.funcs: + return self.funcs[types] + try: + return next(self.dispatch_iter(*types)) + except StopIteration: + return None
+ + +
+[docs] + def encode_args(self, args: Any) -> tuple[type]: + r"""Converts arguments into a tuple of types used during method lookup.""" + return tuple(map(self.encoder, args if isinstance(args, tuple) else (args,)))
+ + + def _help(self, *args: Any) -> str: + r"""Returns the retrieved method's docstring.""" + return self.dispatch(*self.encode_args(args)).__doc__ + +
+[docs] + def help(self, *args: Any, **kwargs: Any) -> None: + r"""Prints the retrieved method's docstring.""" + print(self._help(*args))
+ + + def _source(self, *args: Any) -> str: + r"""Returns the retrieved method's source types as a string.""" + func = self.dispatch(*self.encode_args(args)) + if not func: + raise TypeError("No function found") + return f"File: {getsourcefile(func)}\n\n{getsource(func)}" + +
+[docs] + def source(self, *args, **kwargs) -> None: + r"""Prints the retrieved method's source types.""" + print(self._source(*args))
+ + + @property + def encoder(self) -> Callable[Any, type]: + return self._encoder
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/feasible_volume.html b/website-old/pages/api/_modules/botorch/utils/feasible_volume.html new file mode 100644 index 0000000000..2ada3f2343 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/feasible_volume.html @@ -0,0 +1,272 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.feasible_volume

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import botorch.models.model as model
+import torch
+from botorch.logging import _get_logger
+from botorch.utils.sampling import manual_seed
+from torch import Tensor
+
+
+logger = _get_logger(name="Feasibility")
+
+
+
+[docs] +def get_feasible_samples( + samples: Tensor, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, +) -> tuple[Tensor, float]: + r""" + Checks which of the samples satisfy all of the inequality constraints. + + Args: + samples: A `sample size x d` size tensor of feature samples, + where d is a feature dimension. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + Returns: + 2-element tuple containing + + - Samples satisfying the linear constraints. + - Estimated proportion of samples satisfying the linear constraints. + """ + + if inequality_constraints is None: + return samples, 1.0 + + nsamples = samples.size(0) + + feasible = torch.ones(nsamples, device=samples.device, dtype=torch.bool) + + for indices, coefficients, rhs in inequality_constraints: + lhs = samples.index_select(1, indices) @ coefficients.to(dtype=samples.dtype) + feasible &= lhs >= rhs + + feasible_samples = samples[feasible] + + p_linear = feasible_samples.size(0) / nsamples + + return feasible_samples, p_linear
+ + + +
+[docs] +def get_outcome_feasibility_probability( + model: model.Model, + X: Tensor, + outcome_constraints: list[Callable[[Tensor], Tensor]], + threshold: float = 0.1, + nsample_outcome: int = 1000, + seed: int | None = None, +) -> float: + r""" + Monte Carlo estimate of the feasible volume with respect to the outcome constraints. + + Args: + model: The model used for sampling the posterior. + X: A tensor of dimension `batch-shape x 1 x d`, where d is feature dimension. + outcome_constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply feasibility. + threshold: A lower limit for the probability of posterior samples feasibility. + nsample_outcome: The number of samples from the model posterior. + seed: The seed for the posterior sampler. If omitted, use a random seed. + + Returns: + Estimated proportion of features for which posterior samples satisfy + given outcome constraints with probability above or equal to + the given threshold. + """ + if outcome_constraints is None: + return 1.0 + + from botorch.sampling.get_sampler import get_sampler + + seed = seed if seed is not None else torch.randint(0, 1000000, (1,)).item() + + posterior = model.posterior(X) # posterior consists of batch_shape marginals + sampler = get_sampler( + posterior=posterior, sample_shape=torch.Size([nsample_outcome]), seed=seed + ) + # size of samples: (num outcome samples, batch_shape, 1, outcome dim) + samples = sampler(posterior) + + feasible = torch.ones(samples.shape[:-1], dtype=torch.bool, device=samples.device) + + # a sample passes if each constraint applied to the sample + # produces a non-negative tensor + for oc in outcome_constraints: + # broadcasted evaluation of the outcome constraints + feasible &= oc(samples) <= 0 + + # proportion of feasibile samples for each of the elements of X + # summation is done across feasible outcome samples + p_feas = feasible.sum(0).float() / feasible.size(0) + + # proportion of features leading to the posterior outcome + # satisfying the given outcome constraints + # with at probability above a given threshold + p_outcome = (p_feas >= threshold).sum().item() / X.size(0) + + return p_outcome
+ + + +
+[docs] +def estimate_feasible_volume( + bounds: Tensor, + model: model.Model, + outcome_constraints: list[Callable[[Tensor], Tensor]], + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + nsample_feature: int = 1000, + nsample_outcome: int = 1000, + threshold: float = 0.1, + verbose: bool = False, + seed: int | None = None, + device: torch.device | None = None, + dtype: torch.dtype | None = None, +) -> tuple[float, float]: + r""" + Monte Carlo estimate of the feasible volume with respect + to feature constraints and outcome constraints. + + Args: + bounds: A `2 x d` tensor of lower and upper bounds + for each column of `X`. + model: The model used for sampling the outcomes. + outcome_constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. + inequality constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + nsample_feature: The number of feature samples satisfying the bounds. + nsample_outcome: The number of outcome samples from the model posterior. + threshold: A lower limit for the probability of outcome feasibility + seed: The seed for both feature and outcome samplers. If omitted, + use a random seed. + verbose: An indicator for whether to log the results. + + Returns: + 2-element tuple containing: + + - Estimated proportion of volume in feature space that is + feasible wrt the bounds and the inequality constraints (linear). + - Estimated proportion of feasible features for which + posterior samples (outcome) satisfies the outcome constraints + with probability above the given threshold. + """ + + seed = seed if seed is not None else torch.randint(0, 1000000, (1,)).item() + + with manual_seed(seed=seed): + box_samples = bounds[0] + (bounds[1] - bounds[0]) * torch.rand( + (nsample_feature, bounds.size(1)), dtype=dtype, device=device + ) + + features, p_feature = get_feasible_samples( + samples=box_samples, inequality_constraints=inequality_constraints + ) # each new feature sample is a row + + p_outcome = get_outcome_feasibility_probability( + model=model, + X=features.unsqueeze(-2), + outcome_constraints=outcome_constraints, + threshold=threshold, + nsample_outcome=nsample_outcome, + seed=seed, + ) + + if verbose: # pragma: no cover + logger.info( + "Proportion of volume that satisfies linear constraints: " + + f"{p_feature:.4e}" + ) + if p_feature <= 0.01: + logger.warning( + "The proportion of satisfying volume is very low and may lead to " + + "very long run times. Consider making your constraints less " + + "restrictive." + ) + logger.info( + "Proportion of linear-feasible volume that also satisfies each " + + f"outcome constraint with probability > 0.1: {p_outcome:.4e}" + ) + if p_outcome <= 0.001: + logger.warning( + "The proportion of volume that also satisfies the outcome constraint " + + "is very low. Consider making your parameter and outcome constraints " + + "less restrictive." + ) + return p_feature, p_outcome
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/gp_sampling.html b/website-old/pages/api/_modules/botorch/utils/gp_sampling.html new file mode 100644 index 0000000000..6873f4e88e --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/gp_sampling.html @@ -0,0 +1,629 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.gp_sampling

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import warnings
+from copy import deepcopy
+from math import pi
+
+import torch
+from botorch.models.converter import batched_to_model_list
+from botorch.models.deterministic import GenericDeterministicModel
+from botorch.models.model import Model, ModelList
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.multitask import MultiTaskGP
+from botorch.utils.sampling import manual_seed
+from botorch.utils.transforms import is_ensemble
+from gpytorch.kernels import Kernel, MaternKernel, RBFKernel, ScaleKernel
+from linear_operator.utils.cholesky import psd_safe_cholesky
+from torch import Tensor
+from torch.distributions import MultivariateNormal
+from torch.nn import Module
+
+
+
+[docs] +class GPDraw(Module): + r"""Convenience wrapper for sampling a function from a GP prior. + + This wrapper implicitly defines the GP sample as a self-updating function by keeping + track of the evaluated points and respective base samples used during the + evaluation. + + This does not yet support multi-output models. + """ + + def __init__(self, model: Model, seed: int | None = None) -> None: + r"""Construct a GP function sampler. + + Args: + model: The Model defining the GP prior. + """ + warnings.warn( + "`GPDraw` is deprecated and will be removed in v0.13 release. " + "For drawing GP sample paths, we recommend using pathwise " + "sampling code found in `botorch/sampling/pathwise`. We recommend " + "`get_matheron_path_model` for most use cases.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__() + self._model = deepcopy(model) + self._num_outputs = self._model.num_outputs + seed = torch.tensor( + seed if seed is not None else torch.randint(0, 1000000, (1,)).item() + ) + self.register_buffer("_seed", seed) + + @property + def Xs(self) -> Tensor: + """A `(batch_shape) x n_eval x d`-dim tensor of locations at which the GP was + evaluated (or `None` if the sample has never been evaluated). + """ + try: + return self._Xs + except AttributeError: + return None + + @property + def Ys(self) -> Tensor: + """A `(batch_shape) x n_eval x d`-dim tensor of associated function values (or + `None` if the sample has never been evaluated). + """ + try: + return self._Ys + except AttributeError: + return None + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + r"""Evaluate the GP sample function at a set of points X. + + Args: + X: A `batch_shape x n x d`-dim tensor of points + + Returns: + The value of the GP sample at the `n` points. + """ + if self.Xs is None: + X_eval = X # first time, no previous evaluation points + else: + X_eval = torch.cat([self.Xs, X], dim=-2) + posterior = self._model.posterior(X=X_eval) + base_sample_shape = posterior.base_sample_shape + if self._num_outputs == 1: + # Needed to comply with base sample shape assumptions made here. + base_sample_shape = base_sample_shape + (1,) + # re-use old samples + bs_shape = base_sample_shape[:-2] + X.shape[-2:-1] + base_sample_shape[-1:] + with manual_seed(seed=int(self._seed)): + new_base_samples = torch.randn(bs_shape, device=X.device, dtype=X.dtype) + seed = self._seed + 1 + if self.Xs is None: + base_samples = new_base_samples + else: + base_samples = torch.cat([self._base_samples, new_base_samples], dim=-2) + # TODO: Deduplicate repeated evaluations / deal with numerical degeneracies + # that could lead to non-deterministic evaluations. We could use SVD- or + # eigendecomposition-based sampling, but we probably don't want to use this + # by default for performance reasonse. + Ys = posterior.rsample_from_base_samples( + torch.Size(), + base_samples=( + base_samples.squeeze(-1) if self._num_outputs == 1 else base_samples + ), + ) + self.register_buffer("_Xs", X_eval) + self.register_buffer("_Ys", Ys) + self.register_buffer("_seed", seed) + self.register_buffer("_base_samples", base_samples) + return self.Ys[..., -(X.size(-2)) :, :]
+
+ + + +
+[docs] +class RandomFourierFeatures(Module): + """A class that represents Random Fourier Features.""" + + def __init__( + self, + kernel: Kernel, + input_dim: int, + num_rff_features: int, + sample_shape: torch.Size | None = None, + ) -> None: + r"""Initialize RandomFourierFeatures. + + Args: + kernel: The GP kernel. + input_dim: The input dimension to the GP kernel. + num_rff_features: The number of Fourier features. + sample_shape: The shape of a single sample. For a single-element + `torch.Size` object, this is simply the number of RFF draws. + """ + if not isinstance(kernel, ScaleKernel): + base_kernel = kernel + outputscale = torch.ones(kernel.batch_shape).to( + dtype=kernel.lengthscale.dtype, + device=kernel.lengthscale.device, + ) + else: + base_kernel = kernel.base_kernel + outputscale = kernel.outputscale.detach().clone() + if not isinstance(base_kernel, (MaternKernel, RBFKernel)): + raise NotImplementedError("Only Matern and RBF kernels are supported.") + super().__init__() + self.kernel_batch_shape = base_kernel.batch_shape + self.register_buffer("outputscale", outputscale) + + self.register_buffer("lengthscale", base_kernel.lengthscale.detach().clone()) + self.sample_shape = torch.Size() if sample_shape is None else sample_shape + self.register_buffer( + "weights", + self._get_weights( + base_kernel=base_kernel, + input_dim=input_dim, + num_rff_features=num_rff_features, + sample_shape=self.sample_shape, + ), + ) + # initialize uniformly in [0, 2 * pi] + self.register_buffer( + "bias", + 2 + * pi + * torch.rand( + *self.sample_shape, + *self.kernel_batch_shape, + num_rff_features, + dtype=base_kernel.lengthscale.dtype, + device=base_kernel.lengthscale.device, + ), + ) + + def _get_weights( + self, + base_kernel: Kernel, + input_dim: int, + num_rff_features: int, + sample_shape: torch.Size | None = None, + ) -> Tensor: + r"""Sample weights for RFF. + + Args: + kernel: The GP base kernel. + input_dim: The input dimension to the GP kernel. + num_rff_features: The number of Fourier features. + sample_shape: The sample shape of weights. + Returns: + A tensor of weights with shape + `(*sample_shape, *kernel_batch_shape, input_dim, num_rff_features)`. + """ + sample_shape = torch.Size() if sample_shape is None else sample_shape + weights = torch.randn( + *sample_shape, + *self.kernel_batch_shape, + input_dim, + num_rff_features, + dtype=base_kernel.lengthscale.dtype, + device=base_kernel.lengthscale.device, + ) + if isinstance(base_kernel, MaternKernel): + gamma_dist = torch.distributions.Gamma(base_kernel.nu, base_kernel.nu) + gamma_samples = gamma_dist.sample(torch.Size([1, num_rff_features])).to( + weights + ) + weights = torch.rsqrt(gamma_samples) * weights + return weights + +
+[docs] + def forward(self, X: Tensor) -> Tensor: + """Get Fourier basis features for the provided inputs. + + Note that the right-most subset of the batch shape of `X` should + be `(sample_shape) x (kernel_batch_shape)` if using either the + `sample_shape` argument or a batched kernel. In other words, + `X` should be of shape `(added_batch_shape) x (sample_shape) x + (kernel_batch_shape) x n x input_dim`, where parantheses denote + that the given batch shape can be empty. `X` can always be + a tensor of shape `n x input_dim`, in which case broadcasting + will take care of the batch shape. This will raise a `ValueError` + if the batch shapes are not compatible. + + Args: + X: Input tensor of shape `(batch_shape) x n x input_dim`. + + Returns: + A Tensor of shape `(batch_shape) x n x rff`. If `X` does not have + a `batch_shape`, the output `batch_shape` will be + `(sample_shape) x (kernel_batch_shape)`. + """ + try: + self._check_forward_X_shape_compatibility(X) + except ValueError as e: + # A workaround to support batched SAAS models. + # TODO: Support batch evaluation of multi-sample RFFs as well. + # Multi-sample RFFs have input batch as the 0-th dimension, + # which is different than other posteriors which would have + # the sample shape as the 0-th dimension. + if len(self.kernel_batch_shape) == 1: + X = X.unsqueeze(-3) + self._check_forward_X_shape_compatibility(X) + else: + raise e + + # X is of shape (additional_batch_shape) x (sample_shape) + # x (kernel_batch_shape) x n x d. + # Weights is of shape (sample_shape) x (kernel_batch_shape) x d x num_rff. + X_scaled = torch.div(X, self.lengthscale) + batchmatmul = X_scaled @ self.weights + bias = self.bias + # Bias is of shape (sample_shape) x (kernel_batch_shape) x num_rff. + # Batchmatmul is of shape (additional_batch_shape) x (sample_shape) + # x (kernel_batch_shape) x n x num_rff. + outputs = torch.cos(batchmatmul + bias.unsqueeze(-2)) + # Make sure we divide at the correct (i.e., kernel's) batch dimension. + if len(self.kernel_batch_shape) > 0: + outputscale = self.outputscale.view(*self.kernel_batch_shape, 1, 1) + else: + outputscale = self.outputscale + return torch.sqrt(2.0 * outputscale / self.weights.shape[-1]) * outputs
+ + + def _check_forward_X_shape_compatibility(self, X: Tensor) -> None: + r"""Check that the `batch_shape` of X, if any, is compatible with the + `sample_shape` & `kernel_batch_shape`. + """ + full_batch_shape_X = X.shape[:-2] + len_full_batch_shape_X = len(full_batch_shape_X) + if len_full_batch_shape_X == 0: + # Non-batched X. + return + expected_batch_shape = self.sample_shape + self.kernel_batch_shape + # Check if they're broadcastable. + for b_idx in range(min(len(expected_batch_shape), len_full_batch_shape_X)): + neg_idx = -b_idx - 1 + if ( + full_batch_shape_X[neg_idx] != expected_batch_shape[neg_idx] + and full_batch_shape_X[neg_idx] != 1 + ): + raise ValueError( + "the batch shape of X is expected to follow the pattern: " + f"`... x {tuple(expected_batch_shape)}`" + )
+ + + +
+[docs] +def get_deterministic_model_multi_samples( + weights: list[Tensor], + bases: list[RandomFourierFeatures], +) -> GenericDeterministicModel: + """ + Get a batched deterministic model that batch evaluates `n_samples` function + samples. This supports multi-output models as well. + + Args: + weights: A list of weights with `num_outputs` elements. Each weight is of + shape `(batch_shape_input) x n_samples x num_rff_features`, where + `(batch_shape_input)` is the batch shape of the inputs used to obtain the + posterior weights. + bases: A list of `RandomFourierFeatures` with `num_outputs` elements. Each + basis has a sample shape of `n_samples`. + n_samples: The number of function samples. + + Returns: + A batched `GenericDeterministicModel`s that batch evaluates `n_samples` + function samples. + """ + eval_callables = [ + get_eval_gp_sample_callable(w=w, basis=basis) + for w, basis in zip(weights, bases) + ] + + def evaluate_gps_X(X): + return torch.cat([_f(X) for _f in eval_callables], dim=-1) + + return GenericDeterministicModel( + f=evaluate_gps_X, + num_outputs=len(weights), + )
+ + + +
+[docs] +def get_eval_gp_sample_callable(w: Tensor, basis: RandomFourierFeatures) -> Tensor: + def _f(X): + return basis(X) @ w.unsqueeze(-1) + + return _f
+ + + +
+[docs] +def get_deterministic_model( + weights: list[Tensor], bases: list[RandomFourierFeatures] +) -> GenericDeterministicModel: + """Get a deterministic model using the provided weights and bases for each output. + + Args: + weights: A list of weights with `m` elements. + bases: A list of `RandomFourierFeatures` with `m` elements. + + Returns: + A deterministic model. + """ + callables = [ + get_eval_gp_sample_callable(w=w, basis=basis) + for w, basis in zip(weights, bases) + ] + + def evaluate_gp_sample(X): + return torch.cat([c(X) for c in callables], dim=-1) + + return GenericDeterministicModel(f=evaluate_gp_sample, num_outputs=len(weights))
+ + + +
+[docs] +def get_deterministic_model_list( + weights: list[Tensor], + bases: list[RandomFourierFeatures], +) -> ModelList: + """Get a deterministic model list using the provided weights and bases + for each output. + + Args: + weights: A list of weights with `m` elements. + bases: A list of `RandomFourierFeatures` with `m` elements. + + Returns: + A deterministic model. + """ + samples = [] + for w, basis in zip(weights, bases): + sample = GenericDeterministicModel( + f=get_eval_gp_sample_callable(w=w, basis=basis), + num_outputs=1, + ) + samples.append(sample) + return ModelList(*samples)
+ + + +
+[docs] +def get_weights_posterior(X: Tensor, y: Tensor, sigma_sq: Tensor) -> MultivariateNormal: + r"""Sample bayesian linear regression weights. + + Args: + X: A tensor of inputs with shape `(*batch_shape, n num_rff_features)`. + y: A tensor of outcomes with shape `(*batch_shape, n)`. + sigma_sq: The likelihood noise variance. This should be a tensor with + shape `kernel_batch_shape, 1, 1` if using a batched kernel. + Otherwise, it should be a scalar tensor. + + Returns: + The posterior distribution over the weights. + """ + with torch.no_grad(): + X_trans = X.transpose(-2, -1) + A = X_trans @ X + sigma_sq * torch.eye( + X.shape[-1], dtype=X.dtype, device=X.device + ) + # mean is given by: m = S @ x.T @ y, where S = A_inv + # compute inverse of A using solves + # covariance is A_inv * sigma + L_A = psd_safe_cholesky(A) + # solve L_A @ u = I + Iw = torch.eye(L_A.shape[-1], dtype=X.dtype, device=X.device) + u = torch.linalg.solve_triangular(L_A, Iw, upper=False) + + # solve L_A^T @ S = u + A_inv = torch.linalg.solve_triangular(L_A.transpose(-2, -1), u, upper=True) + m = (A_inv @ X_trans @ y.unsqueeze(-1)).squeeze(-1) + L = psd_safe_cholesky(A_inv * sigma_sq) + return MultivariateNormal(loc=m, scale_tril=L)
+ + + +
+[docs] +def get_gp_samples( + model: Model, num_outputs: int, n_samples: int, num_rff_features: int = 512 +) -> GenericDeterministicModel: + r"""Sample functions from GP posterior using RFFs. The returned + `GenericDeterministicModel` effectively wraps `num_outputs` models, + each of which has a batch shape of `n_samples`. Refer + `get_deterministic_model_multi_samples` for more details. + + NOTE: If using input / outcome transforms, the gp samples must be accessed via + the `gp_sample.posterior(X)` call. Otherwise, `gp_sample(X)` will produce bogus + values that do not agree with the underlying `model`. It is also highly recommended + to use outcome transforms to standardize the input data, since the gp samples do + not work well when training outcomes are not zero-mean. + + Args: + model: The model. + num_outputs: The number of outputs. + n_samples: The number of functions to be sampled IID. + num_rff_features: The number of random Fourier features. + + Returns: + A `GenericDeterministicModel` that evaluates `n_samples` sampled functions. + If `n_samples > 1`, this will be a batched model. + """ + warnings.warn( + "`get_gp_samples` is deprecated and will be removed in v0.13 release. " + "For drawing GP sample paths, we recommend using pathwise " + "sampling code found in `botorch/sampling/pathwise`. We recommend " + "`get_matheron_path_model` for most use cases.", + DeprecationWarning, + stacklevel=2, + ) + # Get transforms from the model. + intf = getattr(model, "input_transform", None) + octf = getattr(model, "outcome_transform", None) + # Remove the outcome transform - leads to buggy draws. + if octf is not None: + del model.outcome_transform + if intf is not None: + del model.input_transform + + if num_outputs > 1: + if not isinstance(model, ModelListGP): + models = batched_to_model_list(model).models + else: + models = model.models + else: + models = [model] + if isinstance(models[0], MultiTaskGP): + raise NotImplementedError + + weights = [] + bases = [] + octfs = [] + intfs = [] + for m in range(num_outputs): + train_X = models[m].train_inputs[0] + train_targets = models[m].train_targets + _model = models[m] + _intf = getattr(_model, "input_transform", None) + _octf = getattr(_model, "outcome_transform", None) + # Remove the outcome transform - leads to buggy draws. + if _octf is not None: + del _model.outcome_transform + + octfs.append(_octf) + intfs.append(_intf) + # Get random Fourier features. + # sample_shape controls the number of iid functions. + basis = RandomFourierFeatures( + kernel=_model.covar_module, + input_dim=train_X.shape[-1], + num_rff_features=num_rff_features, + sample_shape=torch.Size([n_samples] if n_samples > 1 else []), + ) + bases.append(basis) + phi_X = basis(train_X) + # Sample weights from bayesian linear model. + # weights.sample().shape == (n_samples, batch_shape_input, num_rff_features) + sigma_sq = _model.likelihood.noise.mean(dim=-1, keepdim=True) + if len(basis.kernel_batch_shape) > 0: + sigma_sq = sigma_sq.unsqueeze(-2) + mvn = get_weights_posterior( + X=phi_X, + y=train_targets, + sigma_sq=sigma_sq, + ) + weights.append(mvn.sample()) + + # TODO: Ideally support RFFs for multi-outputs instead of having to + # generate a basis for each output serially. + if any(_octf is not None for _octf in octfs) or any( + _intf is not None for _intf in intfs + ): + base_gp_samples = get_deterministic_model_list( + weights=weights, + bases=bases, + ) + for m in range(len(weights)): + _octf = octfs[m] + _intf = intfs[m] + if _octf is not None: + base_gp_samples.models[m].outcome_transform = _octf + models[m].outcome_transform = _octf + if _intf is not None: + base_gp_samples.models[m].input_transform = _intf + base_gp_samples._is_ensemble = is_ensemble(model=model) + return base_gp_samples + elif n_samples > 1: + base_gp_samples = get_deterministic_model_multi_samples( + weights=weights, + bases=bases, + ) + else: + base_gp_samples = get_deterministic_model( + weights=weights, + bases=bases, + ) + # Load the transforms on the models. + if intf is not None: + base_gp_samples.input_transform = intf + model.input_transform = intf + if octf is not None: + base_gp_samples.outcome_transform = octf + model.outcome_transform = octf + base_gp_samples._is_ensemble = is_ensemble(model=model) + return base_gp_samples
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/low_rank.html b/website-old/pages/api/_modules/botorch/utils/low_rank.html new file mode 100644 index 0000000000..a122901489 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/low_rank.html @@ -0,0 +1,237 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.low_rank

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import torch
+from botorch.exceptions.errors import BotorchError
+from botorch.posteriors.base_samples import _reshape_base_samples_non_interleaved
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from gpytorch.distributions.multitask_multivariate_normal import (
+    MultitaskMultivariateNormal,
+)
+from linear_operator.operators import BlockDiagLinearOperator, LinearOperator
+
+from linear_operator.utils.cholesky import psd_safe_cholesky
+from linear_operator.utils.errors import NanError
+from torch import Tensor
+
+
+
+[docs] +def extract_batch_covar(mt_mvn: MultitaskMultivariateNormal) -> LinearOperator: + r"""Extract a batched independent covariance matrix from an MTMVN. + + Args: + mt_mvn: A multi-task multivariate normal with a block diagonal + covariance matrix. + + Returns: + A lazy covariance matrix consisting of a batch of the blocks of + the diagonal of the MultitaskMultivariateNormal. + + """ + lazy_covar = mt_mvn.lazy_covariance_matrix + if not isinstance(lazy_covar, BlockDiagLinearOperator): + raise BotorchError( + f"Expected BlockDiagLinearOperator, but got {type(lazy_covar)}." + ) + return lazy_covar.base_linear_op
+ + + +def _reshape_base_samples( + base_samples: Tensor, sample_shape: torch.Size, posterior: GPyTorchPosterior +) -> Tensor: + r"""Manipulate shape of base_samples to match `MultivariateNormal.rsample`. + + This ensure that base_samples are used in the same way as in + gpytorch.distributions.MultivariateNormal. For CBD, it is important to ensure + that the same base samples are used for the in-sample points here and in the + cached box decompositions. + + Args: + base_samples: The base samples. + sample_shape: The sample shape. + posterior: The joint posterior is over (X_baseline, X). + + Returns: + Reshaped and expanded base samples. + """ + mvn = posterior.distribution + loc = mvn.loc + peshape = posterior._extended_shape() + base_samples = base_samples.view( + sample_shape + torch.Size([1] * (loc.ndim - 1)) + peshape[-2:] + ).expand(sample_shape + loc.shape[:-1] + peshape[-2:]) + if posterior._is_mt: + base_samples = _reshape_base_samples_non_interleaved( + mvn=posterior.distribution, + base_samples=base_samples, + sample_shape=sample_shape, + ) + base_samples = base_samples.reshape( + -1, *loc.shape[:-1], mvn.lazy_covariance_matrix.shape[-1] + ) + base_samples = base_samples.permute(*range(1, loc.dim() + 1), 0) + return base_samples.reshape( + *peshape[:-2], + peshape[-1], + peshape[-2], + *sample_shape, + ) + + +
+[docs] +def sample_cached_cholesky( + posterior: GPyTorchPosterior, + baseline_L: Tensor, + q: int, + base_samples: Tensor, + sample_shape: torch.Size, + max_tries: int = 6, +) -> Tensor: + r"""Get posterior samples at the `q` new points from the joint multi-output + posterior. + + Args: + posterior: The joint posterior is over (X_baseline, X). + baseline_L: The baseline lower triangular cholesky factor. + q: The number of new points in X. + base_samples: The base samples. + sample_shape: The sample shape. + max_tries: The number of tries for computing the Cholesky + decomposition with increasing jitter. + + + Returns: + A `sample_shape x batch_shape x q x m`-dim tensor of posterior + samples at the new points. + """ + # compute bottom left covariance block + mvn = posterior.distribution + lazy_covar = ( + extract_batch_covar(mt_mvn=mvn) + if isinstance(mvn, MultitaskMultivariateNormal) + else mvn.lazy_covariance_matrix + ) + # Get the `q` new rows of the batched covariance matrix + bottom_rows = lazy_covar[..., -q:, :].to_dense() + # The covariance in block form is: + # [K(X_baseline, X_baseline), K(X_baseline, X)] + # [K(X, X_baseline), K(X, X)] + # bl := K(X, X_baseline) + # br := K(X, X) + # Get bottom right block of new covariance + bl, br = bottom_rows.split([bottom_rows.shape[-1] - q, q], dim=-1) + # Solve Ax = b + # where A = K(X_baseline, X_baseline) and b = K(X, X_baseline)^T + # and bl_chol := x^T + # bl_chol is the new `(batch_shape) x q x n`-dim bottom left block + # of the cholesky decomposition + bl_chol = torch.linalg.solve_triangular( + baseline_L, bl.transpose(-2, -1), upper=False + ).transpose(-2, -1) + + # Compute the new bottom right block of the Cholesky + # decomposition via: + # Cholesky(K(X, X) - bl_chol @ bl_chol^T) + br_to_chol = br - bl_chol @ bl_chol.transpose(-2, -1) + # TODO: technically we should make sure that we add a + # consistent nugget to the cached covariance and the new block + br_chol = psd_safe_cholesky(br_to_chol, max_tries=max_tries) + # Create a `(batch_shape) x q x (n+q)`-dim tensor containing the + # `q` new bottom rows of the Cholesky decomposition + new_Lq = torch.cat([bl_chol, br_chol], dim=-1) + mean = posterior.distribution.mean + base_samples = _reshape_base_samples( + base_samples=base_samples, + sample_shape=sample_shape, + posterior=posterior, + ) + if not isinstance(posterior.distribution, MultitaskMultivariateNormal): + # add output dim + mean = mean.unsqueeze(-1) + # add batch dim corresponding to output dim + new_Lq = new_Lq.unsqueeze(-3) + new_mean = mean[..., -q:, :] + res = ( + new_Lq.matmul(base_samples) + .add(new_mean.transpose(-1, -2).unsqueeze(-1)) + .permute(-1, *range(posterior.distribution.loc.dim() - 1), -2, -3) + .contiguous() + ) + contains_nans = torch.isnan(res).any() + contains_infs = torch.isinf(res).any() + if contains_nans or contains_infs: + suffix_args = [] + if contains_nans: + suffix_args.append("nans") + if contains_infs: + suffix_args.append("infs") + suffix = " and ".join(suffix_args) + raise NanError(f"Samples contain {suffix}.") + return res
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/box_decomposition.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/box_decomposition.html new file mode 100644 index 0000000000..682d3256ed --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/box_decomposition.html @@ -0,0 +1,462 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.box_decompositions.box_decomposition

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Box decomposition algorithms.
+
+References
+
+.. [Lacour17]
+    R. Lacour, K. Klamroth, C. Fonseca. A box decomposition algorithm to
+    compute the hypervolume indicator. Computers & Operations Research,
+    Volume 79, 2017.
+
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+import torch
+from botorch.exceptions.errors import BotorchError
+from botorch.utils.multi_objective.box_decompositions.utils import (
+    _expand_ref_point,
+    _pad_batch_pareto_frontier,
+    update_local_upper_bounds_incremental,
+)
+from botorch.utils.multi_objective.pareto import is_non_dominated
+from torch import Tensor
+from torch.nn import Module
+
+
+
+[docs] +class BoxDecomposition(Module, ABC): + r"""An abstract class for box decompositions. + + Note: Internally, we store the negative reference point (minimization). + """ + + def __init__(self, ref_point: Tensor, sort: bool, Y: Tensor | None = None) -> None: + """Initialize BoxDecomposition. + + Args: + ref_point: A `m`-dim tensor containing the reference point. + sort: A boolean indicating whether to sort the Pareto frontier. + Y: A `(batch_shape) x n x m`-dim tensor of outcomes. + """ + super().__init__() + self._neg_ref_point = -ref_point + self.sort = torch.tensor(sort, dtype=torch.bool) + self.num_outcomes = ref_point.shape[-1] + self.register_buffer("hypercell_bounds", None) + + if Y is not None: + if Y.isnan().any(): + raise ValueError( + "NaN inputs are not supported. Got Y with " + f"{Y.isnan().sum()} NaN values." + ) + self._neg_Y = -Y + self._validate_inputs() + self._neg_pareto_Y = self._compute_pareto_Y() + self.partition_space() + else: + self._neg_Y = None + self._neg_pareto_Y = None + + @property + def pareto_Y(self) -> Tensor: + r"""This returns the non-dominated set. + + Returns: + A `n_pareto x m`-dim tensor of outcomes. + """ + if self._neg_pareto_Y is not None: + return -self._neg_pareto_Y + raise BotorchError("pareto_Y has not been initialized") + + @property + def ref_point(self) -> Tensor: + r"""Get the reference point. + + Returns: + A `m`-dim tensor of outcomes. + """ + return -self._neg_ref_point + + @property + def Y(self) -> Tensor: + r"""Get the raw outcomes. + + Returns: + A `n x m`-dim tensor of outcomes. + """ + if self._neg_Y is not None: + return -self._neg_Y + raise BotorchError("Y data has not been initialized") + + def _compute_pareto_Y(self) -> Tensor: + if self._neg_Y is None: + raise BotorchError("Y data has not been initialized") + # is_non_dominated assumes maximization + if self._neg_Y.shape[-2] == 0: + return self._neg_Y + # assumes maximization + pareto_Y = -_pad_batch_pareto_frontier( + Y=self.Y, + ref_point=_expand_ref_point( + ref_point=self.ref_point, batch_shape=self.batch_shape + ), + ) + if not self.sort: + return pareto_Y + # sort by first objective + if len(self.batch_shape) > 0: + pareto_Y = pareto_Y.gather( + index=torch.argsort(pareto_Y[..., :1], dim=-2).expand(pareto_Y.shape), + dim=-2, + ) + else: + pareto_Y = pareto_Y[torch.argsort(pareto_Y[:, 0])] + return pareto_Y + + def _reset_pareto_Y(self) -> bool: + r"""Update the non-dominated front. + + Returns: + A boolean indicating whether the Pareto frontier has changed. + """ + pareto_Y = self._compute_pareto_Y() + + if (self._neg_pareto_Y is None) or not torch.equal( + pareto_Y, self._neg_pareto_Y + ): + self._neg_pareto_Y = pareto_Y + return True + return False + +
+[docs] + def partition_space(self) -> None: + r"""Compute box decomposition.""" + if self.num_outcomes == 2: + try: + self._partition_space_2d() + except NotImplementedError: + self._partition_space() + else: + self._partition_space()
+ + + def _partition_space_2d(self) -> None: + r"""Compute box decomposition for 2 objectives.""" + raise NotImplementedError + + @abstractmethod + def _partition_space(self) -> None: + r"""Partition the non-dominated space into disjoint hypercells. + + This method supports an arbitrary number of outcomes, but is + less efficient than `partition_space_2d` for the 2-outcome case. + """ + +
+[docs] + @abstractmethod + def get_hypercell_bounds(self) -> Tensor: + r"""Get the bounds of each hypercell in the decomposition. + + Returns: + A `2 x num_cells x num_outcomes`-dim tensor containing the + lower and upper vertices bounding each hypercell. + """
+ + + def _update_neg_Y(self, Y: Tensor) -> bool: + r"""Update the set of outcomes. + + Returns: + A boolean indicating if _neg_Y was initialized. + """ + if Y.isnan().any(): + raise ValueError( + "NaN inputs are not supported. Got Y with " + f"{Y.isnan().sum()} NaN values." + ) + # multiply by -1, since internally we minimize. + if self._neg_Y is not None: + self._neg_Y = torch.cat([self._neg_Y, -Y], dim=-2) + return False + self._neg_Y = -Y + return True + +
+[docs] + def update(self, Y: Tensor) -> None: + r"""Update non-dominated front and decomposition. + + By default, the partitioning is recomputed. Subclasses can override + this functionality. + + Args: + Y: A `(batch_shape) x n x m`-dim tensor of new, incremental outcomes. + """ + self._update_neg_Y(Y=Y) + self.reset()
+ + + def _validate_inputs(self) -> None: + self.batch_shape = self.Y.shape[:-2] + self.num_outcomes = self.Y.shape[-1] + if len(self.batch_shape) > 1: + raise NotImplementedError( + f"{type(self).__name__} only supports a single " + f"batch dimension, but got {len(self.batch_shape)} " + "batch dimensions." + ) + elif len(self.batch_shape) > 0 and self.num_outcomes > 2: + raise NotImplementedError( + f"{type(self).__name__} only supports a batched box " + f"decompositions in the 2-objective setting." + ) + +
+[docs] + def reset(self) -> None: + r"""Reset non-dominated front and decomposition.""" + self._validate_inputs() + is_new_pareto = self._reset_pareto_Y() + # Update decomposition if the Pareto front changed + if is_new_pareto: + self.partition_space()
+ + + @abstractmethod + def _compute_hypervolume_if_y_has_data(self) -> Tensor: + """Compute hypervolume for the case that there is data in self._neg_pareto_Y.""" + +
+[docs] + def compute_hypervolume(self) -> Tensor: + r"""Compute hypervolume that is dominated by the Pareto Froniter. + + Returns: + A `(batch_shape)`-dim tensor containing the hypervolume dominated by + each Pareto frontier. + """ + if self._neg_pareto_Y is None: + return torch.tensor(0.0) + + if self._neg_pareto_Y.shape[-2] == 0: + return torch.zeros( + self._neg_pareto_Y.shape[:-2], + dtype=self._neg_pareto_Y.dtype, + device=self._neg_pareto_Y.device, + ) + return self._compute_hypervolume_if_y_has_data()
+
+ + + +
+[docs] +class FastPartitioning(BoxDecomposition, ABC): + r"""A class for partitioning the (non-)dominated space into hyper-cells. + + Note: this assumes maximization. Internally, it multiplies outcomes by -1 + and performs the decomposition under minimization. + + This class is abstract to support to two applications of Alg 1 from + [Lacour17]_: 1) partitioning the space that is dominated by the Pareto + frontier and 2) partitioning the space that is not dominated by the + Pareto frontier. + """ + + def __init__( + self, + ref_point: Tensor, + Y: Tensor | None = None, + ) -> None: + """ + Args: + ref_point: A `m`-dim tensor containing the reference point. + Y: A `(batch_shape) x n x m`-dim tensor + """ + super().__init__(ref_point=ref_point, Y=Y, sort=ref_point.shape[-1] == 2) + +
+[docs] + def update(self, Y: Tensor) -> None: + r"""Update non-dominated front and decomposition. + + Args: + Y: A `(batch_shape) x n x m`-dim tensor of new, incremental outcomes. + """ + if self._update_neg_Y(Y=Y): + self.reset() + else: + if self.num_outcomes == 2 or self._neg_pareto_Y.shape[-2] == 0: + # If there are two objective, recompute the box decomposition + # because the partitions can be computed analytically. + # If the current pareto set has no points, recompute the box + # decomposition. + self.reset() + else: + # only include points that are better than the reference point + better_than_ref = (Y > self.ref_point).all(dim=-1) + Y = Y[better_than_ref] + Y_all = torch.cat([self._neg_pareto_Y, -Y], dim=-2) + pareto_mask = is_non_dominated(-Y_all) + # determine the number of points in Y that are Pareto optimal + num_new_pareto = pareto_mask[-Y.shape[-2] :].sum() + self._neg_pareto_Y = Y_all[pareto_mask] + if num_new_pareto > 0: + # update local upper bounds for the minimization problem + self._U, self._Z = update_local_upper_bounds_incremental( + # this assumes minimization + new_pareto_Y=self._neg_pareto_Y[-num_new_pareto:], + U=self._U, + Z=self._Z, + ) + # use the negative local upper bounds as the new pareto + # frontier for the minimization problem and perform + # box decomposition on dominated space. + self._get_partitioning()
+ + + @abstractmethod + def _get_single_cell(self) -> None: + r"""Set the partitioning to be a single cell in the case of no Pareto points. + + This method should set self.hypercell_bounds + """ + pass # pragma: no cover + +
+[docs] + def partition_space(self) -> None: + if self._neg_pareto_Y.shape[-2] == 0: + self._get_single_cell() + else: + super().partition_space()
+ + + def _partition_space(self): + r"""Partition the non-dominated space into disjoint hypercells. + + This method supports an arbitrary number of outcomes, but is + less efficient than `partition_space_2d` for the 2-outcome case. + """ + if len(self.batch_shape) > 0: + # this could be triggered when m=2 outcomes and + # BoxDecomposition._partition_space_2d is not overridden. + raise NotImplementedError( + "_partition_space does not support batch dimensions." + ) + # this assumes minimization + # initialize local upper bounds + self.register_buffer("_U", self._neg_ref_point.unsqueeze(-2).clone()) + # initialize defining points to be the dummy points \hat{z} that are + # defined in Sec 2.1 in [Lacour17]_. Note that in [Lacour17]_, outcomes + # are assumed to be between [0,1], so they used 0 rather than -inf. + self._Z = torch.zeros( + 1, + self.num_outcomes, + self.num_outcomes, + dtype=self.Y.dtype, + device=self.Y.device, + ) + for j in range(self.ref_point.shape[-1]): + # use ref point for maximization as the ideal point for minimization. + self._Z[0, j] = float("-inf") + self._Z[0, j, j] = self._U[0, j] + # incrementally update local upper bounds and defining points + # for each new Pareto point + self._U, self._Z = update_local_upper_bounds_incremental( + new_pareto_Y=self._neg_pareto_Y, + U=self._U, + Z=self._Z, + ) + self._get_partitioning() + + @abstractmethod + def _get_partitioning(self) -> None: + r"""Compute partitioning given local upper bounds for the minimization problem. + + This method should set self.hypercell_bounds + """ + pass # pragma: no cover + +
+[docs] + def get_hypercell_bounds(self) -> Tensor: + r"""Get the bounds of each hypercell in the decomposition. + + Returns: + A `2 x (batch_shape) x num_cells x m`-dim tensor containing the + lower and upper vertices bounding each hypercell. + """ + return self.hypercell_bounds
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/box_decomposition_list.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/box_decomposition_list.html new file mode 100644 index 0000000000..bf9f2855ed --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/box_decomposition_list.html @@ -0,0 +1,195 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.box_decompositions.box_decomposition_list

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Box decomposition container."""
+
+from __future__ import annotations
+
+import torch
+from botorch.exceptions.errors import BotorchTensorDimensionError
+from botorch.utils.multi_objective.box_decompositions.box_decomposition import (
+    BoxDecomposition,
+)
+from torch import Tensor
+from torch.nn import Module, ModuleList
+
+
+
+[docs] +class BoxDecompositionList(Module): + r"""A list of box decompositions.""" + + def __init__(self, *box_decompositions: BoxDecomposition) -> None: + r"""Initialize the box decomposition list. + + Args: + *box_decompositions: An variable number of box decompositions + + Example: + >>> bd1 = FastNondominatedPartitioning(ref_point, Y=Y1) + >>> bd2 = FastNondominatedPartitioning(ref_point, Y=Y2) + >>> bd = BoxDecompositionList(bd1, bd2) + """ + super().__init__() + self.box_decompositions = ModuleList(box_decompositions) + + @property + def pareto_Y(self) -> list[Tensor]: + r"""This returns the non-dominated set. + + Note: Internally, we store the negative pareto set (minimization). + + Returns: + A list where the ith element is the `n_pareto_i x m`-dim tensor + of pareto optimal outcomes for each box_decomposition `i`. + """ + return [p.pareto_Y for p in self.box_decompositions] + + @property + def ref_point(self) -> Tensor: + r"""Get the reference point. + + Note: Internally, we store the negative reference point (minimization). + + Returns: + A `n_box_decompositions x m`-dim tensor of outcomes. + """ + return torch.stack([p.ref_point for p in self.box_decompositions], dim=0) + +
+[docs] + def get_hypercell_bounds(self) -> Tensor: + r"""Get the bounds of each hypercell in the decomposition. + + Returns: + A `2 x n_box_decompositions x num_cells x num_outcomes`-dim tensor + containing the lower and upper vertices bounding each hypercell. + """ + bounds_list = [] + max_num_cells = 0 + for p in self.box_decompositions: + bounds = p.get_hypercell_bounds() + max_num_cells = max(max_num_cells, bounds.shape[-2]) + bounds_list.append(bounds) + # pad the decomposition with empty cells so that all + # decompositions have the same number of cells + for i, bounds in enumerate(bounds_list): + num_missing = max_num_cells - bounds.shape[-2] + if num_missing > 0: + padding = torch.zeros( + 2, + num_missing, + bounds.shape[-1], + dtype=bounds.dtype, + device=bounds.device, + ) + bounds_list[i] = torch.cat( + [ + bounds, + padding, + ], + dim=-2, + ) + + return torch.stack(bounds_list, dim=-3)
+ + +
+[docs] + def update(self, Y: list[Tensor] | Tensor) -> None: + r"""Update the partitioning. + + Args: + Y: A `n_box_decompositions x n x num_outcomes`-dim tensor or a list + where the ith element contains the new points for + box_decomposition `i`. + """ + if ( + torch.is_tensor(Y) + and Y.ndim != 3 + and Y.shape[0] != len(self.box_decompositions) + ) or (isinstance(Y, list) and len(Y) != len(self.box_decompositions)): + raise BotorchTensorDimensionError( + "BoxDecompositionList.update requires either a batched tensor Y, " + "with one batch per box decomposition or a list of tensors with " + "one element per box decomposition." + ) + for i, p in enumerate(self.box_decompositions): + p.update(Y[i])
+ + +
+[docs] + def compute_hypervolume(self) -> Tensor: + r"""Compute hypervolume that is dominated by the Pareto Froniter. + + Returns: + A `(batch_shape)`-dim tensor containing the hypervolume dominated by + each Pareto frontier. + """ + return torch.stack( + [p.compute_hypervolume() for p in self.box_decompositions], dim=0 + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/dominated.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/dominated.html new file mode 100644 index 0000000000..6996a54113 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/dominated.html @@ -0,0 +1,131 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.box_decompositions.dominated

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Algorithms for partitioning the dominated space into hyperrectangles."""
+
+from __future__ import annotations
+
+from botorch.utils.multi_objective.box_decompositions.box_decomposition import (
+    FastPartitioning,
+)
+from botorch.utils.multi_objective.box_decompositions.utils import (
+    compute_dominated_hypercell_bounds_2d,
+    get_partition_bounds,
+)
+from torch import Tensor
+
+
+
+[docs] +class DominatedPartitioning(FastPartitioning): + r"""Partition dominated space into axis-aligned hyperrectangles. + + This uses the Algorithm 1 from [Lacour17]_. + + Example: + >>> bd = DominatedPartitioning(ref_point, Y) + """ + + def _partition_space_2d(self) -> None: + r"""Partition the non-dominated space into disjoint hypercells. + + This direct method works for `m=2` outcomes. + """ + cell_bounds = compute_dominated_hypercell_bounds_2d( + # flip self.pareto_Y because it is sorted in decreasing order (since + # self._pareto_Y was sorted in increasing order and we multiplied by -1) + pareto_Y_sorted=self.pareto_Y.flip(-2), + ref_point=self.ref_point, + ) + self.hypercell_bounds = cell_bounds + + def _get_partitioning(self) -> None: + r"""Get the bounds of each hypercell in the decomposition.""" + minimization_cell_bounds = get_partition_bounds( + Z=self._Z, U=self._U, ref_point=self._neg_ref_point.view(-1) + ) + cell_bounds = -minimization_cell_bounds.flip(0) + self.hypercell_bounds = cell_bounds + + def _compute_hypervolume_if_y_has_data(self) -> Tensor: + r"""Compute hypervolume that is dominated by the Pareto Frontier. + + Returns: + A `(batch_shape)`-dim tensor containing the hypervolume dominated by + each Pareto frontier. + """ + return ( + (self.hypercell_bounds[1] - self.hypercell_bounds[0]) + .prod(dim=-1) + .sum(dim=-1) + ) + + def _get_single_cell(self) -> None: + r"""Set the partitioning to be a single cell in the case of no Pareto points.""" + # Set lower and upper bounds to be the reference point to define an empty cell + cell_bounds = self.ref_point.expand( + 2, *self._neg_pareto_Y.shape[:-2], 1, self.num_outcomes + ).clone() + self.hypercell_bounds = cell_bounds
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/non_dominated.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/non_dominated.html new file mode 100644 index 0000000000..badf30e4cf --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/non_dominated.html @@ -0,0 +1,522 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.box_decompositions.non_dominated

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Algorithms for partitioning the non-dominated space into rectangles.
+
+References
+
+.. [Couckuyt2012]
+    I. Couckuyt, D. Deschrijver and T. Dhaene, "Towards Efficient
+    Multiobjective Optimization: Multiobjective statistical criterions,"
+    2012 IEEE Congress on Evolutionary Computation, Brisbane, QLD, 2012,
+    pp. 1-8.
+
+"""
+
+from __future__ import annotations
+
+import torch
+from botorch.utils.multi_objective.box_decompositions.box_decomposition import (
+    BoxDecomposition,
+    FastPartitioning,
+)
+from botorch.utils.multi_objective.box_decompositions.utils import (
+    _expand_ref_point,
+    compute_non_dominated_hypercell_bounds_2d,
+    get_partition_bounds,
+    update_local_upper_bounds_incremental,
+)
+from torch import Tensor
+
+
+
+[docs] +class NondominatedPartitioning(BoxDecomposition): + r"""A class for partitioning the non-dominated space into hyper-cells. + + Note: this assumes maximization. Internally, it multiplies outcomes by -1 and + performs the decomposition under minimization. TODO: use maximization + internally as well. + + Note: it is only feasible to use this algorithm to compute an exact + decomposition of the non-dominated space for `m<5` objectives (alpha=0.0). + + The alpha parameter can be increased to obtain an approximate partitioning + faster. The `alpha` is a fraction of the total hypervolume encapsuling the + entire Pareto set. When a hypercell's volume divided by the total hypervolume + is less than `alpha`, we discard the hypercell. See Figure 2 in + [Couckuyt2012]_ for a visual representation. + + This PyTorch implementation of the binary partitioning algorithm ([Couckuyt2012]_) + is adapted from numpy/tensorflow implementation at: + https://github.com/GPflow/GPflowOpt/blob/master/gpflowopt/pareto.py. + + TODO: replace this with a more efficient decomposition. E.g. + https://link.springer.com/content/pdf/10.1007/s10898-019-00798-7.pdf + """ + + def __init__( + self, + ref_point: Tensor, + Y: Tensor | None = None, + alpha: float = 0.0, + ) -> None: + """Initialize NondominatedPartitioning. + + Args: + ref_point: A `m`-dim tensor containing the reference point. + Y: A `(batch_shape) x n x m`-dim tensor. + alpha: A thresold fraction of total volume used in an approximate + decomposition. + + Example: + >>> bd = NondominatedPartitioning(ref_point, Y=Y1) + """ + self.alpha = alpha + super().__init__(ref_point=ref_point, sort=True, Y=Y) + + def _partition_space(self) -> None: + r"""Partition the non-dominated space into disjoint hypercells. + + This method supports an arbitrary number of outcomes, but is + less efficient than `partition_space_2d` for the 2-outcome case. + """ + # The binary parititoning algorithm uses indices the augmented Pareto front. + # n_pareto + 2 x m + aug_pareto_Y_idcs = self._get_augmented_pareto_front_indices() + + # Initialize one cell over entire pareto front + cell = torch.zeros( + 2, self.num_outcomes, dtype=torch.long, device=self._neg_Y.device + ) + cell[1] = aug_pareto_Y_idcs.shape[0] - 1 + stack = [cell] + + # hypercells contains the indices of the (augmented) Pareto front + # that specify that bounds of the each hypercell. + # It is a `2 x num_cells x m`-dim tensor + self.hypercells = torch.empty( + 2, 0, self.num_outcomes, dtype=torch.long, device=self._neg_Y.device + ) + outcome_idxr = torch.arange( + self.num_outcomes, dtype=torch.long, device=self._neg_Y.device + ) + + # edge case: empty pareto set + # use a single cell + if self._neg_pareto_Y.shape[-2] == 0: + # 2 x m + cell_bounds_pareto_idcs = aug_pareto_Y_idcs[cell, outcome_idxr] + self.hypercells = torch.cat( + [self.hypercells, cell_bounds_pareto_idcs.unsqueeze(1)], dim=1 + ) + else: + # Extend Pareto front with the ideal and anti-ideal point + ideal_point = self._neg_pareto_Y.min(dim=0, keepdim=True).values - 1 + anti_ideal_point = self._neg_pareto_Y.max(dim=0, keepdim=True).values + 1 + # `n_pareto + 2 x m` + aug_pareto_Y = torch.cat( + [ideal_point, self._neg_pareto_Y, anti_ideal_point], dim=0 + ) + + total_volume = (anti_ideal_point - ideal_point).prod() + + # Use binary partitioning + while len(stack) > 0: + # The following 3 tensors are all `2 x m` + cell = stack.pop() + cell_bounds_pareto_idcs = aug_pareto_Y_idcs[cell, outcome_idxr] + cell_bounds_pareto_values = aug_pareto_Y[ + cell_bounds_pareto_idcs, outcome_idxr + ] + # Check cell bounds + # - if cell upper bound is better than Pareto front on all outcomes: + # - accept the cell + # - elif cell lower bound is better than Pareto front on all outcomes: + # - this means the cell overlaps the Pareto front. Divide the cell + # along its longest edge. + if ( + (cell_bounds_pareto_values[1] <= self._neg_pareto_Y) + .any(dim=1) + .all() + ): + # Cell is entirely non-dominated + self.hypercells = torch.cat( + [self.hypercells, cell_bounds_pareto_idcs.unsqueeze(1)], dim=1 + ) + elif ( + (cell_bounds_pareto_values[0] <= self._neg_pareto_Y) + .any(dim=1) + .all() + ): + # The cell overlaps the pareto front + # compute the distance (in integer indices) + # This has shape `m` + idx_dist = cell[1] - cell[0] + + any_not_adjacent = (idx_dist > 1).any() + cell_volume = ( + (cell_bounds_pareto_values[1] - cell_bounds_pareto_values[0]) + .prod(dim=-1) + .item() + ) + + # Only divide a cell when it is not composed of adjacent indices + # and the fraction of total volume is above the approximation + # threshold fraction + if ( + any_not_adjacent + and ((cell_volume / total_volume) > self.alpha).all() + ): + # Divide the test cell over its largest dimension + # largest (by index length) + length, longest_dim = torch.max(idx_dist, dim=0) + length = length.item() + longest_dim = longest_dim.item() + + new_length1 = int(round(length / 2.0)) + new_length2 = length - new_length1 + + # Store divided cells + # cell 1: subtract new_length1 from the upper bound of the cell + # cell 2: add new_length2 to the lower bound of the cell + for bound_idx, length_delta in ( + (1, -new_length1), + (0, new_length2), + ): + new_cell = cell.clone() + new_cell[bound_idx, longest_dim] += length_delta + stack.append(new_cell) + + def _partition_space_2d(self) -> None: + r"""Partition the non-dominated space into disjoint hypercells. + + This direct method works for `m=2` outcomes. + """ + pf_ext_idx = self._get_augmented_pareto_front_indices() + n_pf_plus_1 = self._neg_pareto_Y.shape[-2] + 1 + view_shape = torch.Size([1] * len(self.batch_shape) + [n_pf_plus_1]) + expand_shape = self.batch_shape + torch.Size([n_pf_plus_1]) + range_pf_plus1 = torch.arange( + n_pf_plus_1, dtype=torch.long, device=self._neg_pareto_Y.device + ) + range_pf_plus1_expanded = range_pf_plus1.view(view_shape).expand(expand_shape) + + lower = torch.stack( + [range_pf_plus1_expanded, torch.zeros_like(range_pf_plus1_expanded)], dim=-1 + ) + upper = torch.stack( + [1 + range_pf_plus1_expanded, pf_ext_idx[..., -range_pf_plus1 - 1, -1]], + dim=-1, + ) + # 2 x batch_shape x n_cells x 2 + self.hypercells = torch.stack([lower, upper], dim=0) + + def _get_augmented_pareto_front_indices(self) -> Tensor: + r"""Get indices of augmented Pareto front.""" + pf_idx = torch.argsort(self._neg_pareto_Y, dim=-2) + return torch.cat( + [ + torch.zeros( + *self.batch_shape, + 1, + self.num_outcomes, + dtype=torch.long, + device=self._neg_Y.device, + ), + # Add 1 because index zero is used for the ideal point + pf_idx + 1, + torch.full( + torch.Size( + [ + *self.batch_shape, + 1, + self.num_outcomes, + ] + ), + self._neg_pareto_Y.shape[-2] + 1, + dtype=torch.long, + device=self._neg_Y.device, + ), + ], + dim=-2, + ) + +
+[docs] + def get_hypercell_bounds(self) -> Tensor: + r"""Get the bounds of each hypercell in the decomposition. + + Args: + ref_point: A `(batch_shape) x m`-dim tensor containing the reference point. + + Returns: + A `2 x num_cells x m`-dim tensor containing the + lower and upper vertices bounding each hypercell. + """ + ref_point = _expand_ref_point( + ref_point=self.ref_point, batch_shape=self.batch_shape + ) + aug_pareto_Y = torch.cat( + [ + # -inf is the lower bound of the non-dominated space + torch.full( + torch.Size( + [ + *self.batch_shape, + 1, + self.num_outcomes, + ] + ), + float("-inf"), + dtype=self._neg_pareto_Y.dtype, + device=self._neg_pareto_Y.device, + ), + self._neg_pareto_Y, + # note: internally, this class minimizes, so use negative here + -(ref_point.unsqueeze(-2)), + ], + dim=-2, + ) + minimization_cell_bounds = self._get_hypercell_bounds(aug_pareto_Y=aug_pareto_Y) + # swap upper and lower bounds and multiply by -1 + return -minimization_cell_bounds.flip(0)
+ + + def _get_hypercell_bounds(self, aug_pareto_Y: Tensor) -> Tensor: + r"""Get the bounds of each hypercell in the decomposition. + + Args: + aug_pareto_Y: A `n_pareto + 2 x m`-dim tensor containing + the augmented Pareto front. + + Returns: + A `2 x (batch_shape) x num_cells x m`-dim tensor containing the + lower and upper vertices bounding each hypercell. + """ + num_cells = self.hypercells.shape[-2] + cells_times_outcomes = num_cells * self.num_outcomes + outcome_idxr = ( + torch.arange(self.num_outcomes, dtype=torch.long, device=self._neg_Y.device) + .repeat(num_cells) + .view( + *(1 for _ in self.hypercells.shape[:-2]), + cells_times_outcomes, + ) + .expand(*self.hypercells.shape[:-2], cells_times_outcomes) + ) + + # this tensor is 2 x (num_cells * m) x 2 + # the batch dim corresponds to lower/upper bound + cell_bounds_idxr = torch.stack( + [ + self.hypercells.view(*self.hypercells.shape[:-2], -1), + outcome_idxr, + ], + dim=-1, + ).view(2, -1, 2) + if len(self.batch_shape) > 0: + # TODO: support multiple batch dimensions here + batch_idxr = ( + torch.arange( + self.batch_shape[0], dtype=torch.long, device=self._neg_Y.device + ) + .unsqueeze(1) + .expand(-1, cells_times_outcomes) + .reshape(1, -1, 1) + .expand(2, -1, 1) + ) + cell_bounds_idxr = torch.cat([batch_idxr, cell_bounds_idxr], dim=-1) + + cell_bounds_values = aug_pareto_Y[ + cell_bounds_idxr.chunk(cell_bounds_idxr.shape[-1], dim=-1) + ] + view_shape = (2, *self.batch_shape, num_cells, self.num_outcomes) + return cell_bounds_values.view(view_shape) + + def _compute_hypervolume_if_y_has_data(self) -> Tensor: + ref_point = _expand_ref_point( + ref_point=self.ref_point, batch_shape=self.batch_shape + ) + # internally we minimize + ref_point = -ref_point.unsqueeze(-2) + ideal_point = self._neg_pareto_Y.min(dim=-2, keepdim=True).values + aug_pareto_Y = torch.cat([ideal_point, self._neg_pareto_Y, ref_point], dim=-2) + cell_bounds_values = self._get_hypercell_bounds(aug_pareto_Y=aug_pareto_Y) + total_volume = (ref_point - ideal_point).squeeze(-2).prod(dim=-1) + non_dom_volume = ( + (cell_bounds_values[1] - cell_bounds_values[0]).prod(dim=-1).sum(dim=-1) + ) + return total_volume - non_dom_volume
+ + + +
+[docs] +class FastNondominatedPartitioning(FastPartitioning): + r"""A class for partitioning the non-dominated space into hyper-cells. + + Note: this assumes maximization. Internally, it multiplies by -1 and performs + the decomposition under minimization. + + This class is far more efficient than NondominatedPartitioning for exact box + partitionings + + This class uses the two-step approach similar to that in [Yang2019]_, where: + a) first, Alg 1 from [Lacour17]_ is used to find the local lower bounds + for the maximization problem + b) second, the local lower bounds are used as the Pareto frontier for the + minimization problem, and [Lacour17]_ is applied again to partition + the space dominated by that Pareto frontier. + """ + + def __init__( + self, + ref_point: Tensor, + Y: Tensor | None = None, + ) -> None: + """Initialize FastNondominatedPartitioning. + + Args: + ref_point: A `m`-dim tensor containing the reference point. + Y: A `(batch_shape) x n x m`-dim tensor. + + Example: + >>> bd = FastNondominatedPartitioning(ref_point, Y=Y1) + """ + super().__init__(ref_point=ref_point, Y=Y) + + def _get_single_cell(self) -> None: + r"""Set the partitioning to be a single cell in the case of no Pareto points.""" + cell_bounds = torch.full( + (2, *self._neg_pareto_Y.shape[:-2], 1, self.num_outcomes), + float("inf"), + dtype=self._neg_pareto_Y.dtype, + device=self._neg_pareto_Y.device, + ) + cell_bounds[0] = self.ref_point + self.hypercell_bounds = cell_bounds + + def _get_partitioning(self) -> None: + r"""Compute non-dominated partitioning. + + Given local upper bounds for the minimization problem (self._U), this computes + the non-dominated partitioning for the maximization problem. Note that + -self.U contains the local lower bounds for the maximization problem. Following + [Yang2019]_, this treats -self.U as a *new* pareto frontier for a minimization + problem with a reference point of [infinity]^m and computes a dominated + partitioning for this minimization problem. + """ + new_ref_point = torch.full( + torch.Size([1]) + self._neg_ref_point.shape, + float("inf"), + dtype=self._neg_ref_point.dtype, + device=self._neg_ref_point.device, + ) + # initialize local upper bounds for the second minimization problem + self._U2 = new_ref_point + # initialize defining points for the second minimization problem + # use ref point for maximization as the ideal point for minimization. + self._Z2 = self.ref_point.expand( + 1, self.num_outcomes, self.num_outcomes + ).clone() + for j in range(self._neg_ref_point.shape[-1]): + self._Z2[0, j, j] = self._U2[0, j] + # incrementally update local upper bounds and defining points + # for each new Pareto point + self._U2, self._Z2 = update_local_upper_bounds_incremental( + new_pareto_Y=-self._U, + U=self._U2, + Z=self._Z2, + ) + cell_bounds = get_partition_bounds( + Z=self._Z2, U=self._U2, ref_point=new_ref_point.view(-1) + ) + self.hypercell_bounds = cell_bounds + + def _partition_space_2d(self) -> None: + r"""Partition the non-dominated space into disjoint hypercells. + + This direct method works for `m=2` outcomes. + """ + cell_bounds = compute_non_dominated_hypercell_bounds_2d( + pareto_Y_sorted=self.pareto_Y.flip(-2), + ref_point=self.ref_point, + ) + self.hypercell_bounds = cell_bounds + + def _compute_hypervolume_if_y_has_data(self) -> Tensor: + ideal_point = self.pareto_Y.max(dim=-2, keepdim=True).values + total_volume = ( + (ideal_point.squeeze(-2) - self.ref_point).clamp_min(0.0).prod(dim=-1) + ) + finite_cell_bounds = torch.min(self.hypercell_bounds, ideal_point) + non_dom_volume = ( + (finite_cell_bounds[1] - finite_cell_bounds[0]) + .clamp_min(0.0) + .prod(dim=-1) + .sum(dim=-1) + ) + return total_volume - non_dom_volume
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/utils.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/utils.html new file mode 100644 index 0000000000..bed05f06f9 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/box_decompositions/utils.html @@ -0,0 +1,407 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.box_decompositions.utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Utilities for box decomposition algorithms."""
+
+import torch
+from botorch.exceptions.errors import BotorchTensorDimensionError, UnsupportedError
+from botorch.utils.multi_objective.pareto import is_non_dominated
+from torch import Size, Tensor
+
+
+def _expand_ref_point(ref_point: Tensor, batch_shape: Size) -> Tensor:
+    r"""Expand reference point to the proper batch_shape.
+
+    Args:
+        ref_point: A `(batch_shape) x m`-dim tensor containing the reference
+            point.
+        batch_shape: The batch shape.
+
+    Returns:
+        A `batch_shape x m`-dim tensor containing the expanded reference point
+    """
+    if ref_point.shape[:-1] != batch_shape:
+        if ref_point.ndim > 1:
+            raise BotorchTensorDimensionError(
+                "Expected ref_point to be a `batch_shape x m` or `m`-dim tensor, "
+                f"but got {ref_point.shape}."
+            )
+        ref_point = ref_point.view(
+            *(1 for _ in batch_shape), ref_point.shape[-1]
+        ).expand(batch_shape + ref_point.shape[-1:])
+    return ref_point
+
+
+def _pad_batch_pareto_frontier(
+    Y: Tensor,
+    ref_point: Tensor,
+    is_pareto: bool = False,
+    feasibility_mask: Tensor | None = None,
+) -> Tensor:
+    r"""Get a batch Pareto frontier by padding the pareto frontier with repeated points.
+
+    This assumes maximization.
+
+    Args:
+        Y: A `(batch_shape) x n x m`-dim tensor of points
+        ref_point: a `(batch_shape) x m`-dim tensor containing the reference point
+        is_pareto: a boolean indicating whether the points in Y are already
+            non-dominated.
+        feasibility_mask: A `(batch_shape) x n`-dim tensor of booleans indicating
+            whether each point is feasible.
+
+    Returns:
+        A `(batch_shape) x max_num_pareto x m`-dim tensor of padded Pareto
+            frontiers.
+    """
+    tkwargs = {"dtype": Y.dtype, "device": Y.device}
+    ref_point = ref_point.unsqueeze(-2)
+    batch_shape = Y.shape[:-2]
+    if len(batch_shape) > 1:
+        raise UnsupportedError(
+            "_pad_batch_pareto_frontier only supports a single "
+            f"batch dimension, but got {len(batch_shape)} "
+            "batch dimensions."
+        )
+    if feasibility_mask is not None:
+        # set infeasible points to be the reference point (corresponding to the batch)
+        Y = torch.where(feasibility_mask.unsqueeze(-1), Y, ref_point)
+    if not is_pareto:
+        pareto_mask = is_non_dominated(Y)
+    else:
+        pareto_mask = torch.ones(Y.shape[:-1], dtype=torch.bool, device=Y.device)
+    better_than_ref = (Y > ref_point).all(dim=-1)
+    # is_non_dominated assumes maximization
+    # TODO: filter out points that are worse than the reference point first here
+    pareto_mask = pareto_mask & better_than_ref
+    if len(batch_shape) == 0:
+        return Y[pareto_mask]
+    # Note: in the batch case, the Pareto frontier is padded by repeating
+    # a Pareto point. This ensures that the padded box-decomposition has
+    # the same number of points, which enables fast batch operations.
+    max_n_pareto = pareto_mask.sum(dim=-1).max().item()
+    pareto_Y = torch.empty(*batch_shape, max_n_pareto, Y.shape[-1], **tkwargs)
+    for i, pareto_i in enumerate(pareto_mask):
+        pareto_i = Y[i, pareto_mask[i]]
+        n_pareto = pareto_i.shape[0]
+        if n_pareto > 0:
+            pareto_Y[i, :n_pareto] = pareto_i
+            # pad pareto_Y, so that all batches have the same size Pareto set
+            pareto_Y[i, n_pareto:] = pareto_i[-1]
+        else:
+            # if there are no pareto points in this batch, use the reference
+            # point
+            pareto_Y[i, :] = ref_point[i]
+    return pareto_Y
+
+
+
+[docs] +def compute_local_upper_bounds( + U: Tensor, Z: Tensor, z: Tensor +) -> tuple[Tensor, Tensor]: + r"""Compute local upper bounds. + + Note: this assumes minimization. + + This uses the incremental algorithm (Alg. 1) from [Lacour17]_. + + Args: + U: A `n x m`-dim tensor containing the local upper bounds. + Z: A `n x m x m`-dim tensor containing the defining points. + z: A `m`-dim tensor containing the new point. + + Returns: + 2-element tuple containing: + + - A new `n' x m`-dim tensor local upper bounds. + - A `n' x m x m`-dim tensor containing the defining points. + """ + num_outcomes = U.shape[-1] + z_dominates_U = (U > z).all(dim=-1) + # Select upper bounds that are dominated by z. + # These are the search zones that contain z. + if not z_dominates_U.any(): + return U, Z + A = U[z_dominates_U] + A_Z = Z[z_dominates_U] + P = [] + P_Z = [] + mask = torch.ones(num_outcomes, dtype=torch.bool, device=U.device) + for j in range(num_outcomes): + mask[j] = 0 + z_uj_max = A_Z[:, mask, j].max(dim=-1).values.view(-1) + add_z = z[j] >= z_uj_max + if add_z.any(): + u_j = A[add_z].clone() + u_j[:, j] = z[j] + P.append(u_j) + A_Z_filtered = A_Z[add_z] + Z_ku = A_Z_filtered[:, mask] + lt_zj = Z_ku[..., j] <= z[j] + P_uj = torch.zeros( + u_j.shape[0], num_outcomes, num_outcomes, dtype=U.dtype, device=U.device + ) + P_uj[:, mask] = Z_ku[lt_zj].view(P_uj.shape[0], num_outcomes - 1, -1) + P_uj[:, ~mask] = z + P_Z.append(P_uj) + mask[j] = 1 + # filter out elements of U that are in A + not_z_dominates_U = ~z_dominates_U + U = U[not_z_dominates_U] + # remaining indices + Z = Z[not_z_dominates_U] + if len(P) > 0: + # add points from P_Z + Z = torch.cat([Z, *P_Z], dim=0) + # return elements in P or elements in (U that are not in A) + U = torch.cat([U, *P], dim=-2) + return U, Z
+ + + +
+[docs] +def get_partition_bounds(Z: Tensor, U: Tensor, ref_point: Tensor) -> Tensor: + r"""Get the cell bounds given the local upper bounds and the defining points. + + This implements Equation 2 in [Lacour17]_. + + Args: + Z: A `n x m x m`-dim tensor containing the defining points. The first + dimension corresponds to u_idx, the second dimension corresponds to j, + and Z[u_idx, j] is the set of definining points Z^j(u) where + u = U[u_idx]. + U: A `n x m`-dim tensor containing the local upper bounds. + ref_point: A `m`-dim tensor containing the reference point. + + Returns: + A `2 x num_cells x m`-dim tensor containing the lower and upper vertices + bounding each hypercell. + """ + bounds = torch.empty(2, U.shape[0], U.shape[-1], dtype=U.dtype, device=U.device) + for u_idx in range(U.shape[0]): + # z_1^1(u) + bounds[0, u_idx, 0] = Z[u_idx, 0, 0] + # z_1^r(u) + bounds[1, u_idx, 0] = ref_point[0] + for j in range(1, U.shape[-1]): + bounds[0, u_idx, j] = Z[u_idx, :j, j].max() + bounds[1, u_idx, j] = U[u_idx, j] + # remove empty partitions + # Note: the equality will evaluate as True if the lower and upper bound + # are both (-inf), which could happen if the reference point is -inf. + empty = (bounds[1] <= bounds[0]).any(dim=-1) + return bounds[:, ~empty]
+ + + +
+[docs] +def update_local_upper_bounds_incremental( + new_pareto_Y: Tensor, U: Tensor, Z: Tensor +) -> tuple[Tensor, Tensor]: + r"""Update the current local upper with the new pareto points. + + This assumes minimization. + + Args: + new_pareto_Y: A `n x m`-dim tensor containing the new + Pareto points. + U: A `n' x m`-dim tensor containing the local upper bounds. + Z: A `n x m x m`-dim tensor containing the defining points. + + Returns: + 2-element tuple containing: + + - A new `n' x m`-dim tensor local upper bounds. + - A `n' x m x m`-dim tensor containing the defining points + """ + for i in range(new_pareto_Y.shape[-2]): + U, Z = compute_local_upper_bounds(U=U, Z=Z, z=new_pareto_Y[i]) + return U, Z
+ + + +
+[docs] +def compute_non_dominated_hypercell_bounds_2d( + pareto_Y_sorted: Tensor, ref_point: Tensor +) -> Tensor: + r"""Compute an axis-aligned partitioning of the non-dominated space for 2 + objectives. + + Args: + pareto_Y_sorted: A `(batch_shape) x n_pareto x 2`-dim tensor of pareto outcomes + that are sorted by the 0th dimension in increasing order. All points must be + better than the reference point. + ref_point: A `(batch_shape) x 2`-dim reference point. + + Returns: + A `2 x (batch_shape) x n_pareto + 1 x m`-dim tensor of cell bounds. + """ + # add boundary point to each front + # the boundary point is the extreme value in each outcome + # (a single coordinate of reference point) + batch_shape = pareto_Y_sorted.shape[:-2] + if ref_point.ndim == pareto_Y_sorted.ndim - 1: + expanded_boundary_point = ref_point.unsqueeze(-2) + else: + view_shape = torch.Size([1] * len(batch_shape)) + torch.Size([1, 2]) + expanded_shape = batch_shape + torch.Size([1, 2]) + expanded_boundary_point = ref_point.view(view_shape).expand(expanded_shape) + + # add the points (ref, y) and (x, ref) to the corresponding ends + pareto_Y_sorted0, pareto_Y_sorted1 = torch.split(pareto_Y_sorted, 1, dim=-1) + expanded_boundary_point0, expanded_boundary_point1 = torch.split( + expanded_boundary_point, 1, dim=-1 + ) + left_end = torch.cat( + [expanded_boundary_point0[..., :1, :], pareto_Y_sorted1[..., :1, :]], dim=-1 + ) + right_end = torch.cat( + [pareto_Y_sorted0[..., -1:, :], expanded_boundary_point1[..., :1, :]], dim=-1 + ) + front = torch.cat([left_end, pareto_Y_sorted, right_end], dim=-2) + # The top left corners of axis-aligned rectangles in dominated partitioning. + # These are the bottom left corners of the non-dominated partitioning + front0, front1 = torch.split(front, 1, dim=-1) + bottom_lefts = torch.cat([front0[..., :-1, :], front1[..., 1:, :]], dim=-1) + top_right_xs = torch.cat( + [ + front0[..., 1:-1, :], + torch.full( + bottom_lefts.shape[:-2] + torch.Size([1, 1]), + float("inf"), + dtype=front.dtype, + device=front.device, + ), + ], + dim=-2, + ) + top_rights = torch.cat( + [ + top_right_xs, + torch.full( + bottom_lefts.shape[:-1] + torch.Size([1]), + float("inf"), + dtype=front.dtype, + device=front.device, + ), + ], + dim=-1, + ) + return torch.stack([bottom_lefts, top_rights], dim=0)
+ + + +
+[docs] +def compute_dominated_hypercell_bounds_2d( + pareto_Y_sorted: Tensor, ref_point: Tensor +) -> Tensor: + r"""Compute an axis-aligned partitioning of the dominated space for 2-objectives. + + Args: + pareto_Y_sorted: A `(batch_shape) x n_pareto x 2`-dim tensor of pareto outcomes + that are sorted by the 0th dimension in increasing order. + ref_point: A `2`-dim reference point. + + Returns: + A `2 x (batch_shape) x n_pareto x m`-dim tensor of cell bounds. + """ + # add boundary point to each front + # the boundary point is the extreme value in each outcome + # (a single coordinate of reference point) + batch_shape = pareto_Y_sorted.shape[:-2] + if ref_point.ndim == pareto_Y_sorted.ndim - 1: + expanded_boundary_point = ref_point.unsqueeze(-2) + else: + view_shape = torch.Size([1] * len(batch_shape)) + torch.Size([1, 2]) + expanded_shape = batch_shape + torch.Size([1, 2]) + expanded_boundary_point = ref_point.view(view_shape).expand(expanded_shape) + + # add the points (ref, y) and (x, ref) to the corresponding ends + pareto_Y_sorted0, pareto_Y_sorted1 = torch.split(pareto_Y_sorted, 1, dim=-1) + expanded_boundary_point0, expanded_boundary_point1 = torch.split( + expanded_boundary_point, 1, dim=-1 + ) + left_end = torch.cat( + [expanded_boundary_point0[..., :1, :], pareto_Y_sorted0[..., :1, :]], dim=-1 + ) + right_end = torch.cat( + [pareto_Y_sorted1[..., :1, :], expanded_boundary_point1[..., :1, :]], dim=-1 + ) + front = torch.cat([left_end, pareto_Y_sorted, right_end], dim=-2) + # compute hypervolume by summing rectangles from min_x -> max_x + top_rights = front[..., 1:-1, :] + bottom_lefts = torch.cat( + [ + front[..., :-2, :1], + expanded_boundary_point1.expand(*top_rights.shape[:-1], 1), + ], + dim=-1, + ) + return torch.stack([bottom_lefts, top_rights], dim=0)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/hypervolume.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/hypervolume.html new file mode 100644 index 0000000000..9d94b30acd --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/hypervolume.html @@ -0,0 +1,938 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.hypervolume

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Hypervolume Utilities.
+
+References
+
+.. [Fonseca2006]
+    C. M. Fonseca, L. Paquete, and M. Lopez-Ibanez. An improved dimension-sweep
+    algorithm for the hypervolume indicator. In IEEE Congress on Evolutionary
+    Computation, pages 1157-1163, Vancouver, Canada, July 2006.
+
+.. [Ishibuchi2011]
+    H. Ishibuchi, N. Akedo, and Y. Nojima. A many-objective test problem
+    for visually examining diversity maintenance behavior in a decision
+    space. Proc. 13th Annual Conf. Genetic Evol. Comput., 2011.
+"""
+
+from __future__ import annotations
+
+import warnings
+from collections.abc import Callable
+from copy import deepcopy
+
+from itertools import combinations
+
+import torch
+from botorch.acquisition.cached_cholesky import CachedCholeskyMCSamplerMixin
+from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
+from botorch.acquisition.multi_objective.utils import (
+    prune_inferior_points_multi_objective,
+)
+from botorch.exceptions.errors import (
+    BotorchError,
+    BotorchTensorDimensionError,
+    UnsupportedError,
+)
+from botorch.exceptions.warnings import BotorchWarning
+from botorch.models.model import Model
+from botorch.sampling.base import MCSampler
+from botorch.utils.multi_objective.box_decompositions.box_decomposition_list import (
+    BoxDecompositionList,
+)
+from botorch.utils.multi_objective.box_decompositions.dominated import (
+    DominatedPartitioning,
+)
+from botorch.utils.multi_objective.box_decompositions.non_dominated import (
+    FastNondominatedPartitioning,
+    NondominatedPartitioning,
+)
+from botorch.utils.multi_objective.box_decompositions.utils import (
+    _pad_batch_pareto_frontier,
+)
+from botorch.utils.objective import compute_feasibility_indicator
+from botorch.utils.torch import BufferDict
+from torch import Tensor
+
+MIN_Y_RANGE = 1e-7
+
+
+
+[docs] +def infer_reference_point( + pareto_Y: Tensor, + max_ref_point: Tensor | None = None, + scale: float = 0.1, + scale_max_ref_point: bool = False, +) -> Tensor: + r"""Get reference point for hypervolume computations. + + This sets the reference point to be `ref_point = nadir - scale * range` + when there is no `pareto_Y` that is better than `max_ref_point`. + If there's `pareto_Y` better than `max_ref_point`, the reference point + will be set to `max_ref_point - scale * range` if `scale_max_ref_point` + is true and to `max_ref_point` otherwise. + + [Ishibuchi2011]_ find 0.1 to be a robust multiplier for scaling the + nadir point. + + Note: this assumes maximization of all objectives. + + Args: + pareto_Y: A `n x m`-dim tensor of Pareto-optimal points. + max_ref_point: A `m` dim tensor indicating the maximum reference point. + Some elements can be NaN, except when `pareto_Y` is empty, + in which case these dimensions will be treated as if no + `max_ref_point` was provided and set to `nadir - scale * range`. + scale: A multiplier used to scale back the reference point based on the + range of each objective. + scale_max_ref_point: A boolean indicating whether to apply scaling to + the max_ref_point based on the range of each objective. + + Returns: + A `m`-dim tensor containing the reference point. + """ + if pareto_Y.shape[0] == 0: + if max_ref_point is None: + raise BotorchError("Empty pareto set and no max ref point provided") + if max_ref_point.isnan().any(): + raise BotorchError("Empty pareto set and max ref point includes NaN.") + if scale_max_ref_point: + return max_ref_point - scale * max_ref_point.abs() + return max_ref_point + if max_ref_point is not None: + non_nan_idx = ~max_ref_point.isnan() + # Count all points exceeding non-NaN reference point as being better. + better_than_ref = (pareto_Y[:, non_nan_idx] > max_ref_point[non_nan_idx]).all( + dim=-1 + ) + else: + non_nan_idx = torch.ones( + pareto_Y.shape[-1], dtype=torch.bool, device=pareto_Y.device + ) + better_than_ref = torch.ones( + pareto_Y.shape[:1], dtype=torch.bool, device=pareto_Y.device + ) + if max_ref_point is not None and better_than_ref.any() and non_nan_idx.all(): + Y_range = pareto_Y[better_than_ref].max(dim=0).values - max_ref_point + if scale_max_ref_point: + return max_ref_point - scale * Y_range + return max_ref_point + elif pareto_Y.shape[0] == 1: + # no points better than max_ref_point and only a single observation + # subtract MIN_Y_RANGE to handle the case that pareto_Y is a singleton + # with objective value of 0. + Y_range = pareto_Y.abs().clamp_min(MIN_Y_RANGE).view(-1) + ref_point = pareto_Y.view(-1) - scale * Y_range + else: + # no points better than max_ref_point and multiple observations + # make sure that each dimension of the nadir point is no greater than + # the max_ref_point + nadir = pareto_Y.min(dim=0).values + if max_ref_point is not None: + nadir[non_nan_idx] = torch.min( + nadir[non_nan_idx], max_ref_point[non_nan_idx] + ) + ideal = pareto_Y.max(dim=0).values + # handle case where all values for one objective are the same + Y_range = (ideal - nadir).clamp_min(MIN_Y_RANGE) + ref_point = nadir - scale * Y_range + # Set not-nan indices - if any - to max_ref_point. + if non_nan_idx.any() and not non_nan_idx.all() and better_than_ref.any(): + if scale_max_ref_point: + ref_point[non_nan_idx] = (max_ref_point - scale * Y_range)[non_nan_idx] + else: + ref_point[non_nan_idx] = max_ref_point[non_nan_idx] + return ref_point
+ + + +
+[docs] +class Hypervolume: + r"""Hypervolume computation dimension sweep algorithm from [Fonseca2006]_. + + Adapted from Simon Wessing's implementation of the algorithm + (Variant 3, Version 1.2) in [Fonseca2006]_ in PyMOO: + https://github.com/msu-coinlab/pymoo/blob/master/pymoo/vendor/hv.py + + Maximization is assumed. + + TODO: write this in C++ for faster looping. + """ + + def __init__(self, ref_point: Tensor) -> None: + r"""Initialize hypervolume object. + + Args: + ref_point: `m`-dim Tensor containing the reference point. + + """ + self.ref_point = ref_point + + @property + def ref_point(self) -> Tensor: + r"""Get reference point (for maximization). + + Returns: + A `m`-dim tensor containing the reference point. + """ + return -self._ref_point + + @ref_point.setter + def ref_point(self, ref_point: Tensor) -> None: + r"""Set the reference point for maximization + + Args: + ref_point: A `m`-dim tensor containing the reference point. + """ + self._ref_point = -ref_point + +
+[docs] + def compute(self, pareto_Y: Tensor) -> float: + r"""Compute hypervolume. + + Args: + pareto_Y: A `n x m`-dim tensor of pareto optimal outcomes + + Returns: + The hypervolume. + """ + if pareto_Y.shape[-1] != self._ref_point.shape[0]: + raise BotorchTensorDimensionError( + "pareto_Y must have the same number of objectives as ref_point. " + f"Got {pareto_Y.shape[-1]}, expected {self._ref_point.shape[0]}." + ) + elif pareto_Y.ndim != 2: + raise BotorchTensorDimensionError( + f"pareto_Y must have exactly two dimensions, got {pareto_Y.ndim}." + ) + # This assumes maximization, but internally flips the sign of the pareto front + # and the reference point and computes hypervolume for the minimization problem. + pareto_Y = -pareto_Y + better_than_ref = (pareto_Y <= self._ref_point).all(dim=-1) + pareto_Y = pareto_Y[better_than_ref] + # shift the pareto front so that reference point is all zeros + pareto_Y = pareto_Y - self._ref_point + self._initialize_multilist(pareto_Y) + bounds = torch.full_like(self._ref_point, float("-inf")) + return self._hv_recursive( + i=self._ref_point.shape[0] - 1, n_pareto=pareto_Y.shape[0], bounds=bounds + )
+ + + def _hv_recursive(self, i: int, n_pareto: int, bounds: Tensor) -> float: + r"""Recursive method for hypervolume calculation. + + This assumes minimization (internally). + + In contrast to the paper, this code assumes that the reference point + is the origin. This enables pruning a few operations. + + Args: + i: objective index + n_pareto: number of pareto points + bounds: objective bounds + + Returns: + The hypervolume. + """ + hvol = torch.tensor(0.0, dtype=bounds.dtype, device=bounds.device) + sentinel = self.list.sentinel + if n_pareto == 0: + # base case: one dimension + return hvol.item() + elif i == 0: + # base case: one dimension + return -sentinel.next[0].data[0].item() + elif i == 1: + # two dimensions, end recursion + q = sentinel.next[1] + h = q.data[0] + p = q.next[1] + while p is not sentinel: + hvol += h * (q.data[1] - p.data[1]) + if p.data[0] < h: + h = p.data[0] + q = p + p = q.next[1] + hvol += h * q.data[1] + return hvol.item() + else: + p = sentinel + q = p.prev[i] + while q.data is not None: + if q.ignore < i: + q.ignore = 0 + q = q.prev[i] + q = p.prev[i] + while n_pareto > 1 and ( + q.data[i] > bounds[i] or q.prev[i].data[i] >= bounds[i] + ): + p = q + self.list.remove(p, i, bounds) + q = p.prev[i] + n_pareto -= 1 + q_prev = q.prev[i] + if n_pareto > 1: + hvol = q_prev.volume[i] + q_prev.area[i] * (q.data[i] - q_prev.data[i]) + else: + q.area[0] = 1 + q.area[1 : i + 1] = q.area[:i] * -(q.data[:i]) + q.volume[i] = hvol + if q.ignore >= i: + q.area[i] = q_prev.area[i] + else: + q.area[i] = self._hv_recursive(i - 1, n_pareto, bounds) + if q.area[i] <= q_prev.area[i]: + q.ignore = i + while p is not sentinel: + p_data = p.data[i] + hvol += q.area[i] * (p_data - q.data[i]) + bounds[i] = p_data + self.list.reinsert(p, i, bounds) + n_pareto += 1 + q = p + p = p.next[i] + q.volume[i] = hvol + if q.ignore >= i: + q.area[i] = q.prev[i].area[i] + else: + q.area[i] = self._hv_recursive(i - 1, n_pareto, bounds) + if q.area[i] <= q.prev[i].area[i]: + q.ignore = i + hvol -= q.area[i] * q.data[i] + return hvol.item() + + def _initialize_multilist(self, pareto_Y: Tensor) -> None: + r"""Sets up the multilist data structure needed for calculation. + + Note: this assumes minimization. + + Args: + pareto_Y: A `n x m`-dim tensor of pareto optimal objectives. + + """ + m = pareto_Y.shape[-1] + nodes = [ + Node(m=m, dtype=pareto_Y.dtype, device=pareto_Y.device, data=point) + for point in pareto_Y + ] + self.list = MultiList(m=m, dtype=pareto_Y.dtype, device=pareto_Y.device) + for i in range(m): + sort_by_dimension(nodes, i) + self.list.extend(nodes, i)
+ + + +
+[docs] +def sort_by_dimension(nodes: list[Node], i: int) -> None: + r"""Sorts the list of nodes in-place by the specified objective. + + Args: + nodes: A list of Nodes + i: The index of the objective to sort by + + """ + # build a list of tuples of (point[i], node) + decorated = [(node.data[i], index, node) for index, node in enumerate(nodes)] + # sort by this value + decorated.sort() + # write back to original list + nodes[:] = [node for (_, _, node) in decorated]
+ + + +
+[docs] +class Node: + r"""Node in the MultiList data structure.""" + + def __init__( + self, + m: int, + dtype: torch.dtype, + device: torch.device, + data: Tensor | None = None, + ) -> None: + r"""Initialize MultiList. + + Args: + m: The number of objectives + dtype: The dtype + device: The device + data: The tensor data to be stored in this Node. + """ + self.data = data + self.next = [None] * m + self.prev = [None] * m + self.ignore = 0 + self.area = torch.zeros(m, dtype=dtype, device=device) + self.volume = torch.zeros_like(self.area)
+ + + +
+[docs] +class MultiList: + r"""A special data structure used in hypervolume computation. + + It consists of several doubly linked lists that share common nodes. + Every node has multiple predecessors and successors, one in every list. + """ + + def __init__(self, m: int, dtype: torch.dtype, device: torch.device) -> None: + r"""Initialize `m` doubly linked lists. + + Args: + m: number of doubly linked lists + dtype: the dtype + device: the device + + """ + self.m = m + self.sentinel = Node(m=m, dtype=dtype, device=device) + self.sentinel.next = [self.sentinel] * m + self.sentinel.prev = [self.sentinel] * m + +
+[docs] + def append(self, node: Node, index: int) -> None: + r"""Appends a node to the end of the list at the given index. + + Args: + node: the new node + index: the index where the node should be appended. + """ + last = self.sentinel.prev[index] + node.next[index] = self.sentinel + node.prev[index] = last + # set the last element as the new one + self.sentinel.prev[index] = node + last.next[index] = node
+ + +
+[docs] + def extend(self, nodes: list[Node], index: int) -> None: + r"""Extends the list at the given index with the nodes. + + Args: + nodes: list of nodes to append at the given index. + index: the index where the nodes should be appended. + + """ + for node in nodes: + self.append(node=node, index=index)
+ + +
+[docs] + def remove(self, node: Node, index: int, bounds: Tensor) -> Node: + r"""Removes and returns 'node' from all lists in [0, 'index']. + + Args: + node: The node to remove + index: The upper bound on the range of indices + bounds: A `2 x m`-dim tensor bounds on the objectives + """ + for i in range(index): + predecessor = node.prev[i] + successor = node.next[i] + predecessor.next[i] = successor + successor.prev[i] = predecessor + bounds.data = torch.min(bounds, node.data) + return node
+ + +
+[docs] + def reinsert(self, node: Node, index: int, bounds: Tensor) -> None: + r"""Re-inserts the node at its original position. + + Re-inserts the node at its original position in all lists in [0, 'index'] + before it was removed. This method assumes that the next and previous + nodes of the node that is reinserted are in the list. + + Args: + node: The node + index: The upper bound on the range of indices + bounds: A `2 x m`-dim tensor bounds on the objectives + + """ + for i in range(index): + node.prev[i].next[i] = node + node.next[i].prev[i] = node + bounds.data = torch.min(bounds, node.data)
+
+ + + +
+[docs] +class SubsetIndexCachingMixin: + """A Mixin class that adds q-subset index computations and caching.""" + + def __init__(self): + """Initializes the class with q_out = -1 and an empty q_subset_indices dict.""" + self.q_out: int = -1 + self.q_subset_indices: BufferDict[str, Tensor] = BufferDict() + +
+[docs] + def compute_q_subset_indices( + self, q_out: int, device: torch.device + ) -> BufferDict[str, Tensor]: + r"""Returns and caches a dict of indices equal to subsets of `{1, ..., q_out}`. + + This means that consecutive calls to `self.compute_q_subset_indices` with + the same `q_out` do not recompute the indices for all (2^q_out - 1) subsets. + + NOTE: This will use more memory than regenerating the indices + for each i and then deleting them, but it will be faster for + repeated evaluations (e.g. during optimization). + + Args: + q_out: The batch size of the objectives. This is typically equal + to the q-batch size of `X`. However, if using a set valued + objective (e.g., MVaR) that produces `s` objective values for + each point on the q-batch of `X`, we need to properly account + for each objective while calculating the hypervolume contributions + by using `q_out = q * s`. + + Returns: + A dict that maps "q choose i" to all size-i subsets of `{1, ..., q_out}`. + """ + if q_out != self.q_out: + self.q_subset_indices = compute_subset_indices(q_out, device=device) + self.q_out = q_out + return self.q_subset_indices
+
+ + + +
+[docs] +def compute_subset_indices( + q: int, device: torch.device | None = None +) -> BufferDict[str, Tensor]: + r"""Compute all (2^q - 1) distinct subsets of {1, ..., `q`}. + + Args: + q: An integer defininig the set {1, ..., `q`} whose subsets to compute. + + Returns: + A dict that maps "q choose i" to all size-i subsets of {1, ..., `q_out`}. + """ + indices = torch.arange(q, dtype=torch.long, device=device) + return BufferDict( + { + f"q_choose_{i}": torch.tensor( + list(combinations(indices, i)), dtype=torch.long, device=device + ) + for i in range(1, q + 1) + } + )
+ + + +
+[docs] +class NoisyExpectedHypervolumeMixin(CachedCholeskyMCSamplerMixin): + def __init__( + self, + model: Model, + ref_point: list[float] | Tensor, + X_baseline: Tensor, + sampler: MCSampler | None = None, + objective: MCMultiOutputObjective | None = None, + constraints: list[Callable[[Tensor], Tensor]] | None = None, + X_pending: Tensor | None = None, + prune_baseline: bool = False, + alpha: float = 0.0, + cache_pending: bool = True, + max_iep: int = 0, + incremental_nehvi: bool = True, + cache_root: bool = True, + marginalize_dim: int | None = None, + ): + """Initialize a mixin that contains functions for the batched Pareto-frontier + partitioning used by the noisy hypervolume-improvement-based acquisition + functions, i.e. qNEHVI and qLogNEHVI. + + Args: + model: A fitted model. + ref_point: A list or tensor with `m` elements representing the reference + point (in the outcome space) w.r.t. to which compute the hypervolume. + This is a reference point for the objective values (i.e. after + applying `objective` to the samples). + X_baseline: A `r x d`-dim Tensor of `r` design points that have already + been observed. These points are considered as potential approximate + pareto-optimal design points. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. NOTE: A box decomposition is + of the Pareto front is created for each MC sample, an operation that + scales as `O(n^m)` and thus becomes particularly costly for `m` > 2. + objective: The MCMultiOutputObjective under which the samples are + evaluated. Defaults to `IdentityMCMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. The acqusition function will compute expected feasible + hypervolume. + X_pending: A `batch_shape x m x d`-dim Tensor of `m` design points that + have points that have been submitted for function evaluation, but + have not yet been evaluated. + prune_baseline: If True, remove points in `X_baseline` that are + highly unlikely to be the pareto optimal and better than the + reference point. This can significantly improve computation time and + is generally recommended. In order to customize pruning parameters, + instead manually call `prune_inferior_points_multi_objective` on + `X_baseline` before instantiating the acquisition function. + alpha: The hyperparameter controlling the approximate non-dominated + partitioning. The default value of 0.0 means an exact partitioning + is used. As the number of objectives `m` increases, consider increasing + this parameter in order to limit computational complexity. + cache_pending: A boolean indicating whether to use cached box + decompositions (CBD) for handling pending points. This is + generally recommended. + max_iep: The maximum number of pending points before the box + decompositions will be recomputed. + incremental_nehvi: A boolean indicating whether to compute the + incremental NEHVI from the `i`th point where `i=1, ..., q` + under sequential greedy optimization, or the full qNEHVI over + `q` points. + cache_root: A boolean indicating whether to cache the root + decomposition over `X_baseline` and use low-rank updates. + marginalize_dim: A batch dimension that should be marginalized. For example, + this is useful when using a batched fully Bayesian model. + """ + super().__init__(model=model, cache_root=cache_root, sampler=sampler) + if len(ref_point) < 2: + raise ValueError( + "NoisyExpectedHypervolumeMixin supports m>=2 outcomes " + f"but ref_point has length {len(ref_point)}, which is smaller than 2." + ) + tkwargs = {"dtype": X_baseline.dtype, "device": X_baseline.device} + ref_point = torch.as_tensor(ref_point, **tkwargs) + self.register_buffer("ref_point", ref_point) + + if X_baseline.ndim > 2: + raise UnsupportedError( + f"NoisyExpectedHypervolumeMixin does not support batched " + f"X_baseline. Expected 2 dims, got {X_baseline.ndim}." + ) + if prune_baseline: + X_baseline = prune_inferior_points_multi_objective( + model=model, + X=X_baseline, + objective=objective, + constraints=constraints, + ref_point=ref_point, + marginalize_dim=marginalize_dim, + ) + + self.alpha = alpha + self.q_in = -1 + self.q_out = -1 + + self.partitioning = None + # set partitioning class and args + self.p_kwargs = {} + if self.alpha > 0: + self.p_kwargs["alpha"] = self.alpha + self.p_class = NondominatedPartitioning + else: + self.p_class = FastNondominatedPartitioning + self.register_buffer("_X_baseline", X_baseline) + self.register_buffer("_X_baseline_and_pending", X_baseline) + self.register_buffer( + "cache_pending", + torch.tensor(cache_pending, dtype=bool), + ) + self.register_buffer( + "_prev_nehvi", + torch.tensor(0.0, **tkwargs), + ) + self.register_buffer( + "_max_iep", + torch.tensor(max_iep, dtype=torch.long), + ) + self.register_buffer( + "incremental_nehvi", + torch.tensor(incremental_nehvi, dtype=torch.bool), + ) + # Base sampler is initialized in _set_cell_bounds. + self.base_sampler = None + + # is this called twice, once here, once in MultiObjectiveMCAcquisitionFunction? + if X_pending is not None: + # This will call self._set_cell_bounds if the number of pending + # points is greater than self._max_iep. + self.set_X_pending(X_pending) + # In the case that X_pending is not None, but there are fewer than + # max_iep pending points, the box decompositions are not performed in + # set_X_pending. Therefore, we need to perform a box decomposition over + # f(X_baseline) here. + if X_pending is None or X_pending.shape[-2] <= self._max_iep: + self._set_cell_bounds(num_new_points=X_baseline.shape[0]) + + # Set q_in=-1 to so that self.sampler is updated at the next forward call. + self.q_in = -1 + + @property + def X_baseline(self) -> Tensor: + r"""Return X_baseline augmented with pending points cached using CBD.""" + return self._X_baseline_and_pending + + def _compute_initial_hvs(self, obj: Tensor, feas: Tensor | None = None) -> None: + r"""Compute hypervolume dominated by f(X_baseline) under each sample. + + Args: + obj: A `sample_shape x batch_shape x n x m`-dim tensor of samples + of objectives. + feas: `sample_shape x batch_shape x n`-dim tensor of samples + of feasibility indicators. + """ + initial_hvs = [] + for i, sample in enumerate(obj): + if self.constraints is not None: + sample = sample[feas[i]] + dominated_partitioning = DominatedPartitioning( + ref_point=self.ref_point, + Y=sample, + ) + hv = dominated_partitioning.compute_hypervolume() + initial_hvs.append(hv) + self.register_buffer( + "_initial_hvs", + torch.tensor(initial_hvs, dtype=obj.dtype, device=obj.device).view( + self._batch_sample_shape, *obj.shape[-2:] + ), + ) + + def _set_cell_bounds(self, num_new_points: int) -> None: + r"""Compute the box decomposition under each posterior sample. + + Args: + num_new_points: The number of new points (beyond the points + in X_baseline) that were used in the previous box decomposition. + In the first box decomposition, this should be the number of points + in X_baseline. + """ + if self.X_baseline.shape[0] > 0: + with torch.no_grad(): + posterior = self.model.posterior(self.X_baseline) + # Reset sampler, accounting for possible one-to-many transform. + self.q_in = -1 + if self.base_sampler is None: + # Initialize the base sampler if needed. + samples = self.get_posterior_samples(posterior) + self.base_sampler = deepcopy(self.sampler) + else: + samples = self.base_sampler(posterior) + n_w = posterior._extended_shape()[-2] // self.X_baseline.shape[-2] + self._set_sampler(q_in=num_new_points * n_w, posterior=posterior) + # cache posterior + if self._cache_root: + # Note that this implicitly uses LinearOperator's caching to check if + # the proper root decomposition has already been cached to + # `posterior.mvn.lazy_covariance_matrix`, which it may have been in + # the call to `self.base_sampler`, and computes it if not found + self._baseline_L = self._compute_root_decomposition(posterior=posterior) + obj = self.objective(samples, X=self.X_baseline) + + else: + sample_shape = ( + self.sampler.sample_shape + if self.sampler is not None + else self._default_sample_shape + ) + obj = torch.empty( + *sample_shape, + 0, + self.ref_point.shape[-1], + dtype=self.ref_point.dtype, + device=self.ref_point.device, + ) + + # compute feasibility indicator if there are constraints + if self.constraints is None or self.X_baseline.shape[0] == 0: + feas = None + else: + feas = compute_feasibility_indicator( + constraints=self.constraints, samples=samples + ) + + self._batch_sample_shape = obj.shape[:-2] + # collapse batch dimensions + # use numel() rather than view(-1) to handle case of no baseline points + new_batch_shape = self._batch_sample_shape.numel() + obj = obj.view(new_batch_shape, *obj.shape[-2:]) + if feas is not None: + feas = feas.view(new_batch_shape, *feas.shape[-1:]) + + if self.partitioning is None and not self.incremental_nehvi: + self._compute_initial_hvs(obj=obj, feas=feas) + + if self.ref_point.shape[-1] > 2: + # the partitioning algorithms run faster on the CPU + # due to advanced indexing + ref_point_cpu = self.ref_point.cpu() + obj_cpu = obj.cpu() + if feas is not None: + feas_cpu = feas.cpu() + obj_cpu = [obj_cpu[i][feas_cpu[i]] for i in range(obj.shape[0])] + partitionings = [] + for sample in obj_cpu: + partitioning = self.p_class( + ref_point=ref_point_cpu, Y=sample, **self.p_kwargs + ) + partitionings.append(partitioning) + self.partitioning = BoxDecompositionList(*partitionings) + else: + # use batched partitioning + obj = _pad_batch_pareto_frontier( + Y=obj, + ref_point=self.ref_point.unsqueeze(0).expand( + obj.shape[0], self.ref_point.shape[-1] + ), + feasibility_mask=feas, + ) + self.partitioning = self.p_class( + ref_point=self.ref_point, Y=obj, **self.p_kwargs + ) + cell_bounds = self.partitioning.get_hypercell_bounds().to(self.ref_point) + cell_bounds = cell_bounds.view( + 2, *self._batch_sample_shape, *cell_bounds.shape[-2:] + ) # 2 x batch_shape x sample_shape x num_cells x m + self.register_buffer("cell_lower_bounds", cell_bounds[0]) + self.register_buffer("cell_upper_bounds", cell_bounds[1]) + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None) -> None: + r"""Informs the acquisition function about pending design points. + + Args: + X_pending: `n x d` Tensor with `n` `d`-dim design points that have + been submitted for evaluation but have not yet been evaluated. + """ + if X_pending is None: + self.X_pending = None + else: + if X_pending.requires_grad: + warnings.warn( + "Pending points require a gradient but the acquisition function" + " will not provide a gradient to these points.", + BotorchWarning, + stacklevel=2, + ) + X_pending = X_pending.detach().clone() + if self.cache_pending: + X_baseline = torch.cat([self._X_baseline, X_pending], dim=-2) + # Number of new points is the total number of points minus + # (the number of previously cached pending points plus the + # of number of baseline points). + num_new_points = X_baseline.shape[0] - self.X_baseline.shape[0] + if num_new_points > 0: + if num_new_points > self._max_iep: + # Set the new baseline points to include pending points. + self.register_buffer("_X_baseline_and_pending", X_baseline) + # Recompute box decompositions. + self._set_cell_bounds(num_new_points=num_new_points) + if not self.incremental_nehvi: + self._prev_nehvi = ( + (self._hypervolumes - self._initial_hvs) + .clamp_min(0.0) + .mean() + ) + # Set to None so that pending points are not concatenated in + # forward. + self.X_pending = None + # Set q_in=-1 to so that self.sampler is updated at the next + # forward call. + self.q_in = -1 + else: + self.X_pending = X_pending[-num_new_points:] + else: + self.X_pending = X_pending
+ + + @property + def _hypervolumes(self) -> Tensor: + r"""Compute hypervolume over X_baseline under each posterior sample. + + Returns: + A `sample_shape`-dim tensor of hypervolumes. + """ + return ( + self.partitioning.compute_hypervolume() + .to(self.ref_point) # for m > 2, the partitioning is on the CPU + .view(self._batch_sample_shape) + )
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/pareto.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/pareto.html new file mode 100644 index 0000000000..f9cd0b9049 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/pareto.html @@ -0,0 +1,185 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.pareto

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import torch
+from torch import Tensor
+
+# maximum tensor size for simple pareto computation
+MAX_BYTES = 5e6
+
+
+
+[docs] +def is_non_dominated( + Y: Tensor, + maximize: bool = True, + deduplicate: bool = True, +) -> Tensor: + r"""Computes the non-dominated front. + + Note: this assumes maximization. + + For small `n`, this method uses a highly parallel methodology + that compares all pairs of points in Y. However, this is memory + intensive and slow for large `n`. For large `n` (or if Y is larger + than 5MB), this method will dispatch to a loop-based approach + that is faster and has a lower memory footprint. + + Args: + Y: A `(batch_shape) x n x m`-dim tensor of outcomes. + If any element of `Y` is NaN, the corresponding point + will be treated as a dominated point (returning False). + maximize: If True, assume maximization (default). + deduplicate: A boolean indicating whether to only return + unique points on the pareto frontier. + + Returns: + A `(batch_shape) x n`-dim boolean tensor indicating whether + each point is non-dominated. + """ + n = Y.shape[-2] + if n == 0: + return torch.zeros(Y.shape[:-1], dtype=torch.bool, device=Y.device) + el_size = 64 if Y.dtype == torch.double else 32 + if n > 1000 or n**2 * Y.shape[:-2].numel() * el_size / 8 > MAX_BYTES: + return _is_non_dominated_loop(Y, maximize=maximize, deduplicate=deduplicate) + + Y1 = Y.unsqueeze(-3) + Y2 = Y.unsqueeze(-2) + if maximize: + dominates = (Y1 >= Y2).all(dim=-1) & (Y1 > Y2).any(dim=-1) + else: + dominates = (Y1 <= Y2).all(dim=-1) & (Y1 < Y2).any(dim=-1) + nd_mask = ~(dominates.any(dim=-1)) + if deduplicate: + # remove duplicates + # find index of first occurrence of each unique element + indices = (Y1 == Y2).all(dim=-1).long().argmax(dim=-1) + keep = torch.zeros_like(nd_mask) + keep.scatter_(dim=-1, index=indices, value=1.0) + return nd_mask & keep + return nd_mask
+ + + +def _is_non_dominated_loop( + Y: Tensor, + maximize: bool = True, + deduplicate: bool = True, +) -> Tensor: + r"""Determine which points are non-dominated. + + Compared to `is_non_dominated`, this method is significantly + faster for large `n` on a CPU and will significant reduce memory + overhead. However, `is_non_dominated` is faster for smaller problems. + + Args: + Y: A `(batch_shape) x n x m` Tensor of outcomes. + maximize: If True, assume maximization (default). + deduplicate: A boolean indicating whether to only return unique points on + the pareto frontier. + + Returns: + A `(batch_shape) x n`-dim Tensor of booleans indicating whether each point is + non-dominated. + """ + is_efficient = torch.ones(*Y.shape[:-1], dtype=bool, device=Y.device) + for i in range(Y.shape[-2]): + i_is_efficient = is_efficient[..., i] + if i_is_efficient.any(): + vals = Y[..., i : i + 1, :] + if maximize: + update = (Y > vals).any(dim=-1) + else: + update = (Y < vals).any(dim=-1) + # If an element in Y[..., i, :] is efficient, mark it as efficient + update[..., i] = i_is_efficient.clone() + # Only include batches where Y[..., i, :] is efficient + # Create a copy + is_efficient2 = is_efficient.clone() + if Y.ndim > 2: + # Set all elements in all batches where Y[..., i, :] is not + # efficient to False + is_efficient2[~i_is_efficient] = False + # Only include elements from is_efficient from the batches + # where Y[..., i, :] is efficient + is_efficient[is_efficient2] = update[is_efficient2] + + if not deduplicate: + # Doing another pass over the data to remove duplicates. There may be a + # more efficient way to do this. One could broadcast this as in + # `is_non_dominated`, but we loop here to avoid high memory usage. + is_efficient_dedup = is_efficient.clone() + for i in range(Y.shape[-2]): + i_is_efficient = is_efficient[..., i] + if i_is_efficient.any(): + vals = Y[..., i : i + 1, :] + duplicate = (vals == Y).all(dim=-1) & i_is_efficient.unsqueeze(-1) + if duplicate.any(): + is_efficient_dedup[duplicate] = True + return is_efficient_dedup + + return is_efficient +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multi_objective/scalarization.html b/website-old/pages/api/_modules/botorch/utils/multi_objective/scalarization.html new file mode 100644 index 0000000000..7da63226d6 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multi_objective/scalarization.html @@ -0,0 +1,173 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multi_objective.scalarization

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Helper utilities for constructing scalarizations.
+
+References
+
+.. [Knowles2005]
+    J. Knowles, "ParEGO: a hybrid algorithm with on-line landscape approximation
+    for expensive multiobjective optimization problems," in IEEE Transactions
+    on Evolutionary Computation, vol. 10, no. 1, pp. 50-66, Feb. 2006.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import torch
+from botorch.exceptions.errors import BotorchTensorDimensionError, UnsupportedError
+from botorch.utils.transforms import normalize
+from torch import Tensor
+
+
+
+[docs] +def get_chebyshev_scalarization( + weights: Tensor, Y: Tensor, alpha: float = 0.05 +) -> Callable[[Tensor, Tensor | None], Tensor]: + r"""Construct an augmented Chebyshev scalarization. + + The augmented Chebyshev scalarization is given by + g(y) = max_i(w_i * y_i) + alpha * sum_i(w_i * y_i) + + where the goal is to minimize g(y) in the setting where all objectives y_i are + to be minimized. Since the default in BoTorch is to maximize all objectives, + this method constructs a Chebyshev scalarization where the inputs are first + multiplied by -1, so that all objectives are to be minimized. Then, it computes + g(y) (which should be minimized), and returns -g(y), which should be maximized. + + Minimizing an objective is supported by passing a negative + weight for that objective. To make all w * y's have the same sign + such that they are comparable when computing max(w * y), outcomes of minimization + objectives are shifted from [0,1] to [-1,0]. + + See [Knowles2005]_ for details. + + This scalarization can be used with qExpectedImprovement to implement q-ParEGO + as proposed in [Daulton2020qehvi]_. + + Args: + weights: A `m`-dim tensor of weights. + Positive for maximization and negative for minimization. + Y: A `n x m`-dim tensor of observed outcomes, which are used for + scaling the outcomes to [0,1] or [-1,0]. If `n=0`, then outcomes + are left unnormalized. + alpha: Parameter governing the influence of the weighted sum term. The + default value comes from [Knowles2005]_. + + Returns: + Transform function using the objective weights. + + Example: + >>> weights = torch.tensor([0.75, -0.25]) + >>> transform = get_aug_chebyshev_scalarization(weights, Y) + """ + # the chebyshev_obj assumes all objectives should be minimized, so + # multiply Y by -1 + Y = -Y + if weights.shape != Y.shape[-1:]: + raise BotorchTensorDimensionError( + "weights must be an `m`-dim tensor where Y is `... x m`." + f"Got shapes {weights.shape} and {Y.shape}." + ) + elif Y.ndim > 2: + raise NotImplementedError("Batched Y is not currently supported.") + + def chebyshev_obj(Y: Tensor, X: Tensor | None = None) -> Tensor: + product = weights * Y + return product.max(dim=-1).values + alpha * product.sum(dim=-1) + + # A boolean mask indicating if minimizing an objective + minimize = weights < 0 + if Y.shape[-2] == 0: + if minimize.any(): + raise UnsupportedError( + "negative weights (for minimization) are only supported if " + "Y is provided." + ) + # If there are no observations, we do not need to normalize the objectives + + def obj(Y: Tensor, X: Tensor | None = None) -> Tensor: + # multiply the scalarization by -1, so that the scalarization should + # be maximized + return -chebyshev_obj(Y=-Y) + + return obj + # Set the bounds to be [min(Y_m), max(Y_m)], for each objective m. + Y_bounds = torch.stack([Y.min(dim=-2).values, Y.max(dim=-2).values]) + + def obj(Y: Tensor, X: Tensor | None = None) -> Tensor: + # scale to [0,1] + Y_normalized = normalize(-Y, bounds=Y_bounds) + # If minimizing an objective, convert Y_normalized values to [-1,0], + # such that min(w*y) makes sense, we want all w*y's to be positive + Y_normalized[..., minimize] = Y_normalized[..., minimize] - 1 + # multiply the scalarization by -1, so that the scalarization should + # be maximized + return -chebyshev_obj(Y=Y_normalized) + + return obj
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/multitask.html b/website-old/pages/api/_modules/botorch/utils/multitask.html new file mode 100644 index 0000000000..077a68cf76 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/multitask.html @@ -0,0 +1,107 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.multitask

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Helpers for multitask modeling.
+"""
+
+from __future__ import annotations
+
+import torch
+from gpytorch.distributions import MultitaskMultivariateNormal
+from gpytorch.distributions.multivariate_normal import MultivariateNormal
+from linear_operator import to_linear_operator
+
+
+
+[docs] +def separate_mtmvn(mvn: MultitaskMultivariateNormal) -> list[MultivariateNormal]: + """ + Separate a MTMVN into a list of MVNs, where covariance across data within each task + are preserved, while covariance across task are dropped. + """ + # T150340766 Upstream as a class method on gpytorch MultitaskMultivariateNormal. + full_covar = mvn.lazy_covariance_matrix + num_data, num_tasks = mvn.mean.shape[-2:] + if mvn._interleaved: + data_indices = torch.arange( + 0, num_data * num_tasks, num_tasks, device=full_covar.device + ).view(-1, 1, 1) + task_indices = torch.arange(num_tasks, device=full_covar.device) + else: + data_indices = torch.arange(num_data, device=full_covar.device).view(-1, 1, 1) + task_indices = torch.arange( + 0, num_data * num_tasks, num_data, device=full_covar.device + ) + slice_ = (data_indices + task_indices).transpose(-1, -3) + data_covars = full_covar[..., slice_, slice_.transpose(-1, -2)] + mvns = [] + for c in range(num_tasks): + mvns.append( + MultivariateNormal( + mvn.mean[..., c], to_linear_operator(data_covars[..., c, :, :]) + ) + ) + return mvns
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/objective.html b/website-old/pages/api/_modules/botorch/utils/objective.html new file mode 100644 index 0000000000..4b7be5132d --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/objective.html @@ -0,0 +1,296 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.objective

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Helpers for handling objectives.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+import torch
+from botorch.utils.safe_math import log_fatmoid, logexpit
+from botorch.utils.transforms import normalize_indices
+from torch import Tensor
+
+
+
+[docs] +def get_objective_weights_transform( + weights: Tensor | None, +) -> Callable[[Tensor, Tensor | None], Tensor]: + r"""Create a linear objective callable from a set of weights. + + Create a callable mapping a Tensor of size `b x q x m` and an (optional) + Tensor of size `b x q x d` to a Tensor of size `b x q`, where `m` is the + number of outputs of the model using scalarization via the objective weights. + This callable supports broadcasting (e.g. for calling on a tensor of shape + `mc_samples x b x q x m`). For `m = 1`, the objective weight is used to + determine the optimization direction. + + Args: + weights: a 1-dimensional Tensor containing a weight for each task. + If not provided, the identity mapping is used. + + Returns: + Transform function using the objective weights. + + Example: + >>> weights = torch.tensor([0.75, 0.25]) + >>> transform = get_objective_weights_transform(weights) + """ + + def _objective(Y: Tensor, X: Tensor | None = None): + r"""Evaluate objective. + + Note: einsum multiples Y by weights and sums over the `m`-dimension. + Einsum is ~2x faster than using `(Y * weights.view(1, 1, -1)).sum(dim-1)`. + + Args: + Y: A `... x b x q x m` tensor of function values. + X: Ignored. + + Returns: + A `... x b x q`-dim tensor of objective values. + """ + # if no weights provided, just extract the single output + if weights is None: + return Y.squeeze(-1) + return torch.einsum("...m, m", [Y, weights]) + + return _objective
+ + + +
+[docs] +def apply_constraints_nonnegative_soft( + obj: Tensor, + constraints: list[Callable[[Tensor], Tensor]], + samples: Tensor, + eta: Tensor | float, +) -> Tensor: + r"""Applies constraints to a non-negative objective. + + This function uses a sigmoid approximation to an indicator function for + each constraint. + + Args: + obj: A `n_samples x b x q (x m')`-dim Tensor of objective values. + constraints: A list of callables, each mapping a Tensor of size `b x q x m` + to a Tensor of size `b x q`, where negative values imply feasibility. + This callable must support broadcasting. Only relevant for multi- + output models (`m` > 1). + samples: A `n_samples x b x q x m` Tensor of samples drawn from the posterior. + eta: The temperature parameter for the sigmoid function. Can be either a float + or a 1-dim tensor. In case of a float the same eta is used for every + constraint in constraints. In case of a tensor the length of the tensor + must match the number of provided constraints. The i-th constraint is + then estimated with the i-th eta value. + + Returns: + A `n_samples x b x q (x m')`-dim tensor of feasibility-weighted objectives. + """ + w = compute_smoothed_feasibility_indicator( + constraints=constraints, samples=samples, eta=eta + ) + if obj.dim() == samples.dim(): + w = w.unsqueeze(-1) # Need to unsqueeze to accommodate the outcome dimension. + return obj.clamp_min(0).mul(w) # Enforce non-negativity of obj, apply constraints.
+ + + +
+[docs] +def compute_feasibility_indicator( + constraints: list[Callable[[Tensor], Tensor]] | None, + samples: Tensor, + marginalize_dim: int | None = None, +) -> Tensor: + r"""Computes the feasibility of a list of constraints given posterior samples. + + Args: + constraints: A list of callables, each mapping a batch_shape x q x m`-dim Tensor + to a `batch_shape x q`-dim Tensor, where negative values imply feasibility. + samples: A batch_shape x q x m`-dim Tensor of posterior samples. + marginalize_dim: A batch dimension that should be marginalized. + For example, this is useful when using a batched fully Bayesian + model. + + Returns: + A `batch_shape x q`-dim tensor of Boolean feasibility values. + """ + ind = torch.ones(samples.shape[:-1], dtype=torch.bool, device=samples.device) + if constraints is not None: + for constraint in constraints: + ind = ind.logical_and(constraint(samples) <= 0) + if ind.ndim >= 3 and marginalize_dim is not None: + # make sure marginalize_dim is not negative + if marginalize_dim < 0: + # add 1 to the normalize marginalize_dim since we have already + # removed the output dim + marginalize_dim = 1 + normalize_indices([marginalize_dim], d=ind.ndim)[0] + + ind = ind.float().mean(dim=marginalize_dim).round().bool() + return ind
+ + + +
+[docs] +def compute_smoothed_feasibility_indicator( + constraints: list[Callable[[Tensor], Tensor]], + samples: Tensor, + eta: Tensor | float, + log: bool = False, + fat: bool = False, +) -> Tensor: + r"""Computes the smoothed feasibility indicator of a list of constraints. + + Given posterior samples, using a sigmoid to smoothly approximate the feasibility + indicator of each individual constraint to ensure differentiability and high + gradient signal. The `fat` and `log` options improve the numerical behavior of + the smooth approximation. + + NOTE: *Negative* constraint values are associated with feasibility. + + Args: + constraints: A list of callables, each mapping a Tensor of size `b x q x m` + to a Tensor of size `b x q`, where negative values imply feasibility. + This callable must support broadcasting. Only relevant for multi- + output models (`m` > 1). + samples: A `n_samples x b x q x m` Tensor of samples drawn from the posterior. + eta: The temperature parameter for the sigmoid function. Can be either a float + or a 1-dim tensor. In case of a float the same eta is used for every + constraint in constraints. In case of a tensor the length of the tensor + must match the number of provided constraints. The i-th constraint is + then estimated with the i-th eta value. + log: Toggles the computation of the log-feasibility indicator. + fat: Toggles the computation of the fat-tailed feasibility indicator. + + Returns: + A `n_samples x b x q`-dim tensor of feasibility indicator values. + """ + if type(eta) is not Tensor: + eta = torch.full((len(constraints),), eta) + if len(eta) != len(constraints): + raise ValueError( + "Number of provided constraints and number of provided etas do not match." + ) + if not (eta > 0).all(): + raise ValueError("eta must be positive.") + is_feasible = torch.zeros_like(samples[..., 0]) + log_sigmoid = log_fatmoid if fat else logexpit + for constraint, e in zip(constraints, eta): + is_feasible = is_feasible + log_sigmoid(-constraint(samples) / e) + + return is_feasible if log else is_feasible.exp()
+ + + +
+[docs] +def apply_constraints( + obj: Tensor, + constraints: list[Callable[[Tensor], Tensor]], + samples: Tensor, + infeasible_cost: float, + eta: Tensor | float = 1e-3, +) -> Tensor: + r"""Apply constraints using an infeasible_cost `M` for negative objectives. + + This allows feasibility-weighting an objective for the case where the + objective can be negative by using the following strategy: + (1) Add `M` to make obj non-negative; + (2) Apply constraints using the sigmoid approximation; + (3) Shift by `-M`. + + Args: + obj: A `n_samples x b x q (x m')`-dim Tensor of objective values. + constraints: A list of callables, each mapping a Tensor of size `b x q x m` + to a Tensor of size `b x q`, where negative values imply feasibility. + This callable must support broadcasting. Only relevant for multi- + output models (`m` > 1). + samples: A `n_samples x b x q x m` Tensor of samples drawn from the posterior. + infeasible_cost: The infeasible value. + eta: The temperature parameter of the sigmoid function. Can be either a float + or a 1-dim tensor. In case of a float the same eta is used for every + constraint in constraints. In case of a tensor the length of the tensor + must match the number of provided constraints. The i-th constraint is + then estimated with the i-th eta value. + + Returns: + A `n_samples x b x q (x m')`-dim tensor of feasibility-weighted objectives. + """ + # obj has dimensions n_samples x b x q (x m') + obj = obj.add(infeasible_cost) # now it is nonnegative + obj = apply_constraints_nonnegative_soft( + obj=obj, + constraints=constraints, + samples=samples, + eta=eta, + ) + return obj.add(-infeasible_cost)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/probability/bvn.html b/website-old/pages/api/_modules/botorch/utils/probability/bvn.html new file mode 100644 index 0000000000..f7c1cc19e7 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/probability/bvn.html @@ -0,0 +1,359 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.probability.bvn

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Methods for computing bivariate normal probabilities and statistics.
+
+.. [Genz2004bvnt]
+    A. Genz. Numerical computation of rectangular bivariate and trivariate normal and
+    t probabilities. Statistics and Computing, 2004.
+
+.. [Muthen1990moments]
+    B. Muthen. Moments of the censored and truncated bivariate normal distribution.
+    British Journal of Mathematical and Statistical Psychology, 1990.
+"""
+
+from __future__ import annotations
+
+from math import pi as _pi
+
+import torch
+from botorch.exceptions import UnsupportedError
+from botorch.utils.probability.utils import (
+    case_dispatcher,
+    get_constants_like,
+    leggauss,
+    ndtr as Phi,
+    phi,
+    STANDARDIZED_RANGE,
+)
+from botorch.utils.safe_math import (
+    div as safe_div,
+    exp as safe_exp,
+    mul as safe_mul,
+    sub as safe_sub,
+)
+from torch import Tensor
+
+# Some useful constants
+_inf = float("inf")
+_2pi = 2 * _pi
+_sqrt_2pi = _2pi**0.5
+_inv_2pi = 1 / _2pi
+
+
+
+[docs] +def bvn(r: Tensor, xl: Tensor, yl: Tensor, xu: Tensor, yu: Tensor) -> Tensor: + r"""A function for computing bivariate normal probabilities. + + Calculates `P(xl < x < xu, yl < y < yu)` where `x` and `y` are bivariate normal with + unit variance and correlation coefficient `r`. See Section 2.4 of [Genz2004bvnt]_. + + This method uses a sign flip trick to improve numerical performance. Many of `bvnu`s + internal branches rely on evaluations `Phi(-bound)`. For `a < b < 0`, the term + `Phi(-a) - Phi(-b)` goes to zero faster than `Phi(b) - Phi(a)` because + `finfo(dtype).epsneg` is typically much larger than `finfo(dtype).tiny`. In these + cases, flipping the sign can prevent situations where `bvnu(...) - bvnu(...)` would + otherwise be zero due to round-off error. + + Args: + r: Tensor of correlation coefficients. + xl: Tensor of lower bounds for `x`, same shape as `r`. + yl: Tensor of lower bounds for `y`, same shape as `r`. + xu: Tensor of upper bounds for `x`, same shape as `r`. + yu: Tensor of upper bounds for `y`, same shape as `r`. + + Returns: + Tensor of probabilities `P(xl < x < xu, yl < y < yu)`. + + """ + if not (r.shape == xl.shape == xu.shape == yl.shape == yu.shape): + raise UnsupportedError("Arguments to `bvn` must have the same shape.") + + # Sign flip trick + _0, _1, _2 = get_constants_like(values=(0, 1, 2), ref=r) + flip_x = xl.abs() > xu # is xl more negative than xu is positive? + flip_y = yl.abs() > yu + flip = (flip_x & (~flip_y | yu.isinf())) | (flip_y & (~flip_x | xu.isinf())) + if flip.any(): # symmetric calls to `bvnu` below makes swapping bounds unnecessary + sign = _1 - _2 * flip.to(dtype=r.dtype) + xl = sign * xl # becomes `-xu` if flipped + xu = sign * xu # becomes `-xl` + yl = sign * yl # becomes `-yu` + yu = sign * yu # becomes `-yl` + + p = bvnu(r, xl, yl) - bvnu(r, xu, yl) - bvnu(r, xl, yu) + bvnu(r, xu, yu) + return p.clip(_0, _1)
+ + + +
+[docs] +def bvnu(r: Tensor, h: Tensor, k: Tensor) -> Tensor: + r"""Solves for `P(x > h, y > k)` where `x` and `y` are standard bivariate normal + random variables with correlation coefficient `r`. In [Genz2004bvnt]_, this is (1) + + `L(h, k, r) = P(x < -h, y < -k) \ + = 1/(a 2\pi) \int_{h}^{\infty} \int_{k}^{\infty} f(x, y, r) dy dx,` + + where `f(x, y, r) = e^{-1/(2a^2) (x^2 - 2rxy + y^2)}` and `a = (1 - r^2)^{1/2}`. + + [Genz2004bvnt]_ report the following integation scheme incurs a maximum of 5e-16 + error when run in double precision: if `|r| >= 0.925`, use a 20-point quadrature + rule on a 5th order Taylor expansion; else, numerically integrate in polar + coordinates using no more than 20 quadrature points. + + Args: + r: Tensor of correlation coefficients. + h: Tensor of negative upper bounds for `x`, same shape as `r`. + k: Tensor of negative upper bounds for `y`, same shape as `r`. + + Returns: + A tensor of probabilities `P(x > h, y > k)`. + """ + if not (r.shape == h.shape == k.shape): + raise UnsupportedError("Arguments to `bvnu` must have the same shape.") + _0, _1, lower, upper = get_constants_like((0, 1) + STANDARDIZED_RANGE, r) + x_free = h < lower + y_free = k < lower + return case_dispatcher( + out=torch.empty_like(r), + cases=( # Special cases admitting closed-form solutions + (lambda: (h > upper) | (k > upper), lambda mask: _0), + (lambda: x_free & y_free, lambda mask: _1), + (lambda: x_free, lambda mask: Phi(-k[mask])), + (lambda: y_free, lambda mask: Phi(-h[mask])), + (lambda: r == _0, lambda mask: Phi(-h[mask]) * Phi(-k[mask])), + ( # For |r| >= 0.925, use a Taylor approximation + lambda: r.abs() >= get_constants_like(0.925, r), + lambda m: _bvnu_taylor(r[m], h[m], k[m]), + ), + ), # For |r| < 0.925, integrate in polar coordinates. + default=lambda mask: _bvnu_polar(r[mask], h[mask], k[mask]), + )
+ + + +def _bvnu_polar( + r: Tensor, h: Tensor, k: Tensor, num_points: int | None = None +) -> Tensor: + r"""Solves for `P(x > h, y > k)` by integrating in polar coordinates as + + `L(h, k, r) = \Phi(-h)\Phi(-k) + 1/(2\pi) \int_{0}^{sin^{-1}(r)} f(t) dt \ + f(t) = e^{-0.5 cos(t)^{-2} (h^2 + k^2 - 2hk sin(t))}` + + For details, see Section 2.2 of [Genz2004bvnt]_. + """ + if num_points is None: + mar = r.abs().max() + num_points = 6 if mar < 0.3 else 12 if mar < 0.75 else 20 + + _0, _1, _i2, _i2pi = get_constants_like(values=(0, 1, 0.5, _inv_2pi), ref=r) + + x, w = leggauss(num_points, dtype=r.dtype, device=r.device) + x = x + _1 + asin_r = _i2 * torch.asin(r) + sin_asrx = (asin_r.unsqueeze(-1) * x).sin() + + _h = h.unsqueeze(-1) + _k = k.unsqueeze(-1) + vals = safe_exp( + safe_sub(safe_mul(sin_asrx, _h * _k), _i2 * (_h.square() + _k.square())) + / (_1 - sin_asrx.square()) + ) + probs = Phi(-h) * Phi(-k) + _i2pi * asin_r * (vals @ w) + return probs.clip(min=_0, max=_1) # necessary due to "safe" handling of inf + + +def _bvnu_taylor(r: Tensor, h: Tensor, k: Tensor, num_points: int = 20) -> Tensor: + r"""Solves for `P(x > h, y > k)` via Taylor expansion. + + Per Section 2.3 of [Genz2004bvnt]_, the bvnu equation (1) may be rewritten as + + `L(h, k, r) = L(h, k, s) - s/(2\pi) \int_{0}^{a} f(x) dx \ + f(x) = (1 - x^2){-1/2} e^{-0.5 ((h - sk)/ x)^2} e^{-shk/(1 + (1 - x^2)^{1/2})},` + + where `s = sign(r)` and `a = sqrt(1 - r^{2})`. The term `L(h, k, s)` is analytic. + The second integral is approximated via Taylor expansion. See Sections 2.3 and + 2.4 of [Genz2004bvnt]_. + """ + _0, _1, _ni2, _i2pi, _sq2pi = get_constants_like( + values=(0, 1, -0.5, _inv_2pi, _sqrt_2pi), ref=r + ) + + x, w = leggauss(num_points, dtype=r.dtype, device=r.device) + x = x + _1 + + s = get_constants_like(2, r) * (r > _0).to(r) - _1 # sign of `r` where sign(0) := 1 + sk = s * k + skh = sk * h + comp_r2 = _1 - r.square() + + a = comp_r2.clip(min=0).sqrt() + b = safe_sub(h, sk) + b2 = b.square() + c = get_constants_like(1 / 8, r) * (get_constants_like(4, r) - skh) + d = get_constants_like(1 / 80, r) * (get_constants_like(12, r) - skh) + + # ---- Solve for `L(h, k, s)` + int_from_0_to_s = case_dispatcher( + out=torch.empty_like(r), + cases=[(lambda: r > _0, lambda mask: Phi(-torch.maximum(h[mask], k[mask])))], + default=lambda mask: (Phi(sk[mask]) - Phi(h[mask])).clip(min=_0), + ) + + # ---- Solve for `s/(2\pi) \int_{0}^{a} f(x) dx` + # Analytic part + _a0 = _ni2 * (safe_div(b2, comp_r2) + skh) + _a1 = c * get_constants_like(1 / 3, r) * (_1 - d * b2) + _a2 = _1 - b2 * _a1 + abs_b = b.abs() + analytic_part = torch.subtract( # analytic part of solution + a * (_a2 + comp_r2 * _a1 + c * d * comp_r2.square()) * safe_exp(_a0), + _sq2pi * Phi(safe_div(-abs_b, a)) * abs_b * _a2 * safe_exp(_ni2 * skh), + ) + + # Quadrature part + _b2 = b2.unsqueeze(-1) + _skh = skh.unsqueeze(-1) + _q0 = get_constants_like(0.25, r) * comp_r2.unsqueeze(-1) * x.square() + _q1 = (_1 - _q0).sqrt() + _q2 = _ni2 * (_b2 / _q0 + _skh) + + _b2 = b2.unsqueeze(-1) + _c = c.unsqueeze(-1) + _d = d.unsqueeze(-1) + vals = (_ni2 * (_b2 / _q0 + _skh)).exp() * torch.subtract( + _1 + _c * _q0 * (_1 + get_constants_like(5, r) * _d * _q0), + safe_exp(_ni2 * _q0 / (_1 + _q1).square() * _skh) / _q1, + ) + mask = _q2 > get_constants_like(-100, r) + if not mask.all(): + vals[~mask] = _0 + quadrature_part = _ni2 * a * (vals @ w) + + # Return `P(x > h, y > k)` + int_from_0_to_a = _i2pi * s * (analytic_part + quadrature_part) + return (int_from_0_to_s - int_from_0_to_a).clip(min=_0, max=_1) + + +
+[docs] +def bvnmom( + r: Tensor, + xl: Tensor, + yl: Tensor, + xu: Tensor, + yu: Tensor, + p: Tensor | None = None, +) -> tuple[Tensor, Tensor]: + r"""Computes the expected values of truncated, bivariate normal random variables. + + Let `x` and `y` be a pair of standard bivariate normal random variables having + correlation `r`. This function computes `E([x,y] \| [xl,yl] < [x,y] < [xu,yu])`. + + Following [Muthen1990moments]_ equations (4) and (5), we have + + `E(x \| [xl, yl] < [x, y] < [xu, yu]) \ + = Z^{-1} \phi(xl) P(yl < y < yu \| x=xl) - \phi(xu) P(yl < y < yu \| x=xu),` + + where `Z = P([xl, yl] < [x, y] < [xu, yu])` and `\phi` is the standard normal PDF. + + Args: + r: Tensor of correlation coefficients. + xl: Tensor of lower bounds for `x`, same shape as `r`. + xu: Tensor of upper bounds for `x`, same shape as `r`. + yl: Tensor of lower bounds for `y`, same shape as `r`. + yu: Tensor of upper bounds for `y`, same shape as `r`. + p: Tensor of probabilities `P(xl < x < xu, yl < y < yu)`, same shape as `r`. + + Returns: + `E(x \| [xl, yl] < [x, y] < [xu, yu])` and + `E(y \| [xl, yl] < [x, y] < [xu, yu])`. + """ + if not (r.shape == xl.shape == xu.shape == yl.shape == yu.shape): + raise UnsupportedError("Arguments to `bvn` must have the same shape.") + + if p is None: + p = bvn(r=r, xl=xl, xu=xu, yl=yl, yu=yu) + + corr = r[..., None, None] + istd = (1 - corr.square()).rsqrt() + lower = torch.stack([xl, yl], -1) + upper = torch.stack([xu, yu], -1) + bounds = torch.stack([lower, upper], -1) + deltas = safe_mul(corr, bounds) + + # Compute densities and conditional probabilities + density_at_bounds = phi(bounds) + prob_given_bounds = Phi( + safe_mul(istd, safe_sub(upper.flip(-1).unsqueeze(-1), deltas)) + ) - Phi(safe_mul(istd, safe_sub(lower.flip(-1).unsqueeze(-1), deltas))) + + # Evaluate Muthen's formula + p_diffs = -(density_at_bounds * prob_given_bounds).diff().squeeze(-1) + moments = (1 / p).unsqueeze(-1) * (p_diffs + r.unsqueeze(-1) * p_diffs.flip(-1)) + return moments.unbind(-1)
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/probability/lin_ess.html b/website-old/pages/api/_modules/botorch/utils/probability/lin_ess.html new file mode 100644 index 0000000000..273f3d5faa --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/probability/lin_ess.html @@ -0,0 +1,582 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.probability.lin_ess

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""Linear Elliptical Slice Sampler.
+
+References
+
+.. [Gessner2020]
+    A. Gessner, O. Kanjilal, and P. Hennig. Integrals over gaussians under
+    linear domain constraints. AISTATS 2020.
+
+.. [Wu2024]
+    K. Wu, and J. Gardner. A Fast, Robust Elliptical Slice Sampling Implementation for
+    Linearly Truncated Multivariate Normal Distributions. arXiv:2407.10449. 2024.
+
+This implementation is based (with multiple changes / optimiations) on
+the following implementations based on the algorithm in [Gessner2020]_:
+- https://github.com/alpiges/LinConGauss
+- https://github.com/wjmaddox/pytorch_ess
+
+In addition, the active intervals (from which the angle is sampled) are computed using
+the improved algorithm described in [Wu2024]_:
+https://github.com/kayween/linear-ess
+
+The implementation here differentiates itself from the original implementations with:
+1) Support for fixed feature equality constraints.
+2) Support for non-standard Normal distributions.
+3) Numerical stability improvements, especially relevant for high-dimensional cases.
+4) Support multiple Markov chains running in parallel.
+"""
+
+from __future__ import annotations
+
+import math
+
+import torch
+from botorch.utils.sampling import PolytopeSampler
+from linear_operator.operators import DiagLinearOperator, LinearOperator
+from torch import Tensor
+
+_twopi = 2.0 * math.pi
+
+
+
+[docs] +class LinearEllipticalSliceSampler(PolytopeSampler): + r"""Linear Elliptical Slice Sampler. + + Ideas: + - Optimize computations if possible, potentially with torch.compile. + - Extend fixed features constraint to general linear equality constraints. + """ + + def __init__( + self, + inequality_constraints: tuple[Tensor, Tensor] | None = None, + bounds: Tensor | None = None, + interior_point: Tensor | None = None, + fixed_indices: list[int] | Tensor | None = None, + mean: Tensor | None = None, + covariance_matrix: Tensor | LinearOperator | None = None, + covariance_root: Tensor | LinearOperator | None = None, + check_feasibility: bool = False, + burnin: int = 0, + thinning: int = 0, + num_chains: int = 1, + ) -> None: + r"""Initialize LinearEllipticalSliceSampler. + + Args: + inequality_constraints: Tensors `(A, b)` describing inequality constraints + `A @ x <= b`, where `A` is an `n_ineq_con x d`-dim Tensor and `b` is + an `n_ineq_con x 1`-dim Tensor, with `n_ineq_con` the number of + inequalities and `d` the dimension of the sample space. If omitted, + must provide `bounds` instead. + bounds: A `2 x d`-dim tensor of box bounds. If omitted, must provide + `inequality_constraints` instead. + interior_point: A `d x 1`-dim Tensor presenting a point in the (relative) + interior of the polytope. If omitted, an interior point is determined + automatically by solving a Linear Program. Note: It is crucial that + the point lie in the interior of the feasible set (rather than on the + boundary), otherwise the sampler will produce invalid samples. + fixed_indices: Integer list or `d`-dim Tensor representing the indices of + dimensions that are constrained to be fixed to the values specified in + the `interior_point`, which is required to be passed in conjunction with + `fixed_indices`. + mean: The `d x 1`-dim mean of the MVN distribution (if omitted, use zero). + covariance_matrix: The `d x d`-dim covariance matrix of the MVN + distribution (if omitted, use the identity). + covariance_root: A `d x d`-dim root of the covariance matrix such that + covariance_root @ covariance_root.T = covariance_matrix. NOTE: This + matrix is assumed to be lower triangular. covariance_root can only be + passed in conjunction with fixed_indices if covariance_root is a + DiagLinearOperator. Otherwise the factorization would need to be re- + computed, as we need to solve in `standardize`. + check_feasibility: If True, raise an error if the sampling results in an + infeasible sample. This creates some overhead and so is switched off + by default. + burnin: Number of samples to generate upon initialization to warm up the + sampler. + thinning: Number of samples to skip before returning a sample in `draw`. + num_chains: Number of Markov chains to run in parallel. + + This sampler samples from a multivariante Normal `N(mean, covariance_matrix)` + subject to linear domain constraints `A x <= b` (intersected with box bounds, + if provided). + """ + if interior_point is not None and interior_point.ndim == 1: + interior_point = interior_point.unsqueeze(-1) + + if mean is not None and mean.ndim == 1: + mean = mean.unsqueeze(-1) + + super().__init__( + inequality_constraints=inequality_constraints, + # TODO: Support equality constraints? + interior_point=interior_point, + bounds=bounds, + ) + tkwargs = {"device": self.x0.device, "dtype": self.x0.dtype} + if covariance_matrix is not None and covariance_root is not None: + raise ValueError( + "Provide either covariance_matrix or covariance_root, not both." + ) + + # can't unpack inequality constraints directly if bounds are passed + A, b = self.A, self.b + self._Az, self._bz = A, b + self._is_fixed, self._not_fixed = None, None + if fixed_indices is not None: + mean, covariance_matrix, covariance_root = ( + self._fixed_features_initialization( + A=A, + b=b, + interior_point=interior_point, + fixed_indices=fixed_indices, + mean=mean, + covariance_matrix=covariance_matrix, + covariance_root=covariance_root, + ) + ) + + self._mean = mean + # Have to delay factorization until after fixed features initialization. + if covariance_matrix is not None: # implies root is None + covariance_root, info = torch.linalg.cholesky_ex(covariance_matrix) + not_psd = torch.any(info) + if not_psd: + raise ValueError( + "Covariance matrix is not positive definite. " + "Currently only non-degenerate distributions are supported." + ) + self._covariance_root = covariance_root + + # Rewrite the constraints as a system that constrains a standard Normal. + self._standardization_initialization() + + # state of the sampler ("current point") + self._x = self.x0.clone() + self._z = self._transform(self._x) + + # Expand the shape to (d, num_chains) for running parallel Markov chains. + if num_chains > 1: + self._z = self._z.expand(-1, num_chains).clone() + + # We will need the following repeatedly, let's allocate them once + self.zeros = torch.zeros((num_chains, 1), **tkwargs) + self.ones = torch.ones((num_chains, 1), **tkwargs) + self.indices_batch = torch.arange( + num_chains, dtype=torch.int64, device=tkwargs["device"] + ) + + self.check_feasibility = check_feasibility + self._lifetime_samples = 0 + if burnin > 0: + self.thinning = 0 + self.draw(burnin) + self.thinning = thinning + + def _fixed_features_initialization( + self, + A: Tensor, + b: Tensor, + interior_point: Tensor | None, + fixed_indices: list[int] | Tensor, + mean: Tensor | None, + covariance_matrix: Tensor | None, + covariance_root: Tensor | None, + ) -> tuple[Tensor | None, Tensor | None]: + """Modifies the constraint system (A, b) due to fixed indices and assigns + the modified constraints system to `self._Az`, `self._bz`. NOTE: Needs to be + called prior to `self._standardization_initialization` in the constructor. + covariance_root and fixed_indices can both not be None only if covariance_root + is a DiagLinearOperator. Otherwise, the covariance matrix would need to be + refactorized. + + Returns: + Tuple of `mean` and `covariance_matrix` tensors of the non-fixed dimensions. + """ + if interior_point is None: + raise ValueError( + "If `fixed_indices` are provided, an interior point must also be " + "provided in order to infer feasible values of the fixed features." + ) + + root_is_diag = isinstance(covariance_root, DiagLinearOperator) + if covariance_root is not None and not root_is_diag: + root_is_diag = (covariance_root.diag().diag() == covariance_root).all() + if root_is_diag: # convert the diagonal root to a DiagLinearOperator + covariance_root = DiagLinearOperator(covariance_root.diagonal()) + else: # otherwise, fail + raise ValueError( + "Provide either covariance_root or fixed_indices, not both." + ) + d = interior_point.shape[0] + is_fixed, not_fixed = get_index_tensors(fixed_indices=fixed_indices, d=d) + self._is_fixed = is_fixed + self._not_fixed = not_fixed + # Transforming constraint system to incorporate fixed features: + # A @ x - b = (A[:, fixed] @ x[fixed] + A[:, not fixed] @ x[not fixed]) - b + # = A[:, not fixed] @ x[not fixed] - (b - A[:, fixed] @ x[fixed]) + # = Az @ z - bz + self._Az = A[:, not_fixed] + self._bz = b - A[:, is_fixed] @ interior_point[is_fixed] + if mean is not None: + mean = mean[not_fixed] + if covariance_matrix is not None: # subselect active dimensions + covariance_matrix = covariance_matrix[ + not_fixed.unsqueeze(-1), not_fixed.unsqueeze(0) + ] + if root_is_diag: # in the special case of diagonal root, can subselect + covariance_root = DiagLinearOperator(covariance_root.diagonal()[not_fixed]) + + return mean, covariance_matrix, covariance_root + + def _standardization_initialization(self) -> None: + """For non-standard mean and covariance, we're going to rewrite the problem as + sampling from a standard normal distribution subject to modified constraints. + A @ x - b = A @ (covar_root @ z + mean) - b + = (A @ covar_root) @ z - (b - A @ mean) + = _Az @ z - _bz + NOTE: We need to standardize bz before Az in the following, because it relies + on the untransformed Az. We can't simply use A instead because Az might have + been subject to the fixed features transformation. + """ + if self._mean is not None: + self._bz = self._bz - self._Az @ self._mean + if self._covariance_root is not None: + self._Az = self._Az @ self._covariance_root + + @property + def lifetime_samples(self) -> int: + """The total number of samples generated by the sampler during its lifetime.""" + return self._lifetime_samples + +
+[docs] + def draw(self, n: int = 1) -> Tensor: + r"""Draw samples. + + Args: + n: The number of samples. + + Returns: + A `(n * num_chains) x d`-dim tensor of `n * num_chains` samples. + """ + samples = [] + for _ in range(n): + for _ in range(self.thinning): + self.step() + samples.append(self.step()) + return torch.cat(samples, dim=-1).transpose(-1, -2)
+ + +
+[docs] + def step(self) -> Tensor: + r"""Take a step, return the new sample, update the internal state. + + Returns: + A `d x num_chains`-dim tensor, where each column is a sample from a Markov + chain. + """ + nu = torch.randn_like(self._z) + theta = self._draw_angle(nu=nu) + + self._z = z = self._get_cart_coords(nu=nu, theta=theta) + self._x = x = self._untransform(z) + + self._lifetime_samples += 1 + if self.check_feasibility and (not self._is_feasible(self._x).all()): + Axmb = self.A @ self._x - self.b + violated_indices = Axmb > 0 + raise RuntimeError( + "Sampling resulted in infeasible point. \n\t- Number " + f"of violated constraints: {violated_indices.sum()}." + f"\n\t- Magnitude of violations: {Axmb[violated_indices]}" + "\n\t- If the error persists, please report this bug on GitHub." + ) + return x
+ + + def _draw_angle(self, nu: Tensor) -> Tensor: + r"""Draw the rotation angle. + + Args: + nu: A `d x num_chains`-dim tensor (the "new" direction, drawn from N(0, I)). + + Returns: + A `num_chains`-dim Tensor containing the rotation angle (radians). + """ + left, right = self._find_active_intersection_angles(nu) + left, right = self._trim_intervals(left, right) + + # If left[i, j] <= right[i, j], then [left[i, j], right[i, j]] is an active + # interval. On the other hand, if left[i, j] > right[i, j], then they are both + # dummy variables and should be discarded. Thus, we clamp their difference so + # that they do not contribute to the cumulative length. + csum = right.sub(left).clamp(min=0.0).cumsum(dim=-1) + + u = csum[:, -1] * torch.rand( + right.size(-2), dtype=right.dtype, device=right.device + ) + + # The returned index i satisfies csum[i - 1] < u <= csum[i] + idx = torch.searchsorted(csum, u.unsqueeze(-1)).squeeze(-1) + + # Do a zero padding so that padded_csum[i] = csum[i - 1] + padded_csum = torch.cat([self.zeros, csum], dim=-1) + + return u - padded_csum[self.indices_batch, idx] + left[self.indices_batch, idx] + + def _get_cart_coords(self, nu: Tensor, theta: Tensor) -> Tensor: + r"""Determine location on the ellipse in Cartesian coordinates. + + Args: + nu: A `d x num_chains`-dim tensor (the "new" direction, drawn from N(0, I)). + theta: A `num_chains`-dim tensor of angles. + + Returns: + A `d x num_chains`-dim tensor of samples from the domain in Cartesian + coordinates. + """ + return self._z * torch.cos(theta) + nu * torch.sin(theta) + + def _trim_intervals(self, left: Tensor, right: Tensor) -> tuple[Tensor, Tensor]: + """Trim the intervals by a small positive constant. This encourages the Markov + chain to stay in the interior of the domain. + """ + gap = torch.clamp(right - left, min=0.0) + eps = gap.mul(0.25).clamp(max=1e-6 if gap.dtype == torch.float32 else 1e-12) + + return left + eps, right - eps + + def _find_active_intersection_angles(self, nu: Tensor) -> tuple[Tensor, Tensor]: + """Construct the active intersection angles. + + Args: + nu: A `d x num_chains`-dim tensor (the "new" direction, drawn from N(0, I)). + + Returns: + A tuple (left, right) of two tensors of size `num_chains x m` representing + the active intersection angles. For the i-th Markov chain and the j-th + constraint, a pair of angles left[i, j] and right[i, j] is active if and + only if left[i, j] <= right[i, j]. If left[i, j] > right[i, j], they are + inactive and should be ignored. + """ + alpha, beta = self._find_intersection_angles(nu) + + # It's easier to put `num_chains` as the first dimension, + # because `torch.searchsorted` only supports searching in the last dimension + alpha, beta = alpha.T, beta.T + + srted, indices = torch.sort(alpha, descending=False) + cummax = beta[self.indices_batch.unsqueeze(-1), indices].cummax(dim=-1).values + + srted = torch.cat([srted, self.ones * 2 * math.pi], dim=-1) + cummax = torch.cat([self.zeros, cummax], dim=-1) + + return cummax, srted + + def _find_intersection_angles(self, nu: Tensor) -> tuple[Tensor, Tensor]: + """Compute all 2 * m intersections of the ellipse and the domain, where + `m = n_ineq_con` is the number of inequality constraints defining the domain. + If the i-th linear inequality constraint has no intersection with the ellipse, + we will create two dummy intersection angles alpha_i = beta_i = 0. + + Args: + nu: A `d x num_chains`-dim tensor (the "new" direction, drawn from N(0, I)). + + Returns: + A tuple of two tensors with the same size `m x num_chains`. The first tensor + represents the smaller intersection angles. The second tensor represents the + larger intersection angles. + """ + p = self._Az @ self._z + q = self._Az @ nu + + radius = torch.sqrt(p**2 + q**2) + + ratio = self._bz / radius + + has_solution = ratio < 1.0 + + arccos = torch.arccos(ratio) + arccos[~has_solution] = 0.0 + arctan = torch.arctan2(q, p) + + theta1 = arctan + arccos + theta2 = arctan - arccos + + # translate every angle to [0, 2 * pi] + theta1 = theta1 + theta1.lt(0.0) * _twopi + theta2 = theta2 + theta2.lt(0.0) * _twopi + + alpha = torch.minimum(theta1, theta2) + beta = torch.maximum(theta1, theta2) + + return alpha, beta + + def _is_feasible(self, points: Tensor, transformed: bool = False) -> Tensor: + r"""Returns a Boolean tensor indicating whether the `points` are feasible, + i.e. they satisfy `A @ points <= b`, where `(A, b)` are the tensors passed + as the `inequality_constraints` to the constructor of the sampler. + + Args: + points: A `d x M`-dim tensor of points. + transformed: Wether points are assumed to be transformed by a change of + basis, which means feasibility should be computed based on the + transformed constraint system (_Az, _bz), instead of (A, b). + + Returns: + An `M`-dim binary tensor where `True` indicates that the associated + point is feasible. + """ + A, b = (self._Az, self._bz) if transformed else (self.A, self.b) + return (A @ points <= b).all(dim=0) + + def _transform(self, x: Tensor) -> Tensor: + """Transforms the input so that it is equivalent to a standard Normal variable + constrained with the modified system constraints (self._Az, self._bz). + + Args: + x: The input tensor to be transformed, usually `d x 1`-dimensional. + + Returns: + A `d x 1`-dimensional tensor of transformed values subject to the modified + system of constraints. + """ + if self._not_fixed is not None: + x = x[self._not_fixed] + return self._standardize(x) + + def _untransform(self, z: Tensor) -> Tensor: + """The inverse transform of the `_transform`, i.e. maps `z` back to the original + space where it is subject to the original constraint system (self.A, self.b). + + Args: + z: The transformed tensor to be un-transformed, usually `d x 1`-dimensional. + + Returns: + A `d x 1`-dimensional tensor of un-transformed values subject to the + original system of constraints. + """ + if self._is_fixed is None: + return self._unstandardize(z) + else: + x = self._x.clone() # _x already contains the fixed values + x[self._not_fixed] = self._unstandardize(z) + return x + + def _standardize(self, x: Tensor) -> Tensor: + """_transform helper standardizing the input `x`, which is assumed to be a + `d x 1`-dim Tensor, or a `len(self._not_fixed) x 1`-dim if there are fixed + indices. + """ + z = x + if self._mean is not None: + z = z - self._mean + root = self._covariance_root + if root is not None: + z = torch.linalg.solve_triangular(root, z, upper=False) + + return z + + def _unstandardize(self, z: Tensor) -> Tensor: + """_untransform helper un-standardizing the input `z`, which is assumed to be a + `d x 1`-dim Tensor, or a `len(self._not_fixed) x 1`-dim if there are fixed + indices. + """ + x = z + if self._covariance_root is not None: + x = self._covariance_root @ x + if self._mean is not None: + x = x + self._mean + return x
+ + + +
+[docs] +def get_index_tensors( + fixed_indices: list[int] | Tensor, d: int +) -> tuple[Tensor, Tensor]: + """Converts `fixed_indices` to a `d`-dim integral Tensor that is True at indices + that are contained in `fixed_indices` and False otherwise. + + Args: + fixed_indices: A list or Tensoro of integer indices to fix. + d: The dimensionality of the Tensors to be indexed. + + Returns: + A Tuple of integral Tensors partitioning [1, d] into indices that are fixed + (first tensor) and non-fixed (second tensor). + """ + is_fixed = torch.as_tensor(fixed_indices) + dtype, device = is_fixed.dtype, is_fixed.device + dims = torch.arange(d, dtype=dtype, device=device) + not_fixed = torch.tensor([i for i in dims if i not in is_fixed], dtype=dtype) + return is_fixed, not_fixed
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/probability/linalg.html b/website-old/pages/api/_modules/botorch/utils/probability/linalg.html new file mode 100644 index 0000000000..c2493cbdf8 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/probability/linalg.html @@ -0,0 +1,301 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.probability.linalg

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from dataclasses import dataclass, InitVar
+from itertools import chain
+from typing import Any
+
+import torch
+from botorch.utils.probability.utils import swap_along_dim_
+from linear_operator.utils.errors import NotPSDError
+from torch import LongTensor, Tensor
+from torch.nn.functional import pad
+
+
+
+[docs] +def block_matrix_concat(blocks: Sequence[Sequence[Tensor]]) -> Tensor: + rows = [] + shape = torch.broadcast_shapes(*(x.shape[:-2] for x in chain.from_iterable(blocks))) + for tensors in blocks: + parts = [x.expand(*shape, *x.shape[-2:]) for x in tensors] + if len(parts) > 1: + rows.append(torch.cat(parts, dim=-1)) + else: + rows.extend(parts) + return torch.concat(rows, dim=-2)
+ + + +
+[docs] +def augment_cholesky( + Laa: Tensor, + Kbb: Tensor, + Kba: Tensor | None = None, + Lba: Tensor | None = None, + jitter: float | None = None, +) -> Tensor: + r"""Computes the Cholesky factor of a block matrix `K = [[Kaa, Kab], [Kba, Kbb]]` + based on a precomputed Cholesky factor `Kaa = Laa Laa^T`. + + Args: + Laa: Cholesky factor of K's upper left block. + Kbb: Lower-right block of K. + Kba: Lower-left block of K. + Lba: Precomputed solve `Kba Laa^{-T}`. + jitter: Optional nugget to be added to the diagonal of Kbb. + """ + if not (Kba is None) ^ (Lba is None): + raise ValueError("One and only one of `Kba` or `Lba` must be provided.") + + if jitter is not None: + Kbb = Kbb.clone() + Kbb.diagonal(dim1=-2, dim2=-1).add_(jitter) + + if Lba is None: + Lba = torch.linalg.solve_triangular( + Laa.transpose(-2, -1), Kba, left=False, upper=True + ) + + Lbb, info = torch.linalg.cholesky_ex(Kbb - Lba @ Lba.transpose(-2, -1)) + if info.any(): + raise NotPSDError( + "Schur complement of `K` with respect to `Kaa` not PSD for the given " + "Cholesky factor `Laa`" + f"{'.' if jitter is None else f' and nugget jitter={jitter}.'}" + ) + + n = Lbb.shape[-1] + return block_matrix_concat(blocks=([pad(Laa, (0, n))], [Lba, Lbb]))
+ + + +
+[docs] +@dataclass +class PivotedCholesky: + step: int + tril: Tensor + perm: LongTensor + diag: Tensor | None = None + validate_init: InitVar[bool] = True + + def __post_init__(self, validate_init: bool = True): + if not validate_init: + return + + if self.tril.shape[-2] != self.tril.shape[-1]: + raise ValueError( + f"Expected square matrices but `matrix` has shape `{self.tril.shape}`." + ) + + if self.perm.shape != self.tril.shape[:-1]: + raise ValueError( + f"`perm` of shape `{self.perm.shape}` incompatible with " + f"`matrix` of shape `{self.tril.shape}`." + ) + + if self.diag is not None and self.diag.shape != self.tril.shape[:-1]: + raise ValueError( + f"`diag` of shape `{self.diag.shape}` incompatible with " + f"`matrix` of shape `{self.tril.shape}`." + ) + + def __getitem__(self, key: Any) -> PivotedCholesky: + return PivotedCholesky( + step=self.step, + tril=self.tril[key], + perm=self.perm[key], + diag=None if self.diag is None else self.diag[key], + ) + +
+[docs] + def update_(self, eps: float = 1e-10) -> None: + r"""Performs a single matrix decomposition step.""" + i = self.step + L = self.tril + Lii = self.tril[..., i, i].clone().clip(min=0).sqrt() + + # Finalize `i-th` row and column of Cholesky factor + L[..., i, i] = Lii + L[..., i, i + 1 :] = 0 + L[..., i + 1 :, i] = L[..., i + 1 :, i].clone() / Lii.unsqueeze(-1) + + # Update `tril(L[i + 1:, i + 1:])` to be the lower triangular part + # of the Schur complement of `cov` with respect to `cov[:i, :i]`. + rank1 = L[..., i + 1 :, i : i + 1].clone() + rank1 = (rank1 * rank1.transpose(-1, -2)).tril() + L[..., i + 1 :, i + 1 :] = L[..., i + 1 :, i + 1 :].clone() - rank1 + L[Lii <= i * eps, i:, i] = 0 # numerical stability clause + self.step += 1
+ + +
+[docs] + def pivot_(self, pivot: LongTensor) -> None: + *batch_shape, _, size = self.tril.shape + if pivot.shape != tuple(batch_shape): + raise ValueError("Argument `pivot` does to match with batch shape`.") + + # Perform basic swaps + for key in ("perm", "diag"): + tnsr = getattr(self, key, None) + if tnsr is not None: + swap_along_dim_(tnsr, i=self.step, j=pivot, dim=tnsr.ndim - 1) + + # Perform matrix swaps; prealloacte buffers for row/column linear indices + size2 = size**2 + min_pivot = pivot.min() + tkwargs = {"device": pivot.device, "dtype": pivot.dtype} + buffer_col = torch.arange(size * (1 + min_pivot), size2, size, **tkwargs) + buffer_row = torch.arange(0, max(self.step, pivot.max()), **tkwargs) + head = buffer_row[: self.step] + + indices_v1 = [] + indices_v2 = [] + for i, piv in enumerate(pivot.view(-1, 1)): + v1 = pad(piv, (1, 0), value=self.step).unsqueeze(-1) + v2 = pad(piv, (0, 1), value=self.step).unsqueeze(-1) + start = i * size2 + + indices_v1.extend((start + v1 + size * v1).ravel()) + indices_v2.extend((start + v2 + size * v2).ravel()) + + indices_v1.extend((start + size * v1 + head).ravel()) + indices_v2.extend((start + size * v2 + head).ravel()) + + tail = buffer_col[piv - min_pivot :] + indices_v1.extend((start + v1 + tail).ravel()) + indices_v2.extend((start + v2 + tail).ravel()) + + interior = buffer_row[min(piv, self.step + 1) : piv] + indices_v1.extend(start + size * interior + self.step) + indices_v2.extend(start + size * piv + interior) + + swap_along_dim_( + self.tril.view(-1), + i=torch.as_tensor(indices_v1, **tkwargs), + j=torch.as_tensor(indices_v2, **tkwargs), + dim=0, + )
+ + +
+[docs] + def expand(self, *sizes: int) -> PivotedCholesky: + fields = {} + for name, ndim in {"perm": 1, "diag": 1, "tril": 2}.items(): + src = getattr(self, name) + if src is not None: + fields[name] = src.expand(sizes + src.shape[-ndim:]) + return type(self)(step=self.step, **fields)
+ + +
+[docs] + def concat(self, other: PivotedCholesky, dim: int = 0) -> PivotedCholesky: + if self.step != other.step: + raise ValueError("Cannot conncatenate decompositions at different steps.") + + fields = {} + for name in ("tril", "perm", "diag"): + a = getattr(self, name) + b = getattr(other, name) + if type(a) is not type(b): + raise NotImplementedError(f"Types of field {name} do not match.") + + if a is not None: + fields[name] = torch.concat((a, b), dim=dim) + + return type(self)(step=self.step, **fields)
+ + +
+[docs] + def detach(self) -> PivotedCholesky: + fields = {} + for name in ("tril", "perm", "diag"): + obj = getattr(self, name) + if obj is not None: + fields[name] = obj.detach() + return type(self)(step=self.step, **fields)
+ + +
+[docs] + def clone(self) -> PivotedCholesky: + fields = {} + for name in ("tril", "perm", "diag"): + obj = getattr(self, name) + if obj is not None: + fields[name] = obj.clone() + return type(self)(step=self.step, **fields)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/probability/mvnxpb.html b/website-old/pages/api/_modules/botorch/utils/probability/mvnxpb.html new file mode 100644 index 0000000000..64f1676b48 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/probability/mvnxpb.html @@ -0,0 +1,522 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.probability.mvnxpb

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Bivariate conditioning algorithm for approximating Gaussian probabilities,
+see [Genz2016numerical]_ and [Trinh2015bivariate]_.
+
+.. [Trinh2015bivariate]
+    G. Trinh and A. Genz. Bivariate conditioning approximations for
+    multivariate normal probabilities. Statistics and Computing, 2015.
+
+.. [Genz2016numerical]
+    A. Genz and G. Tring. Numerical Computation of Multivariate Normal Probabilities
+    using Bivariate Conditioning. Monte Carlo and Quasi-Monte Carlo Methods, 2016.
+
+.. [Gibson1994monte]
+    GJ. Gibson, CA Galsbey, and DA Elston. Monte Carlo evaluation of multivariate normal
+    integrals and sensitivity to variate ordering. Advances in Numerical Methods and
+    Applications. 1994.
+"""
+
+from __future__ import annotations
+
+from typing import Any, TypedDict
+from warnings import warn
+
+import torch
+from botorch.utils.probability.bvn import bvn, bvnmom
+from botorch.utils.probability.linalg import (
+    augment_cholesky,
+    block_matrix_concat,
+    PivotedCholesky,
+)
+from botorch.utils.probability.utils import (
+    case_dispatcher,
+    get_constants_like,
+    ndtr as Phi,
+    phi,
+    STANDARDIZED_RANGE,
+    swap_along_dim_,
+)
+from botorch.utils.safe_math import log as safe_log, mul as safe_mul
+from linear_operator.utils.cholesky import psd_safe_cholesky
+from linear_operator.utils.errors import NotPSDError
+from torch import LongTensor, Tensor
+from torch.nn.functional import pad
+
+
+
+[docs] +class mvnxpbState(TypedDict): + step: int + perm: LongTensor + bounds: Tensor + piv_chol: PivotedCholesky + plug_ins: Tensor + log_prob: Tensor + log_prob_extra: Tensor | None
+ + + +
+[docs] +class MVNXPB: + r"""An algorithm for approximating Gaussian probabilities `P(X \in bounds)`, where + `X ~ N(0, covariance_matrix)`. + """ + + def __init__(self, covariance_matrix: Tensor, bounds: Tensor) -> None: + r"""Initializes an MVNXPB instance. + + Args: + covariance_matrix: Covariance matrices of shape `batch_shape x [n, n]`. + bounds: Tensor of lower and upper bounds, `batch_shape x [n, 2]`. These + bounds are standardized internally and clipped to STANDARDIZED_RANGE. + """ + *batch_shape, _, n = covariance_matrix.shape + device = covariance_matrix.device + dtype = covariance_matrix.dtype + perm = torch.arange(0, n, device=device).expand(*batch_shape, n).contiguous() + + # Standardize covariance matrices and bounds + var = covariance_matrix.diagonal(dim1=-2, dim2=-1).unsqueeze(-1) + std = var.sqrt() + istd = var.rsqrt() + matrix = istd * covariance_matrix * istd.transpose(-1, -2) + + # Clip first to avoid differentiating through `istd * inf` + bounds = istd * bounds.clip(*(std * lim for lim in STANDARDIZED_RANGE)) + + # Initialize partial pivoted Cholesky + piv_chol = PivotedCholesky( + step=0, + perm=perm.clone(), + diag=std.squeeze(-1).clone(), + tril=matrix.tril(), + ) + self.step = 0 + self.perm = perm + self.bounds = bounds + self.piv_chol = piv_chol + self.plug_ins = torch.full( + batch_shape + [n], float("nan"), device=device, dtype=dtype + ) + self.log_prob = torch.zeros(batch_shape, device=device, dtype=dtype) + self.log_prob_extra: Tensor | None = None + +
+[docs] + @classmethod + def build( + cls, + step: int, + perm: Tensor, + bounds: Tensor, + piv_chol: PivotedCholesky, + plug_ins: Tensor, + log_prob: Tensor, + log_prob_extra: Tensor | None = None, + ) -> MVNXPB: + r"""Creates an MVNXPB instance from raw arguments. Unlike MVNXPB.__init__, + this methods does not preprocess or copy terms. + + Args: + step: Integer used to track the solver's progress. + bounds: Tensor of lower and upper bounds, `batch_shape x [n, 2]`. + piv_chol: A PivotedCholesky instance for the system. + plug_ins: Tensor of plug-in estimators used to update lower and upper bounds + on random variables that have yet to be integrated out. + log_prob: Tensor of log probabilities. + log_prob_extra: Tensor of conditional log probabilities for the next random + variable. Used when integrating over an odd number of random variables. + """ + new = cls.__new__(cls) + new.step = step + new.perm = perm + new.bounds = bounds + new.piv_chol = piv_chol + new.plug_ins = plug_ins + new.log_prob = log_prob + new.log_prob_extra = log_prob_extra + return new
+ + +
+[docs] + def solve(self, num_steps: int | None = None, eps: float = 1e-10) -> Tensor: + r"""Runs the MVNXPB solver instance for a fixed number of steps. + + Calculates a bivariate conditional approximation to P(X \in bounds), where + X ~ N(0, Σ). For details, see [Genz2016numerical] or [Trinh2015bivariate]_. + """ + if self.step > self.piv_chol.step: + raise ValueError("Invalid state: solver ran ahead of matrix decomposition.") + + # Unpack some terms + start = self.step + bounds = self.bounds + piv_chol = self.piv_chol + L = piv_chol.tril + y = self.plug_ins + + # Subtract marginal log probability of final term from previous result if + # it did not fit in a block. + ndim = y.shape[-1] + if ndim > start and start % 2: + self.log_prob = self.log_prob - self.log_prob_extra + self.log_prob_extra = None + + # Iteratively compute bivariate conditional approximation + zero = get_constants_like(0, L) # needed when calling `torch.where` below + num_steps = num_steps or ndim - start + for i in range(start, start + num_steps): + should_update_chol = self.step == piv_chol.step + + # Determine next pivot element + if should_update_chol: + pivot = self.select_pivot() + else: # pivot using order specified by precomputed pivoted Cholesky step + mask = self.perm[..., i:] == piv_chol.perm[..., i : i + 1] + pivot = i + torch.nonzero(mask, as_tuple=True)[-1] + + if pivot is not None and torch.any(pivot > i): + self.pivot_(pivot=pivot) + + # Compute whitened bounds conditional on preceding plug-ins + Lii = L[..., i, i].clone() + if should_update_chol: + Lii = Lii.clip(min=0).sqrt() # conditional stddev + inv_Lii = Lii.reciprocal() + bounds_i = bounds[..., i, :].clone() + if i != 0: + bounds_i = bounds_i - torch.sum( + L[..., i, :i].clone() * y[..., :i].clone(), dim=-1, keepdim=True + ) + lb, ub = (inv_Lii.unsqueeze(-1) * bounds_i).unbind(dim=-1) + + # Initialize `i`-th plug-in value as univariate conditional expectation + Phi_i = Phi(ub) - Phi(lb) + small = Phi_i <= i * eps + y[..., i] = case_dispatcher( # used to select next pivot + out=(phi(lb) - phi(ub)) / Phi_i, + cases=( # fallback cases for enhanced numerical stability + (lambda: small & (lb < -9), lambda m: ub[m]), + (lambda: small & (lb > 9), lambda m: lb[m]), + (lambda: small, lambda m: 0.5 * (lb[m] + ub[m])), + ), + ) + + # Maybe finalize the current block + if i and i % 2: + h = i - 1 + blk = slice(h, i + 1) + Lhh = L[..., h, h].clone() + Lih = L[..., i, h].clone() + + std_i = (Lii.square() + Lih.square()).sqrt() + istds = 1 / torch.stack([Lhh, std_i], -1) + blk_bounds = bounds[..., blk, :].clone() + if i > 1: + blk_bounds = blk_bounds - ( + L[..., blk, : i - 1].clone() @ y[..., : i - 1, None].clone() + ) + + blk_lower, blk_upper = ( + pair.unbind(-1) # pair of bounds for `yh` and `yi` + for pair in safe_mul(istds.unsqueeze(-1), blk_bounds).unbind(-1) + ) + blk_corr = Lhh * Lih * istds.prod(-1) + blk_prob = bvn(blk_corr, *blk_lower, *blk_upper) + zh, zi = bvnmom(blk_corr, *blk_lower, *blk_upper, p=blk_prob) + + # Replace 1D expectations with 2D ones `L[blk, blk]^{-1} y[..., blk]` + mask = blk_prob > zero + y[..., h] = torch.where(mask, zh, zero) + y[..., i] = torch.where(mask, inv_Lii * (std_i * zi - Lih * zh), zero) + + # Update running approximation to log probability + self.log_prob = self.log_prob + safe_log(blk_prob) + + self.step += 1 + if should_update_chol: + piv_chol.update_(eps=eps) + + # Factor in univariate probability if final term fell outside of a block. + if self.step % 2: + self.log_prob_extra = safe_log(Phi_i) + self.log_prob = self.log_prob + self.log_prob_extra + + return self.log_prob
+ + +
+[docs] + def select_pivot(self) -> LongTensor | None: + r"""GGE variable prioritization strategy from [Gibson1994monte]_. + + Returns the index of the random variable least likely to satisfy its bounds + when conditioning on the previously integrated random variables `X[:t - 1]` + attaining the values of plug-in estimators `y[:t - 1]`. Equivalently, + ``` + argmin_{i = t, ..., n} P(X[i] \in bounds[i] | X[:t-1] = y[:t -1]), + ``` + where `t` denotes the current step.""" + i = self.piv_chol.step + L = self.piv_chol.tril + bounds = self.bounds + if i: + bounds = bounds[..., i:, :] - L[..., i:, :i] @ self.plug_ins[..., :i, None] + + inv_stddev = torch.diagonal(L, dim1=-2, dim2=-1)[..., i:].clip(min=0).rsqrt() + probs_1d = Phi(inv_stddev.unsqueeze(-1) * bounds).diff(dim=-1).squeeze(-1) + return i + torch.argmin(probs_1d, dim=-1)
+ + +
+[docs] + def pivot_(self, pivot: LongTensor) -> None: + r"""Swap random variables at `pivot` and `step` positions.""" + step = self.step + if self.piv_chol.step == step: + self.piv_chol.pivot_(pivot) + elif self.step > self.piv_chol.step: + raise ValueError + + for tnsr in (self.perm, self.bounds): + swap_along_dim_(tnsr, i=self.step, j=pivot, dim=pivot.ndim)
+ + + def __getitem__(self, key: Any) -> MVNXPB: + return self.build( + step=self.step, + perm=self.perm[key], + bounds=self.bounds[key], + piv_chol=self.piv_chol[key], + plug_ins=self.plug_ins[key], + log_prob=self.log_prob[key], + log_prob_extra=( + None if self.log_prob_extra is None else self.log_prob_extra[key] + ), + ) + +
+[docs] + def concat(self, other: MVNXPB, dim: int) -> MVNXPB: + if not isinstance(other, MVNXPB): + raise TypeError( + f"Expected `other` to be {type(self)} typed but was {type(other)}." + ) + + batch_ndim = self.log_prob.ndim + if dim > batch_ndim or dim < -batch_ndim: + raise ValueError(f"`dim={dim}` is not a valid batch dimension.") + + state_dict = self.asdict() + for key, _other in other.asdict().items(): + _self = state_dict.get(key) + if _self is None and _other is None: + continue + + if type(_self) is not type(_other): + raise TypeError( + f"Concatenation failed: `self.{key}` has type {type(_self)}, " + f"but `other.{key}` is of type {type(_self)}." + ) + + if isinstance(_self, PivotedCholesky): + state_dict[key] = _self.concat(_other, dim=dim) + elif isinstance(_self, Tensor): + state_dict[key] = torch.concat((_self, _other), dim=dim) + elif _self != _other: + raise ValueError( + f"Concatenation failed: `self.{key}` does not equal `other.{key}`." + ) + + return self.build(**state_dict)
+ + +
+[docs] + def expand(self, *sizes: int) -> MVNXPB: + state_dict = self.asdict() + state_dict["piv_chol"] = state_dict["piv_chol"].expand(*sizes) + for name, ndim in { + "bounds": 2, + "perm": 1, + "plug_ins": 1, + "log_prob": 0, + "log_prob_extra": 0, + }.items(): + src = state_dict[name] + if isinstance(src, Tensor): + state_dict[name] = src.expand( + sizes + src.shape[-ndim:] if ndim else sizes + ) + return self.build(**state_dict)
+ + +
+[docs] + def augment( + self, + covariance_matrix: Tensor, + bounds: Tensor, + cross_covariance_matrix: Tensor, + disable_pivoting: bool = False, + jitter: float | None = None, + max_tries: int | None = None, + ) -> MVNXPB: + r"""Augment an `n`-dimensional MVNXPB instance to include `m` additional random + variables. + """ + n = self.perm.shape[-1] + m = covariance_matrix.shape[-1] + if n != self.piv_chol.step: + raise NotImplementedError( + "Augmentation of incomplete solutions not implemented yet." + ) + + var = covariance_matrix.diagonal(dim1=-2, dim2=-1).unsqueeze(-1) + std = var.sqrt() + istd = var.rsqrt() + Kmn = istd * cross_covariance_matrix + if self.piv_chol.diag is None: + diag = pad(std.squeeze(-1), (cross_covariance_matrix.shape[-1], 0), value=1) + else: + Kmn = Kmn * (1 / self.piv_chol.diag).unsqueeze(-2) + diag = torch.concat([self.piv_chol.diag, std.squeeze(-1)], -1) + + # Augment partial pivoted Cholesky factor + Kmm = istd * covariance_matrix * istd.transpose(-1, -2) + Lnn = self.piv_chol.tril + try: + L = augment_cholesky(Laa=Lnn, Kba=Kmn, Kbb=Kmm, jitter=jitter) + except NotPSDError: + warn("Joint covariance matrix not positive definite, attempting recovery.") + Knn = Lnn @ Lnn.transpose(-1, -2) + Knm = Kmn.transpose(-1, -2) + K = block_matrix_concat(blocks=((Knn, Knm), (Kmn, Kmm))) + L = psd_safe_cholesky(K, jitter=jitter, max_tries=max_tries) + + if not disable_pivoting: + Lmm = L[..., n:, n:].clone() + L[..., n:, n:] = (Lmm @ Lmm.transpose(-2, -1)).tril() + + _bounds = istd * bounds.clip(*(std * lim for lim in STANDARDIZED_RANGE)) + _perm = torch.arange(n, n + m, dtype=self.perm.dtype, device=self.perm.device) + _perm = _perm.expand(covariance_matrix.shape[:-2] + (m,)) + + piv_chol = PivotedCholesky( + step=n + m if disable_pivoting else n, + tril=L.contiguous(), + perm=torch.cat([self.piv_chol.perm, _perm], dim=-1).contiguous(), + diag=diag, + ) + + return self.build( + step=self.step, + perm=torch.cat([self.perm, _perm], dim=-1), + bounds=torch.cat([self.bounds, _bounds], dim=-2), + piv_chol=piv_chol, + plug_ins=pad(self.plug_ins, (0, m), value=float("nan")), + log_prob=self.log_prob, + log_prob_extra=self.log_prob_extra, + )
+ + +
+[docs] + def detach(self) -> MVNXPB: + state_dict = self.asdict() + for key, obj in state_dict.items(): + if isinstance(obj, (PivotedCholesky, Tensor)): + state_dict[key] = obj.detach() + return self.build(**state_dict)
+ + +
+[docs] + def clone(self) -> MVNXPB: + state_dict = self.asdict() + for key, obj in state_dict.items(): + if isinstance(obj, (PivotedCholesky, Tensor)): + state_dict[key] = obj.clone() + return self.build(**state_dict)
+ + +
+[docs] + def asdict(self) -> mvnxpbState: + return mvnxpbState( + step=self.step, + perm=self.perm, + bounds=self.bounds, + piv_chol=self.piv_chol, + plug_ins=self.plug_ins, + log_prob=self.log_prob, + log_prob_extra=self.log_prob_extra, + )
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/probability/truncated_multivariate_normal.html b/website-old/pages/api/_modules/botorch/utils/probability/truncated_multivariate_normal.html new file mode 100644 index 0000000000..5eae737d9f --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/probability/truncated_multivariate_normal.html @@ -0,0 +1,218 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.probability.truncated_multivariate_normal

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+import torch
+from botorch.utils.probability.lin_ess import LinearEllipticalSliceSampler
+from botorch.utils.probability.mvnxpb import MVNXPB
+from botorch.utils.probability.utils import get_constants_like
+from torch import Tensor
+from torch.distributions.multivariate_normal import MultivariateNormal
+
+
+
+[docs] +class TruncatedMultivariateNormal(MultivariateNormal): + def __init__( + self, + loc: Tensor, + covariance_matrix: Tensor | None = None, + precision_matrix: Tensor | None = None, + scale_tril: Tensor | None = None, + bounds: Tensor = None, + solver: MVNXPB | None = None, + sampler: LinearEllipticalSliceSampler | None = None, + validate_args: bool | None = None, + ): + r"""Initializes an instance of a TruncatedMultivariateNormal distribution. + + Let `x ~ N(0, K)` be an `n`-dimensional Gaussian random vector. This class + represents the distribution of the truncated Multivariate normal random vector + `x | a <= x <= b`. + + Args: + loc: A mean vector for the distribution, `batch_shape x event_shape`. + covariance_matrix: Covariance matrix distribution parameter. + precision_matrix: Inverse covariance matrix distribution parameter. + scale_tril: Lower triangular, square-root covariance matrix distribution + parameter. + bounds: A `batch_shape x event_shape x 2` tensor of strictly increasing + bounds for `x` so that `bounds[..., 0] < bounds[..., 1]` everywhere. + solver: A pre-solved MVNXPB instance used to approximate the log partition. + sampler: A LinearEllipticalSliceSampler instance used for sample generation. + validate_args: Optional argument to super().__init__. + """ + if bounds is None: + raise SyntaxError("Missing required argument `bounds`.") + elif bounds.shape[-1] != 2: + raise ValueError( + f"Expected bounds.shape[-1] to be 2 but bounds shape is {bounds.shape}" + ) + elif torch.gt(*bounds.unbind(dim=-1)).any(): + raise ValueError("`bounds` must be strictly increasing along dim=-1.") + + super().__init__( + loc=loc, + covariance_matrix=covariance_matrix, + precision_matrix=precision_matrix, + scale_tril=scale_tril, + validate_args=validate_args, + ) + self.bounds = bounds + self._solver = solver + self._sampler = sampler + +
+[docs] + def log_prob(self, value: Tensor) -> Tensor: + r"""Approximates the true log probability.""" + neg_inf = get_constants_like(-float("inf"), value) + inbounds = torch.logical_and( + (self.bounds[..., 0] < value).all(-1), + (self.bounds[..., 1] > value).all(-1), + ) + if inbounds.any(): + return torch.where( + inbounds, + super().log_prob(value) - self.log_partition, + neg_inf, + ) + return torch.full(value.shape[: -len(self.event_shape)], neg_inf)
+ + +
+[docs] + def rsample(self, sample_shape: torch.Size = torch.Size()) -> Tensor: # noqa: B008 + r"""Draw samples from the Truncated Multivariate Normal. + + Args: + sample_shape: The shape of the samples. + + Returns: + The (sample_shape x batch_shape x event_shape) tensor of samples. + """ + num_samples = sample_shape.numel() if sample_shape else 1 + return self.loc + self.sampler.draw(n=num_samples).view(*sample_shape, -1)
+ + + @property + def log_partition(self) -> Tensor: + return self.solver.log_prob + + @property + def solver(self) -> MVNXPB: + if self._solver is None: + self._solver = MVNXPB( + covariance_matrix=self.covariance_matrix, + bounds=self.bounds - self.loc.unsqueeze(-1), + ) + self._solver.solve() + return self._solver + + @property + def sampler(self) -> LinearEllipticalSliceSampler: + if self._sampler is None: + eye = torch.eye( + self.scale_tril.shape[-1], + dtype=self.scale_tril.dtype, + device=self.scale_tril.device, + ) + + A = torch.concat([-eye, eye]) + b = torch.concat( + [ + self.loc - self.bounds[..., 0], + self.bounds[..., 1] - self.loc, + ], + dim=-1, + ).unsqueeze(-1) + + self._sampler = LinearEllipticalSliceSampler( + inequality_constraints=(A, b), + covariance_root=self.scale_tril, + ) + return self._sampler + +
+[docs] + def expand( + self, batch_shape: Sequence[int], _instance: TruncatedMultivariateNormal = None + ) -> TruncatedMultivariateNormal: + new = self._get_checked_instance(TruncatedMultivariateNormal, _instance) + super().expand(batch_shape=batch_shape, _instance=new) + + new.bounds = self.bounds.expand(*new.batch_shape, *self.event_shape, 2) + new._sampler = None # does not implement `expand` + new._solver = ( + None if self._solver is None else self._solver.expand(*batch_shape) + ) + return new
+ + + def __repr__(self) -> str: + return super().__repr__()[:-1] + f", bounds: {self.bounds.shape})"
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/probability/unified_skew_normal.html b/website-old/pages/api/_modules/botorch/utils/probability/unified_skew_normal.html new file mode 100644 index 0000000000..c3b4e81125 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/probability/unified_skew_normal.html @@ -0,0 +1,333 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.probability.unified_skew_normal

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from inspect import getmembers
+
+import torch
+from botorch.utils.probability.linalg import augment_cholesky, block_matrix_concat
+from botorch.utils.probability.mvnxpb import MVNXPB
+from botorch.utils.probability.truncated_multivariate_normal import (
+    TruncatedMultivariateNormal,
+)
+from linear_operator.operators import LinearOperator
+from linear_operator.utils.errors import NotPSDError
+from torch import Tensor
+from torch.distributions.multivariate_normal import Distribution, MultivariateNormal
+from torch.distributions.utils import lazy_property
+from torch.nn.functional import pad
+
+
+
+[docs] +class UnifiedSkewNormal(Distribution): + arg_constraints = {} + + def __init__( + self, + trunc: TruncatedMultivariateNormal, + gauss: MultivariateNormal, + cross_covariance_matrix: Tensor | LinearOperator, + validate_args: bool | None = None, + ): + r"""Unified Skew Normal distribution of `Y | a < X < b` for jointly Gaussian + random vectors `X ∈ R^m` and `Y ∈ R^n`. + + Batch shapes `trunc.batch_shape` and `gauss.batch_shape` must be broadcastable. + Care should be taken when choosing `trunc.batch_shape`. When `trunc` is of lower + batch dimensionality than `gauss`, the user should consider expanding `trunc` to + hasten `UnifiedSkewNormal.log_prob`. In these cases, it is suggested that the + user invoke `trunc.solver` before calling `trunc.expand` to avoid paying for + multiple, identical solves. + + Args: + trunc: Distribution of `Z = (X | a < X < b) ∈ R^m`. + gauss: Distribution of `Y ∈ R^n`. + cross_covariance_matrix: Cross-covariance `Cov(X, Y) ∈ R^{m x n}`. + validate_args: Optional argument to super().__init__. + """ + if len(trunc.event_shape) != len(gauss.event_shape): + raise ValueError( + f"{len(trunc.event_shape)}-dimensional `trunc` incompatible with" + f"{len(gauss.event_shape)}-dimensional `gauss`." + ) + # LinearOperator currently doesn't support torch.linalg.solve_triangular, + # so for the time being, we cast the operator to dense here + if isinstance(cross_covariance_matrix, LinearOperator): + cross_covariance_matrix = cross_covariance_matrix.to_dense() + try: + batch_shape = torch.broadcast_shapes(trunc.batch_shape, gauss.batch_shape) + except RuntimeError as e: + raise ValueError("Incompatible batch shapes") from e + + super().__init__( + batch_shape=batch_shape, + event_shape=gauss.event_shape, + validate_args=validate_args, + ) + self.trunc = trunc + self.gauss = gauss + self.cross_covariance_matrix = cross_covariance_matrix + if self._validate_args: + try: + # calling _orthogonalized_gauss first makes the following call + # _orthogonalized_gauss.scale_tril which is used by self.rsample + self._orthogonalized_gauss + self.scale_tril + except Exception as e: + # error could be thrown by linalg.augment_cholesky (NotPSDError) + # or torch.linalg.cholesky (with "positive-definite" in the message) + if ( + isinstance(e, NotPSDError) + or "positive-definite" in str(e) + or "PositiveDefinite" in str(e) + ): + e = ValueError( + "UnifiedSkewNormal is only well-defined for positive definite" + " joint covariance matrices." + ) + raise e + +
+[docs] + def log_prob(self, value: Tensor) -> Tensor: + r"""Computes the log probability `ln p(Y = value | a < X < b)`.""" + event_ndim = len(self.event_shape) + if value.ndim < event_ndim or value.shape[-event_ndim:] != self.event_shape: + raise ValueError( + f"`value` with shape {value.shape} does not comply with the instance's" + f"`event_shape` of {self.event_shape}." + ) + + # Iterate with a fixed batch size to keep memory overhead in check + i = 0 + pre_shape = value.shape[: -len(self.event_shape) - len(self.batch_shape)] + batch_size = self.batch_shape.numel() + log_probs = torch.empty( + pre_shape.numel() * batch_size, device=value.device, dtype=value.dtype + ) + for batch in value.view(-1, *value.shape[len(pre_shape) :]): + log_probs[i : i + batch_size] = self._log_prob(batch).view(-1) + i += batch_size + + return log_probs.view(pre_shape + self.batch_shape)
+ + + def _log_prob(self, value: Tensor) -> Tensor: + r"""Computes the log probability `ln p(Y = value | a < X < b)`.""" + # Center by subtracting E[X | Y = value] from `bounds`. + bounds = ( + self.trunc.bounds + - self.trunc.loc.unsqueeze(-1) + - self._iKyy_Kyx.transpose(-2, -1) @ (value - self.gauss.loc).unsqueeze(-1) + ) + + # Approximately solve for MVN CDF + solver = MVNXPB(covariance_matrix=self._K_schur_Kyy, bounds=bounds) + + # p(Y = value | a < X < b) = P(a < X < b | Y = value)p(Y = value)/P(a < X < b) + return solver.solve() + self.gauss.log_prob(value) - self.trunc.log_partition + +
+[docs] + def rsample(self, sample_shape: torch.Size = torch.Size()) -> Tensor: # noqa: B008 + r"""Draw samples from the Unified Skew Normal. + + Args: + sample_shape: The shape of the samples. + + Returns: + The (sample_shape x batch_shape x event_shape) tensor of samples. + """ + residuals = self._orthogonalized_gauss.rsample(sample_shape=sample_shape) + trunc_rvs = self.trunc.rsample(sample_shape=sample_shape) - self.trunc.loc + cond_expectations = self.gauss.loc + trunc_rvs @ self._iKxx_Kxy + return cond_expectations + residuals
+ + +
+[docs] + def expand( + self, batch_shape: Sequence[int], _instance: UnifiedSkewNormal = None + ) -> UnifiedSkewNormal: + new = self._get_checked_instance(UnifiedSkewNormal, _instance) + super(UnifiedSkewNormal, new).__init__( + batch_shape=batch_shape, event_shape=self.event_shape, validate_args=False + ) + + new._validate_args = self._validate_args + new.gauss = self.gauss.expand(batch_shape=batch_shape) + new.trunc = self.trunc.expand(batch_shape=batch_shape) + new.cross_covariance_matrix = self.cross_covariance_matrix.expand( + batch_shape + self.cross_covariance_matrix.shape[-2:] + ) + + # Expand cached properties + for name, _ in getmembers( + UnifiedSkewNormal, lambda x: isinstance(x, lazy_property) + ): + if name not in self.__dict__: + continue + + obj = getattr(self, name) + if isinstance(obj, Tensor): + base = obj if (obj._base is None) else obj._base + new_obj = obj.expand(batch_shape + base.shape) + elif isinstance(obj, Distribution): + new_obj = obj.expand(batch_shape=batch_shape) + else: + raise TypeError( + f"Type {type(obj)} of UnifiedSkewNormal's lazy property " + f"{name} not supported." + ) + + setattr(new, name, new_obj) + return new
+ + + def __repr__(self) -> str: + args_string = ", ".join( + ( + f"trunc: {self.trunc}", + f"gauss: {self.gauss}", + f"cross_covariance_matrix: {self.cross_covariance_matrix.shape}", + ) + ) + return self.__class__.__name__ + "(" + args_string + ")" + + @lazy_property + def covariance_matrix(self) -> Tensor: + Kxx = self.trunc.covariance_matrix + Kxy = self.cross_covariance_matrix + Kyy = self.gauss.covariance_matrix + return block_matrix_concat(blocks=([Kxx, Kxy], [Kxy.transpose(-2, -1), Kyy])) + + @lazy_property + def scale_tril(self) -> Tensor: + Lxx = self.trunc.scale_tril + Lyx = self._iLxx_Kxy.transpose(-2, -1) + if "_orthogonalized_gauss" in self.__dict__: + n = Lyx.shape[-2] + Lyy = self._orthogonalized_gauss.scale_tril + return block_matrix_concat(blocks=([pad(Lxx, (0, n))], [Lyx, Lyy])) + return augment_cholesky(Laa=Lxx, Lba=Lyx, Kbb=self.gauss.covariance_matrix) + + @lazy_property + def _orthogonalized_gauss(self) -> MultivariateNormal: + r"""Distribution of `Y ⊥ X = Y - E[Y | X]`, where `Y ~ gauss` and `X ~ untrunc` + is the untruncated version of `Z ~ trunc`.""" + n = self.gauss.loc.shape[-1] + parameters = {"loc": torch.zeros_like(self.gauss.loc)} + if "scale_tril" in self.__dict__: + parameters["scale_tril"] = self.scale_tril[..., -n:, -n:] + else: + beta = self._iLxx_Kxy + parameters["covariance_matrix"] = ( + self.gauss.covariance_matrix - beta.transpose(-1, -2) @ beta + ) + return MultivariateNormal(**parameters, validate_args=self._validate_args) + + @lazy_property + def _iLyy_Kyx(self) -> Tensor: + r"""Cov(Y, Y)^{-1/2}Cov(Y, X)`.""" + return torch.linalg.solve_triangular( + self.gauss.scale_tril, + self.cross_covariance_matrix.transpose(-1, -2), + upper=False, + ) + + @lazy_property + def _iKyy_Kyx(self) -> Tensor: + r"""Cov(Y, Y)^{-1}Cov(Y, X)`.""" + return torch.linalg.solve_triangular( + self.gauss.scale_tril.transpose(-1, -2), + self._iLyy_Kyx, + upper=True, + ) + + @lazy_property + def _iLxx_Kxy(self) -> Tensor: + r"""Cov(X, X)^{-1/2}Cov(X, Y)`.""" + return torch.linalg.solve_triangular( + self.trunc.scale_tril, self.cross_covariance_matrix, upper=False + ) + + @lazy_property + def _iKxx_Kxy(self) -> Tensor: + r"""Cov(X, X)^{-1}Cov(X, Y)`.""" + return torch.linalg.solve_triangular( + self.trunc.scale_tril.transpose(-1, -2), + self._iLxx_Kxy, + upper=True, + ) + + @lazy_property + def _K_schur_Kyy(self) -> Tensor: + r"""Cov(X, X) - Cov(X, Y)Cov(Y, Y)^{-1} Cov(Y, X)`.""" + beta = self._iLyy_Kyx + return self.trunc.covariance_matrix - beta.transpose(-1, -2) @ beta
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/probability/utils.html b/website-old/pages/api/_modules/botorch/utils/probability/utils.html new file mode 100644 index 0000000000..7da437063a --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/probability/utils.html @@ -0,0 +1,516 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.probability.utils

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import math
+from collections.abc import Callable, Iterable, Iterator
+
+from functools import lru_cache
+from math import pi
+from numbers import Number
+from typing import Any
+
+import torch
+from botorch.utils.safe_math import logdiffexp
+from numpy.polynomial.legendre import leggauss as numpy_leggauss
+from torch import BoolTensor, LongTensor, Tensor
+
+CaseNd = tuple[Callable[[], BoolTensor], Callable[[BoolTensor], Tensor]]
+
+_log_2 = math.log(2)
+_sqrt_pi = math.sqrt(pi)
+_inv_sqrt_pi = 1 / _sqrt_pi
+_inv_sqrt_2pi = 1 / math.sqrt(2 * pi)
+_inv_sqrt_2 = 1 / math.sqrt(2)
+_neg_inv_sqrt_2 = -_inv_sqrt_2
+_log_sqrt_2pi = math.log(2 * pi) / 2
+STANDARDIZED_RANGE: tuple[float, float] = (-1e6, 1e6)
+_log_two_inv_sqrt_2pi = _log_2 - _log_sqrt_2pi  # = log(2 / sqrt(2 * pi))
+
+
+
+[docs] +def case_dispatcher( + out: Tensor, + cases: Iterable[CaseNd] = (), + default: Callable[[BoolTensor], Tensor] = None, +) -> Tensor: + r"""Basic implementation of a tensorized switching case statement. + + Args: + out: Tensor to which case outcomes are written. + cases: Iterable of function pairs (pred, func), where `mask=pred()` specifies + whether `func` is applicable for each entry in `out`. Note that cases are + resolved first-come, first-serve. + default: Optional `func` to which all unclaimed entries of `out` are dispatched. + """ + active = None + for closure, func in cases: + pred = closure() + if not pred.any(): + continue + + mask = pred if (active is None) else pred & active + if not mask.any(): + continue + + if mask.all(): # where possible, use Ellipsis to avoid indexing + out[...] = func(...) + return out + + out[mask] = func(mask) + if active is None: + active = ~mask + else: + active[mask] = False + + if not active.any(): + break + + if default is not None: + if active is None: + out[...] = default(...) + elif active.any(): + out[active] = default(active) + + return out
+ + + +
+[docs] +@lru_cache(maxsize=None) +def get_constants( + values: Number | Iterator[Number], + device: torch.device | None = None, + dtype: torch.dtype | None = None, +) -> Tensor | tuple[Tensor, ...]: + r"""Returns scalar-valued Tensors containing each of the given constants. + Used to expedite tensor operations involving scalar arithmetic. Note that + the returned Tensors should not be modified in-place.""" + if isinstance(values, Number): + return torch.full((), values, dtype=dtype, device=device) + + return tuple(torch.full((), val, dtype=dtype, device=device) for val in values)
+ + + +
+[docs] +def get_constants_like( + values: Number | Iterator[Number], + ref: Tensor, +) -> Tensor | Iterator[Tensor]: + return get_constants(values, device=ref.device, dtype=ref.dtype)
+ + + +
+[docs] +def gen_positional_indices( + shape: torch.Size, + dim: int, + device: torch.device | None = None, +) -> Iterator[torch.LongTensor]: + ndim = len(shape) + _dim = ndim + dim if dim < 0 else dim + if _dim >= ndim or _dim < 0: + raise ValueError(f"dim={dim} invalid for shape {shape}.") + + cumsize = shape[_dim + 1 :].numel() + for i, s in enumerate(reversed(shape[: _dim + 1])): + yield torch.arange(0, s * cumsize, cumsize, device=device)[(...,) + i * (None,)] + cumsize *= s
+ + + +
+[docs] +def build_positional_indices( + shape: torch.Size, + dim: int, + device: torch.device | None = None, +) -> LongTensor: + return sum(gen_positional_indices(shape=shape, dim=dim, device=device))
+ + + +
+[docs] +@lru_cache(maxsize=None) +def leggauss(deg: int, **tkwargs: Any) -> tuple[Tensor, Tensor]: + x, w = numpy_leggauss(deg) + return torch.as_tensor(x, **tkwargs), torch.as_tensor(w, **tkwargs)
+ + + +
+[docs] +def ndtr(x: Tensor) -> Tensor: + r"""Standard normal CDF.""" + half, neg_inv_sqrt_2 = get_constants_like((0.5, _neg_inv_sqrt_2), x) + return half * torch.erfc(neg_inv_sqrt_2 * x)
+ + + +
+[docs] +def phi(x: Tensor) -> Tensor: + r"""Standard normal PDF.""" + inv_sqrt_2pi, neg_half = get_constants_like((_inv_sqrt_2pi, -0.5), x) + return inv_sqrt_2pi * (neg_half * x.square()).exp()
+ + + +
+[docs] +def log_phi(x: Tensor) -> Tensor: + r"""Logarithm of standard normal pdf""" + log_sqrt_2pi, neg_half = get_constants_like((_log_sqrt_2pi, -0.5), x) + return neg_half * x.square() - log_sqrt_2pi
+ + + +
+[docs] +def log_ndtr(x: Tensor) -> Tensor: + """Implementation of log_ndtr that remedies problems of torch.special's version + for large negative x, where the torch implementation yields Inf or NaN gradients. + + Args: + x: An input tensor with dtype torch.float32 or torch.float64. + + Returns: + A tensor of values of the same type and shape as x containing log(ndtr(x)). + """ + if not (x.dtype == torch.float32 or x.dtype == torch.float64): + raise TypeError( + f"log_Phi only supports torch.float32 and torch.float64 " + f"dtypes, but received {x.dtype=}." + ) + neg_inv_sqrt_2, log_2 = get_constants_like((_neg_inv_sqrt_2, _log_2), x) + return log_erfc(neg_inv_sqrt_2 * x) - log_2
+ + + +
+[docs] +def log_erfc(x: Tensor) -> Tensor: + """Computes the logarithm of the complementary error function in a numerically + stable manner. The GitHub issue https://github.com/pytorch/pytorch/issues/31945 + tracks progress toward moving this feature into PyTorch in C++. + + Args: + x: An input tensor with dtype torch.float32 or torch.float64. + + Returns: + A tensor of values of the same type and shape as x containing log(erfc(x)). + """ + if not (x.dtype == torch.float32 or x.dtype == torch.float64): + raise TypeError( + f"log_erfc only supports torch.float32 and torch.float64 " + f"dtypes, but received {x.dtype=}." + ) + is_pos = x > 0 + x_pos = x.masked_fill(~is_pos, 0) + x_neg = x.masked_fill(is_pos, 0) + return torch.where( + is_pos, + torch.log(torch.special.erfcx(x_pos)) - x_pos.square(), + torch.log(torch.special.erfc(x_neg)), + )
+ + + +
+[docs] +def log_erfcx(x: Tensor) -> Tensor: + """Computes the logarithm of the complementary scaled error function in a + numerically stable manner. The GitHub issue tracks progress toward moving this + feature into PyTorch in C++: https://github.com/pytorch/pytorch/issues/31945. + + Args: + x: An input tensor with dtype torch.float32 or torch.float64. + + Returns: + A tensor of values of the same type and shape as x containing log(erfcx(x)). + """ + is_pos = x > 0 + x_pos = x.masked_fill(~is_pos, 0) + x_neg = x.masked_fill(is_pos, 0) + return torch.where( + is_pos, + torch.special.erfcx(x_pos).log(), + torch.special.erfc(x_neg).log() + x.square(), + )
+ + + +
+[docs] +def standard_normal_log_hazard(x: Tensor) -> Tensor: + """Computes the logarithm of the hazard function of the standard normal + distribution, i.e. `log(phi(x) / Phi(-x))`. + + Args: + x: A tensor of any shape, with either float32 or float64 dtypes. + + Returns: + A Tensor of the same shape `x`, containing the values of the logarithm of the + hazard function evaluated at `x`. + """ + # NOTE: using _inv_sqrt_2 instead of _neg_inv_sqrt_2 means we are computing Phi(-x). + a, b = get_constants_like((_log_two_inv_sqrt_2pi, _inv_sqrt_2), x) + return a - log_erfcx(b * x)
+ + + +
+[docs] +def log_prob_normal_in(a: Tensor, b: Tensor) -> Tensor: + r"""Computes the probability that a standard normal random variable takes a value + in \[a, b\], i.e. log(Phi(b) - Phi(a)), where Phi is the standard normal CDF. + Returns accurate values and permits numerically stable backward passes for inputs + in [-1e100, 1e100] for double precision and [-1e20, 1e20] for single precision. + In contrast, a naive approach is not numerically accurate beyond [-10, 10]. + + Args: + a: Tensor of lower integration bounds of the Gaussian probability measure. + b: Tensor of upper integration bounds of the Gaussian probability measure. + + Returns: + Tensor of the log probabilities. + """ + if not (a < b).all(): + raise ValueError("Received input tensors a, b for which not all a < b.") + # if abs(b) > abs(a), we use Phi(b) - Phi(a) = Phi(-a) - Phi(-b), since the + # right tail converges to 0 from below, leading to digit cancellation issues, while + # the left tail of log_ndtr is well behaved and results in large negative numbers + rev_cond = b.abs() > a.abs() # condition for reversal of inputs + if rev_cond.any(): + c = torch.where(rev_cond, -b, a) + b = torch.where(rev_cond, -a, b) + a = c # after we updated b, can assign c to a + return logdiffexp(log_a=log_ndtr(a), log_b=log_ndtr(b))
+ + + +
+[docs] +def swap_along_dim_( + values: Tensor, + i: int | LongTensor, + j: int | LongTensor, + dim: int, + buffer: Tensor | None = None, +) -> Tensor: + r"""Swaps Tensor slices in-place along dimension `dim`. + + When passed as Tensors, `i` (and `j`) should be `dim`-dimensional tensors + with the same shape as `values.shape[:dim]`. The xception to this rule occurs + when `dim=0`, in which case `i` (and `j`) should be (at most) one-dimensional + when passed as a Tensor. + + Args: + values: Tensor whose values are to be swapped. + i: Indices for slices along dimension `dim`. + j: Indices for slices along dimension `dim`. + dim: The dimension of `values` along which to swap slices. + buffer: Optional buffer used internally to store copied values. + + Returns: + The original `values` tensor. + """ + dim = values.ndim + dim if dim < 0 else dim + if dim and ( + (isinstance(i, Tensor) and i.ndim) or (isinstance(j, Tensor) and j.ndim) + ): + # Handle n-dimensional batches of heterogeneous swaps via linear indexing + if isinstance(i, Tensor) and i.shape != values.shape[:dim]: + raise ValueError("Batch shapes of `i` and `values` do not match.") + + if isinstance(j, Tensor) and j.shape != values.shape[:dim]: + raise ValueError("Batch shapes of `j` and `values` do not match.") + + pidx = build_positional_indices( + shape=values.shape[: dim + 1], dim=-2, device=values.device + ) + + swap_along_dim_( + values.view(-1, *values.shape[dim + 1 :]), + i=(pidx + i).view(-1), + j=(pidx + j).view(-1), + dim=0, + buffer=buffer, + ) + + else: + # Base cases: homogeneous swaps and 1-dimenensional heterogeneous swaps + if isinstance(i, Tensor) and i.ndim > 1: + raise ValueError("Tensor `i` must be at most 1-dimensional when `dim=0`.") + + if isinstance(j, Tensor) and j.ndim > 1: + raise ValueError("Tensor `j` must be at most 1-dimensional when `dim=0`.") + + if dim: + ctx = tuple(slice(None) for _ in range(dim)) + i = ctx + (i,) + j = ctx + (j,) + + if buffer is None: + buffer = values[i].clone() + else: + buffer.copy_(values[i]) + + values[i] = values[j] + values[j] = buffer + + return values
+ + + +
+[docs] +def compute_log_prob_feas_from_bounds( + con_lower_inds: Tensor, + con_upper_inds: Tensor, + con_both_inds: Tensor, + con_lower: Tensor, + con_upper: Tensor, + con_both: Tensor, + means: Tensor, + sigmas: Tensor, +) -> Tensor: + r"""Compute logarithm of the feasibility probability for each batch of mean/sigma. + + Args: + means: A `(b) x m`-dim Tensor of means. + sigmas: A `(b) x m`-dim Tensor of standard deviations. + con_lower_inds: 1d Tensor of indices con_lower applies to + in the second dimension of means and sigmas. + con_upper_inds: 1d Tensor of indices con_upper applies to + in the second dimension of means and sigmas. + con_both_inds: 1d Tensor of indices con_both applies to + in the second dimension of means and sigmas. + con_lower: 1d Tensor of lower bounds on the constraints + equal in dimension to con_lower_inds. + con_upper: 1d Tensor of upper bounds on the constraints + equal in dimension to con_upper_inds. + con_both: 2d Tensor of "both" bounds on the constraints + equal in length to con_both_inds. + Returns: + A `b`-dim tensor of log feasibility probabilities + """ + log_prob = torch.zeros_like(means[..., 0]) + if len(con_lower_inds) > 0: + i = con_lower_inds + dist_l = (con_lower - means[..., i]) / sigmas[..., i] + log_prob = log_prob + log_ndtr(-dist_l).sum(dim=-1) # 1 - Phi(x) = Phi(-x) + if len(con_upper_inds) > 0: + i = con_upper_inds + dist_u = (con_upper - means[..., i]) / sigmas[..., i] + log_prob = log_prob + log_ndtr(dist_u).sum(dim=-1) + if len(con_both_inds) > 0: + i = con_both_inds + con_lower, con_upper = con_both[:, 0], con_both[:, 1] + # scaled distance to lower and upper constraint boundary: + dist_l = (con_lower - means[..., i]) / sigmas[..., i] + dist_u = (con_upper - means[..., i]) / sigmas[..., i] + log_prob = log_prob + log_prob_normal_in(a=dist_l, b=dist_u).sum(dim=-1) + return log_prob
+ + + +
+[docs] +def percentile_of_score(data: Tensor, score: Tensor, dim: int = -1) -> Tensor: + """Compute the percentile rank of `score` relative to `data`. + For example, if this function returns 70 then 70% of the + values in `data` are below `score`. + + This implementation is based on `scipy.stats.percentileofscore`, + with `kind='rank'` and `nan_policy='propagate'`, which is the default. + + Args: + data: A `... x n x output_shape`-dim Tensor of data. + score: A `... x 1 x output_shape`-dim Tensor of scores. + + Returns: + A `... x output_shape`-dim Tensor of percentile ranks. + """ + # based on scipy.stats.percentileofscore + left = torch.count_nonzero(data < score, dim=dim) + right = torch.count_nonzero(data <= score, dim=dim) + plus1 = left < right + perct = (left + right + plus1) * (50.0 / data.shape[dim]) + # perct shape: `... x output_shape` + # fill in nans due to current trial progression being nan + nan_mask = torch.broadcast_to(torch.isnan(score.squeeze(dim)), perct.shape) + perct[nan_mask] = torch.nan + # fill in nans due to previous trial progressions being nan + nan_mask = torch.broadcast_to(torch.any(torch.isnan(data), dim=dim), perct.shape) + perct[nan_mask] = torch.nan + return perct
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/rounding.html b/website-old/pages/api/_modules/botorch/utils/rounding.html new file mode 100644 index 0000000000..2596298567 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/rounding.html @@ -0,0 +1,187 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.rounding

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Discretization (rounding) functions for acquisition optimization.
+
+References
+
+.. [Daulton2022bopr]
+    S. Daulton, X. Wan, D. Eriksson, M. Balandat, M. A. Osborne, E. Bakshy.
+    Bayesian Optimization over Discrete and Mixed Spaces via Probabilistic
+    Reparameterization. Advances in Neural Information Processing Systems
+    35, 2022.
+"""
+
+from __future__ import annotations
+
+import torch
+from torch import Tensor
+from torch.autograd import Function
+from torch.nn.functional import one_hot
+
+
+
+[docs] +def approximate_round(X: Tensor, tau: float = 1e-3) -> Tensor: + r"""Diffentiable approximate rounding function. + + This method is a piecewise approximation of a rounding function where + each piece is a hyperbolic tangent function. + + Args: + X: The tensor to round to the nearest integer (element-wise). + tau: A temperature hyperparameter. + + Returns: + The approximately rounded input tensor. + """ + offset = X.floor() + scaled_remainder = (X - offset - 0.5) / tau + rounding_component = (torch.tanh(scaled_remainder) + 1) / 2 + return offset + rounding_component
+ + + +
+[docs] +class IdentitySTEFunction(Function): + """Base class for functions using straight through gradient estimators. + + This class approximates the gradient with the identity function. + """ + +
+[docs] + @staticmethod + def backward(ctx, grad_output: Tensor) -> Tensor: + r"""Use a straight-through estimator the gradient. + + This uses the identity function. + + Args: + grad_output: A tensor of gradients. + + Returns: + The provided tensor. + """ + return grad_output
+
+ + + +
+[docs] +class RoundSTE(IdentitySTEFunction): + r"""Round the input tensor and use a straight-through gradient estimator. + + [Daulton2022bopr]_ proposes using this in acquisition optimization. + """ + +
+[docs] + @staticmethod + def forward(ctx, X: Tensor) -> Tensor: + r"""Round the input tensor element-wise. + + Args: + X: The tensor to be rounded. + + Returns: + A tensor where each element is rounded to the nearest integer. + """ + return X.round()
+
+ + + +
+[docs] +class OneHotArgmaxSTE(IdentitySTEFunction): + r"""Discretize a continuous relaxation of a one-hot encoded categorical. + + This returns a one-hot encoded categorical and use a straight-through + gradient estimator via an identity function. + + [Daulton2022bopr]_ proposes using this in acquisition optimization. + """ + +
+[docs] + @staticmethod + def forward(ctx, X: Tensor) -> Tensor: + r"""Discretize the input tensor. + + This applies a argmax along the last dimensions of the input tensor + and one-hot encodes the result. + + Args: + X: The tensor to be rounded. + + Returns: + A tensor where each element is rounded to the nearest integer. + """ + return one_hot(X.argmax(dim=-1), num_classes=X.shape[-1]).to(X)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/safe_math.html b/website-old/pages/api/_modules/botorch/utils/safe_math.html new file mode 100644 index 0000000000..60613f36f5 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/safe_math.html @@ -0,0 +1,627 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.safe_math

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Special implementations of mathematical functions that
+solve numerical issues of naive implementations.
+
+.. [Maechler2012accurate]
+    M. Mächler. Accurately Computing log (1 - exp (-| a|))
+        Assessed by the Rmpfr package. Technical report, 2012.
+"""
+
+from __future__ import annotations
+
+import math
+from collections.abc import Callable
+
+import torch
+from botorch.exceptions import UnsupportedError
+from botorch.utils.constants import get_constants_like
+from torch import finfo, Tensor
+from torch.nn.functional import softplus
+
+_log2 = math.log(2)
+_inv_sqrt_3 = math.sqrt(1 / 3)
+
+TAU = 1.0  # default temperature parameter for smooth approximations to non-linearities
+ALPHA = 2.0  # default alpha parameter for the asymptotic power decay of _pareto
+
+
+# Unary ops
+
+[docs] +def exp(x: Tensor, **kwargs) -> Tensor: + info = finfo(x.dtype) + maxexp = get_constants_like(math.log(info.max) - 1e-4, x) + return torch.exp(x.clip(max=maxexp), **kwargs)
+ + + +
+[docs] +def log(x: Tensor, **kwargs) -> Tensor: + info = finfo(x.dtype) + return torch.log(x.clip(min=info.tiny), **kwargs)
+ + + +# Binary ops +
+[docs] +def add(a: Tensor, b: Tensor, **kwargs) -> Tensor: + _0 = get_constants_like(0, a) + case = a.isinf() & b.isinf() & (a != b) + return torch.where(case, _0, a + b)
+ + + +
+[docs] +def sub(a: Tensor, b: Tensor) -> Tensor: + _0 = get_constants_like(0, a) + case = (a.isinf() & b.isinf()) & (a == b) + return torch.where(case, _0, a - b)
+ + + +
+[docs] +def div(a: Tensor, b: Tensor) -> Tensor: + _0, _1 = get_constants_like(values=(0, 1), ref=a) + case = ((a == _0) & (b == _0)) | (a.isinf() & a.isinf()) + return torch.where(case, torch.where(a != b, -_1, _1), a / torch.where(case, _1, b))
+ + + +
+[docs] +def mul(a: Tensor, b: Tensor) -> Tensor: + _0 = get_constants_like(values=0, ref=a) + case = (a.isinf() & (b == _0)) | (b.isinf() & (a == _0)) + return torch.where(case, _0, a * torch.where(case, _0, b))
+ + + +
+[docs] +def log1mexp(x: Tensor) -> Tensor: + """Numerically accurate evaluation of log(1 - exp(x)) for x < 0. + See [Maechler2012accurate]_ for details. + """ + log2 = get_constants_like(values=_log2, ref=x) + is_small = -log2 < x # x < 0 + return torch.where( + is_small, + (-x.expm1()).log(), + (-x.exp()).log1p(), + )
+ + + +
+[docs] +def log1pexp(x: Tensor) -> Tensor: + """Numerically accurate evaluation of log(1 + exp(x)). + See [Maechler2012accurate]_ for details. + """ + mask = x <= 18 + return torch.where( + mask, + (lambda z: z.exp().log1p())(x.masked_fill(~mask, 0)), + (lambda z: z + (-z).exp())(x.masked_fill(mask, 0)), + )
+ + + +
+[docs] +def logexpit(X: Tensor) -> Tensor: + """Computes the logarithm of the expit (a.k.a. sigmoid) function.""" + return -log1pexp(-X)
+ + + +
+[docs] +def logplusexp(a: Tensor, b: Tensor) -> Tensor: + """Computes log(exp(a) + exp(b)) similar to logsumexp.""" + ab = torch.stack(torch.broadcast_tensors(a, b), dim=-1) + return logsumexp(ab, dim=-1)
+ + + +
+[docs] +def logdiffexp(log_a: Tensor, log_b: Tensor) -> Tensor: + """Computes log(b - a) accurately given log(a) and log(b). + Assumes, log_b > log_a, i.e. b > a > 0. + + Args: + log_a (Tensor): The logarithm of a, assumed to be less than log_b. + log_b (Tensor): The logarithm of b, assumed to be larger than log_a. + + Returns: + A Tensor of values corresponding to log(b - a). + """ + log_a, log_b = torch.broadcast_tensors(log_a, log_b) + is_inf = log_b == -torch.inf # implies log_a == -torch.inf by assumption + return log_b + log1mexp(log_a - log_b.masked_fill(is_inf, 0.0))
+ + + +
+[docs] +def logsumexp(x: Tensor, dim: int | tuple[int, ...], keepdim: bool = False) -> Tensor: + """Version of logsumexp that has a well-behaved backward pass when + x contains infinities. + + In particular, the gradient of the standard torch version becomes NaN + 1) for any element that is positive infinity, and 2) for any slice that + only contains negative infinities. + + This version returns a gradient of 1 for any positive infinities in case 1, and + for all elements of the slice in case 2, in agreement with the asymptotic behavior + of the function. + + Args: + x: The Tensor to which to apply `logsumexp`. + dim: An integer or a tuple of integers, representing the dimensions to reduce. + keepdim: Whether to keep the reduced dimensions. Defaults to False. + + Returns: + A Tensor representing the log of the summed exponentials of `x`. + """ + return _inf_max_helper(torch.logsumexp, x=x, dim=dim, keepdim=keepdim)
+ + + +def _inf_max_helper( + max_fun: Callable[[Tensor], Tensor], + x: Tensor, + dim: int | tuple[int, ...], + keepdim: bool, +) -> Tensor: + """Helper function that generalizes the treatment of infinities for approximations + to the maximum operator, i.e., `max(X, dim, keepdim)`. At the point of writing of + this function, it is used to define `logsumexp` and `fatmax`. + + Args: + max_fun: The function that is used to smoothly penalize the difference of an + element to the true maximum. + x: The Tensor on which to compute the smooth approximation to the maximum. + dim: The dimension(s) to reduce over. + keepdim: Whether to keep the reduced dimension. Defaults to False. + + Returns: + The Tensor representing the smooth approximation to the maximum over the + specified dimensions. + """ + M = x.amax(dim=dim, keepdim=True) + is_inf_max = torch.logical_and(*torch.broadcast_tensors(M.isinf(), x == M)) + has_inf_max = _any(is_inf_max, dim=dim, keepdim=True) + + y_inf = x.masked_fill(~is_inf_max, 0.0) + M_no_inf = M.masked_fill(M.isinf(), 0.0) + y_no_inf = x.masked_fill(has_inf_max, 0.0) - M_no_inf + + res = torch.where( + has_inf_max, + y_inf.sum(dim=dim, keepdim=True), + M_no_inf + max_fun(y_no_inf, dim=dim, keepdim=True), + ) + # NOTE: Using `sum` instead of `squeeze` because PyTorch < 2.0 does not support + # tuple `dim` arguments. `sum` and `squeeze` are equivalent here because the + # `dim` dimensions have length one after the reductions in the previous lines. + # TODO: Replace `sum` with `squeeze` once PyTorch >= 2.0 is required. + return res if keepdim else res.sum(dim=dim) + + +def _any(x: Tensor, dim: int | tuple[int, ...], keepdim: bool = False) -> Tensor: + """Extension of torch.any, which supports reducing over tuples of dimensions. + + Args: + x: The Tensor to reduce over. + dim: An integer or a tuple of integers, representing the dimensions to reduce. + keepdim: Whether to keep the reduced dimensions. Defaults to False. + + Returns: + The Tensor corresponding to `any` over the specified dimensions. + """ + if isinstance(dim, tuple): + for d in dim: + x = x.any(dim=d, keepdim=True) + else: + x = x.any(dim, keepdim=True) + return x if keepdim else x.squeeze(dim) + + +
+[docs] +def logmeanexp(X: Tensor, dim: int | tuple[int, ...], keepdim: bool = False) -> Tensor: + """Computes `log(mean(exp(X), dim=dim, keepdim=keepdim))`. + + Args: + X: Values of which to compute the logmeanexp. + dim: The dimension(s) over which to compute the mean. + keepdim: If True, keeps the reduced dimensions. + + Returns: + A Tensor of values corresponding to `log(mean(exp(X), dim=dim))`. + """ + n = X.shape[dim] if isinstance(dim, int) else math.prod(X.shape[i] for i in dim) + return logsumexp(X, dim=dim, keepdim=keepdim) - math.log(n)
+ + + +
+[docs] +def log_softplus(x: Tensor, tau: float | Tensor = TAU) -> Tensor: + """Computes the logarithm of the softplus function with high numerical accuracy. + + Args: + x: Input tensor, should have single or double precision floats. + tau: Decreasing tau increases the tightness of the + approximation to ReLU. Non-negative and defaults to 1.0. + + Returns: + Tensor corresponding to `log(softplus(x))`. + """ + check_dtype_float32_or_float64(x) + tau = torch.as_tensor(tau, dtype=x.dtype, device=x.device) + # cutoff chosen to achieve accuracy to machine epsilon + upper = 16 if x.dtype == torch.float32 else 32 + lower = -15 if x.dtype == torch.float32 else -35 + mask = x / tau > lower + return torch.where( + mask, + softplus(x.masked_fill(~mask, lower), beta=(1 / tau), threshold=upper).log(), + x / tau + tau.log(), + )
+ + + +
+[docs] +def smooth_amax( + X: Tensor, + dim: int | tuple[int, ...] = -1, + keepdim: bool = False, + tau: float | Tensor = 1.0, +) -> Tensor: + """Computes a smooth approximation to `max(X, dim=dim)`, i.e the maximum value of + `X` over dimension `dim`, using the logarithm of the `l_(1/tau)` norm of `exp(X)`. + Note that when `X = log(U)` is the *logarithm* of an acquisition utility `U`, + + `logsumexp(log(U) / tau) * tau = log(sum(U^(1/tau))^tau) = log(norm(U, ord=(1/tau))` + + Args: + X: A Tensor from which to compute the smoothed amax. + dim: The dimensions to reduce over. + keepdim: If True, keeps the reduced dimensions. + tau: Temperature parameter controlling the smooth approximation + to max operator, becomes tighter as tau goes to 0. Needs to be positive. + + Returns: + A Tensor of smooth approximations to `max(X, dim=dim)`. + """ + # consider normalizing by log_n = math.log(X.shape[dim]) to reduce error + return logsumexp(X / tau, dim=dim, keepdim=keepdim) * tau # ~ X.amax(dim=dim)
+ + + +
+[docs] +def smooth_amin( + X: Tensor, + dim: int | tuple[int, ...] = -1, + keepdim: bool = False, + tau: float | Tensor = 1.0, +) -> Tensor: + """A smooth approximation to `min(X, dim=dim)`, similar to `smooth_amax`.""" + return -smooth_amax(X=-X, dim=dim, keepdim=keepdim, tau=tau)
+ + + +
+[docs] +def check_dtype_float32_or_float64(X: Tensor) -> None: + if X.dtype != torch.float32 and X.dtype != torch.float64: + raise UnsupportedError( + f"Only dtypes float32 and float64 are supported, but received {X.dtype}." + )
+ + + +
+[docs] +def log_fatplus(x: Tensor, tau: float | Tensor = TAU) -> Tensor: + """Computes the logarithm of the fat-tailed softplus. + + NOTE: Separated out in case the complexity of the `log` implementation increases + in the future. + """ + return fatplus(x, tau=tau).log()
+ + + +
+[docs] +def fatplus(x: Tensor, tau: float | Tensor = TAU) -> Tensor: + """Computes a fat-tailed approximation to `ReLU(x) = max(x, 0)` by linearly + combining a regular softplus function and the density function of a Cauchy + distribution. The coefficient `alpha` of the Cauchy density is chosen to guarantee + monotonicity and convexity. + + Args: + x: A Tensor on whose values to compute the smoothed function. + tau: Temperature parameter controlling the smoothness of the approximation. + + Returns: + A Tensor of values of the fat-tailed softplus. + """ + + def _fatplus(x: Tensor) -> Tensor: + alpha = 1e-1 # guarantees monotonicity and convexity (TODO: ref + Lemma 4) + return softplus(x) + alpha * cauchy(x) + + return tau * _fatplus(x / tau)
+ + + +
+[docs] +def fatmax( + x: Tensor, + dim: int | tuple[int, ...], + keepdim: bool = False, + tau: float | Tensor = TAU, + alpha: float = ALPHA, +) -> Tensor: + """Computes a smooth approximation to amax(X, dim=dim) with a fat tail. + + Args: + X: A Tensor from which to compute the smoothed maximum. + dim: The dimensions to reduce over. + keepdim: If True, keeps the reduced dimensions. + tau: Temperature parameter controlling the smooth approximation + to max operator, becomes tighter as tau goes to 0. Needs to be positive. + alpha: The exponent of the asymptotic power decay of the approximation. The + default value is 2. Higher alpha parameters make the function behave more + similarly to the standard logsumexp approximation to the max, so it is + recommended to keep this value low or moderate, e.g. < 10. + + Returns: + A Tensor of smooth approximations to `amax(X, dim=dim)` with a fat tail. + """ + + def max_fun(x: Tensor, dim: int | tuple[int, ...], keepdim: bool = False) -> Tensor: + return tau * _pareto(-x / tau, alpha=alpha).sum(dim=dim, keepdim=keepdim).log() + + return _inf_max_helper(max_fun=max_fun, x=x, dim=dim, keepdim=keepdim)
+ + + +
+[docs] +def fatmin( + x: Tensor, + dim: int | tuple[int, ...], + keepdim: bool = False, + tau: float | Tensor = TAU, + alpha: float = ALPHA, +) -> Tensor: + """Computes a smooth approximation to amin(X, dim=dim) with a fat tail. + + Args: + X: A Tensor from which to compute the smoothed minimum. + dim: The dimensions to reduce over. + keepdim: If True, keeps the reduced dimensions. + tau: Temperature parameter controlling the smooth approximation + to min operator, becomes tighter as tau goes to 0. Needs to be positive. + alpha: The exponent of the asymptotic power decay of the approximation. The + default value is 2. Higher alpha parameters make the function behave more + similarly to the standard logsumexp approximation to the max, so it is + recommended to keep this value low or moderate, e.g. < 10. + + Returns: + A Tensor of smooth approximations to `amin(X, dim=dim)` with a fat tail. + """ + return -fatmax(-x, dim=dim, keepdim=keepdim, tau=tau, alpha=alpha)
+ + + +
+[docs] +def fatmaximum( + a: Tensor, b: Tensor, tau: float | Tensor = TAU, alpha: float = ALPHA +) -> Tensor: + """Computes a smooth approximation to torch.maximum(a, b) with a fat tail. + + Args: + a: The first Tensor from which to compute the smoothed component-wise maximum. + b: The second Tensor from which to compute the smoothed component-wise maximum. + tau: Temperature parameter controlling the smoothness of the approximation. A + smaller tau corresponds to a tighter approximation that leads to a sharper + objective landscape that might be more difficult to optimize. + + Returns: + A smooth approximation of torch.maximum(a, b). + """ + return fatmax( + torch.stack(torch.broadcast_tensors(a, b), dim=-1), + dim=-1, + keepdim=False, + tau=tau, + )
+ + + +
+[docs] +def fatminimum( + a: Tensor, b: Tensor, tau: float | Tensor = TAU, alpha: float = ALPHA +) -> Tensor: + """Computes a smooth approximation to torch.minimum(a, b) with a fat tail. + + Args: + a: The first Tensor from which to compute the smoothed component-wise minimum. + b: The second Tensor from which to compute the smoothed component-wise minimum. + tau: Temperature parameter controlling the smoothness of the approximation. A + smaller tau corresponds to a tighter approximation that leads to a sharper + objective landscape that might be more difficult to optimize. + + Returns: + A smooth approximation of torch.minimum(a, b). + """ + return -fatmaximum(-a, -b, tau=tau, alpha=alpha)
+ + + +
+[docs] +def log_fatmoid(X: Tensor, tau: float | Tensor = 1.0) -> Tensor: + """Computes the logarithm of the fatmoid. Separated out in case the implementation + of the logarithm becomes more complex in the future to ensure numerical stability. + """ + return fatmoid(X, tau=tau).log()
+ + + +
+[docs] +def fatmoid(X: Tensor, tau: float | Tensor = 1.0) -> Tensor: + """Computes a twice continuously differentiable approximation to the Heaviside + step function with a fat tail, i.e. `O(1 / x^2)` as `x` goes to -inf. + + Args: + X: A Tensor from which to compute the smoothed step function. + tau: Temperature parameter controlling the smoothness of the approximation. + + Returns: + A tensor of fat-tailed approximations to the Heaviside step function. + """ + X = X / tau + m = _inv_sqrt_3 # this defines the inflection point + return torch.where( + X < 0, + 2 / 3 * cauchy(X - m), + 1 - 2 / 3 * cauchy(X + m), + )
+ + + +
+[docs] +def cauchy(x: Tensor) -> Tensor: + """Computes a Lorentzian, i.e. an un-normalized Cauchy density function.""" + return 1 / (1 + x.square())
+ + + +def _pareto(x: Tensor, alpha: float, check: bool = True) -> Tensor: + """Computes a rational polynomial that is + 1) monotonically decreasing for `x > 0`, + 2) is equal to 1 at `x = 0`, + 3) has a first and second derivative of 1 at `x = 0`, and + 4) has an asymptotic decay of `O(1 / x^alpha)`. + These properties make it possible to use the function to define a smooth and + fat-tailed approximation to the maximum, which enables better gradient propagation, + see `fatmax` for details. + + Args: + x: The input tensor. + alpha: The exponent of the asymptotic decay. + check: Whether to check if the input tensor only contains non-negative values. + + Returns: + The tensor corresponding to the rational polynomial with the stated properties. + """ + if check and (x < 0).any(): + raise ValueError("Argument `x` must be non-negative.") + alpha = alpha / 2 # so that alpha stands for the power decay + # choosing beta_0, beta_1 so that first and second derivatives at x = 0 are 1. + beta_1 = 2 * alpha + beta_0 = alpha * beta_1 + return (beta_0 / (beta_0 + beta_1 * x + x.square())).pow(alpha) + + +
+[docs] +def sigmoid(X: Tensor, log: bool = False, fat: bool = False) -> Tensor: + """A sigmoid function with an optional fat tail and evaluation in log space for + better numerical behavior. Notably, the fat-tailed sigmoid can be used to remedy + numerical underflow problems in the value and gradient of the canonical sigmoid. + + Args: + X: The Tensor on which to evaluate the sigmoid. + log: Toggles the evaluation of the log sigmoid. + fat: Toggles the evaluation of the fat-tailed sigmoid. + + Returns: + A Tensor of (log-)sigmoid values. + """ + Y = log_fatmoid(X) if fat else logexpit(X) + return Y if log else Y.exp()
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/sampling.html b/website-old/pages/api/_modules/botorch/utils/sampling.html new file mode 100644 index 0000000000..ce81421ce9 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/sampling.html @@ -0,0 +1,1187 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.sampling

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Utilities for MC and qMC sampling.
+
+References
+
+.. [Trikalinos2014polytope]
+    T. A. Trikalinos and G. van Valkenhoef. Efficient sampling from uniform
+    density n-polytopes. Technical report, Brown University, 2014.
+"""
+
+from __future__ import annotations
+
+import warnings
+
+from abc import ABC, abstractmethod
+from collections.abc import Callable, Generator, Iterable
+from contextlib import contextmanager
+from typing import TYPE_CHECKING
+
+import numpy as np
+import numpy.typing as npt
+import scipy
+import torch
+from botorch.exceptions.errors import BotorchError
+from botorch.exceptions.warnings import UserInputWarning
+from botorch.sampling.qmc import NormalQMCEngine
+from botorch.utils.transforms import unnormalize
+from scipy.spatial import Delaunay, HalfspaceIntersection
+from torch import LongTensor, Tensor
+from torch.quasirandom import SobolEngine
+
+
+if TYPE_CHECKING:
+    from botorch.models.deterministic import (  # pragma: no cover
+        GenericDeterministicModel,
+    )
+
+
+
+[docs] +@contextmanager +def manual_seed(seed: int | None = None) -> Generator[None, None, None]: + r"""Contextmanager for manual setting the torch.random seed. + + Args: + seed: The seed to set the random number generator to. + + Returns: + Generator + + Example: + >>> with manual_seed(1234): + >>> X = torch.rand(3) + """ + old_state = torch.random.get_rng_state() + try: + if seed is not None: + torch.random.manual_seed(seed) + yield + finally: + if seed is not None: + torch.random.set_rng_state(old_state)
+ + + +
+[docs] +def draw_sobol_samples( + bounds: Tensor, + n: int, + q: int, + batch_shape: Iterable[int] | torch.Size | None = None, + seed: int | None = None, +) -> Tensor: + r"""Draw qMC samples from the box defined by bounds. + + Args: + bounds: A `2 x d` dimensional tensor specifying box constraints on a + `d`-dimensional space, where bounds[0, :] and bounds[1, :] correspond + to lower and upper bounds, respectively. + n: The number of (q-batch) samples. As a best practice, use powers of 2. + q: The size of each q-batch. + batch_shape: The batch shape of the samples. If given, returns samples + of shape `n x batch_shape x q x d`, where each batch is an + `n x q x d`-dim tensor of qMC samples. + seed: The seed used for initializing Owen scrambling. If None (default), + use a random seed. + + Returns: + A `n x batch_shape x q x d`-dim tensor of qMC samples from the box + defined by bounds. + + Example: + >>> bounds = torch.stack([torch.zeros(3), torch.ones(3)]) + >>> samples = draw_sobol_samples(bounds, 16, 2) + """ + batch_shape = batch_shape or torch.Size() + batch_size = int(torch.prod(torch.tensor(batch_shape))) + d = bounds.shape[-1] + lower = bounds[0] + rng = bounds[1] - bounds[0] + sobol_engine = SobolEngine(q * d, scramble=True, seed=seed) + samples_raw = sobol_engine.draw(batch_size * n, dtype=lower.dtype) + samples_raw = samples_raw.view(*batch_shape, n, q, d).to(device=lower.device) + if batch_shape != torch.Size(): + samples_raw = samples_raw.permute(-3, *range(len(batch_shape)), -2, -1) + return lower + rng * samples_raw
+ + + +
+[docs] +def draw_sobol_normal_samples( + d: int, + n: int, + device: torch.device | None = None, + dtype: torch.dtype | None = None, + seed: int | None = None, +) -> Tensor: + r"""Draw qMC samples from a multi-variate standard normal N(0, I_d). + + A primary use-case for this functionality is to compute an QMC average + of f(X) over X where each element of X is drawn N(0, 1). + + Args: + d: The dimension of the normal distribution. + n: The number of samples to return. As a best practice, use powers of 2. + device: The torch device. + dtype: The torch dtype. + seed: The seed used for initializing Owen scrambling. If None (default), + use a random seed. + + Returns: + A tensor of qMC standard normal samples with dimension `n x d` with device + and dtype specified by the input. + + Example: + >>> samples = draw_sobol_normal_samples(2, 16) + """ + normal_qmc_engine = NormalQMCEngine(d=d, seed=seed, inv_transform=True) + samples = normal_qmc_engine.draw(n, dtype=dtype) + return samples.to(device=device)
+ + + +
+[docs] +def sample_hypersphere( + d: int, + n: int = 1, + qmc: bool = False, + seed: int | None = None, + device: torch.device | None = None, + dtype: torch.dtype | None = None, +) -> Tensor: + r"""Sample uniformly from a unit d-sphere. + + Args: + d: The dimension of the hypersphere. + n: The number of samples to return. + qmc: If True, use QMC Sobol sampling (instead of i.i.d. uniform). + seed: If provided, use as a seed for the RNG. + device: The torch device. + dtype: The torch dtype. + + Returns: + An `n x d` tensor of uniform samples from from the d-hypersphere. + + Example: + >>> sample_hypersphere(d=5, n=10) + """ + if d == 1: + rnd = torch.randint(0, 2, (n, 1), device=device, dtype=dtype) + return 2 * rnd - 1 + if qmc: + rnd = draw_sobol_normal_samples(d=d, n=n, device=device, dtype=dtype, seed=seed) + else: + with manual_seed(seed=seed): + rnd = torch.randn(n, d, dtype=dtype) + samples = rnd / torch.linalg.norm(rnd, dim=-1, keepdim=True) + if device is not None: + samples = samples.to(device) + return samples
+ + + +
+[docs] +def sample_simplex( + d: int, + n: int = 1, + qmc: bool = False, + seed: int | None = None, + device: torch.device | None = None, + dtype: torch.dtype | None = None, +) -> Tensor: + r"""Sample uniformly from a d-simplex. + + Args: + d: The dimension of the simplex. + n: The number of samples to return. + qmc: If True, use QMC Sobol sampling (instead of i.i.d. uniform). + seed: If provided, use as a seed for the RNG. + device: The torch device. + dtype: The torch dtype. + + Returns: + An `n x d` tensor of uniform samples from from the d-simplex. + + Example: + >>> sample_simplex(d=3, n=10) + """ + if d == 1: + return torch.ones(n, 1, device=device, dtype=dtype) + if qmc: + sobol_engine = SobolEngine(d - 1, scramble=True, seed=seed) + rnd = sobol_engine.draw(n, dtype=dtype) + else: + with manual_seed(seed=seed): + rnd = torch.rand(n, d - 1, dtype=dtype) + srnd, _ = torch.sort(rnd, dim=-1) + zeros = torch.zeros(n, 1, dtype=dtype) + ones = torch.ones(n, 1, dtype=dtype) + srnd = torch.cat([zeros, srnd, ones], dim=-1) + if device is not None: + srnd = srnd.to(device) + return srnd[..., 1:] - srnd[..., :-1]
+ + + +
+[docs] +def sample_polytope( + A: Tensor, + b: Tensor, + x0: Tensor, + n: int = 10000, + n0: int = 100, + n_thinning: int = 1, + seed: int | None = None, +) -> Tensor: + r""" + Hit and run sampler from uniform sampling points from a polytope, + described via inequality constraints A*x<=b. + + Args: + A: A `m x d`-dim Tensor describing inequality constraints + so that all samples satisfy `Ax <= b`. + b: A `m`-dim Tensor describing the inequality constraints + so that all samples satisfy `Ax <= b`. + x0: A `d`-dim Tensor representing a starting point of the chain + satisfying the constraints. + n: The number of resulting samples kept in the output. + n0: The number of burn-in samples. The chain will produce + n+n0 samples but the first n0 samples are not saved. + n_thinning: The amount of thinnning. This function will return every + `n_thinning`-th sample from the chain (after burn-in). + seed: The seed for the sampler. If omitted, use a random seed. + + Returns: + (n, d) dim Tensor containing the resulting samples. + """ + # Check that starting point satisfies the constraints. + if not ((slack := A @ x0 - b) <= 0).all(): + raise ValueError( + f"Starting point does not satisfy the constraints. Inputs: {A=}," + f"{b=}, {x0=}, A@x0-b={slack}." + ) + # Remove rows where all elements of A are 0. This avoids nan and infs later. + # A may have zero rows in it when this is called from PolytopeSampler + # with equality constraints (which are absorbed into A & b). + non_zero_rows = torch.any(A != 0, dim=-1) + A = A[non_zero_rows] + b = b[non_zero_rows] + + n_tot = n0 + n * n_thinning + seed = seed if seed is not None else torch.randint(0, 1000000, (1,)).item() + with manual_seed(seed=seed): + rands = torch.rand(n_tot, dtype=A.dtype, device=A.device) + + # Sample uniformly from unit hypersphere in d dims. + # Increment seed by +1 to avoid correlation with step size, see #2156 for details. + Rs = sample_hypersphere( + d=x0.shape[0], n=n_tot, dtype=A.dtype, device=A.device, seed=seed + 1 + ).unsqueeze(-1) + + # Use batch operations for matrix multiplication. + ARs = (A @ Rs).squeeze(-1) + out = torch.empty(n, A.size(-1), dtype=A.dtype, device=A.device) + x = x0.clone() + large_constant = torch.finfo().max + for i, (ar, r, rnd) in enumerate(zip(ARs, Rs, rands)): + # Given x, the next point in the chain is x+alpha*r. + # It must satisfy A(x+alpha*r)<=b, which implies A*alpha*r<=b-Ax, + # so alpha<=(b-Ax)/ar for ar>0, and alpha>=(b-Ax)/ar for ar<0. + # If x is at the boundary, b - Ax = 0. If ar > 0, then we must + # have alpha <= 0. If ar < 0, we must have alpha >= 0. + # ar == 0 is an unlikely event that provides no signal. + # b - A @ x is always >= 0, clamping for numerical tolerances. + w = (b - A @ x).squeeze().clamp(min=0.0) / ar + # Find upper bound for alpha. If there are no constraints on + # the upper bound of alpha, set it to a large value. + pos = w > 0 + alpha_max = w[pos].min().item() if pos.any() else large_constant + # Find lower bound for alpha. + neg = w < 0 + alpha_min = w[neg].max().item() if neg.any() else -large_constant + # Handle the boundary case. + if (w_eq_0 := (w == 0)).any(): + # If ar > 0 at the boundary, alpha <= 0. + if w_eq_0.logical_and(ar > 0).any(): + alpha_max = min(alpha_max, 0.0) + # If ar < 0 at the boundary, alpha >= 0. + if w_eq_0.logical_and(ar < 0).any(): + alpha_min = max(alpha_min, 0.0) + # alpha ~ Uniform[alpha_min, alpha_max] + alpha = alpha_min + rnd * (alpha_max - alpha_min) + x = x + alpha * r + if (k := i - n0) >= 0: # save samples after burn-in period + idx, rem = divmod(k, n_thinning) + if rem == 0: + out[idx] = x.squeeze() + return out
+ + + +
+[docs] +def batched_multinomial( + weights: Tensor, + num_samples: int, + replacement: bool = False, + generator: torch.Generator | None = None, + out: Tensor | None = None, +) -> LongTensor: + r"""Sample from multinomial with an arbitrary number of batch dimensions. + + Args: + weights: A `batch_shape x num_categories` tensor of weights. For each batch + index `i, j, ...`, this functions samples from a multinomial with `input` + `weights[i, j, ..., :]`. Note that the weights need not sum to one, but must + be non-negative, finite and have a non-zero sum. + num_samples: The number of samples to draw for each batch index. Must be smaller + than `num_categories` if `replacement=False`. + replacement: If True, samples are drawn with replacement. + generator: A a pseudorandom number generator for sampling. + out: The output tensor (optional). If provided, must be of size + `batch_shape x num_samples`. + + Returns: + A `batch_shape x num_samples` tensor of samples. + + This is a thin wrapper around `torch.multinomial` that allows weight (`input`) + tensors with an arbitrary number of batch dimensions (`torch.multinomial` only + allows a single batch dimension). The calling signature is the same as for + `torch.multinomial`. + + Example: + >>> weights = torch.rand(2, 3, 10) + >>> samples = batched_multinomial(weights, 4) # shape is 2 x 3 x 4 + """ + batch_shape, n_categories = weights.shape[:-1], weights.size(-1) + flat_samples = torch.multinomial( + input=weights.view(-1, n_categories), + num_samples=num_samples, + replacement=replacement, + generator=generator, + out=None if out is None else out.view(-1, num_samples), + ) + return flat_samples.view(*batch_shape, num_samples)
+ + + +def _convert_bounds_to_inequality_constraints(bounds: Tensor) -> tuple[Tensor, Tensor]: + r"""Convert bounds into inequality constraints of the form Ax <= b. + + Args: + bounds: A `2 x d`-dim tensor of bounds + + Returns: + A two-element tuple containing + - A: A `2d x d`-dim tensor of coefficients + - b: A `2d x 1`-dim tensor containing the right hand side + """ + d = bounds.shape[-1] + eye = torch.eye(d, dtype=bounds.dtype, device=bounds.device) + lower, upper = bounds + lower_finite, upper_finite = bounds.isfinite() + A = torch.cat([-eye[lower_finite], eye[upper_finite]], dim=0) + b = torch.cat([-lower[lower_finite], upper[upper_finite]], dim=0).unsqueeze(-1) + return A, b + + +
+[docs] +def find_interior_point( + A: npt.NDArray, + b: npt.NDArray, + A_eq: npt.NDArray | None = None, + b_eq: npt.NDArray | None = None, +) -> npt.NDArray: + r"""Find an interior point of a polytope via linear programming. + + Args: + A: A `n_ineq x d`-dim numpy array containing the coefficients of the + constraint inequalities. + b: A `n_ineq x 1`-dim numpy array containing the right hand sides of + the constraint inequalities. + A_eq: A `n_eq x d`-dim numpy array containing the coefficients of the + constraint equalities. + b_eq: A `n_eq x 1`-dim numpy array containing the right hand sides of + the constraint equalities. + + Returns: + A `d`-dim numpy array containing an interior point of the polytope. + This function will raise a ValueError if there is no such point. + + This method solves the following Linear Program: + + min -s subject to A @ x <= b - 2 * s, s >= 0, A_eq @ x = b_eq + + In case the polytope is unbounded, then it will also constrain the slack + variable `s` to `s<=1`. + """ + # augment inequality constraints: A @ (x, s) <= b + d = A.shape[-1] + ncon = A.shape[-2] + 1 + c = np.zeros(d + 1) + c[-1] = -1 + b_ub = np.zeros(ncon) + b_ub[:-1] = b.reshape(-1) + A_ub = np.zeros((ncon, d + 1)) + A_ub[:-1, :-1] = A + A_ub[:-1, -1] = 2.0 + A_ub[-1, -1] = -1.0 + + result = scipy.optimize.linprog( + c=c, + A_ub=A_ub, + b_ub=b_ub, + A_eq=A_eq, + b_eq=b_eq, + bounds=(None, None), + method="highs", + ) + + if result.status == 3: + # problem is unbounded - to find a bounded solution we constrain the + # slack variable `s` to `s <= 1.0`. + A_s = np.concatenate([np.zeros((1, d)), np.ones((1, 1))], axis=-1) + A_ub = np.concatenate([A_ub, A_s], axis=0) + b_ub = np.concatenate([b_ub, np.ones(1)], axis=-1) + result = scipy.optimize.linprog( + c=c, + A_ub=A_ub, + b_ub=b_ub, + A_eq=A_eq, + b_eq=b_eq, + bounds=(None, None), + method="highs", + ) + + if result.status == 2: + raise ValueError( + "No feasible point found. Constraint polytope appears empty. " + + "Check your constraints." + ) + elif result.status > 0: + raise ValueError( + "Problem checking constraint specification. " + + f"linprog status: {result.message}" + ) + # the x in the result is really (x, s) + return result.x[:-1]
+ + + +
+[docs] +class PolytopeSampler(ABC): + """Base class for samplers that sample points from a polytope.""" + + def __init__( + self, + inequality_constraints: tuple[Tensor, Tensor] | None = None, + equality_constraints: tuple[Tensor, Tensor] | None = None, + bounds: Tensor | None = None, + interior_point: Tensor | None = None, + ) -> None: + r""" + Args: + inequality_constraints: Tensors `(A, b)` describing inequality + constraints `A @ x <= b`, where `A` is a `n_ineq_con x d`-dim + Tensor and `b` is a `n_ineq_con x 1`-dim Tensor, with `n_ineq_con` + the number of inequalities and `d` the dimension of the sample space. + equality_constraints: Tensors `(C, d)` describing the equality constraints + `C @ x = d`, where `C` is a `n_eq_con x d`-dim Tensor and `d` is a + `n_eq_con x 1`-dim Tensor with `n_eq_con` the number of equalities. + bounds: A `2 x d`-dim tensor of box bounds, where `inf` (`-inf`) means + that the respective dimension is unbounded above (below). + interior_point: A `d x 1`-dim Tensor presenting a point in the + (relative) interior of the polytope. If omitted, determined + automatically by solving a Linear Program. + """ + if inequality_constraints is None: + if bounds is None: + raise BotorchError( + "PolytopeSampler requires either inequality constraints or bounds." + ) + A = torch.empty( + 0, bounds.shape[-1], dtype=bounds.dtype, device=bounds.device + ) + b = torch.empty(0, 1, dtype=bounds.dtype, device=bounds.device) + else: + A, b = inequality_constraints + if bounds is not None: + # add inequality constraints for bounds + # TODO: make sure there are not deduplicate constraints + A2, b2 = _convert_bounds_to_inequality_constraints(bounds=bounds) + A = torch.cat([A, A2], dim=0) + b = torch.cat([b, b2], dim=0) + self.A = A + self.b = b + self.equality_constraints = equality_constraints + + if equality_constraints is not None: + self.C, self.d = equality_constraints + U, S, Vh = torch.linalg.svd(self.C) + r = torch.nonzero(S).size(0) # rank of matrix C + self.nullC = Vh[r:, :].transpose(-1, -2) # orthonormal null space of C, + # satisfying # C @ nullC = 0 and nullC.T @ nullC = I + # using the change of variables x=x0+nullC*y, + # sample y satisfies A*nullC*y<=b-A*x0. + # the linear constraint is automatically satisfied as x0 satisfies it. + else: + self.C = None + self.d = None + self.nullC = torch.eye( + self.A.size(-1), dtype=self.A.dtype, device=self.A.device + ) + + self.new_A = self.A @ self.nullC # doesn't depend on the initial point + + # initial point for the original, not transformed, problem + if interior_point is not None: + if self.feasible(interior_point): + self.x0 = interior_point + else: + raise ValueError("The given input point is not feasible.") + else: + self.x0 = self.find_interior_point() + +
+[docs] + def feasible(self, x: Tensor) -> bool: + r"""Check whether a point is contained in the polytope. + + Args: + x: A `d x 1`-dim Tensor. + + Returns: + True if `x` is contained inside the polytope (incl. its boundary), + False otherwise. + """ + ineq = (self.A @ x - self.b <= 0).all() + if self.equality_constraints is not None: + eq = (self.C @ x - self.d == 0).all() + return ineq & eq + return ineq
+ + +
+[docs] + def find_interior_point(self) -> Tensor: + r"""Find an interior point of the polytope. + + Returns: + A `d x 1`-dim Tensor representing a point contained in the polytope. + This function will raise a ValueError if there is no such point. + """ + if self.equality_constraints: + # equality constraints: A_eq * (x, s) = b_eq + A_eq = np.zeros((self.C.size(0), self.C.size(-1) + 1)) + A_eq[:, :-1] = self.C.cpu().numpy() + b_eq = self.d.cpu().numpy() + else: + A_eq = None + b_eq = None + x0 = find_interior_point( + A=self.A.cpu().numpy(), b=self.b.cpu().numpy(), A_eq=A_eq, b_eq=b_eq + ) + return torch.from_numpy(x0).to(self.A).unsqueeze(-1)
+ + + # -------- Abstract methods to be implemented by subclasses -------- # + +
+[docs] + @abstractmethod + def draw(self, n: int = 1) -> Tensor: + r"""Draw samples from the polytope. + + Args: + n: The number of samples. + + Returns: + A `n x d` Tensor of samples from the polytope. + """ + pass # pragma: no cover
+
+ + + +
+[docs] +class HitAndRunPolytopeSampler(PolytopeSampler): + r"""A sampler for sampling from a polyope using a hit-and-run algorithm.""" + + def __init__( + self, + inequality_constraints: tuple[Tensor, Tensor] | None = None, + equality_constraints: tuple[Tensor, Tensor] | None = None, + bounds: Tensor | None = None, + interior_point: Tensor | None = None, + n_burnin: int = 200, + n_thinning: int = 20, + seed: int | None = None, + ) -> None: + r"""A sampler for sampling from a polyope using a hit-and-run algorithm. + + Args: + inequality_constraints: Tensors `(A, b)` describing inequality + constraints `A @ x <= b`, where `A` is a `n_ineq_con x d`-dim + Tensor and `b` is a `n_ineq_con x 1`-dim Tensor, with `n_ineq_con` + the number of inequalities and `d` the dimension of the sample space. + equality_constraints: Tensors `(C, d)` describing the equality constraints + `C @ x = d`, where `C` is a `n_eq_con x d`-dim Tensor and `d` is a + `n_eq_con x 1`-dim Tensor with `n_eq_con` the number of equalities. + bounds: A `2 x d`-dim tensor of box bounds, where `inf` (`-inf`) means + that the respective dimension is unbounded from above (below). If + omitted, no bounds (in addition to the above constraints) are applied. + interior_point: A `d x 1`-dim Tensor representing a point in the + (relative) interior of the polytope. If omitted, determined + automatically by solving a Linear Program. + n_burnin: The number of burn in samples. The sampler will discard + `n_burnin` samples before returning the first sample. + n_thinning: The amount of thinning. The sampler will return every + `n_thinning` sample (after burn-in). This may need to be increased + for sets of constraints that are difficult to satisfy (i.e. in which + case the volume of the constraint polytope is small relative to that + of its bounding box). + seed: The random seed. + """ + if inequality_constraints is None and bounds is None: + raise BotorchError( + "HitAndRunPolytopeSampler requires either inequality constraints " + "or bounds." + ) + # Normalize constraints to avoid the following issue: + # https://github.com/pytorch/botorch/issues/1225 + offset, scale = None, None + if inequality_constraints or equality_constraints: + if bounds is None: + warnings.warn( + "HitAndRunPolytopeSampler did not receive `bounds`, which can " + "lead to non-uniform sampling if the parameter ranges are very " + "different (see https://github.com/pytorch/botorch/issues/1225).", + UserInputWarning, + stacklevel=3, + ) + else: + if inequality_constraints: + inequality_constraints = normalize_dense_linear_constraints( + bounds=bounds, constraints=inequality_constraints + ) + if equality_constraints: + equality_constraints = normalize_dense_linear_constraints( + bounds=bounds, constraints=equality_constraints + ) + lower, upper = bounds + offset = lower + scale = upper - lower + if interior_point is not None: + # If provided, we also need to normalize the interior point + interior_point = (interior_point - offset[:, None]) / scale[:, None] + bounds = torch.zeros_like(bounds) + bounds[1, :] = 1.0 + + super().__init__( + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + bounds=bounds, + interior_point=interior_point, + ) + self.n_burnin: int = n_burnin + self.n_thinning: int = n_thinning + self.num_samples_generated: int = 0 + self._seed: int | None = seed + self._offset: Tensor | None = offset + self._scale: Tensor | None = scale + +
+[docs] + def draw(self, n: int = 1) -> Tensor: + r"""Draw samples from the polytope. + + Args: + n: The number of samples. + + Returns: + A `n x d` Tensor of samples from the polytope. + """ + # There are two layers of normalization. In the outer layer, the space + # has been normalized to the unit cube. In the inner layer, we remove + # any equality constraints and sample on the subspace defined by those + # equality constraints, with an additional shift to normalize the interior + # point to the origin. Below, after sampling in that inner layer, we have + # to reverse both layers of normalization. + transformed_samples = sample_polytope( + # Run this on the cpu since there is a lot of looping going on + A=self.new_A.cpu(), + b=(self.b - self.A @ self.x0).cpu(), + x0=torch.zeros( + (self.nullC.size(1), 1), dtype=self.A.dtype, device=torch.device("cpu") + ), + n=n, + n0=self.n_burnin if self.num_samples_generated == 0 else 0, + n_thinning=self.n_thinning, + seed=self._seed, + ).to(self.b) + # Update the seed for the next call in a deterministic fashion + if self._seed is not None: + self._seed += n + # Unnormalize the inner layer + init_shift = self.x0.transpose(-1, -2) + samples = init_shift + transformed_samples @ self.nullC.transpose(-1, -2) + # Keep the last element as the beginning of the next chain + self.x0 = samples[-1].reshape(-1, 1) + # Unnormalize the outer layer + if self._scale is not None: + samples = self._offset + self._scale * samples + self.num_samples_generated += n + return samples
+
+ + + +
+[docs] +class DelaunayPolytopeSampler(PolytopeSampler): + r"""A polytope sampler using Delaunay triangulation. + + This sampler first enumerates the vertices of the constraint polytope and + then uses a Delaunay triangulation to tesselate its convex hull. + + The sampling happens in two stages: + 1. First, we sample from the set of hypertriangles generated by the + Delaunay triangulation (i.e. which hyper-triangle to draw the sample + from) with probabilities proportional to the triangle volumes. + 2. Then, we sample uniformly from the chosen hypertriangle by sampling + uniformly from the unit simplex of the appropriate dimension, and + then computing the convex combination of the vertices of the + hypertriangle according to that draw from the simplex. + + The best reference (not exactly the same, but functionally equivalent) is + [Trikalinos2014polytope]_. A simple R implementation is available at + https://github.com/gertvv/tesselample. + """ + + def __init__( + self, + inequality_constraints: tuple[Tensor, Tensor] | None = None, + equality_constraints: tuple[Tensor, Tensor] | None = None, + bounds: Tensor | None = None, + interior_point: Tensor | None = None, + ) -> None: + r"""Initialize DelaunayPolytopeSampler. + + Args: + inequality_constraints: Tensors `(A, b)` describing inequality + constraints `A @ x <= b`, where `A` is a `n_ineq_con x d`-dim + Tensor and `b` is a `n_ineq_con x 1`-dim Tensor, with `n_ineq_con` + the number of inequalities and `d` the dimension of the sample space. + equality_constraints: Tensors `(C, d)` describing the equality constraints + `C @ x = d`, where `C` is a `n_eq_con x d`-dim Tensor and `d` is a + `n_eq_con x 1`-dim Tensor with `n_eq_con` the number of equalities. + bounds: A `2 x d`-dim tensor of box bounds, where `inf` (`-inf`) means + that the respective dimension is unbounded from above (below). + interior_point: A `d x 1`-dim Tensor representing a point in the + (relative) interior of the polytope. If omitted, determined + automatically by solving a Linear Program. + + Warning: The vertex enumeration performed in this algorithm can become + extremely costly if there are a large number of inequalities. Similarly, + the triangulation can get very expensive in high dimensions. Only use + this algorithm for moderate dimensions / moderately complex constraint sets. + An alternative is the `HitAndRunPolytopeSampler`. + """ + super().__init__( + inequality_constraints=inequality_constraints, + equality_constraints=equality_constraints, + bounds=bounds, + interior_point=interior_point, + ) + # shift coordinate system to be anchored at x0 + new_b = self.b - self.A @ self.x0 + if self.new_A.shape[-1] < 2: + # if the polytope is in dim 1 (i.e. a line segment) Qhull won't work + tshlds = new_b / self.new_A + neg = self.new_A < 0 + self.y_min = tshlds[neg].max() + self.y_max = tshlds[~neg].min() + self.dim = 1 + else: + # Qhull expects inputs of the form A @ x + b <= 0, so we need to negate here + halfspaces = torch.cat([self.new_A, -new_b], dim=-1).cpu().numpy() + vertices = HalfspaceIntersection( + halfspaces=halfspaces, interior_point=np.zeros(self.new_A.shape[-1]) + ).intersections + self.dim = vertices.shape[-1] + try: + delaunay = Delaunay(vertices) + except ValueError as e: + if "Points cannot contain NaN" in str(e): + raise ValueError("Polytope is unbounded.") + raise e # pragma: no cover + polytopes = torch.from_numpy( + np.array([delaunay.points[s] for s in delaunay.simplices]), + ).to(self.A) + volumes = torch.stack([torch.det(p[1:] - p[0]).abs() for p in polytopes]) + self._polytopes = polytopes + self._p = volumes / volumes.sum() + +
+[docs] + def draw(self, n: int = 1, seed: int | None = None) -> Tensor: + r"""Draw samples from the polytope. + + Args: + n: The number of samples. + seed: The random seed. + + Returns: + A `n x d` Tensor of samples from the polytope. + """ + if self.dim == 1: + with manual_seed(seed): + e = torch.rand(n, 1, device=self.new_A.device, dtype=self.new_A.dtype) + transformed_samples = self.y_min + (self.y_max - self.y_min) * e + else: + if seed is None: + generator = None + else: + generator = torch.Generator(device=self.A.device) + generator.manual_seed(seed) + index_rvs = torch.multinomial( + self._p, + num_samples=n, + replacement=True, + generator=generator, + ) + simplex_rvs = sample_simplex( + d=self.dim + 1, n=n, seed=seed, device=self.A.device, dtype=self.A.dtype + ) + transformed_samples = torch.stack( + [rv @ self._polytopes[idx] for rv, idx in zip(simplex_rvs, index_rvs)] + ) + init_shift = self.x0.transpose(-1, -2) + samples = init_shift + transformed_samples @ self.nullC.transpose(-1, -2) + return samples
+
+ + + +
+[docs] +def normalize_sparse_linear_constraints( + bounds: Tensor, constraints: list[tuple[Tensor, Tensor, float]] +) -> list[tuple[Tensor, Tensor, float]]: + r"""Normalize sparse linear constraints to the unit cube. + + Args: + bounds: A `2 x d`-dim tensor containing the box bounds. + constraints: A list of tuples (`indices`, `coefficients`, `rhs`), with + `indices` and `coefficients` one-dimensional tensors and `rhs` a + scalar, where each tuple encodes an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs` or + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + """ + new_constraints = [] + for index, coefficient, rhs in constraints: + if index.ndim != 1: + raise ValueError( + "`indices` must be a one-dimensional tensor. This method does not " + "support the kind of 'inter-point constraints' that are supported by " + "`optimize_acqf()`. To achieve this behavior, you need define the " + "problem on the joint space over `q` points and impose use constraints," + "see https://github.com/pytorch/botorch/issues/2468#issuecomment-2287706461" # noqa: E501 + ) + lower, upper = bounds[:, index] + s = upper - lower + new_constraints.append( + (index, s * coefficient, (rhs - torch.dot(coefficient, lower)).item()) + ) + return new_constraints
+ + + +
+[docs] +def normalize_dense_linear_constraints( + bounds: Tensor, + constraints: tuple[Tensor, Tensor], +) -> tuple[Tensor, Tensor]: + r"""Normalize dense linear constraints to the unit cube. + + Args: + bounds: A `2 x d`-dim tensor containing the box bounds. + constraints: A tensor tuple `(A, b)` describing constraints + `A @ x (<)= b`, where `A` is a `n_con x d`-dim Tensor and + `b` is a `n_con x 1`-dim Tensor, with `n_con` the number of + constraints and `d` the dimension of the sample space. + + Returns: + A tensor tuple `(A_nlz, b_nlz)` of normalized constraints. + """ + lower, upper = bounds + A, b = constraints + A_nlz = (upper - lower) * A + b_nlz = b - (A @ lower).unsqueeze(-1) + return A_nlz, b_nlz
+ + + +
+[docs] +def get_polytope_samples( + n: int, + bounds: Tensor, + inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None, + seed: int | None = None, + n_burnin: int = 10_000, + n_thinning: int = 32, +) -> Tensor: + r"""Sample from polytope defined by box bounds and (in)equality constraints. + + This uses a hit-and-run Markov chain sampler. + + NOTE: Much of the functionality of this method has been moved into + `HitAndRunPolytopeSampler`. If you want to repeatedly draw samples, you should + use `HitAndRunPolytopeSampler` directly in order to avoid repeatedly running + a burn-in of the chain. To do so, you need to convert the sparse constraint + format that `get_polytope_samples` expects to the dense constraint format that + `HitAndRunPolytopeSampler` expects. This can be done via the + `sparse_to_dense_constraints` method (but remember to adjust the constraint + from the `Ax >= b` format expecxted here to the `Ax <= b` format expected by + `PolytopeSampler` by multiplying both `A` and `b` by -1.) + + NOTE: This method does not support the kind of "inter-point constraints" that + are supported by `optimize_acqf()`. To achieve this behavior, you need define the + problem on the joint space over `q` points and impose use constraints, see: + https://github.com/pytorch/botorch/issues/2468#issuecomment-2287706461 + + Args: + n: The number of samples. + bounds: A `2 x d`-dim tensor containing the box bounds. + inequality_constraints: A list of tuples (`indices`, `coefficients`, `rhs`), + with `indices` and `coefficients` one-dimensional tensors and `rhs` a + scalar, where each tuple encodes an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. + equality_constraints: A list of tuples (`indices`, `coefficients`, `rhs`), + with `indices` and `coefficients` one-dimensional tensors and `rhs` a + scalar, where each tuple encodes an equality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + seed: The random seed. + n_burnin: The number of burn-in samples for the Markov chain sampler. + n_thinning: The amount of thinnning. This function will return every + `n_thinning`-th sample from the chain (after burn-in). + + Returns: + A `n x d`-dim tensor of samples. + """ + if inequality_constraints: + A, b = sparse_to_dense_constraints( + d=bounds.shape[-1], + constraints=inequality_constraints, + ) + # Note that the inequality constraints are of the form Ax >= b, + # but PolytopeSampler expects inequality constraints of the + # form Ax <= b, so we multiply by -1 below. + dense_inequality_constraints = (-A, -b) + else: + dense_inequality_constraints = None + if equality_constraints: + dense_equality_constraints = sparse_to_dense_constraints( + d=bounds.shape[-1], constraints=equality_constraints + ) + else: + dense_equality_constraints = None + polytope_sampler = HitAndRunPolytopeSampler( + bounds=bounds, + inequality_constraints=dense_inequality_constraints, + equality_constraints=dense_equality_constraints, + n_burnin=n_burnin, + n_thinning=n_thinning, + seed=seed, + ) + return polytope_sampler.draw(n=n)
+ + + +
+[docs] +def sparse_to_dense_constraints( + d: int, + constraints: list[tuple[Tensor, Tensor, float]], +) -> tuple[Tensor, Tensor]: + r"""Convert parameter constraints from a sparse format into a dense format. + + This method converts sparse triples of the form (indices, coefficients, rhs) + to constraints of the form Ax >= b or Ax = b. + + Args: + d: The input dimension. + constraints: A list of tuples (`indices`, `coefficients`, `rhs`), + with `indices` and `coefficients` one-dimensional tensors and `rhs` a + scalar, where each tuple encodes an (in)equality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs` or + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`. + + Returns: + A two-element tuple containing: + - A: A `n_constraints x d`-dim tensor of coefficients. + - b: A `n_constraints x 1`-dim tensor of right hand sides. + """ + _t = constraints[0][1] + A = torch.zeros(len(constraints), d, dtype=_t.dtype, device=_t.device) + b = torch.zeros(len(constraints), 1, dtype=_t.dtype, device=_t.device) + for i, (indices, coefficients, rhs) in enumerate(constraints): + A[i, indices.long()] = coefficients + b[i] = rhs + return A, b
+ + + +
+[docs] +def optimize_posterior_samples( + paths: GenericDeterministicModel, + bounds: Tensor, + raw_samples: int = 1024, + num_restarts: int = 20, + sample_transform: Callable[[Tensor], Tensor] | None = None, + return_transformed: bool = False, +) -> tuple[Tensor, Tensor]: + r"""Cheaply maximizes posterior samples by random querying followed by + gradient-based optimization using SciPy's L-BFGS-B routine. + + Args: + paths: Random Fourier Feature-based sample paths from the GP + bounds: The bounds on the search space. + raw_samples: The number of samples with which to query the samples initially. + num_restarts: The number of points selected for gradient-based optimization. + sample_transform: A callable transform of the sample outputs (e.g. + MCAcquisitionObjective or ScalarizedPosteriorTransform.evaluate) used to + negate the objective or otherwise transform the output. + return_transformed: A boolean indicating whether to return the transformed + or non-transformed samples. + + Returns: + A two-element tuple containing: + - X_opt: A `num_optima x [batch_size] x d`-dim tensor of optimal inputs x*. + - f_opt: A `num_optima x [batch_size] x m`-dim, optionally + `num_optima x [batch_size] x 1`-dim, tensor of optimal outputs f*. + """ + + def path_func(x) -> Tensor: + res = paths(x) + if sample_transform: + res = sample_transform(res) + + return res.squeeze(-1) + + candidate_set = unnormalize( + SobolEngine(dimension=bounds.shape[1], scramble=True).draw(n=raw_samples), + bounds=bounds, + ) + # queries all samples on all candidates - output shape + # raw_samples * num_optima * num_models + candidate_queries = path_func(candidate_set) + argtop_k = torch.topk(candidate_queries, num_restarts, dim=-1).indices + X_top_k = candidate_set[argtop_k, :] + + # to avoid circular import, the import occurs here + from botorch.generation.gen import gen_candidates_scipy + + X_top_k, f_top_k = gen_candidates_scipy( + X_top_k, + path_func, + lower_bounds=bounds[0], + upper_bounds=bounds[1], + ) + f_opt, arg_opt = f_top_k.max(dim=-1, keepdim=True) + + # For each sample (and possibly for every model in the batch of models), this + # retrieves the argmax. We flatten, pick out the indices and then reshape to + # the original batch shapes (so instead of pickig out the argmax of a + # (3, 7, num_restarts, D)) along the num_restarts dim, we pick it out of a + # (21, num_restarts, D) + final_shape = candidate_queries.shape[:-1] + X_opt = X_top_k.reshape(final_shape.numel(), num_restarts, -1)[ + torch.arange(final_shape.numel()), arg_opt.flatten() + ].reshape(*final_shape, -1) + + # if we return transformed, we do not need to pass the samples through paths + # paths a second time but rather just return the transformed optimal values + if return_transformed: + return X_opt, f_opt + + f_opt = paths(X_opt.unsqueeze(-2)).squeeze(-2) + return X_opt, f_opt
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/test_helpers.html b/website-old/pages/api/_modules/botorch/utils/test_helpers.html new file mode 100644 index 0000000000..20c29c4657 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/test_helpers.html @@ -0,0 +1,443 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.test_helpers

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+"""
+Dummy classes and other helpers that are used in multiple test files
+should be defined here to avoid relative imports.
+"""
+
+from __future__ import annotations
+
+import math
+from typing import Any
+
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.errors import UnsupportedError
+from botorch.models import SingleTaskGP
+from botorch.models.fully_bayesian import SaasFullyBayesianSingleTaskGP
+from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel, GPyTorchModel
+from botorch.models.model import FantasizeMixin, Model
+from botorch.models.model_list_gp_regression import ModelListGP
+from botorch.models.transforms.outcome import Standardize
+from botorch.models.utils import add_output_dim
+from botorch.models.utils.assorted import fantasize
+from botorch.posteriors.torch import TorchPosterior
+from botorch.utils.datasets import MultiTaskDataset, SupervisedDataset
+from gpytorch.distributions.multivariate_normal import MultivariateNormal
+from gpytorch.kernels import RBFKernel, ScaleKernel
+from gpytorch.likelihoods.gaussian_likelihood import (
+    FixedNoiseGaussianLikelihood,
+    GaussianLikelihood,
+)
+from gpytorch.means import ConstantMean
+from gpytorch.models.exact_gp import ExactGP
+from torch import Size, Tensor
+from torch.nn.functional import pad
+
+
+def _get_mcmc_samples(num_samples: int, dim: int, infer_noise: bool, **tkwargs):
+    mcmc_samples = {
+        "lengthscale": 1 + torch.rand(num_samples, 1, dim, **tkwargs),
+        "outputscale": 1 + torch.rand(num_samples, **tkwargs),
+        "mean": torch.randn(num_samples, **tkwargs),
+    }
+    if infer_noise:
+        mcmc_samples["noise"] = torch.rand(num_samples, 1, **tkwargs)
+    mcmc_samples["lengthscale"] = mcmc_samples["lengthscale"]
+
+    return mcmc_samples
+
+
+
+[docs] +def get_model( + train_X: Tensor, + train_Y: Tensor, + standardize_model: bool = False, + use_model_list: bool = False, +) -> SingleTaskGP | ModelListGP: + num_objectives = train_Y.shape[-1] + + if standardize_model: + if use_model_list: + outcome_transform = Standardize(m=1) + else: + outcome_transform = Standardize(m=num_objectives) + else: + outcome_transform = None + + if use_model_list: + model = ModelListGP( + *[ + SingleTaskGP( + train_X=train_X, + train_Y=train_Y[:, i : i + 1], + outcome_transform=outcome_transform, + ) + for i in range(num_objectives) + ] + ) + else: + model = SingleTaskGP( + train_X=train_X, + train_Y=train_Y, + outcome_transform=outcome_transform, + ) + + return model
+ + + +
+[docs] +def get_fully_bayesian_model( + train_X: Tensor, + train_Y: Tensor, + num_models: int, + standardize_model: bool, + infer_noise: bool, + **tkwargs: Any, +) -> SaasFullyBayesianSingleTaskGP: + num_objectives = train_Y.shape[-1] + + if standardize_model: + outcome_transform = Standardize(m=num_objectives) + else: + outcome_transform = None + mcmc_samples = _get_mcmc_samples( + num_samples=num_models, + dim=train_X.shape[-1], + infer_noise=infer_noise, + **tkwargs, + ) + train_Yvar = None if infer_noise else torch.full_like(train_Y, 0.01) + + model = SaasFullyBayesianSingleTaskGP( + train_X=train_X, + train_Y=train_Y, + train_Yvar=train_Yvar, + outcome_transform=outcome_transform, + ) + model.load_mcmc_samples(mcmc_samples) + + return model
+ + + +
+[docs] +def get_fully_bayesian_model_list( + train_X: Tensor, + train_Y: Tensor, + num_models: int, + standardize_model: bool, + infer_noise: bool, + **tkwargs: Any, +) -> ModelListGP: + model = ModelListGP( + *[ + get_fully_bayesian_model( + train_X, train_Y, num_models, standardize_model, infer_noise, **tkwargs + ) + for _ in range(2) + ] + ) + return model
+ + + +
+[docs] +def get_sample_moments(samples: Tensor, sample_shape: Size) -> tuple[Tensor, Tensor]: + """Computes the mean and covariance of a set of samples. + + Args: + samples: A tensor of shape `sample_shape x batch_shape x q`. + sample_shape: The sample_shape input used while generating the samples using + the pathwise sampling API. + """ + sample_dim = len(sample_shape) + samples = samples.view(-1, *samples.shape[sample_dim:]) + loc = samples.mean(dim=0) + residuals = (samples - loc).permute(*range(1, samples.ndim), 0) + return loc, (residuals @ residuals.transpose(-2, -1)) / sample_shape.numel()
+ + + +
+[docs] +def standardize_moments( + transform: Standardize, + loc: Tensor, + covariance_matrix: Tensor, +) -> tuple[Tensor, Tensor]: + """Standardizes the loc and covariance_matrix using the mean and standard + deviations from a Standardize transform. + """ + m = transform.means.squeeze().unsqueeze(-1) + s = transform.stdvs.squeeze().reciprocal().unsqueeze(-1) + loc = s * (loc - m) + correlation_matrix = s.unsqueeze(-1) * covariance_matrix * s.unsqueeze(-2) + return loc, correlation_matrix
+ + + +
+[docs] +def gen_multi_task_dataset( + yvar: float | None = None, + task_values: list[int] | None = None, + skip_task_features_in_datasets: bool = False, + **tkwargs, +) -> tuple[MultiTaskDataset, tuple[Tensor, Tensor, Tensor | None]]: + """Constructs a multi-task dataset with two tasks, each with 10 data points. + + Args: + yvar: The noise level to use for `train_Yvar`. If None, uses `train_Yvar=None`. + task_values: The values of the task features. If None, uses [0, 1]. + skip_task_features_in_datasets: If True, the task features are not included in + Xs of the datasets used to construct the datasets. This is useful for + testing `MultiTaskDataset`. + """ + if task_values is not None and skip_task_features_in_datasets: + raise UnsupportedError( # pragma: no cover + "`task_values` and `skip_task_features_in_datasets` can't be used together." + ) + X = torch.linspace(0, 0.95, 10, **tkwargs) + 0.05 * torch.rand(10, **tkwargs) + X = X.unsqueeze(dim=-1) + Y1 = torch.sin(X * (2 * math.pi)) + torch.randn_like(X) * 0.2 + Y2 = torch.cos(X * (2 * math.pi)) + torch.randn_like(X) * 0.2 + if task_values is None: + task_values = [0, 1] + train_X = torch.cat([pad(X, (1, 0), value=i) for i in task_values]) + train_Y = torch.cat([Y1, Y2]) + + Yvar1 = None if yvar is None else torch.full_like(Y1, yvar) + Yvar2 = None if yvar is None else torch.full_like(Y2, 2 * yvar) + train_Yvar = None if yvar is None else torch.cat([Yvar1, Yvar2]) + Y3 = torch.tan(X * (2 * math.pi)) + torch.randn_like(X) * 0.2 + Yvar3 = None if yvar is None else torch.full_like(Y3, yvar) + if len(task_values) == 3: + train_Y = torch.cat([train_Y, Y3]) + if train_Yvar is not None: + train_Yvar = torch.cat([train_Yvar, Yvar3]) + feature_slice = slice(1, None) if skip_task_features_in_datasets else slice(None) + datasets = [ + SupervisedDataset( + X=train_X[:10, feature_slice], + Y=Y1, + Yvar=Yvar1, + feature_names=["task", "X"][feature_slice], + outcome_names=["y"], + ), + SupervisedDataset( + X=train_X[10:20, feature_slice], + Y=Y2, + Yvar=Yvar2, + feature_names=["task", "X"][feature_slice], + outcome_names=["y1"], + ), + ] + if len(task_values) == 3: + datasets.append( + SupervisedDataset( + X=train_X[20:, feature_slice], + Y=Y3, + Yvar=Yvar3, + feature_names=["task", "X"][feature_slice], + outcome_names=["y2"], + ) + ) + dataset = MultiTaskDataset( + datasets=datasets, + target_outcome_name="y", + task_feature_index=None if skip_task_features_in_datasets else 0, + ) + return dataset, (train_X, train_Y, train_Yvar)
+ + + +
+[docs] +def get_pvar_expected( + posterior: TorchPosterior, model: Model, X: Tensor, m: int +) -> Tensor: + """Computes the expected variance of a posterior after adding the + predictive noise from the likelihood. + + Args: + posterior: The posterior to compute the variance of. Must be a + `TorchPosterior` object. + model: The model that generated the posterior. If `m > 1`, this must be + a `BatchedMultiOutputGPyTorchModel`. + X: The test inputs. + m: The number of outputs. + + Returns: + The expected variance of the posterior after adding the observation + noise from the likelihood. + """ + X = model.transform_inputs(X) + lh_kwargs = {} + odim = -1 # this is the output dimension index + + if m > 1: + if not isinstance(model, BatchedMultiOutputGPyTorchModel): + raise UnsupportedError( + "`get_pvar_expected` only supports `BatchedMultiOutputGPyTorchModel`s." + ) + # We need to add a batch dimension to the input to be compatible with the + # augmented batch shape of the model. This also changes the output dimension + # index. + X, odim = add_output_dim(X=X, original_batch_shape=model._input_batch_shape) + + if isinstance(model.likelihood, FixedNoiseGaussianLikelihood): + noise = model.likelihood.noise.mean(dim=-1, keepdim=True) + broadcasted_shape = torch.broadcast_shapes(noise.shape, X.shape[:-1]) + lh_kwargs["noise"] = noise.expand(broadcasted_shape) + + pvar_exp = model.likelihood(model(X), X, **lh_kwargs).variance + if m == 1: + pvar_exp = pvar_exp.unsqueeze(-1) + pvar_exp = torch.stack( + [pvar_exp.select(dim=odim, index=i) for i in range(m)], dim=-1 + ) + + # If the model has an outcome transform, we need to untransform the + # variance according to that transform. + if hasattr(model, "outcome_transform"): + _, pvar_exp = model.outcome_transform.untransform( + Y=torch.zeros_like(pvar_exp), Yvar=pvar_exp + ) + + return pvar_exp
+ + + +
+[docs] +class DummyNonScalarizingPosteriorTransform(PosteriorTransform): + scalarize = False + +
+[docs] + def evaluate(self, Y): + pass # pragma: no cover
+ + +
+[docs] + def forward(self, posterior): + pass # pragma: no cover
+
+ + + +
+[docs] +class SimpleGPyTorchModel(GPyTorchModel, ExactGP, FantasizeMixin): + last_fantasize_flag: bool = False + + def __init__(self, train_X, train_Y, outcome_transform=None, input_transform=None): + r""" + Args: + train_X: A tensor of inputs, passed to self.transform_inputs. + train_Y: Passed to outcome_transform. + outcome_transform: Transform applied to train_Y. + input_transform: A Module that performs the input transformation, passed to + self.transform_inputs. + """ + with torch.no_grad(): + transformed_X = self.transform_inputs( + X=train_X, input_transform=input_transform + ) + if outcome_transform is not None: + train_Y, _ = outcome_transform(train_Y) + self._validate_tensor_args(transformed_X, train_Y) + train_Y = train_Y.squeeze(-1) + likelihood = GaussianLikelihood() + super().__init__(train_X, train_Y, likelihood) + self.mean_module = ConstantMean() + self.covar_module = ScaleKernel(RBFKernel()) + if outcome_transform is not None: + self.outcome_transform = outcome_transform + if input_transform is not None: + self.input_transform = input_transform + self._num_outputs = 1 + self.to(train_X) + self.transformed_call_args = [] + +
+[docs] + def forward(self, x): + self.last_fantasize_flag = fantasize.on() + if self.training: + x = self.transform_inputs(x) + self.transformed_call_args.append(x) + mean_x = self.mean_module(x) + covar_x = self.covar_module(x) + return MultivariateNormal(mean_x, covar_x)
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/testing.html b/website-old/pages/api/_modules/botorch/utils/testing.html new file mode 100644 index 0000000000..d515175a52 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/testing.html @@ -0,0 +1,658 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.testing

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+import math
+import warnings
+from abc import abstractmethod
+from collections import OrderedDict
+from collections.abc import Sequence
+from itertools import product
+from typing import Any
+from unittest import mock, TestCase
+
+import torch
+from botorch.acquisition.objective import PosteriorTransform
+from botorch.exceptions.warnings import (
+    BotorchTensorDimensionWarning,
+    InputDataWarning,
+    NumericsWarning,
+)
+from botorch.models.model import FantasizeMixin, Model
+from botorch.posteriors.gpytorch import GPyTorchPosterior
+from botorch.posteriors.posterior import Posterior
+from botorch.sampling.base import MCSampler
+from botorch.sampling.get_sampler import GetSampler
+from botorch.sampling.stochastic_samplers import StochasticSampler
+from botorch.test_functions.base import BaseTestProblem
+from botorch.utils.transforms import unnormalize
+from gpytorch.distributions import MultitaskMultivariateNormal, MultivariateNormal
+from linear_operator.operators import AddedDiagLinearOperator, DiagLinearOperator
+from torch import Tensor
+
+
+EMPTY_SIZE = torch.Size()
+
+
+
+[docs] +class BotorchTestCase(TestCase): + r"""Basic test case for Botorch. + + This + 1. sets the default device to be `torch.device("cpu")` + 2. ensures that no warnings are suppressed by default. + """ + + device = torch.device("cpu") + +
+[docs] + def setUp(self, suppress_input_warnings: bool = True) -> None: + warnings.resetwarnings() + warnings.simplefilter("always", append=True) + if suppress_input_warnings: + warnings.filterwarnings( + "ignore", + message="The model inputs are of type", + category=InputDataWarning, + ) + warnings.filterwarnings( + "ignore", + message="Non-strict enforcement of botorch tensor conventions.", + category=BotorchTensorDimensionWarning, + ) + warnings.filterwarnings( + "ignore", + message=r"Data \(outcome observations\) is not standardized ", + category=InputDataWarning, + ) + warnings.filterwarnings( + "ignore", + message=r"Data \(input features\) is not", + category=InputDataWarning, + ) + warnings.filterwarnings( + "ignore", + message="has known numerical issues", + category=NumericsWarning, + ) + warnings.filterwarnings( + "ignore", + message="Model converter code is deprecated", + category=DeprecationWarning, + )
+ + +
+[docs] + def assertAllClose( + self, + input: Any, + other: Any, + rtol: float = 1e-05, + atol: float = 1e-08, + equal_nan: bool = False, + ) -> None: + r""" + Calls torch.testing.assert_close, using the signature and default behavior + of torch.allclose. + + Example output: + AssertionError: Scalars are not close! + + Absolute difference: 1.0000034868717194 (up to 0.0001 allowed) + Relative difference: 0.8348668001940709 (up to 1e-05 allowed) + """ + # Why not just use the signature and behavior of `torch.testing.assert_close`? + # Because we used `torch.allclose` for testing in the past, and the two don't + # behave exactly the same. In particular, `assert_close` requires both `atol` + # and `rtol` to be set if either one is. + torch.testing.assert_close( + input, + other, + rtol=rtol, + atol=atol, + equal_nan=equal_nan, + )
+
+ + + +
+[docs] +class BaseTestProblemTestCaseMixIn: +
+[docs] + def test_forward_and_evaluate_true(self): + dtypes = (torch.float, torch.double) + batch_shapes = (torch.Size(), torch.Size([2]), torch.Size([2, 3])) + for dtype, batch_shape, f in product(dtypes, batch_shapes, self.functions): + f.to(device=self.device, dtype=dtype) + X = torch.rand(*batch_shape, f.dim, device=self.device, dtype=dtype) + X = f.bounds[0] + X * (f.bounds[1] - f.bounds[0]) + res_forward = f(X) + res_evaluate_true = f.evaluate_true(X) + for method, res in { + "forward": res_forward, + "evaluate_true": res_evaluate_true, + }.items(): + with self.subTest( + f"{dtype}_{batch_shape}_{f.__class__.__name__}_{method}" + ): + self.assertEqual(res.dtype, dtype) + self.assertEqual(res.device.type, self.device.type) + tail_shape = torch.Size( + [f.num_objectives] if f.num_objectives > 1 else [] + ) + self.assertEqual(res.shape, batch_shape + tail_shape)
+ + + @property + @abstractmethod + def functions(self) -> Sequence[BaseTestProblem]: + # The functions that should be tested. Typically defined as a class + # attribute on the test case subclassing this class. + pass # pragma: no cover
+ + + +
+[docs] +class SyntheticTestFunctionTestCaseMixin: +
+[docs] + def test_optimal_value(self): + for dtype in (torch.float, torch.double): + for f in self.functions: + f.to(device=self.device, dtype=dtype) + if f._optimal_value is None: + with self.assertRaisesRegex(NotImplementedError, "optimal value"): + f.optimal_value + else: + optval = f.optimal_value + optval_exp = -f._optimal_value if f.negate else f._optimal_value + self.assertEqual(optval, optval_exp)
+ + +
+[docs] + def test_optimizer(self): + for dtype in (torch.float, torch.double): + for f in self.functions: + f.to(device=self.device, dtype=dtype) + try: + Xopt = f.optimizers.clone().requires_grad_(True) + except NotImplementedError: + continue + res = f(Xopt, noise=False) + # if we have optimizers, we have the optimal value + res_exp = torch.full_like(res, f.optimal_value) + self.assertAllClose(res, res_exp, atol=1e-3, rtol=1e-3) + if f._check_grad_at_opt: + grad = torch.autograd.grad([*res], Xopt)[0] + self.assertLess(grad.abs().max().item(), 1e-3)
+
+ + + +
+[docs] +class MultiObjectiveTestProblemTestCaseMixin: +
+[docs] + def test_attributes(self): + for f in self.functions: + self.assertTrue(hasattr(f, "dim")) + self.assertTrue(hasattr(f, "num_objectives")) + self.assertEqual(f.bounds.shape, torch.Size([2, f.dim]))
+ + +
+[docs] + def test_max_hv(self): + for dtype in (torch.float, torch.double): + for f in self.functions: + f.to(device=self.device, dtype=dtype) + if f._max_hv is None: + with self.assertRaises(NotImplementedError): + f.max_hv + else: + self.assertEqual(f.max_hv, f._max_hv)
+ + +
+[docs] + def test_ref_point(self): + for dtype in (torch.float, torch.double): + for f in self.functions: + f.to(dtype=dtype, device=self.device) + self.assertTrue( + torch.allclose( + f.ref_point, + torch.tensor(f._ref_point, dtype=dtype, device=self.device), + ) + )
+
+ + + +
+[docs] +class ConstrainedTestProblemTestCaseMixin: +
+[docs] + def test_num_constraints(self): + for f in self.functions: + self.assertTrue(hasattr(f, "num_constraints"))
+ + +
+[docs] + def test_evaluate_slack(self): + for dtype in (torch.float, torch.double): + for f in self.functions: + f.to(device=self.device, dtype=dtype) + X = unnormalize( + torch.rand(1, f.dim, device=self.device, dtype=dtype), + bounds=f.bounds, + ) + slack_true = f.evaluate_slack_true(X) + # Mock out the random generator to ensure that noise realizations are + # sizable so we don't run into any floating point comparison issues. + with mock.patch( + "botorch.test_functions.base.torch.randn_like", + side_effect=lambda y: y, + ): + slack_observed = f.evaluate_slack(X) + + self.assertEqual(slack_true.shape, torch.Size([1, f.num_constraints])) + self.assertEqual( + slack_observed.shape, torch.Size([1, f.num_constraints]) + ) + is_equal = (slack_observed == slack_true).bool() + if isinstance(f.constraint_noise_std, float): + self.assertEqual( + is_equal.all().item(), f.constraint_noise_std == 0.0 + ) + elif isinstance(f.constraint_noise_std, list): + for i, noise_std in enumerate(f.constraint_noise_std): + self.assertEqual( + is_equal[:, i].item(), noise_std in (0.0, None) + ) + else: + self.assertTrue(is_equal.all().item())
+
+ + + +
+[docs] +class MockPosterior(Posterior): + r"""Mock object that implements dummy methods and feeds through specified outputs""" + + def __init__( + self, mean=None, variance=None, samples=None, base_shape=None, batch_range=None + ) -> None: + r""" + Args: + mean: The mean of the posterior. + variance: The variance of the posterior. + samples: Samples to return from `rsample`, unless `base_samples` is + provided. + base_shape: If given, this is returned as `base_sample_shape`, and also + used as the base of the `_extended_shape`. + batch_range: If given, this is returned as `batch_range`. + Defaults to (0, -2). + """ + self._mean = mean + self._variance = variance + self._samples = samples + self._base_shape = base_shape + self._batch_range = batch_range or (0, -2) + + @property + def device(self) -> torch.device: + for t in (self._mean, self._variance, self._samples): + if torch.is_tensor(t): + return t.device + return torch.device("cpu") + + @property + def dtype(self) -> torch.dtype: + for t in (self._mean, self._variance, self._samples): + if torch.is_tensor(t): + return t.dtype + return torch.float32 + + @property + def batch_shape(self) -> torch.Size: + for t in (self._mean, self._variance, self._samples): + if torch.is_tensor(t): + return t.shape[:-2] + raise NotImplementedError # pragma: no cover + + def _extended_shape( + self, + sample_shape: torch.Size = torch.Size(), # noqa: B008 + ) -> torch.Size: + return sample_shape + self.base_sample_shape + + @property + def base_sample_shape(self) -> torch.Size: + if self._base_shape is not None: + return self._base_shape + if self._samples is not None: + return self._samples.shape + if self._mean is not None: + return self._mean.shape + if self._variance is not None: + return self._variance.shape + return torch.Size() + + @property + def batch_range(self) -> tuple[int, int]: + return self._batch_range + + @property + def mean(self): + return self._mean + + @property + def variance(self): + return self._variance + +
+[docs] + def rsample( + self, + sample_shape: torch.Size | None = None, + ) -> Tensor: + """Mock sample by repeating self._samples. If base_samples is provided, + do a shape check but return the same mock samples.""" + if sample_shape is None: + sample_shape = torch.Size() + return self._samples.expand(sample_shape + self._samples.shape)
+ + +
+[docs] + def rsample_from_base_samples( + self, + sample_shape: torch.Size, + base_samples: Tensor, + ) -> Tensor: + if base_samples.shape[: len(sample_shape)] != sample_shape: + raise RuntimeError( + "`sample_shape` disagrees with shape of `base_samples`. " + f"Got {sample_shape=} and {base_samples.shape=}." + ) + return self.rsample(sample_shape)
+
+ + + +@GetSampler.register(MockPosterior) +def _get_sampler_mock( + posterior: MockPosterior, sample_shape: torch.Size, **kwargs: Any +) -> MCSampler: + r"""Get the dummy `StochasticSampler` for `MockPosterior`.""" + return StochasticSampler(sample_shape=sample_shape, **kwargs) + + +
+[docs] +class MockModel(Model, FantasizeMixin): + r"""Mock object that implements dummy methods and feeds through specified outputs""" + + def __init__(self, posterior: MockPosterior) -> None: # noqa: D107 + super(Model, self).__init__() + self._posterior = posterior + +
+[docs] + def posterior( + self, + X: Tensor, + output_indices: list[int] | None = None, + posterior_transform: PosteriorTransform | None = None, + observation_noise: bool | torch.Tensor = False, + ) -> MockPosterior: + if posterior_transform is not None: + return posterior_transform(self._posterior) + else: + return self._posterior
+ + + @property + def num_outputs(self) -> int: + extended_shape = self._posterior._extended_shape() + return extended_shape[-1] if len(extended_shape) > 0 else 0 + + @property + def batch_shape(self) -> torch.Size: + extended_shape = self._posterior._extended_shape() + return extended_shape[:-2] + +
+[docs] + def state_dict(self, *args, **kwargs) -> None: + pass
+ + +
+[docs] + def load_state_dict( + self, state_dict: OrderedDict | None = None, strict: bool = False + ) -> None: + pass
+
+ + + +
+[docs] +class MockAcquisitionFunction: + r"""Mock acquisition function object that implements dummy methods.""" + + def __init__(self): # noqa: D107 + self.model = None + self.X_pending = None + + def __call__(self, X): + return X[..., 0].max(dim=-1).values + +
+[docs] + def set_X_pending(self, X_pending: Tensor | None = None): + self.X_pending = X_pending
+
+ + + +def _get_random_data( + batch_shape: torch.Size, m: int, d: int = 1, n: int = 10, **tkwargs +) -> tuple[Tensor, Tensor]: + r"""Generate random data for testing purposes. + + Args: + batch_shape: The batch shape of the data. + m: The number of outputs. + d: The dimension of the input. + n: The number of data points. + tkwargs: `device` and `dtype` tensor constructor kwargs. + + Returns: + A tuple `(train_X, train_Y)` with randomly generated training data. + """ + rep_shape = batch_shape + torch.Size([1, 1]) + train_x = torch.stack( + [torch.linspace(0, 0.95, n, **tkwargs) for _ in range(d)], dim=-1 + ) + train_x = train_x + 0.05 * torch.rand_like(train_x).repeat(rep_shape) + train_x[0] += 0.02 # modify the first batch + train_y = torch.sin(train_x[..., :1] * (2 * math.pi)) + train_y = train_y + 0.2 * torch.randn(n, m, **tkwargs).repeat(rep_shape) + return train_x, train_y + + +def _get_test_posterior( + batch_shape: torch.Size, + q: int = 1, + m: int = 1, + interleaved: bool = True, + lazy: bool = False, + independent: bool = False, + **tkwargs, +) -> GPyTorchPosterior: + r"""Generate a Posterior for testing purposes. + + Args: + batch_shape: The batch shape of the data. + q: The number of candidates + m: The number of outputs. + interleaved: A boolean indicating the format of the + MultitaskMultivariateNormal + lazy: A boolean indicating if the posterior should be lazy + independent: A boolean indicating whether the outputs are independent + tkwargs: `device` and `dtype` tensor constructor kwargs. + + + """ + if independent: + mvns = [] + for _ in range(m): + mean = torch.rand(*batch_shape, q, **tkwargs) + a = torch.rand(*batch_shape, q, q, **tkwargs) + covar = a @ a.transpose(-1, -2) + flat_diag = torch.rand(*batch_shape, q, **tkwargs) + covar = covar + torch.diag_embed(flat_diag) + mvns.append(MultivariateNormal(mean, covar)) + mtmvn = MultitaskMultivariateNormal.from_independent_mvns(mvns) + else: + mean = torch.rand(*batch_shape, q, m, **tkwargs) + a = torch.rand(*batch_shape, q * m, q * m, **tkwargs) + covar = a @ a.transpose(-1, -2) + flat_diag = torch.rand(*batch_shape, q * m, **tkwargs) + if lazy: + covar = AddedDiagLinearOperator(covar, DiagLinearOperator(flat_diag)) + else: + covar = covar + torch.diag_embed(flat_diag) + mtmvn = MultitaskMultivariateNormal(mean, covar, interleaved=interleaved) + return GPyTorchPosterior(mtmvn) + + +def _get_max_violation_of_bounds(samples: torch.Tensor, bounds: torch.Tensor) -> float: + """ + The maximum value by which samples lie outside bounds. + + A negative value indicates that all samples lie within bounds. + + Args: + samples: An `n x q x d` - dimension tensor, as might be returned from + `sample_q_batches_from_polytope`. + bounds: A `2 x d` tensor of lower and upper bounds for each column. + """ + n, q, d = samples.shape + samples = samples.reshape((n * q, d)) + lower = samples.min(0).values + upper = samples.max(0).values + lower_dist = (bounds[0, :] - lower).max().item() + upper_dist = (upper - bounds[1, :]).max().item() + return max(lower_dist, upper_dist) + + +def _get_max_violation_of_constraints( + samples: torch.Tensor, + constraints: list[tuple[Tensor, Tensor, float]] | None, + equality: bool, +) -> float: + r""" + Amount by which equality constraints are not obeyed. + + Args: + samples: An `n x q x d` - dimension tensor, as might be returned from + `sample_q_batches_from_polytope`. + constraints: A list of tuples (indices, coefficients, rhs), + with each tuple encoding an inequality constraint of the form + `\sum_i (X[indices[i]] * coefficients[i]) = rhs`, or `>=` if + `equality` is False. + equality: Whether these are equality constraints (not inequality). + """ + n, q, d = samples.shape + max_error = 0 + if constraints is not None: + for ind, coef, rhs in constraints: + if ind.ndim == 1: + constr = samples[:, :, ind] @ coef + else: + constr = samples[:, ind[:, 0], ind[:, 1]] @ coef + + if equality: + error = (constr - rhs).abs().max() + else: + error = (rhs - constr).max() + max_error = max(max_error, error) + return max_error +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/torch.html b/website-old/pages/api/_modules/botorch/utils/torch.html new file mode 100644 index 0000000000..e666666a21 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/torch.html @@ -0,0 +1,246 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.torch

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+# NOTE: To be removed once (if) https://github.com/pytorch/pytorch/pull/37385 lands
+
+from __future__ import annotations
+
+import collections
+from collections import OrderedDict
+
+import torch
+from torch.nn import Module
+
+
+
+[docs] +class BufferDict(Module): + r"""Holds buffers in a dictionary. + + BufferDict can be indexed like a regular Python dictionary, but buffers it + contains are properly registered, and will be visible by all Module methods. + + :class:`~torch.nn.BufferDict` is an **ordered** dictionary that respects + + * the order of insertion, and + + * in :meth:`~torch.nn.BufferDict.update`, the order of the merged ``OrderedDict`` + or another :class:`~torch.nn.BufferDict` (the argument to + :meth:`~torch.nn.BufferDict.update`). + + Note that :meth:`~torch.nn.BufferDict.update` with other unordered mapping + types (e.g., Python's plain ``dict``) does not preserve the order of the + merged mapping. + + Args: + buffers (iterable, optional): a mapping (dictionary) of + (string : :class:`~torch.Tensor`) or an iterable of key-value pairs + of type (string, :class:`~torch.Tensor`) + + Example:: + + class MyModule(nn.Module): + def __init__(self): + super(MyModule, self).__init__() + self.buffers = nn.BufferDict({ + 'left': torch.randn(5, 10), + 'right': torch.randn(5, 10) + }) + + def forward(self, x, choice): + x = self.buffers[choice].mm(x) + return x + """ + + def __init__(self, buffers=None): + r""" + Args: + buffers: A mapping (dictionary) from string to :class:`~torch.Tensor`, or + an iterable of key-value pairs of type (string, :class:`~torch.Tensor`). + """ + super().__init__() + if buffers is not None: + self.update(buffers) + + def __getitem__(self, key): + return self._buffers[key] + + def __setitem__(self, key, buffer): + self.register_buffer(key, buffer) + + def __delitem__(self, key): + del self._buffers[key] + + def __len__(self): + return len(self._buffers) + + def __iter__(self): + return iter(self._buffers.keys()) + + def __contains__(self, key): + return key in self._buffers + +
+[docs] + def clear(self): + """Remove all items from the BufferDict.""" + self._buffers.clear()
+ + +
+[docs] + def pop(self, key): + r"""Remove key from the BufferDict and return its buffer. + + Args: + key (string): key to pop from the BufferDict + """ + v = self[key] + del self[key] + return v
+ + +
+[docs] + def keys(self): + r"""Return an iterable of the BufferDict keys.""" + return self._buffers.keys()
+ + +
+[docs] + def items(self): + r"""Return an iterable of the BufferDict key/value pairs.""" + return self._buffers.items()
+ + +
+[docs] + def values(self): + r"""Return an iterable of the BufferDict values.""" + return self._buffers.values()
+ + +
+[docs] + def update(self, buffers): + r"""Update the :class:`~torch.nn.BufferDict` with the key-value pairs from a + mapping or an iterable, overwriting existing keys. + + .. note:: + If :attr:`buffers` is an ``OrderedDict``, a :class:`~torch.nn.BufferDict`, + or an iterable of key-value pairs, the order of new elements in it is + preserved. + + Args: + buffers (iterable): a mapping (dictionary) from string to + :class:`~torch.Tensor`, or an iterable of + key-value pairs of type (string, :class:`~torch.Tensor`) + """ + if not isinstance(buffers, collections.abc.Iterable): + raise TypeError( + "BuffersDict.update should be called with an " + "iterable of key/value pairs, but got " + type(buffers).__name__ + ) + + if isinstance(buffers, collections.abc.Mapping): + if isinstance(buffers, (OrderedDict, BufferDict)): + for key, buffer in buffers.items(): + self[key] = buffer + else: + for key, buffer in sorted(buffers.items()): + self[key] = buffer + else: + for j, p in enumerate(buffers): + if not isinstance(p, collections.abc.Iterable): + raise TypeError( + "BufferDict update sequence element " + "#" + str(j) + " should be Iterable; is" + type(p).__name__ + ) + if not len(p) == 2: + raise ValueError( + "BufferDict update sequence element " + "#" + str(j) + " has length " + str(len(p)) + "; 2 is required" + ) + self[p[0]] = p[1]
+ + +
+[docs] + def extra_repr(self): + child_lines = [] + for k, p in self._buffers.items(): + size_str = "x".join(str(size) for size in p.size()) + device_str = "" if not p.is_cuda else f" (GPU {p.get_device()})" + parastr = "Buffer containing: [{} of size {}{}]".format( + torch.typename(p), size_str, device_str + ) + child_lines.append(" (" + k + "): " + parastr) + tmpstr = "\n".join(child_lines) + return tmpstr
+ + + def __call__(self, input): + raise RuntimeError("BufferDict should not be called.")
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/transforms.html b/website-old/pages/api/_modules/botorch/utils/transforms.html new file mode 100644 index 0000000000..3b04f204d2 --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/transforms.html @@ -0,0 +1,456 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.transforms

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+r"""
+Some basic data transformation helpers.
+"""
+
+from __future__ import annotations
+
+import warnings
+from collections.abc import Callable
+from functools import wraps
+from typing import Any, TYPE_CHECKING
+
+import torch
+from botorch.utils.safe_math import logmeanexp
+from torch import Tensor
+
+if TYPE_CHECKING:  # pragma: no cover
+    from botorch.acquisition import AcquisitionFunction
+    from botorch.models.model import Model
+
+
+
+[docs] +def standardize(Y: Tensor) -> Tensor: + r"""Standardizes (zero mean, unit variance) a tensor by dim=-2. + + If the tensor is single-dimensional, simply standardizes the tensor. + If for some batch index all elements are equal (or if there is only a single + data point), this function will return 0 for that batch index. + + Args: + Y: A `batch_shape x n x m`-dim tensor. + + Returns: + The standardized `Y`. + + Example: + >>> Y = torch.rand(4, 3) + >>> Y_standardized = standardize(Y) + """ + stddim = -1 if Y.dim() < 2 else -2 + Y_std = Y.std(dim=stddim, keepdim=True) + Y_std = Y_std.where(Y_std >= 1e-9, torch.full_like(Y_std, 1.0)) + return (Y - Y.mean(dim=stddim, keepdim=True)) / Y_std
+ + + +def _update_constant_bounds(bounds: Tensor) -> Tensor: + r"""If the lower and upper bounds are identical for a dimension, set + the upper bound to lower bound + 1. + + If any modification is needed, this will return a clone of the original + tensor to avoid in-place modification. + + Args: + bounds: A `2 x d`-dim tensor of lower and upper bounds. + + Returns: + A `2 x d`-dim tensor of updated lower and upper bounds. + """ + if (constant_dims := (bounds[1] == bounds[0])).any(): + bounds = bounds.clone() + bounds[1, constant_dims] = bounds[0, constant_dims] + 1 + return bounds + + +
+[docs] +def normalize(X: Tensor, bounds: Tensor) -> Tensor: + r"""Min-max normalize X w.r.t. the provided bounds. + + NOTE: If the upper and lower bounds are identical for a dimension, that dimension + will not be scaled. Such dimensions will only be shifted as + `new_X[..., i] = X[..., i] - bounds[0, i]`. This avoids division by zero issues. + + Args: + X: `... x d` tensor of data + bounds: `2 x d` tensor of lower and upper bounds for each of the X's d + columns. + + Returns: + A `... x d`-dim tensor of normalized data, given by + `(X - bounds[0]) / (bounds[1] - bounds[0])`. If all elements of `X` + are contained within `bounds`, the normalized values will be + contained within `[0, 1]^d`. + + Example: + >>> X = torch.rand(4, 3) + >>> bounds = torch.stack([torch.zeros(3), 0.5 * torch.ones(3)]) + >>> X_normalized = normalize(X, bounds) + """ + bounds = _update_constant_bounds(bounds=bounds) + return (X - bounds[0]) / (bounds[1] - bounds[0])
+ + + +
+[docs] +def unnormalize(X: Tensor, bounds: Tensor) -> Tensor: + r"""Un-normalizes X w.r.t. the provided bounds. + + NOTE: If the upper and lower bounds are identical for a dimension, that dimension + will not be scaled. Such dimensions will only be shifted as + `new_X[..., i] = X[..., i] + bounds[0, i]`, matching the behavior of `normalize`. + + Args: + X: `... x d` tensor of data + bounds: `2 x d` tensor of lower and upper bounds for each of the X's d + columns. + + Returns: + A `... x d`-dim tensor of unnormalized data, given by + `X * (bounds[1] - bounds[0]) + bounds[0]`. If all elements of `X` + are contained in `[0, 1]^d`, the un-normalized values will be + contained within `bounds`. + + Example: + >>> X_normalized = torch.rand(4, 3) + >>> bounds = torch.stack([torch.zeros(3), 0.5 * torch.ones(3)]) + >>> X = unnormalize(X_normalized, bounds) + """ + bounds = _update_constant_bounds(bounds=bounds) + return X * (bounds[1] - bounds[0]) + bounds[0]
+ + + +
+[docs] +def normalize_indices(indices: list[int] | None, d: int) -> list[int] | None: + r"""Normalize a list of indices to ensure that they are positive. + + Args: + indices: A list of indices (may contain negative indices for indexing + "from the back"). + d: The dimension of the tensor to index. + + Returns: + A normalized list of indices such that each index is between `0` and + `d-1`, or None if indices is None. + """ + if indices is None: + return indices + normalized_indices = [] + for i in indices: + if i < 0: + i = i + d + if i < 0 or i > d - 1: + raise ValueError(f"Index {i} out of bounds for tensor or length {d}.") + normalized_indices.append(i) + return normalized_indices
+ + + +def _verify_output_shape(acqf: Any, X: Tensor, output: Tensor) -> bool: + r""" + Performs the output shape checks for `t_batch_mode_transform`. Output shape checks + help in catching the errors due to AcquisitionFunction arguments with erroneous + return shapes before these errors propagate further down the line. + + This method checks that the `output` shape matches either the t-batch shape of X + or the `batch_shape` of `acqf.model`. + + Args: + acqf: The AcquisitionFunction object being evaluated. + X: The `... x q x d`-dim input tensor with an explicit t-batch. + output: The return value of `acqf.method(X, ...)`. + + Returns: + True if `output` has the correct shape, False otherwise. + """ + try: + X_batch_shape = X.shape[:-2] + if output.shape == X_batch_shape: + return True + if output.shape == torch.Size() and X_batch_shape == torch.Size([1]): + # X has a batch shape of [1] which gets squeezed. + return True + # Cases with model batch shape involved. + model_b_shape = acqf.model.batch_shape + if output.shape == model_b_shape: + # Simple inputs with batched model. + return True + model_b_dim = len(model_b_shape) + if output.shape == X_batch_shape[:-model_b_dim] + model_b_shape and all( + xs in [1, ms] for xs, ms in zip(X_batch_shape[-model_b_dim:], model_b_shape) + ): + # X has additional batch dimensions beyond the model batch shape. + # For a batched model, some of the input dimensions might get broadcasted + # to the model batch shape. In that case the acquisition function output + # should replace the right-most batch dim of X with the model's batch shape. + return True + return False + except (AttributeError, NotImplementedError): + # acqf does not have model or acqf.model does not define `batch_shape` + warnings.warn( + "Output shape checks failed! Expected output shape to match t-batch shape" + f"of X, but got output with shape {output.shape} for X with shape " + f"{X.shape}. Make sure that this is the intended behavior!", + RuntimeWarning, + stacklevel=3, + ) + return True + + +
+[docs] +def is_fully_bayesian(model: Model) -> bool: + r"""Check if at least one model is a fully Bayesian model. + + Args: + model: A BoTorch model (may be a `ModelList` or `ModelListGP`) + + Returns: + True if at least one model is a fully Bayesian model. + """ + from botorch.models import ModelList + + if isinstance(model, ModelList): + return any(is_fully_bayesian(m) for m in model.models) + return getattr(model, "_is_fully_bayesian", False)
+ + + +
+[docs] +def is_ensemble(model: Model) -> bool: + r"""Check if at least one model is an ensemble model. + + Args: + model: A BoTorch model (may be a `ModelList` or `ModelListGP`) + + Returns: + True if at least one model is an ensemble model. + """ + from botorch.models import ModelList + + if isinstance(model, ModelList): + return any(is_ensemble(m) for m in model.models) + return getattr(model, "_is_ensemble", False)
+ + + +
+[docs] +def t_batch_mode_transform( + expected_q: int | None = None, + assert_output_shape: bool = True, +) -> Callable[ + [Callable[[AcquisitionFunction, Any], Any]], + Callable[[AcquisitionFunction, Any], Any], +]: + r"""Factory for decorators enabling consistent t-batch behavior. + + This method creates decorators for instance methods to transform an input tensor + `X` to t-batch mode (i.e. with at least 3 dimensions). This assumes the tensor + has a q-batch dimension. The decorator also checks the q-batch size if `expected_q` + is provided, and the output shape if `assert_output_shape` is `True`. + + Args: + expected_q: The expected q-batch size of `X`. If specified, this will raise an + AssertionError if `X`'s q-batch size does not equal expected_q. + assert_output_shape: If `True`, this will raise an AssertionError if the + output shape does not match either the t-batch shape of `X`, + or the `acqf.model.batch_shape` for acquisition functions using + batched models. + + Returns: + The decorated instance method. + + Example: + >>> class ExampleClass: + >>> @t_batch_mode_transform(expected_q=1) + >>> def single_q_method(self, X): + >>> ... + >>> + >>> @t_batch_mode_transform() + >>> def arbitrary_q_method(self, X): + >>> ... + """ + + def decorator( + method: Callable[[AcquisitionFunction, Any], Any], + ) -> Callable[[AcquisitionFunction, Any], Any]: + @wraps(method) + def decorated( + acqf: AcquisitionFunction, X: Any, *args: Any, **kwargs: Any + ) -> Any: + # Allow using acquisition functions for other inputs (e.g. lists of strings) + if not isinstance(X, Tensor): + return method(acqf, X, *args, **kwargs) + + if X.dim() < 2: + raise ValueError( + f"{type(acqf).__name__} requires X to have at least 2 dimensions," + f" but received X with only {X.dim()} dimensions." + ) + elif expected_q is not None and X.shape[-2] != expected_q: + raise AssertionError( + f"Expected X to be `batch_shape x q={expected_q} x d`, but" + f" got X with shape {X.shape}." + ) + # add t-batch dim + X = X if X.dim() > 2 else X.unsqueeze(0) + output = method(acqf, X, *args, **kwargs) + if hasattr(acqf, "model") and is_ensemble(acqf.model): + # IDEA: this could be wrapped into SampleReducingMCAcquisitionFunction + output = ( + output.mean(dim=-1) if not acqf._log else logmeanexp(output, dim=-1) + ) + if assert_output_shape and not _verify_output_shape( + acqf=acqf, + X=X, + output=output, + ): + raise AssertionError( + "Expected the output shape to match either the t-batch shape of " + "X, or the `model.batch_shape` in the case of acquisition " + "functions using batch models; but got output with shape " + f"{output.shape} for X with shape {X.shape}." + ) + return output + + return decorated + + return decorator
+ + + +
+[docs] +def concatenate_pending_points( + method: Callable[[Any, Tensor], Any], +) -> Callable[[Any, Tensor], Any]: + r"""Decorator concatenating X_pending into an acquisition function's argument. + + This decorator works on the `forward` method of acquisition functions taking + a tensor `X` as the argument. If the acquisition function has an `X_pending` + attribute (that is not `None`), this is concatenated into the input `X`, + appropriately expanding the pending points to match the batch shape of `X`. + + Example: + >>> class ExampleAcquisitionFunction: + >>> @concatenate_pending_points + >>> @t_batch_mode_transform() + >>> def forward(self, X): + >>> ... + """ + + @wraps(method) + def decorated(cls: Any, X: Tensor, **kwargs: Any) -> Any: + if cls.X_pending is not None: + X = torch.cat([X, match_batch_shape(cls.X_pending, X)], dim=-2) + return method(cls, X, **kwargs) + + return decorated
+ + + +
+[docs] +def match_batch_shape(X: Tensor, Y: Tensor) -> Tensor: + r"""Matches the batch dimension of a tensor to that of another tensor. + + Args: + X: A `batch_shape_X x q x d` tensor, whose batch dimensions that + correspond to batch dimensions of `Y` are to be matched to those + (if compatible). + Y: A `batch_shape_Y x q' x d` tensor. + + Returns: + A `batch_shape_Y x q x d` tensor containing the data of `X` expanded to + the batch dimensions of `Y` (if compatible). For instance, if `X` is + `b'' x b' x q x d` and `Y` is `b x q x d`, then the returned tensor is + `b'' x b x q x d`. + + Example: + >>> X = torch.rand(2, 1, 5, 3) + >>> Y = torch.rand(2, 6, 4, 3) + >>> X_matched = match_batch_shape(X, Y) + >>> X_matched.shape + torch.Size([2, 6, 5, 3]) + + """ + return X.expand(X.shape[: -(Y.dim())] + Y.shape[:-2] + X.shape[-2:])
+ + + +
+[docs] +def convert_to_target_pre_hook(module, *args): + r"""Pre-hook for automatically calling `.to(X)` on module prior to `forward`""" + module.to(args[0][0])
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/botorch/utils/types.html b/website-old/pages/api/_modules/botorch/utils/types.html new file mode 100644 index 0000000000..bef55bc71e --- /dev/null +++ b/website-old/pages/api/_modules/botorch/utils/types.html @@ -0,0 +1,76 @@ + + + + + + + +
+
+
+
+

Source code for botorch.utils.types

+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from __future__ import annotations
+
+
+class _DefaultType(type):
+    r"""
+    Private class whose sole instance `DEFAULT` is as a special indicator
+    representing that a default value should be assigned to an argument.
+    Typically used in cases where `None` is an allowed argument.
+    """
+
+
+DEFAULT = _DefaultType("DEFAULT", (), {})
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/collections.html b/website-old/pages/api/_modules/collections.html new file mode 100644 index 0000000000..3725637f58 --- /dev/null +++ b/website-old/pages/api/_modules/collections.html @@ -0,0 +1,1650 @@ + + + + + + + +
+
+
+
+

Source code for collections

+'''This module implements specialized container datatypes providing
+alternatives to Python's general purpose built-in containers, dict,
+list, set, and tuple.
+
+* namedtuple   factory function for creating tuple subclasses with named fields
+* deque        list-like container with fast appends and pops on either end
+* ChainMap     dict-like class for creating a single view of multiple mappings
+* Counter      dict subclass for counting hashable objects
+* OrderedDict  dict subclass that remembers the order entries were added
+* defaultdict  dict subclass that calls a factory function to supply missing values
+* UserDict     wrapper around dictionary objects for easier dict subclassing
+* UserList     wrapper around list objects for easier list subclassing
+* UserString   wrapper around string objects for easier string subclassing
+
+'''
+
+__all__ = [
+    'ChainMap',
+    'Counter',
+    'OrderedDict',
+    'UserDict',
+    'UserList',
+    'UserString',
+    'defaultdict',
+    'deque',
+    'namedtuple',
+]
+
+import _collections_abc
+import sys as _sys
+
+from itertools import chain as _chain
+from itertools import repeat as _repeat
+from itertools import starmap as _starmap
+from keyword import iskeyword as _iskeyword
+from operator import eq as _eq
+from operator import itemgetter as _itemgetter
+from reprlib import recursive_repr as _recursive_repr
+from _weakref import proxy as _proxy
+
+try:
+    from _collections import deque
+except ImportError:
+    pass
+else:
+    _collections_abc.MutableSequence.register(deque)
+
+try:
+    from _collections import _deque_iterator
+except ImportError:
+    pass
+
+try:
+    from _collections import defaultdict
+except ImportError:
+    pass
+
+
+################################################################################
+### OrderedDict
+################################################################################
+
+class _OrderedDictKeysView(_collections_abc.KeysView):
+
+    def __reversed__(self):
+        yield from reversed(self._mapping)
+
+class _OrderedDictItemsView(_collections_abc.ItemsView):
+
+    def __reversed__(self):
+        for key in reversed(self._mapping):
+            yield (key, self._mapping[key])
+
+class _OrderedDictValuesView(_collections_abc.ValuesView):
+
+    def __reversed__(self):
+        for key in reversed(self._mapping):
+            yield self._mapping[key]
+
+class _Link(object):
+    __slots__ = 'prev', 'next', 'key', '__weakref__'
+
+class OrderedDict(dict):
+    'Dictionary that remembers insertion order'
+    # An inherited dict maps keys to values.
+    # The inherited dict provides __getitem__, __len__, __contains__, and get.
+    # The remaining methods are order-aware.
+    # Big-O running times for all methods are the same as regular dictionaries.
+
+    # The internal self.__map dict maps keys to links in a doubly linked list.
+    # The circular doubly linked list starts and ends with a sentinel element.
+    # The sentinel element never gets deleted (this simplifies the algorithm).
+    # The sentinel is in self.__hardroot with a weakref proxy in self.__root.
+    # The prev links are weakref proxies (to prevent circular references).
+    # Individual links are kept alive by the hard reference in self.__map.
+    # Those hard references disappear when a key is deleted from an OrderedDict.
+
+    def __new__(cls, /, *args, **kwds):
+        "Create the ordered dict object and set up the underlying structures."
+        self = dict.__new__(cls)
+        self.__hardroot = _Link()
+        self.__root = root = _proxy(self.__hardroot)
+        root.prev = root.next = root
+        self.__map = {}
+        return self
+
+    def __init__(self, other=(), /, **kwds):
+        '''Initialize an ordered dictionary.  The signature is the same as
+        regular dictionaries.  Keyword argument order is preserved.
+        '''
+        self.__update(other, **kwds)
+
+    def __setitem__(self, key, value,
+                    dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
+        'od.__setitem__(i, y) <==> od[i]=y'
+        # Setting a new item creates a new link at the end of the linked list,
+        # and the inherited dictionary is updated with the new key/value pair.
+        if key not in self:
+            self.__map[key] = link = Link()
+            root = self.__root
+            last = root.prev
+            link.prev, link.next, link.key = last, root, key
+            last.next = link
+            root.prev = proxy(link)
+        dict_setitem(self, key, value)
+
+    def __delitem__(self, key, dict_delitem=dict.__delitem__):
+        'od.__delitem__(y) <==> del od[y]'
+        # Deleting an existing item uses self.__map to find the link which gets
+        # removed by updating the links in the predecessor and successor nodes.
+        dict_delitem(self, key)
+        link = self.__map.pop(key)
+        link_prev = link.prev
+        link_next = link.next
+        link_prev.next = link_next
+        link_next.prev = link_prev
+        link.prev = None
+        link.next = None
+
+    def __iter__(self):
+        'od.__iter__() <==> iter(od)'
+        # Traverse the linked list in order.
+        root = self.__root
+        curr = root.next
+        while curr is not root:
+            yield curr.key
+            curr = curr.next
+
+    def __reversed__(self):
+        'od.__reversed__() <==> reversed(od)'
+        # Traverse the linked list in reverse order.
+        root = self.__root
+        curr = root.prev
+        while curr is not root:
+            yield curr.key
+            curr = curr.prev
+
+    def clear(self):
+        'od.clear() -> None.  Remove all items from od.'
+        root = self.__root
+        root.prev = root.next = root
+        self.__map.clear()
+        dict.clear(self)
+
+    def popitem(self, last=True):
+        '''Remove and return a (key, value) pair from the dictionary.
+
+        Pairs are returned in LIFO order if last is true or FIFO order if false.
+        '''
+        if not self:
+            raise KeyError('dictionary is empty')
+        root = self.__root
+        if last:
+            link = root.prev
+            link_prev = link.prev
+            link_prev.next = root
+            root.prev = link_prev
+        else:
+            link = root.next
+            link_next = link.next
+            root.next = link_next
+            link_next.prev = root
+        key = link.key
+        del self.__map[key]
+        value = dict.pop(self, key)
+        return key, value
+
+    def move_to_end(self, key, last=True):
+        '''Move an existing element to the end (or beginning if last is false).
+
+        Raise KeyError if the element does not exist.
+        '''
+        link = self.__map[key]
+        link_prev = link.prev
+        link_next = link.next
+        soft_link = link_next.prev
+        link_prev.next = link_next
+        link_next.prev = link_prev
+        root = self.__root
+        if last:
+            last = root.prev
+            link.prev = last
+            link.next = root
+            root.prev = soft_link
+            last.next = link
+        else:
+            first = root.next
+            link.prev = root
+            link.next = first
+            first.prev = soft_link
+            root.next = link
+
+    def __sizeof__(self):
+        sizeof = _sys.getsizeof
+        n = len(self) + 1                       # number of links including root
+        size = sizeof(self.__dict__)            # instance dictionary
+        size += sizeof(self.__map) * 2          # internal dict and inherited dict
+        size += sizeof(self.__hardroot) * n     # link objects
+        size += sizeof(self.__root) * n         # proxy objects
+        return size
+
+    update = __update = _collections_abc.MutableMapping.update
+
+    def keys(self):
+        "D.keys() -> a set-like object providing a view on D's keys"
+        return _OrderedDictKeysView(self)
+
+    def items(self):
+        "D.items() -> a set-like object providing a view on D's items"
+        return _OrderedDictItemsView(self)
+
+    def values(self):
+        "D.values() -> an object providing a view on D's values"
+        return _OrderedDictValuesView(self)
+
+    __ne__ = _collections_abc.MutableMapping.__ne__
+
+    __marker = object()
+
+    def pop(self, key, default=__marker):
+        '''od.pop(k[,d]) -> v, remove specified key and return the corresponding
+        value.  If key is not found, d is returned if given, otherwise KeyError
+        is raised.
+
+        '''
+        marker = self.__marker
+        result = dict.pop(self, key, marker)
+        if result is not marker:
+            # The same as in __delitem__().
+            link = self.__map.pop(key)
+            link_prev = link.prev
+            link_next = link.next
+            link_prev.next = link_next
+            link_next.prev = link_prev
+            link.prev = None
+            link.next = None
+            return result
+        if default is marker:
+            raise KeyError(key)
+        return default
+
+    def setdefault(self, key, default=None):
+        '''Insert key with a value of default if key is not in the dictionary.
+
+        Return the value for key if key is in the dictionary, else default.
+        '''
+        if key in self:
+            return self[key]
+        self[key] = default
+        return default
+
+    @_recursive_repr()
+    def __repr__(self):
+        'od.__repr__() <==> repr(od)'
+        if not self:
+            return '%s()' % (self.__class__.__name__,)
+        return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
+
+    def __reduce__(self):
+        'Return state information for pickling'
+        state = self.__getstate__()
+        if state:
+            if isinstance(state, tuple):
+                state, slots = state
+            else:
+                slots = {}
+            state = state.copy()
+            slots = slots.copy()
+            for k in vars(OrderedDict()):
+                state.pop(k, None)
+                slots.pop(k, None)
+            if slots:
+                state = state, slots
+            else:
+                state = state or None
+        return self.__class__, (), state, None, iter(self.items())
+
+    def copy(self):
+        'od.copy() -> a shallow copy of od'
+        return self.__class__(self)
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        '''Create a new ordered dictionary with keys from iterable and values set to value.
+        '''
+        self = cls()
+        for key in iterable:
+            self[key] = value
+        return self
+
+    def __eq__(self, other):
+        '''od.__eq__(y) <==> od==y.  Comparison to another OD is order-sensitive
+        while comparison to a regular mapping is order-insensitive.
+
+        '''
+        if isinstance(other, OrderedDict):
+            return dict.__eq__(self, other) and all(map(_eq, self, other))
+        return dict.__eq__(self, other)
+
+    def __ior__(self, other):
+        self.update(other)
+        return self
+
+    def __or__(self, other):
+        if not isinstance(other, dict):
+            return NotImplemented
+        new = self.__class__(self)
+        new.update(other)
+        return new
+
+    def __ror__(self, other):
+        if not isinstance(other, dict):
+            return NotImplemented
+        new = self.__class__(other)
+        new.update(self)
+        return new
+
+
+try:
+    from _collections import OrderedDict
+except ImportError:
+    # Leave the pure Python version in place.
+    pass
+
+
+################################################################################
+### namedtuple
+################################################################################
+
+try:
+    from _collections import _tuplegetter
+except ImportError:
+    _tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc)
+
+def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
+    """Returns a new subclass of tuple with named fields.
+
+    >>> Point = namedtuple('Point', ['x', 'y'])
+    >>> Point.__doc__                   # docstring for the new class
+    'Point(x, y)'
+    >>> p = Point(11, y=22)             # instantiate with positional args or keywords
+    >>> p[0] + p[1]                     # indexable like a plain tuple
+    33
+    >>> x, y = p                        # unpack like a regular tuple
+    >>> x, y
+    (11, 22)
+    >>> p.x + p.y                       # fields also accessible by name
+    33
+    >>> d = p._asdict()                 # convert to a dictionary
+    >>> d['x']
+    11
+    >>> Point(**d)                      # convert from a dictionary
+    Point(x=11, y=22)
+    >>> p._replace(x=100)               # _replace() is like str.replace() but targets named fields
+    Point(x=100, y=22)
+
+    """
+
+    # Validate the field names.  At the user's option, either generate an error
+    # message or automatically replace the field name with a valid name.
+    if isinstance(field_names, str):
+        field_names = field_names.replace(',', ' ').split()
+    field_names = list(map(str, field_names))
+    typename = _sys.intern(str(typename))
+
+    if rename:
+        seen = set()
+        for index, name in enumerate(field_names):
+            if (not name.isidentifier()
+                or _iskeyword(name)
+                or name.startswith('_')
+                or name in seen):
+                field_names[index] = f'_{index}'
+            seen.add(name)
+
+    for name in [typename] + field_names:
+        if type(name) is not str:
+            raise TypeError('Type names and field names must be strings')
+        if not name.isidentifier():
+            raise ValueError('Type names and field names must be valid '
+                             f'identifiers: {name!r}')
+        if _iskeyword(name):
+            raise ValueError('Type names and field names cannot be a '
+                             f'keyword: {name!r}')
+
+    seen = set()
+    for name in field_names:
+        if name.startswith('_') and not rename:
+            raise ValueError('Field names cannot start with an underscore: '
+                             f'{name!r}')
+        if name in seen:
+            raise ValueError(f'Encountered duplicate field name: {name!r}')
+        seen.add(name)
+
+    field_defaults = {}
+    if defaults is not None:
+        defaults = tuple(defaults)
+        if len(defaults) > len(field_names):
+            raise TypeError('Got more default values than field names')
+        field_defaults = dict(reversed(list(zip(reversed(field_names),
+                                                reversed(defaults)))))
+
+    # Variables used in the methods and docstrings
+    field_names = tuple(map(_sys.intern, field_names))
+    num_fields = len(field_names)
+    arg_list = ', '.join(field_names)
+    if num_fields == 1:
+        arg_list += ','
+    repr_fmt = '(' + ', '.join(f'{name}=%r' for name in field_names) + ')'
+    tuple_new = tuple.__new__
+    _dict, _tuple, _len, _map, _zip = dict, tuple, len, map, zip
+
+    # Create all the named tuple methods to be added to the class namespace
+
+    namespace = {
+        '_tuple_new': tuple_new,
+        '__builtins__': {},
+        '__name__': f'namedtuple_{typename}',
+    }
+    code = f'lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))'
+    __new__ = eval(code, namespace)
+    __new__.__name__ = '__new__'
+    __new__.__doc__ = f'Create new instance of {typename}({arg_list})'
+    if defaults is not None:
+        __new__.__defaults__ = defaults
+
+    @classmethod
+    def _make(cls, iterable):
+        result = tuple_new(cls, iterable)
+        if _len(result) != num_fields:
+            raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
+        return result
+
+    _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence '
+                              'or iterable')
+
+    def _replace(self, /, **kwds):
+        result = self._make(_map(kwds.pop, field_names, self))
+        if kwds:
+            raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
+        return result
+
+    _replace.__doc__ = (f'Return a new {typename} object replacing specified '
+                        'fields with new values')
+
+    def __repr__(self):
+        'Return a nicely formatted representation string'
+        return self.__class__.__name__ + repr_fmt % self
+
+    def _asdict(self):
+        'Return a new dict which maps field names to their values.'
+        return _dict(_zip(self._fields, self))
+
+    def __getnewargs__(self):
+        'Return self as a plain tuple.  Used by copy and pickle.'
+        return _tuple(self)
+
+    # Modify function metadata to help with introspection and debugging
+    for method in (
+        __new__,
+        _make.__func__,
+        _replace,
+        __repr__,
+        _asdict,
+        __getnewargs__,
+    ):
+        method.__qualname__ = f'{typename}.{method.__name__}'
+
+    # Build-up the class namespace dictionary
+    # and use type() to build the result class
+    class_namespace = {
+        '__doc__': f'{typename}({arg_list})',
+        '__slots__': (),
+        '_fields': field_names,
+        '_field_defaults': field_defaults,
+        '__new__': __new__,
+        '_make': _make,
+        '_replace': _replace,
+        '__repr__': __repr__,
+        '_asdict': _asdict,
+        '__getnewargs__': __getnewargs__,
+        '__match_args__': field_names,
+    }
+    for index, name in enumerate(field_names):
+        doc = _sys.intern(f'Alias for field number {index}')
+        class_namespace[name] = _tuplegetter(index, doc)
+
+    result = type(typename, (tuple,), class_namespace)
+
+    # For pickling to work, the __module__ variable needs to be set to the frame
+    # where the named tuple is created.  Bypass this step in environments where
+    # sys._getframe is not defined (Jython for example) or sys._getframe is not
+    # defined for arguments greater than 0 (IronPython), or where the user has
+    # specified a particular module.
+    if module is None:
+        try:
+            module = _sys._getframemodulename(1) or '__main__'
+        except AttributeError:
+            try:
+                module = _sys._getframe(1).f_globals.get('__name__', '__main__')
+            except (AttributeError, ValueError):
+                pass
+    if module is not None:
+        result.__module__ = module
+
+    return result
+
+
+########################################################################
+###  Counter
+########################################################################
+
+def _count_elements(mapping, iterable):
+    'Tally elements from the iterable.'
+    mapping_get = mapping.get
+    for elem in iterable:
+        mapping[elem] = mapping_get(elem, 0) + 1
+
+try:                                    # Load C helper function if available
+    from _collections import _count_elements
+except ImportError:
+    pass
+
+class Counter(dict):
+    '''Dict subclass for counting hashable items.  Sometimes called a bag
+    or multiset.  Elements are stored as dictionary keys and their counts
+    are stored as dictionary values.
+
+    >>> c = Counter('abcdeabcdabcaba')  # count elements from a string
+
+    >>> c.most_common(3)                # three most common elements
+    [('a', 5), ('b', 4), ('c', 3)]
+    >>> sorted(c)                       # list all unique elements
+    ['a', 'b', 'c', 'd', 'e']
+    >>> ''.join(sorted(c.elements()))   # list elements with repetitions
+    'aaaaabbbbcccdde'
+    >>> sum(c.values())                 # total of all counts
+    15
+
+    >>> c['a']                          # count of letter 'a'
+    5
+    >>> for elem in 'shazam':           # update counts from an iterable
+    ...     c[elem] += 1                # by adding 1 to each element's count
+    >>> c['a']                          # now there are seven 'a'
+    7
+    >>> del c['b']                      # remove all 'b'
+    >>> c['b']                          # now there are zero 'b'
+    0
+
+    >>> d = Counter('simsalabim')       # make another counter
+    >>> c.update(d)                     # add in the second counter
+    >>> c['a']                          # now there are nine 'a'
+    9
+
+    >>> c.clear()                       # empty the counter
+    >>> c
+    Counter()
+
+    Note:  If a count is set to zero or reduced to zero, it will remain
+    in the counter until the entry is deleted or the counter is cleared:
+
+    >>> c = Counter('aaabbc')
+    >>> c['b'] -= 2                     # reduce the count of 'b' by two
+    >>> c.most_common()                 # 'b' is still in, but its count is zero
+    [('a', 3), ('c', 1), ('b', 0)]
+
+    '''
+    # References:
+    #   http://en.wikipedia.org/wiki/Multiset
+    #   http://www.gnu.org/software/smalltalk/manual-base/html_node/Bag.html
+    #   http://www.demo2s.com/Tutorial/Cpp/0380__set-multiset/Catalog0380__set-multiset.htm
+    #   http://code.activestate.com/recipes/259174/
+    #   Knuth, TAOCP Vol. II section 4.6.3
+
+    def __init__(self, iterable=None, /, **kwds):
+        '''Create a new, empty Counter object.  And if given, count elements
+        from an input iterable.  Or, initialize the count from another mapping
+        of elements to their counts.
+
+        >>> c = Counter()                           # a new, empty counter
+        >>> c = Counter('gallahad')                 # a new counter from an iterable
+        >>> c = Counter({'a': 4, 'b': 2})           # a new counter from a mapping
+        >>> c = Counter(a=4, b=2)                   # a new counter from keyword args
+
+        '''
+        super().__init__()
+        self.update(iterable, **kwds)
+
+    def __missing__(self, key):
+        'The count of elements not in the Counter is zero.'
+        # Needed so that self[missing_item] does not raise KeyError
+        return 0
+
+    def total(self):
+        'Sum of the counts'
+        return sum(self.values())
+
+    def most_common(self, n=None):
+        '''List the n most common elements and their counts from the most
+        common to the least.  If n is None, then list all element counts.
+
+        >>> Counter('abracadabra').most_common(3)
+        [('a', 5), ('b', 2), ('r', 2)]
+
+        '''
+        # Emulate Bag.sortedByCount from Smalltalk
+        if n is None:
+            return sorted(self.items(), key=_itemgetter(1), reverse=True)
+
+        # Lazy import to speedup Python startup time
+        import heapq
+        return heapq.nlargest(n, self.items(), key=_itemgetter(1))
+
+    def elements(self):
+        '''Iterator over elements repeating each as many times as its count.
+
+        >>> c = Counter('ABCABC')
+        >>> sorted(c.elements())
+        ['A', 'A', 'B', 'B', 'C', 'C']
+
+        Knuth's example for prime factors of 1836:  2**2 * 3**3 * 17**1
+
+        >>> import math
+        >>> prime_factors = Counter({2: 2, 3: 3, 17: 1})
+        >>> math.prod(prime_factors.elements())
+        1836
+
+        Note, if an element's count has been set to zero or is a negative
+        number, elements() will ignore it.
+
+        '''
+        # Emulate Bag.do from Smalltalk and Multiset.begin from C++.
+        return _chain.from_iterable(_starmap(_repeat, self.items()))
+
+    # Override dict methods where necessary
+
+    @classmethod
+    def fromkeys(cls, iterable, v=None):
+        # There is no equivalent method for counters because the semantics
+        # would be ambiguous in cases such as Counter.fromkeys('aaabbc', v=2).
+        # Initializing counters to zero values isn't necessary because zero
+        # is already the default value for counter lookups.  Initializing
+        # to one is easily accomplished with Counter(set(iterable)).  For
+        # more exotic cases, create a dictionary first using a dictionary
+        # comprehension or dict.fromkeys().
+        raise NotImplementedError(
+            'Counter.fromkeys() is undefined.  Use Counter(iterable) instead.')
+
+    def update(self, iterable=None, /, **kwds):
+        '''Like dict.update() but add counts instead of replacing them.
+
+        Source can be an iterable, a dictionary, or another Counter instance.
+
+        >>> c = Counter('which')
+        >>> c.update('witch')           # add elements from another iterable
+        >>> d = Counter('watch')
+        >>> c.update(d)                 # add elements from another counter
+        >>> c['h']                      # four 'h' in which, witch, and watch
+        4
+
+        '''
+        # The regular dict.update() operation makes no sense here because the
+        # replace behavior results in some of the original untouched counts
+        # being mixed-in with all of the other counts for a mismash that
+        # doesn't have a straight-forward interpretation in most counting
+        # contexts.  Instead, we implement straight-addition.  Both the inputs
+        # and outputs are allowed to contain zero and negative counts.
+
+        if iterable is not None:
+            if isinstance(iterable, _collections_abc.Mapping):
+                if self:
+                    self_get = self.get
+                    for elem, count in iterable.items():
+                        self[elem] = count + self_get(elem, 0)
+                else:
+                    # fast path when counter is empty
+                    super().update(iterable)
+            else:
+                _count_elements(self, iterable)
+        if kwds:
+            self.update(kwds)
+
+    def subtract(self, iterable=None, /, **kwds):
+        '''Like dict.update() but subtracts counts instead of replacing them.
+        Counts can be reduced below zero.  Both the inputs and outputs are
+        allowed to contain zero and negative counts.
+
+        Source can be an iterable, a dictionary, or another Counter instance.
+
+        >>> c = Counter('which')
+        >>> c.subtract('witch')             # subtract elements from another iterable
+        >>> c.subtract(Counter('watch'))    # subtract elements from another counter
+        >>> c['h']                          # 2 in which, minus 1 in witch, minus 1 in watch
+        0
+        >>> c['w']                          # 1 in which, minus 1 in witch, minus 1 in watch
+        -1
+
+        '''
+        if iterable is not None:
+            self_get = self.get
+            if isinstance(iterable, _collections_abc.Mapping):
+                for elem, count in iterable.items():
+                    self[elem] = self_get(elem, 0) - count
+            else:
+                for elem in iterable:
+                    self[elem] = self_get(elem, 0) - 1
+        if kwds:
+            self.subtract(kwds)
+
+    def copy(self):
+        'Return a shallow copy.'
+        return self.__class__(self)
+
+    def __reduce__(self):
+        return self.__class__, (dict(self),)
+
+    def __delitem__(self, elem):
+        'Like dict.__delitem__() but does not raise KeyError for missing values.'
+        if elem in self:
+            super().__delitem__(elem)
+
+    def __repr__(self):
+        if not self:
+            return f'{self.__class__.__name__}()'
+        try:
+            # dict() preserves the ordering returned by most_common()
+            d = dict(self.most_common())
+        except TypeError:
+            # handle case where values are not orderable
+            d = dict(self)
+        return f'{self.__class__.__name__}({d!r})'
+
+    # Multiset-style mathematical operations discussed in:
+    #       Knuth TAOCP Volume II section 4.6.3 exercise 19
+    #       and at http://en.wikipedia.org/wiki/Multiset
+    #
+    # Outputs guaranteed to only include positive counts.
+    #
+    # To strip negative and zero counts, add-in an empty counter:
+    #       c += Counter()
+    #
+    # Results are ordered according to when an element is first
+    # encountered in the left operand and then by the order
+    # encountered in the right operand.
+    #
+    # When the multiplicities are all zero or one, multiset operations
+    # are guaranteed to be equivalent to the corresponding operations
+    # for regular sets.
+    #     Given counter multisets such as:
+    #         cp = Counter(a=1, b=0, c=1)
+    #         cq = Counter(c=1, d=0, e=1)
+    #     The corresponding regular sets would be:
+    #         sp = {'a', 'c'}
+    #         sq = {'c', 'e'}
+    #     All of the following relations would hold:
+    #         set(cp + cq) == sp | sq
+    #         set(cp - cq) == sp - sq
+    #         set(cp | cq) == sp | sq
+    #         set(cp & cq) == sp & sq
+    #         (cp == cq) == (sp == sq)
+    #         (cp != cq) == (sp != sq)
+    #         (cp <= cq) == (sp <= sq)
+    #         (cp < cq) == (sp < sq)
+    #         (cp >= cq) == (sp >= sq)
+    #         (cp > cq) == (sp > sq)
+
+    def __eq__(self, other):
+        'True if all counts agree. Missing counts are treated as zero.'
+        if not isinstance(other, Counter):
+            return NotImplemented
+        return all(self[e] == other[e] for c in (self, other) for e in c)
+
+    def __ne__(self, other):
+        'True if any counts disagree. Missing counts are treated as zero.'
+        if not isinstance(other, Counter):
+            return NotImplemented
+        return not self == other
+
+    def __le__(self, other):
+        'True if all counts in self are a subset of those in other.'
+        if not isinstance(other, Counter):
+            return NotImplemented
+        return all(self[e] <= other[e] for c in (self, other) for e in c)
+
+    def __lt__(self, other):
+        'True if all counts in self are a proper subset of those in other.'
+        if not isinstance(other, Counter):
+            return NotImplemented
+        return self <= other and self != other
+
+    def __ge__(self, other):
+        'True if all counts in self are a superset of those in other.'
+        if not isinstance(other, Counter):
+            return NotImplemented
+        return all(self[e] >= other[e] for c in (self, other) for e in c)
+
+    def __gt__(self, other):
+        'True if all counts in self are a proper superset of those in other.'
+        if not isinstance(other, Counter):
+            return NotImplemented
+        return self >= other and self != other
+
+    def __add__(self, other):
+        '''Add counts from two counters.
+
+        >>> Counter('abbb') + Counter('bcc')
+        Counter({'b': 4, 'c': 2, 'a': 1})
+
+        '''
+        if not isinstance(other, Counter):
+            return NotImplemented
+        result = Counter()
+        for elem, count in self.items():
+            newcount = count + other[elem]
+            if newcount > 0:
+                result[elem] = newcount
+        for elem, count in other.items():
+            if elem not in self and count > 0:
+                result[elem] = count
+        return result
+
+    def __sub__(self, other):
+        ''' Subtract count, but keep only results with positive counts.
+
+        >>> Counter('abbbc') - Counter('bccd')
+        Counter({'b': 2, 'a': 1})
+
+        '''
+        if not isinstance(other, Counter):
+            return NotImplemented
+        result = Counter()
+        for elem, count in self.items():
+            newcount = count - other[elem]
+            if newcount > 0:
+                result[elem] = newcount
+        for elem, count in other.items():
+            if elem not in self and count < 0:
+                result[elem] = 0 - count
+        return result
+
+    def __or__(self, other):
+        '''Union is the maximum of value in either of the input counters.
+
+        >>> Counter('abbb') | Counter('bcc')
+        Counter({'b': 3, 'c': 2, 'a': 1})
+
+        '''
+        if not isinstance(other, Counter):
+            return NotImplemented
+        result = Counter()
+        for elem, count in self.items():
+            other_count = other[elem]
+            newcount = other_count if count < other_count else count
+            if newcount > 0:
+                result[elem] = newcount
+        for elem, count in other.items():
+            if elem not in self and count > 0:
+                result[elem] = count
+        return result
+
+    def __and__(self, other):
+        ''' Intersection is the minimum of corresponding counts.
+
+        >>> Counter('abbb') & Counter('bcc')
+        Counter({'b': 1})
+
+        '''
+        if not isinstance(other, Counter):
+            return NotImplemented
+        result = Counter()
+        for elem, count in self.items():
+            other_count = other[elem]
+            newcount = count if count < other_count else other_count
+            if newcount > 0:
+                result[elem] = newcount
+        return result
+
+    def __pos__(self):
+        'Adds an empty counter, effectively stripping negative and zero counts'
+        result = Counter()
+        for elem, count in self.items():
+            if count > 0:
+                result[elem] = count
+        return result
+
+    def __neg__(self):
+        '''Subtracts from an empty counter.  Strips positive and zero counts,
+        and flips the sign on negative counts.
+
+        '''
+        result = Counter()
+        for elem, count in self.items():
+            if count < 0:
+                result[elem] = 0 - count
+        return result
+
+    def _keep_positive(self):
+        '''Internal method to strip elements with a negative or zero count'''
+        nonpositive = [elem for elem, count in self.items() if not count > 0]
+        for elem in nonpositive:
+            del self[elem]
+        return self
+
+    def __iadd__(self, other):
+        '''Inplace add from another counter, keeping only positive counts.
+
+        >>> c = Counter('abbb')
+        >>> c += Counter('bcc')
+        >>> c
+        Counter({'b': 4, 'c': 2, 'a': 1})
+
+        '''
+        for elem, count in other.items():
+            self[elem] += count
+        return self._keep_positive()
+
+    def __isub__(self, other):
+        '''Inplace subtract counter, but keep only results with positive counts.
+
+        >>> c = Counter('abbbc')
+        >>> c -= Counter('bccd')
+        >>> c
+        Counter({'b': 2, 'a': 1})
+
+        '''
+        for elem, count in other.items():
+            self[elem] -= count
+        return self._keep_positive()
+
+    def __ior__(self, other):
+        '''Inplace union is the maximum of value from either counter.
+
+        >>> c = Counter('abbb')
+        >>> c |= Counter('bcc')
+        >>> c
+        Counter({'b': 3, 'c': 2, 'a': 1})
+
+        '''
+        for elem, other_count in other.items():
+            count = self[elem]
+            if other_count > count:
+                self[elem] = other_count
+        return self._keep_positive()
+
+    def __iand__(self, other):
+        '''Inplace intersection is the minimum of corresponding counts.
+
+        >>> c = Counter('abbb')
+        >>> c &= Counter('bcc')
+        >>> c
+        Counter({'b': 1})
+
+        '''
+        for elem, count in self.items():
+            other_count = other[elem]
+            if other_count < count:
+                self[elem] = other_count
+        return self._keep_positive()
+
+
+########################################################################
+###  ChainMap
+########################################################################
+
+class ChainMap(_collections_abc.MutableMapping):
+    ''' A ChainMap groups multiple dicts (or other mappings) together
+    to create a single, updateable view.
+
+    The underlying mappings are stored in a list.  That list is public and can
+    be accessed or updated using the *maps* attribute.  There is no other
+    state.
+
+    Lookups search the underlying mappings successively until a key is found.
+    In contrast, writes, updates, and deletions only operate on the first
+    mapping.
+
+    '''
+
+    def __init__(self, *maps):
+        '''Initialize a ChainMap by setting *maps* to the given mappings.
+        If no mappings are provided, a single empty dictionary is used.
+
+        '''
+        self.maps = list(maps) or [{}]          # always at least one map
+
+    def __missing__(self, key):
+        raise KeyError(key)
+
+    def __getitem__(self, key):
+        for mapping in self.maps:
+            try:
+                return mapping[key]             # can't use 'key in mapping' with defaultdict
+            except KeyError:
+                pass
+        return self.__missing__(key)            # support subclasses that define __missing__
+
+    def get(self, key, default=None):
+        return self[key] if key in self else default
+
+    def __len__(self):
+        return len(set().union(*self.maps))     # reuses stored hash values if possible
+
+    def __iter__(self):
+        d = {}
+        for mapping in map(dict.fromkeys, reversed(self.maps)):
+            d |= mapping                        # reuses stored hash values if possible
+        return iter(d)
+
+    def __contains__(self, key):
+        return any(key in m for m in self.maps)
+
+    def __bool__(self):
+        return any(self.maps)
+
+    @_recursive_repr()
+    def __repr__(self):
+        return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})'
+
+    @classmethod
+    def fromkeys(cls, iterable, *args):
+        'Create a ChainMap with a single dict created from the iterable.'
+        return cls(dict.fromkeys(iterable, *args))
+
+    def copy(self):
+        'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]'
+        return self.__class__(self.maps[0].copy(), *self.maps[1:])
+
+    __copy__ = copy
+
+    def new_child(self, m=None, **kwargs):      # like Django's Context.push()
+        '''New ChainMap with a new map followed by all previous maps.
+        If no map is provided, an empty dict is used.
+        Keyword arguments update the map or new empty dict.
+        '''
+        if m is None:
+            m = kwargs
+        elif kwargs:
+            m.update(kwargs)
+        return self.__class__(m, *self.maps)
+
+    @property
+    def parents(self):                          # like Django's Context.pop()
+        'New ChainMap from maps[1:].'
+        return self.__class__(*self.maps[1:])
+
+    def __setitem__(self, key, value):
+        self.maps[0][key] = value
+
+    def __delitem__(self, key):
+        try:
+            del self.maps[0][key]
+        except KeyError:
+            raise KeyError(f'Key not found in the first mapping: {key!r}')
+
+    def popitem(self):
+        'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
+        try:
+            return self.maps[0].popitem()
+        except KeyError:
+            raise KeyError('No keys found in the first mapping.')
+
+    def pop(self, key, *args):
+        'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'
+        try:
+            return self.maps[0].pop(key, *args)
+        except KeyError:
+            raise KeyError(f'Key not found in the first mapping: {key!r}')
+
+    def clear(self):
+        'Clear maps[0], leaving maps[1:] intact.'
+        self.maps[0].clear()
+
+    def __ior__(self, other):
+        self.maps[0].update(other)
+        return self
+
+    def __or__(self, other):
+        if not isinstance(other, _collections_abc.Mapping):
+            return NotImplemented
+        m = self.copy()
+        m.maps[0].update(other)
+        return m
+
+    def __ror__(self, other):
+        if not isinstance(other, _collections_abc.Mapping):
+            return NotImplemented
+        m = dict(other)
+        for child in reversed(self.maps):
+            m.update(child)
+        return self.__class__(m)
+
+
+################################################################################
+### UserDict
+################################################################################
+
+class UserDict(_collections_abc.MutableMapping):
+
+    # Start by filling-out the abstract methods
+    def __init__(self, dict=None, /, **kwargs):
+        self.data = {}
+        if dict is not None:
+            self.update(dict)
+        if kwargs:
+            self.update(kwargs)
+
+    def __len__(self):
+        return len(self.data)
+
+    def __getitem__(self, key):
+        if key in self.data:
+            return self.data[key]
+        if hasattr(self.__class__, "__missing__"):
+            return self.__class__.__missing__(self, key)
+        raise KeyError(key)
+
+    def __setitem__(self, key, item):
+        self.data[key] = item
+
+    def __delitem__(self, key):
+        del self.data[key]
+
+    def __iter__(self):
+        return iter(self.data)
+
+    # Modify __contains__ and get() to work like dict
+    # does when __missing__ is present.
+    def __contains__(self, key):
+        return key in self.data
+
+    def get(self, key, default=None):
+        if key in self:
+            return self[key]
+        return default
+
+
+    # Now, add the methods in dicts but not in MutableMapping
+    def __repr__(self):
+        return repr(self.data)
+
+    def __or__(self, other):
+        if isinstance(other, UserDict):
+            return self.__class__(self.data | other.data)
+        if isinstance(other, dict):
+            return self.__class__(self.data | other)
+        return NotImplemented
+
+    def __ror__(self, other):
+        if isinstance(other, UserDict):
+            return self.__class__(other.data | self.data)
+        if isinstance(other, dict):
+            return self.__class__(other | self.data)
+        return NotImplemented
+
+    def __ior__(self, other):
+        if isinstance(other, UserDict):
+            self.data |= other.data
+        else:
+            self.data |= other
+        return self
+
+    def __copy__(self):
+        inst = self.__class__.__new__(self.__class__)
+        inst.__dict__.update(self.__dict__)
+        # Create a copy and avoid triggering descriptors
+        inst.__dict__["data"] = self.__dict__["data"].copy()
+        return inst
+
+    def copy(self):
+        if self.__class__ is UserDict:
+            return UserDict(self.data.copy())
+        import copy
+        data = self.data
+        try:
+            self.data = {}
+            c = copy.copy(self)
+        finally:
+            self.data = data
+        c.update(self)
+        return c
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        d = cls()
+        for key in iterable:
+            d[key] = value
+        return d
+
+
+################################################################################
+### UserList
+################################################################################
+
+class UserList(_collections_abc.MutableSequence):
+    """A more or less complete user-defined wrapper around list objects."""
+
+    def __init__(self, initlist=None):
+        self.data = []
+        if initlist is not None:
+            # XXX should this accept an arbitrary sequence?
+            if type(initlist) == type(self.data):
+                self.data[:] = initlist
+            elif isinstance(initlist, UserList):
+                self.data[:] = initlist.data[:]
+            else:
+                self.data = list(initlist)
+
+    def __repr__(self):
+        return repr(self.data)
+
+    def __lt__(self, other):
+        return self.data < self.__cast(other)
+
+    def __le__(self, other):
+        return self.data <= self.__cast(other)
+
+    def __eq__(self, other):
+        return self.data == self.__cast(other)
+
+    def __gt__(self, other):
+        return self.data > self.__cast(other)
+
+    def __ge__(self, other):
+        return self.data >= self.__cast(other)
+
+    def __cast(self, other):
+        return other.data if isinstance(other, UserList) else other
+
+    def __contains__(self, item):
+        return item in self.data
+
+    def __len__(self):
+        return len(self.data)
+
+    def __getitem__(self, i):
+        if isinstance(i, slice):
+            return self.__class__(self.data[i])
+        else:
+            return self.data[i]
+
+    def __setitem__(self, i, item):
+        self.data[i] = item
+
+    def __delitem__(self, i):
+        del self.data[i]
+
+    def __add__(self, other):
+        if isinstance(other, UserList):
+            return self.__class__(self.data + other.data)
+        elif isinstance(other, type(self.data)):
+            return self.__class__(self.data + other)
+        return self.__class__(self.data + list(other))
+
+    def __radd__(self, other):
+        if isinstance(other, UserList):
+            return self.__class__(other.data + self.data)
+        elif isinstance(other, type(self.data)):
+            return self.__class__(other + self.data)
+        return self.__class__(list(other) + self.data)
+
+    def __iadd__(self, other):
+        if isinstance(other, UserList):
+            self.data += other.data
+        elif isinstance(other, type(self.data)):
+            self.data += other
+        else:
+            self.data += list(other)
+        return self
+
+    def __mul__(self, n):
+        return self.__class__(self.data * n)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, n):
+        self.data *= n
+        return self
+
+    def __copy__(self):
+        inst = self.__class__.__new__(self.__class__)
+        inst.__dict__.update(self.__dict__)
+        # Create a copy and avoid triggering descriptors
+        inst.__dict__["data"] = self.__dict__["data"][:]
+        return inst
+
+    def append(self, item):
+        self.data.append(item)
+
+    def insert(self, i, item):
+        self.data.insert(i, item)
+
+    def pop(self, i=-1):
+        return self.data.pop(i)
+
+    def remove(self, item):
+        self.data.remove(item)
+
+    def clear(self):
+        self.data.clear()
+
+    def copy(self):
+        return self.__class__(self)
+
+    def count(self, item):
+        return self.data.count(item)
+
+    def index(self, item, *args):
+        return self.data.index(item, *args)
+
+    def reverse(self):
+        self.data.reverse()
+
+    def sort(self, /, *args, **kwds):
+        self.data.sort(*args, **kwds)
+
+    def extend(self, other):
+        if isinstance(other, UserList):
+            self.data.extend(other.data)
+        else:
+            self.data.extend(other)
+
+
+################################################################################
+### UserString
+################################################################################
+
+class UserString(_collections_abc.Sequence):
+
+    def __init__(self, seq):
+        if isinstance(seq, str):
+            self.data = seq
+        elif isinstance(seq, UserString):
+            self.data = seq.data[:]
+        else:
+            self.data = str(seq)
+
+    def __str__(self):
+        return str(self.data)
+
+    def __repr__(self):
+        return repr(self.data)
+
+    def __int__(self):
+        return int(self.data)
+
+    def __float__(self):
+        return float(self.data)
+
+    def __complex__(self):
+        return complex(self.data)
+
+    def __hash__(self):
+        return hash(self.data)
+
+    def __getnewargs__(self):
+        return (self.data[:],)
+
+    def __eq__(self, string):
+        if isinstance(string, UserString):
+            return self.data == string.data
+        return self.data == string
+
+    def __lt__(self, string):
+        if isinstance(string, UserString):
+            return self.data < string.data
+        return self.data < string
+
+    def __le__(self, string):
+        if isinstance(string, UserString):
+            return self.data <= string.data
+        return self.data <= string
+
+    def __gt__(self, string):
+        if isinstance(string, UserString):
+            return self.data > string.data
+        return self.data > string
+
+    def __ge__(self, string):
+        if isinstance(string, UserString):
+            return self.data >= string.data
+        return self.data >= string
+
+    def __contains__(self, char):
+        if isinstance(char, UserString):
+            char = char.data
+        return char in self.data
+
+    def __len__(self):
+        return len(self.data)
+
+    def __getitem__(self, index):
+        return self.__class__(self.data[index])
+
+    def __add__(self, other):
+        if isinstance(other, UserString):
+            return self.__class__(self.data + other.data)
+        elif isinstance(other, str):
+            return self.__class__(self.data + other)
+        return self.__class__(self.data + str(other))
+
+    def __radd__(self, other):
+        if isinstance(other, str):
+            return self.__class__(other + self.data)
+        return self.__class__(str(other) + self.data)
+
+    def __mul__(self, n):
+        return self.__class__(self.data * n)
+
+    __rmul__ = __mul__
+
+    def __mod__(self, args):
+        return self.__class__(self.data % args)
+
+    def __rmod__(self, template):
+        return self.__class__(str(template) % self)
+
+    # the following methods are defined in alphabetical order:
+    def capitalize(self):
+        return self.__class__(self.data.capitalize())
+
+    def casefold(self):
+        return self.__class__(self.data.casefold())
+
+    def center(self, width, *args):
+        return self.__class__(self.data.center(width, *args))
+
+    def count(self, sub, start=0, end=_sys.maxsize):
+        if isinstance(sub, UserString):
+            sub = sub.data
+        return self.data.count(sub, start, end)
+
+    def removeprefix(self, prefix, /):
+        if isinstance(prefix, UserString):
+            prefix = prefix.data
+        return self.__class__(self.data.removeprefix(prefix))
+
+    def removesuffix(self, suffix, /):
+        if isinstance(suffix, UserString):
+            suffix = suffix.data
+        return self.__class__(self.data.removesuffix(suffix))
+
+    def encode(self, encoding='utf-8', errors='strict'):
+        encoding = 'utf-8' if encoding is None else encoding
+        errors = 'strict' if errors is None else errors
+        return self.data.encode(encoding, errors)
+
+    def endswith(self, suffix, start=0, end=_sys.maxsize):
+        return self.data.endswith(suffix, start, end)
+
+    def expandtabs(self, tabsize=8):
+        return self.__class__(self.data.expandtabs(tabsize))
+
+    def find(self, sub, start=0, end=_sys.maxsize):
+        if isinstance(sub, UserString):
+            sub = sub.data
+        return self.data.find(sub, start, end)
+
+    def format(self, /, *args, **kwds):
+        return self.data.format(*args, **kwds)
+
+    def format_map(self, mapping):
+        return self.data.format_map(mapping)
+
+    def index(self, sub, start=0, end=_sys.maxsize):
+        return self.data.index(sub, start, end)
+
+    def isalpha(self):
+        return self.data.isalpha()
+
+    def isalnum(self):
+        return self.data.isalnum()
+
+    def isascii(self):
+        return self.data.isascii()
+
+    def isdecimal(self):
+        return self.data.isdecimal()
+
+    def isdigit(self):
+        return self.data.isdigit()
+
+    def isidentifier(self):
+        return self.data.isidentifier()
+
+    def islower(self):
+        return self.data.islower()
+
+    def isnumeric(self):
+        return self.data.isnumeric()
+
+    def isprintable(self):
+        return self.data.isprintable()
+
+    def isspace(self):
+        return self.data.isspace()
+
+    def istitle(self):
+        return self.data.istitle()
+
+    def isupper(self):
+        return self.data.isupper()
+
+    def join(self, seq):
+        return self.data.join(seq)
+
+    def ljust(self, width, *args):
+        return self.__class__(self.data.ljust(width, *args))
+
+    def lower(self):
+        return self.__class__(self.data.lower())
+
+    def lstrip(self, chars=None):
+        return self.__class__(self.data.lstrip(chars))
+
+    maketrans = str.maketrans
+
+    def partition(self, sep):
+        return self.data.partition(sep)
+
+    def replace(self, old, new, maxsplit=-1):
+        if isinstance(old, UserString):
+            old = old.data
+        if isinstance(new, UserString):
+            new = new.data
+        return self.__class__(self.data.replace(old, new, maxsplit))
+
+    def rfind(self, sub, start=0, end=_sys.maxsize):
+        if isinstance(sub, UserString):
+            sub = sub.data
+        return self.data.rfind(sub, start, end)
+
+    def rindex(self, sub, start=0, end=_sys.maxsize):
+        return self.data.rindex(sub, start, end)
+
+    def rjust(self, width, *args):
+        return self.__class__(self.data.rjust(width, *args))
+
+    def rpartition(self, sep):
+        return self.data.rpartition(sep)
+
+    def rstrip(self, chars=None):
+        return self.__class__(self.data.rstrip(chars))
+
+    def split(self, sep=None, maxsplit=-1):
+        return self.data.split(sep, maxsplit)
+
+    def rsplit(self, sep=None, maxsplit=-1):
+        return self.data.rsplit(sep, maxsplit)
+
+    def splitlines(self, keepends=False):
+        return self.data.splitlines(keepends)
+
+    def startswith(self, prefix, start=0, end=_sys.maxsize):
+        return self.data.startswith(prefix, start, end)
+
+    def strip(self, chars=None):
+        return self.__class__(self.data.strip(chars))
+
+    def swapcase(self):
+        return self.__class__(self.data.swapcase())
+
+    def title(self):
+        return self.__class__(self.data.title())
+
+    def translate(self, *args):
+        return self.__class__(self.data.translate(*args))
+
+    def upper(self):
+        return self.__class__(self.data.upper())
+
+    def zfill(self, width):
+        return self.__class__(self.data.zfill(width))
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/index.html b/website-old/pages/api/_modules/index.html new file mode 100644 index 0000000000..4e4f85f1a1 --- /dev/null +++ b/website-old/pages/api/_modules/index.html @@ -0,0 +1,219 @@ + + + + + + + +
+
+
+
+

All modules for which code is available

+ +
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/_modules/torch/distributions/utils.html b/website-old/pages/api/_modules/torch/distributions/utils.html new file mode 100644 index 0000000000..c809c8c5f5 --- /dev/null +++ b/website-old/pages/api/_modules/torch/distributions/utils.html @@ -0,0 +1,258 @@ + + + + + + + +
+
+
+
+

Source code for torch.distributions.utils

+# mypy: allow-untyped-defs
+from functools import update_wrapper
+from numbers import Number
+from typing import Any, Dict
+
+import torch
+import torch.nn.functional as F
+from torch.overrides import is_tensor_like
+
+
+euler_constant = 0.57721566490153286060  # Euler Mascheroni Constant
+
+__all__ = [
+    "broadcast_all",
+    "logits_to_probs",
+    "clamp_probs",
+    "probs_to_logits",
+    "lazy_property",
+    "tril_matrix_to_vec",
+    "vec_to_tril_matrix",
+]
+
+
+def broadcast_all(*values):
+    r"""
+    Given a list of values (possibly containing numbers), returns a list where each
+    value is broadcasted based on the following rules:
+      - `torch.*Tensor` instances are broadcasted as per :ref:`_broadcasting-semantics`.
+      - numbers.Number instances (scalars) are upcast to tensors having
+        the same size and type as the first tensor passed to `values`.  If all the
+        values are scalars, then they are upcasted to scalar Tensors.
+
+    Args:
+        values (list of `numbers.Number`, `torch.*Tensor` or objects implementing __torch_function__)
+
+    Raises:
+        ValueError: if any of the values is not a `numbers.Number` instance,
+            a `torch.*Tensor` instance, or an instance implementing __torch_function__
+    """
+    if not all(is_tensor_like(v) or isinstance(v, Number) for v in values):
+        raise ValueError(
+            "Input arguments must all be instances of numbers.Number, "
+            "torch.Tensor or objects implementing __torch_function__."
+        )
+    if not all(is_tensor_like(v) for v in values):
+        options: Dict[str, Any] = dict(dtype=torch.get_default_dtype())
+        for value in values:
+            if isinstance(value, torch.Tensor):
+                options = dict(dtype=value.dtype, device=value.device)
+                break
+        new_values = [
+            v if is_tensor_like(v) else torch.tensor(v, **options) for v in values
+        ]
+        return torch.broadcast_tensors(*new_values)
+    return torch.broadcast_tensors(*values)
+
+
+def _standard_normal(shape, dtype, device):
+    if torch._C._get_tracing_state():
+        # [JIT WORKAROUND] lack of support for .normal_()
+        return torch.normal(
+            torch.zeros(shape, dtype=dtype, device=device),
+            torch.ones(shape, dtype=dtype, device=device),
+        )
+    return torch.empty(shape, dtype=dtype, device=device).normal_()
+
+
+def _sum_rightmost(value, dim):
+    r"""
+    Sum out ``dim`` many rightmost dimensions of a given tensor.
+
+    Args:
+        value (Tensor): A tensor of ``.dim()`` at least ``dim``.
+        dim (int): The number of rightmost dims to sum out.
+    """
+    if dim == 0:
+        return value
+    required_shape = value.shape[:-dim] + (-1,)
+    return value.reshape(required_shape).sum(-1)
+
+
+def logits_to_probs(logits, is_binary=False):
+    r"""
+    Converts a tensor of logits into probabilities. Note that for the
+    binary case, each value denotes log odds, whereas for the
+    multi-dimensional case, the values along the last dimension denote
+    the log probabilities (possibly unnormalized) of the events.
+    """
+    if is_binary:
+        return torch.sigmoid(logits)
+    return F.softmax(logits, dim=-1)
+
+
+def clamp_probs(probs):
+    """Clamps the probabilities to be in the open interval `(0, 1)`.
+
+    The probabilities would be clamped between `eps` and `1 - eps`,
+    and `eps` would be the smallest representable positive number for the input data type.
+
+    Args:
+        probs (Tensor): A tensor of probabilities.
+
+    Returns:
+        Tensor: The clamped probabilities.
+
+    Examples:
+        >>> probs = torch.tensor([0.0, 0.5, 1.0])
+        >>> clamp_probs(probs)
+        tensor([1.1921e-07, 5.0000e-01, 1.0000e+00])
+
+        >>> probs = torch.tensor([0.0, 0.5, 1.0], dtype=torch.float64)
+        >>> clamp_probs(probs)
+        tensor([2.2204e-16, 5.0000e-01, 1.0000e+00], dtype=torch.float64)
+
+    """
+    eps = torch.finfo(probs.dtype).eps
+    return probs.clamp(min=eps, max=1 - eps)
+
+
+def probs_to_logits(probs, is_binary=False):
+    r"""
+    Converts a tensor of probabilities into logits. For the binary case,
+    this denotes the probability of occurrence of the event indexed by `1`.
+    For the multi-dimensional case, the values along the last dimension
+    denote the probabilities of occurrence of each of the events.
+    """
+    ps_clamped = clamp_probs(probs)
+    if is_binary:
+        return torch.log(ps_clamped) - torch.log1p(-ps_clamped)
+    return torch.log(ps_clamped)
+
+
+class lazy_property:
+    r"""
+    Used as a decorator for lazy loading of class attributes. This uses a
+    non-data descriptor that calls the wrapped method to compute the property on
+    first call; thereafter replacing the wrapped method into an instance
+    attribute.
+    """
+
+    def __init__(self, wrapped):
+        self.wrapped = wrapped
+        update_wrapper(self, wrapped)  # type:ignore[arg-type]
+
+    def __get__(self, instance, obj_type=None):
+        if instance is None:
+            return _lazy_property_and_property(self.wrapped)
+        with torch.enable_grad():
+            value = self.wrapped(instance)
+        setattr(instance, self.wrapped.__name__, value)
+        return value
+
+
+class _lazy_property_and_property(lazy_property, property):
+    """We want lazy properties to look like multiple things.
+
+    * property when Sphinx autodoc looks
+    * lazy_property when Distribution validate_args looks
+    """
+
+    def __init__(self, wrapped):
+        property.__init__(self, wrapped)
+
+
+def tril_matrix_to_vec(mat: torch.Tensor, diag: int = 0) -> torch.Tensor:
+    r"""
+    Convert a `D x D` matrix or a batch of matrices into a (batched) vector
+    which comprises of lower triangular elements from the matrix in row order.
+    """
+    n = mat.shape[-1]
+    if not torch._C._get_tracing_state() and (diag < -n or diag >= n):
+        raise ValueError(f"diag ({diag}) provided is outside [{-n}, {n-1}].")
+    arange = torch.arange(n, device=mat.device)
+    tril_mask = arange < arange.view(-1, 1) + (diag + 1)
+    vec = mat[..., tril_mask]
+    return vec
+
+
+def vec_to_tril_matrix(vec: torch.Tensor, diag: int = 0) -> torch.Tensor:
+    r"""
+    Convert a vector or a batch of vectors into a batched `D x D`
+    lower triangular matrix containing elements from the vector in row order.
+    """
+    # +ve root of D**2 + (1+2*diag)*D - |diag| * (diag+1) - 2*vec.shape[-1] = 0
+    n = (
+        -(1 + 2 * diag)
+        + ((1 + 2 * diag) ** 2 + 8 * vec.shape[-1] + 4 * abs(diag) * (diag + 1)) ** 0.5
+    ) / 2
+    eps = torch.finfo(vec.dtype).eps
+    if not torch._C._get_tracing_state() and (round(n) - n > eps):
+        raise ValueError(
+            f"The size of last dimension is {vec.shape[-1]} which cannot be expressed as "
+            + "the lower triangular part of a square D x D matrix."
+        )
+    n = round(n.item()) if isinstance(n, torch.Tensor) else round(n)
+    mat = vec.new_zeros(vec.shape[:-1] + torch.Size((n, n)))
+    arange = torch.arange(n, device=vec.device)
+    tril_mask = arange < arange.view(-1, 1) + (diag + 1)
+    mat[..., tril_mask] = vec
+    return mat
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/acquisition.html b/website-old/pages/api/acquisition.html new file mode 100644 index 0000000000..c7d743df63 --- /dev/null +++ b/website-old/pages/api/acquisition.html @@ -0,0 +1,8265 @@ + + + + + + + +
+
+
+
+
+

botorch.acquisition

+
+

Acquisition Function APIs

+
+

Abstract Acquisition Function APIs

+

Abstract base module for all botorch acquisition functions.

+
+
+class botorch.acquisition.acquisition.AcquisitionFunction(model)[source]
+

Bases: Module, ABC

+

Abstract base class for acquisition functions.

+

Please note that if your acquisition requires a backwards call, +you will need to wrap the backwards call inside of an enable_grad +context to be able to optimize the acquisition. See #1164.

+

Constructor for the AcquisitionFunction base class.

+
+
Parameters:
+

model (Model) – A fitted model.

+
+
+
+
+set_X_pending(X_pending=None)[source]
+

Informs the acquisition function about pending design points.

+
+
Parameters:
+

X_pending (Tensor | None) – n x d Tensor with n d-dim design points that have +been submitted for evaluation but have not yet been evaluated.

+
+
Return type:
+

None

+
+
+
+
+
+abstract forward(X)[source]
+

Evaluate the acquisition function on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b) x q x d-dim Tensor of (b) t-batches with q d-dim +design points each.

+
+
Returns:
+

A (b)-dim Tensor of acquisition function values at the given +design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.acquisition.OneShotAcquisitionFunction(model)[source]
+

Bases: AcquisitionFunction, ABC

+

Abstract base class for acquisition functions using one-shot optimization

+

Constructor for the AcquisitionFunction base class.

+
+
Parameters:
+

model (Model) – A fitted model.

+
+
+
+
+abstract get_augmented_q_batch_size(q)[source]
+

Get augmented q batch size for one-shot optimization.

+
+
Parameters:
+

q (int) – The number of candidates to consider jointly.

+
+
Returns:
+

The augmented size for one-shot optimization (including variables +parameterizing the fantasy solutions).

+
+
Return type:
+

int

+
+
+
+
+
+abstract extract_candidates(X_full)[source]
+

Extract the candidates from a full “one-shot” parameterization.

+
+
Parameters:
+

X_full (Tensor) – A b x q_aug x d-dim Tensor with b t-batches of q_aug +design points each.

+
+
Returns:
+

A b x q x d-dim Tensor with b t-batches of q design points each.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.acquisition.MCSamplerMixin(sampler=None)[source]
+

Bases: ABC

+

A mix-in for adding sampler functionality into an acquisition function class.

+
+
+_default_sample_shape
+

The sample_shape for the default sampler.

+
+

Register the sampler on the acquisition function.

+
+
Parameters:
+

sampler (MCSampler | None) – The sampler used to draw base samples for MC-based acquisition +functions. If None, a sampler is generated on the fly within +the get_posterior_samples method using get_sampler.

+
+
+
+
+get_posterior_samples(posterior)[source]
+

Sample from the posterior using the sampler.

+
+
Parameters:
+

posterior (Posterior) – The posterior to sample from.

+
+
Return type:
+

Tensor

+
+
+
+
+
+property sample_shape: Size
+
+
+
+
+class botorch.acquisition.acquisition.MultiModelAcquisitionFunction(model_dict)[source]
+

Bases: AcquisitionFunction, ABC

+

Abstract base class for acquisition functions that require +multiple types of models.

+

The intended use case for these acquisition functions are those +where we have multiple models, each serving a distinct purpose. +As an example, we can have a “regression” model that predicts +one or more outcomes, and a “classification” model that predicts +the probabilty that a given parameterization is feasible. The +multi-model acquisition function can then weight the acquisition +value computed with the “regression” model with the feasibility +value predicted by the “classification” model to produce the +composite acquisition value.

+

This is currently only a placeholder to help with some development +in Ax. We plan to add some acquisition functions utilizing multiple +models in the future.

+

Constructor for the MultiModelAcquisitionFunction base class.

+
+
Parameters:
+

model_dict (ModelDict) – A ModelDict mapping labels to models.

+
+
+
+
+
+

Analytic Acquisition Function API

+
+
+class botorch.acquisition.analytic.AnalyticAcquisitionFunction(model, posterior_transform=None)[source]
+

Bases: AcquisitionFunction, ABC

+

Base class for analytic acquisition functions.

+

Base constructor for analytic acquisition functions.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome model.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
+
+
+
+
+set_X_pending(X_pending=None)[source]
+

Informs the acquisition function about pending design points.

+
+
Parameters:
+

X_pending (Tensor | None) – n x d Tensor with n d-dim design points that have +been submitted for evaluation but have not yet been evaluated.

+
+
Return type:
+

None

+
+
+
+
+
+
+

Cached Cholesky Acquisition Function API

+

Abstract class for acquisition functions leveraging a cached Cholesky +decomposition of the posterior covariance over f(X_baseline).

+
+
+botorch.acquisition.cached_cholesky.supports_cache_root(model)[source]
+

Checks if a model supports the cache_root functionality. +The two criteria are that the model is not multi-task and the model +produces a GPyTorchPosterior.

+
+
Parameters:
+

model (Model)

+
+
Return type:
+

bool

+
+
+
+
+
+class botorch.acquisition.cached_cholesky.CachedCholeskyMCSamplerMixin(model, cache_root=False, sampler=None)[source]
+

Bases: MCSamplerMixin

+

Abstract Mixin class for acquisition functions using a cached Cholesky.

+

Specifically, this is for acquisition functions that require sampling from +the posterior P(f(X_baseline, X) | D). The Cholesky of the posterior +covariance over f(X_baseline) is cached.

+

Set class attributes and perform compatibility checks.

+
+
Parameters:
+
    +
  • model (Model) – A model.

  • +
  • cache_root (bool) – A boolean indicating whether to cache the Cholesky. +This might be overridden in the model is not compatible.

  • +
  • sampler (MCSampler | None) – An optional MCSampler object.

  • +
+
+
+
+
+
+

Decoupled Acquisition Function API

+

Abstract base module for decoupled acquisition functions.

+
+
+class botorch.acquisition.decoupled.DecoupledAcquisitionFunction(model, X_evaluation_mask=None, **kwargs)[source]
+

Bases: AcquisitionFunction, ABC

+

Abstract base class for decoupled acquisition functions. +A decoupled acquisition function where one may intend to +evaluate a design on only a subset of the outcomes. +Typically this would be handled by fantasizing, where one +would fantasize as to what the partial observation would +be if one were to evaluate a design on the subset of +outcomes (e.g. you only fantasize at those outcomes). The +X_evaluation_mask specifies which outcomes should be +evaluated for each design. X_evaluation_mask is q x m, +where there are q design points in the batch and m outcomes. +In the asynchronous case, where there are n’ pending points, +we need to track which outcomes each pending point should be +evaluated on. In this case, we concatenate +X_pending_evaluation_mask with X_evaluation_mask to obtain +the full evaluation_mask.

+

This abstract class handles generating and updating an evaluation mask, +which is a boolean tensor indicating which outcomes a given design is +being evaluated on. The evaluation mask has shape (n’ + q) x m, where +n’ is the number of pending points and the q represents the new +candidates to be generated.

+

If X(_pending)_evaluation_mas`k is None, it is assumed that `X(_pending) +will be evaluated on all outcomes.

+

Initialize.

+
+
Parameters:
+
    +
  • model (ModelList) – A model

  • +
  • X_evaluation_mask (Tensor | None) – A q x m-dim boolean tensor +indicating which outcomes the decoupled acquisition +function should generate new candidates for.

  • +
+
+
+
+
+property X_evaluation_mask: Tensor | None
+

Get the evaluation indices for the new candidate.

+
+
+
+set_X_pending(X_pending=None, X_pending_evaluation_mask=None)[source]
+

Informs the AF about pending design points for different outcomes.

+
+
Parameters:
+
    +
  • X_pending (Tensor | None) – A n’ x d Tensor with n’ d-dim design points that have +been submitted for evaluation but have not yet been evaluated.

  • +
  • X_pending_evaluation_mask (Tensor | None) – A n’ x m-dim tensor of booleans indicating +for which outputs the pending point is being evaluated on. If +X_pending_evaluation_mask is None, it is assumed that +X_pending will be evaluated on all outcomes.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+construct_evaluation_mask(X)[source]
+

Construct the boolean evaluation mask for X and X_pending

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of designs.

+
+
Returns:
+

A n + n’ x m-dim tensor of booleans indicating +which outputs should be evaluated.

+
+
Return type:
+

Tensor | None

+
+
+
+
+
+
+

Monte-Carlo Acquisition Function API

+
+
+class botorch.acquisition.monte_carlo.MCAcquisitionFunction(model, sampler=None, objective=None, posterior_transform=None, X_pending=None)[source]
+

Bases: AcquisitionFunction, MCSamplerMixin, ABC

+

Abstract base class for Monte-Carlo based batch acquisition functions.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated on the fly within the +get_posterior_samples method using +botorch.sampling.get_sampler. +NOTE: For posteriors that do not support base samples, +a sampler compatible with intended use case must be provided. +See ForkedRNGSampler and StochasticSampler as examples.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A batch_shape, m x d-dim Tensor of m design points +that have points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
+
+
+
+
+abstract forward(X)[source]
+

Takes in a batch_shape x q x d X Tensor of t-batches with q d-dim +design points each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X. Should +utilize the result of set_X_pending as needed to account for pending function +evaluations.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Base Classes for Multi-Objective Acquisition Function API

+

Base classes for multi-objective acquisition functions.

+
+
+class botorch.acquisition.multi_objective.base.MultiObjectiveAnalyticAcquisitionFunction(model, posterior_transform=None)[source]
+

Bases: AcquisitionFunction

+

Abstract base class for Multi-Objective batch acquisition functions.

+

Constructor for the MultiObjectiveAnalyticAcquisitionFunction base class.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
+
+
+
+
+abstract forward(X)[source]
+

Takes in a batch_shape x 1 x d X Tensor of t-batches with 1 d-dim +design point each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+set_X_pending(X_pending=None)[source]
+

Informs the acquisition function about pending design points.

+
+
Parameters:
+

X_pending (Tensor | None) – n x d Tensor with n d-dim design points that have +been submitted for evaluation but have not yet been evaluated.

+
+
Return type:
+

None

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.base.MultiObjectiveMCAcquisitionFunction(model, sampler=None, objective=None, constraints=None, eta=0.001, X_pending=None)[source]
+

Bases: AcquisitionFunction, MCSamplerMixin, ABC

+

Abstract base class for Multi-Objective batch acquisition functions.

+

NOTE: This does not inherit from MCAcquisitionFunction to avoid circular imports.

+
+
Parameters:
+
    +
  • _default_sample_shape – The sample_shape for the default sampler.

  • +
  • model (Model)

  • +
  • sampler (MCSampler | None)

  • +
  • objective (MCMultiOutputObjective | None)

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None)

  • +
  • eta (Tensor | float)

  • +
  • X_pending (Tensor | None)

  • +
+
+
+

Constructor for the MultiObjectiveMCAcquisitionFunction base class.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler. +NOTE: For posteriors that do not support base samples, +a sampler compatible with intended use case must be provided. +See ForkedRNGSampler and StochasticSampler as examples.

  • +
  • objective (MCMultiOutputObjective | None) – The MCMultiOutputObjective under which the samples are +evaluated. Defaults to IdentityMCMultiOutputObjective().

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function used for the +differentiable approximation of the constraints. In case of a float the +same eta is used for every constraint in constraints. In case of a +tensor the length of the tensor must match the number of provided +constraints. The i-th constraint is then estimated with the i-th +eta value.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
+
+
+
+
+abstract forward(X)[source]
+

Takes in a batch_shape x q x d X Tensor of t-batches with q d-dim +design points each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X. Should +utilize the result of set_X_pending as needed to account for pending function +evaluations.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+
+

Acquisition Functions

+
+

Analytic Acquisition Functions

+

Analytic Acquisition Functions that evaluate the posterior without performing +Monte-Carlo sampling.

+
+
+class botorch.acquisition.analytic.LogProbabilityOfImprovement(model, best_f, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Log Probability of Improvement.

+

Logarithm of the probability of improvement over the current best observed value, +computed using the analytic formula under a Normal posterior distribution. Only +supports the case of q=1. Requires the posterior to be Gaussian. The model must be +single-outcome.

+

The logarithm of the probability of improvement is numerically better behaved +than the original function, which can lead to significantly improved optimization +of the acquisition function. This is analogous to the common practice of optimizing +the log likelihood of a probabilistic model - rather the likelihood - for the +sake of maximium likelihood estimation.

+

logPI(x) = log(P(y >= best_f)), y ~ f(x)

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> LogPI = LogProbabilityOfImprovement(model, best_f=0.2)
+>>> log_pi = LogPI(test_X)
+
+
+

Single-outcome Probability of Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome model.

  • +
  • best_f (float | Tensor) – Either a scalar or a b-dim Tensor (batch mode) representing +the best function value observed so far (assumed noiseless).

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the Log Probability of Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A (b1 x … bk)-dim tensor of Log Probability of Improvement values at +the given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.ProbabilityOfImprovement(model, best_f, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Probability of Improvement.

+

Probability of improvement over the current best observed value, computed +using the analytic formula under a Normal posterior distribution. Only +supports the case of q=1. Requires the posterior to be Gaussian. The model +must be single-outcome.

+

PI(x) = P(y >= best_f), y ~ f(x)

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> PI = ProbabilityOfImprovement(model, best_f=0.2)
+>>> pi = PI(test_X)
+
+
+

Single-outcome Probability of Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome model.

  • +
  • best_f (float | Tensor) – Either a scalar or a b-dim Tensor (batch mode) representing +the best function value observed so far (assumed noiseless).

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the Probability of Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A (b1 x … bk)-dim tensor of Probability of Improvement values at the +given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.qAnalyticProbabilityOfImprovement(model, best_f, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Approximate, single-outcome batch Probability of Improvement using MVNXPB.

+

This implementation uses MVNXPB, a bivariate conditioning algorithm for +approximating P(a <= Y <= b) for multivariate normal Y. +See [Trinh2015bivariate]. This (analytic) approximate q-PI is given by +approx-qPI(X) = P(max Y >= best_f) = 1 - P(Y < best_f), Y ~ f(X), +X = (x_1,…,x_q), where P(Y < best_f) is estimated using MVNXPB.

+

qPI using an analytic approximation.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome model.

  • +
  • best_f (float | Tensor) – Either a scalar or a b-dim Tensor (batch mode) representing +the best function value observed so far (assumed noiseless).

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate approximate qPI on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d-dim Tensor of t-batches with q d-dim design +points each

+
+
Returns:
+

A batch_shape-dim Tensor of approximate Probability of Improvement values +at the given design points X, where batch_shape’ is the broadcasted +batch shape of model and input X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.ExpectedImprovement(model, best_f, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Expected Improvement (analytic).

+

Computes classic Expected Improvement over the current best observed value, +using the analytic formula for a Normal posterior distribution. Unlike the +MC-based acquisition functions, this relies on the posterior at single test +point being Gaussian (and require the posterior to implement mean and +variance properties). Only supports the case of q=1. The model must be +single-outcome.

+

EI(x) = E(max(f(x) - best_f, 0)),

+

where the expectation is taken over the value of stochastic function f at x.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> EI = ExpectedImprovement(model, best_f=0.2)
+>>> ei = EI(test_X)
+
+
+

NOTE: It is strongly recommended to use LogExpectedImprovement instead of regular +EI, as it can lead to substantially improved BO performance through improved +numerics. See https://arxiv.org/abs/2310.20708 for details.

+

Single-outcome Expected Improvement (analytic).

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome model.

  • +
  • best_f (float | Tensor) – Either a scalar or a b-dim Tensor (batch mode) representing +the best function value observed so far (assumed noiseless).

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate Expected Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points. +Expected Improvement is computed for each point individually, +i.e., what is considered are the marginal posteriors, not the +joint.

+
+
Returns:
+

A (b1 x … bk)-dim tensor of Expected Improvement values at the +given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.LogExpectedImprovement(model, best_f, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Log Expected Improvement (analytic).

+

Computes the logarithm of the classic Expected Improvement acquisition function, in +a numerically robust manner. In particular, the implementation takes special care +to avoid numerical issues in the computation of the acquisition value and its +gradient in regions where improvement is predicted to be virtually impossible.

+

See [Ament2023logei] for details. Formally,

+

LogEI(x) = log(E(max(f(x) - best_f, 0))),

+

where the expectation is taken over the value of stochastic function f at x.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> LogEI = LogExpectedImprovement(model, best_f=0.2)
+>>> ei = LogEI(test_X)
+
+
+

Logarithm of single-outcome Expected Improvement (analytic).

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome model.

  • +
  • best_f (float | Tensor) – Either a scalar or a b-dim Tensor (batch mode) representing +the best function value observed so far (assumed noiseless).

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate logarithm of Expected Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points. +Expected Improvement is computed for each point individually, +i.e., what is considered are the marginal posteriors, not the +joint.

+
+
Returns:
+

A (b1 x … bk)-dim tensor of the logarithm of the Expected Improvement +values at the given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.LogConstrainedExpectedImprovement(model, best_f, objective_index, constraints, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Log Constrained Expected Improvement (feasibility-weighted).

+

Computes the logarithm of the analytic expected improvement for a Normal posterior +distribution weighted by a probability of feasibility. The objective and +constraints are assumed to be independent and have Gaussian posterior +distributions. Only supports non-batch mode (i.e. q=1). The model should be +multi-outcome, with the index of the objective and constraints passed to +the constructor.

+

See [Ament2023logei] for details. Formally,

+

LogConstrainedEI(x) = log(EI(x)) + Sum_i log(P(y_i in [lower_i, upper_i])),

+

where y_i ~ constraint_i(x) and lower_i, upper_i are the lower and +upper bounds for the i-th constraint, respectively.

+

Example

+

# example where the 0th output has a non-negativity constraint and +# the 1st output is the objective +>>> model = SingleTaskGP(train_X, train_Y) +>>> constraints = {0: (0.0, None)} +>>> LogCEI = LogConstrainedExpectedImprovement(model, 0.2, 1, constraints) +>>> cei = LogCEI(test_X)

+

Analytic Log Constrained Expected Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted multi-output model.

  • +
  • best_f (float | Tensor) – Either a scalar or a b-dim Tensor (batch mode) representing +the best feasible function value observed so far (assumed noiseless).

  • +
  • objective_index (int) – The index of the objective.

  • +
  • constraints (dict[int, tuple[float | None, float | None]]) – A dictionary of the form {i: [lower, upper]}, where +i is the output index, and lower and upper are lower and upper +bounds on that output (resp. interpreted as -Inf / Inf if None)

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate Constrained Log Expected Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b) x 1 x d-dim Tensor of (b) t-batches of d-dim design +points each.

+
+
Returns:
+

A (b)-dim Tensor of Log Expected Improvement values at the given +design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.ConstrainedExpectedImprovement(model, best_f, objective_index, constraints, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Constrained Expected Improvement (feasibility-weighted).

+

Computes the analytic expected improvement for a Normal posterior +distribution, weighted by a probability of feasibility. The objective and +constraints are assumed to be independent and have Gaussian posterior +distributions. Only supports non-batch mode (i.e. q=1). The model should be +multi-outcome, with the index of the objective and constraints passed to +the constructor.

+

Constrained_EI(x) = EI(x) * Product_i P(y_i in [lower_i, upper_i]), +where y_i ~ constraint_i(x) and lower_i, upper_i are the lower and +upper bounds for the i-th constraint, respectively.

+

Example

+

# example where the 0th output has a non-negativity constraint and +# 1st output is the objective +>>> model = SingleTaskGP(train_X, train_Y) +>>> constraints = {0: (0.0, None)} +>>> cEI = ConstrainedExpectedImprovement(model, 0.2, 1, constraints) +>>> cei = cEI(test_X)

+

NOTE: It is strongly recommended to use LogConstrainedExpectedImprovement instead +of regular CEI, as it can lead to substantially improved BO performance through +improved numerics. See https://arxiv.org/abs/2310.20708 for details.

+

Analytic Constrained Expected Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted multi-output model.

  • +
  • best_f (float | Tensor) – Either a scalar or a b-dim Tensor (batch mode) representing +the best feasible function value observed so far (assumed noiseless).

  • +
  • objective_index (int) – The index of the objective.

  • +
  • constraints (dict[int, tuple[float | None, float | None]]) – A dictionary of the form {i: [lower, upper]}, where +i is the output index, and lower and upper are lower and upper +bounds on that output (resp. interpreted as -Inf / Inf if None)

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate Constrained Expected Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b) x 1 x d-dim Tensor of (b) t-batches of d-dim design +points each.

+
+
Returns:
+

A (b)-dim Tensor of Expected Improvement values at the given +design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.LogNoisyExpectedImprovement(model, X_observed, num_fantasies=20, maximize=True, posterior_transform=None)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Log Noisy Expected Improvement (via fantasies).

+

This computes Log Noisy Expected Improvement by averaging over the Expected +Improvement values of a number of fantasy models. Only supports the case +q=1. Assumes that the posterior distribution of the model is Gaussian. +The model must be single-outcome.

+

See [Ament2023logei] for details. Formally,

+

LogNEI(x) = log(E(max(y - max Y_base), 0))), (y, Y_base) ~ f((x, X_base)),

+

where X_base are previously observed points.

+

Note: This acquisition function currently relies on using a SingleTaskGP +with known observation noise. In other words, train_Yvar must be passed +to the model. (required for noiseless fantasies).

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y, train_Yvar=train_Yvar)
+>>> LogNEI = LogNoisyExpectedImprovement(model, train_X)
+>>> nei = LogNEI(test_X)
+
+
+

Single-outcome Noisy Log Expected Improvement (via fantasies).

+
+
Parameters:
+
    +
  • model (GPyTorchModel) – A fitted single-outcome model. Only SingleTaskGP models with +known observation noise are currently supported.

  • +
  • X_observed (Tensor) – A n x d Tensor of observed points that are likely to +be the best observed points so far.

  • +
  • num_fantasies (int) – The number of fantasies to generate. The higher this +number the more accurate the model (at the expense of model +complexity and performance).

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
  • posterior_transform (PosteriorTransform | None)

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate logarithm of the mean Expected Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A b1 x … bk x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A b1 x … bk-dim tensor of Log Noisy Expected Improvement values at +the given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.NoisyExpectedImprovement(model, X_observed, num_fantasies=20, maximize=True)[source]
+

Bases: ExpectedImprovement

+

Single-outcome Noisy Expected Improvement (via fantasies).

+

This computes Noisy Expected Improvement by averaging over the Expected +Improvement values of a number of fantasy models. Only supports the case +q=1. Assumes that the posterior distribution of the model is Gaussian. +The model must be single-outcome.

+

NEI(x) = E(max(y - max Y_baseline), 0)), (y, Y_baseline) ~ f((x, X_baseline)), +where X_baseline are previously observed points.

+

Note: This acquisition function currently relies on using a SingleTaskGP +with known observation noise. In other words, train_Yvar must be passed +to the model. (required for noiseless fantasies).

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y, train_Yvar=train_Yvar)
+>>> NEI = NoisyExpectedImprovement(model, train_X)
+>>> nei = NEI(test_X)
+
+
+

NOTE: It is strongly recommended to use LogNoisyExpectedImprovement instead +of regular NEI, as it can lead to substantially improved BO performance through +improved numerics. See https://arxiv.org/abs/2310.20708 for details.

+

Single-outcome Noisy Expected Improvement (via fantasies).

+
+
Parameters:
+
    +
  • model (GPyTorchModel) – A fitted single-outcome model. Only SingleTaskGP models with +known observation noise are currently supported.

  • +
  • X_observed (Tensor) – A n x d Tensor of observed points that are likely to +be the best observed points so far.

  • +
  • num_fantasies (int) – The number of fantasies to generate. The higher this +number the more accurate the model (at the expense of model +complexity and performance).

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate Expected Improvement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A b1 x … bk x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A b1 x … bk-dim tensor of Noisy Expected Improvement values at +the given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.UpperConfidenceBound(model, beta, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Upper Confidence Bound (UCB).

+

Analytic upper confidence bound that comprises of the posterior mean plus an +additional term: the posterior standard deviation weighted by a trade-off +parameter, beta. Only supports the case of q=1 (i.e. greedy, non-batch +selection of design points). The model must be single-outcome.

+

UCB(x) = mu(x) + sqrt(beta) * sigma(x), where mu and sigma are the +posterior mean and standard deviation, respectively.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> UCB = UpperConfidenceBound(model, beta=0.2)
+>>> ucb = UCB(test_X)
+
+
+

Single-outcome Upper Confidence Bound.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome GP model (must be in batch mode if +candidate sets X will be)

  • +
  • beta (float | Tensor) – Either a scalar or a one-dim tensor with b elements (batch mode) +representing the trade-off parameter between mean and covariance

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the Upper Confidence Bound on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A (b1 x … bk)-dim tensor of Upper Confidence Bound values at the +given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.PosteriorMean(model, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Posterior Mean.

+

Only supports the case of q=1. Requires the model’s posterior to have a +mean property. The model must be single-outcome.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> PM = PosteriorMean(model)
+>>> pm = PM(test_X)
+
+
+

Single-outcome Posterior Mean.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome GP model (must be in batch mode if +candidate sets X will be)

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem. Note +that if maximize=False, the posterior mean is negated. As a +consequence optimize_acqf(PosteriorMean(gp, maximize=False)) +actually returns -1 * minimum of the posterior mean.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the posterior mean on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A (b1 x … bk)-dim tensor of Posterior Mean values at the +given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.ScalarizedPosteriorMean(model, weights, posterior_transform=None)[source]
+

Bases: AnalyticAcquisitionFunction

+

Scalarized Posterior Mean.

+

This acquisition function returns a scalarized (across the q-batch) +posterior mean given a vector of weights.

+

Scalarized Posterior Mean.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome model.

  • +
  • weights (Tensor) – A tensor of shape q for scalarization. In order to minimize +the scalarized posterior mean, pass -weights.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the scalarized posterior mean on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b) x q x d-dim Tensor of (b) t-batches of d-dim design +points each.

+
+
Returns:
+

A (b)-dim Tensor of Posterior Mean values at the given design +points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.analytic.PosteriorStandardDeviation(model, posterior_transform=None, maximize=True)[source]
+

Bases: AnalyticAcquisitionFunction

+

Single-outcome Posterior Standard Deviation.

+

An acquisition function for pure exploration. +Only supports the case of q=1. Requires the model’s posterior to have +mean and variance properties. The model must be either single-outcome +or combined with a posterior_transform to produce a single-output posterior.

+

Example

+
>>> import torch
+>>> from botorch.models.gp_regression import SingleTaskGP
+>>> from botorch.models.transforms.input import Normalize
+>>> from botorch.models.transforms.outcome import Standardize
+>>>
+>>> # Set up a model
+>>> train_X = torch.rand(20, 2, dtype=torch.float64)
+>>> train_Y = torch.sin(train_X).sum(dim=1, keepdim=True)
+>>> model = SingleTaskGP(
+...     train_X, train_Y, outcome_transform=Standardize(m=1),
+...     input_transform=Normalize(d=2),
+... )
+>>> # Now set up the acquisition function
+>>> PSTD = PosteriorStandardDeviation(model)
+>>> test_X = torch.zeros((1, 2), dtype=torch.float64)
+>>> std = PSTD(test_X)
+>>> std.item()
+0.16341639895667773
+
+
+

Single-outcome Posterior Mean.

+
+
Parameters:
+
    +
  • model (Model) – A fitted single-outcome GP model (must be in batch mode if +candidate sets X will be)

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem. Note +that if maximize=False, the posterior standard deviation is negated. +As a consequence, +optimize_acqf(PosteriorStandardDeviation(gp, maximize=False)) +actually returns -1 * minimum of the posterior standard deviation.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the posterior standard deviation on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A (b1 x … bk)-dim tensor of Posterior Mean values at the +given design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Monte-Carlo Acquisition Functions

+

Batch acquisition functions using the reparameterization trick in combination +with (quasi) Monte-Carlo sampling. See [Rezende2014reparam], [Wilson2017reparam] and +[Balandat2020botorch].

+

References

+
+
+[Rezende2014reparam] +

D. J. Rezende, S. Mohamed, and D. Wierstra. Stochastic backpropagation and +approximate inference in deep generative models. ICML 2014.

+
+
+[Wilson2017reparam] +

J. T. Wilson, R. Moriconi, F. Hutter, and M. P. Deisenroth. +The reparameterization trick for acquisition functions. ArXiv 2017.

+
+
+
+
+class botorch.acquisition.monte_carlo.SampleReductionProtocol(*args, **kwargs)[source]
+

Bases: Protocol

+

For static type check of SampleReducingMCAcquisitionFunction’s mc_reduction.

+
+
+
+class botorch.acquisition.monte_carlo.SampleReducingMCAcquisitionFunction(model, sampler=None, objective=None, posterior_transform=None, X_pending=None, sample_reduction=<built-in method mean of type object>, q_reduction=<built-in method amax of type object>, constraints=None, eta=0.001, fat=False)[source]
+

Bases: MCAcquisitionFunction

+

MC-based batch acquisition function that reduces across samples and implements +a general treatment of outcome constraints.

+

This class’s forward computes the - possibly constrained - acquisition value by +(1) computing the unconstrained utility for each MC sample using _sample_forward, +(2) weighing the utility values by the constraint indicator per MC sample, and +(3) reducing (e.g. averaging) the weighted utility values over the MC dimension.

+

NOTE: Do NOT override the forward method, unless you have thought about it well.

+

forward is implemented generically to incorporate constraints in a principled way, +and takes care of reducing over the Monte Carlo and batch dimensions via the +sample_reduction and q_reduction arguments, which default to torch.mean and +torch.max, respectively.

+

In order to implement a custom SampleReducingMCAcquisitionFunction, we only need to +implement the _sample_forward(obj: Tensor) -> Tensor method, which maps objective +samples to acquisition utility values without reducing the Monte Carlo and batch +(i.e. q) dimensions (see details in the docstring of _sample_forward).

+

A note on design choices:

+

The primary purpose of SampleReducingMCAcquisitionFunction`is to support outcome +constraints. On the surface, designing a wrapper `ConstrainedMCAcquisitionFunction +could be an elegant solution to this end, but it would still require the acquisition +functions to implement a _sample_forward method to weigh acquisition utilities at +the sample level. Further, qNoisyExpectedImprovement is a special case that is +hard to encompass in this pattern, since it requires the computation of the best +feasible objective, which requires access to the constraint functions. However, +if the constraints are stored in a wrapper class, they will be inaccessible to the +forward pass. These problems are circumvented by the design of this class.

+

Constructor of SampleReducingMCAcquisitionFunction.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, a +sampler is generated on the fly within the +get_posterior_samples method using +botorch.sampling.get_sampler. +NOTE: For posteriors that do not support base samples, +a sampler compatible with intended use case must be provided. +See ForkedRNGSampler and StochasticSampler as examples.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective(). +NOTE: ConstrainedMCObjective for outcome constraints is deprecated in +favor of passing the constraints directly to this constructor.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A batch_shape, m x d-dim Tensor of m design points +that have points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
  • sample_reduction (SampleReductionProtocol) – A callable that takes in a sample_shape x batch_shape +Tensor of acquisition utility values, a keyword-argument dim that +specifies the sample dimensions to reduce over, and returns a +batch_shape-dim Tensor of acquisition values.

  • +
  • q_reduction (SampleReductionProtocol) – A callable that takes in a sample_shape x batch_shape x q +Tensor of acquisition utility values, a keyword-argument dim that +specifies the q dimension to reduce over (i.e. -1), and returns a +sample_shape x batch_shape-dim Tensor of acquisition values.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero. +NOTE: Constraint-weighting is only compatible with non-negative +acquistion utilities, e.g. all improvement-based acquisition functions.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
  • fat (bool) – Wether to apply a fat-tailed smooth approximation to the feasibility +indicator or the canonical sigmoid approximation.

  • +
+
+
+
+
+forward(X)[source]
+

Computes the acquisition value associated with the input X. Weighs the +acquisition utility values by smoothed constraint indicators if constraints +was passed to the constructor of the class. Applies self.sample_reduction and +self.q_reduction to reduce over the Monte Carlo and batch (q) dimensions.

+

NOTE: Do NOT override the forward method for a custom acquisition function. +Instead, implement the _sample_forward method. See the docstring of this class +for details.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d Tensor of t-batches with q d-dim +design points each.

+
+
Returns:
+

A Tensor with shape batch_shape’, where batch_shape’ is the broadcasted +batch shape of model and input X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.monte_carlo.qExpectedImprovement(model, best_f, sampler=None, objective=None, posterior_transform=None, X_pending=None, constraints=None, eta=0.001)[source]
+

Bases: SampleReducingMCAcquisitionFunction

+

MC-based batch Expected Improvement.

+

This computes qEI by +(1) sampling the joint posterior over q points +(2) evaluating the improvement over the current best for each sample +(3) maximizing over q +(4) averaging over the samples

+

qEI(X) = E(max(max Y - best_f, 0)), Y ~ f(X), where X = (x_1,…,x_q)

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> best_f = train_Y.max()[0]
+>>> sampler = SobolQMCNormalSampler(1024)
+>>> qEI = qExpectedImprovement(model, best_f, sampler)
+>>> qei = qEI(test_X)
+
+
+

NOTE: It is strongly recommended to use qLogExpectedImprovement instead +of regular qEI, as it can lead to substantially improved BO performance through +improved numerics. See https://arxiv.org/abs/2310.20708 for details.

+

q-Expected Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • best_f (float | Tensor) – The best objective value observed so far (assumed noiseless). Can be +a scalar, or a batch_shape-dim tensor. In case of a batched model, the +tensor can specify different values for each element of the batch.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are evaluated. +Defaults to IdentityMCObjective(). +NOTE: ConstrainedMCObjective for outcome constraints is deprecated in +favor of passing the constraints directly to this constructor.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call. Copied and set to have no +gradient.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
+
+
+
+
+
+class botorch.acquisition.monte_carlo.qNoisyExpectedImprovement(model, X_baseline, sampler=None, objective=None, posterior_transform=None, X_pending=None, prune_baseline=True, cache_root=True, constraints=None, eta=0.001, marginalize_dim=None)[source]
+

Bases: SampleReducingMCAcquisitionFunction, CachedCholeskyMCSamplerMixin

+

MC-based batch Noisy Expected Improvement.

+

This function does not assume a best_f is known (which would require +noiseless observations). Instead, it uses samples from the joint posterior +over the q test points and previously observed points. The improvement +over previously observed points is computed for each sample and averaged.

+

qNEI(X) = E(max(max Y - max Y_baseline, 0)), where +(Y, Y_baseline) ~ f((X, X_baseline)), X = (x_1,…,x_q)

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> sampler = SobolQMCNormalSampler(1024)
+>>> qNEI = qNoisyExpectedImprovement(model, train_X, sampler)
+>>> qnei = qNEI(test_X)
+
+
+

NOTE: It is strongly recommended to use qLogNoisyExpectedImprovement instead +of regular qNEI, as it can lead to substantially improved BO performance through +improved numerics. See https://arxiv.org/abs/2310.20708 for details.

+

q-Noisy Expected Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • X_baseline (Tensor) – A batch_shape x r x d-dim Tensor of r design points +that have already been observed. These points are considered as +the potential best design point.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective(). +NOTE: ConstrainedMCObjective for outcome constraints is deprecated in +favor of passing the constraints directly to this constructor.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points +that have points that have been submitted for function evaluation +but have not yet been evaluated. Concatenated into X upon +forward call. Copied and set to have no gradient.

  • +
  • prune_baseline (bool) – If True, remove points in X_baseline that are +highly unlikely to be the best point. This can significantly +improve performance and is generally recommended. In order to +customize pruning parameters, instead manually call +botorch.acquisition.utils.prune_inferior_points on X_baseline +before instantiating the acquisition function.

  • +
  • cache_root (bool) – A boolean indicating whether to cache the root +decomposition over X_baseline and use low-rank updates.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
  • marginalize_dim (int | None) – The dimension to marginalize over.

  • +
+
+
+

TODO: similar to qNEHVI, when we are using sequential greedy candidate +selection, we could incorporate pending points X_baseline and compute +the incremental qNEI from the new point. This would greatly increase +efficiency for large batches.

+
+
+compute_best_f(obj)[source]
+

Computes the best (feasible) noisy objective value.

+
+
Parameters:
+

obj (Tensor) – sample_shape x batch_shape x q-dim Tensor of objectives in forward.

+
+
Returns:
+

A sample_shape x batch_shape-dim Tensor of best feasible objectives.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.monte_carlo.qProbabilityOfImprovement(model, best_f, sampler=None, objective=None, posterior_transform=None, X_pending=None, tau=0.001, constraints=None, eta=0.001)[source]
+

Bases: SampleReducingMCAcquisitionFunction

+

MC-based batch Probability of Improvement.

+

Estimates the probability of improvement over the current best observed +value by sampling from the joint posterior distribution of the q-batch. +MC-based estimates of a probability involves taking expectation of an +indicator function; to support auto-differentiation, the indicator is +replaced with a sigmoid function with temperature parameter tau.

+

qPI(X) = P(max Y >= best_f), Y ~ f(X), X = (x_1,…,x_q)

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> best_f = train_Y.max()[0]
+>>> sampler = SobolQMCNormalSampler(1024)
+>>> qPI = qProbabilityOfImprovement(model, best_f, sampler)
+>>> qpi = qPI(test_X)
+
+
+

q-Probability of Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • best_f (float | Tensor) – The best objective value observed so far (assumed noiseless). Can +be a batch_shape-shaped tensor, which in case of a batched model +specifies potentially different values for each element of the batch.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective(). +NOTE: ConstrainedMCObjective for outcome constraints is deprecated in +favor of passing the constraints directly to this constructor.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation +but have not yet been evaluated. Concatenated into X upon +forward call. Copied and set to have no gradient.

  • +
  • tau (float) – The temperature parameter used in the sigmoid approximation +of the step function. Smaller values yield more accurate +approximations of the function, but result in gradients +estimates with higher variance.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map posterior samples to +a scalar. The associated constraint is considered satisfied if this +scalar is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
+
+
+
+
+
+class botorch.acquisition.monte_carlo.qSimpleRegret(model, sampler=None, objective=None, posterior_transform=None, X_pending=None)[source]
+

Bases: SampleReducingMCAcquisitionFunction

+

MC-based batch Simple Regret.

+

Samples from the joint posterior over the q-batch and computes the simple regret.

+

qSR(X) = E(max Y), Y ~ f(X), X = (x_1,…,x_q)

+

Constraints should be provided as a ConstrainedMCObjective. +Passing constraints as an argument is not supported. This is because +SampleReducingMCAcquisitionFunction computes the acquisition values on the sample +level and then weights the sample-level acquisition values by a soft feasibility +indicator. Hence, it expects non-log acquisition function values to be +non-negative. qSimpleRegret acquisition values can be negative, so we instead use +a ConstrainedMCObjective which applies constraints to the objectives (e.g. before +computing the acquisition function) and shifts negative objective values using +by an infeasible cost to ensure non-negativity (before applying constraints and +shifting them back).

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> sampler = SobolQMCNormalSampler(1024)
+>>> qSR = qSimpleRegret(model, sampler)
+>>> qsr = qSR(test_X)
+
+
+

q-Simple Regret.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation +but have not yet been evaluated. Concatenated into X upon +forward call. Copied and set to have no gradient.

  • +
+
+
+
+
+
+class botorch.acquisition.monte_carlo.qUpperConfidenceBound(model, beta, sampler=None, objective=None, posterior_transform=None, X_pending=None)[source]
+

Bases: SampleReducingMCAcquisitionFunction

+

MC-based batch Upper Confidence Bound.

+

Uses a reparameterization to extend UCB to qUCB for q > 1 (See Appendix A +of [Wilson2017reparam].)

+

qUCB = E(max(mu + |Y_tilde - mu|)), where Y_tilde ~ N(mu, beta pi/2 Sigma) +and f(X) has distribution N(mu, Sigma).

+

Constraints should be provided as a ConstrainedMCObjective. +Passing constraints as an argument is not supported. This is because +SampleReducingMCAcquisitionFunction computes the acquisition values on the sample +level and then weights the sample-level acquisition values by a soft feasibility +indicator. Hence, it expects non-log acquisition function values to be +non-negative. qSimpleRegret acquisition values can be negative, so we instead use +a ConstrainedMCObjective which applies constraints to the objectives (e.g. before +computing the acquisition function) and shifts negative objective values using +by an infeasible cost to ensure non-negativity (before applying constraints and +shifting them back).

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> sampler = SobolQMCNormalSampler(1024)
+>>> qUCB = qUpperConfidenceBound(model, 0.1, sampler)
+>>> qucb = qUCB(test_X)
+
+
+

q-Upper Confidence Bound.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • beta (float) – Controls tradeoff between mean and standard deviation in UCB.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation but have not yet +been evaluated. Concatenated into X upon forward call. Copied and set to +have no gradient.

  • +
+
+
+
+
+
+class botorch.acquisition.monte_carlo.qLowerConfidenceBound(model, beta, sampler=None, objective=None, posterior_transform=None, X_pending=None)[source]
+

Bases: qUpperConfidenceBound

+

MC-based batched lower confidence bound.

+

This acquisition function is useful for confident/risk-averse decision making. +This acquisition function is intended to be maximized as with qUpperConfidenceBound, +but the qLowerConfidenceBound will be pessimistic in the face of uncertainty and +lead to conservative candidates.

+

q-Upper Confidence Bound.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • beta (float) – Controls tradeoff between mean and standard deviation in UCB.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation but have not yet +been evaluated. Concatenated into X upon forward call. Copied and set to +have no gradient.

  • +
+
+
+
+

Monte-Carlo variants of the LogEI family of improvements-based acquisition functions, +see [Ament2023logei] for details.

+

References

+
+
+[Ament2023logei] +(1,2,3,4,5,6,7,8,9) +

S. Ament, S. Daulton, D. Eriksson, M. Balandat, and E. Bakshy. +Unexpected Improvements to Expected Improvement for Bayesian Optimization. Advances +in Neural Information Processing Systems 36, 2023.

+
+
+
+
+class botorch.acquisition.logei.LogImprovementMCAcquisitionFunction(model, sampler=None, objective=None, posterior_transform=None, X_pending=None, constraints=None, eta=0.001, fat=True, tau_max=0.01)[source]
+

Bases: SampleReducingMCAcquisitionFunction

+

Abstract base class for Monte-Carlo-based batch LogEI acquisition functions.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler. +NOTE: For posteriors that do not support base samples, +a sampler compatible with intended use case must be provided. +See ForkedRNGSampler and StochasticSampler as examples.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A batch_shape, m x d-dim Tensor of m design points +that have points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are satisfied if constraint(samples) < 0.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. See the docs of +compute_(log_)constraint_indicator for more details on this parameter.

  • +
  • fat (bool) – Toggles the logarithmic / linear asymptotic behavior of the smooth +approximation to the ReLU.

  • +
  • tau_max (float) – Temperature parameter controlling the sharpness of the +approximation to the max operator over the q candidate points.

  • +
+
+
+
+
+
+class botorch.acquisition.logei.qLogExpectedImprovement(model, best_f, sampler=None, objective=None, posterior_transform=None, X_pending=None, constraints=None, eta=0.001, fat=True, tau_max=0.01, tau_relu=1e-06)[source]
+

Bases: LogImprovementMCAcquisitionFunction

+

MC-based batch Log Expected Improvement.

+

This computes qLogEI by +(1) sampling the joint posterior over q points, +(2) evaluating the smoothed log improvement over the current best for each sample, +(3) smoothly maximizing over q, and +(4) averaging over the samples in log space.

+

See [Ament2023logei] for details. Formally,

+

qLogEI(X) ~ log(qEI(X)) = log(E(max(max Y - best_f, 0))).

+

where Y ~ f(X), and X = (x_1,…,x_q), .

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> best_f = train_Y.max()[0]
+>>> sampler = SobolQMCNormalSampler(1024)
+>>> qLogEI = qLogExpectedImprovement(model, best_f, sampler)
+>>> qei = qLogEI(test_X)
+
+
+

q-Log Expected Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • best_f (float | Tensor) – The best objective value observed so far (assumed noiseless). Can be +a scalar, or a batch_shape-dim tensor. In case of a batched model, the +tensor can specify different values for each element of the batch.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are evaluated. +Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call. Copied and set to have no +gradient.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are satisfied if constraint(samples) < 0.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. See the docs of +compute_(log_)smoothed_constraint_indicator for details.

  • +
  • fat (bool) – Toggles the logarithmic / linear asymptotic behavior of the smooth +approximation to the ReLU.

  • +
  • tau_max (float) – Temperature parameter controlling the sharpness of the smooth +approximations to max.

  • +
  • tau_relu (float) – Temperature parameter controlling the sharpness of the smooth +approximations to ReLU.

  • +
+
+
+
+
+
+class botorch.acquisition.logei.qLogNoisyExpectedImprovement(model, X_baseline, sampler=None, objective=None, posterior_transform=None, X_pending=None, constraints=None, eta=0.001, fat=True, prune_baseline=False, cache_root=True, tau_max=0.01, tau_relu=1e-06, marginalize_dim=None)[source]
+

Bases: LogImprovementMCAcquisitionFunction, CachedCholeskyMCSamplerMixin

+

MC-based batch Log Noisy Expected Improvement.

+

This function does not assume a best_f is known (which would require +noiseless observations). Instead, it uses samples from the joint posterior +over the q test points and previously observed points. A smooth approximation +to the canonical improvement over previously observed points is computed +for each sample and the logarithm of the average is returned.

+

See [Ament2023logei] for details. Formally,

+

qLogNEI(X) ~ log(qNEI(X)) = Log E(max(max Y - max Y_baseline, 0)),

+

where (Y, Y_baseline) ~ f((X, X_baseline)), X = (x_1,…,x_q).

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> sampler = SobolQMCNormalSampler(1024)
+>>> qLogNEI = qLogNoisyExpectedImprovement(model, train_X, sampler)
+>>> acqval = qLogNEI(test_X)
+
+
+

q-Noisy Expected Improvement.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • X_baseline (Tensor) – A batch_shape x r x d-dim Tensor of r design points +that have already been observed. These points are considered as +the potential best design point.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. See MCAcquisitionFunction +more details.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points +that have points that have been submitted for function evaluation +but have not yet been evaluated. Concatenated into X upon +forward call. Copied and set to have no gradient.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are satisfied if constraint(samples) < 0.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. See the docs of +compute_(log_)smoothed_constraint_indicator for details.

  • +
  • fat (bool) – Toggles the logarithmic / linear asymptotic behavior of the smooth +approximation to the ReLU.

  • +
  • prune_baseline (bool) – If True, remove points in X_baseline that are +highly unlikely to be the best point. This can significantly +improve performance and is generally recommended. In order to +customize pruning parameters, instead manually call +botorch.acquisition.utils.prune_inferior_points on X_baseline +before instantiating the acquisition function.

  • +
  • cache_root (bool) – A boolean indicating whether to cache the root +decomposition over X_baseline and use low-rank updates.

  • +
  • tau_max (float) – Temperature parameter controlling the sharpness of the smooth +approximations to max.

  • +
  • tau_relu (float) – Temperature parameter controlling the sharpness of the smooth +approximations to ReLU.

  • +
  • marginalize_dim (int | None) – The dimension to marginalize over.

  • +
+
+
+

TODO: similar to qNEHVI, when we are using sequential greedy candidate +selection, we could incorporate pending points X_baseline and compute +the incremental q(Log)NEI from the new point. This would greatly increase +efficiency for large batches.

+
+
+compute_best_f(obj)[source]
+

Computes the best (feasible) noisy objective value.

+
+
Parameters:
+

obj (Tensor) – sample_shape x batch_shape x q-dim Tensor of objectives in forward.

+
+
Returns:
+

A sample_shape x batch_shape-dim Tensor of best feasible objectives.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+botorch.acquisition.logei.check_tau(tau, name)[source]
+

Checks the validity of the tau arguments of the functions below, and returns +tau if it is valid.

+
+
Parameters:
+
    +
  • tau (FloatOrTensor)

  • +
  • name (str)

  • +
+
+
Return type:
+

FloatOrTensor

+
+
+
+
+
+

Multi-Objective Analytic Acquisition Functions

+

Analytic Acquisition Functions for Multi-objective Bayesian optimization.

+

References

+
+
+[Yang2019] +(1,2,3,4,5) +

Yang, K., Emmerich, M., Deutz, A. et al. Efficient computation of expected +hypervolume improvement using box decomposition algorithms. J Glob Optim 75, +3–34 (2019)

+
+
+
+
+class botorch.acquisition.multi_objective.analytic.ExpectedHypervolumeImprovement(model, ref_point, partitioning, posterior_transform=None)[source]
+

Bases: MultiObjectiveAnalyticAcquisitionFunction

+

Expected Hypervolume Improvement supporting m>=2 outcomes.

+

This implements the computes EHVI using the algorithm from [Yang2019], but +additionally computes gradients via auto-differentiation as proposed by +[Daulton2020qehvi].

+

Note: this is currently inefficient in two ways due to the binary partitioning +algorithm that we use for the box decomposition:

+
+
    +
  • We have more boxes in our decomposition

  • +
  • +
    If we used a box decomposition that used inf as the upper bound for

    the last dimension in all hypercells, then we could reduce the number +of terms we need to compute from 2^m to 2^(m-1). [Yang2019] do this +by using DKLV17 and LKF17 for the box decomposition.

    +
    +
    +
  • +
+
+

TODO: Use DKLV17 and LKF17 for the box decomposition as in [Yang2019] for +greater efficiency.

+

TODO: Add support for outcome constraints.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> ref_point = [0.0, 0.0]
+>>> EHVI = ExpectedHypervolumeImprovement(model, ref_point, partitioning)
+>>> ehvi = EHVI(test_X)
+
+
+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • ref_point (list[float]) – A list with m elements representing the reference point +(in the outcome space) w.r.t. to which compute the hypervolume. +This is a reference point for the outcome values (i.e., after +applying posterior_transform if provided).

  • +
  • partitioning (NondominatedPartitioning) – A NondominatedPartitioning module that provides the non- +dominated front and a partitioning of the non-dominated space in hyper- +rectangles.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform.

  • +
+
+
+
+
+psi(lower, upper, mu, sigma)[source]
+

Compute Psi function.

+

For each cell i and outcome k:

+
+

Psi(lower_{i,k}, upper_{i,k}, mu_k, sigma_k) = ( +sigma_k * PDF((upper_{i,k} - mu_k) / sigma_k) + ( +mu_k - lower_{i,k} +) * (1 - CDF(upper_{i,k} - mu_k) / sigma_k) +)

+
+

See Equation 19 in [Yang2019] for more details.

+
+
Parameters:
+
    +
  • lower (Tensor) – A num_cells x m-dim tensor of lower cell bounds

  • +
  • upper (Tensor) – A num_cells x m-dim tensor of upper cell bounds

  • +
  • mu (Tensor) – A batch_shape x 1 x m-dim tensor of means

  • +
  • sigma (Tensor) – A batch_shape x 1 x m-dim tensor of standard deviations (clamped).

  • +
+
+
Returns:
+

A batch_shape x num_cells x m-dim tensor of values.

+
+
Return type:
+

Tensor

+
+
+
+
+
+nu(lower, upper, mu, sigma)[source]
+

Compute Nu function.

+

For each cell i and outcome k:

+
+

nu(lower_{i,k}, upper_{i,k}, mu_k, sigma_k) = ( +upper_{i,k} - lower_{i,k} +) * (1 - CDF((upper_{i,k} - mu_k) / sigma_k))

+
+

See Equation 25 in [Yang2019] for more details.

+
+
Parameters:
+
    +
  • lower (Tensor) – A num_cells x m-dim tensor of lower cell bounds

  • +
  • upper (Tensor) – A num_cells x m-dim tensor of upper cell bounds

  • +
  • mu (Tensor) – A batch_shape x 1 x m-dim tensor of means

  • +
  • sigma (Tensor) – A batch_shape x 1 x m-dim tensor of standard deviations (clamped).

  • +
+
+
Returns:
+

A batch_shape x num_cells x m-dim tensor of values.

+
+
Return type:
+

Tensor

+
+
+
+
+
+forward(X)[source]
+

Takes in a batch_shape x 1 x d X Tensor of t-batches with 1 d-dim +design point each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Objective Hypervolume Knowledge Gradient Acquisition Functions

+

The hypervolume knowledge gradient acquisition function (HVKG).

+

References:

+
+
+[Daulton2023hvkg] +(1,2,3,4) +

S. Daulton, M. Balandat, E. Bakshy. Hypervolume Knowledge Gradient: A +Lookahead Approach for Multi-Objective Bayesian Optimization with Partial +Information. Proceedings of the 40th International Conference on Machine +Learning, 2023.

+
+
+
+
+class botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qHypervolumeKnowledgeGradient(model, ref_point, num_fantasies=8, num_pareto=10, sampler=None, objective=None, inner_sampler=None, X_evaluation_mask=None, X_pending=None, X_pending_evaluation_mask=None, current_value=None, use_posterior_mean=True, cost_aware_utility=None)[source]
+

Bases: DecoupledAcquisitionFunction, MultiObjectiveMCAcquisitionFunction, OneShotAcquisitionFunction

+

Batch Hypervolume Knowledge Gradient using one-shot optimization.

+

The hypervolume knowledge gradient seeks to maximize the difference in +hypervolume of the hypervolume-maximizing set of a fixed size after +conditioning the unknown observation(s) that would be recevied if X where +evalauted. See [Daulton2023hvkg] for details.

+

This computes the batch Hypervolume Knowledge Gradient using fantasies for +the outer expectation and MC-sampling for the inner expectation.

+

In addition to the design variables, the input X also includes variables +for the optimal designs for each of the fantasy models (Note this is +N x N_pareto optimal designs). For a fixed number of fantasies, all points +in X can be optimized in a “one-shot” fashion.

+

q-Hypervolume Knowledge Gradient.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model. Must support fantasizing.

  • +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
  • num_fantasies (int) – The number of fantasy points to use. More fantasy +points result in a better approximation, at the expense of +memory and wall time. Unused if sampler is specified.

  • +
  • num_pareto (int) – The number of pareto optimal designs to consider.

  • +
  • sampler (ListSampler | None) – The sampler used to sample fantasy observations. Optional +if num_fantasies is specified. The optimization performance +does not seem particularly sensitive to the number of fantasies. +As the number of fantasies increases, the estimation of the +expectation over fantasies becomes more accurate, but the one- +shot optimization problem gets harder as there are more “fantasy” +designs that need to be optimized.

  • +
  • objective (MCMultiOutputObjective | None) – The objective under which the samples are evaluated. If +None, then the analytic posterior mean is used. Otherwise, the +objective is MC-evaluated (using inner_sampler).

  • +
  • inner_sampler (MCSampler | None) – The sampler used for inner sampling. Ignored if the +objective is None.

  • +
  • X_evaluation_mask (list[Tensor] | None) – A q x m-dim tensor of booleans indicating which +objective(s) each of the q points should be evaluated on.

  • +
  • X_pending (Tensor | None) – A n’ x d-dim Tensor of m design points that have +points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
  • X_pending_evaluation_mask (Tensor | None) – A n’ x m-dim tensor of booleans indicating +which objective(s) each of the n’ pending points are being +evaluated on.

  • +
  • current_value (Tensor | None) – The current value, i.e. the expected best objective +given the observed points D. If omitted, forward will not +return the actual KG value, but the expected best objective +given the data set D u X. If pending points are used, +this should be the current value under the fantasy model +conditioned on the pending points so that the incremental KG value +from the new candidates (not pending points) is used.

  • +
  • use_posterior_mean (bool) – If true, optimize the hypervolume of the posterior +mean, otherwise optimize the expected hypervolume. See +[Daulton2023hvkg] for details.

  • +
  • cost_aware_utility (CostAwareUtility | None) – A CostAwareUtility specifying the cost function for +evaluating the X on the objectives indicated by evaluation_mask.

  • +
+
+
+
+
+property cost_sampler
+
+
+
+forward(X)[source]
+

Evaluate qKnowledgeGradient on the candidate set X.

+
+
Parameters:
+

X (Tensor) –

A b x (q + num_fantasies) x d Tensor with b t-batches of +q + num_fantasies design points each. We split this X tensor +into two parts in the q dimension (dim=-2). The first q +are the q-batch of design points and the last num_fantasies are +the current solutions of the inner optimization problem.

+

X_fantasies = X[…, -num_fantasies:, :] +X_fantasies.shape = b x num_fantasies x d

+

X_actual = X[…, :-num_fantasies, :] +X_actual.shape = b x q x d

+

+
+
Returns:
+

+
A Tensor of shape b. For t-batch b, the q-KG value of the design

X_actual[b] is averaged across the fantasy models, where +X_fantasies[b, i] is chosen as the final selection for the +i-th fantasy model. +NOTE: If current_value is not provided, then this is not the +true KG value of X_actual[b], and X_fantasies[b, : ] must be +maximized at fixed X_actual[b].

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+get_augmented_q_batch_size(q)[source]
+

Get augmented q batch size for one-shot optimization.

+
+
Parameters:
+

q (int) – The number of candidates to consider jointly.

+
+
Returns:
+

The augmented size for one-shot optimization (including variables +parameterizing the fantasy solutions).

+
+
Return type:
+

int

+
+
+
+
+
+extract_candidates(X_full)[source]
+

We only return X as the set of candidates post-optimization.

+
+
Parameters:
+

X_full (Tensor) – A b x (q + num_fantasies) x d-dim Tensor with b +t-batches of q + num_fantasies design points each.

+
+
Returns:
+

A b x q x d-dim Tensor with b t-batches of q design points each.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qMultiFidelityHypervolumeKnowledgeGradient(model, ref_point, target_fidelities, num_fantasies=8, num_pareto=10, sampler=None, objective=None, inner_sampler=None, X_pending=None, X_evaluation_mask=None, X_pending_evaluation_mask=None, current_value=None, cost_aware_utility=None, project=<function qMultiFidelityHypervolumeKnowledgeGradient.<lambda>>, valfunc_cls=None, valfunc_argfac=None, use_posterior_mean=True, **kwargs)[source]
+

Bases: qHypervolumeKnowledgeGradient

+

Batch Hypervolume Knowledge Gradient for multi-fidelity optimization.

+

See [Daulton2023hvkg] for details.

+

A version of qHypervolumeKnowledgeGradient that supports multi-fidelity +optimization via a CostAwareUtility and the project and expand +operators. If none of these are set, this acquisition function reduces to +qHypervolumeKnowledgeGradient. Through valfunc_cls and valfunc_argfac, +this can be changed into a custom multi-fidelity acquisition function.

+

Multi-Fidelity q-Knowledge Gradient (one-shot optimization).

+
+
Parameters:
+
    +
  • model (Model) – A fitted model. Must support fantasizing.

  • +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
  • num_fantasies (int) – The number of fantasy points to use. More fantasy +points result in a better approximation, at the expense of +memory and wall time. Unused if sampler is specified.

  • +
  • num_pareto (int) – The number of pareto optimal designs to consider.

  • +
  • sampler (MCSampler | None) – The sampler used to sample fantasy observations. Optional +if num_fantasies is specified.

  • +
  • objective (MCMultiOutputObjective | None) – The objective under which the samples are evaluated. If +None, then the analytic posterior mean is used. Otherwise, the +objective is MC-evaluated (using inner_sampler).

  • +
  • inner_sampler (MCSampler | None) – The sampler used for inner sampling. Ignored if the +objective is None.

  • +
  • X_evaluation_mask (Tensor | None) – A q x m-dim tensor of booleans indicating which +objective(s) each of the q points should be evaluated on.

  • +
  • X_pending (Tensor | None) – A n’ x d-dim Tensor of m design points that have +points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
  • X_pending_evaluation_mask (Tensor | None) – A n’ x m-dim tensor of booleans indicating +which objective(s) each of the n’ pending points are being +evaluated on.

  • +
  • current_value (Tensor | None) – The current value, i.e. the expected best objective +given the observed points D. If omitted, forward will not +return the actual KG value, but the expected best objective +given the data set D u X. If pending points are used, +this should be the current value under the fantasy model +conditioned on the pending points so that the incremental KG value +from the new candidates (not pending points) is used.

  • +
  • use_posterior_mean (bool) – A boolean indicating whether to use the to optimize +the hypervolume of the posterior mean or whether to optimize the +expected hypervolume. See [Daulton2023hvkg] for details.

  • +
  • cost_aware_utility (CostAwareUtility | None) – A CostAwareUtility specifying the cost function for +evaluating the X on the objectives indicated by evaluation_mask.

  • +
  • project (Callable[[Tensor], Tensor]) – A callable mapping a batch_shape x q x d tensor of design +points to a tensor with shape batch_shape x q_term x d projected +to the desired target set (e.g. the target fidelities in case of +multi-fidelity optimization). For the basic case, q_term = q.

  • +
  • valfunc_cls (type[AcquisitionFunction] | None) – An acquisition function class to be used as the terminal +value function.

  • +
  • valfunc_argfac (Callable[[Model], dict[str, Any]] | None) – An argument factory, i.e. callable that maps a Model +to a dictionary of kwargs for the terminal value function (e.g. +best_f for ExpectedImprovement).

  • +
  • target_fidelities (dict[int, float])

  • +
  • kwargs (Any)

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate qMultiFidelityKnowledgeGradient on the candidate set X.

+
+
Parameters:
+

X (Tensor) –

A b x (q + num_fantasies) x d Tensor with b t-batches of +q + num_fantasies design points each. We split this X tensor +into two parts in the q dimension (dim=-2). The first q +are the q-batch of design points and the last num_fantasies are +the current solutions of the inner optimization problem.

+

X_fantasies = X[…, -num_fantasies:, :] +X_fantasies.shape = b x num_fantasies x d

+

X_actual = X[…, :-num_fantasies, :] +X_actual.shape = b x q x d

+

In addition, X may be augmented with fidelity parameteres as +part of thee d-dimension. Projecting fidelities to the target +fidelity is handled by project.

+

+
+
Returns:
+

+
A Tensor of shape b. For t-batch b, the q-KG value of the design

X_actual[b] is averaged across the fantasy models, where +X_fantasies[b, i] is chosen as the final selection for the +i-th fantasy model. +NOTE: If current_value is not provided, then this is not the +true KG value of X_actual[b], and X_fantasies[b, : ] must be +maximized at fixed X_actual[b].

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+ + +
+

Multi-Objective Monte-Carlo Acquisition Functions

+

Monte-Carlo Acquisition Functions for Multi-objective Bayesian optimization. +In particular, this module contains implementations of +1) qEHVI [Daulton2020qehvi], and +2) qNEHVI [Daulton2021nehvi].

+

References

+
+
+[Daulton2020qehvi] +(1,2,3,4,5) +

S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume +Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural +Information Processing Systems 33, 2020.

+
+
+[Daulton2021nehvi] +(1,2,3) +

S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of +Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances +in Neural Information Processing Systems 34, 2021.

+
+
+
+
+class botorch.acquisition.multi_objective.monte_carlo.qExpectedHypervolumeImprovement(model, ref_point, partitioning, sampler=None, objective=None, constraints=None, X_pending=None, eta=0.001, fat=False)[source]
+

Bases: MultiObjectiveMCAcquisitionFunction, SubsetIndexCachingMixin

+

q-Expected Hypervolume Improvement supporting m>=2 outcomes.

+

See [Daulton2020qehvi] for details.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> ref_point = [0.0, 0.0]
+>>> qEHVI = qExpectedHypervolumeImprovement(model, ref_point, partitioning)
+>>> qehvi = qEHVI(test_X)
+
+
+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • ref_point (list[float] | Tensor) – A list or tensor with m elements representing the reference +point (in the outcome space) w.r.t. to which compute the hypervolume. +This is a reference point for the objective values (i.e. after +applying`objective` to the samples).

  • +
  • partitioning (NondominatedPartitioning) – A NondominatedPartitioning module that provides the non- +dominated front and a partitioning of the non-dominated space in hyper- +rectangles. If constraints are present, this partitioning must only +include feasible points.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler.

  • +
  • objective (MCMultiOutputObjective | None) – The MCMultiOutputObjective under which the samples are evaluated. +Defaults to IdentityMCMultiOutputObjective().

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility. The acquisition function will compute expected feasible +hypervolume.

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation but have not yet +been evaluated. Concatenated into X upon forward call. Copied and set +to have no gradient.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function used for the +differentiable approximation of the constraints. In case of a float the +same eta is used for every constraint in constraints. In case of a +tensor the length of the tensor must match the number of provided +constraints. The i-th constraint is then estimated with the i-th +eta value.

  • +
  • fat (bool) – A Boolean flag indicating whether to use the heavy-tailed approximation +of the constraint indicator.

  • +
+
+
+
+
+forward(X)[source]
+

Takes in a batch_shape x q x d X Tensor of t-batches with q d-dim +design points each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X. Should +utilize the result of set_X_pending as needed to account for pending function +evaluations.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.monte_carlo.qNoisyExpectedHypervolumeImprovement(model, ref_point, X_baseline, sampler=None, objective=None, constraints=None, X_pending=None, eta=0.001, fat=False, prune_baseline=False, alpha=0.0, cache_pending=True, max_iep=0, incremental_nehvi=True, cache_root=True, marginalize_dim=None)[source]
+

Bases: NoisyExpectedHypervolumeMixin, qExpectedHypervolumeImprovement

+

q-Noisy Expected Hypervolume Improvement supporting m>=2 outcomes.

+

See [Daulton2021nehvi] for details.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> ref_point = [0.0, 0.0]
+>>> qNEHVI = qNoisyExpectedHypervolumeImprovement(model, ref_point, train_X)
+>>> qnehvi = qNEHVI(test_X)
+
+
+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • ref_point (list[float] | Tensor) – A list or tensor with m elements representing the reference +point (in the outcome space) w.r.t. to which compute the hypervolume. +This is a reference point for the objective values (i.e. after +applying objective to the samples).

  • +
  • X_baseline (Tensor) – A r x d-dim Tensor of r design points that have already +been observed. These points are considered as potential approximate +pareto-optimal design points.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler. +Note: a pareto front is created for each mc sample, which can be +computationally intensive for m > 2.

  • +
  • objective (MCMultiOutputObjective | None) – The MCMultiOutputObjective under which the samples are +evaluated. Defaults to IdentityMCMultiOutputObjective().

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility. The acquisition function will compute expected feasible +hypervolume.

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that +have points that have been submitted for function evaluation, but +have not yet been evaluated.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function used for the +differentiable approximation of the constraints. In case of a float the +same eta is used for every constraint in constraints. In case of a +tensor the length of the tensor must match the number of provided +constraints. The i-th constraint is then estimated with the i-th +eta value. For more details, on this parameter, see the docs of +compute_smoothed_feasibility_indicator.

  • +
  • fat (bool) – A Boolean flag indicating whether to use the heavy-tailed approximation +of the constraint indicator.

  • +
  • prune_baseline (bool) – If True, remove points in X_baseline that are +highly unlikely to be the pareto optimal and better than the +reference point. This can significantly improve computation time and +is generally recommended. In order to customize pruning parameters, +instead manually call prune_inferior_points_multi_objective on +X_baseline before instantiating the acquisition function.

  • +
  • alpha (float) – The hyperparameter controlling the approximate non-dominated +partitioning. The default value of 0.0 means an exact partitioning +is used. As the number of objectives m increases, consider increasing +this parameter in order to limit computational complexity.

  • +
  • cache_pending (bool) – A boolean indicating whether to use cached box +decompositions (CBD) for handling pending points. This is +generally recommended.

  • +
  • max_iep (int) – The maximum number of pending points before the box +decompositions will be recomputed.

  • +
  • incremental_nehvi (bool) – A boolean indicating whether to compute the +incremental NEHVI from the i`th point where `i=1, …, q +under sequential greedy optimization, or the full qNEHVI over +q points.

  • +
  • cache_root (bool) – A boolean indicating whether to cache the root +decomposition over X_baseline and use low-rank updates.

  • +
  • marginalize_dim (int | None) – A batch dimension that should be marginalized. For example, +this is useful when using a batched fully Bayesian model.

  • +
+
+
+
+
+forward(X)[source]
+

Takes in a batch_shape x q x d X Tensor of t-batches with q d-dim +design points each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X. Should +utilize the result of set_X_pending as needed to account for pending function +evaluations.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+

Multi-objective variants of the LogEI family of acquisition functions, see +[Ament2023logei] for details.

+
+
+class botorch.acquisition.multi_objective.logei.qLogExpectedHypervolumeImprovement(model, ref_point, partitioning, sampler=None, objective=None, constraints=None, X_pending=None, eta=0.01, fat=True, tau_relu=1e-06, tau_max=0.01)[source]
+

Bases: MultiObjectiveMCAcquisitionFunction, SubsetIndexCachingMixin

+

Parallel Log Expected Hypervolume Improvement supporting m>=2 outcomes.

+

See [Ament2023logei] for details and the methodology behind the LogEI family of +acquisition function. Line-by-line differences to the original differentiable +expected hypervolume formulation of [Daulton2020qehvi] are described via inline +comments in forward.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> ref_point = [0.0, 0.0]
+>>> acq = qLogExpectedHypervolumeImprovement(model, ref_point, partitioning)
+>>> value = acq(test_X)
+
+
+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • ref_point (list[float] | Tensor) – A list or tensor with m elements representing the reference +point (in the outcome space) w.r.t. to which compute the hypervolume. +This is a reference point for the objective values (i.e. after +applying`objective` to the samples).

  • +
  • partitioning (NondominatedPartitioning) – A NondominatedPartitioning module that provides the non- +dominated front and a partitioning of the non-dominated space in hyper- +rectangles. If constraints are present, this partitioning must only +include feasible points.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler.

  • +
  • objective (MCMultiOutputObjective | None) – The MCMultiOutputObjective under which the samples are evaluated. +Defaults to IdentityMultiOutputObjective().

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility. The acquisition function will compute expected feasible +hypervolume.

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation but have not yet +been evaluated. Concatenated into X upon forward call. Copied and set +to have no gradient.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function used for the +differentiable approximation of the constraints. In case of a float the +same eta is used for every constraint in constraints. In case of a +tensor the length of the tensor must match the number of provided +constraints. The i-th constraint is then estimated with the i-th +eta value.

  • +
  • fat (bool) – Toggles the logarithmic / linear asymptotic behavior of the smooth +approximation to the ReLU and the maximum.

  • +
  • tau_relu (float) – Temperature parameter controlling the sharpness of the +approximation to the ReLU over the q candidate points. For further +details, see the comments above the definition of TAU_RELU.

  • +
  • tau_max (float) – Temperature parameter controlling the sharpness of the +approximation to the max operator over the q candidate points. +For further details, see the comments above the definition of TAU_MAX.

  • +
+
+
+
+
+forward(X)[source]
+

Takes in a batch_shape x q x d X Tensor of t-batches with q d-dim +design points each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X. Should +utilize the result of set_X_pending as needed to account for pending function +evaluations.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement(model, ref_point, X_baseline, sampler=None, objective=None, constraints=None, X_pending=None, eta=0.001, prune_baseline=False, alpha=0.0, cache_pending=True, max_iep=0, incremental_nehvi=True, cache_root=True, tau_relu=1e-06, tau_max=0.001, fat=True, marginalize_dim=None)[source]
+

Bases: NoisyExpectedHypervolumeMixin, qLogExpectedHypervolumeImprovement

+

q-Log Noisy Expected Hypervolume Improvement supporting m>=2 outcomes.

+

Based on the differentiable hypervolume formulation of [Daulton2021nehvi].

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> ref_point = [0.0, 0.0]
+>>> qNEHVI = qNoisyExpectedHypervolumeImprovement(model, ref_point, train_X)
+>>> qnehvi = qNEHVI(test_X)
+
+
+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • ref_point (list[float] | Tensor) – A list or tensor with m elements representing the reference +point (in the outcome space) w.r.t. to which compute the hypervolume. +This is a reference point for the objective values (i.e. after +applying objective to the samples).

  • +
  • X_baseline (Tensor) – A r x d-dim Tensor of r design points that have already +been observed. These points are considered as potential approximate +pareto-optimal design points.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler. +Note: a pareto front is created for each mc sample, which can be +computationally intensive for m > 2.

  • +
  • objective (MCMultiOutputObjective | None) – The MCMultiOutputObjective under which the samples are +evaluated. Defaults to IdentityMultiOutputObjective().

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility. The acquisition function will compute expected feasible +hypervolume.

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that +have points that have been submitted for function evaluation, but +have not yet been evaluated.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function used for the +differentiable approximation of the constraints. In case of a float the +same eta is used for every constraint in constraints. In case of a +tensor the length of the tensor must match the number of provided +constraints. The i-th constraint is then estimated with the i-th +eta value.

  • +
  • prune_baseline (bool) – If True, remove points in X_baseline that are +highly unlikely to be the pareto optimal and better than the +reference point. This can significantly improve computation time and +is generally recommended. In order to customize pruning parameters, +instead manually call prune_inferior_points_multi_objective on +X_baseline before instantiating the acquisition function.

  • +
  • alpha (float) – The hyperparameter controlling the approximate non-dominated +partitioning. The default value of 0.0 means an exact partitioning +is used. As the number of objectives m increases, consider increasing +this parameter in order to limit computational complexity.

  • +
  • cache_pending (bool) – A boolean indicating whether to use cached box +decompositions (CBD) for handling pending points. This is +generally recommended.

  • +
  • max_iep (int) – The maximum number of pending points before the box +decompositions will be recomputed.

  • +
  • incremental_nehvi (bool) – A boolean indicating whether to compute the +incremental NEHVI from the i`th point where `i=1, …, q +under sequential greedy optimization, or the full qNEHVI over +q points.

  • +
  • cache_root (bool) – A boolean indicating whether to cache the root +decomposition over X_baseline and use low-rank updates.

  • +
  • marginalize_dim (int | None) – A batch dimension that should be marginalized.

  • +
  • tau_relu (float)

  • +
  • tau_max (float)

  • +
  • fat (bool)

  • +
+
+
+
+
+forward(X)[source]
+

Takes in a batch_shape x q x d X Tensor of t-batches with q d-dim +design points each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X. Should +utilize the result of set_X_pending as needed to account for pending function +evaluations.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Objective Multi-Fidelity Acquisition Functions

+

Multi-Fidelity Acquisition Functions for Multi-objective Bayesian optimization.

+

References

+
+
+[Irshad2021MOMF] +

F. Irshad, S. Karsch, and A. Döpp. Expected hypervolume improvement for +simultaneous multi-objective and multi-fidelity optimization. +arXiv preprint arXiv:2112.13901, 2021.

+
+
+
+
+class botorch.acquisition.multi_objective.multi_fidelity.MOMF(model, ref_point, partitioning, sampler=None, objective=None, constraints=None, eta=0.001, X_pending=None, cost_call=None)[source]
+

Bases: qExpectedHypervolumeImprovement

+

MOMF acquisition function supporting m>=2 outcomes. +The model needs to have train_obj that has a fidelity +objective appended to its end. +In the following example we consider a 2-D output space +but the ref_point is 3D because of fidelity objective.

+

See [Irshad2021MOMF] for details.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> ref_point = [0.0, 0.0, 0.0]
+>>> cost_func = lambda X: 5 + X[..., -1]
+>>> momf = MOMF(model, ref_point, partitioning, cost_func)
+>>> momf_val = momf(test_X)
+
+
+
+
Parameters:
+
    +
  • model (Model) – A fitted model. There are two default assumptions in the training +data. train_X should have fidelity parameter s as the last dimension +of the input and train_Y contains a trust objective as its last +dimension.

  • +
  • ref_point (list[float] | Tensor) – A list or tensor with m+1 elements representing the reference +point (in the outcome space) w.r.t. to which compute the hypervolume. +The ‘+1’ takes care of the trust objective appended to train_Y. +This is a reference point for the objective values (i.e. after +applying`objective` to the samples).

  • +
  • partitioning (NondominatedPartitioning) – A NondominatedPartitioning module that provides the non- +dominated front and a partitioning of the non-dominated space in hyper- +rectangles. If constraints are present, this partitioning must only +include feasible points.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler.

  • +
  • objective (MCMultiOutputObjective | None) – The MCMultiOutputObjective under which the samples are evaluated. +Defaults to IdentityMCMultiOutputObjective().

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility. The acquisition function will compute expected feasible +hypervolume.

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation but have not yet +been evaluated. Concatenated into X upon forward call. Copied and set +to have no gradient.

  • +
  • cost_call (Callable[[Tensor], Tensor] | None) – A callable cost function mapping a Tensor of dimension +batch_shape x q x d to a cost Tensor of dimension +batch_shape x q x m. Defaults to an AffineCostModel with +C(s) = 1 + s.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function used for the +differentiable approximation of the constraints. In case of a float the +same eta is used for every constraint in constraints. In case of a +tensor the length of the tensor must match the number of provided +constraints. The i-th constraint is then estimated with the i-th +eta value.

  • +
+
+
+
+
+forward(X)[source]
+

Takes in a batch_shape x q x d X Tensor of t-batches with q d-dim +design points each, and returns a Tensor with shape batch_shape’, where +batch_shape’ is the broadcasted batch shape of model and input X. Should +utilize the result of set_X_pending as needed to account for pending function +evaluations.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+ +
+

ParEGO: Multi-Objective Acquisition Function with Chebyshev Scalarization

+
+
+class botorch.acquisition.multi_objective.parego.qLogNParEGO(model, X_baseline, scalarization_weights=None, sampler=None, objective=None, constraints=None, X_pending=None, eta=0.001, fat=True, prune_baseline=False, cache_root=True, tau_relu=1e-06, tau_max=0.01)[source]
+

Bases: qLogNoisyExpectedImprovement, MultiObjectiveMCAcquisitionFunction

+

q-LogNParEGO supporting m >= 2 outcomes. This acquisition function +utilizes qLogNEI to compute the expected improvement over Chebyshev +scalarization of the objectives.

+

This is adapted from qNParEGO proposed in [Daulton2020qehvi] to utilize +log-improvement acquisition functions of [Ament2023logei]. See [Knowles2005] +for the original ParEGO algorithm.

+

This implementation assumes maximization of all objectives. If any of the model +outputs are to be minimized, either an objective should be used to negate the +model outputs or the scalarization_weights should be provided with negative +weights for the outputs to be minimized.

+
+
+
Args:
+
model: A fitted multi-output model, producing outputs for m objectives

and any number of outcome constraints. +NOTE: The model posterior must have a mean attribute.

+
+
X_baseline: A batch_shape x r x d-dim Tensor of r design points

that have already been observed. These points are considered as +the potential best design point.

+
+
scalarization_weights: A m-dim Tensor of weights to be used in the

Chebyshev scalarization. If omitted, samples from the unit simplex.

+
+
sampler: The sampler used to draw base samples. See MCAcquisitionFunction

more details.

+
+
objective: The MultiOutputMCAcquisitionObjective under which the samples are

evaluated before applying Chebyshev scalarization. +Defaults to IdentityMultiOutputObjective().

+
+
constraints: A list of constraint callables which map a Tensor of posterior

samples of dimension sample_shape x batch-shape x q x m’-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are satisfied if constraint(samples) < 0.

+
+
X_pending: A batch_shape x q’ x d-dim Tensor of q’ design points

that have points that have been submitted for function evaluation +but have not yet been evaluated. Concatenated into X upon +forward call. Copied and set to have no gradient.

+
+
eta: Temperature parameter(s) governing the smoothness of the sigmoid

approximation to the constraint indicators. See the docs of +compute_(log_)smoothed_constraint_indicator for details.

+
+
fat: Toggles the logarithmic / linear asymptotic behavior of the smooth

approximation to the ReLU.

+
+
prune_baseline: If True, remove points in X_baseline that are

highly unlikely to be the best point. This can significantly +improve performance and is generally recommended. In order to +customize pruning parameters, instead manually call +botorch.acquisition.utils.prune_inferior_points on X_baseline +before instantiating the acquisition function.

+
+
cache_root: A boolean indicating whether to cache the root

decomposition over X_baseline and use low-rank updates.

+
+
tau_max: Temperature parameter controlling the sharpness of the smooth

approximations to max.

+
+
tau_relu: Temperature parameter controlling the sharpness of the smooth

approximations to ReLU.

+
+
+
+
+
+
+
Parameters:
+
    +
  • model (Model)

  • +
  • X_baseline (Tensor)

  • +
  • scalarization_weights (Tensor | None)

  • +
  • sampler (MCSampler | None)

  • +
  • objective (MCMultiOutputObjective | None)

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None)

  • +
  • X_pending (Tensor | None)

  • +
  • eta (Tensor | float)

  • +
  • fat (bool)

  • +
  • prune_baseline (bool)

  • +
  • cache_root (bool)

  • +
  • tau_relu (float)

  • +
  • tau_max (float)

  • +
+
+
+
+
+
+

The One-Shot Knowledge Gradient

+

Batch Knowledge Gradient (KG) via one-shot optimization as introduced in +[Balandat2020botorch]. For broader discussion of KG see also [Frazier2008knowledge] +and [Wu2016parallelkg].

+
+
+[Balandat2020botorch] +(1,2) +

M. Balandat, B. Karrer, D. R. Jiang, S. Daulton, B. Letham, A. G. Wilson, and +E. Bakshy. BoTorch: A Framework for Efficient Monte-Carlo Bayesian Optimization. +Advances in Neural Information Processing Systems 33, 2020.

+
+
+[Frazier2008knowledge] +

P. Frazier, W. Powell, and S. Dayanik. A Knowledge-Gradient policy for +sequential information collection. SIAM Journal on Control and Optimization, +2008.

+
+
+[Wu2016parallelkg] +

J. Wu and P. Frazier. The parallel knowledge gradient method for batch +bayesian optimization. NIPS 2016.

+
+
+
+
+class botorch.acquisition.knowledge_gradient.qKnowledgeGradient(model, num_fantasies=64, sampler=None, objective=None, posterior_transform=None, inner_sampler=None, X_pending=None, current_value=None)[source]
+

Bases: MCAcquisitionFunction, OneShotAcquisitionFunction

+

Batch Knowledge Gradient using one-shot optimization.

+

This computes the batch Knowledge Gradient using fantasies for the outer +expectation and either the model posterior mean or MC-sampling for the inner +expectation.

+

In addition to the design variables, the input X also includes variables +for the optimal designs for each of the fantasy models. For a fixed number +of fantasies, all parts of X can be optimized in a “one-shot” fashion.

+

q-Knowledge Gradient (one-shot optimization).

+
+
Parameters:
+
    +
  • model (Model) – A fitted model. Must support fantasizing.

  • +
  • num_fantasies (int | None) – The number of fantasy points to use. More fantasy +points result in a better approximation, at the expense of +memory and wall time. Unused if sampler is specified.

  • +
  • sampler (MCSampler | None) – The sampler used to sample fantasy observations. Optional +if num_fantasies is specified.

  • +
  • objective (MCAcquisitionObjective | None) – The objective under which the samples are evaluated. If +None, then the analytic posterior mean is used. Otherwise, the +objective is MC-evaluated (using inner_sampler).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform. If given, this +transforms the posterior before evaluation. If objective is None, +then the analytic posterior mean of the transformed posterior is +used. If objective is given, the inner_sampler is used to draw +samples from the transformed posterior, which are then evaluated under +the objective.

  • +
  • inner_sampler (MCSampler | None) – The sampler used for inner sampling. Ignored if the +objective is None.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
  • current_value (Tensor | None) – The current value, i.e. the expected best objective +given the observed points D. If omitted, forward will not +return the actual KG value, but the expected best objective +given the data set D u X.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate qKnowledgeGradient on the candidate set X.

+
+
Parameters:
+

X (Tensor) –

A b x (q + num_fantasies) x d Tensor with b t-batches of +q + num_fantasies design points each. We split this X tensor +into two parts in the q dimension (dim=-2). The first q +are the q-batch of design points and the last num_fantasies are +the current solutions of the inner optimization problem.

+

X_fantasies = X[…, -num_fantasies:, :] +X_fantasies.shape = b x num_fantasies x d

+

X_actual = X[…, :-num_fantasies, :] +X_actual.shape = b x q x d

+

+
+
Returns:
+

+
A Tensor of shape b. For t-batch b, the q-KG value of the design

X_actual[b] is averaged across the fantasy models, where +X_fantasies[b, i] is chosen as the final selection for the +i-th fantasy model. +NOTE: If current_value is not provided, then this is not the +true KG value of X_actual[b], and X_fantasies[b, : ] must be +maximized at fixed X_actual[b].

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate(X, bounds, **kwargs)[source]
+

Evaluate qKnowledgeGradient on the candidate set X_actual by +solving the inner optimization problem.

+
+
Parameters:
+
    +
  • X (Tensor) – A b x q x d Tensor with b t-batches of q design points +each. Unlike forward(), this does not include solutions of the +inner optimization problem.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of +the solutions to the inner problem.

  • +
  • kwargs (Any) – Additional keyword arguments. This includes the options for +optimization of the inner problem, i.e. num_restarts, raw_samples, +an options dictionary to be passed on to the optimization helpers, and +a scipy_options dictionary to be passed to scipy.minimize.

  • +
+
+
Returns:
+

+
A Tensor of shape b. For t-batch b, the q-KG value of the design

X[b] is averaged across the fantasy models. +NOTE: If current_value is not provided, then this is not the +true KG value of X[b].

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+get_augmented_q_batch_size(q)[source]
+

Get augmented q batch size for one-shot optimization.

+
+
Parameters:
+

q (int) – The number of candidates to consider jointly.

+
+
Returns:
+

The augmented size for one-shot optimization (including variables +parameterizing the fantasy solutions).

+
+
Return type:
+

int

+
+
+
+
+
+extract_candidates(X_full)[source]
+

We only return X as the set of candidates post-optimization.

+
+
Parameters:
+

X_full (Tensor) – A b x (q + num_fantasies) x d-dim Tensor with b +t-batches of q + num_fantasies design points each.

+
+
Returns:
+

A b x q x d-dim Tensor with b t-batches of q design points each.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.knowledge_gradient.qMultiFidelityKnowledgeGradient(model, num_fantasies=64, sampler=None, objective=None, posterior_transform=None, inner_sampler=None, X_pending=None, current_value=None, cost_aware_utility=None, project=<function qMultiFidelityKnowledgeGradient.<lambda>>, expand=<function qMultiFidelityKnowledgeGradient.<lambda>>, valfunc_cls=None, valfunc_argfac=None)[source]
+

Bases: qKnowledgeGradient

+

Batch Knowledge Gradient for multi-fidelity optimization.

+

A version of qKnowledgeGradient that supports multi-fidelity optimization +via a CostAwareUtility and the project and expand operators. If none +of these are set, this acquisition function reduces to qKnowledgeGradient. +Through valfunc_cls and valfunc_argfac, this can be changed into a custom +multi-fidelity acquisition function (it is only KG if the terminal value is +computed using a posterior mean).

+

Multi-Fidelity q-Knowledge Gradient (one-shot optimization).

+
+
Parameters:
+
    +
  • model (Model) – A fitted model. Must support fantasizing.

  • +
  • num_fantasies (int | None) – The number of fantasy points to use. More fantasy +points result in a better approximation, at the expense of +memory and wall time. Unused if sampler is specified.

  • +
  • sampler (MCSampler | None) – The sampler used to sample fantasy observations. Optional +if num_fantasies is specified.

  • +
  • objective (MCAcquisitionObjective | None) – The objective under which the samples are evaluated. If +None, then the analytic posterior mean is used. Otherwise, the +objective is MC-evaluated (using inner_sampler).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform. If given, this +transforms the posterior before evaluation. If objective is None, +then the analytic posterior mean of the transformed posterior is +used. If objective is given, the inner_sampler is used to draw +samples from the transformed posterior, which are then evaluated under +the objective.

  • +
  • inner_sampler (MCSampler | None) – The sampler used for inner sampling. Ignored if the +objective is None.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have +points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
  • current_value (Tensor | None) – The current value, i.e. the expected best objective +given the observed points D. If omitted, forward will not +return the actual KG value, but the expected best objective +given the data set D u X.

  • +
  • cost_aware_utility (CostAwareUtility | None) – A CostAwareUtility computing the cost-transformed +utility from a candidate set and samples of increases in utility.

  • +
  • project (Callable[[Tensor], Tensor]) – A callable mapping a batch_shape x q x d tensor of design +points to a tensor with shape batch_shape x q_term x d projected +to the desired target set (e.g. the target fidelities in case of +multi-fidelity optimization). For the basic case, q_term = q.

  • +
  • expand (Callable[[Tensor], Tensor]) – A callable mapping a batch_shape x q x d input tensor to +a batch_shape x (q + q_e)’ x d-dim output tensor, where the +q_e additional points in each q-batch correspond to +additional (“trace”) observations.

  • +
  • valfunc_cls (type[AcquisitionFunction] | None) – An acquisition function class to be used as the terminal +value function.

  • +
  • valfunc_argfac (Callable[[Model], dict[str, Any]] | None) – An argument factory, i.e. callable that maps a Model +to a dictionary of kwargs for the terminal value function (e.g. +best_f for ExpectedImprovement).

  • +
+
+
+
+
+property cost_sampler
+
+
+
+forward(X)[source]
+

Evaluate qMultiFidelityKnowledgeGradient on the candidate set X.

+
+
Parameters:
+

X (Tensor) –

A b x (q + num_fantasies) x d Tensor with b t-batches of +q + num_fantasies design points each. We split this X tensor +into two parts in the q dimension (dim=-2). The first q +are the q-batch of design points and the last num_fantasies are +the current solutions of the inner optimization problem.

+

X_fantasies = X[…, -num_fantasies:, :] +X_fantasies.shape = b x num_fantasies x d

+

X_actual = X[…, :-num_fantasies, :] +X_actual.shape = b x q x d

+

In addition, X may be augmented with fidelity parameters as +part of thee d-dimension. Projecting fidelities to the target +fidelity is handled by project.

+

+
+
Returns:
+

+
A Tensor of shape b. For t-batch b, the q-KG value of the design

X_actual[b] is averaged across the fantasy models, where +X_fantasies[b, i] is chosen as the final selection for the +i-th fantasy model. +NOTE: If current_value is not provided, then this is not the +true KG value of X_actual[b], and X_fantasies[b, : ] must be +maximized at fixed X_actual[b].

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.knowledge_gradient.ProjectedAcquisitionFunction(base_value_function, project)[source]
+

Bases: AcquisitionFunction

+

Defines a wrapper around an AcquisitionFunction that incorporates the project +operator. Typically used to handle value functions in look-ahead methods.

+
+
Parameters:
+
    +
  • base_value_function (AcquisitionFunction) – The wrapped AcquisitionFunction.

  • +
  • project (Callable[[Tensor], Tensor]) – A callable mapping a batch_shape x q x d tensor of design +points to a tensor with shape batch_shape x q_term x d projected +to the desired target set (e.g. the target fidelities in case of +multi-fidelity optimization). For the basic case, q_term = q.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the acquisition function on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b) x q x d-dim Tensor of (b) t-batches with q d-dim +design points each.

+
+
Returns:
+

A (b)-dim Tensor of acquisition function values at the given +design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Step Lookahead Acquisition Functions

+

A general implementation of multi-step look-ahead acquisition function with configurable +value functions. See [Jiang2020multistep].

+
+
+[Jiang2020multistep] +

S. Jiang, D. R. Jiang, M. Balandat, B. Karrer, J. Gardner, and R. Garnett. +Efficient Nonmyopic Bayesian Optimization via One-Shot Multi-Step Trees. +In Advances in Neural Information Processing Systems 33, 2020.

+
+
+
+
+class botorch.acquisition.multi_step_lookahead.qMultiStepLookahead(model, batch_sizes, num_fantasies=None, samplers=None, valfunc_cls=None, valfunc_argfacs=None, objective=None, posterior_transform=None, inner_mc_samples=None, X_pending=None, collapse_fantasy_base_samples=True)[source]
+

Bases: MCAcquisitionFunction, OneShotAcquisitionFunction

+

MC-based batch Multi-Step Look-Ahead (one-shot optimization).

+

q-Multi-Step Look-Ahead (one-shot optimization).

+

Performs a k-step lookahead by means of repeated fantasizing.

+

Allows to specify the stage value functions by passing the respective class +objects via the valfunc_cls list. Optionally, valfunc_argfacs takes a list +of callables that generate additional kwargs for these constructors. By default, +valfunc_cls will be chosen as [None, …, None, PosteriorMean], which +corresponds to the (parallel) multi-step KnowledgeGradient. If, in addition, +k=1 and q_1 = 1, this reduces to the classic Knowledge Gradient.

+

WARNING: The complexity of evaluating this function is exponential in the number +of lookahead steps!

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • batch_sizes (list[int]) – A list [q_1, …, q_k] containing the batch sizes for the +k look-ahead steps.

  • +
  • num_fantasies (list[int] | None) – A list [f_1, …, f_k] containing the number of fantasy +points to use for the k look-ahead steps.

  • +
  • samplers (list[MCSampler] | None) – A list of MCSampler objects to be used for sampling fantasies in +each stage.

  • +
  • valfunc_cls (list[type[AcquisitionFunction] | None] | None) – A list of k + 1 acquisition function classes to be used as +the (stage + terminal) value functions. Each element (except for the +last one) can be None, in which case a zero stage value is assumed for +the respective stage. If None, this defaults to +[None, …, None, PosteriorMean]

  • +
  • valfunc_argfacs (list[TAcqfArgConstructor | None] | None) – A list of k + 1 “argument factories”, i.e. callables that +map a Model and input tensor X to a dictionary of kwargs for the +respective stage value function constructor (e.g. best_f for +ExpectedImprovement). If None, only the standard (model, sampler +and objective) kwargs will be used.

  • +
  • objective (MCAcquisitionObjective | None) – The objective under which the output is evaluated. If None, use +the model output (requires a single-output model or a posterior +transform). Otherwise the objective is MC-evaluated +(using inner_sampler).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform. If given, this +transforms the posterior before evaluation. If objective is None, +then the output of the transformed posterior is used. If objective is +given, the inner_sampler is used to draw samples from the transformed +posterior, which are then evaluated under the objective.

  • +
  • inner_mc_samples (list[int] | None) – A list [n_0, …, n_k] containing the number of MC +samples to be used for evaluating the stage value function. Ignored if +the objective is None.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have points that +have been submitted for function evaluation but have not yet been +evaluated. Concatenated into X upon forward call. Copied and set to +have no gradient.

  • +
  • collapse_fantasy_base_samples (bool) – If True, collapse_batch_dims of the Samplers +will be applied on fantasy batch dimensions as well, meaning that base +samples are the same in all subtrees starting from the same level.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate qMultiStepLookahead on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q’ x d-dim Tensor with q’ design points for each +batch, where q’ = q_0 + f_1 q_1 + f_2 f_1 q_2 + …. Here q_i +is the number of candidates jointly considered in look-ahead step +i, and f_i is respective number of fantasies.

+
+
Returns:
+

The acquisition value for each batch as a tensor of shape batch_shape.

+
+
Return type:
+

Tensor

+
+
+
+
+
+get_augmented_q_batch_size(q)[source]
+

Get augmented q batch size for one-shot optimization.

+
+
Parameters:
+

q (int) – The number of candidates to consider jointly.

+
+
Returns:
+

The augmented size for one-shot optimization (including variables +parameterizing the fantasy solutions): q_0 + f_1 q_1 + f_2 f_1 q_2 + …

+
+
Return type:
+

int

+
+
+
+
+
+get_split_shapes(X)[source]
+

Get the split shapes from X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q_aug x d-dim tensor including fantasy points.

+
+
Returns:
+

A 3-tuple (batch_shape, shapes, sizes), where +shape[i] = f_i x …. x f_1 x batch_shape x q_i x d and +size[i] = f_i * … f_1 * q_i.

+
+
Return type:
+

tuple[Size, list[Size], list[int]]

+
+
+
+
+
+get_multi_step_tree_input_representation(X)[source]
+

Get the multi-step tree representation of X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q’ x d-dim Tensor with q’ design points for each +batch, where q’ = q_0 + f_1 q_1 + f_2 f_1 q_2 + …. Here q_i +is the number of candidates jointly considered in look-ahead step +i, and f_i is respective number of fantasies.

+
+
Returns:
+

A list [X_j, …, X_k] of tensors, where X_i has shape +f_i x …. x f_1 x batch_shape x q_i x d.

+
+
Return type:
+

list[Tensor]

+
+
+
+
+
+extract_candidates(X_full)[source]
+

We only return X as the set of candidates post-optimization.

+
+
Parameters:
+

X_full (Tensor) – A batch_shape x q’ x d-dim Tensor with q’ design points for +each batch, where q’ = q + f_1 q_1 + f_2 f_1 q_2 + ….

+
+
Returns:
+

A batch_shape x q x d-dim Tensor with q design points for each batch.

+
+
Return type:
+

Tensor

+
+
+
+
+
+get_induced_fantasy_model(X)[source]
+

Fantasy model induced by X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q’ x d-dim Tensor with q’ design points for each +batch, where q’ = q_0 + f_1 q_1 + f_2 f_1 q_2 + …. Here q_i +is the number of candidates jointly considered in look-ahead step +i, and f_i is respective number of fantasies.

+
+
Returns:
+

The fantasy model induced by X.

+
+
Return type:
+

Model

+
+
+
+
+
+
+botorch.acquisition.multi_step_lookahead.warmstart_multistep(acq_function, bounds, num_restarts, raw_samples, full_optimizer)[source]
+

Warm-start initialization for multi-step look-ahead acquisition functions.

+

For now uses the same q’ as in full_optimizer. TODO: allow different q.

+
+
Parameters:
+
    +
  • acq_function (qMultiStepLookahead) – A qMultiStepLookahead acquisition function.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of features.

  • +
  • num_restarts (int) – The number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int) – The number of raw samples to consider in the initialization +heuristic.

  • +
  • full_optimizer (Tensor) – The full tree of optimizers of the previous iteration of shape +batch_shape x q’ x d. Typically obtained by passing +return_best_only=False and return_full_tree=True into optimize_acqf.

  • +
+
+
Returns:
+

A num_restarts x q’ x d tensor for initial points for optimization.

+
+
Return type:
+

Tensor

+
+
+

This is a very simple initialization heuristic. +TODO: Use the observed values to identify the fantasy sub-tree that is closest to +the observed value.

+
+
+
+botorch.acquisition.multi_step_lookahead.make_best_f(model, X)[source]
+

Extract the best observed training input from the model.

+
+
Parameters:
+
    +
  • model (Model)

  • +
  • X (Tensor)

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+ + + +
+

Active Learning Acquisition Functions

+

Active learning acquisition functions.

+
+
+[Seo2014activedata] +

S. Seo, M. Wallat, T. Graepel, and K. Obermayer. Gaussian process regression: +Active data selection and test point rejection. IJCNN 2000.

+
+
+[Chen2014seqexpdesign] +

X. Chen and Q. Zhou. Sequential experimental designs for stochastic kriging. +Winter Simulation Conference 2014.

+
+
+[Binois2017repexp] +

M. Binois, J. Huang, R. B. Gramacy, and M. Ludkovski. Replication or +exploration? Sequential design for stochastic simulation experiments. +ArXiv 2017.

+
+
+
+
+class botorch.acquisition.active_learning.qNegIntegratedPosteriorVariance(model, mc_points, sampler=None, posterior_transform=None, X_pending=None)[source]
+

Bases: AcquisitionFunction

+

Batch Integrated Negative Posterior Variance for Active Learning.

+

This acquisition function quantifies the (negative) integrated posterior variance +(excluding observation noise, computed using MC integration) of the model. +In that, it is a proxy for global model uncertainty, and thus purely focused on +“exploration”, rather the “exploitation” of many of the classic Bayesian +Optimization acquisition functions.

+

See [Seo2014activedata], [Chen2014seqexpdesign], and [Binois2017repexp].

+

q-Integrated Negative Posterior Variance.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • mc_points (Tensor) – A batch_shape x N x d tensor of points to use for +MC-integrating the posterior variance. Usually, these are qMC +samples on the whole design space, but biased sampling directly +allows weighted integration of the posterior variance.

  • +
  • sampler (MCSampler | None) – The sampler used for drawing fantasy samples. In the basic setting +of a standard GP (default) this is a dummy, since the variance of the +model after conditioning does not actually depend on the sampled values.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • X_pending (Tensor | None) – A n’ x d-dim Tensor of n’ design points that have +points that have been submitted for function evaluation but +have not yet been evaluated.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the acquisition function on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b) x q x d-dim Tensor of (b) t-batches with q d-dim +design points each.

+
+
Returns:
+

A (b)-dim Tensor of acquisition function values at the given +design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.active_learning.PairwiseMCPosteriorVariance(model, objective, sampler=None)[source]
+

Bases: MCAcquisitionFunction

+

Variance of difference for Active Learning

+

Given a model and an objective, calculate the posterior sample variance +of the objective on the difference of pairs of points. See more implementation +details in forward. This acquisition function is typically used with a +pairwise model (e.g., PairwiseGP) and a likelihood/link function +on the pair difference (e.g., logistic or probit) for pure exploration

+

Pairwise Monte Carlo Posterior Variance

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • objective (MCAcquisitionObjective) – An MCAcquisitionObjective representing the link function +(e.g., logistic or probit.) applied on the difference of (usually 1-d) +two samples. Can be implemented via GenericMCObjective.

  • +
  • sampler (MCSampler | None) – The sampler used for drawing MC samples.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate PairwiseMCPosteriorVariance on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A batch_size x q x d-dim Tensor. q should be a multiple of 2.

+
+
Returns:
+

Tensor of shape batch_size x q representing the posterior variance +of link function at X that active learning hopes to maximize

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Bayesian Active Learning Acquisition Functions

+

Acquisition functions for Bayesian active learning. This includes: +BALD [Houlsby2011bald] and its batch version [kirsch2019batchbald].

+

References

+
+
+[kirsch2019batchbald] +(1,2) +

Andreas Kirsch, Joost van Amersfoort, Yarin Gal. +BatchBALD: Efficient and Diverse Batch Acquisition for Deep Bayesian +Active Learning. +In Proceedings of the Annual Conference on Neural Information +Processing Systems (NeurIPS), 2019.

+
+
+
+
+botorch.acquisition.bayesian_active_learning.check_negative_info_gain(info_gain)[source]
+

Check if the (expected) information gain is negative, raise a warning if so.

+
+
Parameters:
+

info_gain (Tensor)

+
+
Return type:
+

None

+
+
+
+
+
+class botorch.acquisition.bayesian_active_learning.FullyBayesianAcquisitionFunction(model)[source]
+

Bases: AcquisitionFunction

+

Base class for acquisition functions which require a Fully Bayesian +model treatment.

+
+
Parameters:
+

model (Model) – A fully bayesian single-outcome model.

+
+
+
+
+
+class botorch.acquisition.bayesian_active_learning.qBayesianActiveLearningByDisagreement(model, sampler=None, posterior_transform=None, X_pending=None)[source]
+

Bases: FullyBayesianAcquisitionFunction, MCSamplerMixin

+

Batch implementation [kirsch2019batchbald] of BALD [Houlsby2011bald], +which maximizes the mutual information between the next observation and the +hyperparameters of the model. Computed by Monte Carlo integration.

+
+
Parameters:
+
    +
  • model (ModelListGP | SaasFullyBayesianSingleTaskGP) – A fully bayesian model (SaasFullyBayesianSingleTaskGP).

  • +
  • sampler (MCSampler | None) – The sampler used for drawing samples to approximate the entropy +of the Gaussian Mixture posterior.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate qBayesianActiveLearningByDisagreement on the candidate set X. +A monte carlo-estimated information gain is computed over a Gaussian Mixture +marginal posterior, and the Gaussian conditional posterior to obtain the +qBayesianActiveLearningByDisagreement on the candidate set X.

+
+
Parameters:
+

X (Tensor) – batch_shape x q x D-dim Tensor of input points.

+
+
Returns:
+

A batch_shape x num_models-dim Tensor of BALD values.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Preference Acquisition Functions

+

Preference acquisition functions. This includes: +Analytical EUBO acquisition function as introduced in [Lin2022preference] +and its MC-based generalization qEUBO as proposed in [Astudillo2023qeubo].

+
+
+[Astudillo2023qeubo] +(1,2) +

Astudillo, R., Lin, Z.J., Bakshy, E. and Frazier, P.I. qEUBO: A Decision-Theoretic +Acquisition Function for Preferential Bayesian Optimization. International +Conference on Artificial Intelligence and Statistics (AISTATS), 2023.

+
+
+[Lin2022preference] +(1,2,3) +

Lin, Z.J., Astudillo, R., Frazier, P.I. and Bakshy, E. Preference Exploration +for Efficient Bayesian Optimization with Multiple Outcomes. International +Conference on Artificial Intelligence and Statistics (AISTATS), 2022.

+
+
+[Houlsby2011bald] +(1,2,3) +

Houlsby, N., Huszár, F., Ghahramani, Z. and Lengyel, M. +Bayesian Active Learning for Gaussian Process Classification. +NIPS Workshop on Bayesian optimization, experimental design and bandits: +Theory and applications, 2011.

+
+
+
+
+class botorch.acquisition.preference.AnalyticExpectedUtilityOfBestOption(pref_model, outcome_model=None, previous_winner=None)[source]
+

Bases: AnalyticAcquisitionFunction

+

Analytic Preferential Expected Utility of Best Options, i.e., Analytical EUBO

+

Analytic implementation of Expected Utility of the Best Option under the +Laplace model (assumes a PairwiseGP is used as the preference model) as +proposed in [Lin2022preference].

+
+
Parameters:
+
    +
  • pref_model (Model) – The preference model that maps the outcomes (i.e., Y) to +scalar-valued utility.

  • +
  • outcome_model (DeterministicModel | None) – A deterministic model that maps parameters (i.e., X) to +outcomes (i.e., Y). The outcome model f defines the search space of +Y = f(X). If model is None, we are directly calculating EUBO on +the parameter space. When used with OneSamplePosteriorDrawModel, +we are obtaining EUBO-zeta as described in [Lin2022preference].

  • +
  • previous_winner (Tensor | None) – Tensor representing the previous winner in the Y space.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate analytical EUBO on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d-dim Tensor, where q = 2 if previous_winner +is not None, and q = 1 otherwise.

+
+
Returns:
+

The acquisition value for each batch as a tensor of shape batch_shape.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.preference.qExpectedUtilityOfBestOption(pref_model, outcome_model=None, sampler=None, objective=None, posterior_transform=None, X_pending=None)[source]
+

Bases: MCAcquisitionFunction

+

MC-based Expected Utility of Best Option (qEUBO)

+

This computes qEUBO by +(1) sampling the joint posterior over q points +(2) evaluating the maximum objective value accross the q points +(3) averaging over the samples

+

qEUBO(X) = E[max Y], Y ~ f(X), where X = (x_1,…,x_q)

+

MC-based Expected Utility of Best Option (qEUBO) as proposed +in [Astudillo2023qeubo].

+
+
Parameters:
+
    +
  • pref_model (Model) – The preference model that maps the outcomes (i.e., Y) to +scalar-valued utility.

  • +
  • outcome_model (DeterministicModel | None) –

    +
    A deterministic model that maps parameters (i.e., X) to

    outcomes (i.e., Y). The outcome model f defines the search space of +Y = f(X). If model is None, we are directly calculating qEUBO on +the parameter space.

    +
    +
    sampler: The sampler used to draw base samples. See MCAcquisitionFunction

    more details.

    +
    +
    +

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are evaluated. +Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call. Copied and set +to have no gradient.

  • +
  • sampler (MCSampler | None)

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate qEUBO on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d-dim Tensor of t-batches with q +d-dim design points each.

+
+
Returns:
+

A batch_shape’-dim Tensor of qEUBO values at the given design +points X, where batch_shape’ is the broadcasted batch shape +of model and input X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.preference.PairwiseBayesianActiveLearningByDisagreement(pref_model, outcome_model=None, num_samples=1024, std_noise=0.0)[source]
+

Bases: MCAcquisitionFunction

+

MC Bayesian Active Learning by Disagreement

+

Monte Carlo implementation of Bayesian Active Learning by Disagreement (BALD) +proposed in [Houlsby2011bald].

+
+
Parameters:
+
    +
  • pref_model (Model) – The preference model that maps the outcomes (i.e., Y) to +scalar-valued utility.

  • +
  • outcome_model (DeterministicModel | None) – A deterministic model that maps parameters (i.e., X) to +outcomes (i.e., Y). The outcome model f defines the search space of +Y = f(X). If model is None, we are directly calculating BALD on +the parameter space.

  • +
  • num_samples (int | None) – number of samples to approximate the conditional_entropy.

  • +
  • std_noise (float | None) – Additional observational noise to include. Defaults to 0.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate MC BALD on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x 2 x d-dim Tensor of t-batches with q=2 +d-dim design points each.

+
+
Returns:
+

A batch_shape’-dim Tensor of MC BALD values at the given +design points pair X, where batch_shape’ is the broadcasted +batch shape of model and input X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+
+

Objectives and Cost-Aware Utilities

+
+

Objectives

+

Objective Modules to be used with acquisition functions.

+
+
+class botorch.acquisition.objective.PosteriorTransform(*args, **kwargs)[source]
+

Bases: Module, ABC

+

Abstract base class for objectives that transform the posterior.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+abstract evaluate(Y)[source]
+

Evaluate the transform on a set of outcomes.

+
+
Parameters:
+

Y (Tensor) – A batch_shape x q x m-dim tensor of outcomes.

+
+
Returns:
+

A batch_shape x q’ [x m’]-dim tensor of transformed outcomes.

+
+
Return type:
+

Tensor

+
+
+
+
+
+abstract forward(posterior)[source]
+

Compute the transformed posterior.

+
+
Parameters:
+

posterior – The posterior to be transformed.

+
+
Returns:
+

The transformed posterior object.

+
+
Return type:
+

Posterior

+
+
+
+
+
+
+class botorch.acquisition.objective.ScalarizedPosteriorTransform(weights, offset=0.0)[source]
+

Bases: PosteriorTransform

+

An affine posterior transform for scalarizing multi-output posteriors.

+

For a Gaussian posterior at a single point (q=1) with mean mu and +covariance matrix Sigma, this yields a single-output posterior with mean +weights^T * mu and variance weights^T Sigma w.

+

Example

+

Example for a model with two outcomes:

+
>>> weights = torch.tensor([0.5, 0.25])
+>>> posterior_transform = ScalarizedPosteriorTransform(weights)
+>>> EI = ExpectedImprovement(
+... model, best_f=0.1, posterior_transform=posterior_transform
+... )
+
+
+
+
Parameters:
+
    +
  • weights (Tensor) – A one-dimensional tensor with m elements representing the +linear weights on the outputs.

  • +
  • offset (float) – An offset to be added to posterior mean.

  • +
+
+
+
+
+scalarize: bool = True
+
+
+
+evaluate(Y)[source]
+

Evaluate the transform on a set of outcomes.

+
+
Parameters:
+

Y (Tensor) – A batch_shape x q x m-dim tensor of outcomes.

+
+
Returns:
+

A batch_shape x q-dim tensor of transformed outcomes.

+
+
Return type:
+

Tensor

+
+
+
+
+
+forward(posterior)[source]
+

Compute the posterior of the affine transformation.

+
+
Parameters:
+

posterior (GPyTorchPosterior | PosteriorList) – A posterior with the same number of outputs as the +elements in self.weights.

+
+
Returns:
+

A single-output posterior.

+
+
Return type:
+

GPyTorchPosterior

+
+
+
+
+
+
+class botorch.acquisition.objective.ExpectationPosteriorTransform(n_w, weights=None)[source]
+

Bases: PosteriorTransform

+

Transform the batch x (q * n_w) x m posterior into a batch x q x m +posterior of the expectation. The expectation is calculated over each +consecutive n_w block of points in the posterior.

+

This is intended for use with InputPerturbation or AppendFeatures for +optimizing the expectation over n_w points. This should not be used when +there are constraints present, since this does not take into account +the feasibility of the objectives.

+

Note: This is different than ScalarizedPosteriorTransform in that +this operates over the q-batch dimension.

+

A posterior transform calculating the expectation over the q-batch +dimension.

+
+
Parameters:
+
    +
  • n_w (int) – The number of points in the q-batch of the posterior to compute +the expectation over. This corresponds to the size of the +feature_set of AppendFeatures or the size of the perturbation_set +of InputPerturbation.

  • +
  • weights (Tensor | None) – An optional n_w x m-dim tensor of weights. Can be used to +compute a weighted expectation. Weights are normalized before use.

  • +
+
+
+
+
+evaluate(Y)[source]
+

Evaluate the expectation of a set of outcomes.

+
+
Parameters:
+

Y (Tensor) – A batch_shape x (q * n_w) x m-dim tensor of outcomes.

+
+
Returns:
+

A batch_shape x q x m-dim tensor of expectation outcomes.

+
+
Return type:
+

Tensor

+
+
+
+
+
+forward(posterior)[source]
+

Compute the posterior of the expectation.

+
+
Parameters:
+

posterior (GPyTorchPosterior) – An m-outcome joint posterior over q * n_w points.

+
+
Returns:
+

An m-outcome joint posterior over q expectations.

+
+
Return type:
+

GPyTorchPosterior

+
+
+
+
+
+
+class botorch.acquisition.objective.MCAcquisitionObjective(*args, **kwargs)[source]
+

Bases: Module, ABC

+

Abstract base class for MC-based objectives.

+
+
Parameters:
+
    +
  • _verify_output_shape – If True and X is given, check that the q-batch +shape of the objectives agrees with that of X.

  • +
  • _is_mo – A boolean denoting whether the objectives are multi-output.

  • +
+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+abstract forward(samples, X=None)[source]
+

Evaluate the objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of +samples from a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Relevant only if +the objective depends on the inputs explicitly.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim Tensor of objective +values (assuming maximization).

+
+
Return type:
+

Tensor

+
+
+

This method is usually not called directly, but via the objectives.

+

Example

+
>>> # `__call__` method:
+>>> samples = sampler(posterior)
+>>> outcome = mc_obj(samples)
+
+
+
+
+
+
+class botorch.acquisition.objective.IdentityMCObjective(*args, **kwargs)[source]
+

Bases: MCAcquisitionObjective

+

Trivial objective extracting the last dimension.

+

Example

+
>>> identity_objective = IdentityMCObjective()
+>>> samples = sampler(posterior)
+>>> objective = identity_objective(samples)
+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of +samples from a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Relevant only if +the objective depends on the inputs explicitly.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim Tensor of objective +values (assuming maximization).

+
+
Return type:
+

Tensor

+
+
+

This method is usually not called directly, but via the objectives.

+

Example

+
>>> # `__call__` method:
+>>> samples = sampler(posterior)
+>>> outcome = mc_obj(samples)
+
+
+
+
+
+
+class botorch.acquisition.objective.LinearMCObjective(weights)[source]
+

Bases: MCAcquisitionObjective

+

Linear objective constructed from a weight tensor.

+

For input samples and mc_obj = LinearMCObjective(weights), this produces +mc_obj(samples) = sum_{i} weights[i] * samples[…, i]

+

Example

+

Example for a model with two outcomes:

+
>>> weights = torch.tensor([0.75, 0.25])
+>>> linear_objective = LinearMCObjective(weights)
+>>> samples = sampler(posterior)
+>>> objective = linear_objective(samples)
+
+
+
+
Parameters:
+

weights (Tensor) – A one-dimensional tensor with m elements representing the +linear weights on the outputs.

+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the linear objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim tensors of +samples from a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Relevant only if +the objective depends on the inputs explicitly.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim tensor of objective values.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.objective.GenericMCObjective(objective)[source]
+

Bases: MCAcquisitionObjective

+

Objective generated from a generic callable.

+

Allows to construct arbitrary MC-objective functions from a generic +callable. In order to be able to use gradient-based acquisition function +optimization it should be possible to backpropagate through the callable.

+

Example

+
>>> generic_objective = GenericMCObjective(
+        lambda Y, X: torch.sqrt(Y).sum(dim=-1),
+    )
+>>> samples = sampler(posterior)
+>>> objective = generic_objective(samples)
+
+
+
+
Parameters:
+

objective (Callable[[Tensor, Tensor | None], Tensor]) – A callable f(samples, X) mapping a +sample_shape x batch-shape x q x m-dim Tensor samples and +an optional batch-shape x q x d-dim Tensor X to a +sample_shape x batch-shape x q-dim Tensor of objective values.

+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of +samples from a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Relevant only if +the objective depends on the inputs explicitly.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim Tensor of objective values.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.objective.ConstrainedMCObjective(objective, constraints, infeasible_cost=0.0, eta=0.001)[source]
+

Bases: GenericMCObjective

+

Feasibility-weighted objective.

+

An Objective allowing to maximize some scalable objective on the model +outputs subject to a number of constraints. Constraint feasibilty is +approximated by a sigmoid function.

+
+

mc_acq(X) = ( +(objective(X) + infeasible_cost) * prod_i (1 - sigmoid(constraint_i(X))) +) - infeasible_cost

+
+

See botorch.utils.objective.apply_constraints for details on the constraint +handling.

+

Example

+
>>> bound = 0.0
+>>> objective = lambda Y: Y[..., 0]
+>>> # apply non-negativity constraint on f(x)[1]
+>>> constraint = lambda Y: bound - Y[..., 1]
+>>> constrained_objective = ConstrainedMCObjective(objective, [constraint])
+>>> samples = sampler(posterior)
+>>> objective = constrained_objective(samples)
+
+
+

TODO: Deprecate this as default way to handle constraints with MC acquisition +functions once we have data on how well SampleReducingMCAcquisitionFunction works.

+
+
Parameters:
+
    +
  • objective (Callable[[Tensor, Tensor | None], Tensor]) – A callable f(samples, X) mapping a +sample_shape x batch-shape x q x m-dim Tensor samples and +an optional batch-shape x q x d-dim Tensor X to a +sample_shape x batch-shape x q-dim Tensor of objective values.

  • +
  • constraints (list[Callable[[Tensor], Tensor]]) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility.

  • +
  • infeasible_cost (Tensor | float) – The cost of a design if all associated samples are +infeasible.

  • +
  • eta (Tensor | float) – The temperature parameter of the sigmoid function approximating +the constraint. Can be either a float or a 1-dim tensor. In case +of a float the same eta is used for every constraint in +constraints. In case of a tensor the length of the tensor must +match the number of provided constraints. The i-th constraint is +then estimated with the i-th eta value.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the feasibility-weighted objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of +samples from a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Relevant only if +the objective depends on the inputs explicitly.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim Tensor of objective values +weighted by feasibility (assuming maximization).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.objective.LearnedObjective(pref_model, sample_shape=None, seed=None)[source]
+

Bases: MCAcquisitionObjective

+

Learned preference objective constructed from a preference model.

+

For input samples, it samples each individual sample again from the latent +preference posterior distribution using pref_model and return the posterior mean.

+

Example

+
>>> train_X = torch.rand(2, 2)
+>>> train_comps = torch.LongTensor([[0, 1]])
+>>> pref_model = PairwiseGP(train_X, train_comps)
+>>> learned_pref_obj = LearnedObjective(pref_model)
+>>> samples = sampler(posterior)
+>>> objective = learned_pref_obj(samples)
+
+
+
+
Parameters:
+
    +
  • pref_model (Model) – A BoTorch model, which models the latent preference/utility +function. Given an input tensor of size +sample_size x batch_shape x q x d, its posterior method should +return a Posterior object with single outcome representing the +utility values of the input.

  • +
  • sample_shape (torch.Size | None) – Determines the number of preference-model samples drawn +per outcome-model sample when the LearnedObjective is called. +Note that this is an additional layer of sampling relative to what +is needed when evaluating most MC acquisition functions in order to +account for uncertainty in the preference model. If None, it will +default to torch.Size([16]), so that 16 samples will be drawn +from the preference model at each outcome sample. This number is +relatively high because sampling from the preference model is general +cheap relative to generating the outcome model posterior.

  • +
  • seed (int | None)

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Sample each element of samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_size x batch_shape x q x d-dim Tensors of +samples from a model posterior.

  • +
  • X (Tensor | None)

  • +
+
+
Returns:
+

A (sample_size * num_samples) x batch_shape x q-dim Tensor of +objective values sampled from utility posterior using pref_model.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Objective Objectives

+
+
+class botorch.acquisition.multi_objective.objective.MCMultiOutputObjective(*args, **kwargs)[source]
+

Bases: MCAcquisitionObjective

+

Abstract base class for MC multi-output objectives.

+
+
Parameters:
+

_is_mo – A boolean denoting whether the objectives are multi-output.

+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+abstract forward(samples, X=None)[source]
+

Evaluate the multi-output objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of samples from +a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim Tensors of inputs.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim Tensor of objective values with +m’ the output dimension. This assumes maximization in each output +dimension).

+
+
Return type:
+

Tensor

+
+
+

This method is usually not called directly, but via the objectives.

+

Example

+
>>> # `__call__` method:
+>>> samples = sampler(posterior)
+>>> outcomes = multi_obj(samples)
+
+
+
+
+
+
+class botorch.acquisition.multi_objective.objective.GenericMCMultiOutputObjective(objective)[source]
+

Bases: GenericMCObjective, MCMultiOutputObjective

+

Multi-output objective generated from a generic callable.

+

Allows to construct arbitrary MC-objective functions from a generic +callable. In order to be able to use gradient-based acquisition function +optimization it should be possible to backpropagate through the callable.

+
+
Parameters:
+

objective (Callable[[Tensor, Tensor | None], Tensor]) – A callable f(samples, X) mapping a +sample_shape x batch-shape x q x m-dim Tensor samples and +an optional batch-shape x q x d-dim Tensor X to a +sample_shape x batch-shape x q-dim Tensor of objective values.

+
+
+
+
+
+class botorch.acquisition.multi_objective.objective.IdentityMCMultiOutputObjective(outcomes=None, num_outcomes=None)[source]
+

Bases: MCMultiOutputObjective

+

Trivial objective that returns the unaltered samples.

+

Example

+
>>> identity_objective = IdentityMCMultiOutputObjective()
+>>> samples = sampler(posterior)
+>>> objective = identity_objective(samples)
+
+
+

Initialize Objective.

+
+
Parameters:
+
    +
  • outcomes (list[int] | None) – A list of the m’ indices that the weights should be +applied to.

  • +
  • num_outcomes (int | None) – The total number of outcomes m

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the multi-output objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of samples from +a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim Tensors of inputs.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim Tensor of objective values with +m’ the output dimension. This assumes maximization in each output +dimension).

+
+
Return type:
+

Tensor

+
+
+

This method is usually not called directly, but via the objectives.

+

Example

+
>>> # `__call__` method:
+>>> samples = sampler(posterior)
+>>> outcomes = multi_obj(samples)
+
+
+
+
+
+
+class botorch.acquisition.multi_objective.objective.WeightedMCMultiOutputObjective(weights, outcomes=None, num_outcomes=None)[source]
+

Bases: IdentityMCMultiOutputObjective

+

Objective that reweights samples by given weights vector.

+

Example

+
>>> weights = torch.tensor([1.0, -1.0])
+>>> weighted_objective = WeightedMCMultiOutputObjective(weights)
+>>> samples = sampler(posterior)
+>>> objective = weighted_objective(samples)
+
+
+

Initialize Objective.

+
+
Parameters:
+
    +
  • weights (Tensor) – m’-dim tensor of outcome weights.

  • +
  • outcomes (list[int] | None) – A list of the m’ indices that the weights should be +applied to.

  • +
  • num_outcomes (int | None) – the total number of outcomes m

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the multi-output objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of samples from +a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim Tensors of inputs.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim Tensor of objective values with +m’ the output dimension. This assumes maximization in each output +dimension).

+
+
Return type:
+

Tensor

+
+
+

This method is usually not called directly, but via the objectives.

+

Example

+
>>> # `__call__` method:
+>>> samples = sampler(posterior)
+>>> outcomes = multi_obj(samples)
+
+
+
+
+
+
+class botorch.acquisition.multi_objective.objective.FeasibilityWeightedMCMultiOutputObjective(model, X_baseline, constraint_idcs, objective=None)[source]
+

Bases: MCMultiOutputObjective

+

Construct a feasibility-weighted objective.

+

This applies feasibility weighting before calculating the objective value. +Defaults to identity if no constraints or objective is present.

+

NOTE: By passing in a single-output MCAcquisitionObjective as the objective, +this can be used as a single-output MCAcquisitionObjective as well.

+
+
Parameters:
+
    +
  • model (Model) – A fitted Model.

  • +
  • X_baseline (Tensor) – An n x d-dim tensor of points already observed.

  • +
  • constraint_idcs (list[int]) – The outcome indices of the constraints. Constraints are +handled by weighting the samples according to a sigmoid approximation +of feasibility. A positive constraint outcome implies feasibility.

  • +
  • objective (MCMultiOutputObjective | None) – An optional objective to apply after feasibility-weighting +the samples.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the multi-output objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of samples from +a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim Tensors of inputs.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim Tensor of objective values with +m’ the output dimension. This assumes maximization in each output +dimension).

+
+
Return type:
+

Tensor

+
+
+

This method is usually not called directly, but via the objectives.

+

Example

+
>>> # `__call__` method:
+>>> samples = sampler(posterior)
+>>> outcomes = multi_obj(samples)
+
+
+
+
+
+
+

Cost-Aware Utility

+

Cost functions for cost-aware acquisition functions, e.g. multi-fidelity KG. +To be used in a context where there is an objective/cost tradeoff.

+
+
+class botorch.acquisition.cost_aware.CostAwareUtility(*args, **kwargs)[source]
+

Bases: Module, ABC

+

Abstract base class for cost-aware utilities.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+abstract forward(X, deltas, sampler=None)[source]
+

Evaluate the cost-aware utility on the candidates and improvements.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x d-dim Tensor of with q d-dim design +points each for each t-batch.

  • +
  • deltas (Tensor) – A num_fantasies x batch_shape-dim Tensor of num_fantasy +samples from the marginal improvement in utility over the +current state at X for each t-batch.

  • +
  • sampler (MCSampler | None) – A sampler used for sampling from the posterior of the cost +model. Some subclasses ignore this argument.

  • +
+
+
Returns:
+

A num_fantasies x batch_shape-dim Tensor of cost-transformed utilities.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.cost_aware.GenericCostAwareUtility(cost)[source]
+

Bases: CostAwareUtility

+

Generic cost-aware utility wrapping a callable.

+

Generic cost-aware utility wrapping a callable.

+
+
Parameters:
+

cost (Callable[[Tensor, Tensor], Tensor]) – A callable mapping a batch_shape x q x d’-dim candidate set +to a batch_shape-dim tensor of costs

+
+
+
+
+forward(X, deltas, sampler=None)[source]
+

Evaluate the cost function on the candidates and improvements.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x d’-dim Tensor of with q d-dim design +points for each t-batch.

  • +
  • deltas (Tensor) – A num_fantasies x batch_shape-dim Tensor of num_fantasy +samples from the marginal improvement in utility over the +current state at X for each t-batch.

  • +
  • sampler (MCSampler | None) – Ignored.

  • +
+
+
Returns:
+

A num_fantasies x batch_shape-dim Tensor of cost-weighted utilities.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.cost_aware.InverseCostWeightedUtility(cost_model, use_mean=True, cost_objective=None, min_cost=0.01)[source]
+

Bases: CostAwareUtility

+

A cost-aware utility using inverse cost weighting based on a model.

+

Computes the cost-aware utility by inverse-weighting samples +U = (u_1, …, u_N) of the increase in utility. If use_mean=True, this +uses the posterior mean mean_cost of the cost model, i.e. +weighted utility = mean(U) / mean_cost. If use_mean=False, it uses +samples C = (c_1, …, c_N) from the posterior of the cost model and +performs the inverse weighting on the sample level: +weighted utility = mean(u_1 / c_1, …, u_N / c_N).

+

Where values in (u_1, …, u_N) are negative, or for mean(U) < 0, the +weighted utility is instead calculated via scaling by the cost, i.e. if +use_mean=True: weighted_utility = mean(U) * mean_cost and if +use_mean=False: +weighted utility = mean(u_1 * c_1, u_2 / c_2, u_3 * c_3, …, u_N / c_N), +depending on whether (u_* >= 0), as with u_2 and u_N in this case, or +(u_* < 0) as with u_1 and u_3.

+

The cost is additive across multiple elements of a q-batch.

+

Cost-aware utility that weights increase in utility by inverse cost. +For negative increases in utility, the utility is instead scaled by the +cost. See the class description for more information.

+
+
Parameters:
+
    +
  • cost_model (DeterministicModel | GPyTorchModel) – A model of the cost of evaluating a candidate +set X, where X are the same features as in the model for the +acquisition function this is to be used with. If no cost_objective +is specified, the outputs are required to be non-negative.

  • +
  • use_mean (bool) – If True, use the posterior mean, otherwise use posterior +samples from the cost model.

  • +
  • cost_objective (MCAcquisitionObjective | None) – If specified, transform the posterior mean / the +posterior samples from the cost model. This can be used e.g. to +un-transform predictions/samples of a cost model fit on the +log-transformed cost (often done to ensure non-negativity). If the +cost model is multi-output, then by default this will sum the cost +across outputs.

  • +
  • min_cost (float) – A value used to clamp the cost samples so that they are not +too close to zero, which may cause numerical issues.

  • +
+
+
Returns:
+

The inverse-cost-weighted utility.

+
+
+
+
+forward(X, deltas, sampler=None, X_evaluation_mask=None)[source]
+

Evaluate the cost function on the candidates and improvements. Note +that negative values of deltas are instead scaled by the cost, and not +inverse-weighted. See the class description for more information.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x d-dim Tensor of with q d-dim design +points each for each t-batch.

  • +
  • deltas (Tensor) – A num_fantasies x batch_shape-dim Tensor of num_fantasy +samples from the marginal improvement in utility over the +current state at X for each t-batch.

  • +
  • sampler (MCSampler | None) – A sampler used for sampling from the posterior of the cost +model (required if use_mean=False, ignored if use_mean=True).

  • +
  • X_evaluation_mask (Tensor | None) – A q x m-dim boolean tensor indicating which +outcomes should be evaluated for each design in the batch.

  • +
+
+
Returns:
+

A num_fantasies x batch_shape-dim Tensor of cost-weighted utilities.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Risk Measures

+

Risk Measures implemented as Monte-Carlo objectives, based on Bayesian +optimization of risk measures as introduced in [Cakmak2020risk]. For a +broader discussion of Monte-Carlo methods for VaR and CVaR risk measures, +see also [Hong2014review].

+
+
+[Cakmak2020risk] +

S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of +Risk Measures. Advances in Neural Information Processing Systems 33, 2020.

+
+
+[Hong2014review] +

L. J. Hong, Z. Hu, and G. Liu. Monte carlo methods for value-at-risk and +conditional value-at-risk: a review. ACM Transactions on Modeling and +Computer Simulation, 2014.

+
+
+
+
+class botorch.acquisition.risk_measures.RiskMeasureMCObjective(n_w, preprocessing_function=None)[source]
+

Bases: MCAcquisitionObjective, ABC

+

Objective transforming the posterior samples to samples of a risk measure.

+

The risk measure is calculated over joint q-batch samples from the posterior. +If the q-batch includes samples corresponding to multiple inputs, it is assumed +that first n_w samples correspond to first input, second n_w samples +correspond to second input etc.

+

The risk measures are commonly defined for minimization by considering the +upper tail of the distribution, i.e., treating larger values as being undesirable. +BoTorch by default assumes a maximization objective, so the default behavior here +is to calculate the risk measures w.r.t. the lower tail of the distribution. +This can be changed by passing a preprocessing function with +weights=torch.tensor([-1.0]).

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the samples +before computing the risk measure. This can be used to scalarize +multi-output samples before calculating the risk measure. +For constrained optimization, this should also apply +feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch-dim tensor.

  • +
+
+
+
+
+abstract forward(samples, X=None)[source]
+

Calculate the risk measure corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim tensor of risk measure samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.risk_measures.CVaR(alpha, n_w, preprocessing_function=None)[source]
+

Bases: RiskMeasureMCObjective

+

The Conditional Value-at-Risk risk measure.

+

The Conditional Value-at-Risk measures the expectation of the worst outcomes +(small rewards or large losses) with a total probability of 1 - alpha. It +is commonly defined as the conditional expectation of the reward function, +with the condition that the reward is smaller than the corresponding +Value-at-Risk (also defined below).

+
+
Note: Due to the use of a discrete w_set of samples, the VaR and CVaR

calculated here are (possibly biased) Monte-Carlo approximations of +the true risk measures.

+
+
+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • alpha (float) – The risk level, float in (0.0, 1.0].

  • +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the samples +before computing the risk measure. This can be used to scalarize +multi-output samples before calculating the risk measure. +For constrained optimization, this should also apply +feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the CVaR corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim tensor of CVaR samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.risk_measures.VaR(alpha, n_w, preprocessing_function=None)[source]
+

Bases: CVaR

+

The Value-at-Risk risk measure.

+

Value-at-Risk measures the smallest possible reward (or largest possible loss) +after excluding the worst outcomes with a total probability of 1 - alpha. It +is commonly used in financial risk management, and it corresponds to the +1 - alpha quantile of a given random variable.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • alpha (float) – The risk level, float in (0.0, 1.0].

  • +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the samples +before computing the risk measure. This can be used to scalarize +multi-output samples before calculating the risk measure. +For constrained optimization, this should also apply +feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the VaR corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim tensor of VaR samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.risk_measures.WorstCase(n_w, preprocessing_function=None)[source]
+

Bases: RiskMeasureMCObjective

+

The worst-case risk measure.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the samples +before computing the risk measure. This can be used to scalarize +multi-output samples before calculating the risk measure. +For constrained optimization, this should also apply +feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the worst-case measure corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim tensor of worst-case samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.risk_measures.Expectation(n_w, preprocessing_function=None)[source]
+

Bases: RiskMeasureMCObjective

+

The expectation risk measure.

+

For unconstrained problems, we recommend using the ExpectationPosteriorTransform +instead. ExpectationPosteriorTransform directly transforms the posterior +distribution over q * n_w to a posterior of q expectations, significantly +reducing the cost of posterior sampling as a result.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the samples +before computing the risk measure. This can be used to scalarize +multi-output samples before calculating the risk measure. +For constrained optimization, this should also apply +feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the expectation corresponding to the given samples. +This calculates the expectation / mean / average of each n_w samples +across the q-batch dimension. If self.weights is given, the samples +are scalarized across the output dimension before taking the expectation.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim tensor of expectation samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Thompson Sampling

+
+
+class botorch.acquisition.thompson_sampling.PathwiseThompsonSampling(model, posterior_transform=None)[source]
+

Bases: AcquisitionFunction

+

Single-outcome Thompson Sampling packaged as an (analytic) +acquisition function. Querying the acquisition function gives the summed +values of one or more draws from a pathwise drawn posterior sample, and thus +it maximization yields one (or multiple) Thompson sample(s).

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> TS = PathwiseThompsonSampling(model)
+
+
+

Single-outcome TS.

+
+
Parameters:
+
    +
  • model (Model) – A fitted GP model.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform. If using a multi-output model, +a PosteriorTransform that transforms the multi-output posterior into a +single-output posterior is required.

  • +
+
+
+
+
+redraw()[source]
+
+
Return type:
+

None

+
+
+
+
+
+forward(X)[source]
+

Evaluate the pathwise posterior sample draws on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b1 x … bk) x 1 x d-dim batched tensor of d-dim design points.

+
+
Returns:
+

A (b1 x … bk) x [num_models for fully bayesian]-dim tensor of +evaluations on the posterior sample draws.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Output Risk Measures

+

Multi-output extensions of the risk measures, implemented as Monte-Carlo +objectives. Except for MVaR, the risk measures are computed over each +output dimension independently. In contrast, MVaR is computed using the +joint distribution of the outputs, and provides more accurate risk estimates.

+

References

+
+
+[Prekopa2012MVaR] +(1,2,3,4) +

A. Prekopa. Multivariate value at risk and related topics. +Annals of Operations Research, 2012.

+
+
+[Cousin2013MVaR] +(1,2) +

A. Cousin and E. Di Bernardino. On multivariate extensions of Value-at-Risk. +Journal of Multivariate Analysis, 2013.

+
+
+[Daulton2022MARS] +(1,2,3) +

S. Daulton, S, Cakmak, M. Balandat, M. Osborne, E. Zhou, and E. Bakshy. +Robust multi-objective Bayesian optimization under input noise. +Proceedings of the 39th International Conference on Machine Learning, 2022.

+
+
+
+
+class botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputRiskMeasureMCObjective(n_w, preprocessing_function=None)[source]
+

Bases: RiskMeasureMCObjective, MCMultiOutputObjective, ABC

+

Objective transforming the multi-output posterior samples to samples +of a multi-output risk measure.

+

The risk measure is calculated over joint q-batch samples from the posterior. +If the q-batch includes samples corresponding to multiple inputs, it is assumed +that first n_w samples correspond to first input, second n_w samples +correspond to second input, etc.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the +samples before computing the risk measure. This can be used to +remove non-objective outcomes or to align all outcomes for +maximization. For constrained optimization, this should also +apply feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch x m’-dim tensor.

  • +
+
+
+
+
+abstract forward(samples, X=None)[source]
+

Calculate the risk measure corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim tensor of risk measure samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputExpectation(n_w, preprocessing_function=None)[source]
+

Bases: MultiOutputRiskMeasureMCObjective

+

A multi-output MC expectation risk measure.

+

For unconstrained problems, we recommend using the ExpectationPosteriorTransform +instead. ExpectationPosteriorTransform directly transforms the posterior +distribution over q * n_w to a posterior of q expectations, significantly +reducing the cost of posterior sampling as a result.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the +samples before computing the risk measure. This can be used to +remove non-objective outcomes or to align all outcomes for +maximization. For constrained optimization, this should also +apply feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch x m’-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the expectation of the given samples. Expectation is +calculated over each n_w samples in the q-batch dimension.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim tensor of expectation samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentCVaR(alpha, n_w, preprocessing_function=None)[source]
+

Bases: CVaR, MultiOutputRiskMeasureMCObjective

+

The multi-output Conditional Value-at-Risk risk measure that operates on +each output dimension independently. Since this does not consider the joint +distribution of the outputs (i.e., that the outputs were evaluated on same +perturbed input and are not independent), the risk estimates provided by +IndependentCVaR in general are more optimistic than the definition of CVaR +would suggest.

+

The Conditional Value-at-Risk measures the expectation of the worst outcomes +(small rewards or large losses) with a total probability of 1 - alpha. It +is commonly defined as the conditional expectation of the reward function, +with the condition that the reward is smaller than the corresponding +Value-at-Risk (also defined below).

+

NOTE: Due to the use of a discrete w_set of samples, the VaR and CVaR +calculated here are (possibly biased) Monte-Carlo approximations of the +true risk measures.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • alpha (float) – The risk level, float in (0.0, 1.0].

  • +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the samples +before computing the risk measure. This can be used to scalarize +multi-output samples before calculating the risk measure. +For constrained optimization, this should also apply +feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the CVaR corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim tensor of CVaR samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentVaR(alpha, n_w, preprocessing_function=None)[source]
+

Bases: IndependentCVaR

+

The multi-output Value-at-Risk risk measure that operates on each output +dimension independently. For the same reasons as IndependentCVaR, the risk +estimates provided by this are in general more optimistic than the definition +of VaR would suggest.

+

Value-at-Risk measures the smallest possible reward (or largest possible loss) +after excluding the worst outcomes with a total probability of 1 - alpha. It +is commonly used in financial risk management, and it corresponds to the +1 - alpha quantile of a given random variable.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • alpha (float) – The risk level, float in (0.0, 1.0].

  • +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the samples +before computing the risk measure. This can be used to scalarize +multi-output samples before calculating the risk measure. +For constrained optimization, this should also apply +feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the VaR corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim tensor of VaR samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputWorstCase(n_w, preprocessing_function=None)[source]
+

Bases: MultiOutputRiskMeasureMCObjective

+

The multi-output worst-case risk measure.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the +samples before computing the risk measure. This can be used to +remove non-objective outcomes or to align all outcomes for +maximization. For constrained optimization, this should also +apply feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch x m’-dim tensor.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the worst-case measure corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim tensor of worst-case samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.multi_output_risk_measures.MVaR(n_w, alpha, expectation=False, preprocessing_function=None, *, pad_to_n_w=False, filter_dominated=True, use_counting=False)[source]
+

Bases: MultiOutputRiskMeasureMCObjective

+

The multivariate Value-at-Risk as introduced in [Prekopa2012MVaR].

+

MVaR is defined as the non-dominated set of points in the extended domain +of the random variable that have multivariate CDF greater than or equal to +alpha. Note that MVaR is set valued and the size of the set depends on the +particular realizations of the random variable. [Cousin2013MVaR] instead +propose to use the expectation of the set-valued MVaR as the multivariate +VaR. We support this alternative with an expectation flag.

+

This supports approximate gradients as discussed in [Daulton2022MARS].

+

The multivariate Value-at-Risk.

+
+
Parameters:
+
    +
  • n_w (int) – The size of the w_set to calculate the risk measure over.

  • +
  • alpha (float) – The risk level of MVaR, float in (0.0, 1.0]. Each MVaR value +dominates alpha fraction of all observations.

  • +
  • expectation (bool) – If True, returns the expectation of the MVaR set as is +done in [Cousin2013MVaR]. Otherwise, it returns the union of all +values in the MVaR set. Default: False.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the +samples before computing the risk measure. This can be used to +remove non-objective outcomes or to align all outcomes for +maximization. For constrained optimization, this should also +apply feasibility-weighting to samples. Given a batch x m-dim +tensor of samples, this should return a batch x m’-dim tensor.

  • +
  • pad_to_n_w (bool) – If True, instead of padding up to k’, which is the size of +the largest MVaR set across all batches, we pad the MVaR set up to +n_w. This produces a return tensor of known size, however, it may +in general be much larger than the alternative. See forward for +more details on the return shape. +NOTE: this is only relevant if expectation=False.

  • +
  • filter_dominated (bool) – If True, returns the non-dominated subset of +alpha level points (this is MVaR as defined by [Prekopa2012MVaR]). +Disabling this will make it faster, and may be preferable if +the dominated points will be filtered out later, e.g., while +calculating the hypervolume. Disabling this is not recommended +if expectation=True.

  • +
  • use_counting (bool) – If True, uses get_mvar_set_via_counting for finding the +MVaR set. This is method is less memory intensive than the vectorized +implementation, which is beneficial when n_w is quite large.

  • +
+
+
+
+
+get_mvar_set_via_counting(Y)[source]
+

Find MVaR set based on the definition in [Prekopa2012MVaR].

+

This first calculates the CDF for each point on the extended domain of the +random variable (the grid defined by the given samples), then takes the +values with CDF equal to (rounded if necessary) alpha. The non-dominated +subset of these form the MVaR set.

+

This implementation processes each batch of Y in a for loop using a counting +based implementation. It requires less memory than the vectorized implementation +and should be used with large (>128) n_w values.

+
+
Parameters:
+

Y (Tensor) – A batch x n_w x m-dim tensor of outcomes.

+
+
Returns:
+

A batch length list of k x m-dim tensor of MVaR values, where k +depends on the corresponding batch inputs. Note that MVaR values in general +are not in-sample points.

+
+
Return type:
+

list[Tensor]

+
+
+
+
+
+get_mvar_set_vectorized(Y)[source]
+

Find MVaR set based on the definition in [Prekopa2012MVaR].

+

This first calculates the CDF for each point on the extended domain of the +random variable (the grid defined by the given samples), then takes the +values with CDF equal to (rounded if necessary) alpha. The non-dominated +subset of these form the MVaR set.

+

This implementation uses computes the CDF of each point using highly vectorized +operations. As such, it may use large amounts of memory, particularly when the +batch size and/or n_w are large. It is typically faster than the alternative +implementation when computing MVaR of a large batch of points with small to +moderate (<128 for m=2, <64 for m=3) n_w.

+
+
Parameters:
+

Y (Tensor) – A batch x n_w x m-dim tensor of observations.

+
+
Returns:
+

A batch length list of k x m-dim tensor of MVaR values, where k +depends on the corresponding batch inputs. Note that MVaR values in general +are not in-sample points.

+
+
Return type:
+

list[Tensor]

+
+
+
+
+
+make_differentiable(prepared_samples, mvars)[source]
+

An experimental approach for obtaining the gradient of the MVaR via +component-wise mapping to original samples. See [Daulton2022MARS].

+
+
Parameters:
+
    +
  • prepared_samples (Tensor) – A (sample_shape * batch_shape * q) x n_w x m-dim tensor +of posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • mvars (Tensor) – A (sample_shape * batch_shape * q) x k x m-dim tensor +of padded MVaR values.

  • +
+
+
Returns:
+

The same mvars with entries mapped to inputs to produce gradients.

+
+
Return type:
+

Tensor

+
+
+
+
+
+forward(samples, X=None)[source]
+

Calculate the MVaR corresponding to the given samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x (q * n_w) x m-dim tensor of +posterior samples. The q-batches should be ordered so that each +n_w block of samples correspond to the same input.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Ignored.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q x m’-dim tensor of MVaR values, +if self.expectation=True. +Otherwise, this returns a sample_shape x batch_shape x (q * k’) x m’-dim +tensor, where k’ is the maximum k across all batches that is returned +by get_mvar_set_…. Each (q * k’) x m’ corresponds to the k MVaR +values for each q batch of n_w inputs, padded up to k’ by repeating +the last element. If self.pad_to_n_w, we set k’ = self.n_w, producing +a deterministic return shape.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.multi_objective.multi_output_risk_measures.MARS(alpha, n_w, chebyshev_weights, baseline_Y=None, ref_point=None, preprocessing_function=None)[source]
+

Bases: VaR, MultiOutputRiskMeasureMCObjective

+

MVaR Approximation based on Random Scalarizations as introduced +in [Daulton2022MARS].

+

This approximates MVaR via VaR of Chebyshev scalarizations, where each +scalarization corresponds to a point in the MVaR set. As implemented, +this uses one set of scalarization weights to approximate a single MVaR value. +Note that due to the normalization within the Chebyshev scalarization, +the output of this risk measure may not be on the same scale as its inputs.

+

Transform the posterior samples to samples of a risk measure.

+
+
Parameters:
+
    +
  • alpha (float) – The risk level, float in (0.0, 1.0].

  • +
  • n_w (int) – The size of the perturbation set to calculate the risk measure over.

  • +
  • chebyshev_weights (Tensor | list[float]) – The weights to use in the Chebyshev scalarization. +The Chebyshev scalarization is applied before computing VaR. +The weights must be non-negative. See preprocessing_function to +support minimization objectives.

  • +
  • baseline_Y (Tensor | None) – An n’ x d-dim tensor of baseline outcomes to use in +determining the normalization bounds for Chebyshev scalarization. +It is recommended to set this via set_baseline_Y helper.

  • +
  • ref_point (Tensor | list[float] | None) – An optional MVaR reference point to use in determining +the normalization bounds for Chebyshev scalarization.

  • +
  • preprocessing_function (Callable[[Tensor], Tensor] | None) – A preprocessing function to apply to the +samples before computing the risk measure. This can be used to +remove non-objective outcomes or to align all outcomes for +maximization. For constrained optimization, this should also +apply feasibility-weighting to samples.

  • +
+
+
+
+
+set_baseline_Y(model, X_baseline, Y_samples=None)[source]
+

Set the baseline_Y based on the MVaR predictions of the model +for X_baseline.

+
+
Parameters:
+
    +
  • model (Model | None) – The model being used for MARS optimization. Must have a compatible +InputPerturbation transform attached. Ignored if Y_samples is given.

  • +
  • X_baseline (Tensor | None) – An n x d-dim tensor of previously evaluated points. +Ignored if Y_samples is given.

  • +
  • Y_samples (Tensor | None) – An optional (n * n_w) x d-dim tensor of predictions. If given, +instead of sampling from the model, these are used.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+property chebyshev_weights: Tensor
+

The weights used in Chebyshev scalarization.

+
+
+
+property baseline_Y: Tensor | None
+

Baseline outcomes used in determining the normalization bounds.

+
+
+
+property chebyshev_objective: Callable[[Tensor, Tensor | None], Tensor]
+

The objective for applying the Chebyshev scalarization.

+
+
+
+
+
+

Utilities

+
+

Fixed Feature Acquisition Function

+

A wrapper around AcquisitionFunctions to fix certain features for optimization. +This is useful e.g. for performing contextual optimization.

+
+
+botorch.acquisition.fixed_feature.get_dtype_of_sequence(values)[source]
+

Return torch.float32 if everything is single-precision and torch.float64 +otherwise.

+

Numbers (non-tensors) are double-precision.

+
+
Parameters:
+

values (Sequence[Tensor | float])

+
+
Return type:
+

dtype

+
+
+
+
+
+botorch.acquisition.fixed_feature.get_device_of_sequence(values)[source]
+

CPU if everything is on the CPU; Cuda otherwise.

+

Numbers (non-tensors) are considered to be on the CPU.

+
+
Parameters:
+

values (Sequence[Tensor | float])

+
+
Return type:
+

dtype

+
+
+
+
+
+class botorch.acquisition.fixed_feature.FixedFeatureAcquisitionFunction(acq_function, d, columns, values)[source]
+

Bases: AcquisitionFunction

+

A wrapper around AcquisitionFunctions to fix a subset of features.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)  # d = 5
+>>> qEI = qExpectedImprovement(model, best_f=0.0)
+>>> columns = [2, 4]
+>>> values = X[..., columns]
+>>> qEI_FF = FixedFeatureAcquisitionFunction(qEI, 5, columns, values)
+>>> qei = qEI_FF(test_X)  # d' = 3
+
+
+

Derived Acquisition Function by fixing a subset of input features.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – The base acquisition function, operating on input +tensors X_full of feature dimension d.

  • +
  • d (int) – The feature dimension expected by acq_function.

  • +
  • columns (list[int]) – d_f < d indices of columns in X_full that are to be +fixed to the provided values.

  • +
  • values (Tensor | Sequence[Tensor | float]) – The values to which to fix the columns in columns. Either +a full batch_shape x q x d_f tensor of values (if values are +different for each of the q input points), or an array-like of +values that is broadcastable to the input across t-batch and +q-batch dimensions, e.g. a list of length d_f if values +are the same across all t and q-batch dimensions, or a +combination of Tensor`s and numbers which can be broadcasted +to form a tensor with trailing dimension size of `d_f.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate base acquisition function under the fixed features.

+
+
Parameters:
+

X (Tensor) – Input tensor of feature dimension d’ < d such that d’ + d_f = d.

+
+
Returns:
+

Base acquisition function evaluated on tensor X_full constructed +by adding values in the appropriate places (see +_construct_X_full).

+
+
+
+
+
+property X_pending
+

Return the X_pending of the base acquisition function.

+
+
+
+
+

Constructors for Acquisition Function Input Arguments

+

A registry of helpers for generating inputs to acquisition function +constructors programmatically from a consistent input format.

+
+
+botorch.acquisition.input_constructors.get_acqf_input_constructor(acqf_cls)[source]
+

Get acquisition function input constructor from registry.

+
+
Parameters:
+

acqf_cls (type[AcquisitionFunction]) – The AcquisitionFunction class (not instance) for which +to retrieve the input constructor.

+
+
Returns:
+

The input constructor associated with acqf_cls.

+
+
Return type:
+

Callable[[…], dict[str, Any]]

+
+
+
+
+
+botorch.acquisition.input_constructors.allow_only_specific_variable_kwargs(f)[source]
+

Decorator for allowing a function to accept keyword arguments that are not +explicitly listed in the function signature, but only specific ones.

+

This decorator is applied in acqf_input_constructor so that all constructors +obtained with acqf_input_constructor allow keyword +arguments such as training_data and objective, even if they do not appear +in the signature of f. Any other keyword arguments will raise an error.

+
+
Parameters:
+

f (Callable[[...], T])

+
+
Return type:
+

Callable[[…], T]

+
+
+
+
+
+botorch.acquisition.input_constructors.acqf_input_constructor(*acqf_cls)[source]
+

Decorator for registering acquisition function input constructors.

+
+
Parameters:
+

acqf_cls (type[AcquisitionFunction]) – The AcquisitionFunction classes (not instances) for which +to register the input constructor.

+
+
Return type:
+

Callable[[…], AcquisitionFunction]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_posterior_mean(model, posterior_transform=None)[source]
+

Construct kwargs for PosteriorMean acquisition function.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Model | PosteriorTransform | None]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_best_f(model, training_data, posterior_transform=None, best_f=None, maximize=True)[source]
+

Construct kwargs for the acquisition functions requiring best_f.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model. +Used to determine default value for best_f.

  • +
  • best_f (float | Tensor | None) – Threshold above (or below) which improvement is defined.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_ucb(model, posterior_transform=None, beta=0.2, maximize=True)[source]
+

Construct kwargs for UpperConfidenceBound.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • beta (float | Tensor) – Either a scalar or a one-dim tensor with b elements (batch mode) +representing the trade-off parameter between mean and covariance

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_noisy_ei(model, training_data, num_fantasies=20, maximize=True)[source]
+

Construct kwargs for NoisyExpectedImprovement.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model.

  • +
  • num_fantasies (int) – The number of fantasies to generate. The higher this +number the more accurate the model (at the expense of model +complexity and performance).

  • +
  • maximize (bool) – If True, consider the problem a maximization problem.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qSimpleRegret(model, objective=None, posterior_transform=None, X_pending=None, sampler=None, constraints=None, X_baseline=None)[source]
+

Construct kwargs for qSimpleRegret.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • X_pending (Tensor | None) – A batch_shape, m x d-dim Tensor of m design points +that have points that have been submitted for function evaluation +but have not yet been evaluated.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • X_baseline (Tensor | None) – A batch_shape x r x d-dim Tensor of r design points +that have already been observed. These points are considered as +the potential best design point. If omitted, checks that all +training_data have the same input features and take the first X.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qEI(model, training_data, objective=None, posterior_transform=None, X_pending=None, sampler=None, best_f=None, constraints=None, eta=0.001)[source]
+

Construct kwargs for the qExpectedImprovement constructor.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • best_f (float | Tensor | None) – Threshold above (or below) which improvement is defined.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qLogEI(model, training_data, objective=None, posterior_transform=None, X_pending=None, sampler=None, best_f=None, constraints=None, eta=0.001, fat=True, tau_max=0.01, tau_relu=1e-06)[source]
+

Construct kwargs for the qExpectedImprovement constructor.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • best_f (float | Tensor | None) – Threshold above (or below) which improvement is defined.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
  • fat (bool) – Toggles the logarithmic / linear asymptotic behavior of the smooth +approximation to the ReLU.

  • +
  • tau_max (float) – Temperature parameter controlling the sharpness of the smooth +approximations to max.

  • +
  • tau_relu (float) – Temperature parameter controlling the sharpness of the smooth +approximations to ReLU.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qNEI(model, training_data, objective=None, posterior_transform=None, X_pending=None, sampler=None, X_baseline=None, prune_baseline=True, cache_root=True, constraints=None, eta=0.001)[source]
+

Construct kwargs for the qNoisyExpectedImprovement constructor.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • X_baseline (Tensor | None) – A batch_shape x r x d-dim Tensor of r design points +that have already been observed. These points are considered as +the potential best design point. If omitted, checks that all +training_data have the same input features and take the first X.

  • +
  • prune_baseline (bool | None) – If True, remove points in X_baseline that are +highly unlikely to be the best point. This can significantly +improve performance and is generally recommended.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
  • cache_root (bool | None)

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qLogNEI(model, training_data, objective=None, posterior_transform=None, X_pending=None, sampler=None, X_baseline=None, prune_baseline=True, cache_root=True, constraints=None, eta=0.001, fat=True, tau_max=0.01, tau_relu=1e-06)[source]
+

Construct kwargs for the qLogNoisyExpectedImprovement constructor.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • X_baseline (Tensor | None) – A batch_shape x r x d-dim Tensor of r design points +that have already been observed. These points are considered as +the potential best design point. If omitted, checks that all +training_data have the same input features and take the first X.

  • +
  • prune_baseline (bool | None) – If True, remove points in X_baseline that are +highly unlikely to be the best point. This can significantly +improve performance and is generally recommended.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
  • fat (bool) – Toggles the use of the fat-tailed non-linearities to smoothly approximate +the constraints indicator function.

  • +
  • tau_max (float) – Temperature parameter controlling the sharpness of the smooth +approximations to max.

  • +
  • tau_relu (float) – Temperature parameter controlling the sharpness of the smooth +approximations to ReLU.

  • +
  • cache_root (bool | None)

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qPI(model, training_data, objective=None, posterior_transform=None, X_pending=None, sampler=None, tau=0.001, best_f=None, constraints=None, eta=0.001)[source]
+

Construct kwargs for the qProbabilityOfImprovement constructor.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • tau (float) – The temperature parameter used in the sigmoid approximation +of the step function. Smaller values yield more accurate +approximations of the function, but result in gradients +estimates with higher variance.

  • +
  • best_f (float | Tensor | None) – The best objective value observed so far (assumed noiseless). Can +be a batch_shape-shaped tensor, which in case of a batched model +specifies potentially different values for each element of the batch.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qUCB(model, objective=None, posterior_transform=None, X_pending=None, sampler=None, X_baseline=None, constraints=None, beta=0.2)[source]
+

Construct kwargs for the qUpperConfidenceBound constructor.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to be used in the acquisition function.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • X_baseline (Tensor | None) – A batch_shape x r x d-dim Tensor of r design points +that have already been observed. These points are used to +compute with infeasible cost when there are constraints.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • beta (float) – Controls tradeoff between mean and standard deviation in UCB.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_EHVI(model, training_data, objective_thresholds, posterior_transform=None, constraints=None, alpha=None, Y_pmean=None)[source]
+

Construct kwargs for ExpectedHypervolumeImprovement constructor.

+
+
Parameters:
+
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qEHVI(model, training_data, objective_thresholds, objective=None, constraints=None, alpha=None, sampler=None, X_pending=None, eta=0.001, mc_samples=128, qmc=True)[source]
+

Construct kwargs for qExpectedHypervolumeImprovement and +qLogExpectedHypervolumeImprovement.

+
+
Parameters:
+
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qNEHVI(model, training_data, objective_thresholds, objective=None, X_baseline=None, constraints=None, alpha=None, sampler=None, X_pending=None, eta=0.001, fat=False, mc_samples=128, qmc=True, prune_baseline=True, cache_pending=True, max_iep=0, incremental_nehvi=True, cache_root=True)[source]
+

Construct kwargs for qNoisyExpectedHypervolumeImprovement’s constructor.

+
+
Parameters:
+
    +
  • model (Model)

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset])

  • +
  • objective_thresholds (Tensor)

  • +
  • objective (MCMultiOutputObjective | None)

  • +
  • X_baseline (Tensor | None)

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None)

  • +
  • alpha (float | None)

  • +
  • sampler (MCSampler | None)

  • +
  • X_pending (Tensor | None)

  • +
  • eta (float)

  • +
  • fat (bool)

  • +
  • mc_samples (int)

  • +
  • qmc (bool)

  • +
  • prune_baseline (bool)

  • +
  • cache_pending (bool)

  • +
  • max_iep (int)

  • +
  • incremental_nehvi (bool)

  • +
  • cache_root (bool)

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qLogNEHVI(model, training_data, objective_thresholds, objective=None, X_baseline=None, constraints=None, alpha=None, sampler=None, X_pending=None, eta=0.001, fat=True, mc_samples=128, qmc=True, prune_baseline=True, cache_pending=True, max_iep=0, incremental_nehvi=True, cache_root=True, tau_relu=1e-06, tau_max=0.01)[source]
+

Construct kwargs for qLogNoisyExpectedHypervolumeImprovement’s constructor.”

+
+
Parameters:
+
    +
  • model (Model)

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset])

  • +
  • objective_thresholds (Tensor)

  • +
  • objective (MCMultiOutputObjective | None)

  • +
  • X_baseline (Tensor | None)

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None)

  • +
  • alpha (float | None)

  • +
  • sampler (MCSampler | None)

  • +
  • X_pending (Tensor | None)

  • +
  • eta (float)

  • +
  • fat (bool)

  • +
  • mc_samples (int)

  • +
  • qmc (bool)

  • +
  • prune_baseline (bool)

  • +
  • cache_pending (bool)

  • +
  • max_iep (int)

  • +
  • incremental_nehvi (bool)

  • +
  • cache_root (bool)

  • +
  • tau_relu (float)

  • +
  • tau_max (float)

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qLogNParEGO(model, training_data, scalarization_weights=None, objective=None, X_pending=None, sampler=None, X_baseline=None, prune_baseline=True, cache_root=True, constraints=None, eta=0.001, fat=True, tau_max=0.01, tau_relu=1e-06)[source]
+

Construct kwargs for the qLogNoisyExpectedImprovement constructor.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the acquisition function.

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Dataset(s) used to train the model.

  • +
  • scalarization_weights (Tensor | None) – A m-dim Tensor of weights to be used in the +Chebyshev scalarization. If omitted, samples from the unit simplex.

  • +
  • objective (MCMultiOutputObjective | None) – The MultiOutputMCAcquisitionObjective under which the samples are +evaluated before applying Chebyshev scalarization. +Defaults to IdentityMultiOutputObjective().

  • +
  • X_pending (Tensor | None) – A m x d-dim Tensor of m design points that have been +submitted for function evaluation but have not yet been evaluated. +Concatenated into X upon forward call.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If omitted, uses +the acquisition functions’s default sampler.

  • +
  • X_baseline (Tensor | None) – A batch_shape x r x d-dim Tensor of r design points +that have already been observed. These points are considered as +the potential best design point. If omitted, checks that all +training_data have the same input features and take the first X.

  • +
  • prune_baseline (bool | None) – If True, remove points in X_baseline that are +highly unlikely to be the best point. This can significantly +improve performance and is generally recommended.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are considered satisfied if the output is less than zero.

  • +
  • eta (Tensor | float) – Temperature parameter(s) governing the smoothness of the sigmoid +approximation to the constraint indicators. For more details, on this +parameter, see the docs of compute_smoothed_feasibility_indicator.

  • +
  • fat (bool) – Toggles the use of the fat-tailed non-linearities to smoothly approximate +the constraints indicator function.

  • +
  • tau_max (float) – Temperature parameter controlling the sharpness of the smooth +approximations to max.

  • +
  • tau_relu (float) – Temperature parameter controlling the sharpness of the smooth +approximations to ReLU.

  • +
  • cache_root (bool | None)

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qMES(model, training_data, bounds, posterior_transform=None, candidate_size=1000, maximize=True)[source]
+

Construct kwargs for qMaxValueEntropy constructor.

+
+
Parameters:
+
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_mf_base(target_fidelities, fidelity_weights=None, cost_intercept=1.0, num_trace_observations=0)[source]
+

Construct kwargs for a multifidelity acquisition function’s constructor.

+
+
Parameters:
+
    +
  • target_fidelities (dict[int, int | float])

  • +
  • fidelity_weights (dict[int, float] | None)

  • +
  • cost_intercept (float)

  • +
  • num_trace_observations (int)

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qKG(model, training_data, bounds, objective=None, posterior_transform=None, num_fantasies=64, with_current_value=False, **optimize_objective_kwargs)[source]
+

Construct kwargs for qKnowledgeGradient constructor.

+
+
Parameters:
+
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qHVKG(model, training_data, bounds, objective_thresholds, objective=None, posterior_transform=None, num_fantasies=8, num_pareto=10, **optimize_objective_kwargs)[source]
+

Construct kwargs for qKnowledgeGradient constructor.

+
+
Parameters:
+
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qMFKG(model, training_data, bounds, target_fidelities, objective=None, posterior_transform=None, fidelity_weights=None, cost_intercept=1.0, num_trace_observations=0, num_fantasies=64, **optimize_objective_kwargs)[source]
+

Construct kwargs for qMultiFidelityKnowledgeGradient constructor.

+
+
Parameters:
+
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qMFHVKG(model, training_data, bounds, target_fidelities, objective_thresholds, objective=None, posterior_transform=None, fidelity_weights=None, cost_intercept=1.0, num_trace_observations=0, num_fantasies=8, num_pareto=10, **optimize_objective_kwargs)[source]
+

Construct kwargs for qMultiFidelityHypervolumeKnowledgeGradient constructor.

+
+
Parameters:
+
    +
  • model (Model)

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset])

  • +
  • bounds (list[tuple[float, float]])

  • +
  • target_fidelities (dict[int, int | float])

  • +
  • objective_thresholds (Tensor)

  • +
  • objective (MCMultiOutputObjective | None)

  • +
  • posterior_transform (PosteriorTransform | None)

  • +
  • fidelity_weights (dict[int, float] | None)

  • +
  • cost_intercept (float)

  • +
  • num_trace_observations (int)

  • +
  • num_fantasies (int)

  • +
  • num_pareto (int)

  • +
  • optimize_objective_kwargs (None | MCAcquisitionObjective | PosteriorTransform | tuple[Tensor, Tensor] | dict[int, float] | bool | int | dict[str, Any] | Callable[[Tensor], Tensor] | Tensor)

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qMFMES(model, training_data, bounds, target_fidelities, num_fantasies=64, fidelity_weights=None, cost_intercept=1.0, num_trace_observations=0, candidate_size=1000, maximize=True)[source]
+

Construct kwargs for qMultiFidelityMaxValueEntropy constructor.

+
+
Parameters:
+
    +
  • model (Model)

  • +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset])

  • +
  • bounds (list[tuple[float, float]])

  • +
  • target_fidelities (dict[int, int | float])

  • +
  • num_fantasies (int)

  • +
  • fidelity_weights (dict[int, float] | None)

  • +
  • cost_intercept (float)

  • +
  • num_trace_observations (int)

  • +
  • candidate_size (int)

  • +
  • maximize (bool)

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_analytic_eubo(model, pref_model=None, previous_winner=None, sample_multiplier=1.0, objective=None, posterior_transform=None)[source]
+

Construct kwargs for the AnalyticExpectedUtilityOfBestOption constructor.

+

model is the primary model defined over the parameter space. It can be the +outcome model in BOPE or the preference model in PBO. pref_model is the model +defined over the outcome/metric space, which is typically the preference model +in BOPE.

+

If both model and pref_model exist, we are performing Bayesian Optimization with +Preference Exploration (BOPE). When only pref_model is None, we are performing +preferential BO (PBO).

+
+
Parameters:
+
    +
  • model (Model) – The outcome model to be used in the acquisition function in BOPE +when pref_model exists; otherwise, model is the preference model and +we are doing Preferential BO

  • +
  • pref_model (Model | None) – The preference model to be used in preference exploration as in +BOPE; if None, we are doing PBO and model is the preference model.

  • +
  • previous_winner (Tensor | None) – The previous winner of the best option.

  • +
  • sample_multiplier (float | None) – The scale factor for the single-sample model.

  • +
  • objective (LearnedObjective | None) – Ignored. This argument is allowed to be passed then ignored +because of the way that EUBO is typically used in a BOPE loop.

  • +
  • posterior_transform (PosteriorTransform | None) – Ignored. This argument is allowed to be passed then +ignored because of the way that EUBO is typically used in a BOPE +loop.

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qeubo(model, pref_model=None, sample_multiplier=1.0, sampler=None, objective=None, posterior_transform=None, X_pending=None)[source]
+

Construct kwargs for the qExpectedUtilityOfBestOption (qEUBO) constructor.

+

model is the primary model defined over the parameter space. It can be the +outcomde model in BOPE or the preference model in PBO. pref_model is the model +defined over the outcome/metric space, which is typically the preference model +in BOPE.

+

If both model and pref_model exist, we are performing Bayesian Optimization with +Preference Exploration (BOPE). When only pref_model is None, we are performing +preferential BO (PBO).

+
+
Parameters:
+
    +
  • model (Model) – The outcome model to be used in the acquisition function in BOPE +when pref_model exists; otherwise, model is the preference model and +we are doing Preferential BO

  • +
  • pref_model (Model | None) – The preference model to be used in preference exploration as in +BOPE; if None, we are doing PBO and model is the preference model.

  • +
  • sample_multiplier (float | None) – The scale factor for the single-sample model.

  • +
  • sampler (MCSampler | None)

  • +
  • objective (MCAcquisitionObjective | None)

  • +
  • posterior_transform (PosteriorTransform | None)

  • +
  • X_pending (Tensor | None)

  • +
+
+
Returns:
+

A dict mapping kwarg names of the constructor to values.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+botorch.acquisition.input_constructors.get_best_f_analytic(training_data, posterior_transform=None)[source]
+
+
Parameters:
+
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.input_constructors.get_best_f_mc(training_data, objective=None, posterior_transform=None, constraints=None, model=None)[source]
+

Computes the maximum value of the objective over the training data.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset | dict[Hashable, SupervisedDataset]) – Has fields Y, which is evaluated by objective, and X, +which is used as X_baseline. Y is of shape +batch_shape x q x m.

  • +
  • objective (MCAcquisitionObjective | None) – The objective under which to evaluate the training data. If +omitted, uses IdentityMCObjective.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform to apply to Y +before computing the objective.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – For assessing feasibility.

  • +
  • model (Model | None) – Used by compute_best_feasible_objective when there are no +feasible observations.

  • +
+
+
Returns:
+

A Tensor of shape batch_shape.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.input_constructors.optimize_objective(model, bounds, q, acq_function=None, objective=None, posterior_transform=None, linear_constraints=None, fixed_features=None, qmc=True, mc_samples=512, seed_inner=None, optimizer_options=None, post_processing_func=None, batch_initial_conditions=None, sequential=False)[source]
+

Optimize an objective under the given model.

+
+
Parameters:
+
    +
  • model (Model) – The model to be used in the objective.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X.

  • +
  • q (int) – The cardinality of input sets on which the objective is to be evaluated.

  • +
  • objective (MCAcquisitionObjective | None) – The objective to optimize.

  • +
  • posterior_transform (PosteriorTransform | None) – The posterior transform to be used in the +acquisition function.

  • +
  • linear_constraints (tuple[Tensor, Tensor] | None) – A tuple of (A, b). Given k linear constraints on a +d-dimensional space, A is k x d and b is k x 1 such that +A x <= b. (Not used by single task models).

  • +
  • fixed_features (dict[int, float] | None) – A dictionary of feature assignments {feature_index: value} to +hold fixed during generation.

  • +
  • qmc (bool) – Toggle for enabling (qmc=1) or disabling (qmc=0) use of Quasi Monte Carlo.

  • +
  • mc_samples (int) – Integer number of samples used to estimate Monte Carlo objectives.

  • +
  • seed_inner (int | None) – Integer seed used to initialize the sampler passed to MCObjective.

  • +
  • optimizer_options (dict[str, Any] | None) – Table used to lookup keyword arguments for the optimizer.

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None) – A function that post-processes an optimization +result appropriately (i.e. according to round-trip transformations).

  • +
  • batch_initial_conditions (Tensor | None) – A Tensor of initial values for the optimizer.

  • +
  • sequential (bool) – If False, uses joint optimization, otherwise uses sequential +optimization.

  • +
  • acq_function (AcquisitionFunction | None)

  • +
+
+
Returns:
+

A tuple containing the best input locations and corresponding objective values.

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_qJES(model, bounds, num_optima=64, condition_noiseless=True, posterior_transform=None, X_pending=None, estimation_type='LB', num_samples=64)[source]
+
+
Parameters:
+
    +
  • model (Model)

  • +
  • bounds (list[tuple[float, float]])

  • +
  • num_optima (int)

  • +
  • condition_noiseless (bool)

  • +
  • posterior_transform (ScalarizedPosteriorTransform | None)

  • +
  • X_pending (Tensor | None)

  • +
  • estimation_type (str)

  • +
  • num_samples (int)

  • +
+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_BALD(model, X_pending=None, sampler=None, posterior_transform=None)[source]
+
+
Parameters:
+
+
+
+
+
+
+botorch.acquisition.input_constructors.construct_inputs_NIPV(model, bounds, num_mc_points=128, X_pending=None, posterior_transform=None)[source]
+

Construct inputs for qNegIntegratedPosteriorVariance.

+
+
Parameters:
+
    +
  • model (Model)

  • +
  • bounds (list[tuple[float, float]])

  • +
  • num_mc_points (int)

  • +
  • X_pending (Tensor | None)

  • +
  • posterior_transform (PosteriorTransform | None)

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+

Penalized Acquisition Function Wrapper

+

Modules to add regularization to acquisition functions.

+
+
+class botorch.acquisition.penalized.L2Penalty(init_point)[source]
+

Bases: Module

+

L2 penalty class to be added to any arbitrary acquisition function +to construct a PenalizedAcquisitionFunction.

+

Initializing L2 regularization.

+
+
Parameters:
+

init_point (Tensor) – The “1 x dim” reference point against which +we want to regularize.

+
+
+
+
+forward(X)[source]
+
+
Parameters:
+

X (Tensor) – A “batch_shape x q x dim” representing the points to be evaluated.

+
+
Returns:
+

A tensor of size “batch_shape” representing the acqfn for each q-batch.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.penalized.L1Penalty(init_point)[source]
+

Bases: Module

+

L1 penalty class to be added to any arbitrary acquisition function +to construct a PenalizedAcquisitionFunction.

+

Initializing L1 regularization.

+
+
Parameters:
+

init_point (Tensor) – The “1 x dim” reference point against which +we want to regularize.

+
+
+
+
+forward(X)[source]
+
+
Parameters:
+

X (Tensor) – A “batch_shape x q x dim” representing the points to be evaluated.

+
+
Returns:
+

A tensor of size “batch_shape” representing the acqfn for each q-batch.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.penalized.GaussianPenalty(init_point, sigma)[source]
+

Bases: Module

+

Gaussian penalty class to be added to any arbitrary acquisition function +to construct a PenalizedAcquisitionFunction.

+

Initializing Gaussian regularization.

+
+
Parameters:
+
    +
  • init_point (Tensor) – The “1 x dim” reference point against which +we want to regularize.

  • +
  • sigma (float) – The parameter used in gaussian function.

  • +
+
+
+
+
+forward(X)[source]
+
+
Parameters:
+

X (Tensor) – A “batch_shape x q x dim” representing the points to be evaluated.

+
+
Returns:
+

A tensor of size “batch_shape” representing the acqfn for each q-batch.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.penalized.GroupLassoPenalty(init_point, groups)[source]
+

Bases: Module

+

Group lasso penalty class to be added to any arbitrary acquisition function +to construct a PenalizedAcquisitionFunction.

+

Initializing Group-Lasso regularization.

+
+
Parameters:
+
    +
  • init_point (Tensor) – The “1 x dim” reference point against which we want +to regularize.

  • +
  • groups (list[list[int]]) – Groups of indices used in group lasso.

  • +
+
+
+
+
+forward(X)[source]
+

X should be batch_shape x 1 x dim tensor. Evaluation for q-batch is not +implemented yet.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+botorch.acquisition.penalized.narrow_gaussian(X, a)[source]
+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • a (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.penalized.nnz_approx(X, target_point, a)[source]
+

Differentiable relaxation of ||X - target_point||_0

+
+
Parameters:
+
    +
  • X (Tensor) – An n x d tensor of inputs.

  • +
  • target_point (Tensor) – A tensor of size n corresponding to the target point.

  • +
  • a (Tensor) – A scalar tensor that controls the differentiable relaxation.

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+class botorch.acquisition.penalized.L0Approximation(target_point, a=1.0, **tkwargs)[source]
+

Bases: Module

+

Differentiable relaxation of the L0 norm using a Gaussian basis function.

+

Initializing L0 penalty with differentiable relaxation.

+
+
Parameters:
+
    +
  • target_point (Tensor) – A tensor corresponding to the target point.

  • +
  • a (float) – A hyperparameter that controls the differentiable relaxation.

  • +
  • tkwargs (Any)

  • +
+
+
+
+
+
+class botorch.acquisition.penalized.L0PenaltyApprox(target_point, a=1.0, **tkwargs)[source]
+

Bases: L0Approximation

+

Differentiable relaxation of the L0 norm to be added to any arbitrary +acquisition function to construct a PenalizedAcquisitionFunction.

+

Initializing L0 penalty with differentiable relaxation.

+
+
Parameters:
+
    +
  • target_point (Tensor) – A tensor corresponding to the target point.

  • +
  • a (float) – A hyperparameter that controls the differentiable relaxation.

  • +
  • tkwargs (Any)

  • +
+
+
+
+
+
+class botorch.acquisition.penalized.PenalizedAcquisitionFunction(raw_acqf, penalty_func, regularization_parameter)[source]
+

Bases: AcquisitionFunction

+

Single-outcome acquisition function regularized by the given penalty.

+
+
The usage is similar to:

raw_acqf = NoisyExpectedImprovement(…) +penalty = GroupLassoPenalty(…) +acqf = PenalizedAcquisitionFunction(raw_acqf, penalty)

+
+
+

Initializing Group-Lasso regularization.

+
+
Parameters:
+
    +
  • raw_acqf (AcquisitionFunction) – The raw acquisition function that is going to be regularized.

  • +
  • penalty_func (torch.nn.Module) – The regularization function.

  • +
  • regularization_parameter (float) – Regularization parameter used in optimization.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the acquisition function on the candidate set X.

+
+
Parameters:
+

X (Tensor) – A (b) x q x d-dim Tensor of (b) t-batches with q d-dim +design points each.

+
+
Returns:
+

A (b)-dim Tensor of acquisition function values at the given +design points X.

+
+
Return type:
+

Tensor

+
+
+
+
+
+property X_pending: Tensor | None
+
+
+
+set_X_pending(X_pending=None)[source]
+

Informs the acquisition function about pending design points.

+
+
Parameters:
+

X_pending (Tensor | None) – n x d Tensor with n d-dim design points that have +been submitted for evaluation but have not yet been evaluated.

+
+
Return type:
+

None

+
+
+
+
+
+
+botorch.acquisition.penalized.group_lasso_regularizer(X, groups)[source]
+

Computes the group lasso regularization function for the given point.

+
+
Parameters:
+
    +
  • X (Tensor) – A bxd tensor representing the points to evaluate the regularization at.

  • +
  • groups (list[list[int]]) – List of indices of different groups.

  • +
+
+
Returns:
+

Computed group lasso norm of at the given points.

+
+
Return type:
+

Tensor

+
+
+
+
+
+class botorch.acquisition.penalized.L1PenaltyObjective(init_point)[source]
+

Bases: Module

+

L1 penalty objective class. An instance of this class can be added to any +arbitrary objective to construct a PenalizedMCObjective.

+

Initializing L1 penalty objective.

+
+
Parameters:
+

init_point (Tensor) – The “1 x dim” reference point against which +we want to regularize.

+
+
+
+
+forward(X)[source]
+
+
Parameters:
+

X (Tensor) – A “batch_shape x q x dim” representing the points to be evaluated.

+
+
Returns:
+

A “1 x batch_shape x q” tensor representing the penalty for each point. +The first dimension corresponds to the dimension of MC samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.penalized.PenalizedMCObjective(objective, penalty_objective, regularization_parameter, expand_dim=None)[source]
+

Bases: GenericMCObjective

+

Penalized MC objective.

+

Allows to construct a penalized MC-objective by adding a penalty term to +the original objective.

+
+

mc_acq(X) = objective(X) + penalty_objective(X)

+
+

Note: PenalizedMCObjective allows adding penalty at the MCObjective level, +different from the AcquisitionFunction level in PenalizedAcquisitionFunction.

+

Example

+
>>> regularization_parameter = 0.01
+>>> init_point = torch.zeros(3) # assume data dim is 3
+>>> objective = lambda Y, X: torch.sqrt(Y).sum(dim=-1)
+>>> l1_penalty_objective = L1PenaltyObjective(init_point=init_point)
+>>> l1_penalized_objective = PenalizedMCObjective(
+        objective, l1_penalty_objective, regularization_parameter
+    )
+>>> samples = sampler(posterior)
+        objective, l1_penalty_objective, regularization_parameter
+
+
+

Penalized MC objective.

+
+
Parameters:
+
    +
  • objective (Callable[[Tensor, Tensor | None], Tensor]) – A callable f(samples, X) mapping a +sample_shape x batch-shape x q x m-dim Tensor samples and +an optional batch-shape x q x d-dim Tensor X to a +sample_shape x batch-shape x q-dim Tensor of objective values.

  • +
  • penalty_objective (torch.nn.Module) – A torch.nn.Module f(X) that takes in a +batch-shape x q x d-dim Tensor X and outputs a +1 x batch-shape x q-dim Tensor of penalty objective values.

  • +
  • regularization_parameter (float) – weight of the penalty (regularization) term

  • +
  • expand_dim (int | None) – dim to expand penalty_objective to match with objective when +fully bayesian model is used. If None, no expansion is performed.

  • +
+
+
+
+
+forward(samples, X=None)[source]
+

Evaluate the penalized objective on the samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample_shape x batch_shape x q x m-dim Tensors of +samples from a model posterior.

  • +
  • X (Tensor | None) – A batch_shape x q x d-dim tensor of inputs. Relevant only if +the objective depends on the inputs explicitly.

  • +
+
+
Returns:
+

A sample_shape x batch_shape x q-dim Tensor of objective values +with penalty added for each point.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.acquisition.penalized.L0PenaltyApproxObjective(target_point, a=1.0, **tkwargs)[source]
+

Bases: L0Approximation

+

Differentiable relaxation of the L0 norm penalty objective class. +An instance of this class can be added to any arbitrary objective to +construct a PenalizedMCObjective.

+

Initializing L0 penalty with differentiable relaxation.

+
+
Parameters:
+
    +
  • target_point (Tensor) – A tensor corresponding to the target point.

  • +
  • a (float) – A hyperparameter that controls the differentiable relaxation.

  • +
  • tkwargs (Any)

  • +
+
+
+
+
+
+

Prior-Guided Acquisition Function Wrapper

+

Prior-Guided Acquisition Functions

+

References

+
+
+[Hvarfner2022] +(1,2) +

C. Hvarfner, D. Stoll, A. Souza, M. Lindauer, F. Hutter, L. Nardi. PiBO: +Augmenting Acquisition Functions with User Beliefs for Bayesian Optimization. +ICLR 2022.

+
+
+
+
+class botorch.acquisition.prior_guided.PriorGuidedAcquisitionFunction(acq_function, prior_module, log=False, prior_exponent=1.0, X_pending=None)[source]
+

Bases: AcquisitionFunction

+

Class for weighting acquisition functions by a prior distribution.

+

Supports MC and batch acquisition functions via +SampleReducingAcquisitionFunction.

+

See [Hvarfner2022] for details.

+

Initialize the prior-guided acquisition function.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – The base acquisition function.

  • +
  • prior_module (Module) – A Module that computes the probability +(or log probability) for the provided inputs. +prior_module.forward should take a batch_shape x q-dim +tensor of inputs and return a batch_shape x q-dim tensor +of probabilities.

  • +
  • log (bool) – A boolean that should be true if the acquisition function emits a +log-transformed value and the prior module emits a log probability.

  • +
  • prior_exponent (float) – The exponent applied to the prior. This can be used +for example to decay the effect the prior over time as in +[Hvarfner2022].

  • +
  • X_pending (Tensor | None) – n x d Tensor with n d-dim design points that have +been submitted for evaluation but have not yet been evaluated. +Note: X_pending should be provided as an argument to or set on +PriorGuidedAcquisitionFunction, but not set on the underlying +acquisition function.

  • +
+
+
+
+
+forward(X)[source]
+

Compute the acquisition function weighted by the prior.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Proximal Acquisition Function Wrapper

+

A wrapper around AcquisitionFunctions to add proximal weighting of the +acquisition function.

+
+
+class botorch.acquisition.proximal.ProximalAcquisitionFunction(acq_function, proximal_weights, transformed_weighting=True, beta=None)[source]
+

Bases: AcquisitionFunction

+

A wrapper around AcquisitionFunctions to add proximal weighting of the +acquisition function. The acquisition function is +weighted via a squared exponential centered at the last training point, +with varying lengthscales corresponding to proximal_weights. Can only be used +with acquisition functions based on single batch models. Acquisition functions +must be positive or beta must be specified to apply a SoftPlus transform before +proximal weighting.

+

Small values of proximal_weights corresponds to strong biasing towards recently +observed points, which smoothes optimization with a small potential decrese in +convergence rate.

+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> EI = ExpectedImprovement(model, best_f=0.0)
+>>> proximal_weights = torch.ones(d)
+>>> EI_proximal = ProximalAcquisitionFunction(EI, proximal_weights)
+>>> eip = EI_proximal(test_X)
+
+
+

Derived Acquisition Function weighted by proximity to recently +observed point.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – The base acquisition function, operating on input tensors +of feature dimension d.

  • +
  • proximal_weights (Tensor) – A d dim tensor used to bias locality +along each axis.

  • +
  • transformed_weighting (bool | None) – If True, the proximal weights are applied in +the transformed input space given by +acq_function.model.input_transform (if available), otherwise +proximal weights are applied in real input space.

  • +
  • beta (float | None) – If not None, apply a softplus transform to the base acquisition +function, allows negative base acquisition function values.

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate base acquisition function with proximal weighting.

+
+
Parameters:
+

X (Tensor) – Input tensor of feature dimension d .

+
+
Returns:
+

Base acquisition function evaluated on tensor X multiplied by proximal +weighting.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Factory Functions for Acquisition Functions

+

Utilities for acquisition functions.

+
+
+botorch.acquisition.factory.get_acquisition_function(acquisition_function_name, model, objective, X_observed, posterior_transform=None, X_pending=None, constraints=None, eta=0.001, mc_samples=512, seed=None, *, tau=0.001, prune_baseline=True, marginalize_dim=None, cache_root=True, beta=None, ref_point=None, Y=None, alpha=0.0)[source]
+

Convenience function for initializing botorch acquisition functions.

+
+
Parameters:
+
    +
  • acquisition_function_name (str) – Name of the acquisition function.

  • +
  • model (Model) – A fitted model.

  • +
  • objective (MCAcquisitionObjective) – A MCAcquisitionObjective.

  • +
  • X_observed (Tensor) – A m1 x d-dim Tensor of m1 design points that have +already been observed.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • X_pending (Tensor | None) – A m2 x d-dim Tensor of m2 design points whose evaluation +is pending.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility. Used for all acquisition functions except qSR and qUCB.

  • +
  • eta (Tensor | float | None) – The temperature parameter for the sigmoid function used for the +differentiable approximation of the constraints. In case of a float the +same eta is used for every constraint in constraints. In case of a +tensor the length of the tensor must match the number of provided +constraints. The i-th constraint is then estimated with the i-th +eta value. Used for all acquisition functions except qSR and qUCB.

  • +
  • mc_samples (int) – The number of samples to use for (q)MC evaluation of the +acquisition function.

  • +
  • seed (int | None) – If provided, perform deterministic optimization (i.e. the +function to optimize is fixed and not stochastic).

  • +
  • tau (float)

  • +
  • prune_baseline (bool)

  • +
  • marginalize_dim (int | None)

  • +
  • cache_root (bool)

  • +
  • beta (float | None)

  • +
  • ref_point (None | list[float] | Tensor)

  • +
  • Y (Tensor | None)

  • +
  • alpha (float)

  • +
+
+
Returns:
+

The requested acquisition function.

+
+
Return type:
+

MCAcquisitionFunction

+
+
+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> obj = LinearMCObjective(weights=torch.tensor([1.0, 2.0]))
+>>> acqf = get_acquisition_function("qEI", model, obj, train_X)
+
+
+
+
+
+

General Utilities for Acquisition Functions

+

Utilities for acquisition functions.

+
+
+botorch.acquisition.utils.get_acquisition_function(*args, **kwargs)[source]
+
+
Return type:
+

None

+
+
+
+
+
+botorch.acquisition.utils.repeat_to_match_aug_dim(target_tensor, reference_tensor)[source]
+

Repeat target_tensor until it has the same first dimension as reference_tensor +This works regardless of the batch shapes and q. +This is useful as we sometimes modify sample shapes such as in LearnedObjective.

+
+
Parameters:
+
    +
  • target_tensor (Tensor) – A sample_size x batch_shape x q x m-dim Tensor

  • +
  • reference_tensor (Tensor) – A (augmented_sample * sample_size) x batch_shape x q-dim +Tensor. augmented_sample could be 1.

  • +
+
+
Returns:
+

The content of target_tensor potentially repeated so that its first dimension +matches that of reference_tensor. +The shape will be (augmented_sample * sample_size) x batch_shape x q x m.

+
+
Return type:
+

Tensor

+
+
+

Examples

+
>>> import torch
+>>> target_tensor = torch.arange(3).repeat(2, 1).T
+>>> target_tensor
+tensor([[0, 0],
+        [1, 1],
+        [2, 2]])
+>>> repeat_to_match_aug_dim(target_tensor, torch.zeros(6))
+tensor([[0, 0],
+        [1, 1],
+        [2, 2],
+        [0, 0],
+        [1, 1],
+        [2, 2]])
+
+
+
+
+
+botorch.acquisition.utils.compute_best_feasible_objective(samples, obj, constraints, model=None, objective=None, posterior_transform=None, X_baseline=None, infeasible_obj=None)[source]
+

Computes the largest obj value that is feasible under the constraints. If +constraints is None, returns the best unconstrained objective value.

+

When no feasible observations exist and infeasible_obj is not None, returns +infeasible_obj (potentially reshaped). When no feasible observations exist and +infeasible_obj is None, uses model, objective, posterior_transform, and +X_baseline to infer and return an infeasible_obj M s.t. M < min_x f(x).

+
+
Parameters:
+
    +
  • samples (Tensor) – (sample_shape) x batch_shape x q x m-dim posterior samples.

  • +
  • obj (Tensor) – A (sample_shape) x batch_shape x q-dim Tensor of MC objective values.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map posterior samples to +a scalar. The associated constraint is considered satisfied if this +scalar is less than zero.

  • +
  • model (Model | None) – A Model, only required when there are no feasible observations.

  • +
  • objective (MCAcquisitionObjective | None) – An MCAcquisitionObjective, only optionally used when there are no +feasible observations.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform, only optionally used when there are +no feasible observations.

  • +
  • X_baseline (Tensor | None) – A batch_shape x d-dim Tensor of baseline points, only required +when there are no feasible observations.

  • +
  • infeasible_obj (Tensor | None) – A Tensor to be returned when no feasible points exist.

  • +
+
+
Returns:
+

A (sample_shape) x batch_shape-dim Tensor of best feasible objectives.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.utils.get_infeasible_cost(X, model, objective=None, posterior_transform=None)[source]
+

Get infeasible cost for a model and objective.

+

For each outcome, computes an infeasible cost M such that +-M < min_x f(x) almost always, so that feasible points are preferred.

+
+
Parameters:
+
    +
  • X (Tensor) – A n x d Tensor of n design points to use in evaluating the +minimum. These points should cover the design space well. The more +points the better the estimate, at the expense of added computation.

  • +
  • model (Model) – A fitted botorch model with m outcomes.

  • +
  • objective (Callable[[Tensor, Tensor | None], Tensor] | None) – The objective with which to evaluate the model output.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
+
+
Returns:
+

An m-dim tensor of infeasible cost values.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> model = SingleTaskGP(train_X, train_Y)
+>>> objective = lambda Y: Y[..., -1] ** 2
+>>> M = get_infeasible_cost(train_X, model, obj)
+
+
+
+
+
+botorch.acquisition.utils.prune_inferior_points(model, X, objective=None, posterior_transform=None, constraints=None, num_samples=2048, max_frac=1.0, sampler=None, marginalize_dim=None)[source]
+

Prune points from an input tensor that are unlikely to be the best point.

+

Given a model, an objective, and an input tensor X, this function returns +the subset of points in X that have some probability of being the best +point under the objective. This function uses sampling to estimate the +probabilities, the higher the number of points n in X the higher the +number of samples num_samples should be to obtain accurate estimates.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model. Batched models are currently not supported.

  • +
  • X (Tensor) – An input tensor of shape n x d. Batched inputs are currently not +supported.

  • +
  • objective (MCAcquisitionObjective | None) – The objective under which to evaluate the posterior.

  • +
  • posterior_transform (PosteriorTransform | None) – A PosteriorTransform (optional).

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of constraint callables which map a Tensor of posterior +samples of dimension sample_shape x batch-shape x q x m-dim to a +sample_shape x batch-shape x q-dim Tensor. The associated constraints +are satisfied if constraint(samples) < 0.

  • +
  • num_samples (int) – The number of samples used to compute empirical +probabilities of being the best point.

  • +
  • max_frac (float) – The maximum fraction of points to retain. Must satisfy +0 < max_frac <= 1. Ensures that the number of elements in the +returned tensor does not exceed ceil(max_frac * n).

  • +
  • sampler (MCSampler | None) – If provided, will use this customized sampler instead of +automatically constructing one with num_samples.

  • +
  • marginalize_dim (int | None) – A batch dimension that should be marginalized. +For example, this is useful when using a batched fully Bayesian +model.

  • +
+
+
Returns:
+

A n’ x d with subset of points in X, where

+
+

n’ = min(N_nz, ceil(max_frac * n))

+
+

with N_nz the number of points in X that have non-zero (empirical, +under num_samples samples) probability of being the best point.

+

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.utils.project_to_target_fidelity(X, target_fidelities=None, d=None)[source]
+

Project X onto the target set of fidelities.

+

This function assumes that the set of feasible fidelities is a box, so +projecting here just means setting each fidelity parameter to its target +value. If X does not contain the fidelity dimensions, this will insert +them and set them to their target values.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x (d or d-d_f)-dim Tensor of with q d or +d-d_f-dim design points for each t-batch, where d_f is the +number of fidelity dimensions. If the argument d is not provided, +X must include the fidelity dimensions and have a trailing`X` must +include the fidelity dimensions and have a trailing

  • +
  • target_fidelities (dict[int, float] | None) – A dictionary mapping a subset of columns of X (the +fidelity parameters) to their respective target fidelity value. If +omitted, assumes that the last column of X is the fidelity parameter +with a target value of 1.0.

  • +
  • d (int | None) – The total dimension d.

  • +
+
+
Returns:
+

+
A batch_shape x q x d-dim Tensor X_proj with fidelity parameters

projected to the provided fidelity values.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.utils.expand_trace_observations(X, fidelity_dims=None, num_trace_obs=0)[source]
+

Expand X with trace observations.

+

Expand a tensor of inputs with “trace observations” that are obtained during +the evaluation of the candidate set. This is used in multi-fidelity +optimization. It can be though of as augmenting the q-batch with additional +points that are the expected trace observations.

+

Let f_i be the i-th fidelity parameter. Then this functions assumes that +for each element of the q-batch, besides the fidelity f_i, we will observe +additonal fidelities f_i1, …, f_iK, where K = num_trace_obs, during +evaluation of the candidate set X. Specifically, this function assumes +that f_ij = (K-j) / (num_trace_obs + 1) * f_i for all i. That is, the +expansion is performed in parallel for all fidelities (it does not expand +out all possible combinations).

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x d-dim Tensor of with q d-dim design points +(incl. the fidelity parameters) for each t-batch.

  • +
  • fidelity_dims (list[int] | None) – The indices of the fidelity parameters. If omitted, +assumes that the last column of X contains the fidelity parameters.

  • +
  • num_trace_obs (int) – The number of trace observations to use.

  • +
+
+
Returns:
+

+
A batch_shape x (q + num_trace_obs x q) x d Tensor X_expanded that

expands X with trace observations.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.utils.project_to_sample_points(X, sample_points)[source]
+

Augment X with sample points at which to take weighted average.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x 1 x d-dim Tensor of with one d`-dim design points +for each t-batch.

  • +
  • sample_points (Tensor) – p x d’-dim Tensor (d’ < d) of d’-dim sample points at +which to compute the expectation. The d’-dims refer to the trailing +columns of X.

  • +
+
+
Returns:
+

A batch_shape x p x d Tensor where the q-batch includes the p sample points.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.utils.get_optimal_samples(model, bounds, num_optima, raw_samples=1024, num_restarts=20, posterior_transform=None, objective=None, return_transformed=False)[source]
+

Draws sample paths from the posterior and maximizes the samples using GD.

+
+
Parameters:
+
    +
  • model (GP) – The model from which samples are drawn.

  • +
  • bounds (Tensor) – Bounds of the search space. If the model inputs are +normalized, the bounds should be normalized as well.

  • +
  • num_optima (int) – The number of paths to be drawn and optimized.

  • +
  • raw_samples (int) – The number of candidates randomly sample. +Defaults to 1024.

  • +
  • num_restarts (int) – The number of candidates to do gradient-based +optimization on. Defaults to 20.

  • +
  • posterior_transform (ScalarizedPosteriorTransform | None) – A ScalarizedPosteriorTransform (may e.g. be used to +scalarize multi-output models or negate the objective).

  • +
  • objective (MCAcquisitionObjective | None) – An MCAcquisitionObjective, used to negate the objective or otherwise +transform sample outputs. Cannot be combined with posterior_transform.

  • +
  • return_transformed (bool) – If True, return the transformed samples.

  • +
+
+
Returns:
+

The optimal input locations and corresponding outputs, x* and f*.

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+

Multi-Objective Utilities for Acquisition Functions

+

Utilities for multi-objective acquisition functions.

+
+
+botorch.acquisition.multi_objective.utils.get_default_partitioning_alpha(num_objectives)[source]
+

Determines an approximation level based on the number of objectives.

+

If alpha is 0, FastNondominatedPartitioning should be used. Otherwise, +an approximate NondominatedPartitioning should be used with approximation +level alpha.

+
+
Parameters:
+

num_objectives (int) – the number of objectives.

+
+
Returns:
+

The approximation level alpha.

+
+
Return type:
+

float

+
+
+
+
+
+botorch.acquisition.multi_objective.utils.prune_inferior_points_multi_objective(model, X, ref_point, objective=None, constraints=None, num_samples=2048, max_frac=1.0, marginalize_dim=None)[source]
+

Prune points from an input tensor that are unlikely to be pareto optimal.

+

Given a model, an objective, and an input tensor X, this function returns +the subset of points in X that have some probability of being pareto +optimal, better than the reference point, and feasible. This function uses +sampling to estimate the probabilities, the higher the number of points n +in X the higher the number of samples num_samples should be to obtain +accurate estimates.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model. Batched models are currently not supported.

  • +
  • X (Tensor) – An input tensor of shape n x d. Batched inputs are currently not +supported.

  • +
  • ref_point (Tensor) – The reference point.

  • +
  • objective (MCMultiOutputObjective | None) – The objective under which to evaluate the posterior.

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility.

  • +
  • num_samples (int) – The number of samples used to compute empirical +probabilities of being the best point.

  • +
  • max_frac (float) – The maximum fraction of points to retain. Must satisfy +0 < max_frac <= 1. Ensures that the number of elements in the +returned tensor does not exceed ceil(max_frac * n).

  • +
  • marginalize_dim (int | None) – A batch dimension that should be marginalized. +For example, this is useful when using a batched fully Bayesian +model.

  • +
+
+
Returns:
+

A n’ x d with subset of points in X, where

+
+

n’ = min(N_nz, ceil(max_frac * n))

+
+

with N_nz the number of points in X that have non-zero (empirical, +under num_samples samples) probability of being pareto optimal.

+

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.multi_objective.utils.compute_sample_box_decomposition(pareto_fronts, partitioning=<class 'botorch.utils.multi_objective.box_decompositions.dominated.DominatedPartitioning'>, maximize=True, num_constraints=0)[source]
+

Computes the box decomposition associated with some sampled optimal +objectives. This also supports the single-objective and constrained optimization +setting. An objective y is feasible if y <= 0.

+

To take advantage of batch computations, we pad the hypercell bounds with a +2 x (M + K)-dim Tensor of zeros [0, 0].

+
+
Parameters:
+
    +
  • pareto_fronts (Tensor) – A num_pareto_samples x num_pareto_points x M dim Tensor +containing the sampled optimal set of objectives.

  • +
  • partitioning (type[BoxDecomposition]) – A BoxDecomposition module that is used to obtain the +hyper-rectangle bounds for integration. In the unconstrained case, this +gives the partition of the dominated space. In the constrained case, this +gives the partition of the feasible dominated space union the infeasible +space.

  • +
  • maximize (bool) – If true, the box-decomposition is computed assuming maximization.

  • +
  • num_constraints (int) – The number of constraints K.

  • +
+
+
Returns:
+

A num_pareto_samples x 2 x J x (M + K)-dim Tensor containing the bounds for +the hyper-rectangles. The number J is the smallest number of boxes needed +to partition all the Pareto samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.acquisition.multi_objective.utils.random_search_optimizer(model, bounds, num_points, maximize, pop_size=1024, max_tries=10)[source]
+

Optimize a function via random search.

+
+
Parameters:
+
    +
  • model (GenericDeterministicModel) – The model.

  • +
  • bounds (Tensor) – A 2 x d-dim Tensor containing the input bounds.

  • +
  • num_points (int) – The number of optimal points to be outputted.

  • +
  • maximize (bool) – If true, we consider a maximization problem.

  • +
  • pop_size (int) – The number of function evaluations per try.

  • +
  • max_tries (int) – The maximum number of tries.

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • A num_points x d-dim Tensor containing the collection of optimal inputs.

  • +
  • +
    A num_points x M-dim Tensor containing the collection of optimal

    objectives.

    +
    +
    +
  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.acquisition.multi_objective.utils.sample_optimal_points(model, bounds, num_samples, num_points, optimizer=<function random_search_optimizer>, maximize=True, optimizer_kwargs=None)[source]
+

Compute a collection of optimal inputs and outputs from samples of a Gaussian +Process (GP).

+

Steps: +(1) The samples are generated using random Fourier features (RFFs). +(2) The samples are optimized sequentially using an optimizer.

+
+
TODO: We can generalize the GP sampling step to accommodate for other sampling

strategies rather than restricting to RFFs e.g. decoupled sampling.

+
+
TODO: Currently this defaults to random search optimization, might want to

explore some other alternatives.

+
+
+
+
Parameters:
+
    +
  • model (Model) – The model. This does not support models which include fantasy +observations.

  • +
  • bounds (Tensor) – A 2 x d-dim Tensor containing the input bounds.

  • +
  • num_samples (int) – The number of GP samples.

  • +
  • num_points (int) – The number of optimal points to be outputted.

  • +
  • optimizer (Callable[[GenericDeterministicModel, Tensor, int, bool, Any], tuple[Tensor, Tensor]]) – A callable that solves the deterministic optimization problem.

  • +
  • maximize (bool) – If true, we consider a maximization problem.

  • +
  • optimizer_kwargs (dict[str, Any] | None) – The additional arguments for the optimizer.

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • +
    A num_samples x num_points x d-dim Tensor containing the collection of

    optimal inputs.

    +
    +
    +
  • +
  • +
    A num_samples x num_points x M-dim Tensor containing the collection of

    optimal objectives.

    +
    +
    +
  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/cross_validation.html b/website-old/pages/api/cross_validation.html new file mode 100644 index 0000000000..1d716bfaa5 --- /dev/null +++ b/website-old/pages/api/cross_validation.html @@ -0,0 +1,256 @@ + + + + + + + +
+
+
+
+
+

botorch.cross_validation

+

Cross-validation utilities using batch evaluation mode.

+
+
+class botorch.cross_validation.CVFolds(train_X, test_X, train_Y, test_Y, train_Yvar, test_Yvar)[source]
+

Bases: NamedTuple

+

Create new instance of CVFolds(train_X, test_X, train_Y, test_Y, train_Yvar, test_Yvar)

+
+
Parameters:
+
    +
  • train_X (Tensor)

  • +
  • test_X (Tensor)

  • +
  • train_Y (Tensor)

  • +
  • test_Y (Tensor)

  • +
  • train_Yvar (Tensor | None)

  • +
  • test_Yvar (Tensor | None)

  • +
+
+
+
+
+train_X: Tensor
+

Alias for field number 0

+
+
+
+test_X: Tensor
+

Alias for field number 1

+
+
+
+train_Y: Tensor
+

Alias for field number 2

+
+
+
+test_Y: Tensor
+

Alias for field number 3

+
+
+
+train_Yvar: Tensor | None
+

Alias for field number 4

+
+
+
+test_Yvar: Tensor | None
+

Alias for field number 5

+
+
+
+
+class botorch.cross_validation.CVResults(model, posterior, observed_Y, observed_Yvar)[source]
+

Bases: NamedTuple

+

Create new instance of CVResults(model, posterior, observed_Y, observed_Yvar)

+
+
Parameters:
+
+
+
+
+
+model: GPyTorchModel
+

Alias for field number 0

+
+
+
+posterior: GPyTorchPosterior
+

Alias for field number 1

+
+
+
+observed_Y: Tensor
+

Alias for field number 2

+
+
+
+observed_Yvar: Tensor | None
+

Alias for field number 3

+
+
+
+
+botorch.cross_validation.gen_loo_cv_folds(train_X, train_Y, train_Yvar=None)[source]
+

Generate LOO CV folds w.r.t. to n.

+
+
Parameters:
+
    +
  • train_X (Tensor) – A n x d or batch_shape x n x d (batch mode) tensor of training +features.

  • +
  • train_Y (Tensor) – A n x (m) or batch_shape x n x (m) (batch mode) tensor of +training observations.

  • +
  • train_Yvar (Tensor | None) – An n x (m) or batch_shape x n x (m) (batch mode) tensor +of observed measurement noise.

  • +
+
+
Returns:
+

    +
  • train_X: A n x (n-1) x d or batch_shape x n x (n-1) x d tensor of +training features.

  • +
  • test_X: A n x 1 x d or batch_shape x n x 1 x d tensor of test features.

  • +
  • train_Y: A n x (n-1) x m or batch_shape x n x (n-1) x m tensor of +training observations.

  • +
  • test_Y: A n x 1 x m or batch_shape x n x 1 x m tensor of test +observations.

  • +
  • train_Yvar: A n x (n-1) x m or batch_shape x n x (n-1) x m tensor +of observed measurement noise.

  • +
  • test_Yvar: A n x 1 x m or batch_shape x n x 1 x m tensor of observed +measurement noise.

  • +
+

+
+
Return type:
+

CVFolds NamedTuple with the following fields

+
+
+

Example

+
>>> train_X = torch.rand(10, 1)
+>>> train_Y = torch.rand_like(train_X)
+>>> cv_folds = gen_loo_cv_folds(train_X, train_Y)
+>>> cv_folds.train_X.shape
+torch.Size([10, 9, 1])
+
+
+
+
+
+botorch.cross_validation.batch_cross_validation(model_cls, mll_cls, cv_folds, fit_args=None, observation_noise=False, model_init_kwargs=None)[source]
+

Perform cross validation by using GPyTorch batch mode.

+
+
WARNING: This function is currently very memory inefficient; use it only

for problems of small size.

+
+
+
+
Parameters:
+
    +
  • model_cls (type[GPyTorchModel]) – A GPyTorchModel class. This class must initialize the likelihood +internally. Note: Multi-task GPs are not currently supported.

  • +
  • mll_cls (type[MarginalLogLikelihood]) – A MarginalLogLikelihood class.

  • +
  • cv_folds (CVFolds) – A CVFolds tuple.

  • +
  • fit_args (dict[str, Any] | None) – Arguments passed along to fit_gpytorch_mll.

  • +
  • model_init_kwargs (dict[str, Any] | None) – Keyword arguments passed to the model constructor.

  • +
  • observation_noise (bool)

  • +
+
+
Returns:
+

A CVResults tuple with the following fields

+
    +
  • model: GPyTorchModel for batched cross validation

  • +
  • posterior: GPyTorchPosterior where the mean has shape n x 1 x m or +batch_shape x n x 1 x m

  • +
  • observed_Y: A n x 1 x m or batch_shape x n x 1 x m tensor of observations.

  • +
  • observed_Yvar: A n x 1 x m or batch_shape x n x 1 x m tensor of observed +measurement noise.

  • +
+

+
+
Return type:
+

CVResults

+
+
+

Example

+
>>> import torch
+>>> from botorch.cross_validation import (
+...     batch_cross_validation, gen_loo_cv_folds
+... )
+>>>
+>>> from botorch.models import SingleTaskGP
+>>> from botorch.models.transforms.input import Normalize
+>>> from botorch.models.transforms.outcome import Standardize
+>>> from gpytorch.mlls import ExactMarginalLogLikelihood
+
+
+
>>> train_X = torch.rand(10, 1)
+>>> train_Y = torch.rand_like(train_X)
+>>> cv_folds = gen_loo_cv_folds(train_X, train_Y)
+>>> input_transform = Normalize(d=train_X.shape[-1])
+>>> outcome_transform = Standardize(
+...     m=train_Y.shape[-1], batch_shape=cv_folds.train_Y.shape[:-2]
+... )
+>>>
+>>> cv_results = batch_cross_validation(
+...    model_cls=SingleTaskGP,
+...    mll_cls=ExactMarginalLogLikelihood,
+...    cv_folds=cv_folds,
+...    model_init_kwargs={
+...        "input_transform": input_transform,
+...        "outcome_transform": outcome_transform,
+...    },
+... )
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/exceptions.html b/website-old/pages/api/exceptions.html new file mode 100644 index 0000000000..bb48640510 --- /dev/null +++ b/website-old/pages/api/exceptions.html @@ -0,0 +1,219 @@ + + + + + + + +
+
+
+
+
+

botorch.exceptions

+
+

Errors

+

Botorch Errors.

+
+
+exception botorch.exceptions.errors.BotorchError[source]
+

Bases: Exception

+

Base botorch exception.

+
+
+
+exception botorch.exceptions.errors.CandidateGenerationError[source]
+

Bases: BotorchError

+

Exception raised during generating candidates.

+
+
+
+exception botorch.exceptions.errors.DeprecationError[source]
+

Bases: BotorchError

+

Exception raised due to deprecations

+
+
+
+exception botorch.exceptions.errors.InputDataError[source]
+

Bases: BotorchError

+

Exception raised when input data does not comply with conventions.

+
+
+
+exception botorch.exceptions.errors.UnsupportedError[source]
+

Bases: BotorchError

+

Currently unsupported feature.

+
+
+
+exception botorch.exceptions.errors.BotorchTensorDimensionError[source]
+

Bases: BotorchError

+

Exception raised when a tensor violates a botorch convention.

+
+
+
+exception botorch.exceptions.errors.ModelFittingError[source]
+

Bases: Exception

+

Exception raised when attempts to fit a model terminate unsuccessfully.

+
+
+
+exception botorch.exceptions.errors.OptimizationTimeoutError(*args, current_x, runtime, **kwargs)[source]
+

Bases: BotorchError

+

Exception raised when optimization times out.

+
+
Parameters:
+
    +
  • *args (Any) – Standard args to BoTorchError.

  • +
  • current_x (ndarray[Any, dtype[_ScalarType_co]]) – A numpy array representing the current iterate.

  • +
  • runtime (float) – The total runtime in seconds after which the optimization +timed out.

  • +
  • **kwargs (Any) – Standard kwargs to BoTorchError.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+exception botorch.exceptions.errors.OptimizationGradientError(*args, current_x, **kwargs)[source]
+

Bases: BotorchError, RuntimeError

+

Exception raised when gradient array gradf containts NaNs.

+
+
Parameters:
+
    +
  • *args (Any) – Standard args to BoTorchError.

  • +
  • current_x (ndarray[Any, dtype[_ScalarType_co]]) – A numpy array representing the current iterate.

  • +
  • **kwargs (Any) – Standard kwargs to BoTorchError.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+

Warnings

+

Botorch Warnings.

+
+
+exception botorch.exceptions.warnings.BotorchWarning[source]
+

Bases: Warning

+

Base botorch warning.

+
+
+
+exception botorch.exceptions.warnings.BadInitialCandidatesWarning[source]
+

Bases: BotorchWarning

+

Warning issued if set of initial candidates for optimziation is bad.

+
+
+
+exception botorch.exceptions.warnings.InputDataWarning[source]
+

Bases: BotorchWarning

+

Warning raised when input data does not comply with conventions.

+
+
+
+exception botorch.exceptions.warnings.CostAwareWarning[source]
+

Bases: BotorchWarning

+

Warning raised in the context of cost-aware acquisition strategies.

+
+
+
+exception botorch.exceptions.warnings.OptimizationWarning[source]
+

Bases: BotorchWarning

+

Optimization-related warnings.

+
+
+
+exception botorch.exceptions.warnings.SamplingWarning[source]
+

Bases: BotorchWarning

+

Sampling related warnings.

+
+
+
+exception botorch.exceptions.warnings.BotorchTensorDimensionWarning[source]
+

Bases: BotorchWarning

+

Warning raised when a tensor possibly violates a botorch convention.

+
+
+
+exception botorch.exceptions.warnings.UserInputWarning[source]
+

Bases: BotorchWarning

+

Warning raised when a potential issue is detected with user provided inputs.

+
+
+
+exception botorch.exceptions.warnings.NumericsWarning[source]
+

Bases: BotorchWarning

+

Warning raised when numerical issues are detected.

+
+
+
+botorch.exceptions.warnings.legacy_ei_numerics_warning(legacy_name)[source]
+

Raises a warning for legacy EI acquisition functions that are known to have +numerical issues and should be replaced with the LogEI version for virtually all +use-cases except for explicit benchmarking of the numerical issues of legacy EI.

+
+
Parameters:
+
    +
  • legacy_name (str) – The name of the legacy EI acquisition function.

  • +
  • logei_name – The name of the associated LogEI acquisition function.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/fit.html b/website-old/pages/api/fit.html new file mode 100644 index 0000000000..f0a6cb1505 --- /dev/null +++ b/website-old/pages/api/fit.html @@ -0,0 +1,119 @@ + + + + + + + +
+
+
+
+
+

botorch.fit

+

Model fitting routines.

+
+
+botorch.fit.fit_gpytorch_mll(mll, closure=None, optimizer=None, closure_kwargs=None, optimizer_kwargs=None, **kwargs)[source]
+

Clearing house for fitting models passed as GPyTorch MarginalLogLikelihoods.

+
+
Parameters:
+
    +
  • mll (MarginalLogLikelihood) – A GPyTorch MarginalLogLikelihood instance.

  • +
  • closure (Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None) – Forward-backward closure for obtaining objective values and gradients. +Responsible for setting parameters’ grad attributes. If no closure is +provided, one will be obtained by calling get_loss_closure_with_grads.

  • +
  • optimizer (Callable | None) – User specified optimization algorithm. When optimizer is None, +this keyword argument is omitted when calling the dispatcher.

  • +
  • closure_kwargs (dict[str, Any] | None) – Keyword arguments passed when calling closure.

  • +
  • optimizer_kwargs (dict[str, Any] | None) – A dictionary of keyword arguments passed when +calling optimizer.

  • +
  • **kwargs (Any) – Keyword arguments passed down through the dispatcher to +fit subroutines. Unexpected keywords are ignored.

  • +
+
+
Returns:
+

The mll instance. If fitting succeeded, then mll will be in evaluation mode, +i.e. mll.training == False. Otherwise, mll will be in training mode.

+
+
Return type:
+

MarginalLogLikelihood

+
+
+
+
+
+botorch.fit.fit_fully_bayesian_model_nuts(model, max_tree_depth=6, warmup_steps=512, num_samples=256, thinning=16, disable_progbar=False, jit_compile=False)[source]
+

Fit a fully Bayesian model using the No-U-Turn-Sampler (NUTS)

+
+
Parameters:
+
    +
  • model (SaasFullyBayesianSingleTaskGP | SaasFullyBayesianMultiTaskGP) – SaasFullyBayesianSingleTaskGP to be fitted.

  • +
  • max_tree_depth (int) – Maximum tree depth for NUTS

  • +
  • warmup_steps (int) – The number of burn-in steps for NUTS.

  • +
  • num_samples (int) – The number of MCMC samples. Note that with thinning, +num_samples / thinning samples are retained.

  • +
  • thinning (int) – The amount of thinning. Every nth sample is retained.

  • +
  • disable_progbar (bool) – A boolean indicating whether to print the progress +bar and diagnostics during MCMC.

  • +
  • jit_compile (bool) – Whether to use jit. Using jit may be ~2X faster (rough estimate), +but it will also increase the memory usage and sometimes result in runtime +errors, e.g., https://github.com/pyro-ppl/pyro/issues/3136.

  • +
+
+
Return type:
+

None

+
+
+

Example

+
>>> gp = SaasFullyBayesianSingleTaskGP(train_X, train_Y)
+>>> fit_fully_bayesian_model_nuts(gp)
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/generation.html b/website-old/pages/api/generation.html new file mode 100644 index 0000000000..e2342e4b0d --- /dev/null +++ b/website-old/pages/api/generation.html @@ -0,0 +1,456 @@ + + + + + + + +
+
+
+
+
+

botorch.generation

+
+

Candidate Generation Utilities for Acquisition Functions

+

Candidate generation utilities.

+
+
+botorch.generation.gen.gen_candidates_scipy(initial_conditions, acquisition_function, lower_bounds=None, upper_bounds=None, inequality_constraints=None, equality_constraints=None, nonlinear_inequality_constraints=None, options=None, fixed_features=None, timeout_sec=None)[source]
+

Generate a set of candidates using scipy.optimize.minimize.

+

Optimizes an acquisition function starting from a set of initial candidates +using scipy.optimize.minimize via a numpy converter.

+
+
Parameters:
+
    +
  • initial_conditions (Tensor) – Starting points for optimization, with shape +(b) x q x d.

  • +
  • acquisition_function (AcquisitionFunction) – Acquisition function to be used.

  • +
  • lower_bounds (float | Tensor | None) – Minimum values for each column of initial_conditions.

  • +
  • upper_bounds (float | Tensor | None) – Maximum values for each column of initial_conditions.

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
  • nonlinear_inequality_constraints (list[tuple[Callable, bool]] | None) – A list of tuples representing the nonlinear +inequality constraints. The first element in the tuple is a callable +representing a constraint of the form callable(x) >= 0. In case of an +intra-point constraint, callable()`takes in an one-dimensional tensor of +shape `d and returns a scalar. In case of an inter-point constraint, +callable() takes a two dimensional tensor of shape q x d and again +returns a scalar. The second element is a boolean, indicating if it is an +intra-point or inter-point constraint (True for intra-point. False for +inter-point). For more information on intra-point vs inter-point +constraints, see the docstring of the inequality_constraints argument to +optimize_acqf(). The constraints will later be passed to the scipy +solver.

  • +
  • options (dict[str, Any] | None) – Options used to control the optimization including “method” +and “maxiter”. Select method for scipy.minimize using the +“method” key. By default uses L-BFGS-B for box-constrained problems +and SLSQP if inequality or equality constraints are present. If +with_grad=False, then we use a two-point finite difference estimate +of the gradient.

  • +
  • fixed_features (dict[int, float | None] | None) – This is a dictionary of feature indices to values, where +all generated candidates will have features fixed to these values. +If the dictionary value is None, then that feature will just be +fixed to the clamped value and not optimized. Assumes values to be +compatible with lower_bounds and upper_bounds!

  • +
  • timeout_sec (float | None) – Timeout (in seconds) for scipy.optimize.minimize routine - +if provided, optimization will stop after this many seconds and return +the best solution found so far.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

2-element tuple containing

+
    +
  • The set of generated candidates.

  • +
  • The acquisition value for each t-batch.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+

Example

+
>>> qEI = qExpectedImprovement(model, best_f=0.2)
+>>> bounds = torch.tensor([[0., 0.], [1., 2.]])
+>>> Xinit = gen_batch_initial_conditions(
+>>>     qEI, bounds, q=3, num_restarts=25, raw_samples=500
+>>> )
+>>> batch_candidates, batch_acq_values = gen_candidates_scipy(
+        initial_conditions=Xinit,
+        acquisition_function=qEI,
+        lower_bounds=bounds[0],
+        upper_bounds=bounds[1],
+    )
+
+
+
+
+
+botorch.generation.gen.gen_candidates_torch(initial_conditions, acquisition_function, lower_bounds=None, upper_bounds=None, optimizer=<class 'torch.optim.adam.Adam'>, options=None, callback=None, fixed_features=None, timeout_sec=None)[source]
+

Generate a set of candidates using a torch.optim optimizer.

+

Optimizes an acquisition function starting from a set of initial candidates +using an optimizer from torch.optim.

+
+
Parameters:
+
    +
  • initial_conditions (Tensor) – Starting points for optimization.

  • +
  • acquisition_function (AcquisitionFunction) – Acquisition function to be used.

  • +
  • lower_bounds (float | Tensor | None) – Minimum values for each column of initial_conditions.

  • +
  • upper_bounds (float | Tensor | None) – Maximum values for each column of initial_conditions.

  • +
  • optimizer (Optimizer) – The pytorch optimizer to use to perform +candidate search.

  • +
  • options (dict[str, float | str] | None) – Options used to control the optimization. Includes +maxiter: Maximum number of iterations

  • +
  • callback (Callable[[int, Tensor, Tensor], NoReturn] | None) – A callback function accepting the current iteration, loss, +and gradients as arguments. This function is executed after computing +the loss and gradients, but before calling the optimizer.

  • +
  • fixed_features (dict[int, float | None] | None) – This is a dictionary of feature indices to values, where +all generated candidates will have features fixed to these values. +If the dictionary value is None, then that feature will just be +fixed to the clamped value and not optimized. Assumes values to be +compatible with lower_bounds and upper_bounds!

  • +
  • timeout_sec (float | None) – Timeout (in seconds) for optimization. If provided, +gen_candidates_torch will stop after this many seconds and return +the best solution found so far.

  • +
+
+
Returns:
+

2-element tuple containing

+
    +
  • The set of generated candidates.

  • +
  • The acquisition value for each t-batch.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+

Example

+
>>> qEI = qExpectedImprovement(model, best_f=0.2)
+>>> bounds = torch.tensor([[0., 0.], [1., 2.]])
+>>> Xinit = gen_batch_initial_conditions(
+>>>     qEI, bounds, q=3, num_restarts=25, raw_samples=500
+>>> )
+>>> batch_candidates, batch_acq_values = gen_candidates_torch(
+        initial_conditions=Xinit,
+        acquisition_function=qEI,
+        lower_bounds=bounds[0],
+        upper_bounds=bounds[1],
+    )
+
+
+
+
+
+botorch.generation.gen.get_best_candidates(batch_candidates, batch_values)[source]
+

Extract best (q-batch) candidate from batch of candidates

+
+
Parameters:
+
    +
  • batch_candidates (Tensor) – A b x q x d tensor of b q-batch candidates, or a +b x d tensor of b single-point candidates.

  • +
  • batch_values (Tensor) – A tensor with b elements containing the value of the +respective candidate (higher is better).

  • +
+
+
Returns:
+

A tensor of size q x d (if q-batch mode) or d from batch_candidates +with the highest associated value.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> qEI = qExpectedImprovement(model, best_f=0.2)
+>>> bounds = torch.tensor([[0., 0.], [1., 2.]])
+>>> Xinit = gen_batch_initial_conditions(
+>>>     qEI, bounds, q=3, num_restarts=25, raw_samples=500
+>>> )
+>>> batch_candidates, batch_acq_values = gen_candidates_scipy(
+        initial_conditions=Xinit,
+        acquisition_function=qEI,
+        lower_bounds=bounds[0],
+        upper_bounds=bounds[1],
+    )
+>>> best_candidates = get_best_candidates(batch_candidates, batch_acq_values)
+
+
+
+
+
+

Sampling Strategies

+

Sampling-based generation strategies.

+

A SamplingStrategy returns samples from the input points (i.e. Tensors in feature +space), rather than the value for a set of tensors, as acquisition functions do. +The q-batch dimension has similar semantics as for acquisition functions in that the +points across the q-batch are considered jointly for sampling (where as for +q-acquisition functions we evaluate the joint value of the q-batch).

+
+
+class botorch.generation.sampling.SamplingStrategy(*args, **kwargs)[source]
+

Bases: Module, ABC

+

Abstract base class for sampling-based generation strategies.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+abstract forward(X, num_samples=1)[source]
+

Sample according to the SamplingStrategy.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x N x d-dim Tensor from which to sample (in the N +dimension).

  • +
  • num_samples (int) – The number of samples to draw.

  • +
+
+
Returns:
+

A batch_shape x num_samples x d-dim Tensor of samples from X, where +X[…, i, :] is the i-th sample.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.generation.sampling.MaxPosteriorSampling(model, objective=None, posterior_transform=None, replacement=True)[source]
+

Bases: SamplingStrategy

+

Sample from a set of points according to their max posterior value.

+

Example

+
>>> MPS = MaxPosteriorSampling(model)  # model w/ feature dim d=3
+>>> X = torch.rand(2, 100, 3)
+>>> sampled_X = MPS(X, num_samples=5)
+
+
+

Constructor for the SamplingStrategy base class.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are +evaluated. Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
  • replacement (bool) – If True, sample with replacement.

  • +
+
+
+
+
+forward(X, num_samples=1, observation_noise=False)[source]
+

Sample from the model posterior.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x N x d-dim Tensor from which to sample (in the N +dimension) according to the maximum posterior value under the objective.

  • +
  • num_samples (int) – The number of samples to draw.

  • +
  • observation_noise (bool) – If True, sample with observation noise.

  • +
+
+
Returns:
+

A batch_shape x num_samples x d-dim Tensor of samples from X, where +X[…, i, :] is the i-th sample.

+
+
Return type:
+

Tensor

+
+
+
+
+
+maximize_samples(X, samples, num_samples=1)[source]
+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • samples (Tensor)

  • +
  • num_samples (int)

  • +
+
+
+
+
+
+
+class botorch.generation.sampling.BoltzmannSampling(acq_func, eta=1.0, replacement=True)[source]
+

Bases: SamplingStrategy

+

Sample from a set of points according to a tempered acquisition value.

+

Given an acquisition function acq_func, this sampling strategies draws +samples from a batch_shape x N x d-dim tensor X according to a multinomial +distribution over its indices given by

+
+

weight(X[…, i, :]) ~ exp(eta * standardize(acq_func(X[…, i, :])))

+
+

where standardize(Y) standardizes Y to zero mean and unit variance. As the +temperature parameter eta -> 0, this approaches uniform sampling, while as +eta -> infty, this approaches selecting the maximizer(s) of the acquisition +function acq_func.

+

Example

+
>>> UCB = UpperConfidenceBound(model, beta=0.1)
+>>> BMUCB = BoltzmannSampling(UCB, eta=0.5)
+>>> X = torch.rand(2, 100, 3)
+>>> sampled_X = BMUCB(X, num_samples=5)
+
+
+

Boltzmann Acquisition Value Sampling.

+
+
Parameters:
+
    +
  • acq_func (AcquisitionFunction) – The acquisition function; to be evaluated in batch at the +individual points of a q-batch (not jointly, as is the case for +acquisition functions). Can be analytic or Monte-Carlo.

  • +
  • eta (float) – The temperature parameter in the softmax.

  • +
  • replacement (bool) – If True, sample with replacement.

  • +
+
+
+
+
+forward(X, num_samples=1)[source]
+

Sample from a tempered value of the acquisition function value.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x N x d-dim Tensor from which to sample (in the N +dimension) according to the maximum posterior value under the objective. +Note that if a batched model is used in the underlying acquisition +function, then its batch shape must be broadcastable to batch_shape.

  • +
  • num_samples (int) – The number of samples to draw.

  • +
+
+
Returns:
+

A batch_shape x num_samples x d-dim Tensor of samples from X, where +X[…, i, :] is the i-th sample.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.generation.sampling.ConstrainedMaxPosteriorSampling(model, constraint_model, objective=None, posterior_transform=None, replacement=True)[source]
+

Bases: MaxPosteriorSampling

+

Constrained max posterior sampling.

+

Posterior sampling where we try to maximize an objective function while +simulatenously satisfying a set of constraints c1(x) <= 0, c2(x) <= 0, +…, cm(x) <= 0 where c1, c2, …, cm are black-box constraint functions. +Each constraint function is modeled by a seperate GP model. We follow the +procedure as described in https://doi.org/10.48550/arxiv.2002.08526.

+

Example

+
>>> CMPS = ConstrainedMaxPosteriorSampling(
+        model,
+        constraint_model=ModelListGP(cmodel1, cmodel2),
+    )
+>>> X = torch.rand(2, 100, 3)
+>>> sampled_X = CMPS(X, num_samples=5)
+
+
+

Constructor for the SamplingStrategy base class.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • objective (MCAcquisitionObjective | None) – The MCAcquisitionObjective under which the samples are evaluated. +Defaults to IdentityMCObjective().

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform for the objective +function (corresponding to model).

  • +
  • replacement (bool) – If True, sample with replacement.

  • +
  • constraint_model (ModelListGP | MultiTaskGP) – either a ModelListGP where each submodel is a GP model for +one constraint function, or a MultiTaskGP model where each task is one +constraint function. All constraints are of the form c(x) <= 0. In the +case when the constraint model predicts that all candidates +violate constraints, we pick the candidates with minimum violation.

  • +
+
+
+
+
+forward(X, num_samples=1, observation_noise=False)[source]
+

Sample from the model posterior.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x N x d-dim Tensor from which to sample (in the N +dimension) according to the maximum posterior value under the objective.

  • +
  • num_samples (int) – The number of samples to draw.

  • +
  • observation_noise (bool) – If True, sample with observation noise.

  • +
+
+
Returns:
+

+
A batch_shape x num_samples x d-dim Tensor of samples from X, where

X[…, i, :] is the i-th sample.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Utilities

+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/genindex.html b/website-old/pages/api/genindex.html new file mode 100644 index 0000000000..5d671e1a5e --- /dev/null +++ b/website-old/pages/api/genindex.html @@ -0,0 +1,4974 @@ + + + + + + + +
+
+
+
+

Index

+
+_ + | A + | B + | C + | D + | E + | F + | G + | H + | I + | K + | L + | M + | N + | O + | P + | Q + | R + | S + | T + | U + | V + | W + | X + | Y + | Z +
+

_

+ + + +
+

A

+ + + +
+

B

+ + + +
+

C

+ + + +
+

D

+ + + +
+

E

+ + + +
+

F

+ + + +
+

G

+ + + +
+

H

+ + + +
+

I

+ + + +
+

K

+ + + +
+

L

+ + + +
+

M

+ + + +
+

N

+ + + +
+

O

+ + + +
+

P

+ + + +
+

Q

+ + + +
+

R

+ + + +
+

S

+ + + +
+

T

+ + + +
+

U

+ + + +
+

V

+ + + +
+

W

+ + + +
+

X

+ + + +
+

Y

+ + + +
+

Z

+ + + +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/index.html b/website-old/pages/api/index.html new file mode 100644 index 0000000000..801e62ff0f --- /dev/null +++ b/website-old/pages/api/index.html @@ -0,0 +1,87 @@ + + + + + + + +
+ + +
+
\ No newline at end of file diff --git a/website-old/pages/api/logging.html b/website-old/pages/api/logging.html new file mode 100644 index 0000000000..19793cc8c6 --- /dev/null +++ b/website-old/pages/api/logging.html @@ -0,0 +1,71 @@ + + + + + + + +
+
+
+
+
+

botorch.logging

+
+
+botorch.logging.shape_to_str(shape)[source]
+
+
Parameters:
+

shape (Size)

+
+
Return type:
+

str

+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/models.html b/website-old/pages/api/models.html new file mode 100644 index 0000000000..7e2165c0d7 --- /dev/null +++ b/website-old/pages/api/models.html @@ -0,0 +1,6955 @@ + + + + + + + +
+
+
+
+
+

botorch.models

+
+

Model APIs

+
+

Base Model API

+

Abstract base module for all BoTorch models.

+

This module contains Model, the abstract base class for all BoTorch models, +and ModelList, a container for a list of Models.

+
+
+class botorch.models.model.Model(*args, **kwargs)[source]
+

Bases: Module, ABC

+

Abstract base class for BoTorch models.

+

The Model base class cannot be used directly; it only defines an API for other +BoTorch models.

+

Model subclasses torch.nn.Module. While a Module is most typically +encountered as a representation of a neural network layer, it can be used more +generally: see +documentation +on custom NN Modules.

+

Module provides several pieces of useful functionality: A Model’s attributes of +Tensor or Module type are automatically registered so they can be moved and/or +cast with the to method, automatically differentiated, and used with CUDA.

+
+
+
+
+_has_transformed_inputs
+

A boolean denoting whether train_inputs are currently +stored as transformed or not.

+
+
Type:
+

bool

+
+
+
+
+
+_original_train_inputs
+

A Tensor storing the original train inputs for use in +_revert_to_original_inputs. Note that this is necessary since +transform / untransform cycle introduces numerical errors which lead +to upstream errors during training.

+
+
Type:
+

torch.Tensor | None

+
+
+
+
+
+_is_fully_bayesian
+

Returns True if this is a fully Bayesian model.

+
+
+
+_is_ensemble
+

Returns True if this model consists of multiple models +that are stored in an additional batch dimension. This is true for the fully +Bayesian models.

+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+abstract posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Note: The input transforms should be applied here using

self.transform_inputs(X) after the self.eval() call and before +any model.forward or model.likelihood calls.

+
+
+
+
Parameters:
+
    +
  • X (Tensor) – A b x q x d-dim Tensor, where d is the dimension of the +feature space, q is the number of points considered jointly, +and b is the batch dimension.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior (if the model is multi-output). +Can be used to speed up computation if only a subset of the +model’s outputs are required for optimization. If omitted, +computes the posterior over all model outputs.

  • +
  • observation_noise (bool | Tensor) – For models with an inferred noise level, if True, +include observation noise. For models with an observed noise level, +this must be a model_batch_shape x 1 x m-dim tensor or +a model_batch_shape x n’ x m-dim tensor containing the average +noise for each batch and output. noise must be in the +outcome-transformed space if an outcome transform is used.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

A Posterior object, representing a batch of b joint distributions +over q points and m outputs each.

+
+
Return type:
+

Posterior

+
+
+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective, independent of the internal +representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). +For a model with m outputs, a test_batch_shape x q x d-shaped input X +to the posterior method returns a Posterior object over an output of +shape broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+property num_outputs: int
+

The number of outputs of the model.

+
+
+
+subset_output(idcs)[source]
+

Subset the model along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the model to.

+
+
Returns:
+

A Model object of the same type and with the same parameters as +the current model, subset to the specified output indices.

+
+
Return type:
+

Model

+
+
+
+
+
+condition_on_observations(X, Y, **kwargs)[source]
+

Condition the model on new observations.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, n’ is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • Y (Tensor) – A batch_shape’ x n’ x m-dim Tensor, where m is the number of +model outputs, n’ is the number of points per batch, and +batch_shape’ is the batch shape of the observations. +batch_shape’ must be broadcastable to batch_shape using +standard broadcasting semantics. If Y has fewer batch dimensions +than X, it is assumed that the missing batch dimensions are +the same for all Y.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

A Model object of the same type, representing the original model +conditioned on the new observations (X, Y) (and possibly noise +observations passed in via kwargs).

+
+
Return type:
+

Model

+
+
+
+
+
+classmethod construct_inputs(training_data)[source]
+

Construct Model keyword arguments from a SupervisedDataset.

+
+
Parameters:
+

training_data (SupervisedDataset) – A SupervisedDataset, with attributes train_X, +train_Y, and, optionally, train_Yvar.

+
+
Returns:
+

A dict of keyword arguments that can be used to initialize a Model, +with keys train_X, train_Y, and, optionally, train_Yvar.

+
+
Return type:
+

dict[str, BotorchContainer | Tensor]

+
+
+
+
+
+transform_inputs(X, input_transform=None)[source]
+

Transform inputs.

+
+
Parameters:
+
    +
  • X (Tensor) – A tensor of inputs

  • +
  • input_transform (Module | None) – A Module that performs the input transformation.

  • +
+
+
Returns:
+

A tensor of transformed inputs

+
+
Return type:
+

Tensor

+
+
+
+
+
+eval()[source]
+

Puts the model in eval mode and sets the transformed inputs.

+
+
Return type:
+

Model

+
+
+
+
+
+train(mode=True)[source]
+

Put the model in train mode. Reverts to the original inputs if in train +mode (mode=True) or sets transformed inputs if in eval mode (mode=False).

+
+
Parameters:
+

mode (bool) – A boolean denoting whether to put in train or eval mode. +If False, model is put in eval mode.

+
+
Return type:
+

Model

+
+
+
+
+
+property dtypes_of_buffers: set[dtype]
+
+
+
+
+class botorch.models.model.FantasizeMixin[source]
+

Bases: ABC

+

Mixin to add a fantasize method to a Model.

+

Example

+
+
class BaseModel:

def __init__(self, …): +def condition_on_observations(self, …): +def posterior(self, …): +def transform_inputs(self, …):

+
+
class ModelThatCanFantasize(BaseModel, FantasizeMixin):
+
def __init__(self, args):

super().__init__(args)

+
+
+
+
+

model = ModelThatCanFantasize(…) +model.fantasize(X)

+
+
+abstract condition_on_observations(X, Y)[source]
+

Classes that inherit from FantasizeMixin must implement +a condition_on_observations method.

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • Y (Tensor)

  • +
+
+
Return type:
+

Self

+
+
+
+
+
+abstract posterior(X, *args, observation_noise=False)[source]
+

Classes that inherit from FantasizeMixin must implement +a posterior method.

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • observation_noise (bool)

  • +
+
+
Return type:
+

Posterior

+
+
+
+
+
+abstract transform_inputs(X, input_transform=None)[source]
+

Classes that inherit from FantasizeMixin must implement +a transform_inputs method.

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • input_transform (Module | None)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+fantasize(X, sampler, observation_noise=None, **kwargs)[source]
+

Construct a fantasy model.

+

Constructs a fantasy model in the following fashion: +(1) compute the model posterior at X, including observation noise. +If observation_noise is a Tensor, use it directly as the observation +noise to add. +(2) sample from this posterior (using sampler) to generate “fake” +observations. +(3) condition the model on the new fake observations.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, n’ is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • sampler (MCSampler) – The sampler used for sampling from the posterior at X.

  • +
  • observation_noise (Tensor | None) – A model_batch_shape x 1 x m-dim tensor or +a model_batch_shape x n’ x m-dim tensor containing the average +noise for each batch and output, where m is the number of outputs. +noise must be in the outcome-transformed space if an outcome +transform is used. +If None and using an inferred noise likelihood, the noise will be the +inferred noise level. If using a fixed noise likelihood, the mean across +the observation noise in the training data is used as observation noise.

  • +
  • kwargs (Any) – Will be passed to model.condition_on_observations

  • +
+
+
Returns:
+

The constructed fantasy model.

+
+
Return type:
+

Self

+
+
+
+
+
+
+class botorch.models.model.ModelList(*models)[source]
+

Bases: Model

+

A multi-output Model represented by a list of independent models.

+

All BoTorch models are acceptable as inputs. The cost of this flexibility is +that ModelList does not support all methods that may be implemented by its +component models. One use case for ModelList is combining a regression +model and a deterministic model in one multi-output container model, e.g. +for cost-aware or multi-objective optimization where one of the outcomes is +a deterministic function of the inputs.

+
+
Parameters:
+

*models (Model) – A variable number of models.

+
+
+

Example

+
>>> m_1 = SingleTaskGP(train_X, train_Y)
+>>> m_2 = GenericDeterministicModel(lambda x: x.sum(dim=-1))
+>>> m_12 = ModelList(m_1, m_2)
+>>> m_12.posterior(test_X)
+
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Note: The input transforms should be applied here using

self.transform_inputs(X) after the self.eval() call and before +any model.forward or model.likelihood calls.

+
+
+
+
Parameters:
+
    +
  • X (Tensor) – A b x q x d-dim Tensor, where d is the dimension of the +feature space, q is the number of points considered jointly, +and b is the batch dimension.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior (if the model is multi-output). +Can be used to speed up computation if only a subset of the +model’s outputs are required for optimization. If omitted, +computes the posterior over all model outputs.

  • +
  • observation_noise (bool | Tensor) – If True, add the observation noise from the +respective likelihoods to the posterior. If a Tensor of shape +(batch_shape) x q x m, use it directly as the observation +noise (with observation_noise[…,i] added to the posterior +of the i-th model). observation_noise is assumed +to be in the outcome-transformed space, if an outcome transform +is used by the model.

  • +
  • posterior_transform (Callable[[PosteriorList], Posterior] | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

A Posterior object, representing a batch of b joint distributions +over q points and m outputs each.

+
+
Return type:
+

Posterior

+
+
+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective, independent of the internal +representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). +For a model with m outputs, a test_batch_shape x q x d-shaped input X +to the posterior method returns a Posterior object over an output of +shape broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+property num_outputs: int
+

The number of outputs of the model.

+

Equal to the sum of the number of outputs of the individual models +in the ModelList.

+
+
+
+subset_output(idcs)[source]
+

Subset the model along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the model to. Relative to the +overall number of outputs of the model.

+
+
Returns:
+

A Model (either a ModelList or one of the submodels) with +the outputs subset to the indices in idcs.

+
+
Return type:
+

Model

+
+
+

Internally, this drops (if single-output) or subsets (if multi-output) +the constitutent models and returns them as a ModelList. If the +result is a single (possibly subset) model from the list, returns this +model (instead of forming a degenerate singe-model ModelList). +For instance, if m = ModelList(m1, m2) with m1 a two-output model +and m2 a single-output model, then m.subset_output([1]) ` will return +the model `m1 subset to its second output.

+
+
+
+transform_inputs(X)[source]
+

Individually transform the inputs for each model.

+
+
Parameters:
+

X (Tensor) – A tensor of inputs.

+
+
Returns:
+

A list of tensors of transformed inputs.

+
+
Return type:
+

list[Tensor]

+
+
+
+
+
+load_state_dict(state_dict, strict=True)[source]
+

Initialize the fully Bayesian models before loading the state dict.

+
+
Parameters:
+
    +
  • state_dict (Mapping[str, Any])

  • +
  • strict (bool)

  • +
+
+
Return type:
+

None

+
+
+
+
+
+fantasize(X, sampler, observation_noise=None, evaluation_mask=None, **kwargs)[source]
+

Construct a fantasy model.

+

Constructs a fantasy model in the following fashion: +(1) compute the model posterior at X (including observation noise if +observation_noise=True). +(2) sample from this posterior (using sampler) to generate “fake” +observations. +(3) condition the model on the new fake observations.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, n’ is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • sampler (MCSampler) – The sampler used for sampling from the posterior at X. If +evaluation_mask is not None, this must be a ListSampler.

  • +
  • observation_noise (Tensor | None) – A model_batch_shape x 1 x m-dim tensor or +a model_batch_shape x n’ x m-dim tensor containing the average +noise for each batch and output, where m is the number of outputs. +noise must be in the outcome-transformed space if an outcome +transform is used. If None, then the noise will be the inferred +noise level.

  • +
  • evaluation_mask (Tensor | None) – A n’ x m-dim tensor of booleans indicating which +outputs should be fantasized for a given design. This uses the same +evaluation mask for all batches.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

The constructed fantasy model.

+
+
Return type:
+

Model

+
+
+
+
+
+
+class botorch.models.model.ModelDict(**models)[source]
+

Bases: ModuleDict

+

A lightweight container mapping model names to models.

+

Initialize a ModelDict.

+
+
Parameters:
+

models (Model) – An arbitrary number of models. Each model can be any type +of BoTorch Model, including multi-output models and ModelList.

+
+
+
+
+
+

GPyTorch Model API

+

Abstract model class for all GPyTorch-based botorch models.

+

To implement your own, simply inherit from both the provided classes and a +GPyTorch Model class such as an ExactGP.

+
+
+class botorch.models.gpytorch.GPyTorchModel(*args, **kwargs)[source]
+

Bases: Model, ABC

+

Abstract base class for models based on GPyTorch models.

+

The easiest way to use this is to subclass a model from a GPyTorch model +class (e.g. an ExactGP) and this GPyTorchModel. See e.g. SingleTaskGP.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+likelihood: Likelihood
+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective, independent of the internal +representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). +For a model with m outputs, a test_batch_shape x q x d-shaped input X +to the posterior method returns a Posterior object over an output of +shape broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+property num_outputs: int
+

The number of outputs of the model.

+
+
+
+posterior(X, observation_noise=False, posterior_transform=None, **kwargs)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X (Tensor) – A (batch_shape) x q x d-dim Tensor, where d is the dimension +of the feature space and q is the number of points considered +jointly.

  • +
  • observation_noise (bool | Tensor) – If True, add the observation noise from the +likelihood to the posterior. If a Tensor, use it directly as the +observation noise (must be of shape (batch_shape) x q). It is +assumed to be in the outcome-transformed space if an outcome +transform is used.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

A GPyTorchPosterior object, representing a batch of b joint +distributions over q points. Includes observation noise if +specified.

+
+
Return type:
+

GPyTorchPosterior | TransformedPosterior

+
+
+
+
+
+condition_on_observations(X, Y, noise=None, **kwargs)[source]
+

Condition the model on new observations.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, n’ is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • Y (Tensor) – A batch_shape’ x n x m-dim Tensor, where m is the number of +model outputs, n’ is the number of points per batch, and +batch_shape’ is the batch shape of the observations. +batch_shape’ must be broadcastable to batch_shape using +standard broadcasting semantics. If Y has fewer batch dimensions +than X, its is assumed that the missing batch dimensions are +the same for all Y.

  • +
  • noise (Tensor | None) – If not None, a tensor of the same shape as Y representing +the associated noise variance.

  • +
  • kwargs (Any) – Passed to self.get_fantasy_model.

  • +
+
+
Returns:
+

A Model object of the same type, representing the original model +conditioned on the new observations (X, Y) (and possibly noise +observations passed in via kwargs).

+
+
Return type:
+

Model

+
+
+

Example

+
>>> train_X = torch.rand(20, 2)
+>>> train_Y = torch.sin(train_X[:, 0]) + torch.cos(train_X[:, 1])
+>>> model = SingleTaskGP(train_X, train_Y)
+>>> new_X = torch.rand(5, 2)
+>>> new_Y = torch.sin(new_X[:, 0]) + torch.cos(new_X[:, 1])
+>>> model = model.condition_on_observations(X=new_X, Y=new_Y)
+
+
+
+
+
+
+class botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel(*args, **kwargs)[source]
+

Bases: GPyTorchModel

+

Base class for batched multi-output GPyTorch models with independent outputs.

+

This model should be used when the same training data is used for all outputs. +Outputs are modeled independently by using a different batch for each output.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+static get_batch_dimensions(train_X, train_Y)[source]
+

Get the raw batch shape and output-augmented batch shape of the inputs.

+
+
Parameters:
+
    +
  • train_X (Tensor) – A n x d or batch_shape x n x d (batch mode) tensor of training +features.

  • +
  • train_Y (Tensor) – A n x m or batch_shape x n x m (batch mode) tensor of +training observations.

  • +
+
+
Returns:
+

2-element tuple containing

+
    +
  • The input_batch_shape

  • +
  • The output-augmented batch shape: input_batch_shape x (m)

  • +
+

+
+
Return type:
+

tuple[Size, Size]

+
+
+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective, independent of the internal +representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). +For a model with m outputs, a test_batch_shape x q x d-shaped input X +to the posterior method returns a Posterior object over an output of +shape broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X (Tensor) – A (batch_shape) x q x d-dim Tensor, where d is the dimension +of the feature space and q is the number of points considered +jointly.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior (if the model is multi-output). +Can be used to speed up computation if only a subset of the +model’s outputs are required for optimization. If omitted, +computes the posterior over all model outputs.

  • +
  • observation_noise (bool | Tensor) – If True, add the observation noise from the +likelihood to the posterior. If a Tensor, use it directly as the +observation noise (must be of shape (batch_shape) x q x m).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

A GPyTorchPosterior object, representing batch_shape joint +distributions over q points and the outputs selected by +output_indices each. Includes observation noise if specified.

+
+
Return type:
+

GPyTorchPosterior | TransformedPosterior

+
+
+
+
+
+condition_on_observations(X, Y, **kwargs)[source]
+

Condition the model on new observations.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, m is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • Y (Tensor) – A batch_shape’ x n’ x m-dim Tensor, where m is the number of +model outputs, n’ is the number of points per batch, and +batch_shape’ is the batch shape of the observations. +batch_shape’ must be broadcastable to batch_shape using +standard broadcasting semantics. If Y has fewer batch dimensions +than X, its is assumed that the missing batch dimensions are +the same for all Y.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

A BatchedMultiOutputGPyTorchModel object of the same type with +n + n’ training examples, representing the original model +conditioned on the new observations (X, Y) (and possibly noise +observations passed in via kwargs).

+
+
Return type:
+

BatchedMultiOutputGPyTorchModel

+
+
+

Example

+
>>> train_X = torch.rand(20, 2)
+>>> train_Y = torch.cat(
+>>>     [torch.sin(train_X[:, 0]), torch.cos(train_X[:, 1])], -1
+>>> )
+>>> model = SingleTaskGP(train_X, train_Y)
+>>> new_X = torch.rand(5, 2)
+>>> new_Y = torch.cat([torch.sin(new_X[:, 0]), torch.cos(new_X[:, 1])], -1)
+>>> model = model.condition_on_observations(X=new_X, Y=new_Y)
+
+
+
+
+
+subset_output(idcs)[source]
+

Subset the model along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the model to.

+
+
Returns:
+

The current model, subset to the specified output indices.

+
+
Return type:
+

BatchedMultiOutputGPyTorchModel

+
+
+
+
+
+
+class botorch.models.gpytorch.ModelListGPyTorchModel(*models)[source]
+

Bases: ModelList, GPyTorchModel, ABC

+

Abstract base class for models based on multi-output GPyTorch models.

+

This is meant to be used with a gpytorch ModelList wrapper for independent +evaluation of submodels. Those submodels can themselves be multi-output +models, in which case the task covariances will be ignored.

+
+
Parameters:
+

*models (Model) – A variable number of models.

+
+
+

Example

+
>>> m_1 = SingleTaskGP(train_X, train_Y)
+>>> m_2 = GenericDeterministicModel(lambda x: x.sum(dim=-1))
+>>> m_12 = ModelList(m_1, m_2)
+>>> m_12.posterior(test_X)
+
+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective, independent of the internal +representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). +For a model with m outputs, a test_batch_shape x q x d-shaped input X +to the posterior method returns a Posterior object over an output of +shape broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points. +If any model returns a MultitaskMultivariateNormal posterior, then that +will be split into individual MVNs per task, with inter-task covariance +ignored.

+
+
Parameters:
+
    +
  • X (Tensor) – A b x q x d-dim Tensor, where d is the dimension of the +feature space, q is the number of points considered jointly, +and b is the batch dimension.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior (if the model is multi-output). +Can be used to speed up computation if only a subset of the +model’s outputs are required for optimization. If omitted, +computes the posterior over all model outputs.

  • +
  • observation_noise (bool | Tensor) – If True, add the observation noise from the +respective likelihoods to the posterior. If a Tensor of shape +(batch_shape) x q x m, use it directly as the observation +noise (with observation_noise[…,i] added to the posterior +of the i-th model).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

    +
  • +
    If no posterior_transform is provided and the component models have no

    outcome_transform, or if the component models only use linear outcome +transforms like Standardize (i.e. not Log), returns a +GPyTorchPosterior or GaussianMixturePosterior object, +representing batch_shape joint distributions over q points +and the outputs selected by output_indices each. Includes +measurement noise if observation_noise is specified.

    +
    +
    +
  • +
  • +
    If no posterior_transform is provided and component models have

    nonlinear transforms like Log, returns a PosteriorList with +sub-posteriors of type TransformedPosterior

    +
    +
    +
  • +
  • +
    If posterior_transform is provided, that posterior transform will be

    applied and will determine the return type. This could potentially be +any subclass of Posterior, but common choices give a +GPyTorchPosterior.

    +
    +
    +
  • +
+

+
+
Return type:
+

GPyTorchPosterior | PosteriorList

+
+
+
+
+
+condition_on_observations(X, Y, **kwargs)[source]
+

Condition the model on new observations.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, n’ is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • Y (Tensor) – A batch_shape’ x n’ x m-dim Tensor, where m is the number of +model outputs, n’ is the number of points per batch, and +batch_shape’ is the batch shape of the observations. +batch_shape’ must be broadcastable to batch_shape using +standard broadcasting semantics. If Y has fewer batch dimensions +than X, it is assumed that the missing batch dimensions are +the same for all Y.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

A Model object of the same type, representing the original model +conditioned on the new observations (X, Y) (and possibly noise +observations passed in via kwargs).

+
+
Return type:
+

Model

+
+
+
+
+
+
+class botorch.models.gpytorch.MultiTaskGPyTorchModel(*args, **kwargs)[source]
+

Bases: GPyTorchModel, ABC

+

Abstract base class for multi-task models based on GPyTorch models.

+

This class provides the posterior method to models that implement a +“long-format” multi-task GP in the style of MultiTaskGP.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X (Tensor) – A tensor of shape batch_shape x q x d or batch_shape x q x (d + 1), +where d is the dimension of the feature space (not including task +indices) and q is the number of points considered jointly. The + 1 +dimension is the optional task feature / index. If given, the model +produces the outputs for the given task indices. If omitted, the +model produces outputs for tasks in in self._output_tasks (specified +as output_tasks while constructing the model), which can overwritten +using output_indices.

  • +
  • output_indices (list[int] | None) – A list of task values over which to compute the posterior. +Only used if X does not include the task feature. If omitted, +defaults to self._output_tasks.

  • +
  • observation_noise (bool | Tensor) – If True, add observation noise from the respective +likelihoods. If a Tensor, specifies the observation noise levels +to add.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

A GPyTorchPosterior object, representing batch_shape joint +distributions over q points. If the task features are included in X, +the posterior will be single output. Otherwise, the posterior will be +single or multi output corresponding to the tasks included in +either the output_indices or self._output_tasks.

+
+
Return type:
+

GPyTorchPosterior | TransformedPosterior

+
+
+
+
+
+subset_output(idcs)[source]
+

Returns a new model that only outputs a subset of the outputs.

+
+
Parameters:
+

idcs (list[int]) – A list of output indices, corresponding to the outputs to keep.

+
+
Returns:
+

A new model that only outputs the requested outputs.

+
+
Return type:
+

MultiTaskGPyTorchModel

+
+
+
+
+
+
+

Deterministic Model API

+

Deterministic Models: Simple wrappers that allow the usage of deterministic +mappings via the BoTorch Model and Posterior APIs.

+

Deterministic models are useful for expressing known input-output relationships +within the BoTorch Model API. This is useful e.g. for multi-objective +optimization with known objective functions (e.g. the number of parameters of a +Neural Network in the context of Neural Architecture Search is usually a known +function of the architecture configuration), or to encode cost functions for +cost-aware acquisition utilities. Cost-aware optimization is desirable when +evaluations have a cost that is heterogeneous, either in the inputs X or in a +particular fidelity parameter that directly encodes the fidelity of the +observation. GenericDeterministicModel supports arbitrary deterministic +functions, while AffineFidelityCostModel is a particular cost model for +multi-fidelity optimization. Other use cases of deterministic models include +representing approximate GP sample paths, e.g. Matheron paths obtained +with get_matheron_path_model, which allows them to be substituted in acquisition +functions or in other places where a Model is expected.

+
+
+class botorch.models.deterministic.DeterministicModel(*args, **kwargs)[source]
+

Bases: EnsembleModel

+

Abstract base class for deterministic models.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+abstract forward(X)[source]
+

Compute the (deterministic) model output at X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim input tensor X.

+
+
Returns:
+

A batch_shape x n x m-dimensional output tensor (the outcome +dimension m must be explicit if m=1).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.deterministic.GenericDeterministicModel(f, num_outputs=1)[source]
+

Bases: DeterministicModel

+

A generic deterministic model constructed from a callable.

+

Example

+
>>> f = lambda x: x.sum(dim=-1, keep_dims=True)
+>>> model = GenericDeterministicModel(f)
+
+
+
+
Parameters:
+
    +
  • f (Callable[[Tensor], Tensor]) – A callable mapping a batch_shape x n x d-dim input tensor X +to a batch_shape x n x m-dimensional output tensor (the +outcome dimension m must be explicit, even if m=1).

  • +
  • num_outputs (int) – The number of outputs m.

  • +
+
+
+
+
+subset_output(idcs)[source]
+

Subset the model along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the model to.

+
+
Returns:
+

The current model, subset to the specified output indices.

+
+
Return type:
+

GenericDeterministicModel

+
+
+
+
+
+forward(X)[source]
+

Compute the (deterministic) model output at X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim input tensor X.

+
+
Returns:
+

A batch_shape x n x m-dimensional output tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.deterministic.AffineDeterministicModel(a, b=0.01)[source]
+

Bases: DeterministicModel

+

An affine deterministic model.

+

Affine deterministic model from weights and offset terms.

+

A simple model of the form

+
+

y[…, m] = b[m] + sum_{i=1}^d a[i, m] * X[…, i]

+
+
+
Parameters:
+
    +
  • a (Tensor) – A d x m-dim tensor of linear weights, where m is the number +of outputs (must be explicit if m=1)

  • +
  • b (Tensor | float) – The affine (offset) term. Either a float (for single-output +models or if the offset is shared), or a m-dim tensor (with +different offset values for for the m different outputs).

  • +
+
+
+
+
+subset_output(idcs)[source]
+

Subset the model along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the model to.

+
+
Returns:
+

The current model, subset to the specified output indices.

+
+
Return type:
+

AffineDeterministicModel

+
+
+
+
+
+forward(X)[source]
+

Compute the (deterministic) model output at X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim input tensor X.

+
+
Returns:
+

A batch_shape x n x m-dimensional output tensor (the outcome +dimension m must be explicit if m=1).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.deterministic.PosteriorMeanModel(model)[source]
+

Bases: DeterministicModel

+

A deterministic model that always returns the posterior mean.

+
+
Parameters:
+

model (Model) – The base model.

+
+
+
+
+forward(X)[source]
+

Compute the (deterministic) model output at X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim input tensor X.

+
+
Returns:
+

A batch_shape x n x m-dimensional output tensor (the outcome +dimension m must be explicit if m=1).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.deterministic.FixedSingleSampleModel(model, w=None, dim=None, jitter=1e-08, dtype=None, device=None)[source]
+

Bases: DeterministicModel

+

A deterministic model defined by a single sample w.

+

Given a base model f and a fixed sample w, the model always outputs

+
+

y = f_mean(x) + f_stddev(x) * w

+
+

We assume the outcomes are uncorrelated here.

+
+
Parameters:
+
    +
  • model (Model) – The base model.

  • +
  • w (Tensor | None) – A 1-d tensor with length model.num_outputs. +If None, draw it from a standard normal distribution.

  • +
  • dim (int | None) – dimensionality of w. +If None and w is not provided, draw w samples of size model.num_outputs.

  • +
  • jitter (float | None) – jitter value to be added for numerical stability, 1e-8 by default.

  • +
  • dtype (torch.dtype | None) – dtype for w if specified

  • +
  • device (torch.dtype | None) – device for w if specified

  • +
+
+
+
+
+forward(X)[source]
+

Compute the (deterministic) model output at X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim input tensor X.

+
+
Returns:
+

A batch_shape x n x m-dimensional output tensor (the outcome +dimension m must be explicit if m=1).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Ensemble Model API

+

Ensemble Models: Simple wrappers that allow the usage of ensembles +via the BoTorch Model and Posterior APIs.

+
+
+class botorch.models.ensemble.EnsembleModel(*args, **kwargs)[source]
+

Bases: Model, ABC

+

Abstract base class for ensemble models.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+abstract forward(X)[source]
+

Compute the (ensemble) model output at X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim input tensor X.

+
+
Returns:
+

A batch_shape x s x n x m-dimensional output tensor where +s is the size of the ensemble.

+
+
Return type:
+

Tensor

+
+
+
+
+
+property num_outputs: int
+

The number of outputs of the model.

+
+
+
+posterior(X, output_indices=None, posterior_transform=None, **kwargs)[source]
+

Compute the ensemble posterior at X.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x d-dim input tensor X.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior. If omitted, computes the posterior +over all model outputs.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

An EnsemblePosterior object, representing batch_shape joint +posteriors over n points and the outputs selected by output_indices.

+
+
Return type:
+

EnsemblePosterior

+
+
+
+
+
+
+
+

Models

+
+

Cost Models (for cost-aware optimization)

+

Cost models to be used with multi-fidelity optimization.

+

Cost are useful for defining known cost functions when the cost of an evaluation +is heterogeneous in fidelity. For a full worked example, see the +tutorial on continuous +multi-fidelity Bayesian Optimization.

+
+
+class botorch.models.cost.AffineFidelityCostModel(fidelity_weights=None, fixed_cost=0.01)[source]
+

Bases: DeterministicModel

+

Deterministic, affine cost model operating on fidelity parameters.

+

For each (q-batch) element of a candidate set X, this module computes a +cost of the form

+
+

cost = fixed_cost + sum_j weights[j] * X[fidelity_dims[j]]

+
+

For a full worked example, see the +tutorial on continuous +multi-fidelity Bayesian Optimization.

+

Example

+
>>> from botorch.models import AffineFidelityCostModel
+>>> from botorch.acquisition.cost_aware import InverseCostWeightedUtility
+>>> cost_model = AffineFidelityCostModel(
+>>>    fidelity_weights={6: 1.0}, fixed_cost=5.0
+>>> )
+>>> cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)
+
+
+
+
Parameters:
+
    +
  • fidelity_weights (dict[int, float] | None) – A dictionary mapping a subset of columns of X +(the fidelity parameters) to its associated weight in the +affine cost expression. If omitted, assumes that the last +column of X is the fidelity parameter with a weight of 1.0.

  • +
  • fixed_cost (float) – The fixed cost of running a single candidate point (i.e. +an element of a q-batch).

  • +
+
+
+
+
+forward(X)[source]
+

Evaluate the cost on a candidate set X.

+

Computes a cost of the form

+
+

cost = fixed_cost + sum_j weights[j] * X[fidelity_dims[j]]

+
+

for each element of the q-batch

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d’-dim tensor of candidate points.

+
+
Returns:
+

A batch_shape x q x 1-dim tensor of costs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.cost.FixedCostModel(fixed_cost)[source]
+

Bases: DeterministicModel

+

Deterministic, fixed cost model.

+

For each (q-batch) element of a candidate set X, this module computes a +fixed cost per objective.

+
+
Parameters:
+

fixed_cost (Tensor) – A m-dim tensor containing the fixed cost of evaluating each +objective.

+
+
+
+
+forward(X)[source]
+

Evaluate the cost on a candidate set X.

+

Computes the fixed cost of evaluating each objective for each element +of the q-batch.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d’-dim tensor of candidate points.

+
+
Returns:
+

A batch_shape x q x m-dim tensor of costs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

GP Regression Models

+

Gaussian Process Regression models based on GPyTorch models.

+

These models are often a good starting point and are further documented in the +tutorials.

+

SingleTaskGP is a single-task exact GP model that uses relatively strong priors on +the Kernel hyperparameters, which work best when covariates are normalized to the unit +cube and outcomes are standardized (zero mean, unit variance). By default, this model +uses a Standardize outcome transform, which applies this standardization. However, +it does not (yet) use an input transform by default.

+

SingleTaskGP model works in batch mode (each batch having its own hyperparameters). +When the training observations include multiple outputs, SingleTaskGP uses +batching to model outputs independently.

+

SingleTaskGP supports multiple outputs. However, as a single-task model, +SingleTaskGP should be used only when the outputs are independent and all +use the same training inputs. If outputs are independent but they have different +training inputs, use the ModelListGP. When modeling correlations between outputs, +use a multi-task model like MultiTaskGP.

+
+
+class botorch.models.gp_regression.SingleTaskGP(train_X, train_Y, train_Yvar=None, likelihood=None, covar_module=None, mean_module=None, outcome_transform=<class 'botorch.utils.types.DEFAULT'>, input_transform=None)[source]
+

Bases: BatchedMultiOutputGPyTorchModel, ExactGP, FantasizeMixin

+

A single-task exact GP model, supporting both known and inferred noise levels.

+

A single-task exact GP which, by default, utilizes hyperparameter priors +from [Hvarfner2024vanilla]. These priors designed to perform well independently of +the dimensionality of the problem. Moreover, they suggest a moderately low level of +noise. Importantly, The model works best when covariates are normalized to the unit +cube and outcomes are standardized (zero mean, unit variance). For a detailed +discussion on the hyperparameter priors, see +https://github.com/pytorch/botorch/discussions/2451.

+

This model works in batch mode (each batch having its own hyperparameters). +When the training observations include multiple outputs, this model will use +batching to model outputs independently.

+

Use this model when you have independent output(s) and all outputs use the +same training data. If outputs are independent and outputs have different +training data, use the ModelListGP. When modeling correlations between +outputs, use the MultiTaskGP.

+

An example of a case in which noise levels are known is online +experimentation, where noise can be measured using the variability of +different observations from the same arm, or provided by outside software. +Another use case is simulation optimization, where the evaluation can +provide variance estimates, perhaps from bootstrapping. In any case, these +noise levels can be provided to SingleTaskGP as train_Yvar.

+

SingleTaskGP can also be used when the observations are known to be +noise-free. Noise-free observations can be modeled using arbitrarily small +noise values, such as train_Yvar=torch.full_like(train_Y, 1e-6).

+

Example

+

Model with inferred noise levels:

+
>>> import torch
+>>> from botorch.models.gp_regression import SingleTaskGP
+>>> from botorch.models.transforms.outcome import Standardize
+>>>
+>>> train_X = torch.rand(20, 2, dtype=torch.float64)
+>>> train_Y = torch.sin(train_X).sum(dim=1, keepdim=True)
+>>> outcome_transform = Standardize(m=1)
+>>> inferred_noise_model = SingleTaskGP(
+...     train_X, train_Y, outcome_transform=outcome_transform,
+... )
+
+
+

Model with a known observation variance of 0.2:

+
>>> train_Yvar = torch.full_like(train_Y, 0.2)
+>>> observed_noise_model = SingleTaskGP(
+...     train_X, train_Y, train_Yvar,
+...     outcome_transform=outcome_transform,
+... )
+
+
+

With noise-free observations:

+
>>> train_Yvar = torch.full_like(train_Y, 1e-6)
+>>> noise_free_model = SingleTaskGP(
+...     train_X, train_Y, train_Yvar,
+...     outcome_transform=outcome_transform,
+... )
+
+
+
+
Parameters:
+
    +
  • train_X (Tensor) – A batch_shape x n x d tensor of training features.

  • +
  • train_Y (Tensor) – A batch_shape x n x m tensor of training observations.

  • +
  • train_Yvar (Tensor | None) – An optional batch_shape x n x m tensor of observed +measurement noise.

  • +
  • likelihood (Likelihood | None) – A likelihood. If omitted, use a standard +GaussianLikelihood with inferred noise level if train_Yvar +is None, and a FixedNoiseGaussianLikelihood with the given +noise observations if train_Yvar is not None.

  • +
  • covar_module (Module | None) – The module computing the covariance (Kernel) matrix. +If omitted, uses an RBFKernel.

  • +
  • mean_module (Mean | None) – The mean function to be used. If omitted, use a +ConstantMean.

  • +
  • outcome_transform (OutcomeTransform | _DefaultType | None) – An outcome transform that is applied to the +training data during instantiation and to the posterior during +inference (that is, the Posterior obtained by calling +.posterior on the model will be on the original scale). We use a +Standardize transform if no outcome_transform is specified. +Pass down None to use no outcome transform.

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass.

  • +
+
+
+
+
+classmethod construct_inputs(training_data, *, task_feature=None)[source]
+

Construct SingleTaskGP keyword arguments from a SupervisedDataset.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset) – A SupervisedDataset, with attributes train_X, +train_Y, and, optionally, train_Yvar.

  • +
  • task_feature (int | None) – Deprecated and allowed only for backward +compatibility; ignored.

  • +
+
+
Returns:
+

A dict of keyword arguments that can be used to initialize a SingleTaskGP, +with keys train_X, train_Y, and, optionally, train_Yvar.

+
+
Return type:
+

dict[str, BotorchContainer | Tensor]

+
+
+
+
+
+forward(x)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

MultivariateNormal

+
+
+
+
+
+
+

Multi-Fidelity GP Regression Models

+

Multi-Fidelity Gaussian Process Regression models based on GPyTorch models.

+

For more on Multi-Fidelity BO, see the +tutorial.

+

A common use case of multi-fidelity regression modeling is optimizing a +“high-fidelity” function that is expensive to simulate when you have access to +one or more cheaper “lower-fidelity” versions that are not fully accurate but +are correlated with the high-fidelity function. The multi-fidelity model models +both the low- and high-fidelity functions together, including the correlation +between them, which can help you predict and optimize the high-fidelity function +without having to do too many expensive high-fidelity evaluations.

+
+
+[Wu2019mf] +

J. Wu, S. Toscano-Palmerin, P. I. Frazier, and A. G. Wilson. Practical +multi-fidelity bayesian optimization for hyperparameter tuning. ArXiv 2019.

+
+
+
+
+class botorch.models.gp_regression_fidelity.SingleTaskMultiFidelityGP(train_X, train_Y, train_Yvar=None, iteration_fidelity=None, data_fidelities=None, linear_truncated=True, nu=2.5, likelihood=None, outcome_transform=<class 'botorch.utils.types.DEFAULT'>, input_transform=None)[source]
+

Bases: SingleTaskGP

+

A single task multi-fidelity GP model.

+

A SingleTaskGP model using a DownsamplingKernel for the data fidelity +parameter (if present) and an ExponentialDecayKernel for the iteration +fidelity parameter (if present).

+

This kernel is described in [Wu2019mf].

+

Example

+
>>> train_X = torch.rand(20, 4)
+>>> train_Y = train_X.pow(2).sum(dim=-1, keepdim=True)
+>>> model = SingleTaskMultiFidelityGP(train_X, train_Y, data_fidelities=[3])
+
+
+
+
Parameters:
+
    +
  • train_X (Tensor) – A batch_shape x n x (d + s) tensor of training features, +where s is the dimension of the fidelity parameters (either one +or two).

  • +
  • train_Y (Tensor) – A batch_shape x n x m tensor of training observations.

  • +
  • train_Yvar (Tensor | None) – An optional batch_shape x n x m tensor of observed +measurement noise.

  • +
  • iteration_fidelity (int | None) – The column index for the training iteration fidelity +parameter (optional).

  • +
  • data_fidelities (Sequence[int] | None) – The column indices for the downsampling fidelity parameter. +If a list/tuple of indices is provided, a kernel will be constructed for +each index (optional).

  • +
  • linear_truncated (bool) – If True, use a LinearTruncatedFidelityKernel instead +of the default kernel.

  • +
  • nu (float) – The smoothness parameter for the Matern kernel: either 1/2, 3/2, or +5/2. Only used when linear_truncated=True.

  • +
  • likelihood (Likelihood | None) – A likelihood. If omitted, use a standard GaussianLikelihood +with inferred noise level.

  • +
  • outcome_transform (OutcomeTransform | _DefaultType | None) – An outcome transform that is applied to the +training data during instantiation and to the posterior during +inference (that is, the Posterior obtained by calling +.posterior on the model will be on the original scale). We use a +Standardize transform if no outcome_transform is specified. +Pass down None to use no outcome transform.

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass.

  • +
+
+
+
+
+classmethod construct_inputs(training_data, fidelity_features)[source]
+

Construct Model keyword arguments from a dict of SupervisedDataset.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset) – Dictionary of SupervisedDataset.

  • +
  • fidelity_features (list[int]) – Index of fidelity parameter as input columns.

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+

GP Regression Models for Mixed Parameter Spaces

+
+
+class botorch.models.gp_regression_mixed.MixedSingleTaskGP(train_X, train_Y, cat_dims, train_Yvar=None, cont_kernel_factory=None, likelihood=None, outcome_transform=<class 'botorch.utils.types.DEFAULT'>, input_transform=None)[source]
+

Bases: SingleTaskGP

+

A single-task exact GP model for mixed search spaces.

+

This model is similar to SingleTaskGP, but supports mixed search spaces, +which combine discrete and continuous features, as well as solely discrete +spaces. It uses a kernel that combines a CategoricalKernel (based on +Hamming distances) and a regular kernel into a kernel of the form

+
+
+
K((x1, c1), (x2, c2)) =

K_cont_1(x1, x2) + K_cat_1(c1, c2) + +K_cont_2(x1, x2) * K_cat_2(c1, c2)

+
+
+
+

where xi and ci are the continuous and categorical features of the +input, respectively. The suffix _i indicates that we fit different +lengthscales for the kernels in the sum and product terms.

+

Since this model does not provide gradients for the categorical features, +optimization of the acquisition function will need to be performed in +a mixed fashion, i.e., treating the categorical features properly as +discrete optimization variables. We recommend using optimize_acqf_mixed.

+

Example

+
>>> train_X = torch.cat(
+        [torch.rand(20, 2), torch.randint(3, (20, 1))], dim=-1)
+    )
+>>> train_Y = (
+        torch.sin(train_X[..., :-1]).sum(dim=1, keepdim=True)
+        + train_X[..., -1:]
+    )
+>>> model = MixedSingleTaskGP(train_X, train_Y, cat_dims=[-1])
+
+
+

A single-task exact GP model supporting categorical parameters.

+
+
Parameters:
+
    +
  • train_X (Tensor) – A batch_shape x n x d tensor of training features.

  • +
  • train_Y (Tensor) – A batch_shape x n x m tensor of training observations.

  • +
  • cat_dims (list[int]) – A list of indices corresponding to the columns of +the input X that should be considered categorical features.

  • +
  • train_Yvar (Tensor | None) – An optional batch_shape x n x m tensor of observed +measurement noise.

  • +
  • cont_kernel_factory (None | Callable[[torch.Size, int, list[int]], Kernel]) – A method that accepts batch_shape, ard_num_dims, +and active_dims arguments and returns an instantiated GPyTorch +Kernel object to be used as the base kernel for the continuous +dimensions. If omitted, this model uses an RBFKernel as +the kernel for the ordinal parameters.

  • +
  • likelihood (Likelihood | None) – A likelihood. If omitted, use a standard +GaussianLikelihood with inferred noise level.

  • +
  • outcome_transform (OutcomeTransform | _DefaultType | None) – An outcome transform that is applied to the +training data during instantiation and to the posterior during +inference (that is, the Posterior obtained by calling +.posterior on the model will be on the original scale). We use a +Standardize transform if no outcome_transform is specified. +Pass down None to use no outcome transform.

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass. Only input transforms are allowed which do not +transform the categorical dimensions. If you want to use it +for example in combination with a OneHotToNumeric input transform +one has to instantiate the transform with transform_on_train == False +and pass in the already transformed input.

  • +
+
+
+
+
+classmethod construct_inputs(training_data, categorical_features, likelihood=None)[source]
+

Construct Model keyword arguments from a dict of SupervisedDataset.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset) – A SupervisedDataset containing the training data.

  • +
  • categorical_features (list[int]) – Column indices of categorical features.

  • +
  • likelihood (Likelihood | None) – Optional likelihood used to constuct the model.

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+

Model List GP Regression Models

+

Model List GP Regression models.

+
+
+class botorch.models.model_list_gp_regression.ModelListGP(*gp_models)[source]
+

Bases: IndependentModelList, ModelListGPyTorchModel, FantasizeMixin

+

A multi-output GP model with independent GPs for the outputs.

+

This model supports different-shaped training inputs for each of its +sub-models. It can be used with any number of single-output +GPyTorchModels and the models can be of different types. Use this model +when you have independent outputs with different training data. When +modeling correlations between outputs, use MultiTaskGP.

+

Internally, this model is just a list of individual models, but it implements +the same input/output interface as all other BoTorch models. This makes it +very flexible and convenient to work with. The sequential evaluation comes +at a performance cost though - if you are using a block design (i.e. the +same number of training example for each output, and a similar model +structure, you should consider using a batched GP model instead, such as +SingleTaskGP with batched inputs).

+
+
Parameters:
+

*gp_models (GPyTorchModel) – A number of single-output GPyTorchModels. +If models have input/output transforms, these are honored +individually for each model.

+
+
+

Example

+
>>> model1 = SingleTaskGP(train_X1, train_Y1)
+>>> model2 = SingleTaskGP(train_X2, train_Y2)
+>>> model = ModelListGP(model1, model2)
+
+
+
+
+condition_on_observations(X, Y, **kwargs)[source]
+

Condition the model on new observations.

+
+
Parameters:
+
    +
  • X (list[Tensor]) – A m-list of batch_shape x n’ x d-dim Tensors, where d is the +dimension of the feature space, n’ is the number of points +per batch, and batch_shape is the batch shape (must be compatible +with the batch shape of the model).

  • +
  • Y (Tensor) – A batch_shape’ x n’ x m-dim Tensor, where m is the number of +model outputs, n’ is the number of points per batch, and +batch_shape’ is the batch shape of the observations. +batch_shape’ must be broadcastable to batch_shape using +standard broadcasting semantics. If Y has fewer batch dimensions +than X, its is assumed that the missing batch dimensions are +the same for all Y.

  • +
  • kwargs (Any) – Keyword arguments passed to +IndependentModelList.get_fantasy_model.

  • +
+
+
Returns:
+

A ModelListGP representing the original model +conditioned on the new observations (X, Y) (and possibly noise +observations passed in via kwargs). Here the i-th model has +n_i + n’ training examples, where the n’ training examples have +been added and all test-time caches have been updated.

+
+
Return type:
+

ModelListGP

+
+
+
+
+
+
+

Multitask GP Models

+

Multi-Task GP models.

+

References

+
+
+[Bonilla2007MTGP] +

E. Bonilla, K. Chai and C. Williams. Multi-task Gaussian Process Prediction. +Advances in Neural Information Processing Systems 20, NeurIPS 2007.

+
+
+[Swersky2013MTBO] +

K. Swersky, J. Snoek and R. Adams. Multi-Task Bayesian Optimization. +Advances in Neural Information Processing Systems 26, NeurIPS 2013.

+
+
+[Doucet2010sampl] +

A. Doucet. A Note on Efficient Conditional Simulation of Gaussian Distributions. +http://www.stats.ox.ac.uk/~doucet/doucet_simulationconditionalgaussian.pdf, +Apr 2010.

+
+
+[Maddox2021bohdo] +

W. Maddox, M. Balandat, A. Wilson, and E. Bakshy. Bayesian Optimization with +High-Dimensional Outputs. https://arxiv.org/abs/2106.12997, Jun 2021.

+
+
+
+
+botorch.models.multitask.get_task_value_remapping(task_values, dtype)[source]
+

Construct an mapping of discrete task values to contiguous int-valued floats.

+
+
Parameters:
+
    +
  • task_values (Tensor) – A sorted long-valued tensor of task values.

  • +
  • dtype (dtype) – The dtype of the model inputs (e.g. X), which the new +task values should have mapped to (e.g. float, double).

  • +
+
+
Returns:
+

A tensor of shape task_values.max() + 1 that maps task values +to new task values. The indexing operation mapper[task_value] +will produce a tensor of new task values, of the same shape as +the original. The elements of the mapper tensor that do not +appear in the original task_values are mapped to nan. The +return value will be None, when the task values are contiguous +integers starting from zero.

+
+
Return type:
+

Tensor | None

+
+
+
+
+
+class botorch.models.multitask.MultiTaskGP(train_X, train_Y, task_feature, train_Yvar=None, mean_module=None, covar_module=None, likelihood=None, task_covar_prior=None, output_tasks=None, rank=None, all_tasks=None, outcome_transform=<class 'botorch.utils.types.DEFAULT'>, input_transform=None)[source]
+

Bases: ExactGP, MultiTaskGPyTorchModel, FantasizeMixin

+

Multi-Task exact GP model using an ICM (intrinsic co-regionalization model) +kernel. See [Bonilla2007MTGP] and [Swersky2013MTBO] for a reference on the +model and its use in Bayesian optimization.

+

The model can be single-output or multi-output, determined by the output_tasks. +This model uses relatively strong priors on the base Kernel hyperparameters, which +work best when covariates are normalized to the unit cube and outcomes are +standardized (zero mean, unit variance) - this standardization should be applied in +a stratified fashion at the level of the tasks, rather than across all data points.

+

If the train_Yvar is None, this model infers the noise level. If you have +known observation noise, you can set train_Yvar to a tensor containing +the noise variance measurements. WARNING: This currently does not support +different noise levels for the different tasks.

+

Multi-Task GP model using an ICM kernel.

+
+
Parameters:
+
    +
  • train_X (Tensor) – A n x (d + 1) or b x n x (d + 1) (batch mode) tensor +of training data. One of the columns should contain the task +features (see task_feature argument).

  • +
  • train_Y (Tensor) – A n x 1 or b x n x 1 (batch mode) tensor of training +observations.

  • +
  • task_feature (int) – The index of the task feature (-d <= task_feature <= d).

  • +
  • train_Yvar (Tensor | None) – An optional n or b x n (batch mode) tensor of observed +measurement noise. If None, we infer the noise. +Note that the inferred noise is common across all tasks.

  • +
  • mean_module (Module | None) – The mean function to be used. Defaults to ConstantMean.

  • +
  • covar_module (Module | None) – The module for computing the covariance matrix between +the non-task features. Defaults to RBFKernel.

  • +
  • likelihood (Likelihood | None) – A likelihood. The default is selected based on train_Yvar. +If train_Yvar is None, a standard GaussianLikelihood with inferred +noise level is used. Otherwise, a FixedNoiseGaussianLikelihood is used.

  • +
  • output_tasks (list[int] | None) – A list of task indices for which to compute model +outputs for. If omitted, return outputs for all task indices.

  • +
  • rank (int | None) – The rank to be used for the index kernel. If omitted, use a +full rank (i.e. number of tasks) kernel.

  • +
  • task_covar_prior (Prior | None) – A Prior on the task covariance matrix. Must operate +on p.s.d. matrices. A common prior for this is the LKJ prior.

  • +
  • all_tasks (list[int] | None) – By default, multi-task GPs infer the list of all tasks from +the task features in train_X. This is an experimental feature that +enables creation of multi-task GPs with tasks that don’t appear in the +training data. Note that when a task is not observed, the corresponding +task covariance will heavily depend on random initialization and may +behave unexpectedly.

  • +
  • outcome_transform (OutcomeTransform | _DefaultType | None) – An outcome transform that is applied to the +training data during instantiation and to the posterior during +inference (that is, the Posterior obtained by calling +.posterior on the model will be on the original scale). We use a +Standardize transform if no outcome_transform is specified. +Pass down None to use no outcome transform. NOTE: Standardization +should be applied in a stratified fashion, separately for each task.

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass.

  • +
+
+
+

Example

+
>>> X1, X2 = torch.rand(10, 2), torch.rand(20, 2)
+>>> i1, i2 = torch.zeros(10, 1), torch.ones(20, 1)
+>>> train_X = torch.cat([
+>>>     torch.cat([X1, i1], -1), torch.cat([X2, i2], -1),
+>>> ])
+>>> train_Y = torch.cat([f1(X1), f2(X2)]).unsqueeze(-1)
+>>> model = MultiTaskGP(train_X, train_Y, task_feature=-1)
+
+
+
+
+forward(x)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

MultivariateNormal

+
+
+
+
+
+classmethod get_all_tasks(train_X, task_feature, output_tasks=None)[source]
+
+
Parameters:
+
    +
  • train_X (Tensor)

  • +
  • task_feature (int)

  • +
  • output_tasks (list[int] | None)

  • +
+
+
Return type:
+

tuple[list[int], int, int]

+
+
+
+
+
+classmethod construct_inputs(training_data, task_feature, output_tasks=None, task_covar_prior=None, prior_config=None, rank=None)[source]
+

Construct Model keyword arguments from a dataset and other args.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset | MultiTaskDataset) – A SupervisedDataset or a MultiTaskDataset.

  • +
  • task_feature (int) – Column index of embedded task indicator features.

  • +
  • output_tasks (list[int] | None) – A list of task indices for which to compute model +outputs for. If omitted, return outputs for all task indices.

  • +
  • task_covar_prior (Prior | None) – A GPyTorch Prior object to use as prior on +the cross-task covariance matrix,

  • +
  • prior_config (dict | None) – Configuration for inter-task covariance prior. +Should only be used if task_covar_prior is not passed directly. Must +contain use_LKJ_prior indicator and should contain float value eta.

  • +
  • rank (int | None) – The rank of the cross-task covariance matrix.

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+class botorch.models.multitask.KroneckerMultiTaskGP(train_X, train_Y, likelihood=None, data_covar_module=None, task_covar_prior=None, rank=None, input_transform=None, outcome_transform=None, **kwargs)[source]
+

Bases: ExactGP, GPyTorchModel, FantasizeMixin

+

Multi-task GP with Kronecker structure, using an ICM kernel.

+

This model assumes the “block design” case, i.e., it requires that all tasks +are observed at all data points.

+

For posterior sampling, this model uses Matheron’s rule [Doucet2010sampl] to compute +the posterior over all tasks as in [Maddox2021bohdo] by exploiting Kronecker +structure.

+

When a multi-fidelity model has Kronecker structure, this means there is one +covariance kernel over the fidelity features (call it K_f) and another over +the rest of the input parameters (call it K_i), and the resulting covariance +across inputs and fidelities is given by the Kronecker product of the two +covariance matrices. This is equivalent to saying the covariance between +two input and feature pairs is given by

+
+
K((parameter_1, fidelity_1), (parameter_2, fidelity_2))

= K_f(fidelity_1, fidelity_2) * K_i(parameter_1, parameter_2).

+
+
+

Then the covariance matrix of n_i parameters and n_f fidelities can be +codified as a Kronecker product of an n_i x n_i matrix and an +n_f x n_f matrix, which is far more parsimonious than specifying the +whole (n_i * n_f) x (n_i * n_f) covariance matrix.

+

Example

+
>>> train_X = torch.rand(10, 2)
+>>> train_Y = torch.cat([f_1(X), f_2(X)], dim=-1)
+>>> model = KroneckerMultiTaskGP(train_X, train_Y)
+
+
+
+
Parameters:
+
    +
  • train_X (Tensor) – A batch_shape x n x d tensor of training features.

  • +
  • train_Y (Tensor) – A batch_shape x n x m tensor of training observations.

  • +
  • likelihood (MultitaskGaussianLikelihood | None) – A MultitaskGaussianLikelihood. If omitted, uses a +MultitaskGaussianLikelihood with a GammaPrior(1.1, 0.05) +noise prior.

  • +
  • data_covar_module (Module | None) – The module computing the covariance (Kernel) matrix +in data space. If omitted, uses an RBFKernel.

  • +
  • task_covar_prior (Prior | None) – A Prior on the task covariance matrix. Must operate +on p.s.d. matrices. A common prior for this is the LKJ prior. If +omitted, uses LKJCovariancePrior with eta parameter as specified +in the keyword arguments (if not specified, use eta=1.5).

  • +
  • rank (int | None) – The rank of the ICM kernel. If omitted, use a full rank kernel.

  • +
  • kwargs (Any) – Additional arguments to override default settings of priors, +including: +- eta: The eta parameter on the default LKJ task_covar_prior. +A value of 1.0 is uninformative, values <1.0 favor stronger +correlations (in magnitude), correlations vanish as eta -> inf. +- sd_prior: A scalar prior over nonnegative numbers, which is used +for the default LKJCovariancePrior task_covar_prior. +- likelihood_rank: The rank of the task covariance matrix to fit. +Defaults to 0 (which corresponds to a diagonal covariance matrix).

  • +
  • input_transform (InputTransform | None)

  • +
  • outcome_transform (OutcomeTransform | None)

  • +
+
+
+
+
+forward(X)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

MultitaskMultivariateNormal

+
+
+
+
+
+property train_full_covar
+
+
+
+property predictive_mean_cache
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X (Tensor) – A (batch_shape) x q x d-dim Tensor, where d is the dimension +of the feature space and q is the number of points considered +jointly.

  • +
  • observation_noise (bool | Tensor) – If True, add the observation noise from the +likelihood to the posterior. If a Tensor, use it directly as the +observation noise (must be of shape (batch_shape) x q). It is +assumed to be in the outcome-transformed space if an outcome +transform is used.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
  • output_indices (list[int] | None)

  • +
+
+
Returns:
+

A GPyTorchPosterior object, representing a batch of b joint +distributions over q points. Includes observation noise if +specified.

+
+
Return type:
+

MultitaskGPPosterior

+
+
+
+
+
+train(val=True, *args, **kwargs)[source]
+

Put the model in train mode. Reverts to the original inputs if in train +mode (mode=True) or sets transformed inputs if in eval mode (mode=False).

+
+
Parameters:
+

mode – A boolean denoting whether to put in train or eval mode. +If False, model is put in eval mode.

+
+
+
+
+
+
+

Higher Order GP Models

+

References

+
+
+[Zhe2019hogp] +

S. Zhe, W. Xing, and R. M. Kirby. Scalable high-order gaussian process regression. +Proceedings of Machine Learning Research, volume 89, Apr 2019.

+
+
+
+
+class botorch.models.higher_order_gp.FlattenedStandardize(output_shape, batch_shape=None, min_stdv=1e-08)[source]
+

Bases: Standardize

+

Standardize outcomes in a structured multi-output settings by reshaping the +batched output dimensions to be a vector. Specifically, an output dimension +of [a x b x c] will be squeezed to be a vector of [a * b * c].

+
+
Parameters:
+
    +
  • output_shape (torch.Size) – A n x output_shape-dim tensor of training targets.

  • +
  • batch_shape (torch.Size | None) – The batch_shape of the training targets.

  • +
  • min_stddv – The minimum standard deviation for which to perform +standardization (if lower, only de-mean the data).

  • +
  • min_stdv (float)

  • +
+
+
+
+
+forward(Y, Yvar=None)[source]
+

Standardize outcomes.

+

If the module is in train mode, this updates the module state (i.e. the +mean/std normalizing constants). If the module is in eval mode, simply +applies the normalization using the module state.

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of observation noises +associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The transformed outcome observations.

  • +
  • The transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the transformed outcomes

+
+
+
+
+
+untransform(Y, Yvar=None)[source]
+

Un-standardize outcomes.

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of standardized targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of standardized observation +noises associated with the targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The un-standardized outcome observations.

  • +
  • The un-standardized observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the un-standardized outcomes

+
+
+
+
+
+untransform_posterior(posterior)[source]
+

Un-standardize the posterior.

+
+
Parameters:
+

posterior (HigherOrderGPPosterior) – A posterior in the standardized space.

+
+
Returns:
+

The un-standardized posterior. If the input posterior is a +GPyTorchPosterior, return a GPyTorchPosterior. Otherwise, return a +TransformedPosterior.

+
+
Return type:
+

TransformedPosterior

+
+
+
+
+
+
+class botorch.models.higher_order_gp.HigherOrderGP(train_X, train_Y, likelihood=None, covar_modules=None, num_latent_dims=None, learn_latent_pars=True, latent_init='default', outcome_transform=<class 'botorch.utils.types.DEFAULT'>, input_transform=None)[source]
+

Bases: BatchedMultiOutputGPyTorchModel, ExactGP, FantasizeMixin

+

A model for high-dimensional output regression.

+

As described in [Zhe2019hogp]. “Higher-order” means that the predictions +are matrices (tensors) with at least two dimensions, such as images or +grids of images, or measurements taken from a region of at least two +dimensions. +The posterior uses Matheron’s rule [Doucet2010sampl] +as described in [Maddox2021bohdo].

+

HigherOrderGP differs from a “vector” multi-output model in that it uses +Kronecker algebra to obtain parsimonious covariance matrices for these +outputs (see KroneckerMultiTaskGP for more information). For example, +imagine a 10 x 20 x 30 grid of images. If we were to vectorize the +resulting 6,000 data points in order to use them in a non-higher-order GP, +they would have a 6,000 x 6,000 covariance matrix, with 36 million entries. +The Kronecker structure allows representing this as a product of 10x10, +20x20, and 30x30 covariance matrices, with only 1,400 entries.

+

NOTE: This model requires the use of specialized Kronecker solves in +linear operator, which are disabled by default in BoTorch. These are enabled +by default in the HigherOrderGP.posterior call. However, they need to be +manually enabled by the user during model fitting.

+

Example

+
>>> from linear_operator.settings import _fast_solves
+>>> model = SingleTaskGP(train_X, train_Y)
+>>> mll = ExactMarginalLogLikelihood(model.likelihood, model)
+>>> with _fast_solves(True):
+>>>     fit_gpytorch_mll_torch(mll)
+>>> samples = model.posterior(test_X).rsample()
+
+
+
+
Parameters:
+
    +
  • train_X (Tensor) – A batch_shape x n x d-dim tensor of training inputs.

  • +
  • train_Y (Tensor) – A batch_shape x n x output_shape-dim tensor of training targets.

  • +
  • likelihood (Likelihood | None) – Gaussian likelihood for the model.

  • +
  • covar_modules (list[Kernel] | None) – List of kernels for each output structure.

  • +
  • num_latent_dims (list[int] | None) – Sizes for the latent dimensions.

  • +
  • learn_latent_pars (bool) – If true, learn the latent parameters.

  • +
  • latent_init (str) – [default or gp] how to initialize the latent parameters.

  • +
  • outcome_transform (OutcomeTransform | _DefaultType | None)

  • +
  • input_transform (InputTransform | None)

  • +
+
+
+
+
+forward(X)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

MultivariateNormal

+
+
+
+
+
+get_fantasy_model(inputs, targets, **kwargs)[source]
+

Returns a new GP model that incorporates the specified inputs and targets as new training data.

+

Using this method is more efficient than updating with set_train_data when the number of inputs is relatively +small, because any computed test-time caches will be updated in linear time rather than computed from scratch.

+
+

Note

+

If targets is a batch (e.g. b x m), then the GP returned from this method will be a batch mode GP. +If inputs is of the same (or lesser) dimension as targets, then it is assumed that the fantasy points +are the same for each target batch.

+
+
+
Parameters:
+
    +
  • inputs (torch.Tensor) – (b1 x … x bk x m x d or f x b1 x … x bk x m x d) Locations of fantasy +observations.

  • +
  • targets (torch.Tensor) – (b1 x … x bk x m or f x b1 x … x bk x m) Labels of fantasy observations.

  • +
+
+
Returns:
+

An ExactGP model with n + m training examples, where the m fantasy examples have been added +and all test-time caches have been updated.

+
+
Return type:
+

ExactGP

+
+
+
+
+
+condition_on_observations(X, Y, noise=None, **kwargs)[source]
+

Condition the model on new observations.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, m is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • Y (Tensor) – A batch_shape’ x n’ x m_d-dim Tensor, where m_d is the shaping +of the model outputs, n’ is the number of points per batch, and +batch_shape’ is the batch shape of the observations. +batch_shape’ must be broadcastable to batch_shape using +standard broadcasting semantics. If Y has fewer batch dimensions +than X, its is assumed that the missing batch dimensions are +the same for all Y.

  • +
  • noise (Tensor | None) – If not None, a tensor of the same shape as Y representing +the noise variance associated with each observation.

  • +
  • kwargs (Any) – Passed to condition_on_observations.

  • +
+
+
Returns:
+

A BatchedMultiOutputGPyTorchModel object of the same type with +n + n’ training examples, representing the original model +conditioned on the new observations (X, Y) (and possibly noise +observations passed in via kwargs).

+
+
Return type:
+

HigherOrderGP

+
+
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X (Tensor) – A (batch_shape) x q x d-dim Tensor, where d is the dimension +of the feature space and q is the number of points considered +jointly.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior (if the model is multi-output). +Can be used to speed up computation if only a subset of the +model’s outputs are required for optimization. If omitted, +computes the posterior over all model outputs.

  • +
  • observation_noise (bool | Tensor) – If True, add the observation noise from the +likelihood to the posterior. If a Tensor, use it directly as the +observation noise (must be of shape (batch_shape) x q x m).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

A GPyTorchPosterior object, representing batch_shape joint +distributions over q points and the outputs selected by +output_indices each. Includes observation noise if specified.

+
+
Return type:
+

GPyTorchPosterior

+
+
+
+
+
+make_posterior_variances(joint_covariance_matrix)[source]
+

Computes the posterior variances given the data points X. As currently +implemented, it computes another forwards call with the stacked data to get out +the joint covariance across all data points.

+
+
Parameters:
+

joint_covariance_matrix (LinearOperator)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Pairwise GP Models

+

Preference Learning with Gaussian Process

+
+
+[Chu2005preference] +(1,2,3) +

Wei Chu, and Zoubin Ghahramani. Preference learning with Gaussian processes. +Proceedings of the 22nd international conference on Machine learning. 2005.

+
+
+[Brochu2010tutorial] +

Eric Brochu, Vlad M. Cora, and Nando De Freitas. +A tutorial on Bayesian optimization of expensive cost functions, +with application to active user modeling and hierarchical reinforcement learning. +arXiv preprint arXiv:1012.2599 (2010).

+
+
+
+
+class botorch.models.pairwise_gp.PairwiseGP(datapoints, comparisons, likelihood=None, covar_module=None, input_transform=None, *, jitter=1e-06, xtol=None, consolidate_rtol=0.0, consolidate_atol=0.0001, maxfev=None)[source]
+

Bases: Model, GP, FantasizeMixin

+

Probit GP for preference learning with Laplace approximation

+

A probit-likelihood GP that learns via pairwise comparison data, using a +Laplace approximation of the posterior of the estimated utility values. By +default it uses a scaled RBF kernel.

+

Implementation is based on [Chu2005preference]. +Also see [Brochu2010tutorial] for additional reference.

+

Note that in [Chu2005preference] the likelihood of a pairwise comparison +is \(\left(\frac{f(x_1) - f(x_2)}{\sqrt{2}\sigma}\right)\), i.e. a scale is +used in the denominator. To maintain consistency with usage of kernels +elsewhere in BoTorch, we instead do not include \(\sigma\) in the code +(implicitly setting it to 1) and use ScaleKernel to scale the function.

+

In the example below, the user/decision maker has stated that they prefer +the first item over the second item and the third item over the second item, +generating comparisons [0, 1] and [2, 1]. +.. rubric:: Example

+
>>> from botorch.models import PairwiseGP
+>>> import torch
+>>> datapoints = torch.Tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+>>> comparisons = torch.Tensor([[0, 1], [2, 1]])
+>>> model = PairwiseGP(datapoints, comparisons)
+
+
+
+
Parameters:
+
    +
  • datapoints (Tensor | None) – Either None or a batch_shape x n x d tensor of +training features. If either datapoints or comparisons is +None, construct a prior-only model.

  • +
  • comparisons (Tensor | None) – Either None or a batch_shape x m x 2 tensor of +training comparisons; comparisons[i] is a noisy indicator +suggesting the utility value of comparisons[i, 0]-th is greater +than comparisons[i, 1]-th. If either comparisons or +datapoints is None, construct a prior-only model.

  • +
  • likelihood (PairwiseLikelihood | None) – A PairwiseLikelihood.

  • +
  • covar_module (ScaleKernel | None) – Covariance module.

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass.

  • +
  • jitter (float) – Value added to diagonal for numerical stability in +psd_safe_cholesky.

  • +
  • xtol (float | None) – Stopping creteria in scipy.optimize.fsolve used to find f_map +in PairwiseGP._update. If None, default behavior is handled by +PairwiseGP._update.

  • +
  • consolidate_rtol (float) – rtol passed to consolidate_duplicates.

  • +
  • consolidate_atol (float) – atol passed to consolidate_duplicates.

  • +
  • maxfev (int | None) – The maximum number of calls to the function in +scipy.optimize.fsolve. If None, default behavior is handled by +PairwiseGP._update.

  • +
+
+
+
+
+property datapoints: Tensor
+

Alias for consolidated datapoints

+
+
+
+property comparisons: Tensor
+

Alias for consolidated comparisons

+
+
+
+property unconsolidated_utility: Tensor
+

Utility of the unconsolidated datapoints

+
+
+
+property num_outputs: int
+

The number of outputs of the model.

+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective, independent of the internal +representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). +For a model with m outputs, a test_batch_shape x q x d-shaped input X +to the posterior method returns a Posterior object over an output of +shape broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+classmethod construct_inputs(training_data)[source]
+

Construct Model keyword arguments from a RankingDataset.

+
+
Parameters:
+

training_data (SupervisedDataset) – A RankingDataset, with attributes train_X, +train_Y, and, optionally, train_Yvar.

+
+
Returns:
+

A dict of keyword arguments that can be used to initialize a +PairwiseGP, including datapoints and comparisons.

+
+
Return type:
+

dict[str, Tensor]

+
+
+
+
+
+set_train_data(datapoints=None, comparisons=None, strict=False, update_model=True)[source]
+

Set datapoints and comparisons and update model properties if needed

+
+
Parameters:
+
    +
  • datapoints (Tensor | None) – Either None or a batch_shape x n x d dimension +tensor X. If there are input transformations, assume the +datapoints are not transformed. If either datapoints or +comparisons is None, construct a prior-only model.

  • +
  • comparisons (Tensor | None) – Either None or a tensor of size batch_shape x m x +2. (i, j) means f_i is preferred over f_j. If either +comparisons or datapoints is None, construct a prior-only +model.

  • +
  • strict (bool) – strict argument as in gpytorch.models.exact_gp for compatibility +when using fit_gpytorch_mll with input_transform.

  • +
  • update_model (bool) – True if we want to refit the model (see _update) after +re-setting the data.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+load_state_dict(state_dict, strict=False)[source]
+

Removes data related buffers from the state_dict and calls +super().load_state_dict with strict=False.

+
+
Parameters:
+
    +
  • state_dict (dict[str, Tensor]) – The state dict.

  • +
  • strict (bool) – Boolean specifying whether or not given and instance-bound +state_dicts should have identical keys. Only implemented for +strict=False since buffers will filters out when calling +_load_from_state_dict.

  • +
+
+
Returns:
+

A named tuple _IncompatibleKeys, containing the missing_keys +and unexpected_keys.

+
+
Return type:
+

_IncompatibleKeys

+
+
+
+
+
+forward(datapoints)[source]
+

Calculate a posterior or prior prediction.

+

During training mode, forward implemented solely for gradient-based +hyperparam opt. Essentially what it does is to re-calculate the utility +f using its analytical form at f_map so that we are able to obtain +gradients of the hyperparameters.

+
+
Parameters:
+

datapoints (Tensor) – A batch_shape x n x d Tensor, +should be the same as self.datapoints during training

+
+
Returns:
+

    +
  1. Posterior centered at MAP points for training data (training mode)

  2. +
  3. Prior predictions (prior mode)

  4. +
  5. Predictive posterior (eval mode)

  6. +
+

+
+
Return type:
+

A MultivariateNormal object, being one of the followings

+
+
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x d-dim Tensor, where d is the dimension +of the feature space and q is the number of points considered jointly.

  • +
  • output_indices (list[int] | None) – As defined in parent Model class, not used for this model.

  • +
  • observation_noise (bool) – Ignored (since noise is not identifiable from scale +in probit models).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

+
A Posterior object, representing joint

distributions over q points.

+
+
+

+
+
Return type:
+

Posterior

+
+
+
+
+
+condition_on_observations(X, Y)[source]
+

Condition the model on new observations.

+

Note that unlike other BoTorch models, PairwiseGP requires Y to be +pairwise comparisons.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n x d dimension tensor X

  • +
  • Y (Tensor) – A tensor of size batch_shape x m x 2. (i, j) means +f_i is preferred over f_j

  • +
  • kwargs – Not used.

  • +
+
+
Returns:
+

A (deepcopied) Model object of the same type, representing the +original model conditioned on the new observations (X, Y).

+
+
Return type:
+

Model

+
+
+
+
+
+
+class botorch.models.pairwise_gp.PairwiseLaplaceMarginalLogLikelihood(likelihood, model)[source]
+

Bases: MarginalLogLikelihood

+

Laplace-approximated marginal log likelihood/evidence for PairwiseGP

+

See (12) from [Chu2005preference].

+
+
Parameters:
+
    +
  • likelihood – Used as in args to GPyTorch MarginalLogLikelihood

  • +
  • model (GP) – Used as in args to GPyTorch MarginalLogLikelihood

  • +
+
+
+
+
+forward(post, comp)[source]
+

Calculate approximated log evidence, i.e., log(P(D|theta))

+

Note that post will be based on the consolidated/deduped datapoints for +numerical stability, but comp will still be the unconsolidated comparisons +so that it’s still compatible with fit_gpytorch_*.

+
+
Parameters:
+
    +
  • post (Posterior) – training posterior distribution from self.model (after consolidation)

  • +
  • comp (Tensor) – Comparisons pairs (before consolidation)

  • +
+
+
Returns:
+

The approximated evidence, i.e., the marginal log likelihood

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Contextual GP Models with Aggregate Rewards

+
+
+class botorch.models.contextual.SACGP(train_X, train_Y, train_Yvar, decomposition)[source]
+

Bases: SingleTaskGP

+

A GP using a Structural Additive Contextual(SAC) kernel.

+
+
Parameters:
+
    +
  • train_X (Tensor) – (n x d) X training data.

  • +
  • train_Y (Tensor) – (n x 1) Y training data.

  • +
  • train_Yvar (Tensor | None) – (n x 1) Noise variances of each training Y. If None, +we use an inferred noise likelihood.

  • +
  • decomposition (dict[str, list[int]]) – Keys are context names. Values are the indexes of +parameters belong to the context. The parameter indexes are in +the same order across contexts.

  • +
+
+
+
+
+classmethod construct_inputs(training_data, decomposition)[source]
+

Construct Model keyword arguments from a dict of SupervisedDataset.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset) – A SupervisedDataset containing the training data.

  • +
  • decomposition (dict[str, list[int]]) – Dictionary of context names and their indexes of the +corresponding active context parameters.

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+class botorch.models.contextual.LCEAGP(train_X, train_Y, train_Yvar, decomposition, train_embedding=True, cat_feature_dict=None, embs_feature_dict=None, embs_dim_list=None, context_weight_dict=None)[source]
+

Bases: SingleTaskGP

+

A GP using a Latent Context Embedding Additive (LCE-A) Kernel.

+

Note that the model does not support batch training. Input training +data sets should have dim = 2.

+
+
Parameters:
+
    +
  • train_X (Tensor) – (n x d) X training data.

  • +
  • train_Y (Tensor) – (n x 1) Y training data.

  • +
  • train_Yvar (Tensor | None) – (n x 1) Noise variance of Y. If None, +we use an inferred noise likelihood.

  • +
  • decomposition (dict[str, list[int]]) – Keys are context names. Values are the indexes of +parameters belong to the context.

  • +
  • train_embedding (bool) – Whether to train the embedding layer or not. If False, +the model will use pre-trained embeddings in embs_feature_dict.

  • +
  • cat_feature_dict (dict | None) – Keys are context names and values are list of categorical +features i.e. {“context_name” : [cat_0, …, cat_k]}, where k is the +number of categorical variables. If None, we use context names in the +decomposition as the only categorical feature, i.e., k = 1.

  • +
  • embs_feature_dict (dict | None) – Pre-trained continuous embedding features of each +context.

  • +
  • embs_dim_list (list[int] | None) – Embedding dimension for each categorical variable. The length +equals the number of categorical features k. If None, the embedding +dimension is set to 1 for each categorical variable.

  • +
  • context_weight_dict (dict | None) – Known population weights of each context.

  • +
+
+
+
+
+classmethod construct_inputs(training_data, decomposition, train_embedding=True, cat_feature_dict=None, embs_feature_dict=None, embs_dim_list=None, context_weight_dict=None)[source]
+

Construct Model keyword arguments from a dict of SupervisedDataset.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset) – A SupervisedDataset containing the training data.

  • +
  • decomposition (dict[str, list[str]]) – Dictionary of context names and the names of the +corresponding active context parameters.

  • +
  • train_embedding (bool) – Whether to train the embedding layer or not.

  • +
  • cat_feature_dict (dict | None) – Keys are context names and values are list of categorical +features i.e. {“context_name” : [cat_0, …, cat_k]}, where k is the +number of categorical variables. If None, we use context names in the +decomposition as the only categorical feature, i.e., k = 1.

  • +
  • embs_feature_dict (dict | None) – Pre-trained continuous embedding features of each +context.

  • +
  • embs_dim_list (list[int] | None) – Embedding dimension for each categorical variable. The length +equals the number of categorical features k. If None, the embedding +dimension is set to 1 for each categorical variable.

  • +
  • context_weight_dict (dict | None) – Known population weights of each context.

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+

Contextual GP Models with Context Rewards

+

References

+
+
+[Feng2020HDCPS] +

Q. Feng, B. Latham, H. Mao and E. Backshy. High-Dimensional Contextual Policy +Search with Unknown Context Rewards using Bayesian Optimization. +Advances in Neural Information Processing Systems 33, NeurIPS 2020.

+
+
+
+
+class botorch.models.contextual_multioutput.LCEMGP(train_X, train_Y, task_feature, train_Yvar=None, mean_module=None, covar_module=None, likelihood=None, context_cat_feature=None, context_emb_feature=None, embs_dim_list=None, output_tasks=None, all_tasks=None, outcome_transform=<class 'botorch.utils.types.DEFAULT'>, input_transform=None)[source]
+

Bases: MultiTaskGP

+

The Multi-Task GP with the latent context embedding multioutput (LCE-M) +kernel. See [Feng2020HDCPS] for a reference on the model and its use in Bayesian +optimization.

+
+
Parameters:
+
    +
  • train_X (Tensor) – (n x d) X training data.

  • +
  • train_Y (Tensor) – (n x 1) Y training data.

  • +
  • task_feature (int) – Column index of train_X to get context indices.

  • +
  • train_Yvar (Tensor | None) – An optional (n x 1) tensor of observed variances of each +training Y. If None, we infer the noise. Note that the inferred noise +is common across all tasks.

  • +
  • mean_module (Module | None) – The mean function to be used. Defaults to ConstantMean.

  • +
  • covar_module (Module | None) – The module for computing the covariance matrix between +the non-task features. Defaults to RBFKernel.

  • +
  • likelihood (Likelihood | None) – A likelihood. The default is selected based on train_Yvar. +If train_Yvar is None, a standard GaussianLikelihood with inferred +noise level is used. Otherwise, a FixedNoiseGaussianLikelihood is used.

  • +
  • context_cat_feature (Tensor | None) – (n_contexts x k) one-hot encoded context +features. Rows are ordered by context indices, where k is the +number of categorical variables. If None, task indices will +be used and k = 1.

  • +
  • context_emb_feature (Tensor | None) – (n_contexts x m) pre-given continuous +embedding features. Rows are ordered by context indices.

  • +
  • embs_dim_list (list[int] | None) – Embedding dimension for each categorical variable. +The length equals k. If None, the embedding dimension is set to 1 +for each categorical variable.

  • +
  • output_tasks (list[int] | None) – A list of task indices for which to compute model +outputs for. If omitted, return outputs for all task indices.

  • +
  • all_tasks (list[int] | None) – By default, multi-task GPs infer the list of all tasks from +the task features in train_X. This is an experimental feature that +enables creation of multi-task GPs with tasks that don’t appear in the +training data. Note that when a task is not observed, the corresponding +task covariance will heavily depend on random initialization and may +behave unexpectedly.

  • +
  • outcome_transform (OutcomeTransform | _DefaultType | None) – An outcome transform that is applied to the +training data during instantiation and to the posterior during +inference (that is, the Posterior obtained by calling +.posterior on the model will be on the original scale). We use a +Standardize transform if no outcome_transform is specified. +Pass down None to use no outcome transform.

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass.

  • +
+
+
+
+
+task_covar_module(task_idcs)[source]
+

Compute the task covariance matrix for a given tensor of +task / context indices.

+
+
Parameters:
+

task_idcs (Tensor) – Task index tensor of shape (n x 1) or (b x n x 1).

+
+
Returns:
+

Task covariance matrix of shape (b x n x n).

+
+
Return type:
+

Tensor

+
+
+
+
+
+classmethod construct_inputs(training_data, task_feature, output_tasks=None, context_cat_feature=None, context_emb_feature=None, embs_dim_list=None, **kwargs)[source]
+

Construct Model keyword arguments from a dataset and other args.

+
+
Parameters:
+
    +
  • training_data (SupervisedDataset | MultiTaskDataset) – A SupervisedDataset or a MultiTaskDataset.

  • +
  • task_feature (int) – Column index of embedded task indicator features.

  • +
  • output_tasks (list[int] | None) – A list of task indices for which to compute model +outputs for. If omitted, return outputs for all task indices.

  • +
  • context_cat_feature (Tensor | None) – (n_contexts x k) one-hot encoded context +features. Rows are ordered by context indices, where k is the +number of categorical variables. If None, task indices will +be used and k = 1.

  • +
  • context_emb_feature (Tensor | None) – (n_contexts x m) pre-given continuous +embedding features. Rows are ordered by context indices.

  • +
  • embs_dim_list (list[int] | None) – Embedding dimension for each categorical variable. +The length equals k. If None, the embedding dimension is set to 1 +for each categorical variable.

  • +
+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+

Variational GP Models

+

References

+
+
+[burt2020svgp] +(1,2,3,4) +

David R. Burt and Carl Edward Rasmussen and Mark van der Wilk, +Convergence of Sparse Variational Inference in Gaussian Process Regression, +Journal of Machine Learning Research, 2020, +http://jmlr.org/papers/v21/19-1015.html.

+
+
+[hensman2013svgp] +

James Hensman and Nicolo Fusi and Neil D. Lawrence, Gaussian Processes +for Big Data, Proceedings of the 29th Conference on Uncertainty in +Artificial Intelligence, 2013, https://arxiv.org/abs/1309.6835.

+
+
+[moss2023ipa] +(1,2,3,4) +

Henry B. Moss and Sebastian W. Ober and Victor Picheny, +Inducing Point Allocation for Sparse Gaussian Processes +in High-Throughput Bayesian Optimization,Proceedings of +the 25th International Conference on Artificial Intelligence +and Statistics, 2023, https://arxiv.org/pdf/2301.10123.pdf.

+
+
+
+
+class botorch.models.approximate_gp.ApproximateGPyTorchModel(model=None, likelihood=None, num_outputs=1, *args, **kwargs)[source]
+

Bases: GPyTorchModel

+

Botorch wrapper class for various (variational) approximate GP models in +GPyTorch.

+

This can either include stochastic variational GPs (SVGPs) or +variational implementations of weight space approximate GPs.

+
+
Parameters:
+
    +
  • model (ApproximateGP | None) – Instance of gpytorch.approximate GP models. If omitted, +constructs a _SingleTaskVariationalGP.

  • +
  • likelihood (Likelihood | None) – Instance of a GPyTorch likelihood. If omitted, uses a +either a GaussianLikelihood (if num_outputs=1) or a +MultitaskGaussianLikelihood`(if `num_outputs>1).

  • +
  • num_outputs (int) – Number of outputs expected for the GP model.

  • +
  • args – Optional positional arguments passed to the +_SingleTaskVariationalGP constructor if no model is provided.

  • +
  • kwargs – Optional keyword arguments passed to the +_SingleTaskVariationalGP constructor if no model is provided.

  • +
+
+
+
+
+property num_outputs
+

The number of outputs of the model.

+
+
+
+eval()[source]
+

Puts the model in eval mode.

+
+
Return type:
+

Self

+
+
+
+
+
+train(mode=True)[source]
+

Put the model in train mode.

+
+
Parameters:
+

mode (bool) – A boolean denoting whether to put in train or eval mode. +If False, model is put in eval mode.

+
+
Return type:
+

Self

+
+
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X – A (batch_shape) x q x d-dim Tensor, where d is the dimension +of the feature space and q is the number of points considered +jointly.

  • +
  • observation_noise (bool) – If True, add the observation noise from the +likelihood to the posterior. If a Tensor, use it directly as the +observation noise (must be of shape (batch_shape) x q). It is +assumed to be in the outcome-transformed space if an outcome +transform is used.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
  • output_indices (list[int] | None)

  • +
+
+
Returns:
+

A GPyTorchPosterior object, representing a batch of b joint +distributions over q points. Includes observation noise if +specified.

+
+
Return type:
+

GPyTorchPosterior

+
+
+
+
+
+forward(X)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Return type:
+

MultivariateNormal

+
+
+
+
+
+
+class botorch.models.approximate_gp.SingleTaskVariationalGP(train_X, train_Y=None, likelihood=None, num_outputs=1, learn_inducing_points=True, covar_module=None, mean_module=None, variational_distribution=None, variational_strategy=<class 'gpytorch.variational.variational_strategy.VariationalStrategy'>, inducing_points=None, inducing_point_allocator=None, outcome_transform=None, input_transform=None)[source]
+

Bases: ApproximateGPyTorchModel

+

A single-task variational GP model following [hensman2013svgp].

+

By default, the inducing points are initialized though the +GreedyVarianceReduction of [burt2020svgp], which is known to be +effective for building globally accurate models. However, custom +inducing point allocators designed for specific down-stream tasks can also be +provided (see [moss2023ipa] for details), e.g. GreedyImprovementReduction +when the goal is to build a model suitable for standard BO.

+

A single-task variational GP using relatively strong priors on the Kernel +hyperparameters, which work best when covariates are normalized to the unit +cube and outcomes are standardized (zero mean, unit variance).

+

This model works in batch mode (each batch having its own hyperparameters). +When the training observations include multiple outputs, this model will use +batching to model outputs independently. However, batches of multi-output models +are not supported at this time, if you need to use those, please use a +ModelListGP.

+

Use this model if you have a lot of data or if your responses are non-Gaussian.

+

To train this model, you should use gpytorch.mlls.VariationalELBO and not +the exact marginal log likelihood.

+

Example

+
>>> import torch
+>>> from botorch.models import SingleTaskVariationalGP
+>>> from gpytorch.mlls import VariationalELBO
+>>>
+>>> train_X = torch.rand(20, 2)
+>>> model = SingleTaskVariationalGP(train_X)
+>>> mll = VariationalELBO(
+>>>     model.likelihood, model.model, num_data=train_X.shape[-2]
+>>> )
+
+
+
+
Parameters:
+
    +
  • train_X (Tensor) – Training inputs (due to the ability of the SVGP to sub-sample +this does not have to be all of the training inputs).

  • +
  • train_Y (Tensor | None) – Training targets (optional).

  • +
  • likelihood (Likelihood | None) – Instance of a GPyTorch likelihood. If omitted, uses a +either a GaussianLikelihood (if num_outputs=1) or a +MultitaskGaussianLikelihood`(if `num_outputs>1).

  • +
  • num_outputs (int) – Number of output responses per input (default: 1).

  • +
  • learn_inducing_points (bool) – If True, the inducing point locations are learned +jointly with the other model parameters.

  • +
  • covar_module (Kernel | None) – Kernel function. If omitted, uses an RBFKernel.

  • +
  • mean_module (Mean | None) – Mean of GP model. If omitted, uses a ConstantMean.

  • +
  • variational_distribution (_VariationalDistribution | None) – Type of variational distribution to use +(default: CholeskyVariationalDistribution), the properties of the +variational distribution will encourage scalability or ease of +optimization.

  • +
  • variational_strategy (type[_VariationalStrategy]) – Type of variational strategy to use (default: +VariationalStrategy). The default setting uses “whitening” of the +variational distribution to make training easier.

  • +
  • inducing_points (Tensor | int | None) – The number or specific locations of the inducing points.

  • +
  • inducing_point_allocator (InducingPointAllocator | None) – The InducingPointAllocator used to +initialize the inducing point locations. If omitted, +uses GreedyVarianceReduction.

  • +
  • outcome_transform (OutcomeTransform | None) – An outcome transform that is applied to the training +data during instantiation and to the posterior during inference. +NOTE: If this model is trained in minibatches, an outcome transform +with learnable parameters (such as Standardize) would update its +parameters for each minibatch, which is undesirable. If you do intend +to train in minibatches, we recommend you not use an outcome transform +and instead pre-transform your whole data set before fitting the model.

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass. +NOTE: If this model is trained in minibatches, an input transform +with learnable parameters (such as Normalize) would update its +parameters for each minibatch, which is undesirable. If you do intend +to train in minibatches, we recommend you not use an input transform +and instead pre-transform your whole data set before fitting the model.

  • +
+
+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective. For a model with m +outputs, a test_batch_shape x q x d-shaped input X to the posterior +method returns a Posterior object over an output of shape +broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+init_inducing_points(inputs)[source]
+

Reinitialize the inducing point locations in-place with the current kernel +applied to inputs through the model’s inducing point allocation strategy. +The variational distribution and variational strategy caches are reset.

+
+
Parameters:
+

inputs (Tensor) – (*batch_shape, n, d)-dim input data tensor.

+
+
Returns:
+

(*batch_shape, m, d)-dim tensor of selected inducing point locations.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Fully Bayesian GP Models

+

Gaussian Process Regression models with fully Bayesian inference.

+

Fully Bayesian models use Bayesian inference over model hyperparameters, such +as lengthscales and noise variance, learning a posterior distribution for the +hyperparameters using the No-U-Turn-Sampler (NUTS). This is followed by +sampling a small set of hyperparameters (often ~16) from the posterior +that we will use for model predictions and for computing acquisition function +values. By contrast, our “standard” models (e.g. +SingleTaskGP) learn only a single best value for each hyperparameter using +MAP. The fully Bayesian method generally results in a better and more +well-calibrated model, but is more computationally intensive. For a full +description, see [Eriksson2021saasbo].

+

We use a lightweight PyTorch implementation of a Matern-5/2 kernel as there are +some performance issues with running NUTS on top of standard GPyTorch models. +The resulting hyperparameter samples are loaded into a batched GPyTorch model +after fitting.

+

References:

+
+
+[Eriksson2021saasbo] +(1,2,3) +

D. Eriksson, M. Jankowiak. High-Dimensional Bayesian Optimization +with Sparse Axis-Aligned Subspaces. Proceedings of the Thirty- +Seventh Conference on Uncertainty in Artificial Intelligence, 2021.

+
+
+
+
+botorch.models.fully_bayesian.matern52_kernel(X, lengthscale)[source]
+

Matern-5/2 kernel.

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • lengthscale (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.models.fully_bayesian.compute_dists(X, lengthscale)[source]
+

Compute kernel distances.

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • lengthscale (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.models.fully_bayesian.reshape_and_detach(target, new_value)[source]
+

Detach and reshape new_value to match target.

+
+
Parameters:
+
    +
  • target (Tensor)

  • +
  • new_value (Tensor)

  • +
+
+
Return type:
+

None

+
+
+
+
+
+class botorch.models.fully_bayesian.PyroModel[source]
+

Bases: object

+

Base class for a Pyro model; used to assist in learning hyperparameters.

+

This class and its subclasses are not a standard BoTorch models; instead +the subclasses are used as inputs to a SaasFullyBayesianSingleTaskGP, +which should then have its hyperparameters fit with +fit_fully_bayesian_model_nuts. (By default, its subclass SaasPyroModel +is used). A PyroModel’s sample method should specify lightweight +PyTorch functionality, which will be used for fast model fitting with NUTS. +The utility of PyroModel is in enabling fast fitting with NUTS, since we +would otherwise need to use GPyTorch, which is computationally infeasible +in combination with Pyro.

+
+
+set_inputs(train_X, train_Y, train_Yvar=None)[source]
+

Set the training data.

+
+
Parameters:
+
    +
  • train_X (Tensor) – Training inputs (n x d)

  • +
  • train_Y (Tensor) – Training targets (n x 1)

  • +
  • train_Yvar (Tensor | None) – Observed noise variance (n x 1). Inferred if None.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+abstract sample()[source]
+

Sample from the model.

+
+
Return type:
+

None

+
+
+
+
+
+abstract postprocess_mcmc_samples(mcmc_samples)[source]
+

Post-process the final MCMC samples.

+
+
Parameters:
+

mcmc_samples (dict[str, Tensor])

+
+
Return type:
+

dict[str, Tensor]

+
+
+
+
+
+abstract load_mcmc_samples(mcmc_samples)[source]
+
+
Parameters:
+

mcmc_samples (dict[str, Tensor])

+
+
Return type:
+

tuple[Mean, Kernel, Likelihood]

+
+
+
+
+
+
+class botorch.models.fully_bayesian.SaasPyroModel[source]
+

Bases: PyroModel

+

Implementation of the sparse axis-aligned subspace priors (SAAS) model.

+

The SAAS model uses sparsity-inducing priors to identify the most important +parameters. This model is suitable for high-dimensional BO with potentially +hundreds of tunable parameters. See [Eriksson2021saasbo] for more details.

+

SaasPyroModel is not a standard BoTorch model; instead, it is used as +an input to SaasFullyBayesianSingleTaskGP. It is used as a default keyword +argument, and end users are not likely to need to instantiate or modify a +SaasPyroModel unless they want to customize its attributes (such as +covar_module).

+
+
+set_inputs(train_X, train_Y, train_Yvar=None)[source]
+

Set the training data.

+
+
Parameters:
+
    +
  • train_X (Tensor) – Training inputs (n x d)

  • +
  • train_Y (Tensor) – Training targets (n x 1)

  • +
  • train_Yvar (Tensor | None) – Observed noise variance (n x 1). Inferred if None.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+sample()[source]
+

Sample from the SAAS model.

+

This samples the mean, noise variance, outputscale, and lengthscales according +to the SAAS prior.

+
+
Return type:
+

None

+
+
+
+
+
+sample_outputscale(concentration=2.0, rate=0.15, **tkwargs)[source]
+

Sample the outputscale.

+
+
Parameters:
+
    +
  • concentration (float)

  • +
  • rate (float)

  • +
  • tkwargs (Any)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+sample_mean(**tkwargs)[source]
+

Sample the mean constant.

+
+
Parameters:
+

tkwargs (Any)

+
+
Return type:
+

Tensor

+
+
+
+
+
+sample_noise(**tkwargs)[source]
+

Sample the noise variance.

+
+
Parameters:
+

tkwargs (Any)

+
+
Return type:
+

Tensor

+
+
+
+
+
+sample_lengthscale(dim, alpha=0.1, **tkwargs)[source]
+

Sample the lengthscale.

+
+
Parameters:
+
    +
  • dim (int)

  • +
  • alpha (float)

  • +
  • tkwargs (Any)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+postprocess_mcmc_samples(mcmc_samples)[source]
+

Post-process the MCMC samples.

+

This computes the true lengthscales and removes the inverse lengthscales and +tausq (global shrinkage).

+
+
Parameters:
+

mcmc_samples (dict[str, Tensor])

+
+
Return type:
+

dict[str, Tensor]

+
+
+
+
+
+load_mcmc_samples(mcmc_samples)[source]
+

Load the MCMC samples into the mean_module, covar_module, and likelihood.

+
+
Parameters:
+

mcmc_samples (dict[str, Tensor])

+
+
Return type:
+

tuple[Mean, Kernel, Likelihood]

+
+
+
+
+
+
+class botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP(train_X, train_Y, train_Yvar=None, outcome_transform=None, input_transform=None, pyro_model=None)[source]
+

Bases: ExactGP, BatchedMultiOutputGPyTorchModel

+

A fully Bayesian single-task GP model with the SAAS prior.

+

This model assumes that the inputs have been normalized to [0, 1]^d and that +the output has been standardized to have zero mean and unit variance. You can +either normalize and standardize the data before constructing the model or use +an input_transform and outcome_transform. The SAAS model [Eriksson2021saasbo] +with a Matern-5/2 kernel is used by default.

+

You are expected to use fit_fully_bayesian_model_nuts to fit this model as it +isn’t compatible with fit_gpytorch_mll.

+

Example

+
>>> saas_gp = SaasFullyBayesianSingleTaskGP(train_X, train_Y)
+>>> fit_fully_bayesian_model_nuts(saas_gp)
+>>> posterior = saas_gp.posterior(test_X)
+
+
+

Initialize the fully Bayesian single-task GP model.

+
+
Parameters:
+
    +
  • train_X (Tensor) – Training inputs (n x d)

  • +
  • train_Y (Tensor) – Training targets (n x 1)

  • +
  • train_Yvar (Tensor | None) – Observed noise variance (n x 1). Inferred if None.

  • +
  • outcome_transform (OutcomeTransform | None) – An outcome transform that is applied to the +training data during instantiation and to the posterior during +inference (that is, the Posterior obtained by calling +.posterior on the model will be on the original scale).

  • +
  • input_transform (InputTransform | None) – An input transform that is applied in the model’s +forward pass.

  • +
  • pyro_model (PyroModel | None) – Optional PyroModel, defaults to SaasPyroModel.

  • +
+
+
+
+
+property median_lengthscale: Tensor
+

Median lengthscales across the MCMC samples.

+
+
+
+property num_mcmc_samples: int
+

Number of MCMC samples in the model.

+
+
+
+property batch_shape: Size
+

Batch shape of the model, equal to the number of MCMC samples. +Note that SaasFullyBayesianSingleTaskGP does not support batching +over input data at this point.

+
+
+
+train(mode=True)[source]
+

Puts the model in train mode.

+
+
Parameters:
+

mode (bool)

+
+
Return type:
+

None

+
+
+
+
+
+load_mcmc_samples(mcmc_samples)[source]
+

Load the MCMC hyperparameter samples into the model.

+

This method will be called by fit_fully_bayesian_model_nuts when the model +has been fitted in order to create a batched SingleTaskGP model.

+
+
Parameters:
+

mcmc_samples (dict[str, Tensor])

+
+
Return type:
+

None

+
+
+
+
+
+load_state_dict(state_dict, strict=True)[source]
+

Custom logic for loading the state dict.

+

The standard approach of calling load_state_dict currently doesn’t play well +with the SaasFullyBayesianSingleTaskGP since the mean_module, covar_module +and likelihood aren’t initialized until the model has been fitted. The reason +for this is that we don’t know the number of MCMC samples until NUTS is called. +Given the state dict, we can initialize a new model with some dummy samples and +then load the state dict into this model. This currently only works for a +SaasPyroModel and supporting more Pyro models likely requires moving the model +construction logic into the Pyro model itself.

+
+
Parameters:
+
    +
  • state_dict (Mapping[str, Any])

  • +
  • strict (bool)

  • +
+
+
+
+
+
+forward(X)[source]
+

Unlike in other classes’ forward methods, there is no if self.training +block, because it ought to be unreachable: If self.train() has been called, +then self.covar_module will be None, check_if_fitted() will fail, and the +rest of this method will not run.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

MultivariateNormal

+
+
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None, **kwargs)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Parameters:
+
    +
  • X (Tensor) – A (batch_shape) x q x d-dim Tensor, where d is the dimension +of the feature space and q is the number of points considered +jointly.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior (if the model is multi-output). +Can be used to speed up computation if only a subset of the +model’s outputs are required for optimization. If omitted, +computes the posterior over all model outputs.

  • +
  • observation_noise (bool) – If True, add the observation noise from the +likelihood to the posterior. If a Tensor, use it directly as the +observation noise (must be of shape (batch_shape) x q x m).

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

+
A GaussianMixturePosterior object. Includes observation noise

if specified.

+
+
+

+
+
Return type:
+

GaussianMixturePosterior

+
+
+
+
+
+condition_on_observations(X, Y, **kwargs)[source]
+

Conditions on additional observations for a Fully Bayesian model (either +identical across models or unique per-model).

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x num_samples x d-dim Tensor, where d is +the dimension of the feature space and batch_shape is the number of +sampled models.

  • +
  • Y (Tensor) – A batch_shape x num_samples x 1-dim Tensor, where d is +the dimension of the feature space and batch_shape is the number of +sampled models.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

+
A fully bayesian model conditioned on

given observations. The returned model has batch_shape copies of the +training data in case of identical observations (and batch_shape +training datasets otherwise).

+
+
+

+
+
Return type:
+

BatchedMultiOutputGPyTorchModel

+
+
+
+
+
+
+

Fully Bayesian Multitask GP Models

+

Multi-task Gaussian Process Regression models with fully Bayesian inference.

+
+
+class botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel[source]
+

Bases: SaasPyroModel

+

Implementation of the multi-task sparse axis-aligned subspace priors (SAAS) model.

+

The multi-task model uses an ICM kernel. The data kernel is same as the single task +SAAS model in order to handle high-dimensional parameter spaces. The task kernel +is a Matern-5/2 kernel using learned task embeddings as the input.

+
+
+set_inputs(train_X, train_Y, train_Yvar, task_feature, task_rank=None)[source]
+

Set the training data.

+
+
Parameters:
+
    +
  • train_X (Tensor) – Training inputs (n x (d + 1))

  • +
  • train_Y (Tensor) – Training targets (n x 1)

  • +
  • train_Yvar (Tensor | None) – Observed noise variance (n x 1). If None, we infer the noise. +Note that the inferred noise is common across all tasks.

  • +
  • task_feature (int) – The index of the task feature (-d <= task_feature <= d).

  • +
  • task_rank (int | None) – The num of learned task embeddings to be used in the task kernel. +If omitted, use a full rank (i.e. number of tasks) kernel.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+sample()[source]
+

Sample from the SAAS model.

+

This samples the mean, noise variance, outputscale, and lengthscales according +to the SAAS prior.

+
+
Return type:
+

None

+
+
+
+
+
+sample_latent_features(**tkwargs)[source]
+
+
Parameters:
+

tkwargs (Any)

+
+
+
+
+
+sample_task_lengthscale(concentration=6.0, rate=3.0, **tkwargs)[source]
+
+
Parameters:
+
    +
  • concentration (float)

  • +
  • rate (float)

  • +
  • tkwargs (Any)

  • +
+
+
+
+
+
+load_mcmc_samples(mcmc_samples)[source]
+

Load the MCMC samples into the mean_module, covar_module, and likelihood.

+
+
Parameters:
+

mcmc_samples (dict[str, Tensor])

+
+
Return type:
+

tuple[Mean, Kernel, Likelihood, Kernel, Parameter]

+
+
+
+
+
+
+class botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP(train_X, train_Y, task_feature, train_Yvar=None, output_tasks=None, rank=None, all_tasks=None, outcome_transform=None, input_transform=None, pyro_model=None)[source]
+

Bases: MultiTaskGP

+

A fully Bayesian multi-task GP model with the SAAS prior.

+

This model assumes that the inputs have been normalized to [0, 1]^d and that the +output has been stratified standardized to have zero mean and unit variance for +each task. The SAAS model [Eriksson2021saasbo] with a Matern-5/2 is used as data +kernel by default.

+

You are expected to use fit_fully_bayesian_model_nuts to fit this model as it +isn’t compatible with fit_gpytorch_mll.

+

Example

+
>>> X1, X2 = torch.rand(10, 2), torch.rand(20, 2)
+>>> i1, i2 = torch.zeros(10, 1), torch.ones(20, 1)
+>>> train_X = torch.cat([
+>>>     torch.cat([X1, i1], -1), torch.cat([X2, i2], -1),
+>>> ])
+>>> train_Y = torch.cat(f1(X1), f2(X2)).unsqueeze(-1)
+>>> train_Yvar = 0.01 * torch.ones_like(train_Y)
+>>> mtsaas_gp = SaasFullyBayesianMultiTaskGP(
+>>>     train_X, train_Y, train_Yvar, task_feature=-1,
+>>> )
+>>> fit_fully_bayesian_model_nuts(mtsaas_gp)
+>>> posterior = mtsaas_gp.posterior(test_X)
+
+
+

Initialize the fully Bayesian multi-task GP model.

+
+
Parameters:
+
    +
  • train_X (Tensor) – Training inputs (n x (d + 1))

  • +
  • train_Y (Tensor) – Training targets (n x 1)

  • +
  • train_Yvar (Tensor | None) – Observed noise variance (n x 1). If None, we infer the noise. +Note that the inferred noise is common across all tasks.

  • +
  • task_feature (int) – The index of the task feature (-d <= task_feature <= d).

  • +
  • output_tasks (list[int] | None) – A list of task indices for which to compute model +outputs for. If omitted, return outputs for all task indices.

  • +
  • rank (int | None) – The num of learned task embeddings to be used in the task kernel. +If omitted, use a full rank (i.e. number of tasks) kernel.

  • +
  • all_tasks (list[int] | None) – NOT SUPPORTED!

  • +
  • outcome_transform (OutcomeTransform | None) – An outcome transform that is applied to the +training data during instantiation and to the posterior during +inference (that is, the Posterior obtained by calling +.posterior on the model will be on the original scale).

  • +
  • input_transform (InputTransform | None) – An input transform that is applied to the inputs X +in the model’s forward pass.

  • +
  • pyro_model (MultitaskSaasPyroModel | None) – Optional PyroModel that has the same signature as +MultitaskSaasPyroModel. Defaults to MultitaskSaasPyroModel.

  • +
+
+
+
+
+train(mode=True)[source]
+

Puts the model in train mode.

+
+
Parameters:
+

mode (bool)

+
+
Return type:
+

None

+
+
+
+
+
+property median_lengthscale: Tensor
+

Median lengthscales across the MCMC samples.

+
+
+
+property num_mcmc_samples: int
+

Number of MCMC samples in the model.

+
+
+
+property batch_shape: Size
+

Batch shape of the model, equal to the number of MCMC samples. +Note that SaasFullyBayesianMultiTaskGP does not support batching +over input data at this point.

+
+
+
+fantasize(*args, **kwargs)[source]
+

Construct a fantasy model.

+

Constructs a fantasy model in the following fashion: +(1) compute the model posterior at X, including observation noise. +If observation_noise is a Tensor, use it directly as the observation +noise to add. +(2) sample from this posterior (using sampler) to generate “fake” +observations. +(3) condition the model on the new fake observations.

+
+
Parameters:
+
    +
  • X – A batch_shape x n’ x d-dim Tensor, where d is the dimension of +the feature space, n’ is the number of points per batch, and +batch_shape is the batch shape (must be compatible with the +batch shape of the model).

  • +
  • sampler – The sampler used for sampling from the posterior at X.

  • +
  • observation_noise – A model_batch_shape x 1 x m-dim tensor or +a model_batch_shape x n’ x m-dim tensor containing the average +noise for each batch and output, where m is the number of outputs. +noise must be in the outcome-transformed space if an outcome +transform is used. +If None and using an inferred noise likelihood, the noise will be the +inferred noise level. If using a fixed noise likelihood, the mean across +the observation noise in the training data is used as observation noise.

  • +
  • kwargs – Will be passed to model.condition_on_observations

  • +
+
+
Returns:
+

The constructed fantasy model.

+
+
Return type:
+

NoReturn

+
+
+
+
+
+load_mcmc_samples(mcmc_samples)[source]
+

Load the MCMC hyperparameter samples into the model.

+

This method will be called by fit_fully_bayesian_model_nuts when the model +has been fitted in order to create a batched MultiTaskGP model.

+
+
Parameters:
+

mcmc_samples (dict[str, Tensor])

+
+
Return type:
+

None

+
+
+
+
+
+posterior(X, output_indices=None, observation_noise=False, posterior_transform=None, **kwargs)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Returns:
+

+
A GaussianMixturePosterior object. Includes observation noise

if specified.

+
+
+

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • output_indices (list[int] | None)

  • +
  • observation_noise (bool)

  • +
  • posterior_transform (PosteriorTransform | None)

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

GaussianMixturePosterior

+
+
+
+
+
+forward(X)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

MultivariateNormal

+
+
+
+
+
+load_state_dict(state_dict, strict=True)[source]
+

Custom logic for loading the state dict.

+

The standard approach of calling load_state_dict currently doesn’t play well +with the SaasFullyBayesianMultiTaskGP since the mean_module, covar_module +and likelihood aren’t initialized until the model has been fitted. The reason +for this is that we don’t know the number of MCMC samples until NUTS is called. +Given the state dict, we can initialize a new model with some dummy samples and +then load the state dict into this model. This currently only works for a +MultitaskSaasPyroModel and supporting more Pyro models likely requires moving +the model construction logic into the Pyro model itself.

+

TODO: If this were to inherif from SaasFullyBayesianSingleTaskGP, we could +simplify this method and eliminate some others.

+
+
Parameters:
+
    +
  • state_dict (Mapping[str, Any])

  • +
  • strict (bool)

  • +
+
+
+
+
+
+
+
+

Model Components

+
+

Kernels

+
+
+class botorch.models.kernels.categorical.CategoricalKernel(ard_num_dims=None, batch_shape=None, active_dims=None, lengthscale_prior=None, lengthscale_constraint=None, eps=1e-06, **kwargs)[source]
+

Bases: Kernel

+

A Kernel for categorical features.

+

Computes exp(-dist(x1, x2) / lengthscale), where +dist(x1, x2) is zero if x1 == x2 and one if x1 != x2. +If the last dimension is not a batch dimension, then the +mean is considered.

+

Note: This kernel is NOT differentiable w.r.t. the inputs.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
Parameters:
+
    +
  • ard_num_dims (Optional[int])

  • +
  • batch_shape (Optional[torch.Size])

  • +
  • active_dims (Optional[Tuple[int, ...]])

  • +
  • lengthscale_prior (Optional[Prior])

  • +
  • lengthscale_constraint (Optional[Interval])

  • +
  • eps (float)

  • +
+
+
+
+
+
+class botorch.models.kernels.downsampling.DownsamplingKernel(power_prior=None, offset_prior=None, power_constraint=None, offset_constraint=None, **kwargs)[source]
+

Bases: Kernel

+

GPyTorch Downsampling Kernel.

+

Computes a covariance matrix based on the down sampling kernel between +inputs x_1 and x_2 (we expect d = 1):

+
+
+
K(mathbf{x_1}, mathbf{x_2}) = c + (1 - x_1)^(1 + delta) *

(1 - x_2)^(1 + delta).

+
+
+
+

where c is an offset parameter, and delta is a power parameter.

+
+
Parameters:
+
    +
  • power_constraint (Interval | None) – Constraint to place on power parameter. Default is +Positive.

  • +
  • power_prior (Prior | None) – Prior over the power parameter.

  • +
  • offset_constraint (Interval | None) – Constraint to place on offset parameter. Default is +Positive.

  • +
  • active_dims – List of data dimensions to operate on. len(active_dims) +should equal num_dimensions.

  • +
  • offset_prior (Prior | None)

  • +
+
+
+
+
+
+class botorch.models.kernels.exponential_decay.ExponentialDecayKernel(power_prior=None, offset_prior=None, power_constraint=None, offset_constraint=None, **kwargs)[source]
+

Bases: Kernel

+

GPyTorch Exponential Decay Kernel.

+

Computes a covariance matrix based on the exponential decay kernel +between inputs x_1 and x_2 (we expect d = 1):

+
+

K(x_1, x_2) = w + beta^alpha / (x_1 + x_2 + beta)^alpha.

+
+

where w is an offset parameter, beta is a lenthscale parameter, and +alpha is a power parameter.

+
+
Parameters:
+
    +
  • lengthscale_constraint – Constraint to place on lengthscale parameter. +Default is Positive.

  • +
  • lengthscale_prior – Prior over the lengthscale parameter.

  • +
  • power_constraint (Interval | None) – Constraint to place on power parameter. Default is +Positive.

  • +
  • power_prior (Prior | None) – Prior over the power parameter.

  • +
  • offset_constraint (Interval | None) – Constraint to place on offset parameter. Default is +Positive.

  • +
  • active_dims – List of data dimensions to operate on. len(active_dims) +should equal num_dimensions.

  • +
  • offset_prior (Prior | None)

  • +
+
+
+
+
+
+class botorch.models.kernels.infinite_width_bnn.InfiniteWidthBNNKernel(depth=3, batch_shape=None, active_dims=None, acos_eps=1e-07, device=None)[source]
+

Bases: Kernel

+

Infinite-width BNN kernel.

+

Defines the GP kernel which is equivalent to performing exact Bayesian +inference on a fully-connected deep neural network with ReLU activations +and i.i.d. priors in the infinite-width limit. +See [Cho2009kernel] and [Lee2018deep] for details.

+
+
+[Cho2009kernel] +

Y. Cho, and L. Saul. Kernel methods for deep learning. +Advances in Neural Information Processing Systems 22. 2009.

+
+
+[Lee2018deep] +

J. Lee, Y. Bahri, R. Novak, S. Schoenholz, J. Pennington, and J. Dickstein. +Deep Neural Networks as Gaussian Processes. +International Conference on Learning Representations. 2018.

+
+
+
+
Parameters:
+
    +
  • depth (int) – Depth of neural network.

  • +
  • batch_shape (torch.Size | None) – This will set a separate weight/bias var for each batch. +It should be \(B_1 \times \ldots \times B_k\) if \(\mathbf\) is +a \(B_1 \times \ldots \times B_k \times N \times D\) tensor.

  • +
  • active_dims (param) – Compute the covariance of only a few input dimensions. +The ints corresponds to the indices of the dimensions.

  • +
  • acos_eps (param) – A small positive value to restrict acos inputs to +:math`[-1 + epsilon, 1 - epsilon]`

  • +
  • device (param) – Device for parameters.

  • +
+
+
+
+
+
+class botorch.models.kernels.linear_truncated_fidelity.LinearTruncatedFidelityKernel(fidelity_dims, dimension=None, power_prior=None, power_constraint=None, nu=2.5, lengthscale_prior_unbiased=None, lengthscale_prior_biased=None, lengthscale_constraint_unbiased=None, lengthscale_constraint_biased=None, covar_module_unbiased=None, covar_module_biased=None, **kwargs)[source]
+

Bases: Kernel

+

GPyTorch Linear Truncated Fidelity Kernel.

+

Computes a covariance matrix based on the Linear truncated kernel between +inputs x_1 and x_2 for up to two fidelity parmeters:

+
+

K(x_1, x_2) = k_0 + c_1(x_1, x_2)k_1 + c_2(x_1,x_2)k_2 + c_3(x_1,x_2)k_3

+
+

where

+
    +
  • +
    k_i(i=0,1,2,3) are Matern kernels calculated between non-fidelity

    parameters of x_1 and x_2 with different priors.

    +
    +
    +
  • +
  • +
    c_1=(1 - x_1[f_1])(1 - x_2[f_1]))(1 + x_1[f_1] x_2[f_1])^p is the kernel

    of the the bias term, which can be decomposed into a determistic part +and a polynomial kernel. Here f_1 is the first fidelity dimension and +p is the order of the polynomial kernel.

    +
    +
    +
  • +
  • +
    c_3 is the same as c_1 but is calculated for the second fidelity

    dimension f_2.

    +
    +
    +
  • +
  • +
    c_2 is the interaction term with four deterministic terms and the

    polynomial kernel between x_1[…, [f_1, f_2]] and +x_2[…, [f_1, f_2]].

    +
    +
    +
  • +
+

Example

+
>>> x = torch.randn(10, 5)
+>>> # Non-batch: Simple option
+>>> covar_module = LinearTruncatedFidelityKernel()
+>>> covar = covar_module(x)  # Output: LinearOperator of size (10 x 10)
+>>>
+>>> batch_x = torch.randn(2, 10, 5)
+>>> # Batch: Simple option
+>>> covar_module = LinearTruncatedFidelityKernel(batch_shape = torch.Size([2]))
+>>> covar = covar_module(x)  # Output: LinearOperator of size (2 x 10 x 10)
+
+
+
+
Parameters:
+
    +
  • fidelity_dims (list[int]) – A list containing either one or two indices specifying +the fidelity parameters of the input.

  • +
  • dimension (int | None) – The dimension of x. Unused if active_dims is specified.

  • +
  • power_prior (Prior | None) – Prior for the power parameter of the polynomial kernel. +Default is None.

  • +
  • power_constraint (Interval | None) – Constraint on the power parameter of the polynomial +kernel. Default is Positive.

  • +
  • nu (float) – The smoothness parameter for the Matern kernel: either 1/2, 3/2, +or 5/2. Unused if both covar_module_unbiased and +covar_module_biased are specified.

  • +
  • lengthscale_prior_unbiased (Prior | None) – Prior on the lengthscale parameter of Matern +kernel k_0. Default is Gamma(1.1, 1/20).

  • +
  • lengthscale_constraint_unbiased (Interval | None) – Constraint on the lengthscale parameter +of the Matern kernel k_0. Default is Positive.

  • +
  • lengthscale_prior_biased (Prior | None) – Prior on the lengthscale parameter of Matern +kernels k_i(i>0). Default is Gamma(5, 1/20).

  • +
  • lengthscale_constraint_biased (Interval | None) – Constraint on the lengthscale parameter +of the Matern kernels k_i(i>0). Default is Positive.

  • +
  • covar_module_unbiased (Kernel | None) – Specify a custom kernel for k_0. If omitted, +use a MaternKernel.

  • +
  • covar_module_biased (Kernel | None) – Specify a custom kernel for the biased parts +k_i(i>0). If omitted, use a MaternKernel.

  • +
  • batch_shape – If specified, use a separate lengthscale for each batch of +input data. If x1 is a batch_shape x n x d tensor, this should +be batch_shape.

  • +
  • active_dims – Compute the covariance of a subset of input dimensions. The +numbers correspond to the indices of the dimensions.

  • +
  • kwargs (Any)

  • +
+
+
+
+
+
+class botorch.models.kernels.contextual_lcea.LCEAKernel(decomposition, batch_shape, train_embedding=True, cat_feature_dict=None, embs_feature_dict=None, embs_dim_list=None, context_weight_dict=None, device=None)[source]
+

Bases: Kernel

+

The Latent Context Embedding Additive (LCE-A) Kernel.

+

This kernel is similar to the SACKernel, and is used when context breakdowns are +unbserverable. It assumes the same additive structure and a spatial kernel shared +across contexts. Rather than assuming independence, LCEAKernel models the +correlation in the latent functions for each context through learning context +embeddings.

+
+
Parameters:
+
    +
  • decomposition (dict[str, list[int]]) – Keys index context names. Values are the indexes of +parameters belong to the context.

  • +
  • batch_shape (Size) – Batch shape as usual for gpytorch kernels. Model does not +support batch training. When batch_shape is non-empty, it is used for +loading hyper-parameter values generated from MCMC sampling.

  • +
  • train_embedding (bool) – A boolean indictor of whether to learn context embeddings.

  • +
  • cat_feature_dict (dict | None) – Keys are context names and values are list of categorical +features i.e. {“context_name” : [cat_0, …, cat_k]}. k equals the +number of categorical variables. If None, uses context names in the +decomposition as the only categorical feature, i.e., k = 1.

  • +
  • embs_feature_dict (dict | None) – Pre-trained continuous embedding features of each +context.

  • +
  • embs_dim_list (list[int] | None) – Embedding dimension for each categorical variable. The length +equals to num of categorical features k. If None, the embedding +dimension is set to 1 for each categorical variable.

  • +
  • context_weight_dict (dict | None) – Known population weights of each context.

  • +
  • device (device | None)

  • +
+
+
+
+
+
+class botorch.models.kernels.contextual_sac.SACKernel(decomposition, batch_shape, device=None)[source]
+

Bases: Kernel

+

The structural additive contextual(SAC) kernel.

+

The kernel is used for contextual BO without oberseving context breakdowns. +There are d parameters and M contexts. In total, the dimension of parameter space +is d*M and input x can be written as +x=[x_11, …, x_1d, x_21, …, x_2d, …, x_M1, …, x_Md].

+

The kernel uses the parameter decomposition and assumes an additive structure +across contexts. Each context compponent is assumed to be independent.

+
+\[\begin{equation*} + k(\mathbf{x}, \mathbf{x'}) = k_1(\mathbf{x_(1)}, \mathbf{x'_(1)}) + \cdots + + k_M(\mathbf{x_(M)}, \mathbf{x'_(M)}) +\end{equation*}\]
+

where +* :math: M is the number of partitions of parameter space. Each partition contains +same number of parameters d. Each kernel k_i acts only on d parameters of ith +partition i.e. mathbf{x}_(i). Each kernel k_i is a scaled RBF kernel +with same lengthscales but different outputscales.

+
+
Parameters:
+
    +
  • decomposition (dict[str, list[int]]) – Keys are context names. Values are the indexes of parameters +belong to the context. The parameter indexes are in the same order +across contexts.

  • +
  • batch_shape (Size) – Batch shape as usual for gpytorch kernels.

  • +
  • device (device | None) – The torch device.

  • +
+
+
+
+
+
+class botorch.models.kernels.orthogonal_additive_kernel.OrthogonalAdditiveKernel(base_kernel, dim, quad_deg=32, second_order=False, batch_shape=None, dtype=None, device=None, coeff_constraint=Positive(), offset_prior=None, coeffs_1_prior=None, coeffs_2_prior=None)[source]
+

Bases: Kernel

+

Orthogonal Additive Kernels (OAKs) were introduced in [Lu2022additive], though +only for the case of Gaussian base kernels with a Gaussian input data distribution.

+

The implementation here generalizes OAKs to arbitrary base kernels by using a +Gauss-Legendre quadrature approximation to the required one-dimensional integrals +involving the base kernels.

+
+
+[Lu2022additive] +

X. Lu, A. Boukouvalas, and J. Hensman. Additive Gaussian processes revisited. +Proceedings of the 39th International Conference on Machine Learning. Jul 2022.

+
+
+
+
Parameters:
+
    +
  • base_kernel (Kernel) – The kernel which to orthogonalize and evaluate in forward.

  • +
  • dim (int) – Input dimensionality of the kernel.

  • +
  • quad_deg (int) – Number of integration nodes for orthogonalization.

  • +
  • second_order (bool) – Toggles second order interactions. If true, both the time and +space complexity of evaluating the kernel are quadratic in dim.

  • +
  • batch_shape (Size | None) – Optional batch shape for the kernel and its parameters.

  • +
  • dtype (dtype | None) – Initialization dtype for required Tensors.

  • +
  • device (device | None) – Initialization device for required Tensors.

  • +
  • coeff_constraint (Interval) – Constraint on the coefficients of the additive kernel.

  • +
  • offset_prior (Prior | None) – Prior on the offset coefficient. Should be prior with non- +negative support.

  • +
  • coeffs_1_prior (Prior | None) – Prior on the parameter main effects. Should be prior with +non-negative support.

  • +
  • coeffs_2_prior (Prior | None) – coeffs_1_prior: Prior on the parameter interactions. Should +be prior with non-negative support.

  • +
+
+
+
+
+
+

Likelihoods

+

Pairwise likelihood for pairwise preference model (e.g., PairwiseGP).

+
+
+class botorch.models.likelihoods.pairwise.PairwiseLikelihood(max_plate_nesting=1)[source]
+

Bases: Likelihood, ABC

+

Pairwise likelihood base class for pairwise preference GP (e.g., PairwiseGP).

+

Initialized like a gpytorch.likelihoods.Likelihood.

+
+
Parameters:
+

max_plate_nesting (int) – Defaults to 1.

+
+
+
+
+forward(utility, D)[source]
+

Given the difference in (estimated) utility util_diff = f(v) - f(u), +return a Bernoulli distribution object representing the likelihood of +the user prefer v over u.

+

Note that this is not used by the PairwiseGP model,

+
+
Parameters:
+
    +
  • utility (Tensor)

  • +
  • D (Tensor)

  • +
+
+
Return type:
+

Bernoulli

+
+
+
+
+
+abstract p(utility, D)[source]
+

Given the difference in (estimated) utility util_diff = f(v) - f(u), +return the probability of the user prefer v over u.

+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size) x n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
  • log – if true, return log probability

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+log_p(utility, D)[source]
+

return the log of p

+
+
Parameters:
+
    +
  • utility (Tensor)

  • +
  • D (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+negative_log_gradient_sum(utility, D)[source]
+
+
Calculate the sum of negative log gradient with respect to each item’s latent

utility values. Useful for models using laplace approximation.

+
+
+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size x) n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
+
+
Returns:
+

A (batch_size x) n Tensor representing the sum of negative log gradient +values of the likelihood over all comparisons (i.e., the m dimension) +with respect to each item.

+
+
Return type:
+

Tensor

+
+
+
+
+
+negative_log_hessian_sum(utility, D)[source]
+
+
Calculate the sum of negative log hessian with respect to each item’s latent

utility values. Useful for models using laplace approximation.

+
+
+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size) x n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
+
+
Returns:
+

A (batch_size x) n x n Tensor representing the sum of negative log hessian +values of the likelihood over all comparisons (i.e., the m dimension) with +respect to each item.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.likelihoods.pairwise.PairwiseProbitLikelihood(max_plate_nesting=1)[source]
+

Bases: PairwiseLikelihood

+

Pairwise likelihood using probit function

+

Given two items v and u with utilities f(v) and f(u), the probability that we +prefer v over u with probability std_normal_cdf((f(v) - f(u))/sqrt(2)). Note +that this formulation implicitly assume the noise term is fixed at 1.

+

Initialized like a gpytorch.likelihoods.Likelihood.

+
+
Parameters:
+

max_plate_nesting (int) – Defaults to 1.

+
+
+
+
+p(utility, D, log=False)[source]
+

Given the difference in (estimated) utility util_diff = f(v) - f(u), +return the probability of the user prefer v over u.

+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size) x n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
  • log (bool) – if true, return log probability

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+negative_log_gradient_sum(utility, D)[source]
+
+
Calculate the sum of negative log gradient with respect to each item’s latent

utility values. Useful for models using laplace approximation.

+
+
+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size x) n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
+
+
Returns:
+

A (batch_size x) n Tensor representing the sum of negative log gradient +values of the likelihood over all comparisons (i.e., the m dimension) +with respect to each item.

+
+
Return type:
+

Tensor

+
+
+
+
+
+negative_log_hessian_sum(utility, D)[source]
+
+
Calculate the sum of negative log hessian with respect to each item’s latent

utility values. Useful for models using laplace approximation.

+
+
+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size) x n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
+
+
Returns:
+

A (batch_size x) n x n Tensor representing the sum of negative log hessian +values of the likelihood over all comparisons (i.e., the m dimension) with +respect to each item.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.likelihoods.pairwise.PairwiseLogitLikelihood(max_plate_nesting=1)[source]
+

Bases: PairwiseLikelihood

+

Pairwise likelihood using logistic (i.e., sigmoid) function

+

Given two items v and u with utilities f(v) and f(u), the probability that we +prefer v over u with probability sigmoid(f(v) - f(u)). Note +that this formulation implicitly assume the beta term in logistic function is +fixed at 1.

+

Initialized like a gpytorch.likelihoods.Likelihood.

+
+
Parameters:
+

max_plate_nesting (int) – Defaults to 1.

+
+
+
+
+log_p(utility, D)[source]
+

return the log of p

+
+
Parameters:
+
    +
  • utility (Tensor)

  • +
  • D (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+p(utility, D)[source]
+

Given the difference in (estimated) utility util_diff = f(v) - f(u), +return the probability of the user prefer v over u.

+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size) x n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
  • log – if true, return log probability

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+negative_log_gradient_sum(utility, D)[source]
+
+
Calculate the sum of negative log gradient with respect to each item’s latent

utility values. Useful for models using laplace approximation.

+
+
+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size x) n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
+
+
Returns:
+

A (batch_size x) n Tensor representing the sum of negative log gradient +values of the likelihood over all comparisons (i.e., the m dimension) +with respect to each item.

+
+
Return type:
+

Tensor

+
+
+
+
+
+negative_log_hessian_sum(utility, D)[source]
+
+
Calculate the sum of negative log hessian with respect to each item’s latent

utility values. Useful for models using laplace approximation.

+
+
+
+
Parameters:
+
    +
  • utility (Tensor) – A Tensor of shape (batch_size) x n, the utility at MAP point

  • +
  • D (Tensor) – D is (batch_size x) m x n matrix with all elements being zero in last +dimension except at two positions D[…, i] = 1 and D[…, j] = -1 +respectively, representing item i is preferred over item j.

  • +
+
+
Returns:
+

A (batch_size x) n x n Tensor representing the sum of negative log hessian +values of the likelihood over all comparisons (i.e., the m dimension) with +respect to each item.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+
+

Transforms

+
+

Outcome Transforms

+

Outcome transformations for automatically transforming and un-transforming +model outputs. Outcome transformations are typically part of a Model and +applied (i) within the model constructor to transform the train observations +to the model space, and (ii) in the Model.posterior call to untransform +the model posterior back to the original space.

+

References

+
+
+[eriksson2021scalable] +

D. Eriksson, M. Poloczek. Scalable Constrained Bayesian Optimization. +International Conference on Artificial Intelligence and Statistics. PMLR, 2021, +http://proceedings.mlr.press/v130/eriksson21a.html

+
+
+
+
+class botorch.models.transforms.outcome.OutcomeTransform(*args, **kwargs)[source]
+

Bases: Module, ABC

+

Abstract base class for outcome transforms.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+abstract forward(Y, Yvar=None)[source]
+

Transform the outcomes in a model’s training targets

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of observation noises +associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The transformed outcome observations.

  • +
  • The transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the transformed outcomes

+
+
+
+
+
+subset_output(idcs)[source]
+

Subset the transform along the output dimension.

+

This functionality is used to properly treat outcome transformations +in the subset_model functionality.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the transform to.

+
+
Returns:
+

The current outcome transform, subset to the specified output indices.

+
+
Return type:
+

OutcomeTransform

+
+
+
+
+
+untransform(Y, Yvar=None)[source]
+

Un-transform previously transformed outcomes

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of transfomred training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of transformed observation +noises associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The un-transformed outcome observations.

  • +
  • The un-transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the un-transformed outcomes

+
+
+
+
+
+untransform_posterior(posterior)[source]
+

Un-transform a posterior.

+

Posteriors with _is_linear=True should return a GPyTorchPosterior when +posterior is a GPyTorchPosterior. Posteriors with _is_linear=False +likely return a TransformedPosterior instead.

+
+
Parameters:
+

posterior (Posterior) – A posterior in the transformed space.

+
+
Returns:
+

The un-transformed posterior.

+
+
Return type:
+

Posterior

+
+
+
+
+
+
+class botorch.models.transforms.outcome.ChainedOutcomeTransform(**transforms)[source]
+

Bases: OutcomeTransform, ModuleDict

+

An outcome transform representing the chaining of individual transforms

+

Chaining of outcome transforms.

+
+
Parameters:
+

transforms (OutcomeTransform) – The transforms to chain. Internally, the names of the +kwargs are used as the keys for accessing the individual +transforms on the module.

+
+
+
+
+forward(Y, Yvar=None)[source]
+

Transform the outcomes in a model’s training targets

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of observation noises +associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The transformed outcome observations.

  • +
  • The transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the transformed outcomes

+
+
+
+
+
+subset_output(idcs)[source]
+

Subset the transform along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the transform to.

+
+
Returns:
+

The current outcome transform, subset to the specified output indices.

+
+
Return type:
+

OutcomeTransform

+
+
+
+
+
+untransform(Y, Yvar=None)[source]
+

Un-transform previously transformed outcomes

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of transfomred training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of transformed observation +noises associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The un-transformed outcome observations.

  • +
  • The un-transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the un-transformed outcomes

+
+
+
+
+
+untransform_posterior(posterior)[source]
+

Un-transform a posterior

+
+
Parameters:
+

posterior (Posterior) – A posterior in the transformed space.

+
+
Returns:
+

The un-transformed posterior.

+
+
Return type:
+

Posterior

+
+
+
+
+
+
+class botorch.models.transforms.outcome.Standardize(m, outputs=None, batch_shape=(), min_stdv=1e-08)[source]
+

Bases: OutcomeTransform

+

Standardize outcomes (zero mean, unit variance).

+

This module is stateful: If in train mode, calling forward updates the +module state (i.e. the mean/std normalizing constants). If in eval mode, +calling forward simply applies the standardization using the current module +state.

+

Standardize outcomes (zero mean, unit variance).

+
+
Parameters:
+
    +
  • m (int) – The output dimension.

  • +
  • outputs (list[int] | None) – Which of the outputs to standardize. If omitted, all +outputs will be standardized.

  • +
  • batch_shape (torch.Size) – The batch_shape of the training targets.

  • +
  • min_stddv – The minimum standard deviation for which to perform +standardization (if lower, only de-mean the data).

  • +
  • min_stdv (float)

  • +
+
+
+
+
+forward(Y, Yvar=None)[source]
+

Standardize outcomes.

+

If the module is in train mode, this updates the module state (i.e. the +mean/std normalizing constants). If the module is in eval mode, simply +applies the normalization using the module state.

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of observation noises +associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The transformed outcome observations.

  • +
  • The transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the transformed outcomes

+
+
+
+
+
+subset_output(idcs)[source]
+

Subset the transform along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the transform to.

+
+
Returns:
+

The current outcome transform, subset to the specified output indices.

+
+
Return type:
+

OutcomeTransform

+
+
+
+
+
+untransform(Y, Yvar=None)[source]
+

Un-standardize outcomes.

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of standardized targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of standardized observation +noises associated with the targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The un-standardized outcome observations.

  • +
  • The un-standardized observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the un-standardized outcomes

+
+
+
+
+
+untransform_posterior(posterior)[source]
+

Un-standardize the posterior.

+
+
Parameters:
+

posterior (Posterior) – A posterior in the standardized space.

+
+
Returns:
+

The un-standardized posterior. If the input posterior is a +GPyTorchPosterior, return a GPyTorchPosterior. Otherwise, return a +TransformedPosterior.

+
+
Return type:
+

GPyTorchPosterior | TransformedPosterior

+
+
+
+
+
+
+class botorch.models.transforms.outcome.Log(outputs=None)[source]
+

Bases: OutcomeTransform

+

Log-transform outcomes.

+

Useful if the targets are modeled using a (multivariate) log-Normal +distribution. This means that we can use a standard GP model on the +log-transformed outcomes and un-transform the model posterior of that GP.

+

Log-transform outcomes.

+
+
Parameters:
+

outputs (list[int] | None) – Which of the outputs to log-transform. If omitted, all +outputs will be standardized.

+
+
+
+
+subset_output(idcs)[source]
+

Subset the transform along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the transform to.

+
+
Returns:
+

The current outcome transform, subset to the specified output indices.

+
+
Return type:
+

OutcomeTransform

+
+
+
+
+
+forward(Y, Yvar=None)[source]
+

Log-transform outcomes.

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of observation noises +associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The transformed outcome observations.

  • +
  • The transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the transformed outcomes

+
+
+
+
+
+untransform(Y, Yvar=None)[source]
+

Un-transform log-transformed outcomes

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of log-transfomred targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of log- transformed +observation noises associated with the training targets +(if applicable).

  • +
+
+
Returns:
+

    +
  • The exponentiated outcome observations.

  • +
  • The exponentiated observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the un-transformed outcomes

+
+
+
+
+
+untransform_posterior(posterior)[source]
+

Un-transform the log-transformed posterior.

+
+
Parameters:
+

posterior (Posterior) – A posterior in the log-transformed space.

+
+
Returns:
+

The un-transformed posterior.

+
+
Return type:
+

TransformedPosterior

+
+
+
+
+
+
+class botorch.models.transforms.outcome.Power(power, outputs=None)[source]
+

Bases: OutcomeTransform

+

Power-transform outcomes.

+

Useful if the targets are modeled using a (multivariate) power transform of +a Normal distribution. This means that we can use a standard GP model on the +power-transformed outcomes and un-transform the model posterior of that GP.

+

Power-transform outcomes.

+
+
Parameters:
+
    +
  • outputs (list[int] | None) – Which of the outputs to power-transform. If omitted, all +outputs will be standardized.

  • +
  • power (float)

  • +
+
+
+
+
+subset_output(idcs)[source]
+

Subset the transform along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the transform to.

+
+
Returns:
+

The current outcome transform, subset to the specified output indices.

+
+
Return type:
+

OutcomeTransform

+
+
+
+
+
+forward(Y, Yvar=None)[source]
+

Power-transform outcomes.

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of observation noises +associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The transformed outcome observations.

  • +
  • The transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the transformed outcomes

+
+
+
+
+
+untransform(Y, Yvar=None)[source]
+

Un-transform power-transformed outcomes

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of power-transfomred targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of power-transformed +observation noises associated with the training targets +(if applicable).

  • +
+
+
Returns:
+

    +
  • The un-power transformed outcome observations.

  • +
  • The un-power transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the un-transformed outcomes

+
+
+
+
+
+untransform_posterior(posterior)[source]
+

Un-transform the power-transformed posterior.

+
+
Parameters:
+

posterior (Posterior) – A posterior in the power-transformed space.

+
+
Returns:
+

The un-transformed posterior.

+
+
Return type:
+

TransformedPosterior

+
+
+
+
+
+
+class botorch.models.transforms.outcome.Bilog(outputs=None)[source]
+

Bases: OutcomeTransform

+

Bilog-transform outcomes.

+

The Bilog transform [eriksson2021scalable] is useful for modeling outcome +constraints as it magnifies values near zero and flattens extreme values.

+

Bilog-transform outcomes.

+
+
Parameters:
+

outputs (list[int] | None) – Which of the outputs to Bilog-transform. If omitted, all +outputs will be transformed.

+
+
+
+
+subset_output(idcs)[source]
+

Subset the transform along the output dimension.

+
+
Parameters:
+

idcs (list[int]) – The output indices to subset the transform to.

+
+
Returns:
+

The current outcome transform, subset to the specified output indices.

+
+
Return type:
+

OutcomeTransform

+
+
+
+
+
+forward(Y, Yvar=None)[source]
+

Bilog-transform outcomes.

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of training targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of observation noises +associated with the training targets (if applicable).

  • +
+
+
Returns:
+

    +
  • The transformed outcome observations.

  • +
  • The transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the transformed outcomes

+
+
+
+
+
+untransform(Y, Yvar=None)[source]
+

Un-transform bilog-transformed outcomes

+
+
Parameters:
+
    +
  • Y (Tensor) – A batch_shape x n x m-dim tensor of bilog-transfomred targets.

  • +
  • Yvar (Tensor | None) – A batch_shape x n x m-dim tensor of bilog-transformed +observation noises associated with the training targets +(if applicable).

  • +
+
+
Returns:
+

    +
  • The un-transformed outcome observations.

  • +
  • The un-transformed observation noise (if applicable).

  • +
+

+
+
Return type:
+

A two-tuple with the un-transformed outcomes

+
+
+
+
+
+untransform_posterior(posterior)[source]
+

Un-transform the bilog-transformed posterior.

+
+
Parameters:
+

posterior (Posterior) – A posterior in the bilog-transformed space.

+
+
Returns:
+

The un-transformed posterior.

+
+
Return type:
+

TransformedPosterior

+
+
+
+
+
+
+

Input Transforms

+

Input Transformations.

+

These classes implement a variety of transformations for +input parameters including: learned input warping functions, +rounding functions, and log transformations. The input transformation +is typically part of a Model and applied within the model.forward() +method.

+
+
+class botorch.models.transforms.input.InputTransform(*args, **kwargs)[source]
+

Bases: Module, ABC

+

Abstract base class for input transforms.

+
+
Properties:
+
is_one_to_many: A boolean denoting whether the transform produces

multiple values for each input.

+
+
transform_on_train: A boolean indicating whether to apply the

transform in train() mode.

+
+
transform_on_eval: A boolean indicating whether to apply the

transform in eval() mode.

+
+
transform_on_fantasize: A boolean indicating whether to apply

the transform when called from within a fantasize call.

+
+
+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+is_one_to_many: bool = False
+
+
+
+transform_on_eval: bool
+
+
+
+transform_on_train: bool
+
+
+
+transform_on_fantasize: bool
+
+
+
+forward(X)[source]
+

Transform the inputs to a model.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n’ x d-dim tensor of transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+abstract transform(X)[source]
+

Transform the inputs to a model.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+untransform(X)[source]
+

Un-transform the inputs to a model.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of transformed inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of un-transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+

Note: The reason that a custom equals method is defined rather than +defining an __eq__ method is because defining an __eq__ method sets +the __hash__ method to None. Hashing modules is currently used in +pytorch. See https://github.com/pytorch/pytorch/issues/7733.

+
+
Parameters:
+

other (InputTransform) – Another input transform.

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+preprocess_transform(X)[source]
+

Apply transforms for preprocessing inputs.

+

The main use cases for this method are 1) to preprocess training data +before calling set_train_data and 2) preprocess X_baseline for noisy +acquisition functions so that X_baseline is “preprocessed” with the +same transformations as the cached training inputs.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of (transformed) inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.transforms.input.BatchBroadcastedInputTransform(transforms, broadcast_index=-3)[source]
+

Bases: InputTransform, ModuleDict

+

An input transform representing a list of transforms to be broadcasted.

+

A transform list that is broadcasted across a batch dimension specified by +broadcast_index. This is allows using a batched Gaussian process model when +the input transforms are different for different batch dimensions.

+
+
Parameters:
+
    +
  • transforms (list[InputTransform]) – The transforms to broadcast across the first batch dimension. +The transform at position i in the list will be applied to X[i] for +a given input tensor X in the forward pass.

  • +
  • broadcast_index (int) – The tensor index at which the transforms are broadcasted.

  • +
+
+
+

Example

+
>>> tf1 = Normalize(d=2)
+>>> tf2 = InputStandardize(d=2)
+>>> tf = BatchBroadcastedTransformList(transforms=[tf1, tf2])
+
+
+
+
+transform(X)[source]
+

Transform the inputs to a model.

+

Individual transforms are applied in sequence and results are returned as +a batched tensor.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+untransform(X)[source]
+

Un-transform the inputs to a model.

+

Un-transforms of the individual transforms are applied in reverse sequence.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of transformed inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of un-transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+
+
Parameters:
+

other (InputTransform) – Another input transform.

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+preprocess_transform(X)[source]
+

Apply transforms for preprocessing inputs.

+

The main use cases for this method are 1) to preprocess training data +before calling set_train_data and 2) preprocess X_baseline for noisy +acquisition functions so that X_baseline is “preprocessed” with the +same transformations as the cached training inputs.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of (transformed) inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.transforms.input.ChainedInputTransform(**transforms)[source]
+

Bases: InputTransform, ModuleDict

+

An input transform representing the chaining of individual transforms.

+

Chaining of input transforms.

+
+
Parameters:
+

transforms (InputTransform) – The transforms to chain. Internally, the names of the +kwargs are used as the keys for accessing the individual +transforms on the module.

+
+
+

Example

+
>>> tf1 = Normalize(d=2)
+>>> tf2 = Normalize(d=2)
+>>> tf = ChainedInputTransform(tf1=tf1, tf2=tf2)
+>>> list(tf.keys())
+['tf1', 'tf2']
+>>> tf["tf1"]
+Normalize()
+
+
+
+
+transform(X)[source]
+

Transform the inputs to a model.

+

Individual transforms are applied in sequence.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+untransform(X)[source]
+

Un-transform the inputs to a model.

+

Un-transforms of the individual transforms are applied in reverse sequence.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of transformed inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of un-transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+
+
Parameters:
+

other (InputTransform) – Another input transform.

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+preprocess_transform(X)[source]
+

Apply transforms for preprocessing inputs.

+

The main use cases for this method are 1) to preprocess training data +before calling set_train_data and 2) preprocess X_baseline for noisy +acquisition functions so that X_baseline is “preprocessed” with the +same transformations as the cached training inputs.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of (transformed) inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.transforms.input.ReversibleInputTransform(*args, **kwargs)[source]
+

Bases: InputTransform, ABC

+

An abstract class for a reversible input transform.

+
+
Properties:
+
reverse: A boolean indicating if the functionality of transform

and untransform methods should be swapped.

+
+
+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+reverse: bool
+
+
+
+transform(X)[source]
+

Transform the inputs.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+untransform(X)[source]
+

Un-transform the inputs.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of un-transformed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+
+
Parameters:
+

other (InputTransform) – Another input transform.

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+
+class botorch.models.transforms.input.AffineInputTransform(d, coefficient, offset, indices=None, batch_shape=(), transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True, reverse=False)[source]
+

Bases: ReversibleInputTransform

+

Apply affine transformation to input:

+
+

output = (input - offset) / coefficient

+
+
+
Parameters:
+
    +
  • d (int) – The dimension of the input space.

  • +
  • coefficient (Tensor) – Tensor of linear coefficients, shape must to be +broadcastable with (batch_shape x n x d)-dim input tensors.

  • +
  • offset (Tensor) – Tensor of offset coefficients, shape must to be +broadcastable with (batch_shape x n x d)-dim input tensors.

  • +
  • indices (list[int] | Tensor | None) – The indices of the inputs to transform. If omitted, +take all dimensions of the inputs into account. Either a list of ints +or a Tensor of type torch.long.

  • +
  • batch_shape (torch.Size) – The batch shape of the inputs (assuming input tensors +of shape batch_shape x n x d). If provided, perform individual +transformation per batch, otherwise uses a single transformation.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transform in train() mode. Default: True.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: True.

  • +
  • reverse (bool) – A boolean indicating whether the forward pass should untransform +the inputs.

  • +
+
+
+
+
+property coefficient: Tensor
+

The tensor of linear coefficients.

+
+
+
+property offset: Tensor
+

The tensor of offset coefficients.

+
+
+
+property learn_coefficients: bool
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+
+
Parameters:
+

other (InputTransform) – Another input transform.

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+
+class botorch.models.transforms.input.Normalize(d, indices=None, bounds=None, batch_shape=(), transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True, reverse=False, min_range=1e-08, learn_bounds=None, almost_zero=1e-12)[source]
+

Bases: AffineInputTransform

+

Normalize the inputs to the unit cube.

+

If no explicit bounds are provided this module is stateful: If in train mode, +calling forward updates the module state (i.e. the normalizing bounds). If +in eval mode, calling forward simply applies the normalization using the +current module state.

+

Normalize the inputs to the unit cube.

+
+
Parameters:
+
    +
  • d (int) – The dimension of the input space.

  • +
  • indices (list[int] | Tensor | None) – The indices of the inputs to normalize. If omitted, +take all dimensions of the inputs into account.

  • +
  • bounds (Tensor | None) – If provided, use these bounds to normalize the inputs. If +omitted, learn the bounds in train mode.

  • +
  • batch_shape (torch.Size) – The batch shape of the inputs (assuming input tensors +of shape batch_shape x n x d). If provided, perform individual +normalization per batch, otherwise uses a single normalization.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: True.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: True.

  • +
  • reverse (bool) – A boolean indicating whether the forward pass should untransform +the inputs.

  • +
  • min_range (float) – If the range of an input dimension is smaller than min_range, +that input dimension will not be normalized. This is equivalent to +using bounds of [0, 1] for this dimension, and helps avoid division +by zero errors and related numerical issues. See the example below. +NOTE: This only applies if learn_bounds=True.

  • +
  • learn_bounds (bool | None) – Whether to learn the bounds in train mode. Defaults +to False if bounds are provided, otherwise defaults to True.

  • +
  • almost_zero (float)

  • +
+
+
+

Example

+
>>> t = Normalize(d=2)
+>>> t(torch.tensor([[3., 2.], [3., 6.]]))
+... tensor([[3., 2.],
+...         [3., 6.]])
+>>> t.eval()
+... Normalize()
+>>> t(torch.tensor([[3.5, 2.8]]))
+... tensor([[3.5, 0.2]])
+>>> t.bounds
+... tensor([[0., 2.],
+...         [1., 6.]])
+>>> t.coefficient
+... tensor([[1., 4.]])
+
+
+
+
+property ranges
+
+
+
+property mins
+
+
+
+property bounds: Tensor
+

The bounds used for normalizing the inputs.

+
+
+
+property learn_bounds: bool
+
+
+
+get_init_args()[source]
+

Get the arguments necessary to construct an exact copy of the transform.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+class botorch.models.transforms.input.InputStandardize(d, indices=None, batch_shape=(), transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True, reverse=False, min_std=1e-08)[source]
+

Bases: AffineInputTransform

+

Standardize inputs (zero mean, unit variance).

+

In train mode, calling forward updates the module state +(i.e. the mean/std normalizing constants). If in eval mode, calling forward +simply applies the standardization using the current module state.

+

Standardize inputs (zero mean, unit variance).

+
+
Parameters:
+
    +
  • d (int) – The dimension of the input space.

  • +
  • indices (list[int] | Tensor | None) – The indices of the inputs to standardize. If omitted, +take all dimensions of the inputs into account.

  • +
  • batch_shape (torch.Size) – The batch shape of the inputs (asssuming input tensors +of shape batch_shape x n x d). If provided, perform individual +normalization per batch, otherwise uses a single normalization.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: True

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True

  • +
  • reverse (bool) – A boolean indicating whether the forward pass should untransform +the inputs.

  • +
  • min_std (float) – If the standard deviation of an input dimension is smaller than +min_std, that input dimension will not be standardized. This is +equivalent to using a standard deviation of 1.0 and a mean of 0.0 for +this dimension, and helps avoid division by zero errors and related +numerical issues.

  • +
  • transform_on_fantasize (bool)

  • +
+
+
+
+
+property stds
+
+
+
+property means
+
+
+
+
+class botorch.models.transforms.input.Round(integer_indices=None, categorical_features=None, transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True, approximate=False, tau=0.001)[source]
+

Bases: InputTransform

+

A discretization transformation for discrete inputs.

+

If approximate=False (the default), uses PyTorch’s round.

+

If approximate=True, a differentiable approximate rounding function is +used, with a temperature parameter of tau. This method is a piecewise +approximation of a rounding function where each piece is a hyperbolic +tangent function.

+

For integers, this will typically be used in conjunction +with normalization as follows:

+

In eval() mode (i.e. after training), the inputs pass +would typically be normalized to the unit cube (e.g. during candidate +optimization). 1. These are unnormalized back to the raw input space. +2. The integers are rounded. 3. All values are normalized to the unit +cube.

+

In train() mode, the inputs can either (a) be normalized to the unit +cube or (b) provided using their raw values. In the case of (a) +transform_on_train should be set to True, so that the normalized inputs +are unnormalized before rounding. In the case of (b) transform_on_train +should be set to False, so that the raw inputs are rounded and then +normalized to the unit cube.

+

By default, the straight through estimators are used for the gradients as +proposed in [Daulton2022bopr]. This transformation supports differentiable +approximate rounding (currently only for integers). The rounding function +is approximated with a piece-wise function where each piece is a hyperbolic +tangent function.

+

For categorical parameters, the input must be one-hot encoded.

+

Example

+
>>> bounds = torch.tensor([[0, 5], [0, 1], [0, 1]]).t()
+>>> integer_indices = [0]
+>>> categorical_features = {1: 2}
+>>> unnormalize_tf = Normalize(
+>>>     d=d,
+>>>     bounds=bounds,
+>>>     transform_on_eval=True,
+>>>     transform_on_train=True,
+>>>     reverse=True,
+>>> )
+>>> round_tf = Round(integer_indices, categorical_features)
+>>> normalize_tf = Normalize(d=d, bounds=bounds)
+>>> tf = ChainedInputTransform(
+>>>     tf1=unnormalize_tf, tf2=round_tf, tf3=normalize_tf
+>>> )
+
+
+

Initialize transform.

+
+
Parameters:
+
    +
  • integer_indices (list[int] | LongTensor | None) – The indices of the integer inputs.

  • +
  • categorical_features (dict[int, int] | None) – A dictionary mapping the starting index of each +categorical feature to its cardinality. This assumes that categoricals +are one-hot encoded.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: True.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: True.

  • +
  • approximate (bool) – A boolean indicating whether approximate or exact +rounding should be used. Default: False.

  • +
  • tau (float) – The temperature parameter for approximate rounding.

  • +
+
+
+
+
+transform(X)[source]
+

Discretize the inputs.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of discretized inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+
+
Parameters:
+

other (InputTransform) – Another input transform.

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+get_init_args()[source]
+

Get the arguments necessary to construct an exact copy of the transform.

+
+
Return type:
+

dict[str, Any]

+
+
+
+
+
+
+class botorch.models.transforms.input.Log10(indices, transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True, reverse=False)[source]
+

Bases: ReversibleInputTransform

+

A base-10 log transformation.

+

Initialize transform.

+
+
Parameters:
+
    +
  • indices (list[int]) – The indices of the inputs to log transform.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: True.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: True.

  • +
  • reverse (bool) – A boolean indicating whether the forward pass should untransform +the inputs.

  • +
+
+
+
+
+
+class botorch.models.transforms.input.Warp(indices, transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True, reverse=False, eps=1e-07, concentration1_prior=None, concentration0_prior=None, batch_shape=None)[source]
+

Bases: ReversibleInputTransform, Module

+

A transform that uses learned input warping functions.

+

Each specified input dimension is warped using the CDF of a +Kumaraswamy distribution. Typically, MAP estimates of the +parameters of the Kumaraswamy distribution, for each input +dimension, are learned jointly with the GP hyperparameters.

+

TODO: implement support using independent warping functions +for each output in batched multi-output and multi-task models.

+

For now, ModelListGPs should be used to learn independent warping +functions for each output.

+

Initialize transform.

+
+
Parameters:
+
    +
  • indices (list[int]) – The indices of the inputs to warp.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: True.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: True.

  • +
  • reverse (bool) – A boolean indicating whether the forward pass should untransform +the inputs.

  • +
  • eps (float) – A small value used to clip values to be in the interval (0, 1).

  • +
  • concentration1_prior (Prior | None) – A prior distribution on the concentration1 parameter +of the Kumaraswamy distribution.

  • +
  • concentration0_prior (Prior | None) – A prior distribution on the concentration0 parameter +of the Kumaraswamy distribution.

  • +
  • batch_shape (torch.Size | None) – An optional batch shape, for learning independent warping +parameters for each batch of inputs. This should match the input batch +shape of the model (i.e., train_X.shape[:-2]). +NOTE: This is only supported for single-output models.

  • +
+
+
+
+
+
+class botorch.models.transforms.input.AppendFeatures(feature_set=None, f=None, indices=None, fkwargs=None, skip_expand=False, transform_on_train=False, transform_on_eval=True, transform_on_fantasize=False)[source]
+

Bases: InputTransform

+

A transform that appends the input with a given set of features either +provided beforehand or generated on the fly via a callable.

+

As an example, the predefined set of features can be used with +RiskMeasureMCObjective to optimize risk measures as described in +[Cakmak2020risk]. A tutorial notebook implementing the rhoKG acqusition +function introduced in [Cakmak2020risk] can be found at +https://botorch.org/tutorials/risk_averse_bo_with_environmental_variables.

+

The steps for using this to obtain samples of a risk measure are as follows:

+
    +
  • Train a model on (x, w) inputs and the corresponding observations;

  • +
  • Pass in an instance of AppendFeatures with the feature_set denoting the +samples of W as the input_transform to the trained model;

  • +
  • Call posterior(…).rsample(…) on the model with x inputs only to +get the joint posterior samples over (x, w)`s, where the `w`s come +from the `feature_set;

  • +
  • Pass these posterior samples through the RiskMeasureMCObjective of choice to +get the samples of the risk measure.

  • +
+

Note: The samples of the risk measure obtained this way are in general biased +since the feature_set does not fully represent the distribution of the +environmental variable.

+

Possible examples for using a callable include statistical models that are built on +PyTorch, built-in mathematical operations such as torch.sum, or custom scripted +functions. By this, this input transform allows for advanced feature engineering +and transfer learning models within the optimization loop.

+

Example

+
>>> # We consider 1D `x` and 1D `w`, with `W` having a
+>>> # uniform distribution over [0, 1]
+>>> model = SingleTaskGP(
+...     train_X=torch.rand(10, 2),
+...     train_Y=torch.randn(10, 1),
+...     input_transform=AppendFeatures(feature_set=torch.rand(10, 1))
+... )
+>>> mll = ExactMarginalLogLikelihood(model.likelihood, model)
+>>> fit_gpytorch_mll(mll)
+>>> test_x = torch.rand(3, 1)
+>>> # `posterior_samples` is a `10 x 30 x 1`-dim tensor
+>>> posterior_samples = model.posterior(test_x).rsamples(torch.size([10]))
+>>> risk_measure = VaR(alpha=0.8, n_w=10)
+>>> # `risk_measure_samples` is a `10 x 3`-dim tensor of samples of the
+>>> # risk measure VaR
+>>> risk_measure_samples = risk_measure(posterior_samples)
+
+
+

Append feature_set to each input or generate a set of features to +append on the fly via a callable.

+
+
Parameters:
+
    +
  • feature_set (Tensor | None) – An n_f x d_f-dim tensor denoting the features to be +appended to the inputs. Default: None.

  • +
  • f (Callable[[Tensor], Tensor] | None) – A callable mapping a batch_shape x q x d-dim input tensor X +to a batch_shape x q x n_f x d_f-dimensional output tensor. +Default: None.

  • +
  • indices (list[int] | None) – List of indices denoting the indices of the features to be +passed into f. Per default all features are passed to f. +Default: None.

  • +
  • fkwargs (dict[str, Any] | None) – Dictionary of keyword arguments passed to the callable f. +Default: None.

  • +
  • skip_expand (bool) – A boolean indicating whether to expand the input tensor +before appending features. This is intended for use with an +InputPerturbation. If True, the input tensor will be expected +to be of shape batch_shape x (q * n_f) x d. Not implemented +in combination with a callable.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: False.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: False.

  • +
+
+
+
+
+is_one_to_many: bool = True
+
+
+
+transform(X)[source]
+

Transform the inputs by appending feature_set to each input or +by generating a set of features to be appended on the fly via a callable.

+

For each 1 x d-dim element in the input tensor, this will produce +an n_f x (d + d_f)-dim tensor with feature_set appended as the last d_f +dimensions. For a generic batch_shape x q x d-dim X, this translates to a +batch_shape x (q * n_f) x (d + d_f)-dim output, where the values corresponding +to X[…, i, :] are found in output[…, i * n_f: (i + 1) * n_f, :].

+

Note: Adding the feature_set on the q-batch dimension is necessary to avoid +introducing additional bias by evaluating the inputs on independent GP +sample paths.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d-dim tensor of inputs. If self.skip_expand is +True, then X should be of shape batch_shape x (q * n_f) x d, +typically obtained by passing a batch_shape x q x d shape input +through an InputPerturbation with n_f perturbation values.

+
+
Returns:
+

A batch_shape x (q * n_f) x (d + d_f)-dim tensor of appended inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.transforms.input.InteractionFeatures(indices=None)[source]
+

Bases: AppendFeatures

+

A transform that appends the first-order interaction terms $x_i * x_j, i < j$, +for all or a subset of the input variables.

+

Initializes the InteractionFeatures transform.

+
+
Parameters:
+

indices (list[int] | None) – Indices of the subset of dimensions to compute interaction +features on.

+
+
+
+
+
+class botorch.models.transforms.input.FilterFeatures(feature_indices, transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True)[source]
+

Bases: InputTransform

+

A transform that filters the input with a given set of features indices.

+

As an example, this can be used in a multiobjective optimization with ModelListGP +in which the specific models only share subsets of features (feature selection). +A reason could be that it is known that specific features do not have any impact on +a specific objective but they need to be included in the model for another one.

+

Filter features from a model.

+
+
Parameters:
+
    +
  • feature_set – An one-dim tensor denoting the indices of the features to be +kept and fed to the model.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: True.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: True.

  • +
  • feature_indices (Tensor)

  • +
+
+
+
+
+transform(X)[source]
+

Transform the inputs by keeping only the in feature_indices specified +feature indices and filtering out the others.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d-dim tensor of inputs.

+
+
Returns:
+

+
A batch_shape x q x e-dim tensor of filtered inputs,

where e is the length of feature_indices.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+
+
Parameters:
+

other (InputTransform) – Another input transform

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+
+class botorch.models.transforms.input.InputPerturbation(perturbation_set, bounds=None, indices=None, multiplicative=False, transform_on_train=False, transform_on_eval=True, transform_on_fantasize=False)[source]
+

Bases: InputTransform

+

A transform that adds the set of perturbations to the given input.

+

Similar to AppendFeatures, this can be used with RiskMeasureMCObjective +to optimize risk measures. See AppendFeatures for additional discussion +on optimizing risk measures.

+

A tutorial notebook using this with qNoisyExpectedImprovement can be found at +https://botorch.org/tutorials/risk_averse_bo_with_input_perturbations.

+

Add perturbation_set to each input.

+
+
Parameters:
+
    +
  • perturbation_set (Tensor | Callable[[Tensor], Tensor]) – An n_p x d-dim tensor denoting the perturbations +to be added to the inputs. Alternatively, this can be a callable that +returns batch x n_p x d-dim tensor of perturbations for input of +shape batch x d. This is useful for heteroscedastic perturbations.

  • +
  • bounds (Tensor | None) – A 2 x d-dim tensor of lower and upper bounds for each +column of the input. If given, the perturbed inputs will be +clamped to these bounds.

  • +
  • indices (list[int] | None) – A list of indices specifying a subset of inputs on which to apply +the transform. Note that len(indices) should be equal to the second +dimension of perturbation_set and bounds. The dimensionality of +the input X.shape[-1] can be larger if we only transform a subset.

  • +
  • multiplicative (bool) – A boolean indicating whether the input perturbations +are additive or multiplicative. If True, inputs will be multiplied +with the perturbations.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: False.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: False.

  • +
+
+
+
+
+is_one_to_many: bool = True
+
+
+
+transform(X)[source]
+

Transform the inputs by adding perturbation_set to each input.

+

For each 1 x d-dim element in the input tensor, this will produce +an n_p x d-dim tensor with the perturbation_set added to the input. +For a generic batch_shape x q x d-dim X, this translates to a +batch_shape x (q * n_p) x d-dim output, where the values corresponding +to X[…, i, :] are found in output[…, i * n_w: (i + 1) * n_w, :].

+

Note: Adding the perturbation_set on the q-batch dimension is necessary +to avoid introducing additional bias by evaluating the inputs on independent +GP sample paths.

+
+
Parameters:
+

X (Tensor) – A batch_shape x q x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x (q * n_p) x d-dim tensor of perturbed inputs.

+
+
Return type:
+

Tensor

+
+
+
+
+
+property batch_shape
+

Returns a shape tuple such that subset_transform pre-allocates +a (b x n_p x n x d) - dim tensor, where b is the batch shape of the +input X of the transform and n_p is the number of perturbations. +NOTE: this function is dependent on calling _expanded_perturbations(X) +because n_p is inaccessible otherwise if perturbation_set is a function.

+
+
+
+
+class botorch.models.transforms.input.OneHotToNumeric(dim, categorical_features=None, transform_on_train=True, transform_on_eval=True, transform_on_fantasize=True)[source]
+

Bases: InputTransform

+

Transform categorical parameters from a one-hot to a numeric representation.

+

Initialize.

+
+
Parameters:
+
    +
  • dim (int) – The dimension of the one-hot-encoded input.

  • +
  • categorical_features (dict[int, int] | None) – A dictionary mapping the starting index of each +categorical feature to its cardinality. This assumes that categoricals +are one-hot encoded.

  • +
  • transform_on_train (bool) – A boolean indicating whether to apply the +transforms in train() mode. Default: False.

  • +
  • transform_on_eval (bool) – A boolean indicating whether to apply the +transform in eval() mode. Default: True.

  • +
  • transform_on_fantasize (bool) – A boolean indicating whether to apply the +transform when called from within a fantasize call. Default: False.

  • +
+
+
Returns:
+

A batch_shape x n x d’-dim tensor of where the one-hot encoded +categoricals are transformed to integer representation.

+
+
+
+
+transform(X)[source]
+

Transform the categorical inputs into integer representation.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of inputs.

+
+
Returns:
+

A batch_shape x n x d’-dim tensor of where the one-hot encoded +categoricals are transformed to integer representation.

+
+
Return type:
+

Tensor

+
+
+
+
+
+untransform(X)[source]
+

Transform the categoricals from integer representation to one-hot.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d’-dim tensor of transformed inputs, where +the categoricals are represented as integers.

+
+
Returns:
+

A batch_shape x n x d-dim tensor of inputs, where the categoricals +have been transformed to one-hot representation.

+
+
Return type:
+

Tensor

+
+
+
+
+
+equals(other)[source]
+

Check if another input transform is equivalent.

+
+
Parameters:
+

other (InputTransform) – Another input transform.

+
+
Returns:
+

A boolean indicating if the other transform is equivalent.

+
+
Return type:
+

bool

+
+
+
+
+
+
+

Transform Factory Methods

+
+
+botorch.models.transforms.factory.get_rounding_input_transform(one_hot_bounds, integer_indices=None, categorical_features=None, initialization=False, return_numeric=False, approximate=False)[source]
+

Get a rounding input transform.

+

The rounding function will take inputs from the unit cube, +unnormalize the integers raw search space, round the inputs, +and normalize them back to the unit cube.

+

Categoricals are assumed to be one-hot encoded. Integers are +currently assumed to be contiguous ranges (e.g. [1,2,3] and not +[1,5,7]).

+

TODO: support non-contiguous sets of integers by modifying +the rounding function.

+
+
Parameters:
+
    +
  • one_hot_bounds (Tensor) – The raw search space bounds where categoricals are +encoded in one-hot representation and the integer parameters +are not normalized.

  • +
  • integer_indices (list[int] | None) – The indices of the integer parameters.

  • +
  • categorical_features (dict[int, int] | None) – A dictionary mapping indices to cardinalities +for the categorical features.

  • +
  • initialization (bool) – A boolean indicating whether this exact rounding +function is for initialization. For initialization, the bounds +for are expanded such that the end point of a range is selected +with same probability that an interior point is selected, after +rounding.

  • +
  • return_numeric (bool) – A boolean indicating whether to return numeric or +one-hot encoded categoricals. Returning a nummeric +representation is helpful if the downstream code (e.g. kernel) +expects a numeric representation of the categoricals.

  • +
  • approximate (bool) – A boolean indicating whether to use an approximate +rounding function.

  • +
+
+
Returns:
+

The rounding function ChainedInputTransform.

+
+
Return type:
+

ChainedInputTransform

+
+
+
+
+
+

Transform Utilities

+
+
+botorch.models.transforms.utils.lognorm_to_norm(mu, Cov)[source]
+

Compute mean and covariance of a MVN from those of the associated log-MVN

+

If Y is log-normal with mean mu_ln and covariance Cov_ln, then +X ~ N(mu_n, Cov_n) with

+
+

Cov_n_{ij} = log(1 + Cov_ln_{ij} / (mu_ln_{i} * mu_n_{j})) +mu_n_{i} = log(mu_ln_{i}) - 0.5 * log(1 + Cov_ln_{ii} / mu_ln_{i}**2)

+
+
+
Parameters:
+
    +
  • mu (Tensor) – A batch_shape x n mean vector of the log-Normal distribution.

  • +
  • Cov (Tensor) – A batch_shape x n x n covariance matrix of the log-Normal +distribution.

  • +
+
+
Returns:
+

    +
  • The batch_shape x n mean vector of the Normal distribution

  • +
  • The batch_shape x n x n covariance matrix of the Normal distribution

  • +
+

+
+
Return type:
+

A two-tuple containing

+
+
+
+
+
+botorch.models.transforms.utils.norm_to_lognorm(mu, Cov)[source]
+

Compute mean and covariance of a log-MVN from its MVN sufficient statistics

+

If X ~ N(mu, Cov) and Y = exp(X), then Y is log-normal with

+
+

mu_ln_{i} = exp(mu_{i} + 0.5 * Cov_{ii}) +Cov_ln_{ij} = exp(mu_{i} + mu_{j} + 0.5 * (Cov_{ii} + Cov_{jj})) * +(exp(Cov_{ij}) - 1)

+
+
+
Parameters:
+
    +
  • mu (Tensor) – A batch_shape x n mean vector of the Normal distribution.

  • +
  • Cov (Tensor) – A batch_shape x n x n covariance matrix of the Normal distribution.

  • +
+
+
Returns:
+

    +
  • The batch_shape x n mean vector of the log-Normal distribution.

  • +
  • +
    The batch_shape x n x n covariance matrix of the log-Normal

    distribution.

    +
    +
    +
  • +
+

+
+
Return type:
+

A two-tuple containing

+
+
+
+
+
+botorch.models.transforms.utils.norm_to_lognorm_mean(mu, var)[source]
+

Compute mean of a log-MVN from its MVN marginals

+
+
Parameters:
+
    +
  • mu (Tensor) – A batch_shape x n mean vector of the Normal distribution.

  • +
  • var (Tensor) – A batch_shape x n variance vectorof the Normal distribution.

  • +
+
+
Returns:
+

The batch_shape x n mean vector of the log-Normal distribution.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.models.transforms.utils.norm_to_lognorm_variance(mu, var)[source]
+

Compute variance of a log-MVN from its MVN marginals

+
+
Parameters:
+
    +
  • mu (Tensor) – A batch_shape x n mean vector of the Normal distribution.

  • +
  • var (Tensor) – A batch_shape x n variance vectorof the Normal distribution.

  • +
+
+
Returns:
+

The batch_shape x n variance vector of the log-Normal distribution.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.models.transforms.utils.expand_and_copy_tensor(X, batch_shape)[source]
+

Expand and copy X according to batch_shape.

+
+
Parameters:
+
    +
  • X (Tensor) – A input_batch_shape x n x d-dim tensor of inputs.

  • +
  • batch_shape (Size) – The new batch shape.

  • +
+
+
Returns:
+

A new_batch_shape x n x d-dim tensor of inputs, where new_batch_shape +is input_batch_shape against batch_shape.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.models.transforms.utils.subset_transform(transform)[source]
+

Decorator of an input transform function to separate out indexing logic.

+
+
+
+botorch.models.transforms.utils.interaction_features(X)[source]
+

Computes the interaction features between the inputs.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x q x d-dim tensor of inputs.

  • +
  • indices – The input dimensions to generate interaction features for.

  • +
+
+
Returns:
+

A n x q x 1 x (d * (d-1) / 2))-dim tensor of interaction features.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Utilities

+
+

GPyTorch Module Constructors

+

Pre-packaged kernels for bayesian optimization, including a Scale/Matern +kernel that is well-suited to low-dimensional high-noise problems, and +a dimension-agnostic RBF kernel without outputscale.

+

References:

+
+
+[Hvarfner2024vanilla] +(1,2,3) +

C. Hvarfner, E. O. Hellsten, L. Nardi, +Vanilla Bayesian Optimization Performs Great in High Dimensions. +In International Conference on Machine Learning, 2024.

+
+
+
+
+botorch.models.utils.gpytorch_modules.get_matern_kernel_with_gamma_prior(ard_num_dims, batch_shape=None)[source]
+

Constructs the Scale-Matern kernel that is used by default by +several models. This uses a Gamma(3.0, 6.0) prior for the lengthscale +and a Gamma(2.0, 0.15) prior for the output scale.

+
+
Parameters:
+
    +
  • ard_num_dims (int)

  • +
  • batch_shape (Size | None)

  • +
+
+
Return type:
+

ScaleKernel

+
+
+
+
+
+botorch.models.utils.gpytorch_modules.get_gaussian_likelihood_with_gamma_prior(batch_shape=None)[source]
+

Constructs the GaussianLikelihood that is used by default by +several models. This uses a Gamma(1.1, 0.05) prior and constrains the +noise level to be greater than MIN_INFERRED_NOISE_LEVEL (=1e-4).

+
+
Parameters:
+

batch_shape (Size | None)

+
+
Return type:
+

GaussianLikelihood

+
+
+
+
+
+botorch.models.utils.gpytorch_modules.get_gaussian_likelihood_with_lognormal_prior(batch_shape=None)[source]
+

Return Gaussian likelihood with a LogNormal(-4.0, 1.0) prior. +This prior is based on [Hvarfner2024vanilla].

+
+
Parameters:
+

batch_shape (Size | None) – Batch shape for the likelihood.

+
+
Returns:
+

GaussianLikelihood with LogNormal(-4.0, 1.0) prior and constrains the +noise level to be greater than MIN_INFERRED_NOISE_LEVEL (=1e-4).

+
+
Return type:
+

GaussianLikelihood

+
+
+
+
+
+botorch.models.utils.gpytorch_modules.get_covar_module_with_dim_scaled_prior(ard_num_dims, batch_shape=None, use_rbf_kernel=True, active_dims=None)[source]
+

Returns an RBF or Matern kernel with priors +from [Hvarfner2024vanilla].

+
+
Parameters:
+
    +
  • ard_num_dims (int) – Number of feature dimensions for ARD.

  • +
  • batch_shape (Size | None) – Batch shape for the covariance module.

  • +
  • use_rbf_kernel (bool) – Whether to use an RBF kernel. If False, uses a Matern kernel.

  • +
  • active_dims (Sequence[int] | None) – The set of input dimensions to compute the covariances on. +By default, the covariance is computed using the full input tensor. +Set this if you’d like to ignore certain dimensions.

  • +
+
+
Returns:
+

A Kernel constructed according to the given arguments. The prior is constrained +to have lengthscales larger than 0.025 for numerical stability.

+
+
Return type:
+

MaternKernel | RBFKernel

+
+
+
+
+
+

Model Conversion

+

Utilities for converting between different models.

+
+
+botorch.models.converter.model_list_to_batched(model_list)[source]
+

Convert a ModelListGP to a BatchedMultiOutputGPyTorchModel.

+
+
Parameters:
+

model_list (ModelListGP) – The ModelListGP to be converted to the appropriate +BatchedMultiOutputGPyTorchModel. All sub-models must be of the same +type and have the shape (batch shape and number of training inputs).

+
+
Returns:
+

The model converted into a BatchedMultiOutputGPyTorchModel.

+
+
Return type:
+

BatchedMultiOutputGPyTorchModel

+
+
+

Example

+
>>> list_gp = ModelListGP(gp1, gp2)
+>>> batch_gp = model_list_to_batched(list_gp)
+
+
+
+
+
+botorch.models.converter.set_attribute(obj, attr, val)[source]
+

Like setattr but works with hierarchical attribute specification. +E.g. if obj=Zoo(), and attr=”tiger.age”, set_attribute(obj, attr, 3), +would set the Zoo’s tiger’s age to three.

+
+
Parameters:
+

attr (str)

+
+
+
+
+
+botorch.models.converter.get_attribute(obj, attr)[source]
+

Like getattr but works with hierarchical attribute specification. +E.g. if obj=Zoo(), and attr=”tiger.age”, get_attribute(obj, attr), +would return the Zoo’s tiger’s age.

+
+
Parameters:
+

attr (str)

+
+
+
+
+
+botorch.models.converter.batched_to_model_list(batch_model)[source]
+

Convert a BatchedMultiOutputGPyTorchModel to a ModelListGP.

+
+
Parameters:
+

batch_model (BatchedMultiOutputGPyTorchModel) – The BatchedMultiOutputGPyTorchModel to be converted to a +ModelListGP.

+
+
Returns:
+

The model converted into a ModelListGP.

+
+
Return type:
+

ModelListGP

+
+
+

Example

+
>>> train_X = torch.rand(5, 2)
+>>> train_Y = torch.rand(5, 2)
+>>> batch_gp = SingleTaskGP(train_X, train_Y)
+>>> list_gp = batched_to_model_list(batch_gp)
+
+
+
+
+
+botorch.models.converter.batched_multi_output_to_single_output(batch_mo_model)[source]
+

Convert a model from batched multi-output to a batched single-output.

+

Note: the underlying GPyTorch GP does not change. The GPyTorch GP’s batch_shape +(referred to as _aug_batch_shape) is still _input_batch_shape x num_outputs. +The only things that change are the attributes of the +BatchedMultiOutputGPyTorchModel that are responsible the internal accounting of +the number of outputs: namely, num_outputs, _input_batch_shape, and +_aug_batch_shape. +Initially for the batched MO models these are: num_outputs = m, +_input_batch_shape = train_X.batch_shape, and +_aug_batch_shape = train_X.batch_shape + torch.Size([num_outputs]). +In the new SO model, these are: num_outputs = 1, +_input_batch_shape = train_X.batch_shape + torch.Size([num_outputs]), +and _aug_batch_shape = train_X.batch_shape + torch.Size([num_outputs]).

+

This is a (hopefully) temporary measure until multi-output MVNs with +independent outputs have better support in GPyTorch (see +https://github.com/cornellius-gp/gpytorch/pull/1083).

+
+
Parameters:
+
+
+
Returns:
+

The model converted into a batch single-output model.

+
+
Return type:
+

BatchedMultiOutputGPyTorchModel

+
+
+

Example

+
>>> train_X = torch.rand(5, 2)
+>>> train_Y = torch.rand(5, 2)
+>>> batch_mo_gp = SingleTaskGP(train_X, train_Y, outcome_transform=None)
+>>> batch_so_gp = batched_multi_output_to_single_output(batch_mo_gp)
+
+
+
+
+
+

Inducing Point Allocators

+

Functionality for allocating the inducing points of sparse Gaussian +process models.

+

References

+
+
+[chen2018dpp] +(1,2) +

Laming Chen and Guoxin Zhang and Hanning Zhou, Fast greedy MAP inference +for determinantal point process to improve recommendation diversity, +Proceedings of the 32nd International Conference on Neural Information +Processing Systems, 2018, https://arxiv.org/abs/1709.05135.

+
+
+
+
+class botorch.models.utils.inducing_point_allocators.InducingPointAllocator[source]
+

Bases: ABC

+

This class provides functionality to initialize the inducing point locations +of an inducing point-based model, e.g. a SingleTaskVariationalGP.

+
+
+allocate_inducing_points(inputs, covar_module, num_inducing, input_batch_shape)[source]
+

Initialize the num_inducing inducing point locations according to a +specific initialization strategy. todo say something about quality

+
+
Parameters:
+
    +
  • inputs (Tensor) – A (*batch_shape, n, d)-dim input data tensor.

  • +
  • covar_module (Module) – GPyTorch Module returning a LinearOperator kernel matrix.

  • +
  • num_inducing (int) – The maximun number (m) of inducing points (m <= n).

  • +
  • input_batch_shape (Size) – The non-task-related batch shape.

  • +
+
+
Returns:
+

A (*batch_shape, m, d)-dim tensor of inducing point locations.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.models.utils.inducing_point_allocators.QualityFunction[source]
+

Bases: ABC

+

A function that scores inputs with respect +to a specific criterion.

+
+
+
+class botorch.models.utils.inducing_point_allocators.UnitQualityFunction[source]
+

Bases: QualityFunction

+

A function returning ones for each element. Using this quality function +for inducing point allocation corresponds to allocating inducing points +with the sole aim of minimizing predictive variance, i.e. the approach +of [burt2020svgp].

+
+
+
+class botorch.models.utils.inducing_point_allocators.ExpectedImprovementQualityFunction(model, maximize)[source]
+

Bases: QualityFunction

+

A function measuring the quality of input points as their expected +improvement with respect to a conservative baseline. Expectations +are according to the model from the previous BO step. See [moss2023ipa] +for details and justification.

+
+
Parameters:
+
    +
  • model (Model) – The model fitted during the previous BO step. For now, this +must be a single task model (i.e. num_outputs=1).

  • +
  • maximize (bool) – Set True if we are performing function maximization, else +set False.

  • +
+
+
+
+
+
+class botorch.models.utils.inducing_point_allocators.GreedyVarianceReduction[source]
+

Bases: InducingPointAllocator

+

The inducing point allocator proposed by [burt2020svgp], that +greedily chooses inducing point locations with maximal (conditional) +predictive variance.

+
+
+
+class botorch.models.utils.inducing_point_allocators.GreedyImprovementReduction(model, maximize)[source]
+

Bases: InducingPointAllocator

+

An inducing point allocator that greedily chooses inducing points with large +predictive variance and that are in promising regions of the search +space (according to the model form the previous BO step), see [moss2023ipa].

+
+
Parameters:
+
    +
  • model (Model) – The model fitted during the previous BO step.

  • +
  • maximize (bool) – Set True if we are performing function maximization, else +set False.

  • +
+
+
+
+
+
+botorch.models.utils.inducing_point_allocators._pivoted_cholesky_init(train_inputs, kernel_matrix, max_length, quality_scores, epsilon=1e-06)[source]
+

A pivoted Cholesky initialization method for the inducing points, +originally proposed in [burt2020svgp] with the algorithm itself coming from +[chen2018dpp]. Code is a PyTorch version from [chen2018dpp], based on +https://github.com/laming-chen/fast-map-dpp/blob/master/dpp.py but with a small +modification to allow the underlying DPP to be defined through its diversity-quality +decomposition,as discussed by [moss2023ipa]. This method returns a greedy +approximation of the MAP estimate of the specified DPP, i.e. its returns a +set of points that are highly diverse (according to the provided kernel_matrix) +and have high quality (according to the provided quality_scores).

+
+
Parameters:
+
    +
  • train_inputs (Tensor) – training inputs (of shape n x d)

  • +
  • kernel_matrix (Tensor | LinearOperator) – kernel matrix on the training inputs

  • +
  • max_length (int) – number of inducing points to initialize

  • +
  • quality_scores (Tensor) – scores representing the quality of each candidate +input (of shape [n])

  • +
  • epsilon (float) – numerical jitter for stability.

  • +
+
+
Returns:
+

max_length x d tensor of the training inputs corresponding to the top +max_length pivots of the training kernel matrix

+
+
Return type:
+

Tensor

+
+
+
+
+
+

Other Utilties

+

Assorted helper methods and objects for working with BoTorch models.

+
+
+botorch.models.utils.assorted.multioutput_to_batch_mode_transform(train_X, train_Y, num_outputs, train_Yvar=None)[source]
+

Transforms training inputs for a multi-output model.

+

Used for multi-output models that internally are represented by a +batched single output model, where each output is modeled as an +independent batch.

+
+
Parameters:
+
    +
  • train_X (Tensor) – A n x d or input_batch_shape x n x d (batch mode) tensor of +training features.

  • +
  • train_Y (Tensor) – A n x m or target_batch_shape x n x m (batch mode) tensor of +training observations.

  • +
  • num_outputs (int) – number of outputs

  • +
  • train_Yvar (Tensor | None) – A n x m or target_batch_shape x n x m tensor of observed +measurement noise.

  • +
+
+
Returns:
+

3-element tuple containing

+
    +
  • A input_batch_shape x m x n x d tensor of training features.

  • +
  • A target_batch_shape x m x n tensor of training observations.

  • +
  • A target_batch_shape x m x n tensor observed measurement noise.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor, Tensor | None]

+
+
+
+
+
+botorch.models.utils.assorted.add_output_dim(X, original_batch_shape)[source]
+

Insert the output dimension at the correct location.

+

The trailing batch dimensions of X must match the original batch dimensions +of the training inputs, but can also include extra batch dimensions.

+
+
Parameters:
+
    +
  • X (Tensor) – A (new_batch_shape) x (original_batch_shape) x n x d tensor of +features.

  • +
  • original_batch_shape (Size) – the batch shape of the model’s training inputs.

  • +
+
+
Returns:
+

2-element tuple containing

+
    +
  • +
    A (new_batch_shape) x (original_batch_shape) x m x n x d tensor of

    features.

    +
    +
    +
  • +
  • The index corresponding to the output dimension.

  • +
+

+
+
Return type:
+

tuple[Tensor, int]

+
+
+
+
+
+botorch.models.utils.assorted.check_no_nans(Z)[source]
+

Check that tensor does not contain NaN values.

+

Raises an InputDataError if Z contains NaN values.

+
+
Parameters:
+

Z (Tensor) – The input tensor.

+
+
Return type:
+

None

+
+
+
+
+
+botorch.models.utils.assorted.check_min_max_scaling(X, strict=False, atol=0.01, raise_on_fail=False, ignore_dims=None)[source]
+

Check that tensor is normalized to the unit cube.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x n x d input tensor. Typically the training inputs +of a model.

  • +
  • strict (bool) – If True, require X to be scaled to the unit cube (rather than +just to be contained within the unit cube).

  • +
  • atol (float) – The tolerance for the boundary check. Only used if strict=True.

  • +
  • raise_on_fail (bool) – If True, raise an exception instead of a warning.

  • +
  • ignore_dims (list[int] | None) – Subset of dimensions where the min-max scaling check is omitted.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+botorch.models.utils.assorted.check_standardization(Y, atol_mean=0.01, atol_std=0.01, raise_on_fail=False)[source]
+

Check that tensor is standardized (zero mean, unit variance).

+
+
Parameters:
+
    +
  • Y (Tensor) – The input tensor of shape batch_shape x n x m. Typically the +train targets of a model. Standardization is checked across the +n-dimension.

  • +
  • atol_mean (float) – The tolerance for the mean check.

  • +
  • atol_std (float) – The tolerance for the std check.

  • +
  • raise_on_fail (bool) – If True, raise an exception instead of a warning.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+botorch.models.utils.assorted.validate_input_scaling(train_X, train_Y, train_Yvar=None, raise_on_fail=False, ignore_X_dims=None)[source]
+

Helper function to validate input data to models.

+
+
Parameters:
+
    +
  • train_X (Tensor) – A n x d or batch_shape x n x d (batch mode) tensor of +training features.

  • +
  • train_Y (Tensor) – A n x m or batch_shape x n x m (batch mode) tensor of +training observations.

  • +
  • train_Yvar (Tensor | None) – A batch_shape x n x m or batch_shape x n x m (batch mode) +tensor of observed measurement noise.

  • +
  • raise_on_fail (bool) – If True, raise an error instead of emitting a warning +(only for normalization/standardization checks, an error is always +raised if NaN values are present).

  • +
  • ignore_X_dims (list[int] | None) – For this subset of dimensions from {1, …, d}, ignore the +min-max scaling check.

  • +
+
+
Return type:
+

None

+
+
+

This function is typically called inside the constructor of standard BoTorch +models. It validates the following: +(i) none of the inputs contain NaN values +(ii) the training data (train_X) is normalized to the unit cube for all +dimensions except those in ignore_X_dims. +(iii) the training targets (train_Y) are standardized (zero mean, unit var) +No checks (other than the NaN check) are performed for observed variances +(train_Yvar) at this point.

+
+
+
+botorch.models.utils.assorted.mod_batch_shape(module, names, b)[source]
+

Recursive helper to modify gpytorch modules’ batch shape attribute.

+

Modifies the module in-place.

+
+
Parameters:
+
    +
  • module (Module) – The module to be modified.

  • +
  • names (list[str]) – The list of names to access the attribute. If the full name of +the module is “module.sub_module.leaf_module”, this will be +[“sub_module”, “leaf_module”].

  • +
  • b (int) – The new size of the last element of the module’s batch_shape +attribute.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+botorch.models.utils.assorted.gpt_posterior_settings()[source]
+

Context manager for settings used for computing model posteriors.

+
+
+
+botorch.models.utils.assorted.detect_duplicates(X, rtol=0, atol=1e-08)[source]
+

Returns an iterator over index pairs (duplicate index, original index) for all +duplicate entries of X. Supporting 2-d Tensor only.

+
+
Parameters:
+
    +
  • X (Tensor) – the datapoints tensor with potential duplicated entries

  • +
  • rtol (float) – relative tolerance

  • +
  • atol (float) – absolute tolerance

  • +
+
+
Return type:
+

Iterator[tuple[int, int]]

+
+
+
+
+
+botorch.models.utils.assorted.consolidate_duplicates(X, Y, rtol=0.0, atol=1e-08)[source]
+

Drop duplicated Xs and update the indices tensor Y accordingly. +Supporting 2d Tensor only as in batch mode block design is not guaranteed.

+
+
Parameters:
+
    +
  • X (Tensor) – the datapoints tensor

  • +
  • Y (Tensor) – the index tensor to be updated (e.g., pairwise comparisons)

  • +
  • rtol (float) – relative tolerance

  • +
  • atol (float) – absolute tolerance

  • +
+
+
Returns:
+

the consolidated X +consolidated_Y: the consolidated Y (e.g., pairwise comparisons indices) +new_indices: new index of each original item in X, a tensor of size X.shape[-2]

+
+
Return type:
+

consolidated_X

+
+
+
+
+
+class botorch.models.utils.assorted.fantasize(state=True)[source]
+

Bases: _Flag

+

A flag denoting whether we are currently in a fantasize context.

+
+
Parameters:
+

state (bool)

+
+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/optim.html b/website-old/pages/api/optim.html new file mode 100644 index 0000000000..98919f3cbc --- /dev/null +++ b/website-old/pages/api/optim.html @@ -0,0 +1,2686 @@ + + + + + + + +
+
+
+
+
+

botorch.optim

+
+

Optimization

+
+

Core

+

Core abstractions and generic optimizers.

+
+
+class botorch.optim.core.OptimizationStatus(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: int, Enum

+
+
+RUNNING = 1
+
+
+
+SUCCESS = 2
+
+
+
+FAILURE = 3
+
+
+
+STOPPED = 4
+
+
+
+
+class botorch.optim.core.OptimizationResult(step: 'int', fval: 'float | int', status: 'OptimizationStatus', runtime: 'float | None' = None, message: 'str | None' = None)[source]
+

Bases: object

+
+
Parameters:
+
    +
  • step (int)

  • +
  • fval (float | int)

  • +
  • status (OptimizationStatus)

  • +
  • runtime (float | None)

  • +
  • message (str | None)

  • +
+
+
+
+
+step: int
+
+
+
+fval: float | int
+
+
+
+status: OptimizationStatus
+
+
+
+runtime: float | None = None
+
+
+
+message: str | None = None
+
+
+
+
+botorch.optim.core.scipy_minimize(closure, parameters, bounds=None, callback=None, x0=None, method='L-BFGS-B', options=None, timeout_sec=None)[source]
+

Generic scipy.optimize.minimize-based optimization routine.

+
+
Parameters:
+
    +
  • closure (Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | NdarrayOptimizationClosure) – Callable that returns a tensor and an iterable of gradient tensors or +NdarrayOptimizationClosure instance.

  • +
  • parameters (dict[str, Tensor]) – A dictionary of tensors to be optimized.

  • +
  • bounds (dict[str, tuple[float | None, float | None]] | None) – A dictionary mapping parameter names to lower and upper bounds.

  • +
  • callback (Callable[[dict[str, Tensor], OptimizationResult], None] | None) – A callable taking parameters and an OptimizationResult as arguments.

  • +
  • x0 (ndarray[Any, dtype[_ScalarType_co]] | None) – An optional initialization vector passed to scipy.optimize.minimize.

  • +
  • method (str) – Solver type, passed along to scipy.minimize.

  • +
  • options (dict[str, Any] | None) – Dictionary of solver options, passed along to scipy.minimize.

  • +
  • timeout_sec (float | None) – Timeout in seconds to wait before aborting the optimization loop +if not converged (will return the best found solution thus far).

  • +
+
+
Returns:
+

An OptimizationResult summarizing the final state of the run.

+
+
Return type:
+

OptimizationResult

+
+
+
+
+
+botorch.optim.core.torch_minimize(closure, parameters, bounds=None, callback=None, optimizer=<class 'torch.optim.adam.Adam'>, scheduler=None, step_limit=None, timeout_sec=None, stopping_criterion=None)[source]
+

Generic torch.optim-based optimization routine.

+
+
Parameters:
+
    +
  • closure (Callable[[], tuple[Tensor, Sequence[Tensor | None]]]) – Callable that returns a tensor and an iterable of gradient tensors. +Responsible for setting relevant parameters’ grad attributes.

  • +
  • parameters (dict[str, Tensor]) – A dictionary of tensors to be optimized.

  • +
  • bounds (dict[str, tuple[float | None, float | None]] | None) – An optional dictionary of bounds for elements of parameters.

  • +
  • callback (Callable[[dict[str, Tensor], OptimizationResult], None] | None) – A callable taking parameters and an OptimizationResult as arguments.

  • +
  • optimizer (Optimizer | Callable[[list[Tensor]], Optimizer]) – A torch.optim.Optimizer instance or a factory that takes +a list of parameters and returns an Optimizer instance.

  • +
  • scheduler (LRScheduler | Callable[[Optimizer], LRScheduler] | None) – A torch.optim.lr_scheduler._LRScheduler instance or a factory +that takes a Optimizer instance and returns a _LRSchedule instance.

  • +
  • step_limit (int | None) – Integer specifying a maximum number of optimization steps. +One of step_limit, stopping_criterion, or timeout_sec must be passed.

  • +
  • timeout_sec (float | None) – Timeout in seconds before terminating the optimization loop. +One of step_limit, stopping_criterion, or timeout_sec must be passed.

  • +
  • stopping_criterion (Callable[[Tensor], bool] | None) – A StoppingCriterion for the optimization loop.

  • +
+
+
Returns:
+

An OptimizationResult summarizing the final state of the run.

+
+
Return type:
+

OptimizationResult

+
+
+
+
+
+

Acquisition Function Optimization

+

Methods for optimizing acquisition functions.

+
+
+class botorch.optim.optimize.OptimizeAcqfInputs(acq_function, bounds, q, num_restarts, raw_samples, options, inequality_constraints, equality_constraints, nonlinear_inequality_constraints, fixed_features, post_processing_func, batch_initial_conditions, return_best_only, gen_candidates, sequential, ic_generator=None, timeout_sec=None, return_full_tree=False, retry_on_optimization_warning=True, ic_gen_kwargs=<factory>)[source]
+

Bases: object

+

Container for inputs to optimize_acqf.

+

See docstring for optimize_acqf for explanation of parameters.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction)

  • +
  • bounds (Tensor)

  • +
  • q (int)

  • +
  • num_restarts (int)

  • +
  • raw_samples (int | None)

  • +
  • options (dict[str, bool | float | int | str] | None)

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • nonlinear_inequality_constraints (list[tuple[Callable, bool]] | None)

  • +
  • fixed_features (dict[int, float] | None)

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None)

  • +
  • batch_initial_conditions (Tensor | None)

  • +
  • return_best_only (bool)

  • +
  • gen_candidates (Callable[[Tensor, AcquisitionFunction, Any], tuple[Tensor, Tensor]])

  • +
  • sequential (bool)

  • +
  • ic_generator (Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None] | None)

  • +
  • timeout_sec (float | None)

  • +
  • return_full_tree (bool)

  • +
  • retry_on_optimization_warning (bool)

  • +
  • ic_gen_kwargs (dict)

  • +
+
+
+
+
+acq_function: AcquisitionFunction
+
+
+
+bounds: Tensor
+
+
+
+q: int
+
+
+
+num_restarts: int
+
+
+
+raw_samples: int | None
+
+
+
+options: dict[str, bool | float | int | str] | None
+
+
+
+inequality_constraints: list[tuple[Tensor, Tensor, float]] | None
+
+
+
+equality_constraints: list[tuple[Tensor, Tensor, float]] | None
+
+
+
+nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None
+
+
+
+fixed_features: dict[int, float] | None
+
+
+
+post_processing_func: Callable[[Tensor], Tensor] | None
+
+
+
+batch_initial_conditions: Tensor | None
+
+
+
+return_best_only: bool
+
+
+
+gen_candidates: Callable[[Tensor, AcquisitionFunction, Any], tuple[Tensor, Tensor]]
+
+
+
+sequential: bool
+
+
+
+ic_generator: Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None] | None = None
+
+
+
+timeout_sec: float | None = None
+
+
+
+return_full_tree: bool = False
+
+
+
+retry_on_optimization_warning: bool = True
+
+
+
+ic_gen_kwargs: dict
+
+
+
+property full_tree: bool
+
+
+
+get_ic_generator()[source]
+
+
Return type:
+

Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None]

+
+
+
+
+
+
+botorch.optim.optimize.optimize_acqf(acq_function, bounds, q, num_restarts, raw_samples=None, options=None, inequality_constraints=None, equality_constraints=None, nonlinear_inequality_constraints=None, fixed_features=None, post_processing_func=None, batch_initial_conditions=None, return_best_only=True, gen_candidates=None, sequential=False, *, ic_generator=None, timeout_sec=None, return_full_tree=False, retry_on_optimization_warning=True, **ic_gen_kwargs)[source]
+

Generate a set of candidates via multi-start optimization.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – An AcquisitionFunction.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X +(if inequality_constraints is provided, these bounds can be -inf and ++inf, respectively).

  • +
  • q (int) – The number of candidates.

  • +
  • num_restarts (int) – The number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int | None) – The number of samples for initialization. This is required +if batch_initial_conditions is not specified.

  • +
  • options (dict[str, bool | float | int | str] | None) – Options for candidate generation.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs. indices and +coefficients should be torch tensors. See the docstring of +make_scipy_linear_constraints for an example. When q=1, or when +applying the same constraint to each candidate in the batch +(intra-point constraint), indices should be a 1-d tensor. +For inter-point constraints, in which the constraint is applied to the +whole batch of candidates, indices must be a 2-d tensor, where +in each row indices[i] =(k_i, l_i) the first index k_i corresponds +to the k_i-th element of the q-batch and the second index l_i +corresponds to the l_i-th feature of that element.

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an equality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs. See the docstring of +make_scipy_linear_constraints for an example.

  • +
  • nonlinear_inequality_constraints (list[tuple[Callable, bool]] | None) – A list of tuples representing the nonlinear +inequality constraints. The first element in the tuple is a callable +representing a constraint of the form callable(x) >= 0. In case of an +intra-point constraint, callable()`takes in an one-dimensional tensor of +shape `d and returns a scalar. In case of an inter-point constraint, +callable() takes a two dimensional tensor of shape q x d and again +returns a scalar. The second element is a boolean, indicating if it is an +intra-point or inter-point constraint (True for intra-point. False for +inter-point). For more information on intra-point vs inter-point +constraints, see the docstring of the inequality_constraints argument to +optimize_acqf(). The constraints will later be passed to the scipy +solver. You need to pass in batch_initial_conditions in this case. +Using non-linear inequality constraints also requires that batch_limit +is set to 1, which will be done automatically if not specified in +options.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation. All indices +should be non-negative.

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None) – A function that post-processes an optimization +result appropriately (i.e., according to round-trip +transformations).

  • +
  • batch_initial_conditions (Tensor | None) – A tensor to specify the initial conditions. Set +this if you do not want to use default initialization strategy.

  • +
  • return_best_only (bool) – If False, outputs the solutions corresponding to all +random restart initializations of the optimization.

  • +
  • gen_candidates (Callable[[Tensor, AcquisitionFunction, Any], tuple[Tensor, Tensor]] | None) – A callable for generating candidates (and their associated +acquisition values) given a tensor of initial conditions and an +acquisition function. Other common inputs include lower and upper bounds +and a dictionary of options, but refer to the documentation of specific +generation functions (e.g gen_candidates_scipy and gen_candidates_torch) +for method-specific inputs. Default: gen_candidates_scipy

  • +
  • sequential (bool) – If False, uses joint optimization, otherwise uses sequential +optimization.

  • +
  • ic_generator (Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None] | None) – Function for generating initial conditions. Not needed when +batch_initial_conditions are provided. Defaults to +gen_one_shot_kg_initial_conditions for qKnowledgeGradient acquisition +functions and gen_batch_initial_conditions otherwise. Must be specified +for nonlinear inequality constraints.

  • +
  • timeout_sec (float | None) – Max amount of time optimization can run for.

  • +
  • return_full_tree (bool) – Return the full tree of optimizers of the previous +iteration.

  • +
  • retry_on_optimization_warning (bool) – Whether to retry candidate generation with a new +set of initial conditions when it fails with an OptimizationWarning.

  • +
  • ic_gen_kwargs (Any) – Additional keyword arguments passed to function specified by +ic_generator

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • +
    A tensor of generated candidates. The shape is

    q x d if return_best_only is True (default) +– num_restarts x q x d if return_best_only is False

    +
    +
    +
  • +
  • +
    a tensor of associated acquisition values. If sequential=False,

    this is a (num_restarts)-dim tensor of joint acquisition values +(with explicit restart dimension if return_best_only=False). If +sequential=True, this is a q-dim tensor of expected acquisition +values conditional on having observed candidates 0,1,…,i-1.

    +
    +
    +
  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+

Example

+
>>> # generate `q=2` candidates jointly using 20 random restarts
+>>> # and 512 raw samples
+>>> candidates, acq_value = optimize_acqf(qEI, bounds, 2, 20, 512)
+
+
+
>>> generate `q=3` candidates sequentially using 15 random restarts
+>>> # and 256 raw samples
+>>> qEI = qExpectedImprovement(model, best_f=0.2)
+>>> bounds = torch.tensor([[0.], [1.]])
+>>> candidates, acq_value_list = optimize_acqf(
+>>>     qEI, bounds, 3, 15, 256, sequential=True
+>>> )
+
+
+
+
+
+botorch.optim.optimize.optimize_acqf_cyclic(acq_function, bounds, q, num_restarts, raw_samples=None, options=None, inequality_constraints=None, equality_constraints=None, fixed_features=None, post_processing_func=None, batch_initial_conditions=None, cyclic_options=None, *, ic_generator=None, timeout_sec=None, return_full_tree=False, retry_on_optimization_warning=True, **ic_gen_kwargs)[source]
+

Generate a set of q candidates via cyclic optimization.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – An AcquisitionFunction

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X +(if inequality_constraints is provided, these bounds can be -inf and ++inf, respectively).

  • +
  • q (int) – The number of candidates.

  • +
  • num_restarts (int) – Number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int | None) – Number of samples for initialization. This is required +if batch_initial_conditions is not specified.

  • +
  • options (dict[str, bool | float | int | str] | None) – Options for candidate generation.

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation. All indices +should be non-negative.

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None) – A function that post-processes an optimization +result appropriately (i.e., according to round-trip +transformations).

  • +
  • batch_initial_conditions (Tensor | None) – A tensor to specify the initial conditions. +If no initial conditions are provided, the default initialization will +be used.

  • +
  • cyclic_options (dict[str, bool | float | int | str] | None) – Options for stopping criterion for outer cyclic optimization.

  • +
  • ic_generator (Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None] | None) – Function for generating initial conditions. Not needed when +batch_initial_conditions are provided. Defaults to +gen_one_shot_kg_initial_conditions for qKnowledgeGradient acquisition +functions and gen_batch_initial_conditions otherwise. Must be specified +for nonlinear inequality constraints.

  • +
  • timeout_sec (float | None) – Max amount of time optimization can run for.

  • +
  • return_full_tree (bool) – Return the full tree of optimizers of the previous +iteration.

  • +
  • retry_on_optimization_warning (bool) – Whether to retry candidate generation with a new +set of initial conditions when it fails with an OptimizationWarning.

  • +
  • ic_gen_kwargs (Any) – Additional keyword arguments passed to function specified by +ic_generator

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • a q x d-dim tensor of generated candidates.

  • +
  • +
    a q-dim tensor of expected acquisition values, where the value at

    index i is the acquisition value conditional on having observed +all candidates except candidate i.

    +
    +
    +
  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+

Example

+
>>> # generate `q=3` candidates cyclically using 15 random restarts
+>>> # 256 raw samples, and 4 cycles
+>>>
+>>> qEI = qExpectedImprovement(model, best_f=0.2)
+>>> bounds = torch.tensor([[0.], [1.]])
+>>> candidates, acq_value_list = optimize_acqf_cyclic(
+>>>     qEI, bounds, 3, 15, 256, cyclic_options={"maxiter": 4}
+>>> )
+
+
+
+
+
+botorch.optim.optimize.optimize_acqf_list(acq_function_list, bounds, num_restarts, raw_samples=None, options=None, inequality_constraints=None, equality_constraints=None, nonlinear_inequality_constraints=None, fixed_features=None, fixed_features_list=None, post_processing_func=None, ic_generator=None, ic_gen_kwargs=None)[source]
+

Generate a list of candidates from a list of acquisition functions.

+

The acquisition functions are optimized in sequence, with previous candidates +set as X_pending. This is also known as sequential greedy optimization.

+
+
Parameters:
+
    +
  • acq_function_list (list[AcquisitionFunction]) – A list of acquisition functions.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X +(if inequality_constraints is provided, these bounds can be -inf and ++inf, respectively).

  • +
  • num_restarts (int) – Number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int | None) – Number of samples for initialization. This is required +if batch_initial_conditions is not specified.

  • +
  • options (dict[str, bool | float | int | str] | None) – Options for candidate generation.

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs

  • +
  • nonlinear_inequality_constraints (list[tuple[Callable, bool]] | None) – A list of tuples representing the nonlinear +inequality constraints. The first element in the tuple is a callable +representing a constraint of the form callable(x) >= 0. In case of an +intra-point constraint, callable()`takes in an one-dimensional tensor of +shape `d and returns a scalar. In case of an inter-point constraint, +callable() takes a two dimensional tensor of shape q x d and again +returns a scalar. The second element is a boolean, indicating if it is an +intra-point or inter-point constraint (True for intra-point. False for +inter-point). For more information on intra-point vs inter-point +constraints, see the docstring of the inequality_constraints argument to +optimize_acqf(). The constraints will later be passed to the scipy +solver. You need to pass in batch_initial_conditions in this case. +Using non-linear inequality constraints also requires that batch_limit +is set to 1, which will be done automatically if not specified in +options.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that should +be fixed to a particular value during generation. All indices +(feature_index) should be non-negative.

  • +
  • fixed_features_list (list[dict[int, float]] | None) – A list of maps {feature_index: value}. The i-th +item represents the fixed_feature for the i-th optimization. If +fixed_features_list is provided, optimize_acqf_mixed is invoked. +All indices (feature_index) should be non-negative.

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None) – A function that post-processes an optimization +result appropriately (i.e., according to round-trip +transformations).

  • +
  • ic_generator (Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None] | None) – Function for generating initial conditions. Not needed when +batch_initial_conditions are provided. Defaults to +gen_one_shot_kg_initial_conditions for qKnowledgeGradient acquisition +functions and gen_batch_initial_conditions otherwise. Must be specified +for nonlinear inequality constraints.

  • +
  • ic_gen_kwargs (dict | None) – Additional keyword arguments passed to function specified by +ic_generator

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • a q x d-dim tensor of generated candidates.

  • +
  • +
    a q-dim tensor of expected acquisition values, where the value at

    index i is the acquisition value conditional on having observed +all candidates except candidate i.

    +
    +
    +
  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.optim.optimize.optimize_acqf_mixed(acq_function, bounds, q, num_restarts, fixed_features_list, raw_samples=None, options=None, inequality_constraints=None, equality_constraints=None, nonlinear_inequality_constraints=None, post_processing_func=None, batch_initial_conditions=None, ic_generator=None, ic_gen_kwargs=None)[source]
+

Optimize over a list of fixed_features and returns the best solution.

+

This is useful for optimizing over mixed continuous and discrete domains. +For q > 1 this function always performs sequential greedy optimization (with +proper conditioning on generated candidates).

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – An AcquisitionFunction

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X +(if inequality_constraints is provided, these bounds can be -inf and ++inf, respectively).

  • +
  • q (int) – The number of candidates.

  • +
  • num_restarts (int) – Number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int | None) – Number of samples for initialization. This is required +if batch_initial_conditions is not specified.

  • +
  • fixed_features_list (list[dict[int, float]]) – A list of maps {feature_index: value}. The i-th +item represents the fixed_feature for the i-th optimization. All +indices (feature_index) should be non-negative.

  • +
  • options (dict[str, bool | float | int | str] | None) – Options for candidate generation.

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs

  • +
  • nonlinear_inequality_constraints (list[tuple[Callable, bool]] | None) – A list of tuples representing the nonlinear +inequality constraints. The first element in the tuple is a callable +representing a constraint of the form callable(x) >= 0. In case of an +intra-point constraint, callable()`takes in an one-dimensional tensor of +shape `d and returns a scalar. In case of an inter-point constraint, +callable() takes a two dimensional tensor of shape q x d and again +returns a scalar. The second element is a boolean, indicating if it is an +intra-point or inter-point constraint (True for intra-point. False for +inter-point). For more information on intra-point vs inter-point +constraints, see the docstring of the inequality_constraints argument to +optimize_acqf(). The constraints will later be passed to the scipy +solver. You need to pass in batch_initial_conditions in this case. +Using non-linear inequality constraints also requires that batch_limit +is set to 1, which will be done automatically if not specified in +options.

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None) – A function that post-processes an optimization +result appropriately (i.e., according to round-trip +transformations).

  • +
  • batch_initial_conditions (Tensor | None) – A tensor to specify the initial conditions. Set +this if you do not want to use default initialization strategy.

  • +
  • ic_generator (Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None] | None) – Function for generating initial conditions. Not needed when +batch_initial_conditions are provided. Defaults to +gen_one_shot_kg_initial_conditions for qKnowledgeGradient acquisition +functions and gen_batch_initial_conditions otherwise. Must be specified +for nonlinear inequality constraints.

  • +
  • ic_gen_kwargs (dict | None) – Additional keyword arguments passed to function specified by +ic_generator

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • a q x d-dim tensor of generated candidates.

  • +
  • an associated acquisition value.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.optim.optimize.optimize_acqf_discrete(acq_function, q, choices, max_batch_size=2048, unique=True, X_avoid=None, inequality_constraints=None)[source]
+

Optimize over a discrete set of points using batch evaluation.

+

For q > 1 this function generates candidates by means of sequential +conditioning (rather than joint optimization), since for all but the +smalles number of choices the set choices^q of discrete points to +evaluate quickly explodes.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – An AcquisitionFunction.

  • +
  • q (int) – The number of candidates.

  • +
  • choices (Tensor) – A num_choices x d tensor of possible choices.

  • +
  • max_batch_size (int) – The maximum number of choices to evaluate in batch. +A large limit can cause excessive memory usage if the model has +a large training set.

  • +
  • unique (bool) – If True return unique choices, o/w choices may be repeated +(only relevant if q > 1).

  • +
  • X_avoid (Tensor | None) – An n x d tensor of candidates that we aren’t allowed to pick. +These will be removed from the set of choices.

  • +
  • constraints (inequality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs. +Infeasible points will be removed from the set of choices.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • a q x d-dim tensor of generated candidates.

  • +
  • an associated acquisition value.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+ +

Optimize acquisition function over a lattice.

+

This is useful when d is large and enumeration of the search space +isn’t possible. For q > 1 this function always performs sequential +greedy optimization (with proper conditioning on generated candidates).

+

NOTE: While this method supports arbitrary lattices, it has only been +thoroughly tested for {0, 1}^d. Consider it to be in alpha stage for +the more general case.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – An AcquisitionFunction

  • +
  • discrete_choices (list[Tensor]) – A list of possible discrete choices for each dimension. +Each element in the list is expected to be a torch tensor.

  • +
  • q (int) – The number of candidates.

  • +
  • num_restarts (int) – Number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int) – Number of samples for initialization. This is required +if batch_initial_conditions is not specified.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs

  • +
  • X_avoid (Tensor | None) – An n x d tensor of candidates that we aren’t allowed to pick.

  • +
  • batch_initial_conditions (Tensor | None) – A tensor of size n x 1 x d to specify the +initial conditions. Set this if you do not want to use default +initialization strategy.

  • +
  • max_batch_size (int) – The maximum number of choices to evaluate in batch. +A large limit can cause excessive memory usage if the model has +a large training set.

  • +
  • unique (bool) – If True return unique choices, o/w choices may be repeated +(only relevant if q > 1).

  • +
+
+
Returns:
+

A two-element tuple containing

+
    +
  • a q x d-dim tensor of generated candidates.

  • +
  • an associated acquisition value.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+

Model Fitting Optimization

+

Tools for model fitting.

+
+
+botorch.optim.fit.fit_gpytorch_mll_scipy(mll, parameters=None, bounds=None, closure=None, closure_kwargs=None, method='L-BFGS-B', options=None, callback=None, timeout_sec=None)[source]
+

Generic scipy.optimized-based fitting routine for GPyTorch MLLs.

+

The model and likelihood in mll must already be in train mode.

+
+
Parameters:
+
    +
  • mll (MarginalLogLikelihood) – MarginalLogLikelihood to be maximized.

  • +
  • parameters (dict[str, Tensor] | None) – Optional dictionary of parameters to be optimized. Defaults +to all parameters of mll that require gradients.

  • +
  • bounds (dict[str, tuple[float | None, float | None]] | None) – A dictionary of user-specified bounds for parameters. Used to update +default parameter bounds obtained from mll.

  • +
  • closure (Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None) – Callable that returns a tensor and an iterable of gradient tensors. +Responsible for setting the grad attributes of parameters. If no closure +is provided, one will be obtained by calling get_loss_closure_with_grads.

  • +
  • closure_kwargs (dict[str, Any] | None) – Keyword arguments passed to closure.

  • +
  • method (str) – Solver type, passed along to scipy.minimize.

  • +
  • options (dict[str, Any] | None) – Dictionary of solver options, passed along to scipy.minimize.

  • +
  • callback (Callable[[dict[str, Tensor], OptimizationResult], None] | None) – Optional callback taking parameters and an OptimizationResult as its +sole arguments.

  • +
  • timeout_sec (float | None) – Timeout in seconds after which to terminate the fitting loop +(note that timing out can result in bad fits!).

  • +
+
+
Returns:
+

The final OptimizationResult.

+
+
Return type:
+

OptimizationResult

+
+
+
+
+
+botorch.optim.fit.fit_gpytorch_mll_torch(mll, parameters=None, bounds=None, closure=None, closure_kwargs=None, step_limit=None, stopping_criterion=<class 'botorch.utils.types.DEFAULT'>, optimizer=<class 'torch.optim.adam.Adam'>, scheduler=None, callback=None, timeout_sec=None)[source]
+

Generic torch.optim-based fitting routine for GPyTorch MLLs.

+
+
Parameters:
+
    +
  • mll (MarginalLogLikelihood) – MarginalLogLikelihood to be maximized.

  • +
  • parameters (dict[str, Tensor] | None) – Optional dictionary of parameters to be optimized. Defaults +to all parameters of mll that require gradients.

  • +
  • bounds (dict[str, tuple[float | None, float | None]] | None) – A dictionary of user-specified bounds for parameters. Used to update +default parameter bounds obtained from mll.

  • +
  • closure (Callable[[], tuple[Tensor, Sequence[Tensor | None]]] | None) – Callable that returns a tensor and an iterable of gradient tensors. +Responsible for setting the grad attributes of parameters. If no closure +is provided, one will be obtained by calling get_loss_closure_with_grads.

  • +
  • closure_kwargs (dict[str, Any] | None) – Keyword arguments passed to closure.

  • +
  • step_limit (int | None) – Optional upper bound on the number of optimization steps.

  • +
  • stopping_criterion (Callable[[Tensor], bool] | None) – A StoppingCriterion for the optimization loop.

  • +
  • optimizer (Optimizer | Callable[[...], Optimizer]) – A torch.optim.Optimizer instance or a factory that takes +a list of parameters and returns an Optimizer instance.

  • +
  • scheduler (_LRScheduler | Callable[[...], _LRScheduler] | None) – A torch.optim.lr_scheduler._LRScheduler instance or a factory +that takes an Optimizer instance and returns an _LRSchedule.

  • +
  • callback (Callable[[dict[str, Tensor], OptimizationResult], None] | None) – Optional callback taking parameters and an OptimizationResult as its +sole arguments.

  • +
  • timeout_sec (float | None) – Timeout in seconds after which to terminate the fitting loop +(note that timing out can result in bad fits!).

  • +
+
+
Returns:
+

The final OptimizationResult.

+
+
Return type:
+

OptimizationResult

+
+
+
+
+
+

Initialization Helpers

+

References

+
+
+[Regis] +

R. G. Regis, C. A. Shoemaker. Combining radial basis function +surrogates and dynamic coordinate search in high-dimensional +expensive black-box optimization, Engineering Optimization, 2013.

+
+
+
+
+botorch.optim.initializers.transform_constraints(constraints, q, d)[source]
+

Transform constraints to sample from a d*q-dimensional space instead of a +d-dimensional state.

+

This function assumes that constraints are the same for each input batch, +and broadcasts the constraints accordingly to the input batch shape.

+
+
Parameters:
+
    +
  • constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), with each tuple +encoding an (in-)equality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) (>)= rhs. +If indices is a 2-d Tensor, this supports specifying constraints across +the points in the q-batch (inter-point constraints). If None, this +function is a nullop and simply returns None.

  • +
  • q (int) – Size of the q-batch.

  • +
  • d (int) – Dimensionality of the problem.

  • +
+
+
Returns:
+

List of transformed constraints, if +there are constraints. Returns None otherwise.

+
+
Return type:
+

List[Tuple[Tensor, Tensor, float]]

+
+
+
+
+
+botorch.optim.initializers.transform_intra_point_constraint(constraint, d, q)[source]
+

Transforms an intra-point/pointwise constraint from +d-dimensional space to a d*q-dimesional space.

+
+
Parameters:
+
    +
  • constraints – A list of tuples (indices, coefficients, rhs), with each tuple +encoding an (in-)equality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) (>)= rhs. Here indices must +be one-dimensional, and the constraint is applied to all points within the +q-batch.

  • +
  • d (int) – Dimensionality of the problem.

  • +
  • constraint (tuple[Tensor, Tensor, float])

  • +
  • q (int)

  • +
+
+
Raises:
+

ValueError – If indices in the constraints are larger than the + dimensionality d of the problem.

+
+
Returns:
+

List of transformed constraints.

+
+
Return type:
+

List[Tuple[Tensor, Tensor, float]]

+
+
+
+
+
+botorch.optim.initializers.transform_inter_point_constraint(constraint, d)[source]
+

Transforms an inter-point constraint from +d-dimensional space to a d*q dimesional space.

+
+
Parameters:
+
    +
  • constraints – A list of tuples (indices, coefficients, rhs), with each tuple +encoding an (in-)equality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) (>)= rhs. indices must be a +2-d Tensor, where in each row indices[i] = (k_i, l_i) the first index +k_i corresponds to the k_i-th element of the q-batch and the second +index l_i corresponds to the l_i-th feature of that element.

  • +
  • constraint (tuple[Tensor, Tensor, float])

  • +
  • d (int)

  • +
+
+
Raises:
+

ValueError – If indices in the constraints are larger than the + dimensionality d of the problem.

+
+
Returns:
+

Transformed constraint.

+
+
Return type:
+

List[Tuple[Tensor, Tensor, float]]

+
+
+
+
+
+botorch.optim.initializers.sample_q_batches_from_polytope(n, q, bounds, n_burnin, n_thinning, seed=None, inequality_constraints=None, equality_constraints=None)[source]
+

Samples n q-baches from a polytope of dimension d.

+
+
Parameters:
+
    +
  • n (int) – Number of q-batches to sample.

  • +
  • q (int) – Number of samples per q-batch

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X.

  • +
  • n_burnin (int) – The number of burn-in samples for the Markov chain sampler.

  • +
  • n_thinning (int) – The amount of thinning. The sampler will return every +n_thinning sample (after burn-in).

  • +
  • seed (int | None) – The random seed.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
+
+
Returns:
+

A n x q x d-dim tensor of samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.initializers.gen_batch_initial_conditions(acq_function, bounds, q, num_restarts, raw_samples, fixed_features=None, options=None, inequality_constraints=None, equality_constraints=None, generator=None, fixed_X_fantasies=None)[source]
+

Generate a batch of initial conditions for random-restart optimziation.

+

TODO: Support t-batches of initial conditions.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – The acquisition function to be optimized.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X.

  • +
  • q (int) – The number of candidates to consider.

  • +
  • num_restarts (int) – The number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int) – The number of raw samples to consider in the initialization +heuristic. Note: if sample_around_best is True (the default is False), +then 2 * raw_samples samples are used.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation.

  • +
  • options (dict[str, bool | float | int] | None) – Options for initial condition generation. For valid options see +initialize_q_batch and initialize_q_batch_nonneg. If options +contains a nonnegative=True entry, then acq_function is +assumed to be non-negative (useful when using custom acquisition +functions). In addition, an “init_batch_limit” option can be passed +to specify the batch limit for the initialization. This is useful +for avoiding memory limits when computing the batch posterior over +raw samples.

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
  • generator (Callable[[int, int, int | None], Tensor] | None) – Callable for generating samples that are then further +processed. It receives n, q and seed as arguments +and returns a tensor of shape n x q x d.

  • +
  • fixed_X_fantasies (Tensor | None) – A fixed set of fantasy points to concatenate to +the q candidates being initialized along the -2 dimension. The +shape should be num_pseudo_points x d. E.g., this should be +num_fantasies x d for KG and num_fantasies*num_pareto x d +for HVKG.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A num_restarts x q x d tensor of initial conditions.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> qEI = qExpectedImprovement(model, best_f=0.2)
+>>> bounds = torch.tensor([[0.], [1.]])
+>>> Xinit = gen_batch_initial_conditions(
+>>>     qEI, bounds, q=3, num_restarts=25, raw_samples=500
+>>> )
+
+
+
+
+
+botorch.optim.initializers.gen_one_shot_kg_initial_conditions(acq_function, bounds, q, num_restarts, raw_samples, fixed_features=None, options=None, inequality_constraints=None, equality_constraints=None)[source]
+

Generate a batch of smart initializations for qKnowledgeGradient.

+

This function generates initial conditions for optimizing one-shot KG using +the maximizer of the posterior objective. Intutively, the maximizer of the +fantasized posterior will often be close to a maximizer of the current +posterior. This function uses that fact to generate the initial conditions +for the fantasy points. Specifically, a fraction of 1 - frac_random (see +options) is generated by sampling from the set of maximizers of the +posterior objective (obtained via random restart optimization) according to +a softmax transformation of their respective values. This means that this +initialization strategy internally solves an acquisition function +maximization problem. The remaining frac_random fantasy points as well as +all q candidate points are chosen according to the standard initialization +strategy in gen_batch_initial_conditions.

+
+
Parameters:
+
    +
  • acq_function (qKnowledgeGradient) – The qHypervolumeKnowledgeGradient instance to be optimized.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of +task features.

  • +
  • q (int) – The number of candidates to consider.

  • +
  • num_restarts (int) – The number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int) – The number of raw samples to consider in the initialization +heuristic.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation.

  • +
  • options (dict[str, bool | float | int] | None) – Options for initial condition generation. These contain all +settings for the standard heuristic initialization from +gen_batch_initial_conditions. In addition, they contain +frac_random (the fraction of fully random fantasy points), +num_inner_restarts and raw_inner_samples (the number of random +restarts and raw samples for solving the posterior objective +maximization problem, respectively) and eta (temperature parameter +for sampling heuristic from posterior objective maximizers).

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A num_restarts x q’ x d tensor that can be used as initial conditions +for optimize_acqf(). Here q’ = q + num_fantasies is the total number +of points (candidate points plus fantasy points).

+
+
Return type:
+

Tensor | None

+
+
+

Example

+
>>> qHVKG = qHypervolumeKnowledgeGradient(model, ref_point=num_fantasies=64)
+>>> bounds = torch.tensor([[0., 0.], [1., 1.]])
+>>> Xinit = gen_one_shot_hvkg_initial_conditions(
+>>>     qHVKG, bounds, q=3, num_restarts=10, raw_samples=512,
+>>>     options={"frac_random": 0.25},
+>>> )
+
+
+
+
+
+botorch.optim.initializers.gen_one_shot_hvkg_initial_conditions(acq_function, bounds, q, num_restarts, raw_samples, fixed_features=None, options=None, inequality_constraints=None, equality_constraints=None)[source]
+

Generate a batch of smart initializations for qHypervolumeKnowledgeGradient.

+

This function generates initial conditions for optimizing one-shot HVKG using +the hypervolume maximizing set (of fixed size) under the posterior mean. +Intutively, the hypervolume maximizing set of the fantasized posterior mean +will often be close to a hypervolume maximizing set under the current posterior +mean. This function uses that fact to generate the initial conditions +for the fantasy points. Specifically, a fraction of 1 - frac_random (see +options) of the restarts are generated by learning the hypervolume maximizing sets +under the current posterior mean, where each hypervolume maximizing set is +obtained from maximizing the hypervolume from a different starting point. Given +a hypervolume maximizing set, the q candidate points are selected using to the +standard initialization strategy in gen_batch_initial_conditions, with the fixed +hypervolume maximizing set. The remaining frac_random restarts fantasy points +as well as all q candidate points are chosen according to the standard +initialization strategy in gen_batch_initial_conditions.

+
+
Parameters:
+
    +
  • acq_function (qHypervolumeKnowledgeGradient) – The qKnowledgeGradient instance to be optimized.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of +task features.

  • +
  • q (int) – The number of candidates to consider.

  • +
  • num_restarts (int) – The number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int) – The number of raw samples to consider in the initialization +heuristic.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation.

  • +
  • options (dict[str, bool | float | int] | None) – Options for initial condition generation. These contain all +settings for the standard heuristic initialization from +gen_batch_initial_conditions. In addition, they contain +frac_random (the fraction of fully random fantasy points), +num_inner_restarts and raw_inner_samples (the number of random +restarts and raw samples for solving the posterior objective +maximization problem, respectively) and eta (temperature parameter +for sampling heuristic from posterior objective maximizers).

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A num_restarts x q’ x d tensor that can be used as initial conditions +for optimize_acqf(). Here q’ = q + num_fantasies is the total number +of points (candidate points plus fantasy points).

+
+
Return type:
+

Tensor | None

+
+
+

Example

+
>>> qHVKG = qHypervolumeKnowledgeGradient(model, ref_point)
+>>> bounds = torch.tensor([[0., 0.], [1., 1.]])
+>>> Xinit = gen_one_shot_hvkg_initial_conditions(
+>>>     qHVKG, bounds, q=3, num_restarts=10, raw_samples=512,
+>>>     options={"frac_random": 0.25},
+>>> )
+
+
+
+
+
+botorch.optim.initializers.gen_value_function_initial_conditions(acq_function, bounds, num_restarts, raw_samples, current_model, fixed_features=None, options=None)[source]
+

Generate a batch of smart initializations for optimizing +the value function of qKnowledgeGradient.

+

This function generates initial conditions for optimizing the inner problem of +KG, i.e. its value function, using the maximizer of the posterior objective. +Intutively, the maximizer of the fantasized posterior will often be close to a +maximizer of the current posterior. This function uses that fact to generate the +initital conditions for the fantasy points. Specifically, a fraction of 1 - +frac_random (see options) of raw samples is generated by sampling from the set of +maximizers of the posterior objective (obtained via random restart optimization) +according to a softmax transformation of their respective values. This means that +this initialization strategy internally solves an acquisition function +maximization problem. The remaining raw samples are generated using +draw_sobol_samples. All raw samples are then evaluated, and the initial +conditions are selected according to the standard initialization strategy in +‘initialize_q_batch’ individually for each inner problem.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – The value function instance to be optimized.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of +task features.

  • +
  • num_restarts (int) – The number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int) – The number of raw samples to consider in the initialization +heuristic.

  • +
  • current_model (Model) – The model of the KG acquisition function that was used to +generate the fantasy model of the value function.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation.

  • +
  • options (dict[str, bool | float | int] | None) – Options for initial condition generation. These contain all +settings for the standard heuristic initialization from +gen_batch_initial_conditions. In addition, they contain +frac_random (the fraction of fully random fantasy points), +num_inner_restarts and raw_inner_samples (the number of random +restarts and raw samples for solving the posterior objective +maximization problem, respectively) and eta (temperature parameter +for sampling heuristic from posterior objective maximizers).

  • +
+
+
Returns:
+

A num_restarts x batch_shape x q x d tensor that can be used as initial +conditions for optimize_acqf(). Here batch_shape is the batch shape +of value function model.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> fant_X = torch.rand(5, 1, 2)
+>>> fantasy_model = model.fantasize(fant_X, SobolQMCNormalSampler(16))
+>>> value_function = PosteriorMean(fantasy_model)
+>>> bounds = torch.tensor([[0., 0.], [1., 1.]])
+>>> Xinit = gen_value_function_initial_conditions(
+>>>     value_function, bounds, num_restarts=10, raw_samples=512,
+>>>     options={"frac_random": 0.25},
+>>> )
+
+
+
+
+
+botorch.optim.initializers.initialize_q_batch(X, acq_vals, n, eta=1.0)[source]
+

Heuristic for selecting initial conditions for candidate generation.

+

This heuristic selects points from X (without replacement) with probability +proportional to exp(eta * Z), where +Z = (acq_vals - mean(acq_vals)) / std(acq_vals) +and eta is a temperature parameter.

+

When using an acquisiton function that is non-negative and possibly zero +over large areas of the feature space (e.g. qEI), you should use +initialize_q_batch_nonneg instead.

+
+
Parameters:
+
    +
  • X (Tensor) – A b x batch_shape x q x d tensor of b - batch_shape samples of +q-batches from a d`-dim feature space. Typically, these are generated +using qMC sampling.

  • +
  • acq_vals (Tensor) – A tensor of b x batch_shape outcomes associated with the samples. +Typically, this is the value of the batch acquisition function to be +maximized.

  • +
  • n (int) – The number of initial condition to be generated. Must be less than b.

  • +
  • eta (float) – Temperature parameter for weighting samples.

  • +
+
+
Returns:
+

    +
  • An n x batch_shape x q x d tensor of n - batch_shape q-batch initial +conditions, where each batch of n x q x d samples is selected independently.

  • +
  • An n x batch_shape tensor of the corresponding acquisition values.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+

Example

+
>>> # To get `n=10` starting points of q-batch size `q=3`
+>>> # for model with `d=6`:
+>>> qUCB = qUpperConfidenceBound(model, beta=0.1)
+>>> X_rnd = torch.rand(500, 3, 6)
+>>> X_init, acq_init = initialize_q_batch(X=X_rnd, acq_vals=qUCB(X_rnd), n=10)
+
+
+
+
+
+botorch.optim.initializers.initialize_q_batch_nonneg(X, acq_vals, n, eta=1.0, alpha=0.0001)[source]
+

Heuristic for selecting initial conditions for non-neg. acquisition functions.

+

This function is similar to initialize_q_batch, but designed specifically +for acquisition functions that are non-negative and possibly zero over +large areas of the feature space (e.g. qEI). All samples for which +acq_vals < alpha * max(acq_vals) will be ignored (assuming that acq_vals +contains at least one positive value).

+
+
Parameters:
+
    +
  • X (Tensor) – A b x q x d tensor of b samples of q-batches from a d-dim. +feature space. Typically, these are generated using qMC.

  • +
  • acq_vals (Tensor) – A tensor of b outcomes associated with the samples. Typically, this +is the value of the batch acquisition function to be maximized.

  • +
  • n (int) – The number of initial condition to be generated. Must be less than b.

  • +
  • eta (float) – Temperature parameter for weighting samples.

  • +
  • alpha (float) – The threshold (as a fraction of the maximum observed value) under +which to ignore samples. All input samples for which +Y < alpha * max(Y) will be ignored.

  • +
+
+
Returns:
+

    +
  • An n x q x d tensor of n q-batch initial conditions.

  • +
  • An n tensor of the corresponding acquisition values.

  • +
+

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+

Example

+
>>> # To get `n=10` starting points of q-batch size `q=3`
+>>> # for model with `d=6`:
+>>> qEI = qExpectedImprovement(model, best_f=0.2)
+>>> X_rnd = torch.rand(500, 3, 6)
+>>> X_init, acq_init = initialize_q_batch_nonneg(
+...     X=X_rnd, acq_vals=qEI(X_rnd), n=10
+... )
+
+
+
+
+
+botorch.optim.initializers.sample_points_around_best(acq_function, n_discrete_points, sigma, bounds, best_pct=5.0, subset_sigma=0.1, prob_perturb=None)[source]
+

Find best points and sample nearby points.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – The acquisition function.

  • +
  • n_discrete_points (int) – The number of points to sample.

  • +
  • sigma (float) – The standard deviation of the additive gaussian noise for +perturbing the best points.

  • +
  • bounds (Tensor) – A 2 x d-dim tensor containing the bounds.

  • +
  • best_pct (float) – The percentage of best points to perturb.

  • +
  • subset_sigma (float) – The standard deviation of the additive gaussian +noise for perturbing a subset of dimensions of the best points.

  • +
  • prob_perturb (float | None) – The probability of perturbing each dimension.

  • +
+
+
Returns:
+

+
An optional n_discrete_points x d-dim tensor containing the

sampled points. This is None if no baseline points are found.

+
+
+

+
+
Return type:
+

Tensor | None

+
+
+
+
+
+botorch.optim.initializers.sample_truncated_normal_perturbations(X, n_discrete_points, sigma, bounds, qmc=True)[source]
+

Sample points around X.

+

Sample perturbed points around X such that the added perturbations +are sampled from N(0, sigma^2 I) and truncated to be within [0,1]^d.

+
+
Parameters:
+
    +
  • X (Tensor) – A n x d-dim tensor starting points.

  • +
  • n_discrete_points (int) – The number of points to sample.

  • +
  • sigma (float) – The standard deviation of the additive gaussian noise for +perturbing the points.

  • +
  • bounds (Tensor) – A 2 x d-dim tensor containing the bounds.

  • +
  • qmc (bool) – A boolean indicating whether to use qmc.

  • +
+
+
Returns:
+

A n_discrete_points x d-dim tensor containing the sampled points.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.initializers.sample_perturbed_subset_dims(X, bounds, n_discrete_points, sigma=0.1, qmc=True, prob_perturb=None)[source]
+

Sample around X by perturbing a subset of the dimensions.

+

By default, dimensions are perturbed with probability equal to +min(20 / d, 1). As shown in [Regis], perturbing a small number +of dimensions can be beneificial. The perturbations are sampled +from N(0, sigma^2 I) and truncated to be within [0,1]^d.

+
+
Parameters:
+
    +
  • X (Tensor) – A n x d-dim tensor starting points. X +must be normalized to be within [0, 1]^d.

  • +
  • bounds (Tensor) – The bounds to sample perturbed values from

  • +
  • n_discrete_points (int) – The number of points to sample.

  • +
  • sigma (float) – The standard deviation of the additive gaussian noise for +perturbing the points.

  • +
  • qmc (bool) – A boolean indicating whether to use qmc.

  • +
  • prob_perturb (float | None) – The probability of perturbing each dimension. If omitted, +defaults to min(20 / d, 1).

  • +
+
+
Returns:
+

A n_discrete_points x d-dim tensor containing the sampled points.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.initializers.is_nonnegative(acq_function)[source]
+

Determine whether a given acquisition function is non-negative.

+
+
Parameters:
+

acq_function (AcquisitionFunction) – The AcquisitionFunction instance.

+
+
Returns:
+

True if acq_function is non-negative, False if not, or if the behavior +is unknown (for custom acquisition functions).

+
+
Return type:
+

bool

+
+
+

Example

+
>>> qEI = qExpectedImprovement(model, best_f=0.1)
+>>> is_nonnegative(qEI)  # returns True
+
+
+
+
+
+

Stopping Criteria

+
+
+class botorch.optim.stopping.StoppingCriterion[source]
+

Bases: ABC

+

Base class for evaluating optimization convergence.

+

Stopping criteria are implemented as a objects rather than a function, so that they +can keep track of past function values between optimization steps.

+
+
+abstract evaluate(fvals)[source]
+

Evaluate the stopping criterion.

+
+
Parameters:
+

fvals (Tensor) – tensor containing function values for the current iteration. If +fvals contains more than one element, then the stopping criterion is +evaluated element-wise and True is returned if the stopping criterion is +true for all elements.

+
+
Returns:
+

Stopping indicator (if True, stop the optimziation).

+
+
Return type:
+

bool

+
+
+
+
+
+
+class botorch.optim.stopping.ExpMAStoppingCriterion(maxiter=10000, minimize=True, n_window=10, eta=1.0, rel_tol=1e-05)[source]
+

Bases: StoppingCriterion

+

Exponential moving average stopping criterion.

+

Computes an exponentially weighted moving average over window length n_window +and checks whether the relative decrease in this moving average between steps +is less than a provided tolerance level. That is, in iteration i, it computes

+
+

v[i,j] := fvals[i - n_window + j] * w[j]

+
+

for all j = 0, …, n_window, where w[j] = exp(-eta * (1 - j / n_window)). +Letting ma[i] := sum_j(v[i,j]), the criterion evaluates to True whenever

+
+

(ma[i-1] - ma[i]) / abs(ma[i-1]) < rel_tol (if minimize=True) +(ma[i] - ma[i-1]) / abs(ma[i-1]) < rel_tol (if minimize=False)

+
+

Exponential moving average stopping criterion.

+
+
Parameters:
+
    +
  • maxiter (int) – Maximum number of iterations.

  • +
  • minimize (bool) – If True, assume minimization.

  • +
  • n_window (int) – The size of the exponential moving average window.

  • +
  • eta (float) – The exponential decay factor in the weights.

  • +
  • rel_tol (float) – Relative tolerance for termination.

  • +
+
+
+
+
+evaluate(fvals)[source]
+

Evaluate the stopping criterion.

+
+
Parameters:
+

fvals (Tensor) – tensor containing function values for the current iteration. If +fvals contains more than one element, then the stopping criterion is +evaluated element-wise and True is returned if the stopping criterion is +true for all elements.

+
+
Return type:
+

bool

+
+
+

TODO: add support for utilizing gradient information

+
+
Returns:
+

Stopping indicator (if True, stop the optimziation).

+
+
Parameters:
+

fvals (Tensor)

+
+
Return type:
+

bool

+
+
+
+
+
+
+

Acquisition Function Optimization with Homotopy

+
+
+botorch.optim.optimize_homotopy.prune_candidates(candidates, acq_values, prune_tolerance)[source]
+

Prune candidates based on their distance to other candidates.

+
+
Parameters:
+
    +
  • candidates (Tensor) – An n x d tensor of candidates.

  • +
  • acq_values (Tensor) – An n tensor of candidate values.

  • +
  • prune_tolerance (float) – The minimum distance to prune candidates.

  • +
+
+
Returns:
+

An m x d tensor of pruned candidates.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.optimize_homotopy.optimize_acqf_homotopy(acq_function, bounds, q, num_restarts, homotopy, prune_tolerance=0.0001, raw_samples=None, options=None, final_options=None, inequality_constraints=None, equality_constraints=None, nonlinear_inequality_constraints=None, fixed_features=None, post_processing_func=None, batch_initial_conditions=None, gen_candidates=None, sequential=False, *, ic_generator=None, timeout_sec=None, return_full_tree=False, retry_on_optimization_warning=True, **ic_gen_kwargs)[source]
+

Generate a set of candidates via multi-start optimization.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – An AcquisitionFunction.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X +(if inequality_constraints is provided, these bounds can be -inf and ++inf, respectively).

  • +
  • q (int) – The number of candidates.

  • +
  • homotopy (Homotopy) – Homotopy object that will make the necessary modifications to the +problem when calling step().

  • +
  • prune_tolerance (float) – The minimum distance to prune candidates.

  • +
  • num_restarts (int) – The number of starting points for multistart acquisition +function optimization.

  • +
  • raw_samples (int | None) – The number of samples for initialization. This is required +if batch_initial_conditions is not specified.

  • +
  • options (dict[str, bool | float | int | str] | None) – Options for candidate generation in the initial step of the homotopy.

  • +
  • final_options (dict[str, bool | float | int | str] | None) – Options for candidate generation in the final step of +the homotopy.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs. indices and +coefficients should be torch tensors. See the docstring of +make_scipy_linear_constraints for an example. When q=1, or when +applying the same constraint to each candidate in the batch +(intra-point constraint), indices should be a 1-d tensor. +For inter-point constraints, in which the constraint is applied to the +whole batch of candidates, indices must be a 2-d tensor, where +in each row indices[i] =(k_i, l_i) the first index k_i corresponds +to the k_i-th element of the q-batch and the second index l_i +corresponds to the l_i-th feature of that element.

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an equality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs. See the docstring of +make_scipy_linear_constraints for an example.

  • +
  • nonlinear_inequality_constraints (list[tuple[Callable, bool]] | None) – A list of tuples representing the nonlinear +inequality constraints. The first element in the tuple is a callable +representing a constraint of the form callable(x) >= 0. In case of an +intra-point constraint, callable()`takes in an one-dimensional tensor of +shape `d and returns a scalar. In case of an inter-point constraint, +callable() takes a two dimensional tensor of shape q x d and again +returns a scalar. The second element is a boolean, indicating if it is an +intra-point or inter-point constraint (True for intra-point. False for +inter-point). For more information on intra-point vs inter-point +constraints, see the docstring of the inequality_constraints argument to +optimize_acqf(). The constraints will later be passed to the scipy +solver. You need to pass in batch_initial_conditions in this case. +Using non-linear inequality constraints also requires that batch_limit +is set to 1, which will be done automatically if not specified in +options.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation.

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None) – A function that post-processes an optimization +result appropriately (i.e., according to round-trip +transformations).

  • +
  • batch_initial_conditions (Tensor | None) – A tensor to specify the initial conditions. Set +this if you do not want to use default initialization strategy.

  • +
  • gen_candidates (Callable[[Tensor, AcquisitionFunction, Any], tuple[Tensor, Tensor]] | None) – A callable for generating candidates (and their associated +acquisition values) given a tensor of initial conditions and an +acquisition function. Other common inputs include lower and upper bounds +and a dictionary of options, but refer to the documentation of specific +generation functions (e.g gen_candidates_scipy and gen_candidates_torch) +for method-specific inputs. Default: gen_candidates_scipy

  • +
  • sequential (bool) – If False, uses joint optimization, otherwise uses sequential +optimization.

  • +
  • ic_generator (Callable[[qKnowledgeGradient, Tensor, int, int, int, dict[int, float] | None, dict[str, bool | float | int] | None, list[tuple[Tensor, Tensor, float]] | None, list[tuple[Tensor, Tensor, float]] | None], Tensor | None] | None) – Function for generating initial conditions. Not needed when +batch_initial_conditions are provided. Defaults to +gen_one_shot_kg_initial_conditions for qKnowledgeGradient acquisition +functions and gen_batch_initial_conditions otherwise. Must be specified +for nonlinear inequality constraints.

  • +
  • timeout_sec (float | None) – Max amount of time optimization can run for.

  • +
  • return_full_tree (bool) – Return the full tree of optimizers of the previous +iteration.

  • +
  • retry_on_optimization_warning (bool) – Whether to retry candidate generation with a new +set of initial conditions when it fails with an OptimizationWarning.

  • +
  • ic_gen_kwargs (Any) – Additional keyword arguments passed to function specified by +ic_generator

  • +
+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+

Acquisition Function Optimization with Mixed Integer Variables

+
+
+botorch.optim.optimize_mixed.get_nearest_neighbors(current_x, bounds, discrete_dims)[source]
+

Generate all 1-Manhattan distance neighbors of a given input. The neighbors +are generated for the discrete dimensions only.

+

NOTE: This assumes that current_x is detached and uses in-place operations, +which are known to be incompatible with autograd.

+
+
Parameters:
+
    +
  • current_x (Tensor) – The design to find the neighbors of. A tensor of shape d.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X.

  • +
  • discrete_dims (Tensor) – A tensor of indices corresponding to binary and +integer parameters.

  • +
+
+
Returns:
+

A tensor of shape num_neighbors x d, denoting all unique 1-Manhattan +distance neighbors.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.optimize_mixed.get_spray_points(X_baseline, cont_dims, discrete_dims, bounds, num_spray_points, std_cont_perturbation=0.1)[source]
+

Generate spray points by perturbing the Pareto optimal points.

+

Given the points on the Pareto frontier, we create perturbations (spray points) +by adding Gaussian perturbation to the continuous parameters and 1-Manhattan +distance neighbors of the discrete (binary and integer) parameters.

+
+
Parameters:
+
    +
  • X_baseline (Tensor) – Tensor of best acquired points across BO run.

  • +
  • cont_dims (Tensor) – Indices of continuous parameters/input dimensions.

  • +
  • discrete_dims (Tensor) – Indices of binary/integer parameters/input dimensions.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X.

  • +
  • num_spray_points (int) – Number of spray points to return.

  • +
  • std_cont_perturbation (float) – standard deviation of Normal perturbations of +continuous dimensions. Default is STD_CONT_PERTURBATION = 0.2.

  • +
+
+
Returns:
+

A (num_spray_points x d)-dim tensor of perturbed points.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.optimize_mixed.sample_feasible_points(opt_inputs, discrete_dims, num_points)[source]
+

Sample feasible points from the optimization domain.

+

Feasibility is determined according to the discrete dimensions taking +integer values and the inequality constraints being satisfied.

+

If there are no inequality constraints, Sobol is used to generate the base points. +Otherwise, we use the polytope sampler to generate the base points. The base points +are then rounded to the nearest integer values for the discrete dimensions, and +the infeasible points are filtered out (in case rounding leads to infeasibility).

+

This method will do 10 attempts to generate num_points feasible points, and +return the points generated so far. If no points are generated, it will error out.

+
+
Parameters:
+
    +
  • opt_inputs (OptimizeAcqfInputs) – Common set of arguments for acquisition optimization.

  • +
  • discrete_dims (Tensor) – A tensor of indices corresponding to binary and +integer parameters.

  • +
  • num_points (int) – The number of points to sample.

  • +
+
+
Returns:
+

A tensor of shape num_points x d containing the sampled points.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.optimize_mixed.generate_starting_points(opt_inputs, discrete_dims, cont_dims)[source]
+

Generate initial starting points for the alternating optimization.

+

This method attempts to generate the initial points using the specified +options and completes any missing points using sample_feasible_points.

+
+
Parameters:
+
    +
  • opt_inputs (OptimizeAcqfInputs) – Common set of arguments for acquisition optimization. +This function utilizes acq_function, bounds, num_restarts, +raw_samples, options, fixed_features and constraints +from opt_inputs.

  • +
  • discrete_dims (Tensor) – A tensor of indices corresponding to integer and +binary parameters.

  • +
  • cont_dims (Tensor) – A tensor of indices corresponding to continuous parameters.

  • +
+
+
Returns:
+

a (num_restarts x d)-dim tensor of starting points +and a (num_restarts)-dim tensor of their respective acquisition values. +In rare cases, this method may return fewer than num_restarts points.

+
+
Return type:
+

A tuple of two tensors

+
+
+
+
+
+botorch.optim.optimize_mixed.discrete_step(opt_inputs, discrete_dims, current_x)[source]
+

Discrete nearest neighbour search.

+
+
Parameters:
+
    +
  • opt_inputs (OptimizeAcqfInputs) – Common set of arguments for acquisition optimization. +This function utilizes acq_function, bounds, options +and constraints from opt_inputs.

  • +
  • discrete_dims (Tensor) – A tensor of indices corresponding to binary and +integer parameters.

  • +
  • current_x (Tensor) – Starting point. A tensor of shape d.

  • +
+
+
Returns:
+

+
a (d)-dim tensor of optimized point

and a scalar tensor of correspondins acquisition value.

+
+
+

+
+
Return type:
+

A tuple of two tensors

+
+
+
+
+
+botorch.optim.optimize_mixed.continuous_step(opt_inputs, discrete_dims, current_x)[source]
+

Continuous search using L-BFGS-B through optimize_acqf.

+
+
Parameters:
+
    +
  • opt_inputs (OptimizeAcqfInputs) – Common set of arguments for acquisition optimization. +This function utilizes acq_function, bounds, options, +fixed_features and constraints from opt_inputs.

  • +
  • discrete_dims (Tensor) – A tensor of indices corresponding to binary and +integer parameters.

  • +
  • current_x (Tensor) – Starting point. A tensor of shape d.

  • +
+
+
Returns:
+

+
a (1 x d)-dim tensor of optimized points

and a (1)-dim tensor of acquisition values.

+
+
+

+
+
Return type:
+

A tuple of two tensors

+
+
+
+
+
+botorch.optim.optimize_mixed.optimize_acqf_mixed_alternating(acq_function, bounds, discrete_dims, options=None, q=1, raw_samples=1024, num_restarts=20, post_processing_func=None, sequential=True, fixed_features=None, inequality_constraints=None)[source]
+

Optimizes acquisition function over mixed binary and continuous input spaces. +Multiple random restarting starting points are picked by evaluating a large set +of initial candidates. From each starting point, alternating discrete local search +and continuous optimization via (L-BFGS) is performed for a fixed number of +iterations.

+

NOTE: This method assumes that all discrete variables are integer valued. +The discrete dimensions that have more than +options.get(“max_discrete_values”, MAX_DISCRETE_VALUES) values will +be optimized using continuous relaxation.

+

# TODO: Support categorical variables.

+
+
Parameters:
+
    +
  • acq_function (AcquisitionFunction) – BoTorch Acquisition function.

  • +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds for each column of X.

  • +
  • discrete_dims (list[int]) – A list of indices corresponding to integer and binary parameters.

  • +
  • options (dict[str, Any] | None) – Dictionary specifying optimization options. Supports the following:

  • +
  • "initialization_strategy" (-) – Strategy used to generate the initial candidates. +“random”, “continuous_relaxation” or “equally_spaced” (linspace style).

  • +
  • "tol" (-) – The algorithm terminates if the absolute improvement in acquisition +value of one iteration is smaller than this number.

  • +
  • "maxiter_alternating" (-) – Number of alternating steps. Defaults to 64.

  • +
  • "maxiter_discrete" (-) – Maximum number of iterations in each discrete step. +Defaults to 4.

  • +
  • "maxiter_continuous" (-) – Maximum number of iterations in each continuous step. +Defaults to 8.

  • +
  • "max_discrete_values" (-) – Maximum number of values for a discrete dimension +to be optimized using discrete step / local search. The discrete dimensions +with more values will be optimized using continuous relaxation.

  • +
  • "num_spray_points" (-) – Number of spray points (around X_baseline) to add to +the points generated by the initialization strategy. Defaults to 20 if +all discrete variables are binary and to 0 otherwise.

  • +
  • "std_cont_perturbation" (-) – Standard deviation of the normal perturbations of +the continuous variables used to generate the spray points. +Defaults to 0.1.

  • +
  • "batch_limit" (-) – The maximum batch size for jointly evaluating candidates +during optimization.

  • +
  • "init_batch_limit" (-) – The maximum batch size for jointly evaluating candidates +during initialization. During initialization, candidates are evaluated +in a no_grad context, which reduces memory usage. As a result, +init_batch_limit can be set to a larger value than batch_limit. +Defaults to batch_limit, if given.

  • +
  • q (int) – Number of candidates.

  • +
  • raw_samples (int) – Number of initial candidates used to select starting points from. +Defaults to 1024.

  • +
  • num_restarts (int) – Number of random restarts. Defaults to 20.

  • +
  • post_processing_func (Callable[[Tensor], Tensor] | None) – A function that post-processes an optimization result +appropriately (i.e., according to round-trip transformations).

  • +
  • sequential (bool) – Whether to use joint or sequential optimization across q-batch. +This currently only supports sequential optimization.

  • +
  • fixed_features (dict[int, float] | None) – A map {feature_index: value} for features that +should be fixed to a particular value during generation.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs. indices and +coefficients should be torch tensors. See the docstring of +make_scipy_linear_constraints for an example.

  • +
+
+
Returns:
+

+
a (q x d)-dim tensor of optimized points

and a (q)-dim tensor of their respective acquisition values.

+
+
+

+
+
Return type:
+

A tuple of two tensors

+
+
+
+
+
+botorch.optim.optimize_mixed.complement_indices_like(indices, d)[source]
+

Computes a tensor of complement indices: {range(d) \ indices}. +Same as complement_indices but returns an integer tensor like indices.

+
+
Parameters:
+
    +
  • indices (Tensor)

  • +
  • d (int)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.optimize_mixed.complement_indices(indices, d)[source]
+

Computes a list of complement indices: {range(d) \ indices}.

+
+
Parameters:
+
    +
  • indices (list[int]) – a list of integers.

  • +
  • d (int) – an integer dimension in which to compute the complement.

  • +
+
+
Returns:
+

A list of integer indices.

+
+
Return type:
+

list[int]

+
+
+
+
+
+
+

Closures

+
+

Core

+

Core methods for building closures in torch and interfacing with numpy.

+
+
+class botorch.optim.closures.core.ForwardBackwardClosure(forward, parameters, backward=<function Tensor.backward>, reducer=<built-in method sum of type object>, callback=None, context_manager=None)[source]
+

Bases: object

+

Wrapper for fused forward and backward closures.

+

Initializes a ForwardBackwardClosure instance.

+
+
Parameters:
+
    +
  • closure – Callable that returns a tensor.

  • +
  • parameters (dict[str, Tensor]) – A dictionary of tensors whose grad fields are to be returned.

  • +
  • backward (Callable[[Tensor], None]) – Callable that takes the (reduced) output of forward and sets the +grad attributes of tensors in parameters.

  • +
  • reducer (Callable[[Tensor], Tensor] | None) – Optional callable used to reduce the output of the forward pass.

  • +
  • callback (Callable[[Tensor, Sequence[Tensor | None]], None] | None) – Optional callable that takes the reduced output of forward and +the gradients of parameters as positional arguments.

  • +
  • context_manager (Callable) – A ContextManager used to wrap each forward-backward call. +When passed as None, context_manager defaults to a zero_grad_ctx +that zeroes the gradients of parameters upon entry.

  • +
  • forward (Callable[[], Tensor])

  • +
+
+
+
+
+
+class botorch.optim.closures.core.NdarrayOptimizationClosure(closure, parameters, as_array=None, get_state=None, set_state=None, fill_value=0.0, persistent=True)[source]
+

Bases: object

+

Adds stateful behavior and a numpy.ndarray-typed API to a closure with an +expected return type Tuple[Tensor, Union[Tensor, Sequence[Optional[Tensor]]]].

+

Initializes a NdarrayOptimizationClosure instance.

+
+
Parameters:
+
    +
  • closure (Callable[[], tuple[Tensor, Sequence[Tensor | None]]]) – A ForwardBackwardClosure instance.

  • +
  • parameters (dict[str, Tensor]) – A dictionary of tensors representing the closure’s state. +Expected to correspond with the first len(parameters) optional +gradient tensors returned by closure.

  • +
  • as_array (Callable[[Tensor], npt.NDArray]) – Callable used to convert tensors to ndarrays.

  • +
  • get_state (Callable[[], npt.NDArray]) – Callable that returns the closure’s state as an ndarray. When +passed as None, defaults to calling get_tensors_as_ndarray_1d +on closure.parameters while passing as_array (if given by the user).

  • +
  • set_state (Callable[[npt.NDArray], None]) – Callable that takes a 1-dimensional ndarray and sets the +closure’s state. When passed as None, set_state defaults to +calling set_tensors_from_ndarray_1d with closure.parameters and +a given ndarray.

  • +
  • fill_value (float) – Fill value for parameters whose gradients are None. In most +cases, fill_value should either be zero or NaN.

  • +
  • persistent (bool) – Boolean specifying whether an ndarray should be retained +as a persistent buffer for gradients.

  • +
+
+
+
+
+property state: ndarray[Any, dtype[_ScalarType_co]]
+
+
+
+
+

Model Fitting Closures

+

Utilities for building model-based closures.

+
+
+botorch.optim.closures.model_closures.get_loss_closure(mll, data_loader=None, **kwargs)[source]
+

Public API for GetLossClosure dispatcher.

+

This method, and the dispatcher that powers it, acts as a clearing house +for factory functions that define how mll is evaluated.

+

Users may specify custom evaluation routines by registering a factory function +with GetLossClosure. These factories should be registered using the type signature

+
+

Type[MarginalLogLikeLihood], Type[Likelihood], Type[Model], Type[DataLoader].

+
+

The final argument, Type[DataLoader], is optional. Evaluation routines that obtain +training data from, e.g., mll.model should register this argument as type(None).

+
+
Parameters:
+
    +
  • mll (MarginalLogLikelihood) – A MarginalLogLikelihood instance whose negative defines the loss.

  • +
  • data_loader (DataLoader | None) – An optional DataLoader instance for cases where training +data is passed in rather than obtained from mll.model.

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

A closure that takes zero positional arguments and returns the negated +value of mll.

+
+
Return type:
+

Callable[[], Tensor]

+
+
+
+
+
+botorch.optim.closures.model_closures.get_loss_closure_with_grads(mll, parameters, data_loader=None, backward=<function Tensor.backward>, reducer=<method 'sum' of 'torch._C.TensorBase' objects>, context_manager=None, **kwargs)[source]
+

Public API for GetLossClosureWithGrads dispatcher.

+

In most cases, this method simply adds a backward pass to a loss closure obtained by +calling get_loss_closure. For further details, see get_loss_closure.

+
+
Parameters:
+
    +
  • mll (MarginalLogLikelihood) – A MarginalLogLikelihood instance whose negative defines the loss.

  • +
  • parameters (dict[str, Tensor]) – A dictionary of tensors whose grad fields are to be returned.

  • +
  • reducer (Callable[[Tensor], Tensor] | None) – Optional callable used to reduce the output of the forward pass.

  • +
  • data_loader (DataLoader | None) – An optional DataLoader instance for cases where training +data is passed in rather than obtained from mll.model.

  • +
  • context_manager (Callable | None) – An optional ContextManager used to wrap each forward-backward +pass. Defaults to a zero_grad_ctx that zeroes the gradients of +parameters upon entry. None may be passed as an alias for nullcontext.

  • +
  • backward (Callable[[Tensor], None])

  • +
  • kwargs (Any)

  • +
+
+
Returns:
+

A closure that takes zero positional arguments and returns the reduced and +negated value of mll along with the gradients of parameters.

+
+
Return type:
+

Callable[[], tuple[Tensor, tuple[Tensor, …]]]

+
+
+
+
+
+
+

Utilities

+
+

General Optimization Utilities

+

General-purpose optimization utilities.

+
+
+

Acquisition Optimization Utilities

+

Utilities for maximizing acquisition functions.

+
+
+botorch.optim.utils.acquisition_utils.columnwise_clamp(X, lower=None, upper=None, raise_on_violation=False)[source]
+

Clamp values of a Tensor in column-wise fashion (with support for t-batches).

+

This function is useful in conjunction with optimizers from the torch.optim +package, which don’t natively handle constraints. If you apply this after +a gradient step you can be fancy and call it “projected gradient descent”. +This funtion is also useful for post-processing candidates generated by the +scipy optimizer that satisfy bounds only up to numerical accuracy.

+
+
Parameters:
+
    +
  • X (Tensor) – The b x n x d input tensor. If 2-dimensional, b is assumed to be 1.

  • +
  • lower (float | Tensor | None) – The column-wise lower bounds. If scalar, apply bound to all columns.

  • +
  • upper (float | Tensor | None) – The column-wise upper bounds. If scalar, apply bound to all columns.

  • +
  • raise_on_violation (bool) – If True, raise an exception when the elments in X +are out of the specified bounds (up to numerical accuracy). This is +useful for post-processing candidates generated by optimizers that +satisfy imposed bounds only up to numerical accuracy.

  • +
+
+
Returns:
+

The clamped tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.utils.acquisition_utils.fix_features(X, fixed_features=None)[source]
+

Fix feature values in a Tensor.

+

The fixed features will have zero gradient in downstream calculations.

+
+
Parameters:
+
    +
  • X (Tensor) – input Tensor with shape … x p, where p is the number of features

  • +
  • fixed_features (Mapping[int, float | None] | None) – A mapping with keys as column indices and values +equal to what the feature should be set to in X. If the value is +None, that column is just considered fixed. Keys should be in the +range [0, p - 1].

  • +
+
+
Returns:
+

The tensor X with fixed features.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.optim.utils.acquisition_utils.get_X_baseline(acq_function)[source]
+

Extract X_baseline from an acquisition function.

+

This tries to find the baseline set of points. First, this checks if the +acquisition function has an X_baseline attribute. If it does not, +then this method attempts to use the model’s train_inputs as X_baseline.

+
+
Parameters:
+

acq_function (AcquisitionFunction) – The acquisition function.

+
+
Return type:
+

Tensor | None

+
+
+
+
Returns
+
An optional n x d-dim tensor of baseline points. This is None if no

baseline points are found.

+
+
+
+
+
+
+
+

Model Fitting Utilities

+

Utilities for fitting and manipulating models.

+
+
+class botorch.optim.utils.model_utils.TorchAttr(shape, dtype, device)[source]
+

Bases: NamedTuple

+

Create new instance of TorchAttr(shape, dtype, device)

+
+
Parameters:
+
    +
  • shape (Size)

  • +
  • dtype (dtype)

  • +
  • device (device)

  • +
+
+
+
+
+shape: Size
+

Alias for field number 0

+
+
+
+dtype: dtype
+

Alias for field number 1

+
+
+
+device: device
+

Alias for field number 2

+
+
+
+
+botorch.optim.utils.model_utils.get_data_loader(model, batch_size=1024, **kwargs)[source]
+
+
Parameters:
+
+
+
Return type:
+

DataLoader

+
+
+
+
+
+botorch.optim.utils.model_utils.get_parameters(module, requires_grad=None, name_filter=None)[source]
+

Helper method for obtaining a module’s parameters and their respective ranges.

+
+
Parameters:
+
    +
  • module (Module) – The target module from which parameters are to be extracted.

  • +
  • requires_grad (bool | None) – Optional Boolean used to filter parameters based on whether +or not their require_grad attribute matches the user provided value.

  • +
  • name_filter (Callable[[str], bool] | None) – Optional Boolean function used to filter parameters by name.

  • +
+
+
Returns:
+

A dictionary of parameters.

+
+
Return type:
+

dict[str, Tensor]

+
+
+
+
+
+botorch.optim.utils.model_utils.get_parameters_and_bounds(module, requires_grad=None, name_filter=None, default_bounds=(-inf, inf))[source]
+

Helper method for obtaining a module’s parameters and their respective ranges.

+
+
Parameters:
+
    +
  • module (Module) – The target module from which parameters are to be extracted.

  • +
  • name_filter (Callable[[str], bool] | None) – Optional Boolean function used to filter parameters by name.

  • +
  • requires_grad (bool | None) – Optional Boolean used to filter parameters based on whether +or not their require_grad attribute matches the user provided value.

  • +
  • default_bounds (tuple[float, float]) – Default lower and upper bounds for constrained parameters +with None typed bounds.

  • +
+
+
Returns:
+

A dictionary of parameters and a dictionary of parameter bounds.

+
+
Return type:
+

tuple[dict[str, Tensor], dict[str, tuple[float | None, float | None]]]

+
+
+
+
+
+botorch.optim.utils.model_utils.get_name_filter(patterns)[source]
+

Returns a binary function that filters strings (or iterables whose first +element is a string) according to a bank of excluded patterns. Typically, used +in conjunction with generators such as module.named_parameters().

+
+
Parameters:
+

patterns (Iterator[Pattern | str]) – A collection of regular expressions or strings that +define the set of names to be excluded.

+
+
Returns:
+

A binary function indicating whether or not an item should be filtered.

+
+
Return type:
+

Callable[[str | tuple[str, Any, …]], bool]

+
+
+
+
+
+botorch.optim.utils.model_utils.sample_all_priors(model, max_retries=100)[source]
+

Sample from hyperparameter priors (in-place).

+
+
Parameters:
+
    +
  • model (GPyTorchModel) – A GPyTorchModel.

  • +
  • max_retries (int)

  • +
+
+
Return type:
+

None

+
+
+
+
+
+

Numpy - Torch Conversion Tools

+

Utilities for interfacing Numpy and Torch.

+
+
+botorch.optim.utils.numpy_utils.as_ndarray(values, dtype=None, inplace=True)[source]
+

Helper for going from torch.Tensor to numpy.ndarray.

+
+
Parameters:
+
    +
  • values (Tensor) – Tensor to be converted to ndarray.

  • +
  • dtype (dtype | None) – Optional numpy.dtype for the converted tensor.

  • +
  • inplace (bool) – Boolean indicating whether memory should be shared if possible.

  • +
+
+
Returns:
+

An ndarray with the same data as values.

+
+
Return type:
+

ndarray[Any, dtype[_ScalarType_co]]

+
+
+
+
+
+botorch.optim.utils.numpy_utils.get_tensors_as_ndarray_1d(tensors, out=None, dtype=None, as_array=<function as_ndarray>)[source]
+
+
Parameters:
+
    +
  • tensors (Iterator[Tensor] | dict[str, Tensor])

  • +
  • out (ndarray[Any, dtype[_ScalarType_co]] | None)

  • +
  • dtype (dtype | str | None)

  • +
  • as_array (Callable[[Tensor], ndarray[Any, dtype[_ScalarType_co]]])

  • +
+
+
Return type:
+

ndarray[Any, dtype[_ScalarType_co]]

+
+
+
+
+
+botorch.optim.utils.numpy_utils.set_tensors_from_ndarray_1d(tensors, array)[source]
+

Sets the values of one more tensors based off of a vector of assignments.

+
+
Parameters:
+
    +
  • tensors (Iterator[Tensor] | dict[str, Tensor])

  • +
  • array (ndarray[Any, dtype[_ScalarType_co]])

  • +
+
+
Return type:
+

None

+
+
+
+
+
+botorch.optim.utils.numpy_utils.get_bounds_as_ndarray(parameters, bounds)[source]
+

Helper method for converting bounds into an ndarray.

+
+
Parameters:
+
    +
  • parameters (dict[str, Tensor]) – A dictionary of parameters.

  • +
  • bounds (dict[str, tuple[float | Tensor | None, float | Tensor | None]]) – A dictionary of (optional) lower and upper bounds.

  • +
+
+
Returns:
+

An ndarray of bounds.

+
+
Return type:
+

ndarray[Any, dtype[_ScalarType_co]] | None

+
+
+
+
+
+

Optimization with Timeouts

+
+
+botorch.optim.utils.timeout.minimize_with_timeout(fun, x0, args=(), method=None, jac=None, hess=None, hessp=None, bounds=None, constraints=(), tol=None, callback=None, options=None, timeout_sec=None)[source]
+

Wrapper around scipy.optimize.minimize to support timeout.

+

This method calls scipy.optimize.minimize with all arguments forwarded +verbatim. The only difference is that if provided a timeout_sec argument, +it will automatically stop the optimziation after the timeout is reached.

+

Internally, this is achieved by automatically constructing a wrapper callback +method that is injected to the scipy.optimize.minimize call and that keeps +track of the runtime and the optimization variables at the current iteration.

+
+
Parameters:
+
    +
  • fun (Callable[[ndarray[Any, dtype[_ScalarType_co]], ...], float])

  • +
  • x0 (ndarray[Any, dtype[_ScalarType_co]])

  • +
  • args (tuple[Any, ...])

  • +
  • method (str | None)

  • +
  • jac (str | Callable | bool | None)

  • +
  • hess (str | Callable | HessianUpdateStrategy | None)

  • +
  • hessp (Callable | None)

  • +
  • bounds (Sequence[tuple[float, float]] | Bounds | None)

  • +
  • tol (float | None)

  • +
  • callback (Callable | None)

  • +
  • options (dict[str, Any] | None)

  • +
  • timeout_sec (float | None)

  • +
+
+
Return type:
+

OptimizeResult

+
+
+
+
+
+

Parameter Constraint Utilities

+

Utility functions for constrained optimization.

+
+
+botorch.optim.parameter_constraints.make_scipy_bounds(X, lower_bounds=None, upper_bounds=None)[source]
+

Creates a scipy Bounds object for optimziation

+
+
Parameters:
+
    +
  • X (Tensor) – … x d tensor

  • +
  • lower_bounds (float | Tensor | None) – Lower bounds on each column (last dimension) of X. If +this is a single float, then all columns have the same bound.

  • +
  • upper_bounds (float | Tensor | None) – Lower bounds on each column (last dimension) of X. If +this is a single float, then all columns have the same bound.

  • +
+
+
Returns:
+

A scipy Bounds object if either lower_bounds or upper_bounds is not +None, and None otherwise.

+
+
Return type:
+

Bounds | None

+
+
+

Example

+
>>> X = torch.rand(5, 2)
+>>> scipy_bounds = make_scipy_bounds(X, 0.1, 0.8)
+
+
+
+
+
+botorch.optim.parameter_constraints.make_scipy_linear_constraints(shapeX, inequality_constraints=None, equality_constraints=None)[source]
+

Generate scipy constraints from torch representation.

+
+
Parameters:
+
    +
  • shapeX (Size) – The shape of the torch.Tensor to optimize over (i.e. (b) x q x d)

  • +
  • constraints (equality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs, where +indices is a single-dimensional index tensor (long dtype) containing +indices into the last dimension of X, coefficients is a +single-dimensional tensor of coefficients of the same length, and +rhs is a scalar.

  • +
  • constraints – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) == rhs (with indices +and coefficients of the same form as in inequality_constraints).

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

A list of dictionaries containing callables for constraint function +values and Jacobians and a string indicating the associated constraint +type (“eq”, “ineq”), as expected by scipy.minimize.

+
+
Return type:
+

list[dict[str, str | Callable[[ndarray], float] | Callable[[ndarray], ndarray]]]

+
+
+

This function assumes that constraints are the same for each input batch, +and broadcasts the constraints accordingly to the input batch shape. This +function does support constraints across elements of a q-batch if the +indices are a 2-d Tensor.

+

Example

+

The following will enforce that x[1] + 0.5 x[3] >= -0.1 for each x +in both elements of the q-batch, and each of the 3 t-batches:

+
>>> constraints = make_scipy_linear_constraints(
+>>>     torch.Size([3, 2, 4]),
+>>>     [(torch.tensor([1, 3]), torch.tensor([1.0, 0.5]), -0.1)],
+>>> )
+
+
+

The following will enforce that x[0, 1] + 0.5 x[1, 3] >= -0.1 where +x[0, :] is the first element of the q-batch and x[1, :] is the second +element of the q-batch, for each of the 3 t-batches:

+
>>> constraints = make_scipy_linear_constraints(
+>>>     torch.size([3, 2, 4])
+>>>     [(torch.tensor([[0, 1], [1, 3]), torch.tensor([1.0, 0.5]), -0.1)],
+>>> )
+
+
+
+
+
+botorch.optim.parameter_constraints.eval_lin_constraint(x, flat_idxr, coeffs, rhs)[source]
+

Evaluate a single linear constraint.

+
+
Parameters:
+
    +
  • x (ndarray[Any, dtype[_ScalarType_co]]) – The input array.

  • +
  • flat_idxr (list[int]) – The indices in x to consider.

  • +
  • coeffs (ndarray[Any, dtype[_ScalarType_co]]) – The coefficients corresponding to the indices.

  • +
  • rhs (float) – The right-hand-side of the constraint.

  • +
+
+
Returns:
+

sum_i (coeffs[i] * x[i]) - rhs

+
+
Return type:
+

The evaluted constraint

+
+
+
+
+
+botorch.optim.parameter_constraints.lin_constraint_jac(x, flat_idxr, coeffs, n)[source]
+

Return the Jacobian associated with a linear constraint.

+
+
Parameters:
+
    +
  • x (ndarray[Any, dtype[_ScalarType_co]]) – The input array.

  • +
  • flat_idxr (list[int]) – The indices for the elements of x that appear in the constraint.

  • +
  • coeffs (ndarray[Any, dtype[_ScalarType_co]]) – The coefficients corresponding to the indices.

  • +
  • n (int) – number of elements

  • +
+
+
Returns:
+

The Jacobian.

+
+
Return type:
+

ndarray[Any, dtype[_ScalarType_co]]

+
+
+
+
+
+botorch.optim.parameter_constraints.nonlinear_constraint_is_feasible(nonlinear_inequality_constraint, is_intrapoint, x)[source]
+

Checks if a nonlinear inequality constraint is fulfilled.

+
+
Parameters:
+
    +
  • nonlinear_inequality_constraint (Callable) – Callable to evaluate the +constraint.

  • +
  • intra – If True, the constraint is an intra-point constraint that +is applied pointwise and is broadcasted over the q-batch. Else, the +constraint has to evaluated over the whole q-batch and is a an +inter-point constraint.

  • +
  • x (Tensor) – Tensor of shape (b x q x d).

  • +
  • is_intrapoint (bool)

  • +
+
+
Returns:
+

True if the constraint is fulfilled, else False.

+
+
Return type:
+

bool

+
+
+
+
+
+botorch.optim.parameter_constraints.make_scipy_nonlinear_inequality_constraints(nonlinear_inequality_constraints, f_np_wrapper, x0, shapeX)[source]
+

Generate Scipy nonlinear inequality constraints from callables.

+
+
Parameters:
+
    +
  • nonlinear_inequality_constraints (list[tuple[Callable, bool]]) – A list of tuples representing the nonlinear +inequality constraints. The first element in the tuple is a callable +representing a constraint of the form callable(x) >= 0. In case of an +intra-point constraint, callable()`takes in an one-dimensional tensor of +shape `d and returns a scalar. In case of an inter-point constraint, +callable() takes a two dimensional tensor of shape q x d and again +returns a scalar. The second element is a boolean, indicating if it is an +intra-point or inter-point constraint (True for intra-point. False for +inter-point). For more information on intra-point vs inter-point +constraints, see the docstring of the inequality_constraints argument to +optimize_acqf(). The constraints will later be passed to the scipy +solver.

  • +
  • f_np_wrapper (Callable) – A wrapper function that given a constraint evaluates the value +and gradient (using autograd) of a numpy input and returns both the +objective and the gradient.

  • +
  • x0 (Tensor) – The starting point for SLSQP. We return this starting point in (rare) +cases where SLSQP fails and thus require it to be feasible.

  • +
  • shapeX (Size) – Shape of the three-dimensional batch X, that should be optimized.

  • +
+
+
Returns:
+

A list of dictionaries containing callables for constraint function +values and Jacobians and a string indicating the associated constraint +type (“eq”, “ineq”), as expected by scipy.minimize.

+
+
Return type:
+

list[dict]

+
+
+
+
+
+

Homotopy Utilities

+
+
+class botorch.optim.homotopy.FixedHomotopySchedule(values)[source]
+

Bases: object

+

Homotopy schedule with a fixed list of values.

+

Initialize FixedHomotopySchedule.

+
+
Parameters:
+

values (list[float]) – A list of values used in homotopy

+
+
+
+
+property num_steps: int
+
+
+
+property value: float
+
+
+
+property should_stop: bool
+
+
+
+restart()[source]
+
+
Return type:
+

None

+
+
+
+
+
+step()[source]
+
+
Return type:
+

None

+
+
+
+
+
+
+class botorch.optim.homotopy.LinearHomotopySchedule(start, end, num_steps)[source]
+

Bases: FixedHomotopySchedule

+

Linear homotopy schedule.

+

Initialize LinearHomotopySchedule.

+
+
Parameters:
+
    +
  • start (float) – start value of homotopy

  • +
  • end (float) – end value of homotopy

  • +
  • num_steps (int) – number of steps in the homotopy schedule.

  • +
+
+
+
+
+
+class botorch.optim.homotopy.LogLinearHomotopySchedule(start, end, num_steps)[source]
+

Bases: FixedHomotopySchedule

+

Log-linear homotopy schedule.

+

Initialize LogLinearHomotopySchedule.

+
+
Parameters:
+
    +
  • start (float) – start value of homotopy

  • +
  • end (float) – end value of homotopy

  • +
  • num_steps (int) – number of steps in the homotopy schedule.

  • +
+
+
+
+
+
+class botorch.optim.homotopy.HomotopyParameter(parameter, schedule)[source]
+

Bases: object

+

Homotopy parameter.

+

The parameter is expected to either be a torch parameter or a torch tensor which may +correspond to a buffer of a module. The parameter has a corresponding schedule.

+
+
Parameters:
+
+
+
+
+
+parameter: Parameter | Tensor
+
+
+
+schedule: FixedHomotopySchedule
+
+
+
+
+class botorch.optim.homotopy.Homotopy(homotopy_parameters, callbacks=None)[source]
+

Bases: object

+

Generic homotopy class.

+

This class is designed to be used in optimize_acqf_homotopy. Given a set of +homotopy parameters and corresponding schedules we step through the homotopies +until we have solved the final problem. We additionally support passing in a list +of callbacks that will be executed each time step, reset, and restart are +called.

+

Initialize the homotopy.

+
+
Parameters:
+
    +
  • homotopy_parameters (list[HomotopyParameter]) – List of homotopy parameters

  • +
  • callbacks (list[Callable] | None) – Optional list of callbacks that are executed each time +restart, reset, or step are called. These may be used to, e.g., +reinitialize the acquisition function which is needed when using qNEHVI.

  • +
+
+
+
+
+property should_stop: bool
+

Returns true if all schedules have reached the end.

+
+
+
+restart()[source]
+

Restart the homotopy to use the initial value in the schedule.

+
+
Return type:
+

None

+
+
+
+
+
+reset()[source]
+

Reset the homotopy parameter to their original values.

+
+
Return type:
+

None

+
+
+
+
+
+step()[source]
+

Take a step according to the schedules.

+
+
Return type:
+

None

+
+
+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/posteriors.html b/website-old/pages/api/posteriors.html new file mode 100644 index 0000000000..13f54767d5 --- /dev/null +++ b/website-old/pages/api/posteriors.html @@ -0,0 +1,1024 @@ + + + + + + + +
+
+
+
+
+

botorch.posteriors

+
+

Posterior APIs

+
+

Abstract Posterior API

+

Abstract base module for all botorch posteriors.

+
+
+class botorch.posteriors.posterior.Posterior[source]
+

Bases: ABC

+

Abstract base class for botorch posteriors.

+
+
+rsample_from_base_samples(sample_shape, base_samples)[source]
+

Sample from the posterior (with gradients) using base samples.

+

This is intended to be used with a sampler that produces the corresponding base +samples, and enables acquisition optimization via Sample Average Approximation.

+
+
Parameters:
+
    +
  • sample_shape (Size) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

  • +
  • base_samples (Tensor) – The base samples, obtained from the appropriate sampler. +This is a tensor of shape sample_shape x base_sample_shape.

  • +
+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+abstract rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+sample(sample_shape=None)[source]
+

Sample from the posterior without gradients.

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+abstract property device: device
+

The torch device of the distribution.

+
+
+
+abstract property dtype: dtype
+

The torch dtype of the distribution.

+
+
+
+quantile(value)[source]
+

Compute quantiles of the distribution.

+

For multi-variate distributions, this may return the quantiles of +the marginal distributions.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+density(value)[source]
+

The probability density (or mass) of the distribution.

+

For multi-variate distributions, this may return the density of +the marginal distributions.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+property base_sample_shape: Size
+

The base shape of the base samples expected in rsample.

+

Informs the sampler to produce base samples of shape +sample_shape x base_sample_shape.

+
+
+
+property batch_range: tuple[int, int]
+

The t-batch range.

+

This is used in samplers to identify the t-batch component of the +base_sample_shape. The base samples are expanded over the t-batches to +provide consistency in the acquisition values, i.e., to ensure that a +candidate produces same value regardless of its position on the t-batch.

+
+
+
+
+

Posterior List API

+

Abstract base module for all botorch posteriors.

+
+
+class botorch.posteriors.posterior_list.PosteriorList(*posteriors)[source]
+

Bases: Posterior

+

A Posterior represented by a list of independent Posteriors.

+

When at least one of the posteriors is a GaussianMixturePosterior, the other +posteriors are expanded to match the size of the GaussianMixturePosterior.

+

A Posterior represented by a list of independent Posteriors.

+
+
Parameters:
+

*posteriors (Posterior) – A variable number of single-outcome posteriors.

+
+
+

Example

+
>>> p_1 = model_1.posterior(test_X)
+>>> p_2 = model_2.posterior(test_X)
+>>> p_12 = PosteriorList(p_1, p_2)
+
+
+

Note: This is typically produced automatically in ModelList; it should +generally not be necessary for the end user to invoke it manually.

+
+
+property device: device
+

The torch device of the posterior.

+
+
+
+property dtype: dtype
+

The torch dtype of the posterior.

+
+
+
+property mean: Tensor
+

The mean of the posterior as a (b) x n x m-dim Tensor.

+

This is only supported if all posteriors provide a mean.

+
+
+
+property variance: Tensor
+

The variance of the posterior as a (b) x n x m-dim Tensor.

+

This is only supported if all posteriors provide a variance.

+
+
+
+rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+
+

Posteriors

+
+

Torch Posterior

+

Posterior module to be used with PyTorch distributions.

+
+
+class botorch.posteriors.torch.TorchPosterior(distribution)[source]
+

Bases: Posterior

+

A posterior based on a PyTorch Distribution.

+

NOTE: For any attribute that is not explicitly defined on the Posterior level, this +returns the corresponding attribute of the distribution. This allows easy access +to the distribution attributes, without having to expose them on the Posterior.

+

A posterior based on a PyTorch Distribution.

+
+
Parameters:
+

distribution (Distribution) – A PyTorch Distribution object.

+
+
+
+
+rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+

This is generally used with a sampler that produces the base samples.

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+property device: device
+

The torch device of the distribution.

+
+
+
+property dtype: dtype
+

The torch dtype of the distribution.

+
+
+
+quantile(value)[source]
+

Compute quantiles of the distribution.

+

For multi-variate distributions, this may return the quantiles of +the marginal distributions.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+density(value)[source]
+

The probability density (or mass if discrete) of the distribution.

+

For multi-variate distributions, this may return the density of +the marginal distributions.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

GPyTorch Posterior

+

Posterior module to be used with GPyTorch models.

+
+
+class botorch.posteriors.gpytorch.GPyTorchPosterior(distribution)[source]
+

Bases: TorchPosterior

+

A posterior based on GPyTorch’s multi-variate Normal distributions.

+

A posterior based on GPyTorch’s multi-variate Normal distributions.

+
+
Parameters:
+

distribution (MultivariateNormal) – A GPyTorch MultivariateNormal (single-output case) or +MultitaskMultivariateNormal (multi-output case).

+
+
+
+
+distribution: MultivariateNormal
+
+
+
+property mvn: MultivariateNormal
+

Expose the distribution as a backwards-compatible attribute.

+
+
+
+property base_sample_shape: Size
+

The shape of a base sample used for constructing posterior samples.

+
+
+
+property batch_range: tuple[int, int]
+

The t-batch range.

+

This is used in samplers to identify the t-batch component of the +base_sample_shape. The base samples are expanded over the t-batches to +provide consistency in the acquisition values, i.e., to ensure that a +candidate produces same value regardless of its position on the t-batch.

+
+
+
+rsample_from_base_samples(sample_shape, base_samples)[source]
+

Sample from the posterior (with gradients) using base samples.

+

This is intended to be used with a sampler that produces the corresponding base +samples, and enables acquisition optimization via Sample Average Approximation.

+
+
Parameters:
+
    +
  • sample_shape (Size) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

  • +
  • base_samples (Tensor) – A Tensor of N(0, I) base samples of shape +sample_shape x base_sample_shape, typically obtained from +a Sampler. This is used for deterministic optimization.

  • +
+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+property mean: Tensor
+

The posterior mean.

+
+
+
+property variance: Tensor
+

The posterior variance.

+
+
+
+quantile(value)[source]
+

Compute the quantiles of the marginal distributions.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+density(value)[source]
+

The probability density of the marginal distributions.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+botorch.posteriors.gpytorch.scalarize_posterior_gpytorch(posterior, weights, offset=0.0)[source]
+

Helper function for scalarize_posterior, producing a mean and +variance.

+

This mean and variance are consumed by scalarize_posterior to produce +a GPyTorchPosterior.

+
+
Parameters:
+
    +
  • posterior (GPyTorchPosterior) – The posterior over m outcomes to be scalarized. +Supports t-batching.

  • +
  • weights (Tensor) – A tensor of weights of size m.

  • +
  • offset (float) – The offset of the affine transformation.

  • +
+
+
Returns:
+

+
The transformed (single-output) posterior. If the input posterior has

mean mu and covariance matrix Sigma, this posterior has mean +weights^T * mu and variance weights^T Sigma w.

+
+
+

+
+
Return type:
+

tuple[Tensor, Tensor | LinearOperator]

+
+
+

Example

+

Example for a model with two outcomes:

+
>>> X = torch.rand(1, 2)
+>>> posterior = model.posterior(X)
+>>> weights = torch.tensor([0.5, 0.25])
+>>> mean, cov = scalarize_posterior_gpytorch(posterior, weights=weights)
+>>> mvn = MultivariateNormal(mean, cov)
+>>> new_posterior = GPyTorchPosterior
+
+
+
+
+
+botorch.posteriors.gpytorch.scalarize_posterior(posterior, weights, offset=0.0)[source]
+

Affine transformation of a multi-output posterior.

+
+
Parameters:
+
    +
  • posterior (GPyTorchPosterior | PosteriorList) – The posterior over m outcomes to be scalarized. +Supports t-batching. Can be either a GPyTorchPosterior, +or a PosteriorList that contains GPyTorchPosteriors all with q=1.

  • +
  • weights (Tensor) – A tensor of weights of size m.

  • +
  • offset (float) – The offset of the affine transformation.

  • +
+
+
Returns:
+

+
The transformed (single-output) posterior. If the input posterior has

mean mu and covariance matrix Sigma, this posterior has mean +weights^T * mu and variance weights^T Sigma w.

+
+
+

+
+
Return type:
+

GPyTorchPosterior

+
+
+

Example

+

Example for a model with two outcomes:

+
>>> X = torch.rand(1, 2)
+>>> posterior = model.posterior(X)
+>>> weights = torch.tensor([0.5, 0.25])
+>>> new_posterior = scalarize_posterior(posterior, weights=weights)
+
+
+
+
+
+

Ensemble Posterior

+

Ensemble posteriors. Used in conjunction with ensemble models.

+
+
+class botorch.posteriors.ensemble.EnsemblePosterior(values)[source]
+

Bases: Posterior

+

Ensemble posterior, that should be used for ensemble models that compute +eagerly a finite number of samples per X value as for example a deep ensemble +or a random forest.

+
+
Parameters:
+

values (Tensor) – Values of the samples produced by this posterior as +a (b) x s x q x m tensor where m is the output size of the +model and s is the ensemble size.

+
+
+
+
+property ensemble_size: int
+

The size of the ensemble

+
+
+
+property weights: Tensor
+

The weights of the individual models in the ensemble. +Equally weighted by default.

+
+
+
+property device: device
+

The torch device of the posterior.

+
+
+
+property dtype: dtype
+

The torch dtype of the posterior.

+
+
+
+property mean: Tensor
+

The mean of the posterior as a (b) x n x m-dim Tensor.

+
+
+
+property variance: Tensor
+

The variance of the posterior as a (b) x n x m-dim Tensor.

+

Computed as the sample variance across the ensemble outputs.

+
+
+
+rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+

Based on the sample shape, base samples are generated and passed to +rsample_from_base_samples.

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample_from_base_samples(sample_shape, base_samples)[source]
+

Sample from the posterior (with gradients) using base samples.

+

This is intended to be used with a sampler that produces the corresponding base +samples, and enables acquisition optimization via Sample Average Approximation.

+
+
Parameters:
+
    +
  • sample_shape (Size) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

  • +
  • base_samples (Tensor) – A Tensor of indices as base samples of shape +sample_shape, typically obtained from IndexSampler. +This is used for deterministic optimization. The predictions of +the ensemble corresponding to the indices are then sampled.

  • +
+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Higher Order GP Posterior

+
+
+class botorch.posteriors.higher_order.HigherOrderGPPosterior(distribution, joint_covariance_matrix, train_train_covar, test_train_covar, train_targets, output_shape, num_outputs)[source]
+

Bases: GPyTorchPosterior

+

Posterior class for a Higher order Gaussian process model [Zhe2019hogp]. Extends +the standard GPyTorch posterior class by overwriting the rsample method. +The posterior variance is handled internally by the HigherOrderGP model. +HOGP is a tensorized GP model so the posterior covariance grows to be extremely +large, but is highly structured, which means that we can exploit Kronecker +identities to sample from the posterior using Matheron’s rule as described in +[Doucet2010sampl].

+

In general, this posterior should ONLY be used for HOGP models +that have highly structured covariances. It should also only be used internally when +called from the HigherOrderGP.posterior(…) method. At this time, the posterior +does not support gradients with respect to the training data.

+

A Posterior for HigherOrderGP models.

+
+
Parameters:
+
    +
  • distribution (MultivariateNormal) – Posterior multivariate normal distribution.

  • +
  • joint_covariance_matrix (LinearOperator) – Joint test train covariance matrix over the entire +tensor.

  • +
  • train_train_covar (LinearOperator) – Covariance matrix of train points in the data space.

  • +
  • test_train_covar (LinearOperator) – Covariance matrix of test x train points +in the data space.

  • +
  • train_targets (Tensor) – Training responses vectorized.

  • +
  • output_shape (Size) – Shape output training responses.

  • +
  • num_outputs (int) – Batch shaping of model.

  • +
+
+
+
+
+property base_sample_shape
+

The shape of a base sample used for constructing posterior samples.

+

Overwrites the standard base_sample_shape call to inform samplers that +n + 2 n_train samples need to be drawn rather than n samples.

+
+
+
+property batch_range: tuple[int, int]
+

The t-batch range.

+

This is used in samplers to identify the t-batch component of the +base_sample_shape. The base samples are expanded over the t-batches to +provide consistency in the acquisition values, i.e., to ensure that a +candidate produces same value regardless of its position on the t-batch.

+
+
+
+rsample_from_base_samples(sample_shape, base_samples)[source]
+

Sample from the posterior (with gradients) using base samples.

+

As the posterior covariance is difficult to draw from in this model, +we implement Matheron’s rule as described in [Doucet2010sampl]-. This may not +work entirely correctly for deterministic base samples unless base samples +are provided that are of shape n + 2 * n_train because the sampling method +draws 2 * n_train samples as well as the standard n. +samples.

+
+
Parameters:
+
    +
  • sample_shape (Size) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

  • +
  • base_samples (Tensor | None) – An (optional) Tensor of N(0, I) base samples of +appropriate dimension, typically obtained from a Sampler. +This is used for deterministic optimization.

  • +
+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multitask GP Posterior

+
+
+class botorch.posteriors.multitask.MultitaskGPPosterior(distribution, joint_covariance_matrix, test_train_covar, train_diff, test_mean, train_train_covar, train_noise, test_noise=None)[source]
+

Bases: GPyTorchPosterior

+

Posterior class for a Kronecker Multi-task GP model using with ICM kernel. +Extends the standard GPyTorch posterior class by overwriting the rsample +method. In general, this posterior should ONLY be used for MTGP models +that have structured covariances. It should also only be used internally when +called from the KroneckerMultiTaskGP.posterior(…) method.

+
+
Parameters:
+
    +
  • distribution (MultivariateNormal) – Posterior multivariate normal distribution.

  • +
  • joint_covariance_matrix (LinearOperator) – Joint test train covariance matrix over the entire +tensor.

  • +
  • test_train_covar (LinearOperator) – Covariance matrix of test x train points in the data +space.

  • +
  • train_diff (Tensor) – Difference between train mean and train responses.

  • +
  • test_mean (Tensor) – Test mean response.

  • +
  • train_train_covar (LinearOperator) – Covariance matrix of train points in the data space.

  • +
  • train_noise (LinearOperator | Tensor) – Training noise covariance.

  • +
  • test_noise (LinearOperator | Tensor | None) – Only used if posterior should contain observation noise. +Testing noise covariance.

  • +
+
+
+
+
+property base_sample_shape: Size
+

The shape of a base sample used for constructing posterior samples.

+

Overwrites the standard base_sample_shape call to inform samplers that +n + 2 n_train samples need to be drawn rather than n samples.

+
+
+
+property batch_range: tuple[int, int]
+

The t-batch range.

+

This is used in samplers to identify the t-batch component of the +base_sample_shape. The base samples are expanded over the t-batches to +provide consistency in the acquisition values, i.e., to ensure that a +candidate produces same value regardless of its position on the t-batch.

+
+
+
+rsample_from_base_samples(sample_shape, base_samples, train_diff=None)[source]
+

Sample from the posterior (with gradients) using base samples.

+
+
Parameters:
+
    +
  • sample_shape (Size) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

  • +
  • base_samples (Tensor | None) – An (optional) Tensor of N(0, I) base samples of +appropriate dimension, typically obtained from a Sampler. +This is used for deterministic optimization.

  • +
  • train_diff (Tensor | None) – Difference between train mean and train responses to assume +during sampling.

  • +
+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Transformed Posterior

+
+
+class botorch.posteriors.transformed.TransformedPosterior(posterior, sample_transform, mean_transform=None, variance_transform=None)[source]
+

Bases: Posterior

+

A generic transformation of a posterior (implicitly represented).

+

An implicitly represented transformed posterior.

+
+
Parameters:
+
    +
  • posterior (Posterior) – The posterior object to be transformed.

  • +
  • sample_transform (Callable[[Tensor], Tensor]) – A callable applying a sample-level transform to a +sample_shape x batch_shape x q x m-dim tensor of samples from +the original posterior, returning a tensor of samples of the +same shape.

  • +
  • mean_transform (Callable[[Tensor, Tensor], Tensor] | None) – A callable transforming a 2-tuple of mean and +variance (both of shape batch_shape x m x o) of the original +posterior to the mean of the transformed posterior.

  • +
  • variance_transform (Callable[[Tensor, Tensor], Tensor] | None) – A callable transforming a 2-tuple of mean and +variance (both of shape batch_shape x m x o) of the original +posterior to a variance of the transformed posterior.

  • +
+
+
+
+
+property base_sample_shape: Size
+

The shape of a base sample used for constructing posterior samples.

+
+
+
+property batch_range: tuple[int, int]
+

The t-batch range.

+

This is used in samplers to identify the t-batch component of the +base_sample_shape. The base samples are expanded over the t-batches to +provide consistency in the acquisition values, i.e., to ensure that a +candidate produces same value regardless of its position on the t-batch.

+
+
+
+property device: device
+

The torch device of the posterior.

+
+
+
+property dtype: dtype
+

The torch dtype of the posterior.

+
+
+
+property mean: Tensor
+

The mean of the posterior as a batch_shape x n x m-dim Tensor.

+
+
+
+property variance: Tensor
+

The variance of the posterior as a batch_shape x n x m-dim Tensor.

+
+
+
+rsample_from_base_samples(sample_shape, base_samples)[source]
+

Sample from the posterior (with gradients) using base samples.

+

This is intended to be used with a sampler that produces the corresponding base +samples, and enables acquisition optimization via Sample Average Approximation.

+
+
Parameters:
+
    +
  • sample_shape (Size) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

  • +
  • base_samples (Tensor) – The base samples, obtained from the appropriate sampler. +This is a tensor of shape sample_shape x base_sample_shape.

  • +
+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample(sample_shape=None)[source]
+

Sample from the posterior (with gradients).

+
+
Parameters:
+

sample_shape (Size | None) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Fully Bayesian Posterior

+
+
+botorch.posteriors.fully_bayesian.batched_bisect(f, target, bounds, tol=1e-06, max_steps=32)[source]
+

Batched bisection with a fixed number of steps.

+
+
Parameters:
+
    +
  • f (Callable) – Target function that takes a (b1 x … x bk)-dim tensor and returns a +(b1 x … x bk)-dim tensor.

  • +
  • target (float) – Scalar target value of type float.

  • +
  • bounds (Tensor) – Lower and upper bounds, of size 2 x b1 x … x bk.

  • +
  • tol (float) – We termniate if all elements satisfy are within tol of the target.

  • +
  • max_steps (int) – Maximum number of bisection steps.

  • +
+
+
Returns:
+

Tensor X of size b1 x … x bk such that f(X) = target.

+
+
+
+
+
+class botorch.posteriors.fully_bayesian.GaussianMixturePosterior(distribution)[source]
+

Bases: GPyTorchPosterior

+

A Gaussian mixture posterior.

+

The MCMC batch dimension that corresponds to the models in the mixture is located +at MCMC_DIM (defined at the top of this file). Note that while each MCMC sample +corresponds to a Gaussian posterior, the posterior is rather a mixture of Gaussian +distributions.

+

A posterior for a fully Bayesian model.

+
+
Parameters:
+

distribution (MultivariateNormal) – A GPyTorch MultivariateNormal (single-output case)

+
+
+
+
+property mixture_mean: Tensor
+

The posterior mean for the mixture of models.

+
+
+
+property mixture_variance: Tensor
+

The posterior variance for the mixture of models.

+
+
+
+property mixture_covariance_matrix: Tensor
+

The posterior covariance matrix for the mixture of models.

+
+
+
+quantile(value)[source]
+

Compute the posterior quantiles for the mixture of models.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+property batch_range: tuple[int, int]
+

The t-batch range.

+

This is used in samplers to identify the t-batch component of the +base_sample_shape. The base samples are expanded over the t-batches to +provide consistency in the acquisition values, i.e., to ensure that a +candidate produces same value regardless of its position on the t-batch.

+
+
+
+
+class botorch.posteriors.fully_bayesian.FullyBayesianPosterior(distribution)[source]
+

Bases: GaussianMixturePosterior

+

For backwards compatibility.

+

DEPRECATED.

+
+
Parameters:
+

distribution (MultivariateNormal)

+
+
+
+
+
+
+

Utilities

+
+

Base Samples

+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/py-modindex.html b/website-old/pages/api/py-modindex.html new file mode 100644 index 0000000000..ce92543c58 --- /dev/null +++ b/website-old/pages/api/py-modindex.html @@ -0,0 +1,938 @@ + + + + + + + +
+
+
+
+

Python Module Index

+
+b +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+b
+botorch +
    + botorch.acquisition +
    + botorch.acquisition.acquisition +
    + botorch.acquisition.active_learning +
    + botorch.acquisition.analytic +
    + botorch.acquisition.bayesian_active_learning +
    + botorch.acquisition.cached_cholesky +
    + botorch.acquisition.cost_aware +
    + botorch.acquisition.decoupled +
    + botorch.acquisition.factory +
    + botorch.acquisition.fixed_feature +
    + botorch.acquisition.input_constructors +
    + botorch.acquisition.joint_entropy_search +
    + botorch.acquisition.knowledge_gradient +
    + botorch.acquisition.logei +
    + botorch.acquisition.max_value_entropy_search +
    + botorch.acquisition.monte_carlo +
    + botorch.acquisition.multi_objective.analytic +
    + botorch.acquisition.multi_objective.base +
    + botorch.acquisition.multi_objective.hypervolume_knowledge_gradient +
    + botorch.acquisition.multi_objective.joint_entropy_search +
    + botorch.acquisition.multi_objective.logei +
    + botorch.acquisition.multi_objective.max_value_entropy_search +
    + botorch.acquisition.multi_objective.monte_carlo +
    + botorch.acquisition.multi_objective.multi_fidelity +
    + botorch.acquisition.multi_objective.multi_output_risk_measures +
    + botorch.acquisition.multi_objective.objective +
    + botorch.acquisition.multi_objective.parego +
    + botorch.acquisition.multi_objective.predictive_entropy_search +
    + botorch.acquisition.multi_objective.utils +
    + botorch.acquisition.multi_step_lookahead +
    + botorch.acquisition.objective +
    + botorch.acquisition.penalized +
    + botorch.acquisition.predictive_entropy_search +
    + botorch.acquisition.preference +
    + botorch.acquisition.prior_guided +
    + botorch.acquisition.proximal +
    + botorch.acquisition.risk_measures +
    + botorch.acquisition.thompson_sampling +
    + botorch.acquisition.utils +
    + botorch.cross_validation +
    + botorch.exceptions +
    + botorch.exceptions.errors +
    + botorch.exceptions.warnings +
    + botorch.fit +
    + botorch.generation +
    + botorch.generation.gen +
    + botorch.generation.sampling +
    + botorch.generation.utils +
    + botorch.logging +
    + botorch.models +
    + botorch.models.approximate_gp +
    + botorch.models.contextual +
    + botorch.models.contextual_multioutput +
    + botorch.models.converter +
    + botorch.models.cost +
    + botorch.models.deterministic +
    + botorch.models.ensemble +
    + botorch.models.fully_bayesian +
    + botorch.models.fully_bayesian_multitask +
    + botorch.models.gp_regression +
    + botorch.models.gp_regression_fidelity +
    + botorch.models.gp_regression_mixed +
    + botorch.models.gpytorch +
    + botorch.models.higher_order_gp +
    + botorch.models.kernels.categorical +
    + botorch.models.kernels.contextual_lcea +
    + botorch.models.kernels.contextual_sac +
    + botorch.models.kernels.downsampling +
    + botorch.models.kernels.exponential_decay +
    + botorch.models.kernels.infinite_width_bnn +
    + botorch.models.kernels.linear_truncated_fidelity +
    + botorch.models.kernels.orthogonal_additive_kernel +
    + botorch.models.likelihoods.pairwise +
    + botorch.models.model +
    + botorch.models.model_list_gp_regression +
    + botorch.models.multitask +
    + botorch.models.pairwise_gp +
    + botorch.models.transforms.factory +
    + botorch.models.transforms.input +
    + botorch.models.transforms.outcome +
    + botorch.models.transforms.utils +
    + botorch.models.utils.assorted +
    + botorch.models.utils.gpytorch_modules +
    + botorch.models.utils.inducing_point_allocators +
    + botorch.optim +
    + botorch.optim.closures.core +
    + botorch.optim.closures.model_closures +
    + botorch.optim.core +
    + botorch.optim.fit +
    + botorch.optim.homotopy +
    + botorch.optim.initializers +
    + botorch.optim.optimize +
    + botorch.optim.optimize_homotopy +
    + botorch.optim.optimize_mixed +
    + botorch.optim.parameter_constraints +
    + botorch.optim.stopping +
    + botorch.optim.utils.acquisition_utils +
    + botorch.optim.utils.common +
    + botorch.optim.utils.model_utils +
    + botorch.optim.utils.numpy_utils +
    + botorch.optim.utils.timeout +
    + botorch.posteriors +
    + botorch.posteriors.base_samples +
    + botorch.posteriors.ensemble +
    + botorch.posteriors.fully_bayesian +
    + botorch.posteriors.gpytorch +
    + botorch.posteriors.higher_order +
    + botorch.posteriors.multitask +
    + botorch.posteriors.posterior +
    + botorch.posteriors.posterior_list +
    + botorch.posteriors.torch +
    + botorch.posteriors.transformed +
    + botorch.sampling +
    + botorch.sampling.base +
    + botorch.sampling.get_sampler +
    + botorch.sampling.index_sampler +
    + botorch.sampling.list_sampler +
    + botorch.sampling.normal +
    + botorch.sampling.pairwise_samplers +
    + botorch.sampling.pathwise.features.generators +
    + botorch.sampling.pathwise.features.maps +
    + botorch.sampling.pathwise.paths +
    + botorch.sampling.pathwise.posterior_samplers +
    + botorch.sampling.pathwise.prior_samplers +
    + botorch.sampling.pathwise.update_strategies +
    + botorch.sampling.pathwise.utils +
    + botorch.sampling.qmc +
    + botorch.sampling.stochastic_samplers +
    + botorch.settings +
    + botorch.test_functions +
    + botorch.test_functions.base +
    + botorch.test_functions.multi_fidelity +
    + botorch.test_functions.multi_objective +
    + botorch.test_functions.multi_objective_multi_fidelity +
    + botorch.test_functions.sensitivity_analysis +
    + botorch.test_functions.synthetic +
    + botorch.test_functions.utils +
    + botorch.test_utils +
    + botorch.test_utils.mock +
    + botorch.utils +
    + botorch.utils.constants +
    + botorch.utils.constraints +
    + botorch.utils.containers +
    + botorch.utils.context_managers +
    + botorch.utils.datasets +
    + botorch.utils.dispatcher +
    + botorch.utils.feasible_volume +
    + botorch.utils.gp_sampling +
    + botorch.utils.low_rank +
    + botorch.utils.multi_objective.box_decompositions.box_decomposition +
    + botorch.utils.multi_objective.box_decompositions.box_decomposition_list +
    + botorch.utils.multi_objective.box_decompositions.dominated +
    + botorch.utils.multi_objective.box_decompositions.non_dominated +
    + botorch.utils.multi_objective.box_decompositions.utils +
    + botorch.utils.multi_objective.hypervolume +
    + botorch.utils.multi_objective.pareto +
    + botorch.utils.multi_objective.scalarization +
    + botorch.utils.multitask +
    + botorch.utils.objective +
    + botorch.utils.probability.bvn +
    + botorch.utils.probability.lin_ess +
    + botorch.utils.probability.linalg +
    + botorch.utils.probability.mvnxpb +
    + botorch.utils.probability.truncated_multivariate_normal +
    + botorch.utils.probability.unified_skew_normal +
    + botorch.utils.probability.utils +
    + botorch.utils.rounding +
    + botorch.utils.safe_math +
    + botorch.utils.sampling +
    + botorch.utils.test_helpers +
    + botorch.utils.testing +
    + botorch.utils.torch +
    + botorch.utils.transforms +
    + botorch.utils.types +
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/sampling.html b/website-old/pages/api/sampling.html new file mode 100644 index 0000000000..01c9476325 --- /dev/null +++ b/website-old/pages/api/sampling.html @@ -0,0 +1,1322 @@ + + + + + + + +
+
+
+
+
+

botorch.sampling

+
+

Monte-Carlo Sampler API

+

The base class for sampler modules to be used with MC-evaluated acquisition functions.

+
+
+class botorch.sampling.base.MCSampler(sample_shape, seed=None)[source]
+

Bases: Module, ABC

+

Abstract base class for Samplers.

+

Subclasses must implement the forward method.

+

Example

+

This method is usually not called directly, but via the sampler’s +__call__ method: +>>> posterior = model.posterior(test_X) +>>> samples = sampler(posterior)

+

Abstract base class for samplers.

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate. The full shape +of the samples is given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – An optional seed to use for sampling.

  • +
+
+
+
+
+abstract forward(posterior)[source]
+

Draws MC samples from the posterior.

+
+
Parameters:
+

posterior (Posterior) – The posterior to sample from.

+
+
Returns:
+

The samples drawn from the posterior.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Index Sampler

+

Sampler to be used with EnsemblePosteriors to enable +deterministic optimization of acquisition functions with ensemble models.

+
+
+class botorch.sampling.index_sampler.IndexSampler(sample_shape, seed=None)[source]
+

Bases: MCSampler

+

A sampler that calls posterior.rsample_from_base_samples to +generate the samples via index base samples.

+

Abstract base class for samplers.

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate. The full shape +of the samples is given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – An optional seed to use for sampling.

  • +
+
+
+
+
+forward(posterior)[source]
+

Draws MC samples from the posterior.

+
+
Parameters:
+

posterior (EnsemblePosterior) – The ensemble posterior to sample from.

+
+
Returns:
+

The samples drawn from the posterior.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Get Sampler Helper

+
+
+botorch.sampling.get_sampler.get_sampler(posterior, sample_shape, *, seed=None)[source]
+

Get the sampler for the given posterior.

+

The sampler can be used as sampler(posterior) to produce samples +suitable for use in acquisition function optimization via SAA.

+
+
Parameters:
+
    +
  • posterior (TorchPosterior) – A Posterior to get the sampler for.

  • +
  • sample_shape (Size) – The sample shape of the samples produced by the +given sampler. The full shape of the resulting samples is +given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – Seed used to initialize sampler.

  • +
+
+
Returns:
+

The MCSampler object for the given posterior.

+
+
Return type:
+

MCSampler

+
+
+
+
+
+

List Sampler

+

A SamplerList for sampling from a PosteriorList.

+
+
+class botorch.sampling.list_sampler.ListSampler(*samplers)[source]
+

Bases: MCSampler

+

A list of samplers for sampling from a PosteriorList.

+
+
Parameters:
+

samplers (MCSampler) – A variable number of samplers. This should include +a sampler for each posterior.

+
+
+
+
+property sample_shape: Size
+

The sample shape of the underlying samplers.

+
+
+
+forward(posterior)[source]
+

Samples from the posteriors and concatenates the samples.

+
+
Parameters:
+

posterior (PosteriorList) – A PosteriorList to sample from.

+
+
Returns:
+

The samples drawn from the posterior.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Gaussian Monte-Carlo Samplers

+

Sampler modules producing N(0,1) samples, to be used with MC-evaluated +acquisition functions and Gaussian posteriors.

+
+
+class botorch.sampling.normal.NormalMCSampler(sample_shape, seed=None)[source]
+

Bases: MCSampler, ABC

+

Base class for samplers producing (possibly QMC) N(0,1) samples.

+

Subclasses must implement the _construct_base_samples method.

+

Abstract base class for samplers.

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate. The full shape +of the samples is given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – An optional seed to use for sampling.

  • +
+
+
+
+
+forward(posterior)[source]
+

Draws MC samples from the posterior.

+
+
Parameters:
+

posterior (Posterior) – The posterior to sample from.

+
+
Returns:
+

The samples drawn from the posterior.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.normal.IIDNormalSampler(sample_shape, seed=None)[source]
+

Bases: NormalMCSampler

+

Sampler for MC base samples using iid N(0,1) samples.

+

Example

+
>>> sampler = IIDNormalSampler(1000, seed=1234)
+>>> posterior = model.posterior(test_X)
+>>> samples = sampler(posterior)
+
+
+

Abstract base class for samplers.

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate. The full shape +of the samples is given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – An optional seed to use for sampling.

  • +
+
+
+
+
+
+class botorch.sampling.normal.SobolQMCNormalSampler(sample_shape, seed=None)[source]
+

Bases: NormalMCSampler

+

Sampler for quasi-MC N(0,1) base samples using Sobol sequences.

+

Example

+
>>> sampler = SobolQMCNormalSampler(torch.Size([1024]), seed=1234)
+>>> posterior = model.posterior(test_X)
+>>> samples = sampler(posterior)
+
+
+

Abstract base class for samplers.

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate. The full shape +of the samples is given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – An optional seed to use for sampling.

  • +
+
+
+
+
+
+

Pairwise Monte-Carlo Samplers

+
+
+class botorch.sampling.pairwise_samplers.PairwiseMCSampler(max_num_comparisons=None, seed=None)[source]
+

Bases: MCSampler

+

Abstract class for Pairwise MC Sampler.

+

This sampler will sample pairwise comparisons. It is to be used together +with PairwiseGP and BoTorch acquisition functions (e.g., qKnowledgeGradient)

+
+
Parameters:
+
    +
  • max_num_comparisons (int) – Max number of comparisons drawn within samples. +If None, use all possible pairwise comparisons

  • +
  • seed (int) – The seed for np.random.seed. If omitted, use a random seed. +May be overwritten by sibling classes or subclasses.

  • +
+
+
+
+
+forward(posterior)[source]
+

Draws MC samples from the posterior and make comparisons

+
+
Parameters:
+

posterior (Posterior) – The Posterior to sample from. +The returned samples are expected to have output dimension of 1.

+
+
Returns:
+

Posterior sample pairwise comparisons.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.pairwise_samplers.PairwiseIIDNormalSampler(sample_shape, seed=None, max_num_comparisons=None, **kwargs)[source]
+

Bases: PairwiseMCSampler, IIDNormalSampler

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate.

  • +
  • seed (int | None) – The seed for the RNG. If omitted, use a random seed.

  • +
  • max_num_comparisons (int) – Max number of comparisons drawn within samples. +If None, use all possible pairwise comparisons.

  • +
  • kwargs (Any) – Catch-all for deprecated arguments.

  • +
+
+
+
+
+
+class botorch.sampling.pairwise_samplers.PairwiseSobolQMCNormalSampler(sample_shape, seed=None, max_num_comparisons=None, **kwargs)[source]
+

Bases: PairwiseMCSampler, SobolQMCNormalSampler

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate.

  • +
  • seed (int | None) – The seed for the RNG. If omitted, use a random seed.

  • +
  • max_num_comparisons (int) – Max number of comparisons drawn within samples. +If None, use all possible pairwise comparisons.

  • +
  • kwargs (Any) – Catch-all for deprecated arguments.

  • +
+
+
+
+
+
+

QMC Base Functionality

+

Quasi Monte-Carlo sampling from Normal distributions.

+

References:

+
+
+[Pages2018numprob] +(1,2) +

G. Pages. Numerical Probability: An Introduction with Applications to +Finance. Universitext. Springer International Publishing, 2018.

+
+
+
+
+class botorch.sampling.qmc.NormalQMCEngine(d, seed=None, inv_transform=False)[source]
+

Bases: object

+

Engine for qMC sampling from a Multivariate Normal N(0, I_d).

+

By default, this implementation uses Box-Muller transformed Sobol samples +following pg. 123 in [Pages2018numprob]. To use the inverse transform +instead, set inv_transform=True.

+

Example

+
>>> engine = NormalQMCEngine(3)
+>>> samples = engine.draw(16)
+
+
+

Engine for drawing qMC samples from a multivariate normal N(0, I_d).

+
+
Parameters:
+
    +
  • d (int) – The dimension of the samples.

  • +
  • seed (int | None) – The seed with which to seed the random number generator of the +underlying SobolEngine.

  • +
  • inv_transform (bool) – If True, use inverse transform instead of Box-Muller.

  • +
+
+
+
+
+draw(n=1, out=None, dtype=None)[source]
+

Draw n qMC samples from the standard Normal.

+
+
Parameters:
+
    +
  • n (int) – The number of samples to draw. As a best practice, use powers of 2.

  • +
  • out (Tensor | None) – An option output tensor. If provided, draws are put into this +tensor, and the function returns None.

  • +
  • dtype (dtype | None) – The desired torch data type (ignored if out is provided). +If None, uses torch.get_default_dtype().

  • +
+
+
Returns:
+

A n x d tensor of samples if out=None and None otherwise.

+
+
Return type:
+

Tensor | None

+
+
+
+
+
+
+class botorch.sampling.qmc.MultivariateNormalQMCEngine(mean, cov, seed=None, inv_transform=False)[source]
+

Bases: object

+

Engine for qMC sampling from a multivariate Normal N(mu, Sigma).

+

By default, this implementation uses Box-Muller transformed Sobol samples +following pg. 123 in [Pages2018numprob]. To use the inverse transform +instead, set inv_transform=True.

+

Example

+
>>> mean = torch.tensor([1.0, 2.0])
+>>> cov = torch.tensor([[1.0, 0.25], [0.25, 2.0]])
+>>> engine = MultivariateNormalQMCEngine(mean, cov)
+>>> samples = engine.draw(16)
+
+
+

Engine for qMC sampling from a multivariate Normal N(mu, Sigma).

+
+
Parameters:
+
    +
  • mean (Tensor) – The mean vector.

  • +
  • cov (Tensor) – The covariance matrix.

  • +
  • seed (int | None) – The seed with which to seed the random number generator of the +underlying SobolEngine.

  • +
  • inv_transform (bool) – If True, use inverse transform instead of Box-Muller.

  • +
+
+
+
+
+draw(n=1, out=None)[source]
+

Draw n qMC samples from the multivariate Normal.

+
+
Parameters:
+
    +
  • n (int) – The number of samples to draw. As a best practice, use powers of 2.

  • +
  • out (Tensor | None) – An option output tensor. If provided, draws are put into this +tensor, and the function returns None.

  • +
+
+
Returns:
+

A n x d tensor of samples if out=None and None otherwise.

+
+
Return type:
+

Tensor | None

+
+
+
+
+
+
+

Stochastic Samplers

+

Samplers to enable use cases that are not base sample driven, such as +stochastic optimization of acquisition functions.

+
+
+class botorch.sampling.stochastic_samplers.ForkedRNGSampler(sample_shape, seed=None)[source]
+

Bases: MCSampler

+

A sampler using torch.fork_rng to enable replicable sampling +from a posterior that does not support base samples.

+

NOTE: This approach is not a one-to-one replacement for base sample +driven sampling. The main missing piece in this approach is that its +outputs are not replicable across the batch dimensions. As a result, +when an acquisition function is batch evaluated with repeated candidates, +each candidate will produce a different acquisition value, which is not +compatible with Sample Average Approximation.

+

Abstract base class for samplers.

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate. The full shape +of the samples is given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – An optional seed to use for sampling.

  • +
+
+
+
+
+forward(posterior)[source]
+

Draws MC samples from the posterior in a fork_rng context.

+
+
Parameters:
+

posterior (Posterior) – The posterior to sample from.

+
+
Returns:
+

The samples drawn from the posterior.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.stochastic_samplers.StochasticSampler(sample_shape, seed=None)[source]
+

Bases: MCSampler

+

A sampler that simply calls posterior.rsample to generate the +samples. This should only be used for stochastic optimization of the +acquisition functions, e.g., via gen_candidates_torch. This should +not be used with optimize_acqf, which uses deterministic optimizers +under the hood.

+

NOTE: This ignores the seed option.

+

Abstract base class for samplers.

+
+
Parameters:
+
    +
  • sample_shape (torch.Size) – The sample_shape of the samples to generate. The full shape +of the samples is given by posterior._extended_shape(sample_shape).

  • +
  • seed (int | None) – An optional seed to use for sampling.

  • +
+
+
+
+
+forward(posterior)[source]
+

Draws MC samples from the posterior.

+
+
Parameters:
+

posterior (Posterior) – The posterior to sample from.

+
+
Returns:
+

The samples drawn from the posterior.

+
+
Return type:
+

Tensor

+
+
+
+
+
+

Pathwise Sampling

+
+
+
+

Feature Maps

+
+
+class botorch.sampling.pathwise.features.maps.FeatureMap(*args, **kwargs)[source]
+

Bases: TransformedModuleMixin, Module

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+num_outputs: int
+
+
+
+batch_shape: Size
+
+
+
+input_transform: TInputTransform | None
+
+
+
+output_transform: TOutputTransform | None
+
+
+
+
+class botorch.sampling.pathwise.features.maps.KernelEvaluationMap(kernel, points, input_transform=None, output_transform=None)[source]
+

Bases: FeatureMap

+

A feature map defined by centering a kernel at a set of points.

+

Initializes a KernelEvaluationMap instance:

+
feature_map(x) = output_transform(kernel(input_transform(x), points)).
+
+
+
+
Parameters:
+
    +
  • kernel (Kernel) – The kernel \(k\) used to define the feature map.

  • +
  • points (Tensor) – A tensor passed as the kernel’s second argument.

  • +
  • input_transform (TInputTransform | None) – An optional input transform for the module.

  • +
  • output_transform (TOutputTransform | None) – An optional output transform for the module.

  • +
+
+
+
+
+forward(x)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor | LinearOperator

+
+
+
+
+
+property num_outputs: int
+
+
+
+property batch_shape: Size
+
+
+
+
+class botorch.sampling.pathwise.features.maps.KernelFeatureMap(kernel, weight, bias=None, input_transform=None, output_transform=None)[source]
+

Bases: FeatureMap

+

Representation of a kernel \(k: \mathcal{X}^2 \to \mathbb{R}\) as an +n-dimensional feature map \(\phi: \mathcal{X} \to \mathbb{R}^n\) satisfying: +\(k(x, x') ≈ \phi(x)^\top \phi(x')\).

+

Initializes a KernelFeatureMap instance:

+
feature_map(x) = output_transform(input_transform(x)^{T} weight + bias).
+
+
+
+
Parameters:
+
    +
  • kernel (Kernel) – The kernel \(k\) used to define the feature map.

  • +
  • weight (Tensor) – A tensor of weights used to linearly combine the module’s inputs.

  • +
  • bias (Tensor | None) – A tensor of biases to be added to the linearly combined inputs.

  • +
  • input_transform (TInputTransform | None) – An optional input transform for the module.

  • +
  • output_transform (TOutputTransform | None) – An optional output transform for the module.

  • +
+
+
+
+
+forward(x)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+property num_outputs: int
+
+
+
+property batch_shape: Size
+
+
+
+
+

Feature Map Generators

+
+
+[rahimi2007random] +

A. Rahimi and B. Recht. Random features for large-scale kernel machines. +Advances in Neural Information Processing Systems 20 (2007).

+
+
+[sutherland2015error] +

D. J. Sutherland and J. Schneider. On the error of random Fourier features. +arXiv preprint arXiv:1506.02785 (2015).

+
+
+
+
+botorch.sampling.pathwise.features.generators.gen_kernel_features(kernel, num_inputs, num_outputs, **kwargs)[source]
+

Generates a feature map \(\phi: \mathcal{X} \to \mathbb{R}^{n}\) such that +\(k(x, x') ≈ \phi(x)^{T} \phi(x')\). For stationary kernels \(k\), defaults +to the method of random Fourier features. For more details, see [rahimi2007random] +and [sutherland2015error].

+
+
Parameters:
+
    +
  • kernel (Kernel) – The kernel \(k\) to be represented via a finite-dim basis.

  • +
  • num_inputs (int) – The number of input features.

  • +
  • num_outputs (int) – The number of kernel features.

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

KernelFeatureMap

+
+
+
+
+
+

Sample Paths

+
+
+class botorch.sampling.pathwise.paths.SamplePath(*args, **kwargs)[source]
+

Bases: ABC, TransformedModuleMixin, Module

+

Abstract base class for Botorch sample paths.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+
+class botorch.sampling.pathwise.paths.PathDict(paths=None, join=None, input_transform=None, output_transform=None)[source]
+

Bases: SamplePath

+

A dictionary of SamplePaths.

+

Initializes a PathDict instance.

+
+
Parameters:
+
    +
  • paths (Mapping[str, SamplePath] | None) – An optional mapping of strings to sample paths.

  • +
  • join (Callable[[list[Tensor]], Tensor] | None) – An optional callable used to combine each path’s outputs.

  • +
  • input_transform (InputTransform | Callable[[Tensor], Tensor] | None) – An optional input transform for the module.

  • +
  • output_transform (OutcomeTransform | Callable[[Tensor], Tensor] | None) – An optional output transform for the module.

  • +
+
+
+
+
+forward(x, **kwargs)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+
    +
  • x (Tensor)

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

Tensor | dict[str, Tensor]

+
+
+
+
+
+items()[source]
+
+
Return type:
+

Iterable[tuple[str, SamplePath]]

+
+
+
+
+
+keys()[source]
+
+
Return type:
+

Iterable[str]

+
+
+
+
+
+values()[source]
+
+
Return type:
+

Iterable[SamplePath]

+
+
+
+
+
+
+class botorch.sampling.pathwise.paths.PathList(paths=None, join=None, input_transform=None, output_transform=None)[source]
+

Bases: SamplePath

+

A list of SamplePaths.

+

Initializes a PathList instance.

+
+
Parameters:
+
    +
  • paths (Iterable[SamplePath] | None) – An optional iterable of sample paths.

  • +
  • join (Callable[[list[Tensor]], Tensor] | None) – An optional callable used to combine each path’s outputs.

  • +
  • input_transform (InputTransform | Callable[[Tensor], Tensor] | None) – An optional input transform for the module.

  • +
  • output_transform (OutcomeTransform | Callable[[Tensor], Tensor] | None) – An optional output transform for the module.

  • +
+
+
+
+
+forward(x, **kwargs)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+
    +
  • x (Tensor)

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

Tensor | list[Tensor]

+
+
+
+
+
+
+class botorch.sampling.pathwise.paths.GeneralizedLinearPath(feature_map, weight, bias_module=None, input_transform=None, output_transform=None)[source]
+

Bases: SamplePath

+

A sample path in the form of a generalized linear model.

+

Initializes a GeneralizedLinearPath instance.

+
path(x) = output_transform(bias_module(z) + feature_map(z)^T weight),
+where z = input_transform(x).
+
+
+
+
Parameters:
+
    +
  • feature_map (FeatureMap) – A map used to featurize the module’s inputs.

  • +
  • weight (Parameter | Tensor) – A tensor of weights used to combine input features.

  • +
  • bias_module (Module | None) – An optional module used to define additive offsets.

  • +
  • input_transform (InputTransform | Callable[[Tensor], Tensor] | None) – An optional input transform for the module.

  • +
  • output_transform (OutcomeTransform | Callable[[Tensor], Tensor] | None) – An optional output transform for the module.

  • +
+
+
+
+
+forward(x, **kwargs)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Pathwise Prior Samplers

+
+
+botorch.sampling.pathwise.prior_samplers.draw_kernel_feature_paths(model, sample_shape, **kwargs)[source]
+

Draws functions from a Bayesian-linear-model-based approximation to a GP prior.

+

When evaluted, sample paths produced by this method return Tensors with dimensions +sample_dims x batch_dims x [joint_dim], where joint_dim denotes the penultimate +dimension of the input tensor. For multioutput models, outputs are returned as the +final batch dimension.

+
+
Parameters:
+
    +
  • model (GP) – The prior over functions.

  • +
  • sample_shape (Size) – The shape of the sample paths to be drawn.

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

GeneralizedLinearPath

+
+
+
+
+
+

Pathwise Posterior Samplers

+
+
+[wilson2020sampling] +(1,2) +

J. Wilson, V. Borovitskiy, A. Terenin, P. Mostowsky, and M. Deisenroth. Efficiently +sampling functions from Gaussian process posteriors. International Conference on +Machine Learning (2020).

+
+
+[wilson2021pathwise] +(1,2) +

J. Wilson, V. Borovitskiy, A. Terenin, P. Mostowsky, and M. Deisenroth. Pathwise +Conditioning of Gaussian Processes. Journal of Machine Learning Research (2021).

+
+
+
+
+class botorch.sampling.pathwise.posterior_samplers.MatheronPath(prior_paths, update_paths, input_transform=None, output_transform=None)[source]
+

Bases: PathDict

+

Represents function draws from a GP posterior via Matheron’s rule:

+
          "Prior path"
+               v
+(f | y)(·) = f(·) + Cov(f(·), y) Cov(y, y)^{-1} (y - f(X) - ε),
+                    \_______________________________________/
+                                        v
+                                  "Update path"
+
+
+

where = denotes equality in distribution, \(f \sim GP(0, k)\), +\(y \sim N(f(X), \Sigma)\), and \(\epsilon \sim N(0, \Sigma)\). +For more information, see [wilson2020sampling] and [wilson2021pathwise].

+

Initializes a MatheronPath instance.

+
+
Parameters:
+
    +
  • prior_paths (SamplePath) – Sample paths used to represent the prior.

  • +
  • update_paths (SamplePath) – Sample paths used to represent the data.

  • +
  • input_transform (InputTransform | Callable[[Tensor], Tensor] | None) – An optional input transform for the module.

  • +
  • output_transform (OutcomeTransform | Callable[[Tensor], Tensor] | None) – An optional output transform for the module.

  • +
+
+
+
+
+
+botorch.sampling.pathwise.posterior_samplers.get_matheron_path_model(model, sample_shape=None)[source]
+

Generates a deterministic model using a single Matheron path drawn +from the model’s posterior.

+

The deterministic model evalutes the output of draw_matheron_paths, +and reshapes it to mimic the output behavior of the model’s posterior.

+
+
Parameters:
+
    +
  • model (GP) – The model whose posterior is to be sampled.

  • +
  • sample_shape (Size | None) – The shape of the sample paths to be drawn, if an ensemble +of sample paths is desired. If this is specified, the resulting +deterministic model will behave as if the sample_shape is prepended +to the batch_shape of the model. The inputs used to evaluate the model +must be adjusted to match.

  • +
+
+
Returns:
+

A deterministic model that evaluates the Matheron path.

+
+
Return type:
+

GenericDeterministicModel

+
+
+
+
+
+botorch.sampling.pathwise.posterior_samplers.draw_matheron_paths(model, sample_shape, prior_sampler=<function draw_kernel_feature_paths>, update_strategy=<function gaussian_update>)[source]
+

Generates function draws from (an approximate) Gaussian process posterior.

+

When evaluted, sample paths produced by this method return Tensors with dimensions +sample_dims x batch_dims x [joint_dim], where joint_dim denotes the penultimate +dimension of the input tensor. For multioutput models, outputs are returned as the +final batch dimension.

+
+
Parameters:
+
    +
  • model (GP) – Gaussian process whose posterior is to be sampled.

  • +
  • sample_shape (Size) – Sizes of sample dimensions.

  • +
  • prior_sample – A callable that takes a model and a sample shape and returns +a set of sample paths representing the prior.

  • +
  • update_strategy (Callable[[GP, Tensor], SamplePath]) – A callable that takes a model and a tensor of prior process +values and returns a set of sample paths representing the data.

  • +
  • prior_sampler (Callable[[GP, Size], SamplePath])

  • +
+
+
Return type:
+

MatheronPath

+
+
+
+
+
+

Pathwise Update Strategies

+
+
+botorch.sampling.pathwise.update_strategies.gaussian_update(model, sample_values, likelihood=<class 'botorch.utils.types.DEFAULT'>, **kwargs)[source]
+

Computes a Gaussian pathwise update in exact arithmetic:

+
(f | y)(·) = f(·) + Cov(f(·), y) Cov(y, y)^{-1} (y - f(X) - ε),
+                    \_______________________________________/
+                                        V
+                            "Gaussian pathwise update"
+
+
+

where = denotes equality in distribution, \(f \sim GP(0, k)\), +\(y \sim N(f(X), \Sigma)\), and \(\epsilon \sim N(0, \Sigma)\). +For more information, see [wilson2020sampling] and [wilson2021pathwise].

+
+
Parameters:
+
    +
  • model (GP) – A Gaussian process prior together with a likelihood.

  • +
  • sample_values (Tensor) – Assumed values for \(f(X)\).

  • +
  • likelihood (Likelihood | None) – An optional likelihood used to help define the desired +update. Defaults to model.likelihood if it exists else None.

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

GeneralizedLinearPath

+
+
+
+
+
+

Utilities

+
+
+class botorch.sampling.pathwise.utils.TransformedModuleMixin[source]
+

Bases: object

+

Mixin that wraps a module’s __call__ method with optional transforms.

+
+
+input_transform: InputTransform | Callable[[Tensor], Tensor] | None
+
+
+
+output_transform: OutcomeTransform | Callable[[Tensor], Tensor] | None
+
+
+
+
+class botorch.sampling.pathwise.utils.TensorTransform(*args, **kwargs)[source]
+

Bases: ABC, Module

+

Abstract base class for transforms that map tensor to tensor.

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+abstract forward(values, **kwargs)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+
    +
  • values (Tensor)

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.pathwise.utils.ChainedTransform(*transforms)[source]
+

Bases: TensorTransform

+

A composition of TensorTransforms.

+

Initializes a ChainedTransform instance.

+
+
Parameters:
+

transforms (TensorTransform) – A set of transforms to be applied from right to left.

+
+
+
+
+forward(values)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

values (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.pathwise.utils.SineCosineTransform(scale=None)[source]
+

Bases: TensorTransform

+

A transform that returns concatenated sine and cosine features.

+

Initializes a SineCosineTransform instance.

+
+
Parameters:
+

scale (Tensor | None) – An optional tensor used to rescale the module’s outputs.

+
+
+
+
+forward(values)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

values (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.pathwise.utils.InverseLengthscaleTransform(kernel)[source]
+

Bases: TensorTransform

+

A transform that divides its inputs by a kernels lengthscales.

+

Initializes an InverseLengthscaleTransform instance.

+
+
Parameters:
+

kernel (Kernel) – The kernel whose lengthscales are to be used.

+
+
+
+
+forward(values)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

values (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.pathwise.utils.OutputscaleTransform(kernel)[source]
+

Bases: TensorTransform

+

A transform that multiplies its inputs by the square root of a +kernel’s outputscale.

+

Initializes an OutputscaleTransform instance.

+
+
Parameters:
+

kernel (ScaleKernel) – A ScaleKernel whose outputscale is to be used.

+
+
+
+
+forward(values)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

values (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.pathwise.utils.FeatureSelector(indices, dim=-1)[source]
+

Bases: TensorTransform

+

A transform that returns a subset of its input’s features. +along a given tensor dimension.

+

Initializes a FeatureSelector instance.

+
+
Parameters:
+
    +
  • indices (Iterable[int]) – A LongTensor of feature indices.

  • +
  • dim (int | LongTensor) – The dimensional along which to index features.

  • +
+
+
+
+
+forward(values)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

values (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.sampling.pathwise.utils.OutcomeUntransformer(transform, num_outputs)[source]
+

Bases: TensorTransform

+

Module acting as a bridge for OutcomeTransform.untransform.

+

Initializes an OutcomeUntransformer instance.

+
+
Parameters:
+
    +
  • transform (OutcomeTransform) – The wrapped OutcomeTransform instance.

  • +
  • num_outputs (int | LongTensor) – The number of outcome features that the +OutcomeTransform transforms.

  • +
+
+
+
+
+forward(values)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
Parameters:
+

values (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+botorch.sampling.pathwise.utils.get_input_transform(model)[source]
+

Returns a model’s input_transform or None.

+
+
Parameters:
+

model (GPyTorchModel)

+
+
Return type:
+

InputTransform | None

+
+
+
+
+
+botorch.sampling.pathwise.utils.get_output_transform(model)[source]
+

Returns a wrapped version of a model’s outcome_transform or None.

+
+
Parameters:
+

model (GPyTorchModel)

+
+
Return type:
+

OutcomeUntransformer | None

+
+
+
+
+
+botorch.sampling.pathwise.utils.get_train_inputs(model: Model, transformed: bool = False) tuple[Tensor, ...][source]
+
+botorch.sampling.pathwise.utils.get_train_inputs(model: ModelList, transformed: bool = False) list[...]
+
+
+
+botorch.sampling.pathwise.utils.get_train_targets(model: Model, transformed: bool = False) Tensor[source]
+
+botorch.sampling.pathwise.utils.get_train_targets(model: ModelList, transformed: bool = False) list[...]
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/search.html b/website-old/pages/api/search.html new file mode 100644 index 0000000000..ea300909d2 --- /dev/null +++ b/website-old/pages/api/search.html @@ -0,0 +1,71 @@ + + + + + + + + + + + +
+
+
+
+

Search

+ +

+ Searching for multiple words only shows matches that contain + all words. +

+
+ + + +
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/settings.html b/website-old/pages/api/settings.html new file mode 100644 index 0000000000..d38a68cc82 --- /dev/null +++ b/website-old/pages/api/settings.html @@ -0,0 +1,109 @@ + + + + + + + +
+
+
+
+
+

botorch.settings

+

BoTorch settings.

+
+
+class botorch.settings.propagate_grads(state=True)[source]
+

Bases: _Flag

+

Flag for propagating gradients to model training inputs / training data.

+

When set to True, gradients will be propagated to the training inputs. +This is useful in particular for propating gradients through fantasy models.

+
+
Parameters:
+

state (bool)

+
+
+
+
+
+class botorch.settings.validate_input_scaling(state=True)[source]
+

Bases: _Flag

+

Flag for validating input normalization/standardization.

+

When set to True, standard botorch models will validate (up to reasonable +tolerance) that +(i) none of the inputs contain NaN values +(ii) the training data (train_X) is normalized to the unit cube +(iii) the training targets (train_Y) are standardized (zero mean, unit var) +No checks (other than the NaN check) are performed for observed variances +(train_Y_var) at this point.

+
+
Parameters:
+

state (bool)

+
+
+
+
+
+class botorch.settings.log_level(level=50)[source]
+

Bases: object

+

Flag for printing verbose logging statements.

+

Applies the given level to logging.getLogger(‘botorch’) calls. For +instance, when set to logging.INFO, all logger calls of level INFO or +above will be printed to STDERR

+
+
Parameters:
+

level (int) – The log level. Defaults to LOG_LEVEL_DEFAULT.

+
+
+
+
+level: int = 50
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/test_functions.html b/website-old/pages/api/test_functions.html new file mode 100644 index 0000000000..db72381890 --- /dev/null +++ b/website-old/pages/api/test_functions.html @@ -0,0 +1,3841 @@ + + + + + + + +
+
+
+
+
+

botorch.test_functions

+
+

Abstract Test Function API

+

Base class for test functions for optimization benchmarks.

+
+
+class botorch.test_functions.base.BaseTestProblem(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: Module, ABC

+

Base class for test functions.

+

Base constructor for test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int
+
+
+
+forward(X, noise=True)[source]
+

Evaluate the function on a set of points.

+
+
Parameters:
+
    +
  • X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to evaluate +the function.

  • +
  • noise (bool) – If True, add observation noise as specified by noise_std.

  • +
+
+
Returns:
+

A batch_shape-dim tensor ouf function evaluations.

+
+
Return type:
+

Tensor

+
+
+
+
+
+abstract evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.base.ConstrainedBaseTestProblem(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: BaseTestProblem, ABC

+

Base class for test functions with constraints.

+

In addition to one or more objectives, a problem may have a number of outcome +constraints of the form c_i(x) >= 0 for i=1, …, n_c.

+

This base class provides common functionality for such problems.

+

Base constructor for test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_constraints: int
+
+
+
+constraint_noise_std: None | float | list[float] = None
+
+
+
+evaluate_slack(X, noise=True)[source]
+

Evaluate the constraint slack on a set of points.

+

Constraints i is assumed to be feasible at x if the associated slack +c_i(x) is positive. Zero slack means that the constraint is active. Negative +slack means that the constraint is violated.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

  • +
  • noise (bool) – If True, add observation noise to the slack as specified by +noise_std.

  • +
+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+is_feasible(X, noise=True)[source]
+

Evaluate whether the constraints are feasible on a set of points.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraints.

  • +
  • noise (bool) – If True, add observation noise as specified by noise_std.

  • +
+
+
Returns:
+

+
A batch_shape-dim boolean tensor that is True iff all constraint

slacks (potentially including observation noise) are positive.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+abstract evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.base.MultiObjectiveTestProblem(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: BaseTestProblem, ABC

+

Base class for multi-objective test functions.

+

TODO: add a pareto distance function that returns the distance +between a provided point and the closest point on the true pareto front.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+num_objectives: int
+
+
+
+property max_hv: float
+
+
+
+gen_pareto_front(n)[source]
+

Generate n pareto optimal points.

+
+
Parameters:
+

n (int)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Synthetic Test Functions

+

Synthetic functions for optimization benchmarks.

+

Most test functions (if not indicated otherwise) are taken from +[Bingham2013virtual].

+

References:

+
+
+[Bingham2013virtual] +

D. Bingham, S. Surjanovic. Virtual Library of Simulation Experiments. +https://www.sfu.ca/~ssurjano/optimization.html

+
+
+[CoelloCoello2002constraint] +(1,2) +

C. A. Coello Coello and E. Mezura Montes. Constraint-handling in genetic +algorithms through the use of dominance-based tournament selection. +Advanced Engineering Informatics, 16(3):193–203, 2002.

+
+
+[Hedar2006derivfree] +

A.-R. Hedar and M. Fukushima. Derivative-free filter simulated annealing +method for constrained continuous global optimization. Journal of Global +Optimization, 35(4):521–549, 2006.

+
+
+[Lemonge2010constrained] +

A. C. C. Lemonge, H. J. C. Barbosa, C. C. H. Borges, and F. B. dos Santos +Silva. Constrained optimization problems in mechanical engineering design +using a real-coded steady-state genetic algorithm. Mecánica Computacional, +XXIX:9287–9303, 2010.

+
+
+[Letham2019] +

B. Letham, B. Karrer, G. Ottoni, and E. Bakshy. Constrained Bayesian +Optimization with Noisy Experiments. Bayesian Analysis, Bayesian Anal. +14(2), 495-519, 2019.

+
+
+[Gramacy2016] +

R. Gramacy, G. Gray, S. Le Digabel, H. Lee, P. Ranjan, G. Wells & S. Wild. +Modeling an Augmented Lagrangian for Blackbox Constrained Optimization, +Technometrics, 2016.

+
+
+
+
+class botorch.test_functions.synthetic.SyntheticTestFunction(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: BaseTestProblem, ABC

+

Base class for synthetic test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_objectives: int = 1
+
+
+
+property optimal_value: float
+

The global minimum (maximum if negate=True) of the function.

+
+
+
+
+class botorch.test_functions.synthetic.Ackley(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Ackley test function.

+

d-dimensional function (usually evaluated on [-32.768, 32.768]^d):

+
+
+
f(x) = -A exp(-B sqrt(1/d sum_{i=1}^d x_i^2)) -

exp(1/d sum_{i=1}^d cos(c x_i)) + A + exp(1)

+
+
+
+

f has one minimizer for its global minimum at z_1 = (0, 0, …, 0) with +f(z_1) = 0.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Beale(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Branin(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Branin test function.

+

Two-dimensional function (usually evaluated on [-5, 10] x [0, 15]):

+
+

B(x) = (x_2 - b x_1^2 + c x_1 - r)^2 + 10 (1-t) cos(x_1) + 10

+
+

Here b, c, r and t are constants where b = 5.1 / (4 * math.pi ** 2) +c = 5 / math.pi, r = 6, t = 1 / (8 * math.pi) +B has 3 minimizers for its global minimum at z_1 = (-pi, 12.275), +z_2 = (pi, 2.275), z_3 = (9.42478, 2.475) with B(z_i) = 0.397887.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Bukin(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Cosine8(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Cosine Mixture test function.

+

8-dimensional function (usually evaluated on [-1, 1]^8):

+
+

f(x) = 0.1 sum_{i=1}^8 cos(5 pi x_i) - sum_{i=1}^8 x_i^2

+
+

f has one maximizer for its global maximum at z_1 = (0, 0, …, 0) with +f(z_1) = 0.8

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 8
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.DropWave(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.DixonPrice(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
  • bounds (list[tuple[float, float]] | None)

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.EggHolder(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Eggholder test function.

+

Two-dimensional function (usually evaluated on [-512, 512]^2):

+
+

E(x) = (x_2 + 47) sin(R1(x)) - x_1 * sin(R2(x))

+
+

where R1(x) = sqrt(|x_2 + x_1 / 2 + 47|), R2(x) = sqrt|x_1 - (x_2 + 47)|).

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Griewank(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Griewank synthetic test function.

+

The Griewank function is defined for any d, is typically evaluated on +[-600, 600]^d, and given by:

+
+

G(x) = sum_{i=1}^d x_i**2 / 4000 - prod_{i=1}^d cos(x_i / sqrt(i)) + 1

+
+

G has many widespread local minima, which are regularly distributed. +The global minimum is at z = (0, …, 0) with G(z) = 0.

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Hartmann(dim=6, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Hartmann synthetic test function.

+

Most commonly used is the six-dimensional version (typically evaluated on +[0, 1]^6):

+
+

H(x) = - sum_{i=1}^4 ALPHA_i exp( - sum_{j=1}^6 A_ij (x_j - P_ij)**2 )

+
+

H has a 6 local minima and a global minimum at

+
+

z = (0.20169, 0.150011, 0.476874, 0.275332, 0.311652, 0.6573)

+
+

with H(z) = -3.32237.

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+property optimizers: Tensor
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.HolderTable(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Holder Table synthetic test function.

+

Two-dimensional function (typically evaluated on [0, 10] x [0, 10]):

+
+

H(x) = - | sin(x_1) * cos(x_2) * exp(| 1 - ||x|| / pi | ) |

+
+

H has 4 global minima with H(z_i) = -19.2085 at

+
+

z_1 = ( 8.05502, 9.66459) +z_2 = (-8.05502, -9.66459) +z_3 = (-8.05502, 9.66459) +z_4 = ( 8.05502, -9.66459)

+
+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Levy(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Levy synthetic test function.

+

d-dimensional function (usually evaluated on [-10, 10]^d):

+
+
+
f(x) = sin^2(pi w_1) +

sum_{i=1}^{d-1} (w_i-1)^2 (1 + 10 sin^2(pi w_i + 1)) + +(w_d - 1)^2 (1 + sin^2(2 pi w_d))

+
+
+
+

where w_i = 1 + (x_i - 1) / 4 for all i.

+

f has one minimizer for its global minimum at z_1 = (1, 1, …, 1) with +f(z_1) = 0.

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Michalewicz(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Michalewicz synthetic test function.

+

d-dim function (usually evaluated on hypercube [0, pi]^d):

+
+

M(x) = sum_{i=1}^d sin(x_i) (sin(i x_i^2 / pi)^20)

+
+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+property optimizers: Tensor
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Powell(dim=4, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Powell synthetic test function.

+

d-dim function (usually evaluated on the hypercube [-4, 5]^d):

+
+

P(x) = sum_{i=1}^d/4 ( +(x_{4i-3} + 10 x_{4i-2})**2 ++ 5 (x_{4i-1} - x_{4i})**2 ++ (x_{4i-2} - 2 x_{4i-1})**4 ++ 10 (x_{4i-3} - x_{4i})**4 +)

+
+

P has a global minimizer at z = (0, …, 0) with P(z) = 0.

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Rastrigin(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Rosenbrock(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Rosenbrock synthetic test function.

+

d-dimensional function (usually evaluated on [-5, 10]^d):

+
+

f(x) = sum_{i=1}^{d-1} (100 (x_{i+1} - x_i^2)^2 + (x_i - 1)^2)

+
+

f has one minimizer for its global minimum at z_1 = (1, 1, …, 1) with +f(z_i) = 0.0.

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.Shekel(m=10, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Shekel synthtetic test function.

+

4-dimensional function (usually evaluated on [0, 10]^4):

+
+

f(x) = -sum_{i=1}^10 (sum_{j=1}^4 (x_j - A_{ji})^2 + C_i)^{-1}

+
+

f has one minimizer for its global minimum at z_1 = (4, 4, 4, 4) with +f(z_1) = -10.5363.

+
+
Parameters:
+
    +
  • m (int) – Defaults to 10.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 4
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.SixHumpCamel(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.StyblinskiTang(dim=2, noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Styblinski-Tang synthtetic test function.

+

d-dimensional function (usually evaluated on the hypercube [-5, 5]^d):

+
+

H(x) = 0.5 * sum_{i=1}^d (x_i^4 - 16 * x_i^2 + 5 * x_i)

+
+

H has a single global mininimum H(z) = -39.166166 * d at z = [-2.903534]^d

+
+
Parameters:
+
    +
  • dim – The (input) dimension.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.ThreeHumpCamel(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.ConstrainedSyntheticTestFunction(noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: ConstrainedBaseTestProblem, SyntheticTestFunction, ABC

+

Base class for constrained synthetic test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+
+class botorch.test_functions.synthetic.ConstrainedGramacy(noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: ConstrainedSyntheticTestFunction

+

Constrained Gramacy test function.

+

This problem comes from [Gramacy2016]. The problem is defined +over the unit cube and the goal is to minimize x1+x2 subject to +1.5 - x1 - 2 * x2 - 0.5 * sin(2*pi*(x1^2 - 2 * x2)) <= 0 +and x1^2 + x2^2 - 1.5 <= 0.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_objectives: int = 1
+
+
+
+num_constraints: int = 2
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +function.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.ConstrainedHartmann(dim=6, noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: Hartmann, ConstrainedSyntheticTestFunction

+

Constrained Hartmann test function.

+

This is a constrained version of the standard Hartmann test function that +uses ||x||_2 <= 1 as the constraint. This problem comes from [Letham2019].

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (None | float) – Standard deviation of the observation noise.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_constraints: int = 1
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.ConstrainedHartmannSmooth(dim=6, noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: Hartmann, ConstrainedSyntheticTestFunction

+

Smooth constrained Hartmann test function.

+

This is a constrained version of the standard Hartmann test function that +uses ||x||_2^2 <= 1 as the constraint to obtain smoother constraint slack.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (None | float) – Standard deviation of the observation noise.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_constraints: int = 1
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.PressureVessel(noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: ConstrainedSyntheticTestFunction

+

Pressure vessel design problem with constraints.

+

The four-dimensional pressure vessel design problem with four black-box +constraints from [CoelloCoello2002constraint].

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 4
+
+
+
+num_constraints: int = 4
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.WeldedBeamSO(noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: ConstrainedSyntheticTestFunction

+

Welded beam design problem with constraints (single-outcome).

+

The four-dimensional welded beam design proble problem with six +black-box constraints from [CoelloCoello2002constraint].

+

For a (somewhat modified) multi-objective version, see +botorch.test_functions.multi_objective.WeldedBeam.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 4
+
+
+
+num_constraints: int = 6
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.TensionCompressionString(noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: ConstrainedSyntheticTestFunction

+

Tension compression string optimization problem with constraints.

+

The three-dimensional tension compression string optimization problem with +four black-box constraints from [Hedar2006derivfree].

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 3
+
+
+
+num_constraints: int = 4
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.synthetic.SpeedReducer(noise_std=None, constraint_noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: ConstrainedSyntheticTestFunction

+

Speed Reducer design problem with constraints.

+

The seven-dimensional speed reducer design problem with eleven black-box +constraints from [Lemonge2010constrained].

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the constraint noise. +If a list is provided, specifies separate noise standard +deviations for each constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 7
+
+
+
+num_constraints: int = 11
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Fidelity Synthetic Test Functions

+

Synthetic functions for multi-fidelity optimization benchmarks.

+
+
+class botorch.test_functions.multi_fidelity.AugmentedBranin(noise_std=None, negate=False, bounds=None, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Augmented Branin test function for multi-fidelity optimization.

+

3-dimensional function with domain [-5, 10] x [0, 15] * [0,1], where +the last dimension of is the fidelity parameter:

+
+
+
B(x) = (x_2 - (b - 0.1 * (1 - x_3))x_1^2 + c x_1 - r)^2 +

10 (1-t) cos(x_1) + 10

+
+
+
+

Here b, c, r and t are constants where b = 5.1 / (4 * math.pi ** 2) +c = 5 / math.pi, r = 6, t = 1 / (8 * math.pi). +B has infinitely many minimizers with x_1 = -pi, pi, 3pi +and B_min = 0.397887

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective in a multiobjective problem.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • bounds (list[tuple[float, float]] | None) – Custom bounds for the function specified as (lower, upper) pairs.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 3
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_fidelity.AugmentedHartmann(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Augmented Hartmann synthetic test function.

+

7-dimensional function (typically evaluated on [0, 1]^7), where the last +dimension is the fidelity parameter.

+
+
+
H(x) = -(ALPHA_1 - 0.1 * (1-x_7)) * exp(- sum_{j=1}^6 A_1j (x_j - P_1j) ** 2) -

sum_{i=2}^4 ALPHA_i exp( - sum_{j=1}^6 A_ij (x_j - P_ij) ** 2)

+
+
+
+

H has a unique global minimizer +x = [0.20169, 0.150011, 0.476874, 0.275332, 0.311652, 0.6573, 1.0]

+

with H_min = -3.32237

+
+
Parameters:
+
    +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 7
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_fidelity.AugmentedRosenbrock(dim=3, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Augmented Rosenbrock synthetic test function for multi-fidelity optimization.

+

d-dimensional function (usually evaluated on [-5, 10]^(d-2) * [0, 1]^2), +where the last two dimensions are the fidelity parameters:

+
+
+
f(x) = sum_{i=1}^{d-1} (100 (x_{i+1} - x_i^2 + 0.1 * (1-x_{d-1}))^2 +

(x_i - 1 + 0.1 * (1 - x_d)^2)^2)

+
+
+
+

f has one minimizer for its global minimum at z_1 = (1, 1, …, 1) with +f(z_i) = 0.0.

+
+
Parameters:
+
    +
  • dim – The (input) dimension. Must be at least 3.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Objective Synthetic Test Functions

+

Multi-objective optimization benchmark problems.

+

References

+
+
+[Daulton2022] +(1,2) +

S. Daulton, S. Cakmak, M. Balandat, M. A. Osborne, E. Zhou, and E. Bakshy. +Robust Multi-Objective Bayesian Optimization Under Input Noise. +Proceedings of the 39th International Conference on Machine Learning, 2022.

+
+
+[Deb2005dtlz] +

K. Deb, L. Thiele, M. Laumanns, E. Zitzler, A. Abraham, L. Jain, and +R. Goldberg. Scalable test problems for evolutionary multi-objective +optimization. Evolutionary Multiobjective Optimization, Springer-Verlag, +pp. 105-145, 2005.

+
+
+[Deb2005robust] +(1,2) +

K. Deb and H. Gupta. Searching for Robust Pareto-Optimal Solutions in +Multi-objective Optimization. Evolutionary Multi-Criterion Optimization, +Springer-Berlin, pp. 150-164, 2005.

+
+
+[Frohlich2020] +

L. Frohlich, E. Klenske, J. Vinogradska, C. Daniel, and M. Zeilinger. +Noisy-Input Entropy Search for Efficient Robust Bayesian Optimization. +Proceedings of the Twenty Third International Conference on Artificial +Intelligence and Statistics, PMLR 108:2262-2272, 2020.

+
+
+[GarridoMerchan2020] +(1,2,3) +

E. C. Garrido-Merch ́an and D. Hern ́andez-Lobato. Parallel Predictive Entropy +Search for Multi-objective Bayesian Optimization with Constraints. +arXiv e-prints, arXiv:2004.00601, Apr. 2020.

+
+
+[Gelbart2014] +

Michael A. Gelbart, Jasper Snoek, and Ryan P. Adams. 2014. Bayesian +optimization with unknown constraints. In Proceedings of the Thirtieth +Conference on Uncertainty in Artificial Intelligence (UAI’14). +AUAI Press, Arlington, Virginia, USA, 250–259.

+
+
+[Liang2021] +

Q. Liang and L. Lai, Scalable Bayesian Optimization Accelerates Process +Optimization of Penicillin Production. NeurIPS 2021 AI for Science Workshop, 2021.

+
+
+[Ma2019] +

Z. Ma and Y. Wang. Evolutionary Constrained Multiobjective Optimization: +Test Suite Construction and Performance Comparisons. IEEE Transactions +on Evolutionary Computation, 23(6):972–986, December 2019.

+
+
+[Oszycka1995] +

A. Osyczka and S. Kundu. A new method to solve generalized +multicriteria optimization problems using the simple genetic algorithm. +In Structural Optimization 10. 94–99, 1995.

+
+
+[Tanabe2020] +(1,2,3,4,5,6,7) +

Ryoji Tanabe and Hisao Ishibuchi. An easy-to-use real-world multi-objective +optimization problem suite, Applied Soft Computing,Volume 89, 2020.

+
+
+[Yang2019a] +(1,2,3) +

K. Yang, M. Emmerich, A. Deutz, and T. Bäck. Multi-Objective Bayesian +Global Optimization using expected hypervolume improvement gradient. +Swarm and evolutionary computation 44, pp. 945–956, 2019.

+
+
+[Zitzler2000] +

E. Zitzler, K. Deb, and L. Thiele. Comparison of multiobjective +evolutionary algorithms: Empirical results. Evolutionary Computation, vol. +8, no. 2,pp. 173–195, 2000.

+
+
+
+
+class botorch.test_functions.multi_objective.BraninCurrin(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

Two objective problem composed of the Branin and Currin functions.

+

Branin (rescaled):

+
+

f(x) = ( +15*x_1 - 5.1 * (15 * x_0 - 5) ** 2 / (4 * pi ** 2) + 5 * (15 * x_0 - 5) +/ pi - 5 +) ** 2 + (10 - 10 / (8 * pi)) * cos(15 * x_0 - 5))

+
+

Currin:

+
+

f(x) = (1 - exp(-1 / (2 * x_1))) * ( +2300 * x_0 ** 3 + 1900 * x_0 ** 2 + 2092 * x_0 + 60 +) / 100 * x_0 ** 3 + 500 * x_0 ** 2 + 4 * x_0 + 20

+
+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+num_objectives: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DH(dim, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ABC

+

Base class for DH problems for robust multi-objective optimization.

+

In their paper, [Deb2005robust] consider these problems under a mean-robustness +setting, and use uniformly distributed input perturbations from the box with +edge lengths delta_0 = delta, delta_i = 2 * delta, i > 0, with delta ranging +up to 0.01 for DH1 and DH2, and delta = 0.03 for DH3 and DH4.

+

These are d-dimensional problems with two objectives:

+
+

f_0(x) = x_0 +f_1(x) = h(x) + g(x) * S(x) for DH1 and DH2 +f_1(x) = h(x) * (g(x) + S(x)) for DH3 and DH4

+
+

The goal is to minimize both objectives. See [Deb2005robust] for more details +on DH. The reference points were set using infer_reference_point.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_objectives: int = 2
+
+
+
+
+class botorch.test_functions.multi_objective.DH1(dim, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DH

+

DH1 test problem.

+

d-dimensional problem evaluated on [0, 1] x [-1, 1]^{d-1}:

+
+

f_0(x) = x_0 +f_1(x) = h(x_0) + g(x) * S(x_0) +h(x_0) = 1 - x_0^2 +g(x) = sum_{i=1}^{d-1} (10 + x_i^2 - 10 * cos(4 * pi * x_i)) +S(x_0) = alpha / (0.2 + x_0) + beta * x_0^2

+
+

where alpha = 1 and beta = 1.

+

The Pareto front corresponds to the equation f_1 = 1 - f_0^2, and it is found at +x_i = 0 for i > 0 and any value of x_0 in (0, 1].

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+alpha = 1.0
+
+
+
+beta = 1.0
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DH2(dim, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DH1

+

DH2 test problem.

+

This is identical to DH1 except for having beta = 10.0.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+beta = 10.0
+
+
+
+
+class botorch.test_functions.multi_objective.DH3(dim, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DH

+

DH3 test problem.

+

d-dimensional problem evaluated on [0, 1]^2 x [-1, 1]^{d-2}:

+
+

f_0(x) = x_0 +f_1(x) = h(x_1) * (g(x) + S(x_0)) +h(x_1) = 2 - 0.8 * exp(-((x_1 - 0.35) / 0.25)^2) - exp(-((x_1 - 0.85) / 0.03)^2) +g(x) = sum_{i=2}^{d-1} (50 * x_i^2) +S(x_0) = 1 - sqrt(x_0)

+
+

The Pareto front is found at x_i = 0 for i > 1. There’s a local and a global +Pareto front, which are found at x_1 = 0.35 and x_1 = 0.85, respectively. +The approximate relationships between the objectives at local and global Pareto +fronts are given by f_1 = 1.2 (1 - sqrt(f_0)) and f_1 = 1 - f_0, respectively. +The specific values on the Pareto fronts can be found by varying x_0.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DH4(dim, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DH3

+

DH4 test problem.

+

This is similar to DH3 except that it is evaluated on +[0, 1] x [-0.15, 1] x [-1, 1]^{d-2} and:

+
+

h(x_0, x_1) = 2 - x_0 - 0.8 * exp(-((x_0 + x_1 - 0.35) / 0.25)^2) +- exp(-((x_0 + x_1 - 0.85) / 0.03)^2)

+
+

The Pareto front is found at x_i = 0 for i > 2, with the local one being +near x_0 + x_1 = 0.35 and the global one near x_0 + x_1 = 0.85.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+
+class botorch.test_functions.multi_objective.DTLZ(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

Base class for DTLZ problems.

+

See [Deb2005dtlz] for more details on DTLZ.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+
+class botorch.test_functions.multi_objective.DTLZ1(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DTLZ

+

DLTZ1 test problem.

+

d-dimensional problem evaluated on [0, 1]^d:

+
+

f_0(x) = 0.5 * x_0 * (1 + g(x)) +f_1(x) = 0.5 * (1 - x_0) * (1 + g(x)) +g(x) = 100 * sum_{i=m}^{d-1} ( +k + (x_i - 0.5)^2 - cos(20 * pi * (x_i - 0.5)) +)

+
+

where k = d - m + 1.

+

The pareto front is given by the line (or hyperplane) sum_i f_i(x) = 0.5. +The goal is to minimize both objectives. The reference point comes from [Yang2019].

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+gen_pareto_front(n)[source]
+

Generate n pareto optimal points.

+

The pareto points randomly sampled from the hyperplane sum_i f(x_i) = 0.5.

+
+
Parameters:
+

n (int)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DTLZ2(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DTLZ

+

DLTZ2 test problem.

+

d-dimensional problem evaluated on [0, 1]^d:

+
+

f_0(x) = (1 + g(x)) * cos(x_0 * pi / 2) +f_1(x) = (1 + g(x)) * sin(x_0 * pi / 2) +g(x) = sum_{i=m}^{d-1} (x_i - 0.5)^2

+
+

The pareto front is given by the unit hypersphere sum{i} f_i^2 = 1. +Note: the pareto front is completely concave. The goal is to minimize +both objectives.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+gen_pareto_front(n)[source]
+

Generate n pareto optimal points.

+

The pareto points are randomly sampled from the hypersphere’s +positive section.

+
+
Parameters:
+

n (int)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DTLZ3(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DTLZ2

+

DTLZ3 test problem.

+

d-dimensional problem evaluated on [0, 1]^d:

+
+

f_0(x) = (1 + g(x)) * cos(x_0 * pi / 2) +f_1(x) = (1 + g(x)) * sin(x_0 * pi / 2) +g(x) = 100 * [k + sum_{i=m}^{n-1} (x_i - 0.5)^2 - cos(20 * pi * (x_i - 0.5))]

+
+

g(x) introduces (3k−1) local Pareto fronts that are parallel to +the one global Pareto-optimal front.

+

The global Pareto-optimal front corresponds to x_i = 0.5 for x_i in X_m.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DTLZ4(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DTLZ2

+

DTLZ4 test problem.

+

This is the same as DTLZ2, but with alpha=100 as the exponent, +resulting in dense solutions near the f_M-f_1 plane.

+

The global Pareto-optimal front corresponds to x_i = 0.5 for x_i in X_m.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+
+class botorch.test_functions.multi_objective.DTLZ5(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DTLZ

+

DTLZ5 test problem.

+

d-dimensional problem evaluated on [0, 1]^d:

+
+

f_0(x) = (1 + g(x)) * cos(theta_0 * pi / 2) +f_1(x) = (1 + g(x)) * sin(theta_0 * pi / 2) +theta_i = pi / (4 * (1 + g(X_m)) * (1 + 2 * g(X_m) * x_i)) for i = 1, … , M-2 +g(x) = sum_{i=m}^{d-1} (x_i - 0.5)^2

+
+

The global Pareto-optimal front corresponds to x_i = 0.5 for x_i in X_m.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DTLZ7(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DTLZ

+

DTLZ7 test problem.

+
+
d-dimensional problem evaluated on [0, 1]^d:

f_0(x) = x_0 +f_1(x) = x_1 +… +f_{M-1}(x) = (1 + g(X_m)) * h(f_0, f_1, …, f_{M-2}, g, x) +h(f_0, f_1, …, f_{M-2}, g, x) = +M - sum_{i=0}^{M-2} f_i(x)/(1+g(x)) * (1 + sin(3 * pi * f_i(x)))

+
+
+

This test problem has 2M-1 disconnected Pareto-optimal regions in the search space.

+

The pareto frontier corresponds to X_m = 0.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.GMM(noise_std=None, negate=False, num_objectives=2, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

A test problem where each objective is a Gaussian mixture model.

+

This implementation is adapted from the single objective version (proposed by +[Frohlich2020]) at +https://github.com/boschresearch/NoisyInputEntropySearch/blob/master/ +core/util/objectives.py.

+

See [Daulton2022] for details on this multi-objective problem.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • num_objectives (int) – The number of objectives.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the GMMs.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.Penicillin(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

A penicillin production simulator from [Liang2021].

+

This implementation is adapted from +https://github.com/HarryQL/TuRBO-Penicillin.

+

The goal is to maximize the penicillin yield while minimizing +time to ferment and the CO2 byproduct.

+

The function is defined for minimization of all objectives.

+

The reference point was set using the infer_reference_point heuristic +on the Pareto frontier over a large discrete set of random designs.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 7
+
+
+
+num_objectives: int = 3
+
+
+
+Y_xs = 0.45
+
+
+
+Y_ps = 0.9
+
+
+
+K_1 = 1e-10
+
+
+
+K_2 = 7.000000000000001e-05
+
+
+
+m_X = 0.014
+
+
+
+alpha_1 = 0.143
+
+
+
+alpha_2 = 4e-07
+
+
+
+alpha_3 = 0.0001
+
+
+
+mu_X = 0.092
+
+
+
+K_X = 0.15
+
+
+
+mu_p = 0.005
+
+
+
+K_p = 0.0002
+
+
+
+K_I = 0.1
+
+
+
+K = 0.04
+
+
+
+k_g = 7000.0
+
+
+
+E_g = 5100.0
+
+
+
+k_d = 1e+33
+
+
+
+E_d = 50000.0
+
+
+
+lambd = 0.00025
+
+
+
+T_v = 273.0
+
+
+
+T_o = 373.0
+
+
+
+R = 1.9872
+
+
+
+V_max = 180.0
+
+
+
+classmethod penicillin_vectorized(X_input)[source]
+

Penicillin simulator, simplified and vectorized.

+

The 7 input parameters are (in order): culture volume, biomass +concentration, temperature, glucose concentration, substrate feed +rate, substrate feed concentration, and H+ concentration.

+
+
Parameters:
+

X_input (Tensor) – A n x 7-dim tensor of inputs.

+
+
Returns:
+

An n x 3-dim tensor of (negative) penicillin yield, CO2 and time.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.ToyRobust(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

A 1D problem where the Pareto frontier is sensitive to input noise.

+

Specifically, the pareto frontier over the nominal objectives is +sensitive to input noise. The first objective is a mixture of a linear +function and a sinusoidal function, and the second objective is a modified +Levy function, where the second parameter is fixed.

+

This function comes from [Daulton2022].

+

The reference point was set using the infer_reference_point +heuristic on the Pareto frontier over a large discrete set of +random designs.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 1
+
+
+
+num_objectives: int = 2
+
+
+
+levy = Levy()
+
+
+
+f_1(X)[source]
+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+f_2(X)[source]
+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.VehicleSafety(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

Optimize Vehicle crash-worthiness.

+

See [Tanabe2020] for details.

+

The reference point is 1.1 * the nadir point from +approximate front provided by [Tanabe2020].

+

The maximum hypervolume is computed using the approximate +pareto front from [Tanabe2020].

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 5
+
+
+
+num_objectives: int = 3
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.ZDT(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

Base class for ZDT problems.

+

See [Zitzler2000] for more details on ZDT.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Number of objectives. Must not be larger than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+
+class botorch.test_functions.multi_objective.ZDT1(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: ZDT

+

ZDT1 test problem.

+

d-dimensional problem evaluated on [0, 1]^d:

+
+

f_0(x) = x_0 +f_1(x) = g(x) * (1 - sqrt(x_0 / g(x)) +g(x) = 1 + 9 / (d - 1) * sum_{i=1}^{d-1} x_i

+
+

The reference point comes from [Yang2019a].

+

The pareto front is convex.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Number of objectives. Must not be larger than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+gen_pareto_front(n)[source]
+

Generate n pareto optimal points.

+
+
Parameters:
+

n (int)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.ZDT2(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: ZDT

+

ZDT2 test problem.

+

d-dimensional problem evaluated on [0, 1]^d:

+
+

f_0(x) = x_0 +f_1(x) = g(x) * (1 - (x_0 / g(x))^2) +g(x) = 1 + 9 / (d - 1) * sum_{i=1}^{d-1} x_i

+
+

The reference point comes from [Yang2019a].

+

The pareto front is concave.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Number of objectives. Must not be larger than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+gen_pareto_front(n)[source]
+

Generate n pareto optimal points.

+
+
Parameters:
+

n (int)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.ZDT3(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: ZDT

+

ZDT3 test problem.

+

d-dimensional problem evaluated on [0, 1]^d:

+
+

f_0(x) = x_0 +f_1(x) = 1 - sqrt(x_0 / g(x)) - x_0 / g * sin(10 * pi * x_0) +g(x) = 1 + 9 / (d - 1) * sum_{i=1}^{d-1} x_i

+
+

The reference point comes from [Yang2019a].

+

The pareto front consists of several discontinuous convex parts.

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Number of objectives. Must not be larger than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+gen_pareto_front(n)[source]
+

Generate n pareto optimal points.

+
+
Parameters:
+

n (int)

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.CarSideImpact(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

Car side impact problem.

+

See [Tanabe2020] for details.

+

The reference point is nadir + 0.1 * (ideal - nadir) +where the ideal and nadir points come from the approximate +Pareto frontier from [Tanabe2020]. The max_hv was computed +based on the approximate Pareto frontier from [Tanabe2020].

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+num_objectives: int = 4
+
+
+
+dim: int = 7
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.BNH(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ConstrainedBaseTestProblem

+

The constrained BNH problem.

+

See [GarridoMerchan2020] for more details on this problem. Note that this is a +minimization problem.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 2
+
+
+
+num_objectives: int = 2
+
+
+
+num_constraints: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.CONSTR(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ConstrainedBaseTestProblem

+

The constrained CONSTR problem.

+

See [GarridoMerchan2020] for more details on this problem. Note that this is a +minimization problem.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 2
+
+
+
+num_objectives: int = 2
+
+
+
+num_constraints: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.ConstrainedBraninCurrin(noise_std=None, constraint_noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: BraninCurrin, ConstrainedBaseTestProblem

+

Constrained Branin Currin Function.

+

This uses the disk constraint from [Gelbart2014].

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise of the objectives.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the observation noise of the +constraint.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+dim: int = 2
+
+
+
+num_objectives: int = 2
+
+
+
+num_constraints: int = 1
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.C2DTLZ2(dim, num_objectives=2, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: DTLZ2, ConstrainedBaseTestProblem

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function.

  • +
  • num_objectives (int) – Must be less than dim.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_constraints: int = 1
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.DiscBrake(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ConstrainedBaseTestProblem

+

The Disc Brake problem.

+

There are 2 objectives and 4 constraints.

+

Both objectives should be minimized.

+

See [Tanabe2020] for details.

+

The reference point was set using the infer_reference_point +heuristic on the Pareto frontier over a large discrete set of +random designs.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 4
+
+
+
+num_objectives: int = 2
+
+
+
+num_constraints: int = 4
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.MW7(dim, noise_std=None, constraint_noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ConstrainedBaseTestProblem

+

The MW7 problem.

+

This problem has 2 objectives, 2 constraints, and a disconnected Pareto +frontier. It supports arbitrary input dimension > 1. See [Ma2019] for details.

+

This implementation is adapted from: +https://github.com/anyoptimization/pymoo/blob/master/pymoo/problems/multi/mw.py

+
+
Parameters:
+
    +
  • dim (int) – The (input) dimension of the function. Must be at least 2.

  • +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise of the objectives.

  • +
  • constraint_noise_std (None | float | list[float]) – Standard deviation of the observation noise of the +constraints.

  • +
  • negate (bool) – If True, negate the function.

  • +
  • dtype (torch.dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+num_constraints: int = 2
+
+
+
+num_objectives: int = 2
+
+
+
+LA2(A, B, C, D, theta)[source]
+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.OSY(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ConstrainedBaseTestProblem

+

The OSY test problem from [Oszycka1995]. +Implementation from +https://github.com/msu-coinlab/pymoo/blob/master/pymoo/problems/multi/osy.py +Note that this implementation assumes minimization, so please choose negate=True.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 6
+
+
+
+num_constraints: int = 6
+
+
+
+num_objectives: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.SRN(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ConstrainedBaseTestProblem

+

The constrained SRN problem.

+

See [GarridoMerchan2020] for more details on this problem. Note that this is a +minimization problem.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 2
+
+
+
+num_objectives: int = 2
+
+
+
+num_constraints: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective.WeldedBeam(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem, ConstrainedBaseTestProblem

+

The Welded Beam multi-objective test problem. Similar to WeldedBeamSO in +botorch.test_function.synthetic, but with an additional output, somewhat +modified constraints, and a different domain.

+

Implementation from +https://github.com/msu-coinlab/pymoo/blob/master/pymoo/problems/multi/welded_beam.py +Note that this implementation assumes minimization, so please choose negate=True.

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 4
+
+
+
+num_constraints: int = 4
+
+
+
+num_objectives: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+evaluate_slack_true(X)[source]
+

Evaluate the constraint slack (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A batch_shape x d-dim tensor of point(s) at which to evaluate the +constraint slacks: c_1(X), …., c_{n_c}(X).

+
+
Returns:
+

+
A batch_shape x n_c-dim tensor of constraint slack (where positive slack

corresponds to the constraint being feasible).

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Multi-Objective Multi-Fidelity Synthetic Test Functions

+

Multi-objective multi-fidelity optimization benchmark problems.

+

References

+
+
+[Irshad2021] +(1,2) +

F. Irshad, S. Karsch, and A. Döpp. Expected hypervolume improvement for +simultaneous multi-objective and multi-fidelity optimization. +arXiv preprint arXiv:2112.13901, 2021.

+
+
+
+
+class botorch.test_functions.multi_objective_multi_fidelity.MOMFBraninCurrin(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

Branin-Currin problem for multi-objective-multi-fidelity optimization.

+

(2+1)-dimensional function with domain [0,1]^3 where the last dimension +is the fidelity parameter s. +Both functions assume minimization. See [Irshad2021] for more details.

+

Modified Branin function:

+
+

B(x,s) = 21-(( +15*x_2 - b(s) * (15 * x_1 - 5) ** 2 + c(s) * (15 * x_1 - 5) - 6 ) ** 2 ++ 10 * (1 - t(s)) * cos(15 * x_1 - 5)+10)/22

+
+
+
Here b, c, r and t are constants and s is the fidelity parameter:

where b = 5.1 / (4 * math.pi ** 2) - 0.01(1-s), +c = 5 / math.pi - 0.1*(1 - s), +r = 6, +t = 1 / (8 * math.pi) + 0.05*(1-s)

+
+
+

Modified Currin function:

+
+

C(x) = 14-((1 - 0.1(1-s)exp(-1 / (2 * x_2))) * ( +2300 * x_1 ** 3 + 1900 * x_1 ** 2 + 2092 * x_1 + 60 +) / 100 * x_1 ** 3 + 500 * x_1 ** 2 + 4 * x_2 + 20)/15

+
+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 3
+
+
+
+num_objectives: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.multi_objective_multi_fidelity.MOMFPark(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: MultiObjectiveTestProblem

+

Modified Park test functions for multi-objective multi-fidelity optimization.

+

(4+1)-dimensional function with domain [0,1]^5 where the last dimension +is the fidelity parameter s. See [Irshad2021] for more details.

+

The first modified Park function is

+
+

P1(x, s)=A*(T1(x,s)+T2(x,s)-B)/22-0.8

+
+

The second modified Park function is

+
+

P2(x,s)=A*(5-2/3*exp(x1+x2)-x4*sin(x3)*A+x3-B)/4 - 0.7

+
+

Here

+
+

T_1(x,s) = (x1+0.001*(1-s))/2*sqrt(1+(x2+x3**2)*x4/(x1**2))

+

T_2(x, s) = (x1+3*x4)*exp(1+sin(x3))

+
+

and A(s)=(0.9+0.1*s), B(s)=0.1*(1-s).

+

Base constructor for multi-objective test functions.

+
+
Parameters:
+
    +
  • noise_std (None | float | list[float]) – Standard deviation of the observation noise. If a list is +provided, specifies separate noise standard deviations for each +objective.

  • +
  • negate (bool) – If True, negate the objectives.

  • +
  • dtype (torch.dtype)

  • +
+
+
+
+
+dim: int = 5
+
+
+
+num_objectives: int = 2
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Sensitivity Analysis Test Functions

+
+
+class botorch.test_functions.sensitivity_analysis.Ishigami(b=0.1, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Ishigami test function.

+

three-dimensional function (usually evaluated on [-pi, pi]^3):

+
+

f(x) = sin(x_1) + a sin(x_2)^2 + b x_3^4 sin(x_1)

+
+

Here a and b are constants where a=7 and b=0.1 or b=0.05 +Proposed to test sensitivity analysis methods because it exhibits strong +nonlinearity and nonmonotonicity and a peculiar dependence on x_3.

+
+
Parameters:
+
    +
  • b (float) – the b constant, should be 0.1 or 0.05.

  • +
  • noise_std (float | None) – Standard deviation of the observation noise.

  • +
  • negative – If True, negative the objective.

  • +
  • dtype (dtype) – The dtype that is used for the bounds of the function.

  • +
  • negate (bool)

  • +
+
+
+
+
+compute_dgsm(X)[source]
+

Compute derivative global sensitivity measures.

+

This function can be called separately to estimate the dgsm measure +The exact global integrals of these values are already added under +as attributes dgsm_gradient, dgsm_gradient_bas, and dgsm_gradient_square.

+
+
Parameters:
+

X (Tensor) – Set of points at which to compute derivative measures.

+
+
Return type:
+

tuple[list[float], list[float], list[float]]

+
+
+

Returns: The average gradient, absolute gradient, and square gradients.

+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.sensitivity_analysis.Gsobol(dim, a=None, noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Gsobol test function.

+

d-dimensional function (usually evaluated on [0, 1]^d):

+
+

f(x) = Prod_{i=1}^{d} ((|4x_i-2|+a_i)/(1+a_i)), a_i >=0

+
+

common combinations of dimension and a vector:

+
+

dim=8, a= [0, 1, 4.5, 9, 99, 99, 99, 99] +dim=6, a=[0, 0.5, 3, 9, 99, 99] +dim = 15, a= [1, 2, 5, 10, 20, 50, 100, 500, 1000, …, 1000]

+
+

Proposed to test sensitivity analysis methods +First order Sobol indices have closed form expression S_i=V_i/V with :

+
+

V_i= 1/(3(1+a_i)^2) +V= Prod_{i=1}^{d} (1+V_i) - 1

+
+
+
Parameters:
+
    +
  • dim (int) – Dimensionality of the problem. If 6, 8, or 15, will use standard a.

  • +
  • a (list) – a parameter, unless dim is 6, 8, or 15.

  • +
  • noise_std (float | None) – Standard deviation of observation noise.

  • +
  • negate (bool) – Return negative of function.

  • +
  • dtype (dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+optimal_sobol_indicies()[source]
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.test_functions.sensitivity_analysis.Morris(noise_std=None, negate=False, dtype=torch.float64)[source]
+

Bases: SyntheticTestFunction

+

Morris test function.

+

20-dimensional function (usually evaluated on [0, 1]^20):

+
+

f(x) = sum_{i=1}^20 beta_i w_i + sum_{i<j}^20 beta_ij w_i w_j ++ sum_{i<j<l}^20 beta_ijl w_i w_j w_l + 5w_1 w_2 w_3 w_4

+
+

Proposed to test sensitivity analysis methods

+
+
Parameters:
+
    +
  • noise_std (float | None) – Standard deviation of observation noise.

  • +
  • negate (bool) – Return negative of function.

  • +
  • dtype (dtype) – The dtype that is used for the bounds of the function.

  • +
+
+
+
+
+evaluate_true(X)[source]
+

Evaluate the function (w/o observation noise) on a set of points.

+
+
Parameters:
+

X (Tensor) – A (batch_shape) x d-dim tensor of point(s) at which to +evaluate.

+
+
Returns:
+

A batch_shape-dim tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Utilities For Test Functions

+
+
+botorch.test_functions.utils.round_nearest(X, increment, bounds)[source]
+

Rounds the input tensor to the nearest multiple of increment.

+
+
Parameters:
+
    +
  • X (Tensor) – The input to be rounded.

  • +
  • increment (float) – The increment to round to.

  • +
  • bounds (tuple[float, float] | None) – An optional tuple of two floats representing the lower and upper +bounds on X. If provided, this will round to the nearest multiple +of increment that lies within the bounds.

  • +
+
+
Returns:
+

The rounded input.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/test_utils.html b/website-old/pages/api/test_utils.html new file mode 100644 index 0000000000..379df3f0a3 --- /dev/null +++ b/website-old/pages/api/test_utils.html @@ -0,0 +1,101 @@ + + + + + + + +
+
+
+
+
+

botorch.test_utils

+

test_utils has its own directory with ‘botorch/’ to avoid circular dependencies: +Anything in ‘tests/’ can depend on anything in ‘botorch/test_utils/’, and +anything in ‘botorch/test_utils/’ can depend on anything in the rest of +‘botorch/’.

+
+

Mock

+

Utilities for speeding up optimization in tests.

+
+
+botorch.test_utils.mock.mock_optimize_context_manager(force=False)[source]
+

A context manager that uses mocks to speed up optimization for testing. +Currently, the primary tactic is to force the underlying scipy methods to stop +after just one iteration.

+
+
+
force: If True will not raise an AssertionError if no mocks are called.

USE RESPONSIBLY.

+
+
+
+
+
Parameters:
+

force (bool)

+
+
Return type:
+

Generator[None, None, None]

+
+
+
+
+
+botorch.test_utils.mock.mock_optimize(f)[source]
+

Wraps f in mock_optimize_context_manager for use as a decorator.

+
+
Parameters:
+

f (Callable)

+
+
Return type:
+

Callable

+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website-old/pages/api/utils.html b/website-old/pages/api/utils.html new file mode 100644 index 0000000000..72c8b4120f --- /dev/null +++ b/website-old/pages/api/utils.html @@ -0,0 +1,5654 @@ + + + + + + + +
+
+
+
+
+

botorch.utils

+
+

Constraints

+

Helpers for handling input or outcome constraints.

+
+
+botorch.utils.constraints.get_outcome_constraint_transforms(outcome_constraints)[source]
+

Create outcome constraint callables from outcome constraint tensors.

+
+
Parameters:
+

outcome_constraints (tuple[Tensor, Tensor] | None) – A tuple of (A, b). For k outcome constraints +and m outputs at f(x)`, A is k x m and b is k x 1 such +that A f(x) <= b.

+
+
Returns:
+

A list of callables, each mapping a Tensor of size b x q x m to a +tensor of size b x q, where m is the number of outputs of the model. +Negative values imply feasibility. The callables support broadcasting +(e.g. for calling on a tensor of shape mc_samples x b x q x m).

+
+
Return type:
+

list[Callable[[Tensor], Tensor]] | None

+
+
+

Example

+
>>> # constrain `f(x)[0] <= 0`
+>>> A = torch.tensor([[1., 0.]])
+>>> b = torch.tensor([[0.]])
+>>> outcome_constraints = get_outcome_constraint_transforms((A, b))
+
+
+
+
+
+botorch.utils.constraints.get_monotonicity_constraints(d, descending=False, dtype=None, device=None)[source]
+

Returns a system of linear inequalities (A, b) that generically encodes order +constraints on the elements of a d-dimsensional space, i.e. A @ x < b implies +x[i] < x[i + 1] for a d-dimensional vector x.

+

Idea: Could encode A as sparse matrix, if it is supported well.

+
+
Parameters:
+
    +
  • d (int) – Dimensionality of the constraint space, i.e. number of monotonic parameters.

  • +
  • descending (bool) – If True, forces the elements of a vector to be monotonically de- +creasing and be monotonically increasing otherwise.

  • +
  • dtype (dtype | None) – The dtype of the returned Tensors.

  • +
  • device (device | None) – The device of the returned Tensors.

  • +
+
+
Returns:
+

A tuple of Tensors (A, b) representing the monotonicity constraint as a system +of linear inequalities A @ x < b. A is (d - 1) x d-dimensional and b is +(d - 1) x 1-dimensional.

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+

Containers

+

Representations for different kinds of data.

+
+
+class botorch.utils.containers.BotorchContainer[source]
+

Bases: ABC

+

Abstract base class for BoTorch’s data containers.

+

A BotorchContainer represents a tensor, which should be the sole object +returned by its __call__ method. Said tensor is expected to consist of +one or more “events” (e.g. data points or feature vectors), whose shape is +given by the required event_shape field.

+

Notice: Once version 3.10 becomes standard, this class should +be reworked to take advantage of dataclasses’ kw_only flag.

+
+
+event_shape: Size
+
+
+
+abstract property shape: Size
+
+
+
+abstract property device: device
+
+
+
+abstract property dtype: dtype
+
+
+
+
+class botorch.utils.containers.DenseContainer(values, event_shape)[source]
+

Bases: BotorchContainer

+

Basic representation of data stored as a dense Tensor.

+
+
Parameters:
+
    +
  • values (Tensor)

  • +
  • event_shape (Size)

  • +
+
+
+
+
+values: Tensor
+
+
+
+event_shape: Size
+
+
+
+property shape: Size
+
+
+
+property device: device
+
+
+
+property dtype: dtype
+
+
+
+clone()[source]
+
+
Return type:
+

DenseContainer

+
+
+
+
+
+
+class botorch.utils.containers.SliceContainer(values, indices, event_shape)[source]
+

Bases: BotorchContainer

+

Represent data points formed by concatenating (n-1)-dimensional slices +taken from the leading dimension of an n-dimensional source tensor.

+
+
Parameters:
+
    +
  • values (Tensor)

  • +
  • indices (LongTensor)

  • +
  • event_shape (Size)

  • +
+
+
+
+
+values: Tensor
+
+
+
+indices: LongTensor
+
+
+
+event_shape: Size
+
+
+
+property shape: Size
+
+
+
+property device: device
+
+
+
+property dtype: dtype
+
+
+
+clone()[source]
+
+
Return type:
+

SliceContainer

+
+
+
+
+
+
+

Context Managers

+

Utilities for optimization.

+
+
+class botorch.utils.context_managers.TensorCheckpoint(values, device, dtype)[source]
+

Bases: NamedTuple

+

Create new instance of TensorCheckpoint(values, device, dtype)

+
+
Parameters:
+
    +
  • values (Tensor)

  • +
  • device (device | None)

  • +
  • dtype (dtype | None)

  • +
+
+
+
+
+values: Tensor
+

Alias for field number 0

+
+
+
+device: device | None
+

Alias for field number 1

+
+
+
+dtype: dtype | None
+

Alias for field number 2

+
+
+
+
+botorch.utils.context_managers.delattr_ctx(instance, *attrs, enforce_hasattr=False)[source]
+

Contextmanager for temporarily deleting attributes.

+
+
Parameters:
+
    +
  • instance (object)

  • +
  • attrs (str)

  • +
  • enforce_hasattr (bool)

  • +
+
+
Return type:
+

Generator[None, None, None]

+
+
+
+
+
+botorch.utils.context_managers.parameter_rollback_ctx(parameters, checkpoint=None, **tkwargs)[source]
+

Contextmanager that exits by rolling back a module’s state_dict.

+
+
Parameters:
+
    +
  • module – Module instance.

  • +
  • name_filter – Optional Boolean function used to filter items by name.

  • +
  • checkpoint (dict[str, TensorCheckpoint] | None) – Optional cache of values and tensor metadata specifying the rollback +state for the module (or some subset thereof).

  • +
  • **tkwargs (Any) – Keyword arguments passed to torch.Tensor.to when copying data from +each tensor in module.state_dict() to the internally created checkpoint. +Only adhered to when the checkpoint argument is None.

  • +
  • parameters (dict[str, Tensor])

  • +
+
+
Yields:
+

A dictionary of TensorCheckpoints for the module’s state_dict. Any in-places +changes to the checkpoint will be observed at rollback time. If the checkpoint +is cleared, no rollback will occur.

+
+
Return type:
+

Generator[dict[str, TensorCheckpoint], None, None]

+
+
+
+
+
+botorch.utils.context_managers.module_rollback_ctx(module, name_filter=None, checkpoint=None, **tkwargs)[source]
+

Contextmanager that exits by rolling back a module’s state_dict.

+
+
Parameters:
+
    +
  • module (Module) – Module instance.

  • +
  • name_filter (Callable[[str], bool] | None) – Optional Boolean function used to filter items by name.

  • +
  • checkpoint (dict[str, TensorCheckpoint] | None) – Optional cache of values and tensor metadata specifying the rollback +state for the module (or some subset thereof).

  • +
  • **tkwargs (Any) – Keyword arguments passed to torch.Tensor.to when copying data from +each tensor in module.state_dict() to the internally created checkpoint. +Only adhered to when the checkpoint argument is None.

  • +
+
+
Yields:
+

A dictionary of TensorCheckpoints for the module’s state_dict. Any in-places +changes to the checkpoint will be observed at rollback time. If the checkpoint +is cleared, no rollback will occur.

+
+
Return type:
+

Generator[dict[str, TensorCheckpoint], None, None]

+
+
+
+
+
+botorch.utils.context_managers.zero_grad_ctx(parameters, zero_on_enter=True, zero_on_exit=False)[source]
+
+
Parameters:
+
    +
  • parameters (dict[str, Tensor] | Iterable[Tensor])

  • +
  • zero_on_enter (bool)

  • +
  • zero_on_exit (bool)

  • +
+
+
Return type:
+

Generator[None, None, None]

+
+
+
+
+
+

Datasets

+

Representations for different kinds of datasets.

+
+
+class botorch.utils.datasets.SupervisedDataset(X, Y, *, feature_names, outcome_names, Yvar=None, validate_init=True)[source]
+

Bases: object

+

Base class for datasets consisting of labelled pairs (X, Y) +and an optional Yvar that stipulates observations variances so +that Y[i] ~ N(f(X[i]), Yvar[i]).

+

Example:

+
X = torch.rand(16, 2)
+Y = torch.rand(16, 1)
+feature_names = ["learning_rate", "embedding_dim"]
+outcome_names = ["neg training loss"]
+A = SupervisedDataset(
+    X=X,
+    Y=Y,
+    feature_names=feature_names,
+    outcome_names=outcome_names,
+)
+B = SupervisedDataset(
+    X=DenseContainer(X, event_shape=X.shape[-1:]),
+    Y=DenseContainer(Y, event_shape=Y.shape[-1:]),
+    feature_names=feature_names,
+    outcome_names=outcome_names,
+)
+assert A == B
+
+
+

Constructs a SupervisedDataset.

+
+
Parameters:
+
    +
  • X (BotorchContainer | Tensor) – A Tensor or BotorchContainer representing the input features.

  • +
  • Y (BotorchContainer | Tensor) – A Tensor or BotorchContainer representing the outcomes.

  • +
  • feature_names (list[str]) – A list of names of the features in X.

  • +
  • outcome_names (list[str]) – A list of names of the outcomes in Y.

  • +
  • Yvar (BotorchContainer | Tensor | None) – An optional Tensor or BotorchContainer representing +the observation noise.

  • +
  • validate_init (bool) – If True, validates the input shapes.

  • +
+
+
+
+
+property X: Tensor
+
+
+
+property Y: Tensor
+
+
+
+property Yvar: Tensor | None
+
+
+
+clone(deepcopy=False, mask=None)[source]
+

Return a copy of the dataset.

+
+
Parameters:
+
    +
  • deepcopy (bool) – If True, perform a deep copy. Otherwise, use the same +tensors/lists.

  • +
  • mask (Tensor | None) – A n-dim boolean mask indicating which rows to keep. This is used +along the -2 dimension.

  • +
+
+
Returns:
+

The new dataset.

+
+
Return type:
+

SupervisedDataset

+
+
+
+
+
+
+class botorch.utils.datasets.RankingDataset(X, Y, feature_names, outcome_names, validate_init=True)[source]
+

Bases: SupervisedDataset

+

A SupervisedDataset whose labelled pairs (x, y) consist of m-ary combinations +x ∈ Z^{m} of elements from a ground set Z = (z_1, …) and ranking vectors +y {0, …, m - 1}^{m} with properties:

+
+
    +
  1. Ranks start at zero, i.e. min(y) = 0.

  2. +
  3. Sorted ranks are contiguous unless one or more ties are present.

  4. +
  5. k ranks are skipped after a k-way tie.

  6. +
+
+

Example:

+
X = SliceContainer(
+    values=torch.rand(16, 2),
+    indices=torch.stack([torch.randperm(16)[:3] for _ in range(8)]),
+    event_shape=torch.Size([3 * 2]),
+)
+Y = DenseContainer(
+    torch.stack([torch.randperm(3) for _ in range(8)]),
+    event_shape=torch.Size([3])
+)
+feature_names = ["item_0", "item_1"]
+outcome_names = ["ranking outcome"]
+dataset = RankingDataset(
+    X=X,
+    Y=Y,
+    feature_names=feature_names,
+    outcome_names=outcome_names,
+)
+
+
+

Construct a RankingDataset.

+
+
Parameters:
+
    +
  • X (SliceContainer) – A SliceContainer representing the input features being ranked.

  • +
  • Y (BotorchContainer | Tensor) – A Tensor or BotorchContainer representing the rankings.

  • +
  • feature_names (list[str]) – A list of names of the features in X.

  • +
  • outcome_names (list[str]) – A list of names of the outcomes in Y.

  • +
  • validate_init (bool) – If True, validates the input shapes.

  • +
+
+
+
+
+
+class botorch.utils.datasets.MultiTaskDataset(datasets, target_outcome_name, task_feature_index=None)[source]
+

Bases: SupervisedDataset

+

This is a multi-task dataset that is constructed from the datasets of +individual tasks. It offers functionality to combine parts of individual +datasets to construct the inputs necessary for the MultiTaskGP models.

+

The datasets of individual tasks are allowed to represent different sets +of features. When there are heterogeneous feature sets, calling +MultiTaskDataset.X will result in an error.

+

Construct a MultiTaskDataset.

+
+
Parameters:
+
    +
  • datasets (list[SupervisedDataset]) – A list of the datasets of individual tasks. Each dataset +is expected to contain data for only one outcome.

  • +
  • target_outcome_name (str) – Name of the target outcome to be modeled.

  • +
  • task_feature_index (int | None) – If the task feature is included in the Xs of the +individual datasets, this should be used to specify its index. +If omitted, the task feature will be appended while concatenating Xs. +If given, we sanity-check that the names of the task features +match between all datasets.

  • +
+
+
+
+
+classmethod from_joint_dataset(dataset, task_feature_index, target_task_value, outcome_names_per_task=None)[source]
+

Construct a MultiTaskDataset from a joint dataset that includes the +data for all tasks with the task feature index.

+

This will break down the joint dataset into individual datasets by the value +of the task feature. Each resulting dataset will have its outcome name set +based on outcome_names_per_task, with the missing values defaulting to +task_<task_feature> (except for the target task, which will retain the +original outcome name from the dataset).

+
+
Parameters:
+
    +
  • dataset (SupervisedDataset) – The joint dataset.

  • +
  • task_feature_index (int) – The column index of the task feature in dataset.X.

  • +
  • target_task_value (int) – The value of the task feature for the target task +in the dataset. The data for the target task is filtered according to +dataset.X[task_feature_index] == target_task_value.

  • +
  • outcome_names_per_task (dict[int, str] | None) – Optional dictionary mapping task feature values +to the outcome names for each task. If not provided, the auxiliary +tasks will be named task_<task_feature> and the target task will +retain the outcome name from the dataset.

  • +
+
+
Returns:
+

A MultiTaskDataset instance.

+
+
Return type:
+

MultiTaskDataset

+
+
+
+
+
+property X: Tensor
+

Appends task features, if needed, and concatenates the Xs of datasets to +produce the train_X expected by MultiTaskGP and subclasses.

+

If appending the task features, 0 is reserved for the target task and the +remaining tasks are populated with 1, 2, …, len(datasets) - 1.

+
+
+
+property Y: Tensor
+

Concatenates Ys of the datasets.

+
+
+
+property Yvar: Tensor | None
+

Concatenates Yvars of the datasets if they exist.

+
+
+
+get_dataset_without_task_feature(outcome_name)[source]
+

A helper for extracting the child datasets with their task features removed.

+

If the task feature index is None, the dataset will be returned as is.

+
+
Parameters:
+

outcome_name (str) – The outcome name for the dataset to extract.

+
+
Returns:
+

The dataset without the task feature.

+
+
Return type:
+

SupervisedDataset

+
+
+
+
+
+clone(deepcopy=False, mask=None)[source]
+

Return a copy of the dataset.

+
+
Parameters:
+
    +
  • deepcopy (bool) – If True, perform a deep copy. Otherwise, use the same +tensors/lists/datasets.

  • +
  • mask (Tensor | None) – A n-dim boolean mask indicating which rows to keep from the target +dataset. This is used along the -2 dimension.

  • +
+
+
Returns:
+

The new dataset.

+
+
Return type:
+

MultiTaskDataset

+
+
+
+
+
+
+class botorch.utils.datasets.ContextualDataset(datasets, parameter_decomposition, metric_decomposition=None)[source]
+

Bases: SupervisedDataset

+

This is a contextual dataset that is constructed from either a single +dateset containing overall outcome or a list of datasets that each corresponds +to a context breakdown.

+

Construct a ContextualDataset.

+
+
Parameters:
+
    +
  • datasets (list[SupervisedDataset]) – A list of the datasets of individual tasks. Each dataset +is expected to contain data for only one outcome.

  • +
  • parameter_decomposition (dict[str, list[str]]) – Dict from context name to list of feature +names corresponding to that context.

  • +
  • metric_decomposition (dict[str, list[str]] | None) – Context breakdown metrics. Keys are context names. +Values are the lists of metric names belonging to the context: +{‘context1’: [‘m1_c1’], ‘context2’: [‘m1_c2’],}.

  • +
+
+
+
+
+property X: Tensor
+
+
+
+property Y: Tensor
+

Concatenates the Ys from the child datasets to create the Y expected +by LCEM model if there are multiple datasets; Or return the Y expected +by LCEA model if there is only one dataset.

+
+
+
+property Yvar: Tensor | None
+

Concatenates the Yvars from the child datasets to create the Y expected +by LCEM model if there are multiple datasets; Or return the Yvar expected +by LCEA model if there is only one dataset.

+
+
+
+clone(deepcopy=False, mask=None)[source]
+

Return a copy of the dataset.

+
+
Parameters:
+
    +
  • deepcopy (bool) – If True, perform a deep copy. Otherwise, use the same +tensors/lists/datasets.

  • +
  • mask (Tensor | None) – A n-dim boolean mask indicating which rows to keep. This is used +along the -2 dimension. n here corresponds to the number of rows in +an individual dataset.

  • +
+
+
Returns:
+

The new dataset.

+
+
Return type:
+

ContextualDataset

+
+
+
+
+
+
+

Dispatcher

+
+
+botorch.utils.dispatcher.type_bypassing_encoder(arg)[source]
+
+
Parameters:
+

arg (Any)

+
+
Return type:
+

type

+
+
+
+
+
+class botorch.utils.dispatcher.Dispatcher(name, doc=None, encoder=<class 'type'>)[source]
+

Bases: Dispatcher

+

Clearing house for multiple dispatch functionality. This class extends +<multipledispatch.Dispatcher> by: (i) generalizing the argument encoding +convention during method lookup, (ii) implementing __getitem__ as a dedicated +method lookup function.

+
+
Parameters:
+
    +
  • name (str) – A string identifier for the Dispatcher instance.

  • +
  • doc (str | None) – A docstring for the multiply dispatched method(s).

  • +
  • encoder (Callable[Any, type]) – A callable that individually transforms the arguments passed +at runtime in order to construct the key used for method lookup as +tuple(map(encoder, args)). Defaults to type.

  • +
+
+
+
+
+dispatch(*types)[source]
+

Method lookup strategy. Checks for an exact match before traversing +the set of registered methods according to the current ordering.

+
+
Parameters:
+

types (type) – A tuple of types that gets compared with the signatures +of registered methods to determine compatibility.

+
+
Returns:
+

The first method encountered with a matching signature.

+
+
Return type:
+

Callable

+
+
+
+
+
+encode_args(args)[source]
+

Converts arguments into a tuple of types used during method lookup.

+
+
Parameters:
+

args (Any)

+
+
Return type:
+

tuple[type]

+
+
+
+
+
+help(*args, **kwargs)[source]
+

Prints the retrieved method’s docstring.

+
+
Parameters:
+
    +
  • args (Any)

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

None

+
+
+
+
+
+source(*args, **kwargs)[source]
+

Prints the retrieved method’s source types.

+
+
Return type:
+

None

+
+
+
+
+
+property encoder: Callable[Any, type]
+
+
+
+name
+
+
+
+funcs
+
+
+
+doc
+
+
+
+
+

Low-Rank Cholesky Update Utils

+
+
+botorch.utils.low_rank.extract_batch_covar(mt_mvn)[source]
+

Extract a batched independent covariance matrix from an MTMVN.

+
+
Parameters:
+

mt_mvn (MultitaskMultivariateNormal) – A multi-task multivariate normal with a block diagonal +covariance matrix.

+
+
Returns:
+

+
A lazy covariance matrix consisting of a batch of the blocks of

the diagonal of the MultitaskMultivariateNormal.

+
+
+

+
+
Return type:
+

LinearOperator

+
+
+
+
+
+botorch.utils.low_rank.sample_cached_cholesky(posterior, baseline_L, q, base_samples, sample_shape, max_tries=6)[source]
+

Get posterior samples at the q new points from the joint multi-output +posterior.

+
+
Parameters:
+
    +
  • posterior (GPyTorchPosterior) – The joint posterior is over (X_baseline, X).

  • +
  • baseline_L (Tensor) – The baseline lower triangular cholesky factor.

  • +
  • q (int) – The number of new points in X.

  • +
  • base_samples (Tensor) – The base samples.

  • +
  • sample_shape (Size) – The sample shape.

  • +
  • max_tries (int) – The number of tries for computing the Cholesky +decomposition with increasing jitter.

  • +
+
+
Returns:
+

+
A sample_shape x batch_shape x q x m-dim tensor of posterior

samples at the new points.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+

Multi-Task Distribution Utils

+

Helpers for multitask modeling.

+
+
+botorch.utils.multitask.separate_mtmvn(mvn)[source]
+

Separate a MTMVN into a list of MVNs, where covariance across data within each task +are preserved, while covariance across task are dropped.

+
+
Parameters:
+

mvn (MultitaskMultivariateNormal)

+
+
Return type:
+

list[MultivariateNormal]

+
+
+
+
+
+

Objective

+

Helpers for handling objectives.

+
+
+botorch.utils.objective.get_objective_weights_transform(weights)[source]
+

Create a linear objective callable from a set of weights.

+

Create a callable mapping a Tensor of size b x q x m and an (optional) +Tensor of size b x q x d to a Tensor of size b x q, where m is the +number of outputs of the model using scalarization via the objective weights. +This callable supports broadcasting (e.g. for calling on a tensor of shape +mc_samples x b x q x m). For m = 1, the objective weight is used to +determine the optimization direction.

+
+
Parameters:
+

weights (Tensor | None) – a 1-dimensional Tensor containing a weight for each task. +If not provided, the identity mapping is used.

+
+
Returns:
+

Transform function using the objective weights.

+
+
Return type:
+

Callable[[Tensor, Tensor | None], Tensor]

+
+
+

Example

+
>>> weights = torch.tensor([0.75, 0.25])
+>>> transform = get_objective_weights_transform(weights)
+
+
+
+
+
+botorch.utils.objective.apply_constraints_nonnegative_soft(obj, constraints, samples, eta)[source]
+

Applies constraints to a non-negative objective.

+

This function uses a sigmoid approximation to an indicator function for +each constraint.

+
+
Parameters:
+
    +
  • obj (Tensor) – A n_samples x b x q (x m’)-dim Tensor of objective values.

  • +
  • constraints (list[Callable[[Tensor], Tensor]]) – A list of callables, each mapping a Tensor of size b x q x m +to a Tensor of size b x q, where negative values imply feasibility. +This callable must support broadcasting. Only relevant for multi- +output models (m > 1).

  • +
  • samples (Tensor) – A n_samples x b x q x m Tensor of samples drawn from the posterior.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function. Can be either a float +or a 1-dim tensor. In case of a float the same eta is used for every +constraint in constraints. In case of a tensor the length of the tensor +must match the number of provided constraints. The i-th constraint is +then estimated with the i-th eta value.

  • +
+
+
Returns:
+

A n_samples x b x q (x m’)-dim tensor of feasibility-weighted objectives.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.objective.compute_feasibility_indicator(constraints, samples, marginalize_dim=None)[source]
+

Computes the feasibility of a list of constraints given posterior samples.

+
+
Parameters:
+
    +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a batch_shape x q x m`-dim Tensor +to a batch_shape x q-dim Tensor, where negative values imply feasibility.

  • +
  • samples (Tensor) – A batch_shape x q x m`-dim Tensor of posterior samples.

  • +
  • marginalize_dim (int | None) – A batch dimension that should be marginalized. +For example, this is useful when using a batched fully Bayesian +model.

  • +
+
+
Returns:
+

A batch_shape x q-dim tensor of Boolean feasibility values.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.objective.compute_smoothed_feasibility_indicator(constraints, samples, eta, log=False, fat=False)[source]
+

Computes the smoothed feasibility indicator of a list of constraints.

+

Given posterior samples, using a sigmoid to smoothly approximate the feasibility +indicator of each individual constraint to ensure differentiability and high +gradient signal. The fat and log options improve the numerical behavior of +the smooth approximation.

+

NOTE: Negative constraint values are associated with feasibility.

+
+
Parameters:
+
    +
  • constraints (list[Callable[[Tensor], Tensor]]) – A list of callables, each mapping a Tensor of size b x q x m +to a Tensor of size b x q, where negative values imply feasibility. +This callable must support broadcasting. Only relevant for multi- +output models (m > 1).

  • +
  • samples (Tensor) – A n_samples x b x q x m Tensor of samples drawn from the posterior.

  • +
  • eta (Tensor | float) – The temperature parameter for the sigmoid function. Can be either a float +or a 1-dim tensor. In case of a float the same eta is used for every +constraint in constraints. In case of a tensor the length of the tensor +must match the number of provided constraints. The i-th constraint is +then estimated with the i-th eta value.

  • +
  • log (bool) – Toggles the computation of the log-feasibility indicator.

  • +
  • fat (bool) – Toggles the computation of the fat-tailed feasibility indicator.

  • +
+
+
Returns:
+

A n_samples x b x q-dim tensor of feasibility indicator values.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.objective.apply_constraints(obj, constraints, samples, infeasible_cost, eta=0.001)[source]
+

Apply constraints using an infeasible_cost M for negative objectives.

+

This allows feasibility-weighting an objective for the case where the +objective can be negative by using the following strategy: +(1) Add M to make obj non-negative; +(2) Apply constraints using the sigmoid approximation; +(3) Shift by -M.

+
+
Parameters:
+
    +
  • obj (Tensor) – A n_samples x b x q (x m’)-dim Tensor of objective values.

  • +
  • constraints (list[Callable[[Tensor], Tensor]]) – A list of callables, each mapping a Tensor of size b x q x m +to a Tensor of size b x q, where negative values imply feasibility. +This callable must support broadcasting. Only relevant for multi- +output models (m > 1).

  • +
  • samples (Tensor) – A n_samples x b x q x m Tensor of samples drawn from the posterior.

  • +
  • infeasible_cost (float) – The infeasible value.

  • +
  • eta (Tensor | float) – The temperature parameter of the sigmoid function. Can be either a float +or a 1-dim tensor. In case of a float the same eta is used for every +constraint in constraints. In case of a tensor the length of the tensor +must match the number of provided constraints. The i-th constraint is +then estimated with the i-th eta value.

  • +
+
+
Returns:
+

A n_samples x b x q (x m’)-dim tensor of feasibility-weighted objectives.

+
+
Return type:
+

Tensor

+
+
+
+
+
+

Rounding

+

Discretization (rounding) functions for acquisition optimization.

+

References

+
+
+[Daulton2022bopr] +(1,2) +

S. Daulton, X. Wan, D. Eriksson, M. Balandat, M. A. Osborne, E. Bakshy. +Bayesian Optimization over Discrete and Mixed Spaces via Probabilistic +Reparameterization. Advances in Neural Information Processing Systems +35, 2022.

+
+
+
+
+botorch.utils.rounding.approximate_round(X, tau=0.001)[source]
+

Diffentiable approximate rounding function.

+

This method is a piecewise approximation of a rounding function where +each piece is a hyperbolic tangent function.

+
+
Parameters:
+
    +
  • X (Tensor) – The tensor to round to the nearest integer (element-wise).

  • +
  • tau (float) – A temperature hyperparameter.

  • +
+
+
Returns:
+

The approximately rounded input tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+class botorch.utils.rounding.IdentitySTEFunction(*args, **kwargs)[source]
+

Bases: Function

+

Base class for functions using straight through gradient estimators.

+

This class approximates the gradient with the identity function.

+
+
+static backward(ctx, grad_output)[source]
+

Use a straight-through estimator the gradient.

+

This uses the identity function.

+
+
Parameters:
+

grad_output (Tensor) – A tensor of gradients.

+
+
Returns:
+

The provided tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.rounding.RoundSTE(*args, **kwargs)[source]
+

Bases: IdentitySTEFunction

+

Round the input tensor and use a straight-through gradient estimator.

+

[Daulton2022bopr] proposes using this in acquisition optimization.

+
+
+static forward(ctx, X)[source]
+

Round the input tensor element-wise.

+
+
Parameters:
+

X (Tensor) – The tensor to be rounded.

+
+
Returns:
+

A tensor where each element is rounded to the nearest integer.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.rounding.OneHotArgmaxSTE(*args, **kwargs)[source]
+

Bases: IdentitySTEFunction

+

Discretize a continuous relaxation of a one-hot encoded categorical.

+

This returns a one-hot encoded categorical and use a straight-through +gradient estimator via an identity function.

+

[Daulton2022bopr] proposes using this in acquisition optimization.

+
+
+static forward(ctx, X)[source]
+

Discretize the input tensor.

+

This applies a argmax along the last dimensions of the input tensor +and one-hot encodes the result.

+
+
Parameters:
+

X (Tensor) – The tensor to be rounded.

+
+
Returns:
+

A tensor where each element is rounded to the nearest integer.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Sampling

+

Utilities for MC and qMC sampling.

+

References

+
+
+[Trikalinos2014polytope] +

T. A. Trikalinos and G. van Valkenhoef. Efficient sampling from uniform +density n-polytopes. Technical report, Brown University, 2014.

+
+
+
+
+botorch.utils.sampling.manual_seed(seed=None)[source]
+

Contextmanager for manual setting the torch.random seed.

+
+
Parameters:
+

seed (int | None) – The seed to set the random number generator to.

+
+
Returns:
+

Generator

+
+
Return type:
+

Generator[None, None, None]

+
+
+

Example

+
>>> with manual_seed(1234):
+>>>     X = torch.rand(3)
+
+
+
+
+
+botorch.utils.sampling.draw_sobol_samples(bounds, n, q, batch_shape=None, seed=None)[source]
+

Draw qMC samples from the box defined by bounds.

+
+
Parameters:
+
    +
  • bounds (Tensor) – A 2 x d dimensional tensor specifying box constraints on a +d-dimensional space, where bounds[0, :] and bounds[1, :] correspond +to lower and upper bounds, respectively.

  • +
  • n (int) – The number of (q-batch) samples. As a best practice, use powers of 2.

  • +
  • q (int) – The size of each q-batch.

  • +
  • batch_shape (Iterable[int] | Size | None) – The batch shape of the samples. If given, returns samples +of shape n x batch_shape x q x d, where each batch is an +n x q x d-dim tensor of qMC samples.

  • +
  • seed (int | None) – The seed used for initializing Owen scrambling. If None (default), +use a random seed.

  • +
+
+
Returns:
+

A n x batch_shape x q x d-dim tensor of qMC samples from the box +defined by bounds.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> bounds = torch.stack([torch.zeros(3), torch.ones(3)])
+>>> samples = draw_sobol_samples(bounds, 16, 2)
+
+
+
+
+
+botorch.utils.sampling.draw_sobol_normal_samples(d, n, device=None, dtype=None, seed=None)[source]
+

Draw qMC samples from a multi-variate standard normal N(0, I_d).

+

A primary use-case for this functionality is to compute an QMC average +of f(X) over X where each element of X is drawn N(0, 1).

+
+
Parameters:
+
    +
  • d (int) – The dimension of the normal distribution.

  • +
  • n (int) – The number of samples to return. As a best practice, use powers of 2.

  • +
  • device (device | None) – The torch device.

  • +
  • dtype (dtype | None) – The torch dtype.

  • +
  • seed (int | None) – The seed used for initializing Owen scrambling. If None (default), +use a random seed.

  • +
+
+
Returns:
+

A tensor of qMC standard normal samples with dimension n x d with device +and dtype specified by the input.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> samples = draw_sobol_normal_samples(2, 16)
+
+
+
+
+
+botorch.utils.sampling.sample_hypersphere(d, n=1, qmc=False, seed=None, device=None, dtype=None)[source]
+

Sample uniformly from a unit d-sphere.

+
+
Parameters:
+
    +
  • d (int) – The dimension of the hypersphere.

  • +
  • n (int) – The number of samples to return.

  • +
  • qmc (bool) – If True, use QMC Sobol sampling (instead of i.i.d. uniform).

  • +
  • seed (int | None) – If provided, use as a seed for the RNG.

  • +
  • device (device | None) – The torch device.

  • +
  • dtype (dtype | None) – The torch dtype.

  • +
+
+
Returns:
+

An n x d tensor of uniform samples from from the d-hypersphere.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> sample_hypersphere(d=5, n=10)
+
+
+
+
+
+botorch.utils.sampling.sample_simplex(d, n=1, qmc=False, seed=None, device=None, dtype=None)[source]
+

Sample uniformly from a d-simplex.

+
+
Parameters:
+
    +
  • d (int) – The dimension of the simplex.

  • +
  • n (int) – The number of samples to return.

  • +
  • qmc (bool) – If True, use QMC Sobol sampling (instead of i.i.d. uniform).

  • +
  • seed (int | None) – If provided, use as a seed for the RNG.

  • +
  • device (device | None) – The torch device.

  • +
  • dtype (dtype | None) – The torch dtype.

  • +
+
+
Returns:
+

An n x d tensor of uniform samples from from the d-simplex.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> sample_simplex(d=3, n=10)
+
+
+
+
+
+botorch.utils.sampling.sample_polytope(A, b, x0, n=10000, n0=100, n_thinning=1, seed=None)[source]
+

Hit and run sampler from uniform sampling points from a polytope, +described via inequality constraints A*x<=b.

+
+
Parameters:
+
    +
  • A (Tensor) – A m x d-dim Tensor describing inequality constraints +so that all samples satisfy Ax <= b.

  • +
  • b (Tensor) – A m-dim Tensor describing the inequality constraints +so that all samples satisfy Ax <= b.

  • +
  • x0 (Tensor) – A d-dim Tensor representing a starting point of the chain +satisfying the constraints.

  • +
  • n (int) – The number of resulting samples kept in the output.

  • +
  • n0 (int) – The number of burn-in samples. The chain will produce +n+n0 samples but the first n0 samples are not saved.

  • +
  • n_thinning (int) – The amount of thinnning. This function will return every +n_thinning-th sample from the chain (after burn-in).

  • +
  • seed (int | None) – The seed for the sampler. If omitted, use a random seed.

  • +
+
+
Returns:
+

(n, d) dim Tensor containing the resulting samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.sampling.batched_multinomial(weights, num_samples, replacement=False, generator=None, out=None)[source]
+

Sample from multinomial with an arbitrary number of batch dimensions.

+
+
Parameters:
+
    +
  • weights (Tensor) – A batch_shape x num_categories tensor of weights. For each batch +index i, j, …, this functions samples from a multinomial with input +weights[i, j, …, :]. Note that the weights need not sum to one, but must +be non-negative, finite and have a non-zero sum.

  • +
  • num_samples (int) – The number of samples to draw for each batch index. Must be smaller +than num_categories if replacement=False.

  • +
  • replacement (bool) – If True, samples are drawn with replacement.

  • +
  • generator (Generator | None) – A a pseudorandom number generator for sampling.

  • +
  • out (Tensor | None) – The output tensor (optional). If provided, must be of size +batch_shape x num_samples.

  • +
+
+
Returns:
+

A batch_shape x num_samples tensor of samples.

+
+
Return type:
+

LongTensor

+
+
+

This is a thin wrapper around torch.multinomial that allows weight (input) +tensors with an arbitrary number of batch dimensions (torch.multinomial only +allows a single batch dimension). The calling signature is the same as for +torch.multinomial.

+

Example

+
>>> weights = torch.rand(2, 3, 10)
+>>> samples = batched_multinomial(weights, 4)  # shape is 2 x 3 x 4
+
+
+
+
+
+botorch.utils.sampling.find_interior_point(A, b, A_eq=None, b_eq=None)[source]
+

Find an interior point of a polytope via linear programming.

+
+
Parameters:
+
    +
  • A (ndarray[Any, dtype[_ScalarType_co]]) – A n_ineq x d-dim numpy array containing the coefficients of the +constraint inequalities.

  • +
  • b (ndarray[Any, dtype[_ScalarType_co]]) – A n_ineq x 1-dim numpy array containing the right hand sides of +the constraint inequalities.

  • +
  • A_eq (ndarray[Any, dtype[_ScalarType_co]] | None) – A n_eq x d-dim numpy array containing the coefficients of the +constraint equalities.

  • +
  • b_eq (ndarray[Any, dtype[_ScalarType_co]] | None) – A n_eq x 1-dim numpy array containing the right hand sides of +the constraint equalities.

  • +
+
+
Returns:
+

A d-dim numpy array containing an interior point of the polytope. +This function will raise a ValueError if there is no such point.

+
+
Return type:
+

ndarray[Any, dtype[_ScalarType_co]]

+
+
+

This method solves the following Linear Program:

+
+

min -s subject to A @ x <= b - 2 * s, s >= 0, A_eq @ x = b_eq

+
+

In case the polytope is unbounded, then it will also constrain the slack +variable s to s<=1.

+
+
+
+class botorch.utils.sampling.PolytopeSampler(inequality_constraints=None, equality_constraints=None, bounds=None, interior_point=None)[source]
+

Bases: ABC

+

Base class for samplers that sample points from a polytope.

+
+
Parameters:
+
    +
  • inequality_constraints (tuple[Tensor, Tensor] | None) – Tensors (A, b) describing inequality +constraints A @ x <= b, where A is a n_ineq_con x d-dim +Tensor and b is a n_ineq_con x 1-dim Tensor, with n_ineq_con +the number of inequalities and d the dimension of the sample space.

  • +
  • equality_constraints (tuple[Tensor, Tensor] | None) – Tensors (C, d) describing the equality constraints +C @ x = d, where C is a n_eq_con x d-dim Tensor and d is a +n_eq_con x 1-dim Tensor with n_eq_con the number of equalities.

  • +
  • bounds (Tensor | None) – A 2 x d-dim tensor of box bounds, where inf (-inf) means +that the respective dimension is unbounded above (below).

  • +
  • interior_point (Tensor | None) – A d x 1-dim Tensor presenting a point in the +(relative) interior of the polytope. If omitted, determined +automatically by solving a Linear Program.

  • +
+
+
+
+
+feasible(x)[source]
+

Check whether a point is contained in the polytope.

+
+
Parameters:
+

x (Tensor) – A d x 1-dim Tensor.

+
+
Returns:
+

True if x is contained inside the polytope (incl. its boundary), +False otherwise.

+
+
Return type:
+

bool

+
+
+
+
+
+find_interior_point()[source]
+

Find an interior point of the polytope.

+
+
Returns:
+

A d x 1-dim Tensor representing a point contained in the polytope. +This function will raise a ValueError if there is no such point.

+
+
Return type:
+

Tensor

+
+
+
+
+
+abstract draw(n=1)[source]
+

Draw samples from the polytope.

+
+
Parameters:
+

n (int) – The number of samples.

+
+
Returns:
+

A n x d Tensor of samples from the polytope.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.sampling.HitAndRunPolytopeSampler(inequality_constraints=None, equality_constraints=None, bounds=None, interior_point=None, n_burnin=200, n_thinning=20, seed=None)[source]
+

Bases: PolytopeSampler

+

A sampler for sampling from a polyope using a hit-and-run algorithm.

+

A sampler for sampling from a polyope using a hit-and-run algorithm.

+
+
Parameters:
+
    +
  • inequality_constraints (tuple[Tensor, Tensor] | None) – Tensors (A, b) describing inequality +constraints A @ x <= b, where A is a n_ineq_con x d-dim +Tensor and b is a n_ineq_con x 1-dim Tensor, with n_ineq_con +the number of inequalities and d the dimension of the sample space.

  • +
  • equality_constraints (tuple[Tensor, Tensor] | None) – Tensors (C, d) describing the equality constraints +C @ x = d, where C is a n_eq_con x d-dim Tensor and d is a +n_eq_con x 1-dim Tensor with n_eq_con the number of equalities.

  • +
  • bounds (Tensor | None) – A 2 x d-dim tensor of box bounds, where inf (-inf) means +that the respective dimension is unbounded from above (below). If +omitted, no bounds (in addition to the above constraints) are applied.

  • +
  • interior_point (Tensor | None) – A d x 1-dim Tensor representing a point in the +(relative) interior of the polytope. If omitted, determined +automatically by solving a Linear Program.

  • +
  • n_burnin (int) – The number of burn in samples. The sampler will discard +n_burnin samples before returning the first sample.

  • +
  • n_thinning (int) – The amount of thinning. The sampler will return every +n_thinning sample (after burn-in). This may need to be increased +for sets of constraints that are difficult to satisfy (i.e. in which +case the volume of the constraint polytope is small relative to that +of its bounding box).

  • +
  • seed (int | None) – The random seed.

  • +
+
+
+
+
+draw(n=1)[source]
+

Draw samples from the polytope.

+
+
Parameters:
+

n (int) – The number of samples.

+
+
Returns:
+

A n x d Tensor of samples from the polytope.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.sampling.DelaunayPolytopeSampler(inequality_constraints=None, equality_constraints=None, bounds=None, interior_point=None)[source]
+

Bases: PolytopeSampler

+

A polytope sampler using Delaunay triangulation.

+

This sampler first enumerates the vertices of the constraint polytope and +then uses a Delaunay triangulation to tesselate its convex hull.

+

The sampling happens in two stages: +1. First, we sample from the set of hypertriangles generated by the +Delaunay triangulation (i.e. which hyper-triangle to draw the sample +from) with probabilities proportional to the triangle volumes. +2. Then, we sample uniformly from the chosen hypertriangle by sampling +uniformly from the unit simplex of the appropriate dimension, and +then computing the convex combination of the vertices of the +hypertriangle according to that draw from the simplex.

+

The best reference (not exactly the same, but functionally equivalent) is +[Trikalinos2014polytope]. A simple R implementation is available at +https://github.com/gertvv/tesselample.

+

Initialize DelaunayPolytopeSampler.

+
+
Parameters:
+
    +
  • inequality_constraints (tuple[Tensor, Tensor] | None) – Tensors (A, b) describing inequality +constraints A @ x <= b, where A is a n_ineq_con x d-dim +Tensor and b is a n_ineq_con x 1-dim Tensor, with n_ineq_con +the number of inequalities and d the dimension of the sample space.

  • +
  • equality_constraints (tuple[Tensor, Tensor] | None) – Tensors (C, d) describing the equality constraints +C @ x = d, where C is a n_eq_con x d-dim Tensor and d is a +n_eq_con x 1-dim Tensor with n_eq_con the number of equalities.

  • +
  • bounds (Tensor | None) – A 2 x d-dim tensor of box bounds, where inf (-inf) means +that the respective dimension is unbounded from above (below).

  • +
  • interior_point (Tensor | None) – A d x 1-dim Tensor representing a point in the +(relative) interior of the polytope. If omitted, determined +automatically by solving a Linear Program.

  • +
+
+
+

Warning: The vertex enumeration performed in this algorithm can become +extremely costly if there are a large number of inequalities. Similarly, +the triangulation can get very expensive in high dimensions. Only use +this algorithm for moderate dimensions / moderately complex constraint sets. +An alternative is the HitAndRunPolytopeSampler.

+
+
+draw(n=1, seed=None)[source]
+

Draw samples from the polytope.

+
+
Parameters:
+
    +
  • n (int) – The number of samples.

  • +
  • seed (int | None) – The random seed.

  • +
+
+
Returns:
+

A n x d Tensor of samples from the polytope.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+botorch.utils.sampling.normalize_sparse_linear_constraints(bounds, constraints)[source]
+

Normalize sparse linear constraints to the unit cube.

+
+
Parameters:
+
    +
  • bounds (Tensor) – A 2 x d-dim tensor containing the box bounds.

  • +
  • constraints (list[tuple[Tensor, Tensor, float]]) – A list of tuples (indices, coefficients, rhs), with +indices and coefficients one-dimensional tensors and rhs a +scalar, where each tuple encodes an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs or +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
+
+
Return type:
+

list[tuple[Tensor, Tensor, float]]

+
+
+
+
+
+botorch.utils.sampling.normalize_dense_linear_constraints(bounds, constraints)[source]
+

Normalize dense linear constraints to the unit cube.

+
+
Parameters:
+
    +
  • bounds (Tensor) – A 2 x d-dim tensor containing the box bounds.

  • +
  • constraints (tuple[Tensor, Tensor]) – A tensor tuple (A, b) describing constraints +A @ x (<)= b, where A is a n_con x d-dim Tensor and +b is a n_con x 1-dim Tensor, with n_con the number of +constraints and d the dimension of the sample space.

  • +
+
+
Returns:
+

A tensor tuple (A_nlz, b_nlz) of normalized constraints.

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.utils.sampling.get_polytope_samples(n, bounds, inequality_constraints=None, equality_constraints=None, seed=None, n_burnin=10000, n_thinning=32)[source]
+

Sample from polytope defined by box bounds and (in)equality constraints.

+

This uses a hit-and-run Markov chain sampler.

+

NOTE: Much of the functionality of this method has been moved into +HitAndRunPolytopeSampler. If you want to repeatedly draw samples, you should +use HitAndRunPolytopeSampler directly in order to avoid repeatedly running +a burn-in of the chain. To do so, you need to convert the sparse constraint +format that get_polytope_samples expects to the dense constraint format that +HitAndRunPolytopeSampler expects. This can be done via the +sparse_to_dense_constraints method (but remember to adjust the constraint +from the Ax >= b format expecxted here to the Ax <= b format expected by +PolytopeSampler by multiplying both A and b by -1.)

+

NOTE: This method does not support the kind of “inter-point constraints” that +are supported by optimize_acqf(). To achieve this behavior, you need define the +problem on the joint space over q points and impose use constraints, see: +https://github.com/pytorch/botorch/issues/2468#issuecomment-2287706461

+
+
Parameters:
+
    +
  • n (int) – The number of samples.

  • +
  • bounds (Tensor) – A 2 x d-dim tensor containing the box bounds.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with indices and coefficients one-dimensional tensors and rhs a +scalar, where each tuple encodes an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • equality_constraints (list[tuple[Tensor, Tensor, float]] | None) – A list of tuples (indices, coefficients, rhs), +with indices and coefficients one-dimensional tensors and rhs a +scalar, where each tuple encodes an equality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
  • seed (int | None) – The random seed.

  • +
  • n_burnin (int) – The number of burn-in samples for the Markov chain sampler.

  • +
  • n_thinning (int) – The amount of thinnning. This function will return every +n_thinning-th sample from the chain (after burn-in).

  • +
+
+
Returns:
+

A n x d-dim tensor of samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.sampling.sparse_to_dense_constraints(d, constraints)[source]
+

Convert parameter constraints from a sparse format into a dense format.

+

This method converts sparse triples of the form (indices, coefficients, rhs) +to constraints of the form Ax >= b or Ax = b.

+
+
Parameters:
+
    +
  • d (int) – The input dimension.

  • +
  • constraints (list[tuple[Tensor, Tensor, float]]) – A list of tuples (indices, coefficients, rhs), +with indices and coefficients one-dimensional tensors and rhs a +scalar, where each tuple encodes an (in)equality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs or +sum_i (X[indices[i]] * coefficients[i]) = rhs.

  • +
+
+
Returns:
+

    +
  • A: A n_constraints x d-dim tensor of coefficients.

  • +
  • b: A n_constraints x 1-dim tensor of right hand sides.

  • +
+

+
+
Return type:
+

A two-element tuple containing

+
+
+
+
+
+botorch.utils.sampling.optimize_posterior_samples(paths, bounds, raw_samples=1024, num_restarts=20, sample_transform=None, return_transformed=False)[source]
+

Cheaply maximizes posterior samples by random querying followed by +gradient-based optimization using SciPy’s L-BFGS-B routine.

+
+
Parameters:
+
    +
  • paths (GenericDeterministicModel) – Random Fourier Feature-based sample paths from the GP

  • +
  • bounds (Tensor) – The bounds on the search space.

  • +
  • raw_samples (int) – The number of samples with which to query the samples initially.

  • +
  • num_restarts (int) – The number of points selected for gradient-based optimization.

  • +
  • sample_transform (Callable[[Tensor], Tensor] | None) – A callable transform of the sample outputs (e.g. +MCAcquisitionObjective or ScalarizedPosteriorTransform.evaluate) used to +negate the objective or otherwise transform the output.

  • +
  • return_transformed (bool) – A boolean indicating whether to return the transformed +or non-transformed samples.

  • +
+
+
Returns:
+

    +
  • X_opt: A num_optima x [batch_size] x d-dim tensor of optimal inputs x*.

  • +
  • +
    f_opt: A num_optima x [batch_size] x m-dim, optionally

    num_optima x [batch_size] x 1-dim, tensor of optimal outputs f*.

    +
    +
    +
  • +
+

+
+
Return type:
+

A two-element tuple containing

+
+
+
+
+
+

Sampling from GP priors

+
+
+class botorch.utils.gp_sampling.GPDraw(model, seed=None)[source]
+

Bases: Module

+

Convenience wrapper for sampling a function from a GP prior.

+

This wrapper implicitly defines the GP sample as a self-updating function by keeping +track of the evaluated points and respective base samples used during the +evaluation.

+

This does not yet support multi-output models.

+

Construct a GP function sampler.

+
+
Parameters:
+
    +
  • model (Model) – The Model defining the GP prior.

  • +
  • seed (int | None)

  • +
+
+
+
+
+property Xs: Tensor
+

A (batch_shape) x n_eval x d-dim tensor of locations at which the GP was +evaluated (or None if the sample has never been evaluated).

+
+
+
+property Ys: Tensor
+

A (batch_shape) x n_eval x d-dim tensor of associated function values (or +None if the sample has never been evaluated).

+
+
+
+forward(X)[source]
+

Evaluate the GP sample function at a set of points X.

+
+
Parameters:
+

X (Tensor) – A batch_shape x n x d-dim tensor of points

+
+
Returns:
+

The value of the GP sample at the n points.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.gp_sampling.RandomFourierFeatures(kernel, input_dim, num_rff_features, sample_shape=None)[source]
+

Bases: Module

+

A class that represents Random Fourier Features.

+

Initialize RandomFourierFeatures.

+
+
Parameters:
+
    +
  • kernel (Kernel) – The GP kernel.

  • +
  • input_dim (int) – The input dimension to the GP kernel.

  • +
  • num_rff_features (int) – The number of Fourier features.

  • +
  • sample_shape (torch.Size | None) – The shape of a single sample. For a single-element +torch.Size object, this is simply the number of RFF draws.

  • +
+
+
+
+
+forward(X)[source]
+

Get Fourier basis features for the provided inputs.

+

Note that the right-most subset of the batch shape of X should +be (sample_shape) x (kernel_batch_shape) if using either the +sample_shape argument or a batched kernel. In other words, +X should be of shape (added_batch_shape) x (sample_shape) x +(kernel_batch_shape) x n x input_dim, where parantheses denote +that the given batch shape can be empty. X can always be +a tensor of shape n x input_dim, in which case broadcasting +will take care of the batch shape. This will raise a ValueError +if the batch shapes are not compatible.

+
+
Parameters:
+

X (Tensor) – Input tensor of shape (batch_shape) x n x input_dim.

+
+
Returns:
+

A Tensor of shape (batch_shape) x n x rff. If X does not have +a batch_shape, the output batch_shape will be +(sample_shape) x (kernel_batch_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+botorch.utils.gp_sampling.get_deterministic_model_multi_samples(weights, bases)[source]
+

Get a batched deterministic model that batch evaluates n_samples function +samples. This supports multi-output models as well.

+
+
Parameters:
+
    +
  • weights (list[Tensor]) – A list of weights with num_outputs elements. Each weight is of +shape (batch_shape_input) x n_samples x num_rff_features, where +(batch_shape_input) is the batch shape of the inputs used to obtain the +posterior weights.

  • +
  • bases (list[RandomFourierFeatures]) – A list of RandomFourierFeatures with num_outputs elements. Each +basis has a sample shape of n_samples.

  • +
  • n_samples – The number of function samples.

  • +
+
+
Returns:
+

A batched GenericDeterministicModel`s that batch evaluates `n_samples +function samples.

+
+
Return type:
+

GenericDeterministicModel

+
+
+
+
+
+botorch.utils.gp_sampling.get_eval_gp_sample_callable(w, basis)[source]
+
+
Parameters:
+
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.gp_sampling.get_deterministic_model(weights, bases)[source]
+

Get a deterministic model using the provided weights and bases for each output.

+
+
Parameters:
+
    +
  • weights (list[Tensor]) – A list of weights with m elements.

  • +
  • bases (list[RandomFourierFeatures]) – A list of RandomFourierFeatures with m elements.

  • +
+
+
Returns:
+

A deterministic model.

+
+
Return type:
+

GenericDeterministicModel

+
+
+
+
+
+botorch.utils.gp_sampling.get_deterministic_model_list(weights, bases)[source]
+

Get a deterministic model list using the provided weights and bases +for each output.

+
+
Parameters:
+
    +
  • weights (list[Tensor]) – A list of weights with m elements.

  • +
  • bases (list[RandomFourierFeatures]) – A list of RandomFourierFeatures with m elements.

  • +
+
+
Returns:
+

A deterministic model.

+
+
Return type:
+

ModelList

+
+
+
+
+
+botorch.utils.gp_sampling.get_weights_posterior(X, y, sigma_sq)[source]
+

Sample bayesian linear regression weights.

+
+
Parameters:
+
    +
  • X (Tensor) – A tensor of inputs with shape (*batch_shape, n num_rff_features).

  • +
  • y (Tensor) – A tensor of outcomes with shape (*batch_shape, n).

  • +
  • sigma_sq (Tensor) – The likelihood noise variance. This should be a tensor with +shape kernel_batch_shape, 1, 1 if using a batched kernel. +Otherwise, it should be a scalar tensor.

  • +
+
+
Returns:
+

The posterior distribution over the weights.

+
+
Return type:
+

MultivariateNormal

+
+
+
+
+
+botorch.utils.gp_sampling.get_gp_samples(model, num_outputs, n_samples, num_rff_features=512)[source]
+

Sample functions from GP posterior using RFFs. The returned +GenericDeterministicModel effectively wraps num_outputs models, +each of which has a batch shape of n_samples. Refer +get_deterministic_model_multi_samples for more details.

+

NOTE: If using input / outcome transforms, the gp samples must be accessed via +the gp_sample.posterior(X) call. Otherwise, gp_sample(X) will produce bogus +values that do not agree with the underlying model. It is also highly recommended +to use outcome transforms to standardize the input data, since the gp samples do +not work well when training outcomes are not zero-mean.

+
+
Parameters:
+
    +
  • model (Model) – The model.

  • +
  • num_outputs (int) – The number of outputs.

  • +
  • n_samples (int) – The number of functions to be sampled IID.

  • +
  • num_rff_features (int) – The number of random Fourier features.

  • +
+
+
Returns:
+

A GenericDeterministicModel that evaluates n_samples sampled functions. +If n_samples > 1, this will be a batched model.

+
+
Return type:
+

GenericDeterministicModel

+
+
+
+
+
+

Testing

+
+
+class botorch.utils.testing.BotorchTestCase(methodName='runTest')[source]
+

Bases: TestCase

+

Basic test case for Botorch.

+
+
This
    +
  1. sets the default device to be torch.device(“cpu”)

  2. +
  3. ensures that no warnings are suppressed by default.

  4. +
+
+
+

Create an instance of the class that will use the named test +method when executed. Raises a ValueError if the instance does +not have a method with the specified name.

+
+
+device = device(type='cpu')
+
+
+
+setUp(suppress_input_warnings=True)[source]
+

Hook method for setting up the test fixture before exercising it.

+
+
Parameters:
+

suppress_input_warnings (bool)

+
+
Return type:
+

None

+
+
+
+
+
+assertAllClose(input, other, rtol=1e-05, atol=1e-08, equal_nan=False)[source]
+

Calls torch.testing.assert_close, using the signature and default behavior +of torch.allclose.

+
+
Example output:

AssertionError: Scalars are not close!

+

Absolute difference: 1.0000034868717194 (up to 0.0001 allowed) +Relative difference: 0.8348668001940709 (up to 1e-05 allowed)

+
+
+
+
Parameters:
+
    +
  • input (Any)

  • +
  • other (Any)

  • +
  • rtol (float)

  • +
  • atol (float)

  • +
  • equal_nan (bool)

  • +
+
+
Return type:
+

None

+
+
+
+
+
+
+class botorch.utils.testing.BaseTestProblemTestCaseMixIn[source]
+

Bases: object

+
+
+test_forward_and_evaluate_true()[source]
+
+
+
+abstract property functions: Sequence[BaseTestProblem]
+
+
+
+
+class botorch.utils.testing.SyntheticTestFunctionTestCaseMixin[source]
+

Bases: object

+
+
+test_optimal_value()[source]
+
+
+
+test_optimizer()[source]
+
+
+
+
+class botorch.utils.testing.MultiObjectiveTestProblemTestCaseMixin[source]
+

Bases: object

+
+
+test_attributes()[source]
+
+
+
+test_max_hv()[source]
+
+
+
+test_ref_point()[source]
+
+
+
+
+class botorch.utils.testing.ConstrainedTestProblemTestCaseMixin[source]
+

Bases: object

+
+
+test_num_constraints()[source]
+
+
+
+test_evaluate_slack()[source]
+
+
+
+
+class botorch.utils.testing.MockPosterior(mean=None, variance=None, samples=None, base_shape=None, batch_range=None)[source]
+

Bases: Posterior

+

Mock object that implements dummy methods and feeds through specified outputs

+
+
Parameters:
+
    +
  • mean – The mean of the posterior.

  • +
  • variance – The variance of the posterior.

  • +
  • samples – Samples to return from rsample, unless base_samples is +provided.

  • +
  • base_shape – If given, this is returned as base_sample_shape, and also +used as the base of the _extended_shape.

  • +
  • batch_range – If given, this is returned as batch_range. +Defaults to (0, -2).

  • +
+
+
+
+
+property device: device
+

The torch device of the distribution.

+
+
+
+property dtype: dtype
+

The torch dtype of the distribution.

+
+
+
+property batch_shape: Size
+
+
+
+property base_sample_shape: Size
+

The base shape of the base samples expected in rsample.

+

Informs the sampler to produce base samples of shape +sample_shape x base_sample_shape.

+
+
+
+property batch_range: tuple[int, int]
+

The t-batch range.

+

This is used in samplers to identify the t-batch component of the +base_sample_shape. The base samples are expanded over the t-batches to +provide consistency in the acquisition values, i.e., to ensure that a +candidate produces same value regardless of its position on the t-batch.

+
+
+
+property mean
+
+
+
+property variance
+
+
+
+rsample(sample_shape=None)[source]
+

Mock sample by repeating self._samples. If base_samples is provided, +do a shape check but return the same mock samples.

+
+
Parameters:
+

sample_shape (Size | None)

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample_from_base_samples(sample_shape, base_samples)[source]
+

Sample from the posterior (with gradients) using base samples.

+

This is intended to be used with a sampler that produces the corresponding base +samples, and enables acquisition optimization via Sample Average Approximation.

+
+
Parameters:
+
    +
  • sample_shape (Size) – A torch.Size object specifying the sample shape. To +draw n samples, set to torch.Size([n]). To draw b batches +of n samples each, set to torch.Size([b, n]).

  • +
  • base_samples (Tensor) – The base samples, obtained from the appropriate sampler. +This is a tensor of shape sample_shape x base_sample_shape.

  • +
+
+
Returns:
+

Samples from the posterior, a tensor of shape +self._extended_shape(sample_shape=sample_shape).

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.testing.MockModel(posterior)[source]
+

Bases: Model, FantasizeMixin

+

Mock object that implements dummy methods and feeds through specified outputs

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
Parameters:
+

posterior (MockPosterior)

+
+
+
+
+posterior(X, output_indices=None, posterior_transform=None, observation_noise=False)[source]
+

Computes the posterior over model outputs at the provided points.

+
+
Note: The input transforms should be applied here using

self.transform_inputs(X) after the self.eval() call and before +any model.forward or model.likelihood calls.

+
+
+
+
Parameters:
+
    +
  • X (Tensor) – A b x q x d-dim Tensor, where d is the dimension of the +feature space, q is the number of points considered jointly, +and b is the batch dimension.

  • +
  • output_indices (list[int] | None) – A list of indices, corresponding to the outputs over +which to compute the posterior (if the model is multi-output). +Can be used to speed up computation if only a subset of the +model’s outputs are required for optimization. If omitted, +computes the posterior over all model outputs.

  • +
  • observation_noise (bool | Tensor) – For models with an inferred noise level, if True, +include observation noise. For models with an observed noise level, +this must be a model_batch_shape x 1 x m-dim tensor or +a model_batch_shape x n’ x m-dim tensor containing the average +noise for each batch and output. noise must be in the +outcome-transformed space if an outcome transform is used.

  • +
  • posterior_transform (PosteriorTransform | None) – An optional PosteriorTransform.

  • +
+
+
Returns:
+

A Posterior object, representing a batch of b joint distributions +over q points and m outputs each.

+
+
Return type:
+

MockPosterior

+
+
+
+
+
+property num_outputs: int
+

The number of outputs of the model.

+
+
+
+property batch_shape: Size
+

The batch shape of the model.

+

This is a batch shape from an I/O perspective, independent of the internal +representation of the model (as e.g. in BatchedMultiOutputGPyTorchModel). +For a model with m outputs, a test_batch_shape x q x d-shaped input X +to the posterior method returns a Posterior object over an output of +shape broadcast(test_batch_shape, model.batch_shape) x q x m.

+
+
+
+state_dict(*args, **kwargs)[source]
+

Return a dictionary containing references to the whole state of the module.

+

Both parameters and persistent buffers (e.g. running averages) are +included. Keys are corresponding parameter and buffer names. +Parameters and buffers set to None are not included.

+
+

Note

+

The returned object is a shallow copy. It contains references +to the module’s parameters and buffers.

+
+
+

Warning

+

Currently state_dict() also accepts positional arguments for +destination, prefix and keep_vars in order. However, +this is being deprecated and keyword arguments will be enforced in +future releases.

+
+
+

Warning

+

Please avoid the use of argument destination as it is not +designed for end-users.

+
+
+
Parameters:
+
    +
  • destination (dict, optional) – If provided, the state of module will +be updated into the dict and the same object is returned. +Otherwise, an OrderedDict will be created and returned. +Default: None.

  • +
  • prefix (str, optional) – a prefix added to parameter and buffer +names to compose the keys in state_dict. Default: ''.

  • +
  • keep_vars (bool, optional) – by default the Tensor s +returned in the state dict are detached from autograd. If it’s +set to True, detaching will not be performed. +Default: False.

  • +
+
+
Returns:
+

a dictionary containing a whole state of the module

+
+
Return type:
+

dict

+
+
+

Example:

+
>>> # xdoctest: +SKIP("undefined vars")
+>>> module.state_dict().keys()
+['bias', 'weight']
+
+
+
+
+
+load_state_dict(state_dict=None, strict=False)[source]
+

Copy parameters and buffers from state_dict into this module and its descendants.

+

If strict is True, then +the keys of state_dict must exactly match the keys returned +by this module’s state_dict() function.

+
+

Warning

+

If assign is True the optimizer must be created after +the call to load_state_dict unless +get_swap_module_params_on_conversion() is True.

+
+
+
Parameters:
+
    +
  • state_dict (dict) – a dict containing parameters and +persistent buffers.

  • +
  • strict (bool, optional) – whether to strictly enforce that the keys +in state_dict match the keys returned by this module’s +state_dict() function. Default: True

  • +
  • assign (bool, optional) – When False, the properties of the tensors +in the current module are preserved while when True, the +properties of the Tensors in the state dict are preserved. The only +exception is the requires_grad field of +Default: ``False`

  • +
+
+
Returns:
+

    +
  • +
    missing_keys is a list of str containing any keys that are expected

    by this module but missing from the provided state_dict.

    +
    +
    +
  • +
  • +
    unexpected_keys is a list of str containing the keys that are not

    expected by this module but present in the provided state_dict.

    +
    +
    +
  • +
+

+
+
Return type:
+

NamedTuple with missing_keys and unexpected_keys fields

+
+
+
+

Note

+

If a parameter or buffer is registered as None and its corresponding key +exists in state_dict, load_state_dict() will raise a +RuntimeError.

+
+
+
+
+
+class botorch.utils.testing.MockAcquisitionFunction[source]
+

Bases: object

+

Mock acquisition function object that implements dummy methods.

+
+
+set_X_pending(X_pending=None)[source]
+
+
Parameters:
+

X_pending (Tensor | None)

+
+
+
+
+
+
+

Test Helpers

+

Dummy classes and other helpers that are used in multiple test files +should be defined here to avoid relative imports.

+
+
+botorch.utils.test_helpers.get_model(train_X, train_Y, standardize_model=False, use_model_list=False)[source]
+
+
Parameters:
+
    +
  • train_X (Tensor)

  • +
  • train_Y (Tensor)

  • +
  • standardize_model (bool)

  • +
  • use_model_list (bool)

  • +
+
+
Return type:
+

SingleTaskGP | ModelListGP

+
+
+
+
+
+botorch.utils.test_helpers.get_fully_bayesian_model(train_X, train_Y, num_models, standardize_model, infer_noise, **tkwargs)[source]
+
+
Parameters:
+
    +
  • train_X (Tensor)

  • +
  • train_Y (Tensor)

  • +
  • num_models (int)

  • +
  • standardize_model (bool)

  • +
  • infer_noise (bool)

  • +
  • tkwargs (Any)

  • +
+
+
Return type:
+

SaasFullyBayesianSingleTaskGP

+
+
+
+
+
+botorch.utils.test_helpers.get_fully_bayesian_model_list(train_X, train_Y, num_models, standardize_model, infer_noise, **tkwargs)[source]
+
+
Parameters:
+
    +
  • train_X (Tensor)

  • +
  • train_Y (Tensor)

  • +
  • num_models (int)

  • +
  • standardize_model (bool)

  • +
  • infer_noise (bool)

  • +
  • tkwargs (Any)

  • +
+
+
Return type:
+

ModelListGP

+
+
+
+
+
+botorch.utils.test_helpers.get_sample_moments(samples, sample_shape)[source]
+

Computes the mean and covariance of a set of samples.

+
+
Parameters:
+
    +
  • samples (Tensor) – A tensor of shape sample_shape x batch_shape x q.

  • +
  • sample_shape (Size) – The sample_shape input used while generating the samples using +the pathwise sampling API.

  • +
+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.utils.test_helpers.standardize_moments(transform, loc, covariance_matrix)[source]
+

Standardizes the loc and covariance_matrix using the mean and standard +deviations from a Standardize transform.

+
+
Parameters:
+
    +
  • transform (Standardize)

  • +
  • loc (Tensor)

  • +
  • covariance_matrix (Tensor)

  • +
+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.utils.test_helpers.gen_multi_task_dataset(yvar=None, task_values=None, skip_task_features_in_datasets=False, **tkwargs)[source]
+

Constructs a multi-task dataset with two tasks, each with 10 data points.

+
+
Parameters:
+
    +
  • yvar (float | None) – The noise level to use for train_Yvar. If None, uses train_Yvar=None.

  • +
  • task_values (list[int] | None) – The values of the task features. If None, uses [0, 1].

  • +
  • skip_task_features_in_datasets (bool) – If True, the task features are not included in +Xs of the datasets used to construct the datasets. This is useful for +testing MultiTaskDataset.

  • +
+
+
Return type:
+

tuple[MultiTaskDataset, tuple[Tensor, Tensor, Tensor | None]]

+
+
+
+
+
+botorch.utils.test_helpers.get_pvar_expected(posterior, model, X, m)[source]
+

Computes the expected variance of a posterior after adding the +predictive noise from the likelihood.

+
+
Parameters:
+
    +
  • posterior (TorchPosterior) – The posterior to compute the variance of. Must be a +TorchPosterior object.

  • +
  • model (Model) – The model that generated the posterior. If m > 1, this must be +a BatchedMultiOutputGPyTorchModel.

  • +
  • X (Tensor) – The test inputs.

  • +
  • m (int) – The number of outputs.

  • +
+
+
Returns:
+

The expected variance of the posterior after adding the observation +noise from the likelihood.

+
+
Return type:
+

Tensor

+
+
+
+
+
+class botorch.utils.test_helpers.DummyNonScalarizingPosteriorTransform(*args, **kwargs)[source]
+

Bases: PosteriorTransform

+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+
+
+scalarize = False
+
+
+
+evaluate(Y)[source]
+

Evaluate the transform on a set of outcomes.

+
+
Parameters:
+

Y – A batch_shape x q x m-dim tensor of outcomes.

+
+
Returns:
+

A batch_shape x q’ [x m’]-dim tensor of transformed outcomes.

+
+
+
+
+
+forward(posterior)[source]
+

Compute the transformed posterior.

+
+
Parameters:
+

posterior – The posterior to be transformed.

+
+
Returns:
+

The transformed posterior object.

+
+
+
+
+
+
+class botorch.utils.test_helpers.SimpleGPyTorchModel(train_X, train_Y, outcome_transform=None, input_transform=None)[source]
+

Bases: GPyTorchModel, ExactGP, FantasizeMixin

+
+
Parameters:
+
    +
  • train_X – A tensor of inputs, passed to self.transform_inputs.

  • +
  • train_Y – Passed to outcome_transform.

  • +
  • outcome_transform – Transform applied to train_Y.

  • +
  • input_transform – A Module that performs the input transformation, passed to +self.transform_inputs.

  • +
+
+
+
+
+last_fantasize_flag: bool = False
+
+
+
+forward(x)[source]
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+
+
+
+

Torch

+
+
+class botorch.utils.torch.BufferDict(buffers=None)[source]
+

Bases: Module

+

Holds buffers in a dictionary.

+

BufferDict can be indexed like a regular Python dictionary, but buffers it +contains are properly registered, and will be visible by all Module methods.

+

BufferDict is an ordered dictionary that respects

+
    +
  • the order of insertion, and

  • +
  • in update(), the order of the merged OrderedDict +or another BufferDict (the argument to +update()).

  • +
+

Note that update() with other unordered mapping +types (e.g., Python’s plain dict) does not preserve the order of the +merged mapping.

+
+
Parameters:
+

buffers (iterable, optional) – a mapping (dictionary) of +(string : Tensor) or an iterable of key-value pairs +of type (string, Tensor)

+
+
+

Example:

+
class MyModule(nn.Module):
+    def __init__(self):
+        super(MyModule, self).__init__()
+        self.buffers = nn.BufferDict({
+                'left': torch.randn(5, 10),
+                'right': torch.randn(5, 10)
+        })
+
+    def forward(self, x, choice):
+        x = self.buffers[choice].mm(x)
+        return x
+
+
+
+
Parameters:
+

buffers – A mapping (dictionary) from string to Tensor, or +an iterable of key-value pairs of type (string, Tensor).

+
+
+
+
+clear()[source]
+

Remove all items from the BufferDict.

+
+
+
+pop(key)[source]
+

Remove key from the BufferDict and return its buffer.

+
+
Parameters:
+

key (string) – key to pop from the BufferDict

+
+
+
+
+
+keys()[source]
+

Return an iterable of the BufferDict keys.

+
+
+
+items()[source]
+

Return an iterable of the BufferDict key/value pairs.

+
+
+
+values()[source]
+

Return an iterable of the BufferDict values.

+
+
+
+update(buffers)[source]
+

Update the BufferDict with the key-value pairs from a +mapping or an iterable, overwriting existing keys.

+
+

Note

+

If buffers is an OrderedDict, a BufferDict, +or an iterable of key-value pairs, the order of new elements in it is +preserved.

+
+
+
Parameters:
+

buffers (iterable) – a mapping (dictionary) from string to +Tensor, or an iterable of +key-value pairs of type (string, Tensor)

+
+
+
+
+
+extra_repr()[source]
+

Set the extra representation of the module.

+

To print customized extra information, you should re-implement +this method in your own modules. Both single-line and multi-line +strings are acceptable.

+
+
+
+
+

Transformations

+

Some basic data transformation helpers.

+
+
+botorch.utils.transforms.standardize(Y)[source]
+

Standardizes (zero mean, unit variance) a tensor by dim=-2.

+

If the tensor is single-dimensional, simply standardizes the tensor. +If for some batch index all elements are equal (or if there is only a single +data point), this function will return 0 for that batch index.

+
+
Parameters:
+

Y (Tensor) – A batch_shape x n x m-dim tensor.

+
+
Returns:
+

The standardized Y.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> Y = torch.rand(4, 3)
+>>> Y_standardized = standardize(Y)
+
+
+
+
+
+botorch.utils.transforms.normalize(X, bounds)[source]
+

Min-max normalize X w.r.t. the provided bounds.

+

NOTE: If the upper and lower bounds are identical for a dimension, that dimension +will not be scaled. Such dimensions will only be shifted as +new_X[…, i] = X[…, i] - bounds[0, i]. This avoids division by zero issues.

+
+
Parameters:
+
    +
  • X (Tensor) – … x d tensor of data

  • +
  • bounds (Tensor) – 2 x d tensor of lower and upper bounds for each of the X’s d +columns.

  • +
+
+
Returns:
+

+
A … x d-dim tensor of normalized data, given by

(X - bounds[0]) / (bounds[1] - bounds[0]). If all elements of X +are contained within bounds, the normalized values will be +contained within [0, 1]^d.

+
+
+

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> X = torch.rand(4, 3)
+>>> bounds = torch.stack([torch.zeros(3), 0.5 * torch.ones(3)])
+>>> X_normalized = normalize(X, bounds)
+
+
+
+
+
+botorch.utils.transforms.unnormalize(X, bounds)[source]
+

Un-normalizes X w.r.t. the provided bounds.

+

NOTE: If the upper and lower bounds are identical for a dimension, that dimension +will not be scaled. Such dimensions will only be shifted as +new_X[…, i] = X[…, i] + bounds[0, i], matching the behavior of normalize.

+
+
Parameters:
+
    +
  • X (Tensor) – … x d tensor of data

  • +
  • bounds (Tensor) – 2 x d tensor of lower and upper bounds for each of the X’s d +columns.

  • +
+
+
Returns:
+

+
A … x d-dim tensor of unnormalized data, given by

X * (bounds[1] - bounds[0]) + bounds[0]. If all elements of X +are contained in [0, 1]^d, the un-normalized values will be +contained within bounds.

+
+
+

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> X_normalized = torch.rand(4, 3)
+>>> bounds = torch.stack([torch.zeros(3), 0.5 * torch.ones(3)])
+>>> X = unnormalize(X_normalized, bounds)
+
+
+
+
+
+botorch.utils.transforms.normalize_indices(indices, d)[source]
+

Normalize a list of indices to ensure that they are positive.

+
+
Parameters:
+
    +
  • indices (list[int] | None) – A list of indices (may contain negative indices for indexing +“from the back”).

  • +
  • d (int) – The dimension of the tensor to index.

  • +
+
+
Returns:
+

A normalized list of indices such that each index is between 0 and +d-1, or None if indices is None.

+
+
Return type:
+

list[int] | None

+
+
+
+
+
+botorch.utils.transforms.is_fully_bayesian(model)[source]
+

Check if at least one model is a fully Bayesian model.

+
+
Parameters:
+

model (Model) – A BoTorch model (may be a ModelList or ModelListGP)

+
+
Returns:
+

True if at least one model is a fully Bayesian model.

+
+
Return type:
+

bool

+
+
+
+
+
+botorch.utils.transforms.is_ensemble(model)[source]
+

Check if at least one model is an ensemble model.

+
+
Parameters:
+

model (Model) – A BoTorch model (may be a ModelList or ModelListGP)

+
+
Returns:
+

True if at least one model is an ensemble model.

+
+
Return type:
+

bool

+
+
+
+
+
+botorch.utils.transforms.t_batch_mode_transform(expected_q=None, assert_output_shape=True)[source]
+

Factory for decorators enabling consistent t-batch behavior.

+

This method creates decorators for instance methods to transform an input tensor +X to t-batch mode (i.e. with at least 3 dimensions). This assumes the tensor +has a q-batch dimension. The decorator also checks the q-batch size if expected_q +is provided, and the output shape if assert_output_shape is True.

+
+
Parameters:
+
    +
  • expected_q (int | None) – The expected q-batch size of X. If specified, this will raise an +AssertionError if X’s q-batch size does not equal expected_q.

  • +
  • assert_output_shape (bool) – If True, this will raise an AssertionError if the +output shape does not match either the t-batch shape of X, +or the acqf.model.batch_shape for acquisition functions using +batched models.

  • +
+
+
Returns:
+

The decorated instance method.

+
+
Return type:
+

Callable[[Callable[[AcquisitionFunction, Any], Any]], Callable[[AcquisitionFunction, Any], Any]]

+
+
+

Example

+
>>> class ExampleClass:
+>>>     @t_batch_mode_transform(expected_q=1)
+>>>     def single_q_method(self, X):
+>>>         ...
+>>>
+>>>     @t_batch_mode_transform()
+>>>     def arbitrary_q_method(self, X):
+>>>         ...
+
+
+
+
+
+botorch.utils.transforms.concatenate_pending_points(method)[source]
+

Decorator concatenating X_pending into an acquisition function’s argument.

+

This decorator works on the forward method of acquisition functions taking +a tensor X as the argument. If the acquisition function has an X_pending +attribute (that is not None), this is concatenated into the input X, +appropriately expanding the pending points to match the batch shape of X.

+

Example

+
>>> class ExampleAcquisitionFunction:
+>>>     @concatenate_pending_points
+>>>     @t_batch_mode_transform()
+>>>     def forward(self, X):
+>>>         ...
+
+
+
+
Parameters:
+

method (Callable[[Any, Tensor], Any])

+
+
Return type:
+

Callable[[Any, Tensor], Any]

+
+
+
+
+
+botorch.utils.transforms.match_batch_shape(X, Y)[source]
+

Matches the batch dimension of a tensor to that of another tensor.

+
+
Parameters:
+
    +
  • X (Tensor) – A batch_shape_X x q x d tensor, whose batch dimensions that +correspond to batch dimensions of Y are to be matched to those +(if compatible).

  • +
  • Y (Tensor) – A batch_shape_Y x q’ x d tensor.

  • +
+
+
Returns:
+

A batch_shape_Y x q x d tensor containing the data of X expanded to +the batch dimensions of Y (if compatible). For instance, if X is +b’’ x b’ x q x d and Y is b x q x d, then the returned tensor is +b’’ x b x q x d.

+
+
Return type:
+

Tensor

+
+
+

Example

+
>>> X = torch.rand(2, 1, 5, 3)
+>>> Y = torch.rand(2, 6, 4, 3)
+>>> X_matched = match_batch_shape(X, Y)
+>>> X_matched.shape
+torch.Size([2, 6, 5, 3])
+
+
+
+
+
+botorch.utils.transforms.convert_to_target_pre_hook(module, *args)[source]
+

Pre-hook for automatically calling .to(X) on module prior to forward

+
+
+
+

Feasible Volume

+
+
+botorch.utils.feasible_volume.get_feasible_samples(samples, inequality_constraints=None)[source]
+

Checks which of the samples satisfy all of the inequality constraints.

+
+
Parameters:
+
    +
  • samples (Tensor) – A sample size x d size tensor of feature samples, +where d is a feature dimension.

  • +
  • constraints (inequality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
+
+
Returns:
+

2-element tuple containing

+
    +
  • Samples satisfying the linear constraints.

  • +
  • Estimated proportion of samples satisfying the linear constraints.

  • +
+

+
+
Return type:
+

tuple[Tensor, float]

+
+
+
+
+
+botorch.utils.feasible_volume.get_outcome_feasibility_probability(model, X, outcome_constraints, threshold=0.1, nsample_outcome=1000, seed=None)[source]
+

Monte Carlo estimate of the feasible volume with respect to the outcome constraints.

+
+
Parameters:
+
    +
  • model (Model) – The model used for sampling the posterior.

  • +
  • X (Tensor) – A tensor of dimension batch-shape x 1 x d, where d is feature dimension.

  • +
  • outcome_constraints (list[Callable[[Tensor], Tensor]]) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply feasibility.

  • +
  • threshold (float) – A lower limit for the probability of posterior samples feasibility.

  • +
  • nsample_outcome (int) – The number of samples from the model posterior.

  • +
  • seed (int | None) – The seed for the posterior sampler. If omitted, use a random seed.

  • +
+
+
Returns:
+

Estimated proportion of features for which posterior samples satisfy +given outcome constraints with probability above or equal to +the given threshold.

+
+
Return type:
+

float

+
+
+
+
+
+botorch.utils.feasible_volume.estimate_feasible_volume(bounds, model, outcome_constraints, inequality_constraints=None, nsample_feature=1000, nsample_outcome=1000, threshold=0.1, verbose=False, seed=None, device=None, dtype=None)[source]
+

Monte Carlo estimate of the feasible volume with respect +to feature constraints and outcome constraints.

+
+
Parameters:
+
    +
  • bounds (Tensor) – A 2 x d tensor of lower and upper bounds +for each column of X.

  • +
  • model (Model) – The model used for sampling the outcomes.

  • +
  • outcome_constraints (list[Callable[[Tensor], Tensor]]) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility.

  • +
  • constraints (inequality) – A list of tuples (indices, coefficients, rhs), +with each tuple encoding an inequality constraint of the form +sum_i (X[indices[i]] * coefficients[i]) >= rhs.

  • +
  • nsample_feature (int) – The number of feature samples satisfying the bounds.

  • +
  • nsample_outcome (int) – The number of outcome samples from the model posterior.

  • +
  • threshold (float) – A lower limit for the probability of outcome feasibility

  • +
  • seed (int | None) – The seed for both feature and outcome samplers. If omitted, +use a random seed.

  • +
  • verbose (bool) – An indicator for whether to log the results.

  • +
  • inequality_constraints (list[tuple[Tensor, Tensor, float]] | None)

  • +
  • device (device | None)

  • +
  • dtype (dtype | None)

  • +
+
+
Returns:
+

    +
  • +
    Estimated proportion of volume in feature space that is

    feasible wrt the bounds and the inequality constraints (linear).

    +
    +
    +
  • +
  • +
    Estimated proportion of feasible features for which

    posterior samples (outcome) satisfies the outcome constraints +with probability above the given threshold.

    +
    +
    +
  • +
+

+
+
Return type:
+

2-element tuple containing

+
+
+
+
+
+

Types and Type Hints

+
+
+class botorch.utils.types.DEFAULT
+

Bases: object

+
+
+
+

Constants

+
+
+botorch.utils.constants.get_constants(values, device=None, dtype=None)[source]
+

Returns scalar-valued Tensors containing each of the given constants. +Used to expedite tensor operations involving scalar arithmetic. Note that +the returned Tensors should not be modified in-place.

+
+
Parameters:
+
    +
  • values (Number | Iterator[Number])

  • +
  • device (device | None)

  • +
  • dtype (dtype | None)

  • +
+
+
Return type:
+

Tensor | tuple[Tensor, …]

+
+
+
+
+
+botorch.utils.constants.get_constants_like(values, ref)[source]
+
+
Parameters:
+
    +
  • values (Number | Iterator[Number])

  • +
  • ref (Tensor)

  • +
+
+
Return type:
+

Tensor | Iterator[Tensor]

+
+
+
+
+
+

Safe Math

+

Special implementations of mathematical functions that +solve numerical issues of naive implementations.

+
+
+[Maechler2012accurate] +(1,2) +
    +
  1. +
    Mächler. Accurately Computing log (1 - exp (-| a|))

    Assessed by the Rmpfr package. Technical report, 2012.

    +
    +
    +
  2. +
+
+
+
+
+botorch.utils.safe_math.exp(x, **kwargs)[source]
+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.log(x, **kwargs)[source]
+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.add(a, b, **kwargs)[source]
+
+
Parameters:
+
    +
  • a (Tensor)

  • +
  • b (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.sub(a, b)[source]
+
+
Parameters:
+
    +
  • a (Tensor)

  • +
  • b (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.div(a, b)[source]
+
+
Parameters:
+
    +
  • a (Tensor)

  • +
  • b (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.mul(a, b)[source]
+
+
Parameters:
+
    +
  • a (Tensor)

  • +
  • b (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.log1mexp(x)[source]
+

Numerically accurate evaluation of log(1 - exp(x)) for x < 0. +See [Maechler2012accurate] for details.

+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.log1pexp(x)[source]
+

Numerically accurate evaluation of log(1 + exp(x)). +See [Maechler2012accurate] for details.

+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.logexpit(X)[source]
+

Computes the logarithm of the expit (a.k.a. sigmoid) function.

+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.logplusexp(a, b)[source]
+

Computes log(exp(a) + exp(b)) similar to logsumexp.

+
+
Parameters:
+
    +
  • a (Tensor)

  • +
  • b (Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.logdiffexp(log_a, log_b)[source]
+

Computes log(b - a) accurately given log(a) and log(b). +Assumes, log_b > log_a, i.e. b > a > 0.

+
+
Parameters:
+
    +
  • log_a (Tensor) – The logarithm of a, assumed to be less than log_b.

  • +
  • log_b (Tensor) – The logarithm of b, assumed to be larger than log_a.

  • +
+
+
Returns:
+

A Tensor of values corresponding to log(b - a).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.logsumexp(x, dim, keepdim=False)[source]
+

Version of logsumexp that has a well-behaved backward pass when +x contains infinities.

+

In particular, the gradient of the standard torch version becomes NaN +1) for any element that is positive infinity, and 2) for any slice that +only contains negative infinities.

+

This version returns a gradient of 1 for any positive infinities in case 1, and +for all elements of the slice in case 2, in agreement with the asymptotic behavior +of the function.

+
+
Parameters:
+
    +
  • x (Tensor) – The Tensor to which to apply logsumexp.

  • +
  • dim (int | tuple[int, ...]) – An integer or a tuple of integers, representing the dimensions to reduce.

  • +
  • keepdim (bool) – Whether to keep the reduced dimensions. Defaults to False.

  • +
+
+
Returns:
+

A Tensor representing the log of the summed exponentials of x.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.logmeanexp(X, dim, keepdim=False)[source]
+

Computes log(mean(exp(X), dim=dim, keepdim=keepdim)).

+
+
Parameters:
+
    +
  • X (Tensor) – Values of which to compute the logmeanexp.

  • +
  • dim (int | tuple[int, ...]) – The dimension(s) over which to compute the mean.

  • +
  • keepdim (bool) – If True, keeps the reduced dimensions.

  • +
+
+
Returns:
+

A Tensor of values corresponding to log(mean(exp(X), dim=dim)).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.log_softplus(x, tau=1.0)[source]
+

Computes the logarithm of the softplus function with high numerical accuracy.

+
+
Parameters:
+
    +
  • x (Tensor) – Input tensor, should have single or double precision floats.

  • +
  • tau (float | Tensor) – Decreasing tau increases the tightness of the +approximation to ReLU. Non-negative and defaults to 1.0.

  • +
+
+
Returns:
+

Tensor corresponding to log(softplus(x)).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.smooth_amax(X, dim=-1, keepdim=False, tau=1.0)[source]
+

Computes a smooth approximation to max(X, dim=dim), i.e the maximum value of +X over dimension dim, using the logarithm of the l_(1/tau) norm of exp(X). +Note that when X = log(U) is the logarithm of an acquisition utility U,

+

logsumexp(log(U) / tau) * tau = log(sum(U^(1/tau))^tau) = log(norm(U, ord=(1/tau))

+
+
Parameters:
+
    +
  • X (Tensor) – A Tensor from which to compute the smoothed amax.

  • +
  • dim (int | tuple[int, ...]) – The dimensions to reduce over.

  • +
  • keepdim (bool) – If True, keeps the reduced dimensions.

  • +
  • tau (float | Tensor) – Temperature parameter controlling the smooth approximation +to max operator, becomes tighter as tau goes to 0. Needs to be positive.

  • +
+
+
Returns:
+

A Tensor of smooth approximations to max(X, dim=dim).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.smooth_amin(X, dim=-1, keepdim=False, tau=1.0)[source]
+

A smooth approximation to min(X, dim=dim), similar to smooth_amax.

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • dim (int | tuple[int, ...])

  • +
  • keepdim (bool)

  • +
  • tau (float | Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.check_dtype_float32_or_float64(X)[source]
+
+
Parameters:
+

X (Tensor)

+
+
Return type:
+

None

+
+
+
+
+
+botorch.utils.safe_math.log_fatplus(x, tau=1.0)[source]
+

Computes the logarithm of the fat-tailed softplus.

+

NOTE: Separated out in case the complexity of the log implementation increases +in the future.

+
+
Parameters:
+
    +
  • x (Tensor)

  • +
  • tau (float | Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.fatplus(x, tau=1.0)[source]
+

Computes a fat-tailed approximation to ReLU(x) = max(x, 0) by linearly +combining a regular softplus function and the density function of a Cauchy +distribution. The coefficient alpha of the Cauchy density is chosen to guarantee +monotonicity and convexity.

+
+
Parameters:
+
    +
  • x (Tensor) – A Tensor on whose values to compute the smoothed function.

  • +
  • tau (float | Tensor) – Temperature parameter controlling the smoothness of the approximation.

  • +
+
+
Returns:
+

A Tensor of values of the fat-tailed softplus.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.fatmax(x, dim, keepdim=False, tau=1.0, alpha=2.0)[source]
+

Computes a smooth approximation to amax(X, dim=dim) with a fat tail.

+
+
Parameters:
+
    +
  • X – A Tensor from which to compute the smoothed maximum.

  • +
  • dim (int | tuple[int, ...]) – The dimensions to reduce over.

  • +
  • keepdim (bool) – If True, keeps the reduced dimensions.

  • +
  • tau (float | Tensor) – Temperature parameter controlling the smooth approximation +to max operator, becomes tighter as tau goes to 0. Needs to be positive.

  • +
  • alpha (float) – The exponent of the asymptotic power decay of the approximation. The +default value is 2. Higher alpha parameters make the function behave more +similarly to the standard logsumexp approximation to the max, so it is +recommended to keep this value low or moderate, e.g. < 10.

  • +
  • x (Tensor)

  • +
+
+
Returns:
+

A Tensor of smooth approximations to amax(X, dim=dim) with a fat tail.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.fatmin(x, dim, keepdim=False, tau=1.0, alpha=2.0)[source]
+

Computes a smooth approximation to amin(X, dim=dim) with a fat tail.

+
+
Parameters:
+
    +
  • X – A Tensor from which to compute the smoothed minimum.

  • +
  • dim (int | tuple[int, ...]) – The dimensions to reduce over.

  • +
  • keepdim (bool) – If True, keeps the reduced dimensions.

  • +
  • tau (float | Tensor) – Temperature parameter controlling the smooth approximation +to min operator, becomes tighter as tau goes to 0. Needs to be positive.

  • +
  • alpha (float) – The exponent of the asymptotic power decay of the approximation. The +default value is 2. Higher alpha parameters make the function behave more +similarly to the standard logsumexp approximation to the max, so it is +recommended to keep this value low or moderate, e.g. < 10.

  • +
  • x (Tensor)

  • +
+
+
Returns:
+

A Tensor of smooth approximations to amin(X, dim=dim) with a fat tail.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.fatmaximum(a, b, tau=1.0, alpha=2.0)[source]
+

Computes a smooth approximation to torch.maximum(a, b) with a fat tail.

+
+
Parameters:
+
    +
  • a (Tensor) – The first Tensor from which to compute the smoothed component-wise maximum.

  • +
  • b (Tensor) – The second Tensor from which to compute the smoothed component-wise maximum.

  • +
  • tau (float | Tensor) – Temperature parameter controlling the smoothness of the approximation. A +smaller tau corresponds to a tighter approximation that leads to a sharper +objective landscape that might be more difficult to optimize.

  • +
  • alpha (float)

  • +
+
+
Returns:
+

A smooth approximation of torch.maximum(a, b).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.fatminimum(a, b, tau=1.0, alpha=2.0)[source]
+

Computes a smooth approximation to torch.minimum(a, b) with a fat tail.

+
+
Parameters:
+
    +
  • a (Tensor) – The first Tensor from which to compute the smoothed component-wise minimum.

  • +
  • b (Tensor) – The second Tensor from which to compute the smoothed component-wise minimum.

  • +
  • tau (float | Tensor) – Temperature parameter controlling the smoothness of the approximation. A +smaller tau corresponds to a tighter approximation that leads to a sharper +objective landscape that might be more difficult to optimize.

  • +
  • alpha (float)

  • +
+
+
Returns:
+

A smooth approximation of torch.minimum(a, b).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.log_fatmoid(X, tau=1.0)[source]
+

Computes the logarithm of the fatmoid. Separated out in case the implementation +of the logarithm becomes more complex in the future to ensure numerical stability.

+
+
Parameters:
+
    +
  • X (Tensor)

  • +
  • tau (float | Tensor)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.fatmoid(X, tau=1.0)[source]
+

Computes a twice continuously differentiable approximation to the Heaviside +step function with a fat tail, i.e. O(1 / x^2) as x goes to -inf.

+
+
Parameters:
+
    +
  • X (Tensor) – A Tensor from which to compute the smoothed step function.

  • +
  • tau (float | Tensor) – Temperature parameter controlling the smoothness of the approximation.

  • +
+
+
Returns:
+

A tensor of fat-tailed approximations to the Heaviside step function.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.cauchy(x)[source]
+

Computes a Lorentzian, i.e. an un-normalized Cauchy density function.

+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.safe_math.sigmoid(X, log=False, fat=False)[source]
+

A sigmoid function with an optional fat tail and evaluation in log space for +better numerical behavior. Notably, the fat-tailed sigmoid can be used to remedy +numerical underflow problems in the value and gradient of the canonical sigmoid.

+
+
Parameters:
+
    +
  • X (Tensor) – The Tensor on which to evaluate the sigmoid.

  • +
  • log (bool) – Toggles the evaluation of the log sigmoid.

  • +
  • fat (bool) – Toggles the evaluation of the fat-tailed sigmoid.

  • +
+
+
Returns:
+

A Tensor of (log-)sigmoid values.

+
+
Return type:
+

Tensor

+
+
+
+
+

Multi-Objective Utilities

+
+
+
+

Abstract Box Decompositions

+

Box decomposition algorithms.

+

References

+
+
+[Lacour17] +(1,2,3,4,5,6) +

R. Lacour, K. Klamroth, C. Fonseca. A box decomposition algorithm to +compute the hypervolume indicator. Computers & Operations Research, +Volume 79, 2017.

+
+
+
+
+class botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition(ref_point, sort, Y=None)[source]
+

Bases: Module, ABC

+

An abstract class for box decompositions.

+

Note: Internally, we store the negative reference point (minimization).

+

Initialize BoxDecomposition.

+
+
Parameters:
+
    +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
  • sort (bool) – A boolean indicating whether to sort the Pareto frontier.

  • +
  • Y (Tensor | None) – A (batch_shape) x n x m-dim tensor of outcomes.

  • +
+
+
+
+
+property pareto_Y: Tensor
+

This returns the non-dominated set.

+
+
Returns:
+

A n_pareto x m-dim tensor of outcomes.

+
+
+
+
+
+property ref_point: Tensor
+

Get the reference point.

+
+
Returns:
+

A m-dim tensor of outcomes.

+
+
+
+
+
+property Y: Tensor
+

Get the raw outcomes.

+
+
Returns:
+

A n x m-dim tensor of outcomes.

+
+
+
+
+
+partition_space()[source]
+

Compute box decomposition.

+
+
Return type:
+

None

+
+
+
+
+
+abstract get_hypercell_bounds()[source]
+

Get the bounds of each hypercell in the decomposition.

+
+
Returns:
+

+
A 2 x num_cells x num_outcomes-dim tensor containing the

lower and upper vertices bounding each hypercell.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+update(Y)[source]
+

Update non-dominated front and decomposition.

+

By default, the partitioning is recomputed. Subclasses can override +this functionality.

+
+
Parameters:
+

Y (Tensor) – A (batch_shape) x n x m-dim tensor of new, incremental outcomes.

+
+
Return type:
+

None

+
+
+
+
+
+reset()[source]
+

Reset non-dominated front and decomposition.

+
+
Return type:
+

None

+
+
+
+
+
+compute_hypervolume()[source]
+

Compute hypervolume that is dominated by the Pareto Froniter.

+
+
Returns:
+

+
A (batch_shape)-dim tensor containing the hypervolume dominated by

each Pareto frontier.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.multi_objective.box_decompositions.box_decomposition.FastPartitioning(ref_point, Y=None)[source]
+

Bases: BoxDecomposition, ABC

+

A class for partitioning the (non-)dominated space into hyper-cells.

+

Note: this assumes maximization. Internally, it multiplies outcomes by -1 +and performs the decomposition under minimization.

+

This class is abstract to support to two applications of Alg 1 from +[Lacour17]: 1) partitioning the space that is dominated by the Pareto +frontier and 2) partitioning the space that is not dominated by the +Pareto frontier.

+
+
Parameters:
+
    +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
  • Y (Tensor | None) – A (batch_shape) x n x m-dim tensor

  • +
+
+
+
+
+update(Y)[source]
+

Update non-dominated front and decomposition.

+
+
Parameters:
+

Y (Tensor) – A (batch_shape) x n x m-dim tensor of new, incremental outcomes.

+
+
Return type:
+

None

+
+
+
+
+
+partition_space()[source]
+

Compute box decomposition.

+
+
Return type:
+

None

+
+
+
+
+
+get_hypercell_bounds()[source]
+

Get the bounds of each hypercell in the decomposition.

+
+
Returns:
+

+
A 2 x (batch_shape) x num_cells x m-dim tensor containing the

lower and upper vertices bounding each hypercell.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Box Decomposition List

+

Box decomposition container.

+
+
+class botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList(*box_decompositions)[source]
+

Bases: Module

+

A list of box decompositions.

+

Initialize the box decomposition list.

+
+
Parameters:
+

*box_decompositions (BoxDecomposition) – An variable number of box decompositions

+
+
+

Example

+
>>> bd1 = FastNondominatedPartitioning(ref_point, Y=Y1)
+>>> bd2 = FastNondominatedPartitioning(ref_point, Y=Y2)
+>>> bd = BoxDecompositionList(bd1, bd2)
+
+
+
+
+property pareto_Y: list[Tensor]
+

This returns the non-dominated set.

+

Note: Internally, we store the negative pareto set (minimization).

+
+
Returns:
+

+
A list where the ith element is the n_pareto_i x m-dim tensor

of pareto optimal outcomes for each box_decomposition i.

+
+
+

+
+
+
+
+
+property ref_point: Tensor
+

Get the reference point.

+

Note: Internally, we store the negative reference point (minimization).

+
+
Returns:
+

A n_box_decompositions x m-dim tensor of outcomes.

+
+
+
+
+
+get_hypercell_bounds()[source]
+

Get the bounds of each hypercell in the decomposition.

+
+
Returns:
+

+
A 2 x n_box_decompositions x num_cells x num_outcomes-dim tensor

containing the lower and upper vertices bounding each hypercell.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+update(Y)[source]
+

Update the partitioning.

+
+
Parameters:
+

Y (list[Tensor] | Tensor) – A n_box_decompositions x n x num_outcomes-dim tensor or a list +where the ith element contains the new points for +box_decomposition i.

+
+
Return type:
+

None

+
+
+
+
+
+compute_hypervolume()[source]
+

Compute hypervolume that is dominated by the Pareto Froniter.

+
+
Returns:
+

+
A (batch_shape)-dim tensor containing the hypervolume dominated by

each Pareto frontier.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+

Box Decomposition Utilities

+

Utilities for box decomposition algorithms.

+
+
+botorch.utils.multi_objective.box_decompositions.utils.compute_local_upper_bounds(U, Z, z)[source]
+

Compute local upper bounds.

+

Note: this assumes minimization.

+

This uses the incremental algorithm (Alg. 1) from [Lacour17].

+
+
Parameters:
+
    +
  • U (Tensor) – A n x m-dim tensor containing the local upper bounds.

  • +
  • Z (Tensor) – A n x m x m-dim tensor containing the defining points.

  • +
  • z (Tensor) – A m-dim tensor containing the new point.

  • +
+
+
Returns:
+

    +
  • A new n’ x m-dim tensor local upper bounds.

  • +
  • A n’ x m x m-dim tensor containing the defining points.

  • +
+

+
+
Return type:
+

2-element tuple containing

+
+
+
+
+
+botorch.utils.multi_objective.box_decompositions.utils.get_partition_bounds(Z, U, ref_point)[source]
+

Get the cell bounds given the local upper bounds and the defining points.

+

This implements Equation 2 in [Lacour17].

+
+
Parameters:
+
    +
  • Z (Tensor) – A n x m x m-dim tensor containing the defining points. The first +dimension corresponds to u_idx, the second dimension corresponds to j, +and Z[u_idx, j] is the set of definining points Z^j(u) where +u = U[u_idx].

  • +
  • U (Tensor) – A n x m-dim tensor containing the local upper bounds.

  • +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
+
+
Returns:
+

+
A 2 x num_cells x m-dim tensor containing the lower and upper vertices

bounding each hypercell.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.multi_objective.box_decompositions.utils.update_local_upper_bounds_incremental(new_pareto_Y, U, Z)[source]
+

Update the current local upper with the new pareto points.

+

This assumes minimization.

+
+
Parameters:
+
    +
  • new_pareto_Y (Tensor) – A n x m-dim tensor containing the new +Pareto points.

  • +
  • U (Tensor) – A n’ x m-dim tensor containing the local upper bounds.

  • +
  • Z (Tensor) – A n x m x m-dim tensor containing the defining points.

  • +
+
+
Returns:
+

    +
  • A new n’ x m-dim tensor local upper bounds.

  • +
  • A n’ x m x m-dim tensor containing the defining points

  • +
+

+
+
Return type:
+

2-element tuple containing

+
+
+
+
+
+botorch.utils.multi_objective.box_decompositions.utils.compute_non_dominated_hypercell_bounds_2d(pareto_Y_sorted, ref_point)[source]
+

Compute an axis-aligned partitioning of the non-dominated space for 2 +objectives.

+
+
Parameters:
+
    +
  • pareto_Y_sorted (Tensor) – A (batch_shape) x n_pareto x 2-dim tensor of pareto outcomes +that are sorted by the 0th dimension in increasing order. All points must be +better than the reference point.

  • +
  • ref_point (Tensor) – A (batch_shape) x 2-dim reference point.

  • +
+
+
Returns:
+

A 2 x (batch_shape) x n_pareto + 1 x m-dim tensor of cell bounds.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.multi_objective.box_decompositions.utils.compute_dominated_hypercell_bounds_2d(pareto_Y_sorted, ref_point)[source]
+

Compute an axis-aligned partitioning of the dominated space for 2-objectives.

+
+
Parameters:
+
    +
  • pareto_Y_sorted (Tensor) – A (batch_shape) x n_pareto x 2-dim tensor of pareto outcomes +that are sorted by the 0th dimension in increasing order.

  • +
  • ref_point (Tensor) – A 2-dim reference point.

  • +
+
+
Returns:
+

A 2 x (batch_shape) x n_pareto x m-dim tensor of cell bounds.

+
+
Return type:
+

Tensor

+
+
+
+
+
+

Dominated Partitionings

+

Algorithms for partitioning the dominated space into hyperrectangles.

+
+
+class botorch.utils.multi_objective.box_decompositions.dominated.DominatedPartitioning(ref_point, Y=None)[source]
+

Bases: FastPartitioning

+

Partition dominated space into axis-aligned hyperrectangles.

+

This uses the Algorithm 1 from [Lacour17].

+

Example

+
>>> bd = DominatedPartitioning(ref_point, Y)
+
+
+
+
Parameters:
+
    +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
  • Y (Tensor | None) – A (batch_shape) x n x m-dim tensor

  • +
+
+
+
+
+
+

Hypervolume

+

Hypervolume Utilities.

+

References

+
+
+[Fonseca2006] +(1,2) +

C. M. Fonseca, L. Paquete, and M. Lopez-Ibanez. An improved dimension-sweep +algorithm for the hypervolume indicator. In IEEE Congress on Evolutionary +Computation, pages 1157-1163, Vancouver, Canada, July 2006.

+
+
+[Ishibuchi2011] +

H. Ishibuchi, N. Akedo, and Y. Nojima. A many-objective test problem +for visually examining diversity maintenance behavior in a decision +space. Proc. 13th Annual Conf. Genetic Evol. Comput., 2011.

+
+
+
+
+botorch.utils.multi_objective.hypervolume.infer_reference_point(pareto_Y, max_ref_point=None, scale=0.1, scale_max_ref_point=False)[source]
+

Get reference point for hypervolume computations.

+

This sets the reference point to be ref_point = nadir - scale * range +when there is no pareto_Y that is better than max_ref_point. +If there’s pareto_Y better than max_ref_point, the reference point +will be set to max_ref_point - scale * range if scale_max_ref_point +is true and to max_ref_point otherwise.

+

[Ishibuchi2011] find 0.1 to be a robust multiplier for scaling the +nadir point.

+

Note: this assumes maximization of all objectives.

+
+
Parameters:
+
    +
  • pareto_Y (Tensor) – A n x m-dim tensor of Pareto-optimal points.

  • +
  • max_ref_point (Tensor | None) – A m dim tensor indicating the maximum reference point. +Some elements can be NaN, except when pareto_Y is empty, +in which case these dimensions will be treated as if no +max_ref_point was provided and set to nadir - scale * range.

  • +
  • scale (float) – A multiplier used to scale back the reference point based on the +range of each objective.

  • +
  • scale_max_ref_point (bool) – A boolean indicating whether to apply scaling to +the max_ref_point based on the range of each objective.

  • +
+
+
Returns:
+

A m-dim tensor containing the reference point.

+
+
Return type:
+

Tensor

+
+
+
+
+
+class botorch.utils.multi_objective.hypervolume.Hypervolume(ref_point)[source]
+

Bases: object

+

Hypervolume computation dimension sweep algorithm from [Fonseca2006].

+

Adapted from Simon Wessing’s implementation of the algorithm +(Variant 3, Version 1.2) in [Fonseca2006] in PyMOO: +https://github.com/msu-coinlab/pymoo/blob/master/pymoo/vendor/hv.py

+

Maximization is assumed.

+

TODO: write this in C++ for faster looping.

+

Initialize hypervolume object.

+
+
Parameters:
+

ref_point (Tensor) – m-dim Tensor containing the reference point.

+
+
+
+
+property ref_point: Tensor
+

Get reference point (for maximization).

+
+
Returns:
+

A m-dim tensor containing the reference point.

+
+
+
+
+
+compute(pareto_Y)[source]
+

Compute hypervolume.

+
+
Parameters:
+

pareto_Y (Tensor) – A n x m-dim tensor of pareto optimal outcomes

+
+
Returns:
+

The hypervolume.

+
+
Return type:
+

float

+
+
+
+
+
+
+botorch.utils.multi_objective.hypervolume.sort_by_dimension(nodes, i)[source]
+

Sorts the list of nodes in-place by the specified objective.

+
+
Parameters:
+
    +
  • nodes (list[Node]) – A list of Nodes

  • +
  • i (int) – The index of the objective to sort by

  • +
+
+
Return type:
+

None

+
+
+
+
+
+class botorch.utils.multi_objective.hypervolume.Node(m, dtype, device, data=None)[source]
+

Bases: object

+

Node in the MultiList data structure.

+

Initialize MultiList.

+
+
Parameters:
+
    +
  • m (int) – The number of objectives

  • +
  • dtype (torch.dtype) – The dtype

  • +
  • device (torch.device) – The device

  • +
  • data (Tensor | None) – The tensor data to be stored in this Node.

  • +
+
+
+
+
+
+class botorch.utils.multi_objective.hypervolume.MultiList(m, dtype, device)[source]
+

Bases: object

+

A special data structure used in hypervolume computation.

+

It consists of several doubly linked lists that share common nodes. +Every node has multiple predecessors and successors, one in every list.

+

Initialize m doubly linked lists.

+
+
Parameters:
+
    +
  • m (int) – number of doubly linked lists

  • +
  • dtype (torch.dtype) – the dtype

  • +
  • device (torch.device) – the device

  • +
+
+
+
+
+append(node, index)[source]
+

Appends a node to the end of the list at the given index.

+
+
Parameters:
+
    +
  • node (Node) – the new node

  • +
  • index (int) – the index where the node should be appended.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+extend(nodes, index)[source]
+

Extends the list at the given index with the nodes.

+
+
Parameters:
+
    +
  • nodes (list[Node]) – list of nodes to append at the given index.

  • +
  • index (int) – the index where the nodes should be appended.

  • +
+
+
Return type:
+

None

+
+
+
+
+
+remove(node, index, bounds)[source]
+

Removes and returns ‘node’ from all lists in [0, ‘index’].

+
+
Parameters:
+
    +
  • node (Node) – The node to remove

  • +
  • index (int) – The upper bound on the range of indices

  • +
  • bounds (Tensor) – A 2 x m-dim tensor bounds on the objectives

  • +
+
+
Return type:
+

Node

+
+
+
+
+
+reinsert(node, index, bounds)[source]
+

Re-inserts the node at its original position.

+

Re-inserts the node at its original position in all lists in [0, ‘index’] +before it was removed. This method assumes that the next and previous +nodes of the node that is reinserted are in the list.

+
+
Parameters:
+
    +
  • node (Node) – The node

  • +
  • index (int) – The upper bound on the range of indices

  • +
  • bounds (Tensor) – A 2 x m-dim tensor bounds on the objectives

  • +
+
+
Return type:
+

None

+
+
+
+
+
+
+class botorch.utils.multi_objective.hypervolume.SubsetIndexCachingMixin[source]
+

Bases: object

+

A Mixin class that adds q-subset index computations and caching.

+

Initializes the class with q_out = -1 and an empty q_subset_indices dict.

+
+
+compute_q_subset_indices(q_out, device)[source]
+

Returns and caches a dict of indices equal to subsets of {1, …, q_out}.

+

This means that consecutive calls to self.compute_q_subset_indices with +the same q_out do not recompute the indices for all (2^q_out - 1) subsets.

+

NOTE: This will use more memory than regenerating the indices +for each i and then deleting them, but it will be faster for +repeated evaluations (e.g. during optimization).

+
+
Parameters:
+
    +
  • q_out (int) – The batch size of the objectives. This is typically equal +to the q-batch size of X. However, if using a set valued +objective (e.g., MVaR) that produces s objective values for +each point on the q-batch of X, we need to properly account +for each objective while calculating the hypervolume contributions +by using q_out = q * s.

  • +
  • device (torch.device)

  • +
+
+
Returns:
+

A dict that maps “q choose i” to all size-i subsets of {1, …, q_out}.

+
+
Return type:
+

BufferDict[str, Tensor]

+
+
+
+
+
+
+botorch.utils.multi_objective.hypervolume.compute_subset_indices(q, device=None)[source]
+

Compute all (2^q - 1) distinct subsets of {1, …, q}.

+
+
Parameters:
+
    +
  • q (int) – An integer defininig the set {1, …, q} whose subsets to compute.

  • +
  • device (torch.device | None)

  • +
+
+
Returns:
+

A dict that maps “q choose i” to all size-i subsets of {1, …, q_out}.

+
+
Return type:
+

BufferDict[str, Tensor]

+
+
+
+
+
+class botorch.utils.multi_objective.hypervolume.NoisyExpectedHypervolumeMixin(model, ref_point, X_baseline, sampler=None, objective=None, constraints=None, X_pending=None, prune_baseline=False, alpha=0.0, cache_pending=True, max_iep=0, incremental_nehvi=True, cache_root=True, marginalize_dim=None)[source]
+

Bases: CachedCholeskyMCSamplerMixin

+

Initialize a mixin that contains functions for the batched Pareto-frontier +partitioning used by the noisy hypervolume-improvement-based acquisition +functions, i.e. qNEHVI and qLogNEHVI.

+
+
Parameters:
+
    +
  • model (Model) – A fitted model.

  • +
  • ref_point (list[float] | Tensor) – A list or tensor with m elements representing the reference +point (in the outcome space) w.r.t. to which compute the hypervolume. +This is a reference point for the objective values (i.e. after +applying objective to the samples).

  • +
  • X_baseline (Tensor) – A r x d-dim Tensor of r design points that have already +been observed. These points are considered as potential approximate +pareto-optimal design points.

  • +
  • sampler (MCSampler | None) – The sampler used to draw base samples. If not given, +a sampler is generated using get_sampler. NOTE: A box decomposition is +of the Pareto front is created for each MC sample, an operation that +scales as O(n^m) and thus becomes particularly costly for m > 2.

  • +
  • objective (MCMultiOutputObjective | None) – The MCMultiOutputObjective under which the samples are +evaluated. Defaults to IdentityMCMultiOutputObjective().

  • +
  • constraints (list[Callable[[Tensor], Tensor]] | None) – A list of callables, each mapping a Tensor of dimension +sample_shape x batch-shape x q x m to a Tensor of dimension +sample_shape x batch-shape x q, where negative values imply +feasibility. The acqusition function will compute expected feasible +hypervolume.

  • +
  • X_pending (Tensor | None) – A batch_shape x m x d-dim Tensor of m design points that +have points that have been submitted for function evaluation, but +have not yet been evaluated.

  • +
  • prune_baseline (bool) – If True, remove points in X_baseline that are +highly unlikely to be the pareto optimal and better than the +reference point. This can significantly improve computation time and +is generally recommended. In order to customize pruning parameters, +instead manually call prune_inferior_points_multi_objective on +X_baseline before instantiating the acquisition function.

  • +
  • alpha (float) – The hyperparameter controlling the approximate non-dominated +partitioning. The default value of 0.0 means an exact partitioning +is used. As the number of objectives m increases, consider increasing +this parameter in order to limit computational complexity.

  • +
  • cache_pending (bool) – A boolean indicating whether to use cached box +decompositions (CBD) for handling pending points. This is +generally recommended.

  • +
  • max_iep (int) – The maximum number of pending points before the box +decompositions will be recomputed.

  • +
  • incremental_nehvi (bool) – A boolean indicating whether to compute the +incremental NEHVI from the i`th point where `i=1, …, q +under sequential greedy optimization, or the full qNEHVI over +q points.

  • +
  • cache_root (bool) – A boolean indicating whether to cache the root +decomposition over X_baseline and use low-rank updates.

  • +
  • marginalize_dim (int | None) – A batch dimension that should be marginalized. For example, +this is useful when using a batched fully Bayesian model.

  • +
+
+
+
+
+property X_baseline: Tensor
+

Return X_baseline augmented with pending points cached using CBD.

+
+
+
+set_X_pending(X_pending=None)[source]
+

Informs the acquisition function about pending design points.

+
+
Parameters:
+

X_pending (Tensor | None) – n x d Tensor with n d-dim design points that have +been submitted for evaluation but have not yet been evaluated.

+
+
Return type:
+

None

+
+
+
+
+
+
+

Non-dominated Partitionings

+

Algorithms for partitioning the non-dominated space into rectangles.

+

References

+
+
+[Couckuyt2012] +(1,2) +

I. Couckuyt, D. Deschrijver and T. Dhaene, “Towards Efficient +Multiobjective Optimization: Multiobjective statistical criterions,” +2012 IEEE Congress on Evolutionary Computation, Brisbane, QLD, 2012, +pp. 1-8.

+
+
+
+
+class botorch.utils.multi_objective.box_decompositions.non_dominated.NondominatedPartitioning(ref_point, Y=None, alpha=0.0)[source]
+

Bases: BoxDecomposition

+

A class for partitioning the non-dominated space into hyper-cells.

+

Note: this assumes maximization. Internally, it multiplies outcomes by -1 and +performs the decomposition under minimization. TODO: use maximization +internally as well.

+

Note: it is only feasible to use this algorithm to compute an exact +decomposition of the non-dominated space for m<5 objectives (alpha=0.0).

+

The alpha parameter can be increased to obtain an approximate partitioning +faster. The alpha is a fraction of the total hypervolume encapsuling the +entire Pareto set. When a hypercell’s volume divided by the total hypervolume +is less than alpha, we discard the hypercell. See Figure 2 in +[Couckuyt2012] for a visual representation.

+

This PyTorch implementation of the binary partitioning algorithm ([Couckuyt2012]) +is adapted from numpy/tensorflow implementation at: +https://github.com/GPflow/GPflowOpt/blob/master/gpflowopt/pareto.py.

+

TODO: replace this with a more efficient decomposition. E.g. +https://link.springer.com/content/pdf/10.1007/s10898-019-00798-7.pdf

+

Initialize NondominatedPartitioning.

+
+
Parameters:
+
    +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
  • Y (Tensor | None) – A (batch_shape) x n x m-dim tensor.

  • +
  • alpha (float) – A thresold fraction of total volume used in an approximate +decomposition.

  • +
+
+
+

Example

+
>>> bd = NondominatedPartitioning(ref_point, Y=Y1)
+
+
+
+
+get_hypercell_bounds()[source]
+

Get the bounds of each hypercell in the decomposition.

+
+
Parameters:
+

ref_point – A (batch_shape) x m-dim tensor containing the reference point.

+
+
Returns:
+

+
A 2 x num_cells x m-dim tensor containing the

lower and upper vertices bounding each hypercell.

+
+
+

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+class botorch.utils.multi_objective.box_decompositions.non_dominated.FastNondominatedPartitioning(ref_point, Y=None)[source]
+

Bases: FastPartitioning

+

A class for partitioning the non-dominated space into hyper-cells.

+

Note: this assumes maximization. Internally, it multiplies by -1 and performs +the decomposition under minimization.

+

This class is far more efficient than NondominatedPartitioning for exact box +partitionings

+
+
This class uses the two-step approach similar to that in [Yang2019], where:
    +
  1. +
    first, Alg 1 from [Lacour17] is used to find the local lower bounds

    for the maximization problem

    +
    +
    +
  2. +
  3. +
    second, the local lower bounds are used as the Pareto frontier for the

    minimization problem, and [Lacour17] is applied again to partition +the space dominated by that Pareto frontier.

    +
    +
    +
  4. +
+
+
+

Initialize FastNondominatedPartitioning.

+
+
Parameters:
+
    +
  • ref_point (Tensor) – A m-dim tensor containing the reference point.

  • +
  • Y (Tensor | None) – A (batch_shape) x n x m-dim tensor.

  • +
+
+
+

Example

+
>>> bd = FastNondominatedPartitioning(ref_point, Y=Y1)
+
+
+
+
+
+

Pareto

+
+
+botorch.utils.multi_objective.pareto.is_non_dominated(Y, maximize=True, deduplicate=True)[source]
+

Computes the non-dominated front.

+

Note: this assumes maximization.

+

For small n, this method uses a highly parallel methodology +that compares all pairs of points in Y. However, this is memory +intensive and slow for large n. For large n (or if Y is larger +than 5MB), this method will dispatch to a loop-based approach +that is faster and has a lower memory footprint.

+
+
Parameters:
+
    +
  • Y (Tensor) – A (batch_shape) x n x m-dim tensor of outcomes. +If any element of Y is NaN, the corresponding point +will be treated as a dominated point (returning False).

  • +
  • maximize (bool) – If True, assume maximization (default).

  • +
  • deduplicate (bool) – A boolean indicating whether to only return +unique points on the pareto frontier.

  • +
+
+
Returns:
+

A (batch_shape) x n-dim boolean tensor indicating whether +each point is non-dominated.

+
+
Return type:
+

Tensor

+
+
+
+
+
+

Scalarization

+

Helper utilities for constructing scalarizations.

+

References

+
+
+[Knowles2005] +(1,2) +

J. Knowles, “ParEGO: a hybrid algorithm with on-line landscape approximation +for expensive multiobjective optimization problems,” in IEEE Transactions +on Evolutionary Computation, vol. 10, no. 1, pp. 50-66, Feb. 2006.

+
+
+
+
+botorch.utils.multi_objective.scalarization.get_chebyshev_scalarization(weights, Y, alpha=0.05)[source]
+

Construct an augmented Chebyshev scalarization.

+
+
The augmented Chebyshev scalarization is given by

g(y) = max_i(w_i * y_i) + alpha * sum_i(w_i * y_i)

+
+
+

where the goal is to minimize g(y) in the setting where all objectives y_i are +to be minimized. Since the default in BoTorch is to maximize all objectives, +this method constructs a Chebyshev scalarization where the inputs are first +multiplied by -1, so that all objectives are to be minimized. Then, it computes +g(y) (which should be minimized), and returns -g(y), which should be maximized.

+

Minimizing an objective is supported by passing a negative +weight for that objective. To make all w * y’s have the same sign +such that they are comparable when computing max(w * y), outcomes of minimization +objectives are shifted from [0,1] to [-1,0].

+

See [Knowles2005] for details.

+

This scalarization can be used with qExpectedImprovement to implement q-ParEGO +as proposed in [Daulton2020qehvi].

+
+
Parameters:
+
    +
  • weights (Tensor) – A m-dim tensor of weights. +Positive for maximization and negative for minimization.

  • +
  • Y (Tensor) – A n x m-dim tensor of observed outcomes, which are used for +scaling the outcomes to [0,1] or [-1,0]. If n=0, then outcomes +are left unnormalized.

  • +
  • alpha (float) – Parameter governing the influence of the weighted sum term. The +default value comes from [Knowles2005].

  • +
+
+
Returns:
+

Transform function using the objective weights.

+
+
Return type:
+

Callable[[Tensor, Tensor | None], Tensor]

+
+
+

Example

+
>>> weights = torch.tensor([0.75, -0.25])
+>>> transform = get_aug_chebyshev_scalarization(weights, Y)
+
+
+
+
+

Probability Utilities

+
+
+
+

Multivariate Gaussian Probabilities via Bivariate Conditioning

+

Bivariate conditioning algorithm for approximating Gaussian probabilities, +see [Genz2016numerical] and [Trinh2015bivariate].

+
+
+[Trinh2015bivariate] +(1,2) +

G. Trinh and A. Genz. Bivariate conditioning approximations for +multivariate normal probabilities. Statistics and Computing, 2015.

+
+
+[Genz2016numerical] +

A. Genz and G. Tring. Numerical Computation of Multivariate Normal Probabilities +using Bivariate Conditioning. Monte Carlo and Quasi-Monte Carlo Methods, 2016.

+
+
+[Gibson1994monte] +

GJ. Gibson, CA Galsbey, and DA Elston. Monte Carlo evaluation of multivariate normal +integrals and sensitivity to variate ordering. Advances in Numerical Methods and +Applications. 1994.

+
+
+
+
+class botorch.utils.probability.mvnxpb.mvnxpbState[source]
+

Bases: TypedDict

+
+
+step: int
+
+
+
+perm: LongTensor
+
+
+
+bounds: Tensor
+
+
+
+piv_chol: PivotedCholesky
+
+
+
+plug_ins: Tensor
+
+
+
+log_prob: Tensor
+
+
+
+log_prob_extra: Tensor | None
+
+
+
+
+class botorch.utils.probability.mvnxpb.MVNXPB(covariance_matrix, bounds)[source]
+

Bases: object

+

An algorithm for approximating Gaussian probabilities P(X in bounds), where +X ~ N(0, covariance_matrix).

+

Initializes an MVNXPB instance.

+
+
Parameters:
+
    +
  • covariance_matrix (Tensor) – Covariance matrices of shape batch_shape x [n, n].

  • +
  • bounds (Tensor) – Tensor of lower and upper bounds, batch_shape x [n, 2]. These +bounds are standardized internally and clipped to STANDARDIZED_RANGE.

  • +
+
+
+
+
+classmethod build(step, perm, bounds, piv_chol, plug_ins, log_prob, log_prob_extra=None)[source]
+

Creates an MVNXPB instance from raw arguments. Unlike MVNXPB.__init__, +this methods does not preprocess or copy terms.

+
+
Parameters:
+
    +
  • step (int) – Integer used to track the solver’s progress.

  • +
  • bounds (Tensor) – Tensor of lower and upper bounds, batch_shape x [n, 2].

  • +
  • piv_chol (PivotedCholesky) – A PivotedCholesky instance for the system.

  • +
  • plug_ins (Tensor) – Tensor of plug-in estimators used to update lower and upper bounds +on random variables that have yet to be integrated out.

  • +
  • log_prob (Tensor) – Tensor of log probabilities.

  • +
  • log_prob_extra (Tensor | None) – Tensor of conditional log probabilities for the next random +variable. Used when integrating over an odd number of random variables.

  • +
  • perm (Tensor)

  • +
+
+
Return type:
+

MVNXPB

+
+
+
+
+
+solve(num_steps=None, eps=1e-10)[source]
+

Runs the MVNXPB solver instance for a fixed number of steps.

+

Calculates a bivariate conditional approximation to P(X in bounds), where +X ~ N(0, Σ). For details, see [Genz2016numerical] or [Trinh2015bivariate].

+
+
Parameters:
+
    +
  • num_steps (int | None)

  • +
  • eps (float)

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+select_pivot()[source]
+

GGE variable prioritization strategy from [Gibson1994monte].

+

Returns the index of the random variable least likely to satisfy its bounds +when conditioning on the previously integrated random variables X[:t - 1] +attaining the values of plug-in estimators y[:t - 1]. Equivalently, +` +argmin_{i = t, ..., n} P(X[i] \in bounds[i] | X[:t-1] = y[:t -1]), +` +where t denotes the current step.

+
+
Return type:
+

LongTensor | None

+
+
+
+
+
+pivot_(pivot)[source]
+

Swap random variables at pivot and step positions.

+
+
Parameters:
+

pivot (LongTensor)

+
+
Return type:
+

None

+
+
+
+
+
+concat(other, dim)[source]
+
+
Parameters:
+
+
+
Return type:
+

MVNXPB

+
+
+
+
+
+expand(*sizes)[source]
+
+
Parameters:
+

sizes (int)

+
+
Return type:
+

MVNXPB

+
+
+
+
+
+augment(covariance_matrix, bounds, cross_covariance_matrix, disable_pivoting=False, jitter=None, max_tries=None)[source]
+

Augment an n-dimensional MVNXPB instance to include m additional random +variables.

+
+
Parameters:
+
    +
  • covariance_matrix (Tensor)

  • +
  • bounds (Tensor)

  • +
  • cross_covariance_matrix (Tensor)

  • +
  • disable_pivoting (bool)

  • +
  • jitter (float | None)

  • +
  • max_tries (int | None)

  • +
+
+
Return type:
+

MVNXPB

+
+
+
+
+
+detach()[source]
+
+
Return type:
+

MVNXPB

+
+
+
+
+
+clone()[source]
+
+
Return type:
+

MVNXPB

+
+
+
+
+
+asdict()[source]
+
+
Return type:
+

mvnxpbState

+
+
+
+
+
+
+

Truncated Multivariate Normal Distribution

+
+
+class botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal(loc, covariance_matrix=None, precision_matrix=None, scale_tril=None, bounds=None, solver=None, sampler=None, validate_args=None)[source]
+

Bases: MultivariateNormal

+

Initializes an instance of a TruncatedMultivariateNormal distribution.

+

Let x ~ N(0, K) be an n-dimensional Gaussian random vector. This class +represents the distribution of the truncated Multivariate normal random vector +x | a <= x <= b.

+
+
Parameters:
+
    +
  • loc (Tensor) – A mean vector for the distribution, batch_shape x event_shape.

  • +
  • covariance_matrix (Tensor | None) – Covariance matrix distribution parameter.

  • +
  • precision_matrix (Tensor | None) – Inverse covariance matrix distribution parameter.

  • +
  • scale_tril (Tensor | None) – Lower triangular, square-root covariance matrix distribution +parameter.

  • +
  • bounds (Tensor) – A batch_shape x event_shape x 2 tensor of strictly increasing +bounds for x so that bounds[…, 0] < bounds[…, 1] everywhere.

  • +
  • solver (MVNXPB | None) – A pre-solved MVNXPB instance used to approximate the log partition.

  • +
  • sampler (LinearEllipticalSliceSampler | None) – A LinearEllipticalSliceSampler instance used for sample generation.

  • +
  • validate_args (bool | None) – Optional argument to super().__init__.

  • +
+
+
+
+
+log_prob(value)[source]
+

Approximates the true log probability.

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample(sample_shape=())[source]
+

Draw samples from the Truncated Multivariate Normal.

+
+
Parameters:
+

sample_shape (Size) – The shape of the samples.

+
+
Returns:
+

The (sample_shape x batch_shape x event_shape) tensor of samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+property log_partition: Tensor
+
+
+
+property solver: MVNXPB
+
+
+
+property sampler: LinearEllipticalSliceSampler
+
+
+
+expand(batch_shape, _instance=None)[source]
+

Returns a new distribution instance (or populates an existing instance +provided by a derived class) with batch dimensions expanded to +batch_shape. This method calls expand on +the distribution’s parameters. As such, this does not allocate new +memory for the expanded distribution instance. Additionally, +this does not repeat any args checking or parameter broadcasting in +__init__.py, when an instance is first created.

+
+
Parameters:
+
    +
  • batch_shape (torch.Size) – the desired expanded size.

  • +
  • _instance (TruncatedMultivariateNormal) – new instance provided by subclasses that +need to override .expand.

  • +
+
+
Returns:
+

New distribution instance with batch dimensions expanded to +batch_size.

+
+
Return type:
+

TruncatedMultivariateNormal

+
+
+
+
+
+
+

Unified Skew Normal Distribution

+
+
+class botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal(trunc, gauss, cross_covariance_matrix, validate_args=None)[source]
+

Bases: Distribution

+

Unified Skew Normal distribution of Y | a < X < b for jointly Gaussian +random vectors X ∈ R^m and Y ∈ R^n.

+

Batch shapes trunc.batch_shape and gauss.batch_shape must be broadcastable. +Care should be taken when choosing trunc.batch_shape. When trunc is of lower +batch dimensionality than gauss, the user should consider expanding trunc to +hasten UnifiedSkewNormal.log_prob. In these cases, it is suggested that the +user invoke trunc.solver before calling trunc.expand to avoid paying for +multiple, identical solves.

+
+
Parameters:
+
    +
  • trunc (TruncatedMultivariateNormal) – Distribution of Z = (X | a < X < b) ∈ R^m.

  • +
  • gauss (MultivariateNormal) – Distribution of Y ∈ R^n.

  • +
  • cross_covariance_matrix (Tensor | LinearOperator) – Cross-covariance Cov(X, Y) ∈ R^{m x n}.

  • +
  • validate_args (bool | None) – Optional argument to super().__init__.

  • +
+
+
+
+
+arg_constraints = {}
+
+
+
+log_prob(value)[source]
+

Computes the log probability ln p(Y = value | a < X < b).

+
+
Parameters:
+

value (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+rsample(sample_shape=())[source]
+

Draw samples from the Unified Skew Normal.

+
+
Parameters:
+

sample_shape (Size) – The shape of the samples.

+
+
Returns:
+

The (sample_shape x batch_shape x event_shape) tensor of samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+expand(batch_shape, _instance=None)[source]
+

Returns a new distribution instance (or populates an existing instance +provided by a derived class) with batch dimensions expanded to +batch_shape. This method calls expand on +the distribution’s parameters. As such, this does not allocate new +memory for the expanded distribution instance. Additionally, +this does not repeat any args checking or parameter broadcasting in +__init__.py, when an instance is first created.

+
+
Parameters:
+
    +
  • batch_shape (torch.Size) – the desired expanded size.

  • +
  • _instance (UnifiedSkewNormal) – new instance provided by subclasses that +need to override .expand.

  • +
+
+
Returns:
+

New distribution instance with batch dimensions expanded to +batch_size.

+
+
Return type:
+

UnifiedSkewNormal

+
+
+
+
+
+property covariance_matrix: Tensor
+
+
+
+property scale_tril: Tensor
+
+
+
+
+

Bivariate Normal Probabilities and Statistics

+

Methods for computing bivariate normal probabilities and statistics.

+
+
+[Genz2004bvnt] +(1,2,3) +

A. Genz. Numerical computation of rectangular bivariate and trivariate normal and +t probabilities. Statistics and Computing, 2004.

+
+
+[Muthen1990moments] +

B. Muthen. Moments of the censored and truncated bivariate normal distribution. +British Journal of Mathematical and Statistical Psychology, 1990.

+
+
+
+
+botorch.utils.probability.bvn.bvn(r, xl, yl, xu, yu)[source]
+

A function for computing bivariate normal probabilities.

+

Calculates P(xl < x < xu, yl < y < yu) where x and y are bivariate normal with +unit variance and correlation coefficient r. See Section 2.4 of [Genz2004bvnt].

+

This method uses a sign flip trick to improve numerical performance. Many of bvnu`s +internal branches rely on evaluations `Phi(-bound). For a < b < 0, the term +Phi(-a) - Phi(-b) goes to zero faster than Phi(b) - Phi(a) because +finfo(dtype).epsneg is typically much larger than finfo(dtype).tiny. In these +cases, flipping the sign can prevent situations where bvnu(…) - bvnu(…) would +otherwise be zero due to round-off error.

+
+
Parameters:
+
    +
  • r (Tensor) – Tensor of correlation coefficients.

  • +
  • xl (Tensor) – Tensor of lower bounds for x, same shape as r.

  • +
  • yl (Tensor) – Tensor of lower bounds for y, same shape as r.

  • +
  • xu (Tensor) – Tensor of upper bounds for x, same shape as r.

  • +
  • yu (Tensor) – Tensor of upper bounds for y, same shape as r.

  • +
+
+
Returns:
+

Tensor of probabilities P(xl < x < xu, yl < y < yu).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.bvn.bvnu(r, h, k)[source]
+

Solves for P(x > h, y > k) where x and y are standard bivariate normal +random variables with correlation coefficient r. In [Genz2004bvnt], this is (1)

+
+

L(h, k, r) = P(x < -h, y < -k) = 1/(a 2pi) int_{h}^{infty} int_{k}^{infty} f(x, y, r) dy dx,

+
+

where f(x, y, r) = e^{-1/(2a^2) (x^2 - 2rxy + y^2)} and a = (1 - r^2)^{1/2}.

+

[Genz2004bvnt] report the following integation scheme incurs a maximum of 5e-16 +error when run in double precision: if |r| >= 0.925, use a 20-point quadrature +rule on a 5th order Taylor expansion; else, numerically integrate in polar +coordinates using no more than 20 quadrature points.

+
+
Parameters:
+
    +
  • r (Tensor) – Tensor of correlation coefficients.

  • +
  • h (Tensor) – Tensor of negative upper bounds for x, same shape as r.

  • +
  • k (Tensor) – Tensor of negative upper bounds for y, same shape as r.

  • +
+
+
Returns:
+

A tensor of probabilities P(x > h, y > k).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.bvn.bvnmom(r, xl, yl, xu, yu, p=None)[source]
+

Computes the expected values of truncated, bivariate normal random variables.

+

Let x and y be a pair of standard bivariate normal random variables having +correlation r. This function computes E([x,y] | [xl,yl] < [x,y] < [xu,yu]).

+

Following [Muthen1990moments] equations (4) and (5), we have

+
+

E(x | [xl, yl] < [x, y] < [xu, yu]) = Z^{-1} phi(xl) P(yl < y < yu | x=xl) - phi(xu) P(yl < y < yu | x=xu),

+
+

where Z = P([xl, yl] < [x, y] < [xu, yu]) and phi is the standard normal PDF.

+
+
Parameters:
+
    +
  • r (Tensor) – Tensor of correlation coefficients.

  • +
  • xl (Tensor) – Tensor of lower bounds for x, same shape as r.

  • +
  • xu (Tensor) – Tensor of upper bounds for x, same shape as r.

  • +
  • yl (Tensor) – Tensor of lower bounds for y, same shape as r.

  • +
  • yu (Tensor) – Tensor of upper bounds for y, same shape as r.

  • +
  • p (Tensor | None) – Tensor of probabilities P(xl < x < xu, yl < y < yu), same shape as r.

  • +
+
+
Returns:
+

E(x | [xl, yl] < [x, y] < [xu, yu]) and +E(y | [xl, yl] < [x, y] < [xu, yu]).

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+

Elliptic Slice Sampler with Linear Constraints

+

Linear Elliptical Slice Sampler.

+

References

+
+
+[Gessner2020] +

A. Gessner, O. Kanjilal, and P. Hennig. Integrals over gaussians under +linear domain constraints. AISTATS 2020.

+
+
+[Wu2024] +

K. Wu, and J. Gardner. A Fast, Robust Elliptical Slice Sampling Implementation for +Linearly Truncated Multivariate Normal Distributions. arXiv:2407.10449. 2024.

+
+
+

This implementation is based (with multiple changes / optimiations) on +the following implementations based on the algorithm in [Gessner2020]: +- https://github.com/alpiges/LinConGauss +- https://github.com/wjmaddox/pytorch_ess

+

In addition, the active intervals (from which the angle is sampled) are computed using +the improved algorithm described in [Wu2024]: +https://github.com/kayween/linear-ess

+

The implementation here differentiates itself from the original implementations with: +1) Support for fixed feature equality constraints. +2) Support for non-standard Normal distributions. +3) Numerical stability improvements, especially relevant for high-dimensional cases. +4) Support multiple Markov chains running in parallel.

+
+
+class botorch.utils.probability.lin_ess.LinearEllipticalSliceSampler(inequality_constraints=None, bounds=None, interior_point=None, fixed_indices=None, mean=None, covariance_matrix=None, covariance_root=None, check_feasibility=False, burnin=0, thinning=0, num_chains=1)[source]
+

Bases: PolytopeSampler

+

Linear Elliptical Slice Sampler.

+

Ideas: +- Optimize computations if possible, potentially with torch.compile. +- Extend fixed features constraint to general linear equality constraints.

+

Initialize LinearEllipticalSliceSampler.

+
+
Parameters:
+
    +
  • inequality_constraints (tuple[Tensor, Tensor] | None) – Tensors (A, b) describing inequality constraints +A @ x <= b, where A is an n_ineq_con x d-dim Tensor and b is +an n_ineq_con x 1-dim Tensor, with n_ineq_con the number of +inequalities and d the dimension of the sample space. If omitted, +must provide bounds instead.

  • +
  • bounds (Tensor | None) – A 2 x d-dim tensor of box bounds. If omitted, must provide +inequality_constraints instead.

  • +
  • interior_point (Tensor | None) – A d x 1-dim Tensor presenting a point in the (relative) +interior of the polytope. If omitted, an interior point is determined +automatically by solving a Linear Program. Note: It is crucial that +the point lie in the interior of the feasible set (rather than on the +boundary), otherwise the sampler will produce invalid samples.

  • +
  • fixed_indices (list[int] | Tensor | None) – Integer list or d-dim Tensor representing the indices of +dimensions that are constrained to be fixed to the values specified in +the interior_point, which is required to be passed in conjunction with +fixed_indices.

  • +
  • mean (Tensor | None) – The d x 1-dim mean of the MVN distribution (if omitted, use zero).

  • +
  • covariance_matrix (Tensor | LinearOperator | None) – The d x d-dim covariance matrix of the MVN +distribution (if omitted, use the identity).

  • +
  • covariance_root (Tensor | LinearOperator | None) – A d x d-dim root of the covariance matrix such that +covariance_root @ covariance_root.T = covariance_matrix. NOTE: This +matrix is assumed to be lower triangular. covariance_root can only be +passed in conjunction with fixed_indices if covariance_root is a +DiagLinearOperator. Otherwise the factorization would need to be re- +computed, as we need to solve in standardize.

  • +
  • check_feasibility (bool) – If True, raise an error if the sampling results in an +infeasible sample. This creates some overhead and so is switched off +by default.

  • +
  • burnin (int) – Number of samples to generate upon initialization to warm up the +sampler.

  • +
  • thinning (int) – Number of samples to skip before returning a sample in draw.

  • +
  • num_chains (int) – Number of Markov chains to run in parallel.

  • +
+
+
+

This sampler samples from a multivariante Normal N(mean, covariance_matrix) +subject to linear domain constraints A x <= b (intersected with box bounds, +if provided).

+
+
+property lifetime_samples: int
+

The total number of samples generated by the sampler during its lifetime.

+
+
+
+draw(n=1)[source]
+

Draw samples.

+
+
Parameters:
+

n (int) – The number of samples.

+
+
Returns:
+

A (n * num_chains) x d-dim tensor of n * num_chains samples.

+
+
Return type:
+

Tensor

+
+
+
+
+
+step()[source]
+

Take a step, return the new sample, update the internal state.

+
+
Returns:
+

A d x num_chains-dim tensor, where each column is a sample from a Markov +chain.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+botorch.utils.probability.lin_ess.get_index_tensors(fixed_indices, d)[source]
+

Converts fixed_indices to a d-dim integral Tensor that is True at indices +that are contained in fixed_indices and False otherwise.

+
+
Parameters:
+
    +
  • fixed_indices (list[int] | Tensor) – A list or Tensoro of integer indices to fix.

  • +
  • d (int) – The dimensionality of the Tensors to be indexed.

  • +
+
+
Returns:
+

A Tuple of integral Tensors partitioning [1, d] into indices that are fixed +(first tensor) and non-fixed (second tensor).

+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+

Linear Algebra Helpers

+
+
+botorch.utils.probability.linalg.block_matrix_concat(blocks)[source]
+
+
Parameters:
+

blocks (Sequence[Sequence[Tensor]])

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.linalg.augment_cholesky(Laa, Kbb, Kba=None, Lba=None, jitter=None)[source]
+

Computes the Cholesky factor of a block matrix K = [[Kaa, Kab], [Kba, Kbb]] +based on a precomputed Cholesky factor Kaa = Laa Laa^T.

+
+
Parameters:
+
    +
  • Laa (Tensor) – Cholesky factor of K’s upper left block.

  • +
  • Kbb (Tensor) – Lower-right block of K.

  • +
  • Kba (Tensor | None) – Lower-left block of K.

  • +
  • Lba (Tensor | None) – Precomputed solve Kba Laa^{-T}.

  • +
  • jitter (float | None) – Optional nugget to be added to the diagonal of Kbb.

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+class botorch.utils.probability.linalg.PivotedCholesky(step: 'int', tril: 'Tensor', perm: 'LongTensor', diag: 'Tensor | None' = None, validate_init: 'InitVar[bool]' = True)[source]
+

Bases: object

+
+
Parameters:
+
    +
  • step (int)

  • +
  • tril (Tensor)

  • +
  • perm (LongTensor)

  • +
  • diag (Tensor | None)

  • +
  • validate_init (dataclasses.InitVar[bool])

  • +
+
+
+
+
+step: int
+
+
+
+tril: Tensor
+
+
+
+perm: LongTensor
+
+
+
+diag: Tensor | None = None
+
+
+
+validate_init: dataclasses.InitVar[bool] = True
+
+
+
+update_(eps=1e-10)[source]
+

Performs a single matrix decomposition step.

+
+
Parameters:
+

eps (float)

+
+
Return type:
+

None

+
+
+
+
+
+pivot_(pivot)[source]
+
+
Parameters:
+

pivot (LongTensor)

+
+
Return type:
+

None

+
+
+
+
+
+expand(*sizes)[source]
+
+
Parameters:
+

sizes (int)

+
+
Return type:
+

PivotedCholesky

+
+
+
+
+
+concat(other, dim=0)[source]
+
+
Parameters:
+
+
+
Return type:
+

PivotedCholesky

+
+
+
+
+
+detach()[source]
+
+
Return type:
+

PivotedCholesky

+
+
+
+
+
+clone()[source]
+
+
Return type:
+

PivotedCholesky

+
+
+
+
+
+
+

Probability Helpers

+
+
+botorch.utils.probability.utils.case_dispatcher(out, cases=(), default=None)[source]
+

Basic implementation of a tensorized switching case statement.

+
+
Parameters:
+
    +
  • out (Tensor) – Tensor to which case outcomes are written.

  • +
  • cases (Iterable[tuple[Callable[[], BoolTensor], Callable[[BoolTensor], Tensor]]]) – Iterable of function pairs (pred, func), where mask=pred() specifies +whether func is applicable for each entry in out. Note that cases are +resolved first-come, first-serve.

  • +
  • default (Callable[[BoolTensor], Tensor]) – Optional func to which all unclaimed entries of out are dispatched.

  • +
+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.get_constants(values, device=None, dtype=None)[source]
+

Returns scalar-valued Tensors containing each of the given constants. +Used to expedite tensor operations involving scalar arithmetic. Note that +the returned Tensors should not be modified in-place.

+
+
Parameters:
+
    +
  • values (Number | Iterator[Number])

  • +
  • device (device | None)

  • +
  • dtype (dtype | None)

  • +
+
+
Return type:
+

Tensor | tuple[Tensor, …]

+
+
+
+
+
+botorch.utils.probability.utils.get_constants_like(values, ref)[source]
+
+
Parameters:
+
    +
  • values (Number | Iterator[Number])

  • +
  • ref (Tensor)

  • +
+
+
Return type:
+

Tensor | Iterator[Tensor]

+
+
+
+
+
+botorch.utils.probability.utils.gen_positional_indices(shape, dim, device=None)[source]
+
+
Parameters:
+
    +
  • shape (Size)

  • +
  • dim (int)

  • +
  • device (device | None)

  • +
+
+
Return type:
+

Iterator[LongTensor]

+
+
+
+
+
+botorch.utils.probability.utils.build_positional_indices(shape, dim, device=None)[source]
+
+
Parameters:
+
    +
  • shape (Size)

  • +
  • dim (int)

  • +
  • device (device | None)

  • +
+
+
Return type:
+

LongTensor

+
+
+
+
+
+botorch.utils.probability.utils.leggauss(deg, **tkwargs)[source]
+
+
Parameters:
+
    +
  • deg (int)

  • +
  • tkwargs (Any)

  • +
+
+
Return type:
+

tuple[Tensor, Tensor]

+
+
+
+
+
+botorch.utils.probability.utils.ndtr(x)[source]
+

Standard normal CDF.

+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.phi(x)[source]
+

Standard normal PDF.

+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.log_phi(x)[source]
+

Logarithm of standard normal pdf

+
+
Parameters:
+

x (Tensor)

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.log_ndtr(x)[source]
+

Implementation of log_ndtr that remedies problems of torch.special’s version +for large negative x, where the torch implementation yields Inf or NaN gradients.

+
+
Parameters:
+

x (Tensor) – An input tensor with dtype torch.float32 or torch.float64.

+
+
Returns:
+

A tensor of values of the same type and shape as x containing log(ndtr(x)).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.log_erfc(x)[source]
+

Computes the logarithm of the complementary error function in a numerically +stable manner. The GitHub issue https://github.com/pytorch/pytorch/issues/31945 +tracks progress toward moving this feature into PyTorch in C++.

+
+
Parameters:
+

x (Tensor) – An input tensor with dtype torch.float32 or torch.float64.

+
+
Returns:
+

A tensor of values of the same type and shape as x containing log(erfc(x)).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.log_erfcx(x)[source]
+

Computes the logarithm of the complementary scaled error function in a +numerically stable manner. The GitHub issue tracks progress toward moving this +feature into PyTorch in C++: https://github.com/pytorch/pytorch/issues/31945.

+
+
Parameters:
+

x (Tensor) – An input tensor with dtype torch.float32 or torch.float64.

+
+
Returns:
+

A tensor of values of the same type and shape as x containing log(erfcx(x)).

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.standard_normal_log_hazard(x)[source]
+

Computes the logarithm of the hazard function of the standard normal +distribution, i.e. log(phi(x) / Phi(-x)).

+
+
Parameters:
+

x (Tensor) – A tensor of any shape, with either float32 or float64 dtypes.

+
+
Returns:
+

A Tensor of the same shape x, containing the values of the logarithm of the +hazard function evaluated at x.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.log_prob_normal_in(a, b)[source]
+

Computes the probability that a standard normal random variable takes a value +in [a, b], i.e. log(Phi(b) - Phi(a)), where Phi is the standard normal CDF. +Returns accurate values and permits numerically stable backward passes for inputs +in [-1e100, 1e100] for double precision and [-1e20, 1e20] for single precision. +In contrast, a naive approach is not numerically accurate beyond [-10, 10].

+
+
Parameters:
+
    +
  • a (Tensor) – Tensor of lower integration bounds of the Gaussian probability measure.

  • +
  • b (Tensor) – Tensor of upper integration bounds of the Gaussian probability measure.

  • +
+
+
Returns:
+

Tensor of the log probabilities.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.swap_along_dim_(values, i, j, dim, buffer=None)[source]
+

Swaps Tensor slices in-place along dimension dim.

+

When passed as Tensors, i (and j) should be dim-dimensional tensors +with the same shape as values.shape[:dim]. The xception to this rule occurs +when dim=0, in which case i (and j) should be (at most) one-dimensional +when passed as a Tensor.

+
+
Parameters:
+
    +
  • values (Tensor) – Tensor whose values are to be swapped.

  • +
  • i (int | LongTensor) – Indices for slices along dimension dim.

  • +
  • j (int | LongTensor) – Indices for slices along dimension dim.

  • +
  • dim (int) – The dimension of values along which to swap slices.

  • +
  • buffer (Tensor | None) – Optional buffer used internally to store copied values.

  • +
+
+
Returns:
+

The original values tensor.

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.compute_log_prob_feas_from_bounds(con_lower_inds, con_upper_inds, con_both_inds, con_lower, con_upper, con_both, means, sigmas)[source]
+

Compute logarithm of the feasibility probability for each batch of mean/sigma.

+
+
Parameters:
+
    +
  • means (Tensor) – A (b) x m-dim Tensor of means.

  • +
  • sigmas (Tensor) – A (b) x m-dim Tensor of standard deviations.

  • +
  • con_lower_inds (Tensor) – 1d Tensor of indices con_lower applies to +in the second dimension of means and sigmas.

  • +
  • con_upper_inds (Tensor) – 1d Tensor of indices con_upper applies to +in the second dimension of means and sigmas.

  • +
  • con_both_inds (Tensor) – 1d Tensor of indices con_both applies to +in the second dimension of means and sigmas.

  • +
  • con_lower (Tensor) – 1d Tensor of lower bounds on the constraints +equal in dimension to con_lower_inds.

  • +
  • con_upper (Tensor) – 1d Tensor of upper bounds on the constraints +equal in dimension to con_upper_inds.

  • +
  • con_both (Tensor) – 2d Tensor of “both” bounds on the constraints +equal in length to con_both_inds.

  • +
+
+
Returns:
+

A b-dim tensor of log feasibility probabilities

+
+
Return type:
+

Tensor

+
+
+
+
+
+botorch.utils.probability.utils.percentile_of_score(data, score, dim=-1)[source]
+

Compute the percentile rank of score relative to data. +For example, if this function returns 70 then 70% of the +values in data are below score.

+

This implementation is based on scipy.stats.percentileofscore, +with kind=’rank’ and nan_policy=’propagate’, which is the default.

+
+
Parameters:
+
    +
  • data (Tensor) – A … x n x output_shape-dim Tensor of data.

  • +
  • score (Tensor) – A … x 1 x output_shape-dim Tensor of scores.

  • +
  • dim (int)

  • +
+
+
Returns:
+

A … x output_shape-dim Tensor of percentile ranks.

+
+
Return type:
+

Tensor

+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/website/pages/en/index.js b/website-old/pages/en/index.js similarity index 100% rename from website/pages/en/index.js rename to website-old/pages/en/index.js diff --git a/website-old/pages/tutorials/GIBBON_for_efficient_batch_entropy_search.js b/website-old/pages/tutorials/GIBBON_for_efficient_batch_entropy_search.js new file mode 100644 index 0000000000..bb6157d08d --- /dev/null +++ b/website-old/pages/tutorials/GIBBON_for_efficient_batch_entropy_search.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/Multi_objective_multi_fidelity_BO.js b/website-old/pages/tutorials/Multi_objective_multi_fidelity_BO.js new file mode 100644 index 0000000000..50ef66cbec --- /dev/null +++ b/website-old/pages/tutorials/Multi_objective_multi_fidelity_BO.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/batch_mode_cross_validation.js b/website-old/pages/tutorials/batch_mode_cross_validation.js new file mode 100644 index 0000000000..05624f22aa --- /dev/null +++ b/website-old/pages/tutorials/batch_mode_cross_validation.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/baxus.js b/website-old/pages/tutorials/baxus.js new file mode 100644 index 0000000000..9278cd3172 --- /dev/null +++ b/website-old/pages/tutorials/baxus.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/bo_with_warped_gp.js b/website-old/pages/tutorials/bo_with_warped_gp.js new file mode 100644 index 0000000000..01b36932ee --- /dev/null +++ b/website-old/pages/tutorials/bo_with_warped_gp.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/bope.js b/website-old/pages/tutorials/bope.js new file mode 100644 index 0000000000..c94e3e5f5f --- /dev/null +++ b/website-old/pages/tutorials/bope.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/closed_loop_botorch_only.js b/website-old/pages/tutorials/closed_loop_botorch_only.js new file mode 100644 index 0000000000..8e7c17f8fc --- /dev/null +++ b/website-old/pages/tutorials/closed_loop_botorch_only.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/compare_mc_analytic_acquisition.js b/website-old/pages/tutorials/compare_mc_analytic_acquisition.js new file mode 100644 index 0000000000..cfe988b937 --- /dev/null +++ b/website-old/pages/tutorials/compare_mc_analytic_acquisition.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/composite_bo_with_hogp.js b/website-old/pages/tutorials/composite_bo_with_hogp.js new file mode 100644 index 0000000000..0b4ed2deff --- /dev/null +++ b/website-old/pages/tutorials/composite_bo_with_hogp.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/composite_mtbo.js b/website-old/pages/tutorials/composite_mtbo.js new file mode 100644 index 0000000000..cc05b11021 --- /dev/null +++ b/website-old/pages/tutorials/composite_mtbo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/constrained_multi_objective_bo.js b/website-old/pages/tutorials/constrained_multi_objective_bo.js new file mode 100644 index 0000000000..b8b842d33b --- /dev/null +++ b/website-old/pages/tutorials/constrained_multi_objective_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/constraint_active_search.js b/website-old/pages/tutorials/constraint_active_search.js new file mode 100644 index 0000000000..0062aac9fd --- /dev/null +++ b/website-old/pages/tutorials/constraint_active_search.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/cost_aware_bayesian_optimization.js b/website-old/pages/tutorials/cost_aware_bayesian_optimization.js new file mode 100644 index 0000000000..01fc9ddde9 --- /dev/null +++ b/website-old/pages/tutorials/cost_aware_bayesian_optimization.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/custom_acquisition.js b/website-old/pages/tutorials/custom_acquisition.js new file mode 100644 index 0000000000..f83bb977d0 --- /dev/null +++ b/website-old/pages/tutorials/custom_acquisition.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/custom_botorch_model_in_ax.js b/website-old/pages/tutorials/custom_botorch_model_in_ax.js new file mode 100644 index 0000000000..d7313a5ac1 --- /dev/null +++ b/website-old/pages/tutorials/custom_botorch_model_in_ax.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/custom_model.js b/website-old/pages/tutorials/custom_model.js new file mode 100644 index 0000000000..4b61365e85 --- /dev/null +++ b/website-old/pages/tutorials/custom_model.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/decoupled_mobo.js b/website-old/pages/tutorials/decoupled_mobo.js new file mode 100644 index 0000000000..711d8147fd --- /dev/null +++ b/website-old/pages/tutorials/decoupled_mobo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/discrete_multi_fidelity_bo.js b/website-old/pages/tutorials/discrete_multi_fidelity_bo.js new file mode 100644 index 0000000000..2fe88d3dd3 --- /dev/null +++ b/website-old/pages/tutorials/discrete_multi_fidelity_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/fit_model_with_torch_optimizer.js b/website-old/pages/tutorials/fit_model_with_torch_optimizer.js new file mode 100644 index 0000000000..2fdd1c982d --- /dev/null +++ b/website-old/pages/tutorials/fit_model_with_torch_optimizer.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/ibnn_bo.js b/website-old/pages/tutorials/ibnn_bo.js new file mode 100644 index 0000000000..9ea566d97b --- /dev/null +++ b/website-old/pages/tutorials/ibnn_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website/pages/tutorials/index.js b/website-old/pages/tutorials/index.js similarity index 100% rename from website/pages/tutorials/index.js rename to website-old/pages/tutorials/index.js diff --git a/website-old/pages/tutorials/information_theoretic_acquisition_functions.js b/website-old/pages/tutorials/information_theoretic_acquisition_functions.js new file mode 100644 index 0000000000..1b296cc9c7 --- /dev/null +++ b/website-old/pages/tutorials/information_theoretic_acquisition_functions.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/max_value_entropy.js b/website-old/pages/tutorials/max_value_entropy.js new file mode 100644 index 0000000000..e79bbf40e7 --- /dev/null +++ b/website-old/pages/tutorials/max_value_entropy.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/meta_learning_with_rgpe.js b/website-old/pages/tutorials/meta_learning_with_rgpe.js new file mode 100644 index 0000000000..aad8a1b6e1 --- /dev/null +++ b/website-old/pages/tutorials/meta_learning_with_rgpe.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/multi_fidelity_bo.js b/website-old/pages/tutorials/multi_fidelity_bo.js new file mode 100644 index 0000000000..984876b74f --- /dev/null +++ b/website-old/pages/tutorials/multi_fidelity_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/multi_objective_bo.js b/website-old/pages/tutorials/multi_objective_bo.js new file mode 100644 index 0000000000..f97e13fd98 --- /dev/null +++ b/website-old/pages/tutorials/multi_objective_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/one_shot_kg.js b/website-old/pages/tutorials/one_shot_kg.js new file mode 100644 index 0000000000..0802bd02fa --- /dev/null +++ b/website-old/pages/tutorials/one_shot_kg.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/optimize_stochastic.js b/website-old/pages/tutorials/optimize_stochastic.js new file mode 100644 index 0000000000..f6fac8ce03 --- /dev/null +++ b/website-old/pages/tutorials/optimize_stochastic.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/optimize_with_cmaes.js b/website-old/pages/tutorials/optimize_with_cmaes.js new file mode 100644 index 0000000000..efb8c466db --- /dev/null +++ b/website-old/pages/tutorials/optimize_with_cmaes.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/preference_bo.js b/website-old/pages/tutorials/preference_bo.js new file mode 100644 index 0000000000..7a28875c8a --- /dev/null +++ b/website-old/pages/tutorials/preference_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/risk_averse_bo_with_environmental_variables.js b/website-old/pages/tutorials/risk_averse_bo_with_environmental_variables.js new file mode 100644 index 0000000000..83c3b145b6 --- /dev/null +++ b/website-old/pages/tutorials/risk_averse_bo_with_environmental_variables.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/risk_averse_bo_with_input_perturbations.js b/website-old/pages/tutorials/risk_averse_bo_with_input_perturbations.js new file mode 100644 index 0000000000..56373df005 --- /dev/null +++ b/website-old/pages/tutorials/risk_averse_bo_with_input_perturbations.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/robust_multi_objective_bo.js b/website-old/pages/tutorials/robust_multi_objective_bo.js new file mode 100644 index 0000000000..b2b761f83c --- /dev/null +++ b/website-old/pages/tutorials/robust_multi_objective_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/saasbo.js b/website-old/pages/tutorials/saasbo.js new file mode 100644 index 0000000000..f175c5e068 --- /dev/null +++ b/website-old/pages/tutorials/saasbo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/scalable_constrained_bo.js b/website-old/pages/tutorials/scalable_constrained_bo.js new file mode 100644 index 0000000000..04ee7e1f92 --- /dev/null +++ b/website-old/pages/tutorials/scalable_constrained_bo.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/thompson_sampling.js b/website-old/pages/tutorials/thompson_sampling.js new file mode 100644 index 0000000000..73cee7099c --- /dev/null +++ b/website-old/pages/tutorials/thompson_sampling.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/turbo_1.js b/website-old/pages/tutorials/turbo_1.js new file mode 100644 index 0000000000..7c65bfb882 --- /dev/null +++ b/website-old/pages/tutorials/turbo_1.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/pages/tutorials/vae_mnist.js b/website-old/pages/tutorials/vae_mnist.js new file mode 100644 index 0000000000..ecadd6d269 --- /dev/null +++ b/website-old/pages/tutorials/vae_mnist.js @@ -0,0 +1,15 @@ +const CWD = process.cwd(); + +const React = require('react'); +const Tutorial = require(`${CWD}/core/Tutorial.js`); + +class TutorialPage extends React.Component { + render() { + const {config: siteConfig} = this.props; + const {baseUrl} = siteConfig; + return ; + } +} + +module.exports = TutorialPage; + diff --git a/website-old/sidebars.json b/website-old/sidebars.json new file mode 100644 index 0000000000..2cd5992c75 --- /dev/null +++ b/website-old/sidebars.json @@ -0,0 +1,9 @@ +{ + "docs": { + "About": ["introduction", "design_philosophy", "botorch_and_ax", "papers"], + "General": ["getting_started"], + "Basic Concepts": ["overview", "models", "posteriors", "acquisition", "optimization"], + "Advanced Topics": ["constraints", "objectives", "batching", "samplers"], + "Multi-Objective Optimization": ["multi_objective"] + } +} diff --git a/website/siteConfig.js b/website-old/siteConfig.js similarity index 100% rename from website/siteConfig.js rename to website-old/siteConfig.js diff --git a/website/versioned_docs/.gitkeep b/website-old/static/.nojekyll similarity index 100% rename from website/versioned_docs/.gitkeep rename to website-old/static/.nojekyll diff --git a/website-old/static/CNAME b/website-old/static/CNAME new file mode 100644 index 0000000000..2d85223b22 --- /dev/null +++ b/website-old/static/CNAME @@ -0,0 +1 @@ +botorch.org diff --git a/website-old/static/_sphinx-sources/acquisition.rst.txt b/website-old/static/_sphinx-sources/acquisition.rst.txt new file mode 100644 index 0000000000..2f6060058a --- /dev/null +++ b/website-old/static/_sphinx-sources/acquisition.rst.txt @@ -0,0 +1,222 @@ +.. role:: hidden + :class: hidden-section + + +botorch.acquisition +======================================================== +.. automodule:: botorch.acquisition + + +Acquisition Function APIs +------------------------------------------- + +Abstract Acquisition Function APIs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.acquisition + :members: + +Analytic Acquisition Function API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. currentmodule:: botorch.acquisition.analytic +.. autoclass:: AnalyticAcquisitionFunction + :members: + +Cached Cholesky Acquisition Function API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.cached_cholesky + :members: + +Decoupled Acquisition Function API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.decoupled + :members: + +Monte-Carlo Acquisition Function API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. currentmodule:: botorch.acquisition.monte_carlo +.. autoclass:: MCAcquisitionFunction + :members: + +Base Classes for Multi-Objective Acquisition Function API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.base + :members: + + +Acquisition Functions +------------------------------------------- + +Analytic Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.analytic + :members: + :exclude-members: AnalyticAcquisitionFunction + +Monte-Carlo Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.monte_carlo + :members: + :exclude-members: MCAcquisitionFunction + +.. automodule:: botorch.acquisition.logei + :members: + +Multi-Objective Analytic Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.analytic + :members: + +Multi-Objective Hypervolume Knowledge Gradient Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.hypervolume_knowledge_gradient + :members: + +Multi-Objective Joint Entropy Search Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.joint_entropy_search + :members: + +Multi-Objective Max-value Entropy Search Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.max_value_entropy_search + :members: + +Multi-Objective Monte-Carlo Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.monte_carlo + :members: + +.. automodule:: botorch.acquisition.multi_objective.logei + :members: + +Multi-Objective Multi-Fidelity Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.multi_fidelity + :members: + +Multi-Objective Predictive Entropy Search Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.predictive_entropy_search + :members: + +ParEGO: Multi-Objective Acquisition Function with Chebyshev Scalarization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.parego + :members: + +The One-Shot Knowledge Gradient +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.knowledge_gradient + :members: + +Multi-Step Lookahead Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_step_lookahead + :members: + +Max-value Entropy Search Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.max_value_entropy_search + :members: + +Joint Entropy Search Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.joint_entropy_search + :members: + +Predictive Entropy Search Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.predictive_entropy_search + :members: + +Active Learning Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.active_learning + :members: + +Bayesian Active Learning Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.bayesian_active_learning + :members: + +Preference Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.preference + :members: + +Objectives and Cost-Aware Utilities +------------------------------------------- + +Objectives +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.objective + :members: + +Multi-Objective Objectives +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.objective + :members: + +Cost-Aware Utility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.cost_aware + :members: + +Risk Measures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.risk_measures + :members: + +Thompson Sampling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.thompson_sampling + :members: + +Multi-Output Risk Measures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.multi_output_risk_measures + :members: + + +Utilities +------------------------------------------- + +Fixed Feature Acquisition Function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.fixed_feature + :members: + +Constructors for Acquisition Function Input Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.input_constructors + :members: + +Penalized Acquisition Function Wrapper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.penalized + :members: + +Prior-Guided Acquisition Function Wrapper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.prior_guided + :members: + +Proximal Acquisition Function Wrapper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.proximal + :members: + +Factory Functions for Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.factory + :members: + +General Utilities for Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.utils + :members: + +Multi-Objective Utilities for Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.utils + :members: diff --git a/website-old/static/_sphinx-sources/cross_validation.rst.txt b/website-old/static/_sphinx-sources/cross_validation.rst.txt new file mode 100644 index 0000000000..4c4d45d326 --- /dev/null +++ b/website-old/static/_sphinx-sources/cross_validation.rst.txt @@ -0,0 +1,8 @@ +.. role:: hidden + :class: hidden-section + + +botorch.cross_validation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.cross_validation + :members: diff --git a/website-old/static/_sphinx-sources/exceptions.rst.txt b/website-old/static/_sphinx-sources/exceptions.rst.txt new file mode 100644 index 0000000000..d7d8b87351 --- /dev/null +++ b/website-old/static/_sphinx-sources/exceptions.rst.txt @@ -0,0 +1,18 @@ +.. role:: hidden + :class: hidden-section + + +botorch.exceptions +======================================================== +.. automodule:: botorch.exceptions + + +Errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.exceptions.errors + :members: + +Warnings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.exceptions.warnings + :members: diff --git a/website-old/static/_sphinx-sources/fit.rst.txt b/website-old/static/_sphinx-sources/fit.rst.txt new file mode 100644 index 0000000000..8503b63def --- /dev/null +++ b/website-old/static/_sphinx-sources/fit.rst.txt @@ -0,0 +1,8 @@ +.. role:: hidden + :class: hidden-section + + +botorch.fit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.fit + :members: diff --git a/website-old/static/_sphinx-sources/generation.rst.txt b/website-old/static/_sphinx-sources/generation.rst.txt new file mode 100644 index 0000000000..62907a2f7c --- /dev/null +++ b/website-old/static/_sphinx-sources/generation.rst.txt @@ -0,0 +1,27 @@ +.. role:: hidden + :class: hidden-section + + +botorch.generation +======================================================== +.. automodule:: botorch.generation + + +Candidate Generation Utilities for Acquisition Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.generation.gen + :members: + + +Sampling Strategies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: botorch.generation.sampling + :members: + + +Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: botorch.generation.utils + :members: diff --git a/website-old/static/_sphinx-sources/index.rst.txt b/website-old/static/_sphinx-sources/index.rst.txt new file mode 100644 index 0000000000..f715932b9a --- /dev/null +++ b/website-old/static/_sphinx-sources/index.rst.txt @@ -0,0 +1,35 @@ +:github_url: https://github.com/pytorch/botorch + +BoTorch API Reference +=================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + :caption: API Reference + + acquisition + models + generation + posteriors + optim + fit + sampling + cross_validation + settings + logging + test_functions + test_utils + exceptions + utils + + +Indices and Tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/website-old/static/_sphinx-sources/logging.rst.txt b/website-old/static/_sphinx-sources/logging.rst.txt new file mode 100644 index 0000000000..3fe13ee943 --- /dev/null +++ b/website-old/static/_sphinx-sources/logging.rst.txt @@ -0,0 +1,8 @@ +.. role:: hidden + :class: hidden-section + + +botorch.logging +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.logging + :members: diff --git a/website-old/static/_sphinx-sources/models.rst.txt b/website-old/static/_sphinx-sources/models.rst.txt new file mode 100644 index 0000000000..ec6995565f --- /dev/null +++ b/website-old/static/_sphinx-sources/models.rst.txt @@ -0,0 +1,183 @@ +.. role:: hidden + :class: hidden-section + + +botorch.models +======================================================== +.. automodule:: botorch.models + + +Model APIs +------------------------------------------- + +Base Model API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.model + :members: + +GPyTorch Model API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.gpytorch + :members: + +Deterministic Model API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.deterministic + :members: + +Ensemble Model API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.ensemble + :members: + + +Models +------------------------------------------- + +Cost Models (for cost-aware optimization) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.cost + :members: + +GP Regression Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.gp_regression + :members: + +Multi-Fidelity GP Regression Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.gp_regression_fidelity + :members: + +GP Regression Models for Mixed Parameter Spaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.gp_regression_mixed + :members: + +Model List GP Regression Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.model_list_gp_regression + :members: + +Multitask GP Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.multitask + :members: + +Higher Order GP Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.higher_order_gp + :members: + +Pairwise GP Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.pairwise_gp + :members: + +Contextual GP Models with Aggregate Rewards +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.contextual + :members: + +Contextual GP Models with Context Rewards +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.contextual_multioutput + :members: + +Variational GP Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.approximate_gp + :members: + +Fully Bayesian GP Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.fully_bayesian + :members: + +Fully Bayesian Multitask GP Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.fully_bayesian_multitask + :members: + + +Model Components +------------------------------------------- + +Kernels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.kernels.categorical +.. autoclass:: CategoricalKernel + +.. automodule:: botorch.models.kernels.downsampling +.. autoclass:: DownsamplingKernel + +.. automodule:: botorch.models.kernels.exponential_decay +.. autoclass:: ExponentialDecayKernel + +.. automodule:: botorch.models.kernels.infinite_width_bnn +.. autoclass:: InfiniteWidthBNNKernel + +.. automodule:: botorch.models.kernels.linear_truncated_fidelity +.. autoclass:: LinearTruncatedFidelityKernel + +.. automodule:: botorch.models.kernels.contextual_lcea +.. autoclass:: LCEAKernel + +.. automodule:: botorch.models.kernels.contextual_sac +.. autoclass:: SACKernel + +.. automodule:: botorch.models.kernels.orthogonal_additive_kernel +.. autoclass:: OrthogonalAdditiveKernel + +Likelihoods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.likelihoods.pairwise + :members: + +Transforms +------------------------------------------- + +Outcome Transforms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.transforms.outcome + :members: + +Input Transforms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.transforms.input + :members: + +Transform Factory Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.transforms.factory + :members: + +Transform Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.transforms.utils + :members: + + +Utilities +------------------------------------------- + +GPyTorch Module Constructors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.utils.gpytorch_modules + :members: + +Model Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.converter + :members: + +Inducing Point Allocators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.utils.inducing_point_allocators + :members: + :private-members: _pivoted_cholesky_init + +Other Utilties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.models.utils.assorted + :members: diff --git a/website-old/static/_sphinx-sources/optim.rst.txt b/website-old/static/_sphinx-sources/optim.rst.txt new file mode 100644 index 0000000000..4ed9941a85 --- /dev/null +++ b/website-old/static/_sphinx-sources/optim.rst.txt @@ -0,0 +1,99 @@ +.. role:: hidden + :class: hidden-section + + +botorch.optim +======================================================== +.. automodule:: botorch.optim + + +Optimization +------------------------------------------- + +Core +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.core + :members: + +Acquisition Function Optimization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.optimize + :members: + +Model Fitting Optimization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.fit + :members: + :exclude-members: OptimizationIteration + +Initialization Helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.initializers + :members: + +Stopping Criteria +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.stopping + :members: + +Acquisition Function Optimization with Homotopy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.optimize_homotopy + :members: + +Acquisition Function Optimization with Mixed Integer Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.optimize_mixed + :members: + +Closures +------------------------------------------- + +Core +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.closures.core + :members: + +Model Fitting Closures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.closures.model_closures + :members: + + +Utilities +------------------------------------------- + +General Optimization Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.utils.common + :members: + +Acquisition Optimization Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.utils.acquisition_utils + :members: + +Model Fitting Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.utils.model_utils + :members: + +Numpy - Torch Conversion Tools +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.utils.numpy_utils + :members: + +Optimization with Timeouts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.utils.timeout + :members: + +Parameter Constraint Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.parameter_constraints + :members: + +Homotopy Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.optim.homotopy + :members: diff --git a/website-old/static/_sphinx-sources/posteriors.rst.txt b/website-old/static/_sphinx-sources/posteriors.rst.txt new file mode 100644 index 0000000000..665a3d9d44 --- /dev/null +++ b/website-old/static/_sphinx-sources/posteriors.rst.txt @@ -0,0 +1,69 @@ +.. role:: hidden + :class: hidden-section + + +botorch.posteriors +======================================================== +.. automodule:: botorch.posteriors + + +Posterior APIs +------------------------------------------- + +Abstract Posterior API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.posterior + :members: + +Posterior List API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.posterior_list + :members: + + +Posteriors +------------------------------------------- + +Torch Posterior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.torch + :members: + +GPyTorch Posterior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.gpytorch + :members: + +Ensemble Posterior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.ensemble + :members: + +Higher Order GP Posterior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.higher_order + :members: + +Multitask GP Posterior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.multitask + :members: + +Transformed Posterior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.transformed + :members: + +Fully Bayesian Posterior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.fully_bayesian + :members: + + +Utilities +--------------------------------------------- + +Base Samples +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.posteriors.base_samples + :members: diff --git a/website-old/static/_sphinx-sources/sampling.rst.txt b/website-old/static/_sphinx-sources/sampling.rst.txt new file mode 100644 index 0000000000..523d27c3b0 --- /dev/null +++ b/website-old/static/_sphinx-sources/sampling.rst.txt @@ -0,0 +1,87 @@ +.. role:: hidden + :class: hidden-section + + +botorch.sampling +======================================================== +.. automodule:: botorch.sampling + + +Monte-Carlo Sampler API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.base + :members: + +Index Sampler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.index_sampler + :members: + +Get Sampler Helper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.get_sampler + :members: + +List Sampler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.list_sampler + :members: + +Gaussian Monte-Carlo Samplers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.normal + :members: + +Pairwise Monte-Carlo Samplers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pairwise_samplers + :members: + +QMC Base Functionality +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.qmc + :members: + +Stochastic Samplers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.stochastic_samplers + :members: + + +Pathwise Sampling +------------------------------------------- + +Feature Maps +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pathwise.features.maps + :members: + +Feature Map Generators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pathwise.features.generators + :members: + +Sample Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pathwise.paths + :members: + +Pathwise Prior Samplers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pathwise.prior_samplers + :members: + +Pathwise Posterior Samplers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pathwise.posterior_samplers + :members: + +Pathwise Update Strategies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pathwise.update_strategies + :members: + +Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.sampling.pathwise.utils + :members: diff --git a/website-old/static/_sphinx-sources/settings.rst.txt b/website-old/static/_sphinx-sources/settings.rst.txt new file mode 100644 index 0000000000..87c225d3b0 --- /dev/null +++ b/website-old/static/_sphinx-sources/settings.rst.txt @@ -0,0 +1,8 @@ +.. role:: hidden + :class: hidden-section + + +botorch.settings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.settings + :members: diff --git a/website-old/static/_sphinx-sources/test_functions.rst.txt b/website-old/static/_sphinx-sources/test_functions.rst.txt new file mode 100644 index 0000000000..bfde346766 --- /dev/null +++ b/website-old/static/_sphinx-sources/test_functions.rst.txt @@ -0,0 +1,42 @@ +.. role:: hidden + :class: hidden-section + +botorch.test_functions +======================================================== +.. automodule:: botorch.test_functions + + +Abstract Test Function API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_functions.base + :members: + +Synthetic Test Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_functions.synthetic + :members: + +Multi-Fidelity Synthetic Test Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_functions.multi_fidelity + :members: + +Multi-Objective Synthetic Test Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_functions.multi_objective + :members: + +Multi-Objective Multi-Fidelity Synthetic Test Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_functions.multi_objective_multi_fidelity + :members: + +Sensitivity Analysis Test Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_functions.sensitivity_analysis + :members: + +Utilities For Test Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_functions.utils + :members: diff --git a/website-old/static/_sphinx-sources/test_utils.rst.txt b/website-old/static/_sphinx-sources/test_utils.rst.txt new file mode 100644 index 0000000000..71aa2d9214 --- /dev/null +++ b/website-old/static/_sphinx-sources/test_utils.rst.txt @@ -0,0 +1,12 @@ +.. role:: hidden + :class: hidden-section + + +botorch.test_utils +======================================================== +.. automodule:: botorch.test_utils + +Mock +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.test_utils.mock + :members: diff --git a/website-old/static/_sphinx-sources/utils.rst.txt b/website-old/static/_sphinx-sources/utils.rst.txt new file mode 100644 index 0000000000..ddcaa59581 --- /dev/null +++ b/website-old/static/_sphinx-sources/utils.rst.txt @@ -0,0 +1,184 @@ +.. role:: hidden + :class: hidden-section + + +botorch.utils +======================================================== +.. automodule:: botorch.utils + + +Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.constraints + :members: + +Containers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.containers + :members: + +Context Managers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.context_managers + :members: + +Datasets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.datasets + :members: + +Dispatcher +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.dispatcher + :members: + +Low-Rank Cholesky Update Utils +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.low_rank + :members: + +Multi-Task Distribution Utils +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multitask + :members: + +Objective +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.objective + :members: + +Rounding +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.rounding + :members: + +Sampling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.sampling + :members: + +Sampling from GP priors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.gp_sampling + :members: + +Testing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.testing + :members: + +Test Helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.test_helpers + :members: + +Torch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.torch + :members: + +Transformations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.transforms + :members: + +Feasible Volume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.feasible_volume + :members: + +Types and Type Hints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.types + :members: + +Constants +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.constants + :members: + +Safe Math +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.safe_math + :members: + +Multi-Objective Utilities +------------------------------------------- + +Abstract Box Decompositions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.box_decompositions.box_decomposition + :members: + +Box Decomposition List +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.box_decompositions.box_decomposition_list + :members: + +Box Decomposition Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.box_decompositions.utils + :members: + +Dominated Partitionings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.box_decompositions.dominated + :members: + +Hypervolume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.hypervolume + :members: + +Non-dominated Partitionings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.box_decompositions.non_dominated + :members: + +Pareto +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.pareto + :members: + +Scalarization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.multi_objective.scalarization + :members: + +Probability Utilities +------------------------------------------- + +Multivariate Gaussian Probabilities via Bivariate Conditioning +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.probability.mvnxpb + :members: + +Truncated Multivariate Normal Distribution +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.probability.truncated_multivariate_normal + :members: + +Unified Skew Normal Distribution +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.probability.unified_skew_normal + :members: + +Bivariate Normal Probabilities and Statistics +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.probability.bvn + :members: + +Elliptic Slice Sampler with Linear Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.probability.lin_ess + :members: + +Linear Algebra Helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.probability.linalg + :members: + +Probability Helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.utils.probability.utils + :members: diff --git a/website-old/static/css/alabaster.css b/website-old/static/css/alabaster.css new file mode 100644 index 0000000000..f533c9c01f --- /dev/null +++ b/website-old/static/css/alabaster.css @@ -0,0 +1,716 @@ +/* +Alabaster theme forked from https://github.com/bitprophet/alabaster/. See +original license below. + +Copyright (c) 2019 Jeff Forcier. + +Based on original work copyright (c) 2011 Kenneth Reitz and copyright (c) 2010 +Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or +without modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +@import url('basic.css'); + +/* -- page layout ----------------------------------------------------------- */ + +div.sphinx div.document { + width: 940px; + margin: 30px auto; +} + +div.sphinx div.documentwrapper { + float: left; + width: 100%; +} + +div.sphinx div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinx div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +div.sphinx hr { + border: 1px solid #b1b4b6; +} + +div.sphinx div.body { + background-color: #fff; + color: #3e4349; + padding: 0 30px 0 30px; +} + +div.sphinx div.body > .section { + text-align: left; +} + +div.sphinx p.caption { + font-family: inherit; + font-size: inherit; +} + +div.sphinx div.relations { + display: none; +} + +div.sphinx div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinx div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinx div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinx div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinx div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinx div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinx div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinx div.sphinxsidebar h3, +div.sphinx div.sphinxsidebar h4 { + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinx div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinx div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinx div.sphinxsidebar p.logo a, +div.sphinx div.sphinxsidebar h3 a, +div.sphinx div.sphinxsidebar p.logo a:hover, +div.sphinx div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinx div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinx div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinx div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinx div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinx div.sphinxsidebar input { + border: 1px solid #ccc; + font-size: 1em; +} + +div.sphinx div.sphinxsidebar hr { + border: none; + height: 1px; + color: #aaa; + background: #aaa; + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinx div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinx div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinx div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +div.sphinx a { + color: #004b6b; + text-decoration: underline; +} + +div.sphinx a:hover { + color: #6d4100; + text-decoration: underline; +} + +div.sphinx div.body h1, +div.sphinx div.body h2, +div.sphinx div.body h3, +div.sphinx div.body h4, +div.sphinx div.body h5, +div.sphinx div.body h6 { + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.sphinx div.body h1 { + margin-top: 0; + padding-top: 0; + font-size: 240%; +} +div.sphinx div.body h2 { + font-size: 180%; +} +div.sphinx div.body h3 { + font-size: 150%; +} +div.sphinx div.body h4 { + font-size: 130%; +} +div.sphinx div.body h5 { + font-size: 100%; +} +div.sphinx div.body h6 { + font-size: 100%; +} + +div.sphinx a.headerlink { + color: #ddd; + padding: 0 4px; + text-decoration: none; +} + +div.sphinx a.headerlink:hover { + color: #444; + background: #eaeaea; +} + +div.sphinx div.body p, +div.sphinx div.body dd, +div.sphinx div.body li { + line-height: 1.4em; +} + +div.sphinx div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #eee; + border: 1px solid #ccc; +} + +div.sphinx div.admonition tt.xref, +div.sphinx div.admonition code.xref, +div.sphinx div.admonition a tt { + background-color: #fbfbfb; + border-bottom: 1px solid #fafafa; +} + +div.sphinx div.admonition p.admonition-title { + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.sphinx div.admonition p.last { + margin-bottom: 0; +} + +div.sphinx div.highlight { + background-color: #fff; +} + +div.sphinx dt:target, +.highlight { + background: #faf3e8; +} + +div.sphinx div.warning { + background-color: #fcc; + border: 1px solid #faa; +} + +div.sphinx div.danger { + background-color: #fcc; + border: 1px solid #faa; + -moz-box-shadow: 2px 2px 4px #d52c2c; + -webkit-box-shadow: 2px 2px 4px #d52c2c; + box-shadow: 2px 2px 4px #d52c2c; +} + +div.sphinx div.error { + background-color: #fcc; + border: 1px solid #faa; + -moz-box-shadow: 2px 2px 4px #d52c2c; + -webkit-box-shadow: 2px 2px 4px #d52c2c; + box-shadow: 2px 2px 4px #d52c2c; +} + +div.sphinx div.caution { + background-color: #fcc; + border: 1px solid #faa; +} + +div.sphinx div.attention { + background-color: #fcc; + border: 1px solid #faa; +} + +div.sphinx div.important { + background-color: #eee; + border: 1px solid #ccc; +} + +div.sphinx div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.sphinx div.tip { + background-color: #eee; + border: 1px solid #ccc; +} + +div.sphinx div.hint { + background-color: #eee; + border: 1px solid #ccc; +} + +div.sphinx div.seealso { + background-color: #eee; + border: 1px solid #ccc; +} + +div.sphinx div.topic { + background-color: #eee; +} + +div.sphinx p.admonition-title { + display: inline; +} + +div.sphinx p.admonition-title:after { + content: ':'; +} + +div.sphinx pre, +div.sphinx tt, +div.sphinx code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +div.sphinx .hll { + background-color: #ffc; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +div.sphinx img.screenshot { +} + +div.sphinx tt.descname, +div.sphinx tt.descclassname, +div.sphinx code.descname, +div.sphinx code.descclassname { + font-size: 0.95em; +} + +div.sphinx tt.descname, +div.sphinx code.descname { + padding-right: 0.08em; +} + +div.sphinx img.screenshot { + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +div.sphinx table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +div.sphinx table.docutils td, +div.sphinx table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +div.sphinx table.field-list, +div.sphinx table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +div.sphinx table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #eee; + background: #fdfdfd; + font-size: 0.9em; +} + +div.sphinx table.footnote + div.sphinx table.footnote { + margin-top: -15px; + border-top: none; +} + +div.sphinx table.field-list th { + padding: 0 0.8em 0 0; +} + +div.sphinx table.field-list td { + padding: 0; +} + +div.sphinx table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +div.sphinx .field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +div.sphinx table.footnote td.label { + width: 0.1px; + padding: 0.3em 0 0.3em 0.5em; +} + +div.sphinx table.footnote td { + padding: 0.3em 0.5em; +} + +div.sphinx dl { + margin: 0; + padding: 0; +} + +div.sphinx dl dd { + margin-left: 30px; +} + +div.sphinx blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +div.sphinx ul, +div.sphinx ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +div.sphinx pre { + background: #eee; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.sphinx div.viewcode-block:target { + background: #ffd; +} + +div.sphinx dl pre, +div.sphinx blockquote pre, +div.sphinx li pre { + margin-left: 0; + padding-left: 30px; +} + +div.sphinx tt, +div.sphinx code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +div.sphinx tt.xref, +div.sphinx code.xref, +div.sphinx a tt { + background-color: #fbfbfb; + border-bottom: 1px solid #fff; +} + +div.sphinx a.reference { + text-decoration: none; + border-bottom: 1px dotted #004b6b; +} + +/* Don't put an underline on images */ +div.sphinx a.image-reference, +div.sphinx a.image-reference:hover { + border-bottom: none; +} + +div.sphinx a.reference:hover { + border-bottom: 1px solid #6d4100; +} + +div.sphinx a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004b6b; +} + +div.sphinx a.footnote-reference:hover { + border-bottom: 1px solid #6d4100; +} + +div.sphinx a:hover tt, +div.sphinx a:hover code { + background: #eee; +} + +@media screen and (max-width: 870px) { + div.sphinx div.sphinxsidebar { + display: none; + } + + div.sphinx div.document { + width: 100%; + } + + div.sphinx div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.sphinx div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + div.sphinx ul { + margin-left: 0; + } + + div.sphinx li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + div.sphinx .document { + width: auto; + } + + div.sphinx .bodywrapper { + margin: 0; + } + + div.sphinx .github { + display: none; + } +} + +@media screen and (max-width: 875px) { + div.sphinx div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinx div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #fff; + } + + div.sphinx div.sphinxsidebar h3, + div.sphinx div.sphinxsidebar h4, + div.sphinx div.sphinxsidebar p, + div.sphinx div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinx div.sphinxsidebar a { + color: #aaa; + } + + div.sphinx div.sphinxsidebar p.logo { + display: none; + } + + div.sphinx div.document { + width: 100%; + margin: 0; + } + + div.sphinx div.bodywrapper { + margin: 0; + } + + div.sphinx div.body { + min-height: 0; + padding: 0; + } + + div.sphinx .rtd_doc_footer { + display: none; + } + + div.sphinx .document { + width: auto; + } + + div.sphinx .github { + display: none; + } +} + +/* misc. */ + +div.sphinx .revsys-inline { + display: none !important; +} + +/* Make nested-list/multi-paragraph items look better in Releases changelog + * pages. Without this, docutils' magical list fuckery causes inconsistent + * formatting between different release sub-lists. + */ +div.sphinx div#changelog > div.section > ul > li > p:only-child { + margin-bottom: 0; +} + +/* Hide fugly table cell borders in ..bibliography:: directive output */ +div.sphinx table.docutils.citation, +div.sphinx table.docutils.citation td, +div.sphinx table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +/* relbar */ + +div.sphinx .related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +div.sphinx .related.top { + border-bottom: 1px solid #eee; + margin-bottom: 20px; +} + +div.sphinx .related.bottom { + border-top: 1px solid #eee; +} + +div.sphinx .related ul { + padding: 0; + margin: 0; + list-style: none; +} + +div.sphinx .related li { + display: inline; +} + +div.sphinx nav#rellinks { + float: right; +} + +div.sphinx nav#rellinks li + li:before { + content: '|'; +} + +div.sphinx nav#breadcrumbs li + li:before { + content: '\00BB'; +} + +/* Hide certain items when printing */ +@media print { + div.sphinx div.related { + display: none; + } +} diff --git a/website-old/static/css/basic.css b/website-old/static/css/basic.css new file mode 100644 index 0000000000..6d073844ae --- /dev/null +++ b/website-old/static/css/basic.css @@ -0,0 +1,736 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 900px; +} + +table { + overflow: hidden; +} + +dl { + margin-bottom: 15px; + } + +dl.class > dt { + background-color: #f8f8f8; + border-left: 3px solid #F15A24; + padding: 2px 0px 2px 5px; +} + +dl.class > dt > code { + background: none; +} + +dl.class > dt > em.property { + color: #F15A24; + font-style: normal; + font-variant: small-caps; +} + +dl.class em.property { + color: #F15A24; + font-style: normal; + font-variant: small-caps; +} + +dl.function > dt { + background-color: #f8f8f8; + border-left: 3px solid #F15A24; + padding: 2px 0px 2px 5px; +} + +dl.function > dt > code { + background: none; +} + +dd { + padding-top: 10px; + padding-bottom: 5px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist td { + vertical-align: top; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +div.code-block-caption { + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +div.code-block-caption + div > div.highlight > pre { + margin-top: 0; +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + padding: 1em 1em 0; +} + +div.literal-block-wrapper div.highlight { + margin: 0; +} + +code.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +code.descclassname { + background-color: transparent; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: relative; + left: 0px; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- source ---------------------------------------------------------- */ + +div.sphinx div.highlight { + width: 120%; +} + +div.sphinx div.highlight pre { + background-color: #fff; + padding: 7px 0 0 0; +} + +div.sphinx div.viewcode-block:target { + background: #fff; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} diff --git a/website-old/static/css/code_block_buttons.css b/website-old/static/css/code_block_buttons.css new file mode 100644 index 0000000000..b733f561b7 --- /dev/null +++ b/website-old/static/css/code_block_buttons.css @@ -0,0 +1,39 @@ +/* "Copy" code block button */ +pre { + position: relative; +} + +pre .btnIcon { + position: absolute; + top: 4px; + z-index: 2; + cursor: pointer; + border: 1px solid transparent; + padding: 0; + color: #000; + background-color: transparent; + height: 30px; + transition: all .25s ease-out; +} + +pre .btnIcon:hover { + text-decoration: none; +} + +.btnIcon__body { + align-items: center; + display: flex; +} + +.btnIcon svg { + fill: currentColor; + margin-right: .4em; +} + +.btnIcon__label { + font-size: 11px; +} + +.btnClipboard { + right: 10px; +} diff --git a/website-old/static/css/custom.css b/website-old/static/css/custom.css new file mode 100644 index 0000000000..bfbf3911aa --- /dev/null +++ b/website-old/static/css/custom.css @@ -0,0 +1,322 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +@import url('https://fonts.googleapis.com/css?family=Montserrat'); +@import url('https://fonts.googleapis.com/css?family=IBM+Plex+Mono'); + +html body { + font-family: 'Montserrat', sans-serif; + overflow-x: hidden; +} + +.fixedHeaderContainer { + background-color: #222222; +} + +.fixedHeaderContainer header .headerTitleWithLogo { + display: none; +} + +.fixedHeaderContainer header a:nth-child(2) { + position: absolute; + right: 0px; +} + +.fixedHeaderContainer header a:nth-child(2) h3 { + font-size: 14px; +} + +.fixedHeaderContainer header a:nth-child(2) h3::before { + content: 'v: '; +} + +.navigationSlider { + margin-right: 80px; +} + +.navigationSlider .slidingNav ul { + background: #222222; +} + +.navigationSlider .slidingNav ul li a { + color: #efefef; + background-color: #222222; +} + +.navigationSlider .slidingNav ul li a:hover, +.navigationSlider .slidingNav ul li a:focus { + color: #f15a24; + background-color: inherit; +} + +.navigationSlider .slidingNav ul li.siteNavItemActive > a, +.navigationSlider .slidingNav ul li.siteNavGroupActive > a { + background-color: inherit; +} + +.homeContainer { + background: rgb(2, 0, 36); + background: linear-gradient( + 180deg, + rgba(2, 0, 36, 1), + rgba(34, 34, 34, 1) 0%, + rgba(28, 96, 247, 1) 100% + ); + padding: 30px 0px; +} + +.splashLogo { + display: block; + margin: 0 auto; + height: 230px; + width: 275px; +} + +.projectTitle { + color: #ffffff; + font-variant: small-caps; + font-weight: 300; +} + +.promoSection .button { + border: 1px solid #fff; + color: #ffffff; +} + +.promoSection .button:hover { + background-color: inherit; + border: 1px solid #f15a24; + color: #f15a24; +} + +.landingPage { + padding: 0px; +} + +div.productShowcaseSection { + padding-top: 40px; + color: #6c6c6c; +} + +.productShowcaseSection > h2 { + font-variant: small-caps; + font-weight: 360; + margin: 0px; + padding: 0px; + color: #1c60f7; +} + +.productShowcaseSection p { + font-weight: 360; +} + +.productShowcaseSection div.container { + padding: 40px 0px; +} + +.productShowcaseSection div.blockImage { + height: 80px; +} + +.productShowcaseSection li { + padding: 10px 0; +} + +.productShowcaseSection pre { + margin: 10px 0; +} + +.productShowcaseSection code { + background: #fff; +} + +.container .wrapper .alignCenter h2 { + color: #222222; +} + +div#quickstart { + background: #efefef; +} + +div#quickstart ol { + margin-bottom: 0px; +} + +.nav-footer { + background-color: #222222; +} + +.nav-footer .sitemap a { + color: #efefef; +} + +.nav-footer .sitemap a:hover { + color: #f15a24; +} + +a, +p a { + color: #1c60f7; +} + +a:hover, +p a:hover { + color: #1c60f7; +} + +/* Style docs */ +.toc .toggleNav .navGroup .navGroupCategoryTitle { + color: #222222; +} + +.toc .toggleNav ul li a { + color: #6c6c6c; +} + +.toc .toggleNav ul li a:hover { + color: #f15a24; +} + +.toc .toggleNav .navGroup .navListItemActive a { + color: #1c60f7; +} + +.mainContainer .wrapper .post .postHeaderTitle { + color: #222222; +} + +.mainContainer .wrapper .post h1, +.mainContainer .wrapper .post h2, +.mainContainer .wrapper .post h3 { + color: #222222; +} + +.mainContainer .wrapper .post { + color: #6c6c6c; +} + +.mainContainer .wrapper .post strong { + color: #222222; +} + +a.edit-page-link { + color: #1c60f7; + border: 1px solid #1c60f7; +} + +a.edit-page-link:hover { + color: #f15a24; + border: 1px solid #f15a24; + background-color: inherit; +} + +a.docs-next, +a.docs-prev { + color: #1c60f7; + border: 1px solid #1c60f7; +} + +a.docs-next:hover, +a.docs-prev:hover { + color: #f15a24; + border: 1px solid #f15a24; + background-color: inherit; +} + +/* Style tutorials */ +.tutorialBody { + margin-top: -20px; + color: #6c6c6c; +} + +.tutorialBody h1 { + margin: 0px; +} + +.tutorialBody h1, +.tutorialBody h2, +.tutorialBody h3 { + color: #222222; +} + +.tutorialBody pre { + font-family: 'IBM Plex Mono', monospace; + font-size: 14px; + margin: 0px; +} + +.tutorialBody .input_prompt, +.tutorialBody .output_prompt { + color: darkred; + font-size: 12px; +} + +.tutorialBody .highlight { + background: #f3f4f7; + padding: 10px 20px; + border: lightgray 1px solid; + border-radius: 3px; +} + +.tutorialBody .cell { + margin: 20px; +} + +.tutorialBody .output_stderr { + background-color: #fdede9; +} + +.tutorialBody .anchor-link { + color: lightgray; +} + +.tutorialButtonWrapper { + margin: 20px; +} + +.tutorialButton { + color: #1c60f7; + border: 1px solid #1c60f7; +} + +.tutorialButton svg { + height: 15px; + margin-right: 5px; +} + +.tutorialButton:hover { + color: #f15a24; + border: 1px solid #f15a24; + background-color: inherit; +} + +.wrapper { + max-width: 1400px; +} + +@media only screen and (min-width: 1024px) { +} + +@media only screen and (max-width: 1023px) { + .fixedHeaderContainer header a:nth-child(2) { + position: absolute; + right: 200px; + } +} + +@media only screen and (min-device-width: 360px) and (max-device-width: 736px) { + .fixedHeaderContainer header a:nth-child(2) { + position: absolute; + right: 150px; + } +} + +@media only screen and (min-width: 1400px) { +} + +@media only screen and (min-width: 1500px) { +} diff --git a/website-old/static/files/GIBBON_for_efficient_batch_entropy_search.ipynb b/website-old/static/files/GIBBON_for_efficient_batch_entropy_search.ipynb new file mode 100644 index 0000000000..8835b0491c --- /dev/null +++ b/website-old/static/files/GIBBON_for_efficient_batch_entropy_search.ipynb @@ -0,0 +1,476 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ee765c8b-00fd-46de-baa3-4c1dca928015" + }, + "source": [ + "## The GIBBON (General-purpose Information-Based Bayesian OptimisatioN) acquisition function\n", + "\n", + "A particularly intuitive and empirically effective class of acquisition functions has arisen based on information theory. Information-theoretic Bayesian Optimisation (BO) seeks to reduce uncertainty in the location of high-performing areas of the search space, as measured in terms of differential entropy. BoTorch already supports information-theoretic BO through an implementation of the Max-value Entropy Search (MES) acquisition function [1] (see the [Max-Value Entropy tutorial](./max_value_entropy) for details), which makes evaluations that reduce uncertainty in the maximum value attained by the objective function. However, in order to support batch and multi-fidelity BO, our implementation of MES employs numerical integrations and fantasy observations (i. e., we generate one point each time and when we try to generate the 𝑖-th point of a batch, we condition the models on the 𝑖−1 points generated prior to this). Unfortunately, Each of these calculations can can add significantly to the computational overhead incurred by BO.\n", + "\n", + "In this notebook, we provide an information-theoretic acquisition function for tasks where objective function query costs are not large enough to overshadow significant optimisation overheads known as General-purpose Information-Based Bayesian OptimisatioN (GIBBON) [2]. In this tutorial, we present a very high-level overview of GIBBON and demonstrate its use within BoTorch.\n", + "\n", + "### Calculating GIBBON\n", + "\n", + "Following a principled information-theoretic construction, the GIBBON acquisition function measures the utility of evaluating a candidate batch of $B$ points $\\{\\textbf{x}\\}_{i=1}^B$ as\n", + "\\begin{align}\n", + " \\alpha_{\\text{GIBBON}}(\\{\\textbf{x}\\}_{i=1}^B)\n", + " &= \\frac{1}{2}\\log |C| + \\sum_{i=1}^B \\hat{\\alpha}_{\\text{MES}}(\\textbf{x}_i)\n", + "\\end{align}\n", + "where $|C|$ is the determinant of the $B\\times B$ correlation matrix between the batch elements and $\\hat{\\alpha}_{\\text{MES}}$ is an analytical approximation of the standard (non-batch) MES acquisition function. The GIBBON acquisition function forms a lower bound on the exact (but intractable) batch MES function and is consequently referred to as the `qLowerBoundMaxValueEntropy` in BoTorch. Crucially, GIBBON can be computed in closed-form and so incurs substantially lower overheads than batch MES via fantasies.\n", + "\n", + "### Interpretating GIBBON\n", + "Note that the above decomposition of GIBBON has two terms and each has a helpful intuitive justification. In particular, the first term encourages diversity within the batch (achieving high values for points with low predictive correlation), whereas the second term ensures that evaluations are targeted in areas of the search space that provide large amounts of information about the maximum value attained by the objective function.\n", + "\n", + "\n", + "
\n", + "__References__\n", + "\n", + "\n", + "[1] [Wang, Z., Jegelka, S., _Max-value Entropy Search for Efficient Bayesian Optimization._ arXiv:1703.01968v3, 2018](https://arxiv.org/abs/1703.01968)\n", + "\n", + "[2] [Moss, M., et al., _GIBBON: General-purpose Information-Based Bayesian Optimisation._ arXiv:2102.03324, 2020](https://arxiv.org/abs/2102.03324)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false, + "executionStartTime": 1648577014199, + "executionStopTime": 1648577014323, + "originalKey": "4c523820-69ed-467b-9a22-3f8f9a42c056", + "requestMsgId": "07db8b0d-d844-4283-8e5c-895f7d2271cb" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "c597f1b7-7841-4058-9773-dfff42267a26" + }, + "source": [ + "### 1. Setting up a toy model\n", + "We will fit a standard SingleTaskGP model on noisy observations of the synthetic 2D SixHumpCamel function." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false, + "executionStartTime": 1648577014352, + "executionStopTime": 1648577015895, + "originalKey": "8900645e-ef50-4d4d-b4ae-9b0f4152aff0", + "requestMsgId": "8d7262fb-6bfe-454f-b465-478d269c184f" + }, + "outputs": [], + "source": [ + "import math\n", + "import torch\n", + "\n", + "from botorch.test_functions import SixHumpCamel\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.utils.transforms import standardize, normalize\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "torch.manual_seed(123456)\n", + "\n", + "bounds = torch.tensor(SixHumpCamel._bounds).T\n", + "bounds_norm = torch.tensor([[0.0, 0.0], [1.0, 1.0]])\n", + "train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(5, 2)\n", + "train_Y = SixHumpCamel(negate=True)(train_X).unsqueeze(-1)\n", + "\n", + "train_X = normalize(train_X, bounds=bounds)\n", + "train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y))\n", + "\n", + "model = SingleTaskGP(train_X, train_Y)\n", + "mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "fit_gpytorch_mll(mll, max_attempts=10);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "1b900ec1-ec7a-4330-bf79-d16b72e53304" + }, + "source": [ + "### 2. Defining the GIBBON acquisition function\n", + "\n", + "GIBBON is implemented in BoTorch as `qLowerBoundMaxValueEntropy` and supports pending points through its `X_pending` argument. Required arguments for the constructor are `model` and `candidate_set` (the discretized candidate points in the design space that will be used to draw max value samples). There are also other optional parameters, such as number of max value samples. Just like in our implementation of MES, two different sampling algorithms are supported for the max value samples: discretized Thompson sampling and Gumbel sampling (the default choice). \n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false, + "executionStartTime": 1648577015914, + "executionStopTime": 1648577016144, + "originalKey": "a01d0c4a-583a-4791-9259-02609b02d6d6", + "requestMsgId": "ad226a16-8b53-418e-bfb2-d3460b270acd" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.max_value_entropy_search import qLowerBoundMaxValueEntropy\n", + "\n", + "candidate_set_size = 1000 if not SMOKE_TEST else 5\n", + "candidate_set = torch.rand(\n", + " candidate_set_size, bounds_norm.size(1), device=bounds.device, dtype=bounds.dtype\n", + ")\n", + "qGIBBON = qLowerBoundMaxValueEntropy(model, candidate_set)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "d7ad6371-414c-4daa-b00a-253c7dbf0dd0" + }, + "source": [ + "### 3. Optimizing the GIBBON acquisition function to get the next candidate points\n", + "\n", + "In order to obtain the next candidate point(s) to query, we need to optimize the acquisition function over the design space. For $q=1$ case, we can simply call the `optimize_acqf` function in the library. For $q>1$, we greedily build batches using sequential optimization. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false, + "executionStartTime": 1648577016206, + "executionStopTime": 1648577016782, + "originalKey": "6b2f24f7-93cb-419b-a36a-626e48077b6c", + "requestMsgId": "dd3c847a-3bca-439f-bc9a-2acb698068a7" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[ 0.1199, -0.0158]]), tensor(0.0085))" + ] + }, + "execution_count": 4, + "metadata": { + "bento_obj_id": "140516803885120" + }, + "output_type": "execute_result" + } + ], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "\n", + "# for q = 1\n", + "candidates, acq_value = optimize_acqf(\n", + " acq_function=qGIBBON,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + ")\n", + "candidates, acq_value" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false, + "executionStartTime": 1648577016794, + "executionStopTime": 1648577017848, + "originalKey": "7ffdf144-60eb-4980-b387-5c03762a1f91", + "requestMsgId": "270506a8-d7dc-42d6-a6f5-b54f77746900" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[ 0.1194, -0.0160],\n", + " [ 1.4241, 0.4417]]),\n", + " tensor([0.0085, 0.0104]))" + ] + }, + "execution_count": 5, + "metadata": { + "bento_obj_id": "140516803794560" + }, + "output_type": "execute_result" + } + ], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "# for q = 2, sequential optimsiation\n", + "candidates, acq_value = optimize_acqf(\n", + " acq_function=qGIBBON,\n", + " bounds=bounds,\n", + " q=2,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " sequential=True,\n", + ")\n", + "candidates, acq_value" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "5e590a09-d151-4578-8558-79a9d9aa20d6" + }, + "source": [ + "### 4. Comparing GIBBON with other acquisition functions\n", + "\n", + "We now perform an illustrative comparison between GIBBON and the other low-cost acquisition functions implemented in BoTorch. We plot points chosen by each of the acquisition functions, each acquisition function's surface.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "669f1fbe-f713-4158-a10a-9fa70ce3f14f" + }, + "source": [ + "#### Sequential BO (q=1)\n", + "\n", + "Firstly, we investigate GIBBON in the purely sequential case, comparing agaisnt MES, Expected Improvement (EI) and Probability of Improvement (PI). We see that GIBBON provides a very high-quality approximation of MES, choosing essentially the same location.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "collapsed": false, + "executionStartTime": 1648577017895, + "executionStopTime": 1648577020377, + "hidden_ranges": [], + "originalKey": "5a4c0f2d-7bd3-4173-9e61-207b02591da7", + "requestMsgId": "e7a1e4c6-cec0-4168-b47a-e0634a7959e8" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(-0.1, 0.5, 'x_2')" + ] + }, + "execution_count": 6, + "metadata": { + "bento_obj_id": "140516721571968" + }, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "bento_obj_id": "140516819586640", + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from botorch.acquisition import (\n", + " ExpectedImprovement,\n", + " ProbabilityOfImprovement,\n", + " qMaxValueEntropy,\n", + ")\n", + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "# prep different acqusition functions\n", + "acqs = {}\n", + "candidate_set = torch.rand(\n", + " 10000, bounds.size(1), device=bounds.device, dtype=bounds.dtype\n", + ")\n", + "acqs[\"GIBBON\"] = qLowerBoundMaxValueEntropy(model, candidate_set)\n", + "acqs[\"MES\"] = qMaxValueEntropy(model, candidate_set)\n", + "acqs[\"EI\"] = ExpectedImprovement(model, best_f=train_Y.max())\n", + "acqs[\"PI\"] = ProbabilityOfImprovement(model, best_f=train_Y.max())\n", + "\n", + "# prep grid to evaluate acq functions\n", + "n = 100 if not SMOKE_TEST else 2\n", + "xv, yv = torch.meshgrid([torch.linspace(0, 1, n), torch.linspace(0, 1, n)])\n", + "test_x = torch.stack([xv.reshape(n * n, 1), yv.reshape(n * n, 1)], -1)\n", + "\n", + "# eval and maximise acq functions\n", + "evals = {}\n", + "candidates = {}\n", + "for acq in acqs.keys():\n", + " evals[acq] = acqs[acq](test_x).detach().reshape(n, n)\n", + " candidates[acq], _ = optimize_acqf(\n", + " acq_function=acqs[acq], bounds=bounds_norm, q=1, num_restarts=5, raw_samples=100\n", + " )\n", + "\n", + "# plot acqusition function values and chosen points\n", + "fig, (ax1, ax2, ax3, ax4) = plt.subplots(\n", + " nrows=1, ncols=4, sharex=True, sharey=True, figsize=(10, 5)\n", + ")\n", + "ax1.contourf(xv.numpy(), yv.numpy(), evals[\"GIBBON\"].numpy(), levels=20)\n", + "ax1.scatter(candidates[\"GIBBON\"][:, 0], candidates[\"GIBBON\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax1.set_title(\"GIBBON\")\n", + "ax2.contourf(xv.numpy(), yv.numpy(), evals[\"MES\"].numpy(), levels=20)\n", + "ax2.scatter(candidates[\"MES\"][:, 0], candidates[\"MES\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax2.set_title(\"MES\")\n", + "ax3.contourf(xv.numpy(), yv.numpy(), evals[\"EI\"].numpy(), levels=20)\n", + "ax3.scatter(candidates[\"EI\"][:, 0], candidates[\"EI\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax3.set_title(\"EI\")\n", + "ax4.contourf(xv.numpy(), yv.numpy(), evals[\"PI\"].numpy(), levels=20)\n", + "ax4.scatter(candidates[\"PI\"][:, 0], candidates[\"PI\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax4.set_title(\"PI\")\n", + "fig.text(0.5, -0.1, \"x_1\", ha=\"center\")\n", + "fig.text(-0.1, 0.5, \"x_2\", va=\"center\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "aa853e07-b322-4e26-ad5b-db81b8a47a33" + }, + "source": [ + "#### Batch BO (q=3)\n", + "\n", + "For the batch BO case, GIBBON selects similar points to MES but with an order-of-magnitude lower computational overhead, i.e perfoming information-theoretic BO at the cost of much simpler acqusition functions like EI and PI. We stress that this gap in computational overhead between GIBBON and MES grows substantially as the optimisation progresses (see [2])." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false, + "executionStartTime": 1648577020385, + "executionStopTime": 1648577031509, + "originalKey": "31ac3a12-eb78-4226-9170-31a22816f6c5", + "requestMsgId": "a7e5671c-1735-46dc-b2f0-6f8b85a997e6" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'Acquisition Function')" + ] + }, + "execution_count": 7, + "metadata": { + "bento_obj_id": "140516625083456" + }, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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F0euvxjAfRUdEzULzyEygecR5cVzqEM4uJIayffcWct2bgVwk2U7j4jpfa4HsFNMy0SUiucQmcDziPLiOdQtAysrbB+iZrJsZxTAuwD8HIAJAF8EcL3vuZ+u+tgIsGzn6+IfxwYAw6KiswLgA77nvqXq4wPPoUbgeUR58RwiE0ydRwzeREREREQlYI83EREREVEJGLyJiIiIiErA4E1EREREVAIGbyIiIiKiEjB4ExERERGVgMGbiIiIiKgEDN5ERERERCVg8CYiIiIiKgGDNxERERFRCRi8iYiIiIhKwOBNRERERFQCBm8iIiIiohIweBMRERERlYDBm4iIiIioBAzeREREREQlYPAmIiIiIioBgzcRERERUQkYvImIiIiISsDgTURERERUAgZvIiIiIqISMHgTEREREZWAwZuIiIiIqAQM3kREREREJWDwJiIiIiIqAYM3EREREVEJGLyJiIiIiErA4E1EREREVAIGbyIiIiKiEjB4ExERERGVgMGbiIiIiKgEDN5ERERERCVg8CYiIiIiKgGDNxERERFRCRi8iYiIiIhKwOBNRERERFQCBm8iIiIiohIweBMRERERlYDBm4iIiIioBAzeREREREQlYPAmIiIiIioBgzcRERERUQkYvImIiIiISsDgTURERERUAgZvIiIiIqISMHgTEREREZWAwZuIiIiIqAQM3kREREREJWDwJiIiIiIqAYM3EREREVEJGLyJiIiIiErA4E1EREREVAIGbyIiIiKiEjB4ExERERGVgMGbiIiIiKgEDN5ERERERCVg8CYiIiIiKsFQ1QdARERUJMt2fhnAHwDY6nvufNXHQ0TdxYo3ERG1lmU7bwSwA8ATVR8LERGDNxERNY5lO79l2c714vPftGzn1xI2dX3P/Q0AK+UeIRHRemw1ISKiJvo9APdatvMxAD8O4EfiNvI9d678QyMiiseKNxERNY7vuQsAfgPApwD8qu+5i1UfExHR/23nDo0TCqIogF4qQMYwqYMG1qzbTlAxEXEIBCYdpAEcCAyUg6IEIvgOZsBkyZ85x71Zc+Wdnd33iOINwFi9JTknmb06CMAzFG8ARqfUNk2ySDJP8jHMAP+a4g3AGC2TrPfbzSnJ9zDfKLV9ltoOw2aTXalt1T8qwNXkcvHRGwAA/pqtJgCMWqntPcnPnaPjfrv5ekEkgLvceAMAQAfeeAMAQAeKNwAAdKB4AwBAB4o3AAB0oHgDAEAHv4fKUMgC2v4PAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": { + "bento_obj_id": "140516819231504", + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "bento_obj_id": "140516625042544", + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from botorch.acquisition import qNoisyExpectedImprovement, qProbabilityOfImprovement\n", + "from time import time\n", + "\n", + "# prep different acqusition functions\n", + "acqs = {}\n", + "candidate_set = torch.rand(\n", + " 10000, bounds.size(1), device=bounds.device, dtype=bounds.dtype\n", + ")\n", + "acqs[\"GIBBON\"] = qLowerBoundMaxValueEntropy(model, candidate_set)\n", + "acqs[\"MES\"] = qMaxValueEntropy(model, candidate_set)\n", + "acqs[\"EI\"] = qNoisyExpectedImprovement(model, train_X)\n", + "acqs[\"PI\"] = qProbabilityOfImprovement(model, best_f=train_Y.max())\n", + "\n", + "# prep grid to evaluate acq functions\n", + "n = 100 if not SMOKE_TEST else 2\n", + "xv, yv = torch.meshgrid([torch.linspace(0, 1, n), torch.linspace(0, 1, n)])\n", + "test_x = torch.stack([xv.reshape(n * n, 1), yv.reshape(n * n, 1)], -1)\n", + "\n", + "# eval and maximise acq functions\n", + "evals = {}\n", + "candidates = {}\n", + "times = {}\n", + "for acq in acqs.keys():\n", + " evals[acq] = acqs[acq](test_x).detach().reshape(n, n)\n", + " t_0 = time()\n", + " candidates[acq], _ = optimize_acqf(\n", + " acq_function=acqs[acq],\n", + " bounds=bounds_norm,\n", + " q=3,\n", + " num_restarts=5,\n", + " raw_samples=100,\n", + " sequential=True,\n", + " )\n", + " times[acq] = time() - t_0\n", + "\n", + "# plot acqusition function values and chosen points\n", + "fig, (ax1, ax2, ax3, ax4) = plt.subplots(\n", + " nrows=1, ncols=4, sharex=True, sharey=True, figsize=(10, 5)\n", + ")\n", + "ax1.contourf(xv.numpy(), yv.numpy(), evals[\"GIBBON\"].numpy(), levels=20)\n", + "ax1.scatter(candidates[\"GIBBON\"][:, 0], candidates[\"GIBBON\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax1.set_title(\"GIBBON\")\n", + "ax2.contourf(xv.numpy(), yv.numpy(), evals[\"MES\"].numpy(), levels=20)\n", + "ax2.scatter(candidates[\"MES\"][:, 0], candidates[\"MES\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax2.set_title(\"MES\")\n", + "ax3.contourf(xv.numpy(), yv.numpy(), evals[\"EI\"].numpy(), levels=20)\n", + "ax3.scatter(candidates[\"EI\"][:, 0], candidates[\"EI\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax3.set_title(\"EI\")\n", + "ax4.contourf(xv.numpy(), yv.numpy(), evals[\"PI\"].numpy(), levels=20)\n", + "ax4.scatter(candidates[\"PI\"][:, 0], candidates[\"PI\"][:, 1], marker=\"X\", c=\"r\")\n", + "ax4.set_title(\"PI\")\n", + "fig.text(0.5, -0.1, \"x_1\", ha=\"center\")\n", + "fig.text(-0.1, 0.5, \"x_2\", va=\"center\")\n", + "\n", + "# plot computational overheads\n", + "plt.figure()\n", + "heights = [times[acq] for acq in acqs.keys()]\n", + "plt.bar(acqs.keys(), heights)\n", + "plt.ylabel(\"Computation Time\")\n", + "plt.xlabel(\"Acquisition Function\")" + ] + } + ], + "metadata": { + "fileHeader": "", + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/GIBBON_for_efficient_batch_entropy_search.py b/website-old/static/files/GIBBON_for_efficient_batch_entropy_search.py new file mode 100644 index 0000000000..af72ac44cb --- /dev/null +++ b/website-old/static/files/GIBBON_for_efficient_batch_entropy_search.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## The GIBBON (General-purpose Information-Based Bayesian OptimisatioN) acquisition function +# +# A particularly intuitive and empirically effective class of acquisition functions has arisen based on information theory. Information-theoretic Bayesian Optimisation (BO) seeks to reduce uncertainty in the location of high-performing areas of the search space, as measured in terms of differential entropy. BoTorch already supports information-theoretic BO through an implementation of the Max-value Entropy Search (MES) acquisition function [1] (see the [Max-Value Entropy tutorial](./max_value_entropy) for details), which makes evaluations that reduce uncertainty in the maximum value attained by the objective function. However, in order to support batch and multi-fidelity BO, our implementation of MES employs numerical integrations and fantasy observations (i. e., we generate one point each time and when we try to generate the 𝑖-th point of a batch, we condition the models on the 𝑖−1 points generated prior to this). Unfortunately, Each of these calculations can can add significantly to the computational overhead incurred by BO. +# +# In this notebook, we provide an information-theoretic acquisition function for tasks where objective function query costs are not large enough to overshadow significant optimisation overheads known as General-purpose Information-Based Bayesian OptimisatioN (GIBBON) [2]. In this tutorial, we present a very high-level overview of GIBBON and demonstrate its use within BoTorch. +# +# ### Calculating GIBBON +# +# Following a principled information-theoretic construction, the GIBBON acquisition function measures the utility of evaluating a candidate batch of $B$ points $\{\textbf{x}\}_{i=1}^B$ as +# \begin{align} +# \alpha_{\text{GIBBON}}(\{\textbf{x}\}_{i=1}^B) +# &= \frac{1}{2}\log |C| + \sum_{i=1}^B \hat{\alpha}_{\text{MES}}(\textbf{x}_i) +# \end{align} +# where $|C|$ is the determinant of the $B\times B$ correlation matrix between the batch elements and $\hat{\alpha}_{\text{MES}}$ is an analytical approximation of the standard (non-batch) MES acquisition function. The GIBBON acquisition function forms a lower bound on the exact (but intractable) batch MES function and is consequently referred to as the `qLowerBoundMaxValueEntropy` in BoTorch. Crucially, GIBBON can be computed in closed-form and so incurs substantially lower overheads than batch MES via fantasies. +# +# ### Interpretating GIBBON +# Note that the above decomposition of GIBBON has two terms and each has a helpful intuitive justification. In particular, the first term encourages diversity within the batch (achieving high values for points with low predictive correlation), whereas the second term ensures that evaluations are targeted in areas of the search space that provide large amounts of information about the maximum value attained by the objective function. +# +# +#
+# __References__ +# +# +# [1] [Wang, Z., Jegelka, S., _Max-value Entropy Search for Efficient Bayesian Optimization._ arXiv:1703.01968v3, 2018](https://arxiv.org/abs/1703.01968) +# +# [2] [Moss, M., et al., _GIBBON: General-purpose Information-Based Bayesian Optimisation._ arXiv:2102.03324, 2020](https://arxiv.org/abs/2102.03324) +# + +# In[1]: + + +import os + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### 1. Setting up a toy model +# We will fit a standard SingleTaskGP model on noisy observations of the synthetic 2D SixHumpCamel function. + +# In[2]: + + +import math +import torch + +from botorch.test_functions import SixHumpCamel +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.utils.transforms import standardize, normalize +from gpytorch.mlls import ExactMarginalLogLikelihood + +torch.manual_seed(123456) + +bounds = torch.tensor(SixHumpCamel._bounds).T +bounds_norm = torch.tensor([[0.0, 0.0], [1.0, 1.0]]) +train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(5, 2) +train_Y = SixHumpCamel(negate=True)(train_X).unsqueeze(-1) + +train_X = normalize(train_X, bounds=bounds) +train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y)) + +model = SingleTaskGP(train_X, train_Y) +mll = ExactMarginalLogLikelihood(model.likelihood, model) +fit_gpytorch_mll(mll, max_attempts=10); + + +# ### 2. Defining the GIBBON acquisition function +# +# GIBBON is implemented in BoTorch as `qLowerBoundMaxValueEntropy` and supports pending points through its `X_pending` argument. Required arguments for the constructor are `model` and `candidate_set` (the discretized candidate points in the design space that will be used to draw max value samples). There are also other optional parameters, such as number of max value samples. Just like in our implementation of MES, two different sampling algorithms are supported for the max value samples: discretized Thompson sampling and Gumbel sampling (the default choice). +# + +# In[3]: + + +from botorch.acquisition.max_value_entropy_search import qLowerBoundMaxValueEntropy + +candidate_set_size = 1000 if not SMOKE_TEST else 5 +candidate_set = torch.rand( + candidate_set_size, bounds_norm.size(1), device=bounds.device, dtype=bounds.dtype +) +qGIBBON = qLowerBoundMaxValueEntropy(model, candidate_set) + + +# ### 3. Optimizing the GIBBON acquisition function to get the next candidate points +# +# In order to obtain the next candidate point(s) to query, we need to optimize the acquisition function over the design space. For $q=1$ case, we can simply call the `optimize_acqf` function in the library. For $q>1$, we greedily build batches using sequential optimization. +# + +# In[4]: + + +from botorch.optim import optimize_acqf + +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 + +# for q = 1 +candidates, acq_value = optimize_acqf( + acq_function=qGIBBON, + bounds=bounds, + q=1, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, +) +candidates, acq_value + + +# In[5]: + + +from botorch.optim import optimize_acqf + +# for q = 2, sequential optimsiation +candidates, acq_value = optimize_acqf( + acq_function=qGIBBON, + bounds=bounds, + q=2, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + sequential=True, +) +candidates, acq_value + + +# ### 4. Comparing GIBBON with other acquisition functions +# +# We now perform an illustrative comparison between GIBBON and the other low-cost acquisition functions implemented in BoTorch. We plot points chosen by each of the acquisition functions, each acquisition function's surface. +# + +# #### Sequential BO (q=1) +# +# Firstly, we investigate GIBBON in the purely sequential case, comparing agaisnt MES, Expected Improvement (EI) and Probability of Improvement (PI). We see that GIBBON provides a very high-quality approximation of MES, choosing essentially the same location. +# + +# In[6]: + + +from botorch.acquisition import ( + ExpectedImprovement, + ProbabilityOfImprovement, + qMaxValueEntropy, +) +import matplotlib.pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + +# prep different acqusition functions +acqs = {} +candidate_set = torch.rand( + 10000, bounds.size(1), device=bounds.device, dtype=bounds.dtype +) +acqs["GIBBON"] = qLowerBoundMaxValueEntropy(model, candidate_set) +acqs["MES"] = qMaxValueEntropy(model, candidate_set) +acqs["EI"] = ExpectedImprovement(model, best_f=train_Y.max()) +acqs["PI"] = ProbabilityOfImprovement(model, best_f=train_Y.max()) + +# prep grid to evaluate acq functions +n = 100 if not SMOKE_TEST else 2 +xv, yv = torch.meshgrid([torch.linspace(0, 1, n), torch.linspace(0, 1, n)]) +test_x = torch.stack([xv.reshape(n * n, 1), yv.reshape(n * n, 1)], -1) + +# eval and maximise acq functions +evals = {} +candidates = {} +for acq in acqs.keys(): + evals[acq] = acqs[acq](test_x).detach().reshape(n, n) + candidates[acq], _ = optimize_acqf( + acq_function=acqs[acq], bounds=bounds_norm, q=1, num_restarts=5, raw_samples=100 + ) + +# plot acqusition function values and chosen points +fig, (ax1, ax2, ax3, ax4) = plt.subplots( + nrows=1, ncols=4, sharex=True, sharey=True, figsize=(10, 5) +) +ax1.contourf(xv.numpy(), yv.numpy(), evals["GIBBON"].numpy(), levels=20) +ax1.scatter(candidates["GIBBON"][:, 0], candidates["GIBBON"][:, 1], marker="X", c="r") +ax1.set_title("GIBBON") +ax2.contourf(xv.numpy(), yv.numpy(), evals["MES"].numpy(), levels=20) +ax2.scatter(candidates["MES"][:, 0], candidates["MES"][:, 1], marker="X", c="r") +ax2.set_title("MES") +ax3.contourf(xv.numpy(), yv.numpy(), evals["EI"].numpy(), levels=20) +ax3.scatter(candidates["EI"][:, 0], candidates["EI"][:, 1], marker="X", c="r") +ax3.set_title("EI") +ax4.contourf(xv.numpy(), yv.numpy(), evals["PI"].numpy(), levels=20) +ax4.scatter(candidates["PI"][:, 0], candidates["PI"][:, 1], marker="X", c="r") +ax4.set_title("PI") +fig.text(0.5, -0.1, "x_1", ha="center") +fig.text(-0.1, 0.5, "x_2", va="center") + + +# #### Batch BO (q=3) +# +# For the batch BO case, GIBBON selects similar points to MES but with an order-of-magnitude lower computational overhead, i.e perfoming information-theoretic BO at the cost of much simpler acqusition functions like EI and PI. We stress that this gap in computational overhead between GIBBON and MES grows substantially as the optimisation progresses (see [2]). + +# In[7]: + + +from botorch.acquisition import qNoisyExpectedImprovement, qProbabilityOfImprovement +from time import time + +# prep different acqusition functions +acqs = {} +candidate_set = torch.rand( + 10000, bounds.size(1), device=bounds.device, dtype=bounds.dtype +) +acqs["GIBBON"] = qLowerBoundMaxValueEntropy(model, candidate_set) +acqs["MES"] = qMaxValueEntropy(model, candidate_set) +acqs["EI"] = qNoisyExpectedImprovement(model, train_X) +acqs["PI"] = qProbabilityOfImprovement(model, best_f=train_Y.max()) + +# prep grid to evaluate acq functions +n = 100 if not SMOKE_TEST else 2 +xv, yv = torch.meshgrid([torch.linspace(0, 1, n), torch.linspace(0, 1, n)]) +test_x = torch.stack([xv.reshape(n * n, 1), yv.reshape(n * n, 1)], -1) + +# eval and maximise acq functions +evals = {} +candidates = {} +times = {} +for acq in acqs.keys(): + evals[acq] = acqs[acq](test_x).detach().reshape(n, n) + t_0 = time() + candidates[acq], _ = optimize_acqf( + acq_function=acqs[acq], + bounds=bounds_norm, + q=3, + num_restarts=5, + raw_samples=100, + sequential=True, + ) + times[acq] = time() - t_0 + +# plot acqusition function values and chosen points +fig, (ax1, ax2, ax3, ax4) = plt.subplots( + nrows=1, ncols=4, sharex=True, sharey=True, figsize=(10, 5) +) +ax1.contourf(xv.numpy(), yv.numpy(), evals["GIBBON"].numpy(), levels=20) +ax1.scatter(candidates["GIBBON"][:, 0], candidates["GIBBON"][:, 1], marker="X", c="r") +ax1.set_title("GIBBON") +ax2.contourf(xv.numpy(), yv.numpy(), evals["MES"].numpy(), levels=20) +ax2.scatter(candidates["MES"][:, 0], candidates["MES"][:, 1], marker="X", c="r") +ax2.set_title("MES") +ax3.contourf(xv.numpy(), yv.numpy(), evals["EI"].numpy(), levels=20) +ax3.scatter(candidates["EI"][:, 0], candidates["EI"][:, 1], marker="X", c="r") +ax3.set_title("EI") +ax4.contourf(xv.numpy(), yv.numpy(), evals["PI"].numpy(), levels=20) +ax4.scatter(candidates["PI"][:, 0], candidates["PI"][:, 1], marker="X", c="r") +ax4.set_title("PI") +fig.text(0.5, -0.1, "x_1", ha="center") +fig.text(-0.1, 0.5, "x_2", va="center") + +# plot computational overheads +plt.figure() +heights = [times[acq] for acq in acqs.keys()] +plt.bar(acqs.keys(), heights) +plt.ylabel("Computation Time") +plt.xlabel("Acquisition Function") + diff --git a/website-old/static/files/Multi_objective_multi_fidelity_BO.ipynb b/website-old/static/files/Multi_objective_multi_fidelity_BO.ipynb new file mode 100644 index 0000000000..d2828c14c9 --- /dev/null +++ b/website-old/static/files/Multi_objective_multi_fidelity_BO.ipynb @@ -0,0 +1,1305 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "4be3ab76-0dde-4daa-81b7-68df50547590", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "# Multi-fidelity Multi-Objective optimization " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8fb7e5e3-8176-4b7f-b9b6-9a20bfb7ad37", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "In this tutorial notebook we demonstrate how to perform multi-objective multi-fidelity optimization in BoTorch using the multi-fidelity Hypervolume Knowledge Gradient (MF-HVKG) [3] and a method called Multi-Objective Multi-Fidelity (MOMF) [1]. \n", + "\n", + "MF-HVKG performs one-step lookahead: it operates under the assumption that we can make one additional observation, and after receiving that additional observation, we will select the Pareto set of optimal designs. HVKG seeks to select the design `x` to evaluate that maximizes the value of information about the Pareto set by maximizing the hypervolume under the posterior mean (conditional on receiving on new observation for the design `x`).\n", + "\n", + "MOMF is an alternative approach that introduces an additional \"fidelity objective\" that is optimized along with the problem objectives. This fidelity objective can be thought of as a trust objective that rewards the optimization when going to higher fidelity. Thus, the MOMF explicitly optimizes for getting more high-fidelity (trustworthy) data while taking into account the higher computational costs associated with it.\n", + "\n", + "HVKG is generally more cost efficient [3], since it explicitly targets the goal of MF optimization: select design points and fidelities that enable identifying about the Pareto Frontier at the target fidelity in a cost-aware fashion. MOMF will typically result in faster candidate generation. If the application is high-throughput and requires fast candidate generation, MOMF will be preferable. Otherwise, MF-HVKG will likely give better sample efficiency and performance [3].\n", + "\n", + "In this tutorial, we will optimize a synthetic function that is a modified multi-fidelity Branin-Currin [1]. This is a 3-dimesional, bi-objective problem with one of the input dimensions being the fidelity. For the MOMF, this results in a 3-objective problem since it also takes the fidelity objective into account. In this case the fidelity objective is a linear function of fidelity, $ f(s)=s$, where $s$ is the fidelity. The MOMF algorithm can accept any discrete or continuous cost functions as an input. In this example, we choose an exponential dependency of the form $C(s)=\\exp(4.8s)$. The goal of the optimization is to find the Pareto front, which is a trade-off solution set for Multi-objective problems, at the highest fidelity. \n", + "\n", + "Note: pymoo is an optional dependency that is used for determining the Pareto set of optimal designs under the model posterior mean using NSGA-II (which is not a sample efficient method, but sample efficiency is not critical for this step). If pymoo is not available, the Pareto set of optimal designs is selected from a discrete set. This will work okay for low-dim (e.g. \n", + " dimensions) problems, but in general NSGA-II will yield far better results.\n", + "\n", + "[1] [Irshad, Faran, Stefan Karsch, and Andreas Döpp. \"Expected hypervolume improvement for simultaneous multi-objective and multi-fidelity optimization.\" arXiv preprint arXiv:2112.13901 (2021).](https://arxiv.org/abs/2112.13901)\n", + "\n", + "[2] [S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives. NeurIPS, 2021.](https://proceedings.neurips.cc/paper/2021/hash/11704817e347269b7254e744b5e22dac-Abstract.html)\n", + "\n", + "[3] [S. Daulton, M. Balandat, and E. Bakshy. Hypervolume Knowledge Gradient for Multi-Objective Bayesian Optimization with Partial Information. ICML, 2023.](https://proceedings.mlr.press/v202/daulton23a.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1700239921224, + "executionStopTime": 1700239921225, + "hidden_ranges": [], + "originalKey": "f53fac8a-e151-4f47-ac57-e3c49bc10fb8", + "output": { + "id": 1104960884022207, + "loadingStatus": "loaded" + }, + "outputsInitialized": true, + "requestMsgId": "8ae17837-397d-4cff-a52a-82c4a268d159" + }, + "outputs": [], + "source": [ + "import os\n", + "from typing import Callable, Dict\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "313624cb-6e9b-4419-aa46-53bc5dfdbae6", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Set dtype and device \n", + "Setting up the global variable that determine the device to run the optimization. The optimization is much faster when it runs on GPU." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1700239921898, + "executionStopTime": 1700239921904, + "hidden_ranges": [], + "originalKey": "e7934480-1e9d-484c-8d49-d15d4cfcccc4", + "outputsInitialized": false, + "requestMsgId": "a2204a02-e2a6-4a9a-92db-effb9607bb05" + }, + "outputs": [], + "source": [ + "tkwargs = { # Tkwargs is a dictionary contaning data about data type and data device\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", + "}\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6ab54ce1-7768-4460-b965-369fa1c4f664", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Define the problem and optimization settings" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1700239922349, + "executionStopTime": 1700239922400, + "hidden_ranges": [], + "originalKey": "1bffa4ee-f329-4e1a-be27-554eb1b7419b", + "outputsInitialized": false, + "requestMsgId": "76495050-3917-414e-94f4-d3bcacd0cfb5" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode\n" + ] + } + ], + "source": [ + "from botorch.test_functions.multi_objective_multi_fidelity import MOMFBraninCurrin\n", + "\n", + "BC = MOMFBraninCurrin(negate=True).to(**tkwargs)\n", + "dim_x = BC.dim\n", + "dim_y = BC.num_objectives\n", + "\n", + "ref_point = torch.zeros(dim_y, **tkwargs)\n", + "\n", + "\n", + "BATCH_SIZE = 1 # For batch optimization, BATCH_SIZE should be greater than 1\n", + "# This evaluation budget is set to be very low to make the notebook run fast. This should be much higher.\n", + "EVAL_BUDGET = 2.05 # in terms of the number of full-fidelity evaluations\n", + "n_INIT = 2 # Initialization budget in terms of the number of full-fidelity evaluations\n", + "# Number of Monte Carlo samples, used to approximate MOMF\n", + "MC_SAMPLES = 2 if SMOKE_TEST else 128\n", + "# Number of restart points for multi-start optimization\n", + "NUM_RESTARTS = 2 if SMOKE_TEST else 10\n", + "# Number of raw samples for initial point selection heuristic\n", + "RAW_SAMPLES = 4 if SMOKE_TEST else 512\n", + "\n", + "standard_bounds = torch.zeros(2, dim_x, **tkwargs)\n", + "standard_bounds[1] = 1\n", + "# mapping from index to target fidelity (highest fidelity)\n", + "target_fidelities = {2: 1.0}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6a44ae07-fe2e-49e1-9825-e4cc13bff750", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Helper functions to define Cost \n", + "\n", + "The cost_func function returns an exponential cost from the fidelity. The cost_callable is a wrapper around it that takes care of the input output shapes. This is provided to the MF algorithms which inversely weight the expected utility by the cost." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "customOutput": null, + "executionStartTime": 1700239923177, + "executionStopTime": 1700239923187, + "originalKey": "248720f2-59b3-4e39-848a-ada6084f8b89", + "output": { + "id": 603532655274696, + "loadingStatus": "loaded" + }, + "outputsInitialized": true, + "requestMsgId": "a77f8c50-0b31-42a4-a036-2223c7da5474" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Min Cost: 1.0\n", + "Max Cost: 121.51041751873485\n" + ] + } + ], + "source": [ + "from math import exp\n", + "\n", + "\n", + "def cost_func(x):\n", + " \"\"\"A simple exponential cost function.\"\"\"\n", + " exp_arg = torch.tensor(4.8, **tkwargs)\n", + " val = torch.exp(exp_arg * x)\n", + " return val\n", + "\n", + "\n", + "# Displaying the min and max costs for this optimization\n", + "print(f\"Min Cost: {cost_func(0)}\")\n", + "print(f\"Max Cost: {cost_func(1)}\")\n", + "\n", + "\n", + "def cost_callable(X: torch.Tensor) -> torch.Tensor:\n", + " r\"\"\"Wrapper for the cost function that takes care of shaping\n", + " input and output arrays for interfacing with cost_func.\n", + " This is passed as a callable function to MOMF.\n", + "\n", + " Args:\n", + " X: A `batch_shape x q x d`-dim Tensor\n", + " Returns:\n", + " Cost `batch_shape x q x m`-dim Tensor of cost generated\n", + " from fidelity dimension using cost_func.\n", + " \"\"\"\n", + "\n", + " return cost_func(X[..., -1:])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "2f3f70a4-3654-4e65-ab6c-69b1e16ab3d6", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Model Initialization \n", + "We use a multi-output SingleTaskGP to model the problem with a homoskedastic Gaussian likelihood with an inferred noise level. \n", + "The model is initialized with random points, where the fidelity is sampled from a probability distribution with a PDF that is inversely proportional to the cost: $p(s)=C(s)^{-1}$. The initialization is given a budget equivalent to 2 full-fidelity evaluations." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "customOutput": null, + "executionStartTime": 1700239924281, + "executionStopTime": 1700239924318, + "originalKey": "b35c722b-c75a-4e13-ab18-ff9fd01fad95", + "outputsInitialized": false, + "requestMsgId": "dcdc2b90-8d52-479b-9aea-e7a69bf194a3" + }, + "outputs": [], + "source": [ + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.model_list_gp_regression import ModelListGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.utils.transforms import normalize\n", + "from gpytorch.kernels import MaternKernel, ScaleKernel\n", + "from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood\n", + "from gpytorch.priors import GammaPrior\n", + "\n", + "\n", + "def inv_transform(u):\n", + " # define inverse transform to sample from the probability distribution with\n", + " # PDF proportional to 1/(c(x))\n", + " # u is a uniform(0,1) rv\n", + " return 5 / 24 * torch.log(-exp(24 / 5) / (exp(24 / 5) * u - u - exp(24 / 5)))\n", + "\n", + "\n", + "def gen_init_data(n: int):\n", + " r\"\"\"\n", + " Generates the initial data. Sample fidelities inversely proportional to cost.\n", + " \"\"\"\n", + " # total cost budget is n\n", + " train_x = torch.empty(\n", + " 0, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device\n", + " )\n", + " total_cost = 0\n", + " # assume target fidelity is 1\n", + " total_cost_limit = (\n", + " n\n", + " * cost_callable(\n", + " torch.ones(\n", + " 1, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device\n", + " )\n", + " ).item()\n", + " )\n", + " while total_cost < total_cost_limit:\n", + " new_x = torch.rand(\n", + " 1, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device\n", + " )\n", + " new_x[:, -1] = inv_transform(new_x[:, -1])\n", + " total_cost += cost_callable(new_x)\n", + " train_x = torch.cat([train_x, new_x], dim=0)\n", + " train_x = train_x[:-1]\n", + " train_obj = BC(train_x)\n", + " return train_x, train_obj\n", + "\n", + "\n", + "def initialize_model(train_x, train_obj, state_dict=None):\n", + " \"\"\"Initializes a ModelList with Matern 5/2 Kernel and returns the model and its MLL.\n", + "\n", + " Note: a batched model could also be used here.\n", + " \"\"\"\n", + " models = []\n", + " for i in range(train_obj.shape[-1]):\n", + " m = SingleTaskGP(\n", + " train_x,\n", + " train_obj[:, i : i + 1],\n", + " train_Yvar=torch.full_like(train_obj[:, i : i + 1], 1e-6),\n", + " outcome_transform=Standardize(m=1),\n", + " covar_module=ScaleKernel(\n", + " MaternKernel(\n", + " nu=2.5,\n", + " ard_num_dims=train_x.shape[-1],\n", + " lengthscale_prior=GammaPrior(2.0, 2.0),\n", + " ),\n", + " outputscale_prior=GammaPrior(2.0, 0.15),\n", + " ),\n", + " )\n", + " models.append(m)\n", + " model = ModelListGP(*models)\n", + " mll = SumMarginalLogLikelihood(model.likelihood, model)\n", + " if state_dict is not None:\n", + " model.load_state_dict(state_dict=state_dict)\n", + " return mll, model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "dcc1666f-f387-40b0-8137-25b133d22dd2", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Helper function to optimize acquisition function \n", + "This is a helper function that initializes, optimizes the acquisition function MOMF and returns the new_x and new_obj. The problem is called from within this helper function.\n", + "\n", + "A simple initialization heuristic is used to select the 20 restart initial locations from a set of 1024 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1700239926777, + "executionStopTime": 1700239926816, + "hidden_ranges": [], + "originalKey": "9095cc6a-0e14-4be1-ae9b-e56535e2dcba", + "outputsInitialized": false, + "requestMsgId": "fe16757e-7f35-462b-a5ca-cd0ddf689f79" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.multi_objective.multi_fidelity import MOMF\n", + "from botorch.optim.optimize import optimize_acqf\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "from botorch.utils.multi_objective.box_decompositions.non_dominated import (\n", + " FastNondominatedPartitioning,\n", + ")\n", + "from botorch.utils.transforms import unnormalize\n", + "\n", + "\n", + "dim_y_momf = dim_y + 1 # Output Dimesnion for MOMF optimization\n", + "ref_point_momf = torch.zeros(dim_y_momf, **tkwargs)\n", + "\n", + "\n", + "def fid_obj(X: torch.Tensor) -> torch.Tensor:\n", + " \"\"\"\n", + " A Fidelity Objective that can be thought of as a trust objective.\n", + " Higher Fidelity simulations are rewarded as being more\n", + " trustworthy. Here we consider just a linear fidelity objective.\n", + " \"\"\"\n", + " fid_obj = 1 * X[..., -1]\n", + " return fid_obj\n", + "\n", + "\n", + "def get_objective_momf(x: torch.Tensor) -> torch.Tensor:\n", + " \"\"\"Wrapper around the Objective function to take care of fid_obj stacking\"\"\"\n", + " y = BC(x) # The Branin-Currin is called\n", + " fid = fid_obj(x) # Getting the fidelity objective values\n", + " fid_out = fid.unsqueeze(-1)\n", + " # Concatenating objective values with fid_objective\n", + " y_out = torch.cat([y, fid_out], -1)\n", + " return y_out\n", + "\n", + "\n", + "def optimize_MOMF_and_get_obs(\n", + " model: SingleTaskGP,\n", + " train_obj: torch.Tensor,\n", + " sampler: SobolQMCNormalSampler,\n", + " ref_point: torch.Tensor,\n", + " standard_bounds: torch.Tensor,\n", + " BATCH_SIZE: int,\n", + " cost_call: Callable[[torch.Tensor], torch.Tensor],\n", + "):\n", + " \"\"\"\n", + " Wrapper to call MOMF and optimizes it in a sequential greedy\n", + " fashion returning a new candidate and evaluation\n", + " \"\"\"\n", + " partitioning = FastNondominatedPartitioning(ref_point=ref_point, Y=train_obj)\n", + " acq_func = MOMF(\n", + " model=model,\n", + " ref_point=ref_point, # use known reference point\n", + " partitioning=partitioning,\n", + " sampler=sampler,\n", + " cost_call=cost_call,\n", + " )\n", + " # Optimization\n", + " candidates, vals = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=standard_bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\n", + " \"batch_limit\": 5,\n", + " \"maxiter\": 3 if SMOKE_TEST else 200,\n", + " \"nonnegative\": True,\n", + " },\n", + " sequential=True,\n", + " )\n", + " # if the AF val is 0, set the fidelity parameter to zero\n", + " if vals.item() == 0.0:\n", + " candidates[:, -1] = 0.0\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=standard_bounds)\n", + " new_obj = get_objective_momf(new_x)\n", + " return new_x, new_obj" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "originalKey": "f580b5fe-d172-4d7f-a40a-98bc8acdd0f8", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Define helper functions for MF-HVKG\n", + "\n", + "`get_current_value` optimizes the current posterior mean at the full fidelity to determine the hypervolume under the current model.\n", + "\n", + "`optimize_HVKG_and_get_obs` creates the MF-HVKG acquisition function, optimizes it, and returns the new design and corresponding observation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "customInput": null, + "executionStartTime": 1700239931973, + "executionStopTime": 1700239932009, + "originalKey": "1d61db27-bd71-4e4c-bffc-b8279771e682", + "outputsInitialized": false, + "requestMsgId": "e3e094aa-d96f-42f2-b499-0bdce21228d6", + "showInput": true + }, + "outputs": [], + "source": [ + "from botorch.acquisition.cost_aware import InverseCostWeightedUtility\n", + "from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction\n", + "from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import (\n", + " _get_hv_value_function,\n", + " qMultiFidelityHypervolumeKnowledgeGradient,\n", + ")\n", + "from botorch.acquisition.utils import project_to_target_fidelity\n", + "from botorch.models.deterministic import GenericDeterministicModel\n", + "from torch import Tensor\n", + "\n", + "NUM_INNER_MC_SAMPLES = 2 if SMOKE_TEST else 32\n", + "NUM_PARETO = 1 if SMOKE_TEST else 10\n", + "NUM_FANTASIES = 2 if SMOKE_TEST else 8\n", + "\n", + "\n", + "def get_current_value(\n", + " model: SingleTaskGP,\n", + " ref_point: torch.Tensor,\n", + " bounds: torch.Tensor,\n", + " normalized_target_fidelities: Dict[int, float],\n", + "):\n", + " \"\"\"Helper to get the hypervolume of the current hypervolume\n", + " maximizing set.\n", + " \"\"\"\n", + " fidelity_dims, fidelity_targets = zip(*normalized_target_fidelities.items())\n", + " # optimize\n", + " non_fidelity_dims = list(set(range(dim_x)) - set(fidelity_dims))\n", + " curr_val_acqf = FixedFeatureAcquisitionFunction(\n", + " acq_function=_get_hv_value_function(\n", + " model=model,\n", + " ref_point=ref_point,\n", + " sampler=SobolQMCNormalSampler(\n", + " sample_shape=torch.Size([NUM_INNER_MC_SAMPLES]),\n", + " ),\n", + " use_posterior_mean=True,\n", + " ),\n", + " d=dim_x,\n", + " columns=fidelity_dims,\n", + " values=fidelity_targets,\n", + " )\n", + " # optimize\n", + " _, current_value = optimize_acqf(\n", + " acq_function=curr_val_acqf,\n", + " bounds=bounds[:, non_fidelity_dims],\n", + " q=NUM_PARETO,\n", + " num_restarts=1,\n", + " raw_samples=2 * RAW_SAMPLES,\n", + " return_best_only=True,\n", + " options={\n", + " \"nonnegative\": True,\n", + " \"maxiter\": 3 if SMOKE_TEST else 200,\n", + " },\n", + " )\n", + " return current_value\n", + "\n", + "\n", + "normalized_target_fidelities = {}\n", + "for idx, fidelity in target_fidelities.items():\n", + " lb = standard_bounds[0, idx].item()\n", + " ub = standard_bounds[1, idx].item()\n", + " normalized_target_fidelities[idx] = (fidelity - lb) / (ub - lb)\n", + "project_d = dim_x\n", + "\n", + "\n", + "def project(X: Tensor) -> Tensor:\n", + "\n", + " return project_to_target_fidelity(\n", + " X=X,\n", + " d=project_d,\n", + " target_fidelities=normalized_target_fidelities,\n", + " )\n", + "\n", + "\n", + "def optimize_HVKG_and_get_obs(\n", + " model: SingleTaskGP,\n", + " ref_point: torch.Tensor,\n", + " standard_bounds: torch.Tensor,\n", + " BATCH_SIZE: int,\n", + " cost_call: Callable[[torch.Tensor], torch.Tensor],\n", + "):\n", + " \"\"\"Utility to initialize and optimize HVKG.\"\"\"\n", + " cost_model = GenericDeterministicModel(cost_call)\n", + " cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)\n", + " current_value = get_current_value(\n", + " model=model,\n", + " ref_point=ref_point,\n", + " bounds=standard_bounds,\n", + " normalized_target_fidelities=normalized_target_fidelities,\n", + " )\n", + "\n", + " acq_func = qMultiFidelityHypervolumeKnowledgeGradient(\n", + " model=model,\n", + " ref_point=ref_point, # use known reference point\n", + " num_fantasies=NUM_FANTASIES,\n", + " num_pareto=NUM_PARETO,\n", + " current_value=current_value,\n", + " cost_aware_utility=cost_aware_utility,\n", + " target_fidelities=normalized_target_fidelities,\n", + " project=project,\n", + " )\n", + " # Optimization\n", + " candidates, vals = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=standard_bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=1,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\n", + " \"batch_limit\": 5,\n", + " \"maxiter\": 3 if SMOKE_TEST else 200,\n", + " },\n", + " )\n", + " # if the AF val is 0, set the fidelity parameter to zero\n", + " if vals.item() == 0.0:\n", + " candidates[:, -1] = 0.0\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=BC.bounds)\n", + " new_obj = BC(new_x)\n", + " return new_x, new_obj" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "bdb86f2e-6774-4b00-af78-d081948812a5", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Define helper functions for MF-HVKG\n", + "\n", + "We run MOMF to optimize the multi-fidelity versions of the Branin-Currin functions. The optimization loop works in the following sequence. \n", + "\n", + "1. At the start with an initialization equivalent to 2 full fidelity evaluations.\n", + "2. The models are used to generate an acquisition function that is optimized to select new input parameters\n", + "3. The objective function is evaluated at the suggested new_x and returns a new_obj.\n", + "4. The models are updated with the new points and then are used again to make the next prediction.\n", + "\n", + "The evaluation budget for the optimization is set to 4 full fidelity evaluations.\n", + "\n", + "Note: running this takes some time.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1700239933421, + "executionStopTime": 1700239933474, + "originalKey": "ec218c40-b012-40cc-bc28-ff0b2bedc941", + "outputsInitialized": false, + "requestMsgId": "e5e1ca9f-350b-4d84-85cc-b5765bd5479e", + "showInput": true + }, + "outputs": [], + "source": [ + "from botorch import fit_gpytorch_mll" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1700239934173, + "executionStopTime": 1700239960450, + "hidden_ranges": [], + "originalKey": "d250edba-f44e-4630-a1d1-90d050115f80", + "output": { + "id": 360706396734736, + "loadingStatus": "loaded" + }, + "outputsInitialized": false, + "requestMsgId": "a740fdb3-387b-46f7-9715-1247be0f0c7b" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 7s, sys: 238 ms, total: 1min 7s\n", + "Wall time: 19.5 s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# Intializing train_x to zero\n", + "verbose = False\n", + "torch.manual_seed(0)\n", + "train_x_momf, _ = gen_init_data(n_INIT)\n", + "train_obj_momf = get_objective_momf(train_x_momf)\n", + "# Generate Sampler\n", + "momf_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + "\n", + "# run N_BATCH rounds of BayesOpt after the initial random batch\n", + "iteration = 0\n", + "total_cost = cost_callable(train_x_momf).sum().item()\n", + "while total_cost < EVAL_BUDGET * cost_func(1):\n", + " if verbose:\n", + " print(f\"cost: {total_cost}\")\n", + "\n", + " # reinitialize the models so they are ready for fitting on next iteration\n", + " mll, model = initialize_model(normalize(train_x_momf, BC.bounds), train_obj_momf)\n", + "\n", + " fit_gpytorch_mll(mll=mll) # Fit the model\n", + "\n", + " # optimize acquisition functions and get new observations\n", + " new_x, new_obj = optimize_MOMF_and_get_obs(\n", + " model=model,\n", + " train_obj=train_obj_momf,\n", + " sampler=momf_sampler,\n", + " ref_point=ref_point_momf,\n", + " standard_bounds=standard_bounds,\n", + " BATCH_SIZE=BATCH_SIZE,\n", + " cost_call=cost_callable,\n", + " )\n", + " # Updating train_x and train_obj\n", + " train_x_momf = torch.cat([train_x_momf, new_x], dim=0)\n", + " train_obj_momf = torch.cat([train_obj_momf, new_obj], dim=0)\n", + " iteration += 1\n", + " total_cost += cost_callable(new_x).sum().item()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "executionStartTime": 1699738891403, + "executionStopTime": 1699738891407, + "originalKey": "2e193c2e-f4c4-4d2c-b592-f51a6cc3d5a9", + "outputsInitialized": false, + "requestMsgId": "2e193c2e-f4c4-4d2c-b592-f51a6cc3d5a9", + "showInput": false + }, + "source": [ + "### Run MF-HVKG" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1700239960506, + "executionStopTime": 1700240950614, + "originalKey": "4a9f3f42-dd1f-45f2-9125-511f72ad337a", + "output": { + "id": 1036214770804288, + "loadingStatus": "loaded" + }, + "outputsInitialized": false, + "requestMsgId": "de9c995c-66dd-49de-b8cf-dc834e52fc38", + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 16min 30s, sys: 3.03 s, total: 16min 33s\n", + "Wall time: 4min 36s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "torch.manual_seed(0)\n", + "train_x_kg, train_obj_kg = gen_init_data(n_INIT)\n", + "MF_n_INIT = train_x_kg.shape[0]\n", + "iteration = 0\n", + "total_cost = cost_callable(train_x_kg).sum().item()\n", + "while total_cost < EVAL_BUDGET * cost_func(1):\n", + " if verbose:\n", + " print(f\"cost: {total_cost}\")\n", + "\n", + " # reinitialize the models so they are ready for fitting on next iteration\n", + " mll, model = initialize_model(normalize(train_x_kg, BC.bounds), train_obj_kg)\n", + "\n", + " fit_gpytorch_mll(mll=mll) # Fit the model\n", + " # optimize acquisition functions and get new observations\n", + " new_x, new_obj = optimize_HVKG_and_get_obs(\n", + " model=model,\n", + " ref_point=ref_point,\n", + " standard_bounds=standard_bounds,\n", + " BATCH_SIZE=BATCH_SIZE,\n", + " cost_call=cost_callable,\n", + " )\n", + " # Updating train_x and train_obj\n", + " train_x_kg = torch.cat([train_x_kg, new_x], dim=0)\n", + " train_obj_kg = torch.cat([train_obj_kg, new_obj], dim=0)\n", + " iteration += 1\n", + " total_cost += cost_callable(new_x).sum().item()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "bc189d13-6ad9-4687-8e67-c506dcc1dad1", + "outputsInitialized": true, + "showInput": false + }, + "source": [ + "### Result: Evaluating the Pareto front at the highest fidelity using NSGA-II on the posterior mean\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "customOutput": null, + "executionStartTime": 1700240950637, + "executionStopTime": 1700240950639, + "originalKey": "275b1f0b-4d84-4318-99b1-c1a7505bf1a4", + "outputsInitialized": false, + "requestMsgId": "19199126-7e1c-4adb-88bb-35d42d1f26af" + }, + "outputs": [], + "source": [ + "from botorch.utils.multi_objective.pareto import (\n", + " _is_non_dominated_loop,\n", + " is_non_dominated,\n", + ")\n", + "from gpytorch import settings\n", + "\n", + "try:\n", + " # Note: These are the pymoo 0.6+ imports, if you happen to be stuck on\n", + " # an older pymoo version you need to replace them with the ones below.\n", + " from pymoo.algorithms.moo.nsga2 import NSGA2\n", + " from pymoo.core.problem import Problem\n", + " from pymoo.optimize import minimize\n", + " from pymoo.termination.max_gen import MaximumGenerationTermination\n", + "\n", + " # from pymoo.algorithms.nsga2 import NSGA2\n", + " # from pymoo.model.problem import Problem\n", + " # from pymoo.util.termination.max_gen import MaximumGenerationTermination\n", + "\n", + " def get_pareto(\n", + " model,\n", + " non_fidelity_indices,\n", + " project,\n", + " population_size=20 if SMOKE_TEST else 250,\n", + " max_gen=10 if SMOKE_TEST else 100,\n", + " is_mf_model=True,\n", + " ):\n", + " \"\"\"Optimize the posterior mean using NSGA-II.\"\"\"\n", + " tkwargs = {\n", + " \"dtype\": BC.ref_point.dtype,\n", + " \"device\": BC.ref_point.device,\n", + " }\n", + " dim = len(non_fidelity_indices)\n", + "\n", + " class PosteriorMeanPymooProblem(Problem):\n", + " def __init__(self):\n", + " super().__init__(\n", + " n_var=dim,\n", + " n_obj=BC.num_objectives,\n", + " type_var=np.double,\n", + " )\n", + " self.xl = np.zeros(dim)\n", + " self.xu = np.ones(dim)\n", + "\n", + " def _evaluate(self, x, out, *args, **kwargs):\n", + " X = torch.from_numpy(x).to(**tkwargs)\n", + " if is_mf_model:\n", + " X = project(X)\n", + " with torch.no_grad():\n", + " with settings.cholesky_max_tries(9):\n", + " # eval in batch mode\n", + " y = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2)\n", + " out[\"F\"] = -y.cpu().numpy()\n", + "\n", + " pymoo_problem = PosteriorMeanPymooProblem()\n", + " algorithm = NSGA2(\n", + " pop_size=population_size,\n", + " eliminate_duplicates=True,\n", + " )\n", + " res = minimize(\n", + " pymoo_problem,\n", + " algorithm,\n", + " termination=MaximumGenerationTermination(max_gen),\n", + " seed=0, # fix seed\n", + " verbose=False,\n", + " )\n", + " X = torch.tensor(\n", + " res.X,\n", + " **tkwargs,\n", + " )\n", + " # project to full fidelity\n", + " if is_mf_model:\n", + " if project is not None:\n", + " X = project(X)\n", + " # determine Pareto set of designs under model\n", + " with torch.no_grad():\n", + " preds = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2)\n", + " pareto_mask = is_non_dominated(preds)\n", + " X = X[pareto_mask]\n", + " # evaluate Pareto set of designs on true function and compute hypervolume\n", + " if not is_mf_model:\n", + " X = project(X)\n", + " X = unnormalize(X, BC.bounds)\n", + " Y = BC(X)\n", + " # compute HV\n", + " partitioning = FastNondominatedPartitioning(ref_point=BC.ref_point, Y=Y)\n", + " return partitioning.compute_hypervolume().item()\n", + "\n", + "except ImportError:\n", + " NUM_DISCRETE_POINTS = 10 if SMOKE_TEST else 100000\n", + " CHUNK_SIZE = 512\n", + "\n", + " def get_pareto(\n", + " model,\n", + " non_fidelity_indices,\n", + " project,\n", + " population_size=20 if SMOKE_TEST else 250,\n", + " max_gen=10 if SMOKE_TEST else 100,\n", + " is_mf_model=True,\n", + " ):\n", + " \"\"\"Optimize the posterior mean over a discrete set.\"\"\"\n", + " tkwargs = {\n", + " \"dtype\": BC.ref_point.dtype,\n", + " \"device\": BC.ref_point.device,\n", + " }\n", + " dim_x = BC.dim\n", + "\n", + " discrete_set = torch.rand(NUM_DISCRETE_POINTS, dim_x - 1, **tkwargs)\n", + " if is_mf_model:\n", + " discrete_set = project(discrete_set)\n", + " discrete_set[:, -1] = 1.0 # set to target fidelity\n", + " with torch.no_grad():\n", + " preds_list = []\n", + " for start in range(0, NUM_DISCRETE_POINTS, CHUNK_SIZE):\n", + " preds = model.posterior(\n", + " discrete_set[start : start + CHUNK_SIZE].unsqueeze(-2)\n", + " ).mean.squeeze(-2)\n", + " preds_list.append(preds)\n", + " preds = torch.cat(preds_list, dim=0)\n", + " pareto_mask = _is_non_dominated_loop(preds)\n", + " pareto_X = discrete_set[pareto_mask]\n", + " if not is_mf_model:\n", + " pareto_X = project(pareto_X)\n", + " pareto_X = unnormalize(pareto_X, BC.bounds)\n", + " Y = BC(pareto_X)\n", + " # compute HV\n", + " partitioning = FastNondominatedPartitioning(ref_point=BC.ref_point, Y=Y)\n", + " return partitioning.compute_hypervolume().item()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "originalKey": "ad70a987-1877-4bc1-a4cd-49fca37dff2b", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "## Evaluate MF-HVKG\n", + "\n", + "We evaluate performance after every 5 evaluations (this is to speed things up, since there are many observations)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1700240950661, + "executionStopTime": 1700241053956, + "originalKey": "41ef8566-7400-441c-8087-d5842df6117d", + "output": { + "id": 1868435296927876, + "loadingStatus": "loaded" + }, + "outputsInitialized": false, + "requestMsgId": "7ff2bd5b-cf9b-4843-9b7b-9fc463b52385", + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 20s, sys: 240 ms, total: 1min 21s\n", + "Wall time: 30.6 s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "hvs_kg = []\n", + "costs = []\n", + "for i in range(MF_n_INIT, train_x_kg.shape[0] + 1, 5):\n", + "\n", + " mll, model = initialize_model(\n", + " normalize(train_x_kg[:i], BC.bounds), train_obj_kg[:i]\n", + " )\n", + " fit_gpytorch_mll(mll)\n", + " hypervolume = get_pareto(model, project=project, non_fidelity_indices=[0, 1])\n", + " hvs_kg.append(hypervolume)\n", + " costs.append(cost_callable(train_x_kg[:i]).sum().item())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "executionStartTime": 1699739200762, + "executionStopTime": 1699739200768, + "originalKey": "11350000-d066-49a3-b950-f7633e1c507a", + "outputsInitialized": false, + "requestMsgId": "11350000-d066-49a3-b950-f7633e1c507a", + "showInput": false + }, + "source": [ + "## Evaluate MOMF\n", + "\n", + "We evaluate performance after every evaluation (there are not as many evaluations since MOMF queries higher fidelities more frequently)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1700241054021, + "executionStopTime": 1700241163531, + "originalKey": "7770db5f-be4d-40eb-b525-c6d18932c198", + "output": { + "id": 370365252377373, + "loadingStatus": "loaded" + }, + "outputsInitialized": false, + "requestMsgId": "89819efc-dd7a-42cf-8144-d9b6480bb5ae", + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n", + "/home/alexander/.local/lib/python3.10/site-packages/gpytorch/likelihoods/noise_models.py:148: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 18s, sys: 260 ms, total: 1min 19s\n", + "Wall time: 28.4 s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "hvs_momf = []\n", + "costs_momf = []\n", + "for i in range(MF_n_INIT, train_x_momf.shape[0] + 1):\n", + "\n", + " mll, model = initialize_model(\n", + " normalize(train_x_momf[:i], BC.bounds), train_obj_momf[:i, :2]\n", + " )\n", + " fit_gpytorch_mll(mll)\n", + " hypervolume = get_pareto(model, project=project, non_fidelity_indices=[0, 1])\n", + " hvs_momf.append(hypervolume)\n", + " costs_momf.append(cost_callable(train_x_momf[:i]).sum().item())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "originalKey": "f9e4b24a-6f83-4dd3-b1a4-9a3ebc231813", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Plot log inference hypervolume regret (under the model) vs cost\n", + "\n", + "Log inference hypervolume regret, defined as the logarithm of the difference between the maximum hypervolume dominated by the Pareto frontier and the hypervolume corresponding to the Pareto set identified by each algorithm, is a performance evaluation criterion for multi-information source multi-objective optimization [3]." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "customInput": null, + "executionStartTime": 1700241163589, + "executionStopTime": 1700241164394, + "originalKey": "0f68e995-1461-40bc-b71b-40d66dabefca", + "output": { + "id": 1032923858006252, + "loadingStatus": "loaded" + }, + "outputsInitialized": true, + "requestMsgId": "0f0468b8-cbaf-47a1-b66a-200ebc3f5d89", + "showInput": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAksAAAG0CAYAAAAikWNTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABvAklEQVR4nO3dd3xUVfrH8c9Mek8gCaGEQAgkdBER6ShdV1Fcd1EsiLprRQVxZVXsYi9r42cDdW27Krp2AemiKBhFCYGEEkoKENJ75v7+GDIkppBJZjKZ5Pt+ve4rmTt3zn3mOjJPzjn3OSbDMAxEREREpE5mVwcgIiIi0popWRIRERFpgJIlERERkQYoWRIRERFpgJIlERERkQYoWRIRERFpgJIlERERkQYoWRIRERFpgJIlERERkQYoWRIRERFpgNskSw899BAjR47E39+f0NBQu19/7bXXYjKZeOaZZ2rsz87OZtasWQQHBxMaGspVV11FQUGBY4IWERERt+fp6gAaq6ysjIsuuogRI0bw2muv2fXa5cuX8/3339OlS5daz82aNYv09HRWrFhBeXk5V155JX/729945513Gt2+xWLh0KFDBAUFYTKZ7IpNREREXMMwDPLz8+nSpQtmcwP9R4abWbp0qRESEtLo4w8cOGB07drV+O2334yYmBjj6aeftj23fft2AzB+/PFH274vv/zSMJlMxsGDBxt9jv379xuANm3atGnTps0Nt/379zf4Pe82PUtNYbFYuOyyy1iwYAH9+/ev9fymTZsIDQ3ltNNOs+2bOHEiZrOZH374gQsuuKDOdktLSyktLbU9NgwDgP379xMcHOzgdyEiIiLOkJeXR3R0NEFBQQ0e16aTpUcffRRPT0/mzp1b5/MZGRlERkbW2Ofp6UmHDh3IyMiot93Fixdz33331dofHBysZElERMTNnGwKjUsneN9xxx2YTKYGtx07djSp7S1btvDss8+ybNkyh88jWrhwIbm5ubZt//79Dm1fREREWg+X9izNnz+f2bNnN3hMbGxsk9pev349WVlZdO/e3bavsrKS+fPn88wzz7B3716ioqLIysqq8bqKigqys7OJioqqt20fHx98fHyaFJeIiIi4F5cmSxEREURERDil7csuu4yJEyfW2DdlyhQuu+wyrrzySgBGjBhBTk4OW7ZsYejQoQB8++23WCwWhg8f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vpO13pKSJRGRJigrgs/ng6mer26T2fp8WVHLxnXc2LFj6dixI2+88Uat59atW0dKSgpXXXVVna+95557+Oc//8knn3zCzJkzAdi8eTOnnnoqSUlJvPDCC/z222+sWbOGq6++mpdeeonff//dqe+nOt0NJy0mPiqIr37PULIkItIUVZO66xtqMywnJnufdWfLxgZ4eXlx2WWXsWzZMv75z3/WeO71119n+PDh9O/fv8Z+wzCYO3cu//73v1mxYgUjR4607Z89ezZ9+vRh48aNmM0nEsTevXtz8cUXY7TgkKN6ltzRoUT4+k745b2THtqaVFXyTs5UsiQiUktZYf1bZlLNSd31qWuyd13tOclVV13Frl27WLfuxLzagoICPvjgg1q9ShUVFVx66aV88MEHrF271pYoASQmJpKUlMRtt91WI1GqzmRquTus1bPkjtI2wabnrXU1Bs90dTSN1ud4srQzM59Ki6FSAiIi1T3cpf7n/DsCjexJsZRbJ4Ff+iGYTPDMQCg6WvOYe3ObFOJnn31GYGBgjX2VlSfmT/Xr148zzjiD119/nbFjxwLwn//8B8MwbMNrVV555RUAfvnlFxISEmo8t3PnTgDi4+Nt+7KysoiNjbU9fuyxx7j++uub9D7spZ4ld1RVbyltE5QWuDYWO/ToGICPp5mScgv7s10zpi4i4paKjloncTdW6io4stPhYZx55pkkJibW2F599dUax8yZM4cPPviA/HzrKMLrr7/ORRddRFBQUI3jRo8eTWBgIHfffXejJmt37NjRds7Q0FDKyuxfXLip1LPkjjrGQWgM5OyDvetPLLbbynmYTfTtHExRWQW5xeWuDkdEpHX556G69xsGvD8L9qyvfRdcXUweEHsmhPexPr5lm8NCDAgIIC4ursa+AwcO1Hg8c+ZMbr31Vv7zn/8wduxYNm7cyOLFi2u1NXDgQJ588kkmTpzIX//6V95//308Pa1pSe/evQFITk5myJAhAHh4eNjOXXVcS1HPkjsymU4UqHSzat4fXTeSb24dx+DoUFeHIiLSungH1L35BMI5T1n/7W8MkxnOefzE8XW16URBQUFcdNFFvP766yxdupQ+ffowZsyYOo895ZRTWLVqFevWreMvf/kL5eXWP6SHDBlCQkICTzzxBBaLxanxNoaSJXdVNRSXssKtipCZNU9JRMR+HXvB6FtP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ERERkQaoZ0lERESkAUqWRERERBqgZElERESkAUqWRERERBqgZKmVWLx4McOGDSMoKIjIyEjOP/98kpOTaxzz97//nV69euHn50dERATTp09nx44dNY4xmUy1tvfee68l34rbacy1r2IYBtOmTcNkMvHxxx/XeC4tLY1zzjkHf39/IiMjWbBgARUVFS3wDtyXo669Pvf2acx1Hz9+fK1reu2119Y4Rp95+znq2usz37KULLUSa9eu5YYbbuD7779nxYoVlJeXM3nyZAoLC23HDB06lKVLl5KUlMTXX3+NYRhMnjyZysrKGm0tXbqU9PR023b++ee38LtxL4259lWeeeaZOpe0qays5JxzzqGsrIzvvvuON954g2XLlrFo0aKWeAtuyxHXvoo+943X2Ot+zTXX1Limjz32mO05feabxhHXvoo+8y3IkFYpKyvLAIy1a9fWe8wvv/xiAEZKSoptH2AsX768BSJsu+q79j///LPRtWtXIz09vdZ1/uKLLwyz2WxkZGTY9r300ktGcHCwUVpa2lKhu72mXHvD0Oe+ueq67uPGjTNuvvnmel+jz7xjNOXaG4Y+8y1NPUutVG5uLgAdOnSo8/nCwkKWLl1Kz549iY6OrvHcDTfcQHh4OKeffjqvv/46hkpp2aWua19UVMQll1zCCy+8QFRUVK3XbNq0iYEDB9KpUyfbvilTppCXl8fvv//u/KDbiKZc+yr63Dddff/evP3224SHhzNgwAAWLlxIUVGR7Tl95h2jKde+ij7zLUcL6bZCFouFW265hVGjRjFgwIAaz7344ovcfvvtFBYWEh8fz4oVK/D29rY9f//993PWWWfh7+/PN998w/XXX09BQQFz585t6bfhluq79rfeeisjR45k+vTpdb4uIyOjxpcGYHuckZHhvIDbkKZee9Dnvjnqu+6XXHIJMTExdOnShV9//ZV//OMfJCcn89FHHwH6zDtCU6896DPf4lzcsyV1uPbaa42YmBhj//79tZ7Lyckxdu7caaxdu9Y499xzjVNPPdUoLi6ut627777b6NatmzPDbVPquvaffPKJERcXZ+Tn59v28Ycu8GuuucaYPHlyjbYKCwsNwPjiiy+cHndb0NRrXxd97huvoX9vqlu1alWNYX995puvqde+LvrMO5eG4VqZG2+8kc8++4zVq1fTrVu3Ws+HhITQu3dvxo4dywcffMCOHTtYvnx5ve0NHz6cAwcOUFpa6syw24T6rv23335LamoqoaGheHp64ulp7ZC98MILGT9+PABRUVFkZmbWaK/qcUNDR2LVnGtfF33uG+dk/95UN3z4cABSUlIAfeabqznXvr5j9Jl3HiVLrYRhGNx4440sX76cb7/9lp49ezbqNYZhNPg/R2JiImFhYVqIsQEnu/Z33HEHv/76K4mJibYN4Omnn2bp0qUAjBgxgm3btpGVlWV73YoVKwgODqZfv34t9l7cjSOufV30uW9YU/69qbr2nTt3BvSZbypHXPv6jtFn3olc2Ksl1Vx33XVGSEiIsWbNGiM9Pd22FRUVGYZhGKmpqcbDDz9s/PTTT8a+ffuMjRs3Gueee67RoUMHIzMz0zAMw/jf//5nvPLKK8a2bduMXbt2GS+++KLh7+9vLFq0yJVvrdU72bWvC38YCqqoqDAGDBhgTJ482UhMTDS++uorIyIiwli4cGELvAP35Yhrr8+9/U523VNSUoz777/f+Omnn4w9e/YYn3zyiREbG2uMHTvW1oY+803jiGuvz3zLU7LUSgB1bkuXLjUMwzAOHjxoTJs2zYiMjDS8vLyMbt26GZdccomxY8cOWxtffvmlccoppxiBgYFGQECAMXjwYGPJkiVGZWWli96VezjZta/vNX+cN7N3715j2rRphp+fnxEeHm7Mnz/fKC8vd27wbs4R116fe/ud7LqnpaUZY8eONTp06GD4+PgYcXFxxoIFC4zc3Nwa7egzbz9HXHt95lueyTB0r6GIiIhIfTRnSURERKQBSpZEREREGqBkSURERKQBSpZEREREGqBkSURERKQBSpZEREREGqBkSURERKQBSpZEREREGqBkSURERKQBSpZEpF3JyMjgpptuIjY2Fh8fH6Kjozn33HNZtWpVs9tetmwZoaGhzQ9SRFoVT1cHICLSUvbu3cuoUaMIDQ3l8ccfZ+DAgZSXl/P1119zww03sGPHDleHKCKtkHqWRKTduP766zGZTGzevJkLL7yQPn360L9/f+bNm8f3338PQFpaGtOnTycwMJDg4GD+8pe/kJmZaWvjl19+4cwzzyQoKIjg4GCGDh3KTz/9xJo1a7jyyivJzc3FZDJhMpm49957XfRORcSRlCyJSLuQnZ3NV199xQ033EBAQECt50NDQ7FYLEyfPp3s7GzWrl3LihUr2L17N3/9619tx82aNYtu3brx448/smXLFu644w68vLwYOXIkzzzzDMHBwaSnp5Oens5tt93Wkm9RRJxEw3Ai0i6kpKRgGAYJCQn1HrNq1Sq2bdvGnj17iI6OBuDNN9+kf//+/PjjjwwbNoy0tDQWLFhga6d3796214eEhGAymYiKinLumxGRFqWeJRFpFwzDOOkxSUlJREdH2xIlgH79+hEaGkpSUhIA8+bN4+qrr2bixIk88sgjpKamOi1mEWkdlCyJSLvQu3dvTCZTsydx33vvvfz++++cc845fPvtt/Tr14/ly5c7KEoRaY2ULIlIu9ChQwemTJnCCy+8QGFhYa3nc3Jy6Nu3L/v372f//v22/du3bycnJ4d+/frZ9vXp04dbb72Vb775hhkzZrB06VIAvL29qaysdP6bEZEWpWRJRNqNF154gcrKSk4//XQ+/PBDdu3aRVJSEv/6178YMWIEEydOZODAgcyaNYutW7eyefNmLr/8csaNG8dpp51GcXExN954I2vWrGHfvn1s3LiRH3/8kb59+wLQo0cPCgoKWLVqFUeOHKGoqMjF71hEHEHJkoi0G7GxsWzdupUzzzyT+fPnM2DAACZNmsSqVat46aWXMJlMfPLJJ4SFhTF27FgmTpxIbGws77//PgAeHh4cPXqUyy+/nD59+vCXv/yFadOmcd999wEwcuRIrr32Wv76178SERHBY4895sq3KyIOYjIaM+tRREREpJ1Sz5KIiIhIA5QsiYiIiDRAyZKIiIhIA5QsiYiIiDRAyZKIiIhIA5QsiYiIiDRAyZKIiIhIA5QsiYiIiDRAyZKIiIhIA5QsiYiIiDRAyZKIiIhIA/4f3PW1byZdp60AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(\n", + " costs_momf,\n", + " np.log10(BC.max_hv - np.array(hvs_momf)),\n", + " \"--\",\n", + " marker=\"s\",\n", + " ms=10,\n", + " label=\"MOMF\",\n", + ")\n", + "plt.plot(\n", + " costs, np.log10(BC.max_hv - np.array(hvs_kg)), \"--\", marker=\"d\", ms=10, label=\"HVKG\"\n", + ")\n", + "plt.ylabel(\"Log Inference Hypervolume Regret\")\n", + "plt.xlabel(\"Cost\")\n", + "plt.legend()" + ] + } + ], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "fileHeader": "", + "fileUid": "de076d04-5d05-41f5-835c-89532288cbc3", + "isAdHoc": false, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.13" + }, + "toc": { + "base_numbering": 1, + "nav_menu": [], + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": [], + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "position": { + "height": "423.4px", + "left": "858.8px", + "right": "20px", + "top": "120px", + "width": "350px" + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "fileHeader": "", + "fileUid": "05dd24d9-3801-45cc-b930-c3be4864c50b", + "indentAmount": 2, + "isAdHoc": false, + "language_info": { + "name": "plaintext" + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "fileHeader": "", + "fileUid": "608ac2c8-1d1c-4562-95a3-b228e435d2cf", + "indentAmount": 2, + "isAdHoc": false, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/website-old/static/files/Multi_objective_multi_fidelity_BO.py b/website-old/static/files/Multi_objective_multi_fidelity_BO.py new file mode 100644 index 0000000000..b3bff3e698 --- /dev/null +++ b/website-old/static/files/Multi_objective_multi_fidelity_BO.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# # Multi-fidelity Multi-Objective optimization + +# In this tutorial notebook we demonstrate how to perform multi-objective multi-fidelity optimization in BoTorch using the multi-fidelity Hypervolume Knowledge Gradient (MF-HVKG) [3] and a method called Multi-Objective Multi-Fidelity (MOMF) [1]. +# +# MF-HVKG performs one-step lookahead: it operates under the assumption that we can make one additional observation, and after receiving that additional observation, we will select the Pareto set of optimal designs. HVKG seeks to select the design `x` to evaluate that maximizes the value of information about the Pareto set by maximizing the hypervolume under the posterior mean (conditional on receiving on new observation for the design `x`). +# +# MOMF is an alternative approach that introduces an additional "fidelity objective" that is optimized along with the problem objectives. This fidelity objective can be thought of as a trust objective that rewards the optimization when going to higher fidelity. Thus, the MOMF explicitly optimizes for getting more high-fidelity (trustworthy) data while taking into account the higher computational costs associated with it. +# +# HVKG is generally more cost efficient [3], since it explicitly targets the goal of MF optimization: select design points and fidelities that enable identifying about the Pareto Frontier at the target fidelity in a cost-aware fashion. MOMF will typically result in faster candidate generation. If the application is high-throughput and requires fast candidate generation, MOMF will be preferable. Otherwise, MF-HVKG will likely give better sample efficiency and performance [3]. +# +# In this tutorial, we will optimize a synthetic function that is a modified multi-fidelity Branin-Currin [1]. This is a 3-dimesional, bi-objective problem with one of the input dimensions being the fidelity. For the MOMF, this results in a 3-objective problem since it also takes the fidelity objective into account. In this case the fidelity objective is a linear function of fidelity, $ f(s)=s$, where $s$ is the fidelity. The MOMF algorithm can accept any discrete or continuous cost functions as an input. In this example, we choose an exponential dependency of the form $C(s)=\exp(4.8s)$. The goal of the optimization is to find the Pareto front, which is a trade-off solution set for Multi-objective problems, at the highest fidelity. +# +# Note: pymoo is an optional dependency that is used for determining the Pareto set of optimal designs under the model posterior mean using NSGA-II (which is not a sample efficient method, but sample efficiency is not critical for this step). If pymoo is not available, the Pareto set of optimal designs is selected from a discrete set. This will work okay for low-dim (e.g. +# dimensions) problems, but in general NSGA-II will yield far better results. +# +# [1] [Irshad, Faran, Stefan Karsch, and Andreas Döpp. "Expected hypervolume improvement for simultaneous multi-objective and multi-fidelity optimization." arXiv preprint arXiv:2112.13901 (2021).](https://arxiv.org/abs/2112.13901) +# +# [2] [S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives. NeurIPS, 2021.](https://proceedings.neurips.cc/paper/2021/hash/11704817e347269b7254e744b5e22dac-Abstract.html) +# +# [3] [S. Daulton, M. Balandat, and E. Bakshy. Hypervolume Knowledge Gradient for Multi-Objective Bayesian Optimization with Partial Information. ICML, 2023.](https://proceedings.mlr.press/v202/daulton23a.html) + +# In[1]: + + +import os +from typing import Callable, Dict + +import matplotlib.pyplot as plt +import numpy as np +import torch + + +# ### Set dtype and device +# Setting up the global variable that determine the device to run the optimization. The optimization is much faster when it runs on GPU. + +# In[2]: + + +tkwargs = { # Tkwargs is a dictionary contaning data about data type and data device + "dtype": torch.double, + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), +} +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Define the problem and optimization settings + +# In[3]: + + +from botorch.test_functions.multi_objective_multi_fidelity import MOMFBraninCurrin + +BC = MOMFBraninCurrin(negate=True).to(**tkwargs) +dim_x = BC.dim +dim_y = BC.num_objectives + +ref_point = torch.zeros(dim_y, **tkwargs) + + +BATCH_SIZE = 1 # For batch optimization, BATCH_SIZE should be greater than 1 +# This evaluation budget is set to be very low to make the notebook run fast. This should be much higher. +EVAL_BUDGET = 2.05 # in terms of the number of full-fidelity evaluations +n_INIT = 2 # Initialization budget in terms of the number of full-fidelity evaluations +# Number of Monte Carlo samples, used to approximate MOMF +MC_SAMPLES = 2 if SMOKE_TEST else 128 +# Number of restart points for multi-start optimization +NUM_RESTARTS = 2 if SMOKE_TEST else 10 +# Number of raw samples for initial point selection heuristic +RAW_SAMPLES = 4 if SMOKE_TEST else 512 + +standard_bounds = torch.zeros(2, dim_x, **tkwargs) +standard_bounds[1] = 1 +# mapping from index to target fidelity (highest fidelity) +target_fidelities = {2: 1.0} + + +# ### Helper functions to define Cost +# +# The cost_func function returns an exponential cost from the fidelity. The cost_callable is a wrapper around it that takes care of the input output shapes. This is provided to the MF algorithms which inversely weight the expected utility by the cost. + +# In[4]: + + +from math import exp + + +def cost_func(x): + """A simple exponential cost function.""" + exp_arg = torch.tensor(4.8, **tkwargs) + val = torch.exp(exp_arg * x) + return val + + +# Displaying the min and max costs for this optimization +print(f"Min Cost: {cost_func(0)}") +print(f"Max Cost: {cost_func(1)}") + + +def cost_callable(X: torch.Tensor) -> torch.Tensor: + r"""Wrapper for the cost function that takes care of shaping + input and output arrays for interfacing with cost_func. + This is passed as a callable function to MOMF. + + Args: + X: A `batch_shape x q x d`-dim Tensor + Returns: + Cost `batch_shape x q x m`-dim Tensor of cost generated + from fidelity dimension using cost_func. + """ + + return cost_func(X[..., -1:]) + + +# ### Model Initialization +# We use a multi-output SingleTaskGP to model the problem with a homoskedastic Gaussian likelihood with an inferred noise level. +# The model is initialized with random points, where the fidelity is sampled from a probability distribution with a PDF that is inversely proportional to the cost: $p(s)=C(s)^{-1}$. The initialization is given a budget equivalent to 2 full-fidelity evaluations. + +# In[5]: + + +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize +from botorch.utils.transforms import normalize +from gpytorch.kernels import MaternKernel, ScaleKernel +from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood +from gpytorch.priors import GammaPrior + + +def inv_transform(u): + # define inverse transform to sample from the probability distribution with + # PDF proportional to 1/(c(x)) + # u is a uniform(0,1) rv + return 5 / 24 * torch.log(-exp(24 / 5) / (exp(24 / 5) * u - u - exp(24 / 5))) + + +def gen_init_data(n: int): + r""" + Generates the initial data. Sample fidelities inversely proportional to cost. + """ + # total cost budget is n + train_x = torch.empty( + 0, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device + ) + total_cost = 0 + # assume target fidelity is 1 + total_cost_limit = ( + n + * cost_callable( + torch.ones( + 1, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device + ) + ).item() + ) + while total_cost < total_cost_limit: + new_x = torch.rand( + 1, BC.bounds.shape[1], dtype=BC.bounds.dtype, device=BC.bounds.device + ) + new_x[:, -1] = inv_transform(new_x[:, -1]) + total_cost += cost_callable(new_x) + train_x = torch.cat([train_x, new_x], dim=0) + train_x = train_x[:-1] + train_obj = BC(train_x) + return train_x, train_obj + + +def initialize_model(train_x, train_obj, state_dict=None): + """Initializes a ModelList with Matern 5/2 Kernel and returns the model and its MLL. + + Note: a batched model could also be used here. + """ + models = [] + for i in range(train_obj.shape[-1]): + m = SingleTaskGP( + train_x, + train_obj[:, i : i + 1], + train_Yvar=torch.full_like(train_obj[:, i : i + 1], 1e-6), + outcome_transform=Standardize(m=1), + covar_module=ScaleKernel( + MaternKernel( + nu=2.5, + ard_num_dims=train_x.shape[-1], + lengthscale_prior=GammaPrior(2.0, 2.0), + ), + outputscale_prior=GammaPrior(2.0, 0.15), + ), + ) + models.append(m) + model = ModelListGP(*models) + mll = SumMarginalLogLikelihood(model.likelihood, model) + if state_dict is not None: + model.load_state_dict(state_dict=state_dict) + return mll, model + + +# ### Helper function to optimize acquisition function +# This is a helper function that initializes, optimizes the acquisition function MOMF and returns the new_x and new_obj. The problem is called from within this helper function. +# +# A simple initialization heuristic is used to select the 20 restart initial locations from a set of 1024 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation. + +# In[6]: + + +from botorch.acquisition.multi_objective.multi_fidelity import MOMF +from botorch.optim.optimize import optimize_acqf +from botorch.sampling.normal import SobolQMCNormalSampler +from botorch.utils.multi_objective.box_decompositions.non_dominated import ( + FastNondominatedPartitioning, +) +from botorch.utils.transforms import unnormalize + + +dim_y_momf = dim_y + 1 # Output Dimesnion for MOMF optimization +ref_point_momf = torch.zeros(dim_y_momf, **tkwargs) + + +def fid_obj(X: torch.Tensor) -> torch.Tensor: + """ + A Fidelity Objective that can be thought of as a trust objective. + Higher Fidelity simulations are rewarded as being more + trustworthy. Here we consider just a linear fidelity objective. + """ + fid_obj = 1 * X[..., -1] + return fid_obj + + +def get_objective_momf(x: torch.Tensor) -> torch.Tensor: + """Wrapper around the Objective function to take care of fid_obj stacking""" + y = BC(x) # The Branin-Currin is called + fid = fid_obj(x) # Getting the fidelity objective values + fid_out = fid.unsqueeze(-1) + # Concatenating objective values with fid_objective + y_out = torch.cat([y, fid_out], -1) + return y_out + + +def optimize_MOMF_and_get_obs( + model: SingleTaskGP, + train_obj: torch.Tensor, + sampler: SobolQMCNormalSampler, + ref_point: torch.Tensor, + standard_bounds: torch.Tensor, + BATCH_SIZE: int, + cost_call: Callable[[torch.Tensor], torch.Tensor], +): + """ + Wrapper to call MOMF and optimizes it in a sequential greedy + fashion returning a new candidate and evaluation + """ + partitioning = FastNondominatedPartitioning(ref_point=ref_point, Y=train_obj) + acq_func = MOMF( + model=model, + ref_point=ref_point, # use known reference point + partitioning=partitioning, + sampler=sampler, + cost_call=cost_call, + ) + # Optimization + candidates, vals = optimize_acqf( + acq_function=acq_func, + bounds=standard_bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={ + "batch_limit": 5, + "maxiter": 3 if SMOKE_TEST else 200, + "nonnegative": True, + }, + sequential=True, + ) + # if the AF val is 0, set the fidelity parameter to zero + if vals.item() == 0.0: + candidates[:, -1] = 0.0 + # observe new values + new_x = unnormalize(candidates.detach(), bounds=standard_bounds) + new_obj = get_objective_momf(new_x) + return new_x, new_obj + + +# ### Define helper functions for MF-HVKG +# +# `get_current_value` optimizes the current posterior mean at the full fidelity to determine the hypervolume under the current model. +# +# `optimize_HVKG_and_get_obs` creates the MF-HVKG acquisition function, optimizes it, and returns the new design and corresponding observation. +# + +# In[7]: + + +from botorch.acquisition.cost_aware import InverseCostWeightedUtility +from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction +from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import ( + _get_hv_value_function, + qMultiFidelityHypervolumeKnowledgeGradient, +) +from botorch.acquisition.utils import project_to_target_fidelity +from botorch.models.deterministic import GenericDeterministicModel +from torch import Tensor + +NUM_INNER_MC_SAMPLES = 2 if SMOKE_TEST else 32 +NUM_PARETO = 1 if SMOKE_TEST else 10 +NUM_FANTASIES = 2 if SMOKE_TEST else 8 + + +def get_current_value( + model: SingleTaskGP, + ref_point: torch.Tensor, + bounds: torch.Tensor, + normalized_target_fidelities: Dict[int, float], +): + """Helper to get the hypervolume of the current hypervolume + maximizing set. + """ + fidelity_dims, fidelity_targets = zip(*normalized_target_fidelities.items()) + # optimize + non_fidelity_dims = list(set(range(dim_x)) - set(fidelity_dims)) + curr_val_acqf = FixedFeatureAcquisitionFunction( + acq_function=_get_hv_value_function( + model=model, + ref_point=ref_point, + sampler=SobolQMCNormalSampler( + sample_shape=torch.Size([NUM_INNER_MC_SAMPLES]), + ), + use_posterior_mean=True, + ), + d=dim_x, + columns=fidelity_dims, + values=fidelity_targets, + ) + # optimize + _, current_value = optimize_acqf( + acq_function=curr_val_acqf, + bounds=bounds[:, non_fidelity_dims], + q=NUM_PARETO, + num_restarts=1, + raw_samples=2 * RAW_SAMPLES, + return_best_only=True, + options={ + "nonnegative": True, + "maxiter": 3 if SMOKE_TEST else 200, + }, + ) + return current_value + + +normalized_target_fidelities = {} +for idx, fidelity in target_fidelities.items(): + lb = standard_bounds[0, idx].item() + ub = standard_bounds[1, idx].item() + normalized_target_fidelities[idx] = (fidelity - lb) / (ub - lb) +project_d = dim_x + + +def project(X: Tensor) -> Tensor: + + return project_to_target_fidelity( + X=X, + d=project_d, + target_fidelities=normalized_target_fidelities, + ) + + +def optimize_HVKG_and_get_obs( + model: SingleTaskGP, + ref_point: torch.Tensor, + standard_bounds: torch.Tensor, + BATCH_SIZE: int, + cost_call: Callable[[torch.Tensor], torch.Tensor], +): + """Utility to initialize and optimize HVKG.""" + cost_model = GenericDeterministicModel(cost_call) + cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) + current_value = get_current_value( + model=model, + ref_point=ref_point, + bounds=standard_bounds, + normalized_target_fidelities=normalized_target_fidelities, + ) + + acq_func = qMultiFidelityHypervolumeKnowledgeGradient( + model=model, + ref_point=ref_point, # use known reference point + num_fantasies=NUM_FANTASIES, + num_pareto=NUM_PARETO, + current_value=current_value, + cost_aware_utility=cost_aware_utility, + target_fidelities=normalized_target_fidelities, + project=project, + ) + # Optimization + candidates, vals = optimize_acqf( + acq_function=acq_func, + bounds=standard_bounds, + q=BATCH_SIZE, + num_restarts=1, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={ + "batch_limit": 5, + "maxiter": 3 if SMOKE_TEST else 200, + }, + ) + # if the AF val is 0, set the fidelity parameter to zero + if vals.item() == 0.0: + candidates[:, -1] = 0.0 + # observe new values + new_x = unnormalize(candidates.detach(), bounds=BC.bounds) + new_obj = BC(new_x) + return new_x, new_obj + + +# ### Define helper functions for MF-HVKG +# +# We run MOMF to optimize the multi-fidelity versions of the Branin-Currin functions. The optimization loop works in the following sequence. +# +# 1. At the start with an initialization equivalent to 2 full fidelity evaluations. +# 2. The models are used to generate an acquisition function that is optimized to select new input parameters +# 3. The objective function is evaluated at the suggested new_x and returns a new_obj. +# 4. The models are updated with the new points and then are used again to make the next prediction. +# +# The evaluation budget for the optimization is set to 4 full fidelity evaluations. +# +# Note: running this takes some time. +# + +# In[8]: + + +from botorch import fit_gpytorch_mll + + +# In[9]: + + +get_ipython().run_cell_magic('time', '', '\n# Intializing train_x to zero\nverbose = False\ntorch.manual_seed(0)\ntrain_x_momf, _ = gen_init_data(n_INIT)\ntrain_obj_momf = get_objective_momf(train_x_momf)\n# Generate Sampler\nmomf_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n\n# run N_BATCH rounds of BayesOpt after the initial random batch\niteration = 0\ntotal_cost = cost_callable(train_x_momf).sum().item()\nwhile total_cost < EVAL_BUDGET * cost_func(1):\n if verbose:\n print(f"cost: {total_cost}")\n\n # reinitialize the models so they are ready for fitting on next iteration\n mll, model = initialize_model(normalize(train_x_momf, BC.bounds), train_obj_momf)\n\n fit_gpytorch_mll(mll=mll) # Fit the model\n\n # optimize acquisition functions and get new observations\n new_x, new_obj = optimize_MOMF_and_get_obs(\n model=model,\n train_obj=train_obj_momf,\n sampler=momf_sampler,\n ref_point=ref_point_momf,\n standard_bounds=standard_bounds,\n BATCH_SIZE=BATCH_SIZE,\n cost_call=cost_callable,\n )\n # Updating train_x and train_obj\n train_x_momf = torch.cat([train_x_momf, new_x], dim=0)\n train_obj_momf = torch.cat([train_obj_momf, new_obj], dim=0)\n iteration += 1\n total_cost += cost_callable(new_x).sum().item()\n') + + +# ### Run MF-HVKG + +# In[10]: + + +get_ipython().run_cell_magic('time', '', '\ntorch.manual_seed(0)\ntrain_x_kg, train_obj_kg = gen_init_data(n_INIT)\nMF_n_INIT = train_x_kg.shape[0]\niteration = 0\ntotal_cost = cost_callable(train_x_kg).sum().item()\nwhile total_cost < EVAL_BUDGET * cost_func(1):\n if verbose:\n print(f"cost: {total_cost}")\n\n # reinitialize the models so they are ready for fitting on next iteration\n mll, model = initialize_model(normalize(train_x_kg, BC.bounds), train_obj_kg)\n\n fit_gpytorch_mll(mll=mll) # Fit the model\n # optimize acquisition functions and get new observations\n new_x, new_obj = optimize_HVKG_and_get_obs(\n model=model,\n ref_point=ref_point,\n standard_bounds=standard_bounds,\n BATCH_SIZE=BATCH_SIZE,\n cost_call=cost_callable,\n )\n # Updating train_x and train_obj\n train_x_kg = torch.cat([train_x_kg, new_x], dim=0)\n train_obj_kg = torch.cat([train_obj_kg, new_obj], dim=0)\n iteration += 1\n total_cost += cost_callable(new_x).sum().item()\n') + + +# ### Result: Evaluating the Pareto front at the highest fidelity using NSGA-II on the posterior mean +# + +# In[11]: + + +from botorch.utils.multi_objective.pareto import ( + _is_non_dominated_loop, + is_non_dominated, +) +from gpytorch import settings + +try: + # Note: These are the pymoo 0.6+ imports, if you happen to be stuck on + # an older pymoo version you need to replace them with the ones below. + from pymoo.algorithms.moo.nsga2 import NSGA2 + from pymoo.core.problem import Problem + from pymoo.optimize import minimize + from pymoo.termination.max_gen import MaximumGenerationTermination + + # from pymoo.algorithms.nsga2 import NSGA2 + # from pymoo.model.problem import Problem + # from pymoo.util.termination.max_gen import MaximumGenerationTermination + + def get_pareto( + model, + non_fidelity_indices, + project, + population_size=20 if SMOKE_TEST else 250, + max_gen=10 if SMOKE_TEST else 100, + is_mf_model=True, + ): + """Optimize the posterior mean using NSGA-II.""" + tkwargs = { + "dtype": BC.ref_point.dtype, + "device": BC.ref_point.device, + } + dim = len(non_fidelity_indices) + + class PosteriorMeanPymooProblem(Problem): + def __init__(self): + super().__init__( + n_var=dim, + n_obj=BC.num_objectives, + type_var=np.double, + ) + self.xl = np.zeros(dim) + self.xu = np.ones(dim) + + def _evaluate(self, x, out, *args, **kwargs): + X = torch.from_numpy(x).to(**tkwargs) + if is_mf_model: + X = project(X) + with torch.no_grad(): + with settings.cholesky_max_tries(9): + # eval in batch mode + y = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2) + out["F"] = -y.cpu().numpy() + + pymoo_problem = PosteriorMeanPymooProblem() + algorithm = NSGA2( + pop_size=population_size, + eliminate_duplicates=True, + ) + res = minimize( + pymoo_problem, + algorithm, + termination=MaximumGenerationTermination(max_gen), + seed=0, # fix seed + verbose=False, + ) + X = torch.tensor( + res.X, + **tkwargs, + ) + # project to full fidelity + if is_mf_model: + if project is not None: + X = project(X) + # determine Pareto set of designs under model + with torch.no_grad(): + preds = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2) + pareto_mask = is_non_dominated(preds) + X = X[pareto_mask] + # evaluate Pareto set of designs on true function and compute hypervolume + if not is_mf_model: + X = project(X) + X = unnormalize(X, BC.bounds) + Y = BC(X) + # compute HV + partitioning = FastNondominatedPartitioning(ref_point=BC.ref_point, Y=Y) + return partitioning.compute_hypervolume().item() + +except ImportError: + NUM_DISCRETE_POINTS = 10 if SMOKE_TEST else 100000 + CHUNK_SIZE = 512 + + def get_pareto( + model, + non_fidelity_indices, + project, + population_size=20 if SMOKE_TEST else 250, + max_gen=10 if SMOKE_TEST else 100, + is_mf_model=True, + ): + """Optimize the posterior mean over a discrete set.""" + tkwargs = { + "dtype": BC.ref_point.dtype, + "device": BC.ref_point.device, + } + dim_x = BC.dim + + discrete_set = torch.rand(NUM_DISCRETE_POINTS, dim_x - 1, **tkwargs) + if is_mf_model: + discrete_set = project(discrete_set) + discrete_set[:, -1] = 1.0 # set to target fidelity + with torch.no_grad(): + preds_list = [] + for start in range(0, NUM_DISCRETE_POINTS, CHUNK_SIZE): + preds = model.posterior( + discrete_set[start : start + CHUNK_SIZE].unsqueeze(-2) + ).mean.squeeze(-2) + preds_list.append(preds) + preds = torch.cat(preds_list, dim=0) + pareto_mask = _is_non_dominated_loop(preds) + pareto_X = discrete_set[pareto_mask] + if not is_mf_model: + pareto_X = project(pareto_X) + pareto_X = unnormalize(pareto_X, BC.bounds) + Y = BC(pareto_X) + # compute HV + partitioning = FastNondominatedPartitioning(ref_point=BC.ref_point, Y=Y) + return partitioning.compute_hypervolume().item() + + +# ## Evaluate MF-HVKG +# +# We evaluate performance after every 5 evaluations (this is to speed things up, since there are many observations). + +# In[12]: + + +get_ipython().run_cell_magic('time', '', '\nhvs_kg = []\ncosts = []\nfor i in range(MF_n_INIT, train_x_kg.shape[0] + 1, 5):\n\n mll, model = initialize_model(\n normalize(train_x_kg[:i], BC.bounds), train_obj_kg[:i]\n )\n fit_gpytorch_mll(mll)\n hypervolume = get_pareto(model, project=project, non_fidelity_indices=[0, 1])\n hvs_kg.append(hypervolume)\n costs.append(cost_callable(train_x_kg[:i]).sum().item())\n') + + +# ## Evaluate MOMF +# +# We evaluate performance after every evaluation (there are not as many evaluations since MOMF queries higher fidelities more frequently). + +# In[13]: + + +get_ipython().run_cell_magic('time', '', '\nhvs_momf = []\ncosts_momf = []\nfor i in range(MF_n_INIT, train_x_momf.shape[0] + 1):\n\n mll, model = initialize_model(\n normalize(train_x_momf[:i], BC.bounds), train_obj_momf[:i, :2]\n )\n fit_gpytorch_mll(mll)\n hypervolume = get_pareto(model, project=project, non_fidelity_indices=[0, 1])\n hvs_momf.append(hypervolume)\n costs_momf.append(cost_callable(train_x_momf[:i]).sum().item())\n') + + +# ### Plot log inference hypervolume regret (under the model) vs cost +# +# Log inference hypervolume regret, defined as the logarithm of the difference between the maximum hypervolume dominated by the Pareto frontier and the hypervolume corresponding to the Pareto set identified by each algorithm, is a performance evaluation criterion for multi-information source multi-objective optimization [3]. + +# In[14]: + + +plt.plot( + costs_momf, + np.log10(BC.max_hv - np.array(hvs_momf)), + "--", + marker="s", + ms=10, + label="MOMF", +) +plt.plot( + costs, np.log10(BC.max_hv - np.array(hvs_kg)), "--", marker="d", ms=10, label="HVKG" +) +plt.ylabel("Log Inference Hypervolume Regret") +plt.xlabel("Cost") +plt.legend() + diff --git a/website-old/static/files/batch_mode_cross_validation.ipynb b/website-old/static/files/batch_mode_cross_validation.ipynb new file mode 100644 index 0000000000..925a6d4142 --- /dev/null +++ b/website-old/static/files/batch_mode_cross_validation.ipynb @@ -0,0 +1,353 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "source": [ + "## Application of batch-mode regression to cross-validation\n", + "\n", + "botorch provides a helper function `gen_loo_cv_folds` to easily perform leave-one-out (LOO) cross-validation (CV) by taking advantage of batch-mode regression and evaluation in GPyTorch. This tutorial illustrates the process on a noisy sinusoidal function, similar to the example from the batch-mode GP regression [tutorial](https://github.com/cornellius-gp/gpytorch/blob/master/examples/01_Exact_GPs/Simple_GP_Regression.ipynb) from GPyTorch:\n", + "\n", + "$$y = \\sin(2\\pi x) + \\epsilon, ~\\epsilon \\sim \\mathcal N(0, 0.2).$$\n", + "\n", + "Note: this tutorial aims to introduce batch-mode regression and evaluation in GPyTorch with CV as an example application. For alternative, more user-friendly functions to perform CV in Ax, see [ax.modelbridge.cross_validation](https://github.com/facebook/Ax/blob/main/ax/modelbridge/cross_validation.py). However, for larger CV tasks, it may be useful to exploit GPyTorch batch-mode, as shown in this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "import math\n", + "\n", + "device = torch.device(\"cpu\")\n", + "dtype = torch.float64\n", + "torch.manual_seed(3);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialize the CV dataset\n", + "\n", + "For our training data, we take 20 regularly spaced points on the interval $[0, 1]$ and generate noisy evaluations with an observed noise variance of 0.2. Remember that botorch requires an explicit output dimension." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "sigma = math.sqrt(0.2)\n", + "train_X = torch.linspace(0, 1, 20, dtype=dtype, device=device).view(-1, 1)\n", + "train_Y_noiseless = torch.sin(train_X * (2 * math.pi))\n", + "train_Y = train_Y_noiseless + sigma * torch.randn_like(train_Y_noiseless)\n", + "train_Yvar = torch.full_like(train_Y, 0.2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The botorch function `gen_loo_cv_folds` takes our observed data `train_X`, `train_Y`, `train_Yvar` as input and returns the LOO CV folds in a `CVFolds` object." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[KeOps] Warning : omp.h header is not in the path, disabling OpenMP. To fix this, you can set the environment\n", + " variable OMP_PATH to the location of the header before importing keopscore or pykeops,\n", + " e.g. using os.environ: import os; os.environ['OMP_PATH'] = '/path/to/omp/header'\n", + "[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode\n" + ] + } + ], + "source": [ + "from botorch.cross_validation import gen_loo_cv_folds\n", + "\n", + "cv_folds = gen_loo_cv_folds(train_X=train_X, train_Y=train_Y, train_Yvar=train_Yvar)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `cv_folds` object contains the data, stored as tensors of appropriate batch shape, necessary to perform 20 CVs of 19 training points and 1 test point. For example, we can check that the shapes of the training inputs and training targets are `b x n x d = 20 x 19 x 1` and `b x n x o = 20 x 19 x 1` respectively, where `o` is the number of outputs." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(torch.Size([20, 19, 1]), torch.Size([20, 19, 1]))" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cv_folds.train_X.shape, cv_folds.train_Y.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(torch.Size([20, 1, 1]), torch.Size([20, 1, 1]))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cv_folds.test_X.shape, cv_folds.test_Y.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that in a situation where the dataset is large, one may not want to perform LOO; in that case, a similar process can be used to perform $k$-fold CV." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perform LOOCV\n", + "\n", + "We can use the `batch_cross_validation` function to perform LOOCV using batching (meaning that the `b = 20` sets of training data can be fit as `b = 20` separate GP models with separate hyperparameters in parallel through GPyTorch) and return a CVResult tuple with the batched `GPyTorchPosterior` object over the LOOCV test points and the observed targets. The `batch_cross_validation` requires a model class (`model_cls`) and a marginal log likelihood class (`mll_cls`). We will use the SingleTaskGP as the `model_cls` and an ExactMarginalLogLikelihood as the `mll_cls`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.cross_validation import batch_cross_validation\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.models.transforms.input import Normalize\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "\n", + "input_transform = Normalize(d=train_X.shape[-1])\n", + "outcome_transform = Standardize(\n", + " m=train_Y.shape[-1],\n", + " batch_shape=cv_folds.train_Y.shape[:-2],\n", + ")\n", + "\n", + "# instantiate and fit model\n", + "cv_results = batch_cross_validation(\n", + " model_cls=SingleTaskGP,\n", + " mll_cls=ExactMarginalLogLikelihood,\n", + " cv_folds=cv_folds,\n", + " model_init_kwargs={\n", + " \"input_transform\": input_transform,\n", + " \"outcome_transform\": outcome_transform,\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Compute the cross-validation error and generate plots\n", + "To compute the cross-validation error, we first evaluate the test points by computing the posterior in batch mode. Next, we compute the squared errors for each test point from the prediction and take an average across all cross-validation folds." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cross-validation error: 0.11\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "posterior = cv_results.posterior\n", + "mean = posterior.mean\n", + "cv_error = ((cv_folds.test_Y.squeeze() - mean.squeeze()) ** 2).mean()\n", + "print(f\"Cross-validation error: {cv_error : 4.2}\")\n", + "\n", + "# get lower and upper confidence bounds\n", + "lower, upper = posterior.mvn.confidence_region()\n", + "\n", + "# scatterplot of predicted versus test\n", + "_, axes = plt.subplots(1, 1, figsize=(6, 4))\n", + "plt.plot([-1.5, 1.5], [-1.5, 1.5], \"k\", label=\"true objective\", linewidth=2)\n", + "\n", + "axes.set_xlabel(\"Actual\")\n", + "axes.set_ylabel(\"Predicted\")\n", + "\n", + "axes.errorbar(\n", + " x=cv_folds.test_Y.numpy().flatten(),\n", + " y=mean.numpy().flatten(),\n", + " xerr=1.96 * sigma,\n", + " yerr=((upper - lower) / 2).numpy().flatten(),\n", + " fmt=\"*\",\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we can visualize the fitted models. To do this, we again take advantage of batch-mode evaluation to obtain predictions, including lower and upper confidence regions, from each of the 20 models." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "model = cv_results.model\n", + "with torch.no_grad():\n", + " # evaluate the models at a series of points for plotting\n", + " plot_x = (\n", + " torch.linspace(0, 1, 101).view(1, -1, 1).repeat(cv_folds.train_X.shape[0], 1, 1)\n", + " )\n", + " posterior = model.posterior(plot_x)\n", + " mean = posterior.mean\n", + "\n", + " # get lower and upper confidence bounds\n", + " lower, upper = posterior.mvn.confidence_region()\n", + " plot_x.squeeze_()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code snippet below plots the result for the 12th CV fold (by setting `num = 12`), but note that we have computed the results for all folds above (other plots can be obtained by iterating `num` from 1 to 20)." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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rifXtGvJ9ex6ck27lr+vZ8hNohRCCCGE9qQHQgghhBBekwRCCCGEEF6TBEIIIYQQXpMEQgghhBBekwTiR4sWLaJHjx6YzWbGjx/PunXrjnn8kiVLGDBgAGazmaFDh7J06VIfRRo8vLmmzz33HKeccgqxsbHExsYyderU4/4MuiNvf08PeeONN1AUhQsuuEDbAIOMt9eztraWm2++mdTUVEwmE/369ZO//V/w9po+8cQT9O/fn/DwcDIzM7nttttobm72UbSB7euvv2bGjBmkpaWhKArvv//+cc9ZtWoVo0aNwmQy0adPHxYvXqxdgKpQ33jjDdVoNKovvPCCun37dvX6669XY2Ji1LKysnaP//bbb1W9Xq8+8sgj6o4dO9Q//elPqsFgULdu3erjyAOXt9f0iiuuUBctWqRu2rRJ3blzp3r11Ver0dHRamFhoY8jD1zeXtNDDhw4oKanp6unnHKKev755/sm2CDg7fV0OBzqmDFj1HPOOUddvXq1euDAAXXVqlVqTk6OjyMPXN5e01dffVU1mUzqq6++qh44cED97LPP1NTUVPW2227zceSBaenSpeof//hH9d1331UB9b333jvm8fv371ctFos6f/58dceOHepTTz2l6vV6ddmyZZrEJwmEqqrjxo1Tb7755rZ/u91uNS0tTV2wYEG7x1966aXqueeee9hz48ePV3/9619rGmcw8faa/pLL5VKjoqLUF198UasQg05nrqnL5VInTpyoPv/88+pVV10lCcTPeHs9//3vf6u9evVSW1pafBVi0PH2mt58883qGWeccdhz8+fPV08++WRN4wxGHUkg7rrrLnXw4MGHPTd79mx12rRpmsTU7YcwWlpa2LBhA1OnTm17TqfTMXXqVNasWdPuOWvWrDnseIBp06Yd9fjupjPX9JcaGxtxOp3ExcVpFWZQ6ew1/etf/0pSUhLXXnutL8IMGp25nh9++CETJkzg5ptvJjk5mSFDhvCPf/wDt9vtq7ADWmeu6cSJE9mwYUPbMMf+/ftZunQp55xzjk9iDjW+vjeF3GZa3qqsrMTtdpOcnHzY88nJyezatavdc0pLS9s9vrS0VLM4g0lnrukv3X333aSlpR3xx9Bddeaarl69mv/+97/k5OT4IMLg0pnruX//fr744gvmzJnD0qVLyc3N5Te/+Q1Op5P777/fF2EHtM5c0yuuuILKykomTZqEqqq4XC5uvPFG/vCHP/gi5JBztHuT3W6nqamJ8PDwLm2v2/dAiMDz0EMP8cYbb/Dee+9hNpv9HU5QqqurY+7cuTz33HMkJCT4O5yQ4PF4SEpK4tlnn2X06NHMnj2bP/7xjzzzzDP+Di1orVq1in/84x88/fTTbNy4kXfffZdPPvmEv/3tb/4OTXRAt++BSEhIQK/XU1ZWdtjzZWVlpKSktHtOSkqKV8d3N525poc8+uijPPTQQ6xYsYJhw4ZpGWZQ8faa7tu3j4MHDzJjxoy25zweDwBhYWHs3r2b3r17axt0AOvM72hqaioGgwG9Xt/23MCBAyktLaWlpQWj0ahpzIGuM9f0z3/+M3PnzuW6664DYOjQoTQ0NHDDDTfwxz/+EZ1OPuN642j3JqvV2uW9DyA9EBiNRkaPHs3KlSvbnvN4PKxcuZIJEya0e86ECRMOOx5g+fLlRz2+u+nMNQV45JFH+Nvf/sayZcsYM2aML0INGt5e0wEDBrB161ZycnLaHjNnzmTy5Mnk5OSQmZnpy/ADTmd+R08++WRyc3PbEjGAPXv2kJqa2u2TB+jcNW1sbDwiSTiUoKmyTZPXfH5v0mRqZpB54403VJPJpC5evFjdsWOHesMNN6gxMTFqaWmpqqqqOnfuXPWee+5pO/7bb79Vw8LC1EcffVTduXOnev/998syzl/w9po+9NBDqtFoVN9++221pKSk7VFXV+evbyHgeHtNf0lWYRzO2+uZn5+vRkVFqbfccou6e/du9eOPP1aTkpLUBx980F/fQsDx9pref//9alRUlPr666+r+/fvVz///HO1d+/e6qWXXuqvbyGg1NXVqZs2bVI3bdqkAurjjz+ubtq0Sc3Ly1NVVVXvuecede7cuW3HH1rGeeedd6o7d+5UFy1aJMs4feGpp55Ss7KyVKPRqI4bN079/vvv27522mmnqVddddVhx7/11ltqv379VKPRqA4ePFj95JNPfBxx4PPmmmZnZ6vAEY/777/f94EHMG9/T39OEogjeXs9v/vuO3X8+PGqyWRSe/Xqpf79739XXS6Xj6MObN5cU6fTqf7lL39Re/furZrNZjUzM1P9zW9+o9bU1Pg+8AD05Zdftvu+eOgaXnXVVeppp512xDkjRoxQjUaj2qtXL/V///ufZvHJdt5CCCGE8Fq3nwMhhBBCCO9JAiGEEEIIr0kCIYQQQgivSQIhhBBCCK9JAiGEEEIIr0kCIYQQQgivSQIhhBBCCK9JAiGEEEIIr0kCIYQQQgivSQIhhBBCCK9JAiGEEEIIr0kCIYQQQgiv/T+UzCM0lKRJSwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "_, axes = plt.subplots(1, 1, figsize=(6, 4))\n", + "\n", + "# plot the 12th CV fold\n", + "num = 12\n", + "\n", + "# plot the training data in black\n", + "axes.plot(\n", + " cv_folds.train_X[num - 1].detach().numpy(),\n", + " cv_folds.train_Y[num - 1].detach().numpy(),\n", + " \"k*\",\n", + ")\n", + "\n", + "# plot the test data in red\n", + "axes.plot(\n", + " cv_folds.test_X[num - 1].detach().numpy(),\n", + " cv_folds.test_Y[num - 1].detach().numpy(),\n", + " \"r*\",\n", + ")\n", + "\n", + "# plot posterior means as blue line\n", + "axes.plot(plot_x[num - 1].numpy(), mean[num - 1].numpy(), \"b\")\n", + "\n", + "# shade between the lower and upper confidence bounds\n", + "axes.fill_between(\n", + " plot_x[num - 1].numpy(), lower[num - 1].numpy(), upper[num - 1].numpy(), alpha=0.5\n", + ");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "fileHeader": "", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/website-old/static/files/batch_mode_cross_validation.py b/website-old/static/files/batch_mode_cross_validation.py new file mode 100644 index 0000000000..8c2fa4db4f --- /dev/null +++ b/website-old/static/files/batch_mode_cross_validation.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Application of batch-mode regression to cross-validation +# +# botorch provides a helper function `gen_loo_cv_folds` to easily perform leave-one-out (LOO) cross-validation (CV) by taking advantage of batch-mode regression and evaluation in GPyTorch. This tutorial illustrates the process on a noisy sinusoidal function, similar to the example from the batch-mode GP regression [tutorial](https://github.com/cornellius-gp/gpytorch/blob/master/examples/01_Exact_GPs/Simple_GP_Regression.ipynb) from GPyTorch: +# +# $$y = \sin(2\pi x) + \epsilon, ~\epsilon \sim \mathcal N(0, 0.2).$$ +# +# Note: this tutorial aims to introduce batch-mode regression and evaluation in GPyTorch with CV as an example application. For alternative, more user-friendly functions to perform CV in Ax, see [ax.modelbridge.cross_validation](https://github.com/facebook/Ax/blob/main/ax/modelbridge/cross_validation.py). However, for larger CV tasks, it may be useful to exploit GPyTorch batch-mode, as shown in this tutorial. + +# In[1]: + + +import torch +import math + +device = torch.device("cpu") +dtype = torch.float64 +torch.manual_seed(3); + + +# ### Initialize the CV dataset +# +# For our training data, we take 20 regularly spaced points on the interval $[0, 1]$ and generate noisy evaluations with an observed noise variance of 0.2. Remember that botorch requires an explicit output dimension. + +# In[2]: + + +sigma = math.sqrt(0.2) +train_X = torch.linspace(0, 1, 20, dtype=dtype, device=device).view(-1, 1) +train_Y_noiseless = torch.sin(train_X * (2 * math.pi)) +train_Y = train_Y_noiseless + sigma * torch.randn_like(train_Y_noiseless) +train_Yvar = torch.full_like(train_Y, 0.2) + + +# The botorch function `gen_loo_cv_folds` takes our observed data `train_X`, `train_Y`, `train_Yvar` as input and returns the LOO CV folds in a `CVFolds` object. + +# In[3]: + + +from botorch.cross_validation import gen_loo_cv_folds + +cv_folds = gen_loo_cv_folds(train_X=train_X, train_Y=train_Y, train_Yvar=train_Yvar) + + +# The `cv_folds` object contains the data, stored as tensors of appropriate batch shape, necessary to perform 20 CVs of 19 training points and 1 test point. For example, we can check that the shapes of the training inputs and training targets are `b x n x d = 20 x 19 x 1` and `b x n x o = 20 x 19 x 1` respectively, where `o` is the number of outputs. + +# In[4]: + + +cv_folds.train_X.shape, cv_folds.train_Y.shape + + +# In[5]: + + +cv_folds.test_X.shape, cv_folds.test_Y.shape + + +# Note that in a situation where the dataset is large, one may not want to perform LOO; in that case, a similar process can be used to perform $k$-fold CV. + +# ### Perform LOOCV +# +# We can use the `batch_cross_validation` function to perform LOOCV using batching (meaning that the `b = 20` sets of training data can be fit as `b = 20` separate GP models with separate hyperparameters in parallel through GPyTorch) and return a CVResult tuple with the batched `GPyTorchPosterior` object over the LOOCV test points and the observed targets. The `batch_cross_validation` requires a model class (`model_cls`) and a marginal log likelihood class (`mll_cls`). We will use the SingleTaskGP as the `model_cls` and an ExactMarginalLogLikelihood as the `mll_cls`. + +# In[6]: + + +from botorch.cross_validation import batch_cross_validation +from botorch.models import SingleTaskGP +from botorch.models.transforms.input import Normalize +from botorch.models.transforms.outcome import Standardize +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood + +input_transform = Normalize(d=train_X.shape[-1]) +outcome_transform = Standardize( + m=train_Y.shape[-1], + batch_shape=cv_folds.train_Y.shape[:-2], +) + +# instantiate and fit model +cv_results = batch_cross_validation( + model_cls=SingleTaskGP, + mll_cls=ExactMarginalLogLikelihood, + cv_folds=cv_folds, + model_init_kwargs={ + "input_transform": input_transform, + "outcome_transform": outcome_transform, + }, +) + + +# #### Compute the cross-validation error and generate plots +# To compute the cross-validation error, we first evaluate the test points by computing the posterior in batch mode. Next, we compute the squared errors for each test point from the prediction and take an average across all cross-validation folds. + +# In[7]: + + +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + +posterior = cv_results.posterior +mean = posterior.mean +cv_error = ((cv_folds.test_Y.squeeze() - mean.squeeze()) ** 2).mean() +print(f"Cross-validation error: {cv_error : 4.2}") + +# get lower and upper confidence bounds +lower, upper = posterior.mvn.confidence_region() + +# scatterplot of predicted versus test +_, axes = plt.subplots(1, 1, figsize=(6, 4)) +plt.plot([-1.5, 1.5], [-1.5, 1.5], "k", label="true objective", linewidth=2) + +axes.set_xlabel("Actual") +axes.set_ylabel("Predicted") + +axes.errorbar( + x=cv_folds.test_Y.numpy().flatten(), + y=mean.numpy().flatten(), + xerr=1.96 * sigma, + yerr=((upper - lower) / 2).numpy().flatten(), + fmt="*", +); + + +# Finally, we can visualize the fitted models. To do this, we again take advantage of batch-mode evaluation to obtain predictions, including lower and upper confidence regions, from each of the 20 models. + +# In[8]: + + +model = cv_results.model +with torch.no_grad(): + # evaluate the models at a series of points for plotting + plot_x = ( + torch.linspace(0, 1, 101).view(1, -1, 1).repeat(cv_folds.train_X.shape[0], 1, 1) + ) + posterior = model.posterior(plot_x) + mean = posterior.mean + + # get lower and upper confidence bounds + lower, upper = posterior.mvn.confidence_region() + plot_x.squeeze_() + + +# The code snippet below plots the result for the 12th CV fold (by setting `num = 12`), but note that we have computed the results for all folds above (other plots can be obtained by iterating `num` from 1 to 20). + +# In[9]: + + +_, axes = plt.subplots(1, 1, figsize=(6, 4)) + +# plot the 12th CV fold +num = 12 + +# plot the training data in black +axes.plot( + cv_folds.train_X[num - 1].detach().numpy(), + cv_folds.train_Y[num - 1].detach().numpy(), + "k*", +) + +# plot the test data in red +axes.plot( + cv_folds.test_X[num - 1].detach().numpy(), + cv_folds.test_Y[num - 1].detach().numpy(), + "r*", +) + +# plot posterior means as blue line +axes.plot(plot_x[num - 1].numpy(), mean[num - 1].numpy(), "b") + +# shade between the lower and upper confidence bounds +axes.fill_between( + plot_x[num - 1].numpy(), lower[num - 1].numpy(), upper[num - 1].numpy(), alpha=0.5 +); + + +# In[ ]: + + + + diff --git a/website-old/static/files/baxus.ipynb b/website-old/static/files/baxus.ipynb new file mode 100644 index 0000000000..6bd064b4e5 --- /dev/null +++ b/website-old/static/files/baxus.ipynb @@ -0,0 +1,1538 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## BO with BAxUS and TS/EI\n", + "\n", + "In this tutorial, we show how to implement **B**ayesian optimization with **a**daptively e**x**panding s**u**bspace**s** (BAxUS) [1] in a closed loop in BoTorch.\n", + "The tutorial is purposefully similar to the [TuRBO tutorial](https://botorch.org/tutorials/turbo_1) to highlight the differences in the implementations.\n", + "\n", + "This implementation supports either Expected Improvement (EI) or Thompson sampling (TS). We optimize the Branin2 function [2] with 498 dummy dimensions and show that BAxUS outperforms EI as well as Sobol.\n", + "\n", + "Since BoTorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\\max_{x\\in \\mathcal{X}} -f(x)=0$.\n", + "\n", + "- [1]: [Papenmeier, Leonard, et al. Increasing the Scope as You Learn: Adaptive Bayesian Optimization in Nested Subspaces. Advances in Neural Information Processing Systems. 2022](https://openreview.net/pdf?id=e4Wf6112DI)\n", + "- [2]: [Branin Test Function](https://www.sfu.ca/~ssurjano/branin.html)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running on cpu\n" + ] + } + ], + "source": [ + "import math\n", + "import os\n", + "from dataclasses import dataclass\n", + "\n", + "import botorch\n", + "import gpytorch\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "from gpytorch.constraints import Interval\n", + "from gpytorch.kernels import MaternKernel, ScaleKernel\n", + "from gpytorch.likelihoods import GaussianLikelihood\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "from torch.quasirandom import SobolEngine\n", + "\n", + "from botorch.acquisition.analytic import LogExpectedImprovement\n", + "from botorch.exceptions import ModelFittingError\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.generation import MaxPosteriorSampling\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.test_functions import Branin\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "print(f\"Running on {device}\")\n", + "dtype = torch.double\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimize the augmented Branin function\n", + "\n", + "The goal is to minimize the embedded Branin function\n", + "\n", + "$f(x_1, x_2, \\ldots, x_{20}) = \\left (x_2-\\frac{5.1}{4\\pi^2}x_1^2+\\frac{5}{\\pi}x_1-6\\right )^2+10\\cdot \\left (1-\\frac{1}{8\\pi}\\right )\\cos(x_1)+10$\n", + "\n", + "with bounds [-5, 10] for $x_1$ and [0, 15] for $x_2$ (all other dimensions are ignored). The function has three minima with an optimal value of $0.397887$.\n", + "\n", + "As mentioned above, since botorch assumes a maximization problem, we instead maximize $-f(x)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define a function with dummy variables\n", + "\n", + "We first define a new function where we only pass the first two input dimensions to the actual Branin function." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "branin = Branin(negate=True).to(device=device, dtype=dtype)\n", + "\n", + "\n", + "def branin_emb(x):\n", + " \"\"\"x is assumed to be in [-1, 1]^D\"\"\"\n", + " lb, ub = branin.bounds\n", + " return branin(lb + (ub - lb) * (x[..., :2] + 1) / 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "fun = branin_emb\n", + "dim = 500 if not SMOKE_TEST else 50\n", + "\n", + "n_init = 10 if not SMOKE_TEST else 4\n", + "max_cholesky_size = float(\"inf\") # Always use Cholesky" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Maintain the BAxUS state\n", + "BAxUS needs to maintain a state, which includes the length of the trust region, success and failure counters, success and failure tolerance, etc. \n", + "In contrast to TuRBO, the failure tolerance depends on the target dimensionality.\n", + "\n", + "In this tutorial we store the state in a dataclass and update the state of TuRBO after each batch evaluation. \n", + "\n", + "**Note**: These settings assume that the domain has been scaled to $[-1, 1]^d$" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "@dataclass\n", + "class BaxusState:\n", + " dim: int\n", + " eval_budget: int\n", + " new_bins_on_split: int = 3\n", + " d_init: int = float(\"nan\") # Note: post-initialized\n", + " target_dim: int = float(\"nan\") # Note: post-initialized\n", + " n_splits: int = float(\"nan\") # Note: post-initialized\n", + " length: float = 0.8\n", + " length_init: float = 0.8\n", + " length_min: float = 0.5**7\n", + " length_max: float = 1.6\n", + " failure_counter: int = 0\n", + " success_counter: int = 0\n", + " success_tolerance: int = 3\n", + " best_value: float = -float(\"inf\")\n", + " restart_triggered: bool = False\n", + "\n", + " def __post_init__(self):\n", + " n_splits = round(math.log(self.dim, self.new_bins_on_split + 1))\n", + " self.d_init = 1 + np.argmin(\n", + " np.abs(\n", + " (1 + np.arange(self.new_bins_on_split))\n", + " * (1 + self.new_bins_on_split) ** n_splits\n", + " - self.dim\n", + " )\n", + " )\n", + " self.target_dim = self.d_init\n", + " self.n_splits = n_splits\n", + "\n", + " @property\n", + " def split_budget(self) -> int:\n", + " return round(\n", + " -1\n", + " * (self.new_bins_on_split * self.eval_budget * self.target_dim)\n", + " / (self.d_init * (1 - (self.new_bins_on_split + 1) ** (self.n_splits + 1)))\n", + " )\n", + "\n", + " @property\n", + " def failure_tolerance(self) -> int:\n", + " if self.target_dim == self.dim:\n", + " return self.target_dim\n", + " k = math.floor(math.log(self.length_min / self.length_init, 0.5))\n", + " split_budget = self.split_budget\n", + " return min(self.target_dim, max(1, math.floor(split_budget / k)))\n", + "\n", + "\n", + "def update_state(state, Y_next):\n", + " if max(Y_next) > state.best_value + 1e-3 * math.fabs(state.best_value):\n", + " state.success_counter += 1\n", + " state.failure_counter = 0\n", + " else:\n", + " state.success_counter = 0\n", + " state.failure_counter += 1\n", + "\n", + " if state.success_counter == state.success_tolerance: # Expand trust region\n", + " state.length = min(2.0 * state.length, state.length_max)\n", + " state.success_counter = 0\n", + " elif state.failure_counter == state.failure_tolerance: # Shrink trust region\n", + " state.length /= 2.0\n", + " state.failure_counter = 0\n", + "\n", + " state.best_value = max(state.best_value, max(Y_next).item())\n", + " if state.length < state.length_min:\n", + " state.restart_triggered = True\n", + " return state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a BAxUS embedding\n", + "\n", + "We now show how to create the BAxUS embedding. The essential idea is to assign input dimensions to target dimensions and to assign a sign $\\in \\pm 1$ to each input dimension, similar to the HeSBO embedding. \n", + "We create the embedding matrix that is used to project points from the target to the input space. The matrix is sparse, each column has precisely one non-zero entry that is either 1 or -1." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([[ 1., 0., 1., 1., 0., 0., 0., 0., 0., -1.],\n", + " [ 0., 0., 0., 0., 1., 0., 1., 0., -1., 0.],\n", + " [ 0., -1., 0., 0., 0., 1., 0., -1., 0., 0.]],\n", + " dtype=torch.float64)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def embedding_matrix(input_dim: int, target_dim: int) -> torch.Tensor:\n", + " if (\n", + " target_dim >= input_dim\n", + " ): # return identity matrix if target size greater than input size\n", + " return torch.eye(input_dim, device=device, dtype=dtype)\n", + "\n", + " input_dims_perm = (\n", + " torch.randperm(input_dim, device=device) + 1\n", + " ) # add 1 to indices for padding column in matrix\n", + "\n", + " bins = torch.tensor_split(\n", + " input_dims_perm, target_dim\n", + " ) # split dims into almost equally-sized bins\n", + " bins = torch.nn.utils.rnn.pad_sequence(\n", + " bins, batch_first=True\n", + " ) # zero pad bins, the index 0 will be cut off later\n", + "\n", + " mtrx = torch.zeros(\n", + " (target_dim, input_dim + 1), dtype=dtype, device=device\n", + " ) # add one extra column for padding\n", + " mtrx = mtrx.scatter_(\n", + " 1,\n", + " bins,\n", + " 2 * torch.randint(2, (target_dim, input_dim), dtype=dtype, device=device) - 1,\n", + " ) # fill mask with random +/- 1 at indices\n", + "\n", + " return mtrx[:, 1:] # cut off index zero as this corresponds to zero padding\n", + "\n", + "\n", + "embedding_matrix(10, 3) # example for an embedding matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Function to increase the embedding\n", + "\n", + "Next, we write a helper function to increase the embedding and to bring observations to the increased target space." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def increase_embedding_and_observations(\n", + " S: torch.Tensor, X: torch.Tensor, n_new_bins: int\n", + ") -> torch.Tensor:\n", + " assert X.size(1) == S.size(0), \"Observations don't lie in row space of S\"\n", + "\n", + " S_update = S.clone()\n", + " X_update = X.clone()\n", + "\n", + " for row_idx in range(len(S)):\n", + " row = S[row_idx]\n", + " idxs_non_zero = torch.nonzero(row)\n", + " idxs_non_zero = idxs_non_zero[torch.randperm(len(idxs_non_zero))].reshape(-1)\n", + "\n", + " if len(idxs_non_zero) <= 1:\n", + " continue\n", + "\n", + " non_zero_elements = row[idxs_non_zero].reshape(-1)\n", + "\n", + " n_row_bins = min(\n", + " n_new_bins, len(idxs_non_zero)\n", + " ) # number of new bins is always less or equal than the contributing input dims in the row minus one\n", + "\n", + " new_bins = torch.tensor_split(idxs_non_zero, n_row_bins)[\n", + " 1:\n", + " ] # the dims in the first bin won't be moved\n", + " elements_to_move = torch.tensor_split(non_zero_elements, n_row_bins)[1:]\n", + "\n", + " new_bins_padded = torch.nn.utils.rnn.pad_sequence(\n", + " new_bins, batch_first=True\n", + " ) # pad the tuples of bins with zeros to apply _scatter\n", + " els_to_move_padded = torch.nn.utils.rnn.pad_sequence(\n", + " elements_to_move, batch_first=True\n", + " )\n", + "\n", + " S_stack = torch.zeros(\n", + " (n_row_bins - 1, len(row) + 1), device=device, dtype=dtype\n", + " ) # submatrix to stack on S_update\n", + "\n", + " S_stack = S_stack.scatter_(\n", + " 1, new_bins_padded + 1, els_to_move_padded\n", + " ) # fill with old values (add 1 to indices for padding column)\n", + "\n", + " S_update[\n", + " row_idx, torch.hstack(new_bins)\n", + " ] = 0 # set values that were move to zero in current row\n", + "\n", + " X_update = torch.hstack(\n", + " (X_update, X[:, row_idx].reshape(-1, 1).repeat(1, len(new_bins)))\n", + " ) # repeat observations for row at the end of X (column-wise)\n", + " S_update = torch.vstack(\n", + " (S_update, S_stack[:, 1:])\n", + " ) # stack onto S_update except for padding column\n", + "\n", + " return S_update, X_update" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "S before increase\n", + "tensor([[ 1., 0., 1., -1., 1., 0., 0., 0., 0., -1.],\n", + " [ 0., 1., 0., 0., 0., 1., -1., 1., -1., 0.]],\n", + " dtype=torch.float64)\n", + "X before increase\n", + "tensor([[66, 38],\n", + " [22, 2],\n", + " [19, 43],\n", + " [51, 10],\n", + " [16, 62],\n", + " [31, 25],\n", + " [27, 22]])\n", + "S after increase\n", + "tensor([[ 0., 0., 1., 0., 0., 0., 0., 0., 0., -1.],\n", + " [ 0., 0., 0., 0., 0., 1., 0., 1., 0., 0.],\n", + " [ 0., 0., 0., -1., 1., 0., 0., 0., 0., 0.],\n", + " [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [ 0., 1., 0., 0., 0., 0., 0., 0., -1., 0.],\n", + " [ 0., 0., 0., 0., 0., 0., -1., 0., 0., 0.]],\n", + " dtype=torch.float64)\n", + "X after increase\n", + "tensor([[66, 38, 66, 66, 38, 38],\n", + " [22, 2, 22, 22, 2, 2],\n", + " [19, 43, 19, 19, 43, 43],\n", + " [51, 10, 51, 51, 10, 10],\n", + " [16, 62, 16, 16, 62, 62],\n", + " [31, 25, 31, 31, 25, 25],\n", + " [27, 22, 27, 27, 22, 22]])\n" + ] + } + ], + "source": [ + "S = embedding_matrix(10, 2)\n", + "X = torch.randint(100, (7, 2))\n", + "print(f\"S before increase\\n{S}\")\n", + "print(f\"X before increase\\n{X}\")\n", + "\n", + "S, X = increase_embedding_and_observations(S, X, 3)\n", + "print(f\"S after increase\\n{S}\")\n", + "print(f\"X after increase\\n{X}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Take a look at the state" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BaxusState(dim=500, eval_budget=500, new_bins_on_split=3, d_init=2, target_dim=2, n_splits=4, length=0.8, length_init=0.8, length_min=0.0078125, length_max=1.6, failure_counter=0, success_counter=0, success_tolerance=3, best_value=-inf, restart_triggered=False)\n" + ] + } + ], + "source": [ + "state = BaxusState(dim=dim, eval_budget=500)\n", + "print(state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate initial points\n", + "This generates an initial set of Sobol points that we use to start of the BO loop." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def get_initial_points(dim: int, n_pts: int, seed=0):\n", + " sobol = SobolEngine(dimension=dim, scramble=True, seed=seed)\n", + " X_init = (\n", + " 2 * sobol.draw(n=n_pts).to(dtype=dtype, device=device) - 1\n", + " ) # points have to be in [-1, 1]^d\n", + " return X_init" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate new batch\n", + "Given the current `state` and a probabilistic (GP) `model` built from observations `X` and `Y`, we generate a new batch of points. \n", + "\n", + "This method works on the domain $[-1, +1]^d$, so make sure to not pass in observations from the true domain. `unnormalize` is called before the true function is evaluated which will first map the points back to the original domain.\n", + "\n", + "We support either TS and qEI which can be specified via the `acqf` argument." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def create_candidate(\n", + " state,\n", + " model, # GP model\n", + " X, # Evaluated points on the domain [-1, 1]^d\n", + " Y, # Function values\n", + " n_candidates=None, # Number of candidates for Thompson sampling\n", + " num_restarts=10,\n", + " raw_samples=512,\n", + " acqf=\"ts\", # \"ei\" or \"ts\"\n", + "):\n", + " assert acqf in (\"ts\", \"ei\")\n", + " assert X.min() >= -1.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))\n", + " if n_candidates is None:\n", + " n_candidates = min(5000, max(2000, 200 * X.shape[-1]))\n", + "\n", + " # Scale the TR to be proportional to the lengthscales\n", + " x_center = X[Y.argmax(), :].clone()\n", + " weights = model.covar_module.lengthscale.detach().view(-1)\n", + " weights = weights / weights.mean()\n", + " weights = weights / torch.prod(weights.pow(1.0 / len(weights)))\n", + " tr_lb = torch.clamp(x_center - weights * state.length, -1.0, 1.0)\n", + " tr_ub = torch.clamp(x_center + weights * state.length, -1.0, 1.0)\n", + "\n", + " if acqf == \"ts\":\n", + " dim = X.shape[-1]\n", + " sobol = SobolEngine(dim, scramble=True)\n", + " pert = sobol.draw(n_candidates).to(dtype=dtype, device=device)\n", + " pert = tr_lb + (tr_ub - tr_lb) * pert\n", + "\n", + " # Create a perturbation mask\n", + " prob_perturb = min(20.0 / dim, 1.0)\n", + " mask = torch.rand(n_candidates, dim, dtype=dtype, device=device) <= prob_perturb\n", + " ind = torch.where(mask.sum(dim=1) == 0)[0]\n", + " mask[ind, torch.randint(0, dim, size=(len(ind),), device=device)] = 1\n", + "\n", + " # Create candidate points from the perturbations and the mask\n", + " X_cand = x_center.expand(n_candidates, dim).clone()\n", + " X_cand[mask] = pert[mask]\n", + "\n", + " # Sample on the candidate points\n", + " thompson_sampling = MaxPosteriorSampling(model=model, replacement=False)\n", + " with torch.no_grad(): # We don't need gradients when using TS\n", + " X_next = thompson_sampling(X_cand, num_samples=1)\n", + "\n", + " elif acqf == \"ei\":\n", + " ei = LogExpectedImprovement(model, train_Y.max())\n", + " X_next, acq_value = optimize_acqf(\n", + " ei,\n", + " bounds=torch.stack([tr_lb, tr_ub]),\n", + " q=1,\n", + " num_restarts=num_restarts,\n", + " raw_samples=raw_samples,\n", + " )\n", + "\n", + " return X_next" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimization loop\n", + "This simple loop runs one instance of BAxUS with Thompson sampling until convergence.\n", + "\n", + "BAxUS works on a fixed evaluation budget and shrinks the trust region until the minimal trust region size is reached (`state[\"restart_triggered\"]` is set to `True`).\n", + "Then, BAxUS increases the target space and carries over the observations to the updated space. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 11, d=2) Best value: -6.04, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 12, d=2) Best value: -0.951, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 13, d=2) Best value: -0.926, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 14, d=2) Best value: -0.925, TR length: 0.8\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 15, d=2) Best value: -0.925, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 16, d=2) Best value: -0.925, TR length: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 17, d=2) Best value: -0.925, TR length: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 18, d=2) Best value: -0.925, TR length: 0.05\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 19, d=2) Best value: -0.925, TR length: 0.025\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 20, d=2) Best value: -0.925, TR length: 0.0125\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 21, d=2) Best value: -0.925, TR length: 0.00625\n", + "increasing target space\n", + "new dimensionality: 6\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 22, d=6) Best value: -0.475, TR length: 0.8\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 23, d=6) Best value: -0.475, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 24, d=6) Best value: -0.475, TR length: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 25, d=6) Best value: -0.475, TR length: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 26, d=6) Best value: -0.475, TR length: 0.05\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 27, d=6) Best value: -0.466, TR length: 0.05\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 28, d=6) Best value: -0.466, TR length: 0.05\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 29, d=6) Best value: -0.458, TR length: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 30, d=6) Best value: -0.455, TR length: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 31, d=6) Best value: -0.444, TR length: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 32, d=6) Best value: -0.436, TR length: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 33, d=6) Best value: -0.423, TR length: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 34, d=6) Best value: -0.413, TR length: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 35, d=6) Best value: -0.408, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 36, d=6) Best value: -0.401, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 37, d=6) Best value: -0.399, TR length: 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 38, d=6) Best value: -0.399, TR length: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 39, d=6) Best value: -0.399, TR length: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 40, d=6) Best value: -0.398, TR length: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 41, d=6) Best value: -0.398, TR length: 0.05\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 42, d=6) Best value: -0.398, TR length: 0.025\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 43, d=6) Best value: -0.398, TR length: 0.0125\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 44, d=6) Best value: -0.398, TR length: 0.00625\n", + "increasing target space\n", + "new dimensionality: 18\n", + "iteration 45, d=18) Best value: -0.398, TR length: 0.4\n", + "iteration 46, d=18) Best value: -0.398, TR length: 0.2\n", + "iteration 47, d=18) Best value: -0.398, TR length: 0.1\n", + "iteration 48, d=18) Best value: -0.398, TR length: 0.05\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 49, d=18) Best value: -0.398, TR length: 0.025\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 50, d=18) Best value: -0.398, TR length: 0.0125\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/linear_operator/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 51, d=18) Best value: -0.398, TR length: 0.00625\n", + "increasing target space\n", + "new dimensionality: 54\n", + "iteration 52, d=54) Best value: -0.398, TR length: 0.4\n", + "iteration 53, d=54) Best value: -0.398, TR length: 0.2\n", + "iteration 54, d=54) Best value: -0.398, TR length: 0.1\n", + "iteration 55, d=54) Best value: -0.398, TR length: 0.05\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/balandat/Code/botorch/botorch/optim/fit.py:104: OptimizationWarning: `scipy_minimize` terminated with status OptimizationStatus.FAILURE, displaying original message from `scipy.optimize.minimize`: ABNORMAL_TERMINATION_IN_LNSRCH\n", + " warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iteration 56, d=54) Best value: -0.398, TR length: 0.025\n", + "iteration 57, d=54) Best value: -0.398, TR length: 0.0125\n", + "iteration 58, d=54) Best value: -0.398, TR length: 0.00625\n", + "increasing target space\n", + "new dimensionality: 162\n", + "iteration 59, d=162) Best value: -0.398, TR length: 0.8\n", + "iteration 60, d=162) Best value: -0.398, TR length: 0.8\n", + "iteration 61, d=162) Best value: -0.398, TR length: 0.4\n", + "iteration 62, d=162) Best value: -0.398, TR length: 0.4\n", + "iteration 63, d=162) Best value: -0.398, TR length: 0.4\n", + "iteration 64, d=162) Best value: -0.398, TR length: 0.2\n", + "iteration 65, d=162) Best value: -0.398, TR length: 0.2\n", + "iteration 66, d=162) Best value: -0.398, TR length: 0.2\n", + "iteration 67, d=162) Best value: -0.398, TR length: 0.1\n", + "iteration 68, d=162) Best value: -0.398, TR length: 0.1\n", + "iteration 69, d=162) Best value: -0.398, TR length: 0.1\n", + "iteration 70, d=162) Best value: -0.398, TR length: 0.05\n", + "iteration 71, d=162) Best value: -0.398, TR length: 0.05\n", + "iteration 72, d=162) Best value: -0.398, TR length: 0.05\n", + "iteration 73, d=162) Best value: -0.398, TR length: 0.025\n", + "iteration 74, d=162) Best value: -0.398, TR length: 0.025\n", + "iteration 75, d=162) Best value: -0.398, TR length: 0.025\n", + "iteration 76, d=162) Best value: -0.398, TR length: 0.0125\n", + "iteration 77, d=162) Best value: -0.398, TR length: 0.0125\n", + "iteration 78, d=162) Best value: -0.398, TR length: 0.0125\n", + "iteration 79, d=162) Best value: -0.398, TR length: 0.00625\n", + "increasing target space\n", + "new dimensionality: 485\n", + "iteration 80, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 81, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 82, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 83, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 84, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 85, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 86, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 87, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 88, d=485) Best value: -0.398, TR length: 0.8\n", + "iteration 89, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 90, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 91, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 92, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 93, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 94, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 95, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 96, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 97, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 98, d=485) Best value: -0.398, TR length: 0.4\n", + "iteration 99, d=485) Best value: -0.398, TR length: 0.2\n", + "iteration 100, d=485) Best value: -0.398, TR length: 0.2\n" + ] + } + ], + "source": [ + "EVALUATION_BUDGET = 100 if not SMOKE_TEST else 10\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "N_CANDIDATES = min(5000, max(2000, 200 * dim)) if not SMOKE_TEST else 4\n", + "\n", + "\n", + "state = BaxusState(dim=dim, eval_budget=EVALUATION_BUDGET - n_init)\n", + "S = embedding_matrix(input_dim=state.dim, target_dim=state.d_init)\n", + "\n", + "X_baxus_target = get_initial_points(state.d_init, n_init)\n", + "X_baxus_input = X_baxus_target @ S\n", + "Y_baxus = torch.tensor(\n", + " [branin_emb(x) for x in X_baxus_input], dtype=dtype, device=device\n", + ").unsqueeze(-1)\n", + "\n", + "\n", + "# Disable input scaling checks as we normalize to [-1, 1]\n", + "with botorch.settings.validate_input_scaling(False):\n", + " for _ in range(EVALUATION_BUDGET - n_init): # Run until evaluation budget depleted\n", + " # Fit a GP model\n", + " train_Y = (Y_baxus - Y_baxus.mean()) / Y_baxus.std()\n", + " likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))\n", + " model = SingleTaskGP(\n", + " X_baxus_target, train_Y, likelihood=likelihood\n", + " )\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "\n", + " # Do the fitting and acquisition function optimization inside the Cholesky context\n", + " with gpytorch.settings.max_cholesky_size(max_cholesky_size):\n", + " # Fit the model\n", + " try:\n", + " fit_gpytorch_mll(mll)\n", + " except ModelFittingError:\n", + " # Right after increasing the target dimensionality, the covariance matrix becomes indefinite\n", + " # In this case, the Cholesky decomposition might fail due to numerical instabilities\n", + " # In this case, we revert to Adam-based optimization\n", + " optimizer = torch.optim.Adam([{\"params\": model.parameters()}], lr=0.1)\n", + "\n", + " for _ in range(100):\n", + " optimizer.zero_grad()\n", + " output = model(X_baxus_target)\n", + " loss = -mll(output, train_Y.flatten())\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " # Create a batch\n", + " X_next_target = create_candidate(\n", + " state=state,\n", + " model=model,\n", + " X=X_baxus_target,\n", + " Y=train_Y,\n", + " n_candidates=N_CANDIDATES,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " acqf=\"ts\",\n", + " )\n", + "\n", + " X_next_input = X_next_target @ S\n", + "\n", + " Y_next = torch.tensor(\n", + " [branin_emb(x) for x in X_next_input], dtype=dtype, device=device\n", + " ).unsqueeze(-1)\n", + "\n", + " # Update state\n", + " state = update_state(state=state, Y_next=Y_next)\n", + "\n", + " # Append data\n", + " X_baxus_input = torch.cat((X_baxus_input, X_next_input), dim=0)\n", + " X_baxus_target = torch.cat((X_baxus_target, X_next_target), dim=0)\n", + " Y_baxus = torch.cat((Y_baxus, Y_next), dim=0)\n", + "\n", + " # Print current status\n", + " print(\n", + " f\"iteration {len(X_baxus_input)}, d={len(X_baxus_target.T)}) Best value: {state.best_value:.3}, TR length: {state.length:.3}\"\n", + " )\n", + "\n", + " if state.restart_triggered:\n", + " state.restart_triggered = False\n", + " print(\"increasing target space\")\n", + " S, X_baxus_target = increase_embedding_and_observations(\n", + " S, X_baxus_target, state.new_bins_on_split\n", + " )\n", + " print(f\"new dimensionality: {len(S)}\")\n", + " state.target_dim = len(S)\n", + " state.length = state.length_init\n", + " state.failure_counter = 0\n", + " state.success_counter = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GP-LogEI\n", + "As a baseline, we compare BAxUS to Log Expected Improvement (LogEI)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "11) Best value: -4.16e-01\n", + "12) Best value: -4.16e-01\n", + "13) Best value: -4.16e-01\n", + "14) Best value: -4.16e-01\n", + "15) Best value: -4.16e-01\n", + "16) Best value: -4.16e-01\n", + "17) Best value: -4.16e-01\n", + "18) Best value: -4.16e-01\n", + "19) Best value: -4.16e-01\n", + "20) Best value: -4.16e-01\n", + "21) Best value: -4.16e-01\n", + "22) Best value: -4.16e-01\n", + "23) Best value: -4.16e-01\n", + "24) Best value: -4.16e-01\n", + "25) Best value: -4.16e-01\n", + "26) Best value: -4.16e-01\n", + "27) Best value: -4.16e-01\n", + "28) Best value: -4.16e-01\n", + "29) Best value: -4.16e-01\n", + "30) Best value: -4.16e-01\n", + "31) Best value: -4.16e-01\n", + "32) Best value: -4.16e-01\n", + "33) Best value: -4.16e-01\n", + "34) Best value: -4.16e-01\n", + "35) Best value: -4.16e-01\n", + "36) Best value: -4.16e-01\n", + "37) Best value: -4.16e-01\n", + "38) Best value: -4.16e-01\n", + "39) Best value: -4.16e-01\n", + "40) Best value: -4.16e-01\n", + "41) Best value: -4.14e-01\n", + "42) Best value: -4.14e-01\n", + "43) Best value: -4.14e-01\n", + "44) Best value: -4.14e-01\n", + "45) Best value: -4.14e-01\n", + "46) Best value: -4.14e-01\n", + "47) Best value: -4.14e-01\n", + "48) Best value: -4.14e-01\n", + "49) Best value: -4.14e-01\n", + "50) Best value: -4.14e-01\n", + "51) Best value: -4.14e-01\n", + "52) Best value: -4.14e-01\n", + "53) Best value: -4.14e-01\n", + "54) Best value: -4.14e-01\n", + "55) Best value: -4.14e-01\n", + "56) Best value: -4.14e-01\n", + "57) Best value: -4.14e-01\n", + "58) Best value: -4.14e-01\n", + "59) Best value: -4.14e-01\n", + "60) Best value: -4.14e-01\n", + "61) Best value: -4.08e-01\n", + "62) Best value: -4.08e-01\n", + "63) Best value: -4.08e-01\n", + "64) Best value: -4.08e-01\n", + "65) Best value: -4.02e-01\n", + "66) Best value: -4.02e-01\n", + "67) Best value: -4.02e-01\n", + "68) Best value: -4.02e-01\n", + "69) Best value: -4.02e-01\n", + "70) Best value: -4.02e-01\n", + "71) Best value: -4.02e-01\n", + "72) Best value: -4.02e-01\n", + "73) Best value: -4.02e-01\n", + "74) Best value: -4.02e-01\n", + "75) Best value: -4.02e-01\n", + "76) Best value: -4.02e-01\n", + "77) Best value: -4.02e-01\n", + "78) Best value: -4.02e-01\n", + "79) Best value: -4.02e-01\n", + "80) Best value: -4.02e-01\n", + "81) Best value: -4.00e-01\n", + "82) Best value: -4.00e-01\n", + "83) Best value: -4.00e-01\n", + "84) Best value: -4.00e-01\n", + "85) Best value: -4.00e-01\n", + "86) Best value: -4.00e-01\n", + "87) Best value: -4.00e-01\n", + "88) Best value: -4.00e-01\n", + "89) Best value: -4.00e-01\n", + "90) Best value: -4.00e-01\n", + "91) Best value: -4.00e-01\n", + "92) Best value: -4.00e-01\n", + "93) Best value: -4.00e-01\n", + "94) Best value: -4.00e-01\n", + "95) Best value: -4.00e-01\n", + "96) Best value: -4.00e-01\n", + "97) Best value: -4.00e-01\n", + "98) Best value: -4.00e-01\n", + "99) Best value: -4.00e-01\n", + "100) Best value: -4.00e-01\n" + ] + } + ], + "source": [ + "X_ei = get_initial_points(dim, n_init)\n", + "Y_ei = torch.tensor(\n", + " [branin_emb(x) for x in X_ei], dtype=dtype, device=device\n", + ").unsqueeze(-1)\n", + "bounds = torch.stack(\n", + " [\n", + " -torch.ones(dim, dtype=dtype, device=device),\n", + " torch.ones(dim, dtype=dtype, device=device),\n", + " ]\n", + ")\n", + "\n", + "\n", + "# Disable input scaling checks as we normalize to [-1, 1]\n", + "with botorch.settings.validate_input_scaling(False):\n", + " while len(Y_ei) < len(Y_baxus):\n", + " train_Y = (Y_ei - Y_ei.mean()) / Y_ei.std()\n", + " likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))\n", + " model = SingleTaskGP(X_ei, train_Y, likelihood=likelihood)\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " optimizer = torch.optim.Adam([{\"params\": model.parameters()}], lr=0.1)\n", + " model.train()\n", + " model.likelihood.train()\n", + " for _ in range(50):\n", + " optimizer.zero_grad()\n", + " output = model(X_ei)\n", + " loss = -mll(output, train_Y.squeeze())\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " # Create a batch\n", + " ei = LogExpectedImprovement(model, train_Y.max())\n", + " candidate, acq_value = optimize_acqf(\n", + " ei,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + " Y_next = torch.tensor(\n", + " [branin_emb(x) for x in candidate], dtype=dtype, device=device\n", + " ).unsqueeze(-1)\n", + "\n", + " # Append data\n", + " X_ei = torch.cat((X_ei, candidate), axis=0)\n", + " Y_ei = torch.cat((Y_ei, Y_next), axis=0)\n", + "\n", + " # Print current status\n", + " print(f\"{len(X_ei)}) Best value: {Y_ei.max().item():.2e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sobol" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "X_Sobol = (\n", + " SobolEngine(dim, scramble=True, seed=0)\n", + " .draw(len(X_baxus_input))\n", + " .to(dtype=dtype, device=device)\n", + " * 2\n", + " - 1\n", + ")\n", + "Y_Sobol = torch.tensor(\n", + " [branin_emb(x) for x in X_Sobol], dtype=dtype, device=device\n", + ").unsqueeze(-1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compare the methods\n", + "\n", + "We show the regret of the different methods." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAKBCAYAAADKqj3oAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACh4ElEQVR4nOzdd3yT1f4H8M+TNE33pi2FMmWVPQriYovoVXEioFa4justrl69ggu5Dq7zhyOKA8EtjisqTqYoMgtFoGzK7C7dM03O7w/sQ5+MJm2SPkn7eb9efdnn5OQ837SnmG/OkoQQAkRERERERC7QqB0AERERERH5PiYWRERERETkMiYWRERERETkMiYWRERERETkMiYWRERERETkMiYWRERERETkMiYWRERERETkMiYWRERERETkMiYWRERERETkMiYWRETtyLFjxyBJEiRJQrdu3dQOx2089bq6desmt3vs2DG3tetObfV36mvWr18v/x7Gjh2rdjhEqmBiQeRFli1bJv+Pydmv22+/vVn3WLNmDW699Vb07t0bwcHBiIqKwqBBg/DQQw9h//79LYp73759eOihhzBo0CBERUUhODgYvXv3RkpKCtasWeN0O029zpCQECQkJKBfv36YPHkyHnnkEXz11VcoKytrUczOuO2225r9++AbC/J1jd8g2/vy9/dHhw4dMGrUKNx3333Yvn272mETkRfwUzsAImodZWVluPPOO7F8+XJFeVVVFYqLi7F792688sorWLBgAebNm+d0u8888wwWLFgAo9GoKD906BAOHTqEDz74ANOnT8dbb72F0NDQFsdfWVmJyspK5OTkYP/+/fjll18AAMHBwbjpppuQlpaGpKSkFrdPRM4zGo0oLCxEYWEhtm7dildffRXTpk3DO++849LfORH5NiYWRF6qb9++mDBhgsN6F1xwgcM6RqMR11xzDdauXSuXDRgwAMOGDUNNTQ1+++035OTkwGg04pFHHoHRaMQTTzzhsN0nnngCTz31lHzdsWNHXHzxxQgICEB6ejr27t0LAPj0009RVFSE77//Hn5+zv2zM3XqVHTq1Em+rq+vR3FxMYqKirBz506cOXMGwNmEY8mSJfjoo4/w7LPP4oEHHoAkSU7dozmc/X006NWrl9tjIFJDamqqVVl1dTVOnTqFP/74AxUVFQCA5cuX49SpU1i/fr3T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fN++fU6126FDB/k5n3/+uc06jdttyYjFzz//rGjj559/bnYbjXl6xEKv1wudTicMBoPVJ6Hr169X7Hj17LPPipCQEBEWFia+/PJLq3aXL1+u+ET3119/tXn/xp+Y6nQ6AUBER0fb/Fl9++23IiwsTK4/efJku69r9erV8qeu/v7+4r///a+orKy0qrdz506RlJQkt3n33XfbbK+wsFDExsbK9QYOHCgyMzMVdUwmk3juueeEJEnC39/fqU/tJ0yYINcLDw8X3333nVWdH374QURGRip+RmhixMJXXruzmjNiUVVVJR588EG5rp+fn0hPT7dbv/HflJ+fnwDO7uRWXl6uqFdXV6f4m1iyZIl47bXXxKlTp+y2vWvXLjFixAi5/aeeespu3cYjFg0jgCkpKaKoqEhRr7KyUkyfPl2uGxISIioqKmy22dwRC39/fyFJkujZs6fYsmWLVd3PP/9c0f/ef/99u6+HyFswsSDyIs4mFg1ff/vb3+z+T04IId544w25rrNTg4qKihT32L9/v+LxzMxMxeNnzpxxqt3G05QWL15ss46ricWxY8cUbXzwwQfNbqOxxm+C+vbtK1JTU53+OnjwoM02GycWAMS7775r9/5PP/20oq4kSWLNmjV2699+++0O37Q2fmMDQGg0GrFx40a7ba5atUpR39b9TSaT6NWrl1znf//7n932hBAiJydHxMXFyW/cT548aVXnkUceUSTFeXl5dtuz/DnZe3P9yy+/KH6Wa9eutdvmhg0bhCRJinZtJRa+8tqbwzKxsNW/b7/9dnHZZZcpEs+wsDDx008/Ndl2478pAOL22293Od7GSkpK5OloHTt2FPX19TbrNU4sAIjp06fbbbO6uloxXeuzzz6zWa+5iUVDUn/69Gm7926ctF122WX2XziRl2BiQeRFli5dKrp06SL+9a9/iR9++EGcPHlS1NTUiMrKSnHgwAHxxhtviL59+yr+x3TVVVfZnff7/PPPy/VGjRrlVAxVVVWK9rdv3654fOvWrYrHq6urnWp35MiR8nNefPFFm3VcTSyKi4sVbbzyyivNbqMxyzdBzfmyF3/jxGLw4MFN3v/IkSOKNqdOndpk/TVr1sh1hw8fbrOO5RubW265xeHP4dprr5Xr33TTTVaPr1ixwukYGyxcuFB+zksvvaR4zGw2K9YqODoXwXIthL031zfeeKNc54YbbnAY44wZMxwmFr7y2pvDMrFw5mvmzJkO1zUIofybCggIcPqDiea4++675Xv8+eefNus0Tiz8/f1FTk5Ok23++9//luunpaXZrNOSxMLy92+p8Qc50dHRTdYl8gbcFYrIi0ydOhVZWVl48cUXMWXKFHTu3Bl6vR5BQUHo3bs37r77buzatQuzZs2Sn/Ptt9/ik08+sdleTU2N/L3lGRP2WM5JttxlqHGbLW23pTsXORISEqK4Li8v98h93OX6669v8vEePXogODjY6foDBgyQv8/KynIqhltvvdVhnca7etnaMemHH36Qv2+87WhTGu8+9vvvvyse27dvH3JzcwEAfn5+DtvU6XRO3bdx7M193fb4ymv3tI8//hhDhw5t1q5vl156KSIjI5t9r/z8fHz77bd47rnnMHfuXNxzzz2YM2eO/LV9+3a5bkZGhsP2LrroIsTHxzdZp/E6sWPHjjU7ZntuuOGGJh/v27cvAgMDAQBFRUVe/28aEVcBEXkRZxYa+/v7491338Xhw4fx22+/AQCee+453HzzzVZ1AwIC5O/r6uqcisFya9GG/6nZarOhXcsyR+1atukulv/TDQsLc1vbKSkp8vaY7tI4EbAnIiJC3m6yf//+TdaNioqSv3fmHA9JkjBq1CiH9UaPHi1/n5eXh5ycHMX2ops2bZK//+qrr/Drr786bLO0tFT+/uTJk4rHGp+f0rdvX6f+LhrHaMvp06dRUFAgX59//vkO2zz//PMhSZK8INsWX3jtrrL1+k0mE4qKipCeno7Fixfj22+/xcmTJ3HttdfirbfeavK8nAbDhw9vVhyZmZl4+OGH8eOPP8oLuR1x5kBOZzaSiI6Olr931xk54eHhTW6GAJz9G42MjJQ/jCkrK0NoaKhb7k/kCUwsiHyQRqPB/PnzMXHiRABnd0A6deoUOnfurKjX+BN8Z0cJLOtZjgJYXldXVzuVWDRu17INd2n8hg1QvtH2RuHh4Q7rNN4FxlH9xnUbdhNqSsMZGI407L7TMFpVUFCgSCyys7Pl7xvvDuSs4uJixXXjBKBLly5OteGoXuM2g4KCEBMT47DNsLAwhIeHW52n0JgvvHZP0Gq1iI2NxZQpUzBlyhQ8+eSTWLBgAQDg3nvvxdixY9G7d+8m2+jQoYPT9/v5559x9dVXN/tMFWc+4Xfm71Cn08nfG43GZsXgyn09dW8iT+FUKCIfdckllyj+h2Nra9jGn7Ll5eU51W7DNIwGlm/OG7fZ0nY99YZ///79imtH0xvU5syWwa7Ud6Q526k2npJl+WbNMqFrLsskqKKiQv7e2Rgbx2dLS9p0pl1feO2t4bHHHpMTnNraWqe2wHV25LKgoADTpk2Tk4quXbti4cKF+P3335GdnY2qqiqYzWaIs+tGMX/+fPm5DdttN8Xdf1fOUuu+RJ7ExILIR+l0OsWnrraG/Pv06SN/n5+fb7U+wpYTJ07I30dFRVl9qhgbG6uYnnH8+HGHbdbU1Cg+ie3bt6/D57TEli1b5O+1Wi2Sk5M9cp+2oqqqyum6jU//tRzlaPzGdseOHfIbPGe/LOesNx7RcjZGR6cTt6RNZ9r1hdfeGvz8/DBhwgT5es2aNW5r+5133pETuMGDB+PPP//E3LlzceGFF6Jjx44IDAxUvEnnOgQi9TCxIPJhjd9Q2PrUsk+fPtBozv6ZCyGcWsjYcCgTAPTr189mncbljeeEO9OmVqt1OEWipb788kv5+8GDB7t1jUVbVFxcrPiE3J7CwkJFUmo5jSguLk7+3nLEqyUaJ7ONE92mWK5VaKrNqqoqFBUVOWyzvLzc4YiEL7z21tJ4epwzHzg4q3GS8thjjzn8u3bnvYmoeZhYEPmoo0ePKhYRJiQkWNUJCAhQLFJdv369w3YbLz5tvHtNY+PGjWtxmxdccIFTp+E21y+//II9e/bI1zfddJPb79HWCCEUozz2NF6gHBcXZ9XXGi8A37hxo8txNd6BZ//+/U5NN2ocoy2dOnVSvGnfvHmzwzY3b97c5MJtwDdee2tpPMLS8IGGOzRex+JoobXJZHLL74GIWoaJBZGPeu+99+Tvw8PDMWTIEJv1pk6dKn/vaFejkydPKj4dbPxce22uXr0ap06darLdxve116YrioqKcPvtt8vX0dHRuPvuu91+n7boww8/dFjngw8+kL9vnFQ2+Nvf/iZ//9577zk15a4pffv2ldfH1NfX49NPP22yvjN1AGXszX3d9vjKa28NjUcmO3Xq5LZ2GycpjqaHrVixwi0jR0TUMkwsiLyEM1NSGvzxxx946aWX5OubbrpJsSNQYykpKfI0qQMHDuDdd9+12+7DDz8sb+M4evRoDBs2zGa95ORkef2CyWTC3Llz7bb59ttv4+DBgwDOzs135vyA5jh8+DDGjx+vmA7y/PPPe2znqbbmo48+anLUYt26dfjqq6/k68YJXIPrrrsO5513HgAgJycH//znPx1+0t+goqLCao2ARqPB7Nmz5esFCxYo1uhYevHFF506t6Nx7J9//jk2bNhgt+7GjRvtng/TmK+8dk/bvn27vP01AMV6C1f16NFD/v7bb7+1W6+goAAPPPCA2+5LRM3HxILIS3z55ZcYOXIkPvjgA7vTH2pqavDqq69i4sSJ8iejERERil1QLMXGxiItLU2+vvfee/H5558r6hiNRsydO1fxyefChQubjLfx4x9//DHmzp1rtRXi559/jvvvv1++fvDBB53a5tMZ+/btw3333YchQ4bgzz//lMvT0tIUb8zIPp1OB5PJhL/97W9YvXq11ePff/89rrnmGvmN8qRJk2y+YdRqtXjzzTeh1WoBAEuXLsUVV1xhc6eyBhkZGXj44YeRmJho843xAw88IPeV3NxcTJo0yWrXL7PZjJdeegmPPvqoUwc1Tpo0SR61EEJg6tSpigPuGvzyyy+46qqrYDabFTuv2eIrr92TVq9ejauvvlruJ35+frjnnnvc1v6VV14pf79w4UJ89NFHVnV27NiBMWPG4OTJk16xSxZRe8VzLIi8yLZt25CSkgI/Pz/07dsXffv2RWRkJEwmE06fPo1NmzYp1lUEBgbim2++USyatOXxxx/Hxo0bsXbtWlRXV2PatGl4+umnMWzYMNTU1GDDhg3IycmR6y9YsABjxoxpss0JEybgsccew9NPPw3g7CF9H374IS6++GIEBAQgPT1dseZh0qRJeOSRR5z+WbzyyiuKxdj19fUoKSlBUVERdu7cabX4NjAwEM899xzmzJnj9D2aY8uWLc1u+4UXXvDYYYDukJCQgGuuuQaLFi3CpEmTMHjwYAwZMgRCCKSnp2Pv3r1y3Y4dO+Kdd96x29bEiRPx5ptv4u6774bJZMKPP/6In376CUlJSRg0aBDCwsJQVVWFnJwc7Nq1q8lP4YGzC8SXLFmCa6+9FiaTCbt27UL//v1x0UUXoXfv3qioqMCGDRvk+fcvvPAC7rvvPoevecmSJRg9ejTy8vJQXFyMK664AgMGDMCwYcMgSRJ27twpJ6ppaWn46quvHC4G9pXX3lK2+r3JZMKZM2eQnp6OI0eOKB578cUX7W780BIpKSl46aWXcPDgQdTW1uKWW27Bs88+i8GDByMgIAB79uyRT9sePHgwJk+ejOeff95t9yeiZhBE5BWWLl0qADj9NXLkSJGZmel0+yUlJeLGG29ssk2dTieeeeYZp9s0m83iqaeeEjqdrsl2b7rpJlFaWuqwvea8/oavkJAQcccdd4j9+/c7HbezUlJSWhRTw1dxcbFVm2PGjJEfX7duncMYunbtKtfPyspyWL/x/W3JysqSH+/atauoq6sTf//735t8HX369BH79u1zeG8hhFi7dq3o1auX0z+j/v37i9OnT9tt74svvhDh4eF2n6/X68Vbb71l9bqasmfPHocx3nHHHaKurq5ZP39feO3OWLduXYv6e1RUlPjwww+bbLvx39TSpUudjunAgQOiR48eTd7/wgsvFKdOnRLz58+Xy+bPn2+zPWfq2PuZjBkzpsV1WvK7au6/AURq4ogFkZeYPn06evfujT/++AObN2/GkSNHUFhYiKKiIpjNZoSHh6N79+44//zzcf311+Oiiy5qVvvh4eFYvnw57rjjDrz//vvYtGkTcnJyoNPpkJiYiMmTJ+Pvf/97sz5plCQJjz32GK677jq8++67+OWXX3Dy5EkYjUZ07NgRo0ePRkpKinxCuCsCAwMRHh6OsLAwdO3aFcOHD0dycjImTZrk1OnRZJtOp8O7776LG264AUuWLMG2bduQk5OD4OBg9OvXD9OmTcOdd97p9E5e48aNw759+7BixQp8//332Lx5M3Jzc1FWVoagoCDExcWhb9++uOCCCzBlyhS7mw40uP7663HBBRfgtddew3fffYfjx49DkiR07twZEydOxN13341+/fpZnQfRlP79++PPP//E22+/jeXLl2P//v2oqqpCx44dkZycjNtvvx2TJk1yuj1feu3uIkkSQkNDERsbiyFDhmDy5Mm46aabPLa2qXfv3ti5cycMBgP+97//4cCBA6irq0N8fDwGDhyIGTNm4MYbb5SnpBGROiQhnFxlRkREREREZAcXbxMRERERkcuYWBARERERkcuYWBARERERkcuYWBARERERkcuYWBARERERkcu43aybmM1mZGdnIzQ0FJIkqR0OEREREZGCEALl5eVISEiARuP+8QUmFm6SnZ2NxMREtcMgIiIiImrSyZMn0blzZ7e3y8TCRQaDAQaDAfX19QCArKwsREVFqRwVeSOj0YhffvkF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RERE1BwuJxa///47wsLC0Lt3b4d1+/Tpg/DwcKxfv14uGz58OAICApCdne1qKOQNLM+x4K5Q5KTwIB0++vsodIpQJqevrD7EUQsiIiIf4HJiUVxcjNraWqf+x282m1FTU4Pi4mJFeWBgYIt2liIvxBELckFEkD/mjD9PUbb9eDE2HS1SKSIiIiJylsuJRUJCAmpra/Hdd985rLty5UrU1tYiISFBLmtINDp06OBqKOQNrM6x4OJtap7rhnW2GrV4dc0hlaIhIiIiZ7mcWFx11VUQQuCOO+7AH3/8Ybfepk2bcOedd0KSJFx11VVy+d69ewEAPXr0cDUU8gZW51jUqhMH+Sx/Pw3uHttTUbb56Bls4agFERGRV3P5gLzHHnsMn3/+OXJycnDJJZfgkksuwZgxY5CQkABJkpCdnY3169djw4YNMJvN6NixIx577DH5+R999BEAYMKECa6GQt7A6hwLjlhQ890wojNeX3sYuWXnptK9uvYQPu4RrWJURERE1BSXE4vo6GisW7cO119/Pfbs2YP169fj119/VdRpWH/Rv39/fPnll4iOPvfm4Oqrr8bYsWNx/vnnuxoKeQMu3iY30PtpcffYnpj/7V65bOPhImw/dgYjukWpGBkRERHZ45aTt3v37o0dO3bgww8/xFVXXYVOnTrJ51h06tQJV111FT744APs2LEDffr0UTx37NixuPrqqxEXF+eOUEhtlokFRyyohaYlJyI2VLlm59W1h1WKhoiIiBxxecRCbsjPDzNnzsTMmTPd1aRPMBgMMBgMMJlMaofiHax2heIaC2qZAJ0Wd43piadWZsplGw4WYOeJYgztEqliZERERGSLW0Ys2jOevG3BaioURyyo5WaM7IKYEOWoxWsctSAiIvJKbk8sCgoKsH37dp6k3V5ZTYXiGgtquUB/Le66RLlj3Nr9+fjzVIk6AREREZFdbkssvv32WwwbNgzx8fEYNWoUxo8fr3i8uLgYl112GS677DKUlpa667bkbSx3heKIBblo5vldEBXsryh7dQ1HLYiIiLyNWxKL//73v7jmmmuQkZEBIYT81VhkZCQCAwOxatUqfPnll+64LXkjnmNBbhbk74c7LlaOWqzel4c9p/kBBRERkTdxObHYvHkzHn30Ufj5+eH//u//UFhYaHeHp5tvvhlCCKxatcrV25K34jkW5AG3ju6K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5MUGdg7H5nkTkFdeA1sTVwWA0iojThZX4eSZKpwqrpb/e6q4CtVGrv9qS4QQkLhohprAPkJqYmJBROTlNBoJHcMD7T7eKSIQSQnunytL3oXrtMgR9hFSG6dCuchgMCApKQnJyclqh0JEREREpBomFi5KTU1FZmYmtm3bpnYoRERERESqYWJBREREREQuY2JBREREREQuY2JBREREREQuY2JBREREREQuY2JBREREREQuY2LhIm43S0RERETExMJl3G6WiIiIiIiJBRERERERuQETCyIiIiIichkTCyIiIiIichkTCyIiIiIichkTCyIiIiIichkTCyIiIiIichkTCyIiIiIichkTCxfxgDwiIiIiIiYWLuMBeUREREREgJ/aAbQVQggAQHl5OXQ6ncrRkDcyGo2oqqpCWVkZ+whZYf8gR9hHyBH2EXKkvLwcwLn3re7GxMJNioqKAADdu3dXORIiIiIiIvuKiooQHh7u9naZWLhJVFQUAODEiRMe+UWR7ysrK0NiYiJOnjyJsLAwtcMhL8P+QY6wj5Aj7CPkSGlpKbp06SK/b3U3JhZuotGcXa4SHh7OP2ZqUlhYGPsI2cX+QY6wj5Aj7CPkSMP7Vre365FWiYiIiIioXWFiQURERERELmNi4SZ6vR7z58+HXq9XOxTyUuwj1BT2D3KEfYQcYR8hRzzdRyThqf2miIiIiIio3eCIBRERERERuYyJBRERERERuYyJBRERERERuYyJBRERERERuYyJhRsYDAZ069YNAQEBGDVqFLZu3ap2SKSShQsXIjk5GaGhoYiNjcXUqVNx4MABRZ2amhqkpqYiOjoaISEhuO6665CXl6dSxKSm//73v5AkCffff79cxv5Bp0+fxs0334zo6GgEBgZi4MCB2L59u/y4EAJPPPEEOnbsiMDAQEycOBGHDh1SMWJqTSaTCY8//ji6d++OwMBA9OzZE0899RQa78XDPtK+bNiwAVdeeSUSEhIgSRJWrFiheNyZ/nDmzBnMnDkTYWFhiIiIwN///ndUVFQ0OxYmFi5avnw50tLSMH/+fOzYsQODBw/G5MmTkZ+fr3ZopIJff/0Vqamp2Lx5M1atWgWj0YhLL70UlZWVcp0HHngA3333Hb744gv8+uuvyM7OxrXXXqti1KSGbdu24a233sKgQYMU5ewf7VtxcTEuvPBC6HQ6/Pjjj8jMzMRLL72EyMhIuc7zzz+PV199FYsXL8aWLVsQHByMyZMno6amRsXIqbU899xzePPNN/H6669j3759eO655/D888/jtddek+uwj7QvlZWVGDx4MAwGg83HnekPM2fOxN69e7Fq1SqsXLkSGzZswJ133tn8YAS5ZOTIkSI1NVW+NplMIiEhQSxcuFDFqMhb5OfnCwDi119/FUIIUVJSInQ6nfjiiy/kOvv27RMAxKZNm9QKk1pZeXm56NWrl1i1apUYM2aMuO+++4QQ7B8kxMMPPywuuugiu4+bzWYRHx8vXnjhBbmspKRE6PV68emnn7ZGiKSyK664QsyePVtRdu2114qZM2cKIdhH2jsA4uuvv5avnekPmZmZAoDYtm2bXOfHH38UkiSJ06dPN+v+HLFwQV1dHdLT0zFx4kS5TKPRYOLEidi0aZOKkZG3KC0tBQBERUUBANLT02E0GhV9pm/fvujSpQv7TDuSmpqKK664QtEPAPYPAr799luMGDECN9xwA2JjYzF06FC888478uNZWVnIzc1V9JHw8HCMGjWKfaSduOCCC7BmzRocPHgQALBr1y78/vvvmDJlCgD2EVJypj9s2rQJERERGDFihFxn4sSJ0Gg02LJlS7Pu5+eesNunwsJCmEwmxMXFKcrj4uKwf/9+laIib2E2m3H//ffjwgsvxIABAwAAubm58Pf3R0REhKJuXFwccnNzVYiSWttnn32GHTt2YNu2bVaPsX/Q0aNH8eabbyItLQ2PPPIItm3bhnvvvRf+/v5ISUmR+4Gt/++wj7QPc+fORVlZGfr27QutVguTyYRnnnkGM2fOBAD2EVJwpj/k5uYiNjZW8bifnx+ioqKa3WeYWBB5SGpqKvbs2YPff/9d7VDIS5w8eRL33XcfVq1ahYCAALXDIS9kNpsxYsQIPPvsswCAoUOHYs+ePVi8eDFSUlJUjo68weeff46PP/4Yn3zyCfr374+MjAzcf//9SEhIYB8h1XEqlAtiYmKg1WqtdmzJy8tDfHy8SlGRN5gzZw5WrlyJdevWoXPnznJ5fHw86urqUFJSoqjPPtM+pKenIz8/H8OGDYOfnx/8/Pzw66+/4tVXX4Wfnx/i4uLYP9q5jh07IikpSVHWr18/nDhxAgDkfsD/77RfDz30EObOnYubbroJAwcOxC233IIHHngACxcuBMA+QkrO9If4+HirTYfq6+tx5syZZvcZJhYu8Pf3x/Dhw7FmzRq5zGw2Y82aNRg9erSKkZFahBCYM2cOvv76a6xduxbdu3dXPD58+HDodDpFnzlw4ABOnDjBPtMOTJgwAbt370ZGRob8NWLECMycOVP+nv2jfbvwwguttqg+ePAgunbtCgDo3r074uPjFX2krKwMW7ZsYR9pJ6qqqqDRKN++abVamM1mAOwjpORMfxg9ejRKSkqQnp4u11m7di3MZjNGjRrVvBu6tPScxGeffSb0er1YtmyZyMzMFHfeeaeIiIgQubm5aodGKrj77rtFeHi4WL9+vcjJyZG/qqqq5Dr/+Mc/RJcuXcTatWvF9u3bxejRo8Xo0aNVjJrU1HhXKCHYP9q7rVu3Cj8/P/HMM8+IQ4cOiY8//lgEBQWJjz76SK7z3//+V0RERIhvvvlG/Pnnn+Lqq68W3bt3F9XV1SpGTq0lJSVFdOrUSaxcuVJkZWWJ//3vfyImJkb8+9//luuwj7Qv5eXlYufOnWLnzp0CgHj55ZfFzp07xfHjx4UQzvWHyy67TAwdOlRs2bJF/P7776JXr15i+vTpzY6FiYUbvPbaa6JLly7C399fjBw5UmzevFntkEglAGx+LV26VK5TXV0t/vnPf4rIyEgRFBQkrrnmGpGTk6Ne0KQqy8SC/YO+++47MWDAAKHX60Xfvn3F22+/rXjcbDaLxx9/XMTFxQm9Xi8mTJggDhw4oFK01NrKysrEfffdJ7p06SICAgJEjx49xKOPPipqa2vlOuwj7cu6detsvvdISUkRQjjXH4qKisT06dNFSEiICAsLE7NmzRLl5eXNjkUSotFRjURERERERC3ANRZEREREROQyJhZEREREROQyJhZEREREROQyJhZEREREROQyJhZEREREROQyJhZEREREROQyJhZEREREROQyJhZEREREROQyJhZERB4wduxYSJKEJ598Uu1QVFVVVYXHH38c/fr1Q2BgICRJgiRJyMjIUDs0j3nyySchSRLGjh2rdigtctttt0GSJNx2221qh0JEPoaJBRG1moY3XJIkISgoCNnZ2XbrHjt2TK67fv361guS3GratGl4+umnsX//fkiShLi4OMTFxUGn06kdWruzfv16PPnkk1i2bJnaoRBRG8XEgohUUV1djQULFqgdBnnQ/v37sXLlSgDA8uXLUVVVhdzcXOTm5qJ///4qR9f+rF+/HgsWLHCYWHTs2BF9+vRBx44dWycwImozmFgQkWree+89HDx4UO0wyEN2794NAIiOjsaNN96ocjTkrIULF2L//v1YuHCh2qEQkY9hYkFErS4xMRGDBg1CfX09HnnkEbXDIQ+pqqoCAISEhKgcCRERtQYmFkTU6jQajfxp6FdffYWtW7c26/mN118cO3bMbr1u3bpBkiSrqR+Wzz9+/DjuuOMOdOnSBQEBAejZsycee+wxVFZWys/Zs2cPbr75ZiQmJiIgIAC9evXC008/DaPR6DDeuro6/Pe//8WgQYMQHByMyMhITJo0CT/++KPD5+7Zswd33nknevXqhaCgIISEhGDQoEF49NFHUVhYaPM5louHv/rqK1x66aWIjY2FRqNp9oLympoaLFq0CBdccAEiIyMREBCArl274tZbb7W5CLvh/g2Lf48fPy7/vFu6KHjjxo24+eab0bVrVwQEBCA8PBwjR47Ec889h4qKCkVdo9GImJgYSJKEV199tcl233vvPUiShLCwMDkRAoDc3Fy89tpruPrqq9GvXz+Eh4cjMDAQ5513Hm6//Xbs3bu32a8BcG5Rf1OLv4uLi7FkyRLceOONGDhwIKKiouTfx4wZM7B582ar5zT094aph7/++qvi92H5N+LM4u3169fjhhtuQKdOnaDX6xETE4MJEyZg6dKlMJlMTr2uNWvW4IorrkCHDh0QEBCAfv36YcGCBaipqbF7359//hnXXnstOnfuDH9/f4SFhaFHjx649NJL8eKLL+LMmTN2n0tErUAQEbWS+fPnCwCia9euQgghxowZIwCIcePGWdXNysoSAAQAsW7dOruPZWVl2b1f165dBQCxdOlSu8//6quvREREhAAgwsLChFarlR+7+OKLRV1dnVi5cqUICgoSAER4eLiQJEmuM23aNJv3bnht8+bNExdffLEAIPz8/OR7NXzNnz/fbvzPPfec0Gg0ct2goCDh7+8vX3fs2FHs2LHD7s95zJgxIi0tTQAQkiSJyMhIodVqm7ynpVOnTokBAwbI99TpdCI8PFy+1mg04tVXX1U854UXXhBxcXEiLCxMrhMXFyd/3XvvvU7f32QyiXvvvVfxMwsJCVH8nvr06SOOHTumeF5qaqoAIEaMGNFk+2PHjhUAxG233aYoT0lJkdv38/MTUVFRws/P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XSe/CX7++efdfv+ePXvi8ccfl9u/9dZbsWfPHvnx+vp6ZGRk4D//+Q/OO+88ZGRk2Gxn0qRJiI+PR319PWbMmIHq6mpERkbib3/7m836l112GYCz2wCnpqbKOxRVVlbirbfewvXXXy+/wW6uG2+8ERqNBkVFRZg+fbo8bai6uhrvv/8+rrnmGkRFRdl87qWXXgqNRoMzZ85g5syZ8jS7uro6fP7557j00kubXBjdkPjt3bsXf/zxR7Njb/z3+emnn+If//gH8vLyAJxdOP/qq6/i/vvvB3B2fdDw4cObfQ97nnvuOUyZMgUffvihYipWbW0tPv/8c7zwwgsAzm23S0QqabUTM4io3bN1QJ6l+vp60bdvX/kgLdg4IE+Is6dfJyUlyXU0Go18qJxOpxOffvqpUwfk2Ttgb926dXIde5o6AK7hILR58+aJiy66SI4rMjJS8doee+wxu+2/+eabwt/fX66r1+tFdHS00Ol0ijY++ugjxfNcOWDN0qlTp0T//v3le/n7+8s/54af+yuvvGLzue44edtsNovHH39cPiEbgAgMDBTR0dHy6egNX7///rvddtLS0hR177rrribve9NNNynqR0REyPcbPny4fAK1rdfm6Off+IRp/HWqecOJ1VOnTpVPG7f1/IcfftjquQ39oXv37uLjjz+222+NRqN8aCAAERkZKbp27Sq6du0qvvjiC7leUydvCyHEAw88ILchSZKIjIyU4wcgxo0bJ8rKypr9cxHC/t9d48P1GvpAVFSUol/069dPPgmciNTBEQsi8iparVaeItSUkJAQ/P7770hLS0P37t3h5+cHnU4nf4reMI1Hbf7+/lizZg2effZZ9OnTB7W1tQgPD8eECRPw/fffNznl6x//+AcOHDiABx98EIMHD4Zer0dJSQlCQkIwYsQI3HPPPVi1apVbp5xY6tSpE7Zv346XX34Z559/PgIDA1FVVYXExETccsstSE9Px7333uux+0uShP/85z/4888/8c9//n97dx4d0/n/Afw9S5aZJCSpLEWksRSlSFQsURG+HFvVkWpsR6zVRVCkDtHlqzmlqOVYjloGcVRiqy2hJSQVYzud4ChaRW0hEpEQEzHL8/vDb6YiE8v3RiaTvF/nzDk193nu/cx0Zm7e997nuZ+iadOmUCgUKCgogJeXFzp06IDY2FhotVrrmAxbnr7sqazLoCw2bNiAhQsXokWLFnBxcYHJZMLbb7+NWbNm4fDhw2Xeh+FF/Pe//8X69evRrl07uLm5wWQyoVWrVli+fDm2bdv2zNm8Zs+ejYSEBISGhkKlUsFgMKBhw4aYPn06MjMzrfeBsEWpVCI1NRWjR49GUFAQHjx4gCtXruDKlSsvNRPb/PnzceDAAURGRsLPzw+FhYXw8PBAREQENBoN9u3b98wzJ/+Ljz76CCtWrMCgQYPQvHlzqNVq60D2d999FwsXLoROp7MO+Cci+5AJwbvLEBERERGRNDxjQUREREREkjFYEBERERGRZAwWREREREQkGYMFERERERFJxmBBRERERESSMVgQEREREZFkDBZERERERCQZgwUREREREUnGYEFERERERJIxWBARERERkWQMFkREREREJBmDBRERERERScZgQUREREREkjFYEBERERGRZAwWREREREQkGYMFERERERFJprR3AUQvSwgBg8EAs9ls71KIiIheCblcDicnJ8hkMnuXQvTCGCzIYZhMJuTm5uL+/fswGAz2LoeIiOiVcnJygoeHB2rVqgWFQmHvcoieSyaEEPYuguh5TCYTrl27huLiYtSsWRPu7u5QKBQ8kkNERFWOEAImkwmFhYUoKCiAi4sLAgICGC6o0mOwIIeQnZ2N/Px81KtXDyqVyt7lEBERVYiioiJcvXoVnp6e8PPzs3c5RM/EwdtU6QkhcP/+fdSsWZOhgoiIqhWVSoUaNWrg/v374LFgquwYLKjSMxgMMBgMcHd3t3cpREREFc7Dw8O6LySqzBgsqNKzzP7Ea0uJiKg6suz/OBsiVXYMFuQwOFCbiIiqI+7/yFEwWBARERERkWQMFkREREREJBmDBRERERERScZgQVRFvPHGG5DJZKUe7u7uaNmyJaZNm4Y7d+48dz3jx4+39t21a1e51/nNN99AJpOhc+fOz2yXlpZmrcMWnU6HkSNHomHDhlCpVFCr1QgMDERYWBimTJmCffv2lXvtVLHK+kw//Vi7dm2pPk8+R47rwoULGDduHN566y24ubnB1dUVdevWRZs2bTBu3Dhs3bpV8jYsvzXP+00qL2vXroVMJsPw4cMrZHtEFUlp7wKIqHyFhYWhYcOGAB7PIJKVlQWtVovZs2cjISEBhw4dQv369W32LS4uxoYNG6z/1mg0eO+99yqk7pexePFi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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "names = [\"BAxUS\", \"EI\", \"Sobol\"]\n", + "runs = [Y_baxus, Y_ei, Y_Sobol]\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "\n", + "for name, run in zip(names, runs):\n", + " fx = np.maximum.accumulate(run.cpu())\n", + " plt.plot(-fx + branin.optimal_value, marker=\"\", lw=3)\n", + "\n", + "plt.ylabel(\"Regret\", fontsize=18)\n", + "plt.xlabel(\"Number of evaluations\", fontsize=18)\n", + "plt.title(f\"{dim}D Embedded Branin\", fontsize=24)\n", + "plt.xlim([0, len(Y_baxus)])\n", + "plt.yscale(\"log\")\n", + "\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.legend(\n", + " names + [\"Global optimal value\"],\n", + " loc=\"lower center\",\n", + " bbox_to_anchor=(0, -0.08, 1, 1),\n", + " bbox_transform=plt.gcf().transFigure,\n", + " ncol=4,\n", + " fontsize=16,\n", + ")\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/website-old/static/files/baxus.py b/website-old/static/files/baxus.py new file mode 100644 index 0000000000..996e33b3aa --- /dev/null +++ b/website-old/static/files/baxus.py @@ -0,0 +1,576 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## BO with BAxUS and TS/EI +# +# In this tutorial, we show how to implement **B**ayesian optimization with **a**daptively e**x**panding s**u**bspace**s** (BAxUS) [1] in a closed loop in BoTorch. +# The tutorial is purposefully similar to the [TuRBO tutorial](https://botorch.org/tutorials/turbo_1) to highlight the differences in the implementations. +# +# This implementation supports either Expected Improvement (EI) or Thompson sampling (TS). We optimize the Branin2 function [2] with 498 dummy dimensions and show that BAxUS outperforms EI as well as Sobol. +# +# Since BoTorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_{x\in \mathcal{X}} -f(x)=0$. +# +# - [1]: [Papenmeier, Leonard, et al. Increasing the Scope as You Learn: Adaptive Bayesian Optimization in Nested Subspaces. Advances in Neural Information Processing Systems. 2022](https://openreview.net/pdf?id=e4Wf6112DI) +# - [2]: [Branin Test Function](https://www.sfu.ca/~ssurjano/branin.html) +# + +# In[1]: + + +import math +import os +from dataclasses import dataclass + +import botorch +import gpytorch +import matplotlib.pyplot as plt +import numpy as np +import torch +from gpytorch.constraints import Interval +from gpytorch.kernels import MaternKernel, ScaleKernel +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.mlls import ExactMarginalLogLikelihood +from torch.quasirandom import SobolEngine + +from botorch.acquisition.analytic import LogExpectedImprovement +from botorch.exceptions import ModelFittingError +from botorch.fit import fit_gpytorch_mll +from botorch.generation import MaxPosteriorSampling +from botorch.models import SingleTaskGP +from botorch.optim import optimize_acqf +from botorch.test_functions import Branin + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +print(f"Running on {device}") +dtype = torch.double +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ## Optimize the augmented Branin function +# +# The goal is to minimize the embedded Branin function +# +# $f(x_1, x_2, \ldots, x_{20}) = \left (x_2-\frac{5.1}{4\pi^2}x_1^2+\frac{5}{\pi}x_1-6\right )^2+10\cdot \left (1-\frac{1}{8\pi}\right )\cos(x_1)+10$ +# +# with bounds [-5, 10] for $x_1$ and [0, 15] for $x_2$ (all other dimensions are ignored). The function has three minima with an optimal value of $0.397887$. +# +# As mentioned above, since botorch assumes a maximization problem, we instead maximize $-f(x)$. + +# ## Define a function with dummy variables +# +# We first define a new function where we only pass the first two input dimensions to the actual Branin function. + +# In[2]: + + +branin = Branin(negate=True).to(device=device, dtype=dtype) + + +def branin_emb(x): + """x is assumed to be in [-1, 1]^D""" + lb, ub = branin.bounds + return branin(lb + (ub - lb) * (x[..., :2] + 1) / 2) + + +# In[3]: + + +fun = branin_emb +dim = 500 if not SMOKE_TEST else 50 + +n_init = 10 if not SMOKE_TEST else 4 +max_cholesky_size = float("inf") # Always use Cholesky + + +# ## Maintain the BAxUS state +# BAxUS needs to maintain a state, which includes the length of the trust region, success and failure counters, success and failure tolerance, etc. +# In contrast to TuRBO, the failure tolerance depends on the target dimensionality. +# +# In this tutorial we store the state in a dataclass and update the state of TuRBO after each batch evaluation. +# +# **Note**: These settings assume that the domain has been scaled to $[-1, 1]^d$ + +# In[4]: + + +@dataclass +class BaxusState: + dim: int + eval_budget: int + new_bins_on_split: int = 3 + d_init: int = float("nan") # Note: post-initialized + target_dim: int = float("nan") # Note: post-initialized + n_splits: int = float("nan") # Note: post-initialized + length: float = 0.8 + length_init: float = 0.8 + length_min: float = 0.5**7 + length_max: float = 1.6 + failure_counter: int = 0 + success_counter: int = 0 + success_tolerance: int = 3 + best_value: float = -float("inf") + restart_triggered: bool = False + + def __post_init__(self): + n_splits = round(math.log(self.dim, self.new_bins_on_split + 1)) + self.d_init = 1 + np.argmin( + np.abs( + (1 + np.arange(self.new_bins_on_split)) + * (1 + self.new_bins_on_split) ** n_splits + - self.dim + ) + ) + self.target_dim = self.d_init + self.n_splits = n_splits + + @property + def split_budget(self) -> int: + return round( + -1 + * (self.new_bins_on_split * self.eval_budget * self.target_dim) + / (self.d_init * (1 - (self.new_bins_on_split + 1) ** (self.n_splits + 1))) + ) + + @property + def failure_tolerance(self) -> int: + if self.target_dim == self.dim: + return self.target_dim + k = math.floor(math.log(self.length_min / self.length_init, 0.5)) + split_budget = self.split_budget + return min(self.target_dim, max(1, math.floor(split_budget / k))) + + +def update_state(state, Y_next): + if max(Y_next) > state.best_value + 1e-3 * math.fabs(state.best_value): + state.success_counter += 1 + state.failure_counter = 0 + else: + state.success_counter = 0 + state.failure_counter += 1 + + if state.success_counter == state.success_tolerance: # Expand trust region + state.length = min(2.0 * state.length, state.length_max) + state.success_counter = 0 + elif state.failure_counter == state.failure_tolerance: # Shrink trust region + state.length /= 2.0 + state.failure_counter = 0 + + state.best_value = max(state.best_value, max(Y_next).item()) + if state.length < state.length_min: + state.restart_triggered = True + return state + + +# ## Create a BAxUS embedding +# +# We now show how to create the BAxUS embedding. The essential idea is to assign input dimensions to target dimensions and to assign a sign $\in \pm 1$ to each input dimension, similar to the HeSBO embedding. +# We create the embedding matrix that is used to project points from the target to the input space. The matrix is sparse, each column has precisely one non-zero entry that is either 1 or -1. + +# In[5]: + + +def embedding_matrix(input_dim: int, target_dim: int) -> torch.Tensor: + if ( + target_dim >= input_dim + ): # return identity matrix if target size greater than input size + return torch.eye(input_dim, device=device, dtype=dtype) + + input_dims_perm = ( + torch.randperm(input_dim, device=device) + 1 + ) # add 1 to indices for padding column in matrix + + bins = torch.tensor_split( + input_dims_perm, target_dim + ) # split dims into almost equally-sized bins + bins = torch.nn.utils.rnn.pad_sequence( + bins, batch_first=True + ) # zero pad bins, the index 0 will be cut off later + + mtrx = torch.zeros( + (target_dim, input_dim + 1), dtype=dtype, device=device + ) # add one extra column for padding + mtrx = mtrx.scatter_( + 1, + bins, + 2 * torch.randint(2, (target_dim, input_dim), dtype=dtype, device=device) - 1, + ) # fill mask with random +/- 1 at indices + + return mtrx[:, 1:] # cut off index zero as this corresponds to zero padding + + +embedding_matrix(10, 3) # example for an embedding matrix + + +# ## Function to increase the embedding +# +# Next, we write a helper function to increase the embedding and to bring observations to the increased target space. + +# In[6]: + + +def increase_embedding_and_observations( + S: torch.Tensor, X: torch.Tensor, n_new_bins: int +) -> torch.Tensor: + assert X.size(1) == S.size(0), "Observations don't lie in row space of S" + + S_update = S.clone() + X_update = X.clone() + + for row_idx in range(len(S)): + row = S[row_idx] + idxs_non_zero = torch.nonzero(row) + idxs_non_zero = idxs_non_zero[torch.randperm(len(idxs_non_zero))].reshape(-1) + + if len(idxs_non_zero) <= 1: + continue + + non_zero_elements = row[idxs_non_zero].reshape(-1) + + n_row_bins = min( + n_new_bins, len(idxs_non_zero) + ) # number of new bins is always less or equal than the contributing input dims in the row minus one + + new_bins = torch.tensor_split(idxs_non_zero, n_row_bins)[ + 1: + ] # the dims in the first bin won't be moved + elements_to_move = torch.tensor_split(non_zero_elements, n_row_bins)[1:] + + new_bins_padded = torch.nn.utils.rnn.pad_sequence( + new_bins, batch_first=True + ) # pad the tuples of bins with zeros to apply _scatter + els_to_move_padded = torch.nn.utils.rnn.pad_sequence( + elements_to_move, batch_first=True + ) + + S_stack = torch.zeros( + (n_row_bins - 1, len(row) + 1), device=device, dtype=dtype + ) # submatrix to stack on S_update + + S_stack = S_stack.scatter_( + 1, new_bins_padded + 1, els_to_move_padded + ) # fill with old values (add 1 to indices for padding column) + + S_update[ + row_idx, torch.hstack(new_bins) + ] = 0 # set values that were move to zero in current row + + X_update = torch.hstack( + (X_update, X[:, row_idx].reshape(-1, 1).repeat(1, len(new_bins))) + ) # repeat observations for row at the end of X (column-wise) + S_update = torch.vstack( + (S_update, S_stack[:, 1:]) + ) # stack onto S_update except for padding column + + return S_update, X_update + + +# In[7]: + + +S = embedding_matrix(10, 2) +X = torch.randint(100, (7, 2)) +print(f"S before increase\n{S}") +print(f"X before increase\n{X}") + +S, X = increase_embedding_and_observations(S, X, 3) +print(f"S after increase\n{S}") +print(f"X after increase\n{X}") + + +# ## Take a look at the state + +# In[8]: + + +state = BaxusState(dim=dim, eval_budget=500) +print(state) + + +# ## Generate initial points +# This generates an initial set of Sobol points that we use to start of the BO loop. + +# In[9]: + + +def get_initial_points(dim: int, n_pts: int, seed=0): + sobol = SobolEngine(dimension=dim, scramble=True, seed=seed) + X_init = ( + 2 * sobol.draw(n=n_pts).to(dtype=dtype, device=device) - 1 + ) # points have to be in [-1, 1]^d + return X_init + + +# ## Generate new batch +# Given the current `state` and a probabilistic (GP) `model` built from observations `X` and `Y`, we generate a new batch of points. +# +# This method works on the domain $[-1, +1]^d$, so make sure to not pass in observations from the true domain. `unnormalize` is called before the true function is evaluated which will first map the points back to the original domain. +# +# We support either TS and qEI which can be specified via the `acqf` argument. + +# In[10]: + + +def create_candidate( + state, + model, # GP model + X, # Evaluated points on the domain [-1, 1]^d + Y, # Function values + n_candidates=None, # Number of candidates for Thompson sampling + num_restarts=10, + raw_samples=512, + acqf="ts", # "ei" or "ts" +): + assert acqf in ("ts", "ei") + assert X.min() >= -1.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y)) + if n_candidates is None: + n_candidates = min(5000, max(2000, 200 * X.shape[-1])) + + # Scale the TR to be proportional to the lengthscales + x_center = X[Y.argmax(), :].clone() + weights = model.covar_module.lengthscale.detach().view(-1) + weights = weights / weights.mean() + weights = weights / torch.prod(weights.pow(1.0 / len(weights))) + tr_lb = torch.clamp(x_center - weights * state.length, -1.0, 1.0) + tr_ub = torch.clamp(x_center + weights * state.length, -1.0, 1.0) + + if acqf == "ts": + dim = X.shape[-1] + sobol = SobolEngine(dim, scramble=True) + pert = sobol.draw(n_candidates).to(dtype=dtype, device=device) + pert = tr_lb + (tr_ub - tr_lb) * pert + + # Create a perturbation mask + prob_perturb = min(20.0 / dim, 1.0) + mask = torch.rand(n_candidates, dim, dtype=dtype, device=device) <= prob_perturb + ind = torch.where(mask.sum(dim=1) == 0)[0] + mask[ind, torch.randint(0, dim, size=(len(ind),), device=device)] = 1 + + # Create candidate points from the perturbations and the mask + X_cand = x_center.expand(n_candidates, dim).clone() + X_cand[mask] = pert[mask] + + # Sample on the candidate points + thompson_sampling = MaxPosteriorSampling(model=model, replacement=False) + with torch.no_grad(): # We don't need gradients when using TS + X_next = thompson_sampling(X_cand, num_samples=1) + + elif acqf == "ei": + ei = LogExpectedImprovement(model, train_Y.max()) + X_next, acq_value = optimize_acqf( + ei, + bounds=torch.stack([tr_lb, tr_ub]), + q=1, + num_restarts=num_restarts, + raw_samples=raw_samples, + ) + + return X_next + + +# ## Optimization loop +# This simple loop runs one instance of BAxUS with Thompson sampling until convergence. +# +# BAxUS works on a fixed evaluation budget and shrinks the trust region until the minimal trust region size is reached (`state["restart_triggered"]` is set to `True`). +# Then, BAxUS increases the target space and carries over the observations to the updated space. +# + +# In[11]: + + +EVALUATION_BUDGET = 100 if not SMOKE_TEST else 10 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 +N_CANDIDATES = min(5000, max(2000, 200 * dim)) if not SMOKE_TEST else 4 + + +state = BaxusState(dim=dim, eval_budget=EVALUATION_BUDGET - n_init) +S = embedding_matrix(input_dim=state.dim, target_dim=state.d_init) + +X_baxus_target = get_initial_points(state.d_init, n_init) +X_baxus_input = X_baxus_target @ S +Y_baxus = torch.tensor( + [branin_emb(x) for x in X_baxus_input], dtype=dtype, device=device +).unsqueeze(-1) + + +# Disable input scaling checks as we normalize to [-1, 1] +with botorch.settings.validate_input_scaling(False): + for _ in range(EVALUATION_BUDGET - n_init): # Run until evaluation budget depleted + # Fit a GP model + train_Y = (Y_baxus - Y_baxus.mean()) / Y_baxus.std() + likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3)) + model = SingleTaskGP( + X_baxus_target, train_Y, likelihood=likelihood + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + + # Do the fitting and acquisition function optimization inside the Cholesky context + with gpytorch.settings.max_cholesky_size(max_cholesky_size): + # Fit the model + try: + fit_gpytorch_mll(mll) + except ModelFittingError: + # Right after increasing the target dimensionality, the covariance matrix becomes indefinite + # In this case, the Cholesky decomposition might fail due to numerical instabilities + # In this case, we revert to Adam-based optimization + optimizer = torch.optim.Adam([{"params": model.parameters()}], lr=0.1) + + for _ in range(100): + optimizer.zero_grad() + output = model(X_baxus_target) + loss = -mll(output, train_Y.flatten()) + loss.backward() + optimizer.step() + + # Create a batch + X_next_target = create_candidate( + state=state, + model=model, + X=X_baxus_target, + Y=train_Y, + n_candidates=N_CANDIDATES, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + acqf="ts", + ) + + X_next_input = X_next_target @ S + + Y_next = torch.tensor( + [branin_emb(x) for x in X_next_input], dtype=dtype, device=device + ).unsqueeze(-1) + + # Update state + state = update_state(state=state, Y_next=Y_next) + + # Append data + X_baxus_input = torch.cat((X_baxus_input, X_next_input), dim=0) + X_baxus_target = torch.cat((X_baxus_target, X_next_target), dim=0) + Y_baxus = torch.cat((Y_baxus, Y_next), dim=0) + + # Print current status + print( + f"iteration {len(X_baxus_input)}, d={len(X_baxus_target.T)}) Best value: {state.best_value:.3}, TR length: {state.length:.3}" + ) + + if state.restart_triggered: + state.restart_triggered = False + print("increasing target space") + S, X_baxus_target = increase_embedding_and_observations( + S, X_baxus_target, state.new_bins_on_split + ) + print(f"new dimensionality: {len(S)}") + state.target_dim = len(S) + state.length = state.length_init + state.failure_counter = 0 + state.success_counter = 0 + + +# ## GP-LogEI +# As a baseline, we compare BAxUS to Log Expected Improvement (LogEI) + +# In[12]: + + +X_ei = get_initial_points(dim, n_init) +Y_ei = torch.tensor( + [branin_emb(x) for x in X_ei], dtype=dtype, device=device +).unsqueeze(-1) +bounds = torch.stack( + [ + -torch.ones(dim, dtype=dtype, device=device), + torch.ones(dim, dtype=dtype, device=device), + ] +) + + +# Disable input scaling checks as we normalize to [-1, 1] +with botorch.settings.validate_input_scaling(False): + while len(Y_ei) < len(Y_baxus): + train_Y = (Y_ei - Y_ei.mean()) / Y_ei.std() + likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3)) + model = SingleTaskGP(X_ei, train_Y, likelihood=likelihood) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + optimizer = torch.optim.Adam([{"params": model.parameters()}], lr=0.1) + model.train() + model.likelihood.train() + for _ in range(50): + optimizer.zero_grad() + output = model(X_ei) + loss = -mll(output, train_Y.squeeze()) + loss.backward() + optimizer.step() + + # Create a batch + ei = LogExpectedImprovement(model, train_Y.max()) + candidate, acq_value = optimize_acqf( + ei, + bounds=bounds, + q=1, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + Y_next = torch.tensor( + [branin_emb(x) for x in candidate], dtype=dtype, device=device + ).unsqueeze(-1) + + # Append data + X_ei = torch.cat((X_ei, candidate), axis=0) + Y_ei = torch.cat((Y_ei, Y_next), axis=0) + + # Print current status + print(f"{len(X_ei)}) Best value: {Y_ei.max().item():.2e}") + + +# ## Sobol + +# In[13]: + + +X_Sobol = ( + SobolEngine(dim, scramble=True, seed=0) + .draw(len(X_baxus_input)) + .to(dtype=dtype, device=device) + * 2 + - 1 +) +Y_Sobol = torch.tensor( + [branin_emb(x) for x in X_Sobol], dtype=dtype, device=device +).unsqueeze(-1) + + +# ## Compare the methods +# +# We show the regret of the different methods. + +# In[14]: + + +get_ipython().run_line_magic('matplotlib', 'inline') + +names = ["BAxUS", "EI", "Sobol"] +runs = [Y_baxus, Y_ei, Y_Sobol] +fig, ax = plt.subplots(figsize=(8, 6)) + +for name, run in zip(names, runs): + fx = np.maximum.accumulate(run.cpu()) + plt.plot(-fx + branin.optimal_value, marker="", lw=3) + +plt.ylabel("Regret", fontsize=18) +plt.xlabel("Number of evaluations", fontsize=18) +plt.title(f"{dim}D Embedded Branin", fontsize=24) +plt.xlim([0, len(Y_baxus)]) +plt.yscale("log") + +plt.grid(True) +plt.tight_layout() +plt.legend( + names + ["Global optimal value"], + loc="lower center", + bbox_to_anchor=(0, -0.08, 1, 1), + bbox_transform=plt.gcf().transFigure, + ncol=4, + fontsize=16, +) +plt.show() + diff --git a/website-old/static/files/bo_with_warped_gp.ipynb b/website-old/static/files/bo_with_warped_gp.ipynb new file mode 100644 index 0000000000..469508b3a9 --- /dev/null +++ b/website-old/static/files/bo_with_warped_gp.ipynb @@ -0,0 +1,392 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## BO with Warped Gaussian Processes\n", + "\n", + "In this tutorial, we illustrate how to use learned input warping functions for robust Bayesian Optimization when the outcome may be non-stationary functions. When the lengthscales are non-stationarity in the raw input space, learning a warping function that maps raw inputs to a warped space where the lengthscales are stationary can be useful, because then standard stationary kernels can be used to effectively model the function.\n", + "\n", + "In general, for a relatively simple setup (like this one), we recommend using [Ax](https://ax.dev), since this will simplify your setup (including the amount of code you need to write) considerably. See the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. To use input warping with `MODULAR_BOTORCH`, we can pass the `warp_tf`, constructed as below, by adding `input_transform=warp_tf` argument to the `Surrogate(...)` call. \n", + "\n", + "We consider use a Kumaraswamy CDF as the class of input warping function and learn the concentration parameters ($a>0$ and $b>0$). Kumaraswamy CDFs are quite flexible and map inputs in [0, 1] to outputs in [0, 1]. This work follows the Beta CDF input warping proposed by Snoek et al., but replaces the Beta distribution Kumaraswamy distribution, which has a *differentiable* and closed-form CDF. \n", + " \n", + " $$K_\\text{cdf}(x) = 1 - (1-x^a)^b$$\n", + " \n", + "This enables maximum likelihood (or maximum a posteriori) estimation of the CDF hyperparameters using gradient methods to maximize the likelihood (or posterior probability) jointly with the GP hyperparameters. (Snoek et al. use a fully Bayesian treatment of the CDF parameters). Each input dimension is transformed using a separate warping function.\n", + "\n", + "We use the Log Noisy Expected Improvement (qLogNEI) acquisition function to optimize a synthetic Hartmann6 test function. The standard problem is\n", + "\n", + "$$f(x) = -\\sum_{i=1}^4 \\alpha_i \\exp \\left( -\\sum_{j=1}^6 A_{ij} (x_j - P_{ij})^2 \\right)$$\n", + "\n", + "over $x \\in [0,1]^6$ (parameter values can be found in `botorch/test_functions/hartmann6.py`). For this demonstration,\n", + "We first warp each input dimension through a different inverse Kumaraswamy CDF.\n", + "\n", + "Since BoTorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\\max_{x} -f(x) = 3.32237$.\n", + "\n", + "[1] [J. Snoek, K. Swersky, R. S. Zemel, R. P. Adams. Input Warping for Bayesian Optimization of Non-Stationary Functions. Proceedings of the 31st International Conference on Machine Learning, PMLR 32(2):1674-1682, 2014.](http://proceedings.mlr.press/v32/snoek14.pdf)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Problem setup\n", + "\n", + "First, we define the sample parameters for the sigmoid functions that transform the respective inputs." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'Transformed Value')" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from torch.distributions import Kumaraswamy\n", + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "fontdict = {\"fontsize\": 15}\n", + "torch.manual_seed(1234567890)\n", + "c1 = torch.rand(6, dtype=dtype, device=device) * 3 + 0.1\n", + "c0 = torch.rand(6, dtype=dtype, device=device) * 3 + 0.1\n", + "x = torch.linspace(0, 1, 101, dtype=dtype, device=device)\n", + "k = Kumaraswamy(concentration1=c1, concentration0=c0)\n", + "k_icdfs = k.icdf(x.unsqueeze(1).expand(101, 6))\n", + "fig, ax = plt.subplots(1, 1, figsize=(5, 5))\n", + "\n", + "for i in range(6):\n", + " ax.plot(x.cpu(), k_icdfs[:, i].cpu())\n", + "ax.set_xlabel(\"Raw Value\", **fontdict)\n", + "ax.set_ylabel(\"Transformed Value\", **fontdict)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.test_functions import Hartmann\n", + "\n", + "neg_hartmann6 = Hartmann(negate=True)\n", + "\n", + "\n", + "def obj(X):\n", + " X_warp = k.icdf(X)\n", + " return neg_hartmann6(X_warp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Initial design\n", + "\n", + "The models are initialized with 14 points in $[0,1]^6$ drawn from a scrambled sobol sequence.\n", + "\n", + "We observe the objectives with additive Gaussian noise with a standard deviation of 0.05." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.models import SingleTaskGP\n", + "from gpytorch.mlls.sum_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "\n", + "NOISE_SE = 0.05\n", + "train_yvar = torch.tensor(NOISE_SE**2, device=device, dtype=dtype)\n", + "\n", + "bounds = torch.tensor([[0.0] * 6, [1.0] * 6], device=device, dtype=dtype)\n", + "\n", + "\n", + "n = 14\n", + "# generate initial training data\n", + "train_x = draw_sobol_samples(\n", + " bounds=bounds, n=n, q=1, seed=torch.randint(0, 10000, (1,)).item()\n", + ").squeeze(1)\n", + "exact_obj = obj(train_x).unsqueeze(-1) # add output dimension\n", + "\n", + "best_observed_value = exact_obj.max().item()\n", + "train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Input warping and model initialization\n", + "We initialize the `Warp` input transformation and pass it a `SingleTaskGP` to model the noiseless objective. The `Warp` object is a `torch.nn.Module` that contains the concentration parameters and applies the warping function in the `Model`'s `forward` pass." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.models.transforms.input import Warp\n", + "from gpytorch.priors.torch_priors import LogNormalPrior\n", + "\n", + "\n", + "def initialize_model(train_x, train_obj):\n", + " # initialize input_warping transformation\n", + " warp_tf = Warp(\n", + " indices=list(range(train_x.shape[-1])),\n", + " # use a prior with median at 1.\n", + " # when a=1 and b=1, the Kumaraswamy CDF is the identity function\n", + " concentration1_prior=LogNormalPrior(0.0, 0.75**0.5),\n", + " concentration0_prior=LogNormalPrior(0.0, 0.75**0.5),\n", + " )\n", + " # define the model for objective\n", + " model = SingleTaskGP(\n", + " train_X=train_x,\n", + " train_Y=train_obj,\n", + " train_Yvar=train_yvar.expand_as(train_obj),\n", + " input_transform=warp_tf,\n", + " ).to(train_x)\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " return mll, model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define a helper function that performs the essential BO step\n", + "The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. For this example, we'll use sequential $q=1$ optimization. A simple initialization heuristic is used to select the 20 restart initial locations from a set of 512 random points. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "\n", + "num_restarts = 20 if not SMOKE_TEST else 2\n", + "raw_samples = 512 if not SMOKE_TEST else 32\n", + "\n", + "\n", + "def optimize_acqf_and_get_observation(acq_func):\n", + " \"\"\"Optimizes the acquisition function, and returns a new candidate and a noisy observation.\"\"\"\n", + " # optimize\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=num_restarts,\n", + " raw_samples=raw_samples, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + " # observe new values\n", + " new_x = candidates.detach()\n", + " exact_obj = obj(new_x).unsqueeze(-1) # add output dimension\n", + " train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)\n", + " return new_x, train_obj\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perform Bayesian Optimization\n", + "The Bayesian optimization loop iterates the following steps:\n", + "1. given a surrogate model, choose a candidate point $x$\n", + "2. observe $f(x)$\n", + "3. update the surrogate model. \n", + "\n", + "We do `N_BATCH=50` rounds of optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ".................................................." + ] + } + ], + "source": [ + "from botorch import fit_gpytorch_mll\n", + "from botorch.acquisition.logei import qLogNoisyExpectedImprovement\n", + "from botorch.exceptions import BadInitialCandidatesWarning\n", + "\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\", category=BadInitialCandidatesWarning)\n", + "warnings.filterwarnings(\"ignore\", category=RuntimeWarning)\n", + "\n", + "N_BATCH = 50 if not SMOKE_TEST else 5\n", + "\n", + "torch.manual_seed(0)\n", + "\n", + "best_observed = [best_observed_value]\n", + "mll, model = initialize_model(train_x, train_obj)\n", + "\n", + "# run N_BATCH rounds of BayesOpt after the initial random batch\n", + "for iteration in range(1, N_BATCH + 1):\n", + "\n", + " # fit the models\n", + " fit_gpytorch_mll(mll)\n", + " ei = qLogNoisyExpectedImprovement(model=model, X_baseline=train_x)\n", + "\n", + " # optimize and get new observation\n", + " new_x, new_obj = optimize_acqf_and_get_observation(ei)\n", + "\n", + " # update training points\n", + " train_x = torch.cat([train_x, new_x])\n", + " train_obj = torch.cat([train_obj, new_obj])\n", + "\n", + " # update progress\n", + " best_value = obj(train_x).max().item()\n", + " best_observed.append(best_value)\n", + "\n", + " mll, model = initialize_model(train_x, train_obj)\n", + "\n", + " print(\".\", end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Plot the results\n", + "The plot below shows the log regret at each step of the optimization for each of the algorithms." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "GLOBAL_MAXIMUM = neg_hartmann6.optimal_value\n", + "\n", + "iters = np.arange(N_BATCH + 1)\n", + "y_ei = np.log10(GLOBAL_MAXIMUM - np.asarray(best_observed))\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n", + "\n", + "ax.plot(\n", + " iters,\n", + " y_ei,\n", + " linewidth=1.5,\n", + " alpha=0.6,\n", + ")\n", + "\n", + "ax.set_xlabel(\"number of observations (beyond initial points)\")\n", + "ax.set_ylabel(\"Log10 Regret\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/bo_with_warped_gp.py b/website-old/static/files/bo_with_warped_gp.py new file mode 100644 index 0000000000..960868e9bf --- /dev/null +++ b/website-old/static/files/bo_with_warped_gp.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## BO with Warped Gaussian Processes +# +# In this tutorial, we illustrate how to use learned input warping functions for robust Bayesian Optimization when the outcome may be non-stationary functions. When the lengthscales are non-stationarity in the raw input space, learning a warping function that maps raw inputs to a warped space where the lengthscales are stationary can be useful, because then standard stationary kernels can be used to effectively model the function. +# +# In general, for a relatively simple setup (like this one), we recommend using [Ax](https://ax.dev), since this will simplify your setup (including the amount of code you need to write) considerably. See the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. To use input warping with `MODULAR_BOTORCH`, we can pass the `warp_tf`, constructed as below, by adding `input_transform=warp_tf` argument to the `Surrogate(...)` call. +# +# We consider use a Kumaraswamy CDF as the class of input warping function and learn the concentration parameters ($a>0$ and $b>0$). Kumaraswamy CDFs are quite flexible and map inputs in [0, 1] to outputs in [0, 1]. This work follows the Beta CDF input warping proposed by Snoek et al., but replaces the Beta distribution Kumaraswamy distribution, which has a *differentiable* and closed-form CDF. +# +# $$K_\text{cdf}(x) = 1 - (1-x^a)^b$$ +# +# This enables maximum likelihood (or maximum a posteriori) estimation of the CDF hyperparameters using gradient methods to maximize the likelihood (or posterior probability) jointly with the GP hyperparameters. (Snoek et al. use a fully Bayesian treatment of the CDF parameters). Each input dimension is transformed using a separate warping function. +# +# We use the Log Noisy Expected Improvement (qLogNEI) acquisition function to optimize a synthetic Hartmann6 test function. The standard problem is +# +# $$f(x) = -\sum_{i=1}^4 \alpha_i \exp \left( -\sum_{j=1}^6 A_{ij} (x_j - P_{ij})^2 \right)$$ +# +# over $x \in [0,1]^6$ (parameter values can be found in `botorch/test_functions/hartmann6.py`). For this demonstration, +# We first warp each input dimension through a different inverse Kumaraswamy CDF. +# +# Since BoTorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_{x} -f(x) = 3.32237$. +# +# [1] [J. Snoek, K. Swersky, R. S. Zemel, R. P. Adams. Input Warping for Bayesian Optimization of Non-Stationary Functions. Proceedings of the 31st International Conference on Machine Learning, PMLR 32(2):1674-1682, 2014.](http://proceedings.mlr.press/v32/snoek14.pdf) + +# In[1]: + + +import os +import torch + + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +dtype = torch.double +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Problem setup +# +# First, we define the sample parameters for the sigmoid functions that transform the respective inputs. + +# In[2]: + + +from torch.distributions import Kumaraswamy +import matplotlib.pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +fontdict = {"fontsize": 15} +torch.manual_seed(1234567890) +c1 = torch.rand(6, dtype=dtype, device=device) * 3 + 0.1 +c0 = torch.rand(6, dtype=dtype, device=device) * 3 + 0.1 +x = torch.linspace(0, 1, 101, dtype=dtype, device=device) +k = Kumaraswamy(concentration1=c1, concentration0=c0) +k_icdfs = k.icdf(x.unsqueeze(1).expand(101, 6)) +fig, ax = plt.subplots(1, 1, figsize=(5, 5)) + +for i in range(6): + ax.plot(x.cpu(), k_icdfs[:, i].cpu()) +ax.set_xlabel("Raw Value", **fontdict) +ax.set_ylabel("Transformed Value", **fontdict) + + +# In[3]: + + +from botorch.test_functions import Hartmann + +neg_hartmann6 = Hartmann(negate=True) + + +def obj(X): + X_warp = k.icdf(X) + return neg_hartmann6(X_warp) + + +# #### Initial design +# +# The models are initialized with 14 points in $[0,1]^6$ drawn from a scrambled sobol sequence. +# +# We observe the objectives with additive Gaussian noise with a standard deviation of 0.05. + +# In[4]: + + +from botorch.models import SingleTaskGP +from gpytorch.mlls.sum_marginal_log_likelihood import ExactMarginalLogLikelihood +from botorch.utils.sampling import draw_sobol_samples + +NOISE_SE = 0.05 +train_yvar = torch.tensor(NOISE_SE**2, device=device, dtype=dtype) + +bounds = torch.tensor([[0.0] * 6, [1.0] * 6], device=device, dtype=dtype) + + +n = 14 +# generate initial training data +train_x = draw_sobol_samples( + bounds=bounds, n=n, q=1, seed=torch.randint(0, 10000, (1,)).item() +).squeeze(1) +exact_obj = obj(train_x).unsqueeze(-1) # add output dimension + +best_observed_value = exact_obj.max().item() +train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj) + + +# #### Input warping and model initialization +# We initialize the `Warp` input transformation and pass it a `SingleTaskGP` to model the noiseless objective. The `Warp` object is a `torch.nn.Module` that contains the concentration parameters and applies the warping function in the `Model`'s `forward` pass. + +# In[5]: + + +from botorch.models.transforms.input import Warp +from gpytorch.priors.torch_priors import LogNormalPrior + + +def initialize_model(train_x, train_obj): + # initialize input_warping transformation + warp_tf = Warp( + indices=list(range(train_x.shape[-1])), + # use a prior with median at 1. + # when a=1 and b=1, the Kumaraswamy CDF is the identity function + concentration1_prior=LogNormalPrior(0.0, 0.75**0.5), + concentration0_prior=LogNormalPrior(0.0, 0.75**0.5), + ) + # define the model for objective + model = SingleTaskGP( + train_X=train_x, + train_Y=train_obj, + train_Yvar=train_yvar.expand_as(train_obj), + input_transform=warp_tf, + ).to(train_x) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + return mll, model + + +# #### Define a helper function that performs the essential BO step +# The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. For this example, we'll use sequential $q=1$ optimization. A simple initialization heuristic is used to select the 20 restart initial locations from a set of 512 random points. + +# In[6]: + + +from botorch.optim import optimize_acqf + + +num_restarts = 20 if not SMOKE_TEST else 2 +raw_samples = 512 if not SMOKE_TEST else 32 + + +def optimize_acqf_and_get_observation(acq_func): + """Optimizes the acquisition function, and returns a new candidate and a noisy observation.""" + # optimize + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=bounds, + q=1, + num_restarts=num_restarts, + raw_samples=raw_samples, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + ) + # observe new values + new_x = candidates.detach() + exact_obj = obj(new_x).unsqueeze(-1) # add output dimension + train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj) + return new_x, train_obj + + +# ### Perform Bayesian Optimization +# The Bayesian optimization loop iterates the following steps: +# 1. given a surrogate model, choose a candidate point $x$ +# 2. observe $f(x)$ +# 3. update the surrogate model. +# +# We do `N_BATCH=50` rounds of optimization. + +# In[7]: + + +from botorch import fit_gpytorch_mll +from botorch.acquisition.logei import qLogNoisyExpectedImprovement +from botorch.exceptions import BadInitialCandidatesWarning + +import warnings + +warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning) +warnings.filterwarnings("ignore", category=RuntimeWarning) + +N_BATCH = 50 if not SMOKE_TEST else 5 + +torch.manual_seed(0) + +best_observed = [best_observed_value] +mll, model = initialize_model(train_x, train_obj) + +# run N_BATCH rounds of BayesOpt after the initial random batch +for iteration in range(1, N_BATCH + 1): + + # fit the models + fit_gpytorch_mll(mll) + ei = qLogNoisyExpectedImprovement(model=model, X_baseline=train_x) + + # optimize and get new observation + new_x, new_obj = optimize_acqf_and_get_observation(ei) + + # update training points + train_x = torch.cat([train_x, new_x]) + train_obj = torch.cat([train_obj, new_obj]) + + # update progress + best_value = obj(train_x).max().item() + best_observed.append(best_value) + + mll, model = initialize_model(train_x, train_obj) + + print(".", end="") + + +# #### Plot the results +# The plot below shows the log regret at each step of the optimization for each of the algorithms. + +# In[8]: + + +import numpy as np +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +GLOBAL_MAXIMUM = neg_hartmann6.optimal_value + +iters = np.arange(N_BATCH + 1) +y_ei = np.log10(GLOBAL_MAXIMUM - np.asarray(best_observed)) + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) + +ax.plot( + iters, + y_ei, + linewidth=1.5, + alpha=0.6, +) + +ax.set_xlabel("number of observations (beyond initial points)") +ax.set_ylabel("Log10 Regret") + + +# In[ ]: + + + + diff --git a/website-old/static/files/bope.ipynb b/website-old/static/files/bope.ipynb new file mode 100644 index 0000000000..9de494f77d --- /dev/null +++ b/website-old/static/files/bope.ipynb @@ -0,0 +1,694 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "6778d143-3808-45b3-bcbe-da7c7475280a", + "showInput": false + }, + "source": [ + "# Bayesian Optimization with Preference Exploration\n", + "\n", + "In this tutorial, we demonstrate how to implement a closed loop of Bayesian optimization with preference exploration, or BOPE [1].\n", + "BOPE is designed for Bayesian optimization of expensive-to-evaluate experiments,\n", + "where the response surface function of the experiment $f_\\mathrm{true}$ generates vector-valued outcomes over which a decision-maker (DM) has preferences.\n", + "These preferences are encoded by a utility function $g_\\mathrm{true}$ that is not known in closed form but can be estimated by\n", + "asking the DM to express preferences over pairs of outcome vectors.\n", + "\n", + "In other words, with BOPE, we wish to solve the following optimization problem:\n", + "\n", + "$$\n", + " \\max_{x \\in \\mathcal{X}} g_\\mathrm{true}(f_\\mathrm{true}(x))\n", + "$$\n", + "\n", + "Unlike many other Bayesian optimization setups where multiple consecutive batches of experiments are performed,\n", + "in BOPE, we alternate between two stages: *preference exploration* and *experimentation*.\n", + "\n", + "In the preference exploration stage, we use an acquisition function (i.e., a preference exploration strategy, or PE strategy)\n", + "to adaptively generate pairs of hypothetical outcome and ask the decision-maker’s preference within each pair.\n", + "In the experimentation stage, we use a batch version of noisy expected improvement that integrates over our uncertainty in the\n", + "utility function called $\\text{qNEIUU}$ to generate experimental candidates for evaluation.\n", + "\n", + "\n", + "[1] [Z.J. Lin, R. Astudillo, P.I. Frazier, and E. Bakshy, Preference Exploration for Efficient Bayesian Optimization with Multiple Outcomes. AISTATS, 2022.](https://arxiv.org/abs/2203.11382)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1679689485463, + "executionStopTime": 1679689500450, + "originalKey": "f4c4c433-22c1-432c-8b68-e994bcd1881b", + "output": { + "id": 795773092378270, + "loadingStatus": "loaded" + }, + "requestMsgId": "9aa7b0c4-ac2c-4e47-a067-7e0797bdd89d", + "showInput": true, + "vscode": { + "languageId": "python" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "I0214 125107.545 font_manager.py:1550] generated new fontManager\n", + "I0214 125107.837 _utils_internal.py:247] NCCL_DEBUG env var is set to None\n", + "I0214 125107.838 _utils_internal.py:265] NCCL_DEBUG is forced to WARN from None\n" + ] + } + ], + "source": [ + "import os\n", + "from typing import Optional, Tuple\n", + "\n", + "import matplotlib as mpl\n", + "import matplotlib.pylab as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import torch\n", + "from botorch.acquisition import (\n", + " GenericMCObjective,\n", + " LearnedObjective,\n", + " MCAcquisitionObjective,\n", + ")\n", + "from botorch.acquisition.logei import qLogNoisyExpectedImprovement\n", + "from botorch.acquisition.monte_carlo import qSimpleRegret\n", + "from botorch.acquisition.preference import AnalyticExpectedUtilityOfBestOption\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models.deterministic import FixedSingleSampleModel\n", + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.pairwise_gp import PairwiseGP, PairwiseLaplaceMarginalLogLikelihood\n", + "from botorch.models.transforms.input import Normalize\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.optim.optimize import optimize_acqf\n", + "from botorch.sampling import SobolQMCNormalSampler\n", + "from botorch.test_functions.multi_objective import DTLZ2\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "\n", + "\n", + "%matplotlib inline\n", + "\n", + "# Set plotting colors\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "mpl.rcParams[\"axes.prop_cycle\"] = mpl.cycler(color=colors)\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "7729000d-0661-4689-8f2b-f99e32c52b70", + "showInput": false + }, + "source": [ + "## Problem Setup\n", + "\n", + "In this tutorial, we use the DTLZ2 problem with d=5 inputs and k=4 outcomes as our test problem $f_\\mathrm{true}$.\n", + "\n", + "For the utility function $g_\\mathrm{true}$, we use the negative L1 distance from a Pareto-optimal point the outcome space:\n", + "$Y^* = f(X^*)$ where $X^* = [0.5, 0.5, 0.5, 0.5, 0.5]$. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1679689500531, + "executionStopTime": 1679689500534, + "hidden_ranges": [], + "originalKey": "470d8e92-e1ff-4ae7-8343-3423273900f8", + "requestMsgId": "f8b170ce-5da8-40f6-aae4-1d637ea97e33", + "showInput": true, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def neg_l1_dist(Y: torch.Tensor, X: Optional[torch.Tensor] = None) -> torch.Tensor:\n", + " \"\"\"Negative L1 distance from a Pareto optimal points\"\"\"\n", + " if len(Y.shape) == 1:\n", + " Y = Y.unsqueeze(0)\n", + " dist = torch.cdist(\n", + " Y, torch.full(Y.shape[-1:], fill_value=0.5, dtype=Y.dtype).unsqueeze(0), p=1\n", + " ).squeeze(-1)\n", + " return -dist\n", + "\n", + "\n", + "if SMOKE_TEST:\n", + " NUM_RESTARTS = 2\n", + " NUM_OUTCOME_SAMPLES = 2\n", + " RAW_SAMPLES = 4\n", + " BATCH_LIMIT = 2\n", + "else:\n", + " NUM_RESTARTS = 8\n", + " # Since BOPE samples from both the outcome and preference models, we take\n", + " # fewer samples than usual from the outcome model for speed.\n", + " NUM_OUTCOME_SAMPLES = 32\n", + " RAW_SAMPLES = 64\n", + " BATCH_LIMIT = 4\n", + "\n", + "X_dim = 5\n", + "Y_dim = 4\n", + "problem = DTLZ2(dim=X_dim, num_objectives=Y_dim)\n", + "util_func = neg_l1_dist" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "bf98ebb8-36d5-423a-9eea-5ff99c44975b", + "showInput": false + }, + "source": [ + "Here we define a collection of helper functions for BOPE:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1679689500831, + "executionStopTime": 1679689500838, + "hidden_ranges": [], + "originalKey": "8a6b9736-1ba8-4914-b295-7406bec5ece6", + "requestMsgId": "43fea609-1909-4ba8-af8f-7ac3de4016dd", + "showInput": true, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def fit_outcome_model(X: torch.Tensor, Y: torch.Tensor) -> SingleTaskGP:\n", + " \"\"\"Fit the outcome model f\"\"\"\n", + " outcome_model = SingleTaskGP(\n", + " train_X=X,\n", + " train_Y=Y,\n", + " input_transform=Normalize(d=X.shape[-1]),\n", + " outcome_transform=Standardize(m=Y.shape[-1]),\n", + " )\n", + " mll = ExactMarginalLogLikelihood(outcome_model.likelihood, outcome_model)\n", + " fit_gpytorch_mll(mll)\n", + " return outcome_model\n", + "\n", + "\n", + "def fit_pref_model(Y: torch.Tensor, comps: torch.Tensor) -> PairwiseGP:\n", + " \"\"\"Fit the preference model g.\"\"\"\n", + " model = PairwiseGP(Y, comps, input_transform=Normalize(d=Y.shape[-1]))\n", + " mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + " return model\n", + "\n", + "\n", + "def gen_rand_X(problem, n: int) -> torch.Tensor:\n", + " \"\"\"Generate n quasi-random Sobol points in the design space.\"\"\"\n", + " return draw_sobol_samples(bounds=problem.bounds, n=1, q=n).squeeze(0)\n", + "\n", + "\n", + "def generate_random_exp_data(problem, n: int) -> Tuple[torch.Tensor, torch.Tensor]:\n", + " \"\"\"Generate n observations of (X, Y) Pairs\"\"\"\n", + " X = gen_rand_X(problem, n)\n", + " Y = problem(X)\n", + " return X, Y\n", + "\n", + "\n", + "def generate_random_pref_data(\n", + " outcome_model: SingleTaskGP, n: int\n", + ") -> Tuple[torch.Tensor, torch.Tensor]:\n", + " \"\"\"Generate n pairwise comparison data between 2n points.\"\"\"\n", + " X = gen_rand_X(problem, 2 * n)\n", + " Y = outcome_model.posterior(X).sample().squeeze(0)\n", + " util = util_func(Y)\n", + " comps = gen_comps(util)\n", + " return Y, comps\n", + "\n", + "\n", + "def gen_comps(util: torch.Tensor) -> torch.Tensor:\n", + " \"\"\"Given an 1-d tensor of utility, create pairwise comparisons between adjacent items.\"\"\"\n", + " util = util.reshape(-1, 2)\n", + " comps = torch.arange(util.numel()).reshape(-1, 2)\n", + " flip = util[:, 0] < util[:, 1]\n", + " comps[flip, [0]], comps[flip, [1]] = comps[flip, [1]], comps[flip, [0]]\n", + "\n", + " return comps\n", + "\n", + "\n", + "def run_pref_learn(\n", + " outcome_model: SingleTaskGP,\n", + " train_Y: torch.Tensor,\n", + " train_comps: torch.Tensor,\n", + " n_comps: int,\n", + " pe_strategy: str,\n", + " verbose: bool = False,\n", + ") -> Tuple[torch.Tensor, torch.Tensor]:\n", + " \"\"\"Perform preference exploration with a given PE strategy for n_comps rounds.\"\"\"\n", + " for i in range(n_comps):\n", + " if verbose:\n", + " print(f\"Running {i+1}/{n_comps} preference learning using {pe_strategy}\")\n", + " pref_model = fit_pref_model(train_Y, train_comps)\n", + " if pe_strategy == \"EUBO-zeta\":\n", + " # EUBO-zeta\n", + " one_sample_outcome_model = FixedSingleSampleModel(model=outcome_model)\n", + " acqf = AnalyticExpectedUtilityOfBestOption(\n", + " pref_model=pref_model, outcome_model=one_sample_outcome_model\n", + " )\n", + " cand_X, acqf_val = optimize_acqf(\n", + " acq_function=acqf,\n", + " q=2,\n", + " bounds=problem.bounds,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": BATCH_LIMIT},\n", + " )\n", + " cand_Y = one_sample_outcome_model(cand_X)\n", + " elif pe_strategy == \"Random-f\":\n", + " # Random-f\n", + " cand_X = gen_rand_X(problem, n=2)\n", + " cand_Y = outcome_model.posterior(cand_X).sample().squeeze(0)\n", + " else:\n", + " raise RuntimeError(\"Unknown preference exploration strategy!\")\n", + "\n", + " cand_Y = cand_Y.detach().clone()\n", + " cand_comps = gen_comps(util_func(cand_Y))\n", + "\n", + " train_comps = torch.cat((train_comps, cand_comps + train_Y.shape[0]))\n", + " train_Y = torch.cat((train_Y, cand_Y))\n", + "\n", + " return train_Y, train_comps\n", + "\n", + "\n", + "def gen_exp_cand(\n", + " outcome_model: SingleTaskGP,\n", + " objective: MCAcquisitionObjective,\n", + " q: int,\n", + " acqf_name: str,\n", + ") -> torch.Tensor:\n", + " \"\"\"Given an outcome model and an objective, generate q experimental candidates\n", + " using specified acquisition function.\"\"\"\n", + " sampler = SobolQMCNormalSampler(sample_shape=torch.Size([NUM_OUTCOME_SAMPLES]))\n", + " if acqf_name == \"qNEI\":\n", + " # generate experimental candidates with qNEI/qNEIUU\n", + " acq_func = qLogNoisyExpectedImprovement(\n", + " model=outcome_model,\n", + " objective=objective,\n", + " X_baseline=X,\n", + " sampler=sampler,\n", + " prune_baseline=True,\n", + " )\n", + " elif acqf_name == \"posterior_mean\":\n", + " # generate experimental candidates with maximum posterior mean\n", + " acq_func = qSimpleRegret(\n", + " model=outcome_model,\n", + " sampler=sampler,\n", + " objective=objective,\n", + " )\n", + " else:\n", + " raise RuntimeError(\"Unknown acquisition function name!\")\n", + "\n", + " # optimize the acquisition function\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " q=q,\n", + " bounds=problem.bounds,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " options={\"batch_limit\": BATCH_LIMIT},\n", + " sequential=True,\n", + " )\n", + " return candidates\n", + "\n", + "\n", + "def find_max_posterior_mean(\n", + " outcome_model: SingleTaskGP,\n", + " train_Y: torch.Tensor,\n", + " train_comps: torch.Tensor,\n", + " verbose: bool = False,\n", + "):\n", + " \"\"\"Helper function that find the max posterior mean under current outcome and\n", + " preference model\"\"\"\n", + " pref_model = fit_pref_model(train_Y, train_comps)\n", + " pref_obj = LearnedObjective(pref_model=pref_model)\n", + " post_mean_cand_X = gen_exp_cand(\n", + " outcome_model, pref_obj, q=1, acqf_name=\"posterior_mean\"\n", + " )\n", + "\n", + " post_mean_util = util_func(problem(post_mean_cand_X)).item()\n", + " if verbose:\n", + " print(f\"Max posterior mean utility: {post_mean_util:.3f}\")\n", + " within_result = {\n", + " \"n_comps\": train_comps.shape[0],\n", + " \"util\": post_mean_util,\n", + " }\n", + " return within_result" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "7d9d4982-ca40-4744-88d5-775a5c306ddd", + "showInput": false + }, + "source": [ + "## Closed Loop BOPE\n", + "\n", + "### Setup\n", + "The following cell shows the core part of this tutorial.\n", + "In BOPE, we use two probablistic models (in this case, two Gaussian processes) $f$ and $g$\n", + "to model $f_\\mathrm{true}$ and $g_\\mathrm{true}$ respectively.\n", + "\n", + "We start by initializing the outcome model $f$ with 8 quasi-random points (the initial experimentation stage).\n", + "\n", + "Next, we enter the preference exploration (PE) stage.\n", + "A straightforward strategy of performing PE is to present the decision-maker with comparisons using outcomes sampled from the outcome model at random design points. We refer this method as $\\text{Random}\\mathrm{-}f$.\n", + "\n", + "Alternatively, we could initialize the preference model with $\\text{Random}\\mathrm{-}f$,\n", + "then perform PE using the $\\text{EUBO}\\mathrm{-}\\zeta$ acquisition function as proposed in [1].\n", + " \n", + "\n", + "In this tutorial, we examine both strategies by starting with initializating the preference model with 3 comparisons using $\\text{Random}\\mathrm{-}f$.\n", + "After that, we perform 3 * 5 = 15 pairwise comparisons using either $\\text{EUBO}\\mathrm{-}\\zeta$ or $\\text{Random}\\mathrm{-}f$.\n", + "Then we move on to the second experimentation stage by generating a candidate using qNEIUU by\n", + "leveraging both the outcome model and the learned preference model.\n", + "\n", + "We additionally examine two other experimental candidate generation strategies:\n", + "* \"True utility\": We assume the true utility function is known and generate experiment candidate(s) using qLogNEI. This represents the performance upper bound of PE strategies.\n", + "* \"Random experiment\": We use random design points to generate new candidates.\n", + "\n", + "In this tutorial, we only run one replication of this benchmark; while in general we would expect \"True utility\" to outperform $\\text{EUBO}\\mathrm{-}\\zeta$ and $\\text{EUBO}\\mathrm{-}\\zeta$ to outperform\n", + "the other strategies, this is not guaranteed for a optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1679689500952, + "executionStopTime": 1679689626563, + "hidden_ranges": [], + "originalKey": "4067171b-e552-4ddf-a9e8-a40b53a5c368", + "output": { + "id": 355724710606511, + "loadingStatus": "loaded" + }, + "requestMsgId": "b824d249-226a-497e-8764-fea353877527", + "showInput": true, + "vscode": { + "languageId": "python" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "EUBO-zeta qNEIUU candidate utility: -1.026\n", + "Random-f qNEIUU candidate utility: -1.226\n", + "True objective utility: -1.004\n", + "Random experiment utility: -1.380\n" + ] + } + ], + "source": [ + "verbose = False\n", + "# Number of pairwise comparisons performed before checking posterior mean\n", + "every_n_comps = 3\n", + "# Total number of checking the maximum posterior mean\n", + "n_check_post_mean = 1 if SMOKE_TEST else 5\n", + "n_outcome_model_initialization_points = 8\n", + "within_session_results = []\n", + "exp_candidate_results = []\n", + "\n", + "# Experimentation stage: initial exploration batch\n", + "torch.manual_seed(0)\n", + "np.random.seed(0)\n", + "X, Y = generate_random_exp_data(problem, n_outcome_model_initialization_points)\n", + "outcome_model = fit_outcome_model(X, Y)\n", + "\n", + "# Preference exploration stage: initialize the preference model with comparsions\n", + "# between pairs of outcomes estimated using random design points\n", + "init_train_Y, init_train_comps = generate_random_pref_data(outcome_model, n=1)\n", + "\n", + "# Perform preference exploration using either Random-f or EUBO-zeta\n", + "for pe_strategy in [\"EUBO-zeta\", \"Random-f\"]:\n", + " train_Y, train_comps = init_train_Y.clone(), init_train_comps.clone()\n", + " within_result = find_max_posterior_mean(outcome_model, train_Y, train_comps)\n", + " within_result.update({\"pe_strategy\": pe_strategy})\n", + " within_session_results.append(within_result)\n", + "\n", + " for j in range(n_check_post_mean):\n", + " train_Y, train_comps = run_pref_learn(\n", + " outcome_model,\n", + " train_Y,\n", + " train_comps,\n", + " n_comps=every_n_comps,\n", + " pe_strategy=pe_strategy,\n", + " verbose=verbose,\n", + " )\n", + " if verbose:\n", + " print(\n", + " f\"Checking posterior mean after {(j+1) * every_n_comps} comps using PE strategy {pe_strategy}\"\n", + " )\n", + " within_result = find_max_posterior_mean(\n", + " outcome_model, train_Y, train_comps, verbose=verbose\n", + " )\n", + " within_result.update({\"pe_strategy\": pe_strategy})\n", + " within_session_results.append(within_result)\n", + "\n", + " # Going back to the experimentation stage: generate an additional batch of experimental evaluations\n", + " # with the learned preference model and qNEIUU\n", + " pref_model = fit_pref_model(train_Y, train_comps)\n", + " pref_obj = LearnedObjective(pref_model=pref_model)\n", + " exp_cand_X = gen_exp_cand(outcome_model, pref_obj, q=1, acqf_name=\"qNEI\")\n", + " qneiuu_util = util_func(problem(exp_cand_X)).item()\n", + " print(f\"{pe_strategy} qNEIUU candidate utility: {qneiuu_util:.3f}\")\n", + " exp_result = {\"util\": qneiuu_util, \"strategy\": pe_strategy}\n", + " exp_candidate_results.append(exp_result)\n", + "\n", + "# Generate a batch of experimental evaluations using oracle and random baselines\n", + "# True utility\n", + "true_obj = GenericMCObjective(util_func)\n", + "true_obj_cand_X = gen_exp_cand(outcome_model, true_obj, q=1, acqf_name=\"qNEI\")\n", + "true_obj_util = util_func(problem(true_obj_cand_X)).item()\n", + "print(f\"True objective utility: {true_obj_util:.3f}\")\n", + "exp_result = {\"util\": true_obj_util, \"strategy\": \"True Utility\"}\n", + "exp_candidate_results.append(exp_result)\n", + "\n", + "# Random experiment\n", + "_, random_Y = generate_random_exp_data(problem, 1)\n", + "random_util = util_func(random_Y).item()\n", + "print(f\"Random experiment utility: {random_util:.3f}\")\n", + "exp_result = {\"util\": random_util, \"strategy\": \"Random Experiment\"}\n", + "exp_candidate_results.append(exp_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "64f2fb33-46bc-47ac-b169-ea702e41f6cd", + "showInput": false + }, + "source": [ + "## Plot the Results\n", + "\n", + "We evaluate our results by creating two plots.\n", + "\n", + "In the first plot, we focus on comparing how $\\text{EUBO}\\mathrm{-}\\zeta$ can efficiently identify the maximizer \n", + "of $g_\\mathrm{true}(f_\\mathrm{true}(x))$ within a preference exploration stage.\n", + "We examine this by estimating the maximum posterior mean after every 3 pairwise comparisons.\n", + "\n", + "Here, we plot the the max utility value identified using $\\text{EUBO}\\mathrm{-}\\zeta$ and $\\text{Random}\\mathrm{-}f$\n", + "with increasing number of pairwise comparisons.\n", + "As we can see in this plot, the preference model learned using $\\text{EUBO}\\mathrm{-}\\zeta$ is able to identify the maximum utility more efficiently." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "output": { + "id": 416769047589601, + "loadingStatus": "loaded" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plotting\n", + "plt.figure(figsize=(8, 6))\n", + "for name, group in pd.DataFrame(within_session_results).groupby(\n", + " \"pe_strategy\", sort=True\n", + "):\n", + " plt.plot(\n", + " group[\"n_comps\"],\n", + " group[\"util\"],\n", + " label=name,\n", + " linewidth=1.5,\n", + " )\n", + "plt.xlabel(\"Number of comparisons\")\n", + "plt.ylabel(\"Max value identified\")\n", + "plt.legend(title=\"PE Strategy\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "e407a6c3-0cf3-4ec2-886a-4973db669ceb", + "showInput": false + }, + "source": [ + "In the following cell, we show the utility values achieved using different methods in the 2nd experimentation stage.\n", + "\n", + "In repeated iterations -- not done here for speed and succinctness -- we find that\n", + "$\\text{EUBO}\\mathrm{-}\\zeta$, as a one-step Bayesian optimal PE strategy, performs very similarly to the true utility strategy on average.\n", + "On the other hand, even though $\\text{Random}\\mathrm{-}f$ is a relatively straightforward PE strategy,\n", + "it is still able to suggest experimental candidates with generally higher utility values than the random experiment baseline.\n", + "\n", + "For more rigorous performance comparisons, see [1]." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1679689627689, + "executionStopTime": 1679689628137, + "hidden_ranges": [], + "originalKey": "0737e625-fe4a-47e5-bc4c-ffabb9511435", + "output": { + "id": 710845344563787, + "loadingStatus": "loaded" + }, + "requestMsgId": "5791d879-9ccc-4be5-a671-b3ac431978f4", + "showInput": true, + "vscode": { + "languageId": "python" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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E7jzthBx129g/cYt5Y5v3rpT83VQgD/GjXnlqXU82vNj5+79Ty0zeEBfDBn0eLWsV0e+BGdnJ5wJPYdNW7ZDIpGguZsrRo8cjpYeHCpFRERERI2TKDY29hlTuKmmeXl51dq9VCoVku8nwN7JlasxUIPH9k6NCds7NSZ10d7j4uJq5T4NBf8XIiIiIiIinRI0XAqlKzFdvnIVWVkPoXzKlu4iAN9/O6068RERERERkZ4RlGQcPnoCO3bt0300RERERESk9wQlGadDz8HWxhrjxrwCZydHSKVS3UdGRERERER6SdCcjIzMLPTqEQL35m5MMIiIiIiIqAxBSYaZmSkMDARP5yAiIiIiogZMUJIR2KkjIqOidR8NERERERHpPUFJxpCB/SCTybBh01YkJ6dC9ZTVpYiIiIiIqPHRaszTlGn/qfC4orAQoWEXnnqdSCTC/NnThUdHRERERER6R6skw8TEBCJR+eOmpiY1EBIREREREekzrZKMH7/7ouYjISIiIiKiBkHQnIzrN24iNy+v0jK3bt/BxUtXhMZFRERERER6SlCSMW/BMsTH36q0zK3bCdixm7uCExERERE1NlpvdpGRmYn0jEzN68SkJJiaVTwno6iwCJevXEVeXr5uoiQiqorcVODsIoiuH4RNYT5EhiZAy35A5/cAM9u6jo6IiKjB0zrJCA27gP3/HNa83vv3oWde08rLU3hkRERVVVQAbH8buHceyE2GSK2C9NG5lCjgykbAOQAY/jsgldVtrERERA2Y1knGgL690La1F27dTsD2v/aipWcLWFk2rbCsWCyGjbU1unUN0mWsRERPV1QArBoEJEUCqqLy59UqICcRiN0PrBoIvP43Ew0iIqIaonWSYWBgAPfmbnBv7oZjJ06jX+8eaNumVc1GR0Skre1vPz3BeJKqqKTc9reAketqKzoiIqJGResk40k//IdL2hJRPZKbWjJE6lkJxiOqopLyeWmAqU1NR0dERNToCFpdioioXjm7CMhJrto1uclA6MKaioiIiKhRY5JBRPov/iAAVdWuUatKryMiIiJdY5JBRPpPWSzsOpXA64iIiKhSTDKISP9JBE0vA8QCryMiIqJKMckgIv3n2Q8QVfG/M5G45DoiIiLSOSYZRKT/Or8HmNlX7Roze6DL+zUVERERUaMmeKzAg8QkXImIRGbWQyiVFU+4FImA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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plotting\n", + "plt.figure(figsize=(8, 6))\n", + "for result in exp_candidate_results:\n", + " plt.scatter(\n", + " [result[\"strategy\"]],\n", + " [result[\"util\"]],\n", + " s=100,\n", + " label=result[\"strategy\"],\n", + " )\n", + "\n", + "plt.xlabel(\"Experimentation Strategy\")\n", + "plt.ylabel(\"Utility achieved in the 2nd experiment stage\")\n", + "plt.legend(title=\"Experimentation Strategy\")" + ] + } + ], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "fileHeader": "", + "fileUid": "27675a89-3363-4f66-a496-3c1159099da2", + "isAdHoc": false, + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "fileHeader": "", + "fileUid": "cebcfd90-71e6-4e23-8a86-5514816cb017", + "indentAmount": 2, + "isAdHoc": false, + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/bope.py b/website-old/static/files/bope.py new file mode 100644 index 0000000000..78652560ea --- /dev/null +++ b/website-old/static/files/bope.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# # Bayesian Optimization with Preference Exploration +# +# In this tutorial, we demonstrate how to implement a closed loop of Bayesian optimization with preference exploration, or BOPE [1]. +# BOPE is designed for Bayesian optimization of expensive-to-evaluate experiments, +# where the response surface function of the experiment $f_\mathrm{true}$ generates vector-valued outcomes over which a decision-maker (DM) has preferences. +# These preferences are encoded by a utility function $g_\mathrm{true}$ that is not known in closed form but can be estimated by +# asking the DM to express preferences over pairs of outcome vectors. +# +# In other words, with BOPE, we wish to solve the following optimization problem: +# +# $$ +# \max_{x \in \mathcal{X}} g_\mathrm{true}(f_\mathrm{true}(x)) +# $$ +# +# Unlike many other Bayesian optimization setups where multiple consecutive batches of experiments are performed, +# in BOPE, we alternate between two stages: *preference exploration* and *experimentation*. +# +# In the preference exploration stage, we use an acquisition function (i.e., a preference exploration strategy, or PE strategy) +# to adaptively generate pairs of hypothetical outcome and ask the decision-maker’s preference within each pair. +# In the experimentation stage, we use a batch version of noisy expected improvement that integrates over our uncertainty in the +# utility function called $\text{qNEIUU}$ to generate experimental candidates for evaluation. +# +# +# [1] [Z.J. Lin, R. Astudillo, P.I. Frazier, and E. Bakshy, Preference Exploration for Efficient Bayesian Optimization with Multiple Outcomes. AISTATS, 2022.](https://arxiv.org/abs/2203.11382) + +# In[1]: + + +import os +from typing import Optional, Tuple + +import matplotlib as mpl +import matplotlib.pylab as plt +import numpy as np +import pandas as pd +import torch +from botorch.acquisition import ( + GenericMCObjective, + LearnedObjective, + MCAcquisitionObjective, +) +from botorch.acquisition.logei import qLogNoisyExpectedImprovement +from botorch.acquisition.monte_carlo import qSimpleRegret +from botorch.acquisition.preference import AnalyticExpectedUtilityOfBestOption +from botorch.fit import fit_gpytorch_mll +from botorch.models.deterministic import FixedSingleSampleModel +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.pairwise_gp import PairwiseGP, PairwiseLaplaceMarginalLogLikelihood +from botorch.models.transforms.input import Normalize +from botorch.models.transforms.outcome import Standardize +from botorch.optim.optimize import optimize_acqf +from botorch.sampling import SobolQMCNormalSampler +from botorch.test_functions.multi_objective import DTLZ2 +from botorch.utils.sampling import draw_sobol_samples +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood + + +get_ipython().run_line_magic('matplotlib', 'inline') + +# Set plotting colors +colors = ["tab:blue", "tab:orange", "tab:green", "tab:red"] +mpl.rcParams["axes.prop_cycle"] = mpl.cycler(color=colors) + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ## Problem Setup +# +# In this tutorial, we use the DTLZ2 problem with d=5 inputs and k=4 outcomes as our test problem $f_\mathrm{true}$. +# +# For the utility function $g_\mathrm{true}$, we use the negative L1 distance from a Pareto-optimal point the outcome space: +# $Y^* = f(X^*)$ where $X^* = [0.5, 0.5, 0.5, 0.5, 0.5]$. + +# In[2]: + + +def neg_l1_dist(Y: torch.Tensor, X: Optional[torch.Tensor] = None) -> torch.Tensor: + """Negative L1 distance from a Pareto optimal points""" + if len(Y.shape) == 1: + Y = Y.unsqueeze(0) + dist = torch.cdist( + Y, torch.full(Y.shape[-1:], fill_value=0.5, dtype=Y.dtype).unsqueeze(0), p=1 + ).squeeze(-1) + return -dist + + +if SMOKE_TEST: + NUM_RESTARTS = 2 + NUM_OUTCOME_SAMPLES = 2 + RAW_SAMPLES = 4 + BATCH_LIMIT = 2 +else: + NUM_RESTARTS = 8 + # Since BOPE samples from both the outcome and preference models, we take + # fewer samples than usual from the outcome model for speed. + NUM_OUTCOME_SAMPLES = 32 + RAW_SAMPLES = 64 + BATCH_LIMIT = 4 + +X_dim = 5 +Y_dim = 4 +problem = DTLZ2(dim=X_dim, num_objectives=Y_dim) +util_func = neg_l1_dist + + +# Here we define a collection of helper functions for BOPE: + +# In[3]: + + +def fit_outcome_model(X: torch.Tensor, Y: torch.Tensor) -> SingleTaskGP: + """Fit the outcome model f""" + outcome_model = SingleTaskGP( + train_X=X, + train_Y=Y, + input_transform=Normalize(d=X.shape[-1]), + outcome_transform=Standardize(m=Y.shape[-1]), + ) + mll = ExactMarginalLogLikelihood(outcome_model.likelihood, outcome_model) + fit_gpytorch_mll(mll) + return outcome_model + + +def fit_pref_model(Y: torch.Tensor, comps: torch.Tensor) -> PairwiseGP: + """Fit the preference model g.""" + model = PairwiseGP(Y, comps, input_transform=Normalize(d=Y.shape[-1])) + mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + return model + + +def gen_rand_X(problem, n: int) -> torch.Tensor: + """Generate n quasi-random Sobol points in the design space.""" + return draw_sobol_samples(bounds=problem.bounds, n=1, q=n).squeeze(0) + + +def generate_random_exp_data(problem, n: int) -> Tuple[torch.Tensor, torch.Tensor]: + """Generate n observations of (X, Y) Pairs""" + X = gen_rand_X(problem, n) + Y = problem(X) + return X, Y + + +def generate_random_pref_data( + outcome_model: SingleTaskGP, n: int +) -> Tuple[torch.Tensor, torch.Tensor]: + """Generate n pairwise comparison data between 2n points.""" + X = gen_rand_X(problem, 2 * n) + Y = outcome_model.posterior(X).sample().squeeze(0) + util = util_func(Y) + comps = gen_comps(util) + return Y, comps + + +def gen_comps(util: torch.Tensor) -> torch.Tensor: + """Given an 1-d tensor of utility, create pairwise comparisons between adjacent items.""" + util = util.reshape(-1, 2) + comps = torch.arange(util.numel()).reshape(-1, 2) + flip = util[:, 0] < util[:, 1] + comps[flip, [0]], comps[flip, [1]] = comps[flip, [1]], comps[flip, [0]] + + return comps + + +def run_pref_learn( + outcome_model: SingleTaskGP, + train_Y: torch.Tensor, + train_comps: torch.Tensor, + n_comps: int, + pe_strategy: str, + verbose: bool = False, +) -> Tuple[torch.Tensor, torch.Tensor]: + """Perform preference exploration with a given PE strategy for n_comps rounds.""" + for i in range(n_comps): + if verbose: + print(f"Running {i+1}/{n_comps} preference learning using {pe_strategy}") + pref_model = fit_pref_model(train_Y, train_comps) + if pe_strategy == "EUBO-zeta": + # EUBO-zeta + one_sample_outcome_model = FixedSingleSampleModel(model=outcome_model) + acqf = AnalyticExpectedUtilityOfBestOption( + pref_model=pref_model, outcome_model=one_sample_outcome_model + ) + cand_X, acqf_val = optimize_acqf( + acq_function=acqf, + q=2, + bounds=problem.bounds, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": BATCH_LIMIT}, + ) + cand_Y = one_sample_outcome_model(cand_X) + elif pe_strategy == "Random-f": + # Random-f + cand_X = gen_rand_X(problem, n=2) + cand_Y = outcome_model.posterior(cand_X).sample().squeeze(0) + else: + raise RuntimeError("Unknown preference exploration strategy!") + + cand_Y = cand_Y.detach().clone() + cand_comps = gen_comps(util_func(cand_Y)) + + train_comps = torch.cat((train_comps, cand_comps + train_Y.shape[0])) + train_Y = torch.cat((train_Y, cand_Y)) + + return train_Y, train_comps + + +def gen_exp_cand( + outcome_model: SingleTaskGP, + objective: MCAcquisitionObjective, + q: int, + acqf_name: str, +) -> torch.Tensor: + """Given an outcome model and an objective, generate q experimental candidates + using specified acquisition function.""" + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([NUM_OUTCOME_SAMPLES])) + if acqf_name == "qNEI": + # generate experimental candidates with qNEI/qNEIUU + acq_func = qLogNoisyExpectedImprovement( + model=outcome_model, + objective=objective, + X_baseline=X, + sampler=sampler, + prune_baseline=True, + ) + elif acqf_name == "posterior_mean": + # generate experimental candidates with maximum posterior mean + acq_func = qSimpleRegret( + model=outcome_model, + sampler=sampler, + objective=objective, + ) + else: + raise RuntimeError("Unknown acquisition function name!") + + # optimize the acquisition function + candidates, _ = optimize_acqf( + acq_function=acq_func, + q=q, + bounds=problem.bounds, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + options={"batch_limit": BATCH_LIMIT}, + sequential=True, + ) + return candidates + + +def find_max_posterior_mean( + outcome_model: SingleTaskGP, + train_Y: torch.Tensor, + train_comps: torch.Tensor, + verbose: bool = False, +): + """Helper function that find the max posterior mean under current outcome and + preference model""" + pref_model = fit_pref_model(train_Y, train_comps) + pref_obj = LearnedObjective(pref_model=pref_model) + post_mean_cand_X = gen_exp_cand( + outcome_model, pref_obj, q=1, acqf_name="posterior_mean" + ) + + post_mean_util = util_func(problem(post_mean_cand_X)).item() + if verbose: + print(f"Max posterior mean utility: {post_mean_util:.3f}") + within_result = { + "n_comps": train_comps.shape[0], + "util": post_mean_util, + } + return within_result + + +# ## Closed Loop BOPE +# +# ### Setup +# The following cell shows the core part of this tutorial. +# In BOPE, we use two probablistic models (in this case, two Gaussian processes) $f$ and $g$ +# to model $f_\mathrm{true}$ and $g_\mathrm{true}$ respectively. +# +# We start by initializing the outcome model $f$ with 8 quasi-random points (the initial experimentation stage). +# +# Next, we enter the preference exploration (PE) stage. +# A straightforward strategy of performing PE is to present the decision-maker with comparisons using outcomes sampled from the outcome model at random design points. We refer this method as $\text{Random}\mathrm{-}f$. +# +# Alternatively, we could initialize the preference model with $\text{Random}\mathrm{-}f$, +# then perform PE using the $\text{EUBO}\mathrm{-}\zeta$ acquisition function as proposed in [1]. +# +# +# In this tutorial, we examine both strategies by starting with initializating the preference model with 3 comparisons using $\text{Random}\mathrm{-}f$. +# After that, we perform 3 * 5 = 15 pairwise comparisons using either $\text{EUBO}\mathrm{-}\zeta$ or $\text{Random}\mathrm{-}f$. +# Then we move on to the second experimentation stage by generating a candidate using qNEIUU by +# leveraging both the outcome model and the learned preference model. +# +# We additionally examine two other experimental candidate generation strategies: +# * "True utility": We assume the true utility function is known and generate experiment candidate(s) using qLogNEI. This represents the performance upper bound of PE strategies. +# * "Random experiment": We use random design points to generate new candidates. +# +# In this tutorial, we only run one replication of this benchmark; while in general we would expect "True utility" to outperform $\text{EUBO}\mathrm{-}\zeta$ and $\text{EUBO}\mathrm{-}\zeta$ to outperform +# the other strategies, this is not guaranteed for a optimization. + +# In[4]: + + +verbose = False +# Number of pairwise comparisons performed before checking posterior mean +every_n_comps = 3 +# Total number of checking the maximum posterior mean +n_check_post_mean = 1 if SMOKE_TEST else 5 +n_outcome_model_initialization_points = 8 +within_session_results = [] +exp_candidate_results = [] + +# Experimentation stage: initial exploration batch +torch.manual_seed(0) +np.random.seed(0) +X, Y = generate_random_exp_data(problem, n_outcome_model_initialization_points) +outcome_model = fit_outcome_model(X, Y) + +# Preference exploration stage: initialize the preference model with comparsions +# between pairs of outcomes estimated using random design points +init_train_Y, init_train_comps = generate_random_pref_data(outcome_model, n=1) + +# Perform preference exploration using either Random-f or EUBO-zeta +for pe_strategy in ["EUBO-zeta", "Random-f"]: + train_Y, train_comps = init_train_Y.clone(), init_train_comps.clone() + within_result = find_max_posterior_mean(outcome_model, train_Y, train_comps) + within_result.update({"pe_strategy": pe_strategy}) + within_session_results.append(within_result) + + for j in range(n_check_post_mean): + train_Y, train_comps = run_pref_learn( + outcome_model, + train_Y, + train_comps, + n_comps=every_n_comps, + pe_strategy=pe_strategy, + verbose=verbose, + ) + if verbose: + print( + f"Checking posterior mean after {(j+1) * every_n_comps} comps using PE strategy {pe_strategy}" + ) + within_result = find_max_posterior_mean( + outcome_model, train_Y, train_comps, verbose=verbose + ) + within_result.update({"pe_strategy": pe_strategy}) + within_session_results.append(within_result) + + # Going back to the experimentation stage: generate an additional batch of experimental evaluations + # with the learned preference model and qNEIUU + pref_model = fit_pref_model(train_Y, train_comps) + pref_obj = LearnedObjective(pref_model=pref_model) + exp_cand_X = gen_exp_cand(outcome_model, pref_obj, q=1, acqf_name="qNEI") + qneiuu_util = util_func(problem(exp_cand_X)).item() + print(f"{pe_strategy} qNEIUU candidate utility: {qneiuu_util:.3f}") + exp_result = {"util": qneiuu_util, "strategy": pe_strategy} + exp_candidate_results.append(exp_result) + +# Generate a batch of experimental evaluations using oracle and random baselines +# True utility +true_obj = GenericMCObjective(util_func) +true_obj_cand_X = gen_exp_cand(outcome_model, true_obj, q=1, acqf_name="qNEI") +true_obj_util = util_func(problem(true_obj_cand_X)).item() +print(f"True objective utility: {true_obj_util:.3f}") +exp_result = {"util": true_obj_util, "strategy": "True Utility"} +exp_candidate_results.append(exp_result) + +# Random experiment +_, random_Y = generate_random_exp_data(problem, 1) +random_util = util_func(random_Y).item() +print(f"Random experiment utility: {random_util:.3f}") +exp_result = {"util": random_util, "strategy": "Random Experiment"} +exp_candidate_results.append(exp_result) + + +# ## Plot the Results +# +# We evaluate our results by creating two plots. +# +# In the first plot, we focus on comparing how $\text{EUBO}\mathrm{-}\zeta$ can efficiently identify the maximizer +# of $g_\mathrm{true}(f_\mathrm{true}(x))$ within a preference exploration stage. +# We examine this by estimating the maximum posterior mean after every 3 pairwise comparisons. +# +# Here, we plot the the max utility value identified using $\text{EUBO}\mathrm{-}\zeta$ and $\text{Random}\mathrm{-}f$ +# with increasing number of pairwise comparisons. +# As we can see in this plot, the preference model learned using $\text{EUBO}\mathrm{-}\zeta$ is able to identify the maximum utility more efficiently. + +# In[5]: + + +# Plotting +plt.figure(figsize=(8, 6)) +for name, group in pd.DataFrame(within_session_results).groupby( + "pe_strategy", sort=True +): + plt.plot( + group["n_comps"], + group["util"], + label=name, + linewidth=1.5, + ) +plt.xlabel("Number of comparisons") +plt.ylabel("Max value identified") +plt.legend(title="PE Strategy") + + +# In the following cell, we show the utility values achieved using different methods in the 2nd experimentation stage. +# +# In repeated iterations -- not done here for speed and succinctness -- we find that +# $\text{EUBO}\mathrm{-}\zeta$, as a one-step Bayesian optimal PE strategy, performs very similarly to the true utility strategy on average. +# On the other hand, even though $\text{Random}\mathrm{-}f$ is a relatively straightforward PE strategy, +# it is still able to suggest experimental candidates with generally higher utility values than the random experiment baseline. +# +# For more rigorous performance comparisons, see [1]. + +# In[6]: + + +# Plotting +plt.figure(figsize=(8, 6)) +for result in exp_candidate_results: + plt.scatter( + [result["strategy"]], + [result["util"]], + s=100, + label=result["strategy"], + ) + +plt.xlabel("Experimentation Strategy") +plt.ylabel("Utility achieved in the 2nd experiment stage") +plt.legend(title="Experimentation Strategy") + diff --git a/website-old/static/files/closed_loop_botorch_only.ipynb b/website-old/static/files/closed_loop_botorch_only.ipynb new file mode 100644 index 0000000000..467d40e811 --- /dev/null +++ b/website-old/static/files/closed_loop_botorch_only.ipynb @@ -0,0 +1,570 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "00018e33-90ca-4f63-b741-fe1fd43ca7db", + "showInput": false + }, + "source": [ + "## Closed-loop batch, constrained BO in BoTorch with qLogEI and qLogNEI\n", + "\n", + "In this tutorial, we illustrate how to implement a simple Bayesian Optimization (BO) closed loop in BoTorch.\n", + "\n", + "In general, we recommend for a relatively simple setup (like this one) to use Ax, since this will simplify your setup (including the amount of code you need to write) considerably. See the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial.\n", + "\n", + "However, you may want to do things that are not easily supported in Ax at this time (like running high-dimensional BO using a VAE+GP model that you jointly train on high-dimensional input data). If you find yourself in such a situation, you will need to write your own optimization loop, as we do in this tutorial.\n", + "\n", + "\n", + "We use the batch Log Expected Improvement (`qLogEI`) and batch Noisy Expected Improvement (`qLogNEI`) acquisition functions to optimize a constrained version of the synthetic Hartmann6 test function. The standard problem is\n", + "\n", + "$$f(x) = -\\sum_{i=1}^4 \\alpha_i \\exp \\left( -\\sum_{j=1}^6 A_{ij} (x_j - P_{ij})^2 \\right)$$\n", + "\n", + "over $x \\in [0,1]^6$ (parameter values can be found in `botorch/test_functions/hartmann6.py`).\n", + "\n", + "In real BO applications, the design $x$ can influence multiple metrics in unknown ways, and the decision-maker often wants to optimize one metric without sacrificing another. To illustrate this, we add a synthetic constraint of the form $\\|x\\|_1 - 3 \\le 0$. Both the objective and the constraint are observed with noise. \n", + "\n", + "Since botorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\\max_{x} -f(x) = 3.32237$." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649987115, + "executionStopTime": 1668649987899, + "originalKey": "2c0bfbc7-7e42-4601-83ed-4a77270803a8", + "requestMsgId": "18ccce84-9f39-4c3d-89b1-1e9ed2540859" + }, + "outputs": [], + "source": [ + "import os\n", + "from typing import Optional\n", + "\n", + "import torch\n", + "\n", + "device = torch.device(\"cuda:3\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "4ba4e568-0ef2-430e-aecb-dae8889d6664", + "showInput": false + }, + "source": [ + "### Problem setup\n", + "\n", + "First, we define the constraint used in the example in `outcome_constraint`. The second function `weighted_obj` is a \"feasibility-weighted objective,\" which returns zero when not feasible. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649988205, + "executionStopTime": 1668649988602, + "originalKey": "b1c9de4d-a7ba-4782-ab68-2def8b562f7b", + "output": { + "id": 364616190032149, + "loadingStatus": "loaded" + }, + "requestMsgId": "96673081-cc25-4ca0-a40d-48756fde8647" + }, + "outputs": [], + "source": [ + "from botorch.test_functions import Hartmann\n", + "\n", + "\n", + "neg_hartmann6 = Hartmann(negate=True)\n", + "\n", + "\n", + "def outcome_constraint(X):\n", + " \"\"\"L1 constraint; feasible if less than or equal to zero.\"\"\"\n", + " return X.sum(dim=-1) - 3\n", + "\n", + "\n", + "def weighted_obj(X):\n", + " \"\"\"Feasibility weighted objective; zero if not feasible.\"\"\"\n", + " return neg_hartmann6(X) * (outcome_constraint(X) <= 0).type_as(X)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "c262e98f-924d-414e-889f-7e65a37d3689", + "showInput": false + }, + "source": [ + "#### Model initialization\n", + "\n", + "We use a `MultiOutputGP` to model the objective (output 0) and the constraint (output 1). We assume known homoskedastic observation noise on both the objective and constraint with standard error $\\sigma = 0.5$. \n", + "\n", + "Each component is a `SingleTaskGP`. The models are initialized with 10 points drawn randomly from $[0,1]^6$." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649988934, + "executionStopTime": 1668649992556, + "originalKey": "41db1652-96c8-468d-9913-20a8e78f6fd6", + "requestMsgId": "f83596cc-c30d-43ca-b9a1-fa3f5ad6b4ad" + }, + "outputs": [], + "source": [ + "from botorch.models.transforms.input import Normalize\n", + "from botorch.models import SingleTaskGP, ModelListGP\n", + "from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood\n", + "\n", + "NOISE_SE = 0.25\n", + "train_yvar = torch.tensor(NOISE_SE**2, device=device, dtype=dtype)\n", + "\n", + "\n", + "def generate_initial_data(n=10):\n", + " # generate training data\n", + " train_x = torch.rand(10, 6, device=device, dtype=dtype)\n", + " exact_obj = neg_hartmann6(train_x).unsqueeze(-1) # add output dimension\n", + " exact_con = outcome_constraint(train_x).unsqueeze(-1) # add output dimension\n", + " train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)\n", + " train_con = exact_con + NOISE_SE * torch.randn_like(exact_con)\n", + " best_observed_value = weighted_obj(train_x).max().item()\n", + " return train_x, train_obj, train_con, best_observed_value\n", + "\n", + "\n", + "def initialize_model(train_x, train_obj, train_con, state_dict=None):\n", + " # define models for objective and constraint\n", + " model_obj = SingleTaskGP(\n", + " train_x,\n", + " train_obj,\n", + " train_yvar.expand_as(train_obj),\n", + " input_transform=Normalize(d=train_x.shape[-1]),\n", + " ).to(train_x)\n", + " model_con = SingleTaskGP(\n", + " train_x,\n", + " train_con,\n", + " train_yvar.expand_as(train_con),\n", + " input_transform=Normalize(d=train_x.shape[-1]),\n", + " ).to(train_x)\n", + " # combine into a multi-output GP model\n", + " model = ModelListGP(model_obj, model_con)\n", + " mll = SumMarginalLogLikelihood(model.likelihood, model)\n", + " # load state dict if it is passed\n", + " if state_dict is not None:\n", + " model.load_state_dict(state_dict)\n", + " return mll, model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "c34aa925-9abd-4bf2-9f1f-a4795be57b3b", + "showInput": false + }, + "source": [ + "#### Define a construct to extract the objective and constraint from the GP\n", + "The methods below take the outputs of the GP and return the objective and the constraint. In general, these can be any `Callable`, but here we simply need to index the correct output." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649992926, + "executionStopTime": 1668649993022, + "originalKey": "6af5d96a-1b65-4551-bf7d-b125f9749c63", + "requestMsgId": "ba2d1a49-04d4-4b3e-85ef-f40a51ed3945" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.objective import GenericMCObjective\n", + "\n", + "def obj_callable(Z: torch.Tensor, X: Optional[torch.Tensor] = None):\n", + " return Z[..., 0]\n", + "\n", + "\n", + "def constraint_callable(Z):\n", + " return Z[..., 1]\n", + "\n", + "\n", + "objective = GenericMCObjective(objective=obj_callable)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e45f8a78-36e4-4692-9ce3-7e883f7780cb", + "showInput": false + }, + "source": [ + "#### Define a helper function that performs the essential BO step\n", + "The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. For this example, we'll use a small batch of $q=3$. The function `optimize_acqf` optimizes the $q$ points jointly. A simple initialization heuristic is used to select the 10 restart initial locations from a set of 50 random points. " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649993442, + "executionStopTime": 1668649993515, + "originalKey": "f450c171-6984-4114-bf99-99c3a4e68eb2", + "output": { + "id": 385726674337920, + "loadingStatus": "loaded" + }, + "requestMsgId": "57d29886-0a14-410b-aaba-596c8559f5a0" + }, + "outputs": [], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "\n", + "bounds = torch.tensor([[0.0] * 6, [1.0] * 6], device=device, dtype=dtype)\n", + "\n", + "BATCH_SIZE = 3 if not SMOKE_TEST else 2\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 32\n", + "\n", + "\n", + "def optimize_acqf_and_get_observation(acq_func):\n", + " \"\"\"Optimizes the acquisition function, and returns a new candidate and a noisy observation.\"\"\"\n", + " # optimize\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + " # observe new values\n", + " new_x = candidates.detach()\n", + " exact_obj = neg_hartmann6(new_x).unsqueeze(-1) # add output dimension\n", + " exact_con = outcome_constraint(new_x).unsqueeze(-1) # add output dimension\n", + " new_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj)\n", + " new_con = exact_con + NOISE_SE * torch.randn_like(exact_con)\n", + " return new_x, new_obj, new_con\n", + "\n", + "\n", + "def update_random_observations(best_random):\n", + " \"\"\"Simulates a random policy by taking a the current list of best values observed randomly,\n", + " drawing a new random point, observing its value, and updating the list.\n", + " \"\"\"\n", + " rand_x = torch.rand(BATCH_SIZE, 6)\n", + " next_random_best = weighted_obj(rand_x).max().item()\n", + " best_random.append(max(best_random[-1], next_random_best))\n", + " return best_random" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "b9344aeb-149e-46a5-9c17-dfbd9ae4727c", + "showInput": false + }, + "source": [ + "### Perform Bayesian Optimization loop with qLogNEI\n", + "The Bayesian optimization \"loop\" for a batch size of $q$ simply iterates the following steps:\n", + "1. given a surrogate model, choose a batch of points $\\{x_1, x_2, \\ldots x_q\\}$\n", + "2. observe $f(x)$ for each $x$ in the batch \n", + "3. update the surrogate model. \n", + "\n", + "\n", + "Just for illustration purposes, we run three trials each of which do `N_BATCH=20` rounds of optimization. The acquisition function is approximated using `MC_SAMPLES=256` samples.\n", + "\n", + "*Note*: Running this may take a little while." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649993811, + "executionStopTime": 1668650936026, + "originalKey": "f137bf2a-5d39-4c8c-bb24-84326d4ab5d7", + "requestMsgId": "0b4d1d37-a9cf-4f69-a896-0836506ee521" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Trial 1 of 3 ....................\n", + "Trial 2 of 3 ....................\n", + "Trial 3 of 3 ...................." + ] + } + ], + "source": [ + "import time\n", + "import warnings\n", + "\n", + "from botorch import fit_gpytorch_mll\n", + "from botorch.acquisition import (\n", + " qLogExpectedImprovement,\n", + " qLogNoisyExpectedImprovement,\n", + ")\n", + "from botorch.exceptions import BadInitialCandidatesWarning\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "\n", + "\n", + "warnings.filterwarnings(\"ignore\", category=BadInitialCandidatesWarning)\n", + "warnings.filterwarnings(\"ignore\", category=RuntimeWarning)\n", + "\n", + "\n", + "N_TRIALS = 3 if not SMOKE_TEST else 2\n", + "N_BATCH = 20 if not SMOKE_TEST else 2\n", + "MC_SAMPLES = 256 if not SMOKE_TEST else 32\n", + "\n", + "verbose = False\n", + "\n", + "best_observed_all_ei, best_observed_all_nei, best_random_all = [], [], []\n", + "\n", + "# average over multiple trials\n", + "for trial in range(1, N_TRIALS + 1):\n", + "\n", + " print(f\"\\nTrial {trial:>2} of {N_TRIALS} \", end=\"\")\n", + " best_observed_ei, best_observed_nei, best_random = [], [], []\n", + "\n", + " # call helper functions to generate initial training data and initialize model\n", + " (\n", + " train_x_ei,\n", + " train_obj_ei,\n", + " train_con_ei,\n", + " best_observed_value_ei,\n", + " ) = generate_initial_data(n=10)\n", + " mll_ei, model_ei = initialize_model(train_x_ei, train_obj_ei, train_con_ei)\n", + "\n", + " train_x_nei, train_obj_nei, train_con_nei = train_x_ei, train_obj_ei, train_con_ei\n", + " best_observed_value_nei = best_observed_value_ei\n", + " mll_nei, model_nei = initialize_model(train_x_nei, train_obj_nei, train_con_nei)\n", + "\n", + " best_observed_ei.append(best_observed_value_ei)\n", + " best_observed_nei.append(best_observed_value_nei)\n", + " best_random.append(best_observed_value_ei)\n", + "\n", + " # run N_BATCH rounds of BayesOpt after the initial random batch\n", + " for iteration in range(1, N_BATCH + 1):\n", + "\n", + " t0 = time.monotonic()\n", + "\n", + " # fit the models\n", + " fit_gpytorch_mll(mll_ei)\n", + " fit_gpytorch_mll(mll_nei)\n", + "\n", + " # define the qEI and qNEI acquisition modules using a QMC sampler\n", + " qmc_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + "\n", + " # for best_f, we use the best observed noisy values as an approximation\n", + " qLogEI = qLogExpectedImprovement(\n", + " model=model_ei,\n", + " best_f=(train_obj_ei * (train_con_ei <= 0).to(train_obj_ei)).max(),\n", + " sampler=qmc_sampler,\n", + " objective=objective,\n", + " constraints=[constraint_callable],\n", + " )\n", + "\n", + " qLogNEI = qLogNoisyExpectedImprovement(\n", + " model=model_nei,\n", + " X_baseline=train_x_nei,\n", + " sampler=qmc_sampler,\n", + " objective=objective,\n", + " constraints=[constraint_callable],\n", + " )\n", + "\n", + " # optimize and get new observation\n", + " new_x_ei, new_obj_ei, new_con_ei = optimize_acqf_and_get_observation(qLogEI)\n", + " new_x_nei, new_obj_nei, new_con_nei = optimize_acqf_and_get_observation(qLogNEI)\n", + "\n", + " # update training points\n", + " train_x_ei = torch.cat([train_x_ei, new_x_ei])\n", + " train_obj_ei = torch.cat([train_obj_ei, new_obj_ei])\n", + " train_con_ei = torch.cat([train_con_ei, new_con_ei])\n", + "\n", + " train_x_nei = torch.cat([train_x_nei, new_x_nei])\n", + " train_obj_nei = torch.cat([train_obj_nei, new_obj_nei])\n", + " train_con_nei = torch.cat([train_con_nei, new_con_nei])\n", + "\n", + " # update progress\n", + " best_random = update_random_observations(best_random)\n", + " best_value_ei = weighted_obj(train_x_ei).max().item()\n", + " best_value_nei = weighted_obj(train_x_nei).max().item()\n", + " best_observed_ei.append(best_value_ei)\n", + " best_observed_nei.append(best_value_nei)\n", + "\n", + " # reinitialize the models so they are ready for fitting on next iteration\n", + " # use the current state dict to speed up fitting\n", + " mll_ei, model_ei = initialize_model(\n", + " train_x_ei,\n", + " train_obj_ei,\n", + " train_con_ei,\n", + " model_ei.state_dict(),\n", + " )\n", + " mll_nei, model_nei = initialize_model(\n", + " train_x_nei,\n", + " train_obj_nei,\n", + " train_con_nei,\n", + " model_nei.state_dict(),\n", + " )\n", + "\n", + " t1 = time.monotonic()\n", + "\n", + " if verbose:\n", + " print(\n", + " f\"\\nBatch {iteration:>2}: best_value (random, qEI, qNEI) = \"\n", + " f\"({max(best_random):>4.2f}, {best_value_ei:>4.2f}, {best_value_nei:>4.2f}), \"\n", + " f\"time = {t1-t0:>4.2f}.\",\n", + " end=\"\",\n", + " )\n", + " else:\n", + " print(\".\", end=\"\")\n", + "\n", + " best_observed_all_ei.append(best_observed_ei)\n", + " best_observed_all_nei.append(best_observed_nei)\n", + " best_random_all.append(best_random)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "587be90e-69f5-4b33-ad40-1aafe38d305c", + "showInput": false + }, + "source": [ + "#### Plot the results\n", + "The plot below shows the best objective value observed at each step of the optimization for each of the algorithms. The confidence intervals represent the variance at that step in the optimization across the trial runs. The variance across optimization runs is quite high, so in order to get a better estimate of the average performance one would have to run a much larger number of trials `N_TRIALS` (we avoid this here to limit the runtime of this tutorial). " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668650936442, + "executionStopTime": 1668650937028, + "originalKey": "8729310f-7438-4d16-a2d5-5c46e5ef1c03", + "output": { + "id": 804722568408483, + "loadingStatus": "loaded" + }, + "requestMsgId": "3e10cd44-d4fa-4efc-941c-07dabdd6689c" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "def ci(y):\n", + " return 1.96 * y.std(axis=0) / np.sqrt(N_TRIALS)\n", + "\n", + "\n", + "GLOBAL_MAXIMUM = neg_hartmann6.optimal_value\n", + "\n", + "\n", + "iters = np.arange(N_BATCH + 1) * BATCH_SIZE\n", + "y_ei = np.asarray(best_observed_all_ei)\n", + "y_nei = np.asarray(best_observed_all_nei)\n", + "y_rnd = np.asarray(best_random_all)\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n", + "ax.errorbar(iters, y_rnd.mean(axis=0), yerr=ci(y_rnd), label=\"random\", linewidth=1.5)\n", + "ax.errorbar(iters, y_ei.mean(axis=0), yerr=ci(y_ei), label=\"qLogEI\", linewidth=1.5)\n", + "ax.errorbar(iters, y_nei.mean(axis=0), yerr=ci(y_nei), label=\"qLogNEI\", linewidth=1.5)\n", + "plt.plot(\n", + " [0, N_BATCH * BATCH_SIZE],\n", + " [GLOBAL_MAXIMUM] * 2,\n", + " \"k\",\n", + " label=\"true best feasible objective\",\n", + " linewidth=2,\n", + ")\n", + "ax.set_ylim(bottom=0.5)\n", + "ax.set(\n", + " xlabel=\"number of observations (beyond initial points)\",\n", + " ylabel=\"best objective value\",\n", + ")\n", + "ax.legend(loc=\"lower right\")" + ] + } + ], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "fileHeader": "", + "fileUid": "a216a2b6-1888-4ff1-84ca-31580b9528bd", + "isAdHoc": false, + "kernelspec": { + "display_name": "python3", + "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.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "fileHeader": "", + "fileUid": "17f3c41e-9658-4418-854a-cdfccadfcb1d", + "indentAmount": 2, + "isAdHoc": false, + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/closed_loop_botorch_only.py b/website-old/static/files/closed_loop_botorch_only.py new file mode 100644 index 0000000000..e0ae07be6e --- /dev/null +++ b/website-old/static/files/closed_loop_botorch_only.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Closed-loop batch, constrained BO in BoTorch with qLogEI and qLogNEI +# +# In this tutorial, we illustrate how to implement a simple Bayesian Optimization (BO) closed loop in BoTorch. +# +# In general, we recommend for a relatively simple setup (like this one) to use Ax, since this will simplify your setup (including the amount of code you need to write) considerably. See the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. +# +# However, you may want to do things that are not easily supported in Ax at this time (like running high-dimensional BO using a VAE+GP model that you jointly train on high-dimensional input data). If you find yourself in such a situation, you will need to write your own optimization loop, as we do in this tutorial. +# +# +# We use the batch Log Expected Improvement (`qLogEI`) and batch Noisy Expected Improvement (`qLogNEI`) acquisition functions to optimize a constrained version of the synthetic Hartmann6 test function. The standard problem is +# +# $$f(x) = -\sum_{i=1}^4 \alpha_i \exp \left( -\sum_{j=1}^6 A_{ij} (x_j - P_{ij})^2 \right)$$ +# +# over $x \in [0,1]^6$ (parameter values can be found in `botorch/test_functions/hartmann6.py`). +# +# In real BO applications, the design $x$ can influence multiple metrics in unknown ways, and the decision-maker often wants to optimize one metric without sacrificing another. To illustrate this, we add a synthetic constraint of the form $\|x\|_1 - 3 \le 0$. Both the objective and the constraint are observed with noise. +# +# Since botorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_{x} -f(x) = 3.32237$. + +# In[14]: + + +import os +from typing import Optional + +import torch + +device = torch.device("cuda:3" if torch.cuda.is_available() else "cpu") +dtype = torch.double +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Problem setup +# +# First, we define the constraint used in the example in `outcome_constraint`. The second function `weighted_obj` is a "feasibility-weighted objective," which returns zero when not feasible. + +# In[15]: + + +from botorch.test_functions import Hartmann + + +neg_hartmann6 = Hartmann(negate=True) + + +def outcome_constraint(X): + """L1 constraint; feasible if less than or equal to zero.""" + return X.sum(dim=-1) - 3 + + +def weighted_obj(X): + """Feasibility weighted objective; zero if not feasible.""" + return neg_hartmann6(X) * (outcome_constraint(X) <= 0).type_as(X) + + +# #### Model initialization +# +# We use a `MultiOutputGP` to model the objective (output 0) and the constraint (output 1). We assume known homoskedastic observation noise on both the objective and constraint with standard error $\sigma = 0.5$. +# +# Each component is a `SingleTaskGP`. The models are initialized with 10 points drawn randomly from $[0,1]^6$. + +# In[16]: + + +from botorch.models.transforms.input import Normalize +from botorch.models import SingleTaskGP, ModelListGP +from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood + +NOISE_SE = 0.25 +train_yvar = torch.tensor(NOISE_SE**2, device=device, dtype=dtype) + + +def generate_initial_data(n=10): + # generate training data + train_x = torch.rand(10, 6, device=device, dtype=dtype) + exact_obj = neg_hartmann6(train_x).unsqueeze(-1) # add output dimension + exact_con = outcome_constraint(train_x).unsqueeze(-1) # add output dimension + train_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj) + train_con = exact_con + NOISE_SE * torch.randn_like(exact_con) + best_observed_value = weighted_obj(train_x).max().item() + return train_x, train_obj, train_con, best_observed_value + + +def initialize_model(train_x, train_obj, train_con, state_dict=None): + # define models for objective and constraint + model_obj = SingleTaskGP( + train_x, + train_obj, + train_yvar.expand_as(train_obj), + input_transform=Normalize(d=train_x.shape[-1]), + ).to(train_x) + model_con = SingleTaskGP( + train_x, + train_con, + train_yvar.expand_as(train_con), + input_transform=Normalize(d=train_x.shape[-1]), + ).to(train_x) + # combine into a multi-output GP model + model = ModelListGP(model_obj, model_con) + mll = SumMarginalLogLikelihood(model.likelihood, model) + # load state dict if it is passed + if state_dict is not None: + model.load_state_dict(state_dict) + return mll, model + + +# #### Define a construct to extract the objective and constraint from the GP +# The methods below take the outputs of the GP and return the objective and the constraint. In general, these can be any `Callable`, but here we simply need to index the correct output. + +# In[17]: + + +from botorch.acquisition.objective import GenericMCObjective + +def obj_callable(Z: torch.Tensor, X: Optional[torch.Tensor] = None): + return Z[..., 0] + + +def constraint_callable(Z): + return Z[..., 1] + + +objective = GenericMCObjective(objective=obj_callable) + + +# #### Define a helper function that performs the essential BO step +# The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. For this example, we'll use a small batch of $q=3$. The function `optimize_acqf` optimizes the $q$ points jointly. A simple initialization heuristic is used to select the 10 restart initial locations from a set of 50 random points. + +# In[18]: + + +from botorch.optim import optimize_acqf + + +bounds = torch.tensor([[0.0] * 6, [1.0] * 6], device=device, dtype=dtype) + +BATCH_SIZE = 3 if not SMOKE_TEST else 2 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 32 + + +def optimize_acqf_and_get_observation(acq_func): + """Optimizes the acquisition function, and returns a new candidate and a noisy observation.""" + # optimize + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + ) + # observe new values + new_x = candidates.detach() + exact_obj = neg_hartmann6(new_x).unsqueeze(-1) # add output dimension + exact_con = outcome_constraint(new_x).unsqueeze(-1) # add output dimension + new_obj = exact_obj + NOISE_SE * torch.randn_like(exact_obj) + new_con = exact_con + NOISE_SE * torch.randn_like(exact_con) + return new_x, new_obj, new_con + + +def update_random_observations(best_random): + """Simulates a random policy by taking a the current list of best values observed randomly, + drawing a new random point, observing its value, and updating the list. + """ + rand_x = torch.rand(BATCH_SIZE, 6) + next_random_best = weighted_obj(rand_x).max().item() + best_random.append(max(best_random[-1], next_random_best)) + return best_random + + +# ### Perform Bayesian Optimization loop with qLogNEI +# The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps: +# 1. given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$ +# 2. observe $f(x)$ for each $x$ in the batch +# 3. update the surrogate model. +# +# +# Just for illustration purposes, we run three trials each of which do `N_BATCH=20` rounds of optimization. The acquisition function is approximated using `MC_SAMPLES=256` samples. +# +# *Note*: Running this may take a little while. + +# In[19]: + + +import time +import warnings + +from botorch import fit_gpytorch_mll +from botorch.acquisition import ( + qLogExpectedImprovement, + qLogNoisyExpectedImprovement, +) +from botorch.exceptions import BadInitialCandidatesWarning +from botorch.sampling.normal import SobolQMCNormalSampler + + +warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning) +warnings.filterwarnings("ignore", category=RuntimeWarning) + + +N_TRIALS = 3 if not SMOKE_TEST else 2 +N_BATCH = 20 if not SMOKE_TEST else 2 +MC_SAMPLES = 256 if not SMOKE_TEST else 32 + +verbose = False + +best_observed_all_ei, best_observed_all_nei, best_random_all = [], [], [] + +# average over multiple trials +for trial in range(1, N_TRIALS + 1): + + print(f"\nTrial {trial:>2} of {N_TRIALS} ", end="") + best_observed_ei, best_observed_nei, best_random = [], [], [] + + # call helper functions to generate initial training data and initialize model + ( + train_x_ei, + train_obj_ei, + train_con_ei, + best_observed_value_ei, + ) = generate_initial_data(n=10) + mll_ei, model_ei = initialize_model(train_x_ei, train_obj_ei, train_con_ei) + + train_x_nei, train_obj_nei, train_con_nei = train_x_ei, train_obj_ei, train_con_ei + best_observed_value_nei = best_observed_value_ei + mll_nei, model_nei = initialize_model(train_x_nei, train_obj_nei, train_con_nei) + + best_observed_ei.append(best_observed_value_ei) + best_observed_nei.append(best_observed_value_nei) + best_random.append(best_observed_value_ei) + + # run N_BATCH rounds of BayesOpt after the initial random batch + for iteration in range(1, N_BATCH + 1): + + t0 = time.monotonic() + + # fit the models + fit_gpytorch_mll(mll_ei) + fit_gpytorch_mll(mll_nei) + + # define the qEI and qNEI acquisition modules using a QMC sampler + qmc_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + + # for best_f, we use the best observed noisy values as an approximation + qLogEI = qLogExpectedImprovement( + model=model_ei, + best_f=(train_obj_ei * (train_con_ei <= 0).to(train_obj_ei)).max(), + sampler=qmc_sampler, + objective=objective, + constraints=[constraint_callable], + ) + + qLogNEI = qLogNoisyExpectedImprovement( + model=model_nei, + X_baseline=train_x_nei, + sampler=qmc_sampler, + objective=objective, + constraints=[constraint_callable], + ) + + # optimize and get new observation + new_x_ei, new_obj_ei, new_con_ei = optimize_acqf_and_get_observation(qLogEI) + new_x_nei, new_obj_nei, new_con_nei = optimize_acqf_and_get_observation(qLogNEI) + + # update training points + train_x_ei = torch.cat([train_x_ei, new_x_ei]) + train_obj_ei = torch.cat([train_obj_ei, new_obj_ei]) + train_con_ei = torch.cat([train_con_ei, new_con_ei]) + + train_x_nei = torch.cat([train_x_nei, new_x_nei]) + train_obj_nei = torch.cat([train_obj_nei, new_obj_nei]) + train_con_nei = torch.cat([train_con_nei, new_con_nei]) + + # update progress + best_random = update_random_observations(best_random) + best_value_ei = weighted_obj(train_x_ei).max().item() + best_value_nei = weighted_obj(train_x_nei).max().item() + best_observed_ei.append(best_value_ei) + best_observed_nei.append(best_value_nei) + + # reinitialize the models so they are ready for fitting on next iteration + # use the current state dict to speed up fitting + mll_ei, model_ei = initialize_model( + train_x_ei, + train_obj_ei, + train_con_ei, + model_ei.state_dict(), + ) + mll_nei, model_nei = initialize_model( + train_x_nei, + train_obj_nei, + train_con_nei, + model_nei.state_dict(), + ) + + t1 = time.monotonic() + + if verbose: + print( + f"\nBatch {iteration:>2}: best_value (random, qEI, qNEI) = " + f"({max(best_random):>4.2f}, {best_value_ei:>4.2f}, {best_value_nei:>4.2f}), " + f"time = {t1-t0:>4.2f}.", + end="", + ) + else: + print(".", end="") + + best_observed_all_ei.append(best_observed_ei) + best_observed_all_nei.append(best_observed_nei) + best_random_all.append(best_random) + + +# #### Plot the results +# The plot below shows the best objective value observed at each step of the optimization for each of the algorithms. The confidence intervals represent the variance at that step in the optimization across the trial runs. The variance across optimization runs is quite high, so in order to get a better estimate of the average performance one would have to run a much larger number of trials `N_TRIALS` (we avoid this here to limit the runtime of this tutorial). + +# In[20]: + + +import numpy as np +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +def ci(y): + return 1.96 * y.std(axis=0) / np.sqrt(N_TRIALS) + + +GLOBAL_MAXIMUM = neg_hartmann6.optimal_value + + +iters = np.arange(N_BATCH + 1) * BATCH_SIZE +y_ei = np.asarray(best_observed_all_ei) +y_nei = np.asarray(best_observed_all_nei) +y_rnd = np.asarray(best_random_all) + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +ax.errorbar(iters, y_rnd.mean(axis=0), yerr=ci(y_rnd), label="random", linewidth=1.5) +ax.errorbar(iters, y_ei.mean(axis=0), yerr=ci(y_ei), label="qLogEI", linewidth=1.5) +ax.errorbar(iters, y_nei.mean(axis=0), yerr=ci(y_nei), label="qLogNEI", linewidth=1.5) +plt.plot( + [0, N_BATCH * BATCH_SIZE], + [GLOBAL_MAXIMUM] * 2, + "k", + label="true best feasible objective", + linewidth=2, +) +ax.set_ylim(bottom=0.5) +ax.set( + xlabel="number of observations (beyond initial points)", + ylabel="best objective value", +) +ax.legend(loc="lower right") + diff --git a/website-old/static/files/compare_mc_analytic_acquisition.ipynb b/website-old/static/files/compare_mc_analytic_acquisition.ipynb new file mode 100644 index 0000000000..c61f34689c --- /dev/null +++ b/website-old/static/files/compare_mc_analytic_acquisition.ipynb @@ -0,0 +1,439 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "7b4aef48-3c3f-4bec-8906-2844d31556ba", + "showInput": false + }, + "source": [ + "## Analytic and MC-based Expected Improvement (EI) acquisition\n", + "\n", + "In this tutorial, we compare the analytic and MC-based EI acquisition functions and show both `scipy`- and `torch`-based optimizers for optimizing the acquisition. This tutorial highlights the modularity of botorch and the ability to easily try different acquisition functions and accompanying optimization algorithms on the same fitted model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6d70e250-da2b-460a-8989-5c048f7a6ce8", + "showInput": false + }, + "source": [ + "### Comparison of analytic and MC-based EI\n", + "Note that we use the analytic and MC variants of the LogEI family of acquisition functions, which remedy numerical issues encountered in the naive implementations. See https://arxiv.org/pdf/2310.20708 for more details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649205799, + "executionStopTime": 1668649205822, + "originalKey": "f678d607-be4c-4f37-aed5-3597158432ce", + "output": { + "id": 8143993305683446, + "loadingStatus": "loaded" + }, + "requestMsgId": "0aae9d3f-d796-4a18-a4aa-b015b5b582ac" + }, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.test_functions import Hartmann\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "neg_hartmann6 = Hartmann(dim=6, negate=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "332a3aeb-187f-4265-b52f-4ca7bdcf5e4a", + "showInput": false + }, + "source": [ + "First, we generate some random data and fit a SingleTaskGP for a 6-dimensional synthetic test function 'Hartmann6'." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649205895, + "executionStopTime": 1668649206067, + "originalKey": "a7724f86-8b67-4f70-bf57-f0da79b88f52", + "output": { + "id": 1605553740344114, + "loadingStatus": "loaded" + }, + "requestMsgId": "25794582-0506-4e89-a112-ba362b7c7e59" + }, + "outputs": [], + "source": [ + "torch.manual_seed(seed=12345) # to keep the data conditions the same\n", + "dtype = torch.float64\n", + "train_x = torch.rand(10, 6, dtype=dtype)\n", + "train_obj = neg_hartmann6(train_x).unsqueeze(-1)\n", + "model = SingleTaskGP(train_X=train_x, train_Y=train_obj)\n", + "mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "fit_gpytorch_mll(mll);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "576d19e8-2164-4ae9-8416-4ca205d35a77", + "showInput": false + }, + "source": [ + "Initialize an analytic EI acquisition function on the fitted model.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649206124, + "executionStopTime": 1668649206138, + "originalKey": "1d611db5-29ee-4e9b-8fea-fe9d274b5222", + "requestMsgId": "ba6f0917-f622-486a-818c-bb9ba4580ea5" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.analytic import LogExpectedImprovement\n", + "\n", + "best_value = train_obj.max()\n", + "LogEI = LogExpectedImprovement(model=model, best_f=best_value)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e86567ac-e3f1-46ec-889a-6cc21f2fc918", + "showInput": false + }, + "source": [ + "Next, we optimize the analytic EI acquisition function using 50 random restarts chosen from 100 initial raw samples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649206218, + "executionStopTime": 1668649206938, + "originalKey": "dc5613c6-2f99-4193-8956-6e710fee5fa2", + "output": { + "id": 422599616946465, + "loadingStatus": "loaded" + }, + "requestMsgId": "3df2fc12-7f4c-4abb-b1d2-90bb3b8bf05c" + }, + "outputs": [], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "new_point_analytic, _ = optimize_acqf(\n", + " acq_function=LogEI,\n", + " bounds=torch.tensor([[0.0] * 6, [1.0] * 6]),\n", + " q=1,\n", + " num_restarts=20,\n", + " raw_samples=100,\n", + " options={},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649206992, + "executionStopTime": 1668649207011, + "originalKey": "76fb19a3-c2c2-451a-8c0b-50cb14c55460", + "requestMsgId": "a5cbada9-0b7c-41a2-934f-10d9bbe2e316" + }, + "outputs": [], + "source": [ + "# NOTE: The acquisition value here is the log of the expected improvement.\n", + "LogEI(new_point_analytic), new_point_analytic" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "99862767-916b-4de0-881a-2eafc819f356", + "showInput": false + }, + "source": [ + "Now, let's swap out the analytic acquisition function and replace it with an MC version. Note that we are in the `q = 1` case; for `q > 1`, an analytic version does not exist." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649207083, + "executionStopTime": 1668649207929, + "originalKey": "aaf04cba-3716-4fbd-8baa-2c75dd068860", + "output": { + "id": 495747073400348, + "loadingStatus": "loaded" + }, + "requestMsgId": "0e7691f2-34c7-43df-a247-7f7ba95220f1" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.logei import qLogExpectedImprovement\n", + "from botorch.sampling import SobolQMCNormalSampler\n", + "\n", + "\n", + "sampler = SobolQMCNormalSampler(sample_shape=torch.Size([512]), seed=0)\n", + "MC_LogEI = qLogExpectedImprovement(model, best_f=best_value, sampler=sampler, fat=False)\n", + "torch.manual_seed(seed=0) # to keep the restart conditions the same\n", + "new_point_mc, _ = optimize_acqf(\n", + " acq_function=MC_LogEI,\n", + " bounds=torch.tensor([[0.0] * 6, [1.0] * 6]),\n", + " q=1,\n", + " num_restarts=20,\n", + " raw_samples=100,\n", + " options={},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649207976, + "executionStopTime": 1668649207989, + "originalKey": "73ffa9ea-3cff-46eb-91ea-b2f75fdb07f2", + "requestMsgId": "b780cff4-6e90-4e39-8558-b04136e71e94" + }, + "outputs": [], + "source": [ + "# NOTE: The acquisition value here is the log of the expected improvement.\n", + "MC_LogEI(new_point_mc), new_point_mc" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "41f37b1a-df0a-4bb9-8996-fdddd3fe298c", + "showInput": false + }, + "source": [ + "Check that the two generated points are close." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649208035, + "executionStopTime": 1668649208043, + "originalKey": "c5c20ba9-82af-4d07-832f-86ede74f8959", + "requestMsgId": "0b3db1ad-6ddb-4f86-9767-e0a486914b33" + }, + "outputs": [], + "source": [ + "torch.linalg.norm(new_point_mc - new_point_analytic)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f45c60db-1318-405d-b95a-6b2723bc5f46", + "showInput": false + }, + "source": [ + "### Using a torch optimizer on a stochastic acquisition function\n", + "\n", + "We could also optimize using a `torch` optimizer. This is particularly useful for the case of a stochastic acquisition function, which we can obtain by using a `StochasticSampler`. First, we illustrate the usage of `torch.optim.Adam`. In the code snippet below, `gen_batch_initial_candidates` uses a heuristic to select a set of restart locations, `gen_candidates_torch` is a wrapper to the `torch` optimizer for maximizing the acquisition value, and `get_best_candidates` finds the best result amongst the random restarts.\n", + "\n", + "Under the hood, `gen_candidates_torch` uses a convergence criterion based on exponential moving averages of the loss. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649208095, + "executionStopTime": 1668649208252, + "originalKey": "434e0156-94e1-4aca-a0f6-00f6ffb3a2b0", + "requestMsgId": "3e15f8c7-ee79-4c95-8583-3bd376229a39" + }, + "outputs": [], + "source": [ + "from botorch.sampling.stochastic_samplers import StochasticSampler\n", + "from botorch.generation import get_best_candidates, gen_candidates_torch\n", + "from botorch.optim import gen_batch_initial_conditions\n", + "\n", + "resampler = StochasticSampler(sample_shape=torch.Size([512]))\n", + "MC_LogEI_resample = qLogExpectedImprovement(model, best_f=best_value, sampler=resampler)\n", + "bounds = torch.tensor([[0.0] * 6, [1.0] * 6])\n", + "\n", + "batch_initial_conditions = gen_batch_initial_conditions(\n", + " acq_function=MC_LogEI_resample,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=20,\n", + " raw_samples=100,\n", + ")\n", + "batch_candidates, batch_acq_values = gen_candidates_torch(\n", + " initial_conditions=batch_initial_conditions,\n", + " acquisition_function=MC_LogEI_resample,\n", + " lower_bounds=bounds[0],\n", + " upper_bounds=bounds[1],\n", + " optimizer=torch.optim.Adam,\n", + " options={\"maxiter\": 500},\n", + ")\n", + "new_point_torch_Adam = get_best_candidates(\n", + " batch_candidates=batch_candidates, batch_values=batch_acq_values\n", + ").detach()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649208304, + "executionStopTime": 1668649208320, + "originalKey": "81c29b36-c663-47e1-8155-ad034c214f53", + "requestMsgId": "aac6f703-e046-448a-8abe-1742befb9bf9" + }, + "outputs": [], + "source": [ + "# NOTE: The acquisition value here is the log of the expected improvement.\n", + "MC_LogEI_resample(new_point_torch_Adam), new_point_torch_Adam" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649208364, + "executionStopTime": 1668649208372, + "originalKey": "17fb0de0-3c5a-414e-9aba-b82710d166c0", + "requestMsgId": "a13ce358-3ee6-43ad-9e3a-16181a8cdc1e" + }, + "outputs": [], + "source": [ + "torch.linalg.norm(new_point_torch_Adam - new_point_analytic)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "9cce59c6-18f5-4d61-ace0-a0d88cd4b8eb", + "showInput": false + }, + "source": [ + "By changing the `optimizer` parameter to `gen_candidates_torch`, we can also try `torch.optim.SGD`. Note that without the adaptive step size selection of Adam, basic SGD does worse job at optimizing without further manual tuning of the optimization parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649208422, + "executionStopTime": 1668649208460, + "originalKey": "dce34cee-fe31-476e-a20d-46237d5861e0", + "requestMsgId": "47a5a0d7-3281-403d-910a-97054021b811" + }, + "outputs": [], + "source": [ + "batch_candidates, batch_acq_values = gen_candidates_torch(\n", + " initial_conditions=batch_initial_conditions,\n", + " acquisition_function=MC_LogEI_resample,\n", + " lower_bounds=bounds[0],\n", + " upper_bounds=bounds[1],\n", + " optimizer=torch.optim.SGD,\n", + " options={\"maxiter\": 500},\n", + ")\n", + "new_point_torch_SGD = get_best_candidates(\n", + " batch_candidates=batch_candidates, batch_values=batch_acq_values\n", + ").detach()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649208505, + "executionStopTime": 1668649208523, + "originalKey": "350e456d-0d1c-46dc-a618-0fbba9e0a158", + "requestMsgId": "aa33d42e-c526-4117-88a7-aa3034d82886" + }, + "outputs": [], + "source": [ + "MC_LogEI_resample(new_point_torch_SGD), new_point_torch_SGD" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668649208566, + "executionStopTime": 1668649208574, + "originalKey": "e263cfc7-47a0-4b81-ab33-3aa16320c87e", + "requestMsgId": "3c654fc0-ce64-43c7-a8bf-42935257008a" + }, + "outputs": [], + "source": [ + "torch.linalg.norm(new_point_torch_SGD - new_point_analytic)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/compare_mc_analytic_acquisition.py b/website-old/static/files/compare_mc_analytic_acquisition.py new file mode 100644 index 0000000000..261af4b72d --- /dev/null +++ b/website-old/static/files/compare_mc_analytic_acquisition.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Analytic and MC-based Expected Improvement (EI) acquisition +# +# In this tutorial, we compare the analytic and MC-based EI acquisition functions and show both `scipy`- and `torch`-based optimizers for optimizing the acquisition. This tutorial highlights the modularity of botorch and the ability to easily try different acquisition functions and accompanying optimization algorithms on the same fitted model. + +# ### Comparison of analytic and MC-based EI +# Note that we use the analytic and MC variants of the LogEI family of acquisition functions, which remedy numerical issues encountered in the naive implementations. See https://arxiv.org/pdf/2310.20708 for more details. + +# In[ ]: + + +import torch + +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.test_functions import Hartmann +from gpytorch.mlls import ExactMarginalLogLikelihood + +neg_hartmann6 = Hartmann(dim=6, negate=True) + + +# First, we generate some random data and fit a SingleTaskGP for a 6-dimensional synthetic test function 'Hartmann6'. + +# In[ ]: + + +torch.manual_seed(seed=12345) # to keep the data conditions the same +dtype = torch.float64 +train_x = torch.rand(10, 6, dtype=dtype) +train_obj = neg_hartmann6(train_x).unsqueeze(-1) +model = SingleTaskGP(train_X=train_x, train_Y=train_obj) +mll = ExactMarginalLogLikelihood(model.likelihood, model) +fit_gpytorch_mll(mll); + + +# Initialize an analytic EI acquisition function on the fitted model. +# + +# In[ ]: + + +from botorch.acquisition.analytic import LogExpectedImprovement + +best_value = train_obj.max() +LogEI = LogExpectedImprovement(model=model, best_f=best_value) + + +# Next, we optimize the analytic EI acquisition function using 50 random restarts chosen from 100 initial raw samples. + +# In[ ]: + + +from botorch.optim import optimize_acqf + +new_point_analytic, _ = optimize_acqf( + acq_function=LogEI, + bounds=torch.tensor([[0.0] * 6, [1.0] * 6]), + q=1, + num_restarts=20, + raw_samples=100, + options={}, +) + + +# In[ ]: + + +# NOTE: The acquisition value here is the log of the expected improvement. +LogEI(new_point_analytic), new_point_analytic + + +# Now, let's swap out the analytic acquisition function and replace it with an MC version. Note that we are in the `q = 1` case; for `q > 1`, an analytic version does not exist. + +# In[ ]: + + +from botorch.acquisition.logei import qLogExpectedImprovement +from botorch.sampling import SobolQMCNormalSampler + + +sampler = SobolQMCNormalSampler(sample_shape=torch.Size([512]), seed=0) +MC_LogEI = qLogExpectedImprovement(model, best_f=best_value, sampler=sampler, fat=False) +torch.manual_seed(seed=0) # to keep the restart conditions the same +new_point_mc, _ = optimize_acqf( + acq_function=MC_LogEI, + bounds=torch.tensor([[0.0] * 6, [1.0] * 6]), + q=1, + num_restarts=20, + raw_samples=100, + options={}, +) + + +# In[ ]: + + +# NOTE: The acquisition value here is the log of the expected improvement. +MC_LogEI(new_point_mc), new_point_mc + + +# Check that the two generated points are close. + +# In[ ]: + + +torch.linalg.norm(new_point_mc - new_point_analytic) + + +# ### Using a torch optimizer on a stochastic acquisition function +# +# We could also optimize using a `torch` optimizer. This is particularly useful for the case of a stochastic acquisition function, which we can obtain by using a `StochasticSampler`. First, we illustrate the usage of `torch.optim.Adam`. In the code snippet below, `gen_batch_initial_candidates` uses a heuristic to select a set of restart locations, `gen_candidates_torch` is a wrapper to the `torch` optimizer for maximizing the acquisition value, and `get_best_candidates` finds the best result amongst the random restarts. +# +# Under the hood, `gen_candidates_torch` uses a convergence criterion based on exponential moving averages of the loss. + +# In[ ]: + + +from botorch.sampling.stochastic_samplers import StochasticSampler +from botorch.generation import get_best_candidates, gen_candidates_torch +from botorch.optim import gen_batch_initial_conditions + +resampler = StochasticSampler(sample_shape=torch.Size([512])) +MC_LogEI_resample = qLogExpectedImprovement(model, best_f=best_value, sampler=resampler) +bounds = torch.tensor([[0.0] * 6, [1.0] * 6]) + +batch_initial_conditions = gen_batch_initial_conditions( + acq_function=MC_LogEI_resample, + bounds=bounds, + q=1, + num_restarts=20, + raw_samples=100, +) +batch_candidates, batch_acq_values = gen_candidates_torch( + initial_conditions=batch_initial_conditions, + acquisition_function=MC_LogEI_resample, + lower_bounds=bounds[0], + upper_bounds=bounds[1], + optimizer=torch.optim.Adam, + options={"maxiter": 500}, +) +new_point_torch_Adam = get_best_candidates( + batch_candidates=batch_candidates, batch_values=batch_acq_values +).detach() + + +# In[ ]: + + +# NOTE: The acquisition value here is the log of the expected improvement. +MC_LogEI_resample(new_point_torch_Adam), new_point_torch_Adam + + +# In[ ]: + + +torch.linalg.norm(new_point_torch_Adam - new_point_analytic) + + +# By changing the `optimizer` parameter to `gen_candidates_torch`, we can also try `torch.optim.SGD`. Note that without the adaptive step size selection of Adam, basic SGD does worse job at optimizing without further manual tuning of the optimization parameters. + +# In[ ]: + + +batch_candidates, batch_acq_values = gen_candidates_torch( + initial_conditions=batch_initial_conditions, + acquisition_function=MC_LogEI_resample, + lower_bounds=bounds[0], + upper_bounds=bounds[1], + optimizer=torch.optim.SGD, + options={"maxiter": 500}, +) +new_point_torch_SGD = get_best_candidates( + batch_candidates=batch_candidates, batch_values=batch_acq_values +).detach() + + +# In[ ]: + + +MC_LogEI_resample(new_point_torch_SGD), new_point_torch_SGD + + +# In[ ]: + + +torch.linalg.norm(new_point_torch_SGD - new_point_analytic) + diff --git a/website-old/static/files/composite_bo_with_hogp.ipynb b/website-old/static/files/composite_bo_with_hogp.ipynb new file mode 100644 index 0000000000..2497b4b864 --- /dev/null +++ b/website-old/static/files/composite_bo_with_hogp.ipynb @@ -0,0 +1,606 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8335acc8-138f-4d2d-b65e-73b26f81f37b", + "showInput": false + }, + "source": [ + "## Composite Bayesian Optimization with the High Order Gaussian Process" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "0456cc35-2d2b-4861-a97e-220b06664757", + "showInput": false + }, + "source": [ + "In this tutorial, we're going to explore composite Bayesian optimization [Astudillo & Frazier, ICML, '19](https://proceedings.mlr.press/v97/astudillo19a.html) with the High Order Gaussian Process (HOGP) model of [Zhe et al, AISTATS, '19](http://proceedings.mlr.press/v89/zhe19a.html). The setup for composite Bayesian optimization is that we have an unknown (black box) function mapping input parameters to several outputs, and a second, known function describing the quality of the functional output. We wish to find input parameters that maximize the output metric function. We wish to find input parameters that maximize the output metric function in a black-box manner. \n", + "\n", + "Specifically, this can be described as $\\max_{x \\in \\mathcal{X}} g(f(x)),$ where $f$ is unknown and $g$ is known. As in traditional Bayesian optimization, we are going to construct a Gaussian process surrogate model over the expensive to evaluate function $f(.),$ and will use a HOGP to model this function. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e9d6372a-3961-453d-a2fc-6ed7b649c76c", + "showInput": false + }, + "source": [ + "### HOGP model description\n", + "\n", + "The [High Order Gaussian Process (HOGP) model](https://proceedings.mlr.press/v89/zhe19a.html) is a Gaussian process model designed specifically to operate over tensors or multi-dimensional arrays and exploits structure in the tensor to be able to operate efficiently. Specifically, the HOGP takes as inputs $y \\in \\mathbb{R}^{N \\times d_2 \\times \\cdots \\times d_M}$ and assumes that $\\text{vec}(y) \\sim \\mathcal{N}(0, \\otimes_{i=1}^M K_i + \\sigma^2 I),$ where $K_1 = K_{XX}.$ Each dimension of the tensor has its own kernel function, $K_i,$ as well as a set of $d_i$ latent parameters that can be optimized over.\n", + "\n", + "Recently, [Maddox et al, '21](https://arxiv.org/abs/2106.12997) proposed a method for computing posterior samples from the HOGP by exploiting structure in the posterior distribution, thereby enabling its usage in BO settings. While they show that this approach allows to use composite BO on problems with tens or thousands of outputs, for scalability we consider a much smaller example here (that does not require GPU acceleration)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "code_folding": [], + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179090663, + "executionStopTime": 1677179090688, + "hidden_ranges": [], + "originalKey": "20d1001e-522e-48af-8998-4822f573fffc", + "requestMsgId": "5ce0b166-4ebe-4114-a365-22c94e141254" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "I0215 081151.431 _utils_internal.py:247] NCCL_DEBUG env var is set to None\n", + "I0215 081151.436 _utils_internal.py:265] NCCL_DEBUG is forced to WARN from None\n" + ] + } + ], + "source": [ + "import math\n", + "import os\n", + "import time\n", + "from functools import partial\n", + "\n", + "import gpytorch.settings as gpt_settings\n", + "import matplotlib.pyplot as plt\n", + "import torch\n", + "from botorch.acquisition import qExpectedImprovement\n", + "from botorch.acquisition.objective import GenericMCObjective\n", + "from botorch.models import HigherOrderGP, SingleTaskGP\n", + "from botorch.models.higher_order_gp import FlattenedStandardize\n", + "from botorch.models.transforms import Normalize, Standardize\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.optim.fit import fit_gpytorch_mll_torch\n", + "from botorch.sampling.normal import IIDNormalSampler\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "from linear_operator.settings import _fast_solves\n", + "from torch.optim import Adam\n", + "\n", + "%matplotlib inline\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "c90bc749-758f-49cd-bfb3-970a921c66da", + "showInput": false + }, + "source": [ + "#### Set Device and dtype" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "code_folding": [], + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179090741, + "executionStopTime": 1677179090768, + "hidden_ranges": [], + "originalKey": "70a937d9-4f9a-4403-9b38-ecbb78afc8b3", + "output": { + "id": 1095338551592181, + "loadingStatus": "loaded" + }, + "requestMsgId": "d9112d83-9dac-43c6-9150-c383a077d0eb" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using cpu\n" + ] + } + ], + "source": [ + "torch.manual_seed(0)\n", + "device = (\n", + " torch.device(\"cpu\") if not torch.cuda.is_available() else torch.device(\"cuda:4\")\n", + ")\n", + "dtype = torch.float\n", + "\n", + "print(\"Using \", device)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179090831, + "executionStopTime": 1677179090871, + "originalKey": "6c944fa0-b00c-47e5-8634-33de1718e10c", + "requestMsgId": "3adfd02b-311c-4f6d-b45f-4909c8051ca5" + }, + "outputs": [], + "source": [ + "models_used = (\n", + " \"rnd\",\n", + " \"ei\",\n", + " \"ei_hogp_cf\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "720c695f-d688-4b80-a2b1-f15db6e3c42a", + "showInput": false + }, + "source": [ + "### Problem Description" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "24c31ede-5f02-4b48-8513-10572e33ae78", + "showInput": false + }, + "source": [ + "We use a simple test problem describing the concentration of pollutants after a chemical spill from [Astudillo & Frazier, ICML, '19](https://proceedings.mlr.press/v97/astudillo19a.html) defined over a $3 \\times 4$ grid of values $s,t$ and we wish to optimize the parameters w.r.t. their true values, to estimate the true value of parameters, $x = [M, D, L, \\tau].$ The function is given by \n", + "$$ f(s,t | M, D, L, \\tau) := \\frac{M}{\\sqrt{4 \\pi D t}} \\exp\\{-\\frac{s^2}{4Dt}\\} + \\frac{1_{t > \\tau} M}{\\sqrt{4 \\pi D(t - \\tau)}} \\exp\\{- \\frac{(s - L)^2}{4 D (t - \\tau)}\\}, $$\n", + "with the cheap to evaluate, differentiable function given by $g(y):= \\sum_{(s,t) \\in S \\times T} \\left(c(s, t|x_{\\text{true}}) - y\\right)^2.$ As the objective function itself is going to be implemented in Pytorch, we will be able to differentiate through it, enabling the usage of gradient-based optimization to optimize the objectives with respect to the inputs." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179090923, + "executionStopTime": 1677179090952, + "originalKey": "b583c681-6d77-478c-8dbc-eb9882b85a60", + "requestMsgId": "55885ad5-6197-47b4-8e07-ba464e79f0e0" + }, + "outputs": [], + "source": [ + "def env_cfun(s, t, M, D, L, tau):\n", + " c1 = M / torch.sqrt(4 * math.pi * D * t)\n", + " exp1 = torch.exp(-(s**2) / 4 / D / t)\n", + " term1 = c1 * exp1\n", + " c2 = M / torch.sqrt(4 * math.pi * D * (t - tau))\n", + " exp2 = torch.exp(-((s - L) ** 2) / 4 / D / (t - tau))\n", + " term2 = c2 * exp2\n", + " term2[torch.isnan(term2)] = 0.0\n", + " return term1 + term2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "cd0586f4-a16a-4a4a-b82a-37d23b441c6d", + "showInput": false + }, + "source": [ + "#### Helper Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "412c92c8-ddf6-4adb-b2e3-86196fea6898", + "showInput": false + }, + "source": [ + "These are helper functions for us to maximize the acquisition function and to get random points." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179091007, + "executionStopTime": 1677179091039, + "originalKey": "094f8380-7f4f-48f7-9979-948275a35259", + "requestMsgId": "c7427d19-8e33-4162-99dc-bda85afc6e37" + }, + "outputs": [], + "source": [ + "def gen_rand_points(bounds, num_samples):\n", + " points_nlzd = torch.rand(num_samples, bounds.shape[-1]).to(bounds)\n", + " return bounds[0] + (bounds[1] - bounds[0]) * points_nlzd\n", + "\n", + "\n", + "def optimize_ei(qEI, bounds, **options):\n", + " cands_nlzd, _ = optimize_acqf(qEI, bounds, **options)\n", + " return cands_nlzd" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f0def79f-54e0-4141-801b-2aaa65cb48e3", + "showInput": false + }, + "source": [ + "Below is a wrapped function to help us define bounds on the parameter space, we can also vary the size of the grid if we'd like to." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179091093, + "executionStopTime": 1677179091118, + "originalKey": "637c4435-2df3-49b4-a732-26a24a651610", + "requestMsgId": "4a2cc65a-e1a2-4ddd-ab9e-b038c7052917" + }, + "outputs": [], + "source": [ + "def prepare_data(s_size=3, t_size=4, device=device, dtype=dtype):\n", + " print(\"---- Running the environmental problem with \", s_size, t_size, \" ----\")\n", + " # X = [M, D, L, tau]\n", + " bounds = torch.tensor(\n", + " [[7.0, 0.02, 0.01, 30.010], [13.0, 0.12, 3.00, 30.295]],\n", + " device=device,\n", + " dtype=dtype,\n", + " )\n", + "\n", + " M0 = torch.tensor(10.0, device=device, dtype=dtype)\n", + " D0 = torch.tensor(0.07, device=device, dtype=dtype)\n", + " L0 = torch.tensor(1.505, device=device, dtype=dtype)\n", + " tau0 = torch.tensor(30.1525, device=device, dtype=dtype)\n", + "\n", + " # we can vectorize everything, no need for loops\n", + " if s_size == 3:\n", + " S = torch.tensor([0.0, 1.0, 2.5], device=device, dtype=dtype)\n", + " else:\n", + " S = torch.linspace(0.0, 2.5, s_size, device=device, dtype=dtype)\n", + " if t_size == 4:\n", + " T = torch.tensor([15.0, 30.0, 45.0, 60.0], device=device, dtype=dtype)\n", + " else:\n", + " T = torch.linspace(15.0, 60.0, t_size, device=device, dtype=dtype)\n", + "\n", + " Sgrid, Tgrid = torch.meshgrid(S, T)\n", + "\n", + " # X = [M, D, L, tau]\n", + " def c_batched(X, k=None):\n", + " return torch.stack([env_cfun(Sgrid, Tgrid, *x) for x in X])\n", + "\n", + " c_true = env_cfun(Sgrid, Tgrid, M0, D0, L0, tau0)\n", + "\n", + " def neq_sum_quared_diff(samples, X=None):\n", + " # unsqueeze\n", + " if samples.shape[-1] == (s_size * t_size):\n", + " samples = samples.unsqueeze(-1).reshape(*samples.shape[:-1], s_size, t_size)\n", + "\n", + " sq_diffs = (samples - c_true).pow(2)\n", + " return sq_diffs.sum(dim=(-1, -2)).mul(-1.0)\n", + "\n", + " objective = GenericMCObjective(neq_sum_quared_diff)\n", + " num_samples = 32\n", + "\n", + " return c_batched, objective, bounds, num_samples" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e0284996-a67c-471c-a0bc-3472e6ac8459", + "showInput": false + }, + "source": [ + "In the above, we construct a `GenericMCObjective` instance to codify the objective function (which is minimizing the MSE of the output tensors and the outputs corresponding to the \"true\" parameter values). Note that the objective function is encoded in PyTorch and is differentiable (although it technically doesn't have to be). Ultimately, we backpropagate through the objective with respect to the input parameters (and through the HOGP as well)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "1471cbeb-efdd-409b-9be0-1e360fef4787", + "showInput": false + }, + "source": [ + "## BO Loop" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "3be94f6f-32a8-4caa-ae48-93580c14584e", + "showInput": false + }, + "source": [ + "Finally, we run the BO loop for 10 iterations, generating 3 candidates in each iteration. This loop might take a while.\n", + "\n", + "We will be comparing to both random selection and batch expected improvement on the aggregated metric." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179091164, + "executionStopTime": 1677179091199, + "originalKey": "40ee89df-eff3-4e04-af14-e3d506216ed7", + "requestMsgId": "8d5ff502-7e12-4dfd-a9cb-e6d4ba4d5b48" + }, + "outputs": [], + "source": [ + "n_init = 20\n", + "\n", + "if SMOKE_TEST:\n", + " n_batches = 1\n", + " batch_size = 2\n", + "else:\n", + " n_batches = 10\n", + " batch_size = 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ac61b7da-7376-41fe-aa89-b1fed1b1c1ee", + "showInput": false + }, + "source": [ + "As a word of caution, we've found that when fitting the HOGP model, using first-order optimizers (e.g. Adam) as is used in `fit_gpytorch_torch` tends to outperform second-order optimizers such as L-BFGS-B due to the large number of free parameters in the HOGP. L-BFGS-B tends to overfit in practice here." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "code_folding": [], + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179091279, + "executionStopTime": 1677179582488, + "hidden_ranges": [], + "originalKey": "fe12e9f1-f21a-48dd-8cc6-ba095a75ce80", + "output": { + "id": 1423656321879857, + "loadingStatus": "loaded" + }, + "requestMsgId": "288b42d0-90e2-474a-9dff-79c17999cf8c" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wall time: 12.043408\n", + "rnd: -0.071It 10/10, best obs.: , ei: -0.089It 10/10, best obs.: , ei_hogp_cf: -0.000\n", + "Wall time: 12.193747\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[W 240215 08:15:58 initializers:432] Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n" + ] + } + ], + "source": [ + "with gpt_settings.cholesky_jitter(1e-4):\n", + " c_batched, objective, bounds, num_samples = prepare_data(device=device, dtype=dtype)\n", + "\n", + " train_X_init = gen_rand_points(bounds, n_init)\n", + " train_Y_init = c_batched(train_X_init)\n", + "\n", + " # these will keep track of the points explored\n", + " train_X = {k: train_X_init.clone() for k in models_used}\n", + " train_Y = {k: train_Y_init.clone() for k in train_X}\n", + "\n", + " # run the BO loop\n", + " for i in range(n_batches):\n", + " tic = time.monotonic()\n", + "\n", + " # get best observations, log status\n", + " best_f = {k: objective(v).max().detach() for k, v in train_Y.items()}\n", + "\n", + " print(\n", + " f\"It {i+1:>2}/{n_batches}, best obs.: \"\n", + " \", \".join([f\"{k}: {v:.3f}\" for k, v in best_f.items()])\n", + " )\n", + "\n", + " # generate random candidates\n", + " cands = {}\n", + " cands[\"rnd\"] = gen_rand_points(bounds, batch_size)\n", + "\n", + " optimize_acqf_kwargs = {\n", + " \"q\": batch_size,\n", + " \"num_restarts\": 10,\n", + " \"raw_samples\": 512,\n", + " }\n", + " sampler = IIDNormalSampler(sample_shape=torch.Size([128]))\n", + "\n", + " train_Y_ei = objective(train_Y[\"ei\"]).unsqueeze(-1)\n", + " model_ei = SingleTaskGP(\n", + " train_X[\"ei\"],\n", + " train_Y_ei,\n", + " input_transform=Normalize(train_X[\"ei\"].shape[-1]),\n", + " outcome_transform=Standardize(train_Y_ei.shape[-1]),\n", + " )\n", + "\n", + " mll = ExactMarginalLogLikelihood(model_ei.likelihood, model_ei)\n", + " fit_gpytorch_mll_torch(mll, step_limit=1000, optimizer=partial(Adam, lr=0.01))\n", + "\n", + " # generate qEI candidate (single output modeling)\n", + " qEI = qExpectedImprovement(model_ei, best_f=best_f[\"ei\"], sampler=sampler)\n", + " cands[\"ei\"] = optimize_ei(qEI, bounds, **optimize_acqf_kwargs)\n", + "\n", + " model_ei_hogp_cf = HigherOrderGP(\n", + " train_X[\"ei_hogp_cf\"],\n", + " train_Y[\"ei_hogp_cf\"],\n", + " outcome_transform=FlattenedStandardize(train_Y[\"ei_hogp_cf\"].shape[1:]),\n", + " input_transform=Normalize(train_X[\"ei_hogp_cf\"].shape[-1]),\n", + " latent_init=\"gp\",\n", + " )\n", + "\n", + " mll = ExactMarginalLogLikelihood(model_ei_hogp_cf.likelihood, model_ei_hogp_cf)\n", + " with _fast_solves(True):\n", + " fit_gpytorch_mll_torch(\n", + " mll, step_limit=1000, optimizer=partial(Adam, lr=0.01)\n", + " )\n", + "\n", + " # generate qEI candidate (multi-output modeling)\n", + " qEI_hogp_cf = qExpectedImprovement(\n", + " model_ei_hogp_cf,\n", + " best_f=best_f[\"ei_hogp_cf\"],\n", + " sampler=sampler,\n", + " objective=objective,\n", + " )\n", + " cands[\"ei_hogp_cf\"] = optimize_ei(qEI_hogp_cf, bounds, **optimize_acqf_kwargs)\n", + "\n", + " # make observations and update data\n", + " for k, Xold in train_X.items():\n", + " Xnew = cands[k]\n", + " if Xnew.shape[0] > 0:\n", + " train_X[k] = torch.cat([Xold, Xnew])\n", + " train_Y[k] = torch.cat([train_Y[k], c_batched(Xnew)])\n", + "\n", + " print(f\"Wall time: {time.monotonic() - tic:1f}\")\n", + "\n", + " objective_dict = {k: objective(train_Y[k]) for k in train_Y}" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "code_folding": [], + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179582537, + "executionStopTime": 1677179582560, + "hidden_ranges": [], + "originalKey": "deecf18c-3135-452e-947a-8eb1b627e666", + "requestMsgId": "b1901751-8058-428c-8949-c2dfb0a3a46d" + }, + "outputs": [], + "source": [ + "methods_dict = {k: objective_dict[k].cpu().cummax(0)[0] for k in models_used}\n", + "mean_results = {k: -methods_dict[k][n_init:] for k in models_used}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "c9ef07f3-0f09-4736-8105-36c942073ad2", + "showInput": false + }, + "source": [ + "Finally, we plot the results, showing that the HOGP performs well on this task, and converges to a closer parameter value than a batch GP on the composite metric itself." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "code_folding": [], + "collapsed": false, + "customOutput": null, + "executionStartTime": 1677179582640, + "executionStopTime": 1677179583231, + "hidden_ranges": [], + "originalKey": "f2585125-5d18-40ed-b736-3e4d4ed83947", + "output": { + "id": 1440956203184434, + "loadingStatus": "loaded" + }, + "requestMsgId": "df3421e5-3a8a-462a-9249-1a8ccf2e0576" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'Difference from True Parameter')" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(8, 6))\n", + "labels_dict = {\"rnd\": \"Random\", \"ei\": \"EI\", \"ei_hogp_cf\": \"Composite EI\"}\n", + "for k in models_used:\n", + " plt.plot(\n", + " torch.arange(n_batches * batch_size),\n", + " mean_results[k],\n", + " label=labels_dict[k],\n", + " )\n", + "plt.legend(fontsize=20)\n", + "plt.semilogy()\n", + "plt.xlabel(\"Number of Function Queries\")\n", + "plt.ylabel(\"Difference from True Parameter\")" + ] + } + ], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "fileHeader": "", + "fileUid": "36246e90-ad68-46dd-9118-059d8dd78fb3", + "isAdHoc": false, + "kernelspec": { + "cinder_runtime": true, + "display_name": "python3", + "ipyflow_runtime": false, + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "indentAmount": 2, + "kernelspec": { + "name": "python3", + "display_name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/composite_bo_with_hogp.py b/website-old/static/files/composite_bo_with_hogp.py new file mode 100644 index 0000000000..c32d034999 --- /dev/null +++ b/website-old/static/files/composite_bo_with_hogp.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Composite Bayesian Optimization with the High Order Gaussian Process + +# In this tutorial, we're going to explore composite Bayesian optimization [Astudillo & Frazier, ICML, '19](https://proceedings.mlr.press/v97/astudillo19a.html) with the High Order Gaussian Process (HOGP) model of [Zhe et al, AISTATS, '19](http://proceedings.mlr.press/v89/zhe19a.html). The setup for composite Bayesian optimization is that we have an unknown (black box) function mapping input parameters to several outputs, and a second, known function describing the quality of the functional output. We wish to find input parameters that maximize the output metric function. We wish to find input parameters that maximize the output metric function in a black-box manner. +# +# Specifically, this can be described as $\max_{x \in \mathcal{X}} g(f(x)),$ where $f$ is unknown and $g$ is known. As in traditional Bayesian optimization, we are going to construct a Gaussian process surrogate model over the expensive to evaluate function $f(.),$ and will use a HOGP to model this function. + +# ### HOGP model description +# +# The [High Order Gaussian Process (HOGP) model](https://proceedings.mlr.press/v89/zhe19a.html) is a Gaussian process model designed specifically to operate over tensors or multi-dimensional arrays and exploits structure in the tensor to be able to operate efficiently. Specifically, the HOGP takes as inputs $y \in \mathbb{R}^{N \times d_2 \times \cdots \times d_M}$ and assumes that $\text{vec}(y) \sim \mathcal{N}(0, \otimes_{i=1}^M K_i + \sigma^2 I),$ where $K_1 = K_{XX}.$ Each dimension of the tensor has its own kernel function, $K_i,$ as well as a set of $d_i$ latent parameters that can be optimized over. +# +# Recently, [Maddox et al, '21](https://arxiv.org/abs/2106.12997) proposed a method for computing posterior samples from the HOGP by exploiting structure in the posterior distribution, thereby enabling its usage in BO settings. While they show that this approach allows to use composite BO on problems with tens or thousands of outputs, for scalability we consider a much smaller example here (that does not require GPU acceleration). + +# In[1]: + + +import math +import os +import time +from functools import partial + +import gpytorch.settings as gpt_settings +import matplotlib.pyplot as plt +import torch +from botorch.acquisition import qExpectedImprovement +from botorch.acquisition.objective import GenericMCObjective +from botorch.models import HigherOrderGP, SingleTaskGP +from botorch.models.higher_order_gp import FlattenedStandardize +from botorch.models.transforms import Normalize, Standardize +from botorch.optim import optimize_acqf +from botorch.optim.fit import fit_gpytorch_mll_torch +from botorch.sampling.normal import IIDNormalSampler +from gpytorch.mlls import ExactMarginalLogLikelihood +from linear_operator.settings import _fast_solves +from torch.optim import Adam + +get_ipython().run_line_magic('matplotlib', 'inline') + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# #### Set Device and dtype + +# In[2]: + + +torch.manual_seed(0) +device = ( + torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:4") +) +dtype = torch.float + +print("Using ", device) + + +# In[3]: + + +models_used = ( + "rnd", + "ei", + "ei_hogp_cf", +) + + +# ### Problem Description + +# We use a simple test problem describing the concentration of pollutants after a chemical spill from [Astudillo & Frazier, ICML, '19](https://proceedings.mlr.press/v97/astudillo19a.html) defined over a $3 \times 4$ grid of values $s,t$ and we wish to optimize the parameters w.r.t. their true values, to estimate the true value of parameters, $x = [M, D, L, \tau].$ The function is given by +# $$ f(s,t | M, D, L, \tau) := \frac{M}{\sqrt{4 \pi D t}} \exp\{-\frac{s^2}{4Dt}\} + \frac{1_{t > \tau} M}{\sqrt{4 \pi D(t - \tau)}} \exp\{- \frac{(s - L)^2}{4 D (t - \tau)}\}, $$ +# with the cheap to evaluate, differentiable function given by $g(y):= \sum_{(s,t) \in S \times T} \left(c(s, t|x_{\text{true}}) - y\right)^2.$ As the objective function itself is going to be implemented in Pytorch, we will be able to differentiate through it, enabling the usage of gradient-based optimization to optimize the objectives with respect to the inputs. + +# In[4]: + + +def env_cfun(s, t, M, D, L, tau): + c1 = M / torch.sqrt(4 * math.pi * D * t) + exp1 = torch.exp(-(s**2) / 4 / D / t) + term1 = c1 * exp1 + c2 = M / torch.sqrt(4 * math.pi * D * (t - tau)) + exp2 = torch.exp(-((s - L) ** 2) / 4 / D / (t - tau)) + term2 = c2 * exp2 + term2[torch.isnan(term2)] = 0.0 + return term1 + term2 + + +# #### Helper Functions + +# These are helper functions for us to maximize the acquisition function and to get random points. + +# In[5]: + + +def gen_rand_points(bounds, num_samples): + points_nlzd = torch.rand(num_samples, bounds.shape[-1]).to(bounds) + return bounds[0] + (bounds[1] - bounds[0]) * points_nlzd + + +def optimize_ei(qEI, bounds, **options): + cands_nlzd, _ = optimize_acqf(qEI, bounds, **options) + return cands_nlzd + + +# Below is a wrapped function to help us define bounds on the parameter space, we can also vary the size of the grid if we'd like to. + +# In[6]: + + +def prepare_data(s_size=3, t_size=4, device=device, dtype=dtype): + print("---- Running the environmental problem with ", s_size, t_size, " ----") + # X = [M, D, L, tau] + bounds = torch.tensor( + [[7.0, 0.02, 0.01, 30.010], [13.0, 0.12, 3.00, 30.295]], + device=device, + dtype=dtype, + ) + + M0 = torch.tensor(10.0, device=device, dtype=dtype) + D0 = torch.tensor(0.07, device=device, dtype=dtype) + L0 = torch.tensor(1.505, device=device, dtype=dtype) + tau0 = torch.tensor(30.1525, device=device, dtype=dtype) + + # we can vectorize everything, no need for loops + if s_size == 3: + S = torch.tensor([0.0, 1.0, 2.5], device=device, dtype=dtype) + else: + S = torch.linspace(0.0, 2.5, s_size, device=device, dtype=dtype) + if t_size == 4: + T = torch.tensor([15.0, 30.0, 45.0, 60.0], device=device, dtype=dtype) + else: + T = torch.linspace(15.0, 60.0, t_size, device=device, dtype=dtype) + + Sgrid, Tgrid = torch.meshgrid(S, T) + + # X = [M, D, L, tau] + def c_batched(X, k=None): + return torch.stack([env_cfun(Sgrid, Tgrid, *x) for x in X]) + + c_true = env_cfun(Sgrid, Tgrid, M0, D0, L0, tau0) + + def neq_sum_quared_diff(samples, X=None): + # unsqueeze + if samples.shape[-1] == (s_size * t_size): + samples = samples.unsqueeze(-1).reshape(*samples.shape[:-1], s_size, t_size) + + sq_diffs = (samples - c_true).pow(2) + return sq_diffs.sum(dim=(-1, -2)).mul(-1.0) + + objective = GenericMCObjective(neq_sum_quared_diff) + num_samples = 32 + + return c_batched, objective, bounds, num_samples + + +# In the above, we construct a `GenericMCObjective` instance to codify the objective function (which is minimizing the MSE of the output tensors and the outputs corresponding to the "true" parameter values). Note that the objective function is encoded in PyTorch and is differentiable (although it technically doesn't have to be). Ultimately, we backpropagate through the objective with respect to the input parameters (and through the HOGP as well). + +# ## BO Loop + +# Finally, we run the BO loop for 10 iterations, generating 3 candidates in each iteration. This loop might take a while. +# +# We will be comparing to both random selection and batch expected improvement on the aggregated metric. + +# In[7]: + + +n_init = 20 + +if SMOKE_TEST: + n_batches = 1 + batch_size = 2 +else: + n_batches = 10 + batch_size = 3 + + +# As a word of caution, we've found that when fitting the HOGP model, using first-order optimizers (e.g. Adam) as is used in `fit_gpytorch_torch` tends to outperform second-order optimizers such as L-BFGS-B due to the large number of free parameters in the HOGP. L-BFGS-B tends to overfit in practice here. + +# In[8]: + + +with gpt_settings.cholesky_jitter(1e-4): + c_batched, objective, bounds, num_samples = prepare_data(device=device, dtype=dtype) + + train_X_init = gen_rand_points(bounds, n_init) + train_Y_init = c_batched(train_X_init) + + # these will keep track of the points explored + train_X = {k: train_X_init.clone() for k in models_used} + train_Y = {k: train_Y_init.clone() for k in train_X} + + # run the BO loop + for i in range(n_batches): + tic = time.monotonic() + + # get best observations, log status + best_f = {k: objective(v).max().detach() for k, v in train_Y.items()} + + print( + f"It {i+1:>2}/{n_batches}, best obs.: " + ", ".join([f"{k}: {v:.3f}" for k, v in best_f.items()]) + ) + + # generate random candidates + cands = {} + cands["rnd"] = gen_rand_points(bounds, batch_size) + + optimize_acqf_kwargs = { + "q": batch_size, + "num_restarts": 10, + "raw_samples": 512, + } + sampler = IIDNormalSampler(sample_shape=torch.Size([128])) + + train_Y_ei = objective(train_Y["ei"]).unsqueeze(-1) + model_ei = SingleTaskGP( + train_X["ei"], + train_Y_ei, + input_transform=Normalize(train_X["ei"].shape[-1]), + outcome_transform=Standardize(train_Y_ei.shape[-1]), + ) + + mll = ExactMarginalLogLikelihood(model_ei.likelihood, model_ei) + fit_gpytorch_mll_torch(mll, step_limit=1000, optimizer=partial(Adam, lr=0.01)) + + # generate qEI candidate (single output modeling) + qEI = qExpectedImprovement(model_ei, best_f=best_f["ei"], sampler=sampler) + cands["ei"] = optimize_ei(qEI, bounds, **optimize_acqf_kwargs) + + model_ei_hogp_cf = HigherOrderGP( + train_X["ei_hogp_cf"], + train_Y["ei_hogp_cf"], + outcome_transform=FlattenedStandardize(train_Y["ei_hogp_cf"].shape[1:]), + input_transform=Normalize(train_X["ei_hogp_cf"].shape[-1]), + latent_init="gp", + ) + + mll = ExactMarginalLogLikelihood(model_ei_hogp_cf.likelihood, model_ei_hogp_cf) + with _fast_solves(True): + fit_gpytorch_mll_torch( + mll, step_limit=1000, optimizer=partial(Adam, lr=0.01) + ) + + # generate qEI candidate (multi-output modeling) + qEI_hogp_cf = qExpectedImprovement( + model_ei_hogp_cf, + best_f=best_f["ei_hogp_cf"], + sampler=sampler, + objective=objective, + ) + cands["ei_hogp_cf"] = optimize_ei(qEI_hogp_cf, bounds, **optimize_acqf_kwargs) + + # make observations and update data + for k, Xold in train_X.items(): + Xnew = cands[k] + if Xnew.shape[0] > 0: + train_X[k] = torch.cat([Xold, Xnew]) + train_Y[k] = torch.cat([train_Y[k], c_batched(Xnew)]) + + print(f"Wall time: {time.monotonic() - tic:1f}") + + objective_dict = {k: objective(train_Y[k]) for k in train_Y} + + +# In[9]: + + +methods_dict = {k: objective_dict[k].cpu().cummax(0)[0] for k in models_used} +mean_results = {k: -methods_dict[k][n_init:] for k in models_used} + + +# Finally, we plot the results, showing that the HOGP performs well on this task, and converges to a closer parameter value than a batch GP on the composite metric itself. + +# In[10]: + + +plt.figure(figsize=(8, 6)) +labels_dict = {"rnd": "Random", "ei": "EI", "ei_hogp_cf": "Composite EI"} +for k in models_used: + plt.plot( + torch.arange(n_batches * batch_size), + mean_results[k], + label=labels_dict[k], + ) +plt.legend(fontsize=20) +plt.semilogy() +plt.xlabel("Number of Function Queries") +plt.ylabel("Difference from True Parameter") + diff --git a/website-old/static/files/composite_mtbo.ipynb b/website-old/static/files/composite_mtbo.ipynb new file mode 100644 index 0000000000..4d3d54a2c8 --- /dev/null +++ b/website-old/static/files/composite_mtbo.ipynb @@ -0,0 +1,503 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "2c421274-e807-4d93-8b0e-d23afdb49a2d", + "showInput": false + }, + "source": [ + "## Composite Bayesian Optimization with Multi-Task Gaussian Processes\n", + "\n", + "In this tutorial, we'll be describing how to perform multi-task Bayesian optimization over composite functions. In these types of problems, there are several related outputs, and an overall easy to evaluate objective function that we wish to maximize.\n", + "\n", + "**Multi-task Bayesian Optimization** was first proposed by [Swersky et al, NeurIPS, '13](https://papers.neurips.cc/paper/2013/hash/f33ba15effa5c10e873bf3842afb46a6-Abstract.html) in the context of fast hyper-parameter tuning for neural network models; however, we demonstrate a more advanced use-case of **[composite Bayesian optimization](https://proceedings.mlr.press/v97/astudillo19a.html)** where the overall function that we wish to optimize is a cheap-to-evaluate (and known) function of the outputs. In general, we expect that using more information about the function should yield improved performance when attempting to optimize it, particularly if the metric function itself is quickly varying.\n", + "\n", + "See [the composite BO tutorial w/ HOGP](https://github.com/pytorch/botorch/blob/main/tutorials/composite_bo_with_hogp.ipynb) for a more technical introduction. In general, we suggest using MTGPs for unstructured task outputs and the HOGP for matrix / tensor structured outputs.\n", + "\n", + "\n", + "We will use a Multi-Task Gaussian process ([MTGP](https://papers.nips.cc/paper/2007/hash/66368270ffd51418ec58bd793f2d9b1b-Abstract.html)) with an ICM kernel to model all of the outputs in this problem. MTGPs can be easily accessed in Botorch via the `botorch.models.KroneckerMultiTaskGP` model class (for the \"block design\" case of fully observed outputs at all inputs). Given $T$ tasks (outputs) and $n$ data points, they assume that the responses, $Y \\sim \\mathbb{R}^{n \\times T},$ are distributed as $\\text{vec}(Y) \\sim \\mathcal{N}(f, D)$ and $f \\sim \\mathcal{GP}(\\mu_{\\theta}, K_{XX} \\otimes K_{T}),$ where $D$ is a (diagonal) noise term." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678932909107, + "executionStopTime": 1678932912073, + "originalKey": "8871a990-f29b-45c2-b378-ac2befef0a1f", + "requestMsgId": "e32e501e-2fe4-4b41-b1fd-4a3cf218833e" + }, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "\n", + "import torch\n", + "from botorch.acquisition.logei import qLogExpectedImprovement\n", + "from botorch.acquisition.objective import GenericMCObjective\n", + "from botorch.models import KroneckerMultiTaskGP\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.sampling.normal import IIDNormalSampler\n", + "\n", + "from botorch.test_functions import Hartmann\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "de394597-4088-4f32-94c7-7c611876eebc", + "showInput": false + }, + "source": [ + "### Set device, dtype and random seed" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678932914757, + "executionStopTime": 1678932914766, + "originalKey": "015a9aab-e3a5-4d09-bd5d-209f9b41cbdb", + "requestMsgId": "00e96c89-1a35-4c00-852c-1f5dda614d9a" + }, + "outputs": [], + "source": [ + "torch.random.manual_seed(10)\n", + "\n", + "tkwargs = {\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\"),\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "f151402e-238e-4c5c-be43-55a73f07664e", + "showInput": false + }, + "source": [ + "### Problem Definition\n", + "\n", + "The function that we wish to optimize is based off of a contextual version of the Hartmann-6 test function, where following [Feng et al, NeurIPS, '20](https://proceedings.neurips.cc/paper/2020/hash/faff959d885ec0ecf70741a846c34d1d-Abstract.html) we convert the sixth task dimension into a task indicator. Here we assume that we evaluate all contexts at once." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678932917176, + "executionStopTime": 1678932917215, + "originalKey": "48fabc12-b1ee-4b88-aa97-9ce4bfe83fd4", + "requestMsgId": "0429d2a2-cc39-4838-9b87-f3eebd49e140" + }, + "outputs": [], + "source": [ + "from torch import Tensor\n", + "\n", + "\n", + "class ContextualHartmann6(Hartmann):\n", + " def __init__(self, num_tasks: int = 20, noise_std=None, negate=False):\n", + " super().__init__(dim=6, noise_std=noise_std, negate=negate)\n", + " self.task_range = torch.linspace(0, 1, num_tasks).unsqueeze(-1)\n", + " self._bounds = [(0.0, 1.0) for _ in range(self.dim - 1)]\n", + " self.bounds = torch.tensor(self._bounds).t()\n", + "\n", + " def evaluate_true(self, X: Tensor) -> Tensor:\n", + " batch_X = X.unsqueeze(-2)\n", + " batch_dims = X.ndim - 1\n", + "\n", + " expanded_task_range = self.task_range\n", + " for _ in range(batch_dims):\n", + " expanded_task_range = expanded_task_range.unsqueeze(0)\n", + " task_range = expanded_task_range.repeat(*X.shape[:-1], 1, 1).to(X)\n", + " concatenated_X = torch.cat(\n", + " (\n", + " batch_X.repeat(*[1] * batch_dims, self.task_range.shape[0], 1),\n", + " task_range,\n", + " ),\n", + " dim=-1,\n", + " )\n", + " return super().evaluate_true(concatenated_X)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "a2fbe3e8-98cf-49e0-9891-8d6009790955", + "showInput": false + }, + "source": [ + "We use `GenericMCObjective` to define the differentiable function that we are optimizing. Here, it is defined as \n", + "$$g(f) = \\sum_{i=1}^T \\cos(f_i^2 + f_i w_i)$$\n", + "where $w$ is a weight vector (drawn randomly once at the start of the optimization). As this function is a non-linear function of the outputs $f,$ we cannot compute acquisition functions via computation of the posterior mean and variance, but rather have to compute posterior samples and evaluate acquisitions with Monte Carlo sampling. \n", + "\n", + "For greater than $10$ or so tasks, it is computationally challenging to sample the posterior over all tasks jointly using conventional approaches, except that [Maddox et al, '21](https://arxiv.org/abs/2106.12997) have devised an efficient method for exploiting the structure in the posterior distribution of the MTGP, enabling efficient MC based optimization of objectives using MTGPs. In this tutorial, we choose 6 contexts/tasks for demostration. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678932920811, + "executionStopTime": 1678932929399, + "originalKey": "5938fb26-f773-4f68-a389-c0630e0687eb", + "requestMsgId": "80cec36b-8b73-4c09-9511-f79f59df0af8" + }, + "outputs": [], + "source": [ + "num_tasks = 6\n", + "problem = ContextualHartmann6(num_tasks=num_tasks, noise_std=0.001, negate=True).to(**tkwargs)\n", + "\n", + "# we choose num_tasks random weights\n", + "weights = torch.randn(num_tasks, **tkwargs)\n", + "\n", + "\n", + "def callable_func(samples, X=None):\n", + " res = -torch.cos((samples**2) + samples * weights)\n", + " return res.sum(dim=-1)\n", + "\n", + "\n", + "objective = GenericMCObjective(callable_func)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678932929407, + "executionStopTime": 1678932929411, + "originalKey": "5a85661a-3668-4128-b5f4-8150dbcdce7d", + "requestMsgId": "d1742d93-d229-4400-b3d9-55e5fee5eed6" + }, + "outputs": [], + "source": [ + "bounds = problem.bounds" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "eff2dc86-0797-40a7-962d-04e8c539c21a", + "showInput": false + }, + "source": [ + "## BO Loop" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "53414433-e097-4556-90e9-e99e35cdb390", + "showInput": false + }, + "source": [ + "Set environmental parameters, we use 20 initial data points and optimize for 20 steps with a batch size of 3 candidate points at each evaluation." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678932929437, + "executionStopTime": 1678932929440, + "originalKey": "51f24269-b5de-4dd8-b2b4-18d269f3f3c2", + "requestMsgId": "63c971a6-7e24-4b8a-8ec8-15d0ef21f6ff" + }, + "outputs": [], + "source": [ + "if SMOKE_TEST:\n", + " n_init = 5\n", + " n_steps = 1\n", + " batch_size = 2\n", + " num_samples = 4\n", + " # For L-BFGS inner optimization loop\n", + " MAXITER = 10\n", + "else:\n", + " n_init = 10\n", + " n_steps = 10\n", + " batch_size = 3\n", + " num_samples = 64\n", + " MAXITER = 200" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678932930875, + "executionStopTime": 1678934072848, + "originalKey": "de8d4079-6c17-4041-9ec4-9ebd09d46cd7", + "requestMsgId": "34279809-d637-4ea5-99c2-aaa3957c37ba" + }, + "outputs": [], + "source": [ + "from botorch.fit import fit_gpytorch_mll" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, run the optimization loop.\n", + "\n", + "Warning... this optimization loop can take a while, especially on the CPU. We compare to random sampling. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "originalKey": "b567ab22-f3b4-41e3-aaae-75e6fe820cfc", + "showInput": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Batch 0: best_value (random, mtgp) = (-4.76, -4.76, mtgp time = 15.64\n", + "Batch 1: best_value (random, mtgp) = (-4.76, -4.76, mtgp time = 36.96\n", + "Batch 2: best_value (random, mtgp) = (-4.39, -4.76, mtgp time = 23.20\n", + "Batch 3: best_value (random, mtgp) = (-4.39, -4.76, mtgp time = 23.07\n", + "Batch 4: best_value (random, mtgp) = (-4.39, -4.76, mtgp time = 35.79\n", + "Batch 5: best_value (random, mtgp) = (-4.22, -4.76, mtgp time = 50.71\n", + "Batch 6: best_value (random, mtgp) = (-4.22, -2.88, mtgp time = 61.87\n", + "Batch 7: best_value (random, mtgp) = (-4.22, -2.88, mtgp time = 115.77\n", + "Batch 8: best_value (random, mtgp) = (-4.22, -1.91, mtgp time = 67.89\n", + "Batch 9: best_value (random, mtgp) = (-4.22, -1.91, mtgp time = 48.66" + ] + } + ], + "source": [ + "# New version\n", + "torch.manual_seed(0)\n", + "\n", + "init_x = (bounds[1] - bounds[0]) * torch.rand(\n", + " n_init, bounds.shape[1], **tkwargs\n", + ") + bounds[0]\n", + "\n", + "init_y = problem(init_x)\n", + "\n", + "mtgp_train_x, mtgp_train_y = init_x, init_y\n", + "rand_x, rand_y = init_x, init_y\n", + "\n", + "best_value_mtgp = objective(init_y).max()\n", + "best_random = best_value_mtgp\n", + "\n", + "for iteration in range(n_steps):\n", + " # we empty the cache to clear memory out\n", + " torch.cuda.empty_cache()\n", + "\n", + " # MTGP\n", + " mtgp_t0 = time.monotonic()\n", + " mtgp = KroneckerMultiTaskGP(mtgp_train_x, mtgp_train_y)\n", + " mtgp_mll = ExactMarginalLogLikelihood(mtgp.likelihood, mtgp)\n", + " fit_gpytorch_mll(mll=mtgp_mll, optimizer_kwargs={\"options\": {\"maxiter\": 50}})\n", + "\n", + " sampler = IIDNormalSampler(sample_shape=torch.Size([num_samples]))\n", + " mtgp_acqf = qLogExpectedImprovement(\n", + " model=mtgp,\n", + " best_f=best_value_mtgp,\n", + " sampler=sampler,\n", + " objective=objective,\n", + " )\n", + " new_mtgp_x, _ = optimize_acqf(\n", + " acq_function=mtgp_acqf,\n", + " bounds=bounds,\n", + " q=batch_size,\n", + " num_restarts=10,\n", + " raw_samples=512, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": MAXITER, \"init_batch_limit\": 5},\n", + " )\n", + " mtgp_train_x = torch.cat((mtgp_train_x, new_mtgp_x), dim=0)\n", + " mtgp_train_y = torch.cat((mtgp_train_y, problem(new_mtgp_x)), dim=0)\n", + " best_value_mtgp = objective(mtgp_train_y).max()\n", + " mtgp_t1 = time.monotonic()\n", + "\n", + " # rand\n", + " new_rand_x = (bounds[1] - bounds[0]) * torch.rand(\n", + " batch_size, bounds.shape[1], **tkwargs\n", + " ) + bounds[0]\n", + " rand_x = torch.cat((rand_x, new_rand_x))\n", + " rand_y = torch.cat((rand_y, problem(new_rand_x)))\n", + " best_random = objective(rand_y).max()\n", + "\n", + " print(\n", + " f\"\\nBatch {iteration:>2}: best_value (random, mtgp) = \"\n", + " f\"({best_random:>4.2f}, {best_value_mtgp:>4.2f}, \"\n", + " f\"mtgp time = {mtgp_t1-mtgp_t0:>4.2f}\",\n", + " end=\"\",\n", + " )\n", + "\n", + "objectives = {\n", + " \"MGTP\": objective(mtgp_train_y).detach().cpu(),\n", + " \"Random\": objective(rand_y).detach().cpu(),\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "b567ab22-f3b4-41e3-aaae-75e6fe820cfc", + "showInput": false + }, + "source": [ + "### Plot Results" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1678934072894, + "executionStopTime": 1678934073463, + "originalKey": "8ec120a6-bc85-42cc-9c59-9963964a0da3", + "requestMsgId": "b1f7777e-e686-4325-a0e0-d0c8578ec106" + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "originalKey": "3585efc5-bcb5-49f0-9fe1-e50d2deccfd2", + "showInput": false + }, + "source": [ + "Finally, we plot the results. MTGP will outperform the random baseline." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "results = {\n", + " k: t[n_init:].cummax(0).values for k, t in objectives.items()\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for name, vals in results.items():\n", + " plt.plot(vals, label=name)\n", + "plt.legend()" + ] + } + ], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "fileHeader": "", + "isAdHoc": false, + "kernelspec": { + "display_name": "ae", + "language": "python", + "name": "bento_kernel_ae" + }, + "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.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "fileHeader": "", + "indentAmount": 2, + "isAdHoc": false, + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "plaintext" + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "indentAmount": 2, + "kernelspec": { + "name": "python3", + "display_name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/composite_mtbo.py b/website-old/static/files/composite_mtbo.py new file mode 100644 index 0000000000..890135fcc5 --- /dev/null +++ b/website-old/static/files/composite_mtbo.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Composite Bayesian Optimization with Multi-Task Gaussian Processes +# +# In this tutorial, we'll be describing how to perform multi-task Bayesian optimization over composite functions. In these types of problems, there are several related outputs, and an overall easy to evaluate objective function that we wish to maximize. +# +# **Multi-task Bayesian Optimization** was first proposed by [Swersky et al, NeurIPS, '13](https://papers.neurips.cc/paper/2013/hash/f33ba15effa5c10e873bf3842afb46a6-Abstract.html) in the context of fast hyper-parameter tuning for neural network models; however, we demonstrate a more advanced use-case of **[composite Bayesian optimization](https://proceedings.mlr.press/v97/astudillo19a.html)** where the overall function that we wish to optimize is a cheap-to-evaluate (and known) function of the outputs. In general, we expect that using more information about the function should yield improved performance when attempting to optimize it, particularly if the metric function itself is quickly varying. +# +# See [the composite BO tutorial w/ HOGP](https://github.com/pytorch/botorch/blob/main/tutorials/composite_bo_with_hogp.ipynb) for a more technical introduction. In general, we suggest using MTGPs for unstructured task outputs and the HOGP for matrix / tensor structured outputs. +# +# +# We will use a Multi-Task Gaussian process ([MTGP](https://papers.nips.cc/paper/2007/hash/66368270ffd51418ec58bd793f2d9b1b-Abstract.html)) with an ICM kernel to model all of the outputs in this problem. MTGPs can be easily accessed in Botorch via the `botorch.models.KroneckerMultiTaskGP` model class (for the "block design" case of fully observed outputs at all inputs). Given $T$ tasks (outputs) and $n$ data points, they assume that the responses, $Y \sim \mathbb{R}^{n \times T},$ are distributed as $\text{vec}(Y) \sim \mathcal{N}(f, D)$ and $f \sim \mathcal{GP}(\mu_{\theta}, K_{XX} \otimes K_{T}),$ where $D$ is a (diagonal) noise term. + +# In[1]: + + +import os +import time + +import torch +from botorch.acquisition.logei import qLogExpectedImprovement +from botorch.acquisition.objective import GenericMCObjective +from botorch.models import KroneckerMultiTaskGP +from botorch.optim import optimize_acqf +from botorch.sampling.normal import IIDNormalSampler + +from botorch.test_functions import Hartmann +from gpytorch.mlls import ExactMarginalLogLikelihood + +import warnings +warnings.filterwarnings("ignore") + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Set device, dtype and random seed + +# In[2]: + + +torch.random.manual_seed(10) + +tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda:0" if torch.cuda.is_available() else "cpu"), +} + + +# ### Problem Definition +# +# The function that we wish to optimize is based off of a contextual version of the Hartmann-6 test function, where following [Feng et al, NeurIPS, '20](https://proceedings.neurips.cc/paper/2020/hash/faff959d885ec0ecf70741a846c34d1d-Abstract.html) we convert the sixth task dimension into a task indicator. Here we assume that we evaluate all contexts at once. + +# In[3]: + + +from torch import Tensor + + +class ContextualHartmann6(Hartmann): + def __init__(self, num_tasks: int = 20, noise_std=None, negate=False): + super().__init__(dim=6, noise_std=noise_std, negate=negate) + self.task_range = torch.linspace(0, 1, num_tasks).unsqueeze(-1) + self._bounds = [(0.0, 1.0) for _ in range(self.dim - 1)] + self.bounds = torch.tensor(self._bounds).t() + + def evaluate_true(self, X: Tensor) -> Tensor: + batch_X = X.unsqueeze(-2) + batch_dims = X.ndim - 1 + + expanded_task_range = self.task_range + for _ in range(batch_dims): + expanded_task_range = expanded_task_range.unsqueeze(0) + task_range = expanded_task_range.repeat(*X.shape[:-1], 1, 1).to(X) + concatenated_X = torch.cat( + ( + batch_X.repeat(*[1] * batch_dims, self.task_range.shape[0], 1), + task_range, + ), + dim=-1, + ) + return super().evaluate_true(concatenated_X) + + +# We use `GenericMCObjective` to define the differentiable function that we are optimizing. Here, it is defined as +# $$g(f) = \sum_{i=1}^T \cos(f_i^2 + f_i w_i)$$ +# where $w$ is a weight vector (drawn randomly once at the start of the optimization). As this function is a non-linear function of the outputs $f,$ we cannot compute acquisition functions via computation of the posterior mean and variance, but rather have to compute posterior samples and evaluate acquisitions with Monte Carlo sampling. +# +# For greater than $10$ or so tasks, it is computationally challenging to sample the posterior over all tasks jointly using conventional approaches, except that [Maddox et al, '21](https://arxiv.org/abs/2106.12997) have devised an efficient method for exploiting the structure in the posterior distribution of the MTGP, enabling efficient MC based optimization of objectives using MTGPs. In this tutorial, we choose 6 contexts/tasks for demostration. + +# In[4]: + + +num_tasks = 6 +problem = ContextualHartmann6(num_tasks=num_tasks, noise_std=0.001, negate=True).to(**tkwargs) + +# we choose num_tasks random weights +weights = torch.randn(num_tasks, **tkwargs) + + +def callable_func(samples, X=None): + res = -torch.cos((samples**2) + samples * weights) + return res.sum(dim=-1) + + +objective = GenericMCObjective(callable_func) + + +# In[5]: + + +bounds = problem.bounds + + +# ## BO Loop + +# Set environmental parameters, we use 20 initial data points and optimize for 20 steps with a batch size of 3 candidate points at each evaluation. + +# In[6]: + + +if SMOKE_TEST: + n_init = 5 + n_steps = 1 + batch_size = 2 + num_samples = 4 + # For L-BFGS inner optimization loop + MAXITER = 10 +else: + n_init = 10 + n_steps = 10 + batch_size = 3 + num_samples = 64 + MAXITER = 200 + + +# In[7]: + + +from botorch.fit import fit_gpytorch_mll + + +# Finally, run the optimization loop. +# +# Warning... this optimization loop can take a while, especially on the CPU. We compare to random sampling. + +# In[8]: + + +# New version +torch.manual_seed(0) + +init_x = (bounds[1] - bounds[0]) * torch.rand( + n_init, bounds.shape[1], **tkwargs +) + bounds[0] + +init_y = problem(init_x) + +mtgp_train_x, mtgp_train_y = init_x, init_y +rand_x, rand_y = init_x, init_y + +best_value_mtgp = objective(init_y).max() +best_random = best_value_mtgp + +for iteration in range(n_steps): + # we empty the cache to clear memory out + torch.cuda.empty_cache() + + # MTGP + mtgp_t0 = time.monotonic() + mtgp = KroneckerMultiTaskGP(mtgp_train_x, mtgp_train_y) + mtgp_mll = ExactMarginalLogLikelihood(mtgp.likelihood, mtgp) + fit_gpytorch_mll(mll=mtgp_mll, optimizer_kwargs={"options": {"maxiter": 50}}) + + sampler = IIDNormalSampler(sample_shape=torch.Size([num_samples])) + mtgp_acqf = qLogExpectedImprovement( + model=mtgp, + best_f=best_value_mtgp, + sampler=sampler, + objective=objective, + ) + new_mtgp_x, _ = optimize_acqf( + acq_function=mtgp_acqf, + bounds=bounds, + q=batch_size, + num_restarts=10, + raw_samples=512, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": MAXITER, "init_batch_limit": 5}, + ) + mtgp_train_x = torch.cat((mtgp_train_x, new_mtgp_x), dim=0) + mtgp_train_y = torch.cat((mtgp_train_y, problem(new_mtgp_x)), dim=0) + best_value_mtgp = objective(mtgp_train_y).max() + mtgp_t1 = time.monotonic() + + # rand + new_rand_x = (bounds[1] - bounds[0]) * torch.rand( + batch_size, bounds.shape[1], **tkwargs + ) + bounds[0] + rand_x = torch.cat((rand_x, new_rand_x)) + rand_y = torch.cat((rand_y, problem(new_rand_x))) + best_random = objective(rand_y).max() + + print( + f"\nBatch {iteration:>2}: best_value (random, mtgp) = " + f"({best_random:>4.2f}, {best_value_mtgp:>4.2f}, " + f"mtgp time = {mtgp_t1-mtgp_t0:>4.2f}", + end="", + ) + +objectives = { + "MGTP": objective(mtgp_train_y).detach().cpu(), + "Random": objective(rand_y).detach().cpu(), +} + + +# ### Plot Results + +# In[9]: + + +import matplotlib.pyplot as plt + + +# Finally, we plot the results. MTGP will outperform the random baseline. + +# In[10]: + + +results = { + k: t[n_init:].cummax(0).values for k, t in objectives.items() +} + + +# In[11]: + + +for name, vals in results.items(): + plt.plot(vals, label=name) +plt.legend() + diff --git a/website-old/static/files/constrained_multi_objective_bo.ipynb b/website-old/static/files/constrained_multi_objective_bo.ipynb new file mode 100644 index 0000000000..baa2bd9de5 --- /dev/null +++ b/website-old/static/files/constrained_multi_objective_bo.ipynb @@ -0,0 +1,723 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "20bce0d3-6ea0-4db3-bc3b-2569027a0e3b", + "showInput": false + }, + "source": [ + "## Constrained, Parallel, Multi-Objective BO in BoTorch with qNEHVI, and qParEGO\n", + "\n", + "In this tutorial, we illustrate how to implement a constrained multi-objective (MO) Bayesian Optimization (BO) closed loop in BoTorch.\n", + "\n", + "In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. See [here](https://ax.dev/tutorials/multiobjective_optimization.html) for an Ax tutorial on MOBO. If desired, you can use a custom BoTorch model in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. Given a `MultiObjective`, Ax will default to the $q$NEHVI acquisiton function. If desired, this can also be customized by adding `\"botorch_acqf_class\": ,` to the `model_kwargs`.\n", + "\n", + "We use the parallel ParEGO ($q$ParEGO) [1] and parallel Noisy Expected Hypervolume Improvement ($q$NEHVI) [2] acquisition functions to optimize a synthetic C2-DTLZ2 test function with $M=2$ objectives, $V=1$ constraint, and $d=4$ parameters. The two objectives are\n", + "$$f_1(\\mathbf x) = (1+ g(\\mathbf x_M))\\cos\\big(\\frac{\\pi}{2}x_1\\big)$$\n", + "$$f_2(\\mathbf x) = (1+ g(\\mathbf x_M))\\sin\\big(\\frac{\\pi}{2}x_1\\big)$$\n", + "where $g(\\mathbf x) = \\sum_{x_i \\in \\mathbf x_M} (x_i - 0.5)^2, \\mathbf x \\in [0,1]^d,$ and $\\mathbf x_M$ represents the last $d - M +1$ elements of $\\mathbf x$. Additionally, the C2-DTLZ2 problem uses the following constraint:\n", + "\n", + "$$c(\\mathbf x) = - \\min \\bigg[\\min_{i=1}^M\\bigg((f_i(\\mathbf x) -1 )^2 + \\sum_{j=1, j=i}^M (f_j^2 - r^2) \\bigg), \\bigg(\\sum_{i=1}^M \\big((f_i(\\mathbf x) - \\frac{1}{\\sqrt{M}})^2 - r^2\\big)\\bigg)\\bigg]\\geq 0$$\n", + "\n", + "where $\\mathbf x \\in [0,1]^d$ and $r=0.2$. \n", + "\n", + "The goal here is to *minimize* both objectives. Since BoTorch assumes maximization, we maximize the negative of each objective. Since there typically is no single best solution in multi-objective optimization problems, we seek to find the pareto frontier, the set of optimal trade-offs where improving one metric means deteriorating another.\n", + "\n", + "[1] [S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2006.05078)\n", + "\n", + "[2] [S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances in Neural Information Processing Systems 34, 2021.](https://arxiv.org/abs/2105.08195)\n", + "\n", + "**For batch optimization (or in noisy settings), we strongly recommend using $q$NEHVI rather than $q$EHVI [1] because it is far more efficient than $q$EHVI and mathematically equivalent in the noiseless setting.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "00d9f0ef-fab6-463e-a54d-a548581bc7f4", + "showInput": false + }, + "source": [ + "### Set dtype and device\n", + "Note: $q$EHVI aggressively exploits parallel hardware and is much faster when run on a GPU. See [1] for details." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "customOutput": null, + "executionStartTime": 1668651350300, + "executionStopTime": 1668651350308, + "originalKey": "f27224aa-b567-4a6d-b6b3-74f2ecbfe319", + "requestMsgId": "df1b7814-2d71-4421-b832-e10d0c1e7743" + }, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "\n", + "\n", + "tkwargs = {\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda:3\" if torch.cuda.is_available() else \"cpu\"),\n", + "}\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "89f8b99f-5cb2-45c9-9df6-7e1d18d4f8c6", + "showInput": false + }, + "source": [ + "### Problem setup\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "customOutput": null, + "executionStartTime": 1668651350608, + "executionStopTime": 1668651354486, + "originalKey": "4227f250-60b5-4c97-b04c-3cfe7a1c410a", + "requestMsgId": "83e67907-72c3-4bb8-8468-7eb99e616730" + }, + "outputs": [], + "source": [ + "from botorch.test_functions.multi_objective import C2DTLZ2\n", + "\n", + "\n", + "d = 4\n", + "M = 2\n", + "problem = C2DTLZ2(dim=d, num_objectives=M, negate=True).to(**tkwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "2de9fbab-9d15-4410-8371-d3b1f730e3d7", + "showInput": false + }, + "source": [ + "#### Model initialization\n", + "\n", + "We use a multi-output `SingleTaskGP` to model the two objectives with a homoskedastic Gaussian likelihood with an inferred noise level.\n", + "\n", + "The models are initialized with $2(d+1)=10$ points drawn randomly from $[0,1]^{4}$." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1668651354720, + "executionStopTime": 1668651354729, + "hidden_ranges": [], + "originalKey": "192b8d87-b2e3-4223-b193-6399b8643391", + "requestMsgId": "55d97599-5be9-4a7a-857c-18a9b56bf07d" + }, + "outputs": [], + "source": [ + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.model_list_gp_regression import ModelListGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "from botorch.utils.transforms import normalize, unnormalize\n", + "from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood\n", + "\n", + "\n", + "def generate_initial_data(n):\n", + " # generate training data\n", + " train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1)\n", + " train_obj = problem(train_x)\n", + " # negative values imply feasibility in botorch\n", + " train_con = -problem.evaluate_slack(train_x)\n", + " return train_x, train_obj, train_con\n", + "\n", + "\n", + "def initialize_model(train_x, train_obj, train_con):\n", + " # define models for objective and constraint\n", + " train_x = normalize(train_x, problem.bounds)\n", + " train_y = torch.cat([train_obj, train_con], dim=-1)\n", + " models = []\n", + " for i in range(train_y.shape[-1]):\n", + " models.append(\n", + " SingleTaskGP(\n", + " train_x, train_y[..., i : i + 1], outcome_transform=Standardize(m=1)\n", + " )\n", + " )\n", + " model = ModelListGP(*models)\n", + " mll = SumMarginalLogLikelihood(model.likelihood, model)\n", + " return mll, model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "a0f6fa02-0843-4c45-8a19-8654226c1edc", + "showInput": false + }, + "source": [ + "#### Define a helper function that performs the essential BO step for $q$NEHVI\n", + "The helper function below initializes the $q$NEHVI acquisition function, optimizes it, and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. \n", + "\n", + "For this example, we'll use a small batch of $q=2$. Passing the keyword argument `sequential=True` to the function `optimize_acqf`specifies that candidates should be optimized in a sequential greedy fashion (see [1] for details why this is important). A simple initialization heuristic is used to select the 10 restart initial locations from a set of 512 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation.\n", + "\n", + "**Reference Point**\n", + "\n", + "$q$NEHVI requires specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy.\n", + "\n", + "**Integrating over function values at in-sample designs**\n", + "\n", + "$q$NEHVI integrates over the unknown function values at the previously evaluated designs (see [2] for details). Therefore, we need to provide the previously evaluated designs (`train_x`, *normalized* to be within $[0,1]^d$) to the acquisition function.\n", + "\n", + "**Pruning baseline designs**\n", + "To speed up integration over the function values at the previously evaluated designs, we prune the set of previously evaluated designs (by setting `prune_baseline=True`) to only include those which have positive probability of being on the current in-sample Pareto frontier." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1668651354970, + "executionStopTime": 1668651355060, + "hidden_ranges": [], + "originalKey": "65dcfbb2-f1e9-40a1-9807-8cdc1cc3fdc8", + "requestMsgId": "68a072df-7e90-4c7f-9915-520ca48c5e0a" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.multi_objective.monte_carlo import (\n", + " qNoisyExpectedHypervolumeImprovement,\n", + ")\n", + "from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective\n", + "from botorch.optim.optimize import optimize_acqf, optimize_acqf_list\n", + "from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization\n", + "from botorch.utils.sampling import sample_simplex\n", + "\n", + "\n", + "BATCH_SIZE = 2\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "\n", + "standard_bounds = torch.zeros(2, problem.dim, **tkwargs)\n", + "standard_bounds[1] = 1\n", + "\n", + "\n", + "def optimize_qnehvi_and_get_observation(model, train_x, train_obj, train_con, sampler):\n", + " \"\"\"Optimizes the qNEHVI acquisition function, and returns a new candidate and observation.\"\"\"\n", + " train_x = normalize(train_x, problem.bounds)\n", + " acq_func = qNoisyExpectedHypervolumeImprovement(\n", + " model=model,\n", + " ref_point=problem.ref_point.tolist(), # use known reference point\n", + " X_baseline=train_x,\n", + " sampler=sampler,\n", + " prune_baseline=True,\n", + " # define an objective that specifies which outcomes are the objectives\n", + " objective=IdentityMCMultiOutputObjective(outcomes=[0, 1]),\n", + " # specify that the constraint is on the last outcome\n", + " constraints=[lambda Z: Z[..., -1]],\n", + " )\n", + " # optimize\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=standard_bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " sequential=True,\n", + " )\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=problem.bounds)\n", + " new_obj = problem(new_x)\n", + " # negative values imply feasibility in botorch\n", + " new_con = -problem.evaluate_slack(new_x)\n", + " return new_x, new_obj, new_con" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ae9e19c2-327f-486e-bee3-11f1b71b0dfe", + "showInput": false + }, + "source": [ + "#### Define a helper function that performs the essential BO step for $q$ParEGO\n", + "The helper function below similarly initializes $q$ParEGO, optimizes it, and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. \n", + "\n", + "$q$ParEGO uses random augmented chebyshev scalarization with the `qExpectedImprovement` acquisition function. In the parallel setting ($q>1$), each candidate is optimized in sequential greedy fashion using a different random scalarization (see [1] for details).\n", + "\n", + "To do this, we create a list of `qExpectedImprovement` acquisition functions, each with different random scalarization weights. The `optimize_acqf_list` method sequentially generates one candidate per acquisition function and conditions the next candidate (and acquisition function) on the previously selected pending candidates." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1668651355591, + "executionStopTime": 1668651355682, + "hidden_ranges": [], + "originalKey": "a4a23da4-64de-4948-ad92-76b57c023f62", + "requestMsgId": "f38f70dd-4857-484f-963c-cdfec7d8fc67" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.monte_carlo import qExpectedImprovement\n", + "from botorch.acquisition.objective import GenericMCObjective\n", + "\n", + "\n", + "def optimize_qparego_and_get_observation(model, train_obj, train_con, sampler):\n", + " \"\"\"Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization\n", + " of the qParEGO acquisition function, and returns a new candidate and observation.\"\"\"\n", + " acq_func_list = []\n", + " for _ in range(BATCH_SIZE):\n", + " # sample random weights\n", + " weights = sample_simplex(problem.num_objectives, **tkwargs).squeeze()\n", + " # construct augmented Chebyshev scalarization\n", + " scalarization = get_chebyshev_scalarization(weights=weights, Y=train_obj)\n", + " # initialize the scalarized objective (w/o constraints)\n", + " scalarized_objective = GenericMCObjective(\n", + " # the last element of the model outputs is the constraint\n", + " lambda Z, X: scalarization(Z[..., :-1]),\n", + " )\n", + " train_y = torch.cat([train_obj, train_con], dim=-1)\n", + " acq_func = qExpectedImprovement( # pyre-ignore: [28]\n", + " model=model,\n", + " objective=scalarized_objective,\n", + " best_f=scalarized_objective(train_y).max(),\n", + " constraints=[lambda Z: Z[..., -1]],\n", + " sampler=sampler,\n", + " )\n", + " acq_func_list.append(acq_func)\n", + " # optimize\n", + " candidates, _ = optimize_acqf_list(\n", + " acq_function_list=acq_func_list,\n", + " bounds=standard_bounds,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=problem.bounds)\n", + " new_obj = problem(new_x)\n", + " # negative values imply feasibility in botorch\n", + " new_con = -problem.evaluate_slack(new_x)\n", + " return new_x, new_obj, new_con" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "d4487ba0-4fad-41dd-a2ae-e0b95094dba1", + "showInput": false + }, + "source": [ + "### Perform Bayesian Optimization loop with $q$EHVI and $q$ParEGO\n", + "The Bayesian optimization \"loop\" for a batch size of $q$ simply iterates the following steps:\n", + "1. given a surrogate model, choose a batch of points $\\{x_1, x_2, \\ldots x_q\\}$\n", + "2. observe $f(x)$ for each $x$ in the batch \n", + "3. update the surrogate model. \n", + "\n", + "\n", + "Just for illustration purposes, we run one trial with `N_BATCH=20` rounds of optimization. The acquisition function is approximated using `MC_SAMPLES=128` samples.\n", + "\n", + "*Note*: Running this may take a little while." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1668651356028, + "executionStopTime": 1668651959470, + "hidden_ranges": [], + "originalKey": "4c225d99-6425-4201-ac4a-a042a351c1d3", + "requestMsgId": "be831f3c-ff7c-4c00-a215-fb021f0c5770" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Batch 1: Hypervolume (random, qParEGO, qNEHVI) = (0.00, 0.00, 0.00), time = 4.54.\n", + "Batch 2: Hypervolume (random, qParEGO, qNEHVI) = (0.00, 0.00, 0.00), time = 4.12.\n", + "Batch 3: Hypervolume (random, qParEGO, qNEHVI) = (0.13, 0.00, 0.10), time = 4.10.\n", + "Batch 4: Hypervolume (random, qParEGO, qNEHVI) = (0.13, 0.00, 0.14), time = 4.49.\n", + "Batch 5: Hypervolume (random, qParEGO, qNEHVI) = (0.13, 0.00, 0.17), time = 4.65.\n", + "Batch 6: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.00, 0.23), time = 5.38.\n", + "Batch 7: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.06, 0.25), time = 6.17.\n", + "Batch 8: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.12, 0.27), time = 5.26.\n", + "Batch 9: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.19, 0.28), time = 6.60.\n", + "Batch 10: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.20, 0.28), time = 6.12.\n", + "Batch 11: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.23, 0.32), time = 6.05.\n", + "Batch 12: Hypervolume (random, qParEGO, qNEHVI) = (0.16, 0.25, 0.34), time = 6.76.\n", + "Batch 13: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.25, 0.35), time = 6.47.\n", + "Batch 14: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.27, 0.36), time = 7.86.\n", + "Batch 15: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.28, 0.36), time = 5.15.\n", + "Batch 16: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.28, 0.36), time = 5.09.\n", + "Batch 17: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.31, 0.37), time = 7.28.\n", + "Batch 18: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.32, 0.37), time = 7.97.\n", + "Batch 19: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.34, 0.37), time = 8.76.\n", + "Batch 20: Hypervolume (random, qParEGO, qNEHVI) = (0.17, 0.34, 0.38), time = 5.98." + ] + } + ], + "source": [ + "import time\n", + "import warnings\n", + "\n", + "from botorch import fit_gpytorch_mll\n", + "from botorch.exceptions import BadInitialCandidatesWarning\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "from botorch.utils.multi_objective.hypervolume import Hypervolume\n", + "from botorch.utils.multi_objective.pareto import is_non_dominated\n", + "\n", + "\n", + "warnings.filterwarnings(\"ignore\", category=BadInitialCandidatesWarning)\n", + "warnings.filterwarnings(\"ignore\", category=RuntimeWarning)\n", + "\n", + "N_BATCH = 20 if not SMOKE_TEST else 1\n", + "MC_SAMPLES = 128 if not SMOKE_TEST else 16\n", + "verbose = True\n", + "\n", + "hv = Hypervolume(ref_point=problem.ref_point)\n", + "hvs_qparego, hvs_qnehvi, hvs_random = [], [], []\n", + "\n", + "# call helper functions to generate initial training data and initialize model\n", + "train_x_qparego, train_obj_qparego, train_con_qparego = generate_initial_data(\n", + " n=2 * (d + 1)\n", + ")\n", + "mll_qparego, model_qparego = initialize_model(\n", + " train_x_qparego, train_obj_qparego, train_con_qparego\n", + ")\n", + "\n", + "train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi = (\n", + " train_x_qparego,\n", + " train_obj_qparego,\n", + " train_con_qparego,\n", + ")\n", + "train_x_random, train_obj_random, train_con_random = (\n", + " train_x_qparego,\n", + " train_obj_qparego,\n", + " train_con_qparego,\n", + ")\n", + "\n", + "mll_qnehvi, model_qnehvi = initialize_model(\n", + " train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi\n", + ")\n", + "\n", + "# compute pareto front\n", + "is_feas = (train_con_qparego <= 0).all(dim=-1)\n", + "feas_train_obj = train_obj_qparego[is_feas]\n", + "if feas_train_obj.shape[0] > 0:\n", + " pareto_mask = is_non_dominated(feas_train_obj)\n", + " pareto_y = feas_train_obj[pareto_mask]\n", + " # compute hypervolume\n", + " volume = hv.compute(pareto_y)\n", + "else:\n", + " volume = 0.0\n", + "\n", + "hvs_qparego.append(volume)\n", + "hvs_qnehvi.append(volume)\n", + "hvs_random.append(volume)\n", + "\n", + "# run N_BATCH rounds of BayesOpt after the initial random batch\n", + "for iteration in range(1, N_BATCH + 1):\n", + " t0 = time.monotonic()\n", + "\n", + " # fit the models\n", + " fit_gpytorch_mll(mll_qparego)\n", + " fit_gpytorch_mll(mll_qnehvi)\n", + "\n", + " # define the qParEGO and qNEHVI acquisition modules using a QMC sampler\n", + " qparego_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + " qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + "\n", + " # optimize acquisition functions and get new observations\n", + " (\n", + " new_x_qparego,\n", + " new_obj_qparego,\n", + " new_con_qparego,\n", + " ) = optimize_qparego_and_get_observation(\n", + " model_qparego, train_obj_qparego, train_con_qparego, qparego_sampler\n", + " )\n", + " new_x_qnehvi, new_obj_qnehvi, new_con_qnehvi = optimize_qnehvi_and_get_observation(\n", + " model_qnehvi, train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi, qnehvi_sampler\n", + " )\n", + " new_x_random, new_obj_random, new_con_random = generate_initial_data(n=BATCH_SIZE)\n", + "\n", + " # update training points\n", + " train_x_qparego = torch.cat([train_x_qparego, new_x_qparego])\n", + " train_obj_qparego = torch.cat([train_obj_qparego, new_obj_qparego])\n", + " train_con_qparego = torch.cat([train_con_qparego, new_con_qparego])\n", + "\n", + " train_x_qnehvi = torch.cat([train_x_qnehvi, new_x_qnehvi])\n", + " train_obj_qnehvi = torch.cat([train_obj_qnehvi, new_obj_qnehvi])\n", + " train_con_qnehvi = torch.cat([train_con_qnehvi, new_con_qnehvi])\n", + "\n", + " train_x_random = torch.cat([train_x_random, new_x_random])\n", + " train_obj_random = torch.cat([train_obj_random, new_obj_random])\n", + " train_con_random = torch.cat([train_con_random, new_con_random])\n", + "\n", + " # update progress\n", + " for hvs_list, train_obj, train_con in zip(\n", + " (hvs_random, hvs_qparego, hvs_qnehvi),\n", + " (train_obj_random, train_obj_qparego, train_obj_qnehvi),\n", + " (train_con_random, train_con_qparego, train_con_qnehvi),\n", + " ):\n", + " # compute pareto front\n", + " is_feas = (train_con <= 0).all(dim=-1)\n", + " feas_train_obj = train_obj[is_feas]\n", + " if feas_train_obj.shape[0] > 0:\n", + " pareto_mask = is_non_dominated(feas_train_obj)\n", + " pareto_y = feas_train_obj[pareto_mask]\n", + " # compute feasible hypervolume\n", + " volume = hv.compute(pareto_y)\n", + " else:\n", + " volume = 0.0\n", + " hvs_list.append(volume)\n", + "\n", + " # reinitialize the models so they are ready for fitting on next iteration\n", + " # Note: we find improved performance from not warm starting the model hyperparameters\n", + " # using the hyperparameters from the previous iteration\n", + " mll_qparego, model_qparego = initialize_model(\n", + " train_x_qparego, train_obj_qparego, train_con_qparego\n", + " )\n", + " mll_qnehvi, model_qnehvi = initialize_model(\n", + " train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi\n", + " )\n", + "\n", + " t1 = time.monotonic()\n", + "\n", + " if verbose:\n", + " print(\n", + " f\"\\nBatch {iteration:>2}: Hypervolume (random, qParEGO, qNEHVI) = \"\n", + " f\"({hvs_random[-1]:>4.2f}, {hvs_qparego[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), \"\n", + " f\"time = {t1-t0:>4.2f}.\",\n", + " end=\"\",\n", + " )\n", + " else:\n", + " print(\".\", end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "3132af99-128f-41fc-9e6c-d1eaf6083f81", + "showInput": false + }, + "source": [ + "#### Plot the results\n", + "The plot below shows the log feasible hypervolume difference: the log difference between the hypervolume of the true feasible pareto front and the hypervolume of the observed (feasible) pareto front identified by each algorithm. The log feasible hypervolume difference is plotted at each step of the optimization for each of the algorithms.\n", + "\n", + "The plot show that $q$NEHVI vastly outperforms the $q$ParEGO and Sobol baselines." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1668651959825, + "executionStopTime": 1668651960985, + "hidden_ranges": [], + "originalKey": "38f5ce01-264f-43bd-8bdb-edf756f7c0dc", + "requestMsgId": "ec2a65b4-0cdb-4487-b6f4-da16df97ce18" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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njx58rr5twDnz59PlgynpTJlytCsWTO6dOnC33//zfLly+nZsydPPPFEUqWHI0eOULp0af7++28ASpcuTfHixenatSt///03e/bsYfTo0fz111889NBD6RKniKROnTqmHNpHH0FwsHleo4YZ0LvNl0Pijry94emnYeNG+PVXuPde0zr5q6+gbFlo0waWLzf9tkVEruF08lutWjV+++23pOXLCe8XX3xBrVq10i6ya3z77beULl2axo0b06JFC+rWrctnn32W9HpcXBw7duxIGvH19vZmzpw55MyZk1atWlGhQgWmTJnC5MmTadGiRbrFKSKp4+Vl+hvs2GG6wlmWua+pZElzo5zyGLmOhwe0bAlLlpj2gq1bmwtn1iyoW9fMo2naFF57DebM0V9SIgKkYs7vsmXLaN68Oe3bt2fSpEl07dqVrVu3smLFChYvXkzVqlXTK1ZbqM6viD2WLIEePUybZID77oOvv4bcue2NS1zc9u0wciRMmwb/PxiSTPHiZl7N5UeFCmYkWUQytXRtcgGwd+9ehg8fzr///kt0dDRVqlRh4MCBlC9fPtVBuyolvyL2iYuDjz82N/pfvGgS32++gSZN7I5MXF58vPnLadWqK48dO67fzs8PqlVLnhDrxjmRTCfdkt+4uDi6du3KkCFDbtnpLStR8itivy1bTLfbLVtML4TBg+H1181UCZEUO33a1NlbtQpWrzY/b1QqLX/+5MlwlSrg75/h4YpIyqXryG9ISAgbNmxQ8isiGerCBejXz8wDBnOT3NSpULCgvXFJJpaYaOoGXz06vHHj9RPMvbygUiVzF+blhLhYMbVbFnEh6Zr8duzYkUqVKtGvX787CjKzUPIr4lq+/x6efx6iosz9TBMnmvucRNJEdLRpr3x1QnyjTqLZsycfHb7nHggJyfh4RQRI5+T3rbfeYvTo0TRu3JiqVauSLVu2ZK/37t3b+YhdmJJfEdezd6+ZBrF2rVnu1cvc4+Tra29ckgVZlmmocXUyvG4dxMYm387Dw3w18e67mo8jYoN0TX5vNd3B4XCwd+9eZ3bn8pT8irim2Fgz9/f9981y5cpmVLhECXvjEjcQEwP//ps8Id63z7zWuLG5ELNntzdGETeT7tUe3ImSXxHX9ttv0LGjKeEaGAjjx0P79nZHJW5nxgzTeOP8edNtbtYsU0ZNRDJEurY3viw2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RUUpARYRERG3EhJiRoEPH4aTJ80jMhIKFYLQULujuzO2Jr85c+a8Yf/otODj48M999zDjh07kq3fuXMnhQoVuun7fH198fX1TZeYRERERDILT0+T7IaFwf79ZhR4926zXLCgGSXOjDLNnN+DBw+yYcMGDh48SEJCAhs2bGDDhg1ER0cnbVO6dGlmzrzSreTFF1/k+++/5/PPP2f37t2MHTuWX375he7du9vxEUREREQynaAgKFsWcuc2y6dPw+bN5mdmlGly9tdee43JkycnLVeuXBmAhQsX0qBBAwB27NiRrNNJmzZt+PTTTxk+fDi9e/emVKlSTJ8+nbp162Zo7CIiIiKZmacnFChgborbv99UhNi71yTAhQqZKhGZRabp8GYXdXgTERHJOrJahzc7JCZCRISpCWxZVxLj7NlN7eD04nYd3kRERETk5ho0aIDD4WDatGnJ1o8ZM4bChQsnLU+aNOmGFbauTig7derEQw89dN0xFi1ahKeng2zZItmxYzo1angSEXGE/fvNfODY2CvblihRIqmIQIMGDejbt28aftrUU/IrIiIikkX4+fnx6quvEhcXd8vtgoODiYiISPY4cOCAU8dq2/ZBsmfPztKlk3E44OxZMxf45ElYvHgJu3fvpnPnznfycdKFkl8RERFxW5YF58/b83B24un58+d5+umnCQwMJE+ePIwePfq6EdUnn3ySyMhIPv/881vuy+FwEB4enuyR+/IdbSnk7e1Nhw4dmD59EmXLQrZsZkrEgQMwZsxX3HNPDcqVK+fch8wASn5FRETEbV24AIGB9jwuXHAu1hdffJHFixcze/Zs5s6dy6JFi1i/fn2ybYKDg3nllVd44403OH/+fBqeqRvr3Lkzu3btYs2aJZQubeb+XroUzZ9//kTTpp25zQC0LZT8ioiIiLi46OhovvzyS0aNGkXjxo0pX748kydPJv4Gbde6d++On58f77///k33d/bsWQIDA5M9mjdvnmybX3/99bbblC1blpo1a/LVV1/hcJhyaFu2/ABYPPnkEy5ZBSLTlDoTERERSWsBAXBVy4AMP3ZK7dmzh9jYWGrUqJG0LiwsjFKlSl23ra+vL2+88Qa9evWiW7duN9xfUFDQdaPG/v7+yZYbNmzI+PHjk61bvXo17du3T7bu2WefpV+/fnz88ccEBQXx9ddf8dhjbSlTJijl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1euu3W/NmjWTzSWtVasWu3btIiEhgU2bNpGQkEDJkiUJDAxMeixevJg9e/YkvcfHx+e6z3Ctbdu2UatWrWTHqlOnDtHR0Rw+fDjFn3Hbtm3UqVMn2bo6deokxXz157harVq12LZtGwBt27bl4sWLFC1alC5dujBz5syk6Rd38pnbtWvHokWLOHr0KADffvstLVu2TPoaPCEhgTfffJPy5csTFhZGYGAgf/75502nidzpObg6PofDQXh4eNLvv1OnTmzYsIFSpUrRu3dv5s6de8tjXrx4ET8/vxu+dvW59vLyolq1aknn+t9//2XhwoXJzmXp0qUBks5nly5dmDt3LkeOHAHMdIFOnTolXSs3+7yXj3Gjz5stWzaCg4OTPu+2bduoUaPGTeN2xtXHyZMnD0Cq/r1e7fvvv6dOnTqEh4cTGBjIq6++6vR1Ack/U1hYGKVKlUo6Tym9bq51q/N6IxUrVqRx48aUL1+etm3b8vnnn3PmzJlk2/j7+wNcNw1LJCNljrtKRLIob2/vZMsOhyPpBioPD/O3qXXVPNybzQW8ej8Oh+OW+02J6OhoPD09WbduHZ6ensleCwwMTHru7+9v681YzipQoAA7duxg3rx5/PXXX3Tv3p2RI0eyePHiO/rM99xzD8WKFWPatGl069aNmTNnJs3xBBg5ciQffvghY8aMoXz58mTLlo2+ffsSGxubLp/zVr//KlWqsG/fPn7//XfmzZvHY489RpMmTfjpp59uuK8cOXKwadMmp2OIjo6mVatWjBgx4rrXLieOlStXpmLFikyZMoWmTZuyZcsWfvvtN6ePdafXe2qOc/kauJPjrFy5knbt2vH6669z//33ExISwrRp0244l90Ozp5XT09P/vrrL1asWMHcuXP5+OOPeeWVV1i9ejVFihQB4PTp04C5wU7ELkp+RVzU5f85REREULlyZYA0rau6evXqZMurVq2iRIkSeHp6UrlyZRISEjhx4gT33nvvHR2nTJkyTJ8+HcuykhKG5cuXExQURP78+Z3az/Lly5OtW758OSVLlkyWrK5atSrZNqtWraJMmTJJy/7+/rRq1YpWrVrRo0cPSpcuzaZNm+74M7dr145vv/2W/Pnz4+HhkewGseXLl9O6dWvat28PmIRp586dlC1bNmkbHx+fW47COXMObic4OJjHH3+cxx9/nEcffZRmzZpx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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "iters = np.arange(N_BATCH + 1) * BATCH_SIZE\n", + "log_hv_difference_qparego = np.log10(problem.max_hv - np.asarray(hvs_qparego))\n", + "log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi))\n", + "log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random))\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n", + "ax.plot(\n", + " iters,\n", + " log_hv_difference_rnd,\n", + " label=\"Sobol\",\n", + " linewidth=1.5,\n", + " color=\"gray\",\n", + ")\n", + "ax.plot(\n", + " iters,\n", + " log_hv_difference_qparego,\n", + " label=\"qParEGO\",\n", + " linewidth=1.5,\n", + " color=\"red\",\n", + ")\n", + "ax.plot(\n", + " iters,\n", + " log_hv_difference_qnehvi,\n", + " label=\"qNEHVI\",\n", + " linewidth=1.5,\n", + " color=\"blue\",\n", + ")\n", + "ax.set(\n", + " xlabel=\"number of observations (beyond initial points)\",\n", + " ylabel=\"Log Hypervolume Difference\",\n", + ")\n", + "ax.legend(loc=\"lower right\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "collapsed": true, + "hidden_ranges": [], + "originalKey": "a926c260-dcc2-4ce9-9c53-7b75456c6c0c", + "showInput": false + }, + "source": [ + "#### Plot the observations colored by iteration\n", + "\n", + "To examine optimization process from another perspective, we plot the collected observations under each algorithm where the color corresponds to the BO iteration at which the point was collected. The plot on the right for $q$NEHVI shows that the $q$NEHVI quickly identifies the pareto front and most of its evaluations are very close to the pareto front. $q$ParEGO also identifies has many observations close to the pareto front, but relies on optimizing random scalarizations, which is a less principled way of optimizing the pareto front compared to $q$NEHVI, which explicitly attempts focuses on improving the pareto front. Sobol generates random points and has few points close to the pareto front" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "customOutput": null, + "executionStartTime": 1668651961290, + "executionStopTime": 1668651961935, + "originalKey": "75cb7f30-5ed2-4a02-bf46-b5c660af6494", + "requestMsgId": "fa568aad-a7cb-43e7-b7ec-eff622a5edc9" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/_j/_hhj7k4913d4jlzgq92bw9b00000gn/T/ipykernel_16702/4269187899.py:7: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed two minor releases later. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap(obj)`` instead.\n", + " cm = plt.get_cmap(\"viridis\")\n" + ] + }, + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Iteration')" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABeQAAAHWCAYAAAAWxYndAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAADoGUlEQVR4nOzdd3gc5bXH8e/MbNWqd3e594YbuIEpNqZjegmhQwKkEHKBhFCS3BCS3ISEAAFSIAkkxBAIhF4N2AZswBj33i3Z6tKuts7cP4QFQnKTJa0k/z7PM89FU8/oxjq7Z945r+E4joOIiIiIiIiIiIiIiLQpM9kBiIiIiIiIiIiIiIgcDlSQFxERERERERERERFpByrIi4iIiIiIiIiIiIi0AxXkRURERERERERERETagQryIiIiIiIiIiIiIiLtQAV5EREREREREREREZF2oIK8iIiIiIiIiIiIiEg7UEFeRERERERERERERKQdqCAvIiIiIiIiIiIiItIOVJAXkX0qKirilFNOabXzbdq0CcMwePTRR1vtnCIiIiIiIiIiIp2BCvIiXdBnn33G2WefTZ8+ffD5fPTo0YMTTjiB++67L9mhiYiIdChFRUUYhtGw5OfnM23aNJ555plWvc6dd97Z6DpfXYqLixvtX11dzf/+7/8yfvx4MjIy8Hq99OnTh/POO48XXnih2Wts2bKFa6+9lqKiIrxeL/n5+ZxxxhnMnz+/Ve9FRERkb/bk1RtuuKHJtrfffhvDMHjqqaca1j366KP7zI/vv/9+w76GYXD99dc3e90951m8eDEAo0aNonfv3jiOs9dYp0yZQkFBAfF4vGHg2K9+9auW3rqIyAFzJTsAEWldCxYsYMaMGfTu3ZurrrqKwsJCtm7dyvvvv89vf/vbZj8YiYiIHM7GjBnD9773PQB27NjBQw89xJw5c3jwwQe59tprW/VaDz74IKmpqU3WZ2ZmNvz3unXrmDVrFps3b+bMM8/kkksuITU1la1bt/Liiy9yyimn8Ne//pWvfe1rDcfMnz+fk046CYArr7ySYcOGUVxczKOPPsq0adP0GUBERNrVI488wq233kr37t0PaP8f//jH9O3bt8n6AQMGtOj6F110Ebfccgvvvvsu06dPb7J906ZNLFy4kOuvvx6XS6UxEWlf+qsj0sX87//+LxkZGSxatKjRl3uAXbt2JScoERGRDqxHjx5cfPHFDT9fcsklDBgwgN/85jeHXJAPBoMEAoGGn88++2xyc3P3un88HufMM8+kpKSEefPmMWXKlEbb77jjDl599VUSiUTDuoqKCs4++2z8fj/z58+nf//+DdtuvPFGZs2axXe+8x3GjRvH5MmTD+l+RERE9mf48OGsXr2an//85/zud787oGNmz57N+PHjWy2GCy+8kFtvvZUnnnii2YL8P/7xDxzH4aKLLmq1a4qIHCi1rBHpYtavX8/w4cObFOMB8vPzG/47Ho/zk5/8hP79++P1eikqKuIHP/gBkUik2fO++uqrjBkzBp/Px7Bhw/j3v//dZJ8NGzZwzjnnkJ2dTUpKCkceeeReX6sXERFpa++99x4TJkzA5/PRv39/HnrooYbWMftSWFjI0KFD2bhxIwBLly7l0ksvpV+/fvh8PgoLC7n88sspKytrdNyec69YsYILL7yQrKwspk6delAxz507l2XLlvGjH/2oSTF+j5kzZzJ79uyGnx966CGKi4v55S9/2agYD+D3+3nssccwDIMf//jHBxWLiIjIlx1oXi0qKuKSSy7hkUceYceOHUmJtVevXkyfPp2nnnqKWCzWZPsTTzxB//79mTRpUhKiE5HDnUbIi3Qxffr0YeHChSxbtowRI0bsdb8rr7ySxx57jLPPPpvvfe97fPDBB9x9992sXLmySd/ctWvXct5553Httdfy9a9/nb/85S+cc845vPzyy5xwwgkAlJSUMHnyZEKhEN/61rfIycnhscce47TTTuOpp57izDPPbNP7FhER+bLPPvuMmTNnkpeXx5133kk8HueOO+6goKBgv8fGYjG2bt1KTk4OAK+99hobNmzgsssuo7CwkOXLl/Pwww+zfPly3n///SaFiHPOOYeBAwfys5/9rEnv2vLy8ibXc7lcDQ/Sn3/+eYBGI/b35/nnn8fn83Huuec2u71v375MnTqVN998k7q6Ovx+/wGfW0REBA4+r/7whz/kr3/96wGPkq+qqqK0tLTROsMwGnLxHuFwuMl+ALW1tU3WXXTRRVx99dW88sornHLKKY3uZdmyZdx+++37jUtEpC2oIC/Sxdx0003Mnj2bMWPGMHHiRKZNm8Zxxx3HjBkzcLvdAHz66ac89thjXHnllTzyyCMAfPOb3yQ/P59f/epXvPXWW8yYMaPhnGvWrOHp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H3vssb3u89JLLzW8Xf1lkydPPuS30o4++mh69uzJf/7zH/x+P3PmzDmk84mIHCoV5EW6mF/96lfMnTuXF198kYcffphoNErv3r355je/yW233dYwSd0f//hH+vXrx6OPPsozzzxDYWEht956K3fccUeTcw4cOJD77ruP73//+6xevZq+ffvy5JNPMmvWrIZ9CgoKWLBgATfffDP33Xcf4XCYUaNG8fzzzzcZcS8iItLWRo0axSuvvMKNN97I7bffTs+ePbnrrrvYuXPnQRXkAZ544gluuOEG7r//fhzHYebMmbz00kt07979oOP6xje+0ez6t956i4KCAqD+QfiSJUv43e9+xzPPPMNLL71ENBqloKCASZMmcccddzS0ttlj2rRpLF26lJ/97GfMnTuXnTt3kpGRweTJk/nzn//M1KlTDzpWERGRPQ41r9522238/e9/J5FINLt9b29x/eUvfznkgrxpmlxwwQX88pe/5NRTT9UE5yKSdIbTXG8LEREREZEu6M477+Suu+5qtr2biIiIHBzlVRGRg6ceEiIiIiIiIiIiIiLSKbz99tsYhsHbb7+d7FBaRAV5ERERERERERERkcPUo48+imEYLF68GIAXX3yRO++8M7lBAQ888ACPPvpossNodSrIi4iIiIiIiIiIiAhQX5C/6667kh3GXgvy06dPp66ujunTp7d/UK2g0xXk77//foqKivD5fEyaNIkPP/xwn/vPnTuXIUOG4PP5GDlyJC+++GI7RSoiItKxKIeK1Pe6VZ9bETlYyqEizVNeFZED5TgOdXV1rXIu0zTx+XyYZqcrbQOdrCD/5JNPcuONN3LHHXfw8ccfM3r0aGbNmsWuXbua3X/BggVccMEFXHHFFXzyySecccYZnHHGGSxbtqydIxcREUku5VAREZGWUQ4VEZHDyaWXXsr9998PgGEYDcsetm1z7733Mnz4cHw+HwUFBVxzzTVUVFQ0Ok9RURGnnHIKr7zyCuPHj8fv9/PQQw8B8Je//IVjjz2W/Px8vF4vw4YN48EHH2xy/PLly5k3b15DDMcccwyw9x7yc+fOZdy4cfj9fnJzc7n44ovZvn17k/tLTU1l+/btnHHGGaSmppKXl8dNN91EIpFojV/hfhlOJ3qUOWnSJCZMmMDvf/97oP5/AL169eKGG27glltuabL/eeedRzAY5L///W/DuiOPPJIxY8bwhz/8od3iFhERSTblUBERkZZRDhURka7u0Ucf5bLLLmPRokXEYjHuuOMOXnvtNf72t7817HPxxRcDcNVVVzXsP27cODZu3Mjvf/97hg0bxvz583G73UB9Qd3tdlNWVsY111xDUVERgwcP5phjjmHixIkMHz6c0aNH43K5eP7553n11Vf5/e9/z3XXXQfAs88+yw033EBqaio//OEPASgoKOCEE07g7bffZsaMGbz11lsNRfo9MU2YMIELL7yQkpISfvvb31JQUMAnn3xCZmYmUF+Qf/LJJ+nfv39Djn/99dd5+umneeCBB/jGN77R5r9vV5tfoZVEo1E++ugjbr311oZ1pmly/PHHs3DhwmaPWbhwITfeeGOjdbNmzeLZZ5/d63UikQiRSKThZ9u2KS8vJycnp9HTIBEROXw4jkNNTQ3du3fvlK/EKYeKiEiyKIfWUw4VEZGDlawcetRRRzFo0CBee+21hiL8Hu+99x5//OMfefzxx7nwwgsb1s+YMYMTTzyRuXPnNlq/bt06Xn75ZWbNmtXoPPPmzcPv9zf8fP3113PiiSfy61//uqEgf8YZZ3Dbbbc1jHTfl1gsxs0338yIESN455138Pl8AEydOpVTTjmF3/zmN4164ofDYc477zx+9KMfAXDttddyxBFH8Kc//UkF+S8rLS0lkUhQUFDQaH1BQQGrVq1q9pji4uJm9y8uLt7rde6+++4OMWmBiIh0PFu3bqVnz57JDuOgKYeKiEiyKYcqh4qISMt0pBw6d+5cMjIyOOGEEygtLW1YP27cOFJTU3nrrbcaFeT79u3bpBgPNCrGV1VVEYvFOProo3nllVeoqqoiIyPjoOJavHgxu3bt4s4772woxgOcfPLJDBkyhBdeeKFJnr322msb/Txt2rRGbwS0pU5TkG8vt956a6PRDFVVVfTu3ZutW7eSnp6exMhEOq/q6joeeuQtVq7cTiyWAMPAAHJz07ji8ukMH9YxEovI3lRXV9OrVy/S0tKSHUqHphzatayuXsOfNz6G3/LhMetfOw1HE+zYXYVhgOGPwrZusKEIAMeBsspajh43gG+fd3SbxDR/9/O8X/YSAVcmptF4lFDCjlGXCHJq9ysYmD62Ta7fHn71vcdZ+OZK6vqlE87yYkZtACJZLhwLjARggMvjontuBj6PRXltHZkBP7+47CS++8jz1EVjZAbqv4g4wJpABbk9tzCyx1Zs24Av/e7qm1c6eCyb3ikjuLz/ze1+z9K1KYceGOVQkfrRwD//zhOsW7aV7Px0DMOgri5KybYKDNPAsR0MA3r0ycXltnBsh7Ld1Rx7+hFc/K2ZzZ5z+/pd/O8VDxGLxMjISW1448RxHKrKg7jcFjc/dAVFQ3o0OTacCPPzlf/H9rrd2I7V5LMHxIjbFtnWOH457vxmtoscmo6YQ9euXUtVVRX5+fnNbv/q/Cp9+/Ztdr/58+dzxx13sHDhQkKhUKNtLSnIb968GYDBgwc32TZkyBDee++9Rut8Ph95eXmN1mVlZTXpg99WOk1BPjc3F8uyKCkpabS+pKSEwsLCZo8pLCw8qP0BvF4vXq+3yfr09HR9EBJpAdt2+M3v3mTFil2kpqXh87oxDIN4PEFlVYiH/7iAO390Bn365CY7VJH96qyvjCuHSkuMSzuC9+s+ZE3NWtyWB6/pwZtqECRGnR2CaACjrAjDm0I8nqCipo6srEzOm3Vkm/3/+yjfLDYkPqE6Vo7XlY7LdOM4DjEnSjQepl/KEEZ3n4zr8wcInVHvnvl8bG3ALHVIZHtxUgzMmA1xk6jf+ryCbpCW5iclNZWaujCWx88Fx0+if+8enH3MBP725keEbJM0vxfTMOiGwaYqmyFFFaR5w9iOiYOB4xiAg9uycRl+Tu93Cemp+rcqbUM5VDlU5ECcfflx3Hf70wSr4mRkB0hP8xFMjVFXGwEDMrIDpKQEiMcSVFYGyc3L4dQLpu/138l/33iHeJ1DXrfcJn+H8vJ9lO6s5JPXVzNq4tBG25Zv2MkL761gZzgD17BKbNMBTFymi/rH3XEc3ESjBVwy5FgyMzLb5PchAh0rh9q2TX5+Po8//niz279a5P7ySPg91q9fz3HHHceQIUP49a9/Ta9evfB4PLz44ov85je/wbbtNon9yyzLavNr7EuneXzn8XgYN24cb7zxRsM627Z54403OOqoo5o95qijjmq0P8Brr7221/1FpPV9tmwrK1ZsJy3dj9/naUgkLpdFTnYq1VUhXn1tWZKjFOnalEOlJUzD5LKiSxieMZSYE6MyXkVlrBJ/KgRIJ7SoP6U7E5RW1lJZU0d+Vio3X3Icg3rn7f/kLZTpyWVOr+vI9XWnLlFLTaycmng5MTtMUeow5vT8ZqcuxgPk98gixXIIxGzSNtZgRm1st4kRB1fYxsDANMBymZTWBHG7XHzt2HHMOWokABfNGMu500ZjGga7q4LsrgqSKE/QrSaLFVvGUhNNwTAcLCOBy4zjtmzcZgpzel5H39SmI4pEDnfKoSLta9y0QVz+/ZNIy0ihorSW8t3VuN0uvH4PPr+n/o28XdVUVYTI757Ft356Fr36Nz9SF2DNks1YltlsQdMwDFxuizVLNjda//qHa7jtwRd5c/FaQmvyiK3sDjETgzgJJ4JDlIQDsXghV/abw7T8Qa3+exBJtr09BOjfvz9lZWVMmTKF448/vskyevTo/Z77+eefJxKJ8Nxzz3HNNddw0kkncfzxxzdbvD/QhxF9+vQBYPXq1U22rV69umF7R9FpRsgD3HjjjXz9619n/PjxTJw4kXvvvZdgMMhll10GwCWXXEKPHj24++67Afj2t7/N0Ucfzf/93/9x8skn889//pPFixfz8MMPJ/M2RA4rSz7dQjxu4/M2LZAYhoHH6+b9D9ZxxeVHY5od56mvSFejHCotkepO5Zp+V7E5tIU1NWtJOAm6+7sxPH0YWwZW8dHKrURjcbrnZ3DUiKJm/9a3tm7+vlzW9w42BpdREt6KaVj0ThlED/+ADjV6qKUmzhzFM394Azthk+V2UbclSCjVjeO3cNVAfHcNI04ZzcCjBpAZ8DFlWBF5GakNx1umyVUnTuLUScNYsHIT1aEIeRkBpg7ri+E2eGPzWlYFFxM1NpDt8zE4fQhH58/GazX9AiQi9ZRDRdrX9JNGc8TUQSx6exVlJVWkpPk4YspA6oJRln64gXgsTo+iPI6YOhDPfj57WC4Tp74/W7Mc28GyvhirWlJWw0P/nk80niAv6/MWN9sHQGkvgjk7iKWHGNy7O0f1HMfx3caQ7lb+lK4pEAgAUFlZSWZmZsP6c889lwceeICf/OQn/OxnP2t0TDwep7a2ttH+zdkzOv3L/zarqqr4y1/+0mwclZWV+413/Pjx5Ofn84c//IHLL7+84a2zl156iZUrV3L77bfv9xztqVMV5M877zx2797N7bffTnFxMWPGjOHll19umDBny5YtjWYdnjx5Mk888QS33XYbP/jBDxg4cCDPPvssI0aMSNYtiBx2IpH4PrdbpkEslsC2bUwzua8MiXRlyqHSUoZhUBToQ1Gg8aiSfj1y6NcjJykxuUw3A9PGMjCt8/aK35vC3rnMvmQa/3n4TRI1IdIzUsi0IbwrRKi2jqL+Bdx0zUlk5e+7hUVhVhpzJo9ssv7MgSMA/TsWORjKoSLtLzXdz4zTmub5vkO6HdR5Rk0ZzIoP13/+fbNxkwjbdrATNqOnDWlY99ZHa6kJRcjNSm38oD/iJbCjL7uX15BW25c5k/XGi3Rt48aNA+Bb3/oWs2bNwrIszj//fI4++miuueYa7r77bpYsWcLMmTNxu92sXbuWuXPn8tvf/pazzz57n+eeOXMmHo+HU089lWuuuYba2loeeeQR8vPz2blzZ5M4HnzwQX76058yYMAA8vPzOfbYY5uc0+12c88993DZZZdx9NFHc8EFF1BSUsJvf/tbioqK+O53v9t6v5xW0KkK8gDXX389119/fbPb3n777SbrzjnnHM4555w2jkpE9qZbYf1EHLbtNDsCPhKN07dvHi6XivEibU05VKRzOPv6maRmpPDS396loqQK23bw+j1MmjmKi75/yn6L8SLS+pRDRTqnqacewct/f4/ykiqy8jMaRsPbCZvyXVVk5KYx7bQjGvbfuKMMAHMvb9153C7WbtnV7DaRrmTOnDnccMMN/POf/+Tvf/87juNw/vnnA/CHP/yBcePG8dBDD/GDH/wAl8tFUVERF198MVOmTNnvuQcPHsxTTz3Fbbfdxk033URhYSHf+MY3yMvL4/LLL2+07+23387mzZv5xS9+QU1NDUcffXSzBXmASy+9lJSUFH7+859z8803EwgEOPPMM7nnnnv2O2q/vRnOvt7dEaqrq8nIyKCqqkqT6Yi0QHl5LTfe9AShuhhZmSmNRhlEIjFqgxGuuuJoZp7QdBSfSEehXNAy+r2JHJpIXZR1S7cQi8Tp1jePgl7JeSNB5FAoF7SMfm8irWfFovXc/z9PUF5SheOAAWBAZl4637z7fEZO/qIH/K/+/iavf7iG/Oy0Zs9VUR0iLyuVP//ogvYJXg5rygVdV6cbIS8inUt2dipfv2Qaf/zT25SV1eLzezBNg3BdFNtxmDihHzOOGZbsMEVERDocr9/D8EkDkh2GiIhIpzZsQn/ufvq7LHhxCas/3ojjOAwaU8Tkk8eSmdu48D52cE/eXLSWWDyB+ytvcTuOQyye4KiRRe0YvYh0RSrIi0ibm3HMULKzA7z44qesWLmDRMKmW/csTjhuODNPGIHbrXY1IiIiIiIi0jbSs1M58eKpnHjx1H3uN2VUX+Z2W8KmHeVkpac0FOUTCZvy6hAZqX5mHTlkn+cQEdkfFeRFDkOOXQPx9YABrgEYZqDNrzl6VG9Gj+pNMBghFkuQluZrNJu9iIiIiIiISDL5vG5uu3wmP/vLa2zaWY5t13d5NgyDnIwUbrzwGHoXZiU5ShHp7FQNEzmMOHYIu+Y+nPLzcSpvqF/KL8AJ/gnHibZLDIGAl8zMFBXjRUREREREpMPpmZ/JvTeeyQ8uPYHTpo/g5KnDuP7caTx48zkcMaRXssMTkQN09913M2HCBNLS0sjPz+eMM85g9erVjfYJh8Ncd9115OTkkJqayllnnUVJSck+z+s4DrfffjvdunXD7/dz/PHHs3bt2oOKTRUxkcOE40Rxqn8Edf8AOwRGJhjpYFfXF+Srf4bj2MkOU0RERERERCSpPG4XU8f045tnT+WGc6dz8pRhpAV8yQ5LRA7CvHnzuO6663j//fd57bXXiMVizJw5k2Aw2LDPd7/7XZ5//nnmzp3LvHnz2LFjB3PmzNnneX/xi1/wu9/9jj/84Q988MEHBAIBZs2aRTgcPuDYDMdxnBbf2WFAMxpLV+GEX8Gp/kl9Ed74ygcJJwROCCPzVxieickJUKQDUy5oGf3eREREuaBl9HsT6XhKamr5bEcJCcdmSH4efbIzkx2SdHHKBa1r9+7d5OfnM2/ePKZPn05VVRV5eXk88cQTnH322QCsWrWKoUOHsnDhQo488sgm53Ach+7du/O9732Pm266CYCqqioKCgp49NFHOf/88w8oFvWQFzlMOOFXAadpMR7ASAGnGif8qgryIiIiIiIiIp+rjUS5792FvLlmA8FoFAfwu11M7N2T782YSl5q28/JJtIVhMNhotFDa5fsOA6GYTRa5/V68Xq9+z22qqoKgOzsbAA++ugjYrEYxx9/fMM+Q4YMoXfv3nstyG/cuJHi4uJGx2RkZDBp0iQWLlyogryIfEWiGHDvYwfz831EREREREREJG7b3PXyG8zfuIUUt5v81AAGEIzGeGf9JkpqarnvrFNIPYBioMjhLBwO07dPKsW7Eod0ntTUVGpraxutu+OOO7jzzjv3eZxt23znO99hypQpjBgxAoDi4mI8Hg+ZmZmN9i0oKKC4uPn62J71BQUFB3xMc1SQFzlcmFmQ2LSPHWwwc1vlUol4gg9e/Jh5/1rAjnXFpGSkMPm0CRx97lFk5mW0yjVERES6IuVQERGRlmmLHPrB5q28v3kbGT4vfvcXA9xSvR68Los1u0p5ZdU6zho9vLVuQ6RLikajFO9KsPGjPqSntWxK0+oam77jNrN169ZGLXwOZHT8ddddx7Jly3jvvfdadO3WpoK8yGHC8M3EiX0CThQMT+ONThgwMXwzDvk6sWiMB77zKO8/vxg7YePyuti9rYyNSzfz5j/e4/t/uY6eA7sd8nVERES6GuVQERGRlmmrHPr22o0kbLtRMX4Pt2WBYfDqqrUqyIscoEBq/dISic9nQU1PTz+onvrXX389//3vf3nnnXfo2bNnw/rCwkKi0SiVlZWNRsmXlJRQWFjY7Ln2rC8pKaFbt26NjhkzZswBx9SyRxIi0vl4jwf3MHDKwK4Bx65f7GpwKsEzATyTD/kyL//pTRb8ZxH+NB85PbLJyE0nu1sWWYWZ7FxfzAPf/jO2bR/6/YiIiHQxyqEiItIZ2LbNysUbePe5j/joreWEg5Fkh9RmObQiVIf5lX7VX+YxTcpCdYcSuoi0EcdxuP7663nmmWd488036du3b6Pt48aNw+1288YbbzSsW716NVu2bOGoo45q9px9+/alsLCw0THV1dV88MEHez2mORohL3KYMMwUyLgHp+Y3EF0ATglggBEA7ykYqd/CMPbVY37/4rE4rz/+DqZp4As0njzWclmk5aSxaflWVixcw4gpQw7pWiIiIl2JcqiIiHQGKxat528/f45t64qJReOYpklWfjqnXTmDmRdOaTLZYntoyxxakJ6K7TjNTiQJEE3YdEtPO6T4RQ4nNg42TouPPRjXXXcdTzzxBP/5z39IS0tr6PGekZGB3+8nIyODK664ghtvvJHs7GzS09O54YYbOOqooxpN6DpkyBDuvvtuzjzzTAzD4Dvf+Q4//elPGThwIH379uVHP/oR3bt354wzzjjg2FSQFzmMGGY2RsZPcOJbIb4KMME9HMNq/lWcg7V7WxllOyrwp/mb3e7xuaktr2Xj0s0qJoiIiHyJcqiIiHR0az/dzG++9Ri1VSHSsgJkeN0k4jZVZbX87Z7niccSnHzp0e0eV1vm0OMH9ee/y1cTjMZI9X7R+tXtCeIJFNMjI8KkwfnE7Chu07OPM4kIgI1NS9/3PNgjH3zwQQCOOeaYRuv/8pe/cOmllwLwm9/8BtM0Oeuss4hEIsyaNYsHHnig0f6rV6+mqqqq4ef/+Z//IRgMcvXVV1NZWcnUqVN5+eWX8fkaPxDcFxXkRQ5DhqsXuHq1+nkty8QwDBxn308tTUvdskRERL5MOVRERDq6Zx96g5qKILndsxpGi7vcFll56VSW1vD8n97i6DMnkJqR0q5xtWUOHd2jG7OGDOCF5WuIJOKkeS3yeq0gM3snppnAY1lsNYv54/qFzOx2Mf1TR7b0NkSkle3vbwKAz+fj/vvv5/777z/g8xiGwY9//GN+/OMftzg2faIXkVaT2zOH7v0LCVU330MvHIzg9roZMmlgO0cmIiLSsSmHiohIR1a2s5IVH64nJd3fbOuWtMwUqsprWfLOynaPrS1zqGkYfP/YaVw9eTx+vxtf70/IyttK3LCpi7sJxd2E4y4qY7t5bttDbA+tP9TbEenSEo5zSEtXoYK8iLQa0zQ58fJjMU2D2opgo6eIsUiM2sogwyYPpt+oPkmMUkREpONRDhURkY6stjpEIp7A7W6+0YLlsjAwqKkMtXNkbZ9D3ZbF8cMGkFIYomdOGY7jwsKHx3JhO1BWFyYUdRO2g3xY/mpr3ZZIl7Snh3xLl65CLWtEpFUdfe5RFG/axYsPv07Z9nIM08CxHUzTZMjEAXzjN5cmZaIfERGRjk45VEREOqqMnDRcHhfRSAyPz91kezyWAAOy8tKTEF3b59AnVy/FtrbisRxiCQ97TuUyDBI2VEUiBDxeNtYuI5wI4rMCrXRnIl2LjUOinSZ17chUkBeRVmWaJufffAYTZ4/lvX9/wM4NJQQyUhg/awzjZo7G42364U1ERESUQ0VEpOPKzE3jiGOG8d7zH5GS5sM0v2i44DgO1eW1ZOdnMGZ6ciYeb8scGrMTvLJ5LZlpDg4G0Liwb5kGMdsmHE/g8phE7YgK8iKyTyrIi0irMwyD/qOL6D+6KNmhiIiIdCrKoSIi0lHNufZ4Vi3eQOmOClLS/Xh9HhLxBDWVQbw+D+ffeBK+FG/S4murHBqOx6mLx0hN+DFwAJvmOkAnnBg+M4MUK61Vry/SlRxK65muNEJePeRFRERERERERGSfuvfL55aHr2LCCSNxbJvqilrCoQh9h/Xkul9cwNRTjkh2iG0ixeUmzeOlpDqbeMKLy4rBVwqDBjaGYTMqcwouU2+0ieyNJnWtpxHyIiIiIiIiIiKyXz0HFPC9+y6lZGsZZTsr8ad66TOke6MWNl2NZZqc2m8IjyxbxPbywfTKXY7bipCwLRxMDOL43DY9/EOYkD0z2eGKdGj250tLj+0qVJAXkQ7Ddmw+qVjLvF1L2Fa3ixTLx6ScYUzPG02GJzXZ4YmIiHRIsViCxUs38/4nG6kJhumWl8H0IwcyqG++JoEVEZE2UdArh4JeOckOo92cM3Akb2/bwNoyCEZH0DN7GwFvJRAnZrvo4x3H1/pejt+l760isn8qyItIh5BwbP64/nnm7f6EuG3jNi0Sjs2a2q28UbKYm4ZcSM+UvGSHKSIi0qFU19Rxzx9eZfmanSQSNqZpYNsOr7yzgpNmjODSc47CNFWUFxERORQ5/hR+e8yp3L/kfeZt38gnW9LxuGL0SPVzdv8JnDVwjB6CixyABA6JFvaCb+lxHZEK8iLSIbxWvIi3dn2Mz/KS6f5iIqCEY7OjrpT71z7N/466GtPouq9CioiIHKwH/jaPpSu3k5Hux+up/2jvOA7BUITnX19Kt/x0Zs8YkeQoRUREOr+ClFR+PPl4SkK1bK2pwmtZDMnOw21ayQ5NpNNIOPVLS4/tKlTZEpGkSzg2r5csBiDF8jbaZhkmae4Am0PFLKvamIzwREREOqQt28v56LOtBFK8DcV4AMMwSA34cID/vrmMeKIrddwUERFJroKUVMYX9GBkbqGK8SIHyT7EpavQCHkRaVexaIwPX/yEeXMXUrJpF+m56YyeM5LigWX4v1KM38NruqmN17G+djujMvu3c8QiIiIdw1dzaCgvg+oUD90KMpvdP5DioWR3NTtKKundPbt9gxUREelAmvseOuX0CUw7axKBjECywxORw4wK8iLSbiJ1EX533R/56NVPcWwHl8dF8abdrFy7nrr/TSMnPwuaGWDgOA7gYKCefCIicnhqLodWBSPU9cplR6iEbkX5uDyNP9obhoHjONh2F3q/V0RE5CDt7Xvo6kXrePMf7/E/j15Hbo/DZ4JakWSyMUi0sLZjd6GakAryItJunr3vZRa9vIS0rADelC9Gw8fiMYKbo5R5Kkgt8Dc5LmLHcBkuBqf3bs9wRUREOozmcqjb4yJoQDQSY/e2Mrr1LeDL31NCdVGyMlLolp+RpKhFRESSb2/fQ+OxOBuXbeHh7/+NWx//tiZlFWkHtlO/tPTYrkI95EWkXdQFw7z1z/dwe1yNPgQBuF1u0heZxCMJKkM1n4+Irxe3E9TEQwxM68ngtF7tHbaIiEjS7S2H+qNx/LEEjttNXTBCNBxt2BaOxIjHbWZOG9qov7yIiMjhZF/fQ11uF6mZAVa8v4bNK7YlKUIRORypIC8i7WLHumKqy2rxpzUdAQ+QtdqD59Uo0WiMsmg15dFqSiNVVMWD9EvtzvUD52Aa+pMlIiKHn73lUAPoXlGLN5Eg4bIoLa+lqrqO0rJagsEIR47tyxmzxiQlZhERkY5gf99DfQEv0booGz7d1L6BiRymEp+3rGnp0lVouIyItAvTNDAMwGn+HSMDA99/Ysw5/ijq+hlsC+0ixeVjQvZQJuUMw2d52jdgERGRDmJfOdSTsOmzu4odUZu8yYNwp/npXpDBsZMHM3lcf9zuZiZnEREROUzs73toPQPD1OAvkfZwKIV1FeRFRA5Sz8HdyS7MpHRHOR5/0+J6qKYOf8DLMUdMoFu/giREKCIi0jHtL4dGq0LkJBzuvuk05VAREZEvOZDvob6AlyETByQhOpHDj+0Y2E4LJ3Vt4XEdkR4Biki7cHvczLx0BnbCIVgdatQnPhqOUlddxxEnjFIhQURE5CuUQ0VERFpGOVREOiKNkBeRdnPSlcdRvGkXb/9zPmXV5RiWiZOwMS2TEVOHcsXPLkx2iCIiIh2ScqiIiEjLKIeKdBxqWVNPBXkRaTeWy+LKuy9iyukTmP/sh5Rs3k1GbjoTTzqCI44fidvjTnaIIiIiHZJyqIiISMsoh4p0HAlMEi1s2JJo5ViSSQV5EWlzZXUhNlVX4LEsBmXlMuyowQw7anCywxIREelUDMNQDhUREWkB5VCRjsE5hB7yThfqIa+CvIi0mYpwHfd/+j5vbFlHKB7DMAwKUlI5f/Aozhs0CtPoOn9MRURERERERERE9kcFeRFpE9XRCN+Z91+WlZaQ4naT6fNhOw7FoRp+8/F8SutC3DDmqGSHKSIiIiIiIiIi7UA95Ou1rGmPiMh+PLtuBcvLdpHrTyHD68NtWngtF3n+AF7L4l9rlrKhqjzZYYqIiIiIiIiISDtIOOYhLV1F17kTEelQnt+wEsswcFtWk23pHi918Tivbl6bhMhERERERERERESSQy1rRKTV2Y5DaV0QbzPFeKifUMdxHErrQu0cmYiIiIiIiIiIJIONgd3C8eE2TitHkzwqyItIqzMNg3Svj5JQbbPbHcfBMCDT69vrObbUVrCqchemYTA8q5BuKeltFa6IiMhhLxiPsLOuErfpoldKFqahF2lFREREpHWph3w9FeRFpE3MLhrEI8sWkbBtLLPxl/pgLIbHcjGjV78mx5WFg/z6s3ksKNlIKB4DIOD2cGy3gXx75HTS3N52iV9ERORgOY7D9vW7KN1RQUqaj/6jemNZHbuwXRML8/jGBbxevJzaWATTMOgdyOHs3hM4rnAYhtF1vviIiIiISHIdSi/4hKMR8iIi+3TWwBG8tmUdm6oqSPd6SXG5sR2HmmiEcCLBKX2HMCw7v9ExoXiUmz/8L5+V7yTg9pDvTwWgNhbh+S3L2R2p5ZcTT8Ozl1Y4IiIiybJxxTb+8esXWf3xRmKROJbLpFtRHmdeczxHnTQm2eE1KxiP8KNPn+azim14LRcBlxfbsVlXs4tfrXiJskgt5xVNSnaYIiIiIiJdSscesiMinVaeP8Bvjj6Zo7r3JppIUByqZXddCI/l4uKhY7h14tFNRt29vn0Nyyp2ku1LIc3txTSM+vY3Hh+ZXj+Ldm9hfsnGJN2RiIhI8zav3sEvv/Fnlr63GrfbRWZeOv5UH9vWlfCH257knWcXJzvEZr24/VOWVW4ny5tCpicFr+XC7/KQ70vDMAz+vnEBO+sqkx2miIiIiHQR9T3kW750FRohLyJtpndaJvcdcyprK8tYX1WO2zQ5Ir872b6UZvd/ZdtqADxm0xHwPstFZdThzR1rmdF9QJvGLSIicjCeefB1yndVkdMtC9Os/6JgWR68hR7KS6p48ncvM3HmSHwpHaftmuM4vLRjKaYBHrPpV4JMt59dkRrmlazi/KIjkxChiIiIiHQ1NiYJTeqqgryItC3DMBiUlcugrNz97lseCeJuphi/h4VBaTjYmuGJiIgckopd1Xw6fzUpqf6GYvyXpWenUrGriiXvrOLIE0cnIcLmxewEpZFavKa72e173mLbFa5uz7BEREREpAtTD/l6alkjIh1Gt5QMYnZir9sTOHRLSW/HiERERPatqqyGeDSB29v8OBeX2wKnfr+OxGWa+Ew3caf5vOs4DjgQcHWcUf0iIiIiIgfjnXfe4dRTT6V79+4YhsGzzz7baLthGM0uv/zlL/d6zjvvvLPJ/kOGDDmouFSQF5EOY1bPwRgYhBOxJttC8Shuw2Rmz8FJiExERKR5aZkBXG6LWDTe7PZEvL7gnZ6V2p5h7ZdpmBxbOJRIIo7t2E22hxJRPJbF5LyBSYhORESk83O60GhekdZiYx7ScrCCwSCjR4/m/vvvb3b7zp07Gy1//vOfMQyDs846a5/nHT58eKPj3nvvvYOKSy1rRKTDmNFtIK8WrOa9ko14zTipbg+OA7XxCDE7wayeQ5iY1zvZYYqIiDTI6ZbJiCMHsOj1ZaSk+ppMWF5dESQjN43R0w5u1Ex7OLXnWOaVrGJXuJoMTwpe04UDBOMRQokoxxQMYUh6t2SHKSIi0mnEYgneeXcVb7y5gm3bK/D7LI47LoXJR/WiW+EQLGv/rVxFurKEY5BwWjY5657jqqsbt1T0er14vc2/1Tl79mxmz56913MWFhY2+vk///kPM2bMoF+/fvuMxeVyNTn2YGiEvIh0GB7L4qfjT+Li/uMIuD1URsNUx8JkePxcOfhIfjjmBEyj68yqLSIiXcOZ1x5PRm4apTsrCYciOI5DLBqnvKQKwzCY843jSUnzJTvMJnqkZPGTMWcxKL0bwXiE3ZEadkfqW+uc3GM0Nw2b3eQBg4iIiDQvGo1z729f4Q8Pv8WatTsZPHg5Z5/7JwYMupPyyivYvP0UyivvIJ4oSXaoIkmT+HxS15YuAL169SIjI6Nhufvuu1sltpKSEl544QWuuOKK/e67du1aunfvTr9+/bjooovYsmXLQV1LI+RFJCki8RJ2hV6lJvIZYJLhG0Neygn4XTncMGIalw6awLrqMkwDBqTnEXB7kh2yiIhIs/qN6MWNv7uUv//iOTav2kFtZQjLZZLbPZMzrj6OY86a2OYxVFXXMX/+Gj5btpVEwmHggAKmTx9MQX7GPo8bkFbA/RO/xtKKbWwOluI2LcZm96GbP7PNYxYREelKXn7lMz5YtJ7UVB8TJ3zKlGlvYJoJwnU+wmFI2DW4rH8TjS4jL+chjZYXaaGtW7eSnv7F/IJ7Gx1/sB577DHS0tKYM2fOPvebNGkSjz76KIMHD2bnzp3cddddTJs2jWXLlpGWlnZA11JBXkTaXXndfNaW3000UYHRsO5dtlf/g8E5d5DhO4I0j4+xuT2SGqeIiMiBGnxEET/+xw2sW7qFsp2VBNL9DB7XF4/X3ebXXrO2mF/f+zK7d9fU51UDPvp4Ey+8uISrrpzBlMn77gNvGiZjsnszJltt4URERFoikbB5/Y3lmIZBVlaEiUe+AzjU1u4pGjrU1lqkBgJgrKUm+DiZ6d9OZsgiSWE7JrbTsoYt9ufzMqSnpzcqyLeWP//5z1x00UX4fPt+s/XLLXBGjRrFpEmT6NOnD//6178OaHQ9qCAvIm3Itm02riqmsqyW1Aw/A4b3IGpvZ035/xJPVOO18jCM+j/EjmMTTexmVdmdjCn4E15XXouu6TgO6z7ZyDtPvc+Wldvwp/oYd8JoJp8+nkBGoDVvT0REpBHDMBg4ug8DR/dpt2vW1IQbivHZWQEsa09edaioCPLQI2/Ro0cWRX32PwpPOVRERKRlKiuDFO+sIBKMkJO2FLcrSG1NKqbpgGE0tICLRsHn9xCse56MtG9gGHoTXA4vX249c/DHtt1Eye+++y6rV6/mySefPOhjMzMzGTRoEOvWrTvgY1SQFznMBKNR3l63kSXbdpJwHIbk53L84AFkp/hb9TorPt7EPx94g81rS4jFErhcJt375HLq93bhZFfhsfIb9aU1DBOPlUc0sZvdoVfpmX7RQV/TcRye+vXzPHf/K0TqIpguCydh8/Ebn/Hin17n+3++ju79Wz7phoiISEfz3vw1lH6lGA/1DweysgKUltXw+hvLufLyo/d5HuVQERGRlnEchxcffo2K4kocxyElpQrHhljExjBjuD1uDPPz776GgYEPx67FtqvVtkakg/jTn/7EuHHjGD169EEfW1tby/r16/na1752wMdoUleRw8iaXaVc/sS/+dmrb/Pf5at5aeUa7p23gK///SkWbjq4CSj2ZeUnm/nNrXNZu3w7Xr+b7LxU/AEvW9bvYuO2N4lFnWYniasfLW9QGV7UousufH4xz/zuRTAgp0c22YWZ5PTIJqsgg21rdvLbbz5CIp44xLsTERHpOFas3I6D06gYv4dhGLhdLj79dP85XjlURESkZRY+v5gX//Aq7mgUw20RifgxTDAMcGyHeDSObdsYhoHf78YhhmG4MYyUZIcu0u5sIOEYLVrsFlyvtraWJUuWsGTJEgA2btzIkiVLGk3CWl1dzdy5c7nyyiubPcdxxx3H73//+4afb7rpJubNm8emTZtYsGABZ555JpZlccEFFxxwXCrIixwmasIRfvjCa2yprCInJYVu6al0S0slL5BCeSjEj195i03lFYd8HcdxmPvw29RW1ZFbkI7P78E0Tbw+Nzn5aTjYBGsjOHt708gwcFrwZ9ZxHF597G0S8QRp2amNCv6WyyIjN50tK7bx6bwVLbwzERGRjieRsIGmD7n3MAxI2PvOq8qhIiIiLbMnh9rxBHnYmA4sXdqHaMSFPyWKYRjYjkMiYeP3u/F6LBwnhN93PKapgrwcfmzMQ1oO1uLFixk7dixjx44F4MYbb2Ts2LHcfvvtDfv885//xHGcvRbU169fT2lpacPP27Zt44ILLmDw4MGce+655OTk8P7775OXd+Ctl9WyRuQw8cba9eyoqiYvkILL/OKPmGWa5KcGKKkN8t/lq7l+2pGHdJ1tG3azYeUOUjP8TUbBG4ZB9fZCsnquoi4UISXQeCZsx3FwHJt078iDvm6oOsTGz7bgS21+8g2Pz02N7bBm8XqOOO7gzy8iItIRDRpYyAcfbsC2HUyzcd51HIdoLMGwId33eQ7lUBERkZb5cg4NxOPkhsOUlqbz/sLBTDt6OYblEKr14DJN8nI9JOwSLCuftMDFyQ5dJCkSjkmihZO6tuS4Y445BmevI0LrXX311Vx99dV73b5p06ZGP//zn/886Di+SiPkRQ4Ti7dsx4FGxfg9DMPAbZos2HjobWuqKoLE4wk8HqvZ7TuWDCIedpGgotEfRcdxiNqluM008lNOPOjr7ufv6569DnRHERGRTmHa1MGkp/upqAw2yavV1XV4vS6OO3b4Ps+hHCoiItIyX02NGbEovWtrWPivUcx/eQR21CIrvZbcnDoMM4THPZzcrHtxu/snJ2AR6RBUkBc5TEQTiX280F5flI8lDr03bEZWAJfLIhZt/lzlWzJZ8sxoTMNDJLGLaKKcaKKMaGIXluGnf9ZN+N29Dvq6gYwUeg/tQbg23Oz2WCSGYZj0H9v3oM8tIiLSUeXkpPLNa48jkOKltKyWisoglVUhSstqMEyDC88/iqFD9z1CXjlURESkZZrLoW7HIbMuxvJ/DeHxG4/l2XtGES67lrzshynIfRyvR2+byeHLxjikpatQQV7kMDGkIA8HsPcyui2aSDC8W8EhX6dnvzz6De1OTXVdk9eCHMehuiJEzebxjOl2H91ST8fn6obP1YNuaWczKv8B8gLHt+i6hmEw85JjMC2TYFWo0bZEwqZqdzU9BhYyZsa+RwmKiIh0NhPG9+UnPz6L004dS7fCTPLy0jhuxjB+9MPTOeXkMfs9XjlURESkZfaXQ4s3RKjaOZahR1yPzzsOw1AZTg5ve1rWtHTpKtRDXuQwceKQgTz5yWeUBUPkBlIa9XevqgvjdVmcMnzwIV/HMAzOufoYfn3zvygtqSYtw4/H6yIeTVBdVYfX5+bcq48hMzCMzEDrjgyYcuZENi7bwit/eYvS7eW43BaJuI3jOBQU5fOt+6/E7XG36jVFREQ6gl49s/n616by9a9NbdHxyqEiInK4CUdiLF66hdKKWlJTvIwf1ZvM9IOfaFU5VOTAJTBJtHB8eEuP64hUkBc5THTPSOfGY6bwyzffpaQ2iNeyMAyDcDyOx7L4+sSxjOu571faD9TQsX34zt3n8M8H3mDzuhJqq8O4XCa9+uVx1pVHM/GYoa1yna8yTZOv3X4ORxw3knlzF7J5+Va8AR8TZ49l2lmTyMzLaJPrioiIdHbKoSIicjh5b9E6/vyvBZRVBOtXOJAa8HLaCaM45+RxTSZK35eW5lDHSUB0MSTWAS7wjMNwDWiFuxORjk4FeZHDyKwhA+mVmcFzy1bx/qYtJByHo4p6c+qIIUzq07PRqPlDNXxcEXc9chkbVu6ksqyWtAw/A0b0xLLa9ommYRiMmDqUEVPbpugvIiLSVSmHiojI4WDx0s387i9vEY3GyUz343JZ2LZNTW2Efzy3GNM0OefkIw7qnAebQ53Yapyan0F8I5AAHDC8OJ4jMdJuwTD1IFy6JtsxsJ2W1Z5aelxHpIK8yGFmWGE+wwrz2+VapmkyYHiPdrmWiIiIiIiIyL44jsPcFz6mLhIjLzu1YVCaaZpkpPuprA7x3OtLOfGYYaQFfG0TQ2InTtUtkCgGMxsMLzgOEILI2zhOGDJ+hWFYbXJ9kWSyD6Fljd2FWtZ0mjspLy/noosuIj09nczMTK644gpqa2v3ecwxxxyDYRiNlmuvvbadIhaRg1EZ3c3q6o9ZW7OEULwm2eGIdCnKoSIiIi2jHCrStWzbWcn6LbtJC3ibfUM8LeCjurqOJcu3tV0Qdc+BXQJmAQlMQokyqmJbqYyVErITJCILcaKL2u76IpJ0nWaE/EUXXcTOnTt57bXXiMViXHbZZVx99dU88cQT+zzuqquu4sc//nHDzykpBz9Bh4i0ndp4JW8UP8m62k+J2mEMDHxWCiMypnB0/pm4TW+yQxTp9JRDRQ5vjhMBJwZGCobRacbjiHQIyqEiXUsoHMVOOLh8zY8+tywTBwjVRdssBifyGuAmRoTaWDG2k6D+0YBBXSKGYcSorvoDhXmTWrWtrEhHYDsmttPCEfItPK4j6hQF+ZUrV/Lyyy+zaNEixo8fD8B9993HSSedxK9+9Su6d9/7RJQpKSkUFha2V6gichDCiRBPb72P7XUb8Jp+Ul1ZgEM4EWJR+avUxCs4vcc1mCoeiLSYcqjI4cuJfYYTegqiCwEbrG7gOw38p2MYnmSHJ9LhKYeKdD152al4PBbhaAy3u2lRPhqLY5kGeTmpbReEHcTBIhgrwXYSWMZXS3NxqiKriQUX0Dt1StvFIZIECQz2PIJqybFdRaeoci1cuJDMzMyGD0EAxx9/PKZp8sEHH+zz2Mcff5zc3FxGjBjBrbfeSigU2uf+kUiE6urqRouItI1lVQvZUbeRVFcmPiuAaZiYhkWKKw2fFWBtzSdsCa1OdpginZpyqMjhyYm8jVN5I0ReAycBjgnxjTi19+JU34HjtN3IP5GuQjlUpOvJzgwwYXQRdXUxErbdaJvjOFRVh+lemMmooT3bLgirNwm7moQTxzQsbMfBdhyc+igwDIOg7WJt9SttF4NIkuwZId/SpavoFCPki4uLyc9vPAmly+UiOzub4uLivR534YUX0qdPH7p3787SpUu5+eabWb16Nf/+97/3eszdd9/NXXfd1Wqxi8jeLa9aiIHRzIgA8Jg+wokgq6oXURQ4sJnqD5XjONSFoliWidfnbpdrirQ15VCRw49jV+PU/B84dWAUQMPr7qnghCHybn3/2pSzkxqnSEenHCrSNV10+gTWbChhR0kVfp8bj8dFPJ4gGIqSFvBy1flTcFltV/gz/KdgRz/EdBJEE3xeiK+XYtnEsdhp5xKKrCPhxLAMfTcV6WqSWpC/5ZZbuOeee/a5z8qVK1t8/quvvrrhv0eOHEm3bt047rjjWL9+Pf3792/2mFtvvZUbb7yx4efq6mp69erV4hhEZO9q45X7+XBhUBurbPM4EnGbea8t4/WXlrJzWwWGAcNH92bWaWMZMaZ3m19fpCWUQ0VkryJvgl0ORu6XivGfM3zgVOOEnwP/WW3Wm9a26yCxA8NwwOyOYap/tnQcyqEih7fC/AzuuvFU5r7wEQs/2kBdOIZlGkwaW8RZs8cydEC3VrvWpvW7ePeNFWzasAuv180Rk/oxaerRbKnNo7d/C5bpEHVMDMBrJLAdg49DedRabiwDjC7UokMEIEHLW88kWjeUpEpqQf573/sel1566T736devH4WFhezatavR+ng8Tnl5+UH15Zs0aRIA69at2+sHIa/Xi9erSSRF2kO6K5uaWEWz2xzHAWzS3FltGkMiYfPQva/y3psrcBwHn9+DbTssWrCWpR9v5rJvHssxM0e0aQwiLaEcKiJ748S31P+H0fyEdZACiZ3ghMAItN51nQjB4LPEgn/E5+zAwsbAh2nlY/rPhJSvYZitdz2RllIOFZHCvHRuuHQGXz/7KCqrQgRSvORktW6Oen7uIp56fCGRuiimZWDbDh9/sIG/v5BGYuo4rhxfzQBviBQzgQPsivv5rDaL1XUpFGSE6J8+FbOZt8lFOjNN6lovqf+y8/LyyMvL2+9+Rx11FJWVlXz00UeMGzcOgDfffBPbths+3ByIJUuWANCtW+s97RSRlhuReRTb69YTt2O4zMYj5aN2GMtwMzR94iFfJ1QXZf7i9azbtBvDgKEDCpk0ti8+r5uF81Yz/60V+FM8+FO+mOAukOqlsjzI3x5+mxFjepObn37IcYi0JuVQEdkbw/Di4IDjNB0hD9SPL3JBK74CbzthyipuxhV+nYARB8DBwCaIk9gCwT9hxFdDxt0Yhq/VrivSEsqhIrJHeqqP9NTWz0sffbCef/11PoYBOflpDW+kJeI2SxM11K7PY2G/PiwPlONJuEk4JrWJ+rzs84Soi7oZmH5iq8clkmwJxyTRwsJ6S4/riDrFnQwdOpQTTzyRq666ig8//JD58+dz/fXXc/755zfMbL99+3aGDBnChx9+CMD69ev5yU9+wkcffcSmTZt47rnnuOSSS5g+fTqjRo1K5u2IyOeGZxxFz5QBBBNV1CVqsZ0ECSdOMF5NxA4xNH0iPVMGHtI1Vqzdybfu/Be/f/RtXnprGS+8uYxf//ENvvvjuWzYUsqbL3+GbTuNivEAhmGQkRUgWBNm/turDikGkWRSDhU5DHkmAW4g3HSb4wAh8EzGMDxNt7eA4ySoqPo50bqX6ovxholjuMFwgeHBdmziTgiiH0D4pVa5pkh7UA4VkZZ6/b+fEoslSM9MadQeznKZkOYiHoVFS4+kti6TsJkg5ori9YTweoPYtsW6rRPoljImeTcgIm2q07z78vjjj3P99ddz3HHHYZomZ511Fr/73e8atsdiMVavXt0we73H4+H111/n3nvvJRgM0qtXL8466yxuu+22ZN2CiHyFx/Qxp+f1vL3raVZXLyYYr8LAwO9KY0zmdCbnnoJptPy54e6yGn7xh1cprwyRlZnSMDFPPJ5g285Kfv7AK8Q2VODxNv+n0DQNHGDH1vIWxyDSESiHihxm3KPAMw6i71M/VZy/fqS8EwenHIw0jFaa0DUc+YCKqnuIRBeTZpgYuIk7CQwcMFwYgGO4cJwEjhOFuhcw/Ge2yrVF2oNyqIgcrGgkzurl2/H5m38TzYOJQ4LS8hQWLz+J/OzNZGduxzIS1IayWLalG/2yBrdz1CLto/4Nypb1kHe60JwKnaYgn52dzRNPPLHX7UVFRZ/3nK7Xq1cv5s2b1x6hiXRojuPgOPXF5Y4oxZXGSd0vZXr+GewKb8XApJu/Lz7r0Cd/e2P+asorg+RkpTa6f5fLIicrQPHuagJucEWcfZwFPJ5O86dSpFnKoSKHF8MwIf12nOqfQHQxUA2OAThg5mKkfR/DPfyQrxOJLqG04iZsu34+GJdhsecviUMcw+Hzonz9A24bsBLbD/m6Iu1JOVREDtaevwl7mzc9v9Zilz9G3EkQT3jYsXsgO3bXvxkeSySoDtVx/KT+DeeqiJZQlwiS6s4kw53TLvcg0lbUsqaeqkwiXdSOimqe+3gFb6xYTygao3tWOiePHsyJowbjc3ecf/qOEyFU9xqhuv/iT2zHNHOIGydh+2djmmmHdO5FSzdhmmazDyMsywQcPNl+IuUVOI7T6FVCgGg0jmWZjB5fdEhxiIiItDfDzISMX0FsKUQ/BCJg9QHvDAwztVWuUV3zJ2y7EtMswLZrsD8ft1RfgHdwiAMW1I+Try/Lm5qTRUREujavz03RgHxWLt1GoJn+9BlVEPBBJNWhJhIh1VPfQi4YjVEbjTI4P5cTBvdnc3AV83c/x466DdgksAw3fQPDmZZ3Onm+nu19WyKtwnYMbKdlA0ZbelxH1HGqciLSalbv3M0Pn3qFXdVBPC4Ll2mytriUe4tLmb92Mz+ecwJ+T+tN5NZSth2irOImwpEFgAOGGye+lUj0E2pDz5KX/Vssa/8Tbu1NNJrY55sBpmmSm59OTWmEst01ZGYHcLksACKRGNWVdQwc2p3R4/u2OAYREZFkMQwDPKPrl1YWTxQTiS7CNNIwDRemkULYDhGwrM/L759zbGgo01sYvpmtHouIiEhHc/zJo1m9fAe1NWFS074oytu2TXV5iImBTLKGFbFg8xZKaoMA+FwupvTtw/ePncqu2Bqe3fYQETuI1wzgNnzEnRirqhezo24D5/b+Lvkqyot0WirIi3QxCdvmnhfmsbs6SH5aoKEgne73EonF+XD9Vp78YCmXThuX5EihquYP1EXewzKzMIwvPqQ4Toxo7DPKq/6XvOx7W3z+AUV5bN5e1uzod8dxsG2bEUN7MOH0Sfzh/16mbHdNQwHBskyGjujJDbecjNtttTgGERGRrsi2q3CIYxoBAEwrh0g8RNhJ4DfqR8XbOEACA7AMD4bVG3ynJDNsERGRdnHU9MFsWr+Ll575mNJd1bjcFnbCwU7Y5Oan892bTqH/4EK2VFSybGcJDjAkP4/+udkknAT/2fAvInaINFdOw3dZF268po/qWDnv7H6Gs3vdkNybFGmBBCYJWtiypoXHdUQqyIt0MYs3bmfT7goyU3xNRod73S5clskLS1ZxwZGj8SaxdY1tVxOqex7D8DYqxgMYhhvTSCMSWUgstgG3u1+LrnHc1CG8++E6aoIR0lO/XPB3qKqpw+/zMGPyYIp65vCrhy/lw/lr2bKxFMtlMmJ0b4aP6d1he++LiIgkk2XmYRheHCeCYfgwjQAuq5DKRAm2CX7DbGhWg+HF8IzDSL8dwypIcuQiIiJtzzAMLrhsGiPH9GHe68vZtH4XXq+bCZMHMO34YeTk1rdn7Z2VSe+szEbHbg2tpjxaTIqV1mRgmWGYeK0UNgdXUBndTaan5W+UiySDWtbUU0FepIvZVFqO7Th7LbYHvG4qQ2FKqmvpnZPZvsF9SSy+HtupwjQymt1uGKkk7GKisRUtLsgPH9iNs086grkvfERpWS1erwuH+nY0Xo+LS86aRFHP+klxfH4P048/9AnuREREDgeWlY3fO4Ng3X8wnFQMw8I0MzHMVKoTldTaVfgMH2mBi3D7T8Rwj66fbFZEROQwYRgGI4/ow8gj+hzUcdWxcmwngcv0NLvdbXqoi9dSE69QQV6kk1JBXqSLcZkWjuM026YFwLYdDANcVrK/FO+JzdnL9s/XH8KXd8MwOO/UcfTvk8vL81awcu1ODMNg/IjenDhjOKOHqueeiIhIS6WnXU0k+hHxxDZMIxXD8OE4cSAGZg4pmXfh9Z+Y7DBFREQ6FZ+VgoFBwoljGU3LdgknjmmY+MyUJEQncmhsTOwWtp5p6XEdkQryIl3MuKIeeN0ugpEYqb6mT9RrwlGG9SigW0ZaEqL7gts1CMvMIZEox7K8TbY7Tg2mEcDrHnNI1zEMgwmji5gwugjbri/yqw2NiIhI8xJ2mGBsHWCT4u6Hy0zd675uV2/ych6kquZ+wuH3sJ0qwMLrHkV62pX4fce0V9giIiJdRlFgOKmuTIKJalJdmY22OY5DOB6kR0p/cr09khOgyCFIOAaJFraeaelxHZEK8iJdTFFeFlMG9uGNFeuxTAOf24VhGNiOQ1UojNuyOGfiyGZHz7cn00whkHIO1TX3Y9u1GEagISbbCWM7IQIpp+NydW/Fa3adP94iIiKtyXZibK/5B8W1/yGaKAXAZaaRnzKb3hmXYe1lFJ7bVURu1i+JJ4pJJIoxjABu14Ckf84QERHprDyml8m5J/N6yT+pjVfit9KwDIu4HaMuUYPH8jIl7zTlWumU1EO+ngryIl3Q92ZPoy4aY9HGbVTVRahv/2IQ8Lq5dNo4ZgxtWU/21paeehnxxDZCdS9i2zUYmDjYGFj4vVPJSv+fZIcoIiLS5TmOzfqK/6Mk+F8MLFxm/Vt0CTvEtponCMbXMyz355hG871sAVxWIS6rsL1CFhER6dLGZs2gsjrEB+UvUmGUYVkGLpeLTE8+M/LPoX/qyGSHKCKHQAV5kS4o1eflZ+ecyKdbd7Jg7WZC0RiFGakcN2wA3bPSkx1eA8Nwk51xJwH/KYTqXiKe2IFl5pDin4nPOwXDcCc7RBERkS6vOvIpu4IvYxkBXGagYb1peTCdCJXhDykNvUl+QP3gRURE2looFOFPf3mHDz7YRiQxkJQ+lXgCNoWZhVx05nn0Ss9NdogiLeY4JrbTsl7wTguP64hUkBfpokzTYGyf7ozt03otX9qCYZj4vBPxeScmOxQREZHD0u7QG9hODLeZ1WSbZXiJO7Ar+LIK8iIiIm0skbD57X2vsvijTfj9brLTM6Eik3BxjBU1YX657mXuuv1MsrIC+z2XSEeUwCBBC3vIt/C4jkgFeRERERGRw1gkvhPDMPbai9Y03ITjO9o5KhERkcPPsmXbWPLpFtJSffh8X7wx7vd58LhdbN9ewVtvr2TOmeMBsB2bFdWb2B4qxWO6GJnZn1xvRrLCF9kv22l5L3jbaeVgkkgFeRHpdMKhCO8+/T7z/rWA3VvLSMtOZdpZR3LMeZPJyO04LXlEREQ6muZy6PE/qialrw1W88fYTgy3ld2+gYqIiHQw7fE99MPFG4jHE42K8XtYlollmbz73mrmnDme9bXbeXj9c2wN7SLhJADwW16m543h4j4z8VhqASvSUakgLyKdSrA6xP9d8SDLF6zGAOw8H6sy6nhz+UJ+8cslnD3zSM6dcAQ90lWYFxER+bKv5lC3z01VWQ2v3lvOcXeEMLODeD2NX4G3nRjgkJdyQlJiFhER6Qj2lkMf/+nTzJu7gFv+egP5vfMO+To1NWHYR1sOl8ukuibMjrpSfrnqH5RHqkhzB/CabmzHIZQI83LxB9QlIlw3cM4hxyPS2uxD6CHf0uM6oq5zJyJyWHj618+z7L2VpGWnYo/LZ+WcHpQcmUOoVwql6fDHTz7i0v88zbxNG5MdqoiISIfy5Rya3T2r/v8WZhLa3odti1OprSkhbtfgODaO4xC3g0QTZQQ8g8gPzEp2+CIiIkmztxyaVZjBttU7eOTmv+M4h95PIy8vDXD2eq5oNEFhQQYv7/yA8kgV2Z4MvGb9SHjTMEh1+fGbXhaWLWdTcOchxyPS2myMQ1oO1jvvvMOpp55K9+7dMQyDZ599ttH2Sy+9tKF1457lxBP3P2/S/fffT1FRET6fj0mTJvHhhx8eVFwqyItIpxGsCvLuvz/A4/eQyPGycnwacY+BN2TjCzt462yMijCVwRA/ffdttlRVJjtkERGRDuHLOdTzldfgTcPNRw+NZs3LucSicaKJ3UQSJTjEyfFPZ1juPbjM1CRFLiIiklz7yqGWyyKQGWDlB2vZtHzrIV9ryuSBeL1ugsFIk23RaBxwmH70IBaWLcNjujGbmf8lxfIStWMsKl91yPGItLaEYxzScrCCwSCjR4/m/vvv3+s+J554Ijt37mxY/vGPf+zznE8++SQ33ngjd9xxBx9//DGjR49m1qxZ7Nq164DjUssaEek0tq8rJlgZIpCZwrY+XmLe+mL8nj/Jpmlix+IEEiZV4TAvrl3DteMnJjVmERGRjuDLObQ5lhHgvV8XMXLA6QyZ6QVsUj2DSXH3bd9ARUREOpj95VBfwEuwMsjm5VvpO6L3IV2rb1EeJ84axX//+wnl5bUEAl4Mw6CuLko0lmD0qF5MnNyXJz6L4zKan/zFMAwMIBgPH1IsIl3B7NmzmT179j738Xq9FBYWHvA5f/3rX3PVVVdx2WWXAfCHP/yBF154gT//+c/ccsstB3QOFeRFpNOwXBYYBo7jUJHnxnCa765nmgaWafDhjm1cS9OC/Mbichau2kwwEqUgM5XpI/qRGfC3/Q2IiIgkyZdzaHP2rHfZeeQHjmzP0ERERDq0A82hlmsvs6MfBMMwuPjCyeTlpvHSy0vZvbsa23FIS/Vx8jFDOevM8Xh9LtJdKZRGqkjB1+QctuPgOJDnzTjkeERaW2v0kK+urm603uv14vV6WxzT22+/TX5+PllZWRx77LH89Kc/JScnp9l9o9EoH330EbfeemvDOtM0Of7441m4cOEBX1MFeRHpNHoP7UFO9yxKt5cBBnzl85CdsDEtE1/AR10s0uQDUyQW5/fPz+fNpesIx+KYGNg4PPr6Yq6cNYmTxg9pv5sRERFpR1/OoR6fp8n2UHUdKWl+hh45MAnRiYiIdFztnUNN02D2iaM44fjhbNteQSJh060wg5SULwqOMwqO4J+b3yBmx3GbjUt7NfEgAZePI3OGYztRqiKfEk9U4raySfeOxjRUCpTksTGwW9B6Zs+xAL169Wq0/o477uDOO+9s0TlPPPFE5syZQ9++fVm/fj0/+MEPmD17NgsXLsSymj5kKy0tJZFIUFBQ0Gh9QUEBq1YdeJso/SsUkU7D7XFz4mUz+NuP5+LfFqQyLwOH+lHydsLBtm3SstOw3BaJqM347j0aHf/QS+/z4kerCHjc5KcHMAyDhG1TGQzz++fnkxnwMXloUTJuTUREpE19OYfWVgYJZKRgfN53NhyMEA6GOfbCaeT2aH40kIiIyOEqWTnU5bIo6pPb7LZZhRP5qHw1a2u24TFd+CwPtuMQTIRxGRbn9JqBnVjMJ8V/JBzfju3EMQ03flcv+mReS45/aqvGKtKetm7dSnp6esPPhzI6/vzzz2/475EjRzJq1Cj69+/P22+/zXHHHXdIce6LJnUVkU5l9pXHceLlx5KzqgYjFCfsgVg0jmPbBDJSyOmWSWkoRJrHy+wBgxqOK66o4bVP1uB3u0j1exs+QFmmSXaqn0g8zpPvfrrX1xBFREQ6uz051E7YlG2voHR7OaXbygjXhplw4li+fue5yQ5RRESkQ+poOTTg8nPz0IuY3e1IfJaXukSEqBOnb6Ab3xxwJhMyQqwt/xl1sa24zHS8VgEuM5VQbCNryu6ivO7AW2uItCYHo36UfAsW5/MR8unp6Y2WQynIf1W/fv3Izc1l3bp1zW7Pzc3FsixKSkoarS8pKTmoPvQaIS8inYppmnz9rvOYfvZR/Pm/83jRXUos34vP68byutkVri/G3zxlGv2yshuO+2jdNkLRGHnpgSbnNAyDNJ+HdTtK2VlRQ/fs9Cb7iIiIdHZfzqHzn/2Q0h3lpGenMnH2EQyfMhjT1FgdERGR5nTEHJruDnBZv5M4t/ex7ApX4DZddPfnAAk+Kf4Zth3BY+U1DEYz8OKx8okkSthS9UeyfJMwDOV+aV+2cwgta1p43MHYtm0bZWVldOvWrdntHo+HcePG8cYbb3DGGWfUx2XbvPHGG1x//fUHfB0V5EWk0zEMg36j+vDTUZdwRWUFL65dzaLt23FwGNetB6cMGtyoGA8Qjtb3jDeN5v+AW6ZJJJYgEo23xy2IiIgkxZ4c2m9Un2SHIiIi0ql01BwacPnom1pfPKyuC7N859uEjS14XBkNxfg9DMPAbWYSjG2gNrqaNO/QZIQsh7HWmNT1YNTW1jYa7b5x40aWLFlCdnY22dnZ3HXXXZx11lkUFhayfv16/ud//ocBAwYwa9ashmOOO+44zjzzzIaC+4033sjXv/51xo8fz8SJE7n33nsJBoNcdtllBxyXCvIi0qn1zcziuglHwoR979crt/7DSDQex+Nq+qevLhoj4POQn5naRpGKiIjIoaqObmdD7ZuU1C0DoNA/ir6pM0j3dE9yZCIiIskTjsX5yzuLefmzNRRkLeeUiSFqwxbpvji5aQEs84vCvGm4iTs1xOzK5AUs0k4WL17MjBkzGn6+8cYbAfj617/Ogw8+yNKlS3nssceorKyke/fuzJw5k5/85CeN2uCsX7+e0tLShp/PO+88du/eze23305xcTFjxozh5ZdfbjLR676oIC8ih4WxA3rQMzeDzbsrGiZ03SMWTxCJJTht0kACPk8SoxQREZG92Vw7nw93P0DUrsX4fCqs0vBK1lS9xJH519MrcGSSIxQREWl/CdvmZ8+/xbyVG/C6LUyycBwXlhGnMhQmmkjQIyuDPTV524lg4sJjNT9hrEhbau+WNcccc8w+5wp85ZVX9nuOTZs2NVl3/fXXH1SLmq9SQV5EDgtuy+K7Z0zjrideo6QqiN/jwmWaRGJxYgmbob3yueDosckOU0RERJpRFd3Gh7sfIGaHSLFyGx6sO45DXaKc93fdR2bPPqS5m+/3KSIih6/qyhDz317FkkUbiUXj9B1YwPTjh9OnX16yQ2sVizZu473Vm0j3e/F73JTX9KQqmENW2i5q61IJRWLUhCNk+L04jkPMribDO4aAe0CyQ5fD0J4JWlt6bFehgryIHDZGFnXjnstO5t8LP2P+ik3EEzbZ6QFOPGIwZxw1nPQUHwDRRJCtwYVURjdjGi4K/aMo8I/CNKwk34GIiMjhaWPNm0TtGlK+NDkd1PfC9VvZhBKlbKh5k9HZFyUxShER6Wg2rivhNz99jl3F1RiAYRqs+Gwrb7y4lPMvm8qJpx+R7BAP2Zsr1pOwbfwe9+drDD5eN43pI/9Lmr+WYNhLTV2QVG+CmF2N28qgKPNaDMMgFA+zvW43pmHSK6UAj6kyobStjj6pa3vRvzQROaz075bD9+ccww2nxInE4gR8HlzWFxOD7Ah9wge77yMUL/98jcOqqv+Q4x3ElIKbCLj0Wp+IiEh7K6n7DAOryeR0UF+UNzAb+sqLiIgAhMMxfnf3f9lVXEVWTirW59/7HMehurKOJ/70Lj165zBybMeapPVg7aquxTQb58cdZUW8vfRUxvRfQGZqCS4rho1JhncsRZnX4HYN5olNr/H27k+ojddhANmedGZ1m8TsbkdiGS2bdFNEDowK8iJyWPJ5XPg8jf8EVkQ2sWDX/xFJ1OK3shpGxMftCLvCK3iv+Bec0ONnmIb+dIqIiIiIiHRkixaspXhHJZnZgYZiPNQ/yE3P9FO2u4bXX/i00xfkc1MDJOymPbKLy/vwcnlvbLYyuk8KN846hRR3P2JOgv9b9Q8+qViL23QRsHw4OJRGqvjbplcoCZdzed+Tm30ILnKoNEK+nqpKIiKfW1f9MpFEdZPX4V2mFz+ZlEXXsiP0MRgDeHXHMj6t2IJpmIzN7sMJ3YbTzZ+ZvOBFRES6sAL/SEojq3Ecp0mBwHEcHGwK/SOTFJ2IiHREa1fswLYdXK6mrUcNw8Drc7P8063YttNkhHlncuyw/ry5Yj110diX2tbUi8ZtKkLZjOt9DAFPfwAW7v6MJZXrSHOn4DW/2N/jcROMh3mr5GOm5o5icHrvdr0POTyoIF9PBXkREeq/zG8Nvo9leJsdCWCZHpy4zQelr/O3jQupiYexMHCApRVbeXbrR9w8/GQm5fZv/+BFRES6uH5px7G2+hXqEuX4rexGk7qGEmV4zTT6ps1IcpQiItKROE0HjTezU5uH0eYm9uvFkQN6896aTUTjCVJ9HgCCkRihaIzRvboxY2i/hv3n7VoCjtOoGL9HiuWlLFrNgtLPVJAXaUMqyIvIYSfhJFhRtZKPKz+hMlpJpieTsRljSDgxDPbeKy/hJFhcto5gfCj53rRGxYDdkVruWf4iD0y8hEJ/RnvdioiIyGEh3dODSXnf5IPdDxBKlDbka4cEXjOdSfnXk+buluQoRUSkI+k/uBDjBYNE3MZyNf2eFwnHGXFkn049Oh7AZZn86PRjeeTtD3l12VrKautwHIcUr4cTRw3im8cd1Wjk/K5IBe69TN5aPy8LlEQq2yd4OexohHw9FeRF5LASSUT466a/s7x6BTY2JiZ20OaTiiUMDzikWGG8pDU5znFsInaC8oiXXG+g0Sh6wzDI9aayO1zD68XLubjv5Pa8JRERkb2KhGN8/N4aNq8tweUyGXpEEUPH9sY0O99kbb1Tp5Dp6cvG2rcorluKART4R9EvbQZp7u7JDk9ERDqYiVMG8vTjC9ldXEV2XlpD4d1xHGqrw7jdFsfNHpXkKFuH3+PmWzOncPHksazcsRsHh0GFueSnpzbZN80VoDRStddzOThkuFLaMlw5jDmATcsK613ghZYGKsiLyGHlvztfZGnVMlJdATymp2F91I6yuc5N35QoHrsOt+lv2OY4DnWJCsIJi53h7qS6myYP0zAwDYNPK7aoIC8iIh3C6k+38OBPnmP3zgpw6r/EuP42n4EjenLdnWeSnZ+e7BAPWrqnO6OzL2I0FyU7FBER6eBSAl6u/5+T+O3P/kt5aQ2maWKaBrFYAq/PzVkXHcXYiX2THWaryk5NYcqgfU9SOzVvJOtrtxN3EriMxv31I4kolmExKWdYW4YphzGNkK+ngryIHDZqY7UsKl+Mx/Q0KsYDeEwPtfECKmJhfGaQmB3EZfpxHJuYU1+g3143iojto+kYgy8cUJ9CERGRNrZzSxm//eFTVJYFycwJ4HJbOI5DNBJnxcebufcHT/GjBy7B7dHXARER6boGD+/BT+69kHdeX87HH2wgGo3Tf1Ah008YzpDhPZIdXlJMzxvDW7s+YXOwhFSXD9/n341DiTB1iShjsgYwOnNAkqMU6dr0CVxEDhubQ1uoS9SR5mrakgbAb/nZGunO+OzRhOPLqY5twzAs+qRMZXDGyVTESlhU9iGO4zSZ+NVxHGzHYXRWr/a4FRERkX166/lPqCitJacgveEVfcMw8PrcZGQH2LByB0sWrmPC0UOSHKmIiEjbyslL48wLjuTMC45MdigdQpo7hZuHXMQjG55jRfVmgtFqAHyWh6Pzx3Bp35OwTGuvxzuOTcyuw2V6MQ2VFeXgaIR8Pf3LEZHDSv0A9r39ETdwMMjzT2RY+tdJOBEMw8Iy6ifAmdU9nxe3L6UsWkuOJ7XRpK6lkVrSPX6O7za8PW5DRERknz58cyVuj9XsRHUer4uEbfPp++tVkBcREelCgqEI8z/awJoNJRjAwH4FTBnXj0CKt9F+eb5Mbh36NTaHitlYuxPTMMl3cln2WSkPfrAAj9vF2EE9mDSiDx53fekwnKhmbfWLbKh+k4hdjWm46ROYyuCMU0j3HJ5vG8jBU0G+ngryInLY6JXSE7/lI5yoI+AKNNkeTtTht3z0SumJYRi4DF+j7X1T8/j2kBP47erX2BWu/nzUgEPCdkh3+/j+sJPo5s9sn5sRERHZh0g4hmXtfeJWA4jURdsvoFawuzbIgo1bqI1EKUxPZXLf3vjd7mSHJSIi0iGsWlfMrx55nd1lNdh2/VC0V95Zydz/fsRN15zA4H4FjfY3DIOiQDeKAt14c9Eabv/3a9SGIthO/eeElxeupH+PHH54+UwyM23e2vkTyiPrMHFhmR7idpg11S+wLfQB0wt/QI5XbW5k/1SQr6eCvIgcNtLd6YzNHMv80gV4bA9u84sv8TE7RsSOMiV7MunuvU9yd1y34QxIK+DVnctYUrEFA4NxOX2Y2W0kPVKy2uM2RERE9qtn/zyWL95Ec03aHNvBAbr3yWnvsFokbts8snARzy5dSW0k0vCGWn5qgG8dPZljBnStCflEREQOVllFkF889Cql5bVkZwYaHsrHEzYlpdXc8+Ar/N9tZ5GV0XRg2vINO7nvX+8SjSfIyUhteLsuGkuwdmspdz/6OhdevIuyyFr8VjbWl9rUOI5NKF7Gh7sf4MQev8Iw9j4YQES+oIK8iBxWTu9xCqWRUtbWrsWJg2VYJJwEhgGD0gZyeo9T9nuOPqm5XDXwmLYPVkREpIWOOWUsKz/eQl0ogv9Lr6k7jkNVRZCUgI/JM0cmMcID96f3F/P3xZ/itSzyUgOYhkEskWBXbZCfvvoWAY+bCb17JjtMERGRpHl74WpKy2vJyUpt1K7OZZnkZKVSWl7LWwvXMOfEsU2Off7d5dRFY+RlpjaaK83jtshM87N513bWVLyH2+NrVIwHMAwTr5lOZXQzu8IrKPCPaLublC7BcQycFo50b+lxHZEeXYnIYcVv+bm6/5V8rc9FDMsYQr4vj6EZQ7i4z4Vc0/8q/JY/2SGKiIgcsiOPG8aUmSOoq41StquaUG2EYE2Ysl01mKbJ+d88loIeHf/NrrJgiH9/ugKvZZHp92F+XihwWxZ5gRRC0Rh/X7wEx3GSHKmIiEjyLP5sC4ZhNDt3jGkaGIbBR0u3NNkWT9h8tHIrPo+7UTF+D4/bhS+1hnA8iNv0NdkOYBkebCdOdWw7jlOHE1uNE1uD43Su1njSPmyMQ1q6Co2QF5HDjsd0Mz57HOOzx+11n+211ayrLMNlmozKLSTN493rviIiIh2NZZlc/YNTGDiyJ2/+52OKt5ZjGAZjjhrAzLMnMOaoztHndeGmrdRGIuQFmr5ibxgGqR4Py3aWUFxTS7f05hr0iIiIdH3RWLzZYvwepmkQjSWarHccB9txmi3G72HHLcDAcWyar4c6mNhkxefhlD0IdkX9aisP/GeC/zwMQ+VHqace8vX0L0JE5EtKQrXc+/F83tuxmbp4DNMwyPT6mDNgOJcPH4/HspIdooiIyAGxXBbHnzmOY08fS7A6jOUySUltfnRbRxWMRmEvI/4A3JZJNJqo309EROQwNaiogLUbduE0U1x3HIdE3GZQ3/wmx7ldFn2757BiQzFpKU0HoSUSNjXV6fjoRsTehauZUfIxu4YJ3jKyYm+B4QEjFXAgsQun9gGIb4G0m9VfXuRL9K9BRORzlZE6bpz3Aq9uWYtpQH5KgGyfn9pYlD8uW8wvFr+jV+JFRKTTMU2TtMyUTleMB+ienoYBRBNNR/UB1MXi+Nwu8lObjqA/ULZjs7RyPY9ufIkH1j7DU1vfZmddaYvPJyIi0t6OnToYn89NdU240XrHcaiqqcPnc3PslMHNHjt78lBM0yBYF21ybHl1iJyMVMZ3OxfTcBFOVNaPlP98ezBaTaazm25GCJt0MHPA8ILhAzMXCED4ZYh91Cb3LZ3Pnh7yLV26Co2QFxH53HPrV7G6opRcfwpus34kvGkYZPv81EQjvLhpNWf0H8aI3IIkRyoiInJ4mFTUi+4Z6WyvqiY/kNJo1F/ctqmLxTlx6EDSfS172BCM1/G7NU/zWdV64nYcBzAweH77fOb0OprTuk/Z52v8IiIiHcHAonwuOmMif3vmA3aX1eD11Jf7ItE4Xo+Lr82ZRP8+ec0eO2PcQFZsLOaVhasIhSN4PW5s2yYaS5CR6uO7FxzDkOyeGFaIzyqeJJQow7YdgpEokahBf8umDtgeqiMj4JCbHsDY09vGTAW7Fif8GoZnQjv9NqQjU8uaeirIi4h87oWNq7AMo6EY/2Wpbg/FoVre2LpeBXkREZF24rEsvnP0ZO58+U2Ka4Okety4TYtwPE5dPE6/3Cy+PvGIFp//wXXP8kHZCjymhdfykGL5MDGoiYd4cssb5HjSmZo3qhXvSEREpG2cPnM0RT1zeGXeCj5bvR2ACaOKmHXMMEYP7bnX40zT4LqzpzF6YA9eWbiK9dtL8bgtJo/sy+zJQ+nTLRuAIZmn0Tt1CitK3+K5xQvYXWlTXdqH4455Edtx4ThQUVNHwnYozEzli4bzFiS2E4tvpC78NrZdg8sqxO8/Acvs+BPMi7QFFeRFpNNxHIf1SzbxwYsfU1NWQ2ZBJkedOo4+w3od0nlL64J77RFvGAY4UB4OHdI1REREkqmtcmhbmty3N7887UT+vvgTPtm+k7p4DJ/bzUnDB3PJ+DHkp6W26LzPbHuXN0o+wnZswrYB8RAmJhnuAFmeNMqi1by4830m547AVN9bEZHDXmfIoaOH9WT0sJ4NrVYP9C0v0zSYPrY/08f23+d+Ka4cli8vYv6CMnLTU7BMk9pICt3Sy7BMA9uBmlCYzIAPn9v9+VEJIrGNlO0+H9sJY2DgkMCo/g2BwOVkpV2pt9EOI4fSekYta0REkiQWjfHIzX9nwbOLiIa/6HH3wsOvccLXpnPRj87GauHEq7n+AJuqK5rd5jgOGJDjS2nRuUVERJKtLXNoWxvdo5DRPWZTWhskGI2RE/CT6m06+dyBWly+ir9veoWEY+MyLMzPCwEJx6YiVoNhGKRYXraGSiiNVJHv0wg+EZHDWWfLoW1Z4H7tk7W4LBPLrH9Y/fHWwQzM24plJACLuO1QWxetL8g7UWw7RI1dDUYappFD1CkjYddiOBVEqn/CzuDLdMu8iWz/UW0Ws3QcziG0rOlKBXkN9RCRTuVfv3yOeU8uwOVxkdMjm9yeOeT0yMYwDV545A1eeOj1Fp/75L5DSDgOMbvpxHG1sSg+y8Vxvfc9YkBERKSjassc2l5yUwP0yc48pGK84zg8s+0d4nYcE6OhGA9gGSYGBlWxWsDAcRxidrwVIhcRkc6sK+TQ1mDbDjV1ETyuLx4+fLR1CNsq88lICeJzRXFwSNiJ+t7x9m4iTpw6/MRsL3WJrcQTlTiAY3gwcDDjS1hV9kN2B19L3o1Ju3EAx2nhkuzgW5EK8iLSaVSX1/DmE+/i9rlJSfc3PPU3DIPUzACWy+SVv7xJpC7SovOf3n8oQ7JyKQ2FqI5GSNg2MTtBeThEKBbjpKLBDMvOb81bEhERaRdtnUM7k+11u9kcKiHgqv892E7jr3emYZJwbGpitWS4U8nzZiYnUBER6RCUQ79gmgbZaX4isS8eVtfFvDw0/wxW7OyLy4qTl1pLurcKjDhV8V5srYuxeXeC8uBOYokwCdsExwJMHFy4jQSGE2Jj5f3E7WDybk6kHakgLyKdxqoP1lFbESSQ0XzbmEBGCuUlVaz/dHOLzp/h9XHvMacwu+8gcKC0LkRFOEy6x8fVIyfwPxOmq7ediIh0Sm2dQzuTukQE27FJsby4DRe2Yzf02oX6KegcxyHu2MwoOAKP5d77yb7EdqKUhd5hY+X9bKz8PbuDr5Oww210FyIi0l6UQxubdcTg+jfLE1+8WV5Zl8YD783hrhfP4s/vH0vQfSNLq3/K/R8UEo7bYIDPE8ZxDOJ2/bH1I57rO8q7jTSidinldfOTeGfSHmyMQ1q6CvWQF5FOIx6L4zgOptX8s0TDNHFsh3i05a+W5/oD/HTyTHYGa1hfWYbLNBmRU0Cqp+WvxouIiCRbe+TQziLXm4nHdBF14uR5MymJlBO3E59/xTPqC/TAoLTenNJ98gGdMxTbxOqyOwjGNuA4n39dNAx8rp4Mzr6dNO+wNrsfERFpW8qhjZ00fgjvLNvA6m278XtcpHg92I5NTV2U3dXZjB10DJm54/n2v+eS7k0Fw8TrjmEYDo5jYAC245CwbdxWAgcLDD84dUQSJcm+PWljmtS1nkbIi0in0XtID7wpXupqmx9tFq4N40/10nNQt0O+VrdAGlN7FHFkt94qxouISKfXnjm0o8vypDEuawh1iQgu06KbL4dMTyqWYWFgYBomPf153DXycnyWZ7/ni9u1rCz9AbXRtbjNDHyuAryuAtxmFuHYVlaV/ZBIXAUGEZHOSjm0sfQUH/97yWxOGj8Ey7KoDNZRUxclLyOVa2YfyZWzJvHB+q3srKyhtHYwZbW5pLjDjRqAG4BtJzCwiZCNjQk4uMy0ZN2WtBP780ldW7p0FRohLyKdRs9B3Rk+ZTAfvboUr9+D9aWJZOLROOFgmBnnTyG7MCuJUYqIiHQ8yqGNndf7WNbVbmNnXSle00PA8uM13QTjYdI9AW4cfB5+68AeyJeG3qQuvgWPlQtY1AYjxOI2pmmQ4s8hEt/FrtAr9Eq/pG1vSkRE2oRyaFNZqX6+N+doLjthAlt2V+KyTAZ1z8Xjri8z7qyqAcCyPLy44mTOHjOXLFcE04wTty1Mw8HEIeoEqDO7EbersYwA2b4DezNNpLNTQV5EOpXLf3oBu7aUsm31Diy3hdvrJhqOYSds+o8p4oIfzEl2iCIiIh2ScugX8n1Z/HDYJfxn+7ssLFtOJBHFMiyOzB3O6T2mMjCt1wGfq7xuAY7jEKqzKS2vJh7/oqeuaRpkZScoDb6jgryISCemHNq87LQUstOa9tZP8XhwHAfbdtha0YfHF1/MMYPeYHj3ZVimTdy2KA6n40npCXYQmxg9U8/F68pPwl1Ie3Kc+qWlx3YVKsiLSKeS3zuPH/3re7z5+Lu88/RCqktr6dE/h6PPncyxF04lNTOQ7BBFREQ6JOXQxvJ9WVzV/zQu7HMCFdFaAi4fWZ6Df1XeduqIxx12lVZj2w6WZX4+CbxDIuEQCsUpjuyCw6OTgYhIl6QcenAm9e9Fqs9LdThCZoqPXTWF/Ouji+i5eR0TB72F31eN22XiMmpwGWn0SL2A3hmXJztsaQfqIV9PBXkR6XSy8jM467uncNZ3T8G2bUxT02G0J8dx2BhcxtLK+RSHN+M23AxMG8uozKlkevKSHZ6IiOyDcmhTAZefgMvf4uNT3AMJR+dj2yYulwXs+bJoYFkGLleCdWv9VPQOkpWhgo2ISGelHHrg8tICnDZ2KP94/1OqQmHS/F5Mw2BjSV+WbMxnYPdiLj26N/2zu5Ppm4THyk52yCLtSgV5EenU9CGofTmOw1u75rK4/HUSThzLcOM4Nrsi2/i08l3O6PkNeqUMTHaYIiJyAJRDW4c7No1w3V/xByLEIo0L7m5PGNt2sWLpABblbmHmtKFJilJERFqTcuj+XXH0BAD+8/EKdlcHP397DPLT07lo0okcVdQ3meFJkmiEfD39BRGRDsFxHJyu1BCsi1pVvYhF5a9hGS7S3TkEXOmkujNJc2VRG6/k+e2PELXDyQ5TRESk3djRnnzy4XgMA/yBatyeMG5PGH+gBssVZ/3KsezY2oO6umiyQxUREWk3LsvkmmMn8ejV5/C92dO4ZsYkbj/jOB67+lymD1Ex/nBlO8YhLQfrnXfe4dRTT6V79+4YhsGzzz7bsC0Wi3HzzTczcuRIAoEA3bt355JLLmHHjh37POedd96JYRiNliFDhhxUXBohLyJJY9s2H7y8lDef/oBNK7ZjuS3GzRjGceceRb/hPZMdXptL2DaLV2xl3sfr2FVRS05GCtPH9mfi8D64XVayw2vWJxXzcBwbn6vxCEDDMAm4MqiOlbG6+mNGZk5OUoQiIiLtKycrlfXLx1NTmcmIsavIyd8JQGlxDzauGcXGtf0xCJOfe/D96UVERDq7/PRUTjtiWLLDkA6ivSd1DQaDjB49mssvv5w5cxpPvhwKhfj444/50Y9+xOjRo6moqODb3/42p512GosXL97neYcPH87rr7/e8LPLdXAldhXkRSQpbNvm0Z8+w5tPfYCdsPH4PETDMd6c+wHvv7yUb/zsPMYfNyLZYbaZcDTGr/7+Fu9/tol4wsayTGzb5r0lGxg7uCe3XnoCAb8n2WE2ErdjFIc34Ta9TbY5jkM4GCdkR3ht0XsE0wsYd0QRbnfHfLAgIiLSWlIDXqZNHMALb4ap2DUYt8fGMBwScTeO41BVHaQgN41xI/+/vfuOz6o+/z/+PvfOHiQk7CFTRQQUDA5AUMCJWAfaOoraWmxdtW5x1FpXsY462q9Sf87augeKKC6GgKAiG4GwEkb2uufn90ckGhNC7ju5c2e8no/HeZT7nPM557pPY67c1/051+kZ61ABAAA6lMmTJ2vy5Mn1bktJSdHcuXNrrXv00Uc1cuRI5ebmqmfP/f/t5nA4lJ2dHXFcFOQBxMTiOd/oo/8ultvjUnzSjw9SM8aoYFex/nX7fzVgWG8lpyfGMMroeWHOV/pixfdKSvDI43bWrPf6Alq6eqv+742F+sO5Y2IYYV2WZcmSpZBCtdb7vH7l7yqR3x+UPdGvtWvztPDz99Sje7quunKievboFKOIAQBoGb84ebhWrt2hLdsL5HE75HY5FQh6VV7hU2K8W5dOO0YuJx+9AABoDULGaNXuXSr2VikjPkED0jvV9LhHdFXPkI+0h3z1/5aUlNRa73a75XbXnTgYieLiYlmWpdTU1Ab3W79+vbp27SqPx6OcnBzdc889DRbwf44e8gBiYt5/FykYNLWK8VJ10TctI1nFe8u04N0VsQkuysoqvJq7eI1cLketYrwkuV0Oxbmd+nT599pTVB6jCOtntxzqmTBI/pC3pt9/MBBSXn6xfL6A7A7JZrPLVZal5CSPtuTu0f0PvqvSUnrKAwDat4y0RN1+zSk6+fhD5XY5VOX1KxQyGjm0t27+/WQdObR3rEMEAACSPtuyWRe+/l/99u03dO377+mSN1/Tb95+Q1/n7Yx1aB3Cvoe6RrpIUo8ePZSSklKz3HPPPc0SW1VVla6//npNmzZNycnJ+91v1KhRmj17tubMmaPHH39cmzZt0rHHHqvS0tJGn4tpGgBaXCgU0uZV2+Xx1N+SxWa3yRijrevzWjiylrF+626VVHiVkuCpd3tCnFt7i8u0dku+rLjO+ihvldaW7JTdsmloWk8d13mgEp31j4224WnjtKlspSqCJYq3J6u0rEp+f0gOpyV7gk/BwkT5t2XK6bQpPS1RO3YU6fMv1mnypMNiEi8AAC0lIy1Rv/3lcfrV1FEqKKpQfJxLndISDjwQAAC0iI83f6875n+kykBAKR63nDa7vMGgVuTt1B/nztEDJ07W0Kzw25AU+sq1q6pE8Xa3usenMdu+AeaHJdKxkrR169ZaBfPmmB3v9/t19tlnyxijxx9/vMF9f9oC57DDDtOoUaPUq1cv/ec//9H06dMbdT4K8gBanGVZcjjt8nsD+99H1fu0R0aqvtfqAEl6ddV2zVr0jgr9FZIxMpLm5a3Si5sX6bYhp6t/cuT9yiLVJ/EQjcs6S/N3/U+lgQJVhKrkTA7KZtkULEpQ2adDpVD1zVd2u02WJS1dtomCPACgw0iIdyshvnlumwYAAM3DFwzqH0sWqzIQUOeEhJqiebzNpjiHQ/llZXpy6Zd67KRTG11Qz6ss1rPff64vdq+XNxiQzbJpQHKWzuudo5EZfaP5djq05OTkBmewh2tfMX7Lli366KOPwj52amqqBgwYoA0bNjR6DAV5AC3OsiwNGzNYH//vSxlj6iQ7v9cvm93SIaP6xSjC6DqoWyclxLlVXulVSmJcne3lVT45Ui29Ub5UVSG/Mt2JslnVRe5AKKQdlUW689s39I+RFyopBjPlR6SPV6+EwfqueKHeW/eZykv8su3pIt+WbMlfO63YLEter7/FYwQAALG1q6pE72z/WnM2LIt1KAAAaPnOHdpeUqK0OE+dGoRlWUr2uLVyV742FxepT2panfFlfq8+2rFen+dtUnnApy7xCfq29Hvt9pYo3uFSotOtQCikbwu36Y6S1/XHgydrXPbglnp7bcZPW89EMra57SvGr1+/Xh9//LE6dQr/GXhlZWXauHGjfvWrXzV6TJvpIX/33Xdr9OjRio+PP2Bj/X2MMbrtttvUpUsXxcXFacKECVq/fn10AwXQKCecO1oJyXEq2FWsUPDHh4T6fQEV7S1Vr0FdNWxM+0xeKYlxOv6I/vL6AvL6at8l4PMHVFnlU9oQhyqCPmX8pBgvSQ6bTRmuRO2sLNKn+WtaOvQaGe6uGtP5TPXYeYp2vj1I3vXd6hTjjTEKBkPq3SszRlFiH3IoAKAlbSnfq2uXvainN3yqDaW7Yh1Ok5BDAaB92FtZKX8oJJe9/rnJLrtD/lBIBZUVdbbllhXqss/+o7+s+FCf5G3UV3u26dVty7SuJF822ZXkiJPL5lAoJHn9Ie0oL9Efl/xX1332jr7M2xpRvPmVpXpt0zcRjW3VTBOXMJWVlWnFihVasWKFJGnTpk1asWKFcnNz5ff79Ytf/EJLly7V888/r2AwqLy8POXl5cnn89UcY/z48Xr00UdrXv/xj3/UJ598os2bN2vBggU644wzZLfbNW3atEbH1WYK8j6fT2eddZYuv/zyRo+577779PDDD+uJJ57Q4sWLlZCQoIkTJ6qqigcMArHW99Aeuuyus5WUmqCCXcXavaNAu3cUqqSwTH0P6a7f3X++Fn63RU+++oWe+N8X+mjJOlW1o5nWF5x8pI4Y3EOlFVXaXViqwpIK7SksVXFZlYb066JQll9Om73eW+Xstuoe+18VbIlB5LWNHTNYLpddZeXeOttKS6vk9jg1ZszAGESGnyKHAgBaijFGD3z3rr4r2qEir1eVgWCsQ2oScigAtA9pHo8cNpt8wfrzki8YkNNmU5qn9l3swVBIty2bo40le9XJHa/suCRleOJls1VPrivwVqjU79WeygptKytWmd8nGUshBTRv5xpd/cm7emlt4wvrwVBI/1j1hc776P/pkVWfR/6GW6umPNA1ghnyS5cu1bBhwzRs2DBJ0jXXXKNhw4bptttu0/bt2/Xmm29q27ZtOvzww9WlS5eaZcGCBTXH2Lhxo/bs2VPzetu2bZo2bZoGDhyos88+W506ddKiRYuUmdn4yYhtpmXNHXfcIUmaPXt2o/Y3xuihhx7SLbfcotNPP12S9OyzzyorK0uvv/66zj333GiFCqCRRp14mAYO76OF767Q1vV5crjsOvSo/krt31l3PP+Rtu8ulglV9063WZayOy3V9RdO0MBenWMdepPFe1yaeekkffH1Jn28bL12FZapU3K8xozop2OH9tUlS/6v4b51lqWAif0H3IMHd9Vppw7X628s0569pdUP6jVGVV6/HA67zpp6pPr3a/le96iNHAoAaClrSnZq0Z7v5Q+GZMkmS237wXbkUABoH0Z07aauSUnaXlKizJ/0kJeqf3eXVHk1vEvXOu1qFu/O1fri3Upzx8lhq37OXVBBGRnZLZuCxmhPVbl8P3wB7bTv28coyeOQvzKkx75epOGdu2pAWsYB43xm3Zf6f+uXymW3K8PNw+GbauzYsTJm/1PrG9q2z+bNm2u9fumll5oaVtspyIdr06ZNysvL04QJE2rWpaSkaNSoUVq4cOF+/xDyer3yen+c6VlSUhL1WIGOLDUjSZMvOLbmdXFZpa6e9bp27C5WWnK8nI7qZBYIBLVjT4n+/PQHmnX1GcpIbfuJyemwa+yIfho7om6v/ENSu+nDnavqHRcyRjLSgOQu0Q7xgCzL0rlnj1Kvnp30wdyV2vj9LsmydNiQHpp44hCNPJIH2bRF5FAAQKQW7tooX/CHlnxtuxYfEXIoALROLrtdvxlxpP782XztKi9Xqscjp90uXzCgosoqJbndumzEkXUmxn1bsENBE5L7J61ubD/5wtlm2VQV9EvGkvOHgr35obeKXXYluuOUV1Gmt79fo2tGHNNgjEXeSr2y6Ws5bTaluuLk91c24xVoHYypXiId216024J8Xl6eJCkrK6vW+qysrJpt9bnnnntqZkEAaHmffLVRO3YXq1NKguz2n/ROd9iVkZqo3YVlmrdknc45YVgMo4y+SV0P06e71qnYV6EUV3zNemOM9nrLlez0aEL2wTGM8EeWZWl0Tn+NzumvqqrqtkIejzPGUaEpyKEA0L5tLyzW3JUbtHrHLjntNo3o3V3jDj5IqfFNf1j80j3balq8GmNqihIdBTkUAFqvEw/qL5tl6Z9fLdXWkmIFQiG5bHYNysjUFSOP0vAuXeuMCRmjn3/DbLfscltuVZpKSTaFjJHdsmp2MwrJJpvcJk6WZclh2bSq4MDPVPlyd66KfZXK8OybgBj7u+KbW2t7qGushNVD/t1339Ull1yiP/3pT1qzpvbDBAsLC3X88ceHdfIbbrhBlmU1uPz8PNF24403qri4uGbZujWyhy8AiMySVbmyLNUqxu9js1my2SwtXrm55QNrYYen9dSv+oxWSFJ+ZYmKfBUq9JVrV1WJ4hxOXTn4RGXFpcQ6zDo8HifF+BZCDgUARGLuyvX6zTOv6elPl2rxxlx9unazHnr/c/32mVe1ZufuJh8/0R4v08qnxpNDAaDjmtC3n56ferYemXyK/jp+ov5x8mmaPeVMjezWvd79B6VmyZLq9J5PsiXLJptCJihbTSHeKPRDO5v4UJLsP8yDDhoj9w+tbBpSFfTLSHLavfJ4Nikp6bumvFW0Yo2eIf/CCy/oggsu0KRJk7R27Vo98sgj+te//qXzzz9fUvXDbj755JOwTn7ttdfqoosuanCfvn0ja3eQnV3dszg/P19duvzY1iE/P1+HH374fse53W653e6Izgmg6ap8Adms/X9XaLPZ5PUFWjCi2LAsS9N6H6WByV307o6v9V3Rdtktm0Z26qOTug1V/2T6snd05FAAQLjW7Nytv835XF6/X52TE2T74bb8YCikHUWluuO1D/XPX09Voify3+UHp3TRW9tscjqDP/RlbX0z5MmhANCxOWw2jejSrVH7Hp3VR72S0rSptECZngTZf6hXuG1uxQWTVa4SOe2WvMGgQjKyyaaEUIqSTKqk6hwrSUd37XXAc3WNT1GCs0px8dtktwXk84Uie4OtWYQPZ60Z2040uiB///33629/+5v+8Ic/SJL+85//6Ne//rWqqqo0ffr0iE6emZkZ1hNow9GnTx9lZ2dr3rx5NX/4lJSUaPHixbr88sujck4ATdeveyd9u2GHjDF1ercZYxQMBNWvR3R+b7QGxhityN2p975eq7U7d8vpsOvo/r10yWHHq0tqUqzDQytCDgUAhOvt5atV7vUpK7n2w+zsNpsykuK1o7BE81d/r1OGDY74HJ1tKfJ7nbIsI4cjpIaeUR8r5FAAQGO57HbdMXySbljytnZUlMgmS3abTf5QUC6bQ+f3Ok4Hp2Xo3q/mq8TrVbozVS5b9V3jgVBIeyor1CUhSZN6DzjguQ7v1FXdU/coaPkUCrnbVc/0feghX63RBfn169fr1FNPrXl99tlnKzMzU6eddpr8fr/OOOOMqAS4T25urgoKCpSbm6tgMKgVK1ZIkvr166fExERJ0qBBg3TPPffojDPOkGVZuuqqq/TnP/9Z/fv3V58+fXTrrbeqa9eumjJlSlRjBRC58UcO0JyFa1RcVqXUpLha20orvHK5HJow8sCJrC0yxmj2Z8v0wsIV8gaCctmre9Gty9ujN5ev1u1nTNDQnrF/kCvaHnIoAECSlny/TW6Hvc6kB6l6tqCR0ddbdzapIO8PhZTgTVS5VSq/38jyt+07G8mhAICBqZ31z2PP1ntb12j+zg2qCPjVLzlDJ/c8WKMye8qyLHXzZOjmBXO1s7xURlWSkWyWpe6JyfrL0ROV7ok/4Hk2lW9SkjuoQq9LgZBRsD1VoPdpys1z7ehyNLogn5ycrPz8fPXp06dm3bhx4/T222/rlFNO0bZt26IS4D633Xab/v3vf9e8Hjas+oGOH3/8scaOHStJWrt2rYqLi2v2+dOf/qTy8nJddtllKioq0jHHHKM5c+bI42n6w4oAREe/Hpn65eQj9Ow7S7SrsFRxrupvlqt8fjnsdp174jAdelD7LEovWJ+r5xeskN1u1Zq5FjJGu0vK9ec3P9Izl/xCiR63jDHauGq71n27TSZk1HdwVw0c2kM2W1iPBkEHQQ4FAEhS0Bx4xnog1LTb47slJSsh5FFSwK0iR6kqfL4mHS/WyKEAAEnK8CTqV/2P0K/6H1Hv9sMyu+jFyedo3taN+m7vLlmShmRk6/gefRXvdDXqHHu8e2SzpO7xaSr0Vaqwwt+M7wCtiWVM475umTJlioYOHVrvk9/nz5+vU045RZWVlQoG29cTgEtKSpSSkqLi4mIlJyfHOhygw1iyKlfvfLFKKzfulIw0qHdnnXz0wTpqSO96Z3W1Bze8/J4WbshVVkpinW2BYEh7yyv1p5OOU063bnry7je1ZvkWBfxBGUkOh019BnbRb289XV16dmr54Ns5ckFkuG4A0Lrc+fqHmvfdxnr/1giFjHaXlmvGCTk6e+RhEZ8jZIwueO2/Wrd3j7ISE+WtLNfc6ZeTC8JEDgWAjmdpwTI9u+U5pThSZLNsqiit1AOj/9oucsG+vNbzqdtki4/sC+pQRZVyL7uzXVyPRs+Qv/rqq7VgwYJ6t40dO1ZvvfWWnn322WYLDEDHduTBPXXkwT0VCFR/yedwHPiJ5G1ZKGS0cnu+PK76fy077DYZY/Td1nwtevQLbfhuu5KS45ScVn3bm88b0Lpvt+nB61/WzMcvUlLqgW+HAwAAHcvJhw/WZ+u2qKiiSilx7ppJDsYY7S4rV1pCvCYc3K9J57BZlq4bfYz+9OH7yisrkzvYtlvWAADQUgYmDVC8PV4VwQolOhJlb6eTEdtT65lINbq3wZgxY3TjjTfud/u4ceP0zDPPNEtQALCPw2Fv98X4fSxZB0xM+dsK9P3qHUpNT5An3iXLsmRZltwep9Izk7Rj8159NueblgkYAIADKKiq0JL8bfpq1w5V+Nt265L2YETvbrr42BGSpPySchWUV2pvWYXyS8qVEufRDaeOUXpi07/UH5rdRQ9PPkWT+w2QTe20mAAAQDNLcibp6IzRCpiAygMVamRTkzbFGKtJS3vR6BnyAIDosdksjejdTR+v3qhkY+q05fEHg7JZlgK5pTIhI2c9M+ntDpssm7T4o9U66dyjWip0AADqKPZW6fFvFuuDLetV5vfJktQpLl5n9jtUFxw8TE5bx/iyvTU6L+dwHdItS+9+vUYrt+XLabfrqH49NfmwgeqVkdps5xnYKUN3jhuvbYcOUY/Lf99sxwUAoD2bnD1R/pBfC/cuUnGg+MAD0CZRkAeAVuK04YO1YMMWFZZXKi0hrqYoHwiFtLesUr06pSp9p1d7G3hwq8NhV0Vp5QHPVbirWIvfXqaCvCIlpSVo5EnDldUrs9neCwCg46oM+HXdZ+9p2a7tinM4lebxyBipsKpST3yzWDvLS3XzyLFt9pkw7SGHDu3ZRUN7dmmRcyW7eZApAKBae8ih0eawOTS1+xQdm3mMFm1brCf1eKxDal5GkbesaUc3DFCQB4BWYnjvbpoxIUePz1uk/JJy2Syr5ha17mkpmnnGBH1Wtlhrl26RqWcWvST5/UF16ZVR7/ELisq1/LutWvz+1/r6nWUyOwtl/XCOV/72lk68YIym3TRVdnvbmrVY4P1eG0vmamflchkTUue4Q3RQ0gR1jjsk1qEBQIf03qZ1Wr5rhzp54uSy//hxo1NcvMp8Xr27aa1O7TtIQzNbpiDcXIwxeuepD/W/h95WRXGF1A5yKAAALYEcGr5Md4bGZB4b6zCiwPphiXRs+0BBHgBakdOHH6yhPbrog5XrtXbnbrkcdo06qIeOP/ggJcd5FDxxiOa//bXKS6uUmBxXa2xlhVd2m6XjThpaa30gENRzr32p9z9dpcKCMlWUVMrqmi53dqq6FJUrvsqvsuJyvfXEXLkTPDrrmlNb8i03yZayz7R49z/kD5XLZrlkSfq+9CNtKftCh6f/UoNST4t1iADQ4by9aY0sqVYxfp8Ep0ul/jLN3bKhzRXk57+8QC/85X+yLEtpXVJls9lkQqbN5lAAAFoKORQ1mCEvKYyHuv7chg0b9P7776uysro1Qnt80AAAxELvzDRdNm6kHjzvZN1z9iRNGXGIkuOqb/ceMKS7Tpg6Qj5vQHt3laqqwqeqSp8KdpeqotSr0ScO0bCj+9c63uxXFuq191coEAgqWFopuy8ghzHyOezalp6kKrdDSWmJcrjs+mD2xyovLpc/FFB5oEohE4rFJWiUMn++vtz9hAKhKsXbMxVnT5XHnqp4e4Yko68LntPuqjWxDhMAOpy8ilI59zPLzbIsWbKUV1HawlE1TTAQ1Jv/mKNgMKSUzGTZfmgfZ9msOjkUAAD8iBwK1BX2DPm9e/fqnHPO0UcffSTLsrR+/Xr17dtX06dPV1pamh588MFoxAkAjbK/Vi7thWVZOu+KCeraK0Mf/G+p8rbulYyU1T1d46cM14lnHim7/cfvWvN2FWvuZ6vldjnkslkK+gKyOWyySbKCIfkdNu1NjFP3wjIlpiZor71E93/5vLamFChggkp1Jmpc52E6MXuk4h3N3wPW7wvI7rDV/FEWjk1lH8sXKlO8PaPW/+eWZcltS1ZFcLe+L/1QmZ5BzRkyALRbzZVDO3nitady/x+qjYw6eeKbfJ6WtOnbXO3askeJqQn1bk9MTVBRfrFWfrFWo04a3sLRAQBirb1/Dm0KcihqYYa8pAgK8ldffbUcDodyc3M1ePDgmvXnnHOOrrnmGgryAFpcYX6R5j3/mT797yKVFpYpo1u6xp49WuOmHaP4pLgDH6CNsdlsOv704Rp76uHak1csEzLqlJUih7PubMQvv96iSm9AGekJqir3yujHW6MsSfaQUZnHqaBlyddbKj3DoxX275UYTJDDsmlXVaFezP1QXxWu1/WDz1dCMxTl/b6APnl9iT5+5Uvl5e6Ww+nQERMO1YnTRqvXwK6NPs6eqnWSrHr/8LUsSzbLpV2Vq5ocLwC0J75AQJ99v0Ufr9+kwspKZThdSlxXrNzXvlZZM+XQk3oP1OqC3fKHgnLaauemCr9fTptd43se1Bxvp8V4K30KhUKyO+r/AtlmtykUMvJV+lo4spZljNGaPbu1vbREcU6nhmd3VZzTGeuwACAmOtrn0EiRQ1GLsaqXSMe2E2EX5D/44AO9//776t69e631/fv315YtW5otMABojJ3f5+veCx7Rjo15crgccjgd2rpmu569/T9a9PYyXTd7hpLTk2IdZlTYbDZ17prW4D4VlT7ZrOoCtdPlkM2yFAoZ2e3VicwyUsiyFHBIu04OyCTblG5PlsfpliTFyyN/KKA1JVv05vbPNa3XhCbF7PcF9Mgfn9fSj1ZKktxxLlWVe/XRy4u05INvdcX95+mwowc26lg2y64GvyI3RjbLoYrSSq1auE7eCq+y+3RW38N6MXsFQIdUVFmlW96ZqxXbdypkjBQyKiksl/EHlNbdpj67g82SQ0/uO0jvbFqj1YW7leh0KcHpkpFRqc+nqoBfx/c4SEdkdT/wgVqRrN6Zcv2QsxJT636E8lb45PI4ld2ncwyia36hkNEX6zfrrWWrtHpznpyyNLBnhnYk+PR9UYG8gaBsNksZ8Qk6f8hQnXPIENnIrQA6kI78OTRcHS2H7sPnUDQk7IJ8eXm54uPr3mJaUFAgt9vdLEEBQGMYY/SvG5/X9o15Ss9Old2xbxZegvy+gNYu2aBXHnhL0/9yXkzjjKXMTokKGSkYDMnhcig+JV5lhWUydpssSSFLshsjXy+//OlGnoBTnoTav8udNoecNrs+2bVcZ3Q/Th67K+J45v1nkZZ+tFIJSfHyxP94HGOMCvKL9dRtr+iBt66TJ/7A+SQr7jBtK/9SIRP8oTj/I2OMgvKr6JskXXXTLSreU6pQMCRXnEv9Du+ti/88Tb0P6XHAcwSCu1RZNVdFJbnhv1kAaGUe/PhzfbVth9LiPHI5HNr5fb7sZV7J41DRgBQVyaEemyqanEOTXW79bczJun/ZZ1q0M1f55WWyLEtJLrdOGzBYvz88p80VbzO6puuIE4fq0/8uUlyi5yd/c0ihUEilhWUaNLKf+g3rE8MoD8wYo73eddpWvljeUJniHZ3UK+FYJbt+vEMtGArpgXc/1esLv1VlhVfGH1TIbekbZ4Fkt9Q5IUFZyYkKhEIqrKzUI4sXyhsI6KLDaTMAoGPgc2h42ksObaxQKKQ3//G+3v3nhxF/Dm3PjKleIh3bXoTdtPfYY4/Vs88+W/PasiyFQiHdd999GjduXLMGBwAN2bwyV2u/3KDE1IRaSV2SnC6H3PFuLXjjS5UUtK0HxzWnUYf3UWpynIpKKmWMUXp2qpxul4L+gAKBoEKy5N5bqmJHhWx2mzpnd6r3OB6bS2WBShX4SiKOJRQK6aNXFsmSVasYL1XnkpROSSrIK9bSeSsbdbzeiccp3pGhymCBQiZYs96YkCqDBfIWWfrwzu0qK65QSmayMrqnyx3n1KpF62pms+yPMUYlZf9W3u6pKiy+V6Xlz0f2pgGgldhSUKQFm7YoweWU2+GQr9KnqnKvbHabHMHqO6Z29vAoaGueHNo5PlH3HztZz006W389ZqLuPWaSXjl5mv50xHGKc7TNFifn3nCGegzsqsK8IhXvKVFlWZVKCsq0d3uhMrqm69d3n9eqZ74FQl4t2P2QPtxxi74r+p82lLyvbwpe0Hvbr9a3hS/J/PAp950Va/Tagm9UVVguR7lf7pAUTLJk7JIJGO0pKpXf65fTblen+HjZbTY99+0K7amoiPE7BICWwefQ8LX1HBqOVx54Uy/99bWIPod2CKaJSzsRdkH+vvvu01NPPaXJkyfL5/PpT3/6kw499FB9+umnuvfee6MRIwDUK3f1dvmqfHVmdO/jSfSoorRKOzZ03ISXmODWRWflyOmwa09BmfyhkDJ6ZsiTnqiQ2ylnlU8ZZVU6dOQgJaYlyOmuv0gSkpHNsslphX1jVY3y4krt3l643/+/HE67ZEnbNuQ36ngee4qOzfqTEhyZqgwWqDywW+WB3aoI7pU9FK9v70uVf0+CUjOT5XDaZVmWPAkedeqapj07CvTOU3P3H2vlGyoufVjGVMluy5LDnhXRewaA1mJlXr4q/QEluqq/EPVV+WWMkc1W/eHX7g/J67GpIrH693xz5dBeyWk6oVd/je95kDLi6n+YW1uR2b2Tbnn5ap1+xWQlpiYo6A/K7XFp4sXjdNsr16rPoT1jHWKDVhT8W5tL58tmORVvz1CCI1Px9gwZY/Rt4X+0sXSuQiGj/y78RlUVXtmDRnanXcZuyZ9oyQpVf3gMWZZ27/nxC/oUj0elXq8+3bIpZu8NAFoSn0PD19ZzaGPt3rZX7/1rnhxuZ0SfQzuEfT3kI13aibArK4ceeqjWrVunRx99VElJSSorK9PUqVM1Y8YMdenSJRoxAkC97D88xHR/T7Q3oZAsy5LDGXkRuT04fvRAJca79eqc5dq4ZY9CoZDSM1N0yoi+OumYgerWrZP2mhJd//UTqghWKcFR+wFExhiVB6o0OLmXMtwpEcfhdDlk2SyFgqF6txtjZEImrP+/MjwDNan735Rb9rl2VX6nkILK8AzUxtct7fjsHaV3qdtizWazyRPv1qK3l+lXM8+u0x7HmIBKy/4tmaDsNYX4+mMGgLbC/PweX6u+lz+uJIfWLz07TefffKbOvu40lRdXKD4pTi5P5K3cWkpFYI++L50vh+WRy/ZjbrQsSx57sioCe7Wm+E2l20Zrc95eyReU3V49d8vYqiekWab6J8RYUkWVt/pnxGaTzap+wHpRVVVM3hsAtDQ+h0amrebQcCx5b7kqyqqU3iW1zrYDfQ5FxxLRb4eUlBTdfPPNzR0LAITlkNEDlZASr/LiCiWlJdbZXl5UocweGep1SNt6cFw0jDy8t44c2ks78ovl9fmVkZ6k5ERPzfYuylBOxiH6OH+5LFmKs7urW5KZkIr95XLZnTql29FNuo3Qk+DWIaP6aemHK5WQHFfnWN5Kn5wuh4aM7h/ece3JGpBykgaknFSz7ttdb8hms2Sz1X8jmMPlkN/rV0VJRZ0/hPz+tQoEt8qyRf7lAwC0NoM6Z8rjcKjC71eCy6W4RI9sdptCwZBsdrsCDptcvpDiywOSyKEH4nQ5lZrZdvJEfuVK+UPlirfX35rOZUtUmT9PZcGtCoVCqk7RP3xN80NLI2NV/68kyUihYEh2m03BUEjGSJ3qec4YALRHfA5tmraWQ8NRWlge8efQjsIyP/l7IoKx7UXYLWv69eun22+/XevXr49GPADQaGlZqTruFznyVfhUWVpZM/vPGKOywnIZI02efrycrrbZq7a5WZalbtmp6tszs1Yxfp+L+5ysYzIPU8AEtddXor2+EhX4ShXncOviPifpyPRBTY5h0i+PkTvepcLdJbVmynsrfSotKtfgIw/SgGG9m3ye1M7JMiGz39n4fq9f7niPElPrtk8ImSpJIVmy1x0IAG1Uv8xOGt6jq0qqfPIHg3I47EpKS1AoZOS3jIxdyt5WKVuAHNoehUzgh3/V/8W6ZdlkZBTnstQjPkFBh63myWk2I7nKjIzth/vFjGQPGdns1XmyqKpKqR6PxvTqHfX3AQCtAZ9DsT9N+RzaYdBDXlIEBfkZM2bonXfe0cCBA3XkkUfq73//u/Ly6IsFIDbOu3mqRk8ZKV+VX3u3F2r31r3au71AxhidevmJmngxD5tuLI/dpd/3P1N3DrlEZ/Ucp1O65ujXfU/Wg4dfofFZI5p8fGNC6jvcrel/Ga2kTk4V7CrWnp2F2r2zUJVlVTo0Z4B+99dzm+VhPiNPGq6ElHiVFpTV2RYMhuSr9OmYM0bWe4uk09FTljwyhofTAWhf/nT8sRqclamCikrll5ZLKR5ZneIUdFpK3Fgiz6fbyKHtVIqrh+yWU0HjrXe7P1Qppy1Oya5u+tWEkbLLktf24+feuCIjm18yDkkmpKTEOPlCQe0qL5MlafqwEUr1xNV7bABoj/gcivo05XNoh0EPeUmSZeo0lGycdevW6fnnn9eLL76oTZs2ady4cfrlL3+pCy64oLljjKmSkhKlpKSouLhYycnJsQ4HQD2MMVq37Ht9+e5XKissV3qXNOWcdoR6DuoW69Cg6v9/9lR8qO2lL6kisKn6IYKhRJV+f6h2LB4qpz1RQ48ZqINHHbTfW/si8faTc/XCX/6nUDCkxLQE2R12VZV7VVFSqW79snXLy1cro1v9t+7vLbxF5ZVvyG7rLMtyqLQ0oEH9PyIXhIkcCrQ+ZV6v5q7dqHnrNqqwolJdU5J1mDNJwaXbVVlYQQ5tp4wxmrvjRu2qWqV4e4Zs1o/5Nmj8qgwWalDKqToi41IZY3TzAy/rvS1bFHJYP3xRbilkN/LHh2SLs5TQKVFOh0Pdk5N1wWHDdFL/Afv9Qp1cEBmuG9D68TkU9WnK59Cfa0+5YN976THrLtni6t6x3xihyiptvfrWdnE9Ii7I/9SiRYt0+eWX65tvvlEwGGyOuFqN9vTDDwD1CQSCCviDcrmdstma/xvnbSUvaEvxUzImIIctUZJNQVMhY/xK8RyhgzP+Krut+WfVGWP08Utf6M1/vK9dubsVChq54pwadvyhOu+mM5XVK3O/YwPBPO3ZO0O+wHpZcqmszK6DB35JLggTORQAWo9iX67m7/yzygJ5slku2eVQwHhlZJThHqAxXW6Rx179u9oYoxdnf6iXPvhSe0N+2fwhpe716rhBvTXhyonyJTsV53RqcEamnPaGW7yRCyLDdQOAtqkpn0N/rj3lgpqC/N+aWJC/pn0U5Jv0yOcvv/xSL7zwgl5++WWVlJTorLPOaq64AABRlrtpt+a8sVxLFqxXIBBSeqdEjZs0RBNOOkyeuOa5ha4ysF1bS2ZLsuR2dK5Zb5dHIeNVcdUy5Ze/ra5JzZ8/LMvS8dOO0XG/OErff7NF3gqfsnpnqnOPjAOOddizldnpSZWWv6DyyrckFTZ7fAAAtKQUV09N6Hq31pfO0abS+fKHKpXs6Ka+yePVL+lEuew/9rO1LEvnXXyCzv7luLBzKAAAHVlTPod2CE3pBd+OesiHXZD/eaua448/Xvfee6+mTp2qxMS6T5cGALQ+3329VX//y1sqLqqQ2+OU3W7Tzh2Fev6fn2jFkk269rbTFRff9KL8nop5CobK5bLXnQVgs9ySZUWtIL+Pw+nQgBEHhT3Obs9QavIflJJ0ueLdOyT1bvbYAABoSQnOTB2e/isNTfuljIKyWQ1/HIw0hwIA0NE1JYeGQkZfb9qhuUu/a+ao0FqEXZAfNGiQjjzySM2YMUPnnnuusrKyohEXACBK/P6g/vnwByoprlRG56Sanq8JiW75fAGtXJGrd15dql/8cnSTz+UN5MnIyLLq7w1vt9yqCuQpZAIHLArEimU5ZbelxToMAACajWVZspp2szQAAIiCCq9P9/73Yy1ak6uqivJYh9P8mCEvKYKC/Nq1a9W/f/9oxAIAaAZV/oA++36zNuwpkMNmaUSPbjq8WxfZfii8L//ye+3aUaSUtPg6D2BzuRxyOGya/8FKnXb2SLlcTfuwbrfFy5IlY0y9D3sLmYCctkRZarj/LAAAAAAA7d3j7yzU599tVnKcW0mOhAMPaGuMVb1EOradCLvSQjEeAFqvr7fv1J8/mK+dJaWqfmS30XNLV+jQLtmaOXGcOiclasfWAhlJTmf9RXBPnEslxZUqKihX5+yUJsWTHnesdpT+VyFTKbsVX2ubMSEZ41Nm/An1FusBAAAAAOgo8gpLNf/bjYp3OxXndspf5Y91SM3OMtVLpGPbi/p7CPxMenq69uzZI0lKS0tTenr6fhcAQGzkFhbp5nc+1I7iEqXFeZSVlKDOiQlKcLr01dYduuXdD+ULBuVyOWRM9dPf6xMKhWSzrP0W7MOR7BqidE+OAqFSBUKlNecMGq+8wd1yObKUnTSlyecBAAAAAKAt+2bzTlV4/UrwNP15bmjdGjVDftasWUpKSqr5NzMZAaD1eePb1dpbUaGsxISa9jSWZcnjdCjd8mhV3i4t2rxVQ4b3ktvtUGWFT/EJ7lrHMMaootyrIcN6KTW96bfHWZZN/TvdInvhLO2pnC9fcJckS5ZlV6JrgPqnX684R7eIjl1V4dXid77SV/O+kbfcq56Du+mYqUep56DIjgcAQEdBDgUAIDLRzKGBYEiS1K6rri3cQ/7TTz/V/fffr2XLlmnnzp167bXXNGXKlB8PaYxmzpypf/7znyoqKtLRRx+txx9//IAdYh577DHdf//9ysvL09ChQ/XII49o5MiRjY6rUQX5Cy+8sObfF110UaMPDgBoOZ9s2CSX3VZTjP8pt8OhoKnUos25Om78cTpidD998fHq6oJ9nFOWZSkUCqm4sEJut1MnnTGi2b58ddgSNKDTLerhv1BF3mUKGZ/inX2U6h6x34e9Hkj+lt16YPo/lLt6m0zIyLJZ+urDbzTnmY919h9P1ym/OaFZYpckY/wyoYpmOx4AALHUkjkUAID2JNo59KDsTnI57ar0BRTvdjZT1B1beXm5hg4dql//+teaOnVqne333XefHn74Yf373/9Wnz59dOutt2rixIlatWqVPB5Pvcd8+eWXdc011+iJJ57QqFGj9NBDD2nixIlau3atOnfu3Ki4wu4hb7fbtXPnzjon2Lt3rzp37qxgMBjuIQEAzaAyEJCjoQK3qX7gqyRNv2KC/P6gli/+XuVlXlmWZIyUmOTRedOP07CRfZs9vjhnD8U5ezT5OMFgUH+//Clt/m6r0jqnyPHDg2eNMSrZW6oX//qasvt01hEnDm3SeYx/vUzl/yTvJzKlVU2OGwCAWGupHAoAQHvTEjl0QLcMHdIzS19t3CF3M7SQbY0sNaGHfARjJk+erMmTJ9e7zRijhx56SLfccotOP/10SdKzzz6rrKwsvf766zr33HPrHfe3v/1Nl156qS6++GJJ0hNPPKF33nlHTz/9tG644YZGxRX21MT99Rz2er1yuehxBACx0rdTuqr286VoyBhZlqVe6amSpPgEt66++VTddv85mnreUZp0+nBd/Lvj9cBTF+n4SUNaMOrwffPJKm36NlcpGUk1fwRJ1e15UjKS5ff69f4zHzXpHMa3RKboD1LVm5LxRn5LHQAArUhL5FAAANqjlsihlmXp6tOPU6/MVO0pqVBBaWVTw26XSkpKai1erzei42zatEl5eXmaMGFCzbqUlBSNGjVKCxcurHeMz+fTsmXLao2x2WyaMGHCfsfUp9Ez5B9++GFJ1T8c//rXv5SYmFizLRgM6tNPP9WgQYMafWIAQPM65ZCBWrF9pyp8fsW7fry9zRijwopKJbpdOmFgv5r1lmWp/6Au6j+oSyzCjdiaxRsUCoXk2s+DbuISPVq/7HtVllcpLqH+W8waYkyVTOm9kimRrCzJsiRb+3u6PQCg44l2DgUAoL1qqRzaLSNFD15yquYsW6t3F30d8XFaLWNVL5GOldSjR+0772fOnKnbb7897MPl5eVJkrKysmqtz8rKqtn2c3v27FEwGKx3zJo1axp97kYX5GfNmiWpurDzxBNPyG7/8dYJl8ul3r1764knnmj0iQEAzWvCgIP05ZZten/NepX6fEpwOmWMUbnfL7fDocuPHqWuKcmxDrPJQqFQwzPWf2i/E/rhgThh834hBfMkK726GA8AQDsR9RwKAEA71ZI5ND0pXueNHaZThh+k52+8uMnHa1Wa4aGuW7duVXLyj7UNt9vd5LBaWqML8ps2bZIkjRs3Tq+++qrS0tKiFhQAIHx2m003njBGh3bJ0hsrV2tbUbEsy9LRfXrqzKGH6qjeTe/f3hr0GdJTsiwFfIFatwruU1VWpX7D+ig+KS6yEwQ3V/+vxUN0AADtS9RzKAAA7RQ5tJk0Q0E+OTm5VkE+UtnZ2ZKk/Px8denyY+eA/Px8HX744fWOycjIkN1uV35+fq31+fn5NcdrjLAf6vrxxx+HOwQA0EIcNpvOOOxgnT5ksEqrvHLYbUpoZ8/3OOLEoerSt7N2bMhTetc02Ww/Pg6loqRCNptNJ14wVlbEs9tdkkz19AZmyAMA2pHo51AAANoncmj706dPH2VnZ2vevHk1BfiSkhItXrxYl19+eb1jXC6XRowYoXnz5mnKlCmSqu+emDdvnq644opGnzvsh7qeeeaZuvfee+usv++++3TWWWeFezgAQBTYLEspcZ5WU4yvLK/S1598p6UffK2dm/IPPKABLo9LVzw8XZ26pqtgR6EK84pUvKdEe7YXyFfp14QLxujYXxzVhBOMlCyXpIomxQkAQHNoUzkUAIBWhBza+limaUu4ysrKtGLFCq1YsUJSdQeYFStWKDc3V5Zl6aqrrtKf//xnvfnmm/r22291wQUXqGvXrjXFdkkaP368Hn300ZrX11xzjf75z3/q3//+t1avXq3LL79c5eXluvjixrcXCnuG/Kefflpvo/zJkyfrwQcfDPdwAIB2LBgM6s3H3tf7z3ysot0lMqGQ3PFuHTbmYF1w+9nq3CMjouP2G9ZHd715vea/vECL3/1K3gqveg7urrHnHK3hE4bUmq0QNscAyZUjeedLsiRxyyEAoOW1yRwKAEArQA5txZqhZU04li5dqnHjxtW8vuaaayRJF154oWbPnq0//elPKi8v12WXXaaioiIdc8wxmjNnjjyeHx/Mu3HjRu3Zs6fm9TnnnKPdu3frtttuU15eng4//HDNmTOnzoNeG2IZY8J6O3FxcVqxYoUGDhxYa/2aNWs0bNgwVVZWhnO4Vq+kpEQpKSkqLi5ulv5EANCRPHv7y3rnn/Nkd9iUmJogm92myvIqVZRUqseArrr1lWuV1jkl1mHWYUJlMiV3Sb7FknwqKQ0qrd+X5IIwkUMBIHJtNYf+HLkgMlw3AIgcObT12fdeev/5btl+UuwOR6iqSptvubldXI+wv7oZMmSIXn755TrrX3rpJR188MHNEhQAoPG2FBXpxZXf6Only/T+xvWq9PtjHZIkadu6HZr7/z6VO96l1M4pcrgcstltSkiOV3p2qrat26EPn/0k1mHWy7Ilykr5q6y0R2TFXyh5psY6JABAB9KWcygAALFEDm3lTBOXdiLsljW33nqrpk6dqo0bN+r444+XJM2bN08vvviiXnnllWYPEABQv6qAXw8u/EIfbNygqkCg+vmjRuqckKDrRh+rY3v1jml8i95eJm+lT526ptXZZnfY5XA5NP8/C/SLa09tlQ++sSxLcg6RnENkC5VIuiHWIQEAOoi2nkMBAIgVcmjrFmkv+H1j24uwZ8ifeuqpev3117Vhwwb97ne/07XXXqtt27bpww8/rNXwHgAQXQ8u/EJvrl0jm2Wpc0KCOickKj0+TrsqynX7Jx/p67ydMY2veE+pJLPfP3KcbqfKiyvk97aOGf0AALQW5FAAACJDDm3ljNW0pZ0Ie4a8JJ188sk6+eSTmzsWAEAjbS4q1AcbNyje6VSS212z3mGzKzM+QfllZXr+2681NLtLzGJMyUiSJBlT/x9Dfq9faVmpcrqdLR0aAACtGjkUAIDIkEPRFkT0+N+ioiL961//0k033aSCggJJ0ldffaXt27c3a3AAgPot2JqrqkBAiS5XnW2WZSnB5dKSHdtVXFUVg+iqHXXKCHni3SovrqizLRgIKuALaOzZo7lNEACAnyGHAgAQGXJoK0cPeUkRFOS/+eYbDRgwQPfee6/uv/9+FRUVSZJeffVV3Xjjjc0dHwCgHhV+vyxL+/0jwm6zKWRCqgzE7ja87gO6asIvj5O3wqeiXcUK+AIKBUMqL6lQQV6Rug/sqgkXjIlZfAAAtFbkUAAAIkMObd329ZCPdGkvwm5Zc8011+iiiy7Sfffdp6SkpJr1J510ks4777xmDQ4AUL/uySkyRgqEQnLY6n63WhXwK80TpzRPXAyi+9H5t/5CyZ2S9f7sj1WYXyxjQnLHuXXUySN0we1nK61zSkzj258tuwr16crvVVhWqbTEOA3rlRHrkAAAHUxbzaEAAMQaObQVa8pM945ckF+yZImefPLJOuu7deumvLy8ZgkKANCw43r1VueEBO2uKFdmfEKtmfK+YFC+YFAn9x8otyOiR4U0G7vdrim/n6yJvx6ndUs3yu8NqFv/bHXpkxXTuPYnGArpn3MW660vV6nKF6hZ//8MD/wBALSstpZDAQBoLcihaO3CrtS43W6VlJTUWb9u3TplZmY2S1AAgIbFO526NucY3fHJR8ovL1OC0yWHzabKgF++YFCHZmZp2qGHxTrMGnEJHg0dc0iswzigVz77Rv/94lt5nA51Tqn+osMYo8Ki4liHBgDooNpKDgUAoLUhh7ZCTWk9045myIfdQ/60007TnXfeKb+/eragZVnKzc3V9ddfrzPPPLPZAwQA1G9s7z7624mTNaZXH0mSNxhQqidOFx8+XLMmnqQUjyfGEbYtlV6/Xl+0Ug6bpeR4d81dB5ZlKSmu7sNzAQAAAABAGHioq6QIZsg/+OCD+sUvfqHOnTursrJSY8aMUV5ennJycnT33XdHI0YAwH4M69JVw7p0VVFVpSr8fnWKi2/RNjUhY1QV9Mtlc9Tby74tWbU1XwWlFUpJ4IsMAAAAAACaHT3kJUVQkE9JSdHcuXP1+eef65tvvlFZWZmGDx+uCRMmRCM+AEAjpHrilNqCD3At83v1+uZv9Wbud9pTVS6Xza7xXQfoF32Hqk9SeovF0Zy8/qBCRrL9pB8/AAAAAABAc4p4GuUxxxyjY445pjljAQC0AaV+r65b/KZW7N0hmyV57E5VBPx6ZdMKfbxzg+4deYqGpHeJdZhh652VJo/LoUqfX4ked6zDAQAAAACgXbGa0EM+4t7zrVCjCvIPP/ywLrvsMnk8Hj388MMN7puYmKhDDjlEo0aNapYAAQCty/9bv1TL92xXujtOLvuPaSRkPNpVWaZ7VnyoZ8ee3+Za2HRNT9aR/Xvok5Xfy+N0ymH/Mf5gsB1lfgAAAAAAEDONKsjPmjVL559/vjwej2bNmtXgvl6vV7t27dLVV1+t+++/v1mCBAC0DpUBv97JXSWX3V6rGC9Vt3pJc8dpc1mhvtydq9FZvWMTZBP8dvJRyt1dpM35BXLabXI67PIHgqqsqIh1aAAAAAAAoB1oVEF+06ZN9f57f+bOnavzzjuPgjwAtDN5lSUq9XsV73DWu91td8j4qrSlrKBNFuSz0pJ0/69P1ltfrtIHX61TaaVX6ckJOvaI/vr0b7GODgAAAACANoyHukpqQg/5hhxzzDG65ZZbonFoAEAMuW0OWZaloKk/E4aMkZGRyxaV9FKHMUZlgTIFTVBJziTZLXuTj5meFK8Lxx+hC44fIV8gKJfDrtLSUv2uGeIFAAAAAKCjood8tYgqJvPmzdOsWbO0evVqSdLgwYN11VVXacKECZKkuLg4XXnllc0XJQCgVegSn6wBKZn6tmCHEhyuOtvLAz7F2Z0amdkz6rF8W7xSn+z6TFsqtshISnWmKKfTUTou8xg5bfXP4A+HZVlyO1vmiwUAAAAAADqEdlRYj1TYT9z7xz/+oUmTJikpKUlXXnmlrrzySiUnJ+ukk07SY489Fo0YAQCthGVZOu+g4XLZHNpbVa7QDzPljTEq9/tUHvBpfNcB6pGYGtU4Ptv9uWZvelbrytbLkiW7bNrj3aM3drylf2/+fwqEAlE9PwAAAAAAQCTCnvr3l7/8RbNmzdIVV1xRs+4Pf/iDjj76aP3lL3/RjBkzmjVAAEDrMq5rPxV4j9MTqxdod1VZzXqXzaETuw3UtYeNjer593oL9NbOdxUyRmnO1Jr1HrtH3pBP3xZ/pyUFS5WTcVRU4wAAAAAAAGGgh7ykCAryRUVFmjRpUp31J554oq6//vpmCQoA0Lqd2ecwHZfdVx/uWKe8ilLFO1w6NruPBqdmybKsqJ77q8LlqgxU1irG7+O2uVQRrNDCgsUU5AEAiLFgIKR1q7bHOgwAANBK0EO+WtgF+dNOO02vvfaarrvuulrr33jjDZ1yyinNFhgAoHXLjEvUtIOGt/h5d3t3S9J+C/8uy6X8ql0KmZBsVtid2QAAQDNY8Mka/e/5hdq2JS/WoQAAgNaCGfKSGlmQf/jhh2v+ffDBB+vuu+/W/PnzlZOTI0latGiRvvjiC1177bXRiRIAgB+47W41NAk/pKDibXEU4wEAiJHP5q3SP/8+V35/QHEJdR8CDwAA0JE1qiA/a9asWq/T0tK0atUqrVq1qmZdamqqnn76ad1yyy3NGyEAAD9xcPJgfbb7C/lCPrlstT/kG2PkDwU0PHVYjKIDAKBj81b59Z9/f6FAIKBOmUny+SpjHRIAAGglaFlTrVEF+U2bNkU7DgAAGmVg0gAdlNhX60rXK95u5La5ZFmWAqGASoNlSnYma3RGTqzDBACgQ/p2+Rbt2V2ilNT4WIcCAABaG1rWSJIivp9/z5492rNnT3PGAgDAAdksmy7ufYEOTTlYAeNXUaBYRf4ilQXL1dmdqel9LlJnT2aswwQAoEMqKaqQMZLDaY91KAAAAK1SWA91LSoq0s0336yXX35ZhYWFkqrb15x77rn685//rNTU1GjECABALYnORF3W9xLlVmzVutL1CpqAusR10SHJB8thC/t55QAAoJkkp8bLsqSAP0hRHgAA1MYMeUlhFOQLCgqUk5Oj7du36/zzz9fgwYMlSatWrdLs2bM1b948LViwQGlpaVELFgCAfSzLUq+EnuqV0DPWoQAAgB8MGdZLnTKTVLC7THEJLhUXlcU6JAAA0ErQQ75aowvyd955p1wulzZu3KisrKw620488UTdeeeddR4ACwAAAADoGNwep86YdpQevucdFewtVTDkjXVIAACgtWCGvKQwesi//vrreuCBB+oU4yUpOztb9913n1577bVmDQ4AAAAA0LZsXJsnu92Sy+WQZVmxDgcAAKBVaXRBfufOnTrkkEP2u/3QQw9VXl5eswRVn7vvvlujR49WfHx8o3vVX3TRRbIsq9YyadKkqMUIAEBrRA4FgNZj0+bdeu75BZr10Bz96/8+0dff5CoUaj9Tvnbnl2jhJ2uUnBqvHn0ylNUtNdYhNQk5FACAZmSauLQTjW5Zk5GRoc2bN6t79+71bt+0aZPS09ObLbCf8/l8Ouuss5STk6P/+7//a/S4SZMm6Zlnnql57Xa7oxEeAACtFjkUAGIvFDJ64cWFenfO1/J6/ZIsSUZz563U4UN76srfn6j4+Lb/e3b1t1tVWeFTemaSLMtSnMcV65CahBwKAEDzoYd8tUYX5CdOnKibb75Zc+fOlctV+48qr9erW2+9Narf+t9xxx2SpNmzZ4c1zu12Kzs7OwoRAQDQNpBDASD2Ppj7rd586ys5XQ5ldEqqaeVSVeXX0mWb9fTsz3TF7ybEOMqmCwRCMpLaS6cacigAAM2IHvKSwmhZc+edd2rt2rXq37+/7rvvPr355pt644039Ne//lX9+/fX6tWra/5YaU3mz5+vzp07a+DAgbr88su1d+/eBvf3er0qKSmptQAA0BGRQwGgeQQCQb373jeSpKRET62+6h6PU3EepxYt2qD8XcWxCrHZ9OqbKZfbIW+VP9ahxBQ5FACA2Ordu3edFnKWZWnGjBn17j979uw6+3o8nqjE1ugZ8t27d9fChQv1u9/9TjfeeKOMqf5awrIsnXDCCXr00UfVo0ePqAQZqUmTJmnq1Knq06ePNm7cqJtuukmTJ0/WwoULZbfb6x1zzz33tMovFgAAaEnkUABoPtu2F2r37hIlJNTftiQhwa09e8u0atUOZXVOaeHomlff/lnqP6irvvs6V05Xoz9utivkUAAA6teSLWuWLFmiYDBY83rlypU64YQTdNZZZ+13THJystauXfvjOaN0y1+jZ8hLUp8+ffTee+9pz549WrRokRYtWqTdu3drzpw56tevX9gnv+GGG+r9puKny5o1a8I+7j7nnnuuTjvtNA0ZMkRTpkzR22+/rSVLlmj+/Pn7HXPjjTequLi4Ztm6dWvE5wcAIFrIoQDQdgQDQYWMOeCHukAg2OD2tsCyLF36hxPUrUe6CveWqaigPNYh1UEOBQAgRprhoa4/v6PM6/XWe6rMzExlZ2fXLG+//bYOOuggjRkzZr/hWZZVa0xWVlazvO2fi2jKQlpamkaOHNnkk1977bW66KKLGtynb9++TT7PT4+VkZGhDRs2aPz48fXu43a7eeAOAKDVI4cCQNvRpUuqkhI9Ki/3ylXPrPEqr18up129emXEILrm16V7mm677xx9/P63+nDOV7EOpw5yKAAAMdIMPeR/3qFl5syZuv322xsc6vP59Nxzz+maa65pcIJEWVmZevXqpVAopOHDh+svf/mLDjnkkAgD3r+Y3kOYmZmpzMzMFjvftm3btHfvXnXp0qXFzgkAQDSQQwGg7YiPd2vsmEF67Y2v5PMFahXlg8GQysqqdMjB3dW/X3RmYcVCanqCzph2lMaffLD+8Wz9vVpjhRwKAEDbtXXrViUnJ9e8bswX2q+//rqKiooa/EJ+4MCBevrpp3XYYYepuLhYDzzwgEaPHq3vvvtO3bt3b47Qa4TVsiaWcnNztWLFCuXm5ioYDGrFihVasWKFysrKavYZNGiQXnvtNUnV32hcd911WrRokTZv3qx58+bp9NNPV79+/TRx4sRYvQ0AAFocORQAYu/MqUdq6GE9VFpWpb0FZSoprVRBYbkKCsvVrWu6fvub46PWpxSRI4cCANB8rCYuUnWf958ujSnI/9///Z8mT56srl277nefnJwcXXDBBTr88MM1ZswYvfrqq8rMzNSTTz4Z6dvdrzbzlJ3bbrtN//73v2teDxs2TJL08ccfa+zYsZKktWvXqri4WJJkt9v1zTff6N///reKiorUtWtXnXjiibrrrru4FRAA0KGQQwEg9uLiXLr+upP16Wdr9fH81crfVaKkJI+OO2agxo0drLS0hFiHiHqQQwEAaEbN0LImXFu2bNGHH36oV199NaxxTqdTw4YN04YNGyI7cQMsY0ykl6FDKCkpUUpKioqLi2vdDgEA6DjIBZHhugEAyAWR4boBANpTLtj3Xg6+/C+yuz0RHSPordKqx28K+3rcfvvtevLJJ7V161Y5HI2fmx4MBnXIIYfopJNO0t/+9rdIQt6vNjNDHgAAAAAAAADQNlmmeol0bLhCoZCeeeYZXXjhhXWK8RdccIG6deume+65R5J055136qijjlK/fv1UVFSk+++/X1u2bNEll1wSWcANoCAPAAAAAAAAAIiuFm5Z8+GHHyo3N1e//vWv62zLzc2Vzfbj41ULCwt16aWXKi8vT2lpaRoxYoQWLFiggw8+OMKA94+CPAAAAAAAAAAg+lqwefqJJ56o/XVrnz9/fq3Xs2bN0qxZs1ogKsl24F0AAAAAAAAAAEBTMUMeAAAAAAAAABBVLd1DvrWiIA8AAAAAAAAAiK4W7iHfWlGQBwAAAAAAAABEFTPkq9FDHgAAAAAAAACAFsAMeQAAAAAAAABAdNGyRhIFeQAAAAAAAABAlNGyphotawAAAAAAAAAAaAHMkAcAAAAAAAAARBctayRRkAcAAAAAAAAARBsFeUkU5AEAAAAAAAAAUUYP+Wr0kAcAAAAAAAAAoAUwQx4AAAAAAAAAEF20rJFEQR4AAAAAAAAAEGWWMbJMZJX1SMe1RhTkAQAAAAAAAADRxQx5SfSQBwAAAAAAAACgRTBDHgAAAAAAAAAQVZapXiId215QkAcAAAAAAAAARBctayRRkAcAAAAAAAAARBkz5KvRQx4AAAAAAAAAgBbADHkAAAAAAAAAQHTRskYSBXkAAAAAAAAAQJTRsqYaLWsAAAAAAAAAAGgBzJAHAAAAAAAAgFZiQ8FezfluZazDaH60rJFEQR4AAAAAAAAAYs4XDOrBhZ/rvQ3rVFFaGutwoqI9tZ6JFAV5AAAAAAAAAIixJ5Z+qdfXrFa806mM+MRYh9P8jKleIh3bTtBDHgAAAAAAAABiaE9Fhd5Yu1puh11Jbrcsy4p1SIgSZsgDAAAAAAAAQAwt3bFdpT6vOie0w5nxP7BM5C1r2lOrGwryAAAAAAAAABBD3mBAktSu58XzUFdJFOQBAAAAAAAAIKZ6p6TKZbOrKhBQnNMZ63CiwgpVL5GObS/oIQ8AAAAAAAAAMTQkK1sDOmWouKpKoXb0AFPURUEeAAAAAAAAAGLIZlm6/pjjlJWYqF3lZSqqqox1SM3PNHFpJyjIAwAAAAAAAECMDeyUocdOOlXnHjJECe2wbc2+h7pGuoTj9ttvl2VZtZZBgwY1OOaVV17RoEGD5PF4NGTIEL377rtNeLf7R0EeAAAAAAAAAFqBnimpuibnGL38i3NjHUrzM6ZpS5gOOeQQ7dy5s2b5/PPP97vvggULNG3aNE2fPl3Lly/XlClTNGXKFK1cubIp77heFOQBAAAAAAAAoBVx2u2xDqHNczgcys7OrlkyMjL2u+/f//53TZo0Sdddd50GDx6su+66S8OHD9ejjz7a7HFRkAcAAAAAAAAARFVztKwpKSmptXi93v2eb/369eratav69u2r888/X7m5ufvdd+HChZowYUKtdRMnTtTChQub5b3/FAV5AAAAAAAAAEB0NcNDXXv06KGUlJSa5Z577qn3VKNGjdLs2bM1Z84cPf7449q0aZOOPfZYlZaW1rt/Xl6esrKyaq3LyspSXl5e095zPRzNfkQAAAAAAAAAAJrZ1q1blZycXPPa7XbXu9/kyZNr/n3YYYdp1KhR6tWrl/7zn/9o+vTpUY+zIRTkAQAAAAAAAABR9dPWM5GMlaTk5ORaBfnGSk1N1YABA7Rhw4Z6t2dnZys/P7/Wuvz8fGVnZ4d9rgOhZQ0AAAAAAAAAILqMadrSBGVlZdq4caO6dOlS7/acnBzNmzev1rq5c+cqJyenSeetDwV5AAAAAAAAAEBUNcdDXRvrj3/8oz755BNt3rxZCxYs0BlnnCG73a5p06ZJki644ALdeOONNftfeeWVmjNnjh588EGtWbNGt99+u5YuXaorrriiOS+BJFrWAAAAAAAAAADakW3btmnatGnau3evMjMzdcwxx2jRokXKzMyUJOXm5spm+3Gu+ujRo/XCCy/olltu0U033aT+/fvr9ddf16GHHtrssVGQBwAAAAAAAABEl/lhiXRsGF566aUGt8+fP7/OurPOOktnnXVWeCeKAAV5AAAAAAAAAEBUNcdDXdsDCvIAAAAAAAAAgOgKmeol0rHtBA91BQAAAAAAAACgBTBDHgAAAAAAAAAQXS3YQ741oyAPAAAAAAAAAIgqS03oId+skcQWLWsAAAAAAAAAAGgBzJAHAAAAAAAAAESXMdVLpGPbCQryAAAAAAAAAICoskwTWta0n3o8BXkAAAAAAAAAQJTxUFdJ9JAHAAAAAAAAAKBFMEMeAAAAAAAAABBVljGyIuwFH+m41oiCPAAAAAAAAAAgukI/LJGObScoyAMAAAAAAAAAoooZ8tXoIQ8AAAAAAAAAQAtghjwAAAAAAAAAILrMD0ukY9sJCvIAAAAAAAAAgOgypnqJdGw7QUEeAAAAAAAAABBVlqleIh3bXtBDHgAAAAAAAACAFsAMeQAAAAAAAABAdNGyRhIFeQAAAAAAAABAlFmh6iXSse1Fm2hZs3nzZk2fPl19+vRRXFycDjroIM2cOVM+n6/BcVVVVZoxY4Y6deqkxMREnXnmmcrPz2+hqAEAiD1yKAAAkSGHAgCAaGgTBfk1a9YoFArpySef1HfffadZs2bpiSee0E033dTguKuvvlpvvfWWXnnlFX3yySfasWOHpk6d2kJRAwAQe+RQAAAiQw4FAKCZ7WtZE+nSTljGtM13c//99+vxxx/X999/X+/24uJiZWZm6oUXXtAvfvELSdV/UA0ePFgLFy7UUUcd1ajzlJSUKCUlRcXFxUpOTm62+AEAbUd7ywXkUABAS2lvuYAcCgBoKe0pF+x7L2OPvFkOhyeiYwQCVZq/5O52cT3axAz5+hQXFys9PX2/25ctWya/368JEybUrBs0aJB69uyphQsX7nec1+tVSUlJrQUAgPaEHAoAQGTIoQAARM4ypklLe9EmC/IbNmzQI488ot/85jf73ScvL08ul0upqam11mdlZSkvL2+/4+655x6lpKTULD169GiusAEAiDlyKAAAkSGHAgCA5hDTgvwNN9wgy7IaXNasWVNrzPbt2zVp0iSdddZZuvTSS5s9phtvvFHFxcU1y9atW5v9HAAANBU5FACAyJBDAQCIEXrIS5IcsTz5tddeq4suuqjBffr27Vvz7x07dmjcuHEaPXq0nnrqqQbHZWdny+fzqaioqNbshPz8fGVnZ+93nNvtltvtblT8AADECjkUAIDIkEMBAIgRIynUhLHtREwL8pmZmcrMzGzUvtu3b9e4ceM0YsQIPfPMM7LZGp7cP2LECDmdTs2bN09nnnmmJGnt2rXKzc1VTk5Ok2MHACCWyKEAAESGHAoAQGw0pRc8PeRb2Pbt2zV27Fj17NlTDzzwgHbv3q28vLxaPfi2b9+uQYMG6csvv5QkpaSkaPr06brmmmv08ccfa9myZbr44ouVk5PT6CfbAwDQ1pFDAQCIDDkUAABEQ0xnyDfW3LlztWHDBm3YsEHdu3evtc388O2I3+/X2rVrVVFRUbNt1qxZstlsOvPMM+X1ejVx4kT94x//aNHYAQCIJXIoAACRIYcCANDMjCLvBd9+JsjLMqYdzfePgpKSEqWkpKi4uFjJycmxDgcAEAPkgshw3QAA5ILIcN0AAO0pF+x7L8cPvV4Oe2TPTAkEvfro63vbxfVoEy1rAAAAAAAAAABo69pEyxoAAAAAAAAAQBsWkmQ1YWw7wQx5AAAAAAAAAEBUWcY0aQnHPffcoyOPPFJJSUnq3LmzpkyZorVr1zY4Zvbs2bIsq9bi8Xia8pbrRUEeAAAAAAAAABBdxjRtCcMnn3yiGTNmaNGiRZo7d678fr9OPPFElZeXNzguOTlZO3furFm2bNnSlHdcL1rWAAAAAAAAAADajTlz5tR6PXv2bHXu3FnLli3Tcccdt99xlmUpOzs7qrExQx4AAAAAAAAAEF3NMEO+pKSk1uL1eht16uLiYklSenp6g/uVlZWpV69e6tGjh04//XR99913TXvP9aAgDwAAAAAAAACIrmYoyPfo0UMpKSk1yz333HPA04ZCIV111VU6+uijdeihh+53v4EDB+rpp5/WG2+8oeeee06hUEijR4/Wtm3bmu0SSLSsAQAAAAAAAABEW0iS1YSxkrZu3ark5OSa1W63+4BDZ8yYoZUrV+rzzz9vcL+cnBzl5OTUvB49erQGDx6sJ598UnfddVdkcdeDgjwAAAAAAAAAoNVLTk6uVZA/kCuuuEJvv/22Pv30U3Xv3j2sczmdTg0bNkwbNmwIN8wG0bIGAAAAAAAAABBVljFNWsJhjNEVV1yh1157TR999JH69OkTdrzBYFDffvutunTpEvbYhjBDHgAAAAAAAAAQXT/pBR/R2DDMmDFDL7zwgt544w0lJSUpLy9PkpSSkqK4uDhJ0gUXXKBu3brV9KG/8847ddRRR6lfv34qKirS/fffry1btuiSSy6JLOb9oCAPAAAAAAAAAIiukJGsCAvyofDGPf7445KksWPH1lr/zDPP6KKLLpIk5ebmymb7sYFMYWGhLr30UuXl5SktLU0jRozQggULdPDBB0cW835QkAcAAAAAAAAAtBumETPq58+fX+v1rFmzNGvWrChF9CMK8gAAAAAAAACA6GrBljWtGQV5AAAAAAAAAECUNaEgr/ZTkLcdeBcAAAAAAAAAANBUzJAHAAAAAAAAAEQXLWskUZAHAAAAAAAAAERbyCji1jMhCvIAAAAAAAAAADSOCVUvkY5tJ+ghDwAAAAAAAABAC2CGPAAAAAAAAAAguughL4mCPAAAAAAAAAAg2ughL4mCPAAAAAAAAAAg2pghL4ke8gAAAAAAAAAAtAhmyAMAAAAAAAAAosuoCTPkmzWSmKIgDwAAAAAAAACILlrWSKIgDwAAAAAAAACItlBIUqgJY9sHesgDAAAAAAAAANACmCEPAAAAAAAAAIguWtZIoiAPAAAAAAAAAIg2CvKSaFkDAAAAAAAAAECLYIY8AAAAAAAAACC6QkZShDPdQ+1nhjwFeQAAAAAAAABAVBkTkjGhiMe2FxTkAQAAAAAAAADRZUzkM93pIQ8AAAAAAAAAAMLBDHkAAAAAAAAAQHSZJvSQb0cz5CnIAwAAAAAAAACiKxSSrAh7wdNDHgAAAAAAAACARmKGvCR6yAMAAAAAAAAA0CKYIQ8AAAAAAAAAiCoTCslE2LLG0LIGAAAAAAAAAIBGomWNJFrWAAAAAAAAAADQIpghDwAAAAAAAACIrpCRLGbIU5AHAAAAAAAAAESXMZIi7AVPQR4AAAAAAAAAgMYxISMT4Qx5044K8vSQBwAAAAAAAACgBVCQBwAAAAAAAABElwk1bYnAY489pt69e8vj8WjUqFH68ssvG9z/lVde0aBBg+TxeDRkyBC9++67EZ23IRTkAQAAAAAAAABRZUKmSUu4Xn75ZV1zzTWaOXOmvvrqKw0dOlQTJ07Url276t1/wYIFmjZtmqZPn67ly5drypQpmjJlilauXNnUt14LBXkAAAAAAAAAQHS18Az5v/3tb7r00kt18cUX6+CDD9YTTzyh+Ph4Pf300/Xu//e//12TJk3Sddddp8GDB+uuu+7S8OHD9eijjzb1nddCQR4AAAAAAAAAEFUB+RUwES7yS5JKSkpqLV6vt95z+Xw+LVu2TBMmTKhZZ7PZNGHCBC1cuLDeMQsXLqy1vyRNnDhxv/tHytGsRwMAAAAAAAAA4Acul0vZ2dn6PK9p/dgTExPVo0ePWutmzpyp22+/vc6+e/bsUTAYVFZWVq31WVlZWrNmTb3Hz8vLq3f/vLy8JsX9cxTkAQAAAAAAAABR4fF4tGnTJvl8viYdxxgjy7JqrXO73U06ZixQkAcAAAAAAAAARI3H45HH42mx82VkZMhutys/P7/W+vz8fGVnZ9c7Jjs7O6z9I0UPeQAAAAAAAABAu+FyuTRixAjNmzevZl0oFNK8efOUk5NT75icnJxa+0vS3Llz97t/pJghDwAAAAAAAABoV6655hpdeOGFOuKIIzRy5Eg99NBDKi8v18UXXyxJuuCCC9StWzfdc889kqQrr7xSY8aM0YMPPqiTTz5ZL730kpYuXaqnnnqqWeOiIA8AAAAAAAAAaFfOOecc7d69W7fddpvy8vJ0+OGHa86cOTUPbs3NzZXN9mMDmdGjR+uFF17QLbfcoptuukn9+/fX66+/rkMPPbRZ47KMMaZZj9jOlJSUKCUlRcXFxUpOTo51OACAGCAXRIbrBgAgF0SG6wYAIBe0X/SQBwAAAAAAAACgBVCQBwAAAAAAAACgBVCQBwAAAAAAAACgBVCQBwAAAAAAAACgBVCQBwAAAAAAAACgBVCQBwAAAAAAAACgBVCQBwAAAAAAAACgBVCQBwAAAAAAAACgBVCQBwAAAAAAAACgBbSJgvzmzZs1ffp09enTR3FxcTrooIM0c+ZM+Xy+BseNHTtWlmXVWn7729+2UNQAAMQeORQAgMiQQwEAQDQ4Yh1AY6xZs0ahUEhPPvmk+vXrp5UrV+rSSy9VeXm5HnjggQbHXnrppbrzzjtrXsfHx0c7XAAAWg1yKAAAkSGHAgCAaGgTBflJkyZp0qRJNa/79u2rtWvX6vHHHz/gH0Lx8fHKzs6OdogAALRK5FAAACJDDgUAANHQJgry9SkuLlZ6evoB93v++ef13HPPKTs7W6eeeqpuvfXWBmcneL1eeb3eWueRpJKSkqYHDQBok/blAGNMjCNpHuRQAEBLIYeSQwEAkWlvORQ/Ydqg9evXm+TkZPPUU081uN+TTz5p5syZY7755hvz3HPPmW7dupkzzjijwTEzZ840klhYWFhYWOosGzdubM50FhPkUBYWFhaWWCzkUHIoCwsLC0tkS3vIoajNMiZ2X7PccMMNuvfeexvcZ/Xq1Ro0aFDN6+3bt2vMmDEaO3as/vWvf4V1vo8++kjjx4/Xhg0bdNBBB9W7z89nJhQVFalXr17Kzc1VSkpKWOfrqEpKStSjRw9t3bpVycnJsQ6nzeC6hY9rFhmuW/iKi4vVs2dPFRYWKjU1NdbhSCKHtlf89xkZrlv4uGaR4bqFjxxKDm0p/PcZGa5b+LhmkeG6ha815lA0j5i2rLn22mt10UUXNbhP3759a/69Y8cOjRs3TqNHj9ZTTz0V9vlGjRolSQ3+IeR2u+V2u+usT0lJ4RdGmJKTk7lmEeC6hY9rFhmuW/hsNlusQ6hBDm3f+O8zMly38HHNIsN1Cx85lBzaUvjvMzJct/BxzSLDdQtfa8qhaB4xLchnZmYqMzOzUftu375d48aN04gRI/TMM89E9MO4YsUKSVKXLl3CHgsAQGtCDgUAIDLkUAAAEEtt4iuW7du3a+zYserZs6ceeOAB7d69W3l5ecrLy6u1z6BBg/Tll19KkjZu3Ki77rpLy5Yt0+bNm/Xmm2/qggsu0HHHHafDDjssVm8FAIAWRQ4FACAy5FAAABANMZ0h31hz587Vhg0btGHDBnXv3r3Wtn0t8P1+v9auXauKigpJksvl0ocffqiHHnpI5eXl6tGjh84880zdcsstYZ3b7XZr5syZ9d4+iPpxzSLDdQsf1ywyXLfwteVrRg5tW7hmkeG6hY9rFhmuW/ja8jUjh7YtXLPIcN3CxzWLDNctfFyz9iumD3UFAAAAAAAAAKCjaBMtawAAAAAAAAAAaOsoyAMAAAAAAAAA0AIoyAMAAAAAAAAA0AIoyAMAAAAAAAAA0AIoyP/M5s2bNX36dPXp00dxcXE66KCDNHPmTPl8vgbHjR07VpZl1Vp++9vftlDUsRXpNauqqtKMGTPUqVMnJSYm6swzz1R+fn4LRd063H333Ro9erTi4+OVmpraqDEXXXRRnZ+1SZMmRTfQViSSa2aM0W233aYuXbooLi5OEyZM0Pr166MbaCtTUFCg888/X8nJyUpNTdX06dNVVlbW4JiO9nvtscceU+/eveXxeDRq1Ch9+eWXDe7/yiuvaNCgQfJ4PBoyZIjefffdFoq09SKHho8cGjlyaPjIoZEhhx4YObTpyKHhI4dGjhwaPnJoZMihB0YO7ZgoyP/MmjVrFAqF9OSTT+q7777TrFmz9MQTT+imm2464NhLL71UO3furFnuu+++Fog49iK9ZldffbXeeustvfLKK/rkk0+0Y8cOTZ06tYWibh18Pp/OOussXX755WGNmzRpUq2ftRdffDFKEbY+kVyz++67Tw8//LCeeOIJLV68WAkJCZo4caKqqqqiGGnrcv755+u7777T3Llz9fbbb+vTTz/VZZdddsBxHeX32ssvv6xrrrlGM2fO1FdffaWhQ4dq4sSJ2rVrV737L1iwQNOmTdP06dO1fPlyTZkyRVOmTNHKlStbOPLWhRwaPnJo5Mih4SOHRoYc2jByaPMgh4aPHBo5cmj4yKGRIYc2jBzagRkc0H333Wf69OnT4D5jxowxV155ZcsE1AYc6JoVFRUZp9NpXnnllZp1q1evNpLMwoULWyLEVuWZZ54xKSkpjdr3wgsvNKeffnpU42kLGnvNQqGQyc7ONvfff3/NuqKiIuN2u82LL74YxQhbj1WrVhlJZsmSJTXr3nvvPWNZltm+fft+x3Wk32sjR440M2bMqHkdDAZN165dzT333FPv/meffbY5+eSTa60bNWqU+c1vfhPVONsicmj4yKHhIYeGjxzaeOTQAyOHRg85NHzk0PCQQ8NHDm08cuiBkUM7LmbIN0JxcbHS09MPuN/zzz+vjIwMHXroobrxxhtVUVHRAtG1Tge6ZsuWLZPf79eECRNq1g0aNEg9e/bUwoULWyLENm3+/Pnq3LmzBg4cqMsvv1x79+6NdUit1qZNm5SXl1frZy0lJUWjRo3qMD9rCxcuVGpqqo444oiadRMmTJDNZtPixYsbHNsRfq/5fD4tW7as1s+IzWbThAkT9vszsnDhwlr7S9LEiRM7zM9UOMih4SOHRhc5tPHIoeTQAyGHRhc5NHzk0OgihzYeOZQceiDk0I7NEesAWrsNGzbokUce0QMPPNDgfuedd5569eqlrl276ptvvtH111+vtWvX6tVXX22hSFuPxlyzvLw8uVyuOr3XsrKylJeXF+UI27ZJkyZp6tSp6tOnjzZu3KibbrpJkydP1sKFC2W322MdXquz7+cpKyur1vqO9LOWl5enzp0711rncDiUnp7e4DXoKL/X9uzZo2AwWO/PyJo1a+odk5eX16F/phqLHBo+cmh0kUPDQw4lhx4IOTR6yKHhI4dGFzk0PORQcuiBkEM7tg4zQ/6GG26o81CIny8//4Hfvn27Jk2apLPOOkuXXnppg8e/7LLLNHHiRA0ZMkTnn3++nn32Wb322mvauHFjNN9WVEX7mrVXkVy3cJx77rk67bTTNGTIEE2ZMkVvv/22lixZovnz5zffm2hh0b5m7VW0r1t7/L2GyJBDw0cOjQw5NHzk0MiQQ9FSyKHhI4dGhhwaPnJoZMihQNN1mBny1157rS666KIG9+nbt2/Nv3fs2KFx48Zp9OjReuqpp8I+36hRoyRVf0t/0EEHhT2+NYjmNcvOzpbP51NRUVGt2Qn5+fnKzs5uStgxF+51a6q+ffsqIyNDGzZs0Pjx45vtuC0pmtds389Tfn6+unTpUrM+Pz9fhx9+eETHbC0ae92ys7PrPBQmEAiooKAgrP/e2sPvtfpkZGTIbrcrPz+/1vqGfh9lZ2eHtX9bRw4NHzk0MuTQ8JFDI0MObR7k0AMjh4aPHBoZcmj4yKGRIYc2D3Jox9ZhCvKZmZnKzMxs1L7bt2/XuHHjNGLECD3zzDOy2cK/kWDFihWSVOsXb1sTzWs2YsQIOZ1OzZs3T2eeeaYkae3atcrNzVVOTk6TY4+lcK5bc9i2bZv27t3bYX7WwtWnTx9lZ2dr3rx5NX/4lJSUaPHixbr88sujcs6W0tjrlpOTo6KiIi1btkwjRoyQJH300UcKhUI1f9w0Rnv4vVYfl8ulESNGaN68eZoyZYokKRQKad68ebriiivqHZOTk6N58+bpqquuqlk3d+7cNv/7a3/IoeEjh0aGHBo+cmhkyKHNgxx6YOTQ8JFDI0MODR85NDLk0OZBDu3gYv1U2dZm27Ztpl+/fmb8+PFm27ZtZufOnTXLT/cZOHCgWbx4sTHGmA0bNpg777zTLF261GzatMm88cYbpm/fvua4446L1dtoUZFcM2OM+e1vf2t69uxpPvroI7N06VKTk5NjcnJyYvEWYmbLli1m+fLl5o477jCJiYlm+fLlZvny5aa0tLRmn4EDB5pXX33VGGNMaWmp+eMf/2gWLlxoNm3aZD788EMzfPhw079/f1NVVRWrt9Giwr1mxhjz17/+1aSmppo33njDfPPNN+b00083ffr0MZWVlbF4CzExadIkM2zYMLN48WLz+eefm/79+5tp06bVbO/ov9deeukl43a7zezZs82qVavMZZddZlJTU01eXp4xxphf/epX5oYbbqjZ/4svvjAOh8M88MADZvXq1WbmzJnG6XSab7/9NlZvoVUgh4aPHBo5cmj4yKGRIYc2jBzaPMih4SOHRo4cGj5yaGTIoQ0jh3ZcFOR/5plnnjGS6l322bRpk5FkPv74Y2OMMbm5uea4444z6enpxu12m379+pnrrrvOFBcXx+hdtKxIrpkxxlRWVprf/e53Ji0tzcTHx5szzjij1h9PHcGFF15Y73X76XWSZJ555hljjDEVFRXmxBNPNJmZmcbpdJpevXqZSy+9tOaXdUcQ7jUzxphQKGRuvfVWk5WVZdxutxk/frxZu3ZtywcfQ3v37jXTpk0ziYmJJjk52Vx88cW1/njk95oxjzzyiOnZs6dxuVxm5MiRZtGiRTXbxowZYy688MJa+//nP/8xAwYMMC6XyxxyyCHmnXfeaeGIWx9yaPjIoZEjh4aPHBoZcuiBkUObjhwaPnJo5Mih4SOHRoYcemDk0I7JMsaYsKfVAwAAAAAAAACAsITflA4AAAAAAAAAAISNgjwAAAAAAAAAAC2AgjwAAAAAAAAAAC2AgjwAAAAAAAAAAC2AgjwAAAAAAAAAAC2AgjwAAAAAAAAAAC2AgjwAAAAAAAAAAC2AgjwAAAAAAAAAAC2AgjzQRvXu3VsPPfRQk/dpqtmzZys1NTWq5wAAoDmRQwEAiAw5FACajoI80Mps3bpVv/71r9W1a1e5XC716tVLV155pfbu3Rv2sZYsWaLLLrus2WKr7w+rc845R+vWrWu2c9SnqqpKF110kYYMGSKHw6EpU6ZE9XwAgLaJHFoXORQA0Bjk0LrIoQCihYI80Ip8//33OuKII7R+/Xq9+OKL2rBhg5544gnNmzdPOTk5KigoCOt4mZmZio+Pj1K01eLi4tS5c+eoniMYDCouLk5/+MMfNGHChKieCwDQNpFD60cOBQAcCDm0fuRQANFCQR5oRWbMmCGXy6UPPvhAY8aMUc+ePTV58mR9+OGH2r59u26++eZa+5eWlmratGlKSEhQt27d9Nhjj9Xa/vOZBEVFRbrkkkuUmZmp5ORkHX/88fr6669rjXnrrbd05JFHyuPxKCMjQ2eccYYkaezYsdqyZYuuvvpqWZYly7Ik1b5VcN26dbIsS2vWrKl1zFmzZumggw6qeb1y5UpNnjxZiYmJysrK0q9+9Svt2bNnv9clISFBjz/+uC699FJlZ2c37mICADoUcmj9yKEAgAMhh9aPHAogWijIA61EQUGB3n//ff3ud79TXFxcrW3Z2dk6//zz9fLLL8sYU7P+/vvv19ChQ7V8+XLdcMMNuvLKKzV37tz9nuOss87Srl279N5772nZsmUaPny4xo8fXzPj4Z133tEZZ5yhk046ScuXL9e8efM0cuRISdKrr76q7t27684779TOnTu1c+fOOscfMGCAjjjiCD3//PO11j///PM677zzJFX/MXb88cdr2LBhWrp0qebMmaP8/HydffbZkV04AECHRw4lhwIAIkMOJYcCaHmOWAcAoNr69etljNHgwYPr3T548GAVFhZq9+7dNbfmHX300brhhhskVf8R8sUXX2jWrFk64YQT6oz//PPP9eWXX2rXrl1yu92SpAceeECvv/66/vvf/+qyyy7T3XffrXPPPVd33HFHzbihQ4dKktLT02W325WUlNTg7IDzzz9fjz76qO666y5J1bMVli1bpueee06S9Oijj2rYsGH6y1/+UjPm6aefVo8ePbRu3ToNGDCg0dcMAACJHEoOBQBEihxKDgXQ8pghD7QyP515cCA5OTl1Xq9evbrefb/++muVlZWpU6dOSkxMrFk2bdqkjRs3SpJWrFih8ePHRx68pHPPPVebN2/WokWLJFXPShg+fLgGDRpUE8fHH39cK4Z92/bFAQBAJMihAABEhhwKAC2HGfJAK9GvXz9ZlqXVq1fX9Mv7qdWrVystLU2ZmZkRHb+srExdunTR/Pnz62zb13vv57coRiI7O1vHH3+8XnjhBR111FF64YUXdPnll9eK49RTT9W9995bZ2yXLl2afH4AQMdDDiWHAgAiQw4lhwJoecyQB1qJTp066YQTTtA//vEPVVZW1tqWl5en559/Xuecc07NQ2wk1Xz7/9PX+7vVcPjw4crLy5PD4VC/fv1qLRkZGZKkww47TPPmzdtvjC6XS8Fg8IDvZV+fwYULF+r777/XueeeWyuO7777Tr17964TR0JCwgGPDQDAz5FDyaEAgMiQQ8mhAFoeBXmgFXn00Ufl9Xo1ceJEffrpp9q6davmzJmjE044Qd26ddPdd99da/8vvvhC9913n9atW6fHHntMr7zyiq688sp6jz1hwgTl5ORoypQp+uCDD7R582YtWLBAN998s5YuXSpJmjlzpl588UXNnDlTq1ev1rfffltrBkHv3r316aefavv27Q0+jX7q1KkqLS3V5ZdfrnHjxqlr164122bMmKGCggJNmzZNS5Ys0caNG/X+++/r4osvbvCPrFWrVmnFihUqKChQcXGxVqxYoRUrVjTmsgIAOgByKDkUABAZcig5FEALMwBalc2bN5sLL7zQZGVlGafTaXr06GF+//vfmz179tTar1evXuaOO+4wZ511lomPjzfZ2dnm73//e519Zs2aVfO6pKTE/P73vzddu3atOfb5559vcnNza/b53//+Zw4//HDjcrlMRkaGmTp1as22hQsXmsMOO8y43W6z79fHM888Y1JSUuq8j7PPPttIMk8//XSdbevWrTNnnHGGSU1NNXFxcWbQoEHmqquuMqFQaL/XpVevXkZSnQUAgH3IofUjhwIADoQcWj9yKIBosIwJ48kdANqULl266K677tIll1wS61AAAGhTyKEAAESGHAoADeOhrkA7VFFRoS+++EL5+fk65JBDYh0OAABtBjkUAIDIkEMBoHHoIQ+0Q0899ZTOPfdcXXXVVcrJyYl1OAAAtBnkUAAAIkMOBYDGoWUNAAAAAAAAAAAtgBnyAAAAAAAAAAC0AAryAAAAAAAAAAC0AAryAAAAAAAAAAC0AAryAAAAAAAAAAC0AAryAAAAAAAAAAC0AAryAAAAAAAAAAC0AAryAAAAAAAAAAC0AAryAAAAAAAAAAC0gP8P+8jwOIjhwZAAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib.cm import ScalarMappable\n", + "import matplotlib\n", + "\n", + "\n", + "fig, axes = plt.subplots(1, 3, figsize=(17, 5))\n", + "algos = [\"Sobol\", \"qParEGO\", \"qNEHVI\"]\n", + "cm = plt.get_cmap(\"viridis\")\n", + "\n", + "batch_number = torch.cat(\n", + " [\n", + " torch.zeros(2 * (d + 1)),\n", + " torch.arange(1, N_BATCH + 1).repeat(BATCH_SIZE, 1).t().reshape(-1),\n", + " ]\n", + ").numpy()\n", + "\n", + "for i, train_obj in enumerate((train_obj_random, train_obj_qparego, train_obj_qnehvi)):\n", + " sc = axes[i].scatter(\n", + " train_obj[:, 0].cpu().numpy(),\n", + " train_obj[:, 1].cpu().numpy(),\n", + " c=batch_number,\n", + " alpha=0.8,\n", + " )\n", + " axes[i].set_title(algos[i])\n", + " axes[i].set_xlabel(\"Objective 1\")\n", + " axes[i].set_xlim(-2.5, 0)\n", + " axes[i].set_ylim(-2.5, 0)\n", + "axes[0].set_ylabel(\"Objective 2\")\n", + "norm = plt.Normalize(batch_number.min(), batch_number.max())\n", + "sm = ScalarMappable(norm=norm, cmap=cm)\n", + "sm.set_array([])\n", + "fig.subplots_adjust(right=0.9)\n", + "cbar_ax = fig.add_axes([0.93, 0.15, 0.01, 0.7])\n", + "cbar = fig.colorbar(sm, cax=cbar_ax)\n", + "cbar.ax.set_title(\"Iteration\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/constrained_multi_objective_bo.py b/website-old/static/files/constrained_multi_objective_bo.py new file mode 100644 index 0000000000..592f9c59f5 --- /dev/null +++ b/website-old/static/files/constrained_multi_objective_bo.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Constrained, Parallel, Multi-Objective BO in BoTorch with qNEHVI, and qParEGO +# +# In this tutorial, we illustrate how to implement a constrained multi-objective (MO) Bayesian Optimization (BO) closed loop in BoTorch. +# +# In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. See [here](https://ax.dev/tutorials/multiobjective_optimization.html) for an Ax tutorial on MOBO. If desired, you can use a custom BoTorch model in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. Given a `MultiObjective`, Ax will default to the $q$NEHVI acquisiton function. If desired, this can also be customized by adding `"botorch_acqf_class": ,` to the `model_kwargs`. +# +# We use the parallel ParEGO ($q$ParEGO) [1] and parallel Noisy Expected Hypervolume Improvement ($q$NEHVI) [2] acquisition functions to optimize a synthetic C2-DTLZ2 test function with $M=2$ objectives, $V=1$ constraint, and $d=4$ parameters. The two objectives are +# $$f_1(\mathbf x) = (1+ g(\mathbf x_M))\cos\big(\frac{\pi}{2}x_1\big)$$ +# $$f_2(\mathbf x) = (1+ g(\mathbf x_M))\sin\big(\frac{\pi}{2}x_1\big)$$ +# where $g(\mathbf x) = \sum_{x_i \in \mathbf x_M} (x_i - 0.5)^2, \mathbf x \in [0,1]^d,$ and $\mathbf x_M$ represents the last $d - M +1$ elements of $\mathbf x$. Additionally, the C2-DTLZ2 problem uses the following constraint: +# +# $$c(\mathbf x) = - \min \bigg[\min_{i=1}^M\bigg((f_i(\mathbf x) -1 )^2 + \sum_{j=1, j=i}^M (f_j^2 - r^2) \bigg), \bigg(\sum_{i=1}^M \big((f_i(\mathbf x) - \frac{1}{\sqrt{M}})^2 - r^2\big)\bigg)\bigg]\geq 0$$ +# +# where $\mathbf x \in [0,1]^d$ and $r=0.2$. +# +# The goal here is to *minimize* both objectives. Since BoTorch assumes maximization, we maximize the negative of each objective. Since there typically is no single best solution in multi-objective optimization problems, we seek to find the pareto frontier, the set of optimal trade-offs where improving one metric means deteriorating another. +# +# [1] [S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2006.05078) +# +# [2] [S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances in Neural Information Processing Systems 34, 2021.](https://arxiv.org/abs/2105.08195) +# +# **For batch optimization (or in noisy settings), we strongly recommend using $q$NEHVI rather than $q$EHVI [1] because it is far more efficient than $q$EHVI and mathematically equivalent in the noiseless setting.** + +# ### Set dtype and device +# Note: $q$EHVI aggressively exploits parallel hardware and is much faster when run on a GPU. See [1] for details. + +# In[1]: + + +import os +import torch + + +tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda:3" if torch.cuda.is_available() else "cpu"), +} +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Problem setup +# + +# In[2]: + + +from botorch.test_functions.multi_objective import C2DTLZ2 + + +d = 4 +M = 2 +problem = C2DTLZ2(dim=d, num_objectives=M, negate=True).to(**tkwargs) + + +# #### Model initialization +# +# We use a multi-output `SingleTaskGP` to model the two objectives with a homoskedastic Gaussian likelihood with an inferred noise level. +# +# The models are initialized with $2(d+1)=10$ points drawn randomly from $[0,1]^{4}$. + +# In[3]: + + +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize +from botorch.utils.sampling import draw_sobol_samples +from botorch.utils.transforms import normalize, unnormalize +from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood + + +def generate_initial_data(n): + # generate training data + train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1) + train_obj = problem(train_x) + # negative values imply feasibility in botorch + train_con = -problem.evaluate_slack(train_x) + return train_x, train_obj, train_con + + +def initialize_model(train_x, train_obj, train_con): + # define models for objective and constraint + train_x = normalize(train_x, problem.bounds) + train_y = torch.cat([train_obj, train_con], dim=-1) + models = [] + for i in range(train_y.shape[-1]): + models.append( + SingleTaskGP( + train_x, train_y[..., i : i + 1], outcome_transform=Standardize(m=1) + ) + ) + model = ModelListGP(*models) + mll = SumMarginalLogLikelihood(model.likelihood, model) + return mll, model + + +# #### Define a helper function that performs the essential BO step for $q$NEHVI +# The helper function below initializes the $q$NEHVI acquisition function, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. +# +# For this example, we'll use a small batch of $q=2$. Passing the keyword argument `sequential=True` to the function `optimize_acqf`specifies that candidates should be optimized in a sequential greedy fashion (see [1] for details why this is important). A simple initialization heuristic is used to select the 10 restart initial locations from a set of 512 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation. +# +# **Reference Point** +# +# $q$NEHVI requires specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy. +# +# **Integrating over function values at in-sample designs** +# +# $q$NEHVI integrates over the unknown function values at the previously evaluated designs (see [2] for details). Therefore, we need to provide the previously evaluated designs (`train_x`, *normalized* to be within $[0,1]^d$) to the acquisition function. +# +# **Pruning baseline designs** +# To speed up integration over the function values at the previously evaluated designs, we prune the set of previously evaluated designs (by setting `prune_baseline=True`) to only include those which have positive probability of being on the current in-sample Pareto frontier. + +# In[4]: + + +from botorch.acquisition.multi_objective.monte_carlo import ( + qNoisyExpectedHypervolumeImprovement, +) +from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective +from botorch.optim.optimize import optimize_acqf, optimize_acqf_list +from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization +from botorch.utils.sampling import sample_simplex + + +BATCH_SIZE = 2 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 + +standard_bounds = torch.zeros(2, problem.dim, **tkwargs) +standard_bounds[1] = 1 + + +def optimize_qnehvi_and_get_observation(model, train_x, train_obj, train_con, sampler): + """Optimizes the qNEHVI acquisition function, and returns a new candidate and observation.""" + train_x = normalize(train_x, problem.bounds) + acq_func = qNoisyExpectedHypervolumeImprovement( + model=model, + ref_point=problem.ref_point.tolist(), # use known reference point + X_baseline=train_x, + sampler=sampler, + prune_baseline=True, + # define an objective that specifies which outcomes are the objectives + objective=IdentityMCMultiOutputObjective(outcomes=[0, 1]), + # specify that the constraint is on the last outcome + constraints=[lambda Z: Z[..., -1]], + ) + # optimize + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=standard_bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + sequential=True, + ) + # observe new values + new_x = unnormalize(candidates.detach(), bounds=problem.bounds) + new_obj = problem(new_x) + # negative values imply feasibility in botorch + new_con = -problem.evaluate_slack(new_x) + return new_x, new_obj, new_con + + +# #### Define a helper function that performs the essential BO step for $q$ParEGO +# The helper function below similarly initializes $q$ParEGO, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. +# +# $q$ParEGO uses random augmented chebyshev scalarization with the `qExpectedImprovement` acquisition function. In the parallel setting ($q>1$), each candidate is optimized in sequential greedy fashion using a different random scalarization (see [1] for details). +# +# To do this, we create a list of `qExpectedImprovement` acquisition functions, each with different random scalarization weights. The `optimize_acqf_list` method sequentially generates one candidate per acquisition function and conditions the next candidate (and acquisition function) on the previously selected pending candidates. + +# In[5]: + + +from botorch.acquisition.monte_carlo import qExpectedImprovement +from botorch.acquisition.objective import GenericMCObjective + + +def optimize_qparego_and_get_observation(model, train_obj, train_con, sampler): + """Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization + of the qParEGO acquisition function, and returns a new candidate and observation.""" + acq_func_list = [] + for _ in range(BATCH_SIZE): + # sample random weights + weights = sample_simplex(problem.num_objectives, **tkwargs).squeeze() + # construct augmented Chebyshev scalarization + scalarization = get_chebyshev_scalarization(weights=weights, Y=train_obj) + # initialize the scalarized objective (w/o constraints) + scalarized_objective = GenericMCObjective( + # the last element of the model outputs is the constraint + lambda Z, X: scalarization(Z[..., :-1]), + ) + train_y = torch.cat([train_obj, train_con], dim=-1) + acq_func = qExpectedImprovement( # pyre-ignore: [28] + model=model, + objective=scalarized_objective, + best_f=scalarized_objective(train_y).max(), + constraints=[lambda Z: Z[..., -1]], + sampler=sampler, + ) + acq_func_list.append(acq_func) + # optimize + candidates, _ = optimize_acqf_list( + acq_function_list=acq_func_list, + bounds=standard_bounds, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + ) + # observe new values + new_x = unnormalize(candidates.detach(), bounds=problem.bounds) + new_obj = problem(new_x) + # negative values imply feasibility in botorch + new_con = -problem.evaluate_slack(new_x) + return new_x, new_obj, new_con + + +# ### Perform Bayesian Optimization loop with $q$EHVI and $q$ParEGO +# The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps: +# 1. given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$ +# 2. observe $f(x)$ for each $x$ in the batch +# 3. update the surrogate model. +# +# +# Just for illustration purposes, we run one trial with `N_BATCH=20` rounds of optimization. The acquisition function is approximated using `MC_SAMPLES=128` samples. +# +# *Note*: Running this may take a little while. + +# In[6]: + + +import time +import warnings + +from botorch import fit_gpytorch_mll +from botorch.exceptions import BadInitialCandidatesWarning +from botorch.sampling.normal import SobolQMCNormalSampler +from botorch.utils.multi_objective.hypervolume import Hypervolume +from botorch.utils.multi_objective.pareto import is_non_dominated + + +warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning) +warnings.filterwarnings("ignore", category=RuntimeWarning) + +N_BATCH = 20 if not SMOKE_TEST else 1 +MC_SAMPLES = 128 if not SMOKE_TEST else 16 +verbose = True + +hv = Hypervolume(ref_point=problem.ref_point) +hvs_qparego, hvs_qnehvi, hvs_random = [], [], [] + +# call helper functions to generate initial training data and initialize model +train_x_qparego, train_obj_qparego, train_con_qparego = generate_initial_data( + n=2 * (d + 1) +) +mll_qparego, model_qparego = initialize_model( + train_x_qparego, train_obj_qparego, train_con_qparego +) + +train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi = ( + train_x_qparego, + train_obj_qparego, + train_con_qparego, +) +train_x_random, train_obj_random, train_con_random = ( + train_x_qparego, + train_obj_qparego, + train_con_qparego, +) + +mll_qnehvi, model_qnehvi = initialize_model( + train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi +) + +# compute pareto front +is_feas = (train_con_qparego <= 0).all(dim=-1) +feas_train_obj = train_obj_qparego[is_feas] +if feas_train_obj.shape[0] > 0: + pareto_mask = is_non_dominated(feas_train_obj) + pareto_y = feas_train_obj[pareto_mask] + # compute hypervolume + volume = hv.compute(pareto_y) +else: + volume = 0.0 + +hvs_qparego.append(volume) +hvs_qnehvi.append(volume) +hvs_random.append(volume) + +# run N_BATCH rounds of BayesOpt after the initial random batch +for iteration in range(1, N_BATCH + 1): + t0 = time.monotonic() + + # fit the models + fit_gpytorch_mll(mll_qparego) + fit_gpytorch_mll(mll_qnehvi) + + # define the qParEGO and qNEHVI acquisition modules using a QMC sampler + qparego_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + + # optimize acquisition functions and get new observations + ( + new_x_qparego, + new_obj_qparego, + new_con_qparego, + ) = optimize_qparego_and_get_observation( + model_qparego, train_obj_qparego, train_con_qparego, qparego_sampler + ) + new_x_qnehvi, new_obj_qnehvi, new_con_qnehvi = optimize_qnehvi_and_get_observation( + model_qnehvi, train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi, qnehvi_sampler + ) + new_x_random, new_obj_random, new_con_random = generate_initial_data(n=BATCH_SIZE) + + # update training points + train_x_qparego = torch.cat([train_x_qparego, new_x_qparego]) + train_obj_qparego = torch.cat([train_obj_qparego, new_obj_qparego]) + train_con_qparego = torch.cat([train_con_qparego, new_con_qparego]) + + train_x_qnehvi = torch.cat([train_x_qnehvi, new_x_qnehvi]) + train_obj_qnehvi = torch.cat([train_obj_qnehvi, new_obj_qnehvi]) + train_con_qnehvi = torch.cat([train_con_qnehvi, new_con_qnehvi]) + + train_x_random = torch.cat([train_x_random, new_x_random]) + train_obj_random = torch.cat([train_obj_random, new_obj_random]) + train_con_random = torch.cat([train_con_random, new_con_random]) + + # update progress + for hvs_list, train_obj, train_con in zip( + (hvs_random, hvs_qparego, hvs_qnehvi), + (train_obj_random, train_obj_qparego, train_obj_qnehvi), + (train_con_random, train_con_qparego, train_con_qnehvi), + ): + # compute pareto front + is_feas = (train_con <= 0).all(dim=-1) + feas_train_obj = train_obj[is_feas] + if feas_train_obj.shape[0] > 0: + pareto_mask = is_non_dominated(feas_train_obj) + pareto_y = feas_train_obj[pareto_mask] + # compute feasible hypervolume + volume = hv.compute(pareto_y) + else: + volume = 0.0 + hvs_list.append(volume) + + # reinitialize the models so they are ready for fitting on next iteration + # Note: we find improved performance from not warm starting the model hyperparameters + # using the hyperparameters from the previous iteration + mll_qparego, model_qparego = initialize_model( + train_x_qparego, train_obj_qparego, train_con_qparego + ) + mll_qnehvi, model_qnehvi = initialize_model( + train_x_qnehvi, train_obj_qnehvi, train_con_qnehvi + ) + + t1 = time.monotonic() + + if verbose: + print( + f"\nBatch {iteration:>2}: Hypervolume (random, qParEGO, qNEHVI) = " + f"({hvs_random[-1]:>4.2f}, {hvs_qparego[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), " + f"time = {t1-t0:>4.2f}.", + end="", + ) + else: + print(".", end="") + + +# #### Plot the results +# The plot below shows the log feasible hypervolume difference: the log difference between the hypervolume of the true feasible pareto front and the hypervolume of the observed (feasible) pareto front identified by each algorithm. The log feasible hypervolume difference is plotted at each step of the optimization for each of the algorithms. +# +# The plot show that $q$NEHVI vastly outperforms the $q$ParEGO and Sobol baselines. + +# In[7]: + + +import numpy as np +from matplotlib import pyplot as plt + + +get_ipython().run_line_magic('matplotlib', 'inline') + + +iters = np.arange(N_BATCH + 1) * BATCH_SIZE +log_hv_difference_qparego = np.log10(problem.max_hv - np.asarray(hvs_qparego)) +log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi)) +log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random)) + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +ax.plot( + iters, + log_hv_difference_rnd, + label="Sobol", + linewidth=1.5, + color="gray", +) +ax.plot( + iters, + log_hv_difference_qparego, + label="qParEGO", + linewidth=1.5, + color="red", +) +ax.plot( + iters, + log_hv_difference_qnehvi, + label="qNEHVI", + linewidth=1.5, + color="blue", +) +ax.set( + xlabel="number of observations (beyond initial points)", + ylabel="Log Hypervolume Difference", +) +ax.legend(loc="lower right") + + +# #### Plot the observations colored by iteration +# +# To examine optimization process from another perspective, we plot the collected observations under each algorithm where the color corresponds to the BO iteration at which the point was collected. The plot on the right for $q$NEHVI shows that the $q$NEHVI quickly identifies the pareto front and most of its evaluations are very close to the pareto front. $q$ParEGO also identifies has many observations close to the pareto front, but relies on optimizing random scalarizations, which is a less principled way of optimizing the pareto front compared to $q$NEHVI, which explicitly attempts focuses on improving the pareto front. Sobol generates random points and has few points close to the pareto front + +# In[8]: + + +from matplotlib.cm import ScalarMappable +import matplotlib + + +fig, axes = plt.subplots(1, 3, figsize=(17, 5)) +algos = ["Sobol", "qParEGO", "qNEHVI"] +cm = plt.get_cmap("viridis") + +batch_number = torch.cat( + [ + torch.zeros(2 * (d + 1)), + torch.arange(1, N_BATCH + 1).repeat(BATCH_SIZE, 1).t().reshape(-1), + ] +).numpy() + +for i, train_obj in enumerate((train_obj_random, train_obj_qparego, train_obj_qnehvi)): + sc = axes[i].scatter( + train_obj[:, 0].cpu().numpy(), + train_obj[:, 1].cpu().numpy(), + c=batch_number, + alpha=0.8, + ) + axes[i].set_title(algos[i]) + axes[i].set_xlabel("Objective 1") + axes[i].set_xlim(-2.5, 0) + axes[i].set_ylim(-2.5, 0) +axes[0].set_ylabel("Objective 2") +norm = plt.Normalize(batch_number.min(), batch_number.max()) +sm = ScalarMappable(norm=norm, cmap=cm) +sm.set_array([]) +fig.subplots_adjust(right=0.9) +cbar_ax = fig.add_axes([0.93, 0.15, 0.01, 0.7]) +cbar = fig.colorbar(sm, cax=cbar_ax) +cbar.ax.set_title("Iteration") + diff --git a/website-old/static/files/constraint_active_search.ipynb b/website-old/static/files/constraint_active_search.ipynb new file mode 100644 index 0000000000..e2ca3b90a5 --- /dev/null +++ b/website-old/static/files/constraint_active_search.ipynb @@ -0,0 +1,738 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "c31f62e6-7593-4975-ac72-c8d1a59fe3b7", + "showInput": false + }, + "source": [ + "## Constraint Active Search for Multiobjective Experimental Design\n", + "\n", + "In this tutorial we show how to implement the Expected Coverage Improvement (ECI) [1] acquisition function in BoTorch. For a number of outcome constraints, ECI tries to efficiently discover the feasible region and simultaneously sample diverse feasible configurations. Given a user-specified punchout radius $r$, we center a sphere with that radius around each evaluated configuration. The total coverage is now given by the volume of the union of these sphere intersected with the feasible region; see the paper and, in particular, Figure 2 for a full description of how ECI works.\n", + "\n", + "By design, ECI prefers candidates that are in unexplored regions since the candidate's corresponding sphere won't intersect with the spheres around the previously evaluated configurations. On the other hand, ECI also prefers configurations that are likely to satisfy the constraints and to give an improvement in the total coverage. This results in an exploitation-exploration trade-off similar to other acquisition functions.\n", + "\n", + "ECI may be estimated using the following equation:\n", + "$$\n", + "\\text{ECI}(x) = \\sum_{x' \\in \\mathbb{N}(x) \\setminus \\mathbb{N}_{r}(X)} p(Z(x') = 1 \\;|\\; \\mathcal{D}_t).\n", + "$$\n", + "\n", + "where $\\mathbb{N}(x) \\setminus \\mathbb{N}_{r}(X)$ a set of points generated via Monte Carlo to be inside a sphere of radius $r$ around $x$, but sufficiently far from the set of known evaluations $X$ (where sufficiently far is defined by the punchout radius $r$). The function $p(Z(x') = 1 \\;|\\; \\mathcal{D}_t)$ is the probability that the GP at $x'$ satisfies a user-specified threshold value, or threshold values in the case of multiple objective functions. \n", + "\n", + "[1]: [Malkomes et al., Beyond the Pareto Efficient Frontier: Constraint Active Search for Multiobjective Experimental Design, Proceedings of the 38th International Conference on Machine Learning, 2021](http://proceedings.mlr.press/v139/malkomes21a/malkomes21a.pdf)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489228284, + "executionStopTime": 1638489229640, + "hidden_ranges": [], + "originalKey": "9896cb02-0d0e-498f-bdf7-86ea14baaf40", + "requestMsgId": "9896cb02-0d0e-498f-bdf7-86ea14baaf40" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import torch\n", + "from botorch.acquisition.monte_carlo import MCAcquisitionFunction\n", + "from botorch.acquisition.objective import IdentityMCObjective\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import ModelListGP, SingleTaskGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.utils.sampling import sample_hypersphere\n", + "from botorch.utils.transforms import t_batch_mode_transform\n", + "from gpytorch.constraints import Interval\n", + "from gpytorch.likelihoods import GaussianLikelihood\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "from torch.quasirandom import SobolEngine\n", + "\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489229684, + "executionStopTime": 1638489230490, + "hidden_ranges": [], + "originalKey": "b4b78cb1-b0d4-4203-a97b-7a293ea418d4", + "requestMsgId": "b4b78cb1-b0d4-4203-a97b-7a293ea418d4" + }, + "outputs": [], + "source": [ + "tkwargs = {\n", + " \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", + " \"dtype\": torch.double,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "e9cecfd7-f548-4b66-8009-c97809afc144", + "showInput": false + }, + "source": [ + "To start, we need to be able to sample points in $\\mathbb{N}(x) \\setminus \\mathbb{N}_{r}(X)$. We can generate a pool of points and use standard rejection sampling to do so, but this leads to an acquisition function that isn't immediately differentiable; rejection sampling is essentially providing either a binary weight of either 0 or 1 to each point in the sample pool, which is not a differentiable function. \n", + "\n", + "\n", + "In order to make the acquisition function differentiable, we rely on a differentiable approximation of this binary weight function. For example, `smooth_box_mask` is a continuous differentiable approximation of $a < x < b$ (see the plot below for a visualization). A larger value of eps will make the sigmoid less steep and result in a smoother (and easier to optimize) but less accurate acquisition function. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "executionStartTime": 1638489230493, + "executionStopTime": 1638489230509, + "originalKey": "63c0a300-c3a1-49bb-a6ba-41cf9dfa9632", + "requestMsgId": "63c0a300-c3a1-49bb-a6ba-41cf9dfa9632" + }, + "outputs": [], + "source": [ + "def smooth_mask(x, a, eps=2e-3):\n", + " \"\"\"Returns 0ish for x < a and 1ish for x > a\"\"\"\n", + " return torch.nn.Sigmoid()((x - a) / eps)\n", + "\n", + "\n", + "def smooth_box_mask(x, a, b, eps=2e-3):\n", + " \"\"\"Returns 1ish for a < x < b and 0ish otherwise\"\"\"\n", + " return smooth_mask(x, a, eps) - smooth_mask(x, b, eps)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489230587, + "executionStopTime": 1638489233802, + "hidden_ranges": [], + "originalKey": "7b49f71b-f131-4600-96fd-5aa581212202", + "requestMsgId": "7b49f71b-f131-4600-96fd-5aa581212202" + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "x = torch.linspace(-2, 2, 500, **tkwargs)\n", + "\n", + "fig, ax = plt.subplots(1, 2, figsize=(8, 4))\n", + "ax[0].plot(x.cpu(), smooth_mask(x, -1).cpu(), \"b\")\n", + "ax[1].plot(x.cpu(), smooth_box_mask(x, -1, 1).cpu(), \"b\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "7ff5ed82-355b-45b8-91f9-823c41c46efc", + "showInput": false + }, + "source": [ + "## Implementation of ECI\n", + "\n", + "Once we have defined our smooth mask functions, we can compute a differentiable approximation of ECI in a straightforward manner using Monte Carlo (MC). We use the popular variance reduction technique of Common random numbers (CRN).\n", + "\n", + "We first use a low discrepancy sequence to generate a set of base samples. We integrate (sum) over these base samples to approximate the ECI acquisition function. Fixing these base samples makes the method deterministic and by using the smooth masks defined earlier, we can filter out infeasible points while still having a differentiable acquisition function.\n", + "\n", + "This implementation assumes that the GP models for the different outputs are independent and that each constraints only affects one output (simple box-constraints like f(x) <= 0.5)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489233910, + "executionStopTime": 1638489233950, + "hidden_ranges": [], + "originalKey": "5dd0a6af-0bde-4e57-8bdd-d53baea75075", + "requestMsgId": "5dd0a6af-0bde-4e57-8bdd-d53baea75075" + }, + "outputs": [], + "source": [ + "class ExpectedCoverageImprovement(MCAcquisitionFunction):\n", + " def __init__(\n", + " self,\n", + " model,\n", + " constraints,\n", + " punchout_radius,\n", + " bounds,\n", + " num_samples=128,\n", + " **kwargs,\n", + " ):\n", + " \"\"\"Expected Coverage Improvement (q=1 required, analytic)\n", + "\n", + " Right now, we assume that all the models in the ModelListGP have\n", + " the same training inputs.\n", + "\n", + " Args:\n", + " model: A ModelListGP object containing models matching the corresponding constraints.\n", + " All models are assumed to have the same training data.\n", + " constraints: List containing 2-tuples with (direction, value), e.g.,\n", + " [('gt', 3), ('lt', 4)]. It is necessary that\n", + " len(constraints) == model.num_outputs.\n", + " punchout_radius: Positive value defining the desired minimum distance between points\n", + " bounds: torch.tensor whose first row is the lower bounds and second row is the upper bounds\n", + " num_samples: Number of samples for MC integration\n", + " \"\"\"\n", + " super().__init__(model=model, objective=IdentityMCObjective(), **kwargs)\n", + " assert len(constraints) == model.num_outputs\n", + " assert all(direction in (\"gt\", \"lt\") for direction, _ in constraints)\n", + " assert punchout_radius > 0\n", + " self.constraints = constraints\n", + " self.punchout_radius = punchout_radius\n", + " self.bounds = bounds\n", + " self.base_points = self.train_inputs\n", + " self.ball_of_points = self._generate_ball_of_points(\n", + " num_samples=num_samples,\n", + " radius=punchout_radius,\n", + " device=bounds.device,\n", + " dtype=bounds.dtype,\n", + " )\n", + " self._thresholds = torch.tensor(\n", + " [threshold for _, threshold in self.constraints]\n", + " ).to(bounds)\n", + " assert (\n", + " all(ub > lb for lb, ub in self.bounds.T) and len(self.bounds.T) == self.dim\n", + " )\n", + "\n", + " @property\n", + " def num_outputs(self):\n", + " return self.model.num_outputs\n", + "\n", + " @property\n", + " def dim(self):\n", + " return self.train_inputs.shape[-1]\n", + "\n", + " @property\n", + " def train_inputs(self):\n", + " return self.model.models[0].train_inputs[0]\n", + "\n", + " def _generate_ball_of_points(\n", + " self, num_samples, radius, device=None, dtype=torch.double\n", + " ):\n", + " \"\"\"Creates a ball of points to be used for MC.\"\"\"\n", + " tkwargs = {\"device\": device, \"dtype\": dtype}\n", + " z = sample_hypersphere(d=self.dim, n=num_samples, qmc=True, **tkwargs)\n", + " r = torch.rand(num_samples, 1, **tkwargs) ** (1 / self.dim)\n", + " return radius * r * z\n", + "\n", + " def _get_base_point_mask(self, X):\n", + " distance_matrix = self.model.models[0].covar_module.covar_dist(\n", + " X, self.base_points\n", + " )\n", + " return smooth_mask(distance_matrix, self.punchout_radius)\n", + "\n", + " def _estimate_probabilities_of_satisfaction_at_points(self, points):\n", + " \"\"\"Estimate the probability of satisfying the given constraints.\"\"\"\n", + " posterior = self.model.posterior(X=points)\n", + " mus, sigma2s = posterior.mean, posterior.variance\n", + " dist = torch.distributions.normal.Normal(mus, sigma2s.sqrt())\n", + " norm_cdf = dist.cdf(self._thresholds)\n", + " probs = torch.ones(points.shape[:-1]).to(points)\n", + " for i, (direction, _) in enumerate(self.constraints):\n", + " probs = probs * (\n", + " norm_cdf[..., i] if direction == \"lt\" else 1 - norm_cdf[..., i]\n", + " )\n", + " return probs\n", + "\n", + " @t_batch_mode_transform(expected_q=1)\n", + " def forward(self, X):\n", + " \"\"\"Evaluate Expected Improvement on the candidate set X.\"\"\"\n", + " ball_around_X = self.ball_of_points + X\n", + " domain_mask = smooth_box_mask(\n", + " ball_around_X, self.bounds[0, :], self.bounds[1, :]\n", + " ).prod(dim=-1)\n", + " num_points_in_integral = domain_mask.sum(dim=-1)\n", + " base_point_mask = self._get_base_point_mask(ball_around_X).prod(dim=-1)\n", + " prob = self._estimate_probabilities_of_satisfaction_at_points(ball_around_X)\n", + " masked_prob = prob * domain_mask * base_point_mask\n", + " y = masked_prob.sum(dim=-1) / num_points_in_integral\n", + " return y" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489234035, + "executionStopTime": 1638489234089, + "hidden_ranges": [], + "originalKey": "b56e4297-9927-4a5e-aa8f-f5e93181e44d", + "requestMsgId": "b56e4297-9927-4a5e-aa8f-f5e93181e44d" + }, + "outputs": [], + "source": [ + "def get_and_fit_gp(X, Y):\n", + " \"\"\"Simple method for creating a GP with one output dimension.\n", + "\n", + " X is assumed to be in [0, 1]^d.\n", + " \"\"\"\n", + " assert Y.ndim == 2 and Y.shape[-1] == 1\n", + " likelihood = GaussianLikelihood(noise_constraint=Interval(1e-6, 1e-3)) # Noise-free\n", + " octf = Standardize(m=1)\n", + " gp = SingleTaskGP(X, Y, likelihood=likelihood, outcome_transform=octf)\n", + " mll = ExactMarginalLogLikelihood(model=gp, likelihood=gp.likelihood)\n", + " fit_gpytorch_mll(mll)\n", + " return gp" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "e7c7c1b3-249f-4e32-b8b7-3c7e737e82b2", + "showInput": false + }, + "source": [ + "### Simple 1D function\n", + "\n", + "To sanity check things, we consider the ECI acquisition function on a one-dimensional toy problem. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "executionStartTime": 1638489234145, + "executionStopTime": 1638489234200, + "originalKey": "cc435c3c-c65f-4446-a33e-fd5cda030962", + "requestMsgId": "cc435c3c-c65f-4446-a33e-fd5cda030962" + }, + "outputs": [], + "source": [ + "def yf(x):\n", + " return (1 - torch.exp(-4 * (x[:, 0] - 0.4) ** 2)).unsqueeze(-1)\n", + "\n", + "\n", + "x = torch.tensor([0, 0.15, 0.25, 0.4, 0.8, 1.0], **tkwargs).unsqueeze(-1)\n", + "y = yf(x)\n", + "xx = torch.linspace(0, 1, 200, **tkwargs).unsqueeze(-1)\n", + "yy = yf(xx)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8bbfe7b8-b758-424a-b6bc-f2c91f8b1e95", + "showInput": false + }, + "source": [ + "### Create an ECI acquisition function\n", + "Our implementation assumes that the GP is passed in as a `ModelListGP` and that the GPs match the corresponding constraints. As an example, assume we have two outputs, represented by `gp1` and `gp2` and two constraints corresponding to output 1 and a third constraint corresponding to output 2. In that case we will create a model list GP as `ModelListGP(gp1, gp1, gp2)` so they match the constraints." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489234253, + "executionStopTime": 1638489235584, + "hidden_ranges": [], + "originalKey": "9efe991c-8256-4c7c-b61f-8abb5d258d40", + "requestMsgId": "9efe991c-8256-4c7c-b61f-8abb5d258d40" + }, + "outputs": [], + "source": [ + "gp = get_and_fit_gp(x, y)\n", + "model_list_gp = ModelListGP(gp, gp)\n", + "constraints = [(\"lt\", 0.3), (\"gt\", 0.05)]\n", + "punchout_radius = 0.03\n", + "bounds = torch.tensor([(0, 1)], **tkwargs).T\n", + "eci = ExpectedCoverageImprovement(\n", + " model=model_list_gp,\n", + " constraints=constraints,\n", + " punchout_radius=punchout_radius,\n", + " bounds=bounds,\n", + " num_samples=128 if not SMOKE_TEST else 4,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f1cfa2a0-db3f-49a2-b32f-6fad380b0c3e", + "showInput": false + }, + "source": [ + "### Optimize the acquisition function" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489235787, + "executionStopTime": 1638489236864, + "hidden_ranges": [], + "originalKey": "1ae10691-8d4e-40e7-8c32-f15a35ddf590", + "requestMsgId": "1ae10691-8d4e-40e7-8c32-f15a35ddf590", + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best candidate: 0.617\n" + ] + } + ], + "source": [ + "best_candidate, best_eci_value = optimize_acqf(\n", + " acq_function=eci,\n", + " bounds=torch.tensor([[0.0], [1.0]], **tkwargs),\n", + " q=1,\n", + " num_restarts=10,\n", + " raw_samples=20, # use a small number here to make sure the optimization works\n", + ")\n", + "print(f\"Best candidate: {best_candidate.cpu().item():.3f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "15a4d7cf-be03-4e52-9792-e3a680f37bb7", + "showInput": false + }, + "source": [ + "### Plot the GP and the ECI acquisition function\n", + "The left plot shows the GP posterior with a 95% confidence interval. The two horizontal lines indicate the feasible region defined by $0.05 \\leq f(x) \\leq 0.3$. These inequality constraints implicitly define a feasible region, outside which ECI has value zero. \n", + "\n", + "We can see in the right plot that ECI indeed has a nonzero value inside the feasible region and a zero value outside. We also optimize the acquisition function and mark its argmax with black star; the argmax is around $x=0.62$. This is reasonable because ECI seeks to select diverse points within the feasible region. $x=0.62$ is far away from other evaluations and thus has the highest diversity. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489236964, + "executionStopTime": 1638489237535, + "hidden_ranges": [], + "originalKey": "5f5b4b6a-4d53-4528-8420-53e4f9358f5c", + "requestMsgId": "5f5b4b6a-4d53-4528-8420-53e4f9358f5c" + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9UAAAHECAYAAAA3XwkIAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAA9hAAAPYQGoP6dpAACrP0lEQVR4nOzdeXxU9dU/8M8smZnsKwkBwuKOUqsGF7BYVyz6aCu1UqnyqFClWBVRq5TWClZRqwiioP6UUjdKXWo3HjV1A6WtgmhVcAVMCAkh60wms2Rm7u+PkzuTbZJZ7uyf9+s1LyYz9858o0lmzpzzPUenKIoCIiIiIiIiIgqbPtELICIiIiIiIkpVDKqJiIiIiIiIIsSgmoiIiIiIiChCDKqJiIiIiIiIIsSgmoiIiIiIiChCDKqJiIiIiIiIIsSgmoiIiIiIiChCDKqJiIiIiIiIIsSgmoiIiIiIiChCDKqJiIiIiIiIIsSgmoiIiIgoSe3duxc6nW7Iy3HHHTfgvPr6eixevBgnnHACioqKYDKZUFlZifPPPx/r16+H2+32H/vWW29Bp9Nh/vz5cfzOiNKHMdELICIiIiKioR166KG47LLLBr1v5MiRfb7esGED5s6dC4fDgerqalx22WUoLCxEY2Mj3njjDVx55ZV4+umn8frrr8dj6URpj0E1EREREVGSO+yww3DHHXcMe9wrr7yCyy67DEVFRfjLX/6Cc845p8/9iqLg5ZdfxhNPPBGjlRJlHgbVRERERERpwOv14tprr4XP58Of/vQnnHXWWQOO0el0uOiii3DeeeclYIVE6Yl7qomIiIiI0sCbb76J3bt3Y+rUqYMG1L2ZzeY4rYoo/TFTTURERESU5L766qug5d+nnHIKvve97+Hdd98FAJx55plxXBkRMagmIiIiIkpyX3/9NZYuXTrofTfccAO+973vobGxEQAwZsyYeC6NKOMxqCYiIiIiSnLnnnsuXnnllUQvg4gGwT3VRERERERpQB2tVV9fn+CVEGUWBtVERERERGng1FNPBQDOnyaKMwbVRERERERp4IwzzsAhhxyCrVu34s033xzyWJfLFadVEaU/BtVERERERGnAYDDgkUcegV6vxyWXXII33nhj0OP+9re/4eKLL47z6ojSFxuVEREREREluaFGagHw3/e9730PTz/9NObNm4ezzjoLkydPxpQpU5Cfn48DBw7grbfewtdff42zzz47PgsnygA6RVGURC+CiIiIiIgG2rt3LyZMmDDscf3f0tfX12P16tV47bXX8PXXX6OrqwulpaU4/vjjcckll+Cyyy5DVlYWAOCtt97CGWecgWuuuQaPPvpoTL4PonTGoJqIiIiIiIgoQtxTTURERERERBQhBtVEREREREREEWJQTURERERERBQhBtVEREREREREEYooqF6zZg0mTJgAi8WC6upqbNmyZcjjn332WXz7299GTk4OKisrceWVV6KlpSWiBRMREREREREli7CD6o0bN2LhwoVYsmQJduzYgWnTpmHGjBmora0d9Ph33nkHc+bMwdy5c/Hpp5/i+eefx/vvv4958+ZFvXgiIiIiIiKiRAp7pNbJJ5+ME044AWvXrvXfNnHiRPzgBz/A8uXLBxx///33Y+3atfj666/9t61evRr33Xcf6urqQnpOn8+H/fv3Iz8/HzqdLpzlEhERxYSiKLDZbBg1ahT0eu6mihZf64mIKNmE+lpvDOdB3W43tm/fjttuu63P7dOnT8fWrVsHPWfq1KlYsmQJNm3ahBkzZqCpqQkvvPACzj///KDP43K54HK5/F/X19fj6KOPDmepREREcVFXV4cxY8Ykehkpb//+/aiqqkr0MoiIiAYY7rU+rKC6ubkZXq8XFRUVfW6vqKhAY2PjoOdMnToVzz77LGbNmgWn0wmPx4MLL7wQq1evDvo8y5cvx9KlSwfcXldXh4KCgnCWTEREFBNWqxVVVVXIz89P9FLSgvrfka/1RESULEJ9rQ8rqFb1L8tSFCVoqdbOnTtx/fXX4/bbb8e5556LhoYG3HLLLZg/fz6efPLJQc9ZvHgxFi1a5P9a/WYKCgr4QktEREmFpcraUP878rWeiIiSzXCv9WEF1WVlZTAYDAOy0k1NTQOy16rly5fj1FNPxS233AIAOPbYY5Gbm4tp06bht7/9LSorKwecYzabYTabw1kaERERERERUdyF1VnFZDKhuroaNTU1fW6vqanB1KlTBz2nq6trwKZug8EAQDLcRERERERERKkq7HalixYtwhNPPIF169Zh165duPHGG1FbW4v58+cDkNLtOXPm+I+/4IIL8NJLL2Ht2rXYvXs33n33XVx//fU46aSTMGrUKO2+EyIiIiIiIqI4C3tP9axZs9DS0oJly5ahoaEB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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with torch.no_grad():\n", + " posterior = gp.posterior(X=xx.unsqueeze(1))\n", + "ymean, yvar = posterior.mean.squeeze(-1), posterior.variance.squeeze(-1)\n", + "eci_vals = eci(xx.unsqueeze(1))\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n", + "ax = axes[0]\n", + "ax.plot(xx[:, 0].cpu(), ymean[:, 0].cpu(), \"b\")\n", + "ax.fill_between(\n", + " xx[:, 0].cpu(),\n", + " ymean[:, 0].cpu() - 1.96 * yvar[:, 0].sqrt().cpu(),\n", + " ymean[:, 0].cpu() + 1.96 * yvar[:, 0].sqrt().cpu(),\n", + " alpha=0.1,\n", + " color=\"b\",\n", + ")\n", + "ax.plot(x[:, 0].cpu(), y[:, 0].cpu(), \"or\")\n", + "ax.axhline(0.05, 0, 1)\n", + "ax.axhline(0.3, 0, 1)\n", + "\n", + "ax = axes[1]\n", + "ax.plot(xx[:, 0].cpu(), eci_vals.detach().cpu())\n", + "ax.plot(x[:, 0].cpu(), torch.zeros(len(x), **tkwargs).cpu(), \"or\")\n", + "ax.plot(best_candidate.cpu(), best_eci_value.cpu(), \"*k\", ms=10)\n", + "ax.set_title(\"ECI\", fontsize=14)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "33ea647e-bdaf-4264-ab65-3e6df4ba8c6e", + "showInput": false + }, + "source": [ + "## Full 2D CAS-loop \n", + "This creates a simple function with two outputs that we will consider under the two constraints $f_1(x) \\leq 0.75$ and $f_2(x) \\geq 0.55$. In this particular example, the $f_1(x)$ and $f_2(x)$ are same function for simplicity. \n", + "\n", + "The CAS loop follows the prototypical BO loop: \n", + "1. Given a surrogate model, maximize ECI to select the next evaluation x.\n", + "2. Observe f(x).\n", + "3. Update the surrogate model. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489237543, + "executionStopTime": 1638489237685, + "hidden_ranges": [], + "originalKey": "691460ed-a2c8-45b5-8dc9-c6d8c87ee9d7", + "requestMsgId": "691460ed-a2c8-45b5-8dc9-c6d8c87ee9d7" + }, + "outputs": [], + "source": [ + "def yf2d(x):\n", + " v = torch.exp(-2 * (x[:, 0] - 0.3) ** 2 - 4 * (x[:, 1] - 0.6) ** 2)\n", + " return torch.stack((v, v), dim=-1)\n", + "\n", + "\n", + "bounds = torch.tensor([[0, 0], [1, 1]], **tkwargs)\n", + "lb, ub = bounds\n", + "dim = len(lb)\n", + "constraints = [(\"lt\", 0.75), (\"gt\", 0.55)]\n", + "punchout_radius = 0.1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6f354b25-8703-4156-908d-d53c1c2bbe4a", + "showInput": false + }, + "source": [ + "### CAS loop using 5 initial Sobol points and 15 ECI iterations" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489237803, + "executionStopTime": 1638489266352, + "hidden_ranges": [], + "originalKey": "6d77353b-8dda-4835-9c6a-b0a53fddc67c", + "requestMsgId": "6d77353b-8dda-4835-9c6a-b0a53fddc67c" + }, + "outputs": [], + "source": [ + "num_init_points = 5\n", + "num_total_points = 15 if not SMOKE_TEST else 5\n", + "\n", + "X = lb + (ub - lb) * SobolEngine(dim, scramble=True).draw(num_init_points).to(**tkwargs)\n", + "Y = yf2d(X)\n", + "\n", + "while len(X) < num_total_points:\n", + " # We don't have to normalize X since the domain is [0, 1]^2. Make sure to\n", + " # appropriately adjust the punchout radius if the domain is normalized.\n", + " gp_models = [get_and_fit_gp(X, Y[:, i : i + 1]) for i in range(Y.shape[-1])]\n", + " model_list_gp = ModelListGP(gp_models[0], gp_models[1])\n", + " eci = ExpectedCoverageImprovement(\n", + " model=model_list_gp,\n", + " constraints=constraints,\n", + " punchout_radius=punchout_radius,\n", + " bounds=bounds,\n", + " num_samples=128 if not SMOKE_TEST else 4,\n", + " )\n", + " x_next, _ = optimize_acqf(\n", + " acq_function=eci,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=10 if not SMOKE_TEST else 2,\n", + " raw_samples=512 if not SMOKE_TEST else 4,\n", + " )\n", + " y_next = yf2d(x_next)\n", + " X = torch.cat((X, x_next))\n", + " Y = torch.cat((Y, y_next))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "255bba4f-4d9a-46cc-aa66-16b90287824a", + "showInput": false + }, + "source": [ + "### Plot the selected points\n", + "We plot the feasible region and the points selected by ECI below. The feasible region is outlined with a black ring, and points selected by ECI are marked in green (feasible) and red (infeasible). By design, observe that ECI selects a diverse i.e., well-spaced set of points inside the feasible region. " + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "code_folding": [], + "customInput": null, + "executionStartTime": 1638489266464, + "executionStopTime": 1638489266516, + "hidden_ranges": [], + "originalKey": "6b62af84-01c0-4971-9122-bd5f01b9f31b", + "requestMsgId": "6b62af84-01c0-4971-9122-bd5f01b9f31b", + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/deriksson/opt/anaconda3/lib/python3.9/site-packages/torch/functional.py:504: UserWarning: torch.meshgrid: in an upcoming release, it will be required to pass the indexing argument. (Triggered internally at /Users/runner/work/pytorch/pytorch/pytorch/aten/src/ATen/native/TensorShape.cpp:3191.)\n", + " return _VF.meshgrid(tensors, **kwargs) # type: ignore[attr-defined]\n" + ] + } + ], + "source": [ + "N1, N2 = 30, 30\n", + "Xplt, Yplt = torch.meshgrid(\n", + " torch.linspace(0, 1, N1, **tkwargs), torch.linspace(0, 1, N2, **tkwargs)\n", + ")\n", + "xplt = torch.stack(\n", + " (\n", + " torch.reshape(Xplt, (Xplt.shape[0] * Xplt.shape[1],)),\n", + " torch.reshape(Yplt, (Yplt.shape[0] * Yplt.shape[1],)),\n", + " ),\n", + " dim=1,\n", + ")\n", + "yplt = yf2d(xplt)\n", + "Zplt = torch.reshape(yplt[:, 0], (N1, N2)) # Since f1(x) = f2(x)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "code_folding": [], + "executionStartTime": 1638489266564, + "executionStopTime": 1638489267143, + "hidden_ranges": [], + "originalKey": "a44c258c-0373-4c68-9887-9ae7a57bcccc", + "requestMsgId": "a44c258c-0373-4c68-9887-9ae7a57bcccc" + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def identify_samples_which_satisfy_constraints(X, constraints):\n", + " \"\"\"\n", + " Takes in values (a1, ..., ak, o) and returns (a1, ..., ak, o)\n", + " True/False values, where o is the number of outputs.\n", + " \"\"\"\n", + " successful = torch.ones(X.shape).to(X)\n", + " for model_index in range(X.shape[-1]):\n", + " these_X = X[..., model_index]\n", + " direction, value = constraints[model_index]\n", + " successful[..., model_index] = (\n", + " these_X < value if direction == \"lt\" else these_X > value\n", + " )\n", + " return successful\n", + "\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "h1 = ax.contourf(Xplt.cpu().numpy(), Yplt.cpu().numpy(), Zplt.cpu().numpy(), 20, cmap=\"Blues\", alpha=0.6)\n", + "fig.colorbar(h1)\n", + "ax.contour(Xplt.cpu().numpy(), Yplt.cpu().numpy(), Zplt.cpu().numpy(), [0.55, 0.75], colors=\"k\")\n", + "\n", + "feasible_inds = (\n", + " identify_samples_which_satisfy_constraints(Y, constraints)\n", + " .prod(dim=-1)\n", + " .to(torch.bool)\n", + ")\n", + "ax.plot(X[feasible_inds, 0].cpu(), X[feasible_inds, 1].cpu(), \"sg\", label=\"Feasible\")\n", + "ax.plot(\n", + " X[~feasible_inds, 0].cpu(), X[~feasible_inds, 1].cpu(), \"sr\", label=\"Infeasible\"\n", + ")\n", + "\n", + "ax.legend(loc=[0.7, 0.05])\n", + "ax.set_title(\"$f_1(x)$\") # Recall that f1(x) = f2(x)\n", + "ax.set_xlabel(\"$x_1$\")\n", + "ax.set_ylabel(\"$x_2$\")\n", + "ax.set_aspect(\"equal\", \"box\")\n", + "ax.set_xlim([-0.05, 1.05])\n", + "ax.set_ylim([-0.05, 1.05])\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "executionStartTime": 1638489267152, + "executionStopTime": 1638489267253, + "originalKey": "0ff4a95d-b556-4a21-b794-184ba4181a49", + "requestMsgId": "0ff4a95d-b556-4a21-b794-184ba4181a49" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "fileHeader": "", + "kernelspec": { + "display_name": "python3", + "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.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/constraint_active_search.py b/website-old/static/files/constraint_active_search.py new file mode 100644 index 0000000000..9a9e4f55c4 --- /dev/null +++ b/website-old/static/files/constraint_active_search.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Constraint Active Search for Multiobjective Experimental Design +# +# In this tutorial we show how to implement the Expected Coverage Improvement (ECI) [1] acquisition function in BoTorch. For a number of outcome constraints, ECI tries to efficiently discover the feasible region and simultaneously sample diverse feasible configurations. Given a user-specified punchout radius $r$, we center a sphere with that radius around each evaluated configuration. The total coverage is now given by the volume of the union of these sphere intersected with the feasible region; see the paper and, in particular, Figure 2 for a full description of how ECI works. +# +# By design, ECI prefers candidates that are in unexplored regions since the candidate's corresponding sphere won't intersect with the spheres around the previously evaluated configurations. On the other hand, ECI also prefers configurations that are likely to satisfy the constraints and to give an improvement in the total coverage. This results in an exploitation-exploration trade-off similar to other acquisition functions. +# +# ECI may be estimated using the following equation: +# $$ +# \text{ECI}(x) = \sum_{x' \in \mathbb{N}(x) \setminus \mathbb{N}_{r}(X)} p(Z(x') = 1 \;|\; \mathcal{D}_t). +# $$ +# +# where $\mathbb{N}(x) \setminus \mathbb{N}_{r}(X)$ a set of points generated via Monte Carlo to be inside a sphere of radius $r$ around $x$, but sufficiently far from the set of known evaluations $X$ (where sufficiently far is defined by the punchout radius $r$). The function $p(Z(x') = 1 \;|\; \mathcal{D}_t)$ is the probability that the GP at $x'$ satisfies a user-specified threshold value, or threshold values in the case of multiple objective functions. +# +# [1]: [Malkomes et al., Beyond the Pareto Efficient Frontier: Constraint Active Search for Multiobjective Experimental Design, Proceedings of the 38th International Conference on Machine Learning, 2021](http://proceedings.mlr.press/v139/malkomes21a/malkomes21a.pdf). + +# In[1]: + + +import os + +import torch +from botorch.acquisition.monte_carlo import MCAcquisitionFunction +from botorch.acquisition.objective import IdentityMCObjective +from botorch.fit import fit_gpytorch_mll +from botorch.models import ModelListGP, SingleTaskGP +from botorch.models.transforms.outcome import Standardize +from botorch.optim import optimize_acqf +from botorch.utils.sampling import sample_hypersphere +from botorch.utils.transforms import t_batch_mode_transform +from gpytorch.constraints import Interval +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.mlls import ExactMarginalLogLikelihood +from torch.quasirandom import SobolEngine + + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# In[2]: + + +tkwargs = { + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), + "dtype": torch.double, +} + + +# To start, we need to be able to sample points in $\mathbb{N}(x) \setminus \mathbb{N}_{r}(X)$. We can generate a pool of points and use standard rejection sampling to do so, but this leads to an acquisition function that isn't immediately differentiable; rejection sampling is essentially providing either a binary weight of either 0 or 1 to each point in the sample pool, which is not a differentiable function. +# +# +# In order to make the acquisition function differentiable, we rely on a differentiable approximation of this binary weight function. For example, `smooth_box_mask` is a continuous differentiable approximation of $a < x < b$ (see the plot below for a visualization). A larger value of eps will make the sigmoid less steep and result in a smoother (and easier to optimize) but less accurate acquisition function. + +# In[3]: + + +def smooth_mask(x, a, eps=2e-3): + """Returns 0ish for x < a and 1ish for x > a""" + return torch.nn.Sigmoid()((x - a) / eps) + + +def smooth_box_mask(x, a, b, eps=2e-3): + """Returns 1ish for a < x < b and 0ish otherwise""" + return smooth_mask(x, a, eps) - smooth_mask(x, b, eps) + + +# In[4]: + + +import matplotlib.pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +x = torch.linspace(-2, 2, 500, **tkwargs) + +fig, ax = plt.subplots(1, 2, figsize=(8, 4)) +ax[0].plot(x.cpu(), smooth_mask(x, -1).cpu(), "b") +ax[1].plot(x.cpu(), smooth_box_mask(x, -1, 1).cpu(), "b") +plt.show() + + +# ## Implementation of ECI +# +# Once we have defined our smooth mask functions, we can compute a differentiable approximation of ECI in a straightforward manner using Monte Carlo (MC). We use the popular variance reduction technique of Common random numbers (CRN). +# +# We first use a low discrepancy sequence to generate a set of base samples. We integrate (sum) over these base samples to approximate the ECI acquisition function. Fixing these base samples makes the method deterministic and by using the smooth masks defined earlier, we can filter out infeasible points while still having a differentiable acquisition function. +# +# This implementation assumes that the GP models for the different outputs are independent and that each constraints only affects one output (simple box-constraints like f(x) <= 0.5). + +# In[5]: + + +class ExpectedCoverageImprovement(MCAcquisitionFunction): + def __init__( + self, + model, + constraints, + punchout_radius, + bounds, + num_samples=128, + **kwargs, + ): + """Expected Coverage Improvement (q=1 required, analytic) + + Right now, we assume that all the models in the ModelListGP have + the same training inputs. + + Args: + model: A ModelListGP object containing models matching the corresponding constraints. + All models are assumed to have the same training data. + constraints: List containing 2-tuples with (direction, value), e.g., + [('gt', 3), ('lt', 4)]. It is necessary that + len(constraints) == model.num_outputs. + punchout_radius: Positive value defining the desired minimum distance between points + bounds: torch.tensor whose first row is the lower bounds and second row is the upper bounds + num_samples: Number of samples for MC integration + """ + super().__init__(model=model, objective=IdentityMCObjective(), **kwargs) + assert len(constraints) == model.num_outputs + assert all(direction in ("gt", "lt") for direction, _ in constraints) + assert punchout_radius > 0 + self.constraints = constraints + self.punchout_radius = punchout_radius + self.bounds = bounds + self.base_points = self.train_inputs + self.ball_of_points = self._generate_ball_of_points( + num_samples=num_samples, + radius=punchout_radius, + device=bounds.device, + dtype=bounds.dtype, + ) + self._thresholds = torch.tensor( + [threshold for _, threshold in self.constraints] + ).to(bounds) + assert ( + all(ub > lb for lb, ub in self.bounds.T) and len(self.bounds.T) == self.dim + ) + + @property + def num_outputs(self): + return self.model.num_outputs + + @property + def dim(self): + return self.train_inputs.shape[-1] + + @property + def train_inputs(self): + return self.model.models[0].train_inputs[0] + + def _generate_ball_of_points( + self, num_samples, radius, device=None, dtype=torch.double + ): + """Creates a ball of points to be used for MC.""" + tkwargs = {"device": device, "dtype": dtype} + z = sample_hypersphere(d=self.dim, n=num_samples, qmc=True, **tkwargs) + r = torch.rand(num_samples, 1, **tkwargs) ** (1 / self.dim) + return radius * r * z + + def _get_base_point_mask(self, X): + distance_matrix = self.model.models[0].covar_module.covar_dist( + X, self.base_points + ) + return smooth_mask(distance_matrix, self.punchout_radius) + + def _estimate_probabilities_of_satisfaction_at_points(self, points): + """Estimate the probability of satisfying the given constraints.""" + posterior = self.model.posterior(X=points) + mus, sigma2s = posterior.mean, posterior.variance + dist = torch.distributions.normal.Normal(mus, sigma2s.sqrt()) + norm_cdf = dist.cdf(self._thresholds) + probs = torch.ones(points.shape[:-1]).to(points) + for i, (direction, _) in enumerate(self.constraints): + probs = probs * ( + norm_cdf[..., i] if direction == "lt" else 1 - norm_cdf[..., i] + ) + return probs + + @t_batch_mode_transform(expected_q=1) + def forward(self, X): + """Evaluate Expected Improvement on the candidate set X.""" + ball_around_X = self.ball_of_points + X + domain_mask = smooth_box_mask( + ball_around_X, self.bounds[0, :], self.bounds[1, :] + ).prod(dim=-1) + num_points_in_integral = domain_mask.sum(dim=-1) + base_point_mask = self._get_base_point_mask(ball_around_X).prod(dim=-1) + prob = self._estimate_probabilities_of_satisfaction_at_points(ball_around_X) + masked_prob = prob * domain_mask * base_point_mask + y = masked_prob.sum(dim=-1) / num_points_in_integral + return y + + +# In[6]: + + +def get_and_fit_gp(X, Y): + """Simple method for creating a GP with one output dimension. + + X is assumed to be in [0, 1]^d. + """ + assert Y.ndim == 2 and Y.shape[-1] == 1 + likelihood = GaussianLikelihood(noise_constraint=Interval(1e-6, 1e-3)) # Noise-free + octf = Standardize(m=1) + gp = SingleTaskGP(X, Y, likelihood=likelihood, outcome_transform=octf) + mll = ExactMarginalLogLikelihood(model=gp, likelihood=gp.likelihood) + fit_gpytorch_mll(mll) + return gp + + +# ### Simple 1D function +# +# To sanity check things, we consider the ECI acquisition function on a one-dimensional toy problem. + +# In[7]: + + +def yf(x): + return (1 - torch.exp(-4 * (x[:, 0] - 0.4) ** 2)).unsqueeze(-1) + + +x = torch.tensor([0, 0.15, 0.25, 0.4, 0.8, 1.0], **tkwargs).unsqueeze(-1) +y = yf(x) +xx = torch.linspace(0, 1, 200, **tkwargs).unsqueeze(-1) +yy = yf(xx) + + +# ### Create an ECI acquisition function +# Our implementation assumes that the GP is passed in as a `ModelListGP` and that the GPs match the corresponding constraints. As an example, assume we have two outputs, represented by `gp1` and `gp2` and two constraints corresponding to output 1 and a third constraint corresponding to output 2. In that case we will create a model list GP as `ModelListGP(gp1, gp1, gp2)` so they match the constraints. + +# In[8]: + + +gp = get_and_fit_gp(x, y) +model_list_gp = ModelListGP(gp, gp) +constraints = [("lt", 0.3), ("gt", 0.05)] +punchout_radius = 0.03 +bounds = torch.tensor([(0, 1)], **tkwargs).T +eci = ExpectedCoverageImprovement( + model=model_list_gp, + constraints=constraints, + punchout_radius=punchout_radius, + bounds=bounds, + num_samples=128 if not SMOKE_TEST else 4, +) + + +# ### Optimize the acquisition function + +# In[9]: + + +best_candidate, best_eci_value = optimize_acqf( + acq_function=eci, + bounds=torch.tensor([[0.0], [1.0]], **tkwargs), + q=1, + num_restarts=10, + raw_samples=20, # use a small number here to make sure the optimization works +) +print(f"Best candidate: {best_candidate.cpu().item():.3f}") + + +# ### Plot the GP and the ECI acquisition function +# The left plot shows the GP posterior with a 95% confidence interval. The two horizontal lines indicate the feasible region defined by $0.05 \leq f(x) \leq 0.3$. These inequality constraints implicitly define a feasible region, outside which ECI has value zero. +# +# We can see in the right plot that ECI indeed has a nonzero value inside the feasible region and a zero value outside. We also optimize the acquisition function and mark its argmax with black star; the argmax is around $x=0.62$. This is reasonable because ECI seeks to select diverse points within the feasible region. $x=0.62$ is far away from other evaluations and thus has the highest diversity. + +# In[10]: + + +with torch.no_grad(): + posterior = gp.posterior(X=xx.unsqueeze(1)) +ymean, yvar = posterior.mean.squeeze(-1), posterior.variance.squeeze(-1) +eci_vals = eci(xx.unsqueeze(1)) + +fig, axes = plt.subplots(1, 2, figsize=(12, 5)) +ax = axes[0] +ax.plot(xx[:, 0].cpu(), ymean[:, 0].cpu(), "b") +ax.fill_between( + xx[:, 0].cpu(), + ymean[:, 0].cpu() - 1.96 * yvar[:, 0].sqrt().cpu(), + ymean[:, 0].cpu() + 1.96 * yvar[:, 0].sqrt().cpu(), + alpha=0.1, + color="b", +) +ax.plot(x[:, 0].cpu(), y[:, 0].cpu(), "or") +ax.axhline(0.05, 0, 1) +ax.axhline(0.3, 0, 1) + +ax = axes[1] +ax.plot(xx[:, 0].cpu(), eci_vals.detach().cpu()) +ax.plot(x[:, 0].cpu(), torch.zeros(len(x), **tkwargs).cpu(), "or") +ax.plot(best_candidate.cpu(), best_eci_value.cpu(), "*k", ms=10) +ax.set_title("ECI", fontsize=14) +plt.show() + + +# ## Full 2D CAS-loop +# This creates a simple function with two outputs that we will consider under the two constraints $f_1(x) \leq 0.75$ and $f_2(x) \geq 0.55$. In this particular example, the $f_1(x)$ and $f_2(x)$ are same function for simplicity. +# +# The CAS loop follows the prototypical BO loop: +# 1. Given a surrogate model, maximize ECI to select the next evaluation x. +# 2. Observe f(x). +# 3. Update the surrogate model. + +# In[11]: + + +def yf2d(x): + v = torch.exp(-2 * (x[:, 0] - 0.3) ** 2 - 4 * (x[:, 1] - 0.6) ** 2) + return torch.stack((v, v), dim=-1) + + +bounds = torch.tensor([[0, 0], [1, 1]], **tkwargs) +lb, ub = bounds +dim = len(lb) +constraints = [("lt", 0.75), ("gt", 0.55)] +punchout_radius = 0.1 + + +# ### CAS loop using 5 initial Sobol points and 15 ECI iterations + +# In[12]: + + +num_init_points = 5 +num_total_points = 15 if not SMOKE_TEST else 5 + +X = lb + (ub - lb) * SobolEngine(dim, scramble=True).draw(num_init_points).to(**tkwargs) +Y = yf2d(X) + +while len(X) < num_total_points: + # We don't have to normalize X since the domain is [0, 1]^2. Make sure to + # appropriately adjust the punchout radius if the domain is normalized. + gp_models = [get_and_fit_gp(X, Y[:, i : i + 1]) for i in range(Y.shape[-1])] + model_list_gp = ModelListGP(gp_models[0], gp_models[1]) + eci = ExpectedCoverageImprovement( + model=model_list_gp, + constraints=constraints, + punchout_radius=punchout_radius, + bounds=bounds, + num_samples=128 if not SMOKE_TEST else 4, + ) + x_next, _ = optimize_acqf( + acq_function=eci, + bounds=bounds, + q=1, + num_restarts=10 if not SMOKE_TEST else 2, + raw_samples=512 if not SMOKE_TEST else 4, + ) + y_next = yf2d(x_next) + X = torch.cat((X, x_next)) + Y = torch.cat((Y, y_next)) + + +# ### Plot the selected points +# We plot the feasible region and the points selected by ECI below. The feasible region is outlined with a black ring, and points selected by ECI are marked in green (feasible) and red (infeasible). By design, observe that ECI selects a diverse i.e., well-spaced set of points inside the feasible region. + +# In[13]: + + +N1, N2 = 30, 30 +Xplt, Yplt = torch.meshgrid( + torch.linspace(0, 1, N1, **tkwargs), torch.linspace(0, 1, N2, **tkwargs) +) +xplt = torch.stack( + ( + torch.reshape(Xplt, (Xplt.shape[0] * Xplt.shape[1],)), + torch.reshape(Yplt, (Yplt.shape[0] * Yplt.shape[1],)), + ), + dim=1, +) +yplt = yf2d(xplt) +Zplt = torch.reshape(yplt[:, 0], (N1, N2)) # Since f1(x) = f2(x) + + +# In[14]: + + +def identify_samples_which_satisfy_constraints(X, constraints): + """ + Takes in values (a1, ..., ak, o) and returns (a1, ..., ak, o) + True/False values, where o is the number of outputs. + """ + successful = torch.ones(X.shape).to(X) + for model_index in range(X.shape[-1]): + these_X = X[..., model_index] + direction, value = constraints[model_index] + successful[..., model_index] = ( + these_X < value if direction == "lt" else these_X > value + ) + return successful + + +fig, ax = plt.subplots(figsize=(8, 6)) +h1 = ax.contourf(Xplt.cpu().numpy(), Yplt.cpu().numpy(), Zplt.cpu().numpy(), 20, cmap="Blues", alpha=0.6) +fig.colorbar(h1) +ax.contour(Xplt.cpu().numpy(), Yplt.cpu().numpy(), Zplt.cpu().numpy(), [0.55, 0.75], colors="k") + +feasible_inds = ( + identify_samples_which_satisfy_constraints(Y, constraints) + .prod(dim=-1) + .to(torch.bool) +) +ax.plot(X[feasible_inds, 0].cpu(), X[feasible_inds, 1].cpu(), "sg", label="Feasible") +ax.plot( + X[~feasible_inds, 0].cpu(), X[~feasible_inds, 1].cpu(), "sr", label="Infeasible" +) + +ax.legend(loc=[0.7, 0.05]) +ax.set_title("$f_1(x)$") # Recall that f1(x) = f2(x) +ax.set_xlabel("$x_1$") +ax.set_ylabel("$x_2$") +ax.set_aspect("equal", "box") +ax.set_xlim([-0.05, 1.05]) +ax.set_ylim([-0.05, 1.05]) +plt.show() + + +# In[ ]: + + + + diff --git a/website-old/static/files/cost_aware_bayesian_optimization.ipynb b/website-old/static/files/cost_aware_bayesian_optimization.ipynb new file mode 100644 index 0000000000..400bea2882 --- /dev/null +++ b/website-old/static/files/cost_aware_bayesian_optimization.ipynb @@ -0,0 +1,662 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "26281547", + "metadata": {}, + "source": [ + "# Cost-aware Bayesian Optimization\n", + "\n", + "This tutorial covers cost-aware Bayesian optimization, a situation in which the cost of evaluation is unknown but assumed to depend on the set or a subset of the optimization parameters. \n", + "\n", + "Note that cost-aware Bayesian optimization is a more general form of multifidelity Bayesian optimization: \n", + "* In multi-fidelity Bayesian optimization, the fidelity parameters are typically known ahead of time, an the relationship between cost and performance is typically known e.g., the highest fidelity parameters are the least noisy and the most costly. \n", + "* In cost-aware Bayesian optimization, we do not know a-priori which parameters dictate cost, nor do we make any assumptions about the relationship between cost and performance. \n", + "\n", + "Cost-aware Bayesian optimization is well suited to any problem for which the user suspects there to be a heterogenous cost of evaluation. It can also be used as a simpler alternative to multifidelity optimization, although we recommend a dedicated multifidelity algorithm for more experienced users. In this tutorial, the acquisition function we use for cost-aware Bayesian optimization is expected improvement per unit (EIpu), which has the following formula:\n", + "\n", + "$$\n", + "EIpu(x) = \\frac{EI(x)}{c(x)^\\alpha}\n", + "$$\n", + "\n", + "$c(x)$ is a cost model that predicts the evaluation cost and $\\alpha \\in [0, 1]$ is a decay factor that reduces or increases the cost model's effect to prevent cheap points from dominating the optimization routine. We recommend starting $\\alpha$ at 1 and decreasing it to 0 as the optimization budget is exhausted. \n", + "\n", + "[1]: [Lee, Eric Hans, et al. Cost-aware Bayesian Optimization. International Conference on Machine Learning, AutoML Workshop. 2020. ](https://arxiv.org/pdf/2003.10870.pdf)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "05735702", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "import torch\n", + "import warnings\n", + "\n", + "from abc import ABC, abstractmethod\n", + "\n", + "from botorch.acquisition import AnalyticAcquisitionFunction, ExpectedImprovement\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.models.transforms import Log\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.test_functions import Ackley\n", + "from botorch.utils import standardize\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "device = torch.device(\"cuda:1\" if torch.cuda.is_available() else \"cpu\")\n", + "tkwargs = {\n", + " \"device\": device,\n", + " \"dtype\": torch.double,\n", + "}\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "a6f3faf8", + "metadata": {}, + "source": [ + "# Cost Modeling\n", + "\n", + "The first thing we do in this tutorial is define a simple cost model, in which we make no assumptions other than a positive cost. We will use the mean of a GP for the cost model. To enforce positivity, we will model the log cost and then exponentiate when we perform predictions. Users can use more bespoke cost models should they have a better understanding of their problem. \n", + "\n", + "Having defined the cost model, we'll also generate some simple plots of a 1D synthetic problem for illustrative purposes, where the objective is the Ackley function and the cost is quadratic." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e4aca71c", + "metadata": {}, + "outputs": [], + "source": [ + "class CostModel(torch.nn.Module, ABC):\n", + " \"\"\"\n", + " Simple abstract class for a cost model.\n", + " \"\"\" \n", + " \n", + " @abstractmethod\n", + " def forward(self, X):\n", + " pass\n", + " \n", + "\n", + "class CostModelGP(CostModel):\n", + " \"\"\"\n", + " A basic cost model that assumes the cost is positive.\n", + " It models the log cost to guarantee positive cost predictions.\n", + " \"\"\"\n", + "\n", + " def __init__(self, X, Y_cost):\n", + " assert torch.all(Y_cost > 0)\n", + " super().__init__()\n", + " gp = SingleTaskGP(train_X=X, train_Y=Y_cost, outcome_transform=Log())\n", + " mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)\n", + " fit_gpytorch_mll(mll)\n", + " self.gp = gp\n", + "\n", + " def forward(self, X):\n", + " return torch.exp(self.gp(X).mean)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "12bcb738", + "metadata": {}, + "outputs": [], + "source": [ + "def synthetic_objective_with_cost(x):\n", + " dim = 1\n", + " f = Ackley(dim) # synthetic objective is the Ackley\n", + " fx = f(x).unsqueeze(1)\n", + " cx = 200 * (1.1 - x) ** 2 # synthetic cost is quadratric\n", + " return fx, cx\n", + "\n", + "\n", + "# Generate training data\n", + "dim = 1\n", + "num = 4\n", + "bounds = torch.tensor([[0] * dim, [1] * dim], **tkwargs)\n", + "train_X = draw_sobol_samples(bounds=bounds, n=num, q=1, seed=111).squeeze(1)\n", + "train_Y, cost_Y = synthetic_objective_with_cost(train_X)\n", + "\n", + "# Fit GP to data\n", + "train_Y = standardize(train_Y)\n", + "gp = SingleTaskGP(train_X=train_X, train_Y=train_Y)\n", + "mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)\n", + "fit_gpytorch_mll(mll)\n", + "\n", + "# Fit cost model to data\n", + "cost_model_gp = CostModelGP(train_X, cost_Y)\n" + ] + }, + { + "cell_type": "markdown", + "id": "a500755e", + "metadata": {}, + "source": [ + "# Plot the GP and the Cost Model" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "686cd4d2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'Cost of Evaluation')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(10, 5))\n", + "\n", + "# Plot GP\n", + "X_preds = torch.linspace(0, 1, 100, **tkwargs).unsqueeze(1)\n", + "Y_preds = gp.posterior(X_preds)\n", + "Y_mean = Y_preds.mean.squeeze().detach().numpy()\n", + "Y_var = Y_preds.variance.squeeze().detach().numpy()\n", + "axes[0].plot(X_preds, Y_preds.mean.detach().numpy(), \"r\")\n", + "axes[0].plot(train_X, train_Y, \"k^\")\n", + "axes[0].fill_between(\n", + " X_preds.numpy()[:, 0], Y_mean - Y_var, Y_mean + Y_var, color=\"m\", alpha=0.5\n", + ")\n", + "axes[0].set_title(\"Gaussian Process Model\")\n", + "axes[0].set_ylabel(\"Objective Value\")\n", + "\n", + "# Plot Cost Model\n", + "cost_preds = cost_model_gp(X_preds)\n", + "axes[1].plot(X_preds, cost_preds.detach().numpy())\n", + "axes[1].plot(train_X, cost_Y, \"kv\")\n", + "axes[1].set_title(\"Cost Model\")\n", + "axes[1].set_ylabel(\"Cost of Evaluation\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "2ca3d020", + "metadata": {}, + "source": [ + "# Expected Improvement Per Unit\n", + "\n", + "Having defined the cost model, we can now define our EIpu acquisition function and plot it for different values of $\\alpha$. Note that when $\\alpha=0$, EIpu simply reduces to EI. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "73667f9d", + "metadata": {}, + "outputs": [], + "source": [ + "class ExpectedImprovementWithCost(AnalyticAcquisitionFunction):\n", + " \"\"\"\n", + " This is the acquisition function EI(x) / c(x) ^ alpha, where alpha is a decay\n", + " factor that reduces or increases the emphasis of the cost model c(x).\n", + " \"\"\"\n", + "\n", + " def __init__(self, model, best_f, cost_model, alpha=1):\n", + " super().__init__(model=model)\n", + " self.model = model\n", + " self.cost_model = cost_model\n", + " self.ei = ExpectedImprovement(model=model, best_f=best_f)\n", + " self.alpha = alpha\n", + "\n", + " def forward(self, X):\n", + " return self.ei(X) / torch.pow(self.cost_model(X)[:, 0], self.alpha)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4510d1e6", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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i/Hjx+NPf/rTsFcJbdu2DQsXLjQ4Ceju7o41a9bgk08+wfLlyxESEiJ8f82aNSgsLMR7772H6upqBAUFYcmSJQbHmJKSguuvv16rPzo62ujEFwDceOONePzxx9HW1qZVY23Dhg1QKpV45ZVXUFlZidmzZ+P1119HeHj4oK/5+OOPo6qqCp999hm2bt2KVatW4bvvvtP6swgNDcWRI0fw1FNP4YsvvsCbb76JwMBATJo0Cc8//7wwdvfu3aitrdVbD4yIiIjsl0KtqworERERObS9e/fivPPOw7Zt23DllVea/f2efvppvP/++8jJyRmyQHtycjKCg4Oxc+dOs8c1EuXl5YiNjcWnn35q8IovW9PQ0ICxY8fiL3/5C2666Sa5w9Hr0ksvhUKhwJdffil3KERERGRhrPFFREREstu0aROam5vx6aef9vV1dXVp1dbau3cvTp06haVLl1o4QuO98sormDx5st0mvQDAz88PDzzwAF544YW+kxatTWZmJr755hs8/fTTcodCREREMuCKLyIiItJi6RVfuhQWFmL58uW47rrrEBERgTNnzuCtt96Cn58f0tLSEBgYKEtcRERERGQ7WOOLiIiIrJK/vz9mzJiBf/7zn6iqqoKXlxcuvvhi/PnPf2bSi4iIiIgMwhVfRERERERERERkl1jji4iIiIiIiIiI7BITX0REREREREREZJeY+CIiIiIiIiIiIrvExBcREREREREREdklJr6IiIiIiIiIiMguMfFFRERERERERER2iYkvIiIiIiIiIiKyS0x8ERERERERERGRXWLii4iIiIiIiIiI7BITX2R39u7dC4VCgb179w772s8++8z0gRERkV3gPENERObEeYbItJj4IrJSWVlZ2LRpE+bPnw93d3coFAoUFhYa9RqZmZlYuXIlvL29ERAQgOuvvx5VVVXmCZiIiGxOaWkp1q1bh1GjRsHX1xdr165Ffn6+QdcuXboUCoVC62vlypVmjpqIiGzFSOYZADhw4AAWLlwIT09PhIWF4fe//z2am5vNGDHZI2e5AyAi3Q4ePIi//e1vSExMREJCAlJSUoy6vqSkBIsXL4afnx+ee+45NDc348UXX0RqaiqOHDkCV1dX8wROREQ2obm5Geeddx4aGhrwyCOPwMXFBX/961+xZMkSpKSkIDAwcMjXGD16NDZv3iz0RUREmCtk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f/55FBYWYvz48diyZQtSUlLw9ttvG33s+sSJExEXF4f7778fpaWl8PX1xeeff661Nx4Yfo2vhoYGvPbaawCA/fv3AwBef/11jBo1CqNGjcLdd9/dN3bZsmUANHv7ez3yyCPYtm0bzjvvPPzhD39Ac3MzXnjhBUyePNngJzZERI7E0eaZO++8E++88w4uvvhi3H///XBxccHLL7+M0NBQ3HfffcLYhIQELFmyBHv37gUAnDhxAtdccw2uueYaxMfHo62tDV9++SX279+PW2+9FdOnTzc6HiIie8d5xvB5BgCeffZZzJ8/H0uWLMGtt96KkpISvPTSS7jwwguxcuVKo+Mhx8XEF9mMV199FU5OTvjkk0/Q3t6OBQsWYNeuXQY/Vfb398eHH36I3/3ud3jnnXcQGhqK119/HbfccovRsbi4uOB///sffv/732Pz5s1wd3fHZZddhrvvvhtTp041+vV0qaurw2OPPSb09S6Ljo6OFhJfukRFReGnn37Cvffei4ceegiurq64+OKL8dJLL7G+FxGRDo42z/j4+GDv3r3YtGkTnnnmGahUKixduhR//etfh9yqGB0djUWLFuHLL79EeXk5lEolEhIS8NZbb+HWW281SXxERPaG84zh8wwATJ8+Hbt27cKDDz6ITZs2wcfHBzfddBM2b95skvjIcSjUpioqRGTFli5diurqaqSlpckdChER2SHOM0REZE6cZ4iGTyl3AERERERERERERObAxBcREREREREREdklJr6IiIiIiIiIiMguscYXERERERERERHZJa74IiIiIiIiIiIiu+QsdwCGUKlUOHfuHHx8fKBQKOQOh4jI5qnVajQ1NSEiIgJKJZ+BcJ4hIjItzjMizjNERKZlzDxjE4mvc+fOISoqSu4wiIjsTnFxMUaPHi13GLLjPENEZB6cZzQ4zxARmYch84xNJL58fHwAaH4gX19fmaMhIrJ9jY2NiIqK6vt8dXScZ4iITIvzjIjzDBGRaRkzz9hE4qt3ObCvry8nCiIiE+J2Cw3OM0RE5sF5RoPzDBGReRgyz3DDPRERERERERER2SUmvoiIiIiIiIiIyC4x8UVERERERERERHaJiS8iIiIiIiIiIrJLTHwREREREREREZFdYuKLiIiIiIiIiIjsEhNfRERERERERERkl5j4IiIiIiIiIiIiu8TEFxERERER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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n", + "X_preds = torch.linspace(0, 1, 100, **tkwargs).unsqueeze(1)\n", + "X_batch = X_preds.unsqueeze(1)\n", + "\n", + "\n", + "def normalize_acquisition_values(values):\n", + " max_value = values.max().item()\n", + " min_value = values.min().item()\n", + " return (values - min_value) / (max_value - min_value)\n", + "\n", + "\n", + "# Compute EI\n", + "fmax = torch.max(train_Y)\n", + "ei = ExpectedImprovement(model=gp, best_f=fmax)\n", + "ei_values = normalize_acquisition_values(ei(X_batch))\n", + "\n", + "# Compute and plot EIpu vs EI\n", + "fig.suptitle(\"EIpu (green) vs EI (blue)\")\n", + "for i in range(3):\n", + " alpha = 1 - i / 2\n", + " eipu = ExpectedImprovementWithCost(\n", + " model=gp,\n", + " best_f=fmax,\n", + " cost_model=cost_model_gp,\n", + " alpha=alpha,\n", + " )\n", + " eipu_values = normalize_acquisition_values(eipu(X_batch).squeeze())\n", + " axes[i].plot(X_preds, eipu_values.detach().numpy(), \"-g\", linewidth=3)\n", + " axes[i].plot(X_preds, ei_values.detach().numpy(), \"--b\", alpha=1, linewidth=3)\n", + " axes[i].set_title(f\"alpha={alpha}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "b55bc51c", + "metadata": {}, + "source": [ + "# A Practial Problem\n", + "\n", + "To make things more interesting, let's look at the classic problem of least squares estimation:\n", + "\n", + "$$\n", + "\\text{arg} \\min_{x \\in \\mathbb{R}^d} \\| Ax - b \\|_2\n", + "$$\n", + "\n", + "$A$ is a matrix of size $n \\times d$ and $b$ is a vector of length $n$. Assuming that $n \\geq d$, the solution to this problem is unique and has the following closed form: $(A^T A) ^{-1} (A^T b)$. The problem with explicitly computing this solution is that it will have an $\\mathcal{O}(n^3)$ complexity due to the need to compute a Cholesky factorization of the matrix $A^T A$. \n", + "\n", + "\n", + "These difficulties in computing an explicit solution when $n$ is large lead us to a cost-aware twist on the least squares estimation. An alternative solution is to perform batched gradient descent by sampling rows of $A$. Because the batching introduces noise, we'll use Adam to perform the optimization. This introduces hyperparameters such as the learning rate, batch size, and the number of optimization iterations. These hyperparameters influence the cost immensely, as we'll see in a bit. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "875dd79a", + "metadata": {}, + "outputs": [], + "source": [ + "class NoisyLinearLeastSquares:\n", + " \"\"\"\n", + " The standard linear least squares problem min_x ||Ax - b||_2.\n", + " We compute the loss via batching that introduces noise.\n", + " \"\"\"\n", + "\n", + " def __init__(self, A, b, batch_size=50):\n", + " self.A = A\n", + " self.b = b\n", + " self.batch_size = min(batch_size, self.A.shape[0])\n", + "\n", + " def fit(self, lr=1, niters=100):\n", + " x = torch.zeros(A.shape[1], 1, requires_grad=True, **tkwargs)\n", + " optimizer = torch.optim.Adam([x], lr=lr)\n", + " batch_indices = torch.randperm(A.shape[1])[: self.batch_size]\n", + " for i in range(niters):\n", + " res = torch.matmul(self.A[batch_indices, :], x) - self.b[batch_indices]\n", + " loss = torch.norm(res)\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " return x, loss\n" + ] + }, + { + "cell_type": "markdown", + "id": "baf37696", + "metadata": {}, + "source": [ + "# Cost Analysis\n", + "\n", + "Here, we examine the variation in runtime as we vary both the batch size and the number of Adam iterations. Perhaps unsurpsingly, the runtime varies significantly with these parameters. Though we expect the runtime to be stricly linear in both the batch size and the number of Adam iterations, we can see that in practice the graph is a little variance due to the nuances in which the computer executes the matrix operations." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "07ac8ad5", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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CxY0HB0djRIlShidOnUyQkJC/usjFxGRNKQ+/eF54+LijCFDhhiFCxc2LBbLfZ/VF198YdSsWdNwdnY28ubNa1SpUsV4/fXXjYsXLya2KVGihNGqVatk33PNmjVG1apVDScnJ6NkyZLG5MmTjdmzZxuAcfr06cR2oaGhRqtWrYy8efMaQOL0YQ/q24OCgozq1asbjo6ORoECBYxu3boZ58+fT9KmV69eRu7cue/L9O/vJcuXLzd8fX0NNzc3w8HBwShevLjx0ksvGZcuXXro5ynyIBbDSKPrM0VEREREREQkCd3TLSIiIiIiIpJOVHSLiIiIiIiIpBMV3SIiIiIiIiLpREW3iIiIiIiISDpR0S0iIiIiIiKSTlR0i4iIiIiIiKSTHGYHyIwSEhK4ePEiefPmxWKxmB1HRESyMcMwuHXrFp6entjZ6bfwx6U+XEREMkpK+3AV3cm4ePEixYoVMzuGiIjYkHPnzvHEE0+YHSPLUx8uIiIZ7b/6cBXdycibNy9g/fBcXFxMTiMiItlZREQExYoVS+x75PGoDxcRkYyS0j5cRXcy7l2O5uLiog5bREQyhC6FThvqw0VEJKP9Vx+um8dERERERERE0omKbhEREREREZF0oqJbREREREREJJ2o6BYRERERERFJJyq6RURERERERNKJim4RERERERGRdKKiW0RERERERCSdqOgWERERERERSScqukVERERERETSiYpuERERERERkXSioltEREREREQknajoFhEREREREUknKrpFRERERERE0omKbhEREREREZF0oqJbRETkEYWGwrvvQkKC2UlERETkUZy8fpKpu6dm6HvmyNB3ExERyeKuXwdfX/jtN7h9GyZNMjuRiIiIpMTBSwdpsbAFYZFhuDi68GKNFzPkfVV0i4iIpNCtW9CihbXgLlIE+vc3O5GIiIikxPbT23luyXPcirlFNfdqtCrXKsPeW5eXi4iIpMDdu9C2LezdCwUKwJYtUKaM2alERETkvyz/fTnNFzbnVswtGpZoyHe9v6NI3iIZ9v4qukVERP5DTAx07Ag7dkDevLBpE1SqZHYqERER+S+z9s2i07JOxMTH8EKFFwjuHoyrk2uGZlDRLSIi8hDx8dCzJ6xfD87O1v/WqmV2KhEREXkYwzAYu2Msg9YPwsBgQI0BLO2wFKccThmeRfd0i4iIPIBh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lcj37Su/HxEBrFgB2NjoNi4iIiKqbm36WkzYOQEAMOOfMzBlwBQdR0RERPqKXy/XAzt2lP/v7X37AAsL4MsvgQ0bmHATERHpoy2ntmDM92MAADEBMXj/sfd1HBEREekzJt06dPcuEBsLPPkkcO0a8PDD5f97+6WXAJVK19ERERFRVT9m/ojnv3sealHjZb+XsTR4KVSctImIqA5MunXk7FmgXz/g44/L78fEAL/8AnTrptu4iIiIqGb7L+zH8M3DcU99DyN7jcTKJ1cy4SYionrxN93NTKT8f21PmADcvg3Y2QFr1pSvdhMREZF+Ovz7YTy18SncuXcHTz30FL56+isYGxnrOiwiIjIATLqbUUEBMH488PXX5fcHDQLWrwc6dtRpWERERFSHX/N+RciGEBSVFGGw22Bsfm4zWhm30nVYRERkIPj18maSklL+v7e//howNgb+8x9gzx4m3ERERPos41oG/rXuX8i/k4/+nftj+8jtaG3SWtdhERGRAeFKt5ap1cCHHwLTpwP37gEuLuWJd//+uo6MiIiI6nIh/wKC1gXhz+I/4efohx3P74ClqaWuwyIiIgPDpFuL8vKAUaOAhITy+//3f8CqVYCtrU7DIiIionpcLryMwV8Nxu8Fv6N7++7Y9cIu2La21XVYRERkgPj1ci3as6c84TY3Bz77DNi8mQk3ERGRIfhvyn9x/uZ5uLd1x55Re2Bvaa/rkIiIyEBxpVuLIiOBzEwgPBzo0UPX0RAREVFDzXlsDkQE//b/N5ytnHUdDhERGTAm3VqkUgHvvafrKIiIiOh+GRsZY17QPF2HQURELQC/Xk5ERERERESkJUy6iYiIiIiIiLSESTcRERERERGRljDpJiIiIiIiItISJt1EREREREREWsKkm4iIiIiIiEhLmHQTERERERERaQmTbiIiIiIiIiItYdJNREREREREpCVMuomIiIiIiIi0hEk3ERERERERkZYw6SYiIiIiIiLSEibdRERERERERFrCpJuIiIiIiIhIS5h0ExEREREREWmJia4D0EciAgAoKCjQcSRERNTSVcw1FXMPNQ7ncCIiai4NncOZdNegsLAQANC5c2cdR0JERP8rCgsLYWNjo+swDB7ncCIiam71zeEq4Ufr1ajValy+fBlWVlZQqVSN2ldBQQE6d+6MS5cuwdrauokibJnYVw3Hvmo4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TSUmJoqISFpamgQEBIiNjY20bt1aunfvLh988IFGEqsv7SkuLpYhQ4aIvb29tGrVSlxcXGTcuHEa/+5UxHDGp8LKlSvF3NxcY26toG/j05RUIiLaXEknIiIiIiIi+l/F33QTERERERERaQmTbiIiIiIiIiItYdJNREREREREpCVMuomIiIiIiIi0hEk3ERERERERkZYw6SYiIiIiIiLSEibdRERERERERFrCpJuIiIiIiIhIS5h0E9EDW7NmDWxtbZt8v7Nnz8bDDz/c5PslIiL6X+Dq6oqPPvpI12EQ0f/HpJvIwI0ePRoqlUq52dnZISQkBMePH7+v/TRnort161Y8+uijsLGxgZWVFXr27InY2FilfNKkSdi7d2+zxEJERPSgRo8ejaefflq5P2jQII35TNtq+/A7NTUVr7zyitafX0Tw2WefISAgAG3atIGtrS38/f3x0Ucfobi4WOvPX1nVsSDSJ0y6iVqAkJAQ5ObmIjc3F3v37oWJiQmefPJJXYdVo7179yI8PBzDhw9HSkoK0tLSMHfuXJSWlip12rRpAzs7Ox1GSUREpDslJSWNery9vT0sLCyaKJravfjii4iNjUVYWBgSExORnp6OmTNnYvv27di9e7fWn5/IYAgRGbSoqCgJCwvT2Pbzzz8LALl69aqybcqUKdK1a1cxNzcXNzc3mTFjhpSUlIiIyOrVqwWAxm316tUiInLz5k155ZVXxMHBQczMzKRnz57yww8/KI+zsbGR+Ph48fLyEktLSwkODpbLly/XGm9MTIwMGjSozjbNmjVLfH19lftVYwMgLi4uSvmJEyckJCRELC0txcHBQV544QX5888/G9B7RERED67yHBwVFVVtrsrOzhaR+uepwMBAiY6OlpiYGLGzs1PmycWLF0uvXr3EwsJCOnXqJOPHj5fCwkIREUlMTKz2fLNmzRIRERcXF1m6dKmy/4sXL8qwYcPE0tJSrKys5LnnnpO8vDylvGLe/eqrr8TFxUWsra0lPDxcCgoKam37pk2bBIBs27atWplarZb8/HwRESkrK5P33ntPOnbsKKampuLr6ys7d+5U6la04+bNm8q2Y8eOafRffe83Zs2aVa0vEhMTax84ombGlW6iFqaoqAjr16+Hp6enxmqxlZUV1qxZg9OnT+Pjjz/GqlWrsHTpUgBAeHg4Jk6ciJ49eyor5uHh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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(10, 5))\n", + "n = 30000 if not SMOKE_TEST else 300\n", + "d = 3000 if not SMOKE_TEST else 30\n", + "A = torch.rand(n, d, **tkwargs)\n", + "b = torch.rand(n, 1, **tkwargs)\n", + "\n", + "# Timings varying batch size\n", + "batch_sizes = 100 * torch.arange(4, 10, device=device) \n", + "times_batch = []\n", + "for batch_size in batch_sizes:\n", + " model = NoisyLinearLeastSquares(A, b, batch_size=batch_size)\n", + " t_start = time.time()\n", + " model.fit(lr=0.1, niters=200)\n", + " times_batch.append(time.time() - t_start)\n", + "\n", + "axes[0].set_title(\"Time vs Batch Size\")\n", + "axes[0].set_xlabel(\"Batch Size\")\n", + "axes[0].set_ylabel(\"Runtime (s)\")\n", + "axes[0].plot(batch_sizes, times_batch, \"b\")\n", + "\n", + "# Timings varying number of Adam iterations\n", + "iter_count = 10 * torch.arange(1, 20, device=device)\n", + "times_iters = []\n", + "for niters in iter_count:\n", + " model = NoisyLinearLeastSquares(A, b)\n", + " t_start = time.time()\n", + " model.fit(lr=0.1, niters=niters)\n", + " times_iters.append(time.time() - t_start)\n", + "\n", + "axes[1].set_title(\"Time vs Iterations\")\n", + "axes[1].set_xlabel(\"Iteration Count\")\n", + "axes[1].set_ylabel(\"Runtime (s)\")\n", + "axes[1].plot(iter_count, times_iters, \"g\")\n", + "\n", + "plt.tight_layout()\n" + ] + }, + { + "cell_type": "markdown", + "id": "419ea7eb", + "metadata": {}, + "source": [ + "# Full Optimization Loop\n", + "\n", + "Having defined our problem, let's now run a full optimization loop and see how EIpu does compared to EI. Let's tune three hyperparameters in our least squares estimator: the learning rate, the batch size, and the number of adam iterations. \n", + "\n", + "* $ \\textit{learning_rate} \\in [0.05, 1.0]$\n", + "* $ \\textit{batch_size} \\in [40, 1000] $ \n", + "* $\\textit{num_iters} \\in [10, 400]$. \n", + "\n", + "Previously, we mentioned that we can use bespoke cost models tailored to the specific problem to increase performance. Let's do this by replacing the generic GP cost model with a custom linear one. Note that we can only do this because we performed some cost analysis above and understand well the relationship between hyperparameters and cost. Our cost model will simply scale linearly with both the batch size and the number of iterations: \n", + "\n", + "$$Cost\\big(\\textit{learning_rate}, \\textit{batch_size}, \\textit{num_iters}\\big) \\propto \\textit{batch_size} \\times \\textit{num_iters} $$ " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ae38594f", + "metadata": {}, + "outputs": [], + "source": [ + "# Assume x0 is learning rate, x1 is batch_size, x2 is iterations\n", + "bounds = torch.tensor([[0.05, 40, 10], [1, 1000, 400]], **tkwargs)\n", + "\n", + "\n", + "def objective(x):\n", + " learning_rate = x[0]\n", + " batch_size = int(x[1])\n", + " num_iters = int(x[2])\n", + " model = NoisyLinearLeastSquares(A, b, batch_size=batch_size)\n", + " t_start = time.time()\n", + " x, loss = model.fit(lr=learning_rate, niters=num_iters)\n", + " cost = time.time() - t_start\n", + " return loss.item(), cost\n", + "\n", + "\n", + "# Simplified cost model based on analysis above\n", + "class LinearCostModel(CostModel):\n", + " def __init__(self):\n", + " super().__init__()\n", + "\n", + " # Assume x1 is batch_size, x2 is iterations\n", + " def forward(self, X):\n", + " return X[:, :, 1] * X[:, :, 2]\n", + "\n", + "\n", + "def generate_initial_data(obj, bounds, num):\n", + " dim = bounds.shape[1]\n", + " train_x = draw_sobol_samples(bounds=bounds, n=num, q=1, seed=111).squeeze(1)\n", + " train_y = []\n", + " cost_y = []\n", + " for x in train_x:\n", + " y, c = obj(x)\n", + " train_y.append(y)\n", + " cost_y.append(c)\n", + " return (\n", + " train_x,\n", + " torch.tensor(train_y, **tkwargs).unsqueeze(-1),\n", + " torch.tensor(cost_y, **tkwargs).unsqueeze(-1),\n", + " )\n", + "\n", + "\n", + "# Generate initial data\n", + "budget = 25\n", + "num_initial = 5\n", + "init_X, init_Y, init_C = generate_initial_data(objective, bounds, num_initial)\n" + ] + }, + { + "cell_type": "markdown", + "id": "45e64dbd", + "metadata": {}, + "source": [ + "# Run Bayesian optimization with EIpu" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "488fdef7", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "train_X = init_X\n", + "train_Y = init_Y\n", + "cost_Y = init_C\n", + "\n", + "for i in range(budget):\n", + " alpha = (budget - i - 1) / (budget - 1)\n", + "\n", + " # Train GP\n", + " train_Y_flip = -1 * standardize(train_Y) # we want to minimize so we negate\n", + " gp = SingleTaskGP(train_X=train_X, train_Y=train_Y_flip)\n", + " mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " # Train Cost Model\n", + " cost_model = LinearCostModel()\n", + " fmax = torch.max(train_Y_flip)\n", + " eipu = ExpectedImprovementWithCost(\n", + " model=gp,\n", + " best_f=fmax,\n", + " cost_model=cost_model,\n", + " alpha=alpha,\n", + " )\n", + " new_x, acq_value = optimize_acqf(\n", + " acq_function=eipu,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=1024,\n", + " )\n", + "\n", + " # Get objective value and cost\n", + " new_y, cost_y = objective(new_x.squeeze())\n", + "\n", + " # update training points\n", + " train_X = torch.cat([train_X, new_x])\n", + " train_Y = torch.cat([train_Y, torch.tensor([new_y], **tkwargs).unsqueeze(1)])\n", + " cost_Y = torch.cat([cost_Y, torch.tensor([cost_y], **tkwargs).unsqueeze(1)])\n", + "\n", + "costs_eipu = cost_Y[:, 0]\n", + "results_ei_cost, _ = torch.cummin(train_Y, dim=0)\n", + "times_ei_cost = torch.cumsum(costs_eipu, dim=0)\n" + ] + }, + { + "cell_type": "markdown", + "id": "1a06bd79", + "metadata": {}, + "source": [ + "# Run Bayesian optimization with EI" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e3e6d5cd", + "metadata": {}, + "outputs": [], + "source": [ + "train_X = init_X\n", + "train_Y = init_Y\n", + "cost_Y = init_C\n", + "\n", + "for i in range(budget):\n", + " # Train GP\n", + " train_Y_flip = -1 * standardize(train_Y) # we want to minimize so we negate\n", + " gp = SingleTaskGP(train_X=train_X, train_Y=train_Y_flip)\n", + " mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp)\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " # Train Cost Model\n", + " fmax = torch.max(train_Y_flip)\n", + " ei = ExpectedImprovement(gp, fmax)\n", + " new_x, acq_value = optimize_acqf(\n", + " acq_function=ei,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=1024,\n", + " )\n", + "\n", + " # Get objective value and cost\n", + " new_y, cost_y = objective(new_x.squeeze())\n", + "\n", + " # update training points\n", + " train_X = torch.cat([train_X, new_x])\n", + " train_Y = torch.cat([train_Y, torch.tensor([new_y], **tkwargs).unsqueeze(1)])\n", + " cost_Y = torch.cat([cost_Y, torch.tensor([cost_y], **tkwargs).unsqueeze(1)])\n", + "\n", + "costs_ei = cost_Y[:, 0]\n", + "results_ei, _ = torch.cummin(train_Y, dim=0)\n", + "times_ei = torch.cumsum(costs_ei, dim=0)\n" + ] + }, + { + "cell_type": "markdown", + "id": "6db897f5", + "metadata": {}, + "source": [ + "# Plotting Results\n", + "\n", + "Unlike the usual optimization progress plots, which measure performance by comparing loss to iterations, in the cost aware setting, we measure performance by comparing loss to cumulative training time. \n", + "\n", + "EIpu and EI take the same number of iterations, but we can see that EIpu takes less time to execute those iterations (and finds a better result). We've also plotted a histogram of the evaluation times on the right. We can see that because EI is not cost aware, it has a pretty even spread of evaluation costs, whereas EIpu evaluates many more cheap points. " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a9f10a98", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKMAAAGGCAYAAACno0IzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACXiklEQVR4nOzdeVxUVf8H8M+wDfsgyqqI4oLihksirpgLoplbpWmKZlqGmWtGuVtituiTj2k9mdqjPtqiVmYuuOCvxF1ScScXTAEVGQRlP78/bjMysuPM3GH4vF+v+5qZe8+c+z33DnDnyznnKoQQAkREREREREREREZgIXcARERERERERERUfTAZRURERERERERERsNkFBERERERERERGQ2TUUREREREREREZDRMRhERERERERERkdEwGUVEREREREREREbDZBQRERERERERERkNk1FERERERERERGQ0TEYREREREREREZHRMBlFRGSCRo8ejXr16skdBhERkcHUq1cPo0ePljsMs/fxxx/Dz88PlpaWCAwMlDucIg4cOACFQoEDBw7Isv958+ZBoVDIsu/ykPv4EBkKk1FEVG5r166FQqHA8ePH5Q7FLNy6dQvz5s1DXFyc3KEQERE9lbKuEUJCQtC8efOn3s+OHTswb968p66nuti9ezfeeecddOrUCWvWrMGiRYtKLDt69GgoFIpiF1tbWyNGrX8PHz7EvHnzTCahU9qxLrwwWUvmzEruAIiIqqtbt25h/vz5qFevXpH/VP7nP/9BQUGBPIEREREZwcWLF2FhUbH/je/YsQMrVqxgQqqc9u3bBwsLC6xevRo2NjZlllcqlfj666+LrLe0tDREeEbz8OFDzJ8/H4CUGC1s1qxZePfdd40az+uvv46ePXtqX1+9ehVz5szB+PHj0aVLF+36Bg0aICgoCI8ePSrX+SOqSpiMIiIyoMzMTDg4OFT4fdbW1gaIhoiIyHQolUq5Q6iwyv5dl0tKSgrs7OzKnciwsrLCK6+8YuCoTIuVlRWsrIz7tTg4OBjBwcHa18ePH8ecOXMQHBxc7PGv6j3TiIrDYXpEpHenTp1CWFgYnJ2d4ejoiB49euDw4cM6ZXJzczF//nw0atQItra2qFmzJjp37ow9e/ZoyyQlJWHMmDGoU6cOlEolvLy8MGDAAFy7dq3MGPbt24cuXbrAwcEBLi4u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SDbsGUVmKS8PyM0FLC2l6ZlMBpNRREQEwMLCAopSJjjMz883YjRERERExsVkFJmlVauAt94CXnoJ2LxZ7mgKUamkRyajiIiqta1bt+q8zs3NxalTp7Bu3TrMnz9fpqiIiIiIjIPJKCJj0vSMUquBggLAgiNliYiqowEDBhRZ98ILL6BZs2bYvHkzxo4dK0NURERERMbBb8Jk1oSQO4InODlJCaiCAuCfuygRERFpdOjQAXv37pU7DCIiIiKDYs8oMkulTMMhLwsLoG1bwNrahIMkIiI5PHr0CJ9//jlq164tdyhEREREBsVkFJGx9esndwRERCSzGjVq6ExgLoTAgwcPYG9vj/Xr18sYGREREZHhMRlFZs3khukREREBWLp0qU4yysLCAm5ubggKCkKNGjVkjIyIiIjI8JiMIrNk0iPghJDmi8rLA/iFg4ioWho9erTcIRARERHJRtYJzKOiovDMM8/AyckJ7u7uGDhwIC5evKhTJiQkBAqFQmd54403dMrcuHED/fr1g729Pdzd3TFjxgzk5eUZsylkYvz8gCFDgKAguSMpxp9/Ap9+CmzfLnckRERkRKdPny73UhEHDx5E//794e3tDYVCgW3btulsHz16dJFrqT59+uixZUREREQVI2vPqJiYGEREROCZZ55BXl4e3nvvPfTu3Rvnzp2Dg4ODtty4ceOwYMEC7Wt7e3vt8/z8fPTr1w+enp44dOgQbt++jVGjRsHa2hqLFi0yanvIdPTpIy0mycVFekxLkzMKIiIyssDAQCgUCogyxpArFArk5+eXu97MzEy0atUKr776KgYPHlxsmT59+mDNmjXa10qlstz1ExEREembrMmonTt36rxeu3Yt3N3dceLECXTt2lW73t7eHp6ensXWsXv3bpw7dw7R0dHw8PBAYGAgFi5ciJkzZ2LevHmwsbExaBuIKkylkh7VamnInkmPKSQiIn25evWqQeoNCwtDWFhYqWWUSmWJ11JERERExibrML0nqdVqAICrq6vO+g0bNqBWrVpo3rw5IiMj8fDhQ+222NhYtGjRAh4eHtp1oaGhSE9PR3x8vHECJ6oIZ2cpAZWXJ80dRURE1YKvr2+5F307cOAA3N3d4e/vjwkTJuDevXt63wcRERFReZnMBOYFBQWYPHkyOnXqhObNm2vXDx8+HL6+vvD29sbp06cxc+ZMXLx4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zVevXi329/6T11Sl1WkK9N3moUOHCi8vL2FjYyNq164thg4dKq5cuWLEFpWtIm329fUtts1z587VljG3n+ey2lwVfp4r0t73339fNGzYUNja2ooaNWqI4OBgsWnTJp36zO0cl9XmqnCOhaj436nCiktG6es8K4QQomJ9qYiIiIiIiIiIiCqHc0YREREREREREZHRMBlFRERERERERERGw2QUEREREREREREZDZNRRERERERERERkNExGERERERERERGR0TAZRURERERERERERsNkFBERERERERERGQ2TUUREREREREREZDRMRhFRhSkUCmzbts1k6jGma9euQaFQIC4urtzvmTdvHgIDAw0WU1WJY/bs2Rg/fny5yr777rt46623DBwRERGReajM9UllrV27Fi4uLgbfT1lCQkIwefJkucMgokpiMorIBCUlJeGtt96Cn58flEolfHx80L9/f+zdu1fu0CqlpCTI7du3ERYWZpB9HjhwAAqFotTlwIEDFa7Xx8cHt2/fRvPmzcv9nunTpxv83NWrV6/Uto4ePdoocZQkKSkJ//rXv/D++++Xq/z06dOxbt06/PXXXwaOjIiIyLBGjx5d7N/mPn36yB1amerVq4dly5bprBv6/+3de1BU5RsH8O9ys4VluYv3RblFuN7TXMQbFoQSIKEDa2Ca4iSDIRhjQoqZNgPeczB1gjRxmUlIGxNCBEExFEXAYUMgkFQaMDFbVOTy/v5w9vw4sIAit5rnM3NmeN/znnefc84fPr7nPe9ZuhS3bt3qs99UD6x1tSUkJCA5ORlffPFFn8VBCOlbOgMdACGEr6qqCk5OTjA2NkZMTAykUimampqQlpaGtWvX4rfffhvoEHvNsGHD+qxvmUyGmpoarrxu3To8evQI8fHxXJ2pqSn397Nnz6Cnp9dtv9ra2i8dt0gkgkgkeqljXtbVq1fR0tICAMjNzYWPjw9KS0shFosBAEKhsF/i6MyRI0cgk8kgkUheqL25uTlcXV0RFxeHmJiYPo6OEEII6Vtubm68HAQAhgwZMkDRvBqhUAihUNhn/asf/KnFxsYiNTUV586d4+qMjIz6NAZCSN+jmVGEDDIff/wxBAIBrly5Ah8fH9jZ2cHR0RHr16/Hr7/+CkDzVOyHDx/yZvuoZwalpaVh8uTJEAqFmD9/Pmpra3H27Fk4ODhALBbD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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", + "\n", + "axes[0].plot(times_ei_cost, results_ei_cost, \"--b\", marker=\"^\")\n", + "axes[0].plot(times_ei, results_ei, \"--r\", marker=\"v\", alpha=0.5)\n", + "axes[0].set_xlabel(\"Cumulative Training Time (s)\")\n", + "axes[0].set_ylabel(\"Loss\")\n", + "axes[0].set_title(\"Loss over time\")\n", + "axes[0].legend([\"EIpu\", \"EI\"])\n", + "\n", + "axes[1].hist(costs_eipu, bins=20, color=\"b\")\n", + "axes[1].hist(costs_ei, bins=20, color=\"r\", alpha=0.5)\n", + "axes[1].set_xlabel(\"Evaluation Time\")\n", + "axes[1].set_ylabel(\"Number of Evaluations\")\n", + "axes[1].set_title(\"Histogram of Evaluation Times\")\n", + "axes[1].legend([\"EIpu\", \"EI\"])\n", + "\n", + "plt.tight_layout()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a0cef29", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/website-old/static/files/cost_aware_bayesian_optimization.py b/website-old/static/files/cost_aware_bayesian_optimization.py new file mode 100644 index 0000000000..a0486947b1 --- /dev/null +++ b/website-old/static/files/cost_aware_bayesian_optimization.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# # Cost-aware Bayesian Optimization +# +# This tutorial covers cost-aware Bayesian optimization, a situation in which the cost of evaluation is unknown but assumed to depend on the set or a subset of the optimization parameters. +# +# Note that cost-aware Bayesian optimization is a more general form of multifidelity Bayesian optimization: +# * In multi-fidelity Bayesian optimization, the fidelity parameters are typically known ahead of time, an the relationship between cost and performance is typically known e.g., the highest fidelity parameters are the least noisy and the most costly. +# * In cost-aware Bayesian optimization, we do not know a-priori which parameters dictate cost, nor do we make any assumptions about the relationship between cost and performance. +# +# Cost-aware Bayesian optimization is well suited to any problem for which the user suspects there to be a heterogenous cost of evaluation. It can also be used as a simpler alternative to multifidelity optimization, although we recommend a dedicated multifidelity algorithm for more experienced users. In this tutorial, the acquisition function we use for cost-aware Bayesian optimization is expected improvement per unit (EIpu), which has the following formula: +# +# $$ +# EIpu(x) = \frac{EI(x)}{c(x)^\alpha} +# $$ +# +# $c(x)$ is a cost model that predicts the evaluation cost and $\alpha \in [0, 1]$ is a decay factor that reduces or increases the cost model's effect to prevent cheap points from dominating the optimization routine. We recommend starting $\alpha$ at 1 and decreasing it to 0 as the optimization budget is exhausted. +# +# [1]: [Lee, Eric Hans, et al. Cost-aware Bayesian Optimization. International Conference on Machine Learning, AutoML Workshop. 2020. ](https://arxiv.org/pdf/2003.10870.pdf) + +# In[1]: + + +import os +import time +import torch +import warnings + +from abc import ABC, abstractmethod + +from botorch.acquisition import AnalyticAcquisitionFunction, ExpectedImprovement +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.models.transforms import Log +from botorch.optim import optimize_acqf +from botorch.test_functions import Ackley +from botorch.utils import standardize +from botorch.utils.sampling import draw_sobol_samples + +from gpytorch.mlls import ExactMarginalLogLikelihood + +import matplotlib.pyplot as plt +get_ipython().run_line_magic('matplotlib', 'inline') + +warnings.filterwarnings("ignore") +device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu") +tkwargs = { + "device": device, + "dtype": torch.double, +} +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# # Cost Modeling +# +# The first thing we do in this tutorial is define a simple cost model, in which we make no assumptions other than a positive cost. We will use the mean of a GP for the cost model. To enforce positivity, we will model the log cost and then exponentiate when we perform predictions. Users can use more bespoke cost models should they have a better understanding of their problem. +# +# Having defined the cost model, we'll also generate some simple plots of a 1D synthetic problem for illustrative purposes, where the objective is the Ackley function and the cost is quadratic. + +# In[2]: + + +class CostModel(torch.nn.Module, ABC): + """ + Simple abstract class for a cost model. + """ + + @abstractmethod + def forward(self, X): + pass + + +class CostModelGP(CostModel): + """ + A basic cost model that assumes the cost is positive. + It models the log cost to guarantee positive cost predictions. + """ + + def __init__(self, X, Y_cost): + assert torch.all(Y_cost > 0) + super().__init__() + gp = SingleTaskGP(train_X=X, train_Y=Y_cost, outcome_transform=Log()) + mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp) + fit_gpytorch_mll(mll) + self.gp = gp + + def forward(self, X): + return torch.exp(self.gp(X).mean) + + +# In[3]: + + +def synthetic_objective_with_cost(x): + dim = 1 + f = Ackley(dim) # synthetic objective is the Ackley + fx = f(x).unsqueeze(1) + cx = 200 * (1.1 - x) ** 2 # synthetic cost is quadratric + return fx, cx + + +# Generate training data +dim = 1 +num = 4 +bounds = torch.tensor([[0] * dim, [1] * dim], **tkwargs) +train_X = draw_sobol_samples(bounds=bounds, n=num, q=1, seed=111).squeeze(1) +train_Y, cost_Y = synthetic_objective_with_cost(train_X) + +# Fit GP to data +train_Y = standardize(train_Y) +gp = SingleTaskGP(train_X=train_X, train_Y=train_Y) +mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp) +fit_gpytorch_mll(mll) + +# Fit cost model to data +cost_model_gp = CostModelGP(train_X, cost_Y) + + +# # Plot the GP and the Cost Model + +# In[4]: + + +fig, axes = plt.subplots(1, 2, figsize=(10, 5)) + +# Plot GP +X_preds = torch.linspace(0, 1, 100, **tkwargs).unsqueeze(1) +Y_preds = gp.posterior(X_preds) +Y_mean = Y_preds.mean.squeeze().detach().numpy() +Y_var = Y_preds.variance.squeeze().detach().numpy() +axes[0].plot(X_preds, Y_preds.mean.detach().numpy(), "r") +axes[0].plot(train_X, train_Y, "k^") +axes[0].fill_between( + X_preds.numpy()[:, 0], Y_mean - Y_var, Y_mean + Y_var, color="m", alpha=0.5 +) +axes[0].set_title("Gaussian Process Model") +axes[0].set_ylabel("Objective Value") + +# Plot Cost Model +cost_preds = cost_model_gp(X_preds) +axes[1].plot(X_preds, cost_preds.detach().numpy()) +axes[1].plot(train_X, cost_Y, "kv") +axes[1].set_title("Cost Model") +axes[1].set_ylabel("Cost of Evaluation") + + +# # Expected Improvement Per Unit +# +# Having defined the cost model, we can now define our EIpu acquisition function and plot it for different values of $\alpha$. Note that when $\alpha=0$, EIpu simply reduces to EI. + +# In[5]: + + +class ExpectedImprovementWithCost(AnalyticAcquisitionFunction): + """ + This is the acquisition function EI(x) / c(x) ^ alpha, where alpha is a decay + factor that reduces or increases the emphasis of the cost model c(x). + """ + + def __init__(self, model, best_f, cost_model, alpha=1): + super().__init__(model=model) + self.model = model + self.cost_model = cost_model + self.ei = ExpectedImprovement(model=model, best_f=best_f) + self.alpha = alpha + + def forward(self, X): + return self.ei(X) / torch.pow(self.cost_model(X)[:, 0], self.alpha) + + +# In[6]: + + +fig, axes = plt.subplots(1, 3, figsize=(15, 5)) +X_preds = torch.linspace(0, 1, 100, **tkwargs).unsqueeze(1) +X_batch = X_preds.unsqueeze(1) + + +def normalize_acquisition_values(values): + max_value = values.max().item() + min_value = values.min().item() + return (values - min_value) / (max_value - min_value) + + +# Compute EI +fmax = torch.max(train_Y) +ei = ExpectedImprovement(model=gp, best_f=fmax) +ei_values = normalize_acquisition_values(ei(X_batch)) + +# Compute and plot EIpu vs EI +fig.suptitle("EIpu (green) vs EI (blue)") +for i in range(3): + alpha = 1 - i / 2 + eipu = ExpectedImprovementWithCost( + model=gp, + best_f=fmax, + cost_model=cost_model_gp, + alpha=alpha, + ) + eipu_values = normalize_acquisition_values(eipu(X_batch).squeeze()) + axes[i].plot(X_preds, eipu_values.detach().numpy(), "-g", linewidth=3) + axes[i].plot(X_preds, ei_values.detach().numpy(), "--b", alpha=1, linewidth=3) + axes[i].set_title(f"alpha={alpha}") + + +# # A Practial Problem +# +# To make things more interesting, let's look at the classic problem of least squares estimation: +# +# $$ +# \text{arg} \min_{x \in \mathbb{R}^d} \| Ax - b \|_2 +# $$ +# +# $A$ is a matrix of size $n \times d$ and $b$ is a vector of length $n$. Assuming that $n \geq d$, the solution to this problem is unique and has the following closed form: $(A^T A) ^{-1} (A^T b)$. The problem with explicitly computing this solution is that it will have an $\mathcal{O}(n^3)$ complexity due to the need to compute a Cholesky factorization of the matrix $A^T A$. +# +# +# These difficulties in computing an explicit solution when $n$ is large lead us to a cost-aware twist on the least squares estimation. An alternative solution is to perform batched gradient descent by sampling rows of $A$. Because the batching introduces noise, we'll use Adam to perform the optimization. This introduces hyperparameters such as the learning rate, batch size, and the number of optimization iterations. These hyperparameters influence the cost immensely, as we'll see in a bit. + +# In[7]: + + +class NoisyLinearLeastSquares: + """ + The standard linear least squares problem min_x ||Ax - b||_2. + We compute the loss via batching that introduces noise. + """ + + def __init__(self, A, b, batch_size=50): + self.A = A + self.b = b + self.batch_size = min(batch_size, self.A.shape[0]) + + def fit(self, lr=1, niters=100): + x = torch.zeros(A.shape[1], 1, requires_grad=True, **tkwargs) + optimizer = torch.optim.Adam([x], lr=lr) + batch_indices = torch.randperm(A.shape[1])[: self.batch_size] + for i in range(niters): + res = torch.matmul(self.A[batch_indices, :], x) - self.b[batch_indices] + loss = torch.norm(res) + optimizer.zero_grad() + loss.backward() + optimizer.step() + return x, loss + + +# # Cost Analysis +# +# Here, we examine the variation in runtime as we vary both the batch size and the number of Adam iterations. Perhaps unsurpsingly, the runtime varies significantly with these parameters. Though we expect the runtime to be stricly linear in both the batch size and the number of Adam iterations, we can see that in practice the graph is a little variance due to the nuances in which the computer executes the matrix operations. + +# In[8]: + + +fig, axes = plt.subplots(1, 2, figsize=(10, 5)) +n = 30000 if not SMOKE_TEST else 300 +d = 3000 if not SMOKE_TEST else 30 +A = torch.rand(n, d, **tkwargs) +b = torch.rand(n, 1, **tkwargs) + +# Timings varying batch size +batch_sizes = 100 * torch.arange(4, 10, device=device) +times_batch = [] +for batch_size in batch_sizes: + model = NoisyLinearLeastSquares(A, b, batch_size=batch_size) + t_start = time.time() + model.fit(lr=0.1, niters=200) + times_batch.append(time.time() - t_start) + +axes[0].set_title("Time vs Batch Size") +axes[0].set_xlabel("Batch Size") +axes[0].set_ylabel("Runtime (s)") +axes[0].plot(batch_sizes, times_batch, "b") + +# Timings varying number of Adam iterations +iter_count = 10 * torch.arange(1, 20, device=device) +times_iters = [] +for niters in iter_count: + model = NoisyLinearLeastSquares(A, b) + t_start = time.time() + model.fit(lr=0.1, niters=niters) + times_iters.append(time.time() - t_start) + +axes[1].set_title("Time vs Iterations") +axes[1].set_xlabel("Iteration Count") +axes[1].set_ylabel("Runtime (s)") +axes[1].plot(iter_count, times_iters, "g") + +plt.tight_layout() + + +# # Full Optimization Loop +# +# Having defined our problem, let's now run a full optimization loop and see how EIpu does compared to EI. Let's tune three hyperparameters in our least squares estimator: the learning rate, the batch size, and the number of adam iterations. +# +# * $ \textit{learning_rate} \in [0.05, 1.0]$ +# * $ \textit{batch_size} \in [40, 1000] $ +# * $\textit{num_iters} \in [10, 400]$. +# +# Previously, we mentioned that we can use bespoke cost models tailored to the specific problem to increase performance. Let's do this by replacing the generic GP cost model with a custom linear one. Note that we can only do this because we performed some cost analysis above and understand well the relationship between hyperparameters and cost. Our cost model will simply scale linearly with both the batch size and the number of iterations: +# +# $$Cost\big(\textit{learning_rate}, \textit{batch_size}, \textit{num_iters}\big) \propto \textit{batch_size} \times \textit{num_iters} $$ + +# In[9]: + + +# Assume x0 is learning rate, x1 is batch_size, x2 is iterations +bounds = torch.tensor([[0.05, 40, 10], [1, 1000, 400]], **tkwargs) + + +def objective(x): + learning_rate = x[0] + batch_size = int(x[1]) + num_iters = int(x[2]) + model = NoisyLinearLeastSquares(A, b, batch_size=batch_size) + t_start = time.time() + x, loss = model.fit(lr=learning_rate, niters=num_iters) + cost = time.time() - t_start + return loss.item(), cost + + +# Simplified cost model based on analysis above +class LinearCostModel(CostModel): + def __init__(self): + super().__init__() + + # Assume x1 is batch_size, x2 is iterations + def forward(self, X): + return X[:, :, 1] * X[:, :, 2] + + +def generate_initial_data(obj, bounds, num): + dim = bounds.shape[1] + train_x = draw_sobol_samples(bounds=bounds, n=num, q=1, seed=111).squeeze(1) + train_y = [] + cost_y = [] + for x in train_x: + y, c = obj(x) + train_y.append(y) + cost_y.append(c) + return ( + train_x, + torch.tensor(train_y, **tkwargs).unsqueeze(-1), + torch.tensor(cost_y, **tkwargs).unsqueeze(-1), + ) + + +# Generate initial data +budget = 25 +num_initial = 5 +init_X, init_Y, init_C = generate_initial_data(objective, bounds, num_initial) + + +# # Run Bayesian optimization with EIpu + +# In[10]: + + +train_X = init_X +train_Y = init_Y +cost_Y = init_C + +for i in range(budget): + alpha = (budget - i - 1) / (budget - 1) + + # Train GP + train_Y_flip = -1 * standardize(train_Y) # we want to minimize so we negate + gp = SingleTaskGP(train_X=train_X, train_Y=train_Y_flip) + mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp) + fit_gpytorch_mll(mll) + + # Train Cost Model + cost_model = LinearCostModel() + fmax = torch.max(train_Y_flip) + eipu = ExpectedImprovementWithCost( + model=gp, + best_f=fmax, + cost_model=cost_model, + alpha=alpha, + ) + new_x, acq_value = optimize_acqf( + acq_function=eipu, + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=1024, + ) + + # Get objective value and cost + new_y, cost_y = objective(new_x.squeeze()) + + # update training points + train_X = torch.cat([train_X, new_x]) + train_Y = torch.cat([train_Y, torch.tensor([new_y], **tkwargs).unsqueeze(1)]) + cost_Y = torch.cat([cost_Y, torch.tensor([cost_y], **tkwargs).unsqueeze(1)]) + +costs_eipu = cost_Y[:, 0] +results_ei_cost, _ = torch.cummin(train_Y, dim=0) +times_ei_cost = torch.cumsum(costs_eipu, dim=0) + + +# # Run Bayesian optimization with EI + +# In[11]: + + +train_X = init_X +train_Y = init_Y +cost_Y = init_C + +for i in range(budget): + # Train GP + train_Y_flip = -1 * standardize(train_Y) # we want to minimize so we negate + gp = SingleTaskGP(train_X=train_X, train_Y=train_Y_flip) + mll = ExactMarginalLogLikelihood(likelihood=gp.likelihood, model=gp) + fit_gpytorch_mll(mll) + + # Train Cost Model + fmax = torch.max(train_Y_flip) + ei = ExpectedImprovement(gp, fmax) + new_x, acq_value = optimize_acqf( + acq_function=ei, + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=1024, + ) + + # Get objective value and cost + new_y, cost_y = objective(new_x.squeeze()) + + # update training points + train_X = torch.cat([train_X, new_x]) + train_Y = torch.cat([train_Y, torch.tensor([new_y], **tkwargs).unsqueeze(1)]) + cost_Y = torch.cat([cost_Y, torch.tensor([cost_y], **tkwargs).unsqueeze(1)]) + +costs_ei = cost_Y[:, 0] +results_ei, _ = torch.cummin(train_Y, dim=0) +times_ei = torch.cumsum(costs_ei, dim=0) + + +# # Plotting Results +# +# Unlike the usual optimization progress plots, which measure performance by comparing loss to iterations, in the cost aware setting, we measure performance by comparing loss to cumulative training time. +# +# EIpu and EI take the same number of iterations, but we can see that EIpu takes less time to execute those iterations (and finds a better result). We've also plotted a histogram of the evaluation times on the right. We can see that because EI is not cost aware, it has a pretty even spread of evaluation costs, whereas EIpu evaluates many more cheap points. + +# In[16]: + + +fig, axes = plt.subplots(1, 2, figsize=(12, 4)) + +axes[0].plot(times_ei_cost, results_ei_cost, "--b", marker="^") +axes[0].plot(times_ei, results_ei, "--r", marker="v", alpha=0.5) +axes[0].set_xlabel("Cumulative Training Time (s)") +axes[0].set_ylabel("Loss") +axes[0].set_title("Loss over time") +axes[0].legend(["EIpu", "EI"]) + +axes[1].hist(costs_eipu, bins=20, color="b") +axes[1].hist(costs_ei, bins=20, color="r", alpha=0.5) +axes[1].set_xlabel("Evaluation Time") +axes[1].set_ylabel("Number of Evaluations") +axes[1].set_title("Histogram of Evaluation Times") +axes[1].legend(["EIpu", "EI"]) + +plt.tight_layout() + + +# In[ ]: + + + + diff --git a/website-old/static/files/custom_acquisition.ipynb b/website-old/static/files/custom_acquisition.ipynb new file mode 100644 index 0000000000..2cbf840cf8 --- /dev/null +++ b/website-old/static/files/custom_acquisition.ipynb @@ -0,0 +1,1178 @@ +{ + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "originalKey": "4c6694a4-a6f8-4fc6-a9a7-4a29617406cb", + "showInput": false, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "originalKey": "c901c723-b2f3-4f75-96c0-6555954209f5", + "showInput": false, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Upper Confidence Bound (UCB)\n", + "\n", + "The Upper Confidence Bound (UCB) acquisition function balances exploration and exploitation by assigning a score of $\\mu + \\sqrt{\\beta} \\cdot \\sigma$ if the posterior distribution is normal with mean $\\mu$ and variance $\\sigma^2$. This \"analytic\" version is implemented in the `UpperConfidenceBound` class. The Monte Carlo version of UCB is implemented in the `qUpperConfidenceBound` class, which also allows for q-batches of size greater than one. (The derivation of q-UCB is given in Appendix A of [Wilson et. al., 2017](https://arxiv.org/pdf/1712.00424.pdf)).\n", + "\n", + "### A scalarized version of q-UCB\n", + "\n", + "Suppose now that we are in a multi-output setting, where, e.g., we model the effects of a design on multiple metrics. We first show a simple extension of the q-UCB acquisition function that accepts a multi-output model and performs q-UCB on a scalarized version of the multiple outputs, achieved via a vector of weights. Implementing a new acquisition function in botorch is easy; one simply needs to implement the constructor and a `forward` method." + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + }, + "originalKey": "bdd02e32-c993-4bc7-966a-dfa520e243d3", + "requestMsgId": "2364e148-21c4-4037-998e-e4352a4f4929", + "customOutput": null, + "executionStartTime": 1668651266713, + "executionStopTime": 1668651266719 + }, + "source": [ + "import plotly.io as pio\n", + "\n", + "# Ax uses Plotly to produce interactive plots. These are great for viewing and analysis,\n", + "# though they also lead to large file sizes, which is not ideal for files living in GH.\n", + "# Changing the default to `png` strips the interactive components to get around this.\n", + "pio.renderers.default = \"png\"" + ], + "execution_count": 1, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "75503e22-bf3b-49d6-87a8-cb9c741e941e", + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "28d53a4f-be29-4ec6-8529-e1bdc80b5adf", + "executionStartTime": 1668651267039, + "executionStopTime": 1668651273452, + "customOutput": null + }, + "source": [ + "import math\n", + "from typing import Optional\n", + "\n", + "from botorch.acquisition.monte_carlo import MCAcquisitionFunction\n", + "from botorch.models.model import Model\n", + "from botorch.sampling.base import MCSampler\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "from botorch.utils import t_batch_mode_transform\n", + "from torch import Tensor\n", + "\n", + "\n", + "class qScalarizedUpperConfidenceBound(MCAcquisitionFunction):\n", + " def __init__(\n", + " self,\n", + " model: Model,\n", + " beta: Tensor,\n", + " weights: Tensor,\n", + " sampler: Optional[MCSampler] = None,\n", + " ) -> None:\n", + " # we use the AcquisitionFunction constructor, since that of\n", + " # MCAcquisitionFunction performs some validity checks that we don't want here\n", + " super(MCAcquisitionFunction, self).__init__(model=model)\n", + " if sampler is None:\n", + " sampler = SobolQMCNormalSampler(sample_shape=torch.Size([512]))\n", + " self.sampler = sampler\n", + " self.register_buffer(\"beta\", torch.as_tensor(beta))\n", + " self.register_buffer(\"weights\", torch.as_tensor(weights))\n", + "\n", + " @t_batch_mode_transform()\n", + " def forward(self, X: Tensor) -> Tensor:\n", + " \"\"\"Evaluate scalarized qUCB on the candidate set `X`.\n", + "\n", + " Args:\n", + " X: A `(b) x q x d`-dim Tensor of `(b)` t-batches with `q` `d`-dim\n", + " design points each.\n", + "\n", + " Returns:\n", + " Tensor: A `(b)`-dim Tensor of Upper Confidence Bound values at the\n", + " given design points `X`.\n", + " \"\"\"\n", + " posterior = self.model.posterior(X)\n", + " samples = self.get_posterior_samples(posterior) # n x b x q x o\n", + " scalarized_samples = samples.matmul(self.weights) # n x b x q\n", + " mean = posterior.mean # b x q x o\n", + " scalarized_mean = mean.matmul(self.weights) # b x q\n", + " ucb_samples = (\n", + " scalarized_mean\n", + " + math.sqrt(self.beta * math.pi / 2)\n", + " * (scalarized_samples - scalarized_mean).abs()\n", + " )\n", + " return ucb_samples.max(dim=-1)[0].mean(dim=0)" + ], + "execution_count": 2, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "I1116 181426.999 _utils_internal.py:179] NCCL_DEBUG env var is set to None\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "I1116 181427.000 _utils_internal.py:188] NCCL_DEBUG is INFO from /etc/nccl.conf\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/mpmath/ctx_mp_python.py:892: SyntaxWarning:\n\n\"is\" with a literal. Did you mean \"==\"?\n\n/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/mpmath/ctx_mp_python.py:986: SyntaxWarning:\n\n\"is\" with a literal. Did you mean \"==\"?\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/solvers/diophantine.py:3188: SyntaxWarning:\n\n\"is\" with a literal. Did you mean \"==\"?\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:520: SyntaxWarning:\n\n\"is\" with a literal. Did you mean \"==\"?\n\n/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:540: SyntaxWarning:\n\n\"is\" with a literal. Did you mean \"==\"?\n\n/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:553: SyntaxWarning:\n\n\"is\" with a literal. Did you mean \"==\"?\n\n/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/sympy/plotting/plot.py:560: SyntaxWarning:\n\n\"is\" with a literal. Did you mean \"==\"?\n\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "b1d4ee8e-69e1-4914-b113-473add84f322", + "showInput": false, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "Note that `qScalarizedUpperConfidenceBound` is very similar to `qUpperConfidenceBound` and only requires a few lines of new code to accomodate scalarization of multiple outputs. The `@t_batch_mode_transform` decorator ensures that the input `X` has an explicit t-batch dimension (code comments are added with shapes for clarity).\n", + "\n", + "See the end of this tutorial for a quick and easy way of achieving the same scalarization effect using `ScalarizedPosteriorTransform`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "7122ce31-f5ee-4fdc-8962-73f692eaaac0", + "showInput": false + }, + "source": [ + "#### Ad-hoc testing q-Scalarized-UCB\n", + "\n", + "Before hooking the newly defined acquisition function into a Bayesian Optimization loop, we should test it. For this we'll just make sure that it properly evaluates on a compatible multi-output model. Here we just define a basic multi-output `SingleTaskGP` model trained on synthetic data." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "4958d0f5-cce4-4fc8-8fa2-8b9d7fe2bd5d", + "collapsed": false, + "requestMsgId": "534c2e0c-f4c0-4bb0-bdec-e44f86c0ed2b", + "executionStartTime": 1668651273753, + "executionStopTime": 1668651274217, + "code_folding": [], + "hidden_ranges": [], + "customOutput": null + }, + "source": [ + "import torch\n", + "\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.utils import standardize\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "\n", + "# generate synthetic data\n", + "X = torch.rand(20, 2)\n", + "Y = torch.stack([torch.sin(X[:, 0]), torch.cos(X[:, 1])], -1)\n", + "Y = standardize(Y) # standardize to zero mean unit variance\n", + "\n", + "# construct and fit the multi-output model\n", + "gp = SingleTaskGP(X, Y)\n", + "mll = ExactMarginalLogLikelihood(gp.likelihood, gp)\n", + "fit_gpytorch_mll(mll)\n", + "\n", + "# construct the acquisition function\n", + "qSUCB = qScalarizedUpperConfidenceBound(gp, beta=0.1, weights=torch.tensor([0.1, 0.5]))" + ], + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "5070d96c-5673-4bbf-ab72-781d1eabd1bf", + "collapsed": false, + "requestMsgId": "1a5d87ac-e176-46dd-b7b9-fb29bfd14971", + "executionStartTime": 1668651274500, + "executionStopTime": 1668651274569, + "customOutput": null + }, + "source": [ + "# evaluate on single q-batch with q=3\n", + "qSUCB(torch.rand(3, 2))" + ], + "execution_count": 4, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "tensor([0.4412], grad_fn=)" + }, + "metadata": { + "bento_obj_id": "140146377780496" + }, + "execution_count": 4 + } + ] + }, + { + "cell_type": "code", + "metadata": { + "scrolled": true, + "originalKey": "b3e0a586-b362-4ab4-b4c1-7fc0c99a1be4", + "collapsed": false, + "requestMsgId": "4f73099f-0dd6-4494-9706-71f50e5dbccf", + "executionStartTime": 1668651274799, + "executionStopTime": 1668651274876, + "customOutput": null + }, + "source": [ + "# batch-evaluate on two q-batches with q=3\n", + "qSUCB(torch.rand(2, 3, 2))" + ], + "execution_count": 5, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "tensor([0.5129, 0.5216], grad_fn=)" + }, + "metadata": { + "bento_obj_id": "140146572704240" + }, + "execution_count": 5 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "originalKey": "aa45db76-36cb-42e9-9386-649d9ea5e741", + "showInput": false + }, + "source": [ + "### A scalarized version of analytic UCB (`q=1` only)\n", + "\n", + "We can also write an *analytic* version of UCB for a multi-output model, assuming a multivariate normal posterior and `q=1`. The new class `ScalarizedUpperConfidenceBound` subclasses `AnalyticAcquisitionFunction` instead of `MCAcquisitionFunction`. In contrast to the MC version, instead of using the weights on the MC samples, we directly scalarize the mean vector $\\mu$ and covariance matrix $\\Sigma$ and apply standard UCB on the univariate normal distribution, which has mean $w^T \\mu$ and variance $w^T \\Sigma w$. In addition to the `@t_batch_transform` decorator, here we are also using `expected_q=1` to ensure the input `X` has a `q=1`.\n", + "\n", + "*Note:* BoTorch also provides a `ScalarizedPosteriorTransform` abstraction that can be used with any existing analytic acqusition functions and automatically performs the scalarization we implement manually below. See the end of this tutorial for a usage example." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "5528fc1c-5cf1-4cab-9598-9a41e652d6ea", + "collapsed": false, + "requestMsgId": "22877611-7d10-4e11-8da5-17ae46e362b8", + "executionStartTime": 1668651275106, + "executionStopTime": 1668651275190, + "customOutput": null + }, + "source": [ + "from botorch.acquisition import AnalyticAcquisitionFunction\n", + "\n", + "\n", + "class ScalarizedUpperConfidenceBound(AnalyticAcquisitionFunction):\n", + " def __init__(\n", + " self,\n", + " model: Model,\n", + " beta: Tensor,\n", + " weights: Tensor,\n", + " maximize: bool = True,\n", + " ) -> None:\n", + " # we use the AcquisitionFunction constructor, since that of\n", + " # AnalyticAcquisitionFunction performs some validity checks that we don't want here\n", + " super(AnalyticAcquisitionFunction, self).__init__(model)\n", + " self.maximize = maximize\n", + " self.register_buffer(\"beta\", torch.as_tensor(beta))\n", + " self.register_buffer(\"weights\", torch.as_tensor(weights))\n", + "\n", + " @t_batch_mode_transform(expected_q=1)\n", + " def forward(self, X: Tensor) -> Tensor:\n", + " \"\"\"Evaluate the Upper Confidence Bound on the candidate set X using scalarization\n", + "\n", + " Args:\n", + " X: A `(b) x d`-dim Tensor of `(b)` t-batches of `d`-dim design\n", + " points each.\n", + "\n", + " Returns:\n", + " A `(b)`-dim Tensor of Upper Confidence Bound values at the given\n", + " design points `X`.\n", + " \"\"\"\n", + " self.beta = self.beta.to(X)\n", + " batch_shape = X.shape[:-2]\n", + " posterior = self.model.posterior(X)\n", + " means = posterior.mean.squeeze(dim=-2) # b x o\n", + " scalarized_mean = means.matmul(self.weights) # b\n", + " covs = posterior.mvn.covariance_matrix # b x o x o\n", + " weights = self.weights.view(\n", + " 1, -1, 1\n", + " ) # 1 x o x 1 (assume single batch dimension)\n", + " weights = weights.expand(batch_shape + weights.shape[1:]) # b x o x 1\n", + " weights_transpose = weights.permute(0, 2, 1) # b x 1 x o\n", + " scalarized_variance = torch.bmm(\n", + " weights_transpose, torch.bmm(covs, weights)\n", + " ).view(\n", + " batch_shape\n", + " ) # b\n", + " delta = (self.beta.expand_as(scalarized_mean) * scalarized_variance).sqrt()\n", + " if self.maximize:\n", + " return scalarized_mean + delta\n", + " else:\n", + " return scalarized_mean - delta" + ], + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "54811aec-bcad-4d24-8346-fa69c9e1d4c1", + "showInput": false + }, + "source": [ + "#### Ad-hoc testing Scalarized-UCB\n", + "\n", + "Notice that we pass in an explicit q-batch dimension for consistency, even though `q=1`." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "f7a98f13-7d87-4e87-afb8-a8ff087faeef", + "collapsed": false, + "requestMsgId": "99dedb82-13c3-45ea-8126-a2daa706a14a", + "executionStartTime": 1668651275410, + "executionStopTime": 1668651275490, + "customOutput": null + }, + "source": [ + "# construct the acquisition function\n", + "SUCB = ScalarizedUpperConfidenceBound(gp, beta=0.1, weights=torch.tensor([0.1, 0.5]))" + ], + "execution_count": 7, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "78de901c-d537-4b20-8ab2-f1161d12cca3", + "collapsed": false, + "requestMsgId": "af017973-596f-4bf5-9efe-54062c3c2715", + "executionStartTime": 1668651275716, + "executionStopTime": 1668651275791, + "customOutput": null + }, + "source": [ + "# evaluate on single point\n", + "SUCB(torch.rand(1, 2))" + ], + "execution_count": 8, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "tensor([0.5031], grad_fn=)" + }, + "metadata": { + "bento_obj_id": "140146378011088" + }, + "execution_count": 8 + } + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "3ffbd8b7-11e6-48d1-8d05-eddd3fc8221c", + "collapsed": false, + "requestMsgId": "79891745-83b5-459c-bfc7-d1683396b748", + "executionStartTime": 1668651276011, + "executionStopTime": 1668651276084, + "customOutput": null + }, + "source": [ + "# batch-evaluate on 3 points\n", + "SUCB(torch.rand(3, 1, 2))" + ], + "execution_count": 9, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "tensor([-0.6162, -0.8318, -0.1927], grad_fn=)" + }, + "metadata": { + "bento_obj_id": "140146378289936" + }, + "execution_count": 9 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "23509c96-6cab-4b3c-a8f6-ca87e607f642", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "## Using the custom acquisition function with Ax's Service API" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "636b7375-e46f-4bfb-bd38-724bcaae1b71", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Registering the new acquisition function\n", + "\n", + "In order to use an acquisition function, Ax needs to know how to generate inputs to construct the acquisition function." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "cd81abdf-050f-470c-be2a-432a52f911b5", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "94f7428a-6053-4fca-b902-3d2729c34559", + "executionStartTime": 1668651276301, + "executionStopTime": 1668651276386, + "customOutput": null + }, + "source": [ + "from typing import List\n", + "from typing import Any, Dict\n", + "\n", + "from botorch.acquisition.input_constructors import acqf_input_constructor\n", + "\n", + "\n", + "@acqf_input_constructor(ScalarizedUpperConfidenceBound)\n", + "def construct_inputs_scalarized_ucb(\n", + " model: Model,\n", + " beta: float,\n", + " weights: List[float],\n", + " posterior_transform: None,\n", + ") -> Dict[str, Any]:\n", + " return {\n", + " \"model\": model,\n", + " \"beta\": torch.as_tensor(beta, dtype=torch.double),\n", + " \"weights\": torch.as_tensor(weights, dtype=torch.double),\n", + " }" + ], + "execution_count": 10, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "71d0ade7-1341-4dda-b59e-b4f362d9991d", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Setting up a `GenerationStrategy` using `BOTORCH_MODULAR` with our custom acquistion function.\n", + "\n", + "`BOTORCH_MODULAR` is a convenient wrapper implemented in Ax that facilitates the use of custom BoTorch models and acquisition functions in Ax experiments. In order to customize the way the candidates are generated, we need to construct a new `GenerationStrategy` and pass it into the `AxClient`." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "80c4b078-9787-46da-ac78-a574cc73a17d", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "a4881c76-cd0f-48f0-9b5f-72f08ff15a21", + "executionStartTime": 1668651276695, + "executionStopTime": 1668651283047, + "customOutput": null + }, + "source": [ + "from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy\n", + "from ax.modelbridge.registry import Models\n", + "\n", + "\n", + "gs = GenerationStrategy(\n", + " steps=[\n", + " # Quasi-random initialization step\n", + " GenerationStep(\n", + " model=Models.SOBOL,\n", + " num_trials=5, # How many trials should be produced from this generation step\n", + " model_kwargs={\"seed\": 999}, # Any kwargs you want passed into the model\n", + " ),\n", + " # Bayesian optimization step using the custom acquisition function\n", + " GenerationStep(\n", + " model=Models.BOTORCH_MODULAR,\n", + " num_trials=-1, # No limitation on how many trials should be produced from this step\n", + " # For `BOTORCH_MODULAR`, we pass in kwargs to specify what surrogate or acquisition function to use.\n", + " # `acquisition_options` specifies the set of additional arguments to pass into the input constructor.\n", + " model_kwargs={\n", + " \"botorch_acqf_class\": ScalarizedUpperConfidenceBound,\n", + " \"acquisition_options\": {\"beta\": 0.1, \"weights\": [1.0, 1.0]},\n", + " },\n", + " ),\n", + " ]\n", + ")" + ], + "execution_count": 11, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "b7e98402-9d53-4d33-bfbf-230833edc121", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Setting up the experiment\n", + "\n", + "We will set up a simple experiment to optimize a simple scalarization of the BraninCurrin function (per the weights above). A detailed tutorial on Service API can be found [here](https://ax.dev/tutorials/gpei_hartmann_service.html).\n", + "\n", + "In order to use the `GenerationStrategy` we just created, we will pass it into the `AxClient`." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "e1e4f622-4678-4ae1-98a5-02c770c51f87", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "1a068448-2ed8-4090-8d3b-a2fa10ee42a5", + "executionStartTime": 1668651283379, + "executionStopTime": 1668651283560, + "customOutput": null + }, + "source": [ + "from ax.service.ax_client import AxClient\n", + "from ax.service.utils.instantiation import ObjectiveProperties\n", + "from botorch.test_functions import BraninCurrin\n", + "\n", + "\n", + "# Initialize the client - AxClient offers a convenient API to control the experiment\n", + "ax_client = AxClient(generation_strategy=gs)\n", + "# Setup the experiment\n", + "ax_client.create_experiment(\n", + " name=\"branincurrin_test_experiment\",\n", + " parameters=[\n", + " {\n", + " \"name\": f\"x{i+1}\",\n", + " \"type\": \"range\",\n", + " # It is crucial to use floats for the bounds, i.e., 0.0 rather than 0.\n", + " # Otherwise, the parameter would\n", + " \"bounds\": [0.0, 1.0],\n", + " }\n", + " for i in range(2)\n", + " ],\n", + " objectives={\n", + " \"branin\": ObjectiveProperties(minimize=True),\n", + " \"currin\": ObjectiveProperties(minimize=True),\n", + " },\n", + ")\n", + "# Setup a function to evaluate the trials\n", + "branincurrin = BraninCurrin()\n", + "\n", + "\n", + "def evaluate(parameters):\n", + " x = torch.tensor([[parameters.get(f\"x{i+1}\") for i in range(2)]])\n", + " bc_eval = branincurrin(x).squeeze().tolist()\n", + " # In our case, standard error is 0, since we are computing a synthetic function.\n", + " return {\"branin\": (bc_eval[0], 0.0), \"currin\": (bc_eval[1], 0.0)}" + ], + "execution_count": 12, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Starting optimization with verbose logging. To disable logging, set the `verbose_logging` argument to `False`. Note that float values in the logs are rounded to 6 decimal points.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.utils.instantiation: Due to non-specification, we will use the heuristic for selecting objective thresholds.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x1. If that is not the expected value type, you can explicity specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x2. If that is not the expected value type, you can explicity specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.utils.instantiation: Created search space: SearchSpace(parameters=[RangeParameter(name='x1', parameter_type=FLOAT, range=[0.0, 1.0]), RangeParameter(name='x2', parameter_type=FLOAT, range=[0.0, 1.0])], parameter_constraints=[]).\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ae279099-6f99-4d29-858b-d174d388e2d3", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Running the BO loop\n", + "\n", + "Ax makes this part super simple!" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "11c29a68-56a5-4d48-888b-04dd69cfb0af", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "050fef34-1c80-46c1-83f6-c245385b7f42", + "executionStartTime": 1668651283818, + "executionStopTime": 1668651290050, + "customOutput": null + }, + "source": [ + "for i in range(10):\n", + " parameters, trial_index = ax_client.get_next_trial()\n", + " # Local evaluation here can be replaced with deployment to external system.\n", + " ax_client.complete_trial(trial_index=trial_index, raw_data=evaluate(parameters))" + ], + "execution_count": 13, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 0 with parameters {'x1': 0.62873, 'x2': 0.51481}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 0 with data: {'branin': (46.244598, 0.0), 'currin': (6.842319, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 1 with parameters {'x1': 0.434883, 'x2': 0.396266}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 1 with data: {'branin': (14.735401, 0.0), 'currin': (8.740173, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 2 with parameters {'x1': 0.075645, 'x2': 0.934926}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 2 with data: {'branin': (2.808084, 0.0), 'currin': (4.10731, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 3 with parameters {'x1': 0.863245, 'x2': 0.038764}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 3 with data: {'branin': (9.956846, 0.0), 'currin': (10.342199, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Generated new trial 4 with parameters {'x1': 0.953918, 'x2': 0.808236}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:43] ax.service.ax_client: Completed trial 4 with data: {'branin': (95.420815, 0.0), 'currin': (4.715139, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:44] ax.service.ax_client: Generated new trial 5 with parameters {'x1': 1.0, 'x2': 0.343958}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:45] ax.service.ax_client: Completed trial 5 with data: {'branin': (6.593266, 0.0), 'currin': (7.800417, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:45] ax.service.ax_client: Generated new trial 6 with parameters {'x1': 0.545885, 'x2': 0.0}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:45] ax.service.ax_client: Completed trial 6 with data: {'branin': (5.420934, 0.0), 'currin': (11.428976, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:47] ax.service.ax_client: Generated new trial 7 with parameters {'x1': 0.123588, 'x2': 0.0}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:47] ax.service.ax_client: Completed trial 7 with data: {'branin': (151.344849, 0.0), 'currin': (12.426076, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:48] ax.service.ax_client: Generated new trial 8 with parameters {'x1': 0.045172, 'x2': 0.0}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:48] ax.service.ax_client: Completed trial 8 with data: {'branin': (240.222977, 0.0), 'currin': (7.477064, 0.0)}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:49] ax.service.ax_client: Generated new trial 9 with parameters {'x1': 0.120649, 'x2': 0.032781}.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:49] ax.service.ax_client: Completed trial 9 with data: {'branin': (142.032639, 0.0), 'currin': (12.317077, 0.0)}.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "0e35aa1a-766a-4c5e-924f-852db76c3139", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Viewing trials and plotting the Pareto frontier\n", + "\n", + "View the trials attached to the experiment." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "e3299fb3-d9f7-45e3-ba34-8b5b0c29dea1", + "showInput": true, + "customInput": null, + "collapsed": false, + "requestMsgId": "54a67064-79e2-454a-b3d9-18d0a4f23ac8", + "executionStopTime": 1668651290364, + "executionStartTime": 1668651290295, + "customOutput": null + }, + "source": [ + "ax_client.generation_strategy.trials_as_df" + ], + "execution_count": 14, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 11-16 18:14:50] ax.modelbridge.generation_strategy: Note that parameter values in dataframe are rounded to 2 decimal points; the values in the dataframe are thus not the exact ones suggested by Ax in trials.\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": " Generation Step ... Arm Parameterizations\n0 0 ... {'0_0': {'x1': 0.63, 'x2': 0.51}}\n1 0 ... {'1_0': {'x1': 0.43, 'x2': 0.4}}\n2 0 ... {'2_0': {'x1': 0.08, 'x2': 0.93}}\n3 0 ... {'3_0': {'x1': 0.86, 'x2': 0.04}}\n4 0 ... {'4_0': {'x1': 0.95, 'x2': 0.81}}\n5 1 ... {'5_0': {'x1': 1.0, 'x2': 0.34}}\n6 1 ... {'6_0': {'x1': 0.55, 'x2': 0.0}}\n7 1 ... {'7_0': {'x1': 0.12, 'x2': 0.0}}\n8 1 ... {'8_0': {'x1': 0.05, 'x2': 0.0}}\n9 1 ... {'9_0': {'x1': 0.12, 'x2': 0.03}}\n\n[10 rows x 5 columns]", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Generation StepGeneration ModelTrial IndexTrial StatusArm Parameterizations
00Sobol0COMPLETED{'0_0': {'x1': 0.63, 'x2': 0.51}}
10Sobol1COMPLETED{'1_0': {'x1': 0.43, 'x2': 0.4}}
20Sobol2COMPLETED{'2_0': {'x1': 0.08, 'x2': 0.93}}
30Sobol3COMPLETED{'3_0': {'x1': 0.86, 'x2': 0.04}}
40Sobol4COMPLETED{'4_0': {'x1': 0.95, 'x2': 0.81}}
51BoTorch5COMPLETED{'5_0': {'x1': 1.0, 'x2': 0.34}}
61BoTorch6COMPLETED{'6_0': {'x1': 0.55, 'x2': 0.0}}
71BoTorch7COMPLETED{'7_0': {'x1': 0.12, 'x2': 0.0}}
81BoTorch8COMPLETED{'8_0': {'x1': 0.05, 'x2': 0.0}}
91BoTorch9COMPLETED{'9_0': {'x1': 0.12, 'x2': 0.03}}
\n
", + "application/vnd.dataresource+json": { + "schema": { + "fields": [ + { + "name": "index", + "type": "integer" + }, + { + "name": "Generation Step", + "type": "integer" + }, + { + "name": "Generation Model", + "type": "string" + }, + { + "name": "Trial Index", + "type": "integer" + }, + { + "name": "Trial Status", + "type": "string" + }, + { + "name": "Arm Parameterizations", + "type": "string" + } + ], + "primaryKey": [ + "index" + ], + "pandas_version": "0.20.0" + }, + "data": [ + { + "index": 0, + "Generation Step": 0, + "Generation Model": "Sobol", + "Trial Index": 0, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "0_0": { + "x1": 0.63, + "x2": 0.51 + } + } + }, + { + "index": 1, + "Generation Step": 0, + "Generation Model": "Sobol", + "Trial Index": 1, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "1_0": { + "x1": 0.43, + "x2": 0.4 + } + } + }, + { + "index": 2, + "Generation Step": 0, + "Generation Model": "Sobol", + "Trial Index": 2, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "2_0": { + "x1": 0.08, + "x2": 0.93 + } + } + }, + { + "index": 3, + "Generation Step": 0, + "Generation Model": "Sobol", + "Trial Index": 3, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "3_0": { + "x1": 0.86, + "x2": 0.04 + } + } + }, + { + "index": 4, + "Generation Step": 0, + "Generation Model": "Sobol", + "Trial Index": 4, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "4_0": { + "x1": 0.95, + "x2": 0.81 + } + } + }, + { + "index": 5, + "Generation Step": 1, + "Generation Model": "BoTorch", + "Trial Index": 5, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "5_0": { + "x1": 1, + "x2": 0.34 + } + } + }, + { + "index": 6, + "Generation Step": 1, + "Generation Model": "BoTorch", + "Trial Index": 6, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "6_0": { + "x1": 0.55, + "x2": 0 + } + } + }, + { + "index": 7, + "Generation Step": 1, + "Generation Model": "BoTorch", + "Trial Index": 7, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "7_0": { + "x1": 0.12, + "x2": 0 + } + } + }, + { + "index": 8, + "Generation Step": 1, + "Generation Model": "BoTorch", + "Trial Index": 8, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "8_0": { + "x1": 0.05, + "x2": 0 + } + } + }, + { + "index": 9, + "Generation Step": 1, + "Generation Model": "BoTorch", + "Trial Index": 9, + "Trial Status": "COMPLETED", + "Arm Parameterizations": { + "9_0": { + "x1": 0.12, + "x2": 0.03 + } + } + } + ] + } + }, + "metadata": { + "bento_obj_id": "140146254574736" + }, + "execution_count": 14 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ca47b562-3c01-4557-9c76-a7143a978dae", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "Plot the Pareto frontier.\n", + "\n", + "Note that we do not expect a good coverage of the Pareto frontier since we use very small number of evaluations and our acquisition function naively optimizes the sum of the two objectives." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "5bbd1ba3-1f88-45d3-b4a9-c660a4ff9449", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "6357abd4-a4b5-4bcb-bf11-9d0532db6072", + "executionStopTime": 1668651301173, + "executionStartTime": 1668651290670, + "customOutput": null + }, + "source": [ + "from ax.plot.pareto_frontier import plot_pareto_frontier\n", + "from ax.plot.pareto_utils import compute_posterior_pareto_frontier\n", + "from ax.utils.notebook.plotting import render\n", + "\n", + "\n", + "objectives = ax_client.experiment.optimization_config.objective.objectives\n", + "frontier = compute_posterior_pareto_frontier(\n", + " experiment=ax_client.experiment,\n", + " data=ax_client.experiment.fetch_data(),\n", + " primary_objective=objectives[1].metric,\n", + " secondary_objective=objectives[0].metric,\n", + " absolute_metrics=[\"branin\", \"currin\"],\n", + ")\n", + "render(plot_pareto_frontier(frontier, CI_level=0.90))" + ], + "execution_count": 15, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "b9127c73-b1b6-4e46-b593-43068d5015a8", + "showInput": false, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Appendix: Using `ScalarizedPosteriorTransform`\n", + "\n", + "Using the `ScalarizedPosteriorTransform` abstraction, the functionality of `ScalarizedUpperConfidenceBound` implemented above can be easily achieved in just a few lines of code. `PosteriorTransform`s can be used with both the MC and analytic acquisition functions." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "79a4b36f-4b14-4a62-9dc6-883931ceb5d3", + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "490a2b3a-24f9-4b49-84f1-7a3851d5944f", + "executionStopTime": 1668651301442, + "executionStartTime": 1668651301431, + "customOutput": null + }, + "source": [ + "from botorch.acquisition.objective import ScalarizedPosteriorTransform\n", + "from botorch.acquisition.analytic import UpperConfidenceBound\n", + "\n", + "pt = ScalarizedPosteriorTransform(weights=torch.tensor([0.1, 0.5]))\n", + "SUCB = UpperConfidenceBound(gp, beta=0.1, posterior_transform=pt)" + ], + "execution_count": 16, + "outputs": [] + } + ] +} diff --git a/website-old/static/files/custom_acquisition.py b/website-old/static/files/custom_acquisition.py new file mode 100644 index 0000000000..76432405ca --- /dev/null +++ b/website-old/static/files/custom_acquisition.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# + +# ### Upper Confidence Bound (UCB) +# +# The Upper Confidence Bound (UCB) acquisition function balances exploration and exploitation by assigning a score of $\mu + \sqrt{\beta} \cdot \sigma$ if the posterior distribution is normal with mean $\mu$ and variance $\sigma^2$. This "analytic" version is implemented in the `UpperConfidenceBound` class. The Monte Carlo version of UCB is implemented in the `qUpperConfidenceBound` class, which also allows for q-batches of size greater than one. (The derivation of q-UCB is given in Appendix A of [Wilson et. al., 2017](https://arxiv.org/pdf/1712.00424.pdf)). +# +# ### A scalarized version of q-UCB +# +# Suppose now that we are in a multi-output setting, where, e.g., we model the effects of a design on multiple metrics. We first show a simple extension of the q-UCB acquisition function that accepts a multi-output model and performs q-UCB on a scalarized version of the multiple outputs, achieved via a vector of weights. Implementing a new acquisition function in botorch is easy; one simply needs to implement the constructor and a `forward` method. + +# In[1]: + + +import plotly.io as pio + +# Ax uses Plotly to produce interactive plots. These are great for viewing and analysis, +# though they also lead to large file sizes, which is not ideal for files living in GH. +# Changing the default to `png` strips the interactive components to get around this. +pio.renderers.default = "png" + + +# In[2]: + + +import math +from typing import Optional + +from botorch.acquisition.monte_carlo import MCAcquisitionFunction +from botorch.models.model import Model +from botorch.sampling.base import MCSampler +from botorch.sampling.normal import SobolQMCNormalSampler +from botorch.utils import t_batch_mode_transform +from torch import Tensor + + +class qScalarizedUpperConfidenceBound(MCAcquisitionFunction): + def __init__( + self, + model: Model, + beta: Tensor, + weights: Tensor, + sampler: Optional[MCSampler] = None, + ) -> None: + # we use the AcquisitionFunction constructor, since that of + # MCAcquisitionFunction performs some validity checks that we don't want here + super(MCAcquisitionFunction, self).__init__(model=model) + if sampler is None: + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([512])) + self.sampler = sampler + self.register_buffer("beta", torch.as_tensor(beta)) + self.register_buffer("weights", torch.as_tensor(weights)) + + @t_batch_mode_transform() + def forward(self, X: Tensor) -> Tensor: + """Evaluate scalarized qUCB on the candidate set `X`. + + Args: + X: A `(b) x q x d`-dim Tensor of `(b)` t-batches with `q` `d`-dim + design points each. + + Returns: + Tensor: A `(b)`-dim Tensor of Upper Confidence Bound values at the + given design points `X`. + """ + posterior = self.model.posterior(X) + samples = self.get_posterior_samples(posterior) # n x b x q x o + scalarized_samples = samples.matmul(self.weights) # n x b x q + mean = posterior.mean # b x q x o + scalarized_mean = mean.matmul(self.weights) # b x q + ucb_samples = ( + scalarized_mean + + math.sqrt(self.beta * math.pi / 2) + * (scalarized_samples - scalarized_mean).abs() + ) + return ucb_samples.max(dim=-1)[0].mean(dim=0) + + +# Note that `qScalarizedUpperConfidenceBound` is very similar to `qUpperConfidenceBound` and only requires a few lines of new code to accomodate scalarization of multiple outputs. The `@t_batch_mode_transform` decorator ensures that the input `X` has an explicit t-batch dimension (code comments are added with shapes for clarity). +# +# See the end of this tutorial for a quick and easy way of achieving the same scalarization effect using `ScalarizedPosteriorTransform`. + +# #### Ad-hoc testing q-Scalarized-UCB +# +# Before hooking the newly defined acquisition function into a Bayesian Optimization loop, we should test it. For this we'll just make sure that it properly evaluates on a compatible multi-output model. Here we just define a basic multi-output `SingleTaskGP` model trained on synthetic data. + +# In[3]: + + +import torch + +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.utils import standardize +from gpytorch.mlls import ExactMarginalLogLikelihood + + +# generate synthetic data +X = torch.rand(20, 2) +Y = torch.stack([torch.sin(X[:, 0]), torch.cos(X[:, 1])], -1) +Y = standardize(Y) # standardize to zero mean unit variance + +# construct and fit the multi-output model +gp = SingleTaskGP(X, Y) +mll = ExactMarginalLogLikelihood(gp.likelihood, gp) +fit_gpytorch_mll(mll) + +# construct the acquisition function +qSUCB = qScalarizedUpperConfidenceBound(gp, beta=0.1, weights=torch.tensor([0.1, 0.5])) + + +# In[4]: + + +# evaluate on single q-batch with q=3 +qSUCB(torch.rand(3, 2)) + + +# In[5]: + + +# batch-evaluate on two q-batches with q=3 +qSUCB(torch.rand(2, 3, 2)) + + +# ### A scalarized version of analytic UCB (`q=1` only) +# +# We can also write an *analytic* version of UCB for a multi-output model, assuming a multivariate normal posterior and `q=1`. The new class `ScalarizedUpperConfidenceBound` subclasses `AnalyticAcquisitionFunction` instead of `MCAcquisitionFunction`. In contrast to the MC version, instead of using the weights on the MC samples, we directly scalarize the mean vector $\mu$ and covariance matrix $\Sigma$ and apply standard UCB on the univariate normal distribution, which has mean $w^T \mu$ and variance $w^T \Sigma w$. In addition to the `@t_batch_transform` decorator, here we are also using `expected_q=1` to ensure the input `X` has a `q=1`. +# +# *Note:* BoTorch also provides a `ScalarizedPosteriorTransform` abstraction that can be used with any existing analytic acqusition functions and automatically performs the scalarization we implement manually below. See the end of this tutorial for a usage example. + +# In[6]: + + +from botorch.acquisition import AnalyticAcquisitionFunction + + +class ScalarizedUpperConfidenceBound(AnalyticAcquisitionFunction): + def __init__( + self, + model: Model, + beta: Tensor, + weights: Tensor, + maximize: bool = True, + ) -> None: + # we use the AcquisitionFunction constructor, since that of + # AnalyticAcquisitionFunction performs some validity checks that we don't want here + super(AnalyticAcquisitionFunction, self).__init__(model) + self.maximize = maximize + self.register_buffer("beta", torch.as_tensor(beta)) + self.register_buffer("weights", torch.as_tensor(weights)) + + @t_batch_mode_transform(expected_q=1) + def forward(self, X: Tensor) -> Tensor: + """Evaluate the Upper Confidence Bound on the candidate set X using scalarization + + Args: + X: A `(b) x d`-dim Tensor of `(b)` t-batches of `d`-dim design + points each. + + Returns: + A `(b)`-dim Tensor of Upper Confidence Bound values at the given + design points `X`. + """ + self.beta = self.beta.to(X) + batch_shape = X.shape[:-2] + posterior = self.model.posterior(X) + means = posterior.mean.squeeze(dim=-2) # b x o + scalarized_mean = means.matmul(self.weights) # b + covs = posterior.mvn.covariance_matrix # b x o x o + weights = self.weights.view( + 1, -1, 1 + ) # 1 x o x 1 (assume single batch dimension) + weights = weights.expand(batch_shape + weights.shape[1:]) # b x o x 1 + weights_transpose = weights.permute(0, 2, 1) # b x 1 x o + scalarized_variance = torch.bmm( + weights_transpose, torch.bmm(covs, weights) + ).view( + batch_shape + ) # b + delta = (self.beta.expand_as(scalarized_mean) * scalarized_variance).sqrt() + if self.maximize: + return scalarized_mean + delta + else: + return scalarized_mean - delta + + +# #### Ad-hoc testing Scalarized-UCB +# +# Notice that we pass in an explicit q-batch dimension for consistency, even though `q=1`. + +# In[7]: + + +# construct the acquisition function +SUCB = ScalarizedUpperConfidenceBound(gp, beta=0.1, weights=torch.tensor([0.1, 0.5])) + + +# In[8]: + + +# evaluate on single point +SUCB(torch.rand(1, 2)) + + +# In[9]: + + +# batch-evaluate on 3 points +SUCB(torch.rand(3, 1, 2)) + + +# ## Using the custom acquisition function with Ax's Service API + +# ### Registering the new acquisition function +# +# In order to use an acquisition function, Ax needs to know how to generate inputs to construct the acquisition function. + +# In[10]: + + +from typing import List +from typing import Any, Dict + +from botorch.acquisition.input_constructors import acqf_input_constructor + + +@acqf_input_constructor(ScalarizedUpperConfidenceBound) +def construct_inputs_scalarized_ucb( + model: Model, + beta: float, + weights: List[float], + posterior_transform: None, +) -> Dict[str, Any]: + return { + "model": model, + "beta": torch.as_tensor(beta, dtype=torch.double), + "weights": torch.as_tensor(weights, dtype=torch.double), + } + + +# ### Setting up a `GenerationStrategy` using `BOTORCH_MODULAR` with our custom acquistion function. +# +# `BOTORCH_MODULAR` is a convenient wrapper implemented in Ax that facilitates the use of custom BoTorch models and acquisition functions in Ax experiments. In order to customize the way the candidates are generated, we need to construct a new `GenerationStrategy` and pass it into the `AxClient`. + +# In[11]: + + +from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy +from ax.modelbridge.registry import Models + + +gs = GenerationStrategy( + steps=[ + # Quasi-random initialization step + GenerationStep( + model=Models.SOBOL, + num_trials=5, # How many trials should be produced from this generation step + model_kwargs={"seed": 999}, # Any kwargs you want passed into the model + ), + # Bayesian optimization step using the custom acquisition function + GenerationStep( + model=Models.BOTORCH_MODULAR, + num_trials=-1, # No limitation on how many trials should be produced from this step + # For `BOTORCH_MODULAR`, we pass in kwargs to specify what surrogate or acquisition function to use. + # `acquisition_options` specifies the set of additional arguments to pass into the input constructor. + model_kwargs={ + "botorch_acqf_class": ScalarizedUpperConfidenceBound, + "acquisition_options": {"beta": 0.1, "weights": [1.0, 1.0]}, + }, + ), + ] +) + + +# ### Setting up the experiment +# +# We will set up a simple experiment to optimize a simple scalarization of the BraninCurrin function (per the weights above). A detailed tutorial on Service API can be found [here](https://ax.dev/tutorials/gpei_hartmann_service.html). +# +# In order to use the `GenerationStrategy` we just created, we will pass it into the `AxClient`. + +# In[12]: + + +from ax.service.ax_client import AxClient +from ax.service.utils.instantiation import ObjectiveProperties +from botorch.test_functions import BraninCurrin + + +# Initialize the client - AxClient offers a convenient API to control the experiment +ax_client = AxClient(generation_strategy=gs) +# Setup the experiment +ax_client.create_experiment( + name="branincurrin_test_experiment", + parameters=[ + { + "name": f"x{i+1}", + "type": "range", + # It is crucial to use floats for the bounds, i.e., 0.0 rather than 0. + # Otherwise, the parameter would + "bounds": [0.0, 1.0], + } + for i in range(2) + ], + objectives={ + "branin": ObjectiveProperties(minimize=True), + "currin": ObjectiveProperties(minimize=True), + }, +) +# Setup a function to evaluate the trials +branincurrin = BraninCurrin() + + +def evaluate(parameters): + x = torch.tensor([[parameters.get(f"x{i+1}") for i in range(2)]]) + bc_eval = branincurrin(x).squeeze().tolist() + # In our case, standard error is 0, since we are computing a synthetic function. + return {"branin": (bc_eval[0], 0.0), "currin": (bc_eval[1], 0.0)} + + +# ### Running the BO loop +# +# Ax makes this part super simple! + +# In[13]: + + +for i in range(10): + parameters, trial_index = ax_client.get_next_trial() + # Local evaluation here can be replaced with deployment to external system. + ax_client.complete_trial(trial_index=trial_index, raw_data=evaluate(parameters)) + + +# ### Viewing trials and plotting the Pareto frontier +# +# View the trials attached to the experiment. + +# In[14]: + + +ax_client.generation_strategy.trials_as_df + + +# Plot the Pareto frontier. +# +# Note that we do not expect a good coverage of the Pareto frontier since we use very small number of evaluations and our acquisition function naively optimizes the sum of the two objectives. + +# In[15]: + + +from ax.plot.pareto_frontier import plot_pareto_frontier +from ax.plot.pareto_utils import compute_posterior_pareto_frontier +from ax.utils.notebook.plotting import render + + +objectives = ax_client.experiment.optimization_config.objective.objectives +frontier = compute_posterior_pareto_frontier( + experiment=ax_client.experiment, + data=ax_client.experiment.fetch_data(), + primary_objective=objectives[1].metric, + secondary_objective=objectives[0].metric, + absolute_metrics=["branin", "currin"], +) +render(plot_pareto_frontier(frontier, CI_level=0.90)) + + +# ### Appendix: Using `ScalarizedPosteriorTransform` +# +# Using the `ScalarizedPosteriorTransform` abstraction, the functionality of `ScalarizedUpperConfidenceBound` implemented above can be easily achieved in just a few lines of code. `PosteriorTransform`s can be used with both the MC and analytic acquisition functions. + +# In[16]: + + +from botorch.acquisition.objective import ScalarizedPosteriorTransform +from botorch.acquisition.analytic import UpperConfidenceBound + +pt = ScalarizedPosteriorTransform(weights=torch.tensor([0.1, 0.5])) +SUCB = UpperConfidenceBound(gp, beta=0.1, posterior_transform=pt) + diff --git a/website-old/static/files/custom_botorch_model_in_ax.ipynb b/website-old/static/files/custom_botorch_model_in_ax.ipynb new file mode 100644 index 0000000000..0a6b23a04f --- /dev/null +++ b/website-old/static/files/custom_botorch_model_in_ax.ipynb @@ -0,0 +1,2488 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "8760cbbb-0419-4ddd-b16d-360a0f8efb23", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "## Using a custom BoTorch model with Ax\n", + "\n", + "In this tutorial, we illustrate how to use a custom BoTorch model within Ax's `botorch_modular` API. This allows us to harness the convenience of Ax for running Bayesian Optimization loops while maintaining full flexibility in modeling.\n", + "\n", + "Acquisition functions and their optimizers can be swapped out in much the same fashion. See for example the tutorial for [Implementing a custom acquisition function](./custom_acquisition).\n", + "\n", + "If you want to do something non-standard, or would like to have full insight into every aspect of the implementation, please see [this tutorial](./closed_loop_botorch_only) for how to write your own full optimization loop in BoTorch.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996758749, + "executionStopTime": 1730996764518, + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "e93e2f29-61fe-4f9f-b7bd-b742e2fe9344", + "output": { + "id": "469571788973641" + }, + "outputsInitialized": true, + "requestMsgId": "e93e2f29-61fe-4f9f-b7bd-b742e2fe9344", + "serverExecutionDuration": 3997.7363101207, + "showInput": true + }, + "outputs": [], + "source": [ + "import os\n", + "from contextlib import contextmanager, nullcontext\n", + "\n", + "import plotly.io as pio\n", + "\n", + "from ax.utils.testing.mock import mock_botorch_optimize_context_manager\n", + "\n", + "# Ax uses Plotly to produce interactive plots. These are great for viewing and analysis,\n", + "# though they also lead to large file sizes, which is not ideal for files living in GH.\n", + "# Changing the default to `png` strips the interactive components to get around this.\n", + "pio.renderers.default = \"png\"\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n", + "NUM_EVALS = 10 if SMOKE_TEST else 30" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "007adb71-ee1b-407c-9a61-ff948c104fce", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Implementing the custom model\n", + "\n", + "For this tutorial, we implement a very simple GPyTorch `ExactGP` model that uses an RBF kernel (with ARD) and infers a homoskedastic noise level.\n", + "\n", + "Model definition is straightforward. Here we implement a GPyTorch `ExactGP` that inherits from `GPyTorchModel`; together these two superclasses add all the API calls that BoTorch expects in its various modules. \n", + "\n", + "*Note:* BoTorch allows implementing any custom model that follows the `Model` API. For more information, please see the [Model Documentation](../docs/models)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "code_folding": [], + "collapsed": false, + "executionStartTime": 1730996759581, + "executionStopTime": 1730996764532, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "5aa02924-4c3d-4654-a81e-ef58084337d3", + "outputsInitialized": true, + "requestMsgId": "5aa02924-4c3d-4654-a81e-ef58084337d3", + "serverExecutionDuration": 2.7337353676558 + }, + "outputs": [], + "source": [ + "from typing import Optional\n", + "\n", + "from botorch.models.gpytorch import GPyTorchModel\n", + "from gpytorch.distributions import MultivariateNormal\n", + "from gpytorch.kernels import RBFKernel, ScaleKernel\n", + "from gpytorch.likelihoods import GaussianLikelihood\n", + "from gpytorch.means import ConstantMean\n", + "from gpytorch.models import ExactGP\n", + "from torch import Tensor\n", + "\n", + "\n", + "class SimpleCustomGP(ExactGP, GPyTorchModel):\n", + "\n", + " _num_outputs = 1 # to inform GPyTorchModel API\n", + "\n", + " def __init__(self, train_X, train_Y, train_Yvar: Optional[Tensor] = None):\n", + " # NOTE: This ignores train_Yvar and uses inferred noise instead.\n", + " # squeeze output dim before passing train_Y to ExactGP\n", + " super().__init__(train_X, train_Y.squeeze(-1), GaussianLikelihood())\n", + " self.mean_module = ConstantMean()\n", + " self.covar_module = ScaleKernel(\n", + " base_kernel=RBFKernel(ard_num_dims=train_X.shape[-1]),\n", + " )\n", + " self.to(train_X) # make sure we're on the right device/dtype\n", + "\n", + " def forward(self, x):\n", + " mean_x = self.mean_module(x)\n", + " covar_x = self.covar_module(x)\n", + " return MultivariateNormal(mean_x, covar_x)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "0f22b707-cec8-43a1-9152-503a1904fbd5", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Instantiate a `BoTorchModel` in Ax\n", + "\n", + "A `BoTorchModel` in Ax encapsulates both the surrogate -- which `Ax` calls a `Surrogate` and BoTorch calls a `Model` -- and an acquisition function. Here, we will only specify the custom surrogate and let Ax choose the default acquisition function.\n", + "\n", + "Most models should work with the base `Surrogate` in Ax, except for BoTorch `ModelListGP`, which works with `ListSurrogate`.\n", + "Note that the `Model` (e.g., the `SimpleCustomGP`) must implement `construct_inputs`, as this is used to construct the inputs required for instantiating a `Model` instance from the experiment data." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996760524, + "executionStopTime": 1730996764544, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "14a8c659-c142-4bd6-9a8e-29cfa78d7b98", + "outputsInitialized": true, + "requestMsgId": "14a8c659-c142-4bd6-9a8e-29cfa78d7b98", + "serverExecutionDuration": 2.4179229512811, + "showInput": true + }, + "outputs": [], + "source": [ + "from ax.models.torch.botorch_modular.model import BoTorchModel\n", + "from ax.models.torch.botorch_modular.surrogate import Surrogate, SurrogateSpec\n", + "from ax.models.torch.botorch_modular.utils import ModelConfig\n", + "\n", + "ax_model = BoTorchModel(\n", + " surrogate=Surrogate(\n", + " surrogate_spec=SurrogateSpec(\n", + " model_configs=[\n", + " ModelConfig(\n", + " # The model class to use\n", + " botorch_model_class=SimpleCustomGP,\n", + " # Optional, MLL class with which to optimize model parameters\n", + " # mll_class=ExactMarginalLogLikelihood,\n", + " # Optional, dictionary of keyword arguments to model constructor\n", + " # model_options={}\n", + " )\n", + " ]\n", + " )\n", + " ),\n", + " # Optional, acquisition function class to use - see custom acquisition tutorial\n", + " # botorch_acqf_class=qExpectedImprovement,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "4254a19d-cf71-4a88-a00f-608331cd9f54", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Combine with a `ModelBridge`\n", + "\n", + "`Model`s in Ax require a `ModelBridge` to interface with `Experiment`s. A `ModelBridge` takes the inputs supplied by the `Experiment` and converts them to the inputs expected by the `Model`. For a `BoTorchModel`, we use `TorchModelBridge`. The Modular BoTorch interface creates the `BoTorchModel` and the `TorchModelBridge` in a single step, as follows:\n", + "\n", + "```\n", + "from ax.modelbridge.registry import Models\n", + "model_bridge = Models.BOTORCH_MODULAR(\n", + " experiment=experiment,\n", + " data=data,\n", + " surrogate=Surrogate(SimpleCustomGP),\n", + " # Optional, will use default if unspecified\n", + " # botorch_acqf_class=qLogNoisyExpectedImprovement, \n", + ")\n", + "# To generate a trial\n", + "trial = model_bridge.gen(1)\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "40d38579-279d-49fd-b2aa-e8c1fcab2a61", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "# Using the custom model in Ax to optimize the Branin function\n", + "\n", + "We will demonstrate this with both the Service API (simpler, easier to use) and the Developer API (advanced, more customizable)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "f4553323-645d-40f9-b1d5-b549e5265eb9", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "## Optimization with Ax's Service API\n", + "\n", + "A detailed tutorial on the Service API can be found [here](https://ax.dev/tutorials/gpei_hartmann_service.html).\n", + "\n", + "In order to customize the way the candidates are created in the Service API, we need to construct a new `GenerationStrategy` and pass it into `AxClient`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996762310, + "executionStopTime": 1730996764558, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "4d444eb9-21a3-43fc-a855-8793290d60dc", + "outputsInitialized": true, + "requestMsgId": "4d444eb9-21a3-43fc-a855-8793290d60dc", + "serverExecutionDuration": 2.1021906286478, + "showInput": true + }, + "outputs": [], + "source": [ + "from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy\n", + "from ax.modelbridge.registry import Models\n", + "\n", + "\n", + "gs = GenerationStrategy(\n", + " steps=[\n", + " # Quasi-random initialization step\n", + " GenerationStep(\n", + " model=Models.SOBOL,\n", + " num_trials=5, # How many trials should be produced from this generation step\n", + " ),\n", + " # Bayesian optimization step using the custom acquisition function\n", + " GenerationStep(\n", + " model=Models.BOTORCH_MODULAR,\n", + " num_trials=-1, # No limitation on how many trials should be produced from this step\n", + " # For `BOTORCH_MODULAR`, we pass in kwargs to specify what surrogate or acquisition function to use.\n", + " model_kwargs={\n", + " \"surrogate_spec\": SurrogateSpec(model_configs=[ModelConfig(botorch_model_class=SimpleCustomGP)]),\n", + " },\n", + " ),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "24e0fd52-cf2f-4c5a-8130-82e3b133c7df", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Setting up the experiment\n", + "\n", + "In order to use the `GenerationStrategy` we just created, we will pass it into the `AxClient`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996763198, + "executionStopTime": 1730996765492, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "4ee5509a-7cd5-4e0f-adcc-43961562f5f3", + "output": { + "id": "8782682878442163" + }, + "outputsInitialized": true, + "requestMsgId": "4ee5509a-7cd5-4e0f-adcc-43961562f5f3", + "serverExecutionDuration": 701.53528917581, + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 12:52:13] ax.service.ax_client: Starting optimization with verbose logging. To disable logging, set the `verbose_logging` argument to `False`. Note that float values in the logs are rounded to 6 decimal points.\n", + "[INFO 11-07 12:52:13] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x1. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.\n", + "[INFO 11-07 12:52:13] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x2. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.\n", + "[INFO 11-07 12:52:13] ax.service.utils.instantiation: Created search space: SearchSpace(parameters=[RangeParameter(name='x1', parameter_type=FLOAT, range=[-5.0, 10.0]), RangeParameter(name='x2', parameter_type=FLOAT, range=[0.0, 15.0])], parameter_constraints=[]).\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x1. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.utils.instantiation: Inferred value type of ParameterType.FLOAT for parameter x2. If that is not the expected value type, you can explicitly specify 'value_type' ('int', 'float', 'bool' or 'str') in parameter dict.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.utils.instantiation: Created search space: SearchSpace(parameters=[RangeParameter(name='x1', parameter_type=FLOAT, range=[-5.0, 10.0]), RangeParameter(name='x2', parameter_type=FLOAT, range=[0.0, 15.0])], parameter_constraints=[]).\n" + ] + } + ], + "source": [ + "import torch\n", + "from ax.service.ax_client import AxClient\n", + "from ax.service.utils.instantiation import ObjectiveProperties\n", + "from botorch.test_functions import Branin\n", + "\n", + "\n", + "# Initialize the client - AxClient offers a convenient API to control the experiment\n", + "ax_client = AxClient(generation_strategy=gs)\n", + "# Setup the experiment\n", + "ax_client.create_experiment(\n", + " name=\"branin_test_experiment\",\n", + " parameters=[\n", + " {\n", + " \"name\": \"x1\",\n", + " \"type\": \"range\",\n", + " # It is crucial to use floats for the bounds, i.e., 0.0 rather than 0.\n", + " # Otherwise, the parameter would be inferred as an integer range.\n", + " \"bounds\": [-5.0, 10.0],\n", + " },\n", + " {\n", + " \"name\": \"x2\",\n", + " \"type\": \"range\",\n", + " \"bounds\": [0.0, 15.0],\n", + " },\n", + " ],\n", + " objectives={\n", + " \"branin\": ObjectiveProperties(minimize=True),\n", + " },\n", + ")\n", + "# Setup a function to evaluate the trials\n", + "branin = Branin()\n", + "\n", + "\n", + "def evaluate(parameters):\n", + " x = torch.tensor([[parameters.get(f\"x{i+1}\") for i in range(2)]])\n", + " # The GaussianLikelihood used by our model infers an observation noise level,\n", + " # so we pass an sem value of NaN to indicate that observation noise is unknown\n", + " return {\"branin\": (branin(x).item(), float(\"nan\"))}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "25ac5ba7-291b-466f-bed0-f8cb5f842605", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Running the BO loop" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "173daaa5-4a91-4294-aab8-305cada3efb4", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "The next cell sets up a decorator solely to speed up the testing of the notebook in `SMOKE_TEST` mode. You can safely ignore this cell and the use of the decorator throughout the tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false, + "executionStartTime": 1730996764634, + "executionStopTime": 1730996765512, + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "4cc92119-f4be-42d3-8a3c-c1518bd860dc", + "output": { + "id": "905203617909706" + }, + "outputsInitialized": true, + "requestMsgId": "4cc92119-f4be-42d3-8a3c-c1518bd860dc", + "serverExecutionDuration": 5.7004098780453 + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "if SMOKE_TEST:\n", + " fast_smoke_test = mock_botorch_optimize_context_manager\n", + "else:\n", + " fast_smoke_test = nullcontext\n", + "\n", + "# Set a seed for reproducible tutorial output\n", + "torch.manual_seed(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996765169, + "executionStopTime": 1730996789818, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "1f85e052-7e4f-4d0a-907d-e259fa70f902", + "output": { + "id": "1297036191745441" + }, + "outputsInitialized": true, + "requestMsgId": "1f85e052-7e4f-4d0a-907d-e259fa70f902", + "serverExecutionDuration": 24149.213106837, + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:\n", + "\n", + "Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.\n", + "\n", + "[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 0 with parameters {'x1': 0.62583, 'x2': 14.359564} using model Sobol.\n", + "[INFO 11-07 12:52:17] ax.service.ax_client: Completed trial 0 with data: {'branin': (104.365417, nan)}.\n", + "/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:\n", + "\n", + "Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.\n", + "\n", + "[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 1 with parameters {'x1': 3.166217, 'x2': 3.867106} using model Sobol.\n", + "[INFO 11-07 12:52:17] ax.service.ax_client: Completed trial 1 with data: {'branin': (2.996862, nan)}.\n", + "/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:\n", + "\n", + "Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.\n", + "\n", + "[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 2 with parameters {'x1': 9.560105, 'x2': 10.718323} using model Sobol.\n", + "[INFO 11-07 12:52:17] ax.service.ax_client: Completed trial 2 with data: {'branin': (66.530632, nan)}.\n", + "/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:\n", + "\n", + "Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.\n", + "\n", + "[INFO 11-07 12:52:17] ax.service.ax_client: Generated new trial 3 with parameters {'x1': -3.878664, 'x2': 0.117947} using model Sobol.\n", + "[INFO 11-07 12:52:18] ax.service.ax_client: Completed trial 3 with data: {'branin': (198.850861, nan)}.\n", + "/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/ax/modelbridge/cross_validation.py:464: UserWarning:\n", + "\n", + "Encountered exception in computing model fit quality: RandomModelBridge does not support prediction.\n", + "\n", + "[INFO 11-07 12:52:18] ax.service.ax_client: Generated new trial 4 with parameters {'x1': -2.362858, 'x2': 8.855021} using model Sobol.\n", + "[INFO 11-07 12:52:18] ax.service.ax_client: Completed trial 4 with data: {'branin': (5.811776, nan)}.\n", + "[INFO 11-07 12:52:21] ax.service.ax_client: Generated new trial 5 with parameters {'x1': 2.562464, 'x2': 4.925756} using model BoTorch.\n", + "[INFO 11-07 12:52:21] ax.service.ax_client: Completed trial 5 with data: {'branin': (6.61104, nan)}.\n", + "[INFO 11-07 12:52:21] ax.service.ax_client: Generated new trial 6 with parameters {'x1': 5.503428, 'x2': 4.951339} using model BoTorch.\n", + "[INFO 11-07 12:52:21] ax.service.ax_client: Completed trial 6 with data: {'branin': (31.249773, nan)}.\n", + "[INFO 11-07 12:52:22] ax.service.ax_client: Generated new trial 7 with parameters {'x1': -2.306809, 'x2': 4.436082} using model BoTorch.\n", + "[INFO 11-07 12:52:22] ax.service.ax_client: Completed trial 7 with data: {'branin': (38.632786, nan)}.\n", + "[INFO 11-07 12:52:22] ax.service.ax_client: Generated new trial 8 with parameters {'x1': -1.582296, 'x2': 7.318848} using model BoTorch.\n", + "[INFO 11-07 12:52:22] ax.service.ax_client: Completed trial 8 with data: {'branin': (12.208769, nan)}.\n", + "[INFO 11-07 12:52:23] ax.service.ax_client: Generated new trial 9 with parameters {'x1': -5.0, 'x2': 9.065641} using model BoTorch.\n", + "[INFO 11-07 12:52:23] ax.service.ax_client: Completed trial 9 with data: {'branin': (78.686066, nan)}.\n", + "[INFO 11-07 12:52:23] ax.service.ax_client: Generated new trial 10 with parameters {'x1': 0.779998, 'x2': 6.842907} using model BoTorch.\n", + "[INFO 11-07 12:52:23] ax.service.ax_client: Completed trial 10 with data: {'branin': (20.849186, nan)}.\n", + "[INFO 11-07 12:52:24] ax.service.ax_client: Generated new trial 11 with parameters {'x1': -0.959171, 'x2': 9.756062} using model BoTorch.\n", + "[INFO 11-07 12:52:24] ax.service.ax_client: Completed trial 11 with data: {'branin': (19.968334, nan)}.\n", + "[INFO 11-07 12:52:25] ax.service.ax_client: Generated new trial 12 with parameters {'x1': 1.759405, 'x2': 0.0} using model BoTorch.\n", + "[INFO 11-07 12:52:25] ax.service.ax_client: Completed trial 12 with data: {'branin': (21.157597, nan)}.\n", + "[INFO 11-07 12:52:25] ax.service.ax_client: Generated new trial 13 with parameters {'x1': -3.67521, 'x2': 15.0} using model BoTorch.\n", + "[INFO 11-07 12:52:25] ax.service.ax_client: Completed trial 13 with data: {'branin': (3.70913, nan)}.\n", + "[INFO 11-07 12:52:26] ax.service.ax_client: Generated new trial 14 with parameters {'x1': 10.0, 'x2': 0.0} using model BoTorch.\n", + "[INFO 11-07 12:52:26] ax.service.ax_client: Completed trial 14 with data: {'branin': (10.960894, nan)}.\n", + "/Users/sdaulton/miniconda3/envs/botorch_tut/lib/python3.11/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning:\n", + "\n", + "A not p.d., added jitter of 1.0e-08 to the diagonal\n", + "\n", + "[INFO 11-07 12:52:26] ax.service.ax_client: Generated new trial 15 with parameters {'x1': 4.693345, 'x2': 0.0} using model BoTorch.\n", + "[INFO 11-07 12:52:26] ax.service.ax_client: Completed trial 15 with data: {'branin': (11.7103, nan)}.\n", + "[INFO 11-07 12:52:27] ax.service.ax_client: Generated new trial 16 with parameters {'x1': -3.16039, 'x2': 12.343285} using model BoTorch.\n", + "[INFO 11-07 12:52:27] ax.service.ax_client: Completed trial 16 with data: {'branin': (0.400116, nan)}.\n", + "[INFO 11-07 12:52:27] ax.service.ax_client: Generated new trial 17 with parameters {'x1': 10.0, 'x2': 3.798226} using model BoTorch.\n", + "[INFO 11-07 12:52:27] ax.service.ax_client: Completed trial 17 with data: {'branin': (2.575594, nan)}.\n", + "[INFO 11-07 12:52:28] ax.service.ax_client: Generated new trial 18 with parameters {'x1': 3.304444, 'x2': 2.327283} using model BoTorch.\n", + "[INFO 11-07 12:52:28] ax.service.ax_client: Completed trial 18 with data: {'branin': (0.555859, nan)}.\n", + "[INFO 11-07 12:52:29] ax.service.ax_client: Generated new trial 19 with parameters {'x1': -3.375582, 'x2': 12.520736} using model BoTorch.\n", + "[INFO 11-07 12:52:29] ax.service.ax_client: Completed trial 19 with data: {'branin': (0.764316, nan)}.\n", + "[INFO 11-07 12:52:30] ax.service.ax_client: Generated new trial 20 with parameters {'x1': 9.267105, 'x2': 2.183014} using model BoTorch.\n", + "[INFO 11-07 12:52:30] ax.service.ax_client: Completed trial 20 with data: {'branin': (0.543305, nan)}.\n", + "[INFO 11-07 12:52:30] ax.service.ax_client: Generated new trial 21 with parameters {'x1': 9.536612, 'x2': 2.744301} using model BoTorch.\n", + "[INFO 11-07 12:52:30] ax.service.ax_client: Completed trial 21 with data: {'branin': (0.487921, nan)}.\n", + "[INFO 11-07 12:52:31] ax.service.ax_client: Generated new trial 22 with parameters {'x1': -3.055135, 'x2': 12.529729} using model BoTorch.\n", + "[INFO 11-07 12:52:31] ax.service.ax_client: Completed trial 22 with data: {'branin': (0.646773, nan)}.\n", + "[INFO 11-07 12:52:32] ax.service.ax_client: Generated new trial 23 with parameters {'x1': 3.099745, 'x2': 2.457142} using model BoTorch.\n", + "[INFO 11-07 12:52:32] ax.service.ax_client: Completed trial 23 with data: {'branin': (0.428578, nan)}.\n", + "[INFO 11-07 12:52:33] ax.service.ax_client: Generated new trial 24 with parameters {'x1': 8.94462, 'x2': 0.943412} using model BoTorch.\n", + "[INFO 11-07 12:52:33] ax.service.ax_client: Completed trial 24 with data: {'branin': (2.820818, nan)}.\n", + "[INFO 11-07 12:52:35] ax.service.ax_client: Generated new trial 25 with parameters {'x1': 9.510065, 'x2': 2.361432} using model BoTorch.\n", + "[INFO 11-07 12:52:35] ax.service.ax_client: Completed trial 25 with data: {'branin': (0.467552, nan)}.\n", + "[INFO 11-07 12:52:36] ax.service.ax_client: Generated new trial 26 with parameters {'x1': 9.425844, 'x2': 2.589096} using model BoTorch.\n", + "[INFO 11-07 12:52:36] ax.service.ax_client: Completed trial 26 with data: {'branin': (0.410706, nan)}.\n", + "[INFO 11-07 12:52:37] ax.service.ax_client: Generated new trial 27 with parameters {'x1': -3.091638, 'x2': 12.315311} using model BoTorch.\n", + "[INFO 11-07 12:52:37] ax.service.ax_client: Completed trial 27 with data: {'branin': (0.435478, nan)}.\n", + "[INFO 11-07 12:52:38] ax.service.ax_client: Generated new trial 28 with parameters {'x1': -3.221389, 'x2': 12.345989} using model BoTorch.\n", + "[INFO 11-07 12:52:38] ax.service.ax_client: Completed trial 28 with data: {'branin': (0.443229, nan)}.\n", + "/Users/sdaulton/botorch_2024_11_07/botorch/botorch/optim/optimize.py:576: RuntimeWarning:\n", + "\n", + "Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + "[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]\n", + "Trying again with a new set of initial conditions.\n", + "\n", + "/Users/sdaulton/botorch_2024_11_07/botorch/botorch/optim/optimize.py:576: RuntimeWarning:\n", + "\n", + "Optimization failed on the second try, after generating a new set of initial conditions.\n", + "\n", + "[INFO 11-07 12:52:42] ax.service.ax_client: Generated new trial 29 with parameters {'x1': 3.182468, 'x2': 2.521964} using model BoTorch.\n", + "[INFO 11-07 12:52:42] ax.service.ax_client: Completed trial 29 with data: {'branin': (0.48354, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 1 with parameters {'x1': 3.166217, 'x2': 3.867106} using model Sobol.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 1 with data: {'branin': (2.996862, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 2 with parameters {'x1': 9.560105, 'x2': 10.718323} using model Sobol.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 2 with data: {'branin': (66.530624, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 3 with parameters {'x1': -3.878664, 'x2': 0.117947} using model Sobol.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 3 with data: {'branin': (198.850861, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Generated new trial 4 with parameters {'x1': -2.362858, 'x2': 8.855021} using model Sobol.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:05] ax.service.ax_client: Completed trial 4 with data: {'branin': (5.811776, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:07] ax.service.ax_client: Generated new trial 5 with parameters {'x1': 2.562432, 'x2': 4.925782} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:07] ax.service.ax_client: Completed trial 5 with data: {'branin': (6.611189, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:07] ax.service.ax_client: Generated new trial 6 with parameters {'x1': 5.50005, 'x2': 4.949873} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:07] ax.service.ax_client: Completed trial 6 with data: {'branin': (31.211433, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:08] ax.service.ax_client: Generated new trial 7 with parameters {'x1': -2.300231, 'x2': 4.436402} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:08] ax.service.ax_client: Completed trial 7 with data: {'branin': (38.505764, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:08] ax.service.ax_client: Generated new trial 8 with parameters {'x1': -1.583362, 'x2': 7.318469} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:08] ax.service.ax_client: Completed trial 8 with data: {'branin': (12.206194, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:09] ax.service.ax_client: Generated new trial 9 with parameters {'x1': -5.0, 'x2': 9.066302} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:09] ax.service.ax_client: Completed trial 9 with data: {'branin': (78.675331, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:09] ax.service.ax_client: Generated new trial 10 with parameters {'x1': 0.787884, 'x2': 6.879815} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:09] ax.service.ax_client: Completed trial 10 with data: {'branin': (20.990005, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:10] ax.service.ax_client: Generated new trial 11 with parameters {'x1': 1.60023, 'x2': 0.584966} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:10] ax.service.ax_client: Completed trial 11 with data: {'branin': (19.951, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:10] ax.service.ax_client: Generated new trial 12 with parameters {'x1': 10.0, 'x2': 0.0} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:10] ax.service.ax_client: Completed trial 12 with data: {'branin': (10.960894, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:11] ax.service.ax_client: Generated new trial 13 with parameters {'x1': 7.38266, 'x2': 0.0} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:11] ax.service.ax_client: Completed trial 13 with data: {'branin': (16.027073, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:11] ax.service.ax_client: Generated new trial 14 with parameters {'x1': 4.173322, 'x2': 0.0} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:11] ax.service.ax_client: Completed trial 14 with data: {'branin': (7.656268, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:12] ax.service.ax_client: Generated new trial 15 with parameters {'x1': -3.935855, 'x2': 15.0} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:12] ax.service.ax_client: Completed trial 15 with data: {'branin': (3.810518, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:12] ax.service.ax_client: Generated new trial 16 with parameters {'x1': -3.321259, 'x2': 12.38287} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:12] ax.service.ax_client: Completed trial 16 with data: {'branin': (0.660087, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:13] ax.service.ax_client: Generated new trial 17 with parameters {'x1': 10.0, 'x2': 3.666754} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:13] ax.service.ax_client: Completed trial 17 with data: {'branin': (2.383767, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:14] ax.service.ax_client: Generated new trial 18 with parameters {'x1': 9.34166, 'x2': 2.5446} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:14] ax.service.ax_client: Completed trial 18 with data: {'branin': (0.450308, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:14] ax.service.ax_client: Generated new trial 19 with parameters {'x1': 3.076019, 'x2': 2.418569} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:14] ax.service.ax_client: Completed trial 19 with data: {'branin': (0.426966, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:15] ax.service.ax_client: Generated new trial 20 with parameters {'x1': 9.537424, 'x2': 2.493842} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:15] ax.service.ax_client: Completed trial 20 with data: {'branin': (0.4648, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:16] ax.service.ax_client: Generated new trial 21 with parameters {'x1': -3.360749, 'x2': 15.0} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:16] ax.service.ax_client: Completed trial 21 with data: {'branin': (5.432912, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:17] ax.service.ax_client: Generated new trial 22 with parameters {'x1': 9.516079, 'x2': 2.791557} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:17] ax.service.ax_client: Completed trial 22 with data: {'branin': (0.494746, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:19] ax.service.ax_client: Generated new trial 23 with parameters {'x1': 3.202976, 'x2': 2.439512} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:19] ax.service.ax_client: Completed trial 23 with data: {'branin': (0.460872, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:20] ax.service.ax_client: Generated new trial 24 with parameters {'x1': 9.625609, 'x2': 2.470825} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:20] ax.service.ax_client: Completed trial 24 with data: {'branin': (0.622846, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:21] ax.service.ax_client: Generated new trial 25 with parameters {'x1': -3.235781, 'x2': 12.32664} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:21] ax.service.ax_client: Completed trial 25 with data: {'branin': (0.471375, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:22] ax.service.ax_client: Generated new trial 26 with parameters {'x1': 9.466124, 'x2': 2.301119} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:22] ax.service.ax_client: Completed trial 26 with data: {'branin': (0.449765, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[W 241107 08:26:24 optimize:576] Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + " [OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]\n", + " Trying again with a new set of initial conditions.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:25] ax.service.ax_client: Generated new trial 27 with parameters {'x1': 2.97826, 'x2': 2.43746} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:25] ax.service.ax_client: Completed trial 27 with data: {'branin': (0.526684, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:27] ax.service.ax_client: Generated new trial 28 with parameters {'x1': -3.286554, 'x2': 12.040548} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:27] ax.service.ax_client: Completed trial 28 with data: {'branin': (0.84146, nan)}.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[W 241107 08:26:28 optimize:576] Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + " [OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]\n", + " Trying again with a new set of initial conditions.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:29] ax.service.ax_client: Generated new trial 29 with parameters {'x1': 9.459437, 'x2': 2.554713} using model BoTorch.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 08:26:29] ax.service.ax_client: Completed trial 29 with data: {'branin': (0.406186, nan)}.\n" + ] + } + ], + "source": [ + "with fast_smoke_test():\n", + " for i in range(NUM_EVALS):\n", + " parameters, trial_index = ax_client.get_next_trial()\n", + " # Local evaluation here can be replaced with deployment to external system.\n", + " ax_client.complete_trial(trial_index=trial_index, raw_data=evaluate(parameters))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "2794d041-6a39-483d-a603-cc5088bcd1b2", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Viewing the evaluated trials" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996766045, + "executionStopTime": 1730996789881, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "26f42620-0f39-4836-aa09-42d1718ee6e6", + "output": { + "id": "2736915803137170" + }, + "outputsInitialized": true, + "requestMsgId": "26f42620-0f39-4836-aa09-42d1718ee6e6", + "serverExecutionDuration": 53.62950079143, + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING 11-07 12:53:20] ax.service.utils.report_utils: Column reason missing for all trials. Not appending column.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
trial_indexarm_nametrial_statusgeneration_methodbraninx1x2
000_0COMPLETEDSobol104.3654170.62583014.359564
111_0COMPLETEDSobol2.9968623.1662173.867106
222_0COMPLETEDSobol66.5306329.56010510.718323
333_0COMPLETEDSobol198.850861-3.8786640.117947
444_0COMPLETEDSobol5.811776-2.3628588.855021
555_0COMPLETEDBoTorch6.6110402.5624644.925756
666_0COMPLETEDBoTorch31.2497735.5034284.951339
777_0COMPLETEDBoTorch38.632786-2.3068094.436082
888_0COMPLETEDBoTorch12.208769-1.5822967.318848
999_0COMPLETEDBoTorch78.686066-5.0000009.065641
101010_0COMPLETEDBoTorch20.8491860.7799986.842907
111111_0COMPLETEDBoTorch19.968334-0.9591719.756062
121212_0COMPLETEDBoTorch21.1575971.7594050.000000
131313_0COMPLETEDBoTorch3.709130-3.67521015.000000
141414_0COMPLETEDBoTorch10.96089410.0000000.000000
151515_0COMPLETEDBoTorch11.7103004.6933450.000000
161616_0COMPLETEDBoTorch0.400116-3.16039012.343285
171717_0COMPLETEDBoTorch2.57559410.0000003.798226
181818_0COMPLETEDBoTorch0.5558593.3044442.327283
191919_0COMPLETEDBoTorch0.764316-3.37558212.520736
202020_0COMPLETEDBoTorch0.5433059.2671052.183014
212121_0COMPLETEDBoTorch0.4879219.5366122.744301
222222_0COMPLETEDBoTorch0.646773-3.05513512.529729
232323_0COMPLETEDBoTorch0.4285783.0997452.457142
242424_0COMPLETEDBoTorch2.8208188.9446200.943412
252525_0COMPLETEDBoTorch0.4675529.5100652.361432
262626_0COMPLETEDBoTorch0.4107069.4258442.589096
272727_0COMPLETEDBoTorch0.435478-3.09163812.315311
282828_0COMPLETEDBoTorch0.443229-3.22138912.345989
292929_0COMPLETEDBoTorch0.4835403.1824682.521964
\n", + "
" + ], + "text/plain": [ + " trial_index arm_name trial_status generation_method branin \\\n", + "0 0 0_0 COMPLETED Sobol 104.365417 \n", + "1 1 1_0 COMPLETED Sobol 2.996862 \n", + "2 2 2_0 COMPLETED Sobol 66.530632 \n", + "3 3 3_0 COMPLETED Sobol 198.850861 \n", + "4 4 4_0 COMPLETED Sobol 5.811776 \n", + "5 5 5_0 COMPLETED BoTorch 6.611040 \n", + "6 6 6_0 COMPLETED BoTorch 31.249773 \n", + "7 7 7_0 COMPLETED BoTorch 38.632786 \n", + "8 8 8_0 COMPLETED BoTorch 12.208769 \n", + "9 9 9_0 COMPLETED BoTorch 78.686066 \n", + "10 10 10_0 COMPLETED BoTorch 20.849186 \n", + "11 11 11_0 COMPLETED BoTorch 19.968334 \n", + "12 12 12_0 COMPLETED BoTorch 21.157597 \n", + "13 13 13_0 COMPLETED BoTorch 3.709130 \n", + "14 14 14_0 COMPLETED BoTorch 10.960894 \n", + "15 15 15_0 COMPLETED BoTorch 11.710300 \n", + "16 16 16_0 COMPLETED BoTorch 0.400116 \n", + "17 17 17_0 COMPLETED BoTorch 2.575594 \n", + "18 18 18_0 COMPLETED BoTorch 0.555859 \n", + "19 19 19_0 COMPLETED BoTorch 0.764316 \n", + "20 20 20_0 COMPLETED BoTorch 0.543305 \n", + "21 21 21_0 COMPLETED BoTorch 0.487921 \n", + "22 22 22_0 COMPLETED BoTorch 0.646773 \n", + "23 23 23_0 COMPLETED BoTorch 0.428578 \n", + "24 24 24_0 COMPLETED BoTorch 2.820818 \n", + "25 25 25_0 COMPLETED BoTorch 0.467552 \n", + "26 26 26_0 COMPLETED BoTorch 0.410706 \n", + "27 27 27_0 COMPLETED BoTorch 0.435478 \n", + "28 28 28_0 COMPLETED BoTorch 0.443229 \n", + "29 29 29_0 COMPLETED BoTorch 0.483540 \n", + "\n", + " x1 x2 \n", + "0 0.625830 14.359564 \n", + "1 3.166217 3.867106 \n", + "2 9.560105 10.718323 \n", + "3 -3.878664 0.117947 \n", + "4 -2.362858 8.855021 \n", + "5 2.562464 4.925756 \n", + "6 5.503428 4.951339 \n", + "7 -2.306809 4.436082 \n", + "8 -1.582296 7.318848 \n", + "9 -5.000000 9.065641 \n", + "10 0.779998 6.842907 \n", + "11 -0.959171 9.756062 \n", + "12 1.759405 0.000000 \n", + "13 -3.675210 15.000000 \n", + "14 10.000000 0.000000 \n", + "15 4.693345 0.000000 \n", + "16 -3.160390 12.343285 \n", + "17 10.000000 3.798226 \n", + "18 3.304444 2.327283 \n", + "19 -3.375582 12.520736 \n", + "20 9.267105 2.183014 \n", + "21 9.536612 2.744301 \n", + "22 -3.055135 12.529729 \n", + "23 3.099745 2.457142 \n", + "24 8.944620 0.943412 \n", + "25 9.510065 2.361432 \n", + "26 9.425844 2.589096 \n", + "27 -3.091638 12.315311 \n", + "28 -3.221389 12.345989 \n", + "29 3.182468 2.521964 " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ax_client.get_trials_data_frame()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996766786, + "executionStopTime": 1730996790153, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "89ba4880-e577-40c0-a9f7-5da1df38cb50", + "output": { + "id": "2997713397043895" + }, + "outputsInitialized": true, + "requestMsgId": "89ba4880-e577-40c0-a9f7-5da1df38cb50", + "serverExecutionDuration": 252.73659080267, + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best parameters: {'x1': 9.510065129079985, 'x2': 2.361432108875333}\n", + "Corresponding mean: {'branin': np.float64(0.372037358815291)}, covariance: {'branin': {'branin': np.float64(0.04886421886415146)}}\n" + ] + } + ], + "source": [ + "parameters, values = ax_client.get_best_parameters()\n", + "print(f\"Best parameters: {parameters}\")\n", + "print(f\"Corresponding mean: {values[0]}, covariance: {values[1]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "10562d0a-5fc3-4771-91c3-f881bd211174", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Plotting the response surface and optimization progress" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996819269, + "executionStopTime": 1730996821837, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "9848063a-11f7-41b7-abff-44d6a3d7eb9f", + "output": { + "id": "1070981858093152" + }, + "outputsInitialized": true, + "requestMsgId": "9848063a-11f7-41b7-abff-44d6a3d7eb9f", + "serverExecutionDuration": 2040.9845691174, + "showInput": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO 11-07 12:53:22] ax.service.ax_client: Retrieving contour plot with parameter 'x1' on X-axis and 'x2' on Y-axis, for metric 'branin'. Remaining parameters are affixed to the middle of their range.\n" + ] + }, + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from ax.utils.notebook.plotting import render\n", + "\n", + "render(ax_client.get_contour_plot())" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996822382, + "executionStopTime": 1730996823213, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "2b8c5ca1-db2e-4aa7-8f53-e1d5ea6406e4", + "output": { + "id": "555876497377688" + }, + "outputsInitialized": true, + "requestMsgId": "2b8c5ca1-db2e-4aa7-8f53-e1d5ea6406e4", + "serverExecutionDuration": 255.65920583904, + "showInput": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "({'x1': 9.510065129079985, 'x2': 2.361432108875333},\n", + " {'branin': np.float64(0.372037358815291)})" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "best_parameters, values = ax_client.get_best_parameters()\n", + "best_parameters, values[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996822759, + "executionStopTime": 1730996823380, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "998452cf-6251-4f0b-ad9b-ac35b484b634", + "output": { + "id": "444230641668471" + }, + "outputsInitialized": true, + "requestMsgId": "998452cf-6251-4f0b-ad9b-ac35b484b634", + "serverExecutionDuration": 118.52293275297, + "showInput": true + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAArwAAAH0CAYAAADfWf7fAAAgAElEQVR4XuydB3RUVdeG35BOEkILvSMCShEEVFBBUBBFERQVFKQqRRRQAUEEpSgWULqKFBULKCqgiNKVjhRpIr33ThIghX/twz/zJSFtziaZmeS9a7mWw9yzz7nPvpN55tx9z/W5evXqVXAjARIgARIgARIgARIggSxKwIfCm0Uzy8MiARIgARIgARIgARIwBCi8PBFIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwstzgARIgARIgARIgARIIEsToPBm6fTy4EiABEiABEiABEiABCi8PAdIgARIgARIgARIgASyNAEKb5ZOLw+OBEiABEiABEiABEiAwptJ58Ds35fj/MUoPNP8/jR7jI+/iqjoS/D19UVwUECa+9/IHaRf6T80JPhGhk13rD37j2D52i04ceqsGUOrZvcjZ3Bguttn1o7Rl67gm5/m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VRmnnIM9Oj1uHjrDjQRIgARIIG0CHie8aQ855T3kZjGZzZTlzET+kj7mVy5vHz1+ygQoVCBfinc4y+zPkeOnjLA6bupJa1xS0ykzrkULR5gZoJQ2qT09dPQEIqMumcXucwYHXrerXGp/pM3rZrm1hJfRHTvKl708lCA4MAAR+XOnuHaq7CfryMrDJZLjIT8WRHhl3dSETyZL61jTel/iCg/54hfRTvrUsrTap/S+CJA8NSxfnvBUnwgmpS5Hj59GkYL5EZ4r+SXoXB2DlMhI3/Hx8aZ8JOGT7VyNldr+Uh4gYi3nh7BLaQm9G9mnTSzhIeeOnGPygyZhDbtNvNTapPZ51J5rMoPvuHIkPyqSXrVIOi5ZF1n6lCXrEj5JMbnxp/d8TS8v+fsm50ZIziAUyJ+Hy5OlFxz3IwESIAFPXJYsu2ZFlnwSUcsVGoJPvpxlLq06lnjKrkx43CRAAiRAAiRAAiRwIwhkqRneGwHEXTHkUrYsOO/YpITj2wkDU1xpwV3jZL8kQAIkQAIkQAIk4G0EKLwekjG5VLpk5QZcvBhtHngga4Nm5GViDzlsDoMESIAESIAESIAEMpwAhTfDEbMDEiABEiABEiABEiABdxKg8LqTPvsmARIgARIgARIgARLIcAIU3gxHzA5IgARIgARIgARIgATcSYDC60767JsESIAESIAESIAESCDDCVB4MxwxOyABEiABEiABEiABEnAnAQqvO+mzbxIgARIgARIgARIggQwnQOHNcMTsgARIgARIgARIgARIwJ0EKLzupM++SYAESIAESIAESIAEMpwAhTfDEbMDEiABEiABEiABEiABdxKg8LqTPvsmARIgARIgARIgARLIcAIU3gxHzA5IgARIgARIgARIgATcSYDC60767JsESIAESIAESIAESCDDCVB4MxwxOyABEiABEiABEiABEnAnAQqvO+mzbxIgARIgARIgARIggQwnQOHNcMTsgARIgARIgARIgARIwJ0EKLzupM++SYAESIAESIAESIAEMpwAhTfDEbMDEiABEiABEiABEiABdxKg8LqTPvsmARIgARIgARIgARLIcAIU3gxHzA5IgARIgARIgARIgATcSYDC60767JsESIAESIAESIAESCDDCVB4MxwxOyABEiABEiABEiABEnAnAQqvO+mzbxIgARIgARIgARIggQwnQOHNcMTsgARIgARIgARIgARIwJ0EKLzupM++SYAESIAESIAESIAEMpwAhTfDEbMDEiABEiABEiABEiABdxKg8LqTPvsmARIgARIgARIgARLIcAIU3gxHzA5IgARIgARIgARIgATcSYDC60767JsESIAESIAESIAESCDDCVB4MxwxOyABEiABEiABEiABEnAnAQqvO+mzbxIgARIgARIgARIggQwnQOHNcMTsgARIgARIgARIgARIwJ0EKLzupM++SYAESIAESIAESIAEMpwAhTfDEbMDEiABEiABEiABEiABdxKg8LqTPvsmARIgARIgARIgARLIcAIU3gxHzA5IgARIgARIgARIgATcSYDC60767JsESIAESIAESIAESCDDCVB4MxwxOyABEiABEiABEiABEnAnAQqvO+mzbxIgARIgARIgARIggQwnQOHNcMTsgARIgARIgARIgARIwJ0EKLzupM++SYAESIAESIAESIAEMpwAhTfDEbMDEiABEiABEiABEiABdxKg8LqTPvsmARIgARIgARIgARLIcAIU3gxHzA5IgARIgARIgARIgATcSYDC60767JsESIAESIAESIAESCDDCVB4MxwxOyABEiABEiABEiABEnAnAQqvO+mzbxIgARIgARIgARIggQwnQOFVIj58KloZwXub5w8PxLnIGMTExnvvQbhp5L45fCD8jp255KYReHe3ocF+8PHxwYWoGO8+EDeNPiJ3EM5euIyYuKtuGoH3duvn64O8YYE4fpafXdssFskXbNuU7UjAmgCF1xrdtYYUXgqvzSlE4bWh9r82FF4dPwqvPT8Krz07R0sKr54hI7hOgMLrOrNELSi8FF6bU4jCa0ONwquj9r/WFF57khRee3YUXj07RrAnQOG1Z8cZXpY0WJ89FF5rdKYhZ3h1/Ci89vwovPbsKLx6doxgT4DCa8+OwkvhtT57KLzW6Ci8OnSmNYXXHiKF154dhVfPjhHsCVB47dlReCm81mcPhdcaHYVXh47Cq+RH4VUCBMAaXj1DRnCdAIXXdWaJWrCGlzW8NqcQhdeG2v/asKRBx48zvPb8KLz27DjDq2fHCPYEKLz27DjDyxle67OHwmuNjjO8OnSc4VXyo/AqAXKGVw+QEawIUHitsP2vEWd4OcNrcwpReG2ocYZXR+1/rTnDa0+SwmvPjjO8enaMYE+AwmvP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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "render(ax_client.get_optimization_trace(objective_optimum=0.397887))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "59a5ea1e-1b70-433b-bd3d-e3708d270769", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "## Optimization with the Developer API\n", + "\n", + "A detailed tutorial on the Service API can be found [here](https://ax.dev/tutorials/gpei_hartmann_developer.html).\n", + "\n", + "### Set up the Experiment in Ax\n", + "\n", + "We need 3 inputs for an Ax `Experiment`:\n", + "- A search space to optimize over;\n", + "- An optimization config specifiying the objective / metrics to optimize, and optional outcome constraints;\n", + "- A runner that handles the deployment of trials. For a synthetic optimization problem, such as here, this only returns simple metadata about the trial." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996823528, + "executionStopTime": 1730996823772, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "7af04314-588b-44c9-b041-12795d720597", + "outputsInitialized": true, + "requestMsgId": "7af04314-588b-44c9-b041-12795d720597", + "serverExecutionDuration": 4.5209536328912, + "showInput": true + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import torch\n", + "from ax import (\n", + " Data,\n", + " Experiment,\n", + " Metric,\n", + " Objective,\n", + " OptimizationConfig,\n", + " ParameterType,\n", + " RangeParameter,\n", + " Runner,\n", + " SearchSpace,\n", + ")\n", + "from ax.utils.common.result import Ok\n", + "from botorch.test_functions import Branin\n", + "\n", + "\n", + "branin_func = Branin()\n", + "\n", + "# For our purposes, the metric is a wrapper that structures the function output.\n", + "class BraninMetric(Metric):\n", + " def fetch_trial_data(self, trial):\n", + " records = []\n", + " for arm_name, arm in trial.arms_by_name.items():\n", + " params = arm.parameters\n", + " tensor_params = torch.tensor([params[\"x1\"], params[\"x2\"]])\n", + " records.append(\n", + " {\n", + " \"arm_name\": arm_name,\n", + " \"metric_name\": self.name,\n", + " \"trial_index\": trial.index,\n", + " \"mean\": branin_func(tensor_params),\n", + " \"sem\": float(\n", + " \"nan\"\n", + " ), # SEM (observation noise) - NaN indicates unknown\n", + " }\n", + " )\n", + " return Ok(value=Data(df=pd.DataFrame.from_records(records)))\n", + "\n", + "\n", + "# Search space defines the parameters, their types, and acceptable values.\n", + "search_space = SearchSpace(\n", + " parameters=[\n", + " RangeParameter(\n", + " name=\"x1\", parameter_type=ParameterType.FLOAT, lower=-5, upper=10\n", + " ),\n", + " RangeParameter(\n", + " name=\"x2\", parameter_type=ParameterType.FLOAT, lower=0, upper=15\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "optimization_config = OptimizationConfig(\n", + " objective=Objective(\n", + " metric=BraninMetric(name=\"branin_metric\", lower_is_better=True),\n", + " minimize=True, # This is optional since we specified `lower_is_better=True`\n", + " )\n", + ")\n", + "\n", + "\n", + "class MyRunner(Runner):\n", + " def run(self, trial):\n", + " trial_metadata = {\"name\": str(trial.index)}\n", + " return trial_metadata\n", + "\n", + "\n", + "exp = Experiment(\n", + " name=\"branin_experiment\",\n", + " search_space=search_space,\n", + " optimization_config=optimization_config,\n", + " runner=MyRunner(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "a5cb8ffb-3af8-4fdd-85b4-a10917177dc7", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Run the BO loop\n", + "\n", + "First, we use the Sobol generator to create 5 (quasi-) random initial point in the search space. Ax controls objective evaluations via `Trial`s. \n", + "- We generate a `Trial` using a generator run, e.g., `Sobol` below. A `Trial` specifies relevant metadata as well as the parameters to be evaluated. At this point, the `Trial` is at the `CANDIDATE` stage.\n", + "- We run the `Trial` using `Trial.run()`. In our example, this serves to mark the `Trial` as `RUNNING`. In an advanced application, this can be used to dispatch the `Trial` for evaluation on a remote server.\n", + "- Once the `Trial` is done running, we mark it as `COMPLETED`. This tells the `Experiment` that it can fetch the `Trial` data. \n", + "\n", + "A `Trial` supports evaluation of a single parameterization. For parallel evaluations, see [`BatchTrial`](https://ax.dev/docs/core.html#trial-vs-batch-trial)." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "code_folding": [], + "collapsed": false, + "executionStartTime": 1730996824494, + "executionStopTime": 1730996824758, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "e7c73fd6-9664-4524-b0b1-91e127e26b67", + "outputsInitialized": true, + "requestMsgId": "e7c73fd6-9664-4524-b0b1-91e127e26b67", + "serverExecutionDuration": 11.998974718153 + }, + "outputs": [], + "source": [ + "from ax.modelbridge.registry import Models\n", + "\n", + "\n", + "sobol = Models.SOBOL(exp.search_space)\n", + "\n", + "for i in range(5):\n", + " trial = exp.new_trial(generator_run=sobol.gen(1))\n", + " trial.run()\n", + " trial.mark_completed()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "33fef7a2-67db-43cd-bc84-4d5736c582e7", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "Once the initial (quasi-) random stage is completed, we can use our `SimpleCustomGP` with the default acquisition function chosen by `Ax` to run the BO loop." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996825586, + "executionStopTime": 1730996847043, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "abc178e3-3e93-4855-b03b-c9acaf3fc7a5", + "outputsInitialized": true, + "requestMsgId": "abc178e3-3e93-4855-b03b-c9acaf3fc7a5", + "serverExecutionDuration": 21109.300409909, + "showInput": true + }, + "outputs": [], + "source": [ + "with fast_smoke_test():\n", + " for i in range(NUM_EVALS - 5):\n", + " model_bridge = Models.BOTORCH_MODULAR(\n", + " experiment=exp,\n", + " data=exp.fetch_data(),\n", + " surrogate_spec=SurrogateSpec(model_configs=[ModelConfig(SimpleCustomGP)]),\n", + " )\n", + " trial = exp.new_trial(generator_run=model_bridge.gen(1))\n", + " trial.run()\n", + " trial.mark_completed()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "cb16843a-0e12-47fc-860f-eafb74f1bd3b", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "View the trials attached to the `Experiment`." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996826387, + "executionStopTime": 1730996847147, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "a96bc2d0-161f-4d04-b007-d4dee67495b7", + "output": { + "id": "594908669713548" + }, + "outputsInitialized": true, + "requestMsgId": "a96bc2d0-161f-4d04-b007-d4dee67495b7", + "serverExecutionDuration": 3.2807770185173, + "showInput": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: Trial(experiment_name='branin_experiment', index=0, status=TrialStatus.COMPLETED, arm=Arm(name='0_0', parameters={'x1': 8.268271088600159, 'x2': 13.676363825798035})),\n", + " 1: Trial(experiment_name='branin_experiment', index=1, status=TrialStatus.COMPLETED, arm=Arm(name='1_0', parameters={'x1': -3.0115276388823986, 'x2': 0.19308556336909533})),\n", + " 2: Trial(experiment_name='branin_experiment', index=2, status=TrialStatus.COMPLETED, arm=Arm(name='2_0', parameters={'x1': -0.794604872353375, 'x2': 10.19062165170908})),\n", + " 3: Trial(experiment_name='branin_experiment', index=3, status=TrialStatus.COMPLETED, arm=Arm(name='3_0', parameters={'x1': 2.7553387405350804, 'x2': 4.206141787581146})),\n", + " 4: Trial(experiment_name='branin_experiment', index=4, status=TrialStatus.COMPLETED, arm=Arm(name='4_0', parameters={'x1': 5.150513867847621, 'x2': 9.072991241700947})),\n", + " 5: Trial(experiment_name='branin_experiment', index=5, status=TrialStatus.COMPLETED, arm=Arm(name='5_0', parameters={'x1': 1.5497872135088082, 'x2': 9.19017678783918})),\n", + " 6: Trial(experiment_name='branin_experiment', index=6, status=TrialStatus.COMPLETED, arm=Arm(name='6_0', parameters={'x1': 7.35880350484438, 'x2': 3.182282876550653})),\n", + " 7: Trial(experiment_name='branin_experiment', index=7, status=TrialStatus.COMPLETED, arm=Arm(name='7_0', parameters={'x1': -5.0, 'x2': 8.206550963671184})),\n", + " 8: Trial(experiment_name='branin_experiment', index=8, status=TrialStatus.COMPLETED, arm=Arm(name='8_0', parameters={'x1': 4.765065782500189, 'x2': 1.252289390966608})),\n", + " 9: Trial(experiment_name='branin_experiment', index=9, status=TrialStatus.COMPLETED, arm=Arm(name='9_0', parameters={'x1': 4.505201068669873, 'x2': 3.3644975986881467})),\n", + " 10: Trial(experiment_name='branin_experiment', index=10, status=TrialStatus.COMPLETED, arm=Arm(name='10_0', parameters={'x1': -3.1164341906014323, 'x2': 15.0})),\n", + " 11: Trial(experiment_name='branin_experiment', index=11, status=TrialStatus.COMPLETED, arm=Arm(name='11_0', parameters={'x1': 10.0, 'x2': 0.0})),\n", + " 12: Trial(experiment_name='branin_experiment', index=12, status=TrialStatus.COMPLETED, arm=Arm(name='12_0', parameters={'x1': -5.0, 'x2': 15.0})),\n", + " 13: Trial(experiment_name='branin_experiment', index=13, status=TrialStatus.COMPLETED, arm=Arm(name='13_0', parameters={'x1': 10.0, 'x2': 2.8078129263430163})),\n", + " 14: Trial(experiment_name='branin_experiment', index=14, status=TrialStatus.COMPLETED, arm=Arm(name='14_0', parameters={'x1': 2.445554777112278, 'x2': 2.2462261858873593})),\n", + " 15: Trial(experiment_name='branin_experiment', index=15, status=TrialStatus.COMPLETED, arm=Arm(name='15_0', parameters={'x1': 2.480451132691808, 'x2': 3.056693071733582})),\n", + " 16: Trial(experiment_name='branin_experiment', index=16, status=TrialStatus.COMPLETED, arm=Arm(name='16_0', parameters={'x1': 10.0, 'x2': 3.9059386481288194})),\n", + " 17: Trial(experiment_name='branin_experiment', index=17, status=TrialStatus.COMPLETED, arm=Arm(name='17_0', parameters={'x1': 2.0237354666184544, 'x2': 3.511643776100397})),\n", + " 18: Trial(experiment_name='branin_experiment', index=18, status=TrialStatus.COMPLETED, arm=Arm(name='18_0', parameters={'x1': 2.8769752653087997, 'x2': 2.2483802909633863})),\n", + " 19: Trial(experiment_name='branin_experiment', index=19, status=TrialStatus.COMPLETED, arm=Arm(name='19_0', parameters={'x1': 3.0536513312697213, 'x2': 2.4346471208663614})),\n", + " 20: Trial(experiment_name='branin_experiment', index=20, status=TrialStatus.COMPLETED, arm=Arm(name='20_0', parameters={'x1': 9.427576408084287, 'x2': 2.557223349069929})),\n", + " 21: Trial(experiment_name='branin_experiment', index=21, status=TrialStatus.COMPLETED, arm=Arm(name='21_0', parameters={'x1': 8.84736166287066, 'x2': 0.8696191586858866})),\n", + " 22: Trial(experiment_name='branin_experiment', index=22, status=TrialStatus.COMPLETED, arm=Arm(name='22_0', parameters={'x1': -1.5039526251440347, 'x2': 15.0})),\n", + " 23: Trial(experiment_name='branin_experiment', index=23, status=TrialStatus.COMPLETED, arm=Arm(name='23_0', parameters={'x1': -3.335556146334603, 'x2': 12.910932366431291})),\n", + " 24: Trial(experiment_name='branin_experiment', index=24, status=TrialStatus.COMPLETED, arm=Arm(name='24_0', parameters={'x1': -3.491879380808762, 'x2': 13.514831783855984})),\n", + " 25: Trial(experiment_name='branin_experiment', index=25, status=TrialStatus.COMPLETED, arm=Arm(name='25_0', parameters={'x1': -3.031782920987203, 'x2': 11.976525526649187})),\n", + " 26: Trial(experiment_name='branin_experiment', index=26, status=TrialStatus.COMPLETED, arm=Arm(name='26_0', parameters={'x1': -3.1412934283043814, 'x2': 12.616052311698178})),\n", + " 27: Trial(experiment_name='branin_experiment', index=27, status=TrialStatus.COMPLETED, arm=Arm(name='27_0', parameters={'x1': 9.455852758717114, 'x2': 2.3674336079024183})),\n", + " 28: Trial(experiment_name='branin_experiment', index=28, status=TrialStatus.COMPLETED, arm=Arm(name='28_0', parameters={'x1': 3.1364699781417986, 'x2': 2.025242611585696})),\n", + " 29: Trial(experiment_name='branin_experiment', index=29, status=TrialStatus.COMPLETED, arm=Arm(name='29_0', parameters={'x1': -2.936274156572808, 'x2': 11.259953484546505}))}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp.trials" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "f96540ca-1043-4803-ad8c-d143d98373a2", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "View the evaluation data about these trials." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996827087, + "executionStopTime": 1730996847292, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "fa05b927-d262-46fd-bb82-e7f89f8bbc3d", + "output": { + "id": "483337300710020" + }, + "outputsInitialized": true, + "requestMsgId": "fa05b927-d262-46fd-bb82-e7f89f8bbc3d", + "serverExecutionDuration": 168.97921916097, + "showInput": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
arm_namemetric_namemeansemtrial_index
00_0branin_metric150.233566NaN0
11_0branin_metric139.047729NaN1
22_0branin_metric24.817539NaN2
33_0branin_metric3.699482NaN3
44_0branin_metric75.591110NaN4
55_0branin_metric38.786335NaN5
66_0branin_metric18.167435NaN6
77_0branin_metric93.378693NaN7
88_0branin_metric10.515005NaN8
99_0branin_metric11.683224NaN9
1010_0branin_metric8.159270NaN10
1111_0branin_metric10.960894NaN11
1212_0branin_metric17.508297NaN12
1313_0branin_metric1.981222NaN13
1414_0branin_metric3.033621NaN14
1515_0branin_metric2.465075NaN15
1616_0branin_metric2.758517NaN16
1717_0branin_metric5.839406NaN17
1818_0branin_metric0.790689NaN18
1919_0branin_metric0.443105NaN19
2020_0branin_metric0.404304NaN20
2121_0branin_metric3.303451NaN21
2222_0branin_metric50.510307NaN22
2323_0branin_metric0.605148NaN23
2424_0branin_metric1.127027NaN24
2525_0branin_metric0.457026NaN25
2626_0branin_metric0.514696NaN26
2727_0branin_metric0.420453NaN27
2828_0branin_metric0.462405NaN28
2929_0branin_metric0.877364NaN29
\n", + "
" + ], + "text/plain": [ + " arm_name metric_name mean sem trial_index\n", + "0 0_0 branin_metric 150.233566 NaN 0\n", + "1 1_0 branin_metric 139.047729 NaN 1\n", + "2 2_0 branin_metric 24.817539 NaN 2\n", + "3 3_0 branin_metric 3.699482 NaN 3\n", + "4 4_0 branin_metric 75.591110 NaN 4\n", + "5 5_0 branin_metric 38.786335 NaN 5\n", + "6 6_0 branin_metric 18.167435 NaN 6\n", + "7 7_0 branin_metric 93.378693 NaN 7\n", + "8 8_0 branin_metric 10.515005 NaN 8\n", + "9 9_0 branin_metric 11.683224 NaN 9\n", + "10 10_0 branin_metric 8.159270 NaN 10\n", + "11 11_0 branin_metric 10.960894 NaN 11\n", + "12 12_0 branin_metric 17.508297 NaN 12\n", + "13 13_0 branin_metric 1.981222 NaN 13\n", + "14 14_0 branin_metric 3.033621 NaN 14\n", + "15 15_0 branin_metric 2.465075 NaN 15\n", + "16 16_0 branin_metric 2.758517 NaN 16\n", + "17 17_0 branin_metric 5.839406 NaN 17\n", + "18 18_0 branin_metric 0.790689 NaN 18\n", + "19 19_0 branin_metric 0.443105 NaN 19\n", + "20 20_0 branin_metric 0.404304 NaN 20\n", + "21 21_0 branin_metric 3.303451 NaN 21\n", + "22 22_0 branin_metric 50.510307 NaN 22\n", + "23 23_0 branin_metric 0.605148 NaN 23\n", + "24 24_0 branin_metric 1.127027 NaN 24\n", + "25 25_0 branin_metric 0.457026 NaN 25\n", + "26 26_0 branin_metric 0.514696 NaN 26\n", + "27 27_0 branin_metric 0.420453 NaN 27\n", + "28 28_0 branin_metric 0.462405 NaN 28\n", + "29 29_0 branin_metric 0.877364 NaN 29" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp.fetch_data().df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "c3ed0ec0-5002-4e7c-9e57-915e2a671abc", + "outputsInitialized": false, + "showInput": false + }, + "source": [ + "### Plot results\n", + "\n", + "We can use convenient Ax utilities for plotting the results." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "code_folding": [], + "collapsed": false, + "customInput": null, + "executionStartTime": 1730996827721, + "executionStopTime": 1730996847354, + "hidden_ranges": [], + "isAgentGenerated": false, + "jupyter": { + "outputs_hidden": false + }, + "language": "python", + "originalKey": "1546c0cf-94f1-4573-acd2-fde9e38d105e", + "output": { + "id": "957716379715459" + }, + "outputsInitialized": true, + "requestMsgId": "1546c0cf-94f1-4573-acd2-fde9e38d105e", + "serverExecutionDuration": 78.405936714262, + "showInput": true + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAArwAAAH0CAYAAADfWf7fAAAgAElEQVR4Xuzdd5xU5dmH8d/2XdgCS+9NxUJRBBvGmoi9RU3sBTEYWzQqRl8jKmpMokaJGkVjwRJLbGCNIhYURUEERVF6b8suy/b2fs7R3QCiYe57ZhZmr/nLZeZ55jnfc1YvjmfOJNXV1dWJBwIIIIAAAggggAACCSqQRPAm6J5lsxBAAAEEEEAAAQRCAYKXAwEBBBBAAAEEEEAgoQUI3oTevWwcAggggAACCCCAAMHLMYAAAggggAACCCCQ0AIEb0LvXjYOAQQQQAABBBBAgODlGEAAAQQQQAABBBBIaAGCN6F3LxuHAAIIIIAAAgggQPByDCCAAAIIIIAAAggktADBm9C7l41DAAEEEEAAAQQQIHg5BhBAAAEEEEAAAQQSWoDgTejdy8YhgAACCCCAAAIIELwcAwgggAACCCCAAAIJLUDwJvTuZeMQQAABBBBAAAEECF6OAQQQQAABBBBAAIGEFiB4E3r3snEIIIAAAggggAACBC/HAAIIIIAAAggggEBCCxC8Cb172TgEEEAAAQQQQAABgpdjAAEEEEAAAQQQQCChBQjehN69bBwCCCCAAAIIIIAAwcsxgAACCCCAAAIIIJDQAgRvQu9eNg4BBBBAAAEEEECA4OUYQAABBBBAAAEEEEhoAYI3oXcvG4cAAggggAACCCBA8HIMIIAAAggggAACCCS0AMGb0LuXjUMAAQQQQAABBBAgeDkGEEAAAQQQQAABBBJagOBN6N3LxiGAAAIIIIAAAggQvBwDCCCAAAIIIIAAAgktQPAm9O5l4xBAAAEEEEAAAQQIXo4BBBBAAAEEEEAAgYQWIHgTeveycQgggAACCCCAAAIEL8cAAggggAACCCCAQEILELwJvXvZOAQQQAABBBBAAAGCl2MAAQQQQAABBBBAIKEFCN6E3r1sHAIIIIAAAggggADByzGAAAIIIIAAAgggkNACBG9C7142DgEEEEAAAQQQQIDg5RhAAAEEEEAAAQQQSGgBgjehdy8bhwACCCCAAAIIIEDwcgwggAACCCCAAAIIJLQAwZvQu5eNQwABBBBAAAEEECB4OQYQQAABBBBAAAEEElqA4E3o3cvGIYAAAggggAACCBC8HAMIIIAAAggggAACCS1A8Cb07mXjEEAAAQQQQAABBAhejgEEEEAAAQQQQACBhBYgeBN697JxCCCAAAIIIIAAAgQvxwACCCCAAAIIIIBAQgsQvAm9e9k4BBBAAAEEEEAAAYKXYwABBBBAAAEEEEAgoQUI3oTevWwcAggggAACCCCAAMHLMYAAAggggAACCCCQ0AIEb0LvXjYOAQQQQAABBBBAgODlGEAAAQQQQAABBBBIaAGCN6F3LxuHAAIIIIAAAgggQPByDCCAAAIIIIAAAggktADBm9C7l41DAAEEEEAAAQQQIHg5BhBAAAEEEEAAAQQSWoDgTejdy8YhgAACCCCAAAIIELwcAwgggAACCCCAAAIJLUDwJvTuZeMQQAABBBBAAAEECF6OAQQQQAABBBBAAIGEFiB4E3r3snEIIIAAAggggAACBC/HAAIIIIAAAggggEBCCxC8Cb172TgEEEAAAQQQQAABgpdjAAEEEEAAAQQQQCChBQjehN69bBwCCCCAAAIIIIAAwcsxgAACCCCAAAIIIJDQAgRvQu9eNg4BBBBAAAEEEECA4OUYQAABBBBAAAEEEEhoAYI3oXcvG4cAAggggAACCCBA8HIMIIAAAggggAACCCS0AMGb0LuXjUMAAQQQQAABBBAgeDkGEEAAAQQQQAABBBJagOBN6N3LxiGAAAIIIIAAAggQvBwDCCCAAAIIIIAAAgktQPAm9O5l4xBAAAEEEEAAAQQIXo4BBBBAAAEEEEAAgYQWIHgTeveycQgggAACCCCAAAIEL8cAAggggAACCCCAQEILELwJvXvZOAQQQAABBBBAAAGCl2MAAQQQQAABBBBAIKEFCN6E3r1sHAIIIIAAAggggADByzGAAAIIIIAAAgggkNACBG9C7142DgEEEEAAAQQQQIDg5RhAAAEEEEAAAQQQSGgBgjehdy8bhwACCCCAAAIIIEDwcgwggAACCCCAAAIIJLQAwZvQu5eNQwABBBBAAAEEECB4OQYQQAABBBBAAAEEElqA4E3o3cvGIYAAAggggAACCBC8HAMIIIAAAggggAACCS1A8Cb07mXjEEAAAQQQQAABBAhejgEEEEAAAQQQQACBhBYgeBN697JxCCCAAAIIIIAAAgQvxwACCCCAAAIIIIBAQgsQvAm9e9k4BBBAAAEEEEAAAYKXYwABBBBAAAEEEEAgoQUI3oTevWwcAggggAACCCCAAMHrPAaWrilzzrDtDm+dl6GikipVVdduuxvRSCtPSU5S4LdibXkjrWDbftvsrFQlJSWpuLRq296QRlp9mxaZKiyuUFVNXSOtYNt929SUJOXnZGhlIb+71r3YsVWWdSjjEDALELxmuu8GErwEr+UQIngtav8dQ/D6/Aheux/Ba7erH0nw+g2ZIXIBgjdys41GELwEr+UQIngtagSvT+2/owleuyTBa7cjeP12zGAXIHjtdpzh5ZIG89FD8JrpwoGc4fX5Ebx2P4LXbkfw+u2YwS5A8NrtCF6C13z0ELxmOoLXRxeOJnjtiASv3Y7g9dsxg12A4LXbEbwEr/noIXjNdASvj47gdfoRvE5ASVzD6zdkhsgFCN7IzTYawTW8XMNrOYQIXovaf8dwSYPPjzO8dj+C127HGV6/HTPYBQheux1neDnDaz56CF4zHWd4fXSc4XX6EbxOQM7w+gGZwSRA8JrY/juIM7yc4bUcQgSvRY0zvD61/47mDK9dkuC123GG12/HDHYBgtduxxlezvCajx6C10zHGV4fHWd4nX4ErxOwiZzhnbtwmVavKdIeu+24WbDa2jqVV1QqMyNdyclJftTvZwjmTElOVlpaatTm9Ez06eezlZfTXNv16OSZJipjCV4nI2d4OcNrOYQIXosaZ3h9apzhjYYfwetXbAofWnv4qdf0/pQZeuCvV2wW7Nt5S3TM2dfohYdGafsenU2o73w4XTNmzdWF5xzXMP60C29Sv5166soLTjbNGe1BF1z9N/XbqZd+c/pR0Z464vkI3ojJNh5A8BK8lkOI4LWoEbw+NYI3Gn4Er1/RGryfLVyrwrL4f534bl1bKi8rLaIN/1/BG5yJnTN/qXp17xie5bU8Hn/uTb329scaO/rqhuHzFi5TVlaG2rfJt0wZ9TEEb9RJG29CgpfgtRx9BK9FjeD1qRG80fAjeP2K1uB94N05WlJY5l9AhDOcvld39Wyb/YNRRcUl+vPdT+qNdz5RTnaWTjjyAJ132pFKTUlRELwvvv6+dt6he/h8fosc/fGyMzV4UB+Ne+MDPfbv/4Tz3XvrZeFzZeWVuvOBZ/Xymx+qZV6OfnXMgTr+8P2VlZkePnfPwy/ojXemqLSsXIN23VGn//IQXXztXSooLFaf3j3CuR656w+664F/h5cPHLzv7jrvir/q2svOaHh+1ZpCXXj1nfrLH4era6d2+mT61/rLPf9ScPnFL/bbXScf93P13fG7uTZ8BNsyf9Fyjbz8rIY/vm/sOJWUlumy35ykETfdpw+mzAzX0qtbR11w9nEacsCg8LUbBu8z4ydq4eKV+v3wk8Lnlq0s0O+uHa0Hb79S2c2zftIgwl222ZdzhtepSPASvJZDiOC1qBG8PjWCNxp+BK9f0Rq8E2at0NrSSv8CIpxh3+3bqF1u5g9GXXnjP/TVtwvD6CsoXKdbRj+h3w07Qace//MweP9y7780/Iyjw/+l/68XJ+jrOQs14Zk7wtD7/Ms5umzk3eHP7dq01Mi/PqxZ3yzQpb85UUlJSbr+tod1/hnH6KhD9tG1f/6nJk2ZoYvOOV7dOrfTv19+V8cM2Vf/eXeKPpo6S9deeka4tgF9dwgjuP4SgiB4O3dsqz9+//yjz7yuZ8e/o5ceuVkLl6zUYadeGcbnz/bsp9ffnqLnXn1Xbz19e/j+Gz6mfzlHp/z2Rr31zO3hmeOKyirte8xFuuXqYfr5z3bX48/9R9v16KxWLXI18cPPdMf9z+iDl+5WXm7zjYL3nkde1FffLtBdN14cTr9wyQodduoIfTDu7vA6358yiHCXEbzRANt0DoKX4LUcVwSvRY3g9akRvNHwI3j9itbgXVRQqrLKGv8CIpyhS34zZaWnbDQqPNN62HD95drzdfjBe4bP/envT+ijqV/q+X+OCoN3w2t4VxcUaf/jL9Grj98anl1dubpQB57wuzB4c3Oaa+Ch5+maS07Xbn22C+d67pV3tWL1Wt16zfDwuVEjhuq4w3620Ro2d0nDhmdU//PuJ/rdH/+uT167PzxTfPSZV+vk4w7WycceHJ4xHv/mh7rtut+Gc1ZX1+jX59+gfz9wg3bcrusPhA4/bYSOP3w/nXvKEQrmvfqWB/TBS38PPxxXU1MbxnwQ/8F2jf7nc3rqvuvCM8sbruengjc9Le1HDeoDOcLd9oOXc4bXKfjxnAJ1bpHlnGXbHN6auzSYdxzBa6YLB/LFEz4/bktm9yN47Xb1IxMheINrZY884w965bFbw7OuwWP8fz7U9bc/oimv/uMHwRs8HwTyqBHnaMgBe2wUvEE8B3PttH23ja7nbdu6RXhWN3hu/KO3qEfXDhEFb2VllQYfc1F4KUKwxl/95vqGs6lX3Xy/3npvqnr36rLRnOefeUx42cWmjyCugzPErz3xZ114zZ3huIuH/lIlpeUaPuL2MHYP2nc3dWjbSmMeH68n77lW/XbutcXBW7B23Y8a3D7yAv9BJ4ngdTJe/9JM9evUQnt2b+WcadsbTvDa9xnBa7cjeH12wWiC125I8NrtEil4i9aVaJ+jL9DdN/9OB+yza7hpf//n83plwuQwgjc9w7t8VYEOPvEyPXTHVeGtyjY8w5uZma59jrpAz9w/Mrzmd8NHcJ1w8NydN14UXj6w4eOJ59/SK29N1mN/v6bhjzf9kNjt9z2tmV/NC6/rDeL0pqvODV972z+e1vxFyzT6pku2aIcWFq3X4GMuDM9oX3HjvQ1nqoNoDi6jqL8sIZhslwPO2mzwBtf9fvbFt7r3T5eG77nhJQ3Bzz9msEUL3IIXEbxbgPRTLwmCN3gctkvHJneml+C1HzwEr92O4PXZEbw+P4LX5xeMToQzvMF2BLcAy26eqesuO0tri4p16XV365D9B4XXxQbBG1wy8M87Rig403rvIy+GHzp7/cm/qFlW5kbBG1zDe86lt6qqukZ/vna4WufnhZcIBPewPfPEIeH7BNfVXnPJaerepb1efnOydt2ll4IY/s2Vt4fxmZKSrBa52eHZ1w1vA1Z/JjpY79jR12hA3+3DHTh1xmydftHN+tPV5+mwg/dUEPDBpQoD+/X+0XvmBh9OC85i7z1wl4bbrU3+9EsN/f2f9dyDN4bX97781mTddOfYzQbvx9O+Cs/4BpdNBOt94ImX9fRLbzfE8k8Z+I86zvC6DV+fsVyT561WZmqyThjQVVlpG1/n436DrXgCgte+cwheux3B67MjeH1+BK/PzxO8RWVVqq6p8y8gwhlys1KVlpL8g1FBTF5y7WjNWbA0fC440xsEZE52Mz389Gu6+6EXwrsqBI8gcoOzwfVfRLFi1VoddOKlDR9aC34eedvDenfy9Ib3Ce5dG1w2EHzA7OpbxmjazG/C5zp3aKMxf71CHdu30oVX/03vfTQj/PPgWt0rb7xXfXfqqfNO++99b4OwXbO2SC+P/dNGH0gLrhMOPmhXv8bgsod/3HpZeI3x5h5BsJ596Z90+8jfhpdlBI/gCzSCD98FsRw8Dhq8myZMmqZ/3fvHcB0XXXNnw3qCoP/dH0dr4gefha8N7uTw+sQpDcH7UwYR7rLNvpwzvE7F4ENrr36xVIsLy9Q+J1NH9uuk6H1ninNxMR5O8NqBCV67XTCSa3h9flzSYPcjeO129SOtZ3j97xybGYLLEzIy0sI7DWz6KC2rCGOzQ7tW4e3K6h8zv54XXlP76ev3b3TdbnB/3uBsa6v83I1eH4xbX1Kmyqrq8DZmGz6CM73Bh76CD6ZF+qirq9OatevCD59tbv1bOl/wobzgrG1wS7X/9QjeL/gLwI+t96cM/tfcP/U8wevRkxQEb2V1nZ6ZtkCllTUa0DVfu3dp6Zx12xhO8Nr3E8FrtyN4fXbBaILXbkjw2u0SNXgjEXl94sd6dcLH+nzWHO29+y4N19RGMgevtQkQvDa3hlH1tyVbvb5CL36+WHV1Cs/yBmd7E/1B8Nr3MMFrtyN4fXYEr8+P4PX5BaMT7QxvJCLB5QnBPXV7deukgf17Kzm5qfw/4UiUYvNagtfpuuF9eGcsKdTk+WvC63iD63mD63oT+UHw2vcuwWu3I3h9dgSvz4/g9fk19eD16zGDVYDgtcp9P27TL56ov563Y16mjujTyTn71j2c4LXvH4LXbkfw+uwIXp8fwevzI3j9fsxgEyB4bW4NozYN3g2v5w3uzRvcozdRHwSvfc8SvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtz+9HgDZ6ov543+Oej+3VWm+wM57tsncMJXvt+IXjtdgSvz47g9fkRvD4/gtfvxww2AYLX5vaTwRs8OX1JoT6ev0bN01N04oBuSktJvAvTCV77wUPw2u0IXp8dwevzI3h9fgSv348ZbAIEr83tfwZv8ILxM5doWVG5uuU31yE7tXe+09Y3nOC17xOC125H8PrsCF6fH8Hr8yN4/X7MYBMgeG1uWxS8FVW1embaQpVV1WhwzzbauUOu8922ruEEr31/ELx2O4LXZ0fw+vwIXp8fwev3YwabAMFrc9ui4A1etKK4XOM+XxJ+nd8vd+2iFs3SnO+49QwneO37guC12xG8PjuC1+dH8Pr8CF6/HzPYBJpU8Abf+Rx8jV7w9XeRPorXl6q6puYHX5u36V0aNjfvtMVr9cmCAuVmpumXu3VRaoLcaJrgjfQo+u/rCV67HcHrsyN4fX4Er8+P4PX7MYNNoMkEbxC6I297OFS6/vKzN6u1ZPlqHXv2/+nkYw/SZb85KXxNaVm5Roy6TxMmTQt/7rdzL40edbFa5+eFP29J8Aavq7+et1frbB3Uu51tb21lowhe+w4heO12BK/PjuD1+RG8Pj+C1+/HDDaBJhG8wXdXj/rbWBUUFuuEI/ffbPAGZ3BPvWCU5ixYqqEnH94QvA888bKeGTdRY0dfo6zMdJ1/1R3q0bWDbrzynIiCd8Prefffro12aLftX89L8Np+6YJRBK/djuD12RG8Pj+C1+dH8Pr9mMEm0CSCt7SsQuvWl+iO+59RZkb6D4I3uFThwqv/pvZtWmnd+lJ17tC6IXhPGHadhhwwSMNOPTIUDuL5spH3aObbD4XX5W7pGd5gbP31vEHsBJc2BJc4bMsPgte+9wheux3B67MjeH1+BK/Pj+D1+zGDTaBJBG89zQ13PKqampofBO/Ndz2ub+ct1n1//r1G3HT/RsE76LDhGjViaBi9wePL2fN14nkj9cG4u5WX0zyi4A3GT1tUoE8Wrg0/vHb8rl2UkrTt3p+X4LX90nGG1+5WPzI7KzX8C2dxaZV/siY4Q5sWmSosrlBVTV0T3HrfJhO8Pj+C1+/HDDaBJh+8T77wlh5+6jU9fd9I5eU2D8/e1p/hDa777XPg2brnlku1/979Q+E585fo6LOu0ZtP3aYO7VqpuqY2YvnHJs/XgjWl2q1rCx3et2PE47eWASnJyaqt++6DgDwiFUhSSkqSagzHT6TvlIivT/7+L4rB8ccjcoHwdzf4EK/wi1wvKbwkqaY28n/3R/5eiTki1fDB8cSUYKviKdDkg3fIyVeoW+d22q57p9D9rfenKie7WcNlDMEZ3puuOleH7D8wfH7TM7wrCysi3l/B9bxPfjI/vD/voTt3VI/WzSOeY2sY0DInTcVl1aqu5j+ake6P4N/3LbPTtXpdZaRDeb2kZpkp4RnekrJqPAwC+bnpWldSqeoaw+AmPiQ1Rcprnq41/O6aj4S2LTLMYxmIgFWgyQfvUy9OUFFxSYPfC6+9r/wWuTrqF3vrV8ccpOAa3kMP3EPnnnJE+BrPNbwb7qTl68o0bsZSNUtP0amDulv3X6OO45IGOz/X8NrtgpFc0uDz45IGux+XNNjt6kd2bJXln4QZEIhQoEkEb/C/jWtrazXqzrGqrq7RyN+fpZSUFCVv5n64G17SEFiOeXy8nh3/TniXhmZZGRo+4nbTXRo2t1/GTJoT/vGwwb0i3G1bx8sJXvt+IHjtdgSvzy4YTfDaDQleu10iB++atevCpmiZl+MGCj5on56eqtSUFPdcTPBfgSYRvE+/9Lauv/2RjfZ7cFux4w/f7wfHwqbBW1JarstvuFfvTp4evrZP7x4afdMlatu6RfhzJHdp2PTNHv1oviqqa3TGnt2VEfx/sm3sQfDadxjBa7cjeH12BK/Pj+D1+QWjE+kM76sTPtLNdz0W3vY0eLRr01L/d8npOmjfAVsE9eCTr4SfGxpywB7h68vKKzXw0PPCzjho8G5bNAcv2jKBJhG8W0bx068KLnuoqqpu+MKJ+ld7gvepqQu1rqxKJw3oqrysbe8WZQSv/cgieO12BK/PjuD1+RG8Pr9ECt53Ppyu3/7hDo244GQdd9jPwg9xByfY/jbmWT38t6s0aNcd/yfWxdfepR2366bfnnlM+Nrgw6RffbtAnTu2VW52s/85nhdsuQDBu+VWm32lJ3hf/HyxVhZX6Jj+ndU2e9u7iJ/gtR88BK/djuD12RG8Pj+C1+eXSMEbfMZnx+26hrcu3fBx2ci7tWpNkcaOvlrfzluiq26+X7/Yb6CeemmCiteX6bzTjgzv7R98Juj/bv2nMjPS1LFda23fs3M412kX3qRrLjlNO23fTX/6+xMK7qoyZ8ESvffRDO09cBdddcEpGvPEeE14f5r22G1HXTz0l+rdq4tmfj1Pt/79yfB96x/DR9ymYacepd377aBxb3ygdyZPDz+YP/4/H6pD23yNvPwsffjJF/rXixPCE3oXnn28Dv7Zlp2d9h8J8Z2B4HV6e4L39S+Xa+HaEh26c3t1abnt3amB4LUfPASv3Y7g9dkRvD4/gtfn5wneusWTpLI1P7mApM77SFmtf/I10ZinqrpGu/58qO6++Xc6YJ9dN3q/V976SFfceG/4BVUzv5qnX59/g444eC8ddcg++mjaLD30r1f16uO3KiM9Xb+//h517dQ2PEOc3TwrjNxdDjhLj951dRipwbe7fjL9a132mxPVo0sHXffXh7R42aowmIP4ffSZ18OA/dPV54Vzn3Pprfpi4sMN6/nZsRfpxiuHhmsMbsH6l3v/FX6b7OA9+urx5/6jt96bGl5O8csj9tOnn38dfrPsu8/fFd4FJ9EeBK9zj3qCd+LslfpmVbEO2KGttm/jv9DduSkRDyd4IyZrGEDw2u0IXp8dwevzI3h9fp7grf3wVqlg9k8uIHmvK6RWP30pQTTmWbm6UAee8Ds9ec+16rfzxh88D86Ynnv5X/Th+Hu0YNHyMHjrv501WPzhp40IgzWI3E0vaQie3zR4B/TdvuHbXoPLJb6ZtzgM7eDx9gfT9Mc//1PvvTB6i4L3/Skz9MBfrwjHTpoyU+dd8deGQC5aV6J9jr5Arzx2a3i71kR7ELzOPeoJ3g/nrdbMpUXau0dr9emY51xJ/IcTvHZzgtduR/D67Ahenx/B6/PzBG94ZrZ09U8uIKnL4C07w+ucp/4M799vvkQH7rPxh8tefmuyrrzxH2FIzpg19wfBG1zyENzN4dpLz4g4eO9/bJymfzmnIXgnT/1SQy/7c/heW3KGd8PgnTpjtk6/6OaG4K2orNKAQ4bpuQdvDC+RSLQHwevco57grf+a4QFd8rV715bOlcR/OMFrNyd47XYEr8+O4PX5Ebw+P0/w+t85ujME1/Du0LOzbv7DsI0mDs7arl9fpn/eMWKzwXvQiZfqxKMO0PlnHPNd8Pbqqt+edWzDHD91hje4VepnX3y72eD99PPZOuPi/wZsMOGmlzRsHLzf6PSLbiJ4o3tYJO5snuD9ctk6TZq7Srt0yNU+Pdtsc0gEr32XEbx2O4LXZ0fw+vwIXp9fIgVv/V0arjj/1zr+iP1UV1enJ59/S6P/+ZweufMPGti/d0PwPv/PUWrbqoWee/Vd3faPpxvOogZnbINrdIPbkAW3Qc1vkfOTlzT8VPCWlpUr+HbY4HKH/rv00qsTPtZNd45tuM44uIaX4PUfv012Bk/wzlm1XhNmr1Cv1tk6qPe2d70MwWs/7Aleux3B67MjeH1+BK/PL5GCN9iWTe/DGwTr9Vec03AP3fpLGoI/r79X74bfAzBv4TIFlzjMnrtYu/XZXo/9/ZoweIM7LQzo+92H1oIPr9V/2+umwRtcxnDh1Xdqyqv/CHfMPQ+/oLsffiH85+CDahM/+Ez33HKp9t+7vx5++jV9MGWm7v/L5eHzU2dsfIa3srJKu3FJg/8AT9QZPMG7pLBUr3yxTJ1bNNNhu3TY5ogIXvsuI3jtdgSvz47g9fkRvD6/RAveeo3VBUXhnQ1atczdCKg+eKe/9aCCD4W1yM1WSkryDxCDb2rLzWmutCh8CVVwpjj4Vtm83G3v7k/+o+vHZ+AaXqeuJ3hXF5fr+c+XqE12ho7t39m5kvgPJ3jt5gSv3Y7g9dkRvD4/gtfnl6jB+2Mqm/vQml+QGSwCBK9FbYMxnuAtLq/Wvz5doJzMVP16927OlcR/OMFrNyd47XYEr8+O4PX5Ebw+v6YWvMFlDO98+Fl4CzIejStA8Dr9PcFbWVOrRybPU3pKss7cq4dzJfEfTvDazQleux3B67MjeH1+BK/Pr6kFr1+LGaIlQPA6JT3BG7z1mElzwhUMG7zxjaudy4rLcPZu/sEAACAASURBVILXzkzw2u0IXp8dwevzI3h9fgSv348ZbAIEr82tYZQ3eB+bskBlldU6fY/uykxLca4mvsMJXrs3wWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t6gF7zNTF6mwrFInDuiiFlnpztXEdzjBa/cmeO12BK/PjuD1+RG8Pj+C1+/HDDYBgtfmFrXgHTdjiZavK9fRfTupXW6mczXxHU7w2r0JXrsdweuzI3h9fgSvz4/g9fsxg02A4LW5RS1435i1TAsKSjVkpw7qmt/MuZr4Did47d4Er92O4PXZEbw+P4LX50fw+v2YwSZA8Nrcoha873yzSrNXrtMB27fV9m1znKuJ73CC1+5N8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLWrBO3neGs1YWqi9urdS304tnKuJ73CC1+5N8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLWrB+9nitZqyoEC7dW6pgd3ynauJ73CC1+5N8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLWrBO2t5kd6fs1o7tc/Vvr3aOFcT3+EEr92b4LXbEbw+O4LX50fw+vwIXr8fM9gECF6bW9SCd+7qEr319XL1aN1cP+/d3rma+A4neO3eBK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3KIWvEuLyvTyzKXqmJelI/p0dK4mvsMJXrs3wWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t6gF75qSCj332WK1ap6h43ft7FxNfIcTvHZvgtduR/D67Ahenx/B6/MjeP1+zGATIHhtblEL3vWV1XpyygJlZ6Tq5IHdnKuJ73CC1+5N8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLWrBW11Tp4cmz1XwL9Gz9+rpXE18hxO8dm+C125H8PrsCF6fH8Hr8yN4/X7MYBMgeG1uUQveYKIHP5ir2ro6Dd27p5KTk5writ9wgtduTfDa7Qhenx3B6/MjeH1+BK/fjxlsAgSvzS2qwfvElPkqqazRqQO7qVlGqnNF8RtO8NqtCV67HcHrsyN4fX4Er8+P4PX7MYNNoEkFb21tnerq6pSSkvwDraLiElVUVKlt681/21nx+lJV19SoZd7GX/+7dE2ZTX6DUf+etkgFpZX65W5dlN8s3T1fvCYgeO3SBK/djuD12RG8Pj+C1+dH8Pr9mMEm0GSCNwjdkbc9HCpdf/nZDVqrC4p0xsU3a8HiFeGf9erWUcNOPVJHHbJP+HNpWblGjLpPEyZNC3/ut3MvjR51sVrn54U/RyN4X56xVEvXlenIPh3VIS/LticbYRTBa0cneO12BK/PjuD1+RG8Pj+C1+/HDDaBJhG8r0/8WKP+NlYFhcU64cj9NwrelasL9cJr7+noIYPVPCtTY599Qw899Zreff4uZWWm64EnXtYz4yZq7Ohrwp/Pv+oO9ejaQTdeeU7UgvfNr5Zr3poS/WLH9ureqrltTzbCKILXjk7w2u0IXp8dwevzI3h9fgSv348ZbAJNInhLyyq0bn2J7rj/GWVmpG8UvJuyLV62SkNOvkJjR1+tAX130AnDrtOQAwaFZ32DRxDPl428RzPffkhJSUlROcP73pxV+mr5Ou23XRv1bpdr25ONMIrgtaMTvHY7gtdnR/D6/Ahenx/B6/djBptAkwjeepob7nhUNTU1Pxm8z7/6nv7v1gf13gujld8iR4MOG65RI4aG0Rs8vpw9XyeeN1IfjLtbeTnNoxK8H89fo+lLCrVH91bq32nz1xDbdm9sRxG8dl+C125H8PrsCF6fH8Hr8yN4/X7MYBMgeDdw+2beYp3y21E688QhuvCc48IPuPU58Gzdc8ul2n/v/uEr58xfoqPPukZvPnWbOrRrpcKSKpv8BqM+XVCg979dpYHd8jV4uzbu+eI1QU5WqsoqalRdWxevt0yY9wnuPpeTlaaiUv/xkzAoEWxIZlqylJSk8sqaCEbx0nqBnGZpKiuvUnUtJpEKBJ95bp6ZpnX87kZK1/D6Fs3TzGMZiIBVgOD9Xm7J8tU6/aKbNGjXHXXzVcMa7uQQnOG96apzdcj+A8NXbnqGt7S82mrfMG7GkkK99sVy9evUQkN2ae+eL14TZKSnqLK6VnUEb8TkweUwGenJKq8g2CLGk5SamqzgjtVVFJuFT5npKaqoqg3/Us8jMoHwdzctmb9sRca20aubZW47t990bCZDtzIBglfSt/OW6OxL/6SD9h2gay89Q6kpKQ27KbiG99AD99C5pxwR/lksruGdX1Ci/8xaru75zfWLnbad4OWSBvtvM5c02O2CkdlZqeE19MWcZTNBtmmRqcLiClXVELyRAnJJQ6RiP3x9x1bbzt2I/FvLDFuLQJMI3pqaWtXW1mrUnWNVXV2jkb8/SykpKeG3mn09Z5GOH3qtjjh4L1009HglJ393j95mWRnhPXfHPD5ez45/J7xLQ/Bnw0fcHvW7NCwvKte4mUsU3JIsuDXZtvIgeO17iuC12xG8PrtgNMFrNyR47Xb1IwlevyEzRC7QJIL36Zfe1vW3P7KRTnBbseMP30+vTvhIl99w7w/kgvvw/unq81RSWh4+/+7k6eFr+vTuodE3XdLwBRXRuA/v2tJKPTttUfilE8GXT2wrD4LXvqcIXrsdweuzI3h9fgSvzy8YTfD6DZkhcoEmEbyRs/xwRPBNbFVV1Q1fOFH/imgEb2lltR6fskDN01N0yqDu0VhuXOYgeO3MBK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3BpGRSN4g8nGTJqj5KQkDd2np3NF8RtO8NqtCV67HcHrsyN4fX4Er8+P4PX7MYNNgOC1uUU9eB/+cJ6qamt19l49FfwLdVt4ELz2vUTw2u0IXp8dwevzI3h9fgSv348ZbAIEr80t6sH75CcLtL6iWqcM7KbmGdvGLVsIXvvBQ/Da7Qhenx3B6/MjeH1+BK/fjxlsAgSvzS3qwfvc9MVas75Cx/fvrFbZGc5VxWc4wWt3JnjtdgSvz47g9fkRvD4/gtfvxww2AYLX5hb14H1l5lItKSrT4X06qlPetnGPQoLXfvAQvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtzi3rwvvX1Cs1dvV4H926nnq2znauKz3CC1+5M8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLerB+/6cVZq1fJ327dVGO7XPda4qPsMJXrszwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t6gH75QFBfps8VoN7Jav3Tq3dK4qPsMJXrszwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t6gH74wlhZo8f436dmyhvXq0cq4qPsMJXrszwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t6gH7zcrizXxm5XaoW2O9t++rXNV8RlO8NqdCV67HcHrsyN4fX4Er8+P4PX7MYNNgOC1uUU9eBcWlOj1WcvVtWUzDdm5g3NV8RlO8NqdCV67HcHrsyN4fX4Er8+P4PX7MYNNgOC1uUU9eFcWl+vFz5eoXW6mju7bybmq+AwneO3OBK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3KIevEVllXp66iK1yErXiQO6OFcVn+EEr92Z4LXbEbw+O4LX50fw+vwIXr8fM9gECF6bW9SDt7yqRmM/nq+stBSdtkd356riM5zgtTsTvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtzi3rwBhOOmTQnnHfY4F7OVcVnOMFrdyZ47XYEr8+O4PX5Ebw+P4LX78cMNgGC1+YWk+B9dPI8VdTU6sy9eig9Jdm5stgPJ3jtxgSv3Y7g9dkRvD4/gtfnR/D6/ZjBJkDw2txiErxPfbpA68qr9evduyknM9W5stgPJ3jtxgSv3Y7g9dkRvD4/gtfnR/D6/ZjBJkDw2txiErwvTF+sVesrdGz/zmqTneFcWeyHE7x2Y4LXbkfw+uwIXp8fwevzI3j9fsxgEyB4bW4xCd5Xv1imxYWlOmyXDurcoplzZbEfTvDajQleux3B67MjeH1+BK/Pj+D1+zGDTYDgtbnFJHjf/nqFvl29Xgft0E692mQ7Vxb74QSv3ZjgtdsRvD47gtfnR/D6/Ahevx8z2AQIXptbTIL3g7mr9cWyIg3u2UY7d8h1riz2wwleuzHBa7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3mATvp4vWaurCAu3epaUGdM13riz2wwleuzHBa7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3mARvcHY3OMvbp2Oe9u7R2rmy2A8neO3GBK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3GISvN+uKtbbs1dq+zY5OmCHts6VxX44wWs3JnjtdgSvz47g9fkRvD4/gtfvxww2AYLX5haT4F20tlSvfblMXVo206E7d3CuLPbDCV67McFrtyN4fXYEr8+P4PX5Ebx+P2awCRC8NreYBO/K9RV6cfpitc3O0DH9OztXFvvhBK/dmOC12xG8PjuC1+dH8Pr8CF6/HzPYBAhem1tMgnddWZWemrpQeVlpOmlAV+fKYj+c4LUbE7x2O4LXZ0fw+vwIXp8fwev3YwabAMFrc4tJ8FZU1+rRj+YpIzVFZ+zZ3bmy2A8neO3GBK/djuD12RG8Pj+C1+dH8Pr9mMEm0KSCt7a2TnV1dUpJSf6BVvDcyjVr1To/T6kpKT94vnh9qapratQyL2ej55auKbPJ/8ioMZPmhM8MG9wrqvPGYjKC165K8NrtCF6fHcHr8yN4fX4Er9+PGWwCTSZ4g9AdedvDodL1l5+9kdY7H07X5Tfcq9Ky8vDPr/v9WTrpqAPCfw7+bMSo+zRh0rTw534799LoUReHYRw8oh28Yz+er/KqGp2+Z3dlpv4wvG27OTajCF67K8FrtyN4fXYEr8+P4PX5Ebx+P2awCTSJ4H194sca9bexKigs1glH7r9R8JaVV2q/4y7Wheccp1OP/7kmfvCZLrl2tF5/8i/q3KGNHnjiZT0zbqLGjr5GWZnpOv+qO9SjawfdeOU5MQnepz9dqKLyqvAa3uBa3q35QfDa9w7Ba7cjeH12BK/Pj+D1+RG8fj9msAk0ieAtLavQuvUluuP+Z5SZkb5R8AZnd3/7hzs07Y0xSk//LjAPP21EGL+nHv8LnTDsOg05YJCGnXpk+FwQz5eNvEcz335ISUlJUT/D++LnS7SyuFzH9OustjkZtr0ap1EErx2a4LXbEbw+O4LX50fw+vwIXr8fM9gEmkTw1tPccMejqqmp2Sh4nx43UQ8/9apeeezWBsGLrrlT3bt00O+Hn6RBhw3XqBFDw+gNHl/Onq8TzxupD8bdrbyc5lEP3tdmLdeighIN2am9uuY3t+3VOI0ieO3QBK/djuD12RG8Pj+C1+dH8Pr9mMEm0OSDN7hk4bW3P9azY65vEAyu581ulqXrfn+m+hx4tu655VLtv3f/8Pk585fo6LOu0ZtP3aYO7Vqprs4G/2OjXvxssaYvKtSxu3ZWvy4tojt5lGdLSlLUtz/KS9yqp8PPuXuSJEX598+5om1mOMeeb1fh5/fzzcBoBCIXaPLBuyVneG+66lwdsv/AUHfTM7zLCqJ7l4YP567WjKVF2qdna/Xp+N0H47bWR6vcDK0rrVJVde3WusStdl3BGd7Ab2Xhdx+U5BGZQHZWqpKUpOKyqsgG8upQoHVeporWV6iqhr8xRHpIBGd4W2ZnaFURv7uR2tW/vkN+lnUo4xAwCzT54K2/hvez/zygtLTUEHLIyVfojBMPabiG99AD99C5pxwRPhfra3inLVqrTxYWaLcuLTWwa755x8ZjIJc02JW5pMFuF4wMgzcpScWlBK9Fsk2LTBUWE7wWOy5psKhtPKZjK4LXr8gMkQo0ieCtqalVbW2tRt05VtXVNRr5+7OUkpKi5OQkBR9oG3TYbzTigpN1ymbu0jDm8fF6dvw74V0ammVlaPiI22N6l4ZZy9fp/TmrtHOHXA3u2SbS/RnX1xO8dm6C125H8PrsgtEEr92Q4LXb1Y8keP2GzBC5QJMI3qdfelvX3/7IRjrBbcWOP3y/8M+Ce+wGH1Srf/zf707XycceHP5YUloe3qP33cnTw5/79O6h0Tddoratv7u+Ntr34Z27ar3emr1Cvdpk66Ad2kW+R+M4guC1YxO8djuC12dH8Pr8CF6fXzCa4PUbMkPkAk0ieLeEJTgLvHxVgdq2atFwacOG44qKS1RVVd3whRP1z0U7eJcUluqVL5apc4tmOmyXDluy9EZ7DcFrpyd47XYEr8+O4PX5Ebw+P4LX78cMNgGC1+bWMCrawbt6fYWen75YrZun67hduzhXF9vhBK/dl+C12xG8PjuC1+dH8Pr8CF6/HzPYBAhem1vMgnd9RbWe/GSBcjJS9euB3Zyri+1wgtfuS/Da7Qhenx3B6/MjeH1+BK/fjxlsAgSvzS1mwRvc4uvhj+YpPSVZZ+7Vw7m62A4neO2+BK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3GIWvMHEYybNCecfNriXc3WxHU7w2n0JXrsdweuzI3h9fgSvz4/g9fsxg02A4LW5xTR4H5+yQKWV1TptUDdlpX93b+Ct8UHw2vcKwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t5gG77NTF2ltWaVOHNBFLbLSnSuM3XCC125L8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLabBO27GEi1fV66j+3ZSu9xM5wpjN5zgtdsSvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtzi2nwvjFrmRYUlOqQnTqoW34z5wpjN5zgtdsSvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtzi2nwvvvtKn29Yp32366NdmiX61xh7IYTvHZbgtduR/D67Ahenx/B6/MjeP1+zGATIHhtbjEN3o/mrdHnSwu1V/dW6tvpu68w3hofBK99rxC8djuC12dH8Pr8CF6fH8Hr92MGmwDBa3OLafB+tnitpiwo0K6dW2pQt3znCmM3nOC12xK8djuC12dH8Pr8CF6fH8Hr92MGmwDBa3OLafB+tXyd3puzSju1y9W+27VxrjB2wwleuy3Ba7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3mAbvvDUlevOr5erZqrkO3rG9c4WxG07w2m0JXrsdweuzI3h9fgSvz4/g9fsxg02A4LW5xTR4lxaV6eWZS9UxL0tH9OnoXGHshhO8dluC125H8PrsCF6fH8Hr8yN4/X7MYBMgeG1uMQ3eNSUVeu6zxcpvnqFf7trZucLYDSd47bYEr92O4PXZEbw+P4LX50fw+v2YwSZA8NrcYhq8JZU1emLKfDVPT9Epg7o7Vxi74QSv3ZbgtdsRvD47gtfnR/D6/Ahevx8z2AQIXptbTIO3trZOD344V8G/WM/eq6dzhbEbTvDabQleux3B67MjeH1+BK/Pj+D1+zGDTYDgtbnFNHiDyf/54VzV1NZp6N49lZyc5FxlbIYTvHZXgtduR/D67Ahenx/B6/MjeP1+zGATIHhtbjEP3uCShuDShuCShuDShq3xQfDa9wrBa7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3mAdv8KG14MNrwYfWgg+vbY0Pgte+Vwheux3B67MjeH1+BK/Pj+D1+zGDTYDgtbnFPHiD25IFtyc7sk9HdcjLcq4yNsMJXrsrwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t5gHb/DFE8EXUPx8x/bq0aq5c5WxGU7w2l0JXrsdweuzI3h9fgSvz4/g9fsxg02A4LW5xTx43/92pWatKNbPerXRju1znauMzXCC1+5K8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLebBO2VBgT5bvFZ7dMtX/84tnauMzXCC1+5K8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLebB+/mSQn00f436dWqhPbu3cq4yNsMJXrsrwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t5gH7+wV6/TOt6vUu12u9tuujXOVsRlO8NpdCV67HcHrsyN4fX4Er8+P4PX7MYNNgOC1ucU8eBcUlOiNWcvVPb+5frFTe+cqYzOc4LW7Erx2O4LXZ0fw+vwIXp8fwev3YwabAMG7gduylQVq17rlZr/ZrHh9qapratQyL2cj6aVrymzy/2PU8nXlGjdjiTrkZerIPp1i8h7eSQleuyDBa7cjeH12BK/Pj+D1+RG8fj9msAkQvJIefeZ1Pf7cm6qqrlZVVbWOO+xnuuw3J4WipWXlGjHqPk2YNC38ud/OvTR61MVqnZ8X/hyr4F1bWqlnpy1Sy2bpOmG3Lra9G+NRBK8dmOC12xG8PjuC1+dH8Pr8CF6/HzPYBJp88H7x9Xyd9JuReuiOq7THbjtq7sJlOuqMP+iJe65V/5176YEnXtYz4yZq7OhrlJWZrvOvukM9unbQjVeeE9PgLaus1mNTFqhZWopO3aO7be/GeBTBawcmeO12BK/PjuD1+RG8Pj+C1+/HDDaBJh+8H02bpXMuvVWvPn6runZqFyr+7NiLdOVvT9ZRh+yjE4ZdpyEHDNKwU48Mn3t94se6bOQ9mvn2Q0pKSorZGd7gvcZMmhO+57DBvWx7N8ajCF47MMFrtyN4fXYEr8+P4PX5Ebx+P2awCTT54K2srNLQ3/9FX327UBcPPV7rS8v0xsQpeuSuq5Wb3UyDDhuuUSOGhtEbPL6cPV8nnjdSH4y7W3k5zWMavA9PnqeqmlqdtWcPpaUm2/ZwDEcRvHZcgtduR/D67Ahenx/B6/MjeP1+zGATaPLBG55JfXy8xr3xgbIyMzTz63k695QjdNHQ45WSnKw+B56te265VPvv3T8UnjN/iY4+6xq9+dRt6tCulYrLqm3yWzDqwffmqKi8Suf+rJdyM9O2YER8X9IsI0UVVbWqqa2L7xsnwLslJ0nNMlK1vjx2x08CMP3oJqSnJispSeHxxyNygeaZqSqvqFYNv7oR4wW/u1kZqSrhdzdiu/oBOVmp5rEMRMAq0OSD972PPtfwEbfrw/H3hGd0J02Zqd/98e+6fPhJ+tUxB4VneG+66lwdsv/A0HjTM7zFpVVW+/857vGP5mtFcYVO27O72uZk/M/Xx/sFzYL/aFbWqJbgjZg+uBymWWaKSmL4F6aIF7UNDUhPC/6PR5Iqq2q2oVVvPUvld9e+L5KTk5SVnkLw2gmV02zrO4Hj2ByGbiMCTT54/zbmWU14f6peeuTmhl12wdV/U/OsTP352uHhNbyHHrhHeNY3eMTzGt5XvlimJYWlOqJPR3XMy9rqDikuabDvEi5psNsFI7OzUsNr6GP5F07fCrfu0W1aZKqwuEJVnOKNeEdxSUPEZD8Y0LHV1vffM/9WMcPWLtDkg/eVtz7SFTfeq3/cepn23aOvFi1dpcNOvVJXnP9rnfWrQ8PLHZ4d/054l4ZmWRnh2eB43KUhOHDe+nqF5q5er4N7t1PP1tlb3bFE8Np3CcFrtyN4fXbBaILXbkjw2u3qRxK8fkNmiFygyQdv8L/j73vsJb3w6vsqKCxWTnaWjj5ksC44+zilpaaopLRcl99wr96dPD3U7dO7h0bfdInatm4R/hyr+/AGc0+as0pfLl+nwb3aaOf2uZHv3RiPIHjtwASv3Y7g9dkRvD4/gtfnF4wmeP2GzBC5QJMP3g3Jli5frfZtW232m9aKikvCL6Wo/8KJ+nGxDN5PFhZo2qK1Gtg1X7t1aRn53o3xCILXDkzw2u0IXp8dwevzI3h9fgSv348ZbAIEr82tYVQsg3fG0iJNnrdafTrmae8erZ0rjf5wgtduSvDa7Qhenx3B6/MjeH1+BK/fjxlsAgSvzS0uwTt7ZbHe+Waltm+bowO2b+tcafSHE7x2U4LXbkfw+uwIXp8fwevzI3j9fsxgEyB4bW5xCd6FBSV6fdZydW3ZTEN27uBcafSHE7x2U4LXbkfw+uwIXp8fwevzI3j9fsxgEyB4bW5xCd4VxeV66fMlapeTqaP7dXKuNPrDCV67KcFrtyN4fXYEr8+P4PX5Ebx+P2awCRC8Nre4BG9RaZWenrZQeVlpOmlAV+dKoz+c4LWbErx2O4LXZ0fw+vwIXp8fwev3YwabAMFrc4tL8FZU1ejRj+crMy1Fp+/R3bnS6A8neO2mBK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3OISvMGbjJk0J3yvYYN7OVca/eEEr92U4LXbEbw+O4LX50fw+vwIXr8fM9gECF6bW9yC99GP5qmiulZn7NlDGanJztVGdzjBa/ckeO12BK/PjuD1+RG8Pj+C1+/HDDYBgtfmFrfgferThVpXXqVf7d5VuZlpztVGdzjBa/ckeO12BK/PjuD1+RG8Pj+C1+/HDDYBgtfmFrfgfXH6Yq1cX6Fj+3dWm+wM52qjO5zgtXsSvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtzi1vwvvblMi1aW6rDdu6gzi2bOVcb3eEEr92T4LXbEbw+O4LX50fw+vwIXr8fM9gECF6bW9yC9+3ZK/TtqvU6cId22q5NtnO10R1O8No9CV67HcHrsyN4fX4Er8+P4PX7MYNNgOC1ucUteD+cu1ozlxVpn56ttUuHPOdqozuc4LV7Erx2O4LXZ0fw+vwIXp8fwev3YwabAMFrc4tb8E5dWKBPF63V7l1aakDXfOdqozuc4LV7Erx2O4LXZ0fw+vwIXp8fwev3YwabAMFrc4tb8H6xrEgfzF2tPh3ytHfP1s7VRnc4wWv3JHjtdgSvz47g9fkRvD4/gtfvxww2AYLX5ha34A2u3w2u4w2u3w2u492aHgSvfW8QvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtzi1vwLl5bqle/XKYuLZvp0J07OFcb3eEEr92T4LXbEbw+O4LX50fw+vwIXr8fM9gECF6bW9yCd9X61oFnaAAAIABJREFUCr0wfbHaZmfomP6dnauN7nCC1+5J8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NLW7BG3zLWvBta8G3rAXftrY1PQhe+94geO12BK/PjuD1+RG8Pj+C1+/HDDYBgtfmFrfgrayp1SOT5ykjNVln7NnDudroDid47Z4Er92O4PXZEbw+P4LX50fw+v2YwSZA8Nrc4ha8wRuNmTQnfL9hg3s5Vxvd4QSv3ZPgtdsRvD47gtfnR/D6/Ahevx8z2AQIXptbXIN37MfzVV5VozP26K6MtBTniqM3nOC1WxK8djuC12dH8Pr8CF6fH8Hr92MGmwDBa3OLa/A+PXWhisqqdNJuXZXXLM254ugNJ3jtlgSv3Y7g9dkRvD4/gtfnR/D6/ZjBJkDw2tziGrwvfb5EK4rLdUy/Tmqbk+lccfSGE7x2S4LXbkfw+uwIXp8fwevzI3j9fsxgEyB4bW5xDd7Xv1ymhWtLNWSn9uqa39y54ugNJ3jtlgSv3Y7g9dkRvD4/gtfnR/D6/ZjBJkDw2tziGrwTv1mpb1YWa//t22qHtjnOFUdvOMFrtyR47XYEr8+O4PX5Ebw+P4LX78cMNgGC1+YW1+CdPH+NZiwp1F49WqtvxzzniqM3nOC1WxK8djuC12dH8Pr8CF6fH8Hr92MGmwDBa3OLa/BOW7RWnyws0G5dWmpg13zniqM3nOC1WxK8djuC12dH8Pr8CF6fH8Hr92MGmwDBu4nbmrXrwj9p1TJ3o2eK15equqZGLfM2vqRg6Zoym3wEo75cvk6T5qzSzu1zNbhXmwhGxvalBK/dl+C12xG8PjuC1+dH8Pr8CF6/HzPYBAheSbW1dXrwyZf16DOvq6CwWM2yMjXl1X+EoqVl5Rox6j5NmDQt/Lnfzr00etTFap3/3aUF8QjeuavX662vV6hn62wd3LudbU/HYBTBa0cleO12BK/PjuD1+RG8Pj+C1+/HDDYBglfSbf94Wi+89p6Gn3GMDjtoT1VWVal9m+8uHXjgiZf1zLiJGjv6GmVlpuv8q+5Qj64ddOOV58QteJcWlenlmUvVKS9Lh/fpaNvTMRhF8NpRCV67HcHrsyN4fX4Er8+P4PX7MYNNoMkH76o1hTrgl7/TqBFDddxhP/uB4gnDrtOQAwZp2KlHhs+9PvFjXTbyHs18+yElJSXF5QzvmvUVem76YrXOztBx/Tvb9nQMRhG8dlSC125H8PrsCF6fH8Hr8yN4/X7MYBNo8sH71ntTdfG1d+nXxxyk2XMXKyMjTUcfso+OPmRwKDrosOFhDAfRGzy+nD1fJ543Uh+Mu1t5Oc21vKDcJh/BqOKKaj0xZb6yM1J16qDuEYyM7Uvzc9O1rrRK1dV1sX2jBJw9OTlJrXLTtaqwIgG3Lvab1DwrJfwL5/rS6ti/WQK+Q6u8DBWtr1R1Db+7ke7elJQktcxO1+oifncjtat/ffv8recLlKzbwLhtT6DJB+/jz72pm+96TBeec5x69+yir+cu0t//+bz+fO1wHX7Qnupz4Nm655ZLtf/e/cO9O2f+Eh191jV686nb1KFdK9XWxf4/GFU1tfrTK7OUnpqsEYfttNUcZclJSQo2v06xN9hqNjpKC0mSwmCLx/ETpSVvVdMkKRDk2LPulO9+d/nNtfjxu2tR23hMcPzxQCDeAgTvc2/qqRcn6KVHbm6wv+rm+1VeXqm/3XBheIb3pqvO1SH7Dwyf3/QMbzw+tBa875hJc8L3Hza4V7yPkR99Py5psO8KLmmw2wUjs7NSw78wFJdW+SZqoqPbtMhUYXGFqjjDG/ERwCUNEZP9YEDHVln+SZgBgQgFmnzwvvPhdP32D3foszcfVFpqSsh3+Q33qqy8Qnff/DsF1/AeeuAeOveUI8LnGuMa3uB9H/94vkqranTaoG7KSk+NcDfH5uUEr92V4LXbEbw+u2A0wWs3JHjtdvUjCV6/ITNELtDkg3fd+lIdfOJlOvPEITr/zGM08+t5OuW3N+qaS07XKccdrDGPj9ez498J79LQLCtDw0fcHve7NAS79dlpi7S2tFIn7NZFLZulR76nYzCC4LWjErx2O4LXZ0fw+vwIXp9fMJrg9RsyQ+QCTT54A7IPP/lCF187OrznbvAIQnfEhacoNSVFJaXl4RnfdydPD5/r07uHRt90idq2bhH+HK9LGsbPXKJlReU6qm8ntc/dOi74J3gj/4WrH0Hw2u0IXp8dwevzI3h9fgSv348ZbAIE7/duwbeorVi1Vi3zssMvntj0UVRcoqqq6oYvnKh/Pl7B+59ZyzW/oESH7NRe3fKb2/Z2lEcRvHZQgtduR/D67Ahenx/B6/MjeP1+zGATIHhtbg2j4hW87367Sl+vWKf9tmuj3u02/tpj5yaYhxO8ZjoRvHY7gtdnR/D6/Ahenx/B6/djBpsAwWtzi3vwfjx/jaYvKdSe3VupX6fvLqdo7AfBa98DBK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3OIevNMXr9XHCwq0a+eWGtTtu689buwHwWvfAwSv3Y7g9dkRvD4/gtfnR/D6/ZjBJkDw2tziHrxfLV+n9+as0k7tcrTvdm2dq47OcILX7kjw2u0IXp8dwevzI3h9fgSv348ZbAIEr80t7sE7b02J3vxquXq0aq6f79jeueroDCd47Y4Er92O4PXZEbw+P4LX50fw+v2YwSZA8Nrc4h68y4rKNH7mUnXMy9IRfTo6Vx2d4QSv3ZHgtdsRvD47gtfnR/D6/Ahevx8z2AQIXptb3IO3oKRC//5ssVo1z9Dxu3Z2rjo6wwleuyPBa7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3uAdvSWWNnpgyX83TU3TKoO7OVUdnOMFrdyR47XYEr8+O4PX5Ebw+P4LX78cMNgGC1+YW9+Ctra3Tgx/ODe/fes7ePZ2rjs5wgtfuSPDa7Qhenx3B6/MjeH1+BK/fjxlsAgSvzS3uwRu84UOT56q6pk5D9+6p5OQk58r9wwleuyHBa7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3RgneJ6YsUElldXhJQ3BpQ2M/CF77HiB47XYEr8+O4PX5Ebw+P4LX78cMNgGC1+bWKMEbfGgt+PBa8KG14MNrjf0geO17gOC12xG8PjuC1+dH8Pr8CF6/HzPYBAhem1ujBO/LM5dqaVFZeFuy4PZkjf0geO17gOC12xG8PjuC1+dH8Pr8CF6/HzPYBAhem1ujBO9bXy3X3DUl4RdPBF9A0dgPgte+Bwheux3B67MjeH1+BK/Pj+D1+zGDTYDgtbk1SvC+/+0qzVqxTvv2aq2d2uc5V+4fTvDaDQleux3B67MjeH1+BK/Pj+D1+zGDTYDgtbk1SvBOWVCgzxav1aBu+dq1c0vnyv3DCV67IcFrtyN4fXYEr8+P4PX5Ebx+P2awCRC8NrdGCd4ZSwo1ef4a9evYQnv2aOVcuX84wWs3JHjtdgSvz47g9fkRvD4/gtfvxww2AYLX5tYowTt75Tq9880q9W6Xq/22a+NcuX84wWs3JHjtdgSvz47g9fkRvD4/gtfvxww2AYLX5tYowbugoFRvzFqmbvnNdMhOHZwr9w8neO2GBK/djuD12RG8Pj+C1+dH8Pr9mMEmQPDa3BoleFesK9dLM5aofW6mjurbybly/3CC125I8NrtCF6fHcHr8yN4fX4Er9+PGWwCBK/NrVGCt7CsUs9MXaSWWek6YUAX58r9wwleuyHBa7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3RgnesspqPTZlgbLSU3XaoG7OlfuHE7x2Q4LXbkfw+uwIXp8fwevzI3j9fsxgEyB4bW6NErzBm46ZNCd872GDezlX7h9O8NoNCV67HcHrsyN4fX4Er8+P4PX7MYNNgOC1uTVa8D4yeZ4qa2p11l49lJaS7Fy9bzjBa/cjeO12BK/PjuD1+RG8Pj+C1+/HDDYBgtfm1mjB+69PF6i4vFonD+ym7IxU5+p9wwleux/Ba7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3Rgve5z9bpNUllTquf2e1zs5wrt43nOC1+xG8djuC12dH8Pr8CF6fH8Hr92MGmwDBa3NrtOB99YtlWlxYqsN36aBOLZo5V+8bTvDa/Qheux3B67MjeH1+BK/Pj+D1+zGDTYDg3UK34vWlqq6pUcu8nI1GLF1TtoUzROdlE2av0JxV63XwDu3Us012dCY1zkLwGuEkEbx2O4LXZ0fw+vwIXp8fwev3YwabAMG7gduS5at17Nn/p5OPPUiX/eak8JnSsnKNGHWfJkyaFv7cb+deGj3qYrXOzwt/jnfwTpq7Sl8uW6d9e7XRTu1zbXs9SqMIXjskwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELzfuwVncE+9YJTmLFiqoScf3hC8Dzzxsp4ZN1FjR1+jrMx0nX/VHerRtYNuvPKcRgneTxYWaNqitRrYNV+7dWlp2+tRGkXw2iEJXrsdweuzI3h9fgSvz4/g9fsxg02A4JXCSxUuvPpvat+mldatL1XnDq0bgveEYddpyAGDNOzUI0Ph1yd+rMtG3qOZbz+kpKSkuJ/hnbm0SB/OW62+HfO0V4/Wtr0epVEErx2S4LXbEbw+O4LX50fw+vwIXr8fM9gECF5JN9/1uL6dt1j3/fn3GnHT/RsF76DDhmvUiKFh9AaPL2fP14nnjdQH4+5WXk7zuAfvN6uKNXH2Sm3fNkcHbN/WttejNIrgtUMSvHY7gtdnR/D6/Ahenx/B6/djBptAkw/eJ194Sw8/9Zqevm+k8nKbh2dv68/w1tXVqc+BZ+ueWy7V/nv3D4XnzF+io8+6Rm8+dZs6tGul0opqm7xx1NxV6/XvqYvDD6z9ckBn4yzRGZaRlqKq6lrV1tVFZ8ImNEuSkpSZnqyyypomtNXR29TUlGQlSaqqqY3epE1opsz0FFVW8btr2eXB/9nLSEtWOb+7Fr5wTLNGvoe8eeEM3KYFmnzwDjn5CnXr3E7bde8U7si33p+qnOxmDZcxBGd4b7rqXB2y/8Dw+U3P8BaWVMX1AFheVK6nPlmgDnlZOmlg17i+96ZvlpOVqtKKGtXUEryR7ojkJCknK01FpfE9fiJd59b6+sy0ZCkpiegw7qCcZmkqLatSDb+6EQumJEnNs9K0jt/diO3qB7RonmYey0AErAJNPnifenGCiopLGvxeeO195bfI1VG/2Fu/OuYgBdfwHnrgHjr3lCPC1zT2NbxFZVV6eupC5WWm6aTdGzd4uaTB+mvHbcnsct+NzM5KDa+hLyY6TJRtWmSqsLhCVRRvxH5c0hAx2Q8GdGyV5Z+EGRCIUKDJB++mXhte0hA8N+bx8Xp2/DvhXRqaZWVo+IjbG/UuDeXVNRr70XxlpqXo9D26R7i7o/tygtfuyTW8djuC12cXjCZ47YYEr92ufiTB6zdkhsgFCN5NzDYN3pLScl1+w716d/L08JV9evfQ6JsuUdvWLcKf430f3jDCJ80J33vY4F6R7/EojiB47ZgEr92O4PXZEbw+P4LX5xeMJnj9hswQuQDBu4VmwWUPVVXVDV84UT+sMYL30Y/mq6K6Rmfs2V0ZqSlbuAXRfxnBazcleO12BK/PjuD1+RG8Pj+C1+/HDDYBgtfm1jCqMYL3qakLta6sSr8a0FW5WY138T/Baz94CF67HcHrsyN4fX4Er8+P4PX7MYNNgOC1uTVq8L44fbFWrq/QMf07q212hnML7MMJXrsdwWu3I3h9dgSvz4/g9fkRvH4/ZrAJELw2t0YN3te/XK6Fa0t06M4d1KVlM+cW2IcTvHY7gtduR/D67Ahenx/B6/MjeP1+zGATIHhtbo0avME3rQXfuHbgDm21XZsc5xbYhxO8djuC125H8PrsCF6fH8Hr8yN4/X7MYBMgeG1ujRq8H85brZlLi7RPz9bapUOecwvswwleux3Ba7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3Rg3eqQsL9OmitRrQNV+7d2np3AL7cILXbkfw2u0IXp8dwevzI3h9fgSv348ZbAIEr82tUYP3y2XrNGnuKu3SIVf79Gzj3AL7cILXbkfw2u0IXp8dwevzI3h9fgSv348ZbAIEr82tUYN3zqr1mjB7hbZrna0De7dzboF9OMFrtyN47XYEr8+O4PX5Ebw+P4LX78cMNgGC1+bWqMG7uLBUr36xTJ1bNNNhu3RwboF9OMFrtyN47XYEr8+O4PX5Ebw+P4LX78cMNgGC1+bWqMG7urhcz3++RG2yM3Rs/87OLbAPJ3jtdgSv3Y7g9dkRvD4/gtfnR/D6/ZjBJkDw2twaNXiLy6v1r08XKDczVb/avZtzC+zDCV67HcFrtyN4fXYEr8+P4PX5Ebx+P2awCRC8NrdGDd7Kmlo9MnmeMlKSdcZePZxbYB9O8NrtCF67HcHrsyN4fX4Er8+P4PX7MYNNgOC1uTVq8AZvPmbSnHANwwb3cm6BfTjBa7cjeO12BK/PjuD1+RG8Pj+C1+/HDDYBgtfm1ujB+9jH81VWVaPT9+iuzLQU51bYhhO8NrdgFMFrtyN4fXYEr8+P4PX5Ebx+P2awCRC8NrdGD95npi5SYVmlThrQRXlZ6c6tsA0neG1uBK/drX5kdlaqkpKSVFxa5Z+sCc7QpkWmCosrVFVT1wS33rfJBK/Pj+D1+zGDTYDgtbk1evC+NGOJVqwr19F9O6ldbqZzK2zDCV6bG8FrdyN4/XbBDASv3ZHgtdvVj+zYKss/CTMgEKEAwRsh2KYvX7qmzDmDbfgbs5ZpQUGphuzUXl3zm9smcY4ieO2AXNJgtwtGcobX50fw2v0IXrsdweu3Ywa7AMFrtwtHNlbwvvPNSs1eWawDtm+r7dvmOLfCNpzgtbkFowheux3B67MLRhO8dkOC125H8PrtmMEuQPDa7Ro1eCfPW6MZSwu1V/dW6tuphXMrbMMJXpsbwWt3qx/JGV6fIcFr9yN47XYEr9+OGewCBK/drlGDd9ritfpkQYF27dxSg7rlO7fCNpzgtbkRvHY3gtdvxxlenyHB6/MLRnMNr9+QGSIXIHgjN9toRGNd0jBr+Tq9P2eVdmqfq317tXFuhW04wWtzI3jtbgSv347g9RkSvD4/gtfvxww2AYLX5tYwqrGCd+7q9Xrr6xXq2TpbB/du59wK23CC1+ZG8NrdCF6/HcHrMyR4fX4Er9+PGWwCBK/NrdGDd0lRmV6ZuVQd87J0RJ+Ozq2wDSd4bW4Er92N4PXbEbw+Q4LX50fw+v2YwSZA8NrcGj14C0or9e9pi9S6ebqO27WLcytswwlemxvBa3cjeP12BK/PkOD1+RG8fj9msAkQvDa3Rg/eypo6PTJ5briO0/borqxG+Hphgtd+8HBbMrtdMJK7NPj8uEuD3Y/gtdvVj+RDa35DZohcgOCN3GyjEY11DW+wiImzV+qbVcUa2C1fu3Vu6dySyIcTvJGb1Y8geO12BK/PjjO8Pj+C1+fHGV6/HzPYBAhem1ujn+ENFrCsqEzjZy5VdkaqTh7YzbklkQ8neCM3I3jtZhuO5Ayvz5EzvHY/gtduVz+SM7x+Q2aIXIDgjdxsoxGNeYY3WMiTnyzQ+orq8INrwQfY4vkgeO3anOG123GG12fHGV6fH8Hr8+MMr9+PGWwCBO/3bkXFJaqoqFLb1pv/1rLi9aWqrqlRy7yNv8a3sYN3+uK1+nhBQaPcnozgtf3SBaMIXrsdweuzI3h9fgSvz4/g9fsxg02gyQfv6oIinXHxzVqweEUo2KtbRw079Ugddcg+4c+lZeUaMeo+TZg0Lfy53869NHrUxWqdnxf+3NjBW15dq8c+miclSWfu2VNpKUm2I8EwiuA1oH0/hOC12xG8PjuC1+dH8Pr8CF6/HzPYBJp88K5cXagXXntPRw8ZrOZZmRr77Bt66KnX9O7zdykrM10PPPGynhk3UWNHXxP+fP5Vd6hH1w668cpztorgDRbxn1nLNb+gRHt1b6W+nTZ/htp2ePz0KILXrkrw2u0IXp8dwevzI3h9fgSv348ZbAJNPng3ZVu8bJWGnHyFxo6+WgP67qAThl2nIQcMCs/6Bo/XJ36sy0beo5lvP6SkpKRGP8MbrGlhQYlen7VceZlpOmn3rrYjwTCK4DWgfT+E4LXbEbw+O4LX50fw+vwIXr8fM9gECN5N3J5/9T39360P6r0XRiu/RY4GHTZco0YMDaM3eHw5e75OPG+kPhh3t/JymmtVUYVNPoqj6iQ98sFclVbV6LhdO6tDnD681iI7XevLqlRdE6yARyQCyUlJapmTpjXrKiMZxmu/F2iWkRL+hbOkvBoTg0DLnHQVl1Spupbf3Uj5gr+s5jVPU0Exv7uR2tW/vk1ehnUo4xAwCxC8G9B9M2+xTvntKJ154hBdeM5xqqurU58Dz9Y9t1yq/ffuH75yzvwlOvqsa/TmU7epQ7tWqqquNeNHc+C7s1fq/W9Xq1/nPB3Zr1M0p/7RuYIzHTW1Cp14RCaQlBR8cC1Z1QEgj4gFkpO/u1a9lmCL2C4YkJqSrJraWvGrGzkfv7uRm206Ii012T8JMyAQoQDB+z3YkuWrdfpFN2nQrjvq5quGKSXlu1/I4AzvTVedq0P2Hxj+vOkZ3sb+0Fr9/g5uTRbcoiw4c3jmXt2Vmhz7f6FwSUOEv20bvJxLGux2wUjuw+vz4z68dj8uabDb1Y/kPrx+Q2aIXIDglfTtvCU6+9I/6aB9B+jaS89QakpKg2RwDe+hB+6hc085IvyzrfEa3vrFvvLFMi0pLNXgXm20c/vcyI+GCEcQvBGCEbx2sE1GErw+SoLX7kfw2u0IXr8dM9gFmnzwfj1nkY4feq2OOHgvXTT0eCV/f2a0WVZGeM/dMY+P17Pj3wnv0hD82fARt291d2mo3/1zV6/XW1+vUOvsDB3Xv7P9qNjCkQTvFkJt5mWc4bXbcYbXZxeMJnjthgSv3Y7g9dsxg12gyQfvqxM+0uU33PsDweA+vH+6+jyVlJaHz787eXr4mj69e2j0TZc0fEHF1nJJQ7C24FraRz6ap6qaOp2wWxe1bJZuPzK2YCTBuwVIP/ISgtduR/D67Ahenx/B6/MLRnNJg9+QGSIXaPLBu6VkwTexVVVVN3zhRP24rSl4gzVNnr9GM5YUaqf2udq3V5st3TzT6wheE1s4iOC12xG8PjuC1+dH8Pr8CF6/HzPYBAhem1vDqK0teIvKqvT01IVKTQ4+vNYj/BBbrB4Er12W4LXbEbw+O4LX50fw+vwIXr8fM9gECF6b21YbvMHCXvp8iVYUl+uA7dtq+7Y5zi388eEEr52W4LXbEbw+O4LX50fw+vwIXr8fM9gECF6b21YdvLNXrNM7365Su9xMHd03dvfkJXjtBw/Ba7cjeH12BK/Pj+D1+RG8fj9msAkQvDa3rTp4a4MPr02eF36L0kkDuiovK825lZsfTvDaWQleux3B67MjeH1+BK/Pj+D1+zGDTYDgtblt1cEbLO79b1dq1opi9e3UQnt1b+XcSoI32oAEr0+U+/D6/Lgtmd2P4LXb1Y/kLg1+Q2aIXIDgjdxsoxFb24fW6he3an2FXpi+WJmpyTptj+5KisGH1zjDaz94CF67HWd4fXac4fX5Ebw+P87w+v2YwSZA8NrctvozvMECn526UGvLqnRw73bq2TrbuaU/HE7w2kkJXrsdweuzI3h9fgSvz4/g9fsxg02A4LW5bRPB+8WyIn0wd7U65WXp8D4dnVtK8EYTkOD1aXJJg8+PSxrsfgSv3a5+JJc0+A2ZIXIBgjdys41GbK2XNASLDL5x7dGP5in4ENvJA7spOyPVubUbD+cMr52T4LXbcYbXZ8cZXp8fwevz4wyv348ZbAIEr81tmzjDGyzy7dkr9O2q9dq1c0sN6pbv3FqCN1qABK9PkjO8/9/encDZXDV+HP/OMJttxhop5ZGUQpvWp9LykGzxL2VJypKQJYp4hFBJUZQltFAJFUW2ItKmTT156qmeHkvIzphhdvN/nR/3MsyYued3J7975/N7vXrJ3N859/ze51zzveee37nu/Jjhtfcj8NrbMcPr3o4a7AUIvPZ2Tkkvz/A67UtM0QfrtiouqpjamZvXXF7vscWZ4bXHJPDa2zHD686OGV53fgRed37M8Lr3owY7AQKvnVvIzPCahs76ZqOS0zLV8PwqOqtcCZdXfLQ4gdeeksBrb0fgdWdH4HXnR+B150fgde9HDXYCBF47t5AKvN9v3quvN+5RtbIl1Kh2FZdXTOANBiCB150iSxrc+bGkwd6PwGtv5yvJTWvuDakhcAECb+BmOUp4fUmDaWxq5iG9vma90+529c9WXHQxl1d9uDgzvPaMBF57O1OSwOvOj8Br70fgtbcj8Lq3owZ7AQKvvZ1TMhQCr2nnhz9v04Y9B3RZtXK6+MyyLq+awOsWkMDrTpDA686PwGvvR+C1tyPwurejBnsBAq+9XUgF3k17Dmjpz9ucrcnMFmXBOJjhtVck8NrbMcPrzs6UJvDaGxJ47e0IvO7tqMFegMBrbxdSgTdb0htfbVBKRpZuvaCKqia4v3mNwGs/eAi89nYEXnd2BF53fgRed36mNGt43RtSQ+ACBN7AzXKUCJUlDabR32zao7V/7HW+Zth83bDbg8BrL0jgtbcj8LqzI/C68yPwuvMj8Lr3owY7AQKvnZu/VCgFXrM1mdmiLCJCan95dcUWj3R19QReez4Cr70dgdedHYHXnR+B150fgde9HzXYCRB47dxCMvCaRi9at1VbElN05dnlVadqgqurJ/Da8xF47e0IvO7sCLzu/Ai87vwIvO79qMFOgMBr5xaygfd/u5K1/Jftio+NUutLq7m6egKvPR+B196OwOvOjsDrzo/A686PwOvejxrsBAi8dm4hG3izs7P12pr1ysjKVrM6VVW5TKy1AIHXmk4xeXK1AAAZhklEQVQEXns7Aq87OwKvOz8Crzs/Aq97P2qwEyDw2rmFbOA1Df9i/S6t25qompVKq0HNStYCBF5rOgKvPZ1Tkn143QGyLZm9H4HX3s5Xkl0a3BtSQ+ACBN7AzXKUCKWb1nwNT0zJ0JzvNikyIkIdrqiuqGIRVgoEXis2pxAzvPZ2BF53dszwuvMj8LrzY4bXvR812AkQeO3cQnqG1zT+/R+2aHtyquqdkaAzE0paKcSXjNKB1ExlZpldfjkCEYiMlOJLRmtvUnogxTx1rvnFX7FUzClpEzO87tiZ4bX3I/Da2/lKMsPr3pAaAhcg8AZulqNEKM7wmgv4dWeSVv26w+XVUxwBOaG3QqkYVSwVq/Lm/0tGFzoLgdcdMYHX3o/Aa29H4HVvRw32AgReezunZKgG3qxD2Vr801ZlZ9stZzDXHlU8QllZ2TrEBG/Ao8jshRxVLFLpmYcCLuuVAgfSMpWUlpFrc0wIdv4rHeuE4XIlghuCCbzuRgGB196PwGtvR+B1b0cN9gIE3gLaJSUfVGZWlsrGlw6LGd4CXvZJT2MNr71iuKzhTc/K1q7kNO1KStWOpFTtOpCmpLTME2CKR0aofInDM8GVTAguHaOEuChrQAKvNZ1TkMBr70fgtbcj8Lq3owZ7AQJvPnYHU1I1YOQUrfhsrXNm3do1NGFkL1UoF+/8PVRneO2HzNGSBF57xXAJvLkJmFnrHSYEH0jTjv2pTiA+kH5iCLbXC4+S5psOy8RFq0xsccU7f0YrPq64ysRGKcbltyDmJ0TgzU8o78cJvPZ2BF73dtRgL0Dgzcdu2psfaO6ClZo5YbDiYqP1wMBxql6tikY8ch+BNz5GiQcylBHCH8vbv3TclQznwJubTFqGCcGp2pmcpp1Jh/9MychyhxjGpaOLRxwOwLFRRwJxjMocCcNxUcVcXzmB156QwGtvR+B1b0cN9gIE3nzsbu8yVI0a1FeXdk2dM5eu/EoPDZuodR+/ooiICGZ4CbxWr76iFnitkE5SKByWNCSbNdCpmUpMTVdiivkvU8mpGdqXkqGs7LwXxjvLQ0rGqGpCXIFYzb9TkZERilSEikXK2Y4wvlS00tIyZZ7F/L2YOcf8eeTxyMjIIz87vIWe73FTj+02hgVqbAicROB130ns0uDekBoCFyDw5mNWv3E3jRzQyQm95vjp1w26o+swfb7gRcWXLqlDH3TKV33vtZPyPafs6gfyPcdr9Ry6ZVq+25KF4nX9Fc6+bcm0qOiOn7/C2feiCsVx+FPdcdqXkq59B9OdP/enZGjvwXRlHnOX6JBi4/L9d2NEVt98z6GekxPhE1yfoc0vzHdMcgICwRYg8J5E1HwN74U33KuJT/bV9VfVc878fcMWNe84WB/NflZVTitfoMAb2WR6vv1WkOBMPSdnxAefvATC6fVldscwwdeslT77q975/tvyyTlPKjPrkMzOLGbm2PnzuP/u3PV4vvWMieznlDNfS57XQTAMbjAMV2cCb74vN04oBAECbwFmeEcN7KyG11+W6wyvWZtYVA+2JbPv+XDYlsz+6t2XNB+zG0O+9MTOMrp4pBOC2VIwcD9eu4GbHV8iJirSfSXUgECAAgTefMDMGt5bbrhcnds2cc5kDe9RMHZpCPDVdszprOG1tzMlw2ENrzsBd6W5ac3ejzW89na+kqzhdW9IDYELEHjzMZv6xkK9vXCVs0tDibgYdRswll0ajpgReAN/wflKEHjt7Qi87uxMaQKvvSGB196OwOvejhrsBQi8+dgdOJiq/o9P0idf/uCceWGt6powqrcqVUhw/s4+vGxLZvPyI/DaqB0twwyvOz8Cr70fgdfejsDr3o4a7AUIvAW0S0w6oIyMTP8XTviKEXgJvAUcQjlOI/DaqBF43akdLU3gtZck8NrbEXjd21GDvQCB196OGV6+eMJ69BB4remcgszwuvMj8Nr7EXjt7Qi87u2owV6AwGtvR+Al8FqPHgKvNR2B1x2dU5rAa49I4LW3I/C6t6MGewECr70dgZfAaz16CLzWdARed3QEXpd+BF6XgJLYpcG9ITUELkDgDdwsRwnW8LKG12YIEXht1I6WYUmDOz9meO39CLz2dszwurejBnsBAq+9HTO8zPBajx4CrzUdM7zu6JjhdelH4HUJyAyve0BqsBIg8FqxHS3EDC8zvDZDiMBro8YMrzu1o6WZ4bWXJPDa2zHD696OGuwFCLz2dszwMsNrPXoIvNZ0zPC6o2OG16UfgdclIDO87gGpwUqAwGvFxgyvEeCb1uwHD4HX3s6UZA2vOz9meO39CLz2dszwurejBnsBAq+9HTO8zPBajx4CrzUdM7zu6JjhdelH4HUJyAyve0BqsBIg8FqxMcPLDK+7gUPgdefHDK87P2Z47f0IvPZ2zPC6t6MGewECr70dJRFAAAEEEEAAAQRCQIDAGwKdRBMRQAABBBBAAAEE7AUIvPZ2lEQAAQQQQAABBBAIAQECbwh0Ek0MD4HMrCxFRkQqMjLihAtKT8/Q3sRkVaqQoIiIEx8PDwF3V2H8ihcr5q6SIlg6JTVde/ftV+VK5XMde4cOZWvH7r2qUC4e31zGR2LSAaWlZTivTY7ABMxrdteeRGUfylalCmVVrFjkCRWYx0uWiFNcbHRglXM2AgEKEHgDBON0afSLszRj7tIcFBdfWFOvvzAYnjwETOi48/5h6tq+mZr+4yr/WdnZ2Zo04329+Mo852flEkrrhSf6qF7tGlgeI7Bpyw41bveIPnzrGZ1euYL/EcbiyYfJg4Of14rP1vrH1m23XKt+3Vr7C6364gf1f3ySDqakOj8b2q+jWjdrwNiTnKDWodcT2rh5u+NR46zT1aVdUzVreLXz9+Wrv1OvIeNPsPpu2VTFREcVecPZ763Q4+Nm+B1Oq1hW40f20oW1qjs/27Rlu7oNGOv3bXXrdXrsoXsUVZw3tUV+8BQSAIG3kGDDudqnXnhTf2zdoUe6t/FfZkxMlCpXLBfOl219bc9Mnq1X3lrslB89+P4cgXftut/UvucozZwwSHXO+5vGT39XHyz/Qh/NHpvrbJx1I0K4YJvuI/Svn353ruD4wMtYPHnHvvDyPDVsUF/VqlbSl9/+pB6DntNbkx5TnfP/JvMm7LqWvdTzvpZq1+pmrfz8e/UeMkFLZ43RGVUqhvCICU7Td+zap/lLVqt5o2tUMi5WM99epldmL9En88Y7s5Efrf5Wjz4xVW9PHZ7jCY01n9JIC5Z9roT4Urq0bi2Zmd7+wycqMzNLL48b4Hh1ffgZlSoZp1EDu2jbjt1qff9wPda3g/8NRXB6kVoQOCpA4GU0BCxgQsa+/cl6alDXgMsWxQL7EpOVmp6utt1H6KGurXME3mcnz9HP/92oac887NCYX7I33N7H+SV6fs2ziiLXCddsTMwvRBN8cwu8jMWCD5Mb7+iru1rc6HzSYGZ3uz86TmuXTVX0kRnJW9sPcMJvu1b/KHilReTMzX/uVKM2DztvTi+pc64TeIc/+6pWz59QRATcXab5JMEsnxk7rLvMMpGrm/VwPhU0nw6aY9TzM7Vtxx5NGNXb3RNRGoE8BAi8DI2ABUzgXbbqa115SW2VjS+tG/9+iS6te27A9RS1AuaX5YP3tcoReM0vgbLxpTS4991+jgsadNTEJ/vq+qvqFTWiPK93+869MmEtt8DLWCzYMDEfzZtA6xtbcxas1KuzF2vR66P9FZglEGefWSXHsoeC1R7+Z81bvFr/HD3dCbhm6ZEJvGZGvEWjaxQTE63L6tVSowb1WQd93FB4f9lnWvHpWv36vz80dlgPnXdONf2+YYuadxysle88p4rlD6+NNjPo7y397IQZ8/AfWVzhXyVA4P2rpMPoecxHVRs2b3PWqa37Zb2zls28a2/U4PIwusrgX0pugdd8rFerRrUcAaN+424a1r+jmtx0ZfAbEaI15hV4GYsF69ADB1PVvudIlSpZQq8+N9C5eWjamx9oycdf5QgY5g1YqRJxzvjjOCrw2/rNatt9pO65o5GzBMQcP/5nvZau/ErxpUtq6/bdmvP+x2rb8qYcb14xlJ6b+ra+/dev2rFrr0Y80kmXX3yefEu5Pl/wouNnDvMGbPKM97Ri7jjYECgUAQJvobAWrUoHPvGS9iUmafLofkXrwgO82rxmeM1s0aBe7f21McN7Imxegff4MxmLJ9qZtbq9h4x3Pi6eMX6Qs67SFzCY4c3/Rbxl2y7d/eAo1b/oPD0xsEuuOw2YWt5d9ImGPP2yflg+nVneXFinzFyg199Z5syQ+2Z4V737vLM7iDmY4c1/LHKGOwECrzs/SuvoO3izto0jb4HcAq9Zw/vL75v00pj+TkHW8ObuV9DA65tNYiwedtyffFC9/jleKSlpmvJ0P3/YNY/51vB+/+E0RUUVd843Y7TDHQ1Zw3tkGP53/Rbd2/cpZ9nWkL4dThpkV6/5Ud0GPKtvl76k2Bi22Dr+lbxs1TfqO/QF5w2B+cTh+DW8I8bNcGaBWcPLb9HCEiDwFpZsGNc77qW5at7walU7o7IT1u7tM1qd2zbR/Xc3C+Ortr80c4ey2YeyaYdH1a1DczW9+Sp/wDi6S8Ng587556e9rUXLv2SXhmO4MzKznJvWbmn7iLPe1GxL5tu6iLGY97g8mJKmu7oNd+6QHze8p3NHvDkiIyNVpVI5mcfrN75fA3q0UVt2aTgB8pff/1CrTkOcpUUPdmrluJmjRFyMc+/Cm/OWq1aNM1X73LOVmJSshx+f7IxL3y4E9v9ihEfJia/O1zWX13GMdu/d72x/FxcT7ffp3H+MypQqqVEDO7NLQ3h0ueevgsDr+S7yXgPvvH+4s3bXd9x2y9+d2Q9mNXLvq4eGTXTW+h17LJzxpKpXqyKzD+8Lr8zT5BnvH/llGquXxvTz37nsvd7/61tk1jT79ok1z26WgPjujGcs5t0fvlnx48841s/s0WtuVPMd/+xzt9rcdtNf38kefMbFK9Y4Ie34w+zDa3aoGTtljqbPWuR/uG7tGhozpBtbuh0RGfzUNM1f8qnfx+zG8NTgrn6f9Zv+dPbhNbtfmMP8HhnWr6N/MsCDQ4ImhbgAgTfEO/BUNT8p+aD2JiapYvmyfENOEDohNS1de/bm/W1YQXiKsK2Cseiua7OyDmnbzj2qVD6BsBEgpXnd7ty9T6VLlsixXCTAasL2dPMNkjt273NuhPStHT/+Ys0bM/PpQ8kSsWHrwIV5Q4DA641+oBUIIIAAAggggAAChSRA4C0kWKpFAAEEEEAAAQQQ8IYAgdcb/UArEEAAAQQQQAABBApJgMBbSLBUiwACCCCAAAIIIOANAQKvN/qBViCAAAIIIIAAAggUkgCBt5BgqRYBBBBAAAEEEEDAGwIEXm/0A61AAAEEEEAAAQQQKCQBAm8hwVItAggggAACCCCAgDcECLze6AdagQACCCCAAAIIIFBIAgTeQoKlWgQQQAABBBBAAAFvCBB4vdEPtAIBBBBAAAEEEECgkAQIvIUES7UIIIAAAggggAAC3hAg8HqjH2gFAggggAACCCCAQCEJEHgLCZZqEUAAAQQQQAABBLwhQOD1Rj/QCgQQQAABBBBAAIFCEiDwFhIs1SKAwGGBn3/bqD+37/ZznHVmZdU463TP8Kxd95s2b92pZg2v9kybaAgCCCCAQHAFCLzB9aQ2BBA4TmD89Hc0d8FK7dmXpNMqllXntk3VtuVNWrP2Z93Xd7QWv/G0qlWtVOhuaekZuqRhFz3xaBe1aHSN//mGPfOq5i5cqX+vfLXQ28ATIIAAAgicGgEC76lx51kRKFICq9f8qG4DntW700eoVo0znWv/8tuf1Knf01r8xmhVq3paoXukpqXr0kZdNXJAJ7VsfK3/+Q6mpCkjM1PxpUsWeht4AgQQQACBUyNA4D017jwrAkVK4PjAu33nXt3b9ylt3Lxd59c8S7Ex0Trn7Koa1r+jsrIO6fV3P9Q7C1fp941bde7fzlC3Di3UqEF9x2z2eyu0Zu1/1KNjC70xb7n+t3GrenVqpbS0DD07ZY5T58GUVKfcvXc1VvOGh2dzewx6Tis//15nVKmoiuUTnJ9NfeZhLfzoc33xzb81dlgP52cZGZmaNOM9ffDRl9r8505dcfH56tftTl1Q62zn8R9++l1jJr6lti1v1pwFH+vfv2zQDVdfpHta3+I/p0h1LheLAAIIhIAAgTcEOokmIhDqAscH3qTkgzJLHd6ct1zd72mhcmXLqFxCaTVqcLnGTpmjWfNXqM1tN6pu7Rpa8vFXWrxijd6cOET1atdwHp8+a5FDckmdc51lEnc2v0F79u3Xl9/9rIsuOMcJ0Cs++04LP/xCMycM1iV1ajrLFszyhSY3XamL69R0yt/etIEmvTZf85d8qhVzxzk/8y1xuL3p9U4YnzF3qROil7z5tM48vZJWr/mXug0Y65zb4Y5Gzs9em7NECWVKafaUoaHeVbQfAQQQCEsBAm9YdisXhYC3BAq6pGH33v26rmUvPXR/a3Vqc6tzEZlZWbqqaQ/9X5PrNLBnW38gfv2Fwf7lEcdebXZ2tvYnHdTuffvVrMOj6t/tTmemN68lDSZ4+wLvjl37dMPtfXTfXbeqX7fWTrX7EpN1TYueatfqZg3q1d4feN+Z9rjOO6eac87y1d+p15Dx+vjt51SpwuHZYw4EEEAAAe8IEHi90xe0BIGwFSho4P3mh190T+8nnWUHpUuV8HuYnR4aXH2RXnyijxN4l678WktnjcnhtTcxSc9Mmq1lq75xljT4jh73tnRmkQsSeH030k0e/ZCuvaKuv47buwxVXGyMZk4Y5A+8H80ZqyqVyjnn/Pif9bqr23C9NXmo6pxXPWz7kQtDAAEEQlWAwBuqPUe7EQghgYIGXt95Zib1+J0bEuJLO2Eyr8DbpvsIbd66QwMfbOecV6Fcghq16a82LW8ucOD1Pb8Jtma5hO/o2OcpmV0eZk0ckmvgNYHchGICbwgNSpqKAAJFSoDAW6S6m4tF4NQI5BZ4zf637XuO0vxXRqpm9TOchm3askON2z2ioQ/do9bNb8jRWLNUISIiItfAm3wgRVc0eUB9u96hzm2b+Mtde9uD/sBrlkbUu6mTHuvbQXe2uNF/zrFLGsxa3VvbD1DP+1rqgQ4tnHNSUtN12S1dna3MzJZmvjW8x87wEnhPzbjiWRFAAIGCChB4CyrFeQggYCWwbecerfh0rUY9P1Njh3XXpXVrqUK5eKWnZ+jihl2ctbHm5jETWs3NZWYtrFkTO7z/vbq07rky63o/+fIHRUZGqk+X2/Oc4TUzrMUiI50dFbKysvTOok+cm918SxpM483WaMkHUjW4d3slJh3QZfVqaeKrOW9a69x/jH757yY9eF8r1Tqnml6bs1RLV34ls2b44gtrEnitRgGFEEAAgVMrQOA9tf48OwJhLzDk6Zf17qJP/Nd5bAB9be5STXtjofOlFGZHBrNkwATR56a+rTnvf+wvY3ZwMMscGt94hca9NNfZueH4Nbyffb1Oj499zdlKzBxN/3GVs0vDsbO1ZvuxJye84Wx3Zo6vF092dnyYt3i1f5cGc+PawFFTnC/G8B3H7t3rm+FdPnesKlc8vIbXN8Nrdmm4sBZreMN+UHOBCCAQcgIE3pDrMhqMQHgJmKUGu/YkOrO+xYsV81+c+fnOXfsUGxutsvGlC3TRZtnDhj+2OducneyLJEyoNTfFxcVG51mv2Z1hf/IBnV65Qo52FaghnIQAAggg4CkBAq+nuoPGIIAAAggggAACCARbgMAbbFHqQwABBBBAAAEEEPCUAIHXU91BYxBAAAEEEEAAAQSCLUDgDbYo9SGAAAIIIIAAAgh4SoDA66nuoDEIIIAAAggggAACwRYg8AZblPoQQAABBBBAAAEEPCVA4PVUd9AYBBBAAAEEEEAAgWALEHiDLUp9CCCAAAIIIIAAAp4SIPB6qjtoDAIIIIAAAggggECwBQi8wRalPgQQQAABBBBAAAFPCRB4PdUdNAYBBBBAAAEEEEAg2AIE3mCLUh8CCCCAAAIIIICApwQIvJ7qDhqDAAIIIIAAAgggEGwBAm+wRakPAQQQQAABBBBAwFMCBF5PdQeNQQABBBBAAAEEEAi2AIE32KLUhwACCCCAAAIIIOApAQKvp7qDxiCAAAIIIIAAAggEW4DAG2xR6kMAAQQQQAABBBDwlACB11PdQWMQQAABBBBAAAEEgi1A4A22KPUhgAACCCCAAAIIeEqAwOup7qAxCCCAAAIIIIAAAsEWIPAGW5T6EEAAAQQQQAABBDwlQOD1VHfQGAQQQAABBBBAAIFgCxB4gy1KfQgggAACCCCAAAKeEiDweqo7aAwCCCCAAAIIIIBAsAUIvMEWpT4EEEAAAQQQQAABTwkQeD3VHTQGAQQQQAABBBBAINgCBN5gi1IfAggggAACCCCAgKcECLye6g4agwACCCCAAAIIIBBsAQJvsEWpDwEEEEAAAQQQQMBTAgReT3UHjUEAAQQQQAABBBAItgCBN9ii1IcAAggggAACCCDgKQECr6e6g8YggAACCCCAAAIIBFuAwBtsUepDAAEEEEAAAQQQ8JQAgddT3UFjEEAAAQQQQAABBIItQOANtij1IYAAAggggAACCHhKgMDrqe6gMQgggAACCCCAAALBFiDwBluU+hBAAAEEEEAAAQQ8JUDg9VR30BgEEEAAAQQQQACBYAv8Px/9b9Z65jOOAAAAAElFTkSuQmCC" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "from ax.plot.trace import optimization_trace_single_method\n", + "\n", + "\n", + "# `plot_single_method` expects a 2-d array of means, because it expects to average means from multiple\n", + "# optimization runs, so we wrap out best objectives array in another array.\n", + "objective_means = np.array([[trial.objective_mean for trial in exp.trials.values()]])\n", + "best_objective_plot = optimization_trace_single_method(\n", + " y=np.minimum.accumulate(objective_means, axis=1),\n", + " optimum=0.397887, # Known minimum objective for Branin function.\n", + ")\n", + "render(best_objective_plot)" + ] + } + ], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "fileHeader": "", + "isAdHoc": false, + "kernelspec": { + "display_name": "python3", + "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.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }, + "indentAmount": 2, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.10" + }, + "last_base_url": "https://bento.edge.x2p.facebook.net/", + "last_kernel_id": "2aaa652f-0c5c-4d10-a405-c876ee910cd9", + "last_msg_id": "5171cf29-eaa9a8ddaa473c97a4ec9b4c_3655", + "last_server_session_id": "3a3c3914-d10d-4144-bb71-b197cc08b48a" + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/website-old/static/files/custom_botorch_model_in_ax.py b/website-old/static/files/custom_botorch_model_in_ax.py new file mode 100644 index 0000000000..8befa8bb4c --- /dev/null +++ b/website-old/static/files/custom_botorch_model_in_ax.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Using a custom BoTorch model with Ax +# +# In this tutorial, we illustrate how to use a custom BoTorch model within Ax's `botorch_modular` API. This allows us to harness the convenience of Ax for running Bayesian Optimization loops while maintaining full flexibility in modeling. +# +# Acquisition functions and their optimizers can be swapped out in much the same fashion. See for example the tutorial for [Implementing a custom acquisition function](./custom_acquisition). +# +# If you want to do something non-standard, or would like to have full insight into every aspect of the implementation, please see [this tutorial](./closed_loop_botorch_only) for how to write your own full optimization loop in BoTorch. +# + +# In[ ]: + + +import os +from contextlib import contextmanager, nullcontext + +import plotly.io as pio + +from ax.utils.testing.mock import mock_botorch_optimize_context_manager + +# Ax uses Plotly to produce interactive plots. These are great for viewing and analysis, +# though they also lead to large file sizes, which is not ideal for files living in GH. +# Changing the default to `png` strips the interactive components to get around this. +pio.renderers.default = "png" + +SMOKE_TEST = os.environ.get("SMOKE_TEST") +NUM_EVALS = 10 if SMOKE_TEST else 30 + + +# ### Implementing the custom model +# +# For this tutorial, we implement a very simple GPyTorch `ExactGP` model that uses an RBF kernel (with ARD) and infers a homoskedastic noise level. +# +# Model definition is straightforward. Here we implement a GPyTorch `ExactGP` that inherits from `GPyTorchModel`; together these two superclasses add all the API calls that BoTorch expects in its various modules. +# +# *Note:* BoTorch allows implementing any custom model that follows the `Model` API. For more information, please see the [Model Documentation](../docs/models). + +# In[2]: + + +from typing import Optional + +from botorch.models.gpytorch import GPyTorchModel +from gpytorch.distributions import MultivariateNormal +from gpytorch.kernels import RBFKernel, ScaleKernel +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.means import ConstantMean +from gpytorch.models import ExactGP +from torch import Tensor + + +class SimpleCustomGP(ExactGP, GPyTorchModel): + + _num_outputs = 1 # to inform GPyTorchModel API + + def __init__(self, train_X, train_Y, train_Yvar: Optional[Tensor] = None): + # NOTE: This ignores train_Yvar and uses inferred noise instead. + # squeeze output dim before passing train_Y to ExactGP + super().__init__(train_X, train_Y.squeeze(-1), GaussianLikelihood()) + self.mean_module = ConstantMean() + self.covar_module = ScaleKernel( + base_kernel=RBFKernel(ard_num_dims=train_X.shape[-1]), + ) + self.to(train_X) # make sure we're on the right device/dtype + + def forward(self, x): + mean_x = self.mean_module(x) + covar_x = self.covar_module(x) + return MultivariateNormal(mean_x, covar_x) + + +# ### Instantiate a `BoTorchModel` in Ax +# +# A `BoTorchModel` in Ax encapsulates both the surrogate -- which `Ax` calls a `Surrogate` and BoTorch calls a `Model` -- and an acquisition function. Here, we will only specify the custom surrogate and let Ax choose the default acquisition function. +# +# Most models should work with the base `Surrogate` in Ax, except for BoTorch `ModelListGP`, which works with `ListSurrogate`. +# Note that the `Model` (e.g., the `SimpleCustomGP`) must implement `construct_inputs`, as this is used to construct the inputs required for instantiating a `Model` instance from the experiment data. + +# In[3]: + + +from ax.models.torch.botorch_modular.model import BoTorchModel +from ax.models.torch.botorch_modular.surrogate import Surrogate, SurrogateSpec +from ax.models.torch.botorch_modular.utils import ModelConfig + +ax_model = BoTorchModel( + surrogate=Surrogate( + surrogate_spec=SurrogateSpec( + model_configs=[ + ModelConfig( + # The model class to use + botorch_model_class=SimpleCustomGP, + # Optional, MLL class with which to optimize model parameters + # mll_class=ExactMarginalLogLikelihood, + # Optional, dictionary of keyword arguments to model constructor + # model_options={} + ) + ] + ) + ), + # Optional, acquisition function class to use - see custom acquisition tutorial + # botorch_acqf_class=qExpectedImprovement, +) + + +# ### Combine with a `ModelBridge` +# +# `Model`s in Ax require a `ModelBridge` to interface with `Experiment`s. A `ModelBridge` takes the inputs supplied by the `Experiment` and converts them to the inputs expected by the `Model`. For a `BoTorchModel`, we use `TorchModelBridge`. The Modular BoTorch interface creates the `BoTorchModel` and the `TorchModelBridge` in a single step, as follows: +# +# ``` +# from ax.modelbridge.registry import Models +# model_bridge = Models.BOTORCH_MODULAR( +# experiment=experiment, +# data=data, +# surrogate=Surrogate(SimpleCustomGP), +# # Optional, will use default if unspecified +# # botorch_acqf_class=qLogNoisyExpectedImprovement, +# ) +# # To generate a trial +# trial = model_bridge.gen(1) +# ``` +# + +# # Using the custom model in Ax to optimize the Branin function +# +# We will demonstrate this with both the Service API (simpler, easier to use) and the Developer API (advanced, more customizable). + +# ## Optimization with Ax's Service API +# +# A detailed tutorial on the Service API can be found [here](https://ax.dev/tutorials/gpei_hartmann_service.html). +# +# In order to customize the way the candidates are created in the Service API, we need to construct a new `GenerationStrategy` and pass it into `AxClient`. + +# In[4]: + + +from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy +from ax.modelbridge.registry import Models + + +gs = GenerationStrategy( + steps=[ + # Quasi-random initialization step + GenerationStep( + model=Models.SOBOL, + num_trials=5, # How many trials should be produced from this generation step + ), + # Bayesian optimization step using the custom acquisition function + GenerationStep( + model=Models.BOTORCH_MODULAR, + num_trials=-1, # No limitation on how many trials should be produced from this step + # For `BOTORCH_MODULAR`, we pass in kwargs to specify what surrogate or acquisition function to use. + model_kwargs={ + "surrogate_spec": SurrogateSpec(model_configs=[ModelConfig(botorch_model_class=SimpleCustomGP)]), + }, + ), + ] +) + + +# ### Setting up the experiment +# +# In order to use the `GenerationStrategy` we just created, we will pass it into the `AxClient`. + +# In[5]: + + +import torch +from ax.service.ax_client import AxClient +from ax.service.utils.instantiation import ObjectiveProperties +from botorch.test_functions import Branin + + +# Initialize the client - AxClient offers a convenient API to control the experiment +ax_client = AxClient(generation_strategy=gs) +# Setup the experiment +ax_client.create_experiment( + name="branin_test_experiment", + parameters=[ + { + "name": "x1", + "type": "range", + # It is crucial to use floats for the bounds, i.e., 0.0 rather than 0. + # Otherwise, the parameter would be inferred as an integer range. + "bounds": [-5.0, 10.0], + }, + { + "name": "x2", + "type": "range", + "bounds": [0.0, 15.0], + }, + ], + objectives={ + "branin": ObjectiveProperties(minimize=True), + }, +) +# Setup a function to evaluate the trials +branin = Branin() + + +def evaluate(parameters): + x = torch.tensor([[parameters.get(f"x{i+1}") for i in range(2)]]) + # The GaussianLikelihood used by our model infers an observation noise level, + # so we pass an sem value of NaN to indicate that observation noise is unknown + return {"branin": (branin(x).item(), float("nan"))} + + +# ### Running the BO loop + +# The next cell sets up a decorator solely to speed up the testing of the notebook in `SMOKE_TEST` mode. You can safely ignore this cell and the use of the decorator throughout the tutorial. + +# In[6]: + + +if SMOKE_TEST: + fast_smoke_test = mock_botorch_optimize_context_manager +else: + fast_smoke_test = nullcontext + +# Set a seed for reproducible tutorial output +torch.manual_seed(0) + + +# In[7]: + + +with fast_smoke_test(): + for i in range(NUM_EVALS): + parameters, trial_index = ax_client.get_next_trial() + # Local evaluation here can be replaced with deployment to external system. + ax_client.complete_trial(trial_index=trial_index, raw_data=evaluate(parameters)) + + +# ### Viewing the evaluated trials + +# In[8]: + + +ax_client.get_trials_data_frame() + + +# In[9]: + + +parameters, values = ax_client.get_best_parameters() +print(f"Best parameters: {parameters}") +print(f"Corresponding mean: {values[0]}, covariance: {values[1]}") + + +# ### Plotting the response surface and optimization progress + +# In[10]: + + +from ax.utils.notebook.plotting import render + +render(ax_client.get_contour_plot()) + + +# In[11]: + + +best_parameters, values = ax_client.get_best_parameters() +best_parameters, values[0] + + +# In[12]: + + +render(ax_client.get_optimization_trace(objective_optimum=0.397887)) + + +# ## Optimization with the Developer API +# +# A detailed tutorial on the Service API can be found [here](https://ax.dev/tutorials/gpei_hartmann_developer.html). +# +# ### Set up the Experiment in Ax +# +# We need 3 inputs for an Ax `Experiment`: +# - A search space to optimize over; +# - An optimization config specifiying the objective / metrics to optimize, and optional outcome constraints; +# - A runner that handles the deployment of trials. For a synthetic optimization problem, such as here, this only returns simple metadata about the trial. + +# In[13]: + + +import pandas as pd +import torch +from ax import ( + Data, + Experiment, + Metric, + Objective, + OptimizationConfig, + ParameterType, + RangeParameter, + Runner, + SearchSpace, +) +from ax.utils.common.result import Ok +from botorch.test_functions import Branin + + +branin_func = Branin() + +# For our purposes, the metric is a wrapper that structures the function output. +class BraninMetric(Metric): + def fetch_trial_data(self, trial): + records = [] + for arm_name, arm in trial.arms_by_name.items(): + params = arm.parameters + tensor_params = torch.tensor([params["x1"], params["x2"]]) + records.append( + { + "arm_name": arm_name, + "metric_name": self.name, + "trial_index": trial.index, + "mean": branin_func(tensor_params), + "sem": float( + "nan" + ), # SEM (observation noise) - NaN indicates unknown + } + ) + return Ok(value=Data(df=pd.DataFrame.from_records(records))) + + +# Search space defines the parameters, their types, and acceptable values. +search_space = SearchSpace( + parameters=[ + RangeParameter( + name="x1", parameter_type=ParameterType.FLOAT, lower=-5, upper=10 + ), + RangeParameter( + name="x2", parameter_type=ParameterType.FLOAT, lower=0, upper=15 + ), + ] +) + +optimization_config = OptimizationConfig( + objective=Objective( + metric=BraninMetric(name="branin_metric", lower_is_better=True), + minimize=True, # This is optional since we specified `lower_is_better=True` + ) +) + + +class MyRunner(Runner): + def run(self, trial): + trial_metadata = {"name": str(trial.index)} + return trial_metadata + + +exp = Experiment( + name="branin_experiment", + search_space=search_space, + optimization_config=optimization_config, + runner=MyRunner(), +) + + +# ### Run the BO loop +# +# First, we use the Sobol generator to create 5 (quasi-) random initial point in the search space. Ax controls objective evaluations via `Trial`s. +# - We generate a `Trial` using a generator run, e.g., `Sobol` below. A `Trial` specifies relevant metadata as well as the parameters to be evaluated. At this point, the `Trial` is at the `CANDIDATE` stage. +# - We run the `Trial` using `Trial.run()`. In our example, this serves to mark the `Trial` as `RUNNING`. In an advanced application, this can be used to dispatch the `Trial` for evaluation on a remote server. +# - Once the `Trial` is done running, we mark it as `COMPLETED`. This tells the `Experiment` that it can fetch the `Trial` data. +# +# A `Trial` supports evaluation of a single parameterization. For parallel evaluations, see [`BatchTrial`](https://ax.dev/docs/core.html#trial-vs-batch-trial). + +# In[14]: + + +from ax.modelbridge.registry import Models + + +sobol = Models.SOBOL(exp.search_space) + +for i in range(5): + trial = exp.new_trial(generator_run=sobol.gen(1)) + trial.run() + trial.mark_completed() + + +# Once the initial (quasi-) random stage is completed, we can use our `SimpleCustomGP` with the default acquisition function chosen by `Ax` to run the BO loop. + +# In[15]: + + +with fast_smoke_test(): + for i in range(NUM_EVALS - 5): + model_bridge = Models.BOTORCH_MODULAR( + experiment=exp, + data=exp.fetch_data(), + surrogate_spec=SurrogateSpec(model_configs=[ModelConfig(SimpleCustomGP)]), + ) + trial = exp.new_trial(generator_run=model_bridge.gen(1)) + trial.run() + trial.mark_completed() + + +# View the trials attached to the `Experiment`. + +# In[16]: + + +exp.trials + + +# View the evaluation data about these trials. + +# In[17]: + + +exp.fetch_data().df + + +# ### Plot results +# +# We can use convenient Ax utilities for plotting the results. + +# In[18]: + + +import numpy as np +from ax.plot.trace import optimization_trace_single_method + + +# `plot_single_method` expects a 2-d array of means, because it expects to average means from multiple +# optimization runs, so we wrap out best objectives array in another array. +objective_means = np.array([[trial.objective_mean for trial in exp.trials.values()]]) +best_objective_plot = optimization_trace_single_method( + y=np.minimum.accumulate(objective_means, axis=1), + optimum=0.397887, # Known minimum objective for Branin function. +) +render(best_objective_plot) + diff --git a/website-old/static/files/custom_model.ipynb b/website-old/static/files/custom_model.ipynb new file mode 100644 index 0000000000..0e69ea8c29 --- /dev/null +++ b/website-old/static/files/custom_model.ipynb @@ -0,0 +1,1033 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Custom Models in BoTorch\n", + "In this tutorial, we illustrate how to create a custom surrogate model using the [`Model`](https://github.com/pytorch/botorch/blob/main/botorch/models/model.py) and [`Posterior`](https://github.com/pytorch/botorch/blob/main/botorch/posteriors/posterior.py) interface. We will cover creating surrogate models from: \n", + "- PyTorch distributions\n", + "- Posterior samples (using Pyro)\n", + "- Ensemble of ML predictions\n", + "\n", + "This tutorial differs from the [Using a custom BoTorch model with Ax](https://botorch.org/tutorials/custom_botorch_model_in_ax) tutorial by focusing more on authoring a new model that is compatible with the BoTorch and less on integrating a custom model with Ax's `botorch_modular` API." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "# Set the seed for reproducibility\n", + "torch.manual_seed(1)\n", + "# Double precision is highly recommended for BoTorch.\n", + "# See https://github.com/pytorch/botorch/discussions/1444\n", + "torch.set_default_dtype(torch.float64)\n", + "\n", + "train_X = torch.rand(20, 2) * 2\n", + "Y = 1 - (train_X - 0.5).norm(dim=-1, keepdim=True)\n", + "Y += 0.1 * torch.rand_like(Y)\n", + "bounds = torch.stack([torch.zeros(2), 2 * torch.ones(2)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Code to plot our training data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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eOCBpL6XpCE5ODiyHpfLRba0tJAulyulz/nybw6Tld97kzR+93/pR+X33iS//JvDadrTxUxNTZEEQZD3339/xNd37dpFFhQUkMFgkDQajaRSqSQfeOABsr29nfzlL39J8vl88u233yZJkiRfffVVMi8vj/z73/9O9vX1kYcPHyafffZZ+rO++tWvklu3biX37t1LdnV1kbt37ybFYjHZ0dFBkiRJPv/886RQKCS3bt1K7t+/n2xrayPtdjup0+nI3/zmN/Tn+P3+kOcGBwfJ3bt3kydPniS7u7vp/Tp8eOG82Ww2csuWLeSuXbvIkZERcmRkhPT7/eT7779PAiBnZmZIkiTJY8eOkTwej7znnnvI9vZ28vnnnyelUin5/PPP02MbjUZSrVaTTz75JNnZ2Uk+8MADJI/HI9va2kiSJMndu3eTBoOB3Lt3L2m1Wsl9+/aRv//97yOe23RdN+FwwsHBsQJIxwTQPT4fIhjhj54Jexr3+GMOHTpEAiBfe+21iK8//PDDJABybGyMNBqN5KWXXhry+tVXX01edtllJEmS5C9+8QuypqaG9Hq9iz6nr6+P5PP55NDQUMjzF198Mfn973+fJMkF4QBAnjp1KuQ93/zmN8nt27fTf7/11lukWCymJ/xIXHHFFeTtt99O/71t2zbym9/8Zsh7woXjS1/6EvmJT3wi5D133HEHWV9fT/9tNBrJ6667jv47GAySRUVF5NNPP02SJEnecsst5Pbt28lgMBh13ygyJRycq4qD4wyhb9oZ83XrVGbjHSRJxvW+LVu2LPq7tbUVAPD5z38eLpcLlZWV2LVrF1577TX4/X4AQGNjIwKBAGpqaqBQKOjHnj170N3dTX+eSCTCmjVrQsa49tpr8cEHH2B4eBgA8Morr+CKK66ASqUCsBALuffee7F69Wqo1WooFAq89dZb6O/vT+gctLa24txzzw157txzz0VnZycCgQD9HHP/CIJAcXEx3XfqhhtuwKlTp2CxWHDrrbfi7bffTmgf0gEnHBwcZwhGdex2JSaNPCPjVldXgyAIevIPp7W1FQUFBSgsLFzyswwGA9rb2/HUU09BKpXi61//Oi644AL4fD7Y7Xbw+XwcP34cp06doh+tra147LHH6M+QSqWLgtXnnHMOqqqq8N///d9wuVx47bXXcO2119Kv7969G4899hi++93v4v33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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib import pyplot as plt\n", + "from matplotlib.axes import Axes\n", + "from torch import Tensor\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "# Needed for older versions of matplotlib.\n", + "assert Axes3D\n", + "\n", + "\n", + "def plot_toy_data(x: Tensor, y: Tensor) -> Axes:\n", + " ax = plt.figure().add_subplot(projection=\"3d\")\n", + " ax.scatter(\n", + " x[:, 0].detach().numpy().squeeze(),\n", + " x[:, 1].detach().numpy().squeeze(),\n", + " zs=y.detach().numpy().squeeze(),\n", + " label=\"Observations\",\n", + " )\n", + " ax.set_xlabel(\"X1\")\n", + " ax.set_ylabel(\"X2\")\n", + " ax.set_zlabel(\"Y\")\n", + " ax.set_title(\"Toy Data\")\n", + " ax.view_init(elev=15.0, azim=65)\n", + " ax.legend()\n", + " return ax\n", + "\n", + "\n", + "plot_toy_data(x=train_X, y=Y)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Probabilistic Linear Regression (w/ Torch Distributions)\n", + "BoTorch's `Model` class only requires you to define a `posterior()` method that returns a `Posterior` object, the only requirement of which is to implement an `rsample()` function for drawing posterior samples. Specifically, we can utilize the subclass [`TorchPosterior`](https://github.com/pytorch/botorch/blob/main/botorch/posteriors/torch.py) that directly wraps a [torch distribution](https://pytorch.org/docs/stable/distributions.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[KeOps] Warning : omp.h header is not in the path, disabling OpenMP. To fix this, you can set the environment\n", + " variable OMP_PATH to the location of the header before importing keopscore or pykeops,\n", + " e.g. using os.environ: import os; os.environ['OMP_PATH'] = '/path/to/omp/header'\n", + "[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode\n" + ] + } + ], + "source": [ + "from typing import Optional, Union\n", + "from torch import Tensor, distributions, nn\n", + "from botorch.acquisition.objective import PosteriorTransform\n", + "from botorch.models.model import Model\n", + "from botorch.posteriors.posterior import Posterior\n", + "from botorch.posteriors.torch import TorchPosterior\n", + "\n", + "\n", + "class ProbabilisticRegressionModel(Model):\n", + " _num_outputs: int\n", + "\n", + " def __init__(self, train_X: Tensor, train_Y: Tensor):\n", + " super(ProbabilisticRegressionModel, self).__init__()\n", + " self._num_outputs = train_Y.shape[-1]\n", + " # Linear layer that will compute the regression output.\n", + " self.linear = nn.Linear(train_X.shape[-1], self.num_outputs)\n", + "\n", + " @property\n", + " def num_outputs(self) -> int:\n", + " return self._num_outputs\n", + "\n", + " def forward(self, x: Tensor) -> distributions.Distribution:\n", + " n, p = x.squeeze().shape\n", + " # For now, let's suppose we have known variance 1.\n", + " return distributions.StudentT(df=n - p, loc=self.linear(x), scale=1)\n", + "\n", + " def posterior(\n", + " self,\n", + " X: Tensor,\n", + " output_indices: Optional[list[int]] = None,\n", + " observation_noise: Union[bool, Tensor] = False,\n", + " posterior_transform: Optional[PosteriorTransform] = None,\n", + " ) -> Posterior:\n", + " if output_indices:\n", + " X = X[..., output_indices]\n", + " # TorchPosterior directly wraps our torch.distributions.Distribution output.\n", + " posterior = TorchPosterior(distribution=self(X))\n", + " if posterior_transform is not None:\n", + " posterior = posterior_transform(posterior)\n", + " return posterior" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def fit_prob_reg(\n", + " epochs: int,\n", + " model: ProbabilisticRegressionModel,\n", + " optimizer: torch.optim.Optimizer,\n", + " train_X: Tensor,\n", + " train_Y: Tensor,\n", + ") -> None:\n", + " \"\"\"Optimization loop for linear regression.\"\"\"\n", + " train_X = train_X.requires_grad_()\n", + " for epoch in range(epochs):\n", + " optimizer.zero_grad()\n", + " outputs = model(train_X)\n", + " loss = -outputs.log_prob(train_Y).mean()\n", + " loss.backward()\n", + " optimizer.step()\n", + " if epoch % 10 == 0:\n", + " print(\"epoch {}, loss {}\".format(epoch, loss.item()))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch 0, loss 1.3283335654335957\n", + "epoch 10, loss 1.0691577720241896\n", + "epoch 20, loss 0.9760611872620313\n", + "epoch 30, loss 0.9548081485136333\n", + "epoch 40, loss 0.9551388835842956\n" + ] + } + ], + "source": [ + "prob_regression_model = ProbabilisticRegressionModel(train_X, Y)\n", + "optimizer = torch.optim.Adam(prob_regression_model.parameters(), lr=0.1)\n", + "fit_prob_reg(50, prob_regression_model, optimizer, train_X, Y)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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vfde2Gw2KJVK2O12PProoxldl4whIiIiBc/i4iJpaWkhi4uLWe3nU88dI9VfepXYv/j/8T/VX3qVfOq5Yzka6XImJycJRVHkG9/4Rtz/33PPPaSkpISwLEvsdjvR6XTk0UcfJe3t7eQ///M/iUQiIa+99hohhJAXX3yR6PV68tvf/pb09vaSY8eOke9973v8vj772c+S3bt3kwMHDpCuri6yb98+olAoSEdHByGEkOeff57IZDKye/ducujQIdLW1ka8Xi+xWCzkueee4/cTDoejvhsYGCD79u0jp0+fJm63mx/XsWNL521mZobs2rWL3HPPPWR4eJgMDw+TcDhM3nrrLQKATE9PE0IIOXHiBKFpmjz88MOkvb2dPP/880SlUpHnn3+e79tutxODwUCeeuop0tnZSR599FFC0zRpa2sjhBCyb98+UllZSQ4cOEB6enrIwYMHyc9//vO45zZX900souAQEVkD5GICcI/NRwmM2J/ucW8OR3yJo0ePEgDkpZdeivv/b3/72wQAGR0dJXa7ndx8881R/7/99tvJLbfcQggh5Fvf+hapq6sjwWBw2X56e3uJRCIhg4ODUd/fcMMN5Mtf/jIhZElwACBnzpyJ2ub+++8n119/Pf/597//PVEoFPyEH4/bbruNPPTQQ/znPXv2kPvvvz9qm1jB8YlPfILcdNNNUdt84QtfIE1NTfxnu91OPvnJT/KfWZYlZrOZPPPMM4QQQj7/+c+T66+/nrAsm3BsHEIJDtFUJSJymdA75Uv6/55JYf0dhJCUttu1a9eyz62trQCAj370o1hcXER1dTXuuecevPTSSwiHwwCA8+fPg2EY1NXVQavV8j/79++H2+3m9yeXy7Fp06aoPu644w68/fbbGBoaAgD87Gc/w2233Ybi4mIAS76QRx55BBs3boTBYIBWq8Xvf/979PX1pXUOWltbcfXVV0d9d/XVV6OzsxMMw/DfRY6PoihYrVa+7tRdd92FM2fOoL6+Hvfddx9ee+21tMaQC0TBISJymWA3JC9X4jBqBOm3trYWFEXxk38sra2tKCkpQWlp6Yr7qqysRHt7O55++mmoVCr89V//Nd773vciFArB6/VCIpHg5MmTOHPmDP/T2tqKJ554gt+HSqVa5qy+8sorUVNTg//6r//C4uIiXnrpJdxxxx38//ft24cnnngCX/ziF/HW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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plot_toy_data(x=train_X, y=Y)\n", + "ax.scatter(\n", + " train_X[:, 0].detach().numpy().squeeze(),\n", + " train_X[:, 1].detach().numpy().squeeze(),\n", + " zs=prob_regression_model(train_X).mean.detach().squeeze().numpy(),\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, since our custom model is based off `Model` and `Posterior`, we can use both analytic and MC based acquisition functions for optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0., 0.]]), tensor(-0.1007))" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from botorch.acquisition.analytic import LogExpectedImprovement\n", + "from botorch.optim.optimize import optimize_acqf\n", + "\n", + "candidate, acq_val = optimize_acqf(\n", + " LogExpectedImprovement(model=prob_regression_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + ")\n", + "candidate, acq_val" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before using `qLogExpectedImprovement` we need to register an appropriate sampler for the `TorchPosterior`. We can use the following code to create a `MCSampler` for that is specific to `torch.distributions.StudentT`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.sampling.base import MCSampler\n", + "from botorch.sampling.get_sampler import GetSampler\n", + "from botorch.sampling.stochastic_samplers import ForkedRNGSampler\n", + "\n", + "\n", + "@GetSampler.register(distributions.StudentT)\n", + "def _get_sampler_torch(\n", + " posterior: TorchPosterior,\n", + " sample_shape: torch.Size,\n", + " *,\n", + " seed: Optional[int] = None,\n", + ") -> MCSampler:\n", + " # Use `ForkedRNGSampler` to ensure determinism in acquisition function evaluations.\n", + " return ForkedRNGSampler(sample_shape=sample_shape, seed=seed)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0., 0.]]), tensor(-0.1105))" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from botorch.acquisition.logei import qLogExpectedImprovement\n", + "\n", + "optimize_acqf(\n", + " qLogExpectedImprovement(model=prob_regression_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Supported PyTorch Distributions\n", + "Although we chose the `StudentT` distribution in the above example, any distribution supporting the `rsample` method will work with BoTorch's automatic differentiation. We can use the `has_rsample` attribute to see a complete listing of compatible distributions." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Beta', 'Cauchy', 'Chi2', 'ContinuousBernoulli', 'Dirichlet', 'Exponential', 'FisherSnedecor', 'Gamma', 'Gumbel', 'HalfCauchy', 'HalfNormal', 'Independent', 'InverseGamma', 'Kumaraswamy', 'Laplace', 'LogNormal', 'LogisticNormal', 'LowRankMultivariateNormal', 'MultivariateNormal', 'Normal', 'OneHotCategoricalStraightThrough', 'Pareto', 'RelaxedBernoulli', 'RelaxedOneHotCategorical', 'StudentT', 'Uniform', 'Weibull', 'Wishart', 'TransformedDistribution']\n" + ] + } + ], + "source": [ + "print(\n", + " [\n", + " j.__name__\n", + " for j in [getattr(distributions, i) for i in distributions.__all__]\n", + " if hasattr(j, \"has_rsample\") and j.has_rsample\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bayesian Linear Regression\n", + "In the previous section, we directly parameterized a \"posterior\" with a linear layer. In this section, we will follow Chapter 14.2 of [Bayesian Data Analysis](https://stat.columbia.edu/~gelman/book/) to implement a *proper* posterior analytically. This implementation also uses `TorchPosterior` and the `StudentT` distribution like before." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Optional, Union\n", + "from torch import Tensor, distributions, nn\n", + "from botorch.acquisition.objective import PosteriorTransform\n", + "from botorch.models.model import Model\n", + "from botorch.posteriors.posterior import Posterior\n", + "from botorch.posteriors.torch import TorchPosterior\n", + "\n", + "\n", + "def add_intercept(x: Tensor) -> Tensor:\n", + " \"\"\"Adds an intercept column to the design matrix (i.e. tensor).\"\"\"\n", + " return torch.concat([torch.ones_like(x)[..., 0:1], x], dim=-1)\n", + "\n", + "\n", + "class BayesianRegressionModel(Model):\n", + " _num_outputs: int\n", + " df: int\n", + " s_squared: Tensor\n", + " beta: Tensor\n", + " L: Tensor\n", + " add_intercept: bool\n", + "\n", + " def __init__(self, intercept: bool = True) -> None:\n", + " super(BayesianRegressionModel, self).__init__()\n", + " self.add_intercept = intercept\n", + "\n", + " @property\n", + " def num_outputs(self) -> int:\n", + " return self._num_outputs\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " return x @ self.beta\n", + "\n", + " def fit(self, x: Tensor, y: Tensor) -> None:\n", + " self._num_outputs = y.shape[-1]\n", + " x = add_intercept(x) if self.add_intercept else x\n", + " n, p = x.shape\n", + " self.df = n - p\n", + " # Rather than V = torch.linalg.inv(x.T @ x) as in BDA\n", + " # instead use L = torch.linalg.cholesky(x.T @ x) for stability.\n", + " # To use L, we can simply replace operations like:\n", + " # x = V @ b\n", + " # with a call to `torch.cholesky_solve`:\n", + " # x = torch.cholesky_solve(b, L)\n", + " self.L = torch.linalg.cholesky(x.T @ x)\n", + " # Least squares estimate\n", + " # self.beta = torch.cholesky_solve(x.T, self.L) @ y\n", + " self.beta = torch.cholesky_solve(x.T, self.L) @ y\n", + " # Model's residuals from the labels.\n", + " r: Tensor = y - self(x)\n", + " # Sample variance\n", + " self.s_squared = (1 / self.df) * r.T @ r\n", + "\n", + " def posterior(\n", + " self,\n", + " X: Tensor,\n", + " output_indices: Optional[list[int]] = None,\n", + " observation_noise: Union[bool, Tensor] = False,\n", + " posterior_transform: Optional[PosteriorTransform] = None,\n", + " ) -> Posterior:\n", + " # Squeeze out the q dimension if needed.\n", + " n, q, _ = X.shape\n", + " if output_indices:\n", + " X = X[..., output_indices]\n", + " if self.add_intercept:\n", + " X = add_intercept(X)\n", + " loc = self(X)\n", + " # Full covariance matrix of all test points.\n", + " cov = self.s_squared * (\n", + " torch.eye(n, n) + X.squeeze() @ torch.cholesky_solve(X.squeeze().T, self.L)\n", + " )\n", + " # The batch semantics of BoTorch evaluate each data point in their own batch.\n", + " # So, we extract the diagonal representing Var[\\tilde y_i | y_i] of each test point.\n", + " scale = torch.diag(cov).reshape(n, q, self.num_outputs)\n", + " # Form the posterior predictive dist according to Sec 14.2, Pg 357 of BDA.\n", + " posterior_predictive_dist = distributions.StudentT(\n", + " df=self.df, loc=loc, scale=scale\n", + " )\n", + " posterior = TorchPosterior(distribution=posterior_predictive_dist)\n", + " if posterior_transform is not None:\n", + " posterior = posterior_transform(posterior)\n", + " return posterior" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "bayesian_regression_model = BayesianRegressionModel(intercept=True)\n", + "bayesian_regression_model.fit(train_X, Y)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY4AAAGbCAYAAADTH3dUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAADOkElEQVR4nOy9d3gsV33//57Z3rXaXa2k1Rb1cnsvLve6cTHGMQEcDC4YgiEBY8CGUMIXEhtwjHEDbBMHsE0oIU6w4/xM3O1bdXvT1VXvvXdtnTm/P8TM3a7tWu2d1/PsI+3snjlnZmfOZz71UIQQAgEBAQEBgRihV3oAAgICAgKrC0FwCAgICAjEhSA4BAQEBATiQhAcAgICAgJxIQgOAQEBAYG4EASHgICAgEBcCIJDQEBAQCAuBMEhICAgIBAXguAQEBAQEIgLQXAICAgICMSFIDgEsgaKomJ6vf/++2kdx969e/m+aJqGVqtFdXU17rzzTrz11ltJ7fuZZ57BCy+8kJqBCgisEJRQq0ogW/jtb38b8P43v/kN3nrrLfz7v/97wPYbbrgBZrM5bePYu3cvOjo68PDDDwMAFhYW0N7ejj/96U/o7OzE3/zN3+C3v/0tJBJJ3Pteu3YtjEZj2oWfgEA6Ea/0AAQEOO64446A90ePHsVbb70Vsj0T6HS6kH7/5V/+Bffddx+eeeYZOBwOPPLIIxkfl4BANiCYqgRWFQsLC3jggQdgtVohk8lQXV2Nn/zkJ/BXnPfs2YMNGzaEbV9dXY19+/Yl1LdIJMJPf/pT1NXV4ec//zlmZmb4z55//nlce+21KCgogEwmQ11dHZ599tmA9g6HA42Njdi/fz9vCtu7dy8AYHJyEl//+texbt06qNVqaLVa3HjjjTh37lxCYxUQSCeC4BBYNRBC8Fd/9Vd44okn8MEPfhCPP/44qqur8Y1vfAP3338//70777wT58+fx4ULFwLanzhxAq2trUlpMCKRCJ/85CexuLiIQ4cO8dufffZZ2O12fOc738Fjjz0Gq9WKL37xi3j66af57zz55JMoKSlBTU0N/v3f/x3//u//jn/8x38EAHR2duKVV17Bhz/8YTz++OP4xje+gYaGBuzZsweDg4MJj1dAIC0QAYEs5Utf+hLxv0RfeeUVAoD84Ac/CPjexz/+cUJRFGlvbyeEEDI9PU3kcjn55je/GfC9++67j6hUKjI/Px+13z179pA1a9ZE/Pzll18mAMhTTz3Fb1tcXAz53r59+0hZWVnAtjVr1pA9e/aEfNflchGGYQK2dXV1EZlMRh588MGo4xUQyDSCxiGwavjzn/8MkUiE++67L2D7Aw88AEII/u///g/Akn/illtuwR/+8AfehMUwDP74xz/iIx/5CFQqVVLjUKvVAIC5uTl+m0Kh4P+fmZnB+Pg49uzZg87OzgCTViRkMhlomubHOjExAbVajerqapw+fTqp8QoIpBpBcAisGnp6elBcXAyNRhOwvba2lv+c46677kJvby8OHjwIAHj77bcxMjKCO++8M+lxzM/PA0DAOA4fPozrr78eKpUKeXl5MJlM+M53vgMAMQkOlmXxxBNPoLKyEjKZDEajESaTCefPn4+pvYBAJhEEh0BOsm/fPpjNZj7E97e//S0KCwtx/fXXJ71vzndSUVEBAOjo6MB1112H8fFxPP7443jttdfw1ltv4Wtf+xqAJaGwHD/60Y9w//334+qrr8Zvf/tbvPHGG3jrrbewZs2amNoLCGQSIRxXYNVgt9vx9ttvY25uLuBpv7m5mf+cQyQS4VOf+hReeOEFPPLII3jllVdwzz33QCQSJTUGhmHw+9//HkqlEldeeSUA4H//93/hdrvx6quvwmaz8d997733QtpTFBV2v//1X/+Fa665Br/61a8Ctk9PT8NoNCY1ZgGBVCNoHAKrhg996ENgGAY///nPA7Y/8cQToCgKN954Y8D2O++8E1NTU/jCF76A+fn5pPNBGIbBfffdh6amJtx3333QarUAwAsj4hcSPDMzg+effz5kHyqVCtPT0yHbRSJRQHsAeOmllzAwMJDUmAUE0oGgcQisGm6++WZcc801+Md//Ed0d3djw4YNePPNN/E///M/+OpXv4ry8vKA72/atAlr167FSy+9hNraWmzevDnmvmZmZngz1+LiIp853tHRgdtuuw0PPfQQ/90PfOADkEqluPnmm3kh9W//9m8oKCjA0NBQwH63bNmCZ599Fj/4wQ9QUVGBgoICXHvttfjwhz+MBx98EJ/5zGewe/duNDQ04He/+x3KysqSOGMCAmliZYO6BAQiExyOSwghc3Nz5Gtf+xopLi4mEomEVFZWkkcffZSwLBt2Hz/+8Y8JAPKjH/0o5n737NlDAPAvtVpNKisryR13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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plot_toy_data(x=train_X, y=Y)\n", + "ax.scatter(\n", + " train_X[:, 0].detach().numpy().squeeze(),\n", + " train_X[:, 1].detach().numpy().squeeze(),\n", + " zs=bayesian_regression_model(add_intercept(train_X)).detach().squeeze().numpy(),\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0., 0.]]), tensor(-1.3847))" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optimize_acqf(\n", + " LogExpectedImprovement(model=bayesian_regression_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0., 0.]]), tensor(-1.3684))" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optimize_acqf(\n", + " qLogExpectedImprovement(model=bayesian_regression_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bayesian Linear Regression w/ `EnsemblePosterior`\n", + "The `EnsembleModel` class provides a default implementation for `posterior()`. Then the MC acquisition function will be optimized using samples from the posterior predictive distribution (`EnsemblePosterior` also implements `mean` and `variance` properties, so some other analytic acquisition functions will also work). We follow this [Pyro tutorial](https://pyro.ai/examples/bayesian_regression.html#Bayesian-Regression-with-Pyro%E2%80%99s-Stochastic-Variational-Inference-(SVI)) for a linear regression model fit with [Stochastic Variational Inference](https://pyro.ai/examples/svi_part_i.html) (SVI)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, we define a Pyro model capable of sampling from a posterior predictive distribution for new observations at test points. Later, when we perform posterior predictive inference, we will use Pyro's [`Predictive`](https://docs.pyro.ai/en/dev/_modules/pyro/infer/predictive.html) class. By default, `Predictive` ignores inference gradients with:\n", + "\n", + "```python\n", + "model = torch.no_grad()(poutine.mask(model, mask=False) if mask else model)\n", + "```\n", + "\n", + "Since we need to retain the autograd graph to optimize the acquisition function, we can use `torch.set_grad_enabled(True)` in the `forward()` method to override this behavior." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "import pyro\n", + "import pyro.distributions as dist\n", + "from pyro.infer.autoguide import AutoGuide, AutoDiagonalNormal\n", + "from pyro.nn import PyroSample, PyroModule\n", + "from pyro.infer import SVI, Trace_ELBO\n", + "from pyro.optim import PyroOptim\n", + "\n", + "pyro.set_rng_seed(1)\n", + "\n", + "\n", + "# Bayesian Regression represented as a single hidden layer.\n", + "class BayesianRegression(PyroModule):\n", + " Y: str = \"y\"\n", + "\n", + " def __init__(self, in_features: int, out_features: int):\n", + " super().__init__()\n", + " # Linear layer like before, but wrapped with PyroModule.\n", + " self.linear = PyroModule[nn.Linear](in_features, out_features)\n", + " # Add priors to the weights & bias of the linear layer.\n", + " self.linear.weight = PyroSample(\n", + " dist.Normal(0.0, 1.0)\n", + " .expand(torch.Size([out_features, in_features]))\n", + " .to_event(2)\n", + " )\n", + " self.linear.bias = PyroSample(\n", + " dist.Normal(0.0, 10.0).expand(torch.Size([out_features])).to_event(1)\n", + " )\n", + "\n", + " def forward(self, x: Tensor, y: Optional[Tensor] = None) -> Tensor:\n", + " # NOTE: Enable gradient tracking to override behavior of `Predictive`.\n", + " torch.set_grad_enabled(True)\n", + " # Prior for the noise level.\n", + " sigma = pyro.sample(\"sigma\", dist.Uniform(0.0, 10.0))\n", + " # Linear layer on the inputs.\n", + " mean = self.linear(x).squeeze(-1)\n", + " n, p = x.shape[0], x.shape[-1]\n", + " with pyro.plate(\"data\", x.shape[0]):\n", + " # Observations will be t distributed.\n", + " t_dist = dist.StudentT(df=n - p, loc=mean, scale=sigma)\n", + " _ = pyro.sample(self.Y, t_dist, obs=y)\n", + " return mean" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "def fit_svi(\n", + " epochs: int,\n", + " model: PyroModule,\n", + " guide: AutoGuide,\n", + " optimizer: PyroOptim,\n", + " train_X: Tensor,\n", + " train_Y: Tensor,\n", + ") -> None:\n", + " svi = SVI(\n", + " model,\n", + " guide,\n", + " optimizer,\n", + " loss=Trace_ELBO(),\n", + " )\n", + " pyro.clear_param_store()\n", + " for epoch in range(epochs):\n", + " loss = svi.step(train_X, train_Y.squeeze())\n", + " if epoch % 10 == 0:\n", + " print(\"epoch {}, loss {}\".format(epoch, loss))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we incorporate our Pyro model into the `Model` and `Posterior` interface like before. `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the output size of the model and `s` is the ensemble size." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.models.ensemble import EnsembleModel\n", + "from pyro.infer import Predictive\n", + "\n", + "class EnsembleBayesianRegressionModel(EnsembleModel):\n", + " model: BayesianRegression\n", + " guide: AutoGuide\n", + " num_samples: int\n", + " _num_outputs: int\n", + "\n", + " def __init__(self, train_X: Tensor, train_Y: Tensor, num_samples: int = 100):\n", + " super(EnsembleBayesianRegressionModel, self).__init__()\n", + " self._num_outputs = train_Y.shape[-1]\n", + " self.model = BayesianRegression(train_X.shape[-1], self.num_outputs)\n", + " self.guide = AutoDiagonalNormal(self.model)\n", + " self.num_samples = num_samples\n", + "\n", + " def forward(self, X: Tensor) -> Tensor:\n", + " predictive = Predictive(\n", + " self.model,\n", + " guide=self.guide,\n", + " num_samples=self.num_samples,\n", + " # Only return the posterior predictive distribution for y.\n", + " return_sites=(self.model.Y,),\n", + " )\n", + " # `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the\n", + " # output size of the model and `s` is the ensemble size.\n", + " samples = (\n", + " # Retrieve posterior samples from the observation random variable.\n", + " # This is also known as a posterior predictive distribution.\n", + " predictive(X.squeeze())[self.model.Y]\n", + " # Move the ensemble dimension to \"s\" axis.\n", + " .transpose(0, 1)\n", + " # Reshape for `EnsemblePosterior` as mentioned above.\n", + " .reshape(X.shape[0], -1, 1, self.num_outputs)\n", + " )\n", + " return samples" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch 0, loss 57.859971924474735\n", + "epoch 10, loss 47.17245571053782\n", + "epoch 20, loss 27.547291517941602\n", + "epoch 30, loss 34.39363837327427\n", + "epoch 40, loss 43.94011251783476\n", + "epoch 50, loss 33.11519462561163\n", + "epoch 60, loss 28.7194289840763\n", + "epoch 70, loss 24.450418378181947\n", + "epoch 80, loss 11.057529271793364\n", + "epoch 90, loss 13.638860647173294\n" + ] + } + ], + "source": [ + "ensemble_bayesian_regression_model = EnsembleBayesianRegressionModel(\n", + " train_X=train_X, train_Y=Y\n", + ")\n", + "fit_svi(\n", + " 100,\n", + " ensemble_bayesian_regression_model.model,\n", + " ensemble_bayesian_regression_model.guide,\n", + " pyro.optim.Adam({\"lr\": 0.1}),\n", + " train_X,\n", + " Y,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY4AAAGbCAYAAADTH3dUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAADNWklEQVR4nOy9Z5gkZ32vfVfnMN2TenLP9OS0eVfSaiWkFZJAiHDANsEgEIgXgY8BYYQxxmBzQAYZBELCgDDGINkIbOODABsOytLm1Wql1cbJOec8HaveD0PVds90z3SesHVf11wz091VT3V19fOrf3wESZIkVFRUVFRUokSz3gegoqKiorK5UIVDRUVFRSUmVOFQUVFRUYkJVThUVFRUVGJCFQ4VFRUVlZhQhUNFRUVFJSZU4VBRUVFRiQlVOFRUVFRUYkIVDhUVFRWVmFCFQ0VFRUUlJlThUNkwCIIQ1c8LL7yQ0uO46aablLE0Gg12u526ujo+8IEP8PTTTye07+9///s8+uijyTlQFZV1QlB7ValsFH7605+G/P+v//qvPP300/zbv/1byONveMMbKCgoSNlx3HTTTbS3t3P//fcDMD8/T1tbG7/85S/p6Ojg3e9+Nz/96U/R6/Ux73v79u04HI6Ui5+KSirRrfcBqKjIvP/97w/5/8SJEzz99NMrHk8HmZmZK8b9h3/4B+655x6+//3vU15ezte//vW0H5eKykZAdVWpbCrm5+f5zGc+Q2lpKUajkbq6Or75zW8SbDgfPHiQXbt2hd2+rq6O2267La6xtVot3/nOd2hsbOS73/0u09PTynM/+clPuPnmm8nPz8doNNLY2MgjjzwSsn15eTkXLlzgxRdfVFxhN910EwATExP85V/+JTt27CAjIwO73c7tt9/Oa6+9FtexqqikElU4VDYNkiTxv/7X/+Lb3/42b3rTm3jwwQepq6vjs5/9LPfee6/yug984AOcPXuW8+fPh2x/6tQpWlpaErJgtFot733ve1lYWODIkSPK44888ggul4u/+Zu/4Vvf+halpaX8+Z//Od/73veU1zz00EM4nU7q6+v5t3/7N/7t3/6NL3zhCwB0dHTwq1/9ire+9a08+OCDfPazn+XcuXMcPHiQgYGBuI9XRSUlSCoqG5SPf/zjUvAl+qtf/UoCpL//+78Ped073/lOSRAEqa2tTZIkSZqampJMJpP0uc99LuR199xzj2S1WqW5ublVxz148KC0bdu2iM8/8cQTEiA9/PDDymMLCwsrXnfbbbdJlZWVIY9t27ZNOnjw4IrXut1uKRAIhDzW2dkpGY1G6Stf+cqqx6uikm5Ui0Nl0/C73/0OrVbLPffcE/L4Zz7zGSRJ4v/9v/8HLMUn3v72t/Pzn/9ccWEFAgH+4z/+g3e84x1YrdaEjiMjIwOA2dlZ5TGz2az8PT09zdjYGAcPHqSjoyPEpRUJo9GIRqNRjnV8fJyMjAzq6up45ZVXEjpeFZVkowqHyqahu7ub4uJibDZbyOMNDQ3K8zJ33nknPT09HD58GIBnnnmG4eFhPvCBDyR8HHNzcwAhx3H06FFuvfVWrFYrWVlZ5OXl8Td/8zcAUQmHKIp8+9vfpqamBqPRiMPhIC8vj7Nnz0a1vYpKOlGFQ2VLctttt1FQUKCk+P70pz+lsLCQW2+9NeF9y7GT6upqANrb27nlllsYGxvjwQcf5Le//S1PP/00n/70p4ElUViLr33ta9x7773ceOON/PSnP+XJJ5/k6aefZtu2bVFtr6KSTtR0XJVNg8vl4plnnmF2djbkbr+pqUl5Xkar1fK+972PRx99lK9//ev86le/4u6770ar1SZ0DIFAgJ/97GdYLBZe97rXAfDf//3feDwefvOb31BWVqa89vnnn1+xvSAIYff7X//1X7z+9a/nX/7lX0Ien5qawuFwJHTMKirJRrU4VDYNb37zmwkEAnz3u98Nefzb3/42giBw++23hzz+gQ98gMnJST72sY8xNzeXcD1IIBDgnnvu4dKlS9xzzz3Y7XYARYykoJTg6elpfvKTn6zYh9VqZWpqasXjWq02ZHuAX/ziF/T39yd0zCoqqUC1OFQ2DW9729t4/etfzxe+8AW6urrYtWsXTz31FL/+9a/5i7/4C6qqqkJev2fPHrZv384vfvELGhoa2Lt3b9RjTU9PK26uhYUFpXK8vb2dP/3TP+W+++5TXvvGN74Rg8HA2972NkWk/vmf/5n8/HwGBwdD9rtv3z4eeeQR/v7v/57q6mry8/O5+eabeetb38pXvvIV7rrrLq677jrOnTvH448/TmVlZQJnTEUlRaxvUpeKSmSWp+NKkiTNzs5Kn/70p6Xi4mJJr9dLNTU10gMPPCCJohh2H9/4xjckQPra174W9bgHDx6UAOUnIyNDqqmpkd7//vdLTz31VNhtfvOb30g7d+6UTCaTVF5eLn3961+XfvzjH0uA1NnZqbxuaGhIestb3iLZbDYJUFJz3W639JnPfEYqKiqSzGazdP3110vHjx+XDh48GDZ9V0VlPVF7ValsaR5++GE+/elP09XVFRJ/UFFRiR9VOFS2LJIksWvXLnJzc8MGqlVUVOJDjXGobDnm5+f5zW9+w/PPP8+5c+f49a9/vd6HpKKypVAtDpUtR1dXFxUVFWRlZfHnf/7nfPWrX13vQ1JR2VKowqGioqKiEhNqHYeKioqKSkyowqGioqKiEhOqcKioqKioxIQqHCoqKioqMaEKh4qKiopKTKjCoaKioqISE6pwqKioqKjEhCocKioqKioxobYcUVHZZAQCAXw+33ofhsoGR6/XJ7xwWSRU4VBR2SRIksTQ0FDYhaBUVMKRlZVFYWFhxJUn40UVDhWVTYIsGvn5+VgslqRPBipbB0mSWFhYYGRkBICioqKk7l8VDhWVTUAgEFBEIzc3d70PR2UTYDabARgZGSE/Pz+pbis1OK6isgmQYxoWi2Wdj0RlMyFfL8mOianCoaKyiVDdUyqxkKrrRRUOFRUVFZWYUIVDRUVFRSUmVOFQUVHZMJSXl/PQQw+t92EkjRdeeAFBELZcCrUqHCoqKmmht7eXD3/4wxQXF2MwGHC5XHzqU59ifHx8vQ8tKdx00038xV/8Rchj1113HYODg2RmZq7PQaUIVThUVK5AOkbneL55hM6x+fSM19HBVVddRWtrKz//+c9pa2vjBz/4Ac8++ywHDhxgYmIiLcexnEAggCiKKdu/wWBISQHeeqMKh4rKFcTUgpc7/+Ulbv7Wi9z1k1O8/psvcOe/vMT0QmpbmHz84x/HYDDw1FNPcfDgQcrKyrj99tt55pln6O/v5wtf+ILy2tnZWd773vditVopKSnhe9/7nvKcJEn8n//zfygrK8NoNFJcXMw999yjPO/xePjLv/xLSkpKsFqt7N+/nxdeeEF5/tFHHyUrK4vf/OY3NDY2YjQa+dGPfoTJZFrhTvrUpz7FzTffDMD4+Djvfe97KSkpwWKxsGPHDn7+858rr/3Qhz7Eiy++yMMPP4wgCAiCQFdXV1hX1f/9v/+Xbdu2YTQaKS8v51vf+lbIuOXl5Xzta1/jwx/+MDabjbKyMn74wx8qz3u9Xj7xiU9QVFSEyWTC5XJx//33x/W5xI2koqKy4VlcXJQuXrwoLS4uJrSfD/zopFT517+VXJ/7H+Wn8q9/K33gRyeTdKQrGR8flwRBkL72ta+Fff7uu++WsrOzJVEUJZfLJdlsNun++++Xmpubpe985zuSVquVnnrqKUmSJOkXv/iFZLfbpd/97ndSd3e3dPLkSemHP/yhsq+PfOQj0nXXXScdOnRIamtrkx544AHJaDRKLS0tkiRJ0k9+8hNJr9dL1113nXT06FGpqalJmpubkwoKCqQf/ehHyn78fn/IY319fdIDDzwgvfrqq1J7e7tyXCdPLp23qakp6cCBA9Ldd98tDQ4OSoODg5Lf75eef/55CZAmJyclSZKkl19+WdJoNNJXvvIVqbm5WfrJT34imc1m6Sc/+YkytsvlknJycqTvfe97Umtrq3T//fdLGo1GampqkiRJkh544AGptLRUOnTokNTV1SUdPnxY+tnPfhb23CbrulmOKhwqKpuAZEwA7SOzIYKx/KdjdC6JR3yZEydOSID0xBNPhH3+wQcflABpeHhYcrlc0pve9KaQ59/znvdIt99+uyRJkvStb31Lqq2tlbxe74r9dHd3S1qtVurv7w95/JZbbpE+//nPS5K0JByAdObMmZDXfOpTn5Juvvlm5f8nn3xSMhqNyoQfjre85S3SZz7zGeX/gwcPSp/61KdCXrNcON73vvdJb3jDG0Je89nPflZqbGxU/ne5XNL73/9+5X9RFKX8/HzpkUcekSRJkj75yU9KN998sySKYsRjk0mVcKiuKhWVK4TuiYVVn+8aT228Q5KkqF534MCBFf9funQJgHe9610sLi5SWVnJ3XffzRNPPIHf7wfg3LlzBAIBamtrycjIUH5efPFF2tvblf0ZDAZ27twZMsYdd9zBCy+8wMDAAACPP/44b3nLW8jKygKWYiH33XcfO3bsICcnh4yMDJ588kl6enpiOgeXLl3i+uuvD3ns+uuvp7W1lUAgoDwWfHyCIFBYWKj0nfrQhz7EmTNnqKur45577uGpp56K6RiSgSocKipXCK6c1duVlOdaUzJudXU1giAok/9yLl26RHZ2Nnl5eWvuq7S0lObmZr7//e9jNpv58z//c2688UZ8Ph9zc3NotVpOnz7NmTNnlJ9Lly7x8MMPK/swm80rgtVXX301VVVV/Pu//zuLi4s88cQT3HHHHcrzDzzwAA8//DCf+9zneP755zlz5gy33XYbXq83zrOyOnq9PuR/QRCUIP7evXvp7OzkvvvuY3FxkXe/+928853vTMlxREJtcqiicoVQmZfBjTV5HG0bIxB0968VBK6vdlDhSI1w5Obm8oY3vIHvf//7fPrTn1aa78FSx9/HH3+cO++8U5nMT5w4EbL9iRMnaGhoUP43m8287W1v421vexsf//jHqa+v59y5c+zZs4dAIMDIyAg33HBDzMd5xx138Pjjj+N0OtFoNLzlLW9Rnjt69Chvf/vbef/73w+AKIq0tLTQ2NiovMZgMIRYDeFoaGjg6NGjIY8dPXqU2tramJoQ2u123vOe9/Ce97yHd77znbzpTW9iYmKCnJycqPeRCKrFoaJyBfGP793D9dWOkMeur3bwj+/dk9Jxv/vd7+LxeLjttts4dOgQvb29/P73v+cNb3gDJSUlfPWrX1Vee/ToUb7xjW/Q0tLC9773PX7xi1/wqU99CljKivqXf/kXzp8/T0dHBz/96U8xm824XC5qa2u54447uPPOO/nlL39JZ2cnL730Evfffz+//e1v1zzGO+64g1deeYWvfvWrvPOd78RoNCrP1dTU8PTTT3Ps2DEuXbrExz72MYaHh0O2Ly8v5+TJk3R1dTE2NhY2zfczn/kMzz77LPfddx8tLS089thjfPe73+Uv//Ivoz6XDz74ID//+c9pamqipaWFX/ziFxQWFiputbSQ1IiJiopKSkh2kLNjdE56rmk4ZQHxcHR1dUkf/OAHpYKCAkmv10ulpaXSJz/5SWlsbEx5jcvlkr785S9L73rXuySLxSIVFhZKDz/8sPL8E088Ie3fv1+y2+2S1WqVrr32WumZZ55Rnvd6vdLf/d3fSeXl5ZJer5eKioqkP/qjP5LOnj0rSdJScDwzMzPiMV5zzTUSID333HMhj4+Pj0tvf/vbpYyMDCk/P1/64he/KN15553S29/+duU1zc3N0rXXXiuZzWYJkDo7O1cExyVJkv7rv/5LamxslPR6vVRWViY98MADIWO5XC7p29/+dshju3btkr70pS9JkiRJP/zhD6Xdu3dLVqtVstvt0i233CK98sorYd9PqoLjgiRFGbFSUVFZN9xuN52dnVRUVGAymdb7cFQ2Cam6blRXlYqKiopKTKjCoaKioqISE6pwqKio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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plot_toy_data(x=train_X, y=Y)\n", + "ax.scatter(\n", + " train_X[:, 0].detach().numpy().squeeze(),\n", + " train_X[:, 1].detach().numpy().squeeze(),\n", + " zs=ensemble_bayesian_regression_model(train_X)\n", + " .detach()\n", + " .squeeze()\n", + " .mean(dim=-1)\n", + " .numpy(),\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0., 0.]]), tensor(-1.0121))" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optimize_acqf(\n", + " LogExpectedImprovement(model=ensemble_bayesian_regression_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0., 0.]]), tensor(-0.8815))" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optimize_acqf(\n", + " qLogExpectedImprovement(model=ensemble_bayesian_regression_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Random Forest w/ Ensemble Posterior\n", + "Finally, we move away from linear models to any ML technique that ensembles many models. Specifically, we can use the [RandomForestRegressor](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html) from sklearn which is an ensemble method of individual decision trees. These decision trees can be accessed through the object's `estimators_` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from sklearn.ensemble import RandomForestRegressor\n", + "from botorch.models.ensemble import EnsembleModel\n", + "\n", + "\n", + "class EnsembleRandomForestModel(EnsembleModel):\n", + " model: RandomForestRegressor\n", + " num_samples: int\n", + " _num_outputs: int\n", + "\n", + " def __init__(self, num_samples: int = 100):\n", + " super(EnsembleRandomForestModel, self).__init__()\n", + " self._num_outputs = 1\n", + " self.model = RandomForestRegressor(n_estimators=num_samples)\n", + "\n", + " def fit(self, X: Tensor, y: Tensor) -> None:\n", + " self.model = self.model.fit(\n", + " X=X.detach().numpy(), y=y.detach().numpy().squeeze()\n", + " )\n", + "\n", + " def forward(self, X: Tensor) -> Tensor:\n", + " x = X.detach().numpy().squeeze()\n", + " # Create the ensemble from predictions from each decision tree.\n", + " y = torch.from_numpy(np.array([i.predict(x) for i in self.model.estimators_]))\n", + " # `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the\n", + " # output size of the model and `s` is the ensemble size.\n", + " samples = y.transpose(0, 1).reshape(X.shape[0], -1, 1, self.num_outputs)\n", + " return samples" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "ensemble_random_forest_model = EnsembleRandomForestModel(num_samples=300)\n", + "ensemble_random_forest_model.fit(X=train_X, y=Y)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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m9T3kjREQEAg53G73eTChQvE7Xan9Dmf/NURUvXNl4j1G//LP6q++RL55K+OpGlPFzIxMUEoiiI//vGPI75+2223Eb1eT1iWJVarlWg0GnLvvfeS1tZW8l//9V9EJBKRV199lRBCyPPPP08KCgrIP/7xD9LT00OOHDlCnnzySf6zPvOZz5CtW7eSffv2kY6ODrJr1y4ik8lIW1sbIYSQp556ikgkErJ161Zy4MAB0tLSQhwOBzGbzeRXv/oV/zmBQCDkuf7+frJr1y5y6tQp0tnZye/XkSPz5216epps2bKF3HbbbWRoaIgMDQ2RQCBA3nzzTQKATE1NEUIIOX78OKFpmvzwhz8kra2t5KmnniIKhYI89dRT/NhWq5UYDAby6KOPkvb2dnLvvfcSmqZJS0sLIYSQXbt2EYvFQvbt20fsdjvZv38/+f3vfx/x3KbruglHEA4BgWVAOiaAztG5EMEIf3SNOdK4x+9w+PBhAoC88MILEV+///77CQAyMjJCrFYruf7660Nev/nmm8kNN9xACCHk5z//OamrqyM+n2/B5/T09BCRSEQGBgZCnr/22mvJt771LULIvHAAIKdPnw55z5e+9CVyzTXX8H+/8sorRCaT8RN+JG688UZy11138X9v27aNfOlLXwp5T7hwfPzjHyfvete7Qt7zta99jTQ1NfF/W61W8olPfIL/m2VZUlRURB5//HFCCCFf/OIXyTXXXENYlo26bxyZEg7BVSUgcJHQM+mK+bp9IrPxDkJIXO/bsmXLgr+bm5sBAB/+8IfhdrtRVVWF2267DS+88AICgQAA4OzZs2AYBnV1dVCr1fxj79696Ozs5D9PKpVi9erVIWPs2LEDe/bsweDgIADgueeew4033gidTgdgPhZy9913Y9WqVTAYDFCr1XjllVfQ29ub0Dlobm7GZZddFvLcZZddhvb2djAMwz8XvH8URaG4uJjvO3XLLbfg9OnTqK+vxx133IFXX301oX1IB4JwCAhcJFgNsduV2IyqjIxbU1MDiqL4yT+c5uZm6PV6FBYWLvpZFosFra2teOyxx6BQKPD5z38eV155Jfx+PxwOB0QiEU6cOIHTp0/zj+bmZjz00EP8ZygUigXB6ksvvRTV1dX44x//CLfbjRdeeAE7duzgX9+1axceeughfOMb38Cbb76J06dPY/v27fD5fEmeldhI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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plot_toy_data(x=train_X, y=Y)\n", + "ax.scatter(\n", + " train_X[:, 0].detach().numpy().squeeze(),\n", + " train_X[:, 1].detach().numpy().squeeze(),\n", + " zs=ensemble_random_forest_model(train_X).detach().squeeze().mean(dim=-1).numpy(),\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to use gradient-based optimization of the acquisition function (via the standard `optimize_acqf()` method) we will need to have the samples drawn from the posterior be differentiable w.r.t. to the input to the `posterior()` method (this is not the case for Random Forest models). Instead, we will perform the acquisition function optimization with gradient-free methods." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0.3959, 1.3023]]), tensor(-4.3914))" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optimize_acqf(\n", + " LogExpectedImprovement(model=ensemble_random_forest_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + " options={\"with_grad\": False},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[0.9057, 0.0959]]), tensor(-15.1323))" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optimize_acqf(\n", + " qLogExpectedImprovement(model=ensemble_random_forest_model, best_f=Y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=5,\n", + " raw_samples=10,\n", + " options={\"with_grad\": False},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CMA-ES\n", + "We can also move the optimization loop out of BoTorch entirely and follow the [CMA-ES tutorial](https://botorch.org/tutorials/optimize_with_cmaes) to optimize with an evolution strategy." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(25_w,50)-aCMA-ES (mu_w=14.0,w_1=14%) in dimension 2 (seed=380612, Wed Aug 21 17:25:36 2024)\n" + ] + }, + { + "data": { + "text/plain": [ + "tensor([0.4497, 0.8411])" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import cma\n", + "import numpy as np\n", + "\n", + "x0 = np.random.rand(2)\n", + "\n", + "es = cma.CMAEvolutionStrategy(\n", + " x0=x0,\n", + " sigma0=0.2,\n", + " inopts={\"bounds\": [0, 2], \"popsize\": 50},\n", + ")\n", + "\n", + "log_expected_improvement_ensemble_random_forest_model = LogExpectedImprovement(\n", + " model=ensemble_random_forest_model, best_f=Y.max()\n", + ")\n", + "\n", + "with torch.no_grad():\n", + " while not es.stop():\n", + " xs = es.ask()\n", + " y = (\n", + " -log_expected_improvement_ensemble_random_forest_model(\n", + " torch.from_numpy(np.array(xs)).unsqueeze(-2)\n", + " )\n", + " .view(-1)\n", + " .double()\n", + " .numpy()\n", + " )\n", + " es.tell(xs, y)\n", + "\n", + "torch.from_numpy(es.best.x)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/custom_model.py b/website-old/static/files/custom_model.py new file mode 100644 index 0000000000..9d37684103 --- /dev/null +++ b/website-old/static/files/custom_model.py @@ -0,0 +1,652 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ### Custom Models in BoTorch +# In this tutorial, we illustrate how to create a custom surrogate model using the [`Model`](https://github.com/pytorch/botorch/blob/main/botorch/models/model.py) and [`Posterior`](https://github.com/pytorch/botorch/blob/main/botorch/posteriors/posterior.py) interface. We will cover creating surrogate models from: +# - PyTorch distributions +# - Posterior samples (using Pyro) +# - Ensemble of ML predictions +# +# This tutorial differs from the [Using a custom BoTorch model with Ax](https://botorch.org/tutorials/custom_botorch_model_in_ax) tutorial by focusing more on authoring a new model that is compatible with the BoTorch and less on integrating a custom model with Ax's `botorch_modular` API. + +# In[1]: + + +import torch + +# Set the seed for reproducibility +torch.manual_seed(1) +# Double precision is highly recommended for BoTorch. +# See https://github.com/pytorch/botorch/discussions/1444 +torch.set_default_dtype(torch.float64) + +train_X = torch.rand(20, 2) * 2 +Y = 1 - (train_X - 0.5).norm(dim=-1, keepdim=True) +Y += 0.1 * torch.rand_like(Y) +bounds = torch.stack([torch.zeros(2), 2 * torch.ones(2)]) + + +# Code to plot our training data. + +# In[2]: + + +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from torch import Tensor +from mpl_toolkits.mplot3d import Axes3D + +# Needed for older versions of matplotlib. +assert Axes3D + + +def plot_toy_data(x: Tensor, y: Tensor) -> Axes: + ax = plt.figure().add_subplot(projection="3d") + ax.scatter( + x[:, 0].detach().numpy().squeeze(), + x[:, 1].detach().numpy().squeeze(), + zs=y.detach().numpy().squeeze(), + label="Observations", + ) + ax.set_xlabel("X1") + ax.set_ylabel("X2") + ax.set_zlabel("Y") + ax.set_title("Toy Data") + ax.view_init(elev=15.0, azim=65) + ax.legend() + return ax + + +plot_toy_data(x=train_X, y=Y) +plt.show() + + +# ### Probabilistic Linear Regression (w/ Torch Distributions) +# BoTorch's `Model` class only requires you to define a `posterior()` method that returns a `Posterior` object, the only requirement of which is to implement an `rsample()` function for drawing posterior samples. Specifically, we can utilize the subclass [`TorchPosterior`](https://github.com/pytorch/botorch/blob/main/botorch/posteriors/torch.py) that directly wraps a [torch distribution](https://pytorch.org/docs/stable/distributions.html). + +# In[3]: + + +from typing import Optional, Union +from torch import Tensor, distributions, nn +from botorch.acquisition.objective import PosteriorTransform +from botorch.models.model import Model +from botorch.posteriors.posterior import Posterior +from botorch.posteriors.torch import TorchPosterior + + +class ProbabilisticRegressionModel(Model): + _num_outputs: int + + def __init__(self, train_X: Tensor, train_Y: Tensor): + super(ProbabilisticRegressionModel, self).__init__() + self._num_outputs = train_Y.shape[-1] + # Linear layer that will compute the regression output. + self.linear = nn.Linear(train_X.shape[-1], self.num_outputs) + + @property + def num_outputs(self) -> int: + return self._num_outputs + + def forward(self, x: Tensor) -> distributions.Distribution: + n, p = x.squeeze().shape + # For now, let's suppose we have known variance 1. + return distributions.StudentT(df=n - p, loc=self.linear(x), scale=1) + + def posterior( + self, + X: Tensor, + output_indices: Optional[list[int]] = None, + observation_noise: Union[bool, Tensor] = False, + posterior_transform: Optional[PosteriorTransform] = None, + ) -> Posterior: + if output_indices: + X = X[..., output_indices] + # TorchPosterior directly wraps our torch.distributions.Distribution output. + posterior = TorchPosterior(distribution=self(X)) + if posterior_transform is not None: + posterior = posterior_transform(posterior) + return posterior + + +# In[4]: + + +def fit_prob_reg( + epochs: int, + model: ProbabilisticRegressionModel, + optimizer: torch.optim.Optimizer, + train_X: Tensor, + train_Y: Tensor, +) -> None: + """Optimization loop for linear regression.""" + train_X = train_X.requires_grad_() + for epoch in range(epochs): + optimizer.zero_grad() + outputs = model(train_X) + loss = -outputs.log_prob(train_Y).mean() + loss.backward() + optimizer.step() + if epoch % 10 == 0: + print("epoch {}, loss {}".format(epoch, loss.item())) + + +# In[5]: + + +prob_regression_model = ProbabilisticRegressionModel(train_X, Y) +optimizer = torch.optim.Adam(prob_regression_model.parameters(), lr=0.1) +fit_prob_reg(50, prob_regression_model, optimizer, train_X, Y) + + +# In[6]: + + +ax = plot_toy_data(x=train_X, y=Y) +ax.scatter( + train_X[:, 0].detach().numpy().squeeze(), + train_X[:, 1].detach().numpy().squeeze(), + zs=prob_regression_model(train_X).mean.detach().squeeze().numpy(), +) +plt.show() + + +# Finally, since our custom model is based off `Model` and `Posterior`, we can use both analytic and MC based acquisition functions for optimization. + +# In[7]: + + +from botorch.acquisition.analytic import LogExpectedImprovement +from botorch.optim.optimize import optimize_acqf + +candidate, acq_val = optimize_acqf( + LogExpectedImprovement(model=prob_regression_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, +) +candidate, acq_val + + +# Before using `qLogExpectedImprovement` we need to register an appropriate sampler for the `TorchPosterior`. We can use the following code to create a `MCSampler` for that is specific to `torch.distributions.StudentT`. + +# In[8]: + + +from botorch.sampling.base import MCSampler +from botorch.sampling.get_sampler import GetSampler +from botorch.sampling.stochastic_samplers import ForkedRNGSampler + + +@GetSampler.register(distributions.StudentT) +def _get_sampler_torch( + posterior: TorchPosterior, + sample_shape: torch.Size, + *, + seed: Optional[int] = None, +) -> MCSampler: + # Use `ForkedRNGSampler` to ensure determinism in acquisition function evaluations. + return ForkedRNGSampler(sample_shape=sample_shape, seed=seed) + + +# In[9]: + + +from botorch.acquisition.logei import qLogExpectedImprovement + +optimize_acqf( + qLogExpectedImprovement(model=prob_regression_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, +) + + +# #### Supported PyTorch Distributions +# Although we chose the `StudentT` distribution in the above example, any distribution supporting the `rsample` method will work with BoTorch's automatic differentiation. We can use the `has_rsample` attribute to see a complete listing of compatible distributions. + +# In[10]: + + +print( + [ + j.__name__ + for j in [getattr(distributions, i) for i in distributions.__all__] + if hasattr(j, "has_rsample") and j.has_rsample + ] +) + + +# ### Bayesian Linear Regression +# In the previous section, we directly parameterized a "posterior" with a linear layer. In this section, we will follow Chapter 14.2 of [Bayesian Data Analysis](https://stat.columbia.edu/~gelman/book/) to implement a *proper* posterior analytically. This implementation also uses `TorchPosterior` and the `StudentT` distribution like before. + +# In[11]: + + +from typing import Optional, Union +from torch import Tensor, distributions, nn +from botorch.acquisition.objective import PosteriorTransform +from botorch.models.model import Model +from botorch.posteriors.posterior import Posterior +from botorch.posteriors.torch import TorchPosterior + + +def add_intercept(x: Tensor) -> Tensor: + """Adds an intercept column to the design matrix (i.e. tensor).""" + return torch.concat([torch.ones_like(x)[..., 0:1], x], dim=-1) + + +class BayesianRegressionModel(Model): + _num_outputs: int + df: int + s_squared: Tensor + beta: Tensor + L: Tensor + add_intercept: bool + + def __init__(self, intercept: bool = True) -> None: + super(BayesianRegressionModel, self).__init__() + self.add_intercept = intercept + + @property + def num_outputs(self) -> int: + return self._num_outputs + + def forward(self, x: Tensor) -> Tensor: + return x @ self.beta + + def fit(self, x: Tensor, y: Tensor) -> None: + self._num_outputs = y.shape[-1] + x = add_intercept(x) if self.add_intercept else x + n, p = x.shape + self.df = n - p + # Rather than V = torch.linalg.inv(x.T @ x) as in BDA + # instead use L = torch.linalg.cholesky(x.T @ x) for stability. + # To use L, we can simply replace operations like: + # x = V @ b + # with a call to `torch.cholesky_solve`: + # x = torch.cholesky_solve(b, L) + self.L = torch.linalg.cholesky(x.T @ x) + # Least squares estimate + # self.beta = torch.cholesky_solve(x.T, self.L) @ y + self.beta = torch.cholesky_solve(x.T, self.L) @ y + # Model's residuals from the labels. + r: Tensor = y - self(x) + # Sample variance + self.s_squared = (1 / self.df) * r.T @ r + + def posterior( + self, + X: Tensor, + output_indices: Optional[list[int]] = None, + observation_noise: Union[bool, Tensor] = False, + posterior_transform: Optional[PosteriorTransform] = None, + ) -> Posterior: + # Squeeze out the q dimension if needed. + n, q, _ = X.shape + if output_indices: + X = X[..., output_indices] + if self.add_intercept: + X = add_intercept(X) + loc = self(X) + # Full covariance matrix of all test points. + cov = self.s_squared * ( + torch.eye(n, n) + X.squeeze() @ torch.cholesky_solve(X.squeeze().T, self.L) + ) + # The batch semantics of BoTorch evaluate each data point in their own batch. + # So, we extract the diagonal representing Var[\tilde y_i | y_i] of each test point. + scale = torch.diag(cov).reshape(n, q, self.num_outputs) + # Form the posterior predictive dist according to Sec 14.2, Pg 357 of BDA. + posterior_predictive_dist = distributions.StudentT( + df=self.df, loc=loc, scale=scale + ) + posterior = TorchPosterior(distribution=posterior_predictive_dist) + if posterior_transform is not None: + posterior = posterior_transform(posterior) + return posterior + + +# In[12]: + + +bayesian_regression_model = BayesianRegressionModel(intercept=True) +bayesian_regression_model.fit(train_X, Y) + + +# In[13]: + + +ax = plot_toy_data(x=train_X, y=Y) +ax.scatter( + train_X[:, 0].detach().numpy().squeeze(), + train_X[:, 1].detach().numpy().squeeze(), + zs=bayesian_regression_model(add_intercept(train_X)).detach().squeeze().numpy(), +) +plt.show() + + +# In[14]: + + +optimize_acqf( + LogExpectedImprovement(model=bayesian_regression_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, +) + + +# In[15]: + + +optimize_acqf( + qLogExpectedImprovement(model=bayesian_regression_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, +) + + +# ### Bayesian Linear Regression w/ `EnsemblePosterior` +# The `EnsembleModel` class provides a default implementation for `posterior()`. Then the MC acquisition function will be optimized using samples from the posterior predictive distribution (`EnsemblePosterior` also implements `mean` and `variance` properties, so some other analytic acquisition functions will also work). We follow this [Pyro tutorial](https://pyro.ai/examples/bayesian_regression.html#Bayesian-Regression-with-Pyro%E2%80%99s-Stochastic-Variational-Inference-(SVI)) for a linear regression model fit with [Stochastic Variational Inference](https://pyro.ai/examples/svi_part_i.html) (SVI). + +# First, we define a Pyro model capable of sampling from a posterior predictive distribution for new observations at test points. Later, when we perform posterior predictive inference, we will use Pyro's [`Predictive`](https://docs.pyro.ai/en/dev/_modules/pyro/infer/predictive.html) class. By default, `Predictive` ignores inference gradients with: +# +# ```python +# model = torch.no_grad()(poutine.mask(model, mask=False) if mask else model) +# ``` +# +# Since we need to retain the autograd graph to optimize the acquisition function, we can use `torch.set_grad_enabled(True)` in the `forward()` method to override this behavior. + +# In[16]: + + +import pyro +import pyro.distributions as dist +from pyro.infer.autoguide import AutoGuide, AutoDiagonalNormal +from pyro.nn import PyroSample, PyroModule +from pyro.infer import SVI, Trace_ELBO +from pyro.optim import PyroOptim + +pyro.set_rng_seed(1) + + +# Bayesian Regression represented as a single hidden layer. +class BayesianRegression(PyroModule): + Y: str = "y" + + def __init__(self, in_features: int, out_features: int): + super().__init__() + # Linear layer like before, but wrapped with PyroModule. + self.linear = PyroModule[nn.Linear](in_features, out_features) + # Add priors to the weights & bias of the linear layer. + self.linear.weight = PyroSample( + dist.Normal(0.0, 1.0) + .expand(torch.Size([out_features, in_features])) + .to_event(2) + ) + self.linear.bias = PyroSample( + dist.Normal(0.0, 10.0).expand(torch.Size([out_features])).to_event(1) + ) + + def forward(self, x: Tensor, y: Optional[Tensor] = None) -> Tensor: + # NOTE: Enable gradient tracking to override behavior of `Predictive`. + torch.set_grad_enabled(True) + # Prior for the noise level. + sigma = pyro.sample("sigma", dist.Uniform(0.0, 10.0)) + # Linear layer on the inputs. + mean = self.linear(x).squeeze(-1) + n, p = x.shape[0], x.shape[-1] + with pyro.plate("data", x.shape[0]): + # Observations will be t distributed. + t_dist = dist.StudentT(df=n - p, loc=mean, scale=sigma) + _ = pyro.sample(self.Y, t_dist, obs=y) + return mean + + +# In[17]: + + +def fit_svi( + epochs: int, + model: PyroModule, + guide: AutoGuide, + optimizer: PyroOptim, + train_X: Tensor, + train_Y: Tensor, +) -> None: + svi = SVI( + model, + guide, + optimizer, + loss=Trace_ELBO(), + ) + pyro.clear_param_store() + for epoch in range(epochs): + loss = svi.step(train_X, train_Y.squeeze()) + if epoch % 10 == 0: + print("epoch {}, loss {}".format(epoch, loss)) + + +# Now, we incorporate our Pyro model into the `Model` and `Posterior` interface like before. `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the output size of the model and `s` is the ensemble size. + +# In[18]: + + +from botorch.models.ensemble import EnsembleModel +from pyro.infer import Predictive + +class EnsembleBayesianRegressionModel(EnsembleModel): + model: BayesianRegression + guide: AutoGuide + num_samples: int + _num_outputs: int + + def __init__(self, train_X: Tensor, train_Y: Tensor, num_samples: int = 100): + super(EnsembleBayesianRegressionModel, self).__init__() + self._num_outputs = train_Y.shape[-1] + self.model = BayesianRegression(train_X.shape[-1], self.num_outputs) + self.guide = AutoDiagonalNormal(self.model) + self.num_samples = num_samples + + def forward(self, X: Tensor) -> Tensor: + predictive = Predictive( + self.model, + guide=self.guide, + num_samples=self.num_samples, + # Only return the posterior predictive distribution for y. + return_sites=(self.model.Y,), + ) + # `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the + # output size of the model and `s` is the ensemble size. + samples = ( + # Retrieve posterior samples from the observation random variable. + # This is also known as a posterior predictive distribution. + predictive(X.squeeze())[self.model.Y] + # Move the ensemble dimension to "s" axis. + .transpose(0, 1) + # Reshape for `EnsemblePosterior` as mentioned above. + .reshape(X.shape[0], -1, 1, self.num_outputs) + ) + return samples + + +# In[19]: + + +ensemble_bayesian_regression_model = EnsembleBayesianRegressionModel( + train_X=train_X, train_Y=Y +) +fit_svi( + 100, + ensemble_bayesian_regression_model.model, + ensemble_bayesian_regression_model.guide, + pyro.optim.Adam({"lr": 0.1}), + train_X, + Y, +) + + +# In[20]: + + +ax = plot_toy_data(x=train_X, y=Y) +ax.scatter( + train_X[:, 0].detach().numpy().squeeze(), + train_X[:, 1].detach().numpy().squeeze(), + zs=ensemble_bayesian_regression_model(train_X) + .detach() + .squeeze() + .mean(dim=-1) + .numpy(), +) +plt.show() + + +# In[21]: + + +optimize_acqf( + LogExpectedImprovement(model=ensemble_bayesian_regression_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, +) + + +# In[22]: + + +optimize_acqf( + qLogExpectedImprovement(model=ensemble_bayesian_regression_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, +) + + +# ### Random Forest w/ Ensemble Posterior +# Finally, we move away from linear models to any ML technique that ensembles many models. Specifically, we can use the [RandomForestRegressor](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html) from sklearn which is an ensemble method of individual decision trees. These decision trees can be accessed through the object's `estimators_` attribute. + +# In[23]: + + +import numpy as np +from sklearn.ensemble import RandomForestRegressor +from botorch.models.ensemble import EnsembleModel + + +class EnsembleRandomForestModel(EnsembleModel): + model: RandomForestRegressor + num_samples: int + _num_outputs: int + + def __init__(self, num_samples: int = 100): + super(EnsembleRandomForestModel, self).__init__() + self._num_outputs = 1 + self.model = RandomForestRegressor(n_estimators=num_samples) + + def fit(self, X: Tensor, y: Tensor) -> None: + self.model = self.model.fit( + X=X.detach().numpy(), y=y.detach().numpy().squeeze() + ) + + def forward(self, X: Tensor) -> Tensor: + x = X.detach().numpy().squeeze() + # Create the ensemble from predictions from each decision tree. + y = torch.from_numpy(np.array([i.predict(x) for i in self.model.estimators_])) + # `EnsemblePosterior` expects a `(b) x s x q x m` tensor where `m` is the + # output size of the model and `s` is the ensemble size. + samples = y.transpose(0, 1).reshape(X.shape[0], -1, 1, self.num_outputs) + return samples + + +# In[24]: + + +ensemble_random_forest_model = EnsembleRandomForestModel(num_samples=300) +ensemble_random_forest_model.fit(X=train_X, y=Y) + + +# In[25]: + + +ax = plot_toy_data(x=train_X, y=Y) +ax.scatter( + train_X[:, 0].detach().numpy().squeeze(), + train_X[:, 1].detach().numpy().squeeze(), + zs=ensemble_random_forest_model(train_X).detach().squeeze().mean(dim=-1).numpy(), +) +plt.show() + + +# In order to use gradient-based optimization of the acquisition function (via the standard `optimize_acqf()` method) we will need to have the samples drawn from the posterior be differentiable w.r.t. to the input to the `posterior()` method (this is not the case for Random Forest models). Instead, we will perform the acquisition function optimization with gradient-free methods. + +# In[26]: + + +optimize_acqf( + LogExpectedImprovement(model=ensemble_random_forest_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, + options={"with_grad": False}, +) + + +# In[27]: + + +optimize_acqf( + qLogExpectedImprovement(model=ensemble_random_forest_model, best_f=Y.max()), + bounds=bounds, + q=1, + num_restarts=5, + raw_samples=10, + options={"with_grad": False}, +) + + +# #### CMA-ES +# We can also move the optimization loop out of BoTorch entirely and follow the [CMA-ES tutorial](https://botorch.org/tutorials/optimize_with_cmaes) to optimize with an evolution strategy. + +# In[28]: + + +import cma +import numpy as np + +x0 = np.random.rand(2) + +es = cma.CMAEvolutionStrategy( + x0=x0, + sigma0=0.2, + inopts={"bounds": [0, 2], "popsize": 50}, +) + +log_expected_improvement_ensemble_random_forest_model = LogExpectedImprovement( + model=ensemble_random_forest_model, best_f=Y.max() +) + +with torch.no_grad(): + while not es.stop(): + xs = es.ask() + y = ( + -log_expected_improvement_ensemble_random_forest_model( + torch.from_numpy(np.array(xs)).unsqueeze(-2) + ) + .view(-1) + .double() + .numpy() + ) + es.tell(xs, y) + +torch.from_numpy(es.best.x) + diff --git a/website-old/static/files/decoupled_mobo.ipynb b/website-old/static/files/decoupled_mobo.ipynb new file mode 100644 index 0000000000..855f70974d --- /dev/null +++ b/website-old/static/files/decoupled_mobo.ipynb @@ -0,0 +1,857 @@ +{ + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "python3", + "language": "python", + "isCinder": true + }, + "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.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "61330204-a407-449f-af77-fcd868546651", + "showInput": false, + "outputsInitialized": false + }, + "source": [ + "## Multi-Objective BO with Decoupled Evaluations using HVKG\n", + "In this tutorial, we illustrate how to use the hypervolume knowledge gradient for problems where the objectives can be evaluated independently (decoupled). \n", + "\n", + "There are two types of decoupling:\n", + "\n", + "* **Competitive decoupling**: where the objectives are evaluated using the same evaluation resource. Often the objectives have heterogenous costs and therefore it is prudent to select what design and objective to evaluate in a cost-aware fashion.\n", + "\n", + "* **Non-competitive decoupling**: where the objectives have independent evaluation resources and potentially different numbers of designs can be evaluated in parallel. In this scenario, all available evaluation resources should be exploited and the goal is to optimize the objectives as well as possible within a fixed number of time steps.\n", + "\n", + "In this tutorial, we focus on competitive decoupling and show how HVKG can be used for efficient optimization.\n", + "\n", + "[1] [S. Daulton, M. Balandat, and E. Bakshy. Hypervolume Knowledge Gradient: A Lookahead Approach for Multi-Objective Bayesian Optimization with Partial Information. ICML, 2023.](https://proceedings.mlr.press/v202/daulton23a.html)\n", + "\n", + "Note: `pymoo` is an optional dependency that is used for determining the Pareto set of optimal designs under the model posterior mean using NSGA-II (which is not a sample efficient method, but sample efficiency is not critical for this step). If `pymoo` is not available, the Pareto set of optimal designs is selected from a discrete set. This will work okay for low-dim (e.g. $\\leq2$ dimensions) problems, but in general NSGA-II will yield far better results." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f01416cb-e83e-4263-b599-b2a3221d144d", + "showInput": false, + "outputsInitialized": false + }, + "source": [ + "### Set dtype and device\n", + "Note: HVKG aggressively exploits parallel hardware and is much faster when run on a GPU." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1699627780155, + "executionStopTime": 1699627787200, + "originalKey": "fc7f5adf-9bff-4fe5-8c24-5faad8b7ad01", + "requestMsgId": "a61c88db-2b5e-4aa7-935f-ac82c1bbc845", + "collapsed": false, + "customOutput": null, + "outputsInitialized": false, + "output": { + "id": "804505111685014" + } + }, + "source": [ + "import os\n", + "\n", + "import torch\n", + "\n", + "\n", + "tkwargs = {\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", + "}\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ], + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "I1110 064940.229 _utils_internal.py:230] NCCL_DEBUG env var is set to None\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "I1110 064940.231 _utils_internal.py:239] NCCL_DEBUG is INFO from /etc/nccl.conf\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8d895a93-397c-4d2c-b6f5-96f589312538", + "showInput": false, + "outputsInitialized": false + }, + "source": [ + "### Problem setup\n", + "\n", + "In this tutorial, we optimize a bi-objective synthetic function ZDT2 over a 6-dimensional space. The costs of evaluating each objective are 3 and 1, respectively, which we choose to be different to reflect that many multi-objective optimization problems have heterogeneous costs." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1699627787229, + "executionStopTime": 1699627791232, + "originalKey": "32104fee-5b27-41b3-9007-5a55d04235d3", + "requestMsgId": "a5b45d06-0f88-4371-bde5-bff6aaff5aab", + "collapsed": false, + "customOutput": null, + "outputsInitialized": false + }, + "source": [ + "from botorch.test_functions.multi_objective import ZDT2\n", + "from botorch.models.cost import FixedCostModel\n", + "\n", + "\n", + "problem = ZDT2(negate=True, dim=6).to(**tkwargs)\n", + "\n", + "# define the cost model\n", + "objective_costs = {0: 3.0, 1: 1.0}\n", + "objective_indices = list(objective_costs.keys())\n", + "objective_costs = {int(k): v for k, v in objective_costs.items()}\n", + "objective_costs_t = torch.tensor(\n", + " [objective_costs[k] for k in sorted(objective_costs.keys())], **tkwargs\n", + ")\n", + "cost_model = FixedCostModel(fixed_cost=objective_costs_t)" + ], + "execution_count": 2, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "fc047039-c2a9-4ea8-920e-c057547cfb11", + "showInput": false, + "outputsInitialized": false + }, + "source": [ + "#### Model initialization\n", + "\n", + "We use a list of `SingleTaskGP`s to model the two objectives with known noise variances. The models are initialized with $2(d+1)=14$ points drawn randomly from $[0,1]^2$. Since the objectives can be evaluated independently, the number of observations of each objective can be different. Therefore, we must use a `ModelListGP`." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1699627791256, + "executionStopTime": 1699627791262, + "originalKey": "3ecef619-db90-4676-8c8d-99bbf915a0fa", + "requestMsgId": "d9e8e1fe-509f-4b25-8cf1-bcf16ebb380f", + "collapsed": false, + "customOutput": null, + "outputsInitialized": false + }, + "source": [ + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.model_list_gp_regression import ModelListGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "from botorch.utils.transforms import normalize, unnormalize\n", + "from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood\n", + "from torch import Tensor\n", + "from gpytorch.priors import GammaPrior\n", + "from gpytorch.kernels import MaternKernel, ScaleKernel\n", + "\n", + "\n", + "def generate_initial_data(n):\n", + " # generate training data\n", + " train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1)\n", + " train_obj_true = problem(train_x)\n", + " return train_x, train_obj_true\n", + "\n", + "\n", + "def initialize_model(train_x_list, train_obj_list):\n", + " # define models for objective and constraint\n", + " train_x_list = [normalize(train_x, problem.bounds) for train_x in train_x_list]\n", + " models = []\n", + " for i in range(len(train_obj_list)):\n", + " train_y = train_obj_list[i]\n", + " train_yvar = torch.full_like(train_y, 1e-7) # noiseless\n", + " models.append(\n", + " SingleTaskGP(\n", + " train_X=train_x_list[i],\n", + " train_Y=train_y,\n", + " train_Yvar=train_yvar,\n", + " outcome_transform=Standardize(m=1),\n", + " covar_module=ScaleKernel(\n", + " MaternKernel(\n", + " nu=2.5,\n", + " ard_num_dims=train_x_list[0].shape[-1],\n", + " lengthscale_prior=GammaPrior(2.0, 2.0),\n", + " ),\n", + " outputscale_prior=GammaPrior(2.0, 0.15),\n", + " )\n", + " )\n", + " )\n", + " model = ModelListGP(*models)\n", + " mll = SumMarginalLogLikelihood(model.likelihood, model)\n", + " return mll, model" + ], + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6bfaef9a-3f34-4d51-9700-fbddc79eccf1", + "showInput": false, + "outputsInitialized": false + }, + "source": [ + "#### Define a helper functions that performs the essential BO step for $q$NEHVI and HVKG\n", + "The helper function below initializes the $q$NEHVI acquisition function (a strong baseline, but one that does not support decoupled evaluations), optimizes it, and returns the candidate along with the observed function values. \n", + "\n", + "**Reference Point**\n", + "\n", + "$q$NEHVI and HVKG require specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1699627791276, + "executionStopTime": 1699627791277, + "originalKey": "758fad16-635b-4e2c-a5ae-5fa6d43ae569", + "requestMsgId": "4305049b-e57a-4c3b-85cb-7d83bc1e3b42", + "collapsed": false, + "customOutput": null, + "outputsInitialized": false + }, + "source": [ + "from botorch.acquisition.multi_objective.monte_carlo import (\n", + " qNoisyExpectedHypervolumeImprovement,\n", + ")\n", + "from botorch.optim.optimize import optimize_acqf\n", + "\n", + "\n", + "BATCH_SIZE = 1\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "\n", + "standard_bounds = torch.zeros(2, problem.dim, **tkwargs)\n", + "standard_bounds[1] = 1\n", + "\n", + "\n", + "def optimize_qnehvi_and_get_observation(model, train_x, sampler):\n", + " \"\"\"Optimizes the qNEHVI acquisition function, and returns a new candidate and observation.\"\"\"\n", + " # partition non-dominated space into disjoint rectangles\n", + " acq_func = qNoisyExpectedHypervolumeImprovement(\n", + " model=model,\n", + " ref_point=problem.ref_point.tolist(), # use known reference point\n", + " X_baseline=normalize(train_x, problem.bounds),\n", + " prune_baseline=True, # prune baseline points that have estimated zero probability of being Pareto optimal\n", + " sampler=sampler,\n", + " )\n", + " # optimize\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=standard_bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " sequential=True,\n", + " )\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=problem.bounds)\n", + " new_obj_true = problem(new_x)\n", + " return new_x, new_obj_true" + ], + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "executionStartTime": 1692892083921, + "executionStopTime": 1692892083976, + "originalKey": "e4fb82e5-bdc8-44e4-846c-a3ecf7620a8a", + "requestMsgId": "e4fb82e5-bdc8-44e4-846c-a3ecf7620a8a", + "collapsed": false, + "customOutput": null, + "showInput": false, + "customInput": null, + "outputsInitialized": false + }, + "source": [ + "### Helper Function for initializing and optimizing HVKG\n", + "\n", + "Below we define the following helper functions:\n", + "1. `get_current_value` for computing the current hypervolume of the hypervolume maximizing set under the posterior mean.\n", + "2. `optimize_HVKG_and_get_obs_decoupled` to initialize and optimize HVKG to determine which design to evaluate and which objective to evaluate the design on. This method obtains the observation corresponding to that design." + ], + "attachments": {} + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "0f8cbe2d-1901-4f06-b10a-8ecb7cd9d71a", + "showInput": true, + "customInput": null, + "collapsed": false, + "requestMsgId": "5d9f944b-8c7f-4585-bac5-4d7212b915ff", + "executionStartTime": 1699627791283, + "executionStopTime": 1699627791294, + "outputsInitialized": false, + "customOutput": null + }, + "source": [ + "from botorch.acquisition.cost_aware import InverseCostWeightedUtility\n", + "from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import (\n", + " _get_hv_value_function,\n", + " qHypervolumeKnowledgeGradient,\n", + ")\n", + "from botorch.models.deterministic import GenericDeterministicModel\n", + "from botorch.sampling.list_sampler import ListSampler\n", + "from botorch.sampling.normal import IIDNormalSampler\n", + "\n", + "NUM_PARETO = 2 if SMOKE_TEST else 10\n", + "NUM_FANTASIES = 2 if SMOKE_TEST else 8\n", + "NUM_HVKG_RESTARTS = 1\n", + "\n", + "\n", + "def get_current_value(\n", + " model,\n", + " ref_point,\n", + " bounds,\n", + "):\n", + " \"\"\"Helper to get the hypervolume of the current hypervolume\n", + " maximizing set.\n", + " \"\"\"\n", + " curr_val_acqf = _get_hv_value_function(\n", + " model=model,\n", + " ref_point=ref_point,\n", + " use_posterior_mean=True,\n", + " )\n", + " _, current_value = optimize_acqf(\n", + " acq_function=curr_val_acqf,\n", + " bounds=bounds,\n", + " q=NUM_PARETO,\n", + " num_restarts=20,\n", + " raw_samples=1024,\n", + " return_best_only=True,\n", + " options={\"batch_limit\": 5},\n", + " )\n", + " return current_value\n", + "\n", + "\n", + "def optimize_HVKG_and_get_obs_decoupled(model):\n", + " \"\"\"Utility to initialize and optimize HVKG.\"\"\"\n", + " cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)\n", + "\n", + " current_value = get_current_value(\n", + " model=model,\n", + " ref_point=problem.ref_point,\n", + " bounds=standard_bounds,\n", + " )\n", + "\n", + " acq_func = qHypervolumeKnowledgeGradient(\n", + " model=model,\n", + " ref_point=problem.ref_point, # use known reference point\n", + " num_fantasies=NUM_FANTASIES,\n", + " num_pareto=NUM_PARETO,\n", + " current_value=current_value,\n", + " cost_aware_utility=cost_aware_utility,\n", + " )\n", + "\n", + " # optimize acquisition functions and get new observations\n", + " objective_vals = []\n", + " objective_candidates = []\n", + " for objective_idx in objective_indices:\n", + " # set evaluation index to only condition on one objective\n", + " # this could be multiple objectives\n", + " X_evaluation_mask = torch.zeros(\n", + " 1,\n", + " len(objective_indices),\n", + " dtype=torch.bool,\n", + " device=standard_bounds.device,\n", + " )\n", + " X_evaluation_mask[0, objective_idx] = 1\n", + " acq_func.X_evaluation_mask = X_evaluation_mask\n", + " candidates, vals = optimize_acqf(\n", + " acq_function=acq_func,\n", + " num_restarts=NUM_HVKG_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " bounds=standard_bounds,\n", + " q=BATCH_SIZE,\n", + " sequential=False,\n", + " options={\"batch_limit\": 5},\n", + " )\n", + " objective_vals.append(vals.view(-1))\n", + " objective_candidates.append(candidates)\n", + " best_objective_index = torch.cat(objective_vals, dim=-1).argmax().item()\n", + " eval_objective_indices = [best_objective_index]\n", + " candidates = objective_candidates[best_objective_index]\n", + " vals = objective_vals[best_objective_index]\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=problem.bounds)\n", + " new_obj = problem(new_x)\n", + " new_obj = new_obj[..., eval_objective_indices]\n", + " return new_x, new_obj, eval_objective_indices" + ], + "execution_count": 5, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "678281c4-f8a1-420e-b4be-6a4ee682a2e8", + "showInput": false, + "customInput": null, + "outputsInitialized": false + }, + "source": [ + "## Define function to find model-estimated Pareto set of designs under posterior mean using NSGA-II\n", + "" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "6f682eec-e4f4-4c3f-935a-46dfd3e7801e", + "showInput": true, + "customInput": null, + "collapsed": false, + "requestMsgId": "a086a9e6-4eb3-4f5b-9e08-a5f868fa47e1", + "executionStartTime": 1699627791311, + "executionStopTime": 1699627791486, + "outputsInitialized": false + }, + "source": [ + "import numpy as np\n", + "from botorch.utils.multi_objective.box_decompositions.non_dominated import (\n", + " FastNondominatedPartitioning,\n", + ")\n", + "from botorch.utils.multi_objective.pareto import _is_non_dominated_loop\n", + "from gpytorch import settings\n", + "\n", + "try:\n", + " from pymoo.algorithms.nsga2 import NSGA2\n", + " from pymoo.model.problem import Problem\n", + " from pymoo.optimize import minimize\n", + " from pymoo.util.termination.max_gen import MaximumGenerationTermination\n", + "\n", + " def get_model_identified_hv_maximizing_set(\n", + " model,\n", + " population_size=250,\n", + " max_gen=100,\n", + " ):\n", + " \"\"\"Optimize the posterior mean using NSGA-II.\"\"\"\n", + " tkwargs = {\n", + " \"dtype\": problem.ref_point.dtype,\n", + " \"device\": problem.ref_point.device,\n", + " }\n", + " dim = problem.dim\n", + "\n", + " class PosteriorMeanPymooProblem(Problem):\n", + " def __init__(self):\n", + " super().__init__(\n", + " n_var=dim,\n", + " n_obj=problem.num_objectives,\n", + " type_var=np.double,\n", + " )\n", + " self.xl = np.zeros(dim)\n", + " self.xu = np.ones(dim)\n", + "\n", + " def _evaluate(self, x, out, *args, **kwargs):\n", + " X = torch.from_numpy(x).to(**tkwargs)\n", + " is_fantasy_model = (\n", + " isinstance(model, ModelListGP)\n", + " and model.models[0].train_targets.ndim > 2\n", + " ) or (\n", + " not isinstance(model, ModelListGP) and model.train_targets.ndim > 2\n", + " )\n", + " with torch.no_grad():\n", + " with settings.cholesky_max_tries(9):\n", + " # eval in batch mode\n", + " y = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2)\n", + " if is_fantasy_model:\n", + " y = y.mean(dim=-2)\n", + " out[\"F\"] = -y.cpu().numpy()\n", + "\n", + " pymoo_problem = PosteriorMeanPymooProblem()\n", + " algorithm = NSGA2(\n", + " pop_size=population_size,\n", + " eliminate_duplicates=True,\n", + " )\n", + " res = minimize(\n", + " pymoo_problem,\n", + " algorithm,\n", + " termination=MaximumGenerationTermination(max_gen),\n", + " # seed=0, # fix seed\n", + " verbose=False,\n", + " )\n", + " X = torch.tensor(\n", + " res.X,\n", + " **tkwargs,\n", + " )\n", + " X = unnormalize(X, problem.bounds)\n", + " Y = problem(X)\n", + " # compute HV\n", + " partitioning = FastNondominatedPartitioning(ref_point=problem.ref_point, Y=Y)\n", + " return partitioning.compute_hypervolume().item()\n", + "\n", + "except ImportError:\n", + " NUM_DISCRETE_POINTS = 100 if SMOKE_TEST else 100000\n", + " CHUNK_SIZE = 512\n", + "\n", + " def get_model_identified_hv_maximizing_set(\n", + " model,\n", + " ):\n", + " \"\"\"Optimize the posterior mean over a discrete set.\"\"\"\n", + " tkwargs = {\n", + " \"dtype\": problem.ref_point.dtype,\n", + " \"device\": problem.ref_point.device,\n", + " }\n", + " dim = problem.dim\n", + "\n", + " discrete_set = torch.rand(NUM_DISCRETE_POINTS, dim, **tkwargs)\n", + " with torch.no_grad():\n", + " preds_list = []\n", + " for start in range(0, NUM_DISCRETE_POINTS, CHUNK_SIZE):\n", + " preds = model.posterior(\n", + " discrete_set[start : start + CHUNK_SIZE].unsqueeze(-2)\n", + " ).mean.squeeze(-2)\n", + " preds_list.append(preds)\n", + " preds = torch.cat(preds_list, dim=0)\n", + " pareto_mask = _is_non_dominated_loop(preds)\n", + " pareto_X = discrete_set[pareto_mask]\n", + " pareto_X = unnormalize(pareto_X, problem.bounds)\n", + " Y = problem(pareto_X)\n", + " # compute HV\n", + " partitioning = FastNondominatedPartitioning(ref_point=problem.ref_point, Y=Y)\n", + " return partitioning.compute_hypervolume().item()" + ], + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f8ffd86a-f4bb-4984-9330-04ff4d62f189", + "showInput": false, + "outputsInitialized": false + }, + "source": [ + "### Perform Bayesian Optimization loop with Decoupled HVKG and compared against non-decoupled $q$NEHVI\n", + "The Bayesian optimization \"loop\" for a batch size of 1 simply iterates the following steps:\n", + "1. given a surrogate model, choose a candidate design *and* objective to evaluate (for methods that leverage decoupled evaluations).\n", + "2. observe one or more objectives for the candidate design.\n", + "3. update the surrogate model.\n", + "\n", + "The loop will continue to run until a pre-specified evaluation budget (in terms of cost) is exhausted." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1699627791496, + "executionStopTime": 1699629252322, + "originalKey": "3189ba56-cc8c-4c78-95ab-9bb702f11ecb", + "requestMsgId": "2a400c57-e37a-4a7b-9758-81570d204538", + "collapsed": false, + "customOutput": null, + "outputsInitialized": true, + "output": { + "id": "913932473488501" + } + }, + "source": [ + "import time\n", + "import warnings\n", + "\n", + "from botorch import fit_gpytorch_mll\n", + "from botorch.exceptions import BadInitialCandidatesWarning\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "\n", + "\n", + "warnings.filterwarnings(\"ignore\", category=BadInitialCandidatesWarning)\n", + "warnings.filterwarnings(\"ignore\", category=RuntimeWarning)\n", + "\n", + "MC_SAMPLES = 128 if not SMOKE_TEST else 16\n", + "COST_BUDGET = 90 if not SMOKE_TEST else 54\n", + "torch.manual_seed(0)\n", + "verbose = True\n", + "N_INIT = 2 * problem.dim + 1\n", + "\n", + "total_cost = {\"hvkg\": 0.0, \"qnehvi\": 0.0, \"random\": 0.0}\n", + "\n", + "\n", + "# call helper functions to generate initial training data and initialize model\n", + "train_x_hvkg, train_obj_hvkg = generate_initial_data(n=N_INIT)\n", + "train_obj_hvkg_list = list(train_obj_hvkg.split(1, dim=-1))\n", + "train_x_hvkg_list = [train_x_hvkg] * len(train_obj_hvkg_list)\n", + "mll_hvkg, model_hvkg = initialize_model(train_x_hvkg_list, train_obj_hvkg_list)\n", + "train_obj_random_list = train_obj_hvkg_list\n", + "train_x_random_list = train_x_hvkg_list\n", + "train_x_qnehvi_list, train_obj_qnehvi_list = (\n", + " train_x_hvkg_list,\n", + " train_obj_hvkg_list,\n", + ")\n", + "cost_hvkg = cost_model(train_x_hvkg).sum(dim=-1)\n", + "total_cost[\"hvkg\"] += cost_hvkg.sum().item()\n", + "cost_qnehvi = cost_hvkg\n", + "cost_random = cost_hvkg\n", + "total_cost[\"qnehvi\"] = total_cost[\"hvkg\"]\n", + "total_cost[\"random\"] = total_cost[\"hvkg\"]\n", + "mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi_list, train_obj_qnehvi_list)\n", + "mll_random, model_random = initialize_model(train_x_random_list, train_obj_random_list)\n", + "# fit the models\n", + "fit_gpytorch_mll(mll_hvkg)\n", + "fit_gpytorch_mll(mll_qnehvi)\n", + "fit_gpytorch_mll(mll_random)\n", + "# compute hypervolume\n", + "hv = get_model_identified_hv_maximizing_set(model=model_qnehvi)\n", + "hvs_hvkg, hvs_qehvi, hvs_qnehvi, hvs_random = [hv], [hv], [hv], [hv]\n", + "if verbose:\n", + " print(\n", + " f\"\\nInitial: Hypervolume (random, qHVKG, qNEHVI) = \"\n", + " f\"({hvs_random[-1]:>4.2f}, {hvs_hvkg[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}).\",\n", + " end=\"\",\n", + " )\n", + "# run N_BATCH rounds of BayesOpt after the initial random batch\n", + "iteration = 0\n", + "active_algos = {k for k, v in total_cost.items() if v < COST_BUDGET}\n", + "while any(v < COST_BUDGET for v in total_cost.values()):\n", + "\n", + " t0 = time.monotonic()\n", + " if \"hvkg\" in active_algos:\n", + " # generate candidates\n", + " (\n", + " new_x_hvkg,\n", + " new_obj_hvkg,\n", + " eval_objective_indices_hvkg,\n", + " ) = optimize_HVKG_and_get_obs_decoupled(\n", + " model_hvkg,\n", + " )\n", + " # update training points\n", + " for i in eval_objective_indices_hvkg:\n", + " train_x_hvkg_list[i] = torch.cat([train_x_hvkg_list[i], new_x_hvkg])\n", + " train_obj_hvkg_list[i] = torch.cat(\n", + " [train_obj_hvkg_list[i], new_obj_hvkg], dim=0\n", + " )\n", + " # update costs\n", + " all_outcome_cost = cost_model(new_x_hvkg)\n", + " new_cost_hvkg = all_outcome_cost[..., eval_objective_indices_hvkg].sum(dim=-1)\n", + " cost_hvkg = torch.cat([cost_hvkg, new_cost_hvkg], dim=0)\n", + " total_cost[\"hvkg\"] += new_cost_hvkg.sum().item()\n", + " # fit models\n", + " mll_hvkg, model_hvkg = initialize_model(train_x_hvkg_list, train_obj_hvkg_list)\n", + " fit_gpytorch_mll(mll_hvkg)\n", + "\n", + " if \"qnehvi\" in active_algos:\n", + " qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + " # generate candidates\n", + " new_x_qnehvi, new_obj_qnehvi = optimize_qnehvi_and_get_observation(\n", + " model_qnehvi, train_x_qnehvi_list[0], qnehvi_sampler\n", + " )\n", + " # update training points\n", + " for i in objective_indices:\n", + " train_x_qnehvi_list[i] = torch.cat([train_x_qnehvi_list[i], new_x_qnehvi])\n", + " train_obj_qnehvi_list[i] = torch.cat(\n", + " [train_obj_qnehvi_list[i], new_obj_qnehvi[..., i : i + 1]]\n", + " )\n", + " # update costs\n", + " new_cost_qnehvi = cost_model(new_x_qnehvi).sum(dim=-1)\n", + " cost_qnehvi = torch.cat([cost_qnehvi, new_cost_qnehvi], dim=0)\n", + " total_cost[\"qnehvi\"] += new_cost_qnehvi.sum().item()\n", + " # fit models\n", + " mll_qnehvi, model_qnehvi = initialize_model(\n", + " train_x_qnehvi_list, train_obj_qnehvi_list\n", + " )\n", + " fit_gpytorch_mll(mll_qnehvi)\n", + " if \"random\" in active_algos:\n", + " # generate candidates\n", + " new_x_random, new_obj_random = generate_initial_data(n=BATCH_SIZE)\n", + " # update training points\n", + " for i in objective_indices:\n", + " train_x_random_list[i] = torch.cat([train_x_random_list[i], new_x_random])\n", + " train_obj_random_list[i] = torch.cat(\n", + " [train_obj_random_list[i], new_obj_random[..., i : i + 1]]\n", + " )\n", + " # update costs\n", + " new_cost_random = cost_model(new_x_random).sum(dim=-1)\n", + " cost_random = torch.cat([cost_random, new_cost_random], dim=0)\n", + " total_cost[\"random\"] += new_cost_random.sum().item()\n", + " # fit models\n", + " mll_random, model_random = initialize_model(\n", + " train_x_random_list, train_obj_random_list\n", + " )\n", + " fit_gpytorch_mll(mll_random)\n", + "\n", + " # compute hypervolume\n", + " for label, model, hv_list in zip(\n", + " [\"hvkg\", \"qnehvi\", \"random\"],\n", + " [model_hvkg, model_qnehvi, model_random],\n", + " [hvs_hvkg, hvs_qnehvi, hvs_random],\n", + " ):\n", + " if label in active_algos:\n", + " hv = get_model_identified_hv_maximizing_set(model=model)\n", + " hv_list.append(hv)\n", + " else:\n", + " # no update performed\n", + " hv_list.append(hv_list[-1])\n", + "\n", + " t1 = time.monotonic()\n", + " if verbose:\n", + " print(\n", + " f\"\\nBatch {iteration:>2}: Costs (random, qHVKG, qNEHVI) = \"\n", + " f\"({total_cost['random']:>4.2f}, {total_cost['hvkg']:>4.2f}, {total_cost['qnehvi']:>4.2f}). \"\n", + " )\n", + " print(\n", + " f\"\\nHypervolume (random, qHVKG, qNEHVI) = \"\n", + " f\"({hvs_random[-1]:>4.2f}, {hvs_hvkg[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), \"\n", + " f\"time = {t1-t0:>4.2f}.\",\n", + " end=\"\",\n", + " )\n", + " else:\n", + " print(\".\", end=\"\")\n", + " iteration += 1\n", + " active_algos = {k for k, v in total_cost.items() if v < COST_BUDGET}" + ], + "execution_count": 7, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\nInitial: Hypervolume (random, qHVKG, qNEHVI) = (89.34, 89.34, 89.34)." + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "72232578-0908-4292-aca4-d52197890dd6", + "showInput": false, + "outputsInitialized": false + }, + "source": [ + "#### Plot the cost vs inference regret\n", + "The plot below shows the a common metric of multi-objective optimization performance, the log hypervolume difference: the log difference between the hypervolume of the true pareto front and the hypervolume of the inferred pareto set of designs identified by each algorithm. The log hypervolume difference is plotted cover cost. This is also known as inference regret.\n", + "\n", + "The plot shows that HVKG identifies the Pareto optimal designs much faster than $q$NEHVI, and Sobol." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1699632303324, + "executionStopTime": 1699632303816, + "originalKey": "0187c932-1a45-45d2-82f9-9f96853eff1c", + "requestMsgId": "ad28407c-5b74-4ace-8a6c-dd347fd5571d", + "customOutput": null, + "collapsed": false, + "outputsInitialized": true, + "output": { + "id": "1029368728310675" + } + }, + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "log_hv_difference_hvkg = np.log10(problem.max_hv - np.asarray(hvs_hvkg))\n", + "log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi))\n", + "log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random))\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n", + "running_cost_random = np.cumsum(cost_random.cpu().numpy()[N_INIT-1:])\n", + "running_cost_qnehvi = np.cumsum(cost_qnehvi.cpu().numpy()[N_INIT-1:])\n", + "running_cost_hvkg = np.cumsum(cost_hvkg.cpu().numpy()[N_INIT-1:])\n", + "ax.errorbar(\n", + " running_cost_random,\n", + " log_hv_difference_rnd[: len(running_cost_random)],\n", + " label=\"Sobol\",\n", + " linewidth=1.5,\n", + " ls=\"--\",\n", + " marker=\"s\",\n", + ")\n", + "ax.errorbar(\n", + " running_cost_qnehvi,\n", + " log_hv_difference_qnehvi[: len(running_cost_qnehvi)],\n", + " label=\"qNEHVI\",\n", + " linewidth=1.5,\n", + " ls=\"--\",\n", + " marker=\"o\"\n", + ")\n", + "ax.errorbar(\n", + " running_cost_hvkg,\n", + " log_hv_difference_hvkg[: len(running_cost_hvkg)],\n", + " label=\"HVKG\",\n", + " linewidth=1.5,\n", + " ls=\"--\",\n", + " marker=\"d\"\n", + ")\n", + "ax.set(\n", + " xlabel=\"Cost\",\n", + " ylabel=\"Log Hypervolume Difference\",\n", + ")\n", + "ax.legend(loc=\"upper right\")" + ], + "execution_count": 21, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "" + }, + "metadata": {}, + "execution_count": 21 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "34f90193-bda7-4b4a-9d86-ad87ff778088", + "showInput": true, + "customInput": null + }, + "source": [ + "" + ], + "execution_count": null, + "outputs": [] + } + ] +} diff --git a/website-old/static/files/decoupled_mobo.py b/website-old/static/files/decoupled_mobo.py new file mode 100644 index 0000000000..3e5ceadf54 --- /dev/null +++ b/website-old/static/files/decoupled_mobo.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Multi-Objective BO with Decoupled Evaluations using HVKG +# In this tutorial, we illustrate how to use the hypervolume knowledge gradient for problems where the objectives can be evaluated independently (decoupled). +# +# There are two types of decoupling: +# +# * **Competitive decoupling**: where the objectives are evaluated using the same evaluation resource. Often the objectives have heterogenous costs and therefore it is prudent to select what design and objective to evaluate in a cost-aware fashion. +# +# * **Non-competitive decoupling**: where the objectives have independent evaluation resources and potentially different numbers of designs can be evaluated in parallel. In this scenario, all available evaluation resources should be exploited and the goal is to optimize the objectives as well as possible within a fixed number of time steps. +# +# In this tutorial, we focus on competitive decoupling and show how HVKG can be used for efficient optimization. +# +# [1] [S. Daulton, M. Balandat, and E. Bakshy. Hypervolume Knowledge Gradient: A Lookahead Approach for Multi-Objective Bayesian Optimization with Partial Information. ICML, 2023.](https://proceedings.mlr.press/v202/daulton23a.html) +# +# Note: `pymoo` is an optional dependency that is used for determining the Pareto set of optimal designs under the model posterior mean using NSGA-II (which is not a sample efficient method, but sample efficiency is not critical for this step). If `pymoo` is not available, the Pareto set of optimal designs is selected from a discrete set. This will work okay for low-dim (e.g. $\leq2$ dimensions) problems, but in general NSGA-II will yield far better results. + +# ### Set dtype and device +# Note: HVKG aggressively exploits parallel hardware and is much faster when run on a GPU. + +# In[1]: + + +import os + +import torch + + +tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), +} +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Problem setup +# +# In this tutorial, we optimize a bi-objective synthetic function ZDT2 over a 6-dimensional space. The costs of evaluating each objective are 3 and 1, respectively, which we choose to be different to reflect that many multi-objective optimization problems have heterogeneous costs. + +# In[2]: + + +from botorch.test_functions.multi_objective import ZDT2 +from botorch.models.cost import FixedCostModel + + +problem = ZDT2(negate=True, dim=6).to(**tkwargs) + +# define the cost model +objective_costs = {0: 3.0, 1: 1.0} +objective_indices = list(objective_costs.keys()) +objective_costs = {int(k): v for k, v in objective_costs.items()} +objective_costs_t = torch.tensor( + [objective_costs[k] for k in sorted(objective_costs.keys())], **tkwargs +) +cost_model = FixedCostModel(fixed_cost=objective_costs_t) + + +# #### Model initialization +# +# We use a list of `SingleTaskGP`s to model the two objectives with known noise variances. The models are initialized with $2(d+1)=14$ points drawn randomly from $[0,1]^2$. Since the objectives can be evaluated independently, the number of observations of each objective can be different. Therefore, we must use a `ModelListGP`. + +# In[3]: + + +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize +from botorch.utils.sampling import draw_sobol_samples +from botorch.utils.transforms import normalize, unnormalize +from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood +from torch import Tensor +from gpytorch.priors import GammaPrior +from gpytorch.kernels import MaternKernel, ScaleKernel + + +def generate_initial_data(n): + # generate training data + train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1) + train_obj_true = problem(train_x) + return train_x, train_obj_true + + +def initialize_model(train_x_list, train_obj_list): + # define models for objective and constraint + train_x_list = [normalize(train_x, problem.bounds) for train_x in train_x_list] + models = [] + for i in range(len(train_obj_list)): + train_y = train_obj_list[i] + train_yvar = torch.full_like(train_y, 1e-7) # noiseless + models.append( + SingleTaskGP( + train_X=train_x_list[i], + train_Y=train_y, + train_Yvar=train_yvar, + outcome_transform=Standardize(m=1), + covar_module=ScaleKernel( + MaternKernel( + nu=2.5, + ard_num_dims=train_x_list[0].shape[-1], + lengthscale_prior=GammaPrior(2.0, 2.0), + ), + outputscale_prior=GammaPrior(2.0, 0.15), + ) + ) + ) + model = ModelListGP(*models) + mll = SumMarginalLogLikelihood(model.likelihood, model) + return mll, model + + +# #### Define a helper functions that performs the essential BO step for $q$NEHVI and HVKG +# The helper function below initializes the $q$NEHVI acquisition function (a strong baseline, but one that does not support decoupled evaluations), optimizes it, and returns the candidate along with the observed function values. +# +# **Reference Point** +# +# $q$NEHVI and HVKG require specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy. + +# In[4]: + + +from botorch.acquisition.multi_objective.monte_carlo import ( + qNoisyExpectedHypervolumeImprovement, +) +from botorch.optim.optimize import optimize_acqf + + +BATCH_SIZE = 1 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 + +standard_bounds = torch.zeros(2, problem.dim, **tkwargs) +standard_bounds[1] = 1 + + +def optimize_qnehvi_and_get_observation(model, train_x, sampler): + """Optimizes the qNEHVI acquisition function, and returns a new candidate and observation.""" + # partition non-dominated space into disjoint rectangles + acq_func = qNoisyExpectedHypervolumeImprovement( + model=model, + ref_point=problem.ref_point.tolist(), # use known reference point + X_baseline=normalize(train_x, problem.bounds), + prune_baseline=True, # prune baseline points that have estimated zero probability of being Pareto optimal + sampler=sampler, + ) + # optimize + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=standard_bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + sequential=True, + ) + # observe new values + new_x = unnormalize(candidates.detach(), bounds=problem.bounds) + new_obj_true = problem(new_x) + return new_x, new_obj_true + + +# ### Helper Function for initializing and optimizing HVKG +# +# Below we define the following helper functions: +# 1. `get_current_value` for computing the current hypervolume of the hypervolume maximizing set under the posterior mean. +# 2. `optimize_HVKG_and_get_obs_decoupled` to initialize and optimize HVKG to determine which design to evaluate and which objective to evaluate the design on. This method obtains the observation corresponding to that design. + +# In[5]: + + +from botorch.acquisition.cost_aware import InverseCostWeightedUtility +from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import ( + _get_hv_value_function, + qHypervolumeKnowledgeGradient, +) +from botorch.models.deterministic import GenericDeterministicModel +from botorch.sampling.list_sampler import ListSampler +from botorch.sampling.normal import IIDNormalSampler + +NUM_PARETO = 2 if SMOKE_TEST else 10 +NUM_FANTASIES = 2 if SMOKE_TEST else 8 +NUM_HVKG_RESTARTS = 1 + + +def get_current_value( + model, + ref_point, + bounds, +): + """Helper to get the hypervolume of the current hypervolume + maximizing set. + """ + curr_val_acqf = _get_hv_value_function( + model=model, + ref_point=ref_point, + use_posterior_mean=True, + ) + _, current_value = optimize_acqf( + acq_function=curr_val_acqf, + bounds=bounds, + q=NUM_PARETO, + num_restarts=20, + raw_samples=1024, + return_best_only=True, + options={"batch_limit": 5}, + ) + return current_value + + +def optimize_HVKG_and_get_obs_decoupled(model): + """Utility to initialize and optimize HVKG.""" + cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) + + current_value = get_current_value( + model=model, + ref_point=problem.ref_point, + bounds=standard_bounds, + ) + + acq_func = qHypervolumeKnowledgeGradient( + model=model, + ref_point=problem.ref_point, # use known reference point + num_fantasies=NUM_FANTASIES, + num_pareto=NUM_PARETO, + current_value=current_value, + cost_aware_utility=cost_aware_utility, + ) + + # optimize acquisition functions and get new observations + objective_vals = [] + objective_candidates = [] + for objective_idx in objective_indices: + # set evaluation index to only condition on one objective + # this could be multiple objectives + X_evaluation_mask = torch.zeros( + 1, + len(objective_indices), + dtype=torch.bool, + device=standard_bounds.device, + ) + X_evaluation_mask[0, objective_idx] = 1 + acq_func.X_evaluation_mask = X_evaluation_mask + candidates, vals = optimize_acqf( + acq_function=acq_func, + num_restarts=NUM_HVKG_RESTARTS, + raw_samples=RAW_SAMPLES, + bounds=standard_bounds, + q=BATCH_SIZE, + sequential=False, + options={"batch_limit": 5}, + ) + objective_vals.append(vals.view(-1)) + objective_candidates.append(candidates) + best_objective_index = torch.cat(objective_vals, dim=-1).argmax().item() + eval_objective_indices = [best_objective_index] + candidates = objective_candidates[best_objective_index] + vals = objective_vals[best_objective_index] + # observe new values + new_x = unnormalize(candidates.detach(), bounds=problem.bounds) + new_obj = problem(new_x) + new_obj = new_obj[..., eval_objective_indices] + return new_x, new_obj, eval_objective_indices + + +# ## Define function to find model-estimated Pareto set of designs under posterior mean using NSGA-II +# + +# In[6]: + + +import numpy as np +from botorch.utils.multi_objective.box_decompositions.non_dominated import ( + FastNondominatedPartitioning, +) +from botorch.utils.multi_objective.pareto import _is_non_dominated_loop +from gpytorch import settings + +try: + from pymoo.algorithms.nsga2 import NSGA2 + from pymoo.model.problem import Problem + from pymoo.optimize import minimize + from pymoo.util.termination.max_gen import MaximumGenerationTermination + + def get_model_identified_hv_maximizing_set( + model, + population_size=250, + max_gen=100, + ): + """Optimize the posterior mean using NSGA-II.""" + tkwargs = { + "dtype": problem.ref_point.dtype, + "device": problem.ref_point.device, + } + dim = problem.dim + + class PosteriorMeanPymooProblem(Problem): + def __init__(self): + super().__init__( + n_var=dim, + n_obj=problem.num_objectives, + type_var=np.double, + ) + self.xl = np.zeros(dim) + self.xu = np.ones(dim) + + def _evaluate(self, x, out, *args, **kwargs): + X = torch.from_numpy(x).to(**tkwargs) + is_fantasy_model = ( + isinstance(model, ModelListGP) + and model.models[0].train_targets.ndim > 2 + ) or ( + not isinstance(model, ModelListGP) and model.train_targets.ndim > 2 + ) + with torch.no_grad(): + with settings.cholesky_max_tries(9): + # eval in batch mode + y = model.posterior(X.unsqueeze(-2)).mean.squeeze(-2) + if is_fantasy_model: + y = y.mean(dim=-2) + out["F"] = -y.cpu().numpy() + + pymoo_problem = PosteriorMeanPymooProblem() + algorithm = NSGA2( + pop_size=population_size, + eliminate_duplicates=True, + ) + res = minimize( + pymoo_problem, + algorithm, + termination=MaximumGenerationTermination(max_gen), + # seed=0, # fix seed + verbose=False, + ) + X = torch.tensor( + res.X, + **tkwargs, + ) + X = unnormalize(X, problem.bounds) + Y = problem(X) + # compute HV + partitioning = FastNondominatedPartitioning(ref_point=problem.ref_point, Y=Y) + return partitioning.compute_hypervolume().item() + +except ImportError: + NUM_DISCRETE_POINTS = 100 if SMOKE_TEST else 100000 + CHUNK_SIZE = 512 + + def get_model_identified_hv_maximizing_set( + model, + ): + """Optimize the posterior mean over a discrete set.""" + tkwargs = { + "dtype": problem.ref_point.dtype, + "device": problem.ref_point.device, + } + dim = problem.dim + + discrete_set = torch.rand(NUM_DISCRETE_POINTS, dim, **tkwargs) + with torch.no_grad(): + preds_list = [] + for start in range(0, NUM_DISCRETE_POINTS, CHUNK_SIZE): + preds = model.posterior( + discrete_set[start : start + CHUNK_SIZE].unsqueeze(-2) + ).mean.squeeze(-2) + preds_list.append(preds) + preds = torch.cat(preds_list, dim=0) + pareto_mask = _is_non_dominated_loop(preds) + pareto_X = discrete_set[pareto_mask] + pareto_X = unnormalize(pareto_X, problem.bounds) + Y = problem(pareto_X) + # compute HV + partitioning = FastNondominatedPartitioning(ref_point=problem.ref_point, Y=Y) + return partitioning.compute_hypervolume().item() + + +# ### Perform Bayesian Optimization loop with Decoupled HVKG and compared against non-decoupled $q$NEHVI +# The Bayesian optimization "loop" for a batch size of 1 simply iterates the following steps: +# 1. given a surrogate model, choose a candidate design *and* objective to evaluate (for methods that leverage decoupled evaluations). +# 2. observe one or more objectives for the candidate design. +# 3. update the surrogate model. +# +# The loop will continue to run until a pre-specified evaluation budget (in terms of cost) is exhausted. + +# In[7]: + + +import time +import warnings + +from botorch import fit_gpytorch_mll +from botorch.exceptions import BadInitialCandidatesWarning +from botorch.sampling.normal import SobolQMCNormalSampler + + +warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning) +warnings.filterwarnings("ignore", category=RuntimeWarning) + +MC_SAMPLES = 128 if not SMOKE_TEST else 16 +COST_BUDGET = 90 if not SMOKE_TEST else 54 +torch.manual_seed(0) +verbose = True +N_INIT = 2 * problem.dim + 1 + +total_cost = {"hvkg": 0.0, "qnehvi": 0.0, "random": 0.0} + + +# call helper functions to generate initial training data and initialize model +train_x_hvkg, train_obj_hvkg = generate_initial_data(n=N_INIT) +train_obj_hvkg_list = list(train_obj_hvkg.split(1, dim=-1)) +train_x_hvkg_list = [train_x_hvkg] * len(train_obj_hvkg_list) +mll_hvkg, model_hvkg = initialize_model(train_x_hvkg_list, train_obj_hvkg_list) +train_obj_random_list = train_obj_hvkg_list +train_x_random_list = train_x_hvkg_list +train_x_qnehvi_list, train_obj_qnehvi_list = ( + train_x_hvkg_list, + train_obj_hvkg_list, +) +cost_hvkg = cost_model(train_x_hvkg).sum(dim=-1) +total_cost["hvkg"] += cost_hvkg.sum().item() +cost_qnehvi = cost_hvkg +cost_random = cost_hvkg +total_cost["qnehvi"] = total_cost["hvkg"] +total_cost["random"] = total_cost["hvkg"] +mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi_list, train_obj_qnehvi_list) +mll_random, model_random = initialize_model(train_x_random_list, train_obj_random_list) +# fit the models +fit_gpytorch_mll(mll_hvkg) +fit_gpytorch_mll(mll_qnehvi) +fit_gpytorch_mll(mll_random) +# compute hypervolume +hv = get_model_identified_hv_maximizing_set(model=model_qnehvi) +hvs_hvkg, hvs_qehvi, hvs_qnehvi, hvs_random = [hv], [hv], [hv], [hv] +if verbose: + print( + f"\nInitial: Hypervolume (random, qHVKG, qNEHVI) = " + f"({hvs_random[-1]:>4.2f}, {hvs_hvkg[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}).", + end="", + ) +# run N_BATCH rounds of BayesOpt after the initial random batch +iteration = 0 +active_algos = {k for k, v in total_cost.items() if v < COST_BUDGET} +while any(v < COST_BUDGET for v in total_cost.values()): + + t0 = time.monotonic() + if "hvkg" in active_algos: + # generate candidates + ( + new_x_hvkg, + new_obj_hvkg, + eval_objective_indices_hvkg, + ) = optimize_HVKG_and_get_obs_decoupled( + model_hvkg, + ) + # update training points + for i in eval_objective_indices_hvkg: + train_x_hvkg_list[i] = torch.cat([train_x_hvkg_list[i], new_x_hvkg]) + train_obj_hvkg_list[i] = torch.cat( + [train_obj_hvkg_list[i], new_obj_hvkg], dim=0 + ) + # update costs + all_outcome_cost = cost_model(new_x_hvkg) + new_cost_hvkg = all_outcome_cost[..., eval_objective_indices_hvkg].sum(dim=-1) + cost_hvkg = torch.cat([cost_hvkg, new_cost_hvkg], dim=0) + total_cost["hvkg"] += new_cost_hvkg.sum().item() + # fit models + mll_hvkg, model_hvkg = initialize_model(train_x_hvkg_list, train_obj_hvkg_list) + fit_gpytorch_mll(mll_hvkg) + + if "qnehvi" in active_algos: + qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + # generate candidates + new_x_qnehvi, new_obj_qnehvi = optimize_qnehvi_and_get_observation( + model_qnehvi, train_x_qnehvi_list[0], qnehvi_sampler + ) + # update training points + for i in objective_indices: + train_x_qnehvi_list[i] = torch.cat([train_x_qnehvi_list[i], new_x_qnehvi]) + train_obj_qnehvi_list[i] = torch.cat( + [train_obj_qnehvi_list[i], new_obj_qnehvi[..., i : i + 1]] + ) + # update costs + new_cost_qnehvi = cost_model(new_x_qnehvi).sum(dim=-1) + cost_qnehvi = torch.cat([cost_qnehvi, new_cost_qnehvi], dim=0) + total_cost["qnehvi"] += new_cost_qnehvi.sum().item() + # fit models + mll_qnehvi, model_qnehvi = initialize_model( + train_x_qnehvi_list, train_obj_qnehvi_list + ) + fit_gpytorch_mll(mll_qnehvi) + if "random" in active_algos: + # generate candidates + new_x_random, new_obj_random = generate_initial_data(n=BATCH_SIZE) + # update training points + for i in objective_indices: + train_x_random_list[i] = torch.cat([train_x_random_list[i], new_x_random]) + train_obj_random_list[i] = torch.cat( + [train_obj_random_list[i], new_obj_random[..., i : i + 1]] + ) + # update costs + new_cost_random = cost_model(new_x_random).sum(dim=-1) + cost_random = torch.cat([cost_random, new_cost_random], dim=0) + total_cost["random"] += new_cost_random.sum().item() + # fit models + mll_random, model_random = initialize_model( + train_x_random_list, train_obj_random_list + ) + fit_gpytorch_mll(mll_random) + + # compute hypervolume + for label, model, hv_list in zip( + ["hvkg", "qnehvi", "random"], + [model_hvkg, model_qnehvi, model_random], + [hvs_hvkg, hvs_qnehvi, hvs_random], + ): + if label in active_algos: + hv = get_model_identified_hv_maximizing_set(model=model) + hv_list.append(hv) + else: + # no update performed + hv_list.append(hv_list[-1]) + + t1 = time.monotonic() + if verbose: + print( + f"\nBatch {iteration:>2}: Costs (random, qHVKG, qNEHVI) = " + f"({total_cost['random']:>4.2f}, {total_cost['hvkg']:>4.2f}, {total_cost['qnehvi']:>4.2f}). " + ) + print( + f"\nHypervolume (random, qHVKG, qNEHVI) = " + f"({hvs_random[-1]:>4.2f}, {hvs_hvkg[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), " + f"time = {t1-t0:>4.2f}.", + end="", + ) + else: + print(".", end="") + iteration += 1 + active_algos = {k for k, v in total_cost.items() if v < COST_BUDGET} + + +# #### Plot the cost vs inference regret +# The plot below shows the a common metric of multi-objective optimization performance, the log hypervolume difference: the log difference between the hypervolume of the true pareto front and the hypervolume of the inferred pareto set of designs identified by each algorithm. The log hypervolume difference is plotted cover cost. This is also known as inference regret. +# +# The plot shows that HVKG identifies the Pareto optimal designs much faster than $q$NEHVI, and Sobol. + +# In[21]: + + +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +log_hv_difference_hvkg = np.log10(problem.max_hv - np.asarray(hvs_hvkg)) +log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi)) +log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random)) + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +running_cost_random = np.cumsum(cost_random.cpu().numpy()[N_INIT-1:]) +running_cost_qnehvi = np.cumsum(cost_qnehvi.cpu().numpy()[N_INIT-1:]) +running_cost_hvkg = np.cumsum(cost_hvkg.cpu().numpy()[N_INIT-1:]) +ax.errorbar( + running_cost_random, + log_hv_difference_rnd[: len(running_cost_random)], + label="Sobol", + linewidth=1.5, + ls="--", + marker="s", +) +ax.errorbar( + running_cost_qnehvi, + log_hv_difference_qnehvi[: len(running_cost_qnehvi)], + label="qNEHVI", + linewidth=1.5, + ls="--", + marker="o" +) +ax.errorbar( + running_cost_hvkg, + log_hv_difference_hvkg[: len(running_cost_hvkg)], + label="HVKG", + linewidth=1.5, + ls="--", + marker="d" +) +ax.set( + xlabel="Cost", + ylabel="Log Hypervolume Difference", +) +ax.legend(loc="upper right") + + +# In[ ]: + + + + diff --git a/website-old/static/files/discrete_multi_fidelity_bo.ipynb b/website-old/static/files/discrete_multi_fidelity_bo.ipynb new file mode 100644 index 0000000000..e941633171 --- /dev/null +++ b/website-old/static/files/discrete_multi_fidelity_bo.ipynb @@ -0,0 +1,571 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Multi-Fidelity BO with Discrete Fidelities using KG\n", + "\n", + "In this tutorial, we show how to do multi-fidelity BO with discrete fidelities based on [1], where each fidelity is a different \"information source.\" This tutorial uses the same setup as the [continuous multi-fidelity BO tutorial](https://botorch.org/tutorials/multi_fidelity_bo), except with discrete fidelity parameters that are interpreted as multiple information sources.\n", + "\n", + "We use a GP model with a single task that models the design and fidelity parameters jointly. In some cases, where there is not a natural ordering in the fidelity space, it may be more appropriate to use a multi-task model (with, say, an ICM kernel). We will provide a tutorial once this functionality is in place.\n", + "\n", + "[1] [M. Poloczek, J. Wang, P.I. Frazier. Multi-Information Source Optimization. NeurIPS, 2017](https://papers.nips.cc/paper/2017/file/df1f1d20ee86704251795841e6a9405a-Paper.pdf)\n", + "\n", + "[2] [J. Wu, S. Toscano-Palmerin, P.I. Frazier, A.G. Wilson. Practical Multi-fidelity Bayesian Optimization for Hyperparameter Tuning. Conference on Uncertainty in Artificial Intelligence (UAI), 2019](https://arxiv.org/pdf/1903.04703.pdf)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set dtype and device" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "\n", + "\n", + "tkwargs = {\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", + "}\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Problem setup\n", + "\n", + "We'll consider the Augmented Hartmann multi-fidelity synthetic test problem. This function is a version of the Hartmann6 test function with an additional dimension representing the fidelity parameter; details are in [2]. The function takes the form $f(x,s)$ where $x \\in [0,1]^6$ and $s \\in \\{0.5, 0.75, 1\\}$. The target fidelity is 1.0, which means that our goal is to solve $\\max_x f(x,1.0)$ by making use of cheaper evaluations $f(x,s)$ for $s \\in \\{0.5, 0.75\\}$. In this example, we'll assume that the cost function takes the form $5.0 + s$, illustrating a situation where the fixed cost is $5.0$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.test_functions.multi_fidelity import AugmentedHartmann\n", + "\n", + "\n", + "problem = AugmentedHartmann(negate=True).to(**tkwargs)\n", + "fidelities = torch.tensor([0.5, 0.75, 1.0], **tkwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Model initialization\n", + "\n", + "We use a `SingleTaskMultiFidelityGP` as the surrogate model, which uses a kernel from [2] that is well-suited for multi-fidelity applications. The `SingleTaskMultiFidelityGP` models the design and fidelity parameters jointly, so its domain is $[0,1]^7$." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "\n", + "\n", + "def generate_initial_data(n=16):\n", + " # generate training data\n", + " train_x = torch.rand(n, 6, **tkwargs)\n", + " train_f = fidelities[torch.randint(3, (n, 1))]\n", + " train_x_full = torch.cat((train_x, train_f), dim=1)\n", + " train_obj = problem(train_x_full).unsqueeze(-1) # add output dimension\n", + " return train_x_full, train_obj\n", + "\n", + "\n", + "def initialize_model(train_x, train_obj):\n", + " # define a surrogate model suited for a \"training data\"-like fidelity parameter\n", + " # in dimension 6, as in [2]\n", + " model = SingleTaskMultiFidelityGP(\n", + " train_x, train_obj, outcome_transform=Standardize(m=1), data_fidelities=[6]\n", + " )\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " return mll, model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8add142b-e32b-4f27-8f22-4386879512f6", + "showInput": false + }, + "source": [ + "#### Define a helper function to construct the MFKG acquisition function\n", + "The helper function illustrates how one can initialize an $q$MFKG acquisition function. In this example, we assume that the affine cost is known. We then use the notion of a `CostAwareUtility` in BoTorch to scalarize the \"competing objectives\" of information gain and cost. The MFKG acquisition function optimizes the ratio of information gain to cost, which is captured by the `InverseCostWeightedUtility`.\n", + "\n", + "In order for MFKG to evaluate the information gain, it uses the model to predict the function value at the highest fidelity after conditioning on the observation. This is handled by the `project` argument, which specifies how to transform a tensor `X` to its target fidelity. We use a default helper function called `project_to_target_fidelity` to achieve this.\n", + "\n", + "An important point to keep in mind: in the case of standard KG, one can ignore the current value and simply optimize the expected maximum posterior mean of the next stage. However, for MFKG, since the goal is optimize information *gain* per cost, it is important to first compute the current value (i.e., maximum of the posterior mean at the target fidelity). To accomplish this, we use a `FixedFeatureAcquisitionFunction` on top of a `PosteriorMean`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch import fit_gpytorch_mll\n", + "from botorch.models.cost import AffineFidelityCostModel\n", + "from botorch.acquisition.cost_aware import InverseCostWeightedUtility\n", + "from botorch.acquisition import PosteriorMean\n", + "from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient\n", + "from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction\n", + "from botorch.optim.optimize import optimize_acqf\n", + "from botorch.acquisition.utils import project_to_target_fidelity\n", + "\n", + "bounds = torch.tensor([[0.0] * problem.dim, [1.0] * problem.dim], **tkwargs)\n", + "target_fidelities = {6: 1.0}\n", + "\n", + "cost_model = AffineFidelityCostModel(fidelity_weights={6: 1.0}, fixed_cost=5.0)\n", + "cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)\n", + "\n", + "\n", + "def project(X):\n", + " return project_to_target_fidelity(X=X, target_fidelities=target_fidelities)\n", + "\n", + "\n", + "def get_mfkg(model):\n", + "\n", + " curr_val_acqf = FixedFeatureAcquisitionFunction(\n", + " acq_function=PosteriorMean(model),\n", + " d=7,\n", + " columns=[6],\n", + " values=[1],\n", + " )\n", + "\n", + " _, current_value = optimize_acqf(\n", + " acq_function=curr_val_acqf,\n", + " bounds=bounds[:, :-1],\n", + " q=1,\n", + " num_restarts=10 if not SMOKE_TEST else 2,\n", + " raw_samples=1024 if not SMOKE_TEST else 4,\n", + " options={\"batch_limit\": 10, \"maxiter\": 200},\n", + " )\n", + "\n", + " return qMultiFidelityKnowledgeGradient(\n", + " model=model,\n", + " num_fantasies=128 if not SMOKE_TEST else 2,\n", + " current_value=current_value,\n", + " cost_aware_utility=cost_aware_utility,\n", + " project=project,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define a helper function that performs the essential BO step\n", + "This helper function optimizes the acquisition function and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. The function `optimize_acqf_mixed` sequentially optimizes the acquisition function over $x$ for each value of the fidelity $s \\in \\{0, 0.5, 1.0\\}$." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.optim.optimize import optimize_acqf_mixed\n", + "\n", + "\n", + "torch.set_printoptions(precision=3, sci_mode=False)\n", + "\n", + "NUM_RESTARTS = 5 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 128 if not SMOKE_TEST else 4\n", + "BATCH_SIZE = 4\n", + "\n", + "\n", + "def optimize_mfkg_and_get_observation(mfkg_acqf):\n", + " \"\"\"Optimizes MFKG and returns a new candidate, observation, and cost.\"\"\"\n", + "\n", + " # generate new candidates\n", + " candidates, _ = optimize_acqf_mixed(\n", + " acq_function=mfkg_acqf,\n", + " bounds=bounds,\n", + " fixed_features_list=[{6: 0.5}, {6: 0.75}, {6: 1.0}],\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " # batch_initial_conditions=X_init,\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + "\n", + " # observe new values\n", + " cost = cost_model(candidates).sum()\n", + " new_x = candidates.detach()\n", + " new_obj = problem(new_x).unsqueeze(-1)\n", + " print(f\"candidates:\\n{new_x}\\n\")\n", + " print(f\"observations:\\n{new_obj}\\n\\n\")\n", + " return new_x, new_obj, cost" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perform a few steps of multi-fidelity BO\n", + "First, let's generate some initial random data and fit a surrogate model." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "train_x, train_obj = generate_initial_data(n=16)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now use the helper functions above to run a few iterations of BO." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.199, 0.101, 0.436, 0.433, 0.197, 0.421, 0.750],\n", + " [0.142, 0.274, 0.308, 0.413, 0.298, 0.570, 0.750],\n", + " [0.097, 0.141, 0.417, 0.453, 0.477, 0.536, 0.500],\n", + " [0.123, 0.022, 0.328, 0.430, 0.270, 0.689, 0.500]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[1.369],\n", + " [2.308],\n", + " [1.404],\n", + " [2.297]], device='cuda:0', dtype=torch.float64)\n", + "\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.276, 0.159, 0.231, 0.462, 0.295, 0.633, 1.000],\n", + " [0.213, 0.163, 0.297, 0.336, 0.276, 0.671, 0.750],\n", + " [0.029, 0.235, 0.236, 0.405, 0.290, 0.709, 0.500],\n", + " [0.159, 0.205, 0.360, 0.397, 0.361, 0.717, 1.000]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.170],\n", + " [2.984],\n", + " [2.197],\n", + " [2.588]], device='cuda:0', dtype=torch.float64)\n", + "\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.268, 0.224, 0.340, 0.334, 0.230, 0.751, 0.500],\n", + " [0.263, 0.181, 0.242, 0.307, 0.335, 0.735, 0.500],\n", + " [0.166, 0.163, 0.345, 0.260, 0.278, 0.711, 0.500],\n", + " [0.257, 0.238, 0.337, 0.311, 0.316, 0.639, 0.750]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.565],\n", + " [2.818],\n", + " [3.036],\n", + " [3.036]], device='cuda:0', dtype=torch.float64)\n", + "\n", + "\n" + ] + } + ], + "source": [ + "cumulative_cost = 0.0\n", + "N_ITER = 3 if not SMOKE_TEST else 1\n", + "\n", + "for i in range(N_ITER):\n", + " mll, model = initialize_model(train_x, train_obj)\n", + " fit_gpytorch_mll(mll)\n", + " mfkg_acqf = get_mfkg(model)\n", + " new_x, new_obj, cost = optimize_mfkg_and_get_observation(mfkg_acqf)\n", + " train_x = torch.cat([train_x, new_x])\n", + " train_obj = torch.cat([train_obj, new_obj])\n", + " cumulative_cost += cost" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Make a final recommendation\n", + "In multi-fidelity BO, there are usually fewer observations of the function at the target fidelity, so it is important to use a recommendation function that uses the correct fidelity. Here, we maximize the posterior mean with the fidelity dimension fixed to the target fidelity of 1.0." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def get_recommendation(model):\n", + " rec_acqf = FixedFeatureAcquisitionFunction(\n", + " acq_function=PosteriorMean(model),\n", + " d=7,\n", + " columns=[6],\n", + " values=[1],\n", + " )\n", + "\n", + " final_rec, _ = optimize_acqf(\n", + " acq_function=rec_acqf,\n", + " bounds=bounds[:, :-1],\n", + " q=1,\n", + " num_restarts=10,\n", + " raw_samples=512,\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + "\n", + " final_rec = rec_acqf._construct_X_full(final_rec)\n", + "\n", + " objective_value = problem(final_rec)\n", + " print(f\"recommended point:\\n{final_rec}\\n\\nobjective value:\\n{objective_value}\")\n", + " return final_rec" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recommended point:\n", + "tensor([[0.213, 0.164, 0.302, 0.327, 0.283, 0.689, 1.000]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "objective value:\n", + "tensor([3.021], device='cuda:0', dtype=torch.float64)\n", + "\n", + "total cost: 68.0\n", + "\n" + ] + } + ], + "source": [ + "final_rec = get_recommendation(model)\n", + "print(f\"\\ntotal cost: {cumulative_cost}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Comparison to standard EI (always use target fidelity)\n", + "Let's now repeat the same steps using a standard EI acquisition function (note that this is not a rigorous comparison as we are only looking at one trial in order to keep computational requirements low)." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.acquisition import qExpectedImprovement\n", + "\n", + "\n", + "def get_ei(model, best_f):\n", + "\n", + " return FixedFeatureAcquisitionFunction(\n", + " acq_function=qExpectedImprovement(model=model, best_f=best_f),\n", + " d=7,\n", + " columns=[6],\n", + " values=[1],\n", + " )\n", + "\n", + "\n", + "def optimize_ei_and_get_observation(ei_acqf):\n", + " \"\"\"Optimizes EI and returns a new candidate, observation, and cost.\"\"\"\n", + "\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=ei_acqf,\n", + " bounds=bounds[:, :-1],\n", + " q=BATCH_SIZE,\n", + " num_restarts=10,\n", + " raw_samples=512,\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + "\n", + " # add the fidelity parameter\n", + " candidates = ei_acqf._construct_X_full(candidates)\n", + "\n", + " # observe new values\n", + " cost = cost_model(candidates).sum()\n", + " new_x = candidates.detach()\n", + " new_obj = problem(new_x).unsqueeze(-1)\n", + " print(f\"candidates:\\n{new_x}\\n\")\n", + " print(f\"observations:\\n{new_obj}\\n\\n\")\n", + " return new_x, new_obj, cost" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.247, 0.687, 0.581, 0.760, 0.093, 0.132, 1.000],\n", + " [0.319, 0.850, 0.639, 0.865, 0.000, 0.120, 1.000],\n", + " [0.349, 0.666, 0.555, 0.986, 0.000, 0.126, 1.000],\n", + " [0.297, 0.792, 0.450, 0.889, 0.034, 0.028, 1.000]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[0.973],\n", + " [1.091],\n", + " [0.340],\n", + " [0.902]], device='cuda:0', dtype=torch.float64)\n", + "\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.194, 0.858, 0.622, 0.799, 0.000, 0.095, 1.000],\n", + " [0.341, 0.854, 0.590, 0.767, 0.000, 0.085, 1.000],\n", + " [0.999, 0.439, 0.828, 0.975, 0.633, 0.176, 1.000],\n", + " [0.296, 0.859, 0.677, 0.806, 0.119, 0.054, 1.000]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[ 0.862],\n", + " [ 1.975],\n", + " [ 0.000],\n", + " [ 1.514]], device='cuda:0', dtype=torch.float64)\n", + "\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/data/sandcastle/boxes/fbsource/fbcode/buck-out/opt/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:40: NumericalWarning:\n", + "\n", + "A not p.d., added jitter of 1.0e-08 to the diagonal\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.360, 0.891, 0.588, 0.749, 0.019, 0.036, 1.000],\n", + " [0.049, 0.894, 0.345, 0.210, 0.482, 0.463, 1.000],\n", + " [0.398, 0.970, 0.504, 0.213, 0.814, 0.724, 1.000],\n", + " [0.817, 0.879, 0.691, 0.842, 0.455, 0.937, 1.000]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.271],\n", + " [0.216],\n", + " [0.055],\n", + " [0.036]], device='cuda:0', dtype=torch.float64)\n", + "\n", + "\n" + ] + } + ], + "source": [ + "cumulative_cost = 0.0\n", + "\n", + "train_x, train_obj = generate_initial_data(n=16)\n", + "\n", + "for _ in range(N_ITER):\n", + " mll, model = initialize_model(train_x, train_obj)\n", + " fit_gpytorch_mll(mll)\n", + " ei_acqf = get_ei(model, best_f=train_obj.max())\n", + " new_x, new_obj, cost = optimize_ei_and_get_observation(ei_acqf)\n", + " train_x = torch.cat([train_x, new_x])\n", + " train_obj = torch.cat([train_obj, new_obj])\n", + " cumulative_cost += cost" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recommended point:\n", + "tensor([[0.352, 0.874, 0.589, 0.756, 0.008, 0.060, 1.000]], device='cuda:0',\n", + " dtype=torch.float64)\n", + "\n", + "objective value:\n", + "tensor([2.166], device='cuda:0', dtype=torch.float64)\n", + "\n", + "total cost: 72.0\n", + "\n" + ] + } + ], + "source": [ + "final_rec = get_recommendation(model)\n", + "print(f\"\\ntotal cost: {cumulative_cost}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/discrete_multi_fidelity_bo.py b/website-old/static/files/discrete_multi_fidelity_bo.py new file mode 100644 index 0000000000..656b4c54f3 --- /dev/null +++ b/website-old/static/files/discrete_multi_fidelity_bo.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Multi-Fidelity BO with Discrete Fidelities using KG +# +# In this tutorial, we show how to do multi-fidelity BO with discrete fidelities based on [1], where each fidelity is a different "information source." This tutorial uses the same setup as the [continuous multi-fidelity BO tutorial](https://botorch.org/tutorials/multi_fidelity_bo), except with discrete fidelity parameters that are interpreted as multiple information sources. +# +# We use a GP model with a single task that models the design and fidelity parameters jointly. In some cases, where there is not a natural ordering in the fidelity space, it may be more appropriate to use a multi-task model (with, say, an ICM kernel). We will provide a tutorial once this functionality is in place. +# +# [1] [M. Poloczek, J. Wang, P.I. Frazier. Multi-Information Source Optimization. NeurIPS, 2017](https://papers.nips.cc/paper/2017/file/df1f1d20ee86704251795841e6a9405a-Paper.pdf) +# +# [2] [J. Wu, S. Toscano-Palmerin, P.I. Frazier, A.G. Wilson. Practical Multi-fidelity Bayesian Optimization for Hyperparameter Tuning. Conference on Uncertainty in Artificial Intelligence (UAI), 2019](https://arxiv.org/pdf/1903.04703.pdf) + +# ### Set dtype and device + +# In[1]: + + +import os +import torch + + +tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), +} +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Problem setup +# +# We'll consider the Augmented Hartmann multi-fidelity synthetic test problem. This function is a version of the Hartmann6 test function with an additional dimension representing the fidelity parameter; details are in [2]. The function takes the form $f(x,s)$ where $x \in [0,1]^6$ and $s \in \{0.5, 0.75, 1\}$. The target fidelity is 1.0, which means that our goal is to solve $\max_x f(x,1.0)$ by making use of cheaper evaluations $f(x,s)$ for $s \in \{0.5, 0.75\}$. In this example, we'll assume that the cost function takes the form $5.0 + s$, illustrating a situation where the fixed cost is $5.0$. + +# In[2]: + + +from botorch.test_functions.multi_fidelity import AugmentedHartmann + + +problem = AugmentedHartmann(negate=True).to(**tkwargs) +fidelities = torch.tensor([0.5, 0.75, 1.0], **tkwargs) + + +# #### Model initialization +# +# We use a `SingleTaskMultiFidelityGP` as the surrogate model, which uses a kernel from [2] that is well-suited for multi-fidelity applications. The `SingleTaskMultiFidelityGP` models the design and fidelity parameters jointly, so its domain is $[0,1]^7$. + +# In[3]: + + +from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP +from botorch.models.transforms.outcome import Standardize +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood + + +def generate_initial_data(n=16): + # generate training data + train_x = torch.rand(n, 6, **tkwargs) + train_f = fidelities[torch.randint(3, (n, 1))] + train_x_full = torch.cat((train_x, train_f), dim=1) + train_obj = problem(train_x_full).unsqueeze(-1) # add output dimension + return train_x_full, train_obj + + +def initialize_model(train_x, train_obj): + # define a surrogate model suited for a "training data"-like fidelity parameter + # in dimension 6, as in [2] + model = SingleTaskMultiFidelityGP( + train_x, train_obj, outcome_transform=Standardize(m=1), data_fidelities=[6] + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + return mll, model + + +# #### Define a helper function to construct the MFKG acquisition function +# The helper function illustrates how one can initialize an $q$MFKG acquisition function. In this example, we assume that the affine cost is known. We then use the notion of a `CostAwareUtility` in BoTorch to scalarize the "competing objectives" of information gain and cost. The MFKG acquisition function optimizes the ratio of information gain to cost, which is captured by the `InverseCostWeightedUtility`. +# +# In order for MFKG to evaluate the information gain, it uses the model to predict the function value at the highest fidelity after conditioning on the observation. This is handled by the `project` argument, which specifies how to transform a tensor `X` to its target fidelity. We use a default helper function called `project_to_target_fidelity` to achieve this. +# +# An important point to keep in mind: in the case of standard KG, one can ignore the current value and simply optimize the expected maximum posterior mean of the next stage. However, for MFKG, since the goal is optimize information *gain* per cost, it is important to first compute the current value (i.e., maximum of the posterior mean at the target fidelity). To accomplish this, we use a `FixedFeatureAcquisitionFunction` on top of a `PosteriorMean`. + +# In[4]: + + +from botorch import fit_gpytorch_mll +from botorch.models.cost import AffineFidelityCostModel +from botorch.acquisition.cost_aware import InverseCostWeightedUtility +from botorch.acquisition import PosteriorMean +from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient +from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction +from botorch.optim.optimize import optimize_acqf +from botorch.acquisition.utils import project_to_target_fidelity + +bounds = torch.tensor([[0.0] * problem.dim, [1.0] * problem.dim], **tkwargs) +target_fidelities = {6: 1.0} + +cost_model = AffineFidelityCostModel(fidelity_weights={6: 1.0}, fixed_cost=5.0) +cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) + + +def project(X): + return project_to_target_fidelity(X=X, target_fidelities=target_fidelities) + + +def get_mfkg(model): + + curr_val_acqf = FixedFeatureAcquisitionFunction( + acq_function=PosteriorMean(model), + d=7, + columns=[6], + values=[1], + ) + + _, current_value = optimize_acqf( + acq_function=curr_val_acqf, + bounds=bounds[:, :-1], + q=1, + num_restarts=10 if not SMOKE_TEST else 2, + raw_samples=1024 if not SMOKE_TEST else 4, + options={"batch_limit": 10, "maxiter": 200}, + ) + + return qMultiFidelityKnowledgeGradient( + model=model, + num_fantasies=128 if not SMOKE_TEST else 2, + current_value=current_value, + cost_aware_utility=cost_aware_utility, + project=project, + ) + + +# #### Define a helper function that performs the essential BO step +# This helper function optimizes the acquisition function and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. The function `optimize_acqf_mixed` sequentially optimizes the acquisition function over $x$ for each value of the fidelity $s \in \{0, 0.5, 1.0\}$. + +# In[5]: + + +from botorch.optim.optimize import optimize_acqf_mixed + + +torch.set_printoptions(precision=3, sci_mode=False) + +NUM_RESTARTS = 5 if not SMOKE_TEST else 2 +RAW_SAMPLES = 128 if not SMOKE_TEST else 4 +BATCH_SIZE = 4 + + +def optimize_mfkg_and_get_observation(mfkg_acqf): + """Optimizes MFKG and returns a new candidate, observation, and cost.""" + + # generate new candidates + candidates, _ = optimize_acqf_mixed( + acq_function=mfkg_acqf, + bounds=bounds, + fixed_features_list=[{6: 0.5}, {6: 0.75}, {6: 1.0}], + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + # batch_initial_conditions=X_init, + options={"batch_limit": 5, "maxiter": 200}, + ) + + # observe new values + cost = cost_model(candidates).sum() + new_x = candidates.detach() + new_obj = problem(new_x).unsqueeze(-1) + print(f"candidates:\n{new_x}\n") + print(f"observations:\n{new_obj}\n\n") + return new_x, new_obj, cost + + +# ### Perform a few steps of multi-fidelity BO +# First, let's generate some initial random data and fit a surrogate model. + +# In[6]: + + +train_x, train_obj = generate_initial_data(n=16) + + +# We can now use the helper functions above to run a few iterations of BO. + +# In[7]: + + +cumulative_cost = 0.0 +N_ITER = 3 if not SMOKE_TEST else 1 + +for i in range(N_ITER): + mll, model = initialize_model(train_x, train_obj) + fit_gpytorch_mll(mll) + mfkg_acqf = get_mfkg(model) + new_x, new_obj, cost = optimize_mfkg_and_get_observation(mfkg_acqf) + train_x = torch.cat([train_x, new_x]) + train_obj = torch.cat([train_obj, new_obj]) + cumulative_cost += cost + + +# ### Make a final recommendation +# In multi-fidelity BO, there are usually fewer observations of the function at the target fidelity, so it is important to use a recommendation function that uses the correct fidelity. Here, we maximize the posterior mean with the fidelity dimension fixed to the target fidelity of 1.0. + +# In[8]: + + +def get_recommendation(model): + rec_acqf = FixedFeatureAcquisitionFunction( + acq_function=PosteriorMean(model), + d=7, + columns=[6], + values=[1], + ) + + final_rec, _ = optimize_acqf( + acq_function=rec_acqf, + bounds=bounds[:, :-1], + q=1, + num_restarts=10, + raw_samples=512, + options={"batch_limit": 5, "maxiter": 200}, + ) + + final_rec = rec_acqf._construct_X_full(final_rec) + + objective_value = problem(final_rec) + print(f"recommended point:\n{final_rec}\n\nobjective value:\n{objective_value}") + return final_rec + + +# In[9]: + + +final_rec = get_recommendation(model) +print(f"\ntotal cost: {cumulative_cost}\n") + + +# ### Comparison to standard EI (always use target fidelity) +# Let's now repeat the same steps using a standard EI acquisition function (note that this is not a rigorous comparison as we are only looking at one trial in order to keep computational requirements low). + +# In[10]: + + +from botorch.acquisition import qExpectedImprovement + + +def get_ei(model, best_f): + + return FixedFeatureAcquisitionFunction( + acq_function=qExpectedImprovement(model=model, best_f=best_f), + d=7, + columns=[6], + values=[1], + ) + + +def optimize_ei_and_get_observation(ei_acqf): + """Optimizes EI and returns a new candidate, observation, and cost.""" + + candidates, _ = optimize_acqf( + acq_function=ei_acqf, + bounds=bounds[:, :-1], + q=BATCH_SIZE, + num_restarts=10, + raw_samples=512, + options={"batch_limit": 5, "maxiter": 200}, + ) + + # add the fidelity parameter + candidates = ei_acqf._construct_X_full(candidates) + + # observe new values + cost = cost_model(candidates).sum() + new_x = candidates.detach() + new_obj = problem(new_x).unsqueeze(-1) + print(f"candidates:\n{new_x}\n") + print(f"observations:\n{new_obj}\n\n") + return new_x, new_obj, cost + + +# In[11]: + + +cumulative_cost = 0.0 + +train_x, train_obj = generate_initial_data(n=16) + +for _ in range(N_ITER): + mll, model = initialize_model(train_x, train_obj) + fit_gpytorch_mll(mll) + ei_acqf = get_ei(model, best_f=train_obj.max()) + new_x, new_obj, cost = optimize_ei_and_get_observation(ei_acqf) + train_x = torch.cat([train_x, new_x]) + train_obj = torch.cat([train_obj, new_obj]) + cumulative_cost += cost + + +# In[12]: + + +final_rec = get_recommendation(model) +print(f"\ntotal cost: {cumulative_cost}\n") + + +# In[12]: + + + + diff --git a/website-old/static/files/fit_model_with_torch_optimizer.ipynb b/website-old/static/files/fit_model_with_torch_optimizer.ipynb new file mode 100644 index 0000000000..e3b1682516 --- /dev/null +++ b/website-old/static/files/fit_model_with_torch_optimizer.ipynb @@ -0,0 +1,321 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fitting models in BoTorch with a torch.optim.Optimizer\n", + "\n", + "BoTorch provides a convenient `botorch.fit.fit_gpytorch_mll` function with sensible defaults that work on most basic models, including those that botorch ships with. Internally, this function uses L-BFGS-B to fit the parameters. However, in more advanced use cases you may need or want to implement your own model fitting logic.\n", + "\n", + "This tutorial allows you to customize model fitting to your needs using the familiar PyTorch-style model fitting loop.\n", + "\n", + "This tutorial is adapted from GPyTorch's [Simple GP Regression Tutorial](https://github.com/cornellius-gp/gpytorch/blob/master/examples/01_Exact_GPs/Simple_GP_Regression.ipynb) and has very few changes because the out-of-the box models that BoTorch provides are GPyTorch models; in fact, they are proper subclasses that add the `botorch.models.Model` API functions." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "import torch\n", + "\n", + "# use a GPU if available\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.float" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set up function to model\n", + "In this tutorial we will model a simple sinusoidal function with i.i.d. Gaussian noise:\n", + "\n", + "$$y = \\sin(2\\pi x) + \\epsilon, ~\\epsilon \\sim \\mathcal N(0, 0.15)$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Initialize training data" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# use regular spaced points on the interval [0, 1]\n", + "train_X = torch.linspace(0, 1, 15, dtype=dtype, device=device)\n", + "# training data needs to be explicitly multi-dimensional\n", + "train_X = train_X.unsqueeze(1)\n", + "\n", + "# sample observed values and add some synthetic noise\n", + "train_Y = torch.sin(train_X * (2 * math.pi)) + 0.15 * torch.randn_like(train_X)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Initialize the model\n", + "We will model the function using a `SingleTaskGP`, which by default uses a `GaussianLikelihood` and infers the unknown noise level.\n", + "\n", + "The default optimizer for the `SingleTaskGP` is L-BFGS-B, which takes as input explicit bounds on the noise parameter. However, the `torch` optimizers don't support parameter bounds as input. To use the `torch` optimizers, then, we'll need to manually register a constraint on the noise level. When registering a constraint, the `softplus` transform is applied by default, enabling us to enforce a lower bound on the noise.\n", + "\n", + "**Note**: Without manual registration, the model itself does not apply any constraints, due to the interaction between constraints and transforms. Although the `SingleTaskGP` constructor does in fact define a constraint, the constructor sets `transform=None`, which means that the constraint is not enforced. See the [GPyTorch constraints module](https://github.com/cornellius-gp/gpytorch/blob/master/gpytorch/constraints/constraints.py) for additional information.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from botorch.models import SingleTaskGP\n", + "from gpytorch.constraints import GreaterThan\n", + "\n", + "\n", + "model = SingleTaskGP(train_X=train_X, train_Y=train_Y)\n", + "model.likelihood.noise_covar.register_constraint(\"raw_noise\", GreaterThan(1e-5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define marginal log likelihood \n", + "We will jointly optimize the kernel hyperparameters and the likelihood's noise parameter, by minimizing the negative `gpytorch.mlls.ExactMarginalLogLikelihood` (our loss function)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "mll = ExactMarginalLogLikelihood(likelihood=model.likelihood, model=model)\n", + "# set mll and all submodules to the specified dtype and device\n", + "mll = mll.to(train_X)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define optimizer and specify parameters to optimize\n", + "We will use stochastic gradient descent (`torch.optim.SGD`) to optimize the kernel hyperparameters and the noise level. In this example, we will use a simple fixed learning rate of 0.1, but in practice the learning rate may need to be adjusted.\n", + "\n", + "Notes:\n", + "- As the `GaussianLikelihood` module is a of child (submodule) of the `SingleTaskGP` module, `model.parameters()` will also include the noise level of the `GaussianLikelihood`. \n", + "- A subset of the parameters could be passed to the optimizer to tune those parameters, while leaving the other parameters fixed." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from torch.optim import SGD\n", + "\n", + "optimizer = SGD([{\"params\": model.parameters()}], lr=0.025)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Fit model hyperparameters and noise level\n", + "Now we are ready to write our optimization loop. We will perform 150 epochs of stochastic gradient descent using our entire training set." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 10/150 - Loss: 1.966 lengthscale: 0.645 noise: 2.005\n", + "Epoch 20/150 - Loss: 1.930 lengthscale: 0.599 noise: 1.868\n", + "Epoch 30/150 - Loss: 1.894 lengthscale: 0.560 noise: 1.730\n", + "Epoch 40/150 - Loss: 1.857 lengthscale: 0.527 noise: 1.590\n", + "Epoch 50/150 - Loss: 1.819 lengthscale: 0.497 noise: 1.449\n", + "Epoch 60/150 - Loss: 1.779 lengthscale: 0.471 noise: 1.310\n", + "Epoch 70/150 - Loss: 1.737 lengthscale: 0.448 noise: 1.172\n", + "Epoch 80/150 - Loss: 1.692 lengthscale: 0.427 noise: 1.038\n", + "Epoch 90/150 - Loss: 1.645 lengthscale: 0.407 noise: 0.908\n", + "Epoch 100/150 - Loss: 1.595 lengthscale: 0.389 noise: 0.785\n", + "Epoch 110/150 - Loss: 1.542 lengthscale: 0.372 noise: 0.671\n", + "Epoch 120/150 - Loss: 1.487 lengthscale: 0.355 noise: 0.566\n", + "Epoch 130/150 - Loss: 1.429 lengthscale: 0.341 noise: 0.471\n", + "Epoch 140/150 - Loss: 1.370 lengthscale: 0.328 noise: 0.389\n", + "Epoch 150/150 - Loss: 1.311 lengthscale: 0.317 noise: 0.318\n" + ] + } + ], + "source": [ + "NUM_EPOCHS = 150\n", + "\n", + "model.train()\n", + "\n", + "for epoch in range(NUM_EPOCHS):\n", + " # clear gradients\n", + " optimizer.zero_grad()\n", + " # forward pass through the model to obtain the output MultivariateNormal\n", + " output = model(train_X)\n", + " # Compute negative marginal log likelihood\n", + " loss = -mll(output, model.train_targets)\n", + " # back prop gradients\n", + " loss.backward()\n", + " # print every 10 iterations\n", + " if (epoch + 1) % 10 == 0:\n", + " print(\n", + " f\"Epoch {epoch+1:>3}/{NUM_EPOCHS} - Loss: {loss.item():>4.3f} \"\n", + " f\"lengthscale: {model.covar_module.lengthscale.item():>4.3f} \"\n", + " f\"noise: {model.likelihood.noise.item():>4.3f}\"\n", + " )\n", + " optimizer.step()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Compute posterior over test points and plot fit\n", + "We plot the posterior mean and the 2 standard deviations from the mean.\n", + "\n", + "Note: The posterior below is the posterior prediction for the underlying sinusoidal function, i.e., it does not include the observation noise. If we wanted to get the posterior prediction for the observations (including the predicted observation noise), we would instead use `posterior = posterior = model.posterior(test_X, observation_noise=True)`. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# set model (and likelihood)\n", + "model.eval()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "bento_obj_id": "139942981246032" + }, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "# Initialize plot\n", + "f, ax = plt.subplots(1, 1, figsize=(6, 4))\n", + "# test model on 101 regular spaced points on the interval [0, 1]\n", + "test_X = torch.linspace(0, 1, 101, dtype=dtype, device=device)\n", + "# no need for gradients\n", + "with torch.no_grad():\n", + " # compute posterior\n", + " posterior = model.posterior(test_X)\n", + " # Get upper and lower confidence bounds (2 standard deviations from the mean)\n", + " lower, upper = posterior.mvn.confidence_region()\n", + " # Plot training points as black stars\n", + " ax.plot(train_X.cpu().numpy(), train_Y.cpu().numpy(), \"k*\")\n", + " # Plot posterior means as blue line\n", + " ax.plot(test_X.cpu().numpy(), posterior.mean.cpu().numpy(), \"b\")\n", + " # Shade between the lower and upper confidence bounds\n", + " ax.fill_between(\n", + " test_X.cpu().numpy(), lower.cpu().numpy(), upper.cpu().numpy(), alpha=0.5\n", + " )\n", + "ax.legend([\"Observed Data\", \"Mean\", \"Confidence\"])\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Interfacing with Ax\n", + "\n", + "It is simple to package up a custom optimizer loop like the one above and use it within Ax. As described in the [Using BoTorch with Ax tutorial](./custom_botorch_model_in_ax), this requires defining a custom `model_constructor` callable that can then be passed to the `get_botorch` factory function." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def _get_and_fit_model(Xs, Ys, **kwargs):\n", + "\n", + " train_X, train_Y = Xs[0], Ys[0]\n", + " model = SingleTaskGP(train_X=train_X, train_Y=train_Y)\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model).to(train_X)\n", + " model.train()\n", + "\n", + " optimizer = SGD([{\"params\": model.parameters()}], lr=kwargs.get(\"lr\"))\n", + " for epoch in range(kwargs.get(\"epochs\")):\n", + " optimizer.zero_grad()\n", + " output = model(train_X)\n", + " loss = -mll(output, model.train_targets)\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " return model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/website-old/static/files/fit_model_with_torch_optimizer.py b/website-old/static/files/fit_model_with_torch_optimizer.py new file mode 100644 index 0000000000..ad9145963e --- /dev/null +++ b/website-old/static/files/fit_model_with_torch_optimizer.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Fitting models in BoTorch with a torch.optim.Optimizer +# +# BoTorch provides a convenient `botorch.fit.fit_gpytorch_mll` function with sensible defaults that work on most basic models, including those that botorch ships with. Internally, this function uses L-BFGS-B to fit the parameters. However, in more advanced use cases you may need or want to implement your own model fitting logic. +# +# This tutorial allows you to customize model fitting to your needs using the familiar PyTorch-style model fitting loop. +# +# This tutorial is adapted from GPyTorch's [Simple GP Regression Tutorial](https://github.com/cornellius-gp/gpytorch/blob/master/examples/01_Exact_GPs/Simple_GP_Regression.ipynb) and has very few changes because the out-of-the box models that BoTorch provides are GPyTorch models; in fact, they are proper subclasses that add the `botorch.models.Model` API functions. + +# In[1]: + + +import math + +import torch + +# use a GPU if available +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +dtype = torch.float + + +# ### Set up function to model +# In this tutorial we will model a simple sinusoidal function with i.i.d. Gaussian noise: +# +# $$y = \sin(2\pi x) + \epsilon, ~\epsilon \sim \mathcal N(0, 0.15)$$ + +# #### Initialize training data + +# In[3]: + + +# use regular spaced points on the interval [0, 1] +train_X = torch.linspace(0, 1, 15, dtype=dtype, device=device) +# training data needs to be explicitly multi-dimensional +train_X = train_X.unsqueeze(1) + +# sample observed values and add some synthetic noise +train_Y = torch.sin(train_X * (2 * math.pi)) + 0.15 * torch.randn_like(train_X) + + +# #### Initialize the model +# We will model the function using a `SingleTaskGP`, which by default uses a `GaussianLikelihood` and infers the unknown noise level. +# +# The default optimizer for the `SingleTaskGP` is L-BFGS-B, which takes as input explicit bounds on the noise parameter. However, the `torch` optimizers don't support parameter bounds as input. To use the `torch` optimizers, then, we'll need to manually register a constraint on the noise level. When registering a constraint, the `softplus` transform is applied by default, enabling us to enforce a lower bound on the noise. +# +# **Note**: Without manual registration, the model itself does not apply any constraints, due to the interaction between constraints and transforms. Although the `SingleTaskGP` constructor does in fact define a constraint, the constructor sets `transform=None`, which means that the constraint is not enforced. See the [GPyTorch constraints module](https://github.com/cornellius-gp/gpytorch/blob/master/gpytorch/constraints/constraints.py) for additional information. +# + +# In[4]: + + +from botorch.models import SingleTaskGP +from gpytorch.constraints import GreaterThan + + +model = SingleTaskGP(train_X=train_X, train_Y=train_Y) +model.likelihood.noise_covar.register_constraint("raw_noise", GreaterThan(1e-5)) + + +# #### Define marginal log likelihood +# We will jointly optimize the kernel hyperparameters and the likelihood's noise parameter, by minimizing the negative `gpytorch.mlls.ExactMarginalLogLikelihood` (our loss function). + +# In[5]: + + +from gpytorch.mlls import ExactMarginalLogLikelihood + +mll = ExactMarginalLogLikelihood(likelihood=model.likelihood, model=model) +# set mll and all submodules to the specified dtype and device +mll = mll.to(train_X) + + +# #### Define optimizer and specify parameters to optimize +# We will use stochastic gradient descent (`torch.optim.SGD`) to optimize the kernel hyperparameters and the noise level. In this example, we will use a simple fixed learning rate of 0.1, but in practice the learning rate may need to be adjusted. +# +# Notes: +# - As the `GaussianLikelihood` module is a of child (submodule) of the `SingleTaskGP` module, `model.parameters()` will also include the noise level of the `GaussianLikelihood`. +# - A subset of the parameters could be passed to the optimizer to tune those parameters, while leaving the other parameters fixed. + +# In[6]: + + +from torch.optim import SGD + +optimizer = SGD([{"params": model.parameters()}], lr=0.025) + + +# #### Fit model hyperparameters and noise level +# Now we are ready to write our optimization loop. We will perform 150 epochs of stochastic gradient descent using our entire training set. + +# In[7]: + + +NUM_EPOCHS = 150 + +model.train() + +for epoch in range(NUM_EPOCHS): + # clear gradients + optimizer.zero_grad() + # forward pass through the model to obtain the output MultivariateNormal + output = model(train_X) + # Compute negative marginal log likelihood + loss = -mll(output, model.train_targets) + # back prop gradients + loss.backward() + # print every 10 iterations + if (epoch + 1) % 10 == 0: + print( + f"Epoch {epoch+1:>3}/{NUM_EPOCHS} - Loss: {loss.item():>4.3f} " + f"lengthscale: {model.covar_module.lengthscale.item():>4.3f} " + f"noise: {model.likelihood.noise.item():>4.3f}" + ) + optimizer.step() + + +# #### Compute posterior over test points and plot fit +# We plot the posterior mean and the 2 standard deviations from the mean. +# +# Note: The posterior below is the posterior prediction for the underlying sinusoidal function, i.e., it does not include the observation noise. If we wanted to get the posterior prediction for the observations (including the predicted observation noise), we would instead use `posterior = posterior = model.posterior(test_X, observation_noise=True)`. + +# In[8]: + + +# set model (and likelihood) +model.eval() + + +# In[9]: + + +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + +# Initialize plot +f, ax = plt.subplots(1, 1, figsize=(6, 4)) +# test model on 101 regular spaced points on the interval [0, 1] +test_X = torch.linspace(0, 1, 101, dtype=dtype, device=device) +# no need for gradients +with torch.no_grad(): + # compute posterior + posterior = model.posterior(test_X) + # Get upper and lower confidence bounds (2 standard deviations from the mean) + lower, upper = posterior.mvn.confidence_region() + # Plot training points as black stars + ax.plot(train_X.cpu().numpy(), train_Y.cpu().numpy(), "k*") + # Plot posterior means as blue line + ax.plot(test_X.cpu().numpy(), posterior.mean.cpu().numpy(), "b") + # Shade between the lower and upper confidence bounds + ax.fill_between( + test_X.cpu().numpy(), lower.cpu().numpy(), upper.cpu().numpy(), alpha=0.5 + ) +ax.legend(["Observed Data", "Mean", "Confidence"]) +plt.tight_layout() + + +# ### Interfacing with Ax +# +# It is simple to package up a custom optimizer loop like the one above and use it within Ax. As described in the [Using BoTorch with Ax tutorial](./custom_botorch_model_in_ax), this requires defining a custom `model_constructor` callable that can then be passed to the `get_botorch` factory function. + +# In[11]: + + +def _get_and_fit_model(Xs, Ys, **kwargs): + + train_X, train_Y = Xs[0], Ys[0] + model = SingleTaskGP(train_X=train_X, train_Y=train_Y) + mll = ExactMarginalLogLikelihood(model.likelihood, model).to(train_X) + model.train() + + optimizer = SGD([{"params": model.parameters()}], lr=kwargs.get("lr")) + for epoch in range(kwargs.get("epochs")): + optimizer.zero_grad() + output = model(train_X) + loss = -mll(output, model.train_targets) + loss.backward() + optimizer.step() + + return model + + +# In[ ]: + + + + diff --git a/website-old/static/files/ibnn_bo.ipynb b/website-old/static/files/ibnn_bo.ipynb new file mode 100644 index 0000000000..8d6928d6af --- /dev/null +++ b/website-old/static/files/ibnn_bo.ipynb @@ -0,0 +1,572 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Infinite-Width Bayesian Neural Networks for Bayesian Optimization\n", + "\n", + "In this tutorial, we present an overview of infinite-width Bayesian neural networks (I-BNNs) [1, 2] and show how to use them as surrogate models for Bayesian optimization (BO).\n", + "\n", + "Consider an fully connected neural network with $L$ hidden layers, parameter weights drawn from $\\mathcal{N(0, \\sigma_w)}$, bias terms drawn from $\\mathcal{N(0, \\sigma_b)}$, and nonlinearity $\\phi$. In the infinite-width limit, the output of this network is exactly equivalent to $\\mathcal{GP}(\\mu, K^L)$. By the central limit theorem, we find $\\mu(x) = 0$, and we can also recursively define the covariance function as\n", + "$$K^0(x, x')=\\sigma_b^2+\\sigma_w^2\\frac{x \\cdot x'}{d_\\text{input}}\\qquad K^l(x, x')=\\sigma_b^2+\\sigma_w^2F_\\phi(K^{l-1}(x, x'), K^{l-1}(x, x), K^{l-1}(x', x'))$$\n", + "where $F_\\phi$ is a deterministic function based on the activation function $\\phi$.\n", + "\n", + "We will refer to this kernel as the \"I-BNN kernel\". Unlike many popular GP kernels, I-BNN covariance function is not based on Euclidean distance, allowing the GP to represent nonstationary functions. This is advantageous for many settings of Bayesian optimization, since the function we want to optimize may not have similar behavior throughout the entire input space. Furthermore, I-BNNs have been shown to work particularly well for BO problems with high-dimensional inputs [3].\n", + "\n", + "BoTorch has an implementation of I-BNNs with ReLU activations: `InfiniteWidthBNNKernel`.\n", + "\n", + "\n", + "[1] [Y. Cho, and L. Saul. Kernel Methods for Deep Learning. Advances in Neural Information Processing Systems 22, 2009.](https://papers.nips.cc/paper_files/paper/2009/hash/5751ec3e9a4feab575962e78e006250d-Abstract.html) \n", + "[2] [J. Lee, Y. Bahri, R. Novak, S. Schoenholz, J. Pennington, and J. Dickstein. Deep Neural Networks as Gaussian Processes. International Conference on Learning Representations 2018.](https://arxiv.org/abs/1711.00165) \n", + "[3] [Y.L. Li, T.G.J. Rudner, A.G. Wilson. A Study of Bayesian Neural Network Surrogates for Bayesian Optimization. International Conference on Learning Representations 2024.](https://arxiv.org/abs/2305.20028)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import warnings\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import torch\n", + "from torch import nn\n", + "\n", + "from gpytorch.kernels import MaternKernel, RBFKernel, ScaleKernel\n", + "from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "\n", + "from botorch import manual_seed\n", + "from botorch.acquisition import LogExpectedImprovement\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.kernels import InfiniteWidthBNNKernel\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.optim.optimize import optimize_acqf\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "%matplotlib inline\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "tkwargs = {\"device\": device, \"dtype\": dtype}\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### I-BNN Function Draws\n", + "\n", + "We start by visualizing the posteriors of an I-BNN. Here, we define a toy function and draw five initial function evaluations." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "output": { + "id": 1056366939451157, + "loadingStatus": "loaded" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "torch.manual_seed(1111)\n", + "def f(x):\n", + " x = -(x - 0.15)\n", + " return torch.sin(x * (2 * torch.pi)) + torch.sin(x * (2 * torch.pi) * 2)\n", + "\n", + "x = torch.linspace(0, 1, 100).to(dtype).unsqueeze(-1)\n", + "true_y = f(x)\n", + "\n", + "train_x = torch.rand(5, 1).to(**tkwargs)\n", + "train_y = f(train_x)\n", + "\n", + "# visualize the function and the training data\n", + "plt.figure(figsize=(4, 3))\n", + "plt.plot(x.cpu(), true_y.cpu(), linewidth=2)\n", + "plt.scatter(train_x.cpu(), train_y.cpu(), color=\"black\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Initializing the Model**: We now define two versions of the I-BNN, constructed using a GP with an `InfiniteWidthBNNKernel`. One version has fixed user-specified values for $\\sigma^2_w$ and $\\sigma^2_b$, and the other uses the marginal log likelihood to optimize these hyperparameters." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# Function queries are not noisy\n", + "train_Yvar = torch.full_like(train_y, 1e-8)\n", + "\n", + "# I-BNN with fixed hyperparameters\n", + "ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device)\n", + "ibnn_kernel.weight_var = 10.0\n", + "ibnn_kernel.bias_var = 5.0\n", + "model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel)\n", + "model.eval()\n", + "\n", + "# I-BNN with optimized hyperparameters\n", + "model_optimize = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=InfiniteWidthBNNKernel(depth=3))\n", + "mll = ExactMarginalLogLikelihood(model_optimize.likelihood, model_optimize)\n", + "fit_gpytorch_mll(mll)\n", + "model_optimize.eval()\n", + "\n", + "# Default GP with Matern for comparison\n", + "model_matern = SingleTaskGP(train_x, train_y, train_Yvar)\n", + "mll_matern = ExactMarginalLogLikelihood(model_matern.likelihood, model_matern)\n", + "fit_gpytorch_mll(mll_matern)\n", + "model_matern.eval();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Visualizating the Posterior**: " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_posterior(ax, model, n_draws=5):\n", + " with torch.no_grad():\n", + " ax.plot(x.cpu(), true_y.cpu(), linewidth=2, color=\"black\", label=\"True Objective\", linestyle=\"--\")\n", + " ax.scatter(train_x.cpu(), train_y.cpu(), color=\"black\", s=80, label=\"Observations\")\n", + "\n", + " test_x = torch.linspace(0, 1, 100).to(**tkwargs)\n", + " pred_f = model(test_x)\n", + "\n", + " ax.plot(test_x.cpu(), pred_f.mean.cpu(), linewidth=2, label=\"Mean\")\n", + " lower, upper = pred_f.confidence_region()\n", + " ax.fill_between(test_x.cpu(), lower.cpu(), upper.cpu(), alpha=0.2, label=r'$\\mu \\pm 2\\sigma$')\n", + "\n", + " for i in range(n_draws):\n", + " if i == 0:\n", + " ax.plot(test_x.cpu(), pred_f.sample().cpu(), color=\"green\", linewidth=0.5, label=\"Function Draw\")\n", + " else:\n", + " ax.plot(test_x.cpu(), pred_f.sample().cpu(), color=\"green\", linewidth=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "output": { + "id": 1453096472021894, + "loadingStatus": "loaded" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 3, figsize=(18, 5))\n", + "\n", + "plot_posterior(axs[0], model)\n", + "axs[0].set_title(\"I-BNN (Fixed Hypers)\\nWeight Var: %.2f, Bias Var: %.2f\" %\n", + " (model.covar_module.weight_var.item(), model.covar_module.bias_var.item()),\n", + " fontsize=20)\n", + "axs[0].set_ylim(-7, 8)\n", + "axs[0].legend()\n", + "\n", + "plot_posterior(axs[1], model_optimize)\n", + "axs[1].set_title(\"I-BNN (Optimized Hypers)\\nWeight Var: %.2f, Bias Var: %.2f\" %\n", + " (model_optimize.covar_module.weight_var.item(), model_optimize.covar_module.bias_var.item()),\n", + " fontsize=20)\n", + "axs[1].set_ylim(-7, 8)\n", + "\n", + "plot_posterior(axs[2], model_matern)\n", + "axs[2].set_title(\"GP (Matern Kernel)\\nLength Scale: %.2f\" %\n", + " model_matern.covar_module.lengthscale.item(),\n", + " fontsize=20)\n", + "axs[2].set_ylim(-7, 8)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similar to a Matern kernel, we see that the uncertainty decreases around queried points and increases as we move away. However, we find that the I-BNN function draws are more jagged compared to the Matern draws, and we also note that the uncertainty of an I-BNN towards the edges of the data increases rapidly." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Impact of Hyperparameters\n", + "\n", + "The I-BNN has three hyperparameters: the number of hidden layers, the variance of the weights, and the variance of the bias terms. Here, we visualize how modifying these hyperparameters impacts the posterior." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "output": { + "id": 375446892116177, + "loadingStatus": "loaded" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 4, figsize=(20, 4))\n", + "\n", + "for i, ax in enumerate(axs):\n", + " ibnn_kernel = InfiniteWidthBNNKernel(depth=(i+1), device=device)\n", + " ibnn_kernel.weight_var = 10.0\n", + " ibnn_kernel.bias_var = 2.0\n", + "\n", + " model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval()\n", + " plot_posterior(ax, model, n_draws=5)\n", + " ax.set_title(f\"Depth: {i+1}\")\n", + " ax.set_ylim(-8, 8)\n", + " if i == 0:\n", + " ax.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "output": { + "id": 1155028772461291, + "loadingStatus": "loaded" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 4, figsize=(20, 4))\n", + "\n", + "for i, ax in enumerate(axs):\n", + " ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device)\n", + " ibnn_kernel.weight_var = (i+1) * 5\n", + " ibnn_kernel.bias_var = 2.0\n", + "\n", + " model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval()\n", + " plot_posterior(ax, model, n_draws=5)\n", + " ax.set_title(\"Weight Var: %.1f\" % ((i+1) * 5))\n", + " ax.set_ylim(-10, 10)\n", + " if i == 0:\n", + " ax.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "output": { + "id": 395095789997175, + "loadingStatus": "loaded" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 4, figsize=(20, 4))\n", + "\n", + "for i, ax in enumerate(axs):\n", + " ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device)\n", + " ibnn_kernel.weight_var = 10.0\n", + " ibnn_kernel.bias_var = (i + 1) * 5\n", + "\n", + " model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval()\n", + " plot_posterior(ax, model, n_draws=5)\n", + " ax.set_title(\"Bias Var: %.1f\" % ((i+1) * 5))\n", + " ax.set_ylim(-5, 6)\n", + " if i == 0:\n", + " ax.legend()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### I-BNNs for Bayesian Optimization\n", + "\n", + "We will now use I-BNNs as the surrogate model for a high-dimensional BO problem." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Define High-dimensional Function and BO Setup**: We will optimize the output of a multilayer perceptron (MLP) with 2 hidden layers, 50 nodes per layer, and ReLU nonlinearities. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "class MLP(nn.Module):\n", + " def __init__(self, input_dims):\n", + " super().__init__()\n", + " self.layers = nn.Sequential(\n", + " nn.Linear(input_dims, 50, dtype=torch.float64),\n", + " nn.ReLU(),\n", + " nn.Linear(50, 50, dtype=torch.float64),\n", + " nn.ReLU(),\n", + " nn.Linear(50, 1, dtype=torch.float64)\n", + " )\n", + "\n", + " def forward(self, x):\n", + " return self.layers(x)\n", + "\n", + "def create_f(input_dims, seed):\n", + " # create MLP with weights and biases sampled from N(0, 1)\n", + " with manual_seed(seed):\n", + " model = MLP(input_dims).to(**tkwargs)\n", + " params = torch.nn.utils.parameters_to_vector(model.parameters())\n", + " params = torch.randn_like(params, dtype=torch.float64)\n", + " torch.nn.utils.vector_to_parameters(params, model.parameters())\n", + "\n", + " def f(x):\n", + " with torch.no_grad():\n", + " return model(x)\n", + "\n", + " return f\n", + "\n", + "INPUT_DIMS = 200\n", + "N_ITERATIONS = 100 if not SMOKE_TEST else 5\n", + "N_INIT = 50 if not SMOKE_TEST else 2\n", + "\n", + "f = create_f(INPUT_DIMS, seed=1234)\n", + "bounds = torch.stack([torch.zeros(INPUT_DIMS), torch.ones(INPUT_DIMS)]).to(**tkwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Define BO functions**: We use Sobol sampling to initialize the BO problem, and we use the Expected Improvement acquisition function." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.acquisition.analytic import ExpectedImprovement, LogExpectedImprovement\n", + "\n", + "def generate_initial_data(f, bounds, n, input_dims):\n", + " train_x = draw_sobol_samples(bounds=bounds, n=n, q=1).to(**tkwargs)\n", + " train_x = train_x.squeeze(-2) # remove batch dimension\n", + " train_y = f(train_x)\n", + " return train_x, train_y\n", + "\n", + "\n", + "def gp_bo_loop(f, bounds, init_x, init_y, kernel, n_iterations, acqf_class, optimize_hypers=False):\n", + " train_x = init_x.clone()\n", + " train_y = init_y.clone()\n", + "\n", + " for iteration in range(n_iterations):\n", + "\n", + " # fit model to data\n", + " model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=1), covar_module=kernel)\n", + " if optimize_hypers:\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + " model.eval()\n", + "\n", + " # optimize acquisition function\n", + " candidate_x, acq_value = optimize_acqf(\n", + " acq_function=acqf_class(model, train_y.max()),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=10,\n", + " raw_samples=200,\n", + " )\n", + " candidate_x = candidate_x.double()\n", + "\n", + " # update training points\n", + " train_x = torch.cat([train_x, candidate_x])\n", + " train_y = torch.cat([train_y, f(candidate_x)])\n", + "\n", + " return train_x, train_y\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Compare I-BNN with GP with Matern kernel and RBF kernel**: On this high-dimensional problem, the I-BNN significantly outperforms the standard Matern and RBF kernels and is able to find better rewards." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "from functools import partial\n", + "# define kernels\n", + "ibnn_kernel = InfiniteWidthBNNKernel(2, device=device)\n", + "ibnn_kernel.weight_var = 10.0\n", + "ibnn_kernel.bias_var = 5.0\n", + "ibnn_kernel = ScaleKernel(ibnn_kernel, device=device)\n", + "\n", + "matern_kernel = ScaleKernel(MaternKernel(), device=device)\n", + "rbf_kernel = ScaleKernel(RBFKernel(), device=device)\n", + "\n", + "# initialize problem\n", + "train_x, train_y = generate_initial_data(f, bounds, n=N_INIT, input_dims=INPUT_DIMS)\n", + "\n", + "# run BO loop\n", + "acqf_classes = {\"LogEI\": LogExpectedImprovement}\n", + "results = {}\n", + "for acq_name, acqf_class in acqf_classes.items():\n", + " run_bo_with_acqf = partial(gp_bo_loop, f=f, bounds=bounds, init_x=train_x, init_y=train_y, acqf_class=acqf_class, n_iterations=N_ITERATIONS)\n", + " ibnn_x, ibnn_y = run_bo_with_acqf(kernel=ibnn_kernel, optimize_hypers=False)\n", + " matern_x, matern_y = run_bo_with_acqf(kernel=matern_kernel, optimize_hypers=True)\n", + " rbf_x, rbf_y = run_bo_with_acqf(kernel=rbf_kernel, optimize_hypers=True)\n", + " results[acq_name] = {\n", + " \"BNN\": (ibnn_x, ibnn_y),\n", + " \"Matern\": (matern_x, matern_y),\n", + " \"RBF\": (rbf_x, rbf_y),\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "output": { + "id": 361920396970842, + "loadingStatus": "loaded" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAtAAAAI9CAYAAAATjU/AAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAACnVklEQVR4nOzdd3hUVfoH8O/MJDPpvSckECB0CCUEQm+CiAqiIiII9t7burq7P3V1dW1r74qCgiBVRIr0FjqBAEnoCYT0nkyf3x+BIZN7J5mZTDIl38/z+Mice+65b3ITeOfMe8+RZGVlGUBERERERBaROjoAIiIiIiJXwgSaiIiIiMgKTKCJiIiIiKzABJqIiIiIyApMoImIiIiIrMAEmoiIiIjICkygiYiIiIiswASaiIiIiMgKTKCJiIiIiKzg4egAiIjIMo8+9aLxz6kpAzFn1u0OjccdZOecxv8+/cr4+q6Zt2Fo6iC7jF1SUop/vP628fXkieNxw/UT7DI2ETkWZ6CJiIiIiKzAGWgisprBYEBW9ikcOnIU587nory8ArV1dZB7esLX1wcxMdHo0rkThqQMhJ+fr9XjX75cgAOHM3DiZDbKKypRVVUNhVyOgAB/xMXFYGByX/Ts0Q0ymczqsUtKSnHwyFFkZp5EaXk5Kiur4CGTISDAH9HRkejfrw/69u4JuVxu9diu6MeFvyJ93wGL+0skEnh7e8HH2xvRUZHomBCPgQP6ITwstFXjJCJyJkygicgq5y/kYeGipbh4KV9wTKlSQalSoaS0DEePHcfqNeswcfwYTLpuLKTS5j/wUipVWL5qDXbu3guDwWByTKvVoqa2FvmXC7Bv/yHExkRj9p23o0NcjEVxa7VarF3/F9Zv3AK9Xm9yTKPRoE6pREFhEQ4fOYaQ4GDMvvM2JHXtbNHY7YnBYEBtbR1qa+tQXFKKo5kn8Pva9ejdqwdmTL8ZwcFBjg6RiKjVMYEmIosdPJyBH35aBJ1OZ9IeFBQIfz8/qFQqFJeUGhNUrVaLNX9uwMX8fNx796wmk2ilUoX3P/pckJj7+vggKCgQSpUKZWXlxrEvXsrHux98gsceuQ9dOyc2GbdOp8MXX/+AE1k5Ju1eXgqEBAdDq9WitKwcWq0WAFBaVoaPPvsac2ffgUEDkq38Lrm26KhIKBTmZ981Gi1qa2tRVl5hbDMYDDh67DhyTp3Gow/ei8ROCW0ULRGRYzCBJiKLnD13Ad//+IvJ7G3akBRcN36Mycf3SqUKu9P3YdWadVCr1QCAw0eO4a/N2zBh3Giz43/3488myXOXzp1w0w2T0Dmxo7GtuqYG23fsxh/r/oJer4dWp8PX3/2EF595HKGhIWbHXrJslUnyHBMdhWk3TUb3bl2NSb1SpUL63gNYsXot1Go1DAYDfvp5CcLDQpEQ38Gm75krmnXHdHTq2HwCXFdXh6OZJ7B+4xbkXy4Artz7z778Dn97/skm7wcRkavjQ4REZJGfF/9mkjzfcds0zLrjVkHtq5eXAmNGDcfjD98HjwY1yus2boFSpRId+2jmCWQeP2l83bd3TzzxyP0myTMA+Pn64vqJ43H/PbONbTU1tVj9x3qzcV+8lI8du9KNrxPiO+C5px5Fzx7dTGbEvRQKjBqRhqcee9AYt1arxdLlvzf7vWmPvL29MXjQALz03BPo16eXsb1OqcTyVX84NDYiotbGBJqImpWVfQqX8i8bX/fu2R0jhg1p8pzETgkYOiTF+Lqurg45OadF+65Zu8H4Z18fH9w187YmHxDs27snhgy+ttTY/oOHcbmgULTvH39uNNZTS6VSzJ19R5MlCgnxcZg4Yazx9Zmz53AiK9ts//bOw8MDs++8Db6+Psa2I0czUVNT69C4iIhaE0s4iKhZJ06aJpDNJc9X9evTC9t37jG+Pp+bhz69e5r0KSgoQm7eRePr1JQBJsmYOWNGDcOevfuBKzW4+w8expTrrzPpo1SqcCzzhPF1n949EBEe1uzYI4YPwboNm6C9Uuu9b/8h9OiW1Ox5tjh/IQ979u5Hds5pVFRUQqfXISAgABHhYRg0IBn9+/V2+hVBvL290b9fH+NMv16vR86pM0ju19uk3+70/VjwyxLj6/+++S/4+HijsKgYf6zbiLNnz6P0Sp37px++LbjOVXVKJQ4eOoITWTm4eDEf1TU1UKnU8Pb2gp+vLxLi49AtqSsGJPeBp6enzV/X2XPnsXf/IZw+cw4VFZVQqlTw8/NFVGQE+vbuiaGpg1rl3lzKv4xDh4/iZPYplJWVobqmBjKpDH5+voiICEePbl0xsH8/BAYGNDvW62+9Z3xzOWzoYNw5YzoAoKKyCrv27EXG0UyUlpWjrk4JH29vxMVGI2Vgf6QM6i94ZiE37yK279yDs+cuoLSsDHq9HgH+/uic2BEjh6ehY0L7KXUiYgJNRM1K6toZCoUclVXVqKyqsvghsZDgYJPX1dU1gj5HM4+bvO6f3MeiseNiYxARHobComIAwLHME4IE+vjJLGMSDAD9+1k2tr+fH7p0ScTJK3XTmcezoNfrLVpJxFJ6vR5Ll6/Gth27BSuOFBeXoLi4BMdPZOH3P9bjvnl3ISE+zm7Xbg2xMdEmr8srKsz2vUqtVqO4pBQffPyFsV6+OZu37sTadRtRUyuc4a6urkF1dQ0uFxQifd9BrPp9LW6+cTIGD+pvxVcCaLRa/LTwVxw4dERwrLy8AuXlFTiZlYO/tmzHvNkz0aljvFXjm1NdXYPfVvyOfQcOCX4mNNBCeeUh3eMnsrD6j3UYO3oEbpg0ocmfy4aftqiufI8zj5/Edz/+DKXStKSqqroaJ7JycCIrB/sOHMKD98+Fp4cH9Ho9Fi1Zjp279wrGLy4pRXFJKdL3HcTNUybhuvFj7PCdIHJ+LOEgomb17NEN108cjxm3TsX982bDx6f5GWIAUKqUJq89PYTv2XNOnzX+2UMmQ4cOlieKHROuJS65eZdQ2yipOtVgbABItODhuKs6NRi7uqYGFy9dbrK/tX75dRm2bt9lkih5enogMiIcsTHR8L3yPS4tK8PHn31tfKPgrBSNZmItSYhVKjV+XrzUor56vR4/L/4NS5evMkmepVIpQkND0CEuBiHBwZBIJMZj5RWVmL9gEdau22jV1/LjgsXG5FkmkyEsLBRxsTHw9/Mz6VdSUopPv/zOpLzJViWlZXj/o8+xd/9Bk58Jfz8/xMXGICoywmQ2Xa3W4M/1m/D1dz9Bc2X1GDENS6E0ag1y8y7h6+9/glKpgqenB6KjIhETHSWYqT+RlYPfrzxb8Muvy4zJs4+Pt/HNa8PvNQCs/P1PZJsp0yJyN5yBJqJWU1BYZPI6VGSzjYIGtcsREeGiSbY5cbHRuFLFYbxewxUkGo7tpVBYtTJEXKzp+tIFhYUWrzndnKOZJ7Brzz7jaw8PD9w4+ToMTxsCLy8FcKUs5ey5C1j5+1qcOn0WC35Zapdrt5aqGtNPF/waJZtijhzNRG7eJYQEB2PazZPRPakL5HI5KioqBX03bd1hMgPq7eWFG66/Dqkp/U3e0FVUVmHHzj1Yv3Gz8dOH39duQHyHOPTq2b35mDKO4WjmCSgUcky5/joMGTwIPj7exuNnzp7DoiUrjCvG1NXVYdGS5XjmiYebHdscrVaLL7+Zb/L70qdXD9xw/XUmP3MarRbHMk9gxeq1KC4uAQBkHDuO3/9Yj2k3TRYdWyK5Nk+mVqux4Jcl0On0uPGGiRg9Ypjx502j0eCvzdux+o91xv7bduxCbEw0du3Zh6DAANx+61T06dXDOONdVVWNxUtX4NCRo8ZzNmzayvXTqV3gDDQRtZq9+w6avO7TKIHRarUoKS0zvrZ2E46gwECT1wWFprO0lxskJEHBpn2b07i+tPGbgZZYvWadyeu7Zt6K8WNHGZMZXNnxL7FTAp589AH07tkdp8+cFRnJeZw5e87kdXRUZLPnbN+5BwEB/nju6UcxILkvfHx84OHhIXijU1ZWbpwNxZU3Q089/iDGjBom+DQkMMAfN1w/AffcPctkhnTRkhWCDXTEHM08AU9PTzz+8H0YO3qESfIMAImdOuKpxx5AWIMYT585Zyz3scW6DZtNlnAcM3IYHrp/ruANm6eHB/r364MXnn4MkRHhxvZNW7abnQWXNvgeZOWcRt7FS5h1x62YNGGsyc+bp6cnJl031mTdc7Vag4W/LIG3tzeefOxB9OvTy6RcxN/fD/PmzDQp1TqZlQO1WmPz94LIVTCBJqJWkZV9CiezTxlf9+yeJEiMKquqTJKaIAseimooMMi0f1l5ucnrhjOZjZPt5gQ1Gru8vPmaXktcyr9skix1SohHykDzNbpSqRQzZ0xv0cNwra2gsAhHj117WDMgwN+iB8pKy8pw0w2TEBjg32S/nbv3QqO5lpRNum6s4BOCxvr17YXkvtceYiwtK8Pxk5atpjJm1PAm18L28fHB5InjTdoOHs6waOzGNFottu7YZXwdGxONW6ZOafIcX18fzL7zduNrvV5vslSjOXq9Ht2SumDI4IFm+wwc0M/ktVanw6QJY8w+fCuTyZDc99oyhnq9HgWF4iviELkTJtBEZHdV1dVY8MtSk+Xjpt18g6CfSmVa+2rtigaN624bjnd1MxRzfa0fW3wNa2sdyzxp8npwyoBmzwkKDDBZa9mZlJSW4ctv5pu8EZo0YaygPlaMl0KBlIHN7/S498Ah4589ZDKMGDbUotiGN1ot5qDIQ4FiRg5vfpWZ/sl9TOqLM09kWTR2YxmNlvwbM2qYRQ+rduoYb/ImZf+Bw4IHD8UMGzq4yeMx0VEmr6VSKVKbSLgBICbG9Bx7vdkkcmZMoInIrpRKFT7/6nuUll0rzbhpyiTBP8wQedDMmvpnsf4Nk1xVo7E9rBzbw8N0xrdxsm+rvIuXTF433izGnJ7dW2cZPWvpdDpUVVcjK/sUli5fjTff/sCkvKV7t64YnpZq0VhdOndq9r5UVlWhpKTU+DoxsaNJ6UGT4yd2hKfntfHP5+Y1e05MdBSCg5ovJZLL5SYlFuXlFairq7MoroZOnzEtfUnq2sXic3t272b8c01trUUPmnbt0vS29wH+prXrEeFhgocnG/P3N/0EwV6/K0TOjA8REpHdVNfU4NMvvsOFBonKoAHJmDB2lGh/jcZ09QCZh/nNU8Q0Tr60DVYj0DYa28PqsU37N7XSgTUabvgikUgsWpcaAKIsqCm2h3c//Mzmc3t2T8J982Y3uQlOQ5Z8Tbm5F01ex4q8ETPnaj315cv13/PCwmJotNom36iJvdEzJyw0FOfO5xpfFxQWW70W8vkL186XSCQIDrK81Cg62vT7l5d3yaQ2ujEvhQIB/k2XyzQuFQq34OdT3ugce/2uEDkzJtBEZBeXCwrxxdc/oOjK6gC4Uoc6Z9btZs9pODsIAFqtzmxfMdpG/1A3/MffQzC2df+oN47FXjXIlVVVxj/7+vhYPG5IiHUPWLalsNAQTBg32uKZ56ssqXkvb7QihzUrqeDKWuRXE2i9Xo+a6hoENZGkhoYGmz3WWOMHTWtqhOucN6fx1/fe/yx/A6NsVFbU3NrblszcNy698VJYNttP1N4wgSaiFjuRlY1vv1+IOuW1dZ9TUwbirpm3Nr3JQ6M6Y63Guqf3G890NRxPITf9h7/xbHdztFrTWBrHumjJcpMdFM25/rpx6N2rh/F1w4+3ran5bvz1tJboqMgmtzqXSCRQKBTw9fFGdHQUuiR2QufEjjZtMmPJ169Umq4lrrAyoWt83+rHM59Aeym8LB5bLm9U5mPhZjANNSz7MBgMJjPaVo/V6HvVmFRq3acwuFIDTURCTKCJqEV27ErH4qUrBA+R3XjDxGbPVTSaEbM2AWm8k1rD8eRyT0ilUmNc1o5d12jsxrN3ly8XWpTsVDXafbHhahLWlJVYW4Jiq1l3TG9yBQp7siQ5Uzd6U9X4U4vmNP4kQtXMEmvWfNLQVAmRpax9Y9cU1h4TtR0m0ERkE71ejxWr/8Bfm7cb2zxkMtxx+y0YmjrIojEC/P3hIZMZN7woKytv9pyGShv1Dw0x/fg9ODjI+ACatWM37h8SYvlH+03x8PAwJtHWlKy017V1G88gW/1JQqP+Tc2uA4BOZ/n4jceWe1q30guuzKhfnYUODQ3Ba6++aPUYRNT2mEATkdX0ej3mL1yM/QcOG9v8fH1x/z2z0aVzJ4vHkclkCAsPNdaoNk6Im9M4yY2KjGz0OuJaAl1eAYPBYNHyamKxRDca+6nHH7Qq1qsUCrkxgdZYUbJSp7R+hQd34OVlWlLRuKSjOY3rhL29mi7RsGYWV60x7dtcci7Gy+taAm3t10ZEjsPiJiKyil6vxw8/LTJJnsPDQvHsU49YlTxf1TAxLSouESQ8TWm4goFUKhWsQBAVGWH8s0ajMdnauzkXLpiWZzRe8cBWvg12zquuqbH4Y//iBku5tSfBjXaQtPb70HAJPA+ZDH5+vk32t+ZNXFmZ6UN7Ac1sCCOm4acmNTW1zdYxE5FzYAJNRFZZtuJ3HGiwIUVCfAc899SjFi/H1lj3bl2Nf9br9Th37oJF5+n1epw5d974ukvnToKHuno0GBsATp89D0udarA+b2hoiM1fX2MNk3yDwWBxQnjxkvhWze4uPi7O5HXjdbSbolKpTb6/MTHRzS6xl3+5wOLxG644Y82ShA3Fx8WavG68bB8ROScm0ERksR270rF5207j6/gOsXj84XubndVrSp/ePU3KKvYfPNxk/6tOnMxGdYMH9MR26uvaJdHk4T9Lx758ucBkhQ177gLYeJ3hxhtpmHPcxp3uXJ2vr4/JJwlnz10w2bmvKSezc0webu3UMb7Zc3LzLlo0fl1dncmW7BHhYTYtddi50ac2h44ctfjcqqpqVFVXW31NImo5JtBEZJHComL8tmK18XVYaAgee+g+eHt7t2jcwAB/k1noA4eOoKy8+Y/RN27eZvyzXC7HwP79BH08PDwwaMC1raJzTp0x2eTFnA2bthr/LJFILH4o0hI9epjuKGhJUl9QWIQTJ7PtFoOrSU25tpW0Xq/H1u27LDqvcb8hzWxJfXX8nbvTm+23/+ARk+S8T++eFsXUWO+e3U3egO7Ze8DirbAXL12Bv736Bt794FNs2rLdgjOIyF6YQBORRRb8stS4EoSHhwfumzcbvr4+zZ5niRsnX1vyTq3W4MeFvza5m9lfm7chO+e08fW40SPg7y++3fCk68Yalz4zGAz4ceGvqK01/0DeoSNHsWfvAePrlIH9rdqdrjmJHRNMNvLIzjmNI0czzfbXaLVYuGipSbLW3gxNHWTy8N+GTVtw/kLTb4R27EpHVvYp4+vETgmI7xDX5DlXrdu4GZfyzZfMVFVV48/1f5m0ib2Bs4SHhwdGDh9qfK1Wq/H9T780u+rKth27cTjjGAwGA86ev2BSTkJErY8JNBE16/iJLJw+c9b4etSIoegQF2O38RPi4zA8bYjxdXbOaXzw0Rc41eCaAFBWXo5ff1uJZSvXGNuiIiMw3sxW4QAQHBSEyRMnGF/nXy7Afz/4BJnHT5okpdU1NVjz5wZ8N/9nY1tAgD9umtL8etbWkEqlmDxxnEnbDz/9gi3bdpqsAKHX65Fz6gze/99nOH3mHLp2Tmy0goTBrnE5M39/P0y7+Qbja7Vag48++wrbduwWrAVeXFyCxUtXYNGS5cY2T09PzJox3aJr9ezRDUqlCh9+/CV27k43eajVYDDU/2x+/IXJDoLJfXsjvkOsmRGbN3H8GJM3aadOn8V/P/gEx09kCd445eZdwvwFi7F46QoYDPU/A35+vpg8abzN1yci63EZOyJq1rqNm01eHz+RjdNnPrFprAfumSPYAhkAbp9+E4pLSnAyKwe4ssLGBx99AV9fHwQFBqKuTomy8nJj0oAridUjD8xrdovi68aPRkFhEfbs3Q9cKUf57Kvv4e3lheDgIKg1GpSVlUOnu7Yus1wux0P3zUVwkP230B6amoLDR47h+JWyDLVagyXLVmHF6rUIDQ2GTCpDWXm5cabcy0uBmTNuwQcffwFcWaRBr28/CTQADBs6GAWFRfjrSumOUqnC4qUr8Nvy1QgJCYZCIUdVVbVga2xPT0/Mmz0TUVHiq6jo9KZrcQ8ZPBAKuRyHjhzFz4uX4dffViEkOAhyuRzl5RWobrRdd3BQIG675aYWfW0eHh548L678ekX36KwqBgAcCn/Mj798jt4eSkQEhwMvV6PsvJywTJ7CoUcD947B/5+4p/AEFHrYAJNRM0qLTWtSbZmpYLGzC3bJpPJ8PD9c7Hmz43YuGmrceatpqZW9KGuXj27484Z0xEkkoyLuWvmrYiNicKqNeuM6y/XKZWoE/movmNCB8y5cwYiI8NFRmo5qVSK++bNxk8//2ry0JhGozGuiX1VUFAg7pt7FyIjwqGQy1F1pV1rxYYf7uKWm29ARHgYVv+xzvgAqVanMyadjSXEx+H26VPRMaGD2TF1OtMZXoVCgbtn3wGZTIb9Bw9Dq9WaHT82Jhr33j3LpCTHVmGhIXjmyYfx2/Lfsf/gYeMbRaVSZbacpFPHeNw18zaThyyJqG0wgSYip+Hh4YGbp0zCsCEp2HfwMDKPn0RZeQWqq6rh5e2FwIAAdE7siIED+qFLonVrTkskEowdPQID+/fDgUNHkHH0OEpKy1BZVQW53BOBAQFIiI/DwP790L1bV4u2mW4JhUKO++bdhazsU9h34BBOnzmHysoq6PR6BAb4IyI8DAP690P/5D7wUtTPsPv6+hqXZWuv2zYPT0vFwAH9cPDQERw/kY28S/moqa6BWqOBj4+38WekT68e6NE9qdnxVI3WHffx9oKnhwfmzZmJ0SPTsGfvQZw5ew4VlZVQqdTw8/NFTHQUBiT3weBBA5pdFs8a/n5+mDv7DkycMAaHDh9FVs4pFJeUoqamFnq9Ht5eXggLC0HHhHgk9+uNrp0T7XZtIrKOJCsrq319DkhERERE1AJ8iJCIiIiIyApMoImIiIiIrMAEmoiIiIjICkygiYiIiIis4PSrcOxO34+ly1dBqVThtVdfRGhoiGi/M2fPYe26v3DuQi60Wh3Cw0IxNDUFo0emQSKRmPTV6XTYvG0n9u47iMKiYshkUsTFxmDcmJHo22g7Vmv6EhEREZH7c9pVOKqqq/HL4mXIOHYcnp6eUKvVZhPowxnH8M33CxATHYXhaanw8PDA3v0HkXPqDEaPHCZY5P6r737EkYxM9O7VA8l9e0Or1WLn7nTk5l3CHbdNw4hhQ2zqS0RERETuz2lnoN9+72PodDo88sA8rN+4BTmnz4j2q62txcJFSxEbE43nnnoEnp6eAIDUlAF473+f48zZc1AqVcadyg5nHMORjEwMGpCMeXNmGsdJTRmAN9/5EMtWrkFy397w9/ezqi8RERERtQ9Om0AndozHbdNvhr+fH9Zv3GK2X/q+g6itrcOcO283Js+4sqvZC888Juy/9wAAYNyYkSbtcrkcw9NSsXzVHzhwOAOjR6RZ1dcSSUnNL+pvL3q9HgUXLyAyNr7VN4Qg++P9c328h66P99D18R66Nkfdv+zs7Gb7OO1P0z13z4K/X/Mzu8dPZEEqlaJ7t64AAIPBALVaY7b/mXPn4enpibjYaMGxxE4J9X3OnLO6LxERERG1D047A22p/MsFCA4KRGlZOZavXIMTWTnQarXw9/PD4JT+mHL9RMjl9TPTSqUK1dU1CA8LFX0nExwcBAAoKi6xqq+l9Hp9C75S61y9Vltek+yH98/18R66Pt5D18d76Nqc+f65fAJdXVMLHx9vfPTpV0ju1wf33H0nlEoVdu5Ox1+bt+PixXw89vB9kEgkUKpUAACFQiE6lkJe365UKq3qa6mCixes/vpaqig/r82vSfbD++f6eA9dH++h6+M9dG3OeP9cPoHW6XSoqKjE9KlTMHb0CGN7ysBkvPP+xziZfQqZx0+id68eFoxWvyBJ42XvWt63XmRsvMV9W0qv16MoPw/h0XGs+3JBvH+uj/fQ9fEeuj7eQ9fmqPtXdepUs31cPoFWyOWoUyqRMqi/SbtUKsWQwYOQm7cK2adOo3evHvD28gIA4+xyY1fbvby8rOprKUf88kqlUv6l4cJ4/1wf76Hr4z10fbyHrs0Z759zRWODq+tCy0S+sQEB/sCV2mcAUCjkCAzwR3l5hWg9TUlJGQAgIiLMqr5ERERE1H64fALdObEjAOBC3iXBsZLS+iQ3KDDQ2NalcydotVqcv5Ar6J9zqn6t6aQuiVb3JSIiIqL2weVLONKGpGDbjt1Yu24jkrokGqf41WoNdu5KBwD06X2t/jltaCoOHMrAxk3bcP89s43ttbV12LE7Hb6+Pujfr4/Vfe2lrq4OpaWlLX7i1GAwQK1SIy8vz6o6bXIOznr/pFIpQkJC4O3t7ehQiIiIHMYpE+iS0jKTWd+qmmoAQOaJLPj5+QIAQkNCkBAfh7jYGEyaMBZr1/+Fjz79GqmDB6CuTond6ftRVFyCUSPS0CEu1jhW96QuGJo6CLvT9+Pzr75H/+S+UKlU2Lp9Fyorq3Dv3FnG5MCavvag0+lQVFSE6Ohok01hbGEwGKDRqOHpKXeqBIws46z3T6PRID8/H7GxsZDJZI4Oh4iIyCEkWVlZBkcH0dju9P1Y8MuSJvukpgzEnFm3G1/v3X8IW7fvxKX8AhgMBkRHRWJ4WiqGDR0sOFev12Pbzj3YtXsvCouKIJPJ0DEhHpOuG4uunRNt7tuc5nYiLCwshJ+fH3x8fKwaV4yzJmBkGWe+f7W1taiurkZERISjQ3Fq3AHN9fEeuj7eQ9fmzDsROmUC7a6aS6Bzc3MRFxdnl4TJmRMwap4z3z+DwYC8vDx06NDB0aE4Nf7D7fp4D10f76Frc+YEmj9NTsbZkiWixvgzSkRE7R0TaCIiIiIiKzCBJiIiIiKyAhNoIiIiIiIrMIGmVvfjgkWYMHka1Gp1k/3WbdiECZOnmfw3/Y45ePzpF7F23UaTvkcyjmHC5Gm4+96HodZoBGNdPX65oBAAcLmgEBMmT8Mtt89GeUWFoP/V40cyjjX79VzIzcPU22Zh89btAIBnX3wFjz/9YrPn2cuzL74i+D41/G/qbbOMfRt+76ura3D3vQ/jxwWL2ixWIiIid+SU60BT+/bBf/+NmOhoAEBFZSU2/LUZ7//vUwDA9RPHm/S9XFCIX5csx1133i46VmM1tbX49vuf8OxTj9kUW21tHV79vzcxbswojBk1wqYx7KFrl0S88a9XRI9JpOIP+fn5+eIff38Bjz/9IjondsKwtNRWjpKIiMhGWhWw92tIosY6OhJRTKCd2Eu/ZSCroMq2kw31y41JJBLAzosmdIv0x3+m97XvoA0EBAQgJCQYABASEowH7p2LPXv3Y+euPYIE+qYp12PRkt8wftxoREU2vy7xTVOux8rVf+D6SRPQs3s3q2P79bflqKiowLw5syzo3XpkMg/j98ganRM74brxY/DZl98iJWUA5C3csIeIiKhVHPgB0vV/R7hXCDD6RWDQPYCH3NFRGTGBdmJZBVU4dKHc0WE4BwNEd2ecetMNOHwkA59/+S3+7x9/a3aY4WlDkJt3EZ989hU++fC/Vq0rWVtbh9+Wr8a0m28w7ohpjXUbNuG35auQd/ESPD090atHN9wzdza6dO5k7LNzVzq+m78A+fmXERUViXvnzsaGvzajrLwC/3vvLauvKebOGbfijz83YN36v3DjDZPsMiYREZHdqKqBbf8FAMiUpcCfLwJ7PgVGvwz0nQE4wZrejo+AqAl1SiUWLVmGywWFmHbzFMFxmUyGxx5+ALv27MXefQcsGvOxh+7DuXMXsGbteqtiOXDoMJRKJYamCne3bM6f6//Cux98jCGpg/D5x+/hP2/8E2qNBs+99AqKS0oBAOcv5OL1t/6LqKhIfPT+23jysQfx8+IlOH3mLDw87LdtdkREOBI7dcSuPXvtNiYREZHdpH8O1BSZtpVfAPZ85qiIBDgDTU7n0SeeA65s1qFUKuHl5YXHHr4fffv0Eu3fr29vjB41HJ9++S2Sk/s2W5YQFxeL6dNuwvfzF2Lk8DQEBgZYFFfG0Ux4KRToltTF6q/p16XLkdy3N+65+y5j24vPPYm75j6Idev/wqyZt+GvzVthMBjw7JOPIjg4CBqNGi888yTue/gJRESEm4yXnXMKN94yU/Ra998zBzdNub7JePr17Y3f/1hn9ddBRETUqmpLgZ0fiR8b/0+nmH0GE2hyRv989SVER0UCAOrqlDh1+jR+XLAIGUcz8dLzT4me8+C9c3HPg49hydIVmDXztmavcefM2/DXlm345vuf8OxTj1oUV2lpGYKCg6zeTrSmtha5eRcxeuRwk/bwsDBEhIfh1OkzAIDz53MRGRGOkJBgGAwGAEB8fBxiYqIFY3bqmIBXX35e9HpBFrwhCAkOhlqtRlVVNfz9/az6eoiIiFrNjg8AVaWwPWE40HmcIyISxQTaiXWL9Lf95FZ+iLAlPvz4c/y1eZvx9cwZ03HnjFuNryPCwxDbIGns0rkTIiIi8OLL/8TY0SMwOGWgYMywsFDcNfN2/LhwEcaPHdVsDN5eXnjwvrn493/ew+RJExAcHNTsOdXVNfC3ofa5trYWAEQTVT8/P9TW1tX3q6sTra0OCgwUtHl6epp8j6zl719/nepqJtBEROQkKi8Be78SPzb+n8ZPp50BE2gn1pKVLgwGAzQaNTw95fVJtBO5e/ZM3DZ9qvG1JQnc1Qftzpw7L5pAA8AtU2/Eug2b8PnX32HaTcJ66cZGjRiGP9ZuwMeffYVX//Zcs/39/HyRf/lys/0a8/WpT1arqqoFxyorq4yz7V4KBQqLikX6VNq04kZTqqpqgCsJPBERkVPY+jagVQqaDUnXQ9LB+uePWpNzFJJQuxIcFITYmGjjfwH+zc9onzt3AQAQFhpito+Hhwceffh+7NyVjv0HDlkUy6MP34ez587jjz83NNs3NDQEZeUV0Ov1Fo19lY+PNxLiO+DosUyT9vzLBSguKUG3pK4AgNjYGBQWFpkk2rl5F5F38ZJV17NEaVkZ5HI5Z5+JiMg5lJwGDv4kaDZAAsNY8X0PHIkJNDmdyspKlJaWobS0DPmXC7Brdzre/9+niIuNwfC0oU2eOyC5L0YOT8Nvy1dZdK34DnG45eYpFvXv26cXlEolck6dFhzT6bTGmBv+d7U8Y8Ztt+BwxjH88NPPuJCbh2OZJ/DWO+8jIMAfk66rr+kaPXIYtFotPvz4c5w9dx4ns3Lw3oefGGeoLbne1f80IrszNnQk4xj69O5p0feIiIio1W16AzDoBM11XW4AIpzv3yuWcJDTefr5vxv/LJfLERkRjmFDUzHjtmnw8lI0e/5DD8zD3v0HodFqLbreXXfejk1btqO4pKTJfgP794O3txd279lnnDW+KufUGcy46x7BOTdNuR6PP/IAJowbDcCAX5euwK9LlkOukKNfn954/pknjKuAdO+WhGeefBQLf/kVjz35POLjO+DhB+7BDz/9DLlcbtH1rnr73//CgP79RI8VFhbhzNlzePyRB5r8eomIiNpE/hEgc5mg2SD1RPWAh+HlkKCaJsnKyjI4Ooj2Iikpqcnjubm56NChg12u5cw10K5s/oJfsGLVGvz03Zc2babSnPKKCvj5+kImk0GjUUMm88CsuQ8gbchgPPHog3a5xocff459+w/h+68/ESTmlrLnz6q70uv1KLh4AZGx8Vav3ELOgffQ9fEeOiGDof6/hn6+HTglLKU0pNyHy30fb/P7l52d3WwfzkATWeH26VOxect2fP/jQrvP4ObmXcR9Dz2BcWNHYcb0adDqNFizdgPKysoFW5jb6tTps1i/YRNefulZm5NnIiJqh0pOA+lfAqVnLD/HoKvfVVBVBair65enU1WLlmoIePrAMOI5oELVorBbCxNoIit4e3vjtX++jCeeeRG9e/XAmFEj7DZ2h7hY/Pu1V/HTwsV4/JkXIJXKkBAfh3+/9iq6dunc4vGrq2vw2r/fxozbb8HwtCF2iZmIiNqB8lzgq9Hi6zO3liEPA36RQMWFtrumFZhAE1kpvkMcVixZ2CpjDxqQjEEDklulBMfPzxc/fveFXcYiIqJ2ZM9nbZs8ewUBaU+03fVswIIgIiIiIhJnMABZf7TtNYc/DXg3v8GZIzGBJiIiIiJxJaeBsnNtd73EMfXlG06OJRxEREREJC5nvXh7QBzg6d30uRIJIPcFFP6A3B9Q+NX/WaYQbsstkQKxA4CkSYBH80vWOhoTaCIiIiISJ5ZAS6TAQ9sBH/O7A7s7lnAQERERkZCqGji/U9gel9Kuk2cwgSYiIiIiUWe3ATq1sL3rBEdE41SYQBMRERGRkLn6567XtXUkTocJNLW6d97/CBMmT8O7H3xsts+/3vgPJkyehh8XLGrT2OxJr9fj5X+8jpf/8ToMBgPWbdiECZOn4UJuXptc/+r1mvpv5650AMDlgkJMmDwNq9f8CQD44uvvcf/DT6K2tq5NYiUiIidnMAA5wu214RcFRPV1REROhQ8RUpvw8vLC9h278fgjD0ChMH26trq6Bnv3HYSXwvqnbktKS3HHXfdizYrFDt+a+rv5C3Du/AV8+ckHdtv8xBYf/PffiImOFj3m5+8n2n7/PXNw4mQ23nnvf/jXqy+1coREROT0Ck8AlSITQF3HC1fQaIc4A01tokvnTpB5yLBrz17BsW07diE2JgqBgQFWj3v8eJZd4tNqtS06Py/vIpYuW4W775oJfzNJalsJCAhASEiw6H9yT0/Rc2QyGR68fy527k7H3n0H2jxmIiJyMmbLNya2dSROiTPQzmzV4/XvAG3kYdDXLzVjbxE9gJvMl2OIkUqlGJKago2btmLMqBEmx/7avBVDhwzGps3bTNrLKyrw1bfzcfDQEVRWViEkOAjDhw3FvDl3QqFQ4McFi/DTz4sBADdMnYEJ48fghWeegE6nw8JflmDDpi0oLi5BcFAgxowagblz7oTnlQTy2RdfgZ+vLxITO2LpslWYN+dODE4ZiHn3P4pXXnoOx46fwKYt26DV6tC7Vw88/cQjCAs1/8Txz4uXIiQkGOPHjrLq+wIA5y/k4pvvf8LRY5lQq9SIjo7CTVOux/UTxxn7FBeX4H+ffonDhzPgKffE+LGj0atHd7zxn3fx/defIi42xurrNtazezck9+uDn37+FYNTBrZ4PCIicmFi5RtSDyBxtCOicTpMoJ1Z4Qkgb59Np0qu/OdMRo8Yhn+89hbKyssRHFS/RWdRcTGOHjuOxx66X5BAv/n2+8jPv4xXXnoWEeHhOHf+At58530AwEP3z8Nt029GnVKJpctWYsH3X8LPzxcA8PFnX2HDX1vw8AP3oH9yX+TknMZHn36JispKPPf048bxz13IhVwhx2cfvYvgoCBU19QAABb8shjjx47Bxx+8jUv5Bfi/1/+D7+cvxPPPPA4xOp0Oe9L3Y9TIYZDJZFZ9T8rKy/HsC68gJiYKb/zrFQQE+GP7zt349ItvoNPpMO3mKQCA1996FxcvXsLfXnwGCfEdsGLVGnz/40IAgIeH/X6Nh6am4Iuvv0dJaSlCQ9r3EkVERO2WsgK4sFvYHj8U8LL+02J3xASa2szAAckICPDH5i3bccvUGwEAmzZvR8eEeHTqlCDo/9JzT0EikSA4uD7ZjogIx6AB/bH/wCHg/nnw9vaGt5cXACA4OAhyuRylpWVYu24jbr91GqZMrv+YKTYmGiWlpfjq2/mYO2eWcSa5qLAIH3/wNgL8/QHAmEB3iIvDjNumAQBioqOR3K8PsrJzzH5dZ8+dR1V1Nfr26WX192Td+r9QVV2Nv73wDKKjIgEAs+64DUePHceKVWsw7eYpyLt4CcdPnMQD996NtCGDAQCPPnQfHn/6RQD5gjEffeI50fq0xE4J+N97/2kynn59e8NgMCDjaKbgkwIiImonTm8CDDphO1ffMGICTW1GJpNh9Mjh2Lhpy7UEess2jB0zUrR/VXU15i/4BSdPZqOmphZ6gwEajQYBAf5mr5GVcwp6vR7JfXubtPdP7gu9Xo8TJ7MwYthQAEBUVKQxeW6oR49uJq/9/f2QnXPa7DVLSssAwKYZ25PZOQgPCzUmz1f17NENBw4eRk1tLc6fzwUAdO+WZNJnSOognMzKFoz5z1dfEowHwFi+0pSQkGAAQOmVr4mIiNohsfINMIFuiAk0talxY0Zh+crfcf5CLvR6Pc6eO4/XR70s6FdXV4e//+N16A0GPPrQ/YiLi4GHhwe++e5HZJ44aXb8miuzyP98/S1IGtZ/GwwAgLKycmOTr6+v6BiC1UAkEhhgMHvN6upqAIC/n/UPD9bW1sFfJIn3uzJWXW0dauvql5bzaxRvcFCg6JgR4WGIjRFfhaM5V7+GqitfExERtTN6vXgCHRgPhHcTO6NdYgLtzCJ62HyqAYDBoIdEIrV/LXQL4uqW1AUdOsRi85bt0OnqH9CLiAgX9DtxMhsFhUV48/V/IGVgf2O7WqNpcvyrCeCLzz2FxE4dBceDbFjpozlXk92rJSDW8PXxwaVLwjKMqsqq+uO+PsaEvqa21qRPRUWVjRGbV9WCNwNEROQGLh8BagqF7V0ncPm6BphAOzMrV7owYTBAq1HD01PudD/w48aMws5de6DRaDD1pimifa4miw0T3sKiYhw+chQ+Pt6C/oYrM8xJXbtAKpWiuLjEWKoBAHVKJcrLK8zOOrfE1dKNkpJSq8/t0T0JO3en43JBIaIiI4ztR48dR4e4WHh7extX2Dh15ix697r25mXPXtseMG3K1dKNq6UcRETUzrB8wyJcB5ra3Lgxo3D6zDlcvJiPEcOHivbp1rULZDIZflu+Cpfy87H/4GG89u+3MWpEGiorq5Bz6jTUGo1x5Y1de/bi/IVcBAcH4fqJ4/HTwsXY8NcW5OdfxsmsbLz+73fw3IuvQqlU2f3r6dQxHv7+fsg4lil6vLKyEqWlZSb/lZXXl5JMnDAOQUGBeOudD3D8xEmcv5CL7+cvxJGjx3Db9Jvrx++UgIT4Dlj86zLsO3AIFy/l47Mvv0VdndLi6139r6aZWfIjGccAAH16W/9AJBERuQGx9Z9lCqATHyxviDPQ1OaiIiPQq0d3+Pn7ij7Ehysrbjz71KP4ccEi3P/wU0jsmICHH7gXgQH+yDiaiZdffQ3vvPUaRo8cjvUbN+O9Dz/B0NTB+PtLz+LxRx5AaEgIfly4CMXFJfDz9UFyv754/5034OVl/W6HzZHJZBiSmoK9+w5Ap9MJlrJ7+vm/C87x8vLC6mW/IDAwAO+9/Qa++nY+/vbq69Co1ejQIQ4vPPsERo1IM/b/x99fwIcff45/vvYW/P39MOX6iZh602R88NHngs1RxK531bgxI/HS80+bPb47fR+6J3Vtcs1rIiJyUzUlQN5+YXvH4YDc/p/gujJJVlaW+aejyK6SkpKaPJ6bm4sOHTrY5VoGgwGaKyUcjtxWur3Iu3gJ9z30BJ558hFcN35si8drfP+UShU0Go3JLofffP8jVq5ei9XLfmnx9QDg+MksPPnMS/j3/73S7EYq9vxZdVd6vR4FFy8gMjYeUik/7HNFvIeuj/fQSkcWA8sfELZPehsY8lCbh+Oo+5edLVzhqjH+NBHZQVxsDG6bPhU//PQLKqvs/3Dfy6++hkeffA5HMo7hckEhtmzdgdVr1uGGSRPsMr5Op8OXX/+AtCGDuQshEVF7lf2neHtX+/xb406YQBPZybw5d6JjQjz+898PjQ812surf38evXv1wL/ffg/3Pvg45i/4BdOn3oh75t5ll/G//u5H1NbW4MXnnrLLeERE5GJ0GuD0X8L20K5AaGdHROTUWANNZCdSqRRvvvZqq4wdHBSEF559slXGxpWt0YF5rTY+ERE5udz0+i28G0ua6IhonB5noImIiIjaO3PlG0mT2joSl8AEmoiIiKi9yxZZvk4RCMQPcUQ0To8JNBEREVF7VnoGKM4StncZC8g8xc5o95hAExEREbVnYrPPANCV9c/mOP1DhLvT92Pp8lVQKlV47dUXEWrBBg8nsrLxyeffAgA+/fBtwXGdTofN23Zi776DKCwqhkwmRVxsDMaNGYm+vXva3JeIiIjI5eSsE2mUcPm6JjjtDHRVdTW++vZHLFy0FHq95UuCKZUqLFz0W5N9vp2/EMtXrkFwcBBm3DoVU2+cDJVKhS+/mY/tO/fY3JeIiIjIpaiqgHM7hO1xKYBvmCMicglOOwP99nsfQ6fT4ZEH5mH9xi3IOX3GovOWr1qD6uoaREaEo6CwSHD8cMYxHMnIxKAByZg3Z6axPTVlAN5850MsW7kGyX17w9/fz6q+RERERC7nzBZApxa2J13niGhchtPOQCd2jMfLLzyFnj26WXxOVvYp7Ny9F5MnjkeAv79on/S9BwAA48aMNGmXy+UYnpYKtVqNA4czrO5LRERE5HKyxco3uHxdc5x2Bvqeu2dZ1V+pUmHhoqXoEBeL8WNH4vgJkadJAZw5dx6enp6Ii40WHEvslFDf58w5jB6RZlVfMu+d9z/Cho2bTdokEglCQoLRtXMi5tx1B7p26Wy2v1QqRVBQIDondsKM26ahX5/exmM/LliEn35ebPbaE8aPwQvPPNFkfD8vXorf1/yJzz5+D0qlCrPnPYgnHn0QN97Q+n95XC4oxOx5DzbZZ9rNU/DIg/cCAO6a+wB6dO+Gv7/0LHbt2Yu3//shPnz3LXS68vNIRERkMb0eyBF5gDAgFojsLXYGXeG0CbS1Vq5ei4qKSjx0/1xIpeIT60qlCtXVNQgPCxXtExwcBAAoKi6xqq+l9Hp9k8cNBoPdt4C+Oq5DGYDAwAB8+ckH15oMBlwuKMCCX5bgyWdewvv//Te6JXUV7a/X61FUXIwFvyzBy6++jk8+fAcdE+KvdK3/2n76/gt4egiX2lEo5E1+/Xv3HcBPCxfjvbffQGBAAOrqCk1ibG1Xr3HfvNkYP3a0yTGNVgNPD094eXmZxGJA/c/J0NQUTJk8Ea++9iY+/+g9+Pn5tnq8DeNu7ue5vbv6/eH3yXXxHro+3sNmXDoMaXWBoNnQdUL9vzsOzh+c+f65RQKdnXMa23fuwQ2TxiMmOspsP6VKBQBQKBSixxXy+nalUmlVX0sVXLzQ5HG1Sg2N5lod0ut738Dp8tMWj99WOgd1xquDX7G4v16vg0Qigb+/aYIXEOCHl194Cnff9yh+W7Eazz/9mNn+gYH+ePKxB3DX3IewZ+8+xMbU32e9TgcA8PP1gVwuF71+w+9pQ1qtFp9+8Q1GjUhDl84dodGoob3SV6fTmj3Pnq5eT6GQC74/DV2NxWAwwKDXG1/ffutU/LnhLyxc9KvVn9q0hFqlbPbnmeoV5ec5OgRqId5D18d7KM7v4K8QK3gtCxkAlRP9He+M98/lE2i1Wo2Fi5YiJjoK140f08LR6t9pSSQSO/etFxkb3+TxvLw8eHpeSwLPVJzB0ZKjFo/fViQSiUmczZFKZZBA/BxPTzliY6JRWlpmPG6uv8eVxdy9FF7X+spkxnGsiQkANm3ZjvzLBXjztVeN53pc+b9M5tHkeDU1Nfj2hwXYtWcvKiurEBQUiBHDhmLenFnw8qp/c6XVavHVt/Oxect2qNRq9OvbG/fNm437H34STz3+MCZPmtDk9TQataBNIpFAIpUa2z095Zh60w1YvGQZ7pxxW5s90CpXeCEyNq5NruWq9Ho9ivLzEB4dZ/ZTMXJuvIeuj/ewaZK1wtXEDB5eCBo0HfD0cUhMDTnq/lWdOtVsH5dPoFesXovSsnK88MxjkF1Jpszx9vICGsxEN3a13cvLy6q+lmru5kskEtOE3PLcvG1JrHvjAIn5c9QaDfIvFyB18KBrx0X6l5aW4fOvvkN4WCjGjRlpPCa58k0SfO8ssHP3XnToEIu4uNhroTYYo6nx/vHaW7h4MR+PP/oAOnVMQHbOKXz06ZcoKSnFqy8/DwD46efFWPX7Wjx431ykDh6EjKOZ+Pd/3gMAeHh4CGJu+OeGJRuN45DA9Ly0IYPx44JFOHDoMMaMGmHV98BWEomE/xhZSCqV8nvl4ngPXYCyAqguFLbr9ZCV50OqUPEeNqasAC4dEjRLOo2EROFcq4s54++gSyfQp86cxbYduzFy+FD4+fmirLzceEyr1QKAsS04KAgKhRyBAf4oL6+AXq8X3IySkjIAQEREmFV9yTZlZeX46tv5qK6uwU2NHtgrL6/AjbfULx2o1+uhVqsRFRWJv7/0HAICAuxy/YyjmRgzarjV5x0/mYWjx47jhWefwPC0IQCA2JhoXL5ciO9/XIjComJEhIdh/cbNSBnYH7dMvdHY5+KlSzh3Xvix2OdffYevvp3fqNUAQIJFP30DX1/z5R2dEzvBz88XRzKOtVkCTUTkcAYDkLMB2Pk/4PxO4yfDDUkBRDgkOBeWxN0HLeHSCXRW1ikYDAZs3b4LW7fvEu3zyr/eAhrsSNilcyccOJSB8xdy0amj6coFOafq15pO6pJodV9qWsOEGAAMBj1UKjU6J3bC6/98Gd27JZn0D/D3x//e/4/xdUVlJQ4dysBLr/wfHrxvLqZMNv0Fn37H3aLXvf+eObhpyvWCdqVShdraWot2tmwsKysHANCndy+T9p49u8FgMOD0mbPw8fZGSUkpplxvGueQwSlYvGS5YMw7Z9yKMaOvJb8GgwFarQYeHp7w9vZuNqaQ4GCUlpVZ/bUQEbkcnQY4tqw+cS7MdHQ07ofbd1vEpRPoQQOTER8vXoe56vc/cSn/Mh66f65Je9rQVBw4lIGNm7bh/ntmG9tra+uwY3c6fH190L9fH6v7toauQV1tP9lQn4RJJBK7l4LYElfjhDg75xTefPt9zLhtGganDBT0l8qkiI25tnxgbEw0enbvBo1Gg8+/+g6jRgwzqff95MN34OEh/HEOChSfra6urgYA+PtZ/zFVbW1d/bmN6o2vjlVbW4vauvo+jVfGCAoKFB0zMDDA5Os1GAzGGmhLSlP8/PxQXVVj9ddCROQy9Dpg37fAro+AilxHR+OeInoBQR0cHYVLcMoEuqS0DOcvXPvlqKqpT3YyT2QZE5LQkBAkxMchMiJcdIy/Nm0DAPTp1cOkvXtSFwxNHYTd6fvx+Vffo39yX6hUKmzdvguVlVW4d+4s44yfNX1bw7/S/mXzudYmYK1NLCHevmM3PvviGwzs38/isozOnTtBrVYj7+Il9Oh+bdY6OirS7CocYvyuJLvV1dYnnb6+9Q9WVFVVw9fn2kMWlZVV9cd9fOClqI+lprbW5NzKykqrr2eJ6upqxIqsV05E5Ba0auDXOUD2WkdH4t56TXN0BC7DKRPo7JzTWPDLEkH74qUrjH9OTRmIObNut2n8O2dMR1xcLHbt3otFS5ZBJpOhY0I8Zs64BV07J9rcl6zz8IP34N4HH8fnX32PF5970qJzrtYPh4VZX3rRkJeXAr6+PigpLbX63KuJ+9FjmYiKvFZdd/TYcUilUnTt2hkBAQEICPDH6dNnTc7dnb6/RXGbU1pWhj69e7bK2EREDrftv0yeW1vMAGDIQ46OwmU4ZQI9NHUQhqYOatEYTz1ufnc3qVSK0SPSLNpB0Jq+ZJ3wsDDMnnUHvvrmB4wdPQIpgwYYjxn0epSWXqvpra6pwZGMY1jy2wqMGzMK4WEtf3izb59eOHrsuOix2to6k+tfFRgYgG5JXdE/uS+++e4n+Pr6IiG+AzKPn8DSZSsxYdxohIbUJ/ejRgzD2nUbsWbtevRP7oMjGZk4eOiIRdczGAzGjVRkHjIEBYqXfgDA6TNnUV1dg759uGsUEbmhiweB7e813cc/Bhh0D+AbatKsNxhQVV4K/6AQSJ3g01inFdwRSBgOeFi3HGx75pQJNLUft9w8BRs2bsb/PvkCX3/+P2NJTHlFJWbcdY+xn6+vD6KjonDfvDmYPGmCXa49bOgQvPvBx7iUn4+YaNPyh2++/xHffP+j4JxPPvwvuiV1wT9feRHffv8T/vfxF6iorERYaAim3nQDZs+aYex7/z1zUFtbh6+++QGQSDA0dRCeePRBPPbU85DLPS26HgCEhYbil5++Mft17N6zD3K5HIMGJtvwXSAicmIaJbDiYcCgEz8e1g0Y9iTQ5zbx5E+vR+3FC/CPjQecbBk0cm2SrKwsB+/z3H4kJSU1eTw3NxcdOtineN/ZaqCdkVarxb0PPYHePXvg+Wceb5Xxq6qrERwUZGzbt/8gXv5H/Xbkxq3LRVh6/+rq6jB73kOYeN043H/PHLt/DebY82fVXen1ehRcvIDI2HinW7+ULMN76AQ2/BPY+aGwXeoBTP8G6HFzk4kx76Frc9T9y87ObrYPf5qo3fLw8MBjD92HTVu24fjJLLuPv+CXXzHr7gewcdMWFBQU4sjRY/jq2x/QtUtndO3S2T7X+PlXeHt7Yebt0+0yHhGR08jdV7/ihpgRz9U/8MakmByEJRzUrqUMGoA5s+7AG2/+F599/F6TtcbWmn3nDEgkEsxfsAglJaUICgpEct/euGfubLu8k961Zy9+/2MdPnz3LcFyeURELk1TB6x4CDDohcei+gIjn3NEVERGTKCp3Zs5YzpmzrD/DK5MJsPdd83E3XfNtKC39dKGDMbK335ulbGJiBzqr9eBklPCdqknMO0LQOYpdhZRm+FnH0REROQ8zu8C9nwmfmz0S0BkL/FjRG2ICTQRERE5B4MBWPNs/Xa6jcUOBIY95YioiASYQDsZnc7MUj1EToI/o0TUas7tAApF1ueXKYCpnwMyVp6Sc2AC7UTCw8Nx8eJFJijktHQ6HS5evIjw8HBHh0JE7ujA9+LtY18Bwru1dTREZvGtnBPx8vJCREQE8vPzYTC0bHlug8EAtUoJucKL60C7IGe+fxEREfDy8nJ0GETkbmqKgeOrhO0+YUAqt5gm58IE2sl4eXkhLi6uxeNcW3w8jovHuyDePyJqdw4vBPQaYXv/WdximpwO/2UmIiIix9LrgQM/iB8bcHdbR0PULCbQRERE5FjntgGlZ4TtiaOBUPvs3EpkT0ygiYiIyLH2m3l4cOC8to6EyCJMoImIiMhxqguBk78L230jgO43OCIiomYxgSYiIiLHObQA0GuF7f3v4pbd5LSYQBMREZFj6PXAwfkiByTAQD48SM6LCTQRERE5xpnNQNk5YXvnsUBwR0dERGQRJtBERETkGOZ2HhzEhwfJuTGBJiIiorZXdRk4+Yew3S8SSJrkiIiILMYEmoiIiNreoQWAQSds7z+bDw+S02MCTURERG3v6FKRRj48SK6BCTQRERG1rYLjQNEJYXuXcUBQvCMiIrIKE2giIiJqW8d+E2/vc3tbR0JkEybQRERE1HYMBvEE2sML6Ha9IyIishoTaCIiImo7lw4BZWeF7V2vA7wCHBERkdWYQBMREVHbMVe+0fuWto6EyGZMoImIiKht6PVA5nJhu6cv0HWiIyIisgkTaCIiImobuelA5UVhe/fJgNzHERER2YQJNBEREbWNzGXi7b2nt3UkRC3CBJqIiIhan04rXr7hFQh0HuuIiIhsxgSaiIiIWt/5HUBNkbC9x42Ah8IRERHZjAk0ERERtT6zq2+wfINcDxNoIiIial1aNXB8lbDdJwzoONIRERG1iIejAyAiIiL3oNXp8e76bKzLvIzSGrWxfYThAD5BuaD/4toBePONTa0ak16vh1Sa3arXoNbT8P6tfXIEYoK8HR0SwASaiIiI7OU/a0/imx3CXQbHeu4AZML+v6lSUWHQtEFkuja4BrWe+vunNxgcHYgRE2giIiJqMYPBgGWHTNd47iApwGOylbhJukvQ/7IhGPsM3dowQiL7YQJNRERELVal0hrLNhIkl/GYbAWmyXbAQ6IX7f+7bggMfBSLXBQTaCIiIgLUtcDWt4GjSwFlhdWn+xkMOKqo/6jdF0pIJU1/3P67bqjNoRI5GhNoIiKi9q7wBLBkHlB0wuYhpAD8JZb1zfIdhM49RqGzxMITbGZAXW0NvH18AbT2tcj+TO+fj9x50lbniYSIiIjalsEAHPge+PNvgFbZ+teTegD9Z6PbdW/gPYVfq19Or9ej4OIFRMbGQypluYirceb7xwSaiIioPaorB1Y/ARxf2frXknoC/WcBw58BghNa/3pErYwJNBERkTs4vhI48ANQXWhZ/6rLQG1xq4akNsggGTAbnqOeBYLiW/VaRG2JCTQREZGr2/pfYPMb9hsvohcQ29+qU9LPluJccS0AwAAgxxCLzdI0/HXTLKDVa52J2hYTaCIiIle2zc7J85BHgPH/AjwUVp32ybfp2H7ZdEY7IcgHEibP5IaYQBMREbmqbe8Cm+yUPHuHAFM/B7pNsun0oiqVoC3cz7oknMhVMIEmIiJyRdvfAza9bp+xEoYD078GAmJsHqK4WiSB9mcCTe7J6RPo3en7sXT5KiiVKrz26osIDQ0R9KmtrcOmLdtx5GgmiktKIZEA0VGRSBsyGGlDUgQfH+l0OmzethN79x1EYVExZDIp4mJjMG7MSPTt3dPmvkRERE3SqoG6MkBV2bJxMlc0UbYhARLSAIkFy37J/YCk64ABdwNSmc3haHV6lFzZhbChCCbQ5KacNoGuqq7GL4uXIePYcXh6eprtV15RiXc//BQVFZVITRmAsaNHoK6uDjt2pePnxb+hoLAQt9w8xeScb+cvxJGMTPTu1QNjRg2HVqvFzt3p+PKb+bjjtmkYMWyITX2JiNq1wpPAvm+AopOOjsQuJABCVEpIFF4tG0hZUb9kXF0poK62V3jm3fQxMGB261+ngZIaNQwiGw9yBprcldMm0G+/9zF0Oh0eeWAe1m/cgpzTZ0T7rfr9T5SVleO2W27C6JHDjO1DBg/Ca2+9i01bdmD82FEI8PcHABzOOIYjGZkYNCAZ8+bMNPZPTRmAN9/5EMtWrkFy397w9/ezqi8RUbtWcBz4/npAWe7oSOxGAsDl0j8HJM8wU/8MJtDkxpxrW5cGEjvG4+UXnkLPHt2a7BccHIjkfr2RNiTFpN3HxxudO3WEwWDApfwCY3v63gMAgHFjRpr0l8vlGJ6WCrVajQOHM6zuS0TUbul1wMpH3Sp5dkk3/g8YMMchl2YCTe2N0ybQ99w9C/5+zc/s3jh5Iu6fNxtyuVxwrLauDgDg4+1tbDtz7jw8PT0RFxst6J/YqX53pDNnzlndl4io3Ur/Erh00NFRtG9TPgQGznXY5QurxLcBj/BvYfkLkZNy2hKOlrp4KR+nTp9FRHgYOsTVP1WsVKpQXV2D8LBQ0T3Vg4ODAABFxSVW9bWUXq9vwVdknavXastrkv3w/rm+dnMPyy9AsukNcKVfxzDIFDBM/i/QfzbgwJ+1wkrxBDrU19OhvwPt5vfQTTnz/XPLBLqsrBxfffsjJBIJ7pwx3bgKh1JV/xGTQiH+kZJCXt+uVCqt6mupgosXrPxKWq4oP6/Nr0n2w/vn+tz6HhoMCF7/GLw0NYJDenkAdH7CT+/aI4OHN/RegdAr6v8zKAKhl/tbtkpGM+OqowZA5x8LOODfl4bOXxZOJkkAaMsLUFDl+LdXbv172A444/1zuwT6/IU8fPnND6ipqcXc2Xega5dEK86uf4TYsl2TrOlbLzI23opYWkav16MoPw/h0XGiM+jk3Hj/XF+7uIfHlkKat0P82B0LIes4vK0jsqt2cQ/tpMYgTKBDfOWIjU9wSDxX8R66Nkfdv6pTp5rt41YJ9P4Dh7Fg0VLI5Z549KF7kdS1s8lxb6/6Wqyrs8uNXW338vKyqq+lHPHLK5VK+ZeGC+P9c31uew9rS4E//yZ+rP9sSBNHih9zQW57D+1IdBdCf4XTfN94D12bM94/t0mgN27aiuWr/kBMdBQevO9uhIlsuKJQyBEY4I/y8gro9XrBzSgpKQMARESEWdWXiKjdWf8qUFssbPeNAK6z0+545DKKuAshtTPOlc7baNuO3Vi+6g/06NYVzz75iGjyfFWXzp2g1Wpx/kKu4FjOqfq1ppOulH1Y05eIqN04sxU4vED82OR3AO/gto6IHMzcDDSRu3L5BPrM2XNYsmwVOid2xIP3z4WXV9O/sGlDUwEAGzdtM2mvra3Djt3p8PX1Qf9+fazuS0TULug0wO9Pix9Luh7oObWtIyIHq1ZpUavWCdq5hB25M6cs4SgpLTOZ9a2qqd/6NPNEFvz8fAEAoSEhSIiPw5Jlq6HX69GnVw8cPXZcdLzoqEhER0UCALondcHQ1EHYnb4fn3/1Pfon94VKpcLW7btQWVmFe+fOgveVdaOt6UtE1C5k/AqUnha2y/2AG94FrHiwmtwDN1Gh9sgpE+jsnNNY8MsSQfvipSuMf05NGYg5s27Hhdz6pU1WrF5rdrzJE8fjhusnGF/fOWM64uJisWv3XixasgwymQwdE+Ixc8Yt6NrZtCTDmr5ERG5NrwN2fCB+bNw/gcC4to6InAATaGqPnDKBHpo6CENTB1nU99MP37Z6fKlUitEj0jB6RJpd+xIRubUTq4CSHGF7RE8g5V5HREROwGwC7ccEmtyXy9dAExFRGzAYgO3viR8b8SwglbV1ROQkzG7jHcAEmtwXE2giImpezgbg8lFhe3AnPjjYzrGEg9ojJtBERNQ0gwHY/q74seFPAzKnrAakNiKWQCs8pPBX8OeC3BcTaCIiatr5XUBuurDdPwbod4cjIiInYm4TFQlXZCE3xgSaiIiaZm72Oe1xwIMf07d3hZXCBDqC5Rvk5phAExGReRcPAqc3Cdt9QoGBdzsiInIy3Mab2iMm0EREZJ65lTeGPAzIfds6GnIyOr0BJUygqR1ihT8REQnp9cDJ3+v/a0wRAKTc74ioyMmU1KigNwjbw/24jTe5NybQRER0jVYNHFsK7PwIKDoh3iflPsA7qK0jIydkbgk7rgFN7o4JNBFRe3P5GHD6L0BVZdquVQJHfwOqLpk/18MbGPJIq4dIroG7EFJ7xQSaiKi9qC4E1jxbvyW3rQbeDfiF2zMqcmHcRIXaKybQRETuzmAAMn4F/nwRqCuzfZywbsDov9kzMnJxhSzhoHaKCTQRkTurvAT8/jSQ/aftYwTEAkMfBQbOA+Q+9oyOXJy5GehQXybQ5N6YQBMRWSJjCXD0V6Aq36LuEgBhajUkcnmrh9ak0nOAusqCjiIiegLDngR6TwdknvaOjNyA2BrQwT6ekHtwlVxyb0ygiYias/dr4I/nrDpFAsAlU06ZAkgYCgx9DOgyHuB2zNSEIpFdCFn/TO0BE2gioqaUnQPWv+LoKOyv51Rg4pvC5ehkCkDGfxrIMmIz0BH+XAOa3B//liQiasral+qXd3MXvhHADe8CPW92dCTkBsRqoDkDTe0BE2giInNO/gFkr3V0FPbT9w5g0luAT4ijIyE3UKvWolqlFbQzgab2gAk0EZEYdQ2w9kXxY6FdAf+oJk83GAxQq5WQy70gcXQdcVhXoNctQKcRjo2D3Ao3UaH2jAk0EZGYbe8CFReE7T6hwL3rm53FNej1KL14AZGx8ZBIuSIBuR9u403tGf9WJyJqrCgL2PWx+LEJr7EEgogz0NTOMYEmImrIYKjf7lqvER7rMATod6cjoiJyOmIrcIA10NROsISDiNxXXRlQfAoozgZKTtXXNTentgQ4t13YLpEBU94HWI5BBAAoFFkDGlzGjtoJJtBE5NzyM4D93wJVBZafo6wASnKAmiL7xTHkYSCyl/3GI3JxYiUccpkUAd5MLcj98aeciJyTwQDs/QpY9zKgFy6V1ab8Y4DRLzk2BiInI1bCEe6vcPyqM0RtgAk0ETkfjRL4/WngyM+OjqTepLcAhb+joyByKoVVwg2Gwlj/TO0EE2gici4VecDiu4BLhxwdSb2k67lrH5EIsRKOCCbQ1E4wgSYix6i6DFQXmrZV5AGrHgdqi1vpohLA0o+XvUOAbtcDk/9r+TlE7YReb0BxtVrQzhU4qL1gAk1Ebev87vq65ksHrT83uGN9EtwcmScQlACEJQFhXer/H9oV8ItgMkxkB2W1auj0BkE714Cm9oIJNBG1DVU18Ndr9Q8GQvgPb5MUAcAtX9XPCBORwxWa20SFM9DUTjCBJqLWd2ZrfWlG+Xnrzw1LAu74GQjr2hqREZENzG7jzQSa2gkm0ERkH7l7gQt7AF2jf1iLTwEZi2wbs9tkYNqXgFeAXUIkcgc6vQF7zpTgSF459CJlFG3hxOUq0XbOQFN7wQSaiFpu81vA1v/YbzypBzDyBWDk89z5j6gBnd6Apxcfxqojlxwdiigm0NReMIEmopYpzwW2/df68zy8gGFPXXkwsGG7HIgdBAQn2C1EInexYM95p02ewQSa2hEm0ETUMjnrAYPOunPi04CbPwFCO7dWVERup6hKhXfXZzk6DLPC/BRQeMgcHQZRm2ACTUQtc3ar5X09fYHx/wJS7mNpBpGV3lp7AlVKB29r34Tre0c5OgSiNsMEmohsp9cDZ7c330/uB3S9Dhj/T2HJBhE1a+/ZUiw7eNHRYYhSeEgxsVcUXp7cw9GhELUZJtBEZLuCo0BdqbA95T5gzN+vvZb71dc2E5HVtDo9/rHymOixG/pG4/Wbe7d5TA35KmQs3aB2hwk0EdnuzBbx9q7XAT4hbR0NkVuav/s8ToosG+crl+HVG3oixJdvTonaGosQich2Z0Tqn6UeQEKaI6IhcjuFlUp8sCFb9NhT45MQFejV5jEREWegichWWjVwYbewPXYgoPB3RERENtHpDbhUXido1+v1KK5UQ+1dC6mDHnp9+8+TqFYJHxxMivTD3GF8noDIUZhAE5Ft8vYBmlphe+JoR0RDZJOfdp/D239miSap1+S0YUSWee3m3vCU8UNkIkdhAk1EtjG3fF2nUW0dCZFNjuZV4NWVmY4Ow2pTk2MwJDHU0WEQtWt8+0pEthGrf/b0AeJSHBENkdVWZzjvjn7m+Cs8uFwckRNgAk1E1lNVARf3C9vjh3K5OnIZJ/IrHR2C1Z6ekISIAD44SORoTKCJyHrndwF6kZpR1j+TCzmRL1wazpndnByDOUMTHB0GEbEGmohsIla+AQCJrH8m11BUpUJxtUrQPqJrGMZ1jwAAGAwGVFWUwT8wGBKJxAFR1pNKJegTG4h+cUGQSh0XBxFd4/QJ9O70/Vi6fBWUShVee/VFhIYKN2fQ6XTYvG0n9u47iMKiYshkUsTFxmDcmJHo27tni/pbOzZRuyD2AKF3CBDZxxHREFktS2RjEgC4sV8Mbh/UAbiyjF3BRSkiY+MdtowdETmnFv+NYDAYUF1dA51OZ5+IrqiqrsZX3/6IhYuWQq83NNn32/kLsXzlGgQHB2HGrVMx9cbJUKlU+PKb+di+c0+L+ls7NpHbqy4CCkS2Fe40AmCSQS7i5GXx+uceUQFtHgsRuR6bZ6CLikuw6vc/cfxEFtQaDZ549H507ZwIAFi28ncMG5KKyMhwmwN7+72PodPp8MgD87B+4xbknD4j2u9wxjEcycjEoAHJmDdnprE9NWUA3nznQyxbuQbJfXvD39/P6v7Wjk3ULphbvo71z+RCxOqfpRKgayT/Piei5tk0XVRSUor/fvAJDh05Cm9vLxgM12aIq6qrsWXbLnzw8RcoLim1ObDEjvF4+YWn0LNHtyb7pe89AAAYN2akSbtcLsfwtFSo1WocOJxhU39rxyZqF7j+M7kBsRnoTmG+8PKUOSQeInItNiXQf27YBJ1WhyceuR8vv/C0yTF/Pz8899Qj0Op0WL9xs82B3XP3LPj7NT8TcObceXh6eiIuNlpwLLFT/dPKZ86cs6m/tWMTtQtiDxAGdgBCEh0RDZHVtDo9cgqqBe3do1m+QUSWsamE42TWKQwfNgRJXTujrq5OcDy+QxxGpKXiwKEj9ojRLKVSherqGoSHhYo+4BEcHARcKTextr+1Y1tCr9db8dW1zNVrteU1yX4ccv8MesDQ9PMGKL8Aafl54amdRtZ/EtXc+e0Ifwed1+nCKqh1wvvSPdLP5H7xHro+3kPX5sz3z6YEurKyErExUU32iY6OQuW2HbbGZRGlqn4JIoVCIXpcIa9vVyqVVve3dmxLFFy8YHFfeynKz2vza5L9tMX9k1XmIXDXG5Dn74dEr7FpjPKg3lA64OfbFfB30Pmk51SItkfJVaJ/T/Meuj7eQ9fmjPfPpgRarpCjtlY489xQRUUF5HJH70hWPxtm+fqd1vS3dmwgMjbe4r4tpdfrUZSfh/DoOC6/5ILa7P4VZUGy9h5IqgtaNEzggGkI9Iu0W1jugL+Dziv/WJZo+5BeiYgM8ja+5j10fbyHrs1R96/q1Klm+9iUQCfEd0D6vgMYNSJN9HhxcQk2bd2BhPgOtgxvMW+v+u1Mr84WN3a13etKP2v6Wzu2JRzxyyuVSvmXhgtr1ftXeAL48Uagpqhl44T3gDRA+JwA1ePvoPM5KbIGtL+XB+KCfUQnRXgPXR/voWtzxvtnUwI9bswIfPrFd3j/oy/Qr0/9ZiI5OWdw6dJlnDl3HoePHINerxesXmFvCoUcgQH+KC+vgF6vF3xzS0rKAAAREWFW97d2bCKXUpAJzL8JqC1u+VhJE+0REVGbEUuge0QFOHS3QSJyLTal8z26JWHm7bcg7+JFrFi9FgDwx7qNWLJsFQ4cPAKpVII7bpuG7kld7B2vQJfOnaDVanH+Qq7gWM6p+rWjk7ok2tTf2rGJXMLlo8APU+yTPAfGA0MftUdURG2ivFaN/Arhsyvdo/0dEg8RuSabN1IZNnQw+vXthYyjmbiUXwCVSgUvLy/EREehb++e8PX1sW+kZqQNTcWBQxnYuGkb7r9ntrG9trYOO3anw9fXB/379bGpv7VjEzkNnQY4uw0oOgnoG+wSqtcCuz4C6srEz4sdCCSIl2YJBHcCek0DfELsEzNRGxCbfQaA7tyBkIisYHMCDQB+vr5IGzLYftFcUVJaZjLrW1VTv15n5oks+Pn5AgBCQ0KQEB+H7kldMDR1EHan78fnX32P/sl9oVKpsHX7LlRWVuHeubPg7X3toRBr+ls7NpHD1ZYCB34A9n4NVF2y7tyOI4A7FwNy39aKjsjhTuaLb+HNGWgiskaLEujm6HQ6yGTW7+qUnXMaC35ZImhfvHSF8c+pKQMxZ9btAIA7Z0xHXFwsdu3ei0VLlkEmk6FjQjxmzrjFuL14Q9b0t3ZsIocoPAmkfwEcWQRom14hR1TiaOCOXwB523xyROQo5magu0UygSYiy9mUQD/29EsW9ZNIJPj4/besHn9o6iAMTR1kcX+pVIrRI9Iw2syqIC3pb+3YRG2qpgT4/SngxCrbx+g8FrjjZ8CTn6aQ+zshkkAnhPrAV9Gq80lE5GZs+hsjOCgIYg8ra3U6VFbW/+UUGxMNb2/Ll3gjIhusfBTIXmv7+V0mADMWAJ78XSX3p9MbkC2SQHeP4uwzEVnHpgT69X+an4HWaLXYtXsvtu3cjbmzZ7YkNiJqSslp25NnqScwYA4w6S3AQ3y3TSJ3c6G0FnUanaCdDxASkbXs/pmVp4cHRo1IQ12dEstXrsG8OUyiiVpFxq9NH+88Dkh9CIgSWSnGJ4SJM7U75h4g7BHNBJqIrNNqRV9JXROxeduO1hqeqH0zGICMxcJ2iax+Zjn1ISCiuyMiI3JaYvXPANCDK3AQkZVaLYGuqa2DWq1preGJ2reLB4Cys8L2pEnAjR86ICAi53dCZAbaRy5Dh2CuPkNE1rEpgS4tM7MJAwCtVoeCwiKsWP0HwsNCWxIbEZkjNvsMAH1vb+tIiFzGycvCBLpblD+kUm7hTUTWsSmB/sdrb1vU766Zt9kyPBE1RacBji0TtisC6megiUigSqlBbqlwjXQ+QEhEtrApge6S2Akw84ZdJpMhKDAAyf36oE+vHi0Mj4gETm8GaouF7T1v4nJ0RGZkF7D+mYjsx6YE+qnHH7R/JERkmaNmVt/ow/INInNO5Isn0JyBJiJbSB0dABFZQVUNnFwjbPePBjoOd0RERC5BrP4ZV2qgiYisZdEM9B9/brRpcIkEuH7ieJvOJSIRJ9cAmlphe59bAanMERER2dWu08X4ZW8ucsyUXNgqr0xY/xwb5I1Ab0+7XoeI2gfLEuh1tiXQABNoIrti+Qa5sYMXyjD3+31Qa/Vtcj1u4U1EtrIogeZqGkROoLoQOL1J2B7eQ3y3QSIX8+81J9oseQZ3ICSiFrAogR4yeKDVA1dWVaGy0r4fwRG1a8eWAQaR5KLvbfX1UkQu7NCFMhw4b36PgdaQ3CGoTa9HRO6j1XYizDx+Eit//xP/ef3V1roEkeszGIDSM0BdeaN2PTyLLgMoAiRXnvU98rP4GH34CRG5vm93iOys2YqSIv0wult4m16TiNyHzQl0eUUl9uzdj9LSMuh0prNiGo0GWdmnoNVp7REjkXsqOwcsmgUUHBMckgIIs2SM+DQgKL41oiNqM3lltVh77LKg3Ucus/sssUwqQZ/YQDw4sjM8ZFyIiohsY1MCfbmgEO9/9Dlqa4VPNTc0fsxIW+Micn9rnhNNnq3Sl7PP5Prm7zoHnd4gaJ89NAF/u54bchGR87Epgf597XpoNBrccP0EhIWGYP6CxbjphkkIDQ1Gds5pZBw7jrvvugPdk7rYP2Iid1BdCJza0LIxpJ5Az6n2iojIIapVWizamyto95BKMDeto0NiIiJqjk0J9KnTZzF65HBcf9041NXVz0J36hSPrp0TMbB/P6QM6o+vv/sJTz32IKKjIu0dM5Hry2lh8gwAPaYAPiH2iIbIYX7dl4sqlbDc74a+0YgO9HZESEREzbKpAKy2tg6x0VH1L648/W9o8PFbl8ROGNi/H1b+/qedwiRyMznrWnZ+aBdgwuv2iobIIXR6A77bKf7w4L3DO7V5PERElrJpBtrHxxu1V2ae5Z71uzhVVVWb9OmY0AH7Dx62R4xE7kWnAU5vFrYHdwLGvgIA0Bv0qCgtQWBIKKSSRu9zfcOBDoMBT87OkWtbn3lZdIfAwR1D0DeOS8wRkfOyKYGOiojAnr0HMHjQAHh5KRAY4I/DGccwcEA/Y5/ComLoRR4KIWr3LuwGVJXC9h431m/JDQB6PZQXLyAwNh6QcqUAck/fmFm67t4RnH0mIudmUwI9NHUQfvplCb7+/ic8/vB96N2rB3bu3osvvpmPrp07oaS0DLv27EPH+A72j5jI1WWbKd9ImtjWkRBZpEqpQUm12q5jniqsFt04JSHUB+N78NkZInJuNiXQqYMHoqyiAhUV9bNoU66/Dtk5p3Es8wSOZZ4AAHh7e2HazTfYN1oid5CzXtimCAQ6pDoiGiKz6tQ6PLX4EDaeKBRdZq413DOsE2RS7qxJRM7N4gT6tTffxagRaUhNGQgvLwUmTRhrPObv74eXX3waGRmZKCktRWBgIHr17AY/X9/WipvINZWeAYqzhe1dxgIyT0dERGTWC79lYF1mQZtdL8DLA7cOjGuz6xER2criBLqwqBhLlq3Cqt//xOCUARgxbAhirq7EAcDTw8OkBpqIRGSLzD4DQFeWb5Bz2XOmBKuPXGrTa85MjYevwuYNcomI2ozFf1P97fknsX3nHuw/cBjbd+7B9p170KVzJ4wcPhTJfXtDygediJonunydBOg6wQHBEInT6Q34v9XH2/SacpkUdw/lxilE5BosTqBjY6Jxx23TcMvNN2D/wcPYsSsdp06fxanTZxEY4I/haalIG5qKwAD/1o2YyFWpqoFzO4TtsQMB3zBHREQk6tf9uTiRL7JSTCv67219ERPEpRmJyDVY/VmZXC5H2pDBSBsyGHkXL2H7znTsP3gIa/7ciD/Xb0K/vr0xcsRQdEnkMkREJs5uBXQiKxkkTXJENESiKpUavLsuS/TYHSkd0CsmwK7X85F7YHjXMEQGeNl1XCKi1tSiYrO42BjMvH0apk+9AfsOHMKOXXtx8HAGDh7OQHRUJEaNSMPwNK4sQAQ0tXzddW0dCZFZH/+Vg5Ia4Ru9xDBfvHZzb8g9WK5HRGSXpzXkcjmGDU3FsKGpuFxQiH37D2HT1u1YvHQFE2giADAYgJwNwnb/aCCqryMiIhI4U1SNH3adEz326pSeTJ6JiK6w6+PO5y/kYe/+gzh0OAMajRaenlyWiwgAcPkoUCWyokHXCYCEa96Sc/j3mhPQ6ITrPY9KCseY7hEOiYmIyBm1OIFWqdRXyjfSkXexPkEIDwvF9HGjMWTwQHvESOT6RFffAGo7jkd6ViEqajUm7XqDHpWl5QgokkEq4ayfK3K1e3ipog5/nSwUtMukErw6pYdDYiIiclY2J9C5eRexY1c69h88DJVKDYlEgj69emDkiKHo0S3JvlESuTqR9Z/1UjkmrpQit2ZfEydebNWwqC249j2cMzQBXSK4uhIRUUNWJdBqtdq4hN2F3Pp/FPx8fTFyeBpGDEtFSHBwa8VJ5Fz0OmDjv4CjS4CqfJuGOOrRC7mVzj8zSe1XsI8nnhrHCREiosYsTqAXL12BffsPQalSAQA6JnTAyOFpGJDcBx4e3DmK2pkN/wB2f9KiIVbU9LZbOESt4ZkJSQj04bMsRESNWZz5bt+5B56eHhiaOggjhw9Fh7jY1o2MyFkVngD2fNbiYf7SD7BLOEStoXuUP2YOjnd0GERETsniBHraTZMxNDUFPj7cKYraufWvAAZ9i4Yo9OmCC8pIu4VEZE/do/zx3dwUeMhYYkREJMbiBHrcmJGtGwmRK8jZCJza2LIxZHJ8rZgraPb38sCCe1MhkQB6vQGlRZcREh4FqZTL3LkiV72HwT5ydAjxcXQYREROjcXLRJbSaYH1fxc/ljgGiOjZ/Bg+wdB0uR4/fpYLwHQWe0B8MPp1CAIA6PV6FEgqEBkbCKmUs4CuiPeQiMh9MYEmstTBH4Cik8J2n1Dg9vmAV6BFwxy7UAaV9rygfVACV7EhIiJyBZwWIbJEXTmw+U3xY2Netjh5BoAD58tE2wcygSYiInIJTKCJLLH9PaC2RNge3gMYIKxnbsrBC8IEWiaVGMs3iIiIyLm1aglHXV0dvL25agc5KYMB0Gma71d+AUj/QvzYxDcAmeW/RgaDAfvPCRPoHtH+8FWwooqIiMgV2PQv9qdffIvZs25HgL/57V1PZuVgwS9L8Ma/Xm5JfET2p9MCm14DDswHlOW2j9NlfP1/Vsgrq0NhlUrQPjCe5RtERESuwqYSjhNZOfj32x/g8JFjgmNarRZLl6/Gp19+B5VabY8Yiewr/XNg5/9aljxLZMB1/7b6NLP1zx1DbI+FiIiI2pRNCfSjD94DhUKBb35YgJ9+/hVKZf2MWm7eJfzn3Y+wZdtO9OzRDX9/8Rl7x0vUMjoNsOvjlo8zcC4Q0d3q0/gAIRERkeuzqYSjR/ckvPLiM1j9xzps2bYTOafPIrlvb2zdthMKLwXmzLodgwe13TbF5RWV2PDXFpzMykFpWRm8vLwQGR6OkSOGon+/PpBIrm1ioNPpsHnbTuzddxCFRcWQyaSIi43BuDEj0be3cB1fa/uTkzu5BqguaNkYisD6lTdssF8kgY4O9EJsEJ8VICIichU2P7Ukl3ti+tQpGNi/Hz7+7Gts2rIdMdFRePyR++Dv52ffKJtQUFiE9z78DGqNBsPTUtEhNgZ1KhX27juIb39YiOFpQzDz9mnG/t/OX4gjGZno3asHxowaDq1Wi5270/HlN/Nxx23TMGLYEJPxre1PTm7/dy07X+oBTP0M8A2z+tQqpQZZlysF7QM4+0xERORSWvTY/8VL+fj1t5VQqdUICgzApfzLWL7yD9w67Ub4+LTNjNq6DZtQU1srSGaHDUnB62+9hx279mDC2JEICwvF4YxjOJKRiUEDkjFvzkxj39SUAXjznQ+xbOUaJPftDX//+jcA1vYnJ1d8Cji7VdgeEAv0ua35831CgC4TgEjbPnk4klsBvUHYzgcIiYiIXItNCbRWq8XadX9h46at8Pb2xgP3zkGvHt2w5s8N2PDXVpzMzsEdt01rkxKH4pJSAEDnxI4m7Z6enojvEIeS0jIUl5YhLCwU6XsPAADGjRlp0lcul2N4WiqWr/oDBw5nYPSINACwuj85uQPfi7enPgQMe6LVL7//fKlo+6COTKCJiIhciU0PEb75zodYt3EzundPwt9ffBp9e/eETCbDTTdMwtOPPwRPT0989e2P+P6nX+wfcSPRUVEAgMLCYsGxktJSSKVSREWEAwDOnDsPT09PxMVGC/omdkqo73PmnLHN2v7kxDR1wOGFwnaZAkie1SYhiD1A6O0pQ4/ogDa5PhEREdmHTTPQZeUVuOO2aRielio4ltgpAX9/4SksXb4au/bsw7zZM0XHsJfrxo/G0WOZWLp8FSQSCRISOkClVGHnnnRcyL2ICeNGIygoEEqlCtXVNQgPC4VUKnzfEBxcvwtcUXH9bnPW9reEXq9vwVdqnavXastrOrXM5ZDWCRNYQ8+bYfAOBlr5+6TTG3BIZAfCfnGBkEmE94n3z/XxHro+3kPXx3vo2pz5/tmUQP/t+ScREW7+ISq5XI7pU29E1y6JLYnNIqEhwXj+mcfww0+L8NV3PxrbPT09cOu0GzFm1HAAgFJVv9SeQqEQHUchr29XKpU29bdEwcULFve1l6L8vDa/pjMK3fUl5CLtJQk3QNMG9+VUsRLVKp2gvVuItMmfC94/18d76Pp4D10f76Frc8b7Z1MC3VTyfNXhjGNYveZPpAzsb8slLFZcXILPv/4BVVXVuHHyRMTFRkOpVOHI0UwsXb4axSWluO2WmywYqf7proZL3tm3PxAZG29x35bS6/Uoys9DeHSc6Ax6u1JwDNLCw4JmQ0RPhPS/EbDiHtpqQ+550faRvRMQGRshaOf9c328h66P99D18R66Nkfdv6pTp5rtY/MqHGq1BkeOHkNJaRn0OtOpdY1Gg0NHjqKmts7W4S224JelKCgswvNPP4qE+A7G9kEDkyH/WY4t23aia+dE9OieBDSYWW7saruXlxcAwPvK/y3tbwlH/PJKpVL+pXHgB9FmyaB7IJHJ2iSEQ7kVou0DE0KavD+8f66P99D18R66Pt5D1+aM98+mBLq8ohIffPQ5SkrFd1W7atCAZFvjsohSpcKpM2cRGhJskjxf1bdPT+zZux8nsrKR3K83AgP8UV5eAb1eL7gRJSX1X0tERP3sukIht6o/OSlVFZCxWNju6Qv0ndFmYYg9QNg1wg9BPmKFJUREROTMbEqgV69Zh9KycgxNHYTwsFCsWrMOo0ekITAwAFk5p3EhNw+z7rgV/fr0sn/EDWg0GhgMBmi1WvHjag1wZdk9AOjSuRMOHMrA+Qu56NQxwaRvzqkzAICkBnXb1vZ3ZlVKDX7dn4eMvHLoxBYjdjFeuhqEaS41269XzR7cqK4WtO/wGYNFy5r/iMYeDAbgQmmtoJ3bdxMREbkmmxLok9k5GDFsCG6ffjPq6uqwas069OvXG107J2LCuNE4cPAIfv1tJRI6xCEoKND+UV/h7+eHiPAwFBYVIzvnNJK6djY5fvBwBtBgjei0oak4cCgDGzdtw/33zDb2q62tw47d6fD19UH/fn2M7db2d1YqrQ53fbsXR3LLHR1Ki0mgx789vsNtsq3wlAgfyrPUW4VpyCzIt2ts1uIOhERERK7JpgS6uqoaHROuPBB35QEsQ4NZzYED+uH4ySysWL0Wc2ffYadQxd16y0348pv5+PzrHzA8LRWxMdFQqVTIOHYcJ7NykNgpAakpAwEA3ZO6YGjqIOxO34/Pv/oe/ZP7QqVSYev2XaisrMK9c2fB2/vaDorW9ndWe86UukXyDAAPy1bhTo9NLRrjkL4LMg0dLejZugYxgSYiInJJNiXQXl5e0GjqyyM8PeqHqG30wGBSl85YvvoPe8TYpF49uuH5px/Fxk3bcPDQEWzZthMeHh6ICA/DzVMmYcyo4ZA1eFDszhnTERcXi12792LRkmWQyWTomBCPmTNuQdfOwnIMa/s7o+zLVY4OwS6CUIWHPFa3eJyFunF2iaclwvzk6BTm6+gwiIiIyAY2JdChoSE4dDgDw4YOhoeHB3x9fYwP6l1VWV1trEFubR3iYjFvjmUbtkilUowekWbx9tvW9ndGKq3tpQ7O5EGP3xEgadnKLiUGf/yuG2K3mGx115AEq5ZAJCIiIudhUwKdMjAZv634HV98Mx8P3Xc3uid1xa49++Dh4YGunRNRUlqK9Rs3IzIy3P4Rk9VUWvEdfML9FXCVFC7UUIZ52nUtGqMOCvzb41EEyAPgqM2zQ3zlmNI3Gg+P7uKgCIiIiKilbEqgR48chvzLBVAq69dCnjL5OpzMzsHW7buwdfsu4MrM7Y2TJ9o3WrKJuQR690tj4SFzrnUVzVr7EpCuFrZ3HAH0v6v58z194N1hMN73j2qV8IiIiKj9sCmBlkgkuHPGdOh09aUB4WGheOXFZ7A7fT9KSksRFBiAAcn9OAPtJFQaYQmHTCpxneS5Ig/Y/634sUn/AaJ6ix8jIiIiagU270QIwOThPH9/P1w3frQ9YiI7E5uBVni4SPIMANveBXQis8+9bmHyTERERG3OhbIospVLJ9ClZ4FDPwnbJVJg9N8cERERERG1cxbPQP/v069susCTjz5g03lkP2rRBFom2tfpbH0H0IvsNNn3DiA8yRERERERUTtncQJ9detqcj1iy9gpPF1gBrooG8hYJGyXegCjXnBERERERESWJ9BeCgXUGg0SOyWgb++eGNi/HwIDHbUYGFlDrIRD7goPEG55EzCIrCAyYA4Q0skRERERERFZnkC/9forOHTkKHan78eylWuwfNUf6Nk9CUNSB6Fv754mDxSSc1FpREo4nH0GOj8DyFwubJcpgBHPOSIiIiIiIsCaBNrT0xODBw3A4EEDUFxSit3p+5C+7yC+/WEhfH18kDKoP4amDkJsTHTrRkxWEy3hcPYa6E2vi7en3AsExrZ1NERERERGNi1jFxYaghsnT8SU66/D8ZPZ2J2+Dzt27cGWbTvRIS4GQ1NTMGhAMnx8vO0fMVnN5VbhOL8byFkvbPf0AYY/7YiIiIiIiIxatA60RCJBrx7d0KtHN9TU1GLv/kM4cPAwlixbhWUr16Bf316YN3um/aIlm7hUAm0wAH/9n/ixIQ8DfhFtHRERERGRCbtlUb6+PkgZmIyhQwYhIT4OWq0Whw4ftdfw1AJiOxE6bQlHzgbgwm5hu1cQkPaEIyIiIiIiMtGiGeirMk9kYdeevTiWeRI6nc5Y4jFk8EB7DE8tJDoD7YwPEer1wF+viR8b/hTgHdTWEREREREJ2JxAl5WVY9eefdi9dz/Kyyvg4eGB/sl9kJaagqSune0bJbWI+EYqTphAZy4DCkQ+tfCLAgY/6IiIiIiIiASsSqB1Oh2OHM3Erj37kJV9CgaDAXGxMbhu3GikDEyGtzcfGnRG4jXQTlbCodMAm94QPzbqeUDu09YREREREYmyOIFetnIN9u47iOqaGvj5+WLEsCEYMngQ4jtwSTFnptcboNaJbKTibDPQh34Cys4K24M7Av3nOCIiIiIiIlEWJ9CbtmyHTCpF1y6J6JzYEVKJFMcyT+BY5gmz50gkwPUTx9srVrKBWPIMe5dwFGQCx1cBNYW2j3FitXj7mL8DHnLbxyUiIiKyM+tKOPR65Jw6g5xTZyw+hwm0Y4mVb8CeJRxntgILbwN0KvuM11BEL6D3rfYfl4iIiKgFLE6gn3z0gdaNhFqF2BJ2sOcqHJvfbJ3kGQDGvQpInazUhIiIiNo9ixPorl0SWzcSahXmZ6DtkJiqqoC8vS0fR0zcYCBpUuuMTURERNQCnN5zc61awpG3HzCIj98iMjkw8c36InoiIiIiJ2OXjVTIeYmtAQ17zUDnmpl9lilsTH4lQHRfYMSzQIeUlkZHRERE1CqYQLs5lbYVa6Bz9wjbPLyBv+UCMs+Wj09ERETkhFjC4ebMlXDIZS289XodkLtP2B47kMkzERERuTUm0G7ObA20ZwtroAtPAOoqYXt8asvGJSIiInJyTKDdXKutwiFWvgEAHZhAExERkXtjAu3mzK4D3eIE2swDhHF8+I+IiIjcGxNoN9dqy9hdEJmBDusG+IS0bFwiIiIiJ8cE2s2Zr4Fuwa2vugyUnxe2s/6ZiIiI2gEm0G7O7DJ2LSnhyE0Xb2f9MxEREbUDTKDdnPmNVFpQwnHBXAI9xPYxiYiIiFwEE2g31yolHGIz0D6hQGhn28ckIiIichFMoN2c3TdS0dQB+UeE7R1Sbdy+m4iIiMi1MIF2c3ZfB/rSIUCvEbaz/pmIiIjaCSbQbk5sHWi5hxQSW2eLxZavAxNoIiIiaj+YQLs5sRnolq3AIbKBitQTiOlv+5hERERELoQJtJsTT6BtXIHDYBB/gDAmGfD0sm1MIiIiIhfDBNrNiS1jZ/MMdHEOUFcqbGf5BhEREbUjTKDdnNhGKjYvYWduA5V4rv9MRERE7QcTaDcnPgNtYwlHLh8gJCIiImIC7ebEaqDltpZwiD1AGNwJ8IuwbTwiIiIiF8QE2s3ZbRWO2lKgOFvYztlnIiIiameYQLs5sXWgbUqgz2wRb49nAk1ERETtCxNoN2eXZeyqi4A//yZ+jDPQRERE1M4wgXZzogm0Natw6PXA8geA6svCY36RQHiPFkZIRERE5FqYQLu5FtdA73gfOL1J/NjwZwApf4SIiIioffFwdAD2cubsOaxd9xfOXciFVqtDeFgohqamYPTINEgkEmM/nU6Hzdt2Yu++gygsKoZMJkVcbAzGjRmJvr17Csa1tr+zEV0H2tISjvO7gM3/Fj/WZQIw+IEWRkdERETketxi+vBwxjG8/9EXqKiswo2TJ+K2W26Cj483li5fhaXLV5v0/Xb+QixfuQbBwUGYcetUTL1xMlQqFb78Zj627xSuc2xtf2diMBhsn4GuKQaW3gMYhOfDPwaY9iVnn4mIiKhdcvkZ6NraWixctBSxMdF47qlH4OnpCQBITRmA9/73Oc6cPQelUgUvLwUOZxzDkYxMDBqQjHlzZhrHSE0ZgDff+RDLVq5Bct/e8Pf3A64k5tb0dzY6PWAwCNubrYHW64HlDwJV+cJjEhlw63eAb6j9AiUiIiJyIS4/hZi+7yBqa+sw5foJxuQZAGQyGV545jG8+OwT8PJS1PfdewAAMG7MSJMx5HI5hqelQq1W48DhjGtjW9nf2ah0IrPHABSyZm777o+BUxvFj439O5Aw1A7REREREbkml0+gj5/IglQqRfduXYErZQtqtUa075lz5+Hp6Ym42GjBscROCfV9zpyzub+zUetEpp8BKDybqIGuLQW2/Ef8WOdxwLCn7RQdERERkWty+RKO/MsFCA4KRGlZOZavXIMTWTnQarXw9/PD4JT+mHL9RMjlnlAqVaiurkF4WCikIrW7wcFBAICi4hIAsLq/JfR68Rnh1qDX66HWiifQnjKJ+ViOLIZUUytoNvhHwzD186uD2zVWErp6f9ryZ4bsi/fQ9fEeuj7eQ9fmzPfP5RPo6ppa+Ph446NPv0Jyvz645+47oVSqsHN3Ov7avB0XL+bjsYfvg1KlAgAoFArRcRTy+nalUln/fyv7W6Lg4gUrv7qWUZsp4VBXV4jHYjAgbO83oh9LlI54HeryOqC8bb+G9q4oP8/RIVAL8R66Pt5D18d76Nqc8f65fAKt0+lQUVGJ6VOnYOzoEcb2lIHJeOf9j3Ey+xQyj59EXFxsMyPVz9Y2XPLOvv2ByNh4i/u2lF6vx6mSU6LHwsJCERnbQXggbx+kZcJzDPFDETxoemuESWbo9XoU5echPDpO9BMQcn68h66P99D18R66Nkfdv6pT4vlTQy6fQCvkctQplUgZ1N+kXSqVYsjgQcjNW4XsU6fRtUtnoMHMcmNX2728vAAA3lf+b2l/S7T1L6+5Eg5vuYd4LAd/FO0vGTgXEv7F4xBSqZR/6bs43kPXx3vo+ngPXZsz3j/nisYGoaEhAACZyDc2IMAfuFLPrFDIERjgj/LyCtFampKSMgBAREQYAFjd3xmZK+EQXQdaWQlkLhO2ewUCPW9uheiIiIiIXJPLJ9CdEzsCAC7kXRIcKymtT3KDAgMBAF06d4JWq8X5C7mCvjmnzgAAkrokGtus7e9szK7CIbYT4dElgMjDg+g7A/D0boXoiIiIiFyTyyfQaUNSIJFIsHbdRpOZYrVag5270gEAfXr3qO87NBUAsHHTNpMxamvrsGN3Onx9fdC/X59rY1vZ39mYS6DlYjPQB+eLDzLgbjtHRUREROTaXL4GOi42BpMmjMXa9X/ho0+/RurgAairU2J3+n4UFZdg1Ig0dLjyAGH3pC4YmjoIu9P34/Ovvkf/5L5QqVTYun0XKiurcO/cWfD2vjbbam1/Z6MW2cYbYiUclw4D+UeEHWMHAVG9Wyk6IiIiItfk8gk0AEyZfB0iIsKxdftO/PrbKhgMBkRHReLOGdMxbOhgk753zpiOuLhY7Nq9F4uWLINMJkPHhHjMnHELunYWlmNY29+ZWFzCYW72eSBnn4mIiIgac4sEGgAGD+qPwY1W4hAjlUoxekQaRo9Is2hca/s7E/M7ETaYgVbXABlLhJ3kfkCvW1oxOiIiIiLX5PI10GSeypISjszlgLpK2KnPrYDCrxWjIyIiInJNTKDdmEUlHAf48CARERGRNZhAuzHzCfSV2154AsjbK+wQ1QeIab4choiIiKg9YgLtxsyuwnG1BvrQAvETB9wNWLFFOREREVF7wgTajZldB1p25baf3yk86OEN9L29lSMjIiIicl1MoN2YWAItk0rgcTWBrioQntRpZP323UREREQkigm0G1PrhCUcxvpngwGoKRKe5B/VBpERERERuS4m0G5MrRXOQBsT6LoyQK8RnuQX0QaREREREbkuJtBuTCVSwmFcwk5s9hkAfJlAExERETWFCbQbEy3huLoCR3Wh+El+4a0cFREREZFrYwLtxpos4agxk0BzBpqIiIioSUyg3ZjYKhzGEo5qMyUcrIEmIiIiahITaDfW5CocZmegWcJBRERE1BQm0G5MbAZa7tFEDbRMzjWgiYiIiJrBBNqNNV0DLVLC4RvOLbyJiIiImsEE2o01XQMtMgPN8g0iIiKiZjGBdmNNLmMnNgPNBwiJiIiImsUE2o2pzJVwmNvGm0vYERERETWLCbQbM1vCoaoCtErhCdxEhYiIiKhZTKDdlE5vgFZvZgaa23gTERER2YwJtJtSa4X1z7haA212G28m0ERERETNYQLtplRanWi7wkPGTVSIiIiIWoAJtJsyNwMt9+AMNBEREVFLMIF2UypzJRysgSYiIiJqESbQbsp8Ai0Tn4GWyADv4NYPjIiIiMjFMYF2U+ZroM3MQPuGA1L+OBARERE1hxmTmzI7A21uFQ6uAU1ERERkESbQbkqlaaKEQ2wVDtY/ExEREVmECbSbavIhwmqREg6uwEFERERkESbQbspcDbS3QQloaoQHfMNaPygiIiIiN8AE2k2Zm4H20ZaJn8ASDiIiIiKLMIF2U+Y2UvFRl4qfwBIOIiIiIoswgXZT5magvdQl4idwG28iIiIiizCBdlPmaqC9VMXiJ3AGmoiIiMgiTKDdlLkZaLnS3Aw0E2giIiIiSzCBdlPm1oH2VIrNQEsAn9BWj4mIiIjIHTCBdlPmZqBldSJrQPuEAjKP1g+KiIiIyA0wgXZT5mqgpTXcRIWIiIioJTjt6KbEZqAVHlJIxBJorsBBRETkUpRaJQ4WHkRhbaGjQ2k1BoMBlWUlCFCGQiKR4LqE6+Dj6ePosAAm0O5LbB1oObfxJiIicnkFNQV4YMMDOFNxxtGhtKnBUYOdJoFmCYebEpuB9pfpAFWFsDNX4CAiInIZX2V81e6SZ2fDBNpNic1AR3pUi3f2YwkHERGRKzAYDNiUu8nRYbR7TKDdlNhDhJEykdlncAaaiIjIVeRW5aK4zsymaNRmmEC7KbF1oCOkVeKd+RAhERGRSzhQcMDRIRAfInRfYjXQ4TAzA80SDiIiIpdwsPCgaPt3E79DgDygzeNpTXq9HiWFlxEaEQWpVIpwb+fJV5hAuymxEo4wSbl4Z5ZwEBERuYSDBcIEuoN/B6REpTgkntak1+tRUOeNyJB4SKXOVTThXNGQ3YjNQIeYm4FmCQcREZHTK64rxoWqC4L2AREDHBJPe+aWM9AnsrLxyeffAgA+/fBtk2M6nQ6bt+3E3n0HUVhUDJlMirjYGIwbMxJ9e/cUjGVtf2chlkAH6UVmoL2CAA952wRFRERENjNX/zwwcmCbx9Leud0MtFKpwsJFv5k9/u38hVi+cg2Cg4Mw49apmHrjZKhUKnz5zXxs37mnxf2dhdgydqIJNDdRISIicgli5RsAMCCSM9Btze1moJevWoPq6hpERoSjoNB0173DGcdwJCMTgwYkY96cmcb21JQBePOdD7Fs5Rok9+0Nf38/m/o7E7EZ6EB9mbAj65+JiIhcgtgDhKFeoYj3j3dIPO2ZW81AZ2Wfws7dezF54ngE+PsLjqfvrf/oY9yYkSbtcrkcw9NSoVarceBwhs39nYnYQ4T+WpEEmitwEBEROb0qdRWySrME7QMiB0AikTgkpvbMbRJopUqFhYuWokNcLMaPHSna58y58/D09ERcbLTgWGKnhPo+Z87Z3N9ZGAwGwQy0B7Tw1XEbbyIiIld0uPAwDDAI2ln/7BhuU8KxcvVaVFRU4qH754oudaJUqlBdXYPwsFDR48HBQQCAouISm/pbQq8XllW0BrVWD0Oj37EQiG+iovcNA9ooLrLc1Z+VtvqZIfvjPXR9vIeuz53uobkHCJPDk93i6xPjzPfPLRLo7JzT2L5zD26YNB4x0VGifZQqFQBAoVCIHlfI69uVSqVN/S1RcFG49ExrqFELyzfCJeJL2FVqPFDXRnGR9Yry8xwdArUQ76Hr4z10fe5wD9NzdwvafGQ+CKz1QkGde/877oz3z+UTaLVajYWLliImOgrXjR/TgpHqp2wtryOytj8QGds2Rf7F1SoAJ03aQs0k0AGx3RDQRnGR5fR6PYry8xAeHed0i8eTZXgPXR/voetzl3uo0qmQVZUjaO8f2R8xcZ0cElNbcNT9qzp1qtk+Lp9Ar1i9FqVl5Xjhmccgk8nM9vP28gIazCw3drXd60o/a/tboq1uvkbkk44wM5uoSP2jABf+S8XdSaVSl/5Ln3gP3QHvoetz9Xt4vOg4NHqNoH1g5ECX/ros5Yz3z6UT6FNnzmLbjt0YOXwo/Px8UVZ+bZ1jrVYLAMa24KAgBAb4o7y8Anq9XnAjSkrqV6iIiAgDACgUcqv6OxOVRmwbbzO7EHIVDiIiIqfG9Z+dj0sn0FlZp2AwGLB1+y5s3b5LtM8r/3oLuLIjYZfOnXDgUAbOX8hFp44JJv1yTp0BACR1STS2WdvfWah1winoMEmleGdu401EROTUDhQKHyD0lHqid1hvh8RDLp5ADxqYjPj4ONFjq37/E5fyL+Oh++ca29KGpuLAoQxs3LQN998z29heW1uHHbvT4evrg/79+tjc31moRGo4RGeg5f6Ap3fbBEVERERW0+l1OFJ4RNDeJ6wPFDLxhQ6o9bl0Ah0ZEY7ICPEZ1L82bQMA9OnVw9jWPakLhqYOwu70/fj8q+/RP7kvVCoVtm7fhcrKKtw7dxa8vb1t7u8sxHYhFK2BZvkGERGRU8suy0a1plrQzvINx3LpBNoWd86Yjri4WOzavReLliyDTCZDx4R4zJxxC7p2FpZjWNvfGYjtQii6jB03USEiInJqYtt3A8CACCbQjuS2CfRTjz8o2i6VSjF6RBpGj0izaBxr+zsDi0s4OANNRETk1MQ2UJFAguSIZIfEQ/XcNoFuzxqXcEihRwhEHiLkDDQREZFTMBgM+OPsH9iatxWV6mv/Zh8qOCTo2y2kG/zl/m0cITXEBNoNNS7hCEYVZBKDsKMfE2giIiJn8GXGl/j08KcW9WX5huM516rUZBeNZ6DNrgHNJeyIiIgc7kzFGXx+5HOL+/MBQsdjAu2GGm+kYn4TFc5AExEROdonhz6B3iCyjbAICSQYGDmw1WOipjGBdkONN1IJFat/BmugiYiIHC2zJBMbzm+wuP/kxMkI83a+XZDbG9ZAu6HGq3CES8rFO3IVDiIiIof66OBHou0R3hHwlfsaX/t4+CAtJg0P9XuoDaMjc5hAu6HGNdDRklLxjv7RbRMQERERCey7vA+7Lu0StHt7eOPXG39FqHeoQ+Ki5rGEww01XoUjSiyB9g7hNt5EREQOYjAY8OHBD0WPzek5h8mzk2MC7YYsmoEOiG27gIiIiMjEltwtyCjKELQHKgJxd6+7HRITWY4JtBtqXAMtOgMdENN2AREREZGRTq/DR4fEa5/v630fN0lxAayBdkMNSzik0CMSZcJOTKDJwfQGPbR6raPDaDV6vR5qvQZqnRpSA+cqXBHvoetz1nu49uxanCo/JWiP8InAHd3vcEhMZB0m0G6oYQlHGCrgIRFZW5IlHOQgddo6vLHnDWy6sAnVmmpHh0NE5DQe6vcQvDy8HB0GWYAJtBtqmEBHS0rEO3EGmhzAYDDg+a3PY2veVkeHQkTkVOL94zG1y1RHh0EWcp7PM8hu1A0SaNH6ZzCBJsdYcWoFk2ciIhGP9X8MnlJPR4dBFmIC7YYa1kCbXQOaJRzUxi7XXMY7+95xdBhERE6ne0h3TOw40dFhkBWYQLshlUUz0NxEhdqOwWDAv3b/izXPRESNBCoC8ebwNyGVMCVzJayBdkMNl7ETnYFWBAIKLpFDbWfFqRXYeXGn6LHbk253yyWbDAYDaqor4esXAIlE4uhwyAa8h67P2e9hjF8MRsaNRJRvlKNDISsxgXZDDUs4uAY0OVpTpRs3Jt6IV4e+2uYxtQW9Xo+CixcQGRsPqZQzS66I99D18R5Sa+FPkxsyWYUDIqtwMIGmNmIwGPCvXeKlG+He4Xhx8IsOiYuIiKglOAPthq4m0BLoESmxbROVbXnbkJ6fDqVW2RohUjMMMKCuphreeX6QwPk+drRUmaoMOy+Jl278c+g/EagIbPOYiIiIWooJtBtSaepLOEJQBYVEZKe3Zlbg+PTwp/jiyBetFR4Rbup8E0Z1GOXoMIiIiGzCEg43dHUG2pY1oEuVpfj26LetFRoRIrwj8ELKC44Og4iIyGZMoN2MTm+AVm8AbFwDeufFndDoNa0VHhH+mcbSDSIicm1MoN1MS3chNFevSmQPt3S9BSPjRjo6DCIiohZhDbSbMd2FUGQFDphPoPUGPXZd3CVol0ACuUxuvyDJIgaDwSnXLbVFiFcIJnWchCcGPOHoUIiIiFqMCbSbaXYXQk9fwEv84/MTpSdQphKu2jGz+0z8LfVv9g2UmsS1S4mIiJwX/2V2Mya7EMLMJipmZjXN7RQ3LHaY/QIkIiIicnFMoN1MS3YhFEug5VI5BkUOsl+ARERERC6OCbSbuVbCYRBfhcPMChyV6kocKToiaB8YORA+nj72DpOIiIjIZTGBdjNXZ6ADUQNviVrYwcwMdHp+OnQGnaCd5RtEREREpphAu5mrM9Dm14AWT6DN1j/HMIEmIiIiaogJtJu5tguhuSXshCUcBoMBOy7uELRH+kSic1Bn+wdJRERE5MKYQLuZq6twWDMDfabiDApqCwTtw2OHu806xERERET2wgTazVytgTa/C6FwBlps9hmsfyYiIiISxQTazRhroMXWgJYpAJ8QQbNY/bNMIkNqdGrrBElERETkwphAu5lrNdCWbaJSq6nF/oL9gq79wvshQB7QeoESERERuSgm0G5Gpakv4bB0Dej9Bfuh0WsE7SzfICIiIhLHBNrNNDsD3QiXryMiIiKyDhNoN6PS6uGHWvhL6oQHxRLoS8IEOlgRjB6hPVorRCIiIiKXxgTazai1eotX4MitysX5yvOCbmmxaZBK+KNBREREJIZZkptRaXUWrwG96+Iu0W4s3yAiIiIyjwm0m1E1OQN9LYHW6DX4NftX0W5pMWmtFR4RERGRy2MC7WZUGr34GtAwLeH44dgPyC7LFnTpGdoTod6hrRkiERERkUtjAu1mVFodoiQlwgNSD8A3HABwtuIsvjjyhej5Nybe2NohEhEREbk0JtBuRqXVi9dA+8cAUin0Bj3+tetfUOvVgi5dgrpgRrcZbRMoERERkYtiAu1mzNZAX6l/XpK1BAcLDwoOSyVSvJb2Gjxlnm0RJhEREZHLYgLtZlQaM6twBMTgcs1lvH/gfdHzZvWYhT7hfVo/QCIiIiIX5+HoAOyhtrYOm7Zsx5GjmSguKYVEAkRHRSJtyGCkDUmBRCIx9tXpdNi8bSf27juIwqJiyGRSxMXGYNyYkejbu6dgbGv7O5ymFkGSGkGzwT8ar+95HbXaWsGxWL9YPJb8WBsFSEREROTaXH4GuryiEm/+90Os27gZCfFxuH36zZhy/XWoq1Pi58W/YfmqNSb9v52/EMtXrkFwcBBm3DoVU2+cDJVKhS+/mY/tO/cIxre2v6MFqItE29dK6rAtb5vosX8O/Sd8PH1aOTIiIiIi9+DyM9Crfv8TZWXluO2WmzB65LUNQIYMHoTX3noXm7bswPixoxDg74/DGcdwJCMTgwYkY96cmca+qSkD8OY7H2LZyjVI7tsb/v5+AGB1f2cQoCkUtF3w8MBbxeKbptzc+WYMjRnaBpERERERuQeXn4EODg5Ecr/eSBuSYtLu4+ONzp06wmAw4FJ+AQAgfe8B/H97dx4edXn3e/w9k2SyE7LvCUkIgrKEJQbCjmyCK2q1xVYsaq3LqY8+6ulzTs/zXG2v08fn2B5sy7FgrdZKxUqhgiLIIlsICWEnQCCEJGxmTyDbDJmZ80dgZEyQDAIzk35e18Wlc9/f+eWOHzJ+55f79xuAOyZPcKo1mUyMy83BYrGwa+9+x7ir9Z4gvMP5DHS1jw9PxcXQ2M3WjciASF7Ofvkmrk5ERETE+3l9A333rBk8+fj3MZlMXeZa29oACAoMBKCsvAI/Pz+SEuO71KanpXbWlJU7xlyt9wTh1lrHvzcZjfwoLprTft3/ouGnOT8lzD/sJq5ORERExPt5/RaOKzl95iylx08QEx1FclIC7e1mmptbiI6KxGjs+r4hPLwvADW1nR9C4mp9T9hstm/xHfVMlK2zgW41GHgmNprSbt5YAExNmcrU5Kk3ZU3iuku5KB/vpQy9nzL0fsrQu3lyfr2ygW5oaGTx2+9hMBj43sMPYDAYaDebAfD39+/2Of6mzvH29vbOf7pY3xNVpytd/E5cF2Wr5YIR/iUmiv0B3a99UJ9beL7fU1SfOXnD1yPfTs3ZU+5egnxLytD7KUPvpwy9myfm1+sa6IrKUyz647u0tLQy7/uPkNk/vYfPtAM43fLu+tZDbGJKj2uvhd1up4Y6fhodyfagwG5rMsIyWDTjLW3d8HA2m42as6eIjk/q9jcg4vmUofdTht5PGXo3d+V3vrT0qjW9qoEu2rWX95cuw2Ty49mn5zMgM8MxFxgQAJedWf66S+MBF+tcre+JGx1++4UOlka3szYkuNv5xJBEFk1bRHhg+A1dh1w/RqNRL/peThl6P2Xo/ZShd/PE/HpNA71+42ZWrFxNQnwcP3riMaIiI5zm/f1NhPUJpbGxCZvN1iWIuroGAGJioq6p3hP8fvcCVvXpfttGREAEi6YtIjY49qavS0RERKQ38ax2/hpt2ZbPipWrGXRLJi/95JkuzfMl/TPS6OjooKKy697fY6VlAAy4bMuHq/Xu1HKhhY2V67qdC8CXP0z9A6l9Um/6ukRERER6G69voMtOlPPR8pVkpPfjR0/OI+AKF84B5I7JAWD9RudP5GttbWNbfgHBwUEMHzbkmuvdKdgvmAX9n2GQ2eI07m+z8aT/bAZFDnLb2kRERER6E6/fwvHR8lXYbDaG3DaIAwcPdVsTHxdLfFwsAwf0Z0zOKPILinhz8TsMzxqK2Wxm89btnDt3nvnz5hIY+NXFd67Wu1vf4Djm16bxduRZDgfY8LHbeb26jgtDR7l7aSIiIiK9hqGkpMTu7kV8G8++8OpVa2bNmMrsO6fBxSs6t+TtYHt+IdU1Nfj4+NAvNYWZ06eQmdF1O4ar9d9kwIABLtVfK7PVzCubX2FU8BDmDrwTY1Ak+PX8YkdxP5vNRtXpSmITUzzuwgnpGWXo/ZSh91OG3s1d+R09evSqNV7fQHuTm9VAA1itVqrPnNSLhpfSi773U4beTxl6P2Xo3Ty5gdbfpl7KlftTi4iIiEjPqYEWEREREXGBGmgREREREReogRYRERERcYEaaBERERERF6iBFhERERFxgRpoEREREREXqIEWEREREXGBGmgREREREReogRYRERERcYEaaBERERERF6iBFhERERFxgRpoEREREREXqIEWEREREXGBGmgREREREReogRYRERERcYGhpKTE7u5FiIiIiIh4C52BFhERERFxgRpoEREREREXqIEWEREREXGBGmgREREREReogRYRERERcYEaaBERERERF6iBFhERERFxgRpoEREREREXqIEWEREREXGBr7sXINeP1Wrliy15FO7cTXVNLT4+RpISE7hj8gSGDr7V3cuTy7S2trFx01b2HSimtq4egwHi42LJHX07uaOzMRgMjlrl6h0Olxzl92++DcDCBa85zSlDz1V2opzP1m6gvPIkHR1WoqMiGZOTzaQJufo59HCNTedYt2ETR0qOUd/QQEBAALHR0UwYP4bhw4YoPw+UX1DEshUraW838/OfvUpkZESXGlezcle2+ijvXmTxn95j3/5iBt82iKyhg+no6CAvv4CTp87wyEP3M37saHcvUS6+6L++YCFNTefIyR5BRnoabW1tbNteQFV1DXdMHs+ce+9y1CtXz9febuaXr/2GhoZG6KaBVoaeae/+g/zxnfdJiI9jXG4Ovr6+FBbt5lhpGZMmjOWhOfc4apWhZ6mqruHXC/4flgsXGJebQ3JiAm1mM4U7d1NReZJxuaP57nfud9QrP/c639zMBx8uZ//BQ/j5+WGxWK7YQLualbuy1RnoXmLv/oPs21/MqBFZPP6D7zrGc7JH8L//awHLP/6UrKGDCQ0Nces6BVZ+soaGhkYemnMPkyaMdYyPvn0UP//V62zctI2pUybSJzRUuXqJFSs/pbm5hdiYaKqqa5zmlKFnam1tZcnSZSQmxPOvLzyDn58fXMzl12+8SdmJctrbzQQE+CtDD7R23UZaWlu7NEhjR2fzi1/9mm3bdzBtygSioiKVnwd47de/w2q18sxTj/P5+k0cO17WbZ2rWbkzW+2B7iUKCncBcMfkCU7jJpOJcbk5WCwWdu3d76bVyeXCw8PIGjaY3NHZTuNBQYFkpPXDbrdz5mwVKFevUHK0lLz8QmbNmEqf0NAu88rQMxXs3E1raxt33TnN0TwD+Pj48MqLz/HqS/+NgAD/zlpl6HFq6+oByEjv5zTu5+dHSnJSZ019Ayg/j5DeL4V/e+UFbh10yzfWuZqVO7NVA91LlJVX4OfnR1JifJe59LTUzpqycjesTL7u7lkzePLx72MymbrMtba1ARAUGAjK1eO1m80sWbqM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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib\n", + "def plot_cum_max(y, **kwargs):\n", + " cum_max = (torch.cummax(y, dim=0)[0]).cpu()\n", + " plt.plot(range(len(cum_max)), cum_max, **kwargs)\n", + "\n", + "plt.figure(figsize=(8, 6))\n", + "\n", + "colors = matplotlib.cm.get_cmap(\"tab10\").colors\n", + "linestyles = {\"LogEI\": \"-\"}\n", + "for acq_name, res in results.items():\n", + " ls = linestyles[acq_name]\n", + " ibnn_y = res[\"BNN\"][-1]\n", + " matern_y = res[\"Matern\"][-1]\n", + " rbf_y = res[\"RBF\"][-1]\n", + " plot_cum_max(ibnn_y[N_INIT-1:], label=f\"I-BNN ({acq_name})\", color=colors[0], ls=ls)\n", + " plot_cum_max(matern_y[N_INIT-1:], label=f\"Matern ({acq_name})\", color=colors[1], ls=ls)\n", + " plot_cum_max(rbf_y[N_INIT-1:], label=f\"RBF ({acq_name})\", color=colors[2], ls=ls)\n", + "\n", + "plt.xlabel(\"BO Iterations\")\n", + "plt.ylabel(\"Max Value\")\n", + "plt.title(f\"{INPUT_DIMS}-d Problem\")\n", + "plt.legend()\n", + "plt.show()" + ] + } + ], + "metadata": { + "fileHeader": "", + "kernelspec": { + "display_name": "python3", + "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.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/ibnn_bo.py b/website-old/static/files/ibnn_bo.py new file mode 100644 index 0000000000..4796d546e0 --- /dev/null +++ b/website-old/static/files/ibnn_bo.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Infinite-Width Bayesian Neural Networks for Bayesian Optimization +# +# In this tutorial, we present an overview of infinite-width Bayesian neural networks (I-BNNs) [1, 2] and show how to use them as surrogate models for Bayesian optimization (BO). +# +# Consider an fully connected neural network with $L$ hidden layers, parameter weights drawn from $\mathcal{N(0, \sigma_w)}$, bias terms drawn from $\mathcal{N(0, \sigma_b)}$, and nonlinearity $\phi$. In the infinite-width limit, the output of this network is exactly equivalent to $\mathcal{GP}(\mu, K^L)$. By the central limit theorem, we find $\mu(x) = 0$, and we can also recursively define the covariance function as +# $$K^0(x, x')=\sigma_b^2+\sigma_w^2\frac{x \cdot x'}{d_\text{input}}\qquad K^l(x, x')=\sigma_b^2+\sigma_w^2F_\phi(K^{l-1}(x, x'), K^{l-1}(x, x), K^{l-1}(x', x'))$$ +# where $F_\phi$ is a deterministic function based on the activation function $\phi$. +# +# We will refer to this kernel as the "I-BNN kernel". Unlike many popular GP kernels, I-BNN covariance function is not based on Euclidean distance, allowing the GP to represent nonstationary functions. This is advantageous for many settings of Bayesian optimization, since the function we want to optimize may not have similar behavior throughout the entire input space. Furthermore, I-BNNs have been shown to work particularly well for BO problems with high-dimensional inputs [3]. +# +# BoTorch has an implementation of I-BNNs with ReLU activations: `InfiniteWidthBNNKernel`. +# +# +# [1] [Y. Cho, and L. Saul. Kernel Methods for Deep Learning. Advances in Neural Information Processing Systems 22, 2009.](https://papers.nips.cc/paper_files/paper/2009/hash/5751ec3e9a4feab575962e78e006250d-Abstract.html) +# [2] [J. Lee, Y. Bahri, R. Novak, S. Schoenholz, J. Pennington, and J. Dickstein. Deep Neural Networks as Gaussian Processes. International Conference on Learning Representations 2018.](https://arxiv.org/abs/1711.00165) +# [3] [Y.L. Li, T.G.J. Rudner, A.G. Wilson. A Study of Bayesian Neural Network Surrogates for Bayesian Optimization. International Conference on Learning Representations 2024.](https://arxiv.org/abs/2305.20028) + +# In[13]: + + +import os +import warnings + +import matplotlib.pyplot as plt +import torch +from torch import nn + +from gpytorch.kernels import MaternKernel, RBFKernel, ScaleKernel +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood + +from botorch import manual_seed +from botorch.acquisition import LogExpectedImprovement +from botorch.fit import fit_gpytorch_mll +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.kernels import InfiniteWidthBNNKernel +from botorch.models.transforms.outcome import Standardize +from botorch.optim.optimize import optimize_acqf +from botorch.utils.sampling import draw_sobol_samples + +warnings.filterwarnings('ignore') + +get_ipython().run_line_magic('matplotlib', 'inline') + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +dtype = torch.double +tkwargs = {"device": device, "dtype": dtype} + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### I-BNN Function Draws +# +# We start by visualizing the posteriors of an I-BNN. Here, we define a toy function and draw five initial function evaluations. + +# In[14]: + + +torch.manual_seed(1111) +def f(x): + x = -(x - 0.15) + return torch.sin(x * (2 * torch.pi)) + torch.sin(x * (2 * torch.pi) * 2) + +x = torch.linspace(0, 1, 100).to(dtype).unsqueeze(-1) +true_y = f(x) + +train_x = torch.rand(5, 1).to(**tkwargs) +train_y = f(train_x) + +# visualize the function and the training data +plt.figure(figsize=(4, 3)) +plt.plot(x.cpu(), true_y.cpu(), linewidth=2) +plt.scatter(train_x.cpu(), train_y.cpu(), color="black") +plt.show() + + +# **Initializing the Model**: We now define two versions of the I-BNN, constructed using a GP with an `InfiniteWidthBNNKernel`. One version has fixed user-specified values for $\sigma^2_w$ and $\sigma^2_b$, and the other uses the marginal log likelihood to optimize these hyperparameters. + +# In[15]: + + +# Function queries are not noisy +train_Yvar = torch.full_like(train_y, 1e-8) + +# I-BNN with fixed hyperparameters +ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device) +ibnn_kernel.weight_var = 10.0 +ibnn_kernel.bias_var = 5.0 +model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel) +model.eval() + +# I-BNN with optimized hyperparameters +model_optimize = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=InfiniteWidthBNNKernel(depth=3)) +mll = ExactMarginalLogLikelihood(model_optimize.likelihood, model_optimize) +fit_gpytorch_mll(mll) +model_optimize.eval() + +# Default GP with Matern for comparison +model_matern = SingleTaskGP(train_x, train_y, train_Yvar) +mll_matern = ExactMarginalLogLikelihood(model_matern.likelihood, model_matern) +fit_gpytorch_mll(mll_matern) +model_matern.eval(); + + +# **Visualizating the Posterior**: + +# In[16]: + + +def plot_posterior(ax, model, n_draws=5): + with torch.no_grad(): + ax.plot(x.cpu(), true_y.cpu(), linewidth=2, color="black", label="True Objective", linestyle="--") + ax.scatter(train_x.cpu(), train_y.cpu(), color="black", s=80, label="Observations") + + test_x = torch.linspace(0, 1, 100).to(**tkwargs) + pred_f = model(test_x) + + ax.plot(test_x.cpu(), pred_f.mean.cpu(), linewidth=2, label="Mean") + lower, upper = pred_f.confidence_region() + ax.fill_between(test_x.cpu(), lower.cpu(), upper.cpu(), alpha=0.2, label=r'$\mu \pm 2\sigma$') + + for i in range(n_draws): + if i == 0: + ax.plot(test_x.cpu(), pred_f.sample().cpu(), color="green", linewidth=0.5, label="Function Draw") + else: + ax.plot(test_x.cpu(), pred_f.sample().cpu(), color="green", linewidth=0.5) + + +# In[17]: + + +fig, axs = plt.subplots(1, 3, figsize=(18, 5)) + +plot_posterior(axs[0], model) +axs[0].set_title("I-BNN (Fixed Hypers)\nWeight Var: %.2f, Bias Var: %.2f" % + (model.covar_module.weight_var.item(), model.covar_module.bias_var.item()), + fontsize=20) +axs[0].set_ylim(-7, 8) +axs[0].legend() + +plot_posterior(axs[1], model_optimize) +axs[1].set_title("I-BNN (Optimized Hypers)\nWeight Var: %.2f, Bias Var: %.2f" % + (model_optimize.covar_module.weight_var.item(), model_optimize.covar_module.bias_var.item()), + fontsize=20) +axs[1].set_ylim(-7, 8) + +plot_posterior(axs[2], model_matern) +axs[2].set_title("GP (Matern Kernel)\nLength Scale: %.2f" % + model_matern.covar_module.lengthscale.item(), + fontsize=20) +axs[2].set_ylim(-7, 8) + +plt.show() + + +# Similar to a Matern kernel, we see that the uncertainty decreases around queried points and increases as we move away. However, we find that the I-BNN function draws are more jagged compared to the Matern draws, and we also note that the uncertainty of an I-BNN towards the edges of the data increases rapidly. + +# ### Impact of Hyperparameters +# +# The I-BNN has three hyperparameters: the number of hidden layers, the variance of the weights, and the variance of the bias terms. Here, we visualize how modifying these hyperparameters impacts the posterior. + +# In[18]: + + +fig, axs = plt.subplots(1, 4, figsize=(20, 4)) + +for i, ax in enumerate(axs): + ibnn_kernel = InfiniteWidthBNNKernel(depth=(i+1), device=device) + ibnn_kernel.weight_var = 10.0 + ibnn_kernel.bias_var = 2.0 + + model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval() + plot_posterior(ax, model, n_draws=5) + ax.set_title(f"Depth: {i+1}") + ax.set_ylim(-8, 8) + if i == 0: + ax.legend() + + +# In[19]: + + +fig, axs = plt.subplots(1, 4, figsize=(20, 4)) + +for i, ax in enumerate(axs): + ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device) + ibnn_kernel.weight_var = (i+1) * 5 + ibnn_kernel.bias_var = 2.0 + + model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval() + plot_posterior(ax, model, n_draws=5) + ax.set_title("Weight Var: %.1f" % ((i+1) * 5)) + ax.set_ylim(-10, 10) + if i == 0: + ax.legend() + + +# In[20]: + + +fig, axs = plt.subplots(1, 4, figsize=(20, 4)) + +for i, ax in enumerate(axs): + ibnn_kernel = InfiniteWidthBNNKernel(depth=3, device=device) + ibnn_kernel.weight_var = 10.0 + ibnn_kernel.bias_var = (i + 1) * 5 + + model = SingleTaskGP(train_x, train_y, train_Yvar, covar_module=ibnn_kernel).eval() + plot_posterior(ax, model, n_draws=5) + ax.set_title("Bias Var: %.1f" % ((i+1) * 5)) + ax.set_ylim(-5, 6) + if i == 0: + ax.legend() + + +# ### I-BNNs for Bayesian Optimization +# +# We will now use I-BNNs as the surrogate model for a high-dimensional BO problem. + +# **Define High-dimensional Function and BO Setup**: We will optimize the output of a multilayer perceptron (MLP) with 2 hidden layers, 50 nodes per layer, and ReLU nonlinearities. +# + +# In[21]: + + +class MLP(nn.Module): + def __init__(self, input_dims): + super().__init__() + self.layers = nn.Sequential( + nn.Linear(input_dims, 50, dtype=torch.float64), + nn.ReLU(), + nn.Linear(50, 50, dtype=torch.float64), + nn.ReLU(), + nn.Linear(50, 1, dtype=torch.float64) + ) + + def forward(self, x): + return self.layers(x) + +def create_f(input_dims, seed): + # create MLP with weights and biases sampled from N(0, 1) + with manual_seed(seed): + model = MLP(input_dims).to(**tkwargs) + params = torch.nn.utils.parameters_to_vector(model.parameters()) + params = torch.randn_like(params, dtype=torch.float64) + torch.nn.utils.vector_to_parameters(params, model.parameters()) + + def f(x): + with torch.no_grad(): + return model(x) + + return f + +INPUT_DIMS = 200 +N_ITERATIONS = 100 if not SMOKE_TEST else 5 +N_INIT = 50 if not SMOKE_TEST else 2 + +f = create_f(INPUT_DIMS, seed=1234) +bounds = torch.stack([torch.zeros(INPUT_DIMS), torch.ones(INPUT_DIMS)]).to(**tkwargs) + + +# **Define BO functions**: We use Sobol sampling to initialize the BO problem, and we use the Expected Improvement acquisition function. + +# In[22]: + + +from botorch.acquisition.analytic import ExpectedImprovement, LogExpectedImprovement + +def generate_initial_data(f, bounds, n, input_dims): + train_x = draw_sobol_samples(bounds=bounds, n=n, q=1).to(**tkwargs) + train_x = train_x.squeeze(-2) # remove batch dimension + train_y = f(train_x) + return train_x, train_y + + +def gp_bo_loop(f, bounds, init_x, init_y, kernel, n_iterations, acqf_class, optimize_hypers=False): + train_x = init_x.clone() + train_y = init_y.clone() + + for iteration in range(n_iterations): + + # fit model to data + model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=1), covar_module=kernel) + if optimize_hypers: + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + model.eval() + + # optimize acquisition function + candidate_x, acq_value = optimize_acqf( + acq_function=acqf_class(model, train_y.max()), + bounds=bounds, + q=1, + num_restarts=10, + raw_samples=200, + ) + candidate_x = candidate_x.double() + + # update training points + train_x = torch.cat([train_x, candidate_x]) + train_y = torch.cat([train_y, f(candidate_x)]) + + return train_x, train_y + + +# **Compare I-BNN with GP with Matern kernel and RBF kernel**: On this high-dimensional problem, the I-BNN significantly outperforms the standard Matern and RBF kernels and is able to find better rewards. + +# In[23]: + + +from functools import partial +# define kernels +ibnn_kernel = InfiniteWidthBNNKernel(2, device=device) +ibnn_kernel.weight_var = 10.0 +ibnn_kernel.bias_var = 5.0 +ibnn_kernel = ScaleKernel(ibnn_kernel, device=device) + +matern_kernel = ScaleKernel(MaternKernel(), device=device) +rbf_kernel = ScaleKernel(RBFKernel(), device=device) + +# initialize problem +train_x, train_y = generate_initial_data(f, bounds, n=N_INIT, input_dims=INPUT_DIMS) + +# run BO loop +acqf_classes = {"LogEI": LogExpectedImprovement} +results = {} +for acq_name, acqf_class in acqf_classes.items(): + run_bo_with_acqf = partial(gp_bo_loop, f=f, bounds=bounds, init_x=train_x, init_y=train_y, acqf_class=acqf_class, n_iterations=N_ITERATIONS) + ibnn_x, ibnn_y = run_bo_with_acqf(kernel=ibnn_kernel, optimize_hypers=False) + matern_x, matern_y = run_bo_with_acqf(kernel=matern_kernel, optimize_hypers=True) + rbf_x, rbf_y = run_bo_with_acqf(kernel=rbf_kernel, optimize_hypers=True) + results[acq_name] = { + "BNN": (ibnn_x, ibnn_y), + "Matern": (matern_x, matern_y), + "RBF": (rbf_x, rbf_y), + } + + +# In[24]: + + +import matplotlib +def plot_cum_max(y, **kwargs): + cum_max = (torch.cummax(y, dim=0)[0]).cpu() + plt.plot(range(len(cum_max)), cum_max, **kwargs) + +plt.figure(figsize=(8, 6)) + +colors = matplotlib.cm.get_cmap("tab10").colors +linestyles = {"LogEI": "-"} +for acq_name, res in results.items(): + ls = linestyles[acq_name] + ibnn_y = res["BNN"][-1] + matern_y = res["Matern"][-1] + rbf_y = res["RBF"][-1] + plot_cum_max(ibnn_y[N_INIT-1:], label=f"I-BNN ({acq_name})", color=colors[0], ls=ls) + plot_cum_max(matern_y[N_INIT-1:], label=f"Matern ({acq_name})", color=colors[1], ls=ls) + plot_cum_max(rbf_y[N_INIT-1:], label=f"RBF ({acq_name})", color=colors[2], ls=ls) + +plt.xlabel("BO Iterations") +plt.ylabel("Max Value") +plt.title(f"{INPUT_DIMS}-d Problem") +plt.legend() +plt.show() + diff --git a/website-old/static/files/information_theoretic_acquisition_functions.ipynb b/website-old/static/files/information_theoretic_acquisition_functions.ipynb new file mode 100644 index 0000000000..61dcebab67 --- /dev/null +++ b/website-old/static/files/information_theoretic_acquisition_functions.ipynb @@ -0,0 +1,747 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "738920c6", + "metadata": {}, + "source": [ + "# Information-theoretic acquisition functions" + ] + }, + { + "cell_type": "markdown", + "id": "c6b5d9a9", + "metadata": {}, + "source": [ + "This notebook illustrates the use of some information-theoretic acquisition functions in BoTorch for single and multi-objective optimization. We present a single-objective example in section 1 and a multi-objective example in section 2. Before introducing these examples, we present an overview on the different approaches and how they are estimated." + ] + }, + { + "cell_type": "markdown", + "id": "876c4359", + "metadata": {}, + "source": [ + "## Notation\n", + "\n", + "We consider the problem of maximizing a function $f: \\mathbb{X} \\rightarrow \\mathbb{R}^M$. In the single-objective setting ($M=1$), the maximum is defined as usual with respect to the total ordering over the real numbers. In the multi-objective setting ($M>1$), the maximum is defined with respect to the Pareto partial ordering over vectors. By an abuse in notation, we denote the optimal set of inputs and outputs by\n", + "\n", + "$$\\mathbb{X}^* = \\text{arg}\\max_{\\mathbf{x} \\in \\mathbb{X}} f(\\mathbf{x}) \\subseteq \\mathbb{X} \\quad \\text{and} \\quad \\mathbb{Y}^* = f(\\mathbb{X}^*) = \\max_{\\mathbf{x} \\in \\mathbb{X}} f(\\mathbf{x}) \\subset \\mathbb{R}^M,$$\n", + "\n", + "respectively for both the single and multi-objective setting. We denote the collection of optimal input-output pairs by $(\\mathbb{X}^*, \\mathbb{Y}^*)$." + ] + }, + { + "cell_type": "markdown", + "id": "1499a1cd", + "metadata": {}, + "source": [ + "## Information-theoretic acquisition functions\n", + "\n", + "Information-theoretic (IT) acquisition functions work by quantifying the utility of an input $\\mathbf{x} \\in \\mathbb{X}$ based on how \"informative\" the corresponding observation $\\mathbf{y} \\in \\mathbb{R}^M$ will be in learning more about the distribution of some statistic of the function $S(f)$. Here, we define the notion of information via the mutual information ($\\text{MI}$):\n", + "\n", + "\\begin{equation}\n", + " \\alpha^{\\text{IT}}(\\mathbf{x}|D_n) \n", + " = \\text{MI}(\\mathbf{y}; S(f)| \\mathbf{x}, D_n) \n", + " = H[p(\\mathbf{y}|D_n)] - \\mathbb{E}_{p(S(f)|D_n)}[H[p(\\mathbf{y}| \\mathbf{x}, D_n, S(f)]],\n", + "\\end{equation}\n", + "\n", + "where $D_n = \\{(\\mathbf{x}_t, \\mathbf{y}_t)\\}_{t=1,\\dots,n}$ denotes the data set of sampled inputs and observations and the function $H$ denotes the differential entropy $H[p(\\mathbf{x})] = - \\int p(\\mathbf{x}) \\log(p(\\mathbf{x})) d\\mathbf{x}$. The main difference between existing information-theoretic acquisition functions in the literature is the choice of statistic $S$ and the modelling assumptions that are made in order to estimate the resulting acquisition function. In this notebook, we focus on three particular cases of information-theoretic acquisition functions:" + ] + }, + { + "cell_type": "markdown", + "id": "7218d85d", + "metadata": {}, + "source": [ + "### Predictive Entropy Search (PES)\n", + "\n", + "The PES acquisition function [1] considers the problem of learning more about the distribution of the optimal inputs: $S(f) = \\mathbb{X}^*$.\n", + "\n", + "\\begin{equation}\n", + "\\alpha^{\\text{PES}}(\\mathbf{x}|D_n) \n", + "= \\text{MI}(\\mathbf{y}; \\mathbb{X}^*| \\mathbf{x}, D_n) \n", + "= H[p(\\mathbf{y}|D_n)] - \\mathbb{E}_{p(\\mathbb{X}^*|D_n)}[H[p(\\mathbf{y}| \\mathbf{x}, D_n, \\mathbb{X}^*)]].\n", + "\\end{equation}" + ] + }, + { + "cell_type": "markdown", + "id": "c7b1d071", + "metadata": {}, + "source": [ + "### Max-value Entropy Search (MES)\n", + "\n", + "The MES acquisition function [2] considers the problem of learning more about the distribution of the optimal outputs: $S(f) = \\mathbb{Y}^*$.\n", + "\n", + "\\begin{equation}\n", + "\\alpha^{\\text{MES}}(\\mathbf{x}|D_n) \n", + "= \\text{MI}(\\mathbf{y}; \\mathbb{Y}^*| \\mathbf{x}, D_n) \n", + "= H[p(\\mathbf{y}|D_n)] - \\mathbb{E}_{p(\\mathbb{Y}^*|D_n)}[H[p(\\mathbf{y}| \\mathbf{x}, D_n, \\mathbb{Y}^*)]].\n", + "\\end{equation}\n" + ] + }, + { + "cell_type": "markdown", + "id": "3c8e68ed", + "metadata": {}, + "source": [ + "### Joint Entropy Search (JES)\n", + "\n", + "The JES acquisition function [3] considers the problem of learning more about the distribution of the optimal inputs and outputs: $S(f) = (\\mathbb{X}^*, \\mathbb{Y}^*)$.\n", + "\n", + "\\begin{equation}\n", + "\\alpha^{\\text{JES}}(\\mathbf{x}|D_n) \n", + "= \\text{MI}(\\mathbf{y}; (\\mathbb{X}^*, \\mathbb{Y}^*)| \\mathbf{x}, D_n) \n", + "= H[p(\\mathbf{y}|D_n)] - \\mathbb{E}_{p((\\mathbb{X}^*, \\mathbb{Y}^*)|D_n)}[H[p(\\mathbf{y}| \\mathbf{x}, D_n, (\\mathbb{X}^*, \\mathbb{Y}^*))]].\n", + "\\end{equation}" + ] + }, + { + "cell_type": "markdown", + "id": "f14bbab1", + "metadata": {}, + "source": [ + "## Estimation\n", + "\n", + "In order to estimate the three acquistion functions listed above, we make two simplfying assumptions:\n", + "\n", + "**[Assumption 1]** We assume an independent Gaussian process prior on each objective function.\n", + "\n", + "**[Assumption 2]** We assume a Gaussian observation likelihood." + ] + }, + { + "cell_type": "markdown", + "id": "c69dfe94", + "metadata": {}, + "source": [ + "### First term\n", + "\n", + "Under the modelling assumptions, the first term in each of the acquisition functions is an entropy of a Gaussian random variable, which is analytically tractable.\n", + "\n", + "### Second term\n", + "\n", + "The second term in each of the acquisition functions is an expectation of an entropy over an intractable distribution. The expectation can be estimated using Monte Carlo, whilst the entropy has to be approximated using different strategies such as moment-matching.\n", + "\n", + "**Monte Carlo.** To sample from the distribution over the optimal points, we can first (approximately) sample a collection of posterior paths $f_j \\sim p(f|D_n)$ and then optimize them to obtain the sample of optimal points $(\\mathbb{X}^*_j, \\mathbb{Y}^*_j)$ for $j=1,\\dots,J$. \n", + "\n", + "**PES entropy estimate.** In `qPredictiveEntropySearch` and `qMultiObjectivePredictiveEntropySearch`, we approximate the entropy term arising in PES using the expectation propagation strategy described in [4]. In particular, we first relax the global optimality condition:\n", + "\n", + "\\begin{align}\n", + " H[p(\\mathbf{y}| \\mathbf{x}, D_n, \\mathbb{X}^*)]\n", + " &\\overset{(1)}{=} H[p(\\mathbf{y}| \\mathbf{x}, D_n, f(\\mathbb{X}) \\preceq f(\\mathbb{X}^*))]\n", + " \\\\\\\\\n", + " &\\overset{(2)}{\\leq} H[p(\\mathbf{y}| \\mathbf{x}, D_n, f(X_n \\cup \\{\\mathbf{x}\\}) \\preceq f(\\mathbb{X}^*))].\n", + "\\end{align}\n", + "\n", + "(1) This statement follows from the observation that conditioning on the optimal points $\\mathbb{X}^*$ is equivalent to knowing that all points lie below the objective values at the optimal inputs: $f(\\mathbb{X}) \\preceq f(\\mathbb{X}^*)$. \n", + "\n", + "(2) We replace the global optimality condition with the local optimality condition: $f(X_n \\cup \\{\\mathbf{x}\\}) \\preceq f(\\mathbb{X}^*)$, where $X_n = \\{\\mathbf{x}_t\\}_{t=1,\\dots,n}$. . The upper bound follows from the standard result that conditioning on more information only decreases the entropy: $H(A|B) \\leq H(A)$ for any random variables $A$ and $B$.\n", + "\n", + "We then estimate the resulting lower bound of the PES acquisition function by approximating the intractable distribution $p(\\mathbf{y}| \\mathbf{x}, D_n, f(X_n \\cup \\{\\mathbf{x}\\}) \\preceq f(\\mathbb{X}^*))$ with a product of Gaussian random variables, which is fitted via an iterative moment-matching procedure known as expectation propagation. The entropy of this resulting distribution can then be computed analytically.\n", + "\n", + "**MES and JES entropy estimate.** In `qLowerBoundMultiObjectiveMaxValueEntropySearch`, `qLowerBoundJointEntropySearch` and `qLowerBoundMultiObjectiveJointEntropySearch`, we approximate the entropy term arising in MES and JES using the strategies described in [3]. These estimates rely on different upper bounds of the entropy term, which results in different lower bounds for the mutual information. These estimates are motivated by the following chain inequalities for the entropy in the JES expression:\n", + "\n", + "\\begin{align}\n", + " H[p(\\mathbf{y}| \\mathbf{x}, D_n, (\\mathbb{X}^*, \\mathbb{Y}^*))]\n", + " &\\overset{(1)}{=} H[p(\\mathbf{y}| \\mathbf{x}, D_n \\cup (\\mathbb{X}^*, \\mathbb{Y}^*), f(\\mathbb{X}) \\preceq \\mathbb{Y}^*)]\n", + " \\\\\\\\\n", + " &\\overset{(2)}{\\leq} H[p(\\mathbf{y}| \\mathbf{x}, D_n \\cup (\\mathbb{X}^*, \\mathbb{Y}^*), f(\\mathbf{x}) \\preceq \\mathbb{Y}^*)]\n", + " \\\\\\\\\n", + " &\\overset{(3)}{\\leq} H[\\mathcal{N}(\\mathbf{y}| \\mathbf{m}_{(\\mathbf{x}, (\\mathbb{X}^*, \\mathbb{Y}^*))}, \\mathbf{V}_{(\\mathbf{x}, (\\mathbb{X}^*, \\mathbb{Y}^*))})]\n", + " \\\\\\\\\n", + " &\\overset{(4)}{\\leq} H[\\mathcal{N}(\\mathbf{y}| \\mathbf{m}_{(\\mathbf{x}, (\\mathbb{X}^*, \\mathbb{Y}^*))}, \\text{diag}(\\mathbf{V}_{(\\mathbf{x}, (\\mathbb{X}^*, \\mathbb{Y}^*))}))],\n", + "\\end{align}\n", + "\n", + "where \n", + "\n", + "\\begin{align}\n", + " \\mathbf{m}_{(\\mathbf{x}, (\\mathbb{X}^*, \\mathbb{Y}^*))} = \\mathbb{E}[p(\\mathbf{y}| \\mathbf{x}, D_n \\cup (\\mathbb{X}^*, \\mathbb{Y}^*), f(\\mathbf{x}) \\preceq \\mathbb{Y}^*)]\n", + "\\end{align}\n", + "\n", + "\\begin{align}\n", + " \\mathbf{V}_{(\\mathbf{x}, (\\mathbb{X}^*, \\mathbb{Y}^*))} = \\mathbb{C}\\text{ov}[p(\\mathbf{y}| \\mathbf{x}, D_n \\cup (\\mathbb{X}^*, \\mathbb{Y}^*), f(\\mathbf{x}) \\preceq \\mathbb{Y}^*)].\n", + "\\end{align}\n", + "\n", + "(1) This statement follows from the observation that conditioning on the optimal points $(\\mathbb{X}^*, \\mathbb{Y}^*)$ is equivalent to knowing that $\\mathbb{X}^*$ maps to $\\mathbb{Y}^*$ and that all points lie below the Pareto front, $f(\\mathbb{X}) \\preceq f(\\mathbb{X}^*) = \\mathbb{Y}^*$. \n", + "\n", + "(2) We replace the global optimality condition with the local optimality condition: $f(\\mathbf{x}) \\preceq \\mathbb{Y}^*$. The upper bound follows from the standard result that conditioning on more information only decreases the entropy: $H(A|B) \\leq H(A)$ for any random variables $A$ and $B$.\n", + "\n", + "(3) We upper bound the entropy using the standard result that the multivariate Gaussian distribution has the maximum entropy over all distributions supported on $\\mathbb{R}^M$ with the same first two moments.\n", + "\n", + "(4) We upper bound the entropy by again using the standard result that conditioning on more information only decreases the entropy.\n", + "\n", + "**(Conditioning)** A similar chain of inequalities can be obtained for the entropy in the MES term by replacing the augmented data set $D_n \\cup (\\mathbb{X}^*, \\mathbb{Y}^*)$ with the original data set $D_n$. The only real difference between the JES and MES estimate is whether we condition on the extra samples $(\\mathbb{X}^*_j, \\mathbb{Y}^*_j)$ or not for $j=1,\\dots,J$. As a result of this conditioning, the JES estimate can be more expensive than the MES estimate.\n", + "\n", + "**(Noiseless setting)** When the observations are exact, $\\mathbf{y} = f(\\mathbf{x})$, then the entropy term in (2) can be computed exactly. By setting `estimation_type=\"0\"`, we use this estimate. In the setting where there is observation noise, the estimate also includes an ad-hoc correction which can be useful (more details in the appendix of [3]).\n", + "\n", + "**(Monte Carlo)** The entropy term in (2) can be estimated using Monte Carlo because the distribution has a tractable density under the assumptions. By setting `estimation_type=\"MC\"`, we use this Monte Carlo estimate.\n", + "\n", + "**(Lower bound)** The entropy term in (3) and (4) can be computed exactly. By setting `estimation_type=\"LB\"`, we use this lower bound estimate in (3). By setting `estimation_type=\"LB2\"`, we use lower bound estimate in (4)." + ] + }, + { + "cell_type": "markdown", + "id": "658c9cc3", + "metadata": {}, + "source": [ + "### Batch\n", + "\n", + "For the batch setting, the first term is again analytically tractable. The second term can be estimated using Monte Carlo, whilst the entropy term again has to be estimated.\n", + "\n", + "**PES entropy estimate.** In `qPredictiveEntropySearch` and `qMultiObjectivePredictiveEntropySearch`, the entropy term is again approximated using expectation propagation. In particular, we approximate $p(Y| X, D_n, f(X_n \\cup X) \\preceq f(\\mathbb{X}^*))$ with a product of Gaussian random variables. \n", + "\n", + "**MES and JES entropy estimate** In `qLowerBoundMultiObjectiveMaxValueEntropySearch`, `qLowerBoundJointEntropySearch` and `qLowerBoundMultiObjectiveJointEntropySearch`, we approximate a lower bound to the MES and JES acquisition function:\n", + "\n", + "\\begin{equation}\n", + "\\alpha^{\\text{LB-MES}}(X|D_n) \n", + "= \\text{MI}(Y; \\mathbb{Y}^*| X, D_n) \n", + "= H[p(Y|D_n)] - \\sum_{\\mathbf{x} \\in X} \\mathbb{E}_{p(\\mathbb{Y}^*|D_n)}[H[p(\\mathbf{y}| \\mathbf{x}, D_n, \\mathbb{Y}^*)]],\n", + "\\end{equation}\n", + "\n", + "\\begin{equation}\n", + "\\alpha^{\\text{LB-JES}}(X|D_n) \n", + "= \\text{MI}(Y; (\\mathbb{X}^*, \\mathbb{Y}^*)| X, D_n) \n", + "= H[p(Y|D_n)] - \\sum_{\\mathbf{x} \\in X} \\mathbb{E}_{p((\\mathbb{X}^*, \\mathbb{Y}^*)|D_n)}[H[p(\\mathbf{y}| \\mathbf{x}, D_n, (\\mathbb{X}^*, \\mathbb{Y}^*))]].\n", + "\\end{equation}\n", + "\n", + "The advantage of these expressions is that it allows us to take advantage of the existing entropy estimates for the sequential setting." + ] + }, + { + "cell_type": "markdown", + "id": "1ce75823", + "metadata": {}, + "source": [ + "## References" + ] + }, + { + "cell_type": "markdown", + "id": "262131d3", + "metadata": {}, + "source": [ + "[1] J.M. Hernández-Lobato, M.W. Hoffman and Z. Ghahramani, [**Predictive Entropy Search for Efficient Global Optimization of Black-box Functions**](https://arxiv.org/abs/1406.2541), NeurIPS, 2014.\n", + "\n", + "[2] Z. Wang and S. Jegelka, [**Max-value Entropy Search for Efficient Bayesian Optimization**](https://arxiv.org/abs/1703.01968), ICML, 2017.\n", + "\n", + "[3] B. Tu, A. Gandy, N. Kantas and B. Shafei, [**Joint Entropy Search for Multi-Objective Bayesian Optimization**](https://arxiv.org/abs/2210.02905), NeurIPS, 2022.\n", + "\n", + "[4] C. Hvarfner, F. Hutter and N. Nardi, [**Joint Entropy Search for Maximally-Informed Bayesian Optimization**](https://arxiv.org/abs/2206.04771), NeurIPS, 2022.\n", + "\n", + "[5] E. Garrido-Merchán and D. Hernández-Lobato, [**Predictive Entropy Search for Multi-objective Bayesian Optimization with Constraints**](https://www.sciencedirect.com/science/article/abs/pii/S0925231219308525), Neurocomputing, 2019." + ] + }, + { + "cell_type": "markdown", + "id": "7490ac1c", + "metadata": {}, + "source": [ + "# 1. Single-objective example " + ] + }, + { + "cell_type": "markdown", + "id": "5c3e4976", + "metadata": {}, + "source": [ + "In this section, we present a simple example in one-dimension with one objective to illustrate the use of these acquisition functions. We first define the objective function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "908e289f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n", + "tkwargs = {\"dtype\": torch.double, \"device\": \"cpu\"}\n", + "\n", + "\n", + "def f(x):\n", + " p1 = torch.cos(torch.pi * x)\n", + " p2 = 10 * torch.sin(torch.pi * x)\n", + " p3 = 2 * torch.sin(2 * torch.pi * x)\n", + " p4 = 2 * torch.sin(6 * torch.pi * x)\n", + " return p1 + p2 + p3 + p4\n", + "\n", + "\n", + "bounds = torch.tensor([[0.0], [1.0]], **tkwargs)" + ] + }, + { + "cell_type": "markdown", + "id": "0df52007", + "metadata": {}, + "source": [ + "We now generate some data and then fit the Gaussian process model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5770f703", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "torch.manual_seed(0)\n", + "np.random.seed(0)\n", + "n = 5\n", + "train_X = draw_sobol_samples(bounds=bounds, n=n, q=1, seed=12345678).squeeze(-2)\n", + "train_Y = f(train_X)\n", + "\n", + "\n", + "def fit_model(train_X, train_Y, num_outputs):\n", + " model = SingleTaskGP(train_X, train_Y, outcome_transform=Standardize(m=num_outputs))\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + " return model\n", + "\n", + "\n", + "model = fit_model(train_X=train_X, train_Y=train_Y, num_outputs=1)" + ] + }, + { + "cell_type": "markdown", + "id": "0b6b02f9", + "metadata": {}, + "source": [ + "We now plot the objective function and the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "877a342b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "X = torch.linspace(bounds[0, 0], bounds[1, 0], 1000, **tkwargs)\n", + "mean_fX = model.posterior(X).mean.squeeze(-1).detach().numpy()\n", + "std_fX = torch.sqrt(model.posterior(X).variance).squeeze(-1).detach().numpy()\n", + "\n", + "plt.scatter(train_X, train_Y, color=\"k\", label=\"Observations\")\n", + "plt.plot(X, f(X), color=\"k\", linewidth=2, label=\"Objective function\")\n", + "plt.plot(X, mean_fX, color=\"dodgerblue\", linewidth=3, label=\"Posterior model\")\n", + "plt.fill_between(\n", + " X, (mean_fX + 3 * std_fX), (mean_fX - 3 * std_fX), alpha=0.2, color=\"dodgerblue\"\n", + ")\n", + "plt.xlabel(\"x\", fontsize=15)\n", + "plt.ylabel(\"y\", fontsize=15)\n", + "plt.legend(fontsize=15)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6ec0e247", + "metadata": {}, + "source": [ + "To compute the information-theoretic acquisition functions, we first need to get some Monte Carlo samples of the optimal inputs and outputs. The method `sample_optimal_points` generates `num_samples` approximate samples of the Gaussian process model and optimizes them sequentially using an optimizer. In the single-objective setting, the number of optimal points (`num_points`) should be set to one. For simplicitly, we consider optimization via random search. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79e93848", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from botorch.acquisition.utils import get_optimal_samples\n", + "\n", + "num_samples = 12\n", + "\n", + "optimal_inputs, optimal_outputs = get_optimal_samples(\n", + " model, bounds=bounds, num_optima=num_samples\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "620d538a", + "metadata": {}, + "source": [ + "We now initialize the information-theoretic acquisition functions. The PES can simply be initialized using just the optimal set of inputs. For the MES and JES acquisition function, we also have to specify the region of integration, which is $\\{\\mathbf{y}: \\mathbf{y} \\preceq \\mathbb{Y}^*\\}$ for a maximization problem. This is done by providing a Tensor of bounds, which is obtained via the method `compute_sample_box_decomposition`.\n", + "\n", + "Note that for the MES algorithm, we use the multi-objective implementation `qLowerBoundMultiObjectiveMaxValueEntropySearch`, which implements all the estimation types into one acquisition function. BoTorch alreadys supports many other strategies to estimate the single-objective MES algorithms in `botorch.acquisition.max_value_entropy`, which is described in the other complementary notebooks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "320b07cc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from botorch.acquisition.joint_entropy_search import qJointEntropySearch\n", + "from botorch.acquisition.max_value_entropy_search import qLowerBoundMaxValueEntropy\n", + "from botorch.acquisition.predictive_entropy_search import qPredictiveEntropySearch\n", + "\n", + "pes = qPredictiveEntropySearch(model=model, optimal_inputs=optimal_inputs)\n", + "\n", + "# Here we use the lower bound estimates for the MES and JES\n", + "# Note that the single-objective MES interface is slightly different,\n", + "# as it utilizes the Gumbel max-value approximation internally and\n", + "# therefore does not take the max values as input.\n", + "mes_lb = qLowerBoundMaxValueEntropy(\n", + " model=model,\n", + " candidate_set=torch.rand(1000, 1),\n", + ")\n", + "jes_lb = qJointEntropySearch(\n", + " model=model,\n", + " optimal_inputs=optimal_inputs,\n", + " optimal_outputs=optimal_outputs,\n", + " estimation_type=\"LB\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ec4692e9", + "metadata": {}, + "source": [ + "To illustrate the acquisition functions, we evaluate it over the whole input space and plot it. As described in [3], the JES should be an upper bound to both the PES and MES, although the estimates might not be." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "382e37f4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# the acquisition function call takes a three-dimensional tensor\n", + "fwd_X = X.unsqueeze(-1).unsqueeze(-1)\n", + "\n", + "# make the acquisition functions live on the same scale\n", + "scale_acqvals = True\n", + "\n", + "pes_X = pes(fwd_X).detach().numpy()\n", + "mes_lb_X = mes_lb(fwd_X).detach().numpy()\n", + "jes_lb_X = jes_lb(fwd_X).detach().numpy()\n", + "\n", + "if scale_acqvals:\n", + " pes_X = pes_X / pes_X.max()\n", + " mes_lb_X = mes_lb_X / mes_lb_X.max()\n", + " jes_lb_X = jes_lb_X / jes_lb_X.max()\n", + "\n", + "plt.plot(X, pes_X, color=\"mediumseagreen\", linewidth=3, label=\"PES\")\n", + "plt.plot(X, mes_lb_X, color=\"crimson\", linewidth=3, label=\"MES-LB\")\n", + "plt.plot(X, jes_lb_X, color=\"dodgerblue\", linewidth=3, label=\"JES-LB\")\n", + "\n", + "plt.vlines(\n", + " X[pes_X.argmax()], 0, 1, color=\"mediumseagreen\", linewidth=1.5, linestyle=\"--\"\n", + ")\n", + "plt.vlines(X[mes_lb_X.argmax()], 0, 1, color=\"crimson\", linewidth=1.5, linestyle=\":\")\n", + "plt.vlines(\n", + " X[jes_lb_X.argmax()], 0, 1, color=\"dodgerblue\", linewidth=1.5, linestyle=\"--\"\n", + ")\n", + "plt.legend(fontsize=15)\n", + "plt.xlabel(\"$x$\", fontsize=15)\n", + "plt.ylabel(r\"$\\alpha(x)$\", fontsize=15)\n", + "plt.title(\"Entropy-based acquisition functions\", fontsize=15)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3ce0f584", + "metadata": {}, + "source": [ + "To maximize the acquisition function in a standard Bayesian optimization loop, we can use the standard optimization routines. Note that the PES acquisition function might not be differentiable since some operations that may arise during expectation propagation are not differentiable. Therefore, we use a finite difference approach to optimize this acquisition function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7f639bb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "# Use finite difference for PES\n", + "candidate, acq_value = optimize_acqf(\n", + " acq_function=pes,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=4,\n", + " raw_samples=256,\n", + " options={\"with_grad\": False},\n", + ")\n", + "print(\"PES: candidate={}, acq_value={}\".format(candidate, acq_value))\n", + "\n", + "candidate, acq_value = optimize_acqf(\n", + " acq_function=mes_lb,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=4,\n", + " raw_samples=256,\n", + ")\n", + "print(\"MES-LB: candidate={}, acq_value={}\".format(candidate, acq_value))\n", + "\n", + "candidate, acq_value = optimize_acqf(\n", + " acq_function=jes_lb,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=4,\n", + " raw_samples=256,\n", + ")\n", + "print(\"JES-LB: candidate={}, acq_value={}\".format(candidate, acq_value))" + ] + }, + { + "cell_type": "markdown", + "id": "e95f0846", + "metadata": {}, + "source": [ + "# 2. Multi-objective batch example " + ] + }, + { + "cell_type": "markdown", + "id": "57237806", + "metadata": {}, + "source": [ + "In this section, we illustrate a simple multi-objective example. First we generate some data and fit the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fabc86e9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from botorch.acquisition.multi_objective.utils import (\n", + " compute_sample_box_decomposition,\n", + " random_search_optimizer,\n", + " sample_optimal_points,\n", + ")\n", + "from botorch.test_functions.multi_objective import ZDT1\n", + "\n", + "d = 4\n", + "M = 2\n", + "n = 8\n", + "\n", + "if SMOKE_TEST:\n", + " q = 2\n", + "else:\n", + " q = 4" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34787908", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "problem = ZDT1(dim=d, num_objectives=M, noise_std=0, negate=True)\n", + "bounds = problem.bounds.to(**tkwargs)\n", + "\n", + "train_X = draw_sobol_samples(bounds=bounds, n=n, q=1, seed=123).squeeze(-2)\n", + "train_Y = problem(train_X)\n", + "\n", + "model = fit_model(train_X=train_X, train_Y=train_Y, num_outputs=M)" + ] + }, + { + "cell_type": "markdown", + "id": "b7174710", + "metadata": {}, + "source": [ + "We now obtain Monte Carlo samples of the optimal inputs and outputs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56bd5f5a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "num_pareto_samples = 8\n", + "num_pareto_points = 8\n", + "\n", + "# We set the parameters for the random search\n", + "optimizer_kwargs = {\n", + " \"pop_size\": 500,\n", + " \"max_tries\": 10,\n", + "}\n", + "\n", + "ps, pf = sample_optimal_points(\n", + " model=model,\n", + " bounds=bounds,\n", + " num_samples=num_pareto_samples,\n", + " num_points=num_pareto_points,\n", + " optimizer=random_search_optimizer,\n", + " optimizer_kwargs=optimizer_kwargs,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "76c35b23", + "metadata": {}, + "source": [ + "We initialize the acquisition functions as before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c7dfaf0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from botorch.acquisition.multi_objective.joint_entropy_search import (\n", + " qLowerBoundMultiObjectiveJointEntropySearch,\n", + ")\n", + "from botorch.acquisition.multi_objective.max_value_entropy_search import (\n", + " qLowerBoundMultiObjectiveMaxValueEntropySearch,\n", + ")\n", + "from botorch.acquisition.multi_objective.predictive_entropy_search import (\n", + " qMultiObjectivePredictiveEntropySearch,\n", + ")\n", + "\n", + "pes = qMultiObjectivePredictiveEntropySearch(model=model, pareto_sets=ps)\n", + "\n", + "# Compute the box-decomposition\n", + "hypercell_bounds = compute_sample_box_decomposition(pf)\n", + "\n", + "# # Here we use the lower bound estimates for the MES and JES\n", + "mes_lb = qLowerBoundMultiObjectiveMaxValueEntropySearch(\n", + " model=model,\n", + " hypercell_bounds=hypercell_bounds,\n", + " estimation_type=\"LB\",\n", + ")\n", + "\n", + "jes_lb = qLowerBoundMultiObjectiveJointEntropySearch(\n", + " model=model,\n", + " pareto_sets=ps,\n", + " pareto_fronts=pf,\n", + " hypercell_bounds=hypercell_bounds,\n", + " estimation_type=\"LB\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6a6d071b", + "metadata": {}, + "source": [ + "We now optimize the batch acquistion functions. For the batch PES, we optimize the batch acquisition function directly. Whereas for the MES and JES we use a sequential optimization strategy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ceac58f5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "# Use finite difference for PES. This may take some time\n", + "candidates, acq_values = optimize_acqf(\n", + " acq_function=pes,\n", + " bounds=bounds,\n", + " q=q,\n", + " num_restarts=4,\n", + " raw_samples=512,\n", + " options={\"with_grad\": False},\n", + ")\n", + "print(\"PES: \\ncandidates={}\".format(candidates))\n", + "\n", + "# Sequentially greedy optimization\n", + "candidates, acq_values = optimize_acqf(\n", + " acq_function=mes_lb,\n", + " bounds=bounds,\n", + " q=q,\n", + " num_restarts=4,\n", + " raw_samples=512,\n", + " sequential=True,\n", + ")\n", + "print(\"MES-LB: \\ncandidates={}\".format(candidates))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9281308", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Sequentially greedy optimization\n", + "candidates, acq_values = optimize_acqf(\n", + " acq_function=jes_lb,\n", + " bounds=bounds,\n", + " q=q,\n", + " num_restarts=4,\n", + " raw_samples=512,\n", + " sequential=True,\n", + ")\n", + "print(\"JES-LB: \\ncandidates={}\".format(candidates))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/website-old/static/files/information_theoretic_acquisition_functions.py b/website-old/static/files/information_theoretic_acquisition_functions.py new file mode 100644 index 0000000000..e9815a57b5 --- /dev/null +++ b/website-old/static/files/information_theoretic_acquisition_functions.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# # Information-theoretic acquisition functions + +# This notebook illustrates the use of some information-theoretic acquisition functions in BoTorch for single and multi-objective optimization. We present a single-objective example in section 1 and a multi-objective example in section 2. Before introducing these examples, we present an overview on the different approaches and how they are estimated. + +# ## Notation +# +# We consider the problem of maximizing a function $f: \mathbb{X} \rightarrow \mathbb{R}^M$. In the single-objective setting ($M=1$), the maximum is defined as usual with respect to the total ordering over the real numbers. In the multi-objective setting ($M>1$), the maximum is defined with respect to the Pareto partial ordering over vectors. By an abuse in notation, we denote the optimal set of inputs and outputs by +# +# $$\mathbb{X}^* = \text{arg}\max_{\mathbf{x} \in \mathbb{X}} f(\mathbf{x}) \subseteq \mathbb{X} \quad \text{and} \quad \mathbb{Y}^* = f(\mathbb{X}^*) = \max_{\mathbf{x} \in \mathbb{X}} f(\mathbf{x}) \subset \mathbb{R}^M,$$ +# +# respectively for both the single and multi-objective setting. We denote the collection of optimal input-output pairs by $(\mathbb{X}^*, \mathbb{Y}^*)$. + +# ## Information-theoretic acquisition functions +# +# Information-theoretic (IT) acquisition functions work by quantifying the utility of an input $\mathbf{x} \in \mathbb{X}$ based on how "informative" the corresponding observation $\mathbf{y} \in \mathbb{R}^M$ will be in learning more about the distribution of some statistic of the function $S(f)$. Here, we define the notion of information via the mutual information ($\text{MI}$): +# +# \begin{equation} +# \alpha^{\text{IT}}(\mathbf{x}|D_n) +# = \text{MI}(\mathbf{y}; S(f)| \mathbf{x}, D_n) +# = H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p(S(f)|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, S(f)]], +# \end{equation} +# +# where $D_n = \{(\mathbf{x}_t, \mathbf{y}_t)\}_{t=1,\dots,n}$ denotes the data set of sampled inputs and observations and the function $H$ denotes the differential entropy $H[p(\mathbf{x})] = - \int p(\mathbf{x}) \log(p(\mathbf{x})) d\mathbf{x}$. The main difference between existing information-theoretic acquisition functions in the literature is the choice of statistic $S$ and the modelling assumptions that are made in order to estimate the resulting acquisition function. In this notebook, we focus on three particular cases of information-theoretic acquisition functions: + +# ### Predictive Entropy Search (PES) +# +# The PES acquisition function [1] considers the problem of learning more about the distribution of the optimal inputs: $S(f) = \mathbb{X}^*$. +# +# \begin{equation} +# \alpha^{\text{PES}}(\mathbf{x}|D_n) +# = \text{MI}(\mathbf{y}; \mathbb{X}^*| \mathbf{x}, D_n) +# = H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p(\mathbb{X}^*|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{X}^*)]]. +# \end{equation} + +# ### Max-value Entropy Search (MES) +# +# The MES acquisition function [2] considers the problem of learning more about the distribution of the optimal outputs: $S(f) = \mathbb{Y}^*$. +# +# \begin{equation} +# \alpha^{\text{MES}}(\mathbf{x}|D_n) +# = \text{MI}(\mathbf{y}; \mathbb{Y}^*| \mathbf{x}, D_n) +# = H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p(\mathbb{Y}^*|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{Y}^*)]]. +# \end{equation} +# + +# ### Joint Entropy Search (JES) +# +# The JES acquisition function [3] considers the problem of learning more about the distribution of the optimal inputs and outputs: $S(f) = (\mathbb{X}^*, \mathbb{Y}^*)$. +# +# \begin{equation} +# \alpha^{\text{JES}}(\mathbf{x}|D_n) +# = \text{MI}(\mathbf{y}; (\mathbb{X}^*, \mathbb{Y}^*)| \mathbf{x}, D_n) +# = H[p(\mathbf{y}|D_n)] - \mathbb{E}_{p((\mathbb{X}^*, \mathbb{Y}^*)|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, (\mathbb{X}^*, \mathbb{Y}^*))]]. +# \end{equation} + +# ## Estimation +# +# In order to estimate the three acquistion functions listed above, we make two simplfying assumptions: +# +# **[Assumption 1]** We assume an independent Gaussian process prior on each objective function. +# +# **[Assumption 2]** We assume a Gaussian observation likelihood. + +# ### First term +# +# Under the modelling assumptions, the first term in each of the acquisition functions is an entropy of a Gaussian random variable, which is analytically tractable. +# +# ### Second term +# +# The second term in each of the acquisition functions is an expectation of an entropy over an intractable distribution. The expectation can be estimated using Monte Carlo, whilst the entropy has to be approximated using different strategies such as moment-matching. +# +# **Monte Carlo.** To sample from the distribution over the optimal points, we can first (approximately) sample a collection of posterior paths $f_j \sim p(f|D_n)$ and then optimize them to obtain the sample of optimal points $(\mathbb{X}^*_j, \mathbb{Y}^*_j)$ for $j=1,\dots,J$. +# +# **PES entropy estimate.** In `qPredictiveEntropySearch` and `qMultiObjectivePredictiveEntropySearch`, we approximate the entropy term arising in PES using the expectation propagation strategy described in [4]. In particular, we first relax the global optimality condition: +# +# \begin{align} +# H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{X}^*)] +# &\overset{(1)}{=} H[p(\mathbf{y}| \mathbf{x}, D_n, f(\mathbb{X}) \preceq f(\mathbb{X}^*))] +# \\\\ +# &\overset{(2)}{\leq} H[p(\mathbf{y}| \mathbf{x}, D_n, f(X_n \cup \{\mathbf{x}\}) \preceq f(\mathbb{X}^*))]. +# \end{align} +# +# (1) This statement follows from the observation that conditioning on the optimal points $\mathbb{X}^*$ is equivalent to knowing that all points lie below the objective values at the optimal inputs: $f(\mathbb{X}) \preceq f(\mathbb{X}^*)$. +# +# (2) We replace the global optimality condition with the local optimality condition: $f(X_n \cup \{\mathbf{x}\}) \preceq f(\mathbb{X}^*)$, where $X_n = \{\mathbf{x}_t\}_{t=1,\dots,n}$. . The upper bound follows from the standard result that conditioning on more information only decreases the entropy: $H(A|B) \leq H(A)$ for any random variables $A$ and $B$. +# +# We then estimate the resulting lower bound of the PES acquisition function by approximating the intractable distribution $p(\mathbf{y}| \mathbf{x}, D_n, f(X_n \cup \{\mathbf{x}\}) \preceq f(\mathbb{X}^*))$ with a product of Gaussian random variables, which is fitted via an iterative moment-matching procedure known as expectation propagation. The entropy of this resulting distribution can then be computed analytically. +# +# **MES and JES entropy estimate.** In `qLowerBoundMultiObjectiveMaxValueEntropySearch`, `qLowerBoundJointEntropySearch` and `qLowerBoundMultiObjectiveJointEntropySearch`, we approximate the entropy term arising in MES and JES using the strategies described in [3]. These estimates rely on different upper bounds of the entropy term, which results in different lower bounds for the mutual information. These estimates are motivated by the following chain inequalities for the entropy in the JES expression: +# +# \begin{align} +# H[p(\mathbf{y}| \mathbf{x}, D_n, (\mathbb{X}^*, \mathbb{Y}^*))] +# &\overset{(1)}{=} H[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbb{X}) \preceq \mathbb{Y}^*)] +# \\\\ +# &\overset{(2)}{\leq} H[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbf{x}) \preceq \mathbb{Y}^*)] +# \\\\ +# &\overset{(3)}{\leq} H[\mathcal{N}(\mathbf{y}| \mathbf{m}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))}, \mathbf{V}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))})] +# \\\\ +# &\overset{(4)}{\leq} H[\mathcal{N}(\mathbf{y}| \mathbf{m}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))}, \text{diag}(\mathbf{V}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))}))], +# \end{align} +# +# where +# +# \begin{align} +# \mathbf{m}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))} = \mathbb{E}[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbf{x}) \preceq \mathbb{Y}^*)] +# \end{align} +# +# \begin{align} +# \mathbf{V}_{(\mathbf{x}, (\mathbb{X}^*, \mathbb{Y}^*))} = \mathbb{C}\text{ov}[p(\mathbf{y}| \mathbf{x}, D_n \cup (\mathbb{X}^*, \mathbb{Y}^*), f(\mathbf{x}) \preceq \mathbb{Y}^*)]. +# \end{align} +# +# (1) This statement follows from the observation that conditioning on the optimal points $(\mathbb{X}^*, \mathbb{Y}^*)$ is equivalent to knowing that $\mathbb{X}^*$ maps to $\mathbb{Y}^*$ and that all points lie below the Pareto front, $f(\mathbb{X}) \preceq f(\mathbb{X}^*) = \mathbb{Y}^*$. +# +# (2) We replace the global optimality condition with the local optimality condition: $f(\mathbf{x}) \preceq \mathbb{Y}^*$. The upper bound follows from the standard result that conditioning on more information only decreases the entropy: $H(A|B) \leq H(A)$ for any random variables $A$ and $B$. +# +# (3) We upper bound the entropy using the standard result that the multivariate Gaussian distribution has the maximum entropy over all distributions supported on $\mathbb{R}^M$ with the same first two moments. +# +# (4) We upper bound the entropy by again using the standard result that conditioning on more information only decreases the entropy. +# +# **(Conditioning)** A similar chain of inequalities can be obtained for the entropy in the MES term by replacing the augmented data set $D_n \cup (\mathbb{X}^*, \mathbb{Y}^*)$ with the original data set $D_n$. The only real difference between the JES and MES estimate is whether we condition on the extra samples $(\mathbb{X}^*_j, \mathbb{Y}^*_j)$ or not for $j=1,\dots,J$. As a result of this conditioning, the JES estimate can be more expensive than the MES estimate. +# +# **(Noiseless setting)** When the observations are exact, $\mathbf{y} = f(\mathbf{x})$, then the entropy term in (2) can be computed exactly. By setting `estimation_type="0"`, we use this estimate. In the setting where there is observation noise, the estimate also includes an ad-hoc correction which can be useful (more details in the appendix of [3]). +# +# **(Monte Carlo)** The entropy term in (2) can be estimated using Monte Carlo because the distribution has a tractable density under the assumptions. By setting `estimation_type="MC"`, we use this Monte Carlo estimate. +# +# **(Lower bound)** The entropy term in (3) and (4) can be computed exactly. By setting `estimation_type="LB"`, we use this lower bound estimate in (3). By setting `estimation_type="LB2"`, we use lower bound estimate in (4). + +# ### Batch +# +# For the batch setting, the first term is again analytically tractable. The second term can be estimated using Monte Carlo, whilst the entropy term again has to be estimated. +# +# **PES entropy estimate.** In `qPredictiveEntropySearch` and `qMultiObjectivePredictiveEntropySearch`, the entropy term is again approximated using expectation propagation. In particular, we approximate $p(Y| X, D_n, f(X_n \cup X) \preceq f(\mathbb{X}^*))$ with a product of Gaussian random variables. +# +# **MES and JES entropy estimate** In `qLowerBoundMultiObjectiveMaxValueEntropySearch`, `qLowerBoundJointEntropySearch` and `qLowerBoundMultiObjectiveJointEntropySearch`, we approximate a lower bound to the MES and JES acquisition function: +# +# \begin{equation} +# \alpha^{\text{LB-MES}}(X|D_n) +# = \text{MI}(Y; \mathbb{Y}^*| X, D_n) +# = H[p(Y|D_n)] - \sum_{\mathbf{x} \in X} \mathbb{E}_{p(\mathbb{Y}^*|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, \mathbb{Y}^*)]], +# \end{equation} +# +# \begin{equation} +# \alpha^{\text{LB-JES}}(X|D_n) +# = \text{MI}(Y; (\mathbb{X}^*, \mathbb{Y}^*)| X, D_n) +# = H[p(Y|D_n)] - \sum_{\mathbf{x} \in X} \mathbb{E}_{p((\mathbb{X}^*, \mathbb{Y}^*)|D_n)}[H[p(\mathbf{y}| \mathbf{x}, D_n, (\mathbb{X}^*, \mathbb{Y}^*))]]. +# \end{equation} +# +# The advantage of these expressions is that it allows us to take advantage of the existing entropy estimates for the sequential setting. + +# ## References + +# [1] J.M. Hernández-Lobato, M.W. Hoffman and Z. Ghahramani, [**Predictive Entropy Search for Efficient Global Optimization of Black-box Functions**](https://arxiv.org/abs/1406.2541), NeurIPS, 2014. +# +# [2] Z. Wang and S. Jegelka, [**Max-value Entropy Search for Efficient Bayesian Optimization**](https://arxiv.org/abs/1703.01968), ICML, 2017. +# +# [3] B. Tu, A. Gandy, N. Kantas and B. Shafei, [**Joint Entropy Search for Multi-Objective Bayesian Optimization**](https://arxiv.org/abs/2210.02905), NeurIPS, 2022. +# +# [4] C. Hvarfner, F. Hutter and N. Nardi, [**Joint Entropy Search for Maximally-Informed Bayesian Optimization**](https://arxiv.org/abs/2206.04771), NeurIPS, 2022. +# +# [5] E. Garrido-Merchán and D. Hernández-Lobato, [**Predictive Entropy Search for Multi-objective Bayesian Optimization with Constraints**](https://www.sciencedirect.com/science/article/abs/pii/S0925231219308525), Neurocomputing, 2019. + +# # 1. Single-objective example + +# In this section, we present a simple example in one-dimension with one objective to illustrate the use of these acquisition functions. We first define the objective function. + +# In[ ]: + + +import os + +import matplotlib.pyplot as plt +import numpy as np +import torch +from botorch.fit import fit_gpytorch_mll +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.transforms.outcome import Standardize +from botorch.utils.sampling import draw_sobol_samples +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood + +SMOKE_TEST = os.environ.get("SMOKE_TEST") +tkwargs = {"dtype": torch.double, "device": "cpu"} + + +def f(x): + p1 = torch.cos(torch.pi * x) + p2 = 10 * torch.sin(torch.pi * x) + p3 = 2 * torch.sin(2 * torch.pi * x) + p4 = 2 * torch.sin(6 * torch.pi * x) + return p1 + p2 + p3 + p4 + + +bounds = torch.tensor([[0.0], [1.0]], **tkwargs) + + +# We now generate some data and then fit the Gaussian process model. + +# In[ ]: + + +torch.manual_seed(0) +np.random.seed(0) +n = 5 +train_X = draw_sobol_samples(bounds=bounds, n=n, q=1, seed=12345678).squeeze(-2) +train_Y = f(train_X) + + +def fit_model(train_X, train_Y, num_outputs): + model = SingleTaskGP(train_X, train_Y, outcome_transform=Standardize(m=num_outputs)) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + return model + + +model = fit_model(train_X=train_X, train_Y=train_Y, num_outputs=1) + + +# We now plot the objective function and the model. + +# In[ ]: + + +X = torch.linspace(bounds[0, 0], bounds[1, 0], 1000, **tkwargs) +mean_fX = model.posterior(X).mean.squeeze(-1).detach().numpy() +std_fX = torch.sqrt(model.posterior(X).variance).squeeze(-1).detach().numpy() + +plt.scatter(train_X, train_Y, color="k", label="Observations") +plt.plot(X, f(X), color="k", linewidth=2, label="Objective function") +plt.plot(X, mean_fX, color="dodgerblue", linewidth=3, label="Posterior model") +plt.fill_between( + X, (mean_fX + 3 * std_fX), (mean_fX - 3 * std_fX), alpha=0.2, color="dodgerblue" +) +plt.xlabel("x", fontsize=15) +plt.ylabel("y", fontsize=15) +plt.legend(fontsize=15) +plt.show() + + +# To compute the information-theoretic acquisition functions, we first need to get some Monte Carlo samples of the optimal inputs and outputs. The method `sample_optimal_points` generates `num_samples` approximate samples of the Gaussian process model and optimizes them sequentially using an optimizer. In the single-objective setting, the number of optimal points (`num_points`) should be set to one. For simplicitly, we consider optimization via random search. + +# In[ ]: + + +from botorch.acquisition.utils import get_optimal_samples + +num_samples = 12 + +optimal_inputs, optimal_outputs = get_optimal_samples( + model, bounds=bounds, num_optima=num_samples +) + + +# We now initialize the information-theoretic acquisition functions. The PES can simply be initialized using just the optimal set of inputs. For the MES and JES acquisition function, we also have to specify the region of integration, which is $\{\mathbf{y}: \mathbf{y} \preceq \mathbb{Y}^*\}$ for a maximization problem. This is done by providing a Tensor of bounds, which is obtained via the method `compute_sample_box_decomposition`. +# +# Note that for the MES algorithm, we use the multi-objective implementation `qLowerBoundMultiObjectiveMaxValueEntropySearch`, which implements all the estimation types into one acquisition function. BoTorch alreadys supports many other strategies to estimate the single-objective MES algorithms in `botorch.acquisition.max_value_entropy`, which is described in the other complementary notebooks. + +# In[ ]: + + +from botorch.acquisition.joint_entropy_search import qJointEntropySearch +from botorch.acquisition.max_value_entropy_search import qLowerBoundMaxValueEntropy +from botorch.acquisition.predictive_entropy_search import qPredictiveEntropySearch + +pes = qPredictiveEntropySearch(model=model, optimal_inputs=optimal_inputs) + +# Here we use the lower bound estimates for the MES and JES +# Note that the single-objective MES interface is slightly different, +# as it utilizes the Gumbel max-value approximation internally and +# therefore does not take the max values as input. +mes_lb = qLowerBoundMaxValueEntropy( + model=model, + candidate_set=torch.rand(1000, 1), +) +jes_lb = qJointEntropySearch( + model=model, + optimal_inputs=optimal_inputs, + optimal_outputs=optimal_outputs, + estimation_type="LB", +) + + +# To illustrate the acquisition functions, we evaluate it over the whole input space and plot it. As described in [3], the JES should be an upper bound to both the PES and MES, although the estimates might not be. + +# In[ ]: + + +# the acquisition function call takes a three-dimensional tensor +fwd_X = X.unsqueeze(-1).unsqueeze(-1) + +# make the acquisition functions live on the same scale +scale_acqvals = True + +pes_X = pes(fwd_X).detach().numpy() +mes_lb_X = mes_lb(fwd_X).detach().numpy() +jes_lb_X = jes_lb(fwd_X).detach().numpy() + +if scale_acqvals: + pes_X = pes_X / pes_X.max() + mes_lb_X = mes_lb_X / mes_lb_X.max() + jes_lb_X = jes_lb_X / jes_lb_X.max() + +plt.plot(X, pes_X, color="mediumseagreen", linewidth=3, label="PES") +plt.plot(X, mes_lb_X, color="crimson", linewidth=3, label="MES-LB") +plt.plot(X, jes_lb_X, color="dodgerblue", linewidth=3, label="JES-LB") + +plt.vlines( + X[pes_X.argmax()], 0, 1, color="mediumseagreen", linewidth=1.5, linestyle="--" +) +plt.vlines(X[mes_lb_X.argmax()], 0, 1, color="crimson", linewidth=1.5, linestyle=":") +plt.vlines( + X[jes_lb_X.argmax()], 0, 1, color="dodgerblue", linewidth=1.5, linestyle="--" +) +plt.legend(fontsize=15) +plt.xlabel("$x$", fontsize=15) +plt.ylabel(r"$\alpha(x)$", fontsize=15) +plt.title("Entropy-based acquisition functions", fontsize=15) +plt.show() + + +# To maximize the acquisition function in a standard Bayesian optimization loop, we can use the standard optimization routines. Note that the PES acquisition function might not be differentiable since some operations that may arise during expectation propagation are not differentiable. Therefore, we use a finite difference approach to optimize this acquisition function. + +# In[ ]: + + +from botorch.optim import optimize_acqf + +# Use finite difference for PES +candidate, acq_value = optimize_acqf( + acq_function=pes, + bounds=bounds, + q=1, + num_restarts=4, + raw_samples=256, + options={"with_grad": False}, +) +print("PES: candidate={}, acq_value={}".format(candidate, acq_value)) + +candidate, acq_value = optimize_acqf( + acq_function=mes_lb, + bounds=bounds, + q=1, + num_restarts=4, + raw_samples=256, +) +print("MES-LB: candidate={}, acq_value={}".format(candidate, acq_value)) + +candidate, acq_value = optimize_acqf( + acq_function=jes_lb, + bounds=bounds, + q=1, + num_restarts=4, + raw_samples=256, +) +print("JES-LB: candidate={}, acq_value={}".format(candidate, acq_value)) + + +# # 2. Multi-objective batch example + +# In this section, we illustrate a simple multi-objective example. First we generate some data and fit the model. + +# In[ ]: + + +from botorch.acquisition.multi_objective.utils import ( + compute_sample_box_decomposition, + random_search_optimizer, + sample_optimal_points, +) +from botorch.test_functions.multi_objective import ZDT1 + +d = 4 +M = 2 +n = 8 + +if SMOKE_TEST: + q = 2 +else: + q = 4 + + +# In[ ]: + + +problem = ZDT1(dim=d, num_objectives=M, noise_std=0, negate=True) +bounds = problem.bounds.to(**tkwargs) + +train_X = draw_sobol_samples(bounds=bounds, n=n, q=1, seed=123).squeeze(-2) +train_Y = problem(train_X) + +model = fit_model(train_X=train_X, train_Y=train_Y, num_outputs=M) + + +# We now obtain Monte Carlo samples of the optimal inputs and outputs. + +# In[ ]: + + +num_pareto_samples = 8 +num_pareto_points = 8 + +# We set the parameters for the random search +optimizer_kwargs = { + "pop_size": 500, + "max_tries": 10, +} + +ps, pf = sample_optimal_points( + model=model, + bounds=bounds, + num_samples=num_pareto_samples, + num_points=num_pareto_points, + optimizer=random_search_optimizer, + optimizer_kwargs=optimizer_kwargs, +) + + +# We initialize the acquisition functions as before. + +# In[ ]: + + +from botorch.acquisition.multi_objective.joint_entropy_search import ( + qLowerBoundMultiObjectiveJointEntropySearch, +) +from botorch.acquisition.multi_objective.max_value_entropy_search import ( + qLowerBoundMultiObjectiveMaxValueEntropySearch, +) +from botorch.acquisition.multi_objective.predictive_entropy_search import ( + qMultiObjectivePredictiveEntropySearch, +) + +pes = qMultiObjectivePredictiveEntropySearch(model=model, pareto_sets=ps) + +# Compute the box-decomposition +hypercell_bounds = compute_sample_box_decomposition(pf) + +# # Here we use the lower bound estimates for the MES and JES +mes_lb = qLowerBoundMultiObjectiveMaxValueEntropySearch( + model=model, + hypercell_bounds=hypercell_bounds, + estimation_type="LB", +) + +jes_lb = qLowerBoundMultiObjectiveJointEntropySearch( + model=model, + pareto_sets=ps, + pareto_fronts=pf, + hypercell_bounds=hypercell_bounds, + estimation_type="LB", +) + + +# We now optimize the batch acquistion functions. For the batch PES, we optimize the batch acquisition function directly. Whereas for the MES and JES we use a sequential optimization strategy. + +# In[ ]: + + +get_ipython().run_cell_magic('time', '', '# Use finite difference for PES. This may take some time\ncandidates, acq_values = optimize_acqf(\n acq_function=pes,\n bounds=bounds,\n q=q,\n num_restarts=4,\n raw_samples=512,\n options={"with_grad": False},\n)\nprint("PES: \\ncandidates={}".format(candidates))\n\n# Sequentially greedy optimization\ncandidates, acq_values = optimize_acqf(\n acq_function=mes_lb,\n bounds=bounds,\n q=q,\n num_restarts=4,\n raw_samples=512,\n sequential=True,\n)\nprint("MES-LB: \\ncandidates={}".format(candidates))\n') + + +# In[ ]: + + +# Sequentially greedy optimization +candidates, acq_values = optimize_acqf( + acq_function=jes_lb, + bounds=bounds, + q=q, + num_restarts=4, + raw_samples=512, + sequential=True, +) +print("JES-LB: \ncandidates={}".format(candidates)) + diff --git a/website-old/static/files/max_value_entropy.ipynb b/website-old/static/files/max_value_entropy.ipynb new file mode 100644 index 0000000000..d52b179021 --- /dev/null +++ b/website-old/static/files/max_value_entropy.ipynb @@ -0,0 +1,306 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The max value entropy search acquisition function\n", + "\n", + "Max-value entropy search (MES) acquisition function quantifies the information gain about the maximum of a black-box function by observing this black-box function $f$ at the candidate set $\\{\\textbf{x}\\}$ (see [1, 2]). BoTorch provides implementations of the MES acquisition function and its multi-fidelity (MF) version with support for trace observations. In this tutorial, we explain at a high level how the MES acquisition function works, its implementation in BoTorch and how to use the MES acquisition function to query the next point in the optimization process. \n", + "\n", + "In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. You can use a custom BoTorch model and acquisition function in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. To use the MES acquisition function, it is sufficient to add `\"botorch_acqf_class\": qMaxValueEntropy,` to `model_kwargs`. The linked tutorial shows how to use a custom BoTorch model. If you'd like to let Ax choose which model to use based on the properties of the search space, you can skip the `surrogate` argument in `model_kwargs`.\n", + "\n", + "### 1. MES acquisition function for $q=1$ with noisy observation\n", + "For illustrative purposes, we focus in this section on the non-q-batch-mode case ($q=1$). We also assume that the evaluation of the black-box function is noisy. Let us first introduce some notation: \n", + "+ $f^* = \\max_\\mathcal{X} (f(\\textbf{x}))$, the maximum of the black-box function $f(\\textbf{x})$ in the design space $\\mathcal{X}$\n", + "+ $y = f(\\textbf{x}) + \\epsilon, \\epsilon \\sim N(0, \\sigma^2_\\epsilon)$, the noisy observation at the design point $\\textbf{x}$\n", + "+ $h(Y) = \\mathbb{E}_Y[-\\log(p(y))] = -\\int_\\mathcal{Y} p(y)\\log p(y) dy$, the differential entropy of random variable $Y$ with support $\\mathcal{Y}$: the larger is $h(Y)$, the larger is the uncertainty of $Y$.\n", + "+ $v(\\mathcal{D}) = -\\mathbb{E}_D[h(F^*\\mid\\mathcal{D})]$, the value of data set $\\mathcal{D}$, where $F^*$ denotes the function maximum (a random variable in our context of our model).\n", + "\n", + "\n", + "The Max-value Entropy Search (MES) acquisition function at $\\textbf{x}$ after observing $\\mathcal{D}_t$ can be written as\n", + "\\begin{align}\n", + " \\alpha_{\\text{MES}}(\\textbf{x}) \n", + " &= v(\\mathcal{D}_t\\cup \\{(\\textbf{x}, y)\\}) - v(\\mathcal{D}_t) \\\\\n", + " &= - \\mathbb{E}_Y[h(F^* \\mid \\mathcal{D}_t\\cup \\{(\\textbf{x}, Y)\\})] + h(F^*\\mid\\mathcal{D}_t) \\\\\n", + " &= - \\mathbb{E}_Y[h(F^* \\mid Y)] + h(F^*) \\\\\n", + " &= I(F^*; Y) \\\\\n", + " &= I(Y; F^*) \\quad \\text{(symmetry)} \\\\\n", + " &= - \\mathbb{E}_{F^*}[h(Y \\mid F^*)] + h(Y) \\\\ \n", + "\\end{align}\n", + ", which is the mutual information of random variables \n", + "$F^*\\mid \\mathcal{D}_t$ and $Y \\mid \\textbf{x}, \\mathcal{D}_t$. \n", + "Here $F^*$ follows the max value distribution conditioned on $\\mathcal{D}_t$, and $Y$ follows the GP posterior distribution with noise at $\\textbf{x}$ after observing $\\mathcal{D}_t$.\n", + "\n", + "Rewrite the above formula as\n", + "\\begin{align}\n", + " \\alpha_{\\text{MES}}(\\textbf{x}) &= - H_1 + H_0, \\\\\n", + " H_0 &= h(Y) = \\log \\left(\\sqrt{2\\pi e (\\sigma_f^2 + \\sigma_\\epsilon^2)}\\right) \\\\\n", + " H_1 &= \\mathbb{E}_{F^*}[h(Y \\mid F^*)] \\\\\n", + " &\\simeq \\frac{1}{\\left|\\mathcal{F}_*\\right|} \\Sigma_{\\mathcal{F}_*} h(Y\\mid f^*))\n", + "\\end{align}\n", + ", where $\\mathcal{F}_*$ are the max value samples drawn from the posterior after observing $\\mathcal{D}_t$. Without noise, $p(y \\mid f^*) = p(f \\mid f \\leq f^*)$ is a truncated normal distribution with an analytic expression for its entropy. With noise, $Y\\mid F\\leq f^*$ is not a truncated normal distribution anymore. The question is then how to compute $h(Y\\mid f^*)$ or equivalently $p(y\\mid f \\leq f^*)$?\n", + "\n", + "\n", + "Using Bayes' theorem, \n", + "\\begin{align}\n", + " p(y\\mid f \\leq f^*) = \\frac{P(f \\leq f^* \\mid y) p(y)}{P(f \\leq f^* )}\n", + "\\end{align}\n", + ", where \n", + "+ $p(y)$ is the posterior probability density function (PDF) with observation noise.\n", + "+ $P(f \\leq f^*)$ is the posterior cummulative distribution function (CDF) without observation noise, given any $f^*$.\n", + "\n", + "We also know from the GP predictive distribution\n", + "\\begin{align}\n", + " \\begin{bmatrix}\n", + " y \\\\ f\n", + " \\end{bmatrix}\n", + " \\sim \\mathcal{N} \\left(\n", + " \\begin{bmatrix}\n", + " \\mu \\\\ \\mu\n", + " \\end{bmatrix} , \n", + " \\begin{bmatrix}\n", + " \\sigma_f^2 + \\sigma_\\epsilon^2 & \\sigma_f^2 \\\\ \n", + " \\sigma_f^2 & \\sigma_f^2\n", + " \\end{bmatrix}\n", + " \\right).\n", + "\\end{align}\n", + "So\n", + "\\begin{align}\n", + " f \\mid y \\sim \\mathcal{N} (u, s^2)\n", + "\\end{align}\n", + ", where\n", + "\\begin{align}\n", + " u &= \\frac{\\sigma_f^2(y-\\mu)}{\\sigma_f^2 + \\sigma_\\epsilon^2} + \\mu \\\\\n", + " s^2 &= \\sigma_f^2 - \\frac{(\\sigma_f^2)^2}{\\sigma_f^2 + \\sigma_\\epsilon^2}\n", + " = \\frac{\\sigma_f^2\\sigma_\\epsilon^2}{\\sigma_f^2 + \\sigma_\\epsilon^2}\n", + "\\end{align}\n", + "Thus, $P(f \\leq f^* \\mid y)$ is the CDF of above Gaussian. \n", + "\n", + "Finally, given $f^*$, we have \n", + "\\begin{align}\n", + " h(Y \\mid f^*) \n", + " &= -\\int_\\mathcal{Y} p(y \\mid f^*)\\log(p(y \\mid f^*)) dy\\\\\n", + " &= -\\int_\\mathcal{Y} Zp(y)\\log(Zp(y)) dy \\\\\n", + " &\\simeq -\\frac{1}{\\left|\\mathcal{Y}\\right|} \\Sigma_{\\mathcal{Y}} Z\\log(Zp(y)), \\\\\n", + " Z &= \\frac{P(f \\leq f^* \\mid y)}{P(f \\leq f^* )}\n", + "\\end{align}\n", + ", where $Z$ is the ratio of two CDFs and $\\mathcal{Y}$ is the samples drawn from the posterior distribution with noisy observation. The above formulation for noisy MES is inspired from the MF-MES formulation proposed by Takeno _et. al_ [1], which is essentially the same as what is outlined above. \n", + "\n", + "Putting all together, \n", + "\\begin{align}\n", + " \\alpha_{\\text{MES}}(\\textbf{x}) \n", + " &= H_0 - H_1 \\\\\n", + " &\\simeq H_0 - H_1^{MC}\\\\\n", + " &= \\log \\left(\\sqrt{2\\pi e (\\sigma_f^2 + \\sigma_\\epsilon^2)}\\right) + \\frac{1}{\\left|\\mathcal{F}^*\\right|} \\Sigma_{\\mathcal{F}^*} \\frac{1}{\\left|\\mathcal{Y}\\right|} \\Sigma_{\\mathcal{Y}} (Z\\log Z + Z\\log p(y))\n", + "\\end{align}\n", + "\n", + "The next design point to query is chosen as the point that maximizes this aquisition function, _i. e._, \n", + "\\begin{align}\n", + " \\textbf{x}_{\\text{next}} = \\max_{\\textbf{x} \\in \\mathcal{X}} \\alpha_{\\text{MES}}(\\textbf{x})\n", + "\\end{align}\n", + "\n", + "The implementation in Botorch basically follows the above formulation for both non-MF and MF cases. One difference is that, in order to reduce the variance of the MC estimator for $H_1$, we apply also regression adjustment to get an estimation of $H_1$, \n", + "\\begin{align}\n", + " \\widehat{H}_1 &= H_1^{MC} - \\beta (H_0^{MC} - H_0) \n", + "\\end{align}\n", + ", where\n", + "\\begin{align}\n", + " H_0^{MC} &= - \\frac{1}{\\left|\\mathcal{Y}\\right|} \\Sigma_{\\mathcal{Y}} \\log p(y) \\\\\n", + " \\beta &= \\frac{Cov(h_1, h_0)}{\\sqrt{Var(h_1)Var(h_0)}} \\\\\n", + " h_0 &= -\\log p(y) \\\\\n", + " h_1 &= -Z\\log(Zp(y)) \\\\\n", + "\\end{align}\n", + "This turns out to reduce the variance of the acquisition value by a significant factor, especially when the acquisition value is small, hence making the algorithm numerically more stable. \n", + "\n", + "For the case of $q > 1$, joint optimization becomes difficult, since the q-batch-mode MES acquisiton function becomes not tractable due to the multivariate normal CDF functions in $Z$. Instead, the MES acquisition optimization is solved sequentially and using fantasies, _i. e._, we generate one point each time and when we try to generate the $i$-th point, we condition the models on the $i-1$ points generated prior to this (using the $i-1$ points as fantasies). \n", + "\n", + "
\n", + "__References__\n", + "\n", + "[1] [Takeno, S., et al., _Multi-fidelity Bayesian Optimization with Max-value Entropy Search._ arXiv:1901.08275v1, 2019](https://arxiv.org/abs/1901.08275)\n", + "\n", + "[2] [Wang, Z., Jegelka, S., _Max-value Entropy Search for Efficient Bayesian Optimization._ arXiv:1703.01968v3, 2018](https://arxiv.org/abs/1703.01968)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "### 2. Setting up a toy model\n", + "We will fit a standard SingleTaskGP model on noisy observations of the synthetic 2D Branin function on the hypercube $[-5,10]\\times [0, 15]$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import torch\n", + "\n", + "from botorch.test_functions import Branin\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.utils.transforms import standardize, normalize\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "torch.manual_seed(7)\n", + "\n", + "bounds = torch.tensor(Branin._bounds).T\n", + "train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(10, 2)\n", + "train_Y = Branin(negate=True)(train_X).unsqueeze(-1)\n", + "\n", + "train_X = normalize(train_X, bounds=bounds)\n", + "train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y))\n", + "\n", + "model = SingleTaskGP(train_X, train_Y)\n", + "mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "fit_gpytorch_mll(mll);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Defining the MES acquisition function\n", + "\n", + "The `qMaxValueEntropy` acquisition function is a subclass of `MCAcquisitionFunction` and supports pending points `X_pending`. Required arguments for the constructor are `model` and `candidate_set` (the discretized candidate points in the design space that will be used to draw max value samples). There are also other optional parameters, such as number of max value samples $\\mathcal{F^*}$, number of $\\mathcal{Y}$ samples and number of fantasies (in case of $q>1$). Two different sampling algorithms are supported for the max value samples: the discretized Thompson sampling and the Gumbel sampling introduced in [2]. Gumbel sampling is the default choice in the acquisition function. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.acquisition.max_value_entropy_search import qMaxValueEntropy\n", + "\n", + "candidate_set = torch.rand(\n", + " 1000, bounds.size(1), device=bounds.device, dtype=bounds.dtype\n", + ")\n", + "candidate_set = bounds[0] + (bounds[1] - bounds[0]) * candidate_set\n", + "qMES = qMaxValueEntropy(model, candidate_set)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Optimizing the MES acquisition function to get the next candidate points\n", + "In order to obtain the next candidate point(s) to query, we need to optimize the acquisition function over the design space. For $q=1$ case, we can simply call the `optimize_acqf` function in the library. At $q>1$, due to the intractability of the aquisition function in this case, we need to use either sequential or cyclic optimization (multiple cycles of sequential optimization). " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[1.5350, 0.0758]]), tensor(0.0121))" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "# for q = 1\n", + "candidates, acq_value = optimize_acqf(\n", + " acq_function=qMES,\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=10,\n", + " raw_samples=512,\n", + ")\n", + "candidates, acq_value" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[-0.3238, 0.6565],\n", + " [ 1.5349, 0.0748]]), tensor([0.0135, 0.0065]))" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# for q = 2, sequential optimization\n", + "candidates_q2, acq_value_q2 = optimize_acqf(\n", + " acq_function=qMES,\n", + " bounds=bounds,\n", + " q=2,\n", + " num_restarts=10,\n", + " raw_samples=512,\n", + " sequential=True,\n", + ")\n", + "candidates_q2, acq_value_q2" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(tensor([[-0.3236, 0.6563],\n", + " [ 1.5326, 0.0732]]), tensor([0.0101, 0.0064]))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from botorch.optim import optimize_acqf_cyclic\n", + "\n", + "# for q = 2, cyclic optimization\n", + "candidates_q2_cyclic, acq_value_q2_cyclic = optimize_acqf_cyclic(\n", + " acq_function=qMES,\n", + " bounds=bounds,\n", + " q=2,\n", + " num_restarts=10,\n", + " raw_samples=512,\n", + " cyclic_options={\"maxiter\": 2},\n", + ")\n", + "candidates_q2_cyclic, acq_value_q2_cyclic" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The use of the `qMultiFidelityMaxValueEntropy` acquisition function is very similar to `qMaxValueEntropy`, but requires additional optional arguments related to the fidelity and cost models. We will provide more details on the MF-MES acquisition function in a separate tutorial. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/max_value_entropy.py b/website-old/static/files/max_value_entropy.py new file mode 100644 index 0000000000..160e2416c7 --- /dev/null +++ b/website-old/static/files/max_value_entropy.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## The max value entropy search acquisition function +# +# Max-value entropy search (MES) acquisition function quantifies the information gain about the maximum of a black-box function by observing this black-box function $f$ at the candidate set $\{\textbf{x}\}$ (see [1, 2]). BoTorch provides implementations of the MES acquisition function and its multi-fidelity (MF) version with support for trace observations. In this tutorial, we explain at a high level how the MES acquisition function works, its implementation in BoTorch and how to use the MES acquisition function to query the next point in the optimization process. +# +# In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. You can use a custom BoTorch model and acquisition function in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. To use the MES acquisition function, it is sufficient to add `"botorch_acqf_class": qMaxValueEntropy,` to `model_kwargs`. The linked tutorial shows how to use a custom BoTorch model. If you'd like to let Ax choose which model to use based on the properties of the search space, you can skip the `surrogate` argument in `model_kwargs`. +# +# ### 1. MES acquisition function for $q=1$ with noisy observation +# For illustrative purposes, we focus in this section on the non-q-batch-mode case ($q=1$). We also assume that the evaluation of the black-box function is noisy. Let us first introduce some notation: +# + $f^* = \max_\mathcal{X} (f(\textbf{x}))$, the maximum of the black-box function $f(\textbf{x})$ in the design space $\mathcal{X}$ +# + $y = f(\textbf{x}) + \epsilon, \epsilon \sim N(0, \sigma^2_\epsilon)$, the noisy observation at the design point $\textbf{x}$ +# + $h(Y) = \mathbb{E}_Y[-\log(p(y))] = -\int_\mathcal{Y} p(y)\log p(y) dy$, the differential entropy of random variable $Y$ with support $\mathcal{Y}$: the larger is $h(Y)$, the larger is the uncertainty of $Y$. +# + $v(\mathcal{D}) = -\mathbb{E}_D[h(F^*\mid\mathcal{D})]$, the value of data set $\mathcal{D}$, where $F^*$ denotes the function maximum (a random variable in our context of our model). +# +# +# The Max-value Entropy Search (MES) acquisition function at $\textbf{x}$ after observing $\mathcal{D}_t$ can be written as +# \begin{align} +# \alpha_{\text{MES}}(\textbf{x}) +# &= v(\mathcal{D}_t\cup \{(\textbf{x}, y)\}) - v(\mathcal{D}_t) \\ +# &= - \mathbb{E}_Y[h(F^* \mid \mathcal{D}_t\cup \{(\textbf{x}, Y)\})] + h(F^*\mid\mathcal{D}_t) \\ +# &= - \mathbb{E}_Y[h(F^* \mid Y)] + h(F^*) \\ +# &= I(F^*; Y) \\ +# &= I(Y; F^*) \quad \text{(symmetry)} \\ +# &= - \mathbb{E}_{F^*}[h(Y \mid F^*)] + h(Y) \\ +# \end{align} +# , which is the mutual information of random variables +# $F^*\mid \mathcal{D}_t$ and $Y \mid \textbf{x}, \mathcal{D}_t$. +# Here $F^*$ follows the max value distribution conditioned on $\mathcal{D}_t$, and $Y$ follows the GP posterior distribution with noise at $\textbf{x}$ after observing $\mathcal{D}_t$. +# +# Rewrite the above formula as +# \begin{align} +# \alpha_{\text{MES}}(\textbf{x}) &= - H_1 + H_0, \\ +# H_0 &= h(Y) = \log \left(\sqrt{2\pi e (\sigma_f^2 + \sigma_\epsilon^2)}\right) \\ +# H_1 &= \mathbb{E}_{F^*}[h(Y \mid F^*)] \\ +# &\simeq \frac{1}{\left|\mathcal{F}_*\right|} \Sigma_{\mathcal{F}_*} h(Y\mid f^*)) +# \end{align} +# , where $\mathcal{F}_*$ are the max value samples drawn from the posterior after observing $\mathcal{D}_t$. Without noise, $p(y \mid f^*) = p(f \mid f \leq f^*)$ is a truncated normal distribution with an analytic expression for its entropy. With noise, $Y\mid F\leq f^*$ is not a truncated normal distribution anymore. The question is then how to compute $h(Y\mid f^*)$ or equivalently $p(y\mid f \leq f^*)$? +# +# +# Using Bayes' theorem, +# \begin{align} +# p(y\mid f \leq f^*) = \frac{P(f \leq f^* \mid y) p(y)}{P(f \leq f^* )} +# \end{align} +# , where +# + $p(y)$ is the posterior probability density function (PDF) with observation noise. +# + $P(f \leq f^*)$ is the posterior cummulative distribution function (CDF) without observation noise, given any $f^*$. +# +# We also know from the GP predictive distribution +# \begin{align} +# \begin{bmatrix} +# y \\ f +# \end{bmatrix} +# \sim \mathcal{N} \left( +# \begin{bmatrix} +# \mu \\ \mu +# \end{bmatrix} , +# \begin{bmatrix} +# \sigma_f^2 + \sigma_\epsilon^2 & \sigma_f^2 \\ +# \sigma_f^2 & \sigma_f^2 +# \end{bmatrix} +# \right). +# \end{align} +# So +# \begin{align} +# f \mid y \sim \mathcal{N} (u, s^2) +# \end{align} +# , where +# \begin{align} +# u &= \frac{\sigma_f^2(y-\mu)}{\sigma_f^2 + \sigma_\epsilon^2} + \mu \\ +# s^2 &= \sigma_f^2 - \frac{(\sigma_f^2)^2}{\sigma_f^2 + \sigma_\epsilon^2} +# = \frac{\sigma_f^2\sigma_\epsilon^2}{\sigma_f^2 + \sigma_\epsilon^2} +# \end{align} +# Thus, $P(f \leq f^* \mid y)$ is the CDF of above Gaussian. +# +# Finally, given $f^*$, we have +# \begin{align} +# h(Y \mid f^*) +# &= -\int_\mathcal{Y} p(y \mid f^*)\log(p(y \mid f^*)) dy\\ +# &= -\int_\mathcal{Y} Zp(y)\log(Zp(y)) dy \\ +# &\simeq -\frac{1}{\left|\mathcal{Y}\right|} \Sigma_{\mathcal{Y}} Z\log(Zp(y)), \\ +# Z &= \frac{P(f \leq f^* \mid y)}{P(f \leq f^* )} +# \end{align} +# , where $Z$ is the ratio of two CDFs and $\mathcal{Y}$ is the samples drawn from the posterior distribution with noisy observation. The above formulation for noisy MES is inspired from the MF-MES formulation proposed by Takeno _et. al_ [1], which is essentially the same as what is outlined above. +# +# Putting all together, +# \begin{align} +# \alpha_{\text{MES}}(\textbf{x}) +# &= H_0 - H_1 \\ +# &\simeq H_0 - H_1^{MC}\\ +# &= \log \left(\sqrt{2\pi e (\sigma_f^2 + \sigma_\epsilon^2)}\right) + \frac{1}{\left|\mathcal{F}^*\right|} \Sigma_{\mathcal{F}^*} \frac{1}{\left|\mathcal{Y}\right|} \Sigma_{\mathcal{Y}} (Z\log Z + Z\log p(y)) +# \end{align} +# +# The next design point to query is chosen as the point that maximizes this aquisition function, _i. e._, +# \begin{align} +# \textbf{x}_{\text{next}} = \max_{\textbf{x} \in \mathcal{X}} \alpha_{\text{MES}}(\textbf{x}) +# \end{align} +# +# The implementation in Botorch basically follows the above formulation for both non-MF and MF cases. One difference is that, in order to reduce the variance of the MC estimator for $H_1$, we apply also regression adjustment to get an estimation of $H_1$, +# \begin{align} +# \widehat{H}_1 &= H_1^{MC} - \beta (H_0^{MC} - H_0) +# \end{align} +# , where +# \begin{align} +# H_0^{MC} &= - \frac{1}{\left|\mathcal{Y}\right|} \Sigma_{\mathcal{Y}} \log p(y) \\ +# \beta &= \frac{Cov(h_1, h_0)}{\sqrt{Var(h_1)Var(h_0)}} \\ +# h_0 &= -\log p(y) \\ +# h_1 &= -Z\log(Zp(y)) \\ +# \end{align} +# This turns out to reduce the variance of the acquisition value by a significant factor, especially when the acquisition value is small, hence making the algorithm numerically more stable. +# +# For the case of $q > 1$, joint optimization becomes difficult, since the q-batch-mode MES acquisiton function becomes not tractable due to the multivariate normal CDF functions in $Z$. Instead, the MES acquisition optimization is solved sequentially and using fantasies, _i. e._, we generate one point each time and when we try to generate the $i$-th point, we condition the models on the $i-1$ points generated prior to this (using the $i-1$ points as fantasies). +# +#
+# __References__ +# +# [1] [Takeno, S., et al., _Multi-fidelity Bayesian Optimization with Max-value Entropy Search._ arXiv:1901.08275v1, 2019](https://arxiv.org/abs/1901.08275) +# +# [2] [Wang, Z., Jegelka, S., _Max-value Entropy Search for Efficient Bayesian Optimization._ arXiv:1703.01968v3, 2018](https://arxiv.org/abs/1703.01968) +# + +# ### 2. Setting up a toy model +# We will fit a standard SingleTaskGP model on noisy observations of the synthetic 2D Branin function on the hypercube $[-5,10]\times [0, 15]$. + +# In[1]: + + +import math +import torch + +from botorch.test_functions import Branin +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.utils.transforms import standardize, normalize +from gpytorch.mlls import ExactMarginalLogLikelihood + +torch.manual_seed(7) + +bounds = torch.tensor(Branin._bounds).T +train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(10, 2) +train_Y = Branin(negate=True)(train_X).unsqueeze(-1) + +train_X = normalize(train_X, bounds=bounds) +train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y)) + +model = SingleTaskGP(train_X, train_Y) +mll = ExactMarginalLogLikelihood(model.likelihood, model) +fit_gpytorch_mll(mll); + + +# ### 3. Defining the MES acquisition function +# +# The `qMaxValueEntropy` acquisition function is a subclass of `MCAcquisitionFunction` and supports pending points `X_pending`. Required arguments for the constructor are `model` and `candidate_set` (the discretized candidate points in the design space that will be used to draw max value samples). There are also other optional parameters, such as number of max value samples $\mathcal{F^*}$, number of $\mathcal{Y}$ samples and number of fantasies (in case of $q>1$). Two different sampling algorithms are supported for the max value samples: the discretized Thompson sampling and the Gumbel sampling introduced in [2]. Gumbel sampling is the default choice in the acquisition function. + +# In[2]: + + +from botorch.acquisition.max_value_entropy_search import qMaxValueEntropy + +candidate_set = torch.rand( + 1000, bounds.size(1), device=bounds.device, dtype=bounds.dtype +) +candidate_set = bounds[0] + (bounds[1] - bounds[0]) * candidate_set +qMES = qMaxValueEntropy(model, candidate_set) + + +# ### 4. Optimizing the MES acquisition function to get the next candidate points +# In order to obtain the next candidate point(s) to query, we need to optimize the acquisition function over the design space. For $q=1$ case, we can simply call the `optimize_acqf` function in the library. At $q>1$, due to the intractability of the aquisition function in this case, we need to use either sequential or cyclic optimization (multiple cycles of sequential optimization). + +# In[3]: + + +from botorch.optim import optimize_acqf + +# for q = 1 +candidates, acq_value = optimize_acqf( + acq_function=qMES, + bounds=bounds, + q=1, + num_restarts=10, + raw_samples=512, +) +candidates, acq_value + + +# In[4]: + + +# for q = 2, sequential optimization +candidates_q2, acq_value_q2 = optimize_acqf( + acq_function=qMES, + bounds=bounds, + q=2, + num_restarts=10, + raw_samples=512, + sequential=True, +) +candidates_q2, acq_value_q2 + + +# In[5]: + + +from botorch.optim import optimize_acqf_cyclic + +# for q = 2, cyclic optimization +candidates_q2_cyclic, acq_value_q2_cyclic = optimize_acqf_cyclic( + acq_function=qMES, + bounds=bounds, + q=2, + num_restarts=10, + raw_samples=512, + cyclic_options={"maxiter": 2}, +) +candidates_q2_cyclic, acq_value_q2_cyclic + + +# The use of the `qMultiFidelityMaxValueEntropy` acquisition function is very similar to `qMaxValueEntropy`, but requires additional optional arguments related to the fidelity and cost models. We will provide more details on the MF-MES acquisition function in a separate tutorial. diff --git a/website-old/static/files/meta_learning_with_rgpe.ipynb b/website-old/static/files/meta_learning_with_rgpe.ipynb new file mode 100644 index 0000000000..b183ec883c --- /dev/null +++ b/website-old/static/files/meta_learning_with_rgpe.ipynb @@ -0,0 +1,1134 @@ +{ + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "python3", + "language": "python", + "isCinder": true + }, + "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.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "62d6d3ed-36ff-4609-bc82-1451f8093bd9", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "## Meta-Learning with the Rank-Weighted GP Ensemble (RGPE)\n", + "\n", + "BoTorch is designed in to be model-agnostic and only requries that a model conform to a minimal interface. This tutorial walks through an example of implementing the rank-weighted Gaussian process ensemble (RGPE) [Feurer, Letham, Bakshy ICML 2018 AutoML Workshop] and using the RGPE in BoTorch to do meta-learning across related optimization tasks.\n", + "\n", + "* Original paper: https://arxiv.org/pdf/1802.02219.pdf" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "6fc093c3-2d62-49c1-a4cc-558193bcff8b", + "collapsed": false, + "requestMsgId": "6fc093c3-2d62-49c1-a4cc-558193bcff8b", + "customOutput": null, + "executionStartTime": 1724948296406, + "executionStopTime": 1724948298817, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 2277.4769549724 + }, + "source": [ + "import os\n", + "import torch\n", + "import math\n", + "\n", + "\n", + "torch.manual_seed(29)\n", + "device = torch.device(\"cuda:2\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ], + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "I0829 091817.060 _utils_internal.py:292] NCCL_DEBUG env var is set to None\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "I0829 091817.061 _utils_internal.py:310] NCCL_DEBUG is forced to WARN from None\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "2352cd6d-70b4-4426-9fdc-e0b8ac91aac5", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "### Toy Problem\n", + "* We consider optimizing the following 1-D synthetic function\n", + "$$f(x, s_i) = \\frac{1}{10}\\bigg(x-1\\bigg)\\bigg(\\sin(x+s_i)+\\frac{1}{10}\\bigg)$$\n", + "where\n", + "$$s_i = \\frac{(i+9)\\pi}{8}$$\n", + "is a task-dependent shift parameter and $i$ is the task index $i \\in [1, t]$.\n", + "\n", + "* In this tutorial, we will consider the scenario where we have collected data from 5 prior tasks (referred to as base tasks), which with a different task dependent shift parameter $s_i$.\n", + "\n", + "* The goal now is use meta-learning to improve sample efficiency when optimizing a 6th task." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "babbfc73-97d4-491c-87e0-be07d1acc2d7", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "#### Toy Problem Setup\n", + "\n", + "First let's define a function for compute the shift parameter $s_i$ and set the shift amount for the target task." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "4c0c2b47-6313-4450-bdec-366db7c00643", + "collapsed": false, + "requestMsgId": "4c0c2b47-6313-4450-bdec-366db7c00643", + "customOutput": null, + "executionStartTime": 1724948297830, + "executionStopTime": 1724948298839, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 1.4142158906907 + }, + "source": [ + "NUM_BASE_TASKS = 5 if not SMOKE_TEST else 2\n", + "\n", + "\n", + "def task_shift(task):\n", + " \"\"\"\n", + " Fetch shift amount for task.\n", + " \"\"\"\n", + " return math.pi * task / 12.0\n", + "\n", + "\n", + "# set shift for target task\n", + "\n", + "TARGET_SHIFT = 0.0" + ], + "execution_count": 2, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "d5650131-21e8-40d4-9003-d89b7431fb29", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "Then, let's define our function $f(x, s_i)$ and set bounds on $x$." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "c1abc54e-b410-437d-beee-ce06d248706f", + "collapsed": false, + "requestMsgId": "c1abc54e-b410-437d-beee-ce06d248706f", + "customOutput": null, + "executionStartTime": 1724948298726, + "executionStopTime": 1724948298909, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 15.071736183017 + }, + "source": [ + "BOUNDS = torch.tensor([[-10.0], [10.0]], dtype=dtype, device=device)\n", + "\n", + "\n", + "def f(X, shift=TARGET_SHIFT):\n", + " \"\"\"\n", + " Torch-compatible objective function for the target_task\n", + " \"\"\"\n", + " f_X = X * torch.sin(X + math.pi + shift) + X / 10.0\n", + " return f_X" + ], + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "d9896be8-4cd9-487e-9832-768e533635c4", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "#### Sample training data for prior base tasks" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f27bbe94-c3cb-4d58-9d82-ebd60d4ebd59", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "We sample data from a Sobol sequence to help ensure numerical stability when using a small amount of 1-D data. Sobol sequences help prevent us from sampling a bunch of training points that are close together." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "75962b70-ca73-4ab4-97fd-e28de1abdd81", + "collapsed": false, + "requestMsgId": "75962b70-ca73-4ab4-97fd-e28de1abdd81", + "customOutput": null, + "executionStartTime": 1724948300185, + "executionStopTime": 1724948301395, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 1088.0531340372 + }, + "source": [ + "from botorch.utils.sampling import draw_sobol_samples\n", + "from botorch.utils.transforms import normalize, unnormalize\n", + "\n", + "\n", + "noise_std = 0.05\n", + "\n", + "# Sample data for each base task\n", + "data_by_task = {}\n", + "for task in range(NUM_BASE_TASKS):\n", + " num_training_points = 20\n", + " # draw points from a sobol sequence\n", + " raw_x = draw_sobol_samples(\n", + " bounds=BOUNDS,\n", + " n=num_training_points,\n", + " q=1,\n", + " seed=task + 5397923,\n", + " ).squeeze(1)\n", + " # get observed values\n", + " f_x = f(raw_x, task_shift(task + 1))\n", + " train_y = f_x + noise_std * torch.randn_like(f_x)\n", + " train_yvar = torch.full_like(train_y, noise_std**2)\n", + " # store training data\n", + " data_by_task[task] = {\n", + " # scale x to [0, 1]\n", + " \"train_x\": normalize(raw_x, bounds=BOUNDS),\n", + " \"train_y\": train_y,\n", + " \"train_yvar\": train_yvar,\n", + " }" + ], + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "80336086-3253-4a0a-8875-e5db7440b362", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "#### Let's plot the base tasks and the target task function along with the observed points" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "aeff41b6-623a-4b10-a583-d47563b29700", + "collapsed": false, + "requestMsgId": "aeff41b6-623a-4b10-a583-d47563b29700", + "customOutput": null, + "executionStartTime": 1724948301524, + "executionStopTime": 1724948303012, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 1299.3806430604 + }, + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(12, 8))\n", + "x = torch.linspace(-10, 10, 51)\n", + "for task in data_by_task:\n", + " # plot true function and observed values for base runs\n", + " t = ax.plot(\n", + " unnormalize(data_by_task[task][\"train_x\"], bounds=BOUNDS).cpu().numpy(),\n", + " data_by_task[task][\"train_y\"].cpu().numpy(),\n", + " \".\",\n", + " markersize=10,\n", + " label=f\"Observed task {task}\",\n", + " )\n", + " ax.plot(\n", + " x.detach().numpy(),\n", + " f(x, task_shift(task + 1)).cpu().numpy(),\n", + " label=f\"Base task {task}\",\n", + " color=t[0].get_color(),\n", + " )\n", + "# plot true target function\n", + "ax.plot(\n", + " x.detach().numpy(),\n", + " f(x, TARGET_SHIFT).detach().numpy(),\n", + " \"--\",\n", + " label=\"Target task\",\n", + ")\n", + "ax.legend(loc=\"lower right\", fontsize=10)\n", + "plt.tight_layout()" + ], + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "W0829 091822.520 font_manager.py:1403] findfont: Font family ['Liberation Sans', 'Noto Sans TC', 'Noto Sans SC', 'Noto Sans Thai', 'Noto Naskh Arabic UI', 'Noto Sans UI'] not found. Falling back to DejaVu Sans.\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKAAAAMQCAYAAAAQNB1HAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gc1fk24Ge2F/XerGrLvWEbG4zpvQRCCT0hlISENEjvISH55UshnRQIgUCoCaEF07sNxsZNkm3J6r13bd+Z7w+5SDrnrFbGsi3pua8rV9CZ2dWs1tLMvvOe52jl5eUGiIiIiIiIiIiIJonpaB8AERERERERERFNbyxAERERERERERHRpGIBioiIiIiIiIiIJhULUERERERERERENKlYgCIiIiIiIiIioknFAhQREREREREREU0qFqCIiIiIiIiIiGhSsQBFRERERERERESTigUoIiIiIiIiIiKaVJajfQBHU3Fx8dE+hI9M13W0NdUjPTsXJhPriTMJ3/uZie/7zMT3fWbi+z5z8b2fmfi+z0x832em6fa+V1RURLXf1H+lRERERERERER0TGMBioiIiIiIiIiIJhULUERERERERERENKlYgCIiIiIiIiIioknFAhQREREREREREU0qFqCIiIiIiIiIiGhSsQBFRERERERERESTigUoIiIiIiIiIiKaVCxAERERERERERHRpGIBioiIiIiIiIiIJhULUERERERERERENKlYgCIiIiIiIiIioknFAhQREREREREREU0qFqCIiIiIiIiIiGhSsQBFRERERERERESTigUoIiIiIiIiIiKaVCxAERERERERERHRpGIBioiIiIiIiIiIJhULUERERERERERENKlYgCIiIiIiIiIioknFAhQREREREREREU0qFqCIiIiIiIiIiGhSsQBFRERERERERESTigUoIiIiIiIiIiKaVCxAERERERERERHRpGIBioiIiIiIiIiIJhULUERERERERERENKlYgCIiIiIiIiIioknFAhQRERERERER0RHib2mAEQod7cM44liAIiIiIiIiIiI6AsJeD+p/8S30//HH8FTuOtqHc0SxAEVEREREREREdAS0//sfCHV3INzejLq7bkfLg39A2Dt0tA/riGABioiIiIiIiIhoknkqd6P7lacPDhgGul99BpXfvAmBzrajeWhHBAtQ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" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "25d7a014-8982-40e0-8025-f12bba83dc45", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "### Fit base task models" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "cdf621a8-1057-4cf3-a129-668abb1f9453", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "First, let's define a helper function to fit a SingleTaskGP with an fixed observed noise level." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "5474e712-cefa-4a7d-b673-af10fcf83239", + "collapsed": false, + "requestMsgId": "5474e712-cefa-4a7d-b673-af10fcf83239", + "customOutput": null, + "executionStartTime": 1724948316352, + "executionStopTime": 1724948316501, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 2.9860010836273 + }, + "source": [ + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.fit import fit_gpytorch_mll\n", + "\n", + "\n", + "def get_fitted_model(train_X, train_Y, train_Yvar, state_dict=None):\n", + " \"\"\"\n", + " Get a single task GP. The model will be fit unless a state_dict with model\n", + " hyperparameters is provided.\n", + " \"\"\"\n", + " model = SingleTaskGP(train_X=train_X, train_Y=train_Y, train_Yvar=train_Yvar)\n", + " if state_dict is None:\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model).to(train_X)\n", + " fit_gpytorch_mll(mll)\n", + " else:\n", + " model.load_state_dict(state_dict)\n", + " return model" + ], + "execution_count": 8, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "fe570963-41f1-47a5-8e53-5cf08e6390ba", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "#### Now let's fit a SingleTaskGP for each base task" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "a7bd3664-5585-47e9-9743-2ee53d7a259f", + "collapsed": false, + "requestMsgId": "a7bd3664-5585-47e9-9743-2ee53d7a259f", + "customOutput": null, + "executionStartTime": 1724948317183, + "executionStopTime": 1724948318815, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 1460.1275878958 + }, + "source": [ + "# Fit base model\n", + "base_model_list = []\n", + "for task in range(NUM_BASE_TASKS):\n", + " print(f\"Fitting base model {task}\")\n", + " model = get_fitted_model(\n", + " data_by_task[task][\"train_x\"],\n", + " data_by_task[task][\"train_y\"],\n", + " data_by_task[task][\"train_yvar\"],\n", + " )\n", + " base_model_list.append(model)" + ], + "execution_count": 9, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Fitting base model 0\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Fitting base model 1\nFitting base model 2\nFitting base model 3\nFitting base model 4\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6ea07cc4-db47-4d01-9840-220e9615b6a3", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "### Implement the RGPE\n", + "\n", + "The main idea of the RGPE is to estimate the target function as weighted sum of the target model and the base models:\n", + "$$\\bar f(\\mathbf x | \\mathcal D) =\n", + "\\sum_{i=1}^{t} w_if^i(\\mathbf x |\\mathcal D_i)$$\n", + "Importantly, the ensemble model is also a GP:\n", + "$$\\bar f(\\mathbf x | \\mathcal D) \\sim \\mathcal N\\bigg(\\sum_{i=1}^{t} w_i\\mu_i(\\mathbf x), \\sum_{i=1}^{t}w_i^2\\sigma_i^2\\bigg)$$\n", + "\n", + "The weights $w_i$ for model $i$ are based on the the ranking loss between a draw from the model's posterior and the targets. Specifically, the ranking loss for model $i$ is:\n", + "$$\\mathcal L(f^i, \\mathcal D_t) = \\sum_{j=1}^{n_t}\\sum_{k=1}^{n_t}\\mathbb 1\\bigg[\\bigg(f^i\\big(\\mathbf x^t_j\\big) < f^i\\big(\\mathbf x_k^t\\big)\\bigg)\\oplus \\big(y_j^t < y_k^t\\big)\\bigg]$$\n", + "where $\\oplus$ is exclusive-or.\n", + "\n", + "The loss for the target model is computing using leave-one-out cross-validation (LOOCV) and is given by:\n", + "$$\\mathcal L(f^t, \\mathcal D_t) = \\sum_{j=1}^{n_t}\\sum_{k=1}^{n_t}\\mathbb 1\\bigg[\\bigg(f^t_{-j}\\big(\\mathbf x^t_j\\big) < f^t_{-j}\\big(\\mathbf x_k^t\\big)\\bigg)\\oplus \\big(y_j^t < y_k^t\\big)\\bigg]$$\n", + "where $f^t_{-j}$ model fitted to all data from the target task except training example $j$.\n", + "\n", + "The weights are then computed as:\n", + "$$w_i = \\frac{1}{S}\\sum_{s=1}^S\\mathbb 1\\big(i = \\text{argmin}_{i'}l_{i', s}\\big)$$" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "58ed8284-8181-459c-9025-42532793929f", + "collapsed": false, + "requestMsgId": "58ed8284-8181-459c-9025-42532793929f", + "customOutput": null, + "executionStartTime": 1724948320494, + "executionStopTime": 1724948320631, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 2.3661230225116 + }, + "source": [ + "def roll_col(X, shift):\n", + " \"\"\"\n", + " Rotate columns to right by shift.\n", + " \"\"\"\n", + " return torch.cat((X[..., -shift:], X[..., :-shift]), dim=-1)" + ], + "execution_count": 10, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "bde867e5-67e9-47f0-bae3-20112bebd51d", + "collapsed": false, + "requestMsgId": "bde867e5-67e9-47f0-bae3-20112bebd51d", + "customOutput": null, + "executionStartTime": 1724948325542, + "executionStopTime": 1724948325683, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 3.575277980417 + }, + "source": [ + "def compute_ranking_loss(f_samps, target_y):\n", + " \"\"\"\n", + " Compute ranking loss for each sample from the posterior over target points.\n", + "\n", + " Args:\n", + " f_samps: `n_samples x (n) x n`-dim tensor of samples\n", + " target_y: `n x 1`-dim tensor of targets\n", + " Returns:\n", + " Tensor: `n_samples`-dim tensor containing the ranking loss across each sample\n", + " \"\"\"\n", + " n = target_y.shape[0]\n", + " if f_samps.ndim == 3:\n", + " # Compute ranking loss for target model\n", + " # take cartesian product of target_y\n", + " cartesian_y = torch.cartesian_prod(\n", + " target_y.squeeze(-1),\n", + " target_y.squeeze(-1),\n", + " ).view(n, n, 2)\n", + " # the diagonal of f_samps are the out-of-sample predictions\n", + " # for each LOO model, compare the out of sample predictions to each in-sample prediction\n", + " rank_loss = (\n", + " (\n", + " (f_samps.diagonal(dim1=1, dim2=2).unsqueeze(-1) < f_samps)\n", + " ^ (cartesian_y[..., 0] < cartesian_y[..., 1])\n", + " )\n", + " .sum(dim=-1)\n", + " .sum(dim=-1)\n", + " )\n", + " else:\n", + " rank_loss = torch.zeros(\n", + " f_samps.shape[0], dtype=torch.long, device=target_y.device\n", + " )\n", + " y_stack = target_y.squeeze(-1).expand(f_samps.shape)\n", + " for i in range(1, target_y.shape[0]):\n", + " rank_loss += (\n", + " (roll_col(f_samps, i) < f_samps) ^ (roll_col(y_stack, i) < y_stack)\n", + " ).sum(dim=-1)\n", + " return rank_loss" + ], + "execution_count": 11, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "158d11b4-020f-478c-9ec4-8655ae8c2aac", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "Define a function to:\n", + "1. Create a batch mode-gp LOOCV GP using the hyperparameters from `target_model`\n", + "2. Draw a joint sample across all points from the target task (in-sample and out-of-sample)" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "91127a10-93d6-4cc1-9eaa-3ac49e7be678", + "collapsed": false, + "requestMsgId": "91127a10-93d6-4cc1-9eaa-3ac49e7be678", + "customOutput": null, + "executionStartTime": 1724948361037, + "executionStopTime": 1724948361226, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 4.2148299980909 + }, + "source": [ + "def get_target_model_loocv_sample_preds(\n", + " train_x, train_y, train_yvar, target_model, num_samples\n", + "):\n", + " \"\"\"\n", + " Create a batch-mode LOOCV GP and draw a joint sample across all points from the target task.\n", + "\n", + " Args:\n", + " train_x: `n x d` tensor of training points\n", + " train_y: `n x 1` tensor of training targets\n", + " target_model: fitted target model\n", + " num_samples: number of mc samples to draw\n", + "\n", + " Return: `num_samples x n x n`-dim tensor of samples, where dim=1 represents the `n` LOO models,\n", + " and dim=2 represents the `n` training points.\n", + " \"\"\"\n", + " batch_size = len(train_x)\n", + " masks = torch.eye(len(train_x), dtype=torch.uint8, device=device).bool()\n", + " train_x_cv = torch.stack([train_x[~m] for m in masks])\n", + " train_y_cv = torch.stack([train_y[~m] for m in masks])\n", + " train_yvar_cv = torch.stack([train_yvar[~m] for m in masks])\n", + " state_dict = target_model.state_dict()\n", + " # expand to batch size of batch_mode LOOCV model\n", + " state_dict_expanded = {\n", + " name: t.expand(batch_size, *[-1 for _ in range(t.ndim)])\n", + " for name, t in state_dict.items()\n", + " }\n", + " model = get_fitted_model(\n", + " train_x_cv, train_y_cv, train_yvar_cv, state_dict=state_dict_expanded\n", + " )\n", + " with torch.no_grad():\n", + " posterior = model.posterior(train_x)\n", + " # Since we have a batch mode gp and model.posterior always returns an output dimension,\n", + " # the output from `posterior.sample()` here `num_samples x n x n x 1`, so let's squeeze\n", + " # the last dimension.\n", + " sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples]))\n", + " return sampler(posterior).squeeze(-1)" + ], + "execution_count": 12, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "298fa009-eae9-4199-a682-df730e10c20e", + "collapsed": false, + "requestMsgId": "298fa009-eae9-4199-a682-df730e10c20e", + "customOutput": null, + "executionStartTime": 1724948370606, + "executionStopTime": 1724948370882, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 3.5223178565502 + }, + "source": [ + "def compute_rank_weights(train_x, train_y, base_models, target_model, num_samples):\n", + " \"\"\"\n", + " Compute ranking weights for each base model and the target model (using\n", + " LOOCV for the target model). Note: This implementation does not currently\n", + " address weight dilution, since we only have a small number of base models.\n", + "\n", + " Args:\n", + " train_x: `n x d` tensor of training points (for target task)\n", + " train_y: `n` tensor of training targets (for target task)\n", + " base_models: list of base models\n", + " target_model: target model\n", + " num_samples: number of mc samples\n", + "\n", + " Returns:\n", + " Tensor: `n_t`-dim tensor with the ranking weight for each model\n", + " \"\"\"\n", + " ranking_losses = []\n", + " # compute ranking loss for each base model\n", + " for task in range(len(base_models)):\n", + " model = base_models[task]\n", + " # compute posterior over training points for target task\n", + " posterior = model.posterior(train_x)\n", + " sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples]))\n", + " base_f_samps = sampler(posterior).squeeze(-1).squeeze(-1)\n", + " # compute and save ranking loss\n", + " ranking_losses.append(compute_ranking_loss(base_f_samps, train_y))\n", + " # compute ranking loss for target model using LOOCV\n", + " # f_samps\n", + " target_f_samps = get_target_model_loocv_sample_preds(\n", + " train_x,\n", + " train_y,\n", + " train_yvar,\n", + " target_model,\n", + " num_samples,\n", + " )\n", + " ranking_losses.append(compute_ranking_loss(target_f_samps, train_y))\n", + " ranking_loss_tensor = torch.stack(ranking_losses)\n", + " # compute best model (minimum ranking loss) for each sample\n", + " best_models = torch.argmin(ranking_loss_tensor, dim=0)\n", + " # compute proportion of samples for which each model is best\n", + " rank_weights = (\n", + " best_models.bincount(minlength=len(ranking_losses)).type_as(train_x)\n", + " / num_samples\n", + " )\n", + " return rank_weights" + ], + "execution_count": 13, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "5393f2d7-07c3-4e41-a68f-0cf92fb2aa8f", + "collapsed": false, + "requestMsgId": "5393f2d7-07c3-4e41-a68f-0cf92fb2aa8f", + "customOutput": null, + "executionStartTime": 1724948386869, + "executionStopTime": 1724948387021, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 5.0720849540085 + }, + "source": [ + "from botorch.models.gpytorch import GPyTorchModel\n", + "from gpytorch.models import GP\n", + "from gpytorch.distributions import MultivariateNormal\n", + "from gpytorch.lazy import PsdSumLazyTensor\n", + "from gpytorch.likelihoods import LikelihoodList\n", + "from torch.nn import ModuleList\n", + "\n", + "\n", + "class RGPE(GP, GPyTorchModel):\n", + " \"\"\"\n", + " Rank-weighted GP ensemble. Note: this class inherits from GPyTorchModel which provides an\n", + " interface for GPyTorch models in botorch.\n", + " \"\"\"\n", + "\n", + " _num_outputs = 1 # metadata for botorch\n", + "\n", + " def __init__(self, models, weights):\n", + " super().__init__()\n", + " self.models = ModuleList(models)\n", + " for m in models:\n", + " if not hasattr(m, \"likelihood\"):\n", + " raise ValueError(\n", + " \"RGPE currently only supports models that have a likelihood (e.g. ExactGPs)\"\n", + " )\n", + " self.likelihood = LikelihoodList(*[m.likelihood for m in models])\n", + " self.weights = weights\n", + " self.to(weights)\n", + "\n", + " def forward(self, x):\n", + " weighted_means = []\n", + " weighted_covars = []\n", + " # filter model with zero weights\n", + " # weights on covariance matrices are weight**2\n", + " non_zero_weight_indices = (self.weights**2 > 0).nonzero()\n", + " non_zero_weights = self.weights[non_zero_weight_indices]\n", + " # re-normalize\n", + " non_zero_weights /= non_zero_weights.sum()\n", + "\n", + " for non_zero_weight_idx in range(non_zero_weight_indices.shape[0]):\n", + " raw_idx = non_zero_weight_indices[non_zero_weight_idx].item()\n", + " model = self.models[raw_idx]\n", + " posterior = model.posterior(x)\n", + " # unstandardize predictions\n", + " posterior_mean = posterior.mean.squeeze(-1)\n", + " posterior_cov = posterior.mvn.lazy_covariance_matrix\n", + " # apply weight\n", + " weight = non_zero_weights[non_zero_weight_idx]\n", + " weighted_means.append(weight * posterior_mean)\n", + " weighted_covars.append(posterior_cov * weight**2)\n", + " # set mean and covariance to be the rank-weighted sum the means and covariances of the\n", + " # base models and target model\n", + " mean_x = torch.stack(weighted_means).sum(dim=0)\n", + " covar_x = PsdSumLazyTensor(*weighted_covars)\n", + " return MultivariateNormal(mean_x, covar_x)" + ], + "execution_count": 14, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "49b770aa-d0c1-4e37-8366-ca1debde2f40", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "### Optimize target function using RGPE + qNEI" + ] + }, + { + "cell_type": "code", + "metadata": { + "scrolled": false, + "originalKey": "4670c8c2-c171-4e3e-87f6-0ca3543140df", + "collapsed": false, + "requestMsgId": "4670c8c2-c171-4e3e-87f6-0ca3543140df", + "customOutput": null, + "executionStartTime": 1724948469002, + "executionStopTime": 1724948513622, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 44297.466597985 + }, + "source": [ + "# suppress GPyTorch warnings about adding jitter\n", + "import warnings\n", + "\n", + "from botorch.acquisition.logei import qLogNoisyExpectedImprovement\n", + "from botorch.optim.optimize import optimize_acqf\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "\n", + "\n", + "warnings.filterwarnings(\"ignore\", \"^.*jitter.*\", category=RuntimeWarning)\n", + "\n", + "\n", + "best_rgpe_all = []\n", + "best_random_all = []\n", + "best_vanilla_nei_all = []\n", + "N_BATCH = 10 if not SMOKE_TEST else 2\n", + "NUM_POSTERIOR_SAMPLES = 256 if not SMOKE_TEST else 16\n", + "RANDOM_INITIALIZATION_SIZE = 3\n", + "N_TRIALS = 10 if not SMOKE_TEST else 2\n", + "MC_SAMPLES = 512 if not SMOKE_TEST else 32\n", + "N_RESTART_CANDIDATES = 512 if not SMOKE_TEST else 8\n", + "N_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "Q_BATCH_SIZE = 1\n", + "\n", + "\n", + "# Average over multiple trials\n", + "for trial in range(N_TRIALS):\n", + " print(f\"Trial {trial + 1} of {N_TRIALS}\")\n", + " best_rgpe = []\n", + " best_random = []\n", + " best_vanilla_nei = []\n", + " # Initial random observations\n", + " raw_x = draw_sobol_samples(\n", + " bounds=BOUNDS, n=RANDOM_INITIALIZATION_SIZE, q=1, seed=trial\n", + " ).squeeze(1)\n", + " train_x = normalize(raw_x, bounds=BOUNDS)\n", + " train_y_noiseless = f(raw_x)\n", + " train_y = train_y_noiseless + noise_std * torch.randn_like(train_y_noiseless)\n", + " train_yvar = torch.full_like(train_y, noise_std**2)\n", + " vanilla_nei_train_x = train_x.clone()\n", + " vanilla_nei_train_y = train_y.clone()\n", + " vanilla_nei_train_yvar = train_yvar.clone()\n", + " # keep track of the best observed point at each iteration\n", + " best_value = train_y.max().item()\n", + " best_rgpe.append(best_value)\n", + " best_random.append(best_value)\n", + " vanilla_nei_best_value = best_value\n", + " best_vanilla_nei.append(vanilla_nei_best_value)\n", + "\n", + " # Run N_BATCH rounds of BayesOpt after the initial random batch\n", + " for iteration in range(N_BATCH):\n", + " target_model = get_fitted_model(train_x, train_y, train_yvar)\n", + " model_list = base_model_list + [target_model]\n", + " rank_weights = compute_rank_weights(\n", + " train_x,\n", + " train_y,\n", + " base_model_list,\n", + " target_model,\n", + " NUM_POSTERIOR_SAMPLES,\n", + " )\n", + "\n", + " # create model and acquisition function\n", + " rgpe_model = RGPE(model_list, rank_weights)\n", + " sampler_qnei = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + " qNEI = qLogNoisyExpectedImprovement(\n", + " model=rgpe_model,\n", + " X_baseline=train_x,\n", + " sampler=sampler_qnei,\n", + " prune_baseline=False,\n", + " )\n", + "\n", + " # optimize\n", + " candidate, _ = optimize_acqf(\n", + " acq_function=qNEI,\n", + " bounds=torch.tensor([[0.0], [1.0]], dtype=dtype, device=device),\n", + " q=Q_BATCH_SIZE,\n", + " num_restarts=N_RESTARTS,\n", + " raw_samples=N_RESTART_CANDIDATES,\n", + " )\n", + "\n", + " # fetch the new values\n", + " new_x = candidate.detach()\n", + " new_y_noiseless = f(unnormalize(new_x, bounds=BOUNDS))\n", + " new_y = new_y_noiseless + noise_std * torch.randn_like(new_y_noiseless)\n", + " new_yvar = torch.full_like(new_y, noise_std**2)\n", + "\n", + " # update training points\n", + " train_x = torch.cat((train_x, new_x))\n", + " train_y = torch.cat((train_y, new_y))\n", + " train_yvar = torch.cat((train_yvar, new_yvar))\n", + " random_candidate = torch.rand(1, dtype=dtype, device=device)\n", + " next_random_noiseless = f(unnormalize(random_candidate, bounds=BOUNDS))\n", + " next_random = next_random_noiseless + noise_std * torch.randn_like(\n", + " next_random_noiseless\n", + " )\n", + " next_random_best = next_random.max().item()\n", + " best_random.append(max(best_random[-1], next_random_best))\n", + "\n", + " # get the new best observed value\n", + " best_value = train_y.max().item()\n", + " best_rgpe.append(best_value)\n", + "\n", + " # Run Vanilla NEI for comparison\n", + " vanilla_nei_model = get_fitted_model(\n", + " vanilla_nei_train_x,\n", + " vanilla_nei_train_y,\n", + " vanilla_nei_train_yvar,\n", + " )\n", + " vanilla_nei_sampler = SobolQMCNormalSampler(\n", + " sample_shape=torch.Size([MC_SAMPLES])\n", + " )\n", + " vanilla_qNEI = qLogNoisyExpectedImprovement(\n", + " model=vanilla_nei_model,\n", + " X_baseline=vanilla_nei_train_x,\n", + " sampler=vanilla_nei_sampler,\n", + " )\n", + " vanilla_nei_candidate, _ = optimize_acqf(\n", + " acq_function=vanilla_qNEI,\n", + " bounds=torch.tensor([[0.0], [1.0]], dtype=dtype, device=device),\n", + " q=Q_BATCH_SIZE,\n", + " num_restarts=N_RESTARTS,\n", + " raw_samples=N_RESTART_CANDIDATES,\n", + " )\n", + " # fetch the new values\n", + " vanilla_nei_new_x = vanilla_nei_candidate.detach()\n", + " vanilla_nei_new_y_noiseless = f(unnormalize(vanilla_nei_new_x, bounds=BOUNDS))\n", + " vanilla_nei_new_y = vanilla_nei_new_y_noiseless + noise_std * torch.randn_like(\n", + " new_y_noiseless\n", + " )\n", + " vanilla_nei_new_yvar = torch.full_like(vanilla_nei_new_y, noise_std**2)\n", + "\n", + " # update training points\n", + " vanilla_nei_train_x = torch.cat([vanilla_nei_train_x, vanilla_nei_new_x])\n", + " vanilla_nei_train_y = torch.cat([vanilla_nei_train_y, vanilla_nei_new_y])\n", + " vanilla_nei_train_yvar = torch.cat(\n", + " [vanilla_nei_train_yvar, vanilla_nei_new_yvar]\n", + " )\n", + "\n", + " # get the new best observed value\n", + " vanilla_nei_best_value = vanilla_nei_train_y.max().item()\n", + " best_vanilla_nei.append(vanilla_nei_best_value)\n", + "\n", + " best_rgpe_all.append(best_rgpe)\n", + " best_random_all.append(best_random)\n", + " best_vanilla_nei_all.append(best_vanilla_nei)" + ], + "execution_count": 18, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 1 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 2 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 3 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 4 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 5 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[W 240829 09:21:28 optimize:564] Optimization failed in `gen_candidates_scipy` with the following warning(s):\n [OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]\n Trying again with a new set of initial conditions.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[W 240829 09:21:28 optimize:564] Optimization failed on the second try, after generating a new set of initial conditions.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 6 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 7 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 8 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 9 of 10\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[W 240829 09:21:46 optimize:564] Optimization failed in `gen_candidates_scipy` with the following warning(s):\n [OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]\n Trying again with a new set of initial conditions.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[W 240829 09:21:46 optimize:564] Optimization failed on the second try, after generating a new set of initial conditions.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Trial 10 of 10\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "3dbf06b6-28ec-4b73-99f3-a9327b074159", + "showInput": false, + "outputsInitialized": false, + "language": "markdown" + }, + "source": [ + "#### Plot best observed value vs iteration" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "77d08a71-caf0-444c-994c-94a0f63efc42", + "collapsed": false, + "requestMsgId": "77d08a71-caf0-444c-994c-94a0f63efc42", + "customOutput": null, + "executionStartTime": 1724948509190, + "executionStopTime": 1724948514271, + "outputsInitialized": true, + "language": "python", + "serverExecutionDuration": 412.50938293524 + }, + "source": [ + "import numpy as np\n", + "\n", + "\n", + "best_rgpe_all = np.array(best_rgpe_all)\n", + "best_random_all = np.array(best_random_all)\n", + "best_vanilla_nei_all = np.array(best_vanilla_nei_all)\n", + "\n", + "x = range(RANDOM_INITIALIZATION_SIZE, RANDOM_INITIALIZATION_SIZE + N_BATCH + 1)\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(10, 6))\n", + "# Plot RGPE - LogNEI\n", + "ax.errorbar(\n", + " x,\n", + " best_rgpe_all.mean(axis=0),\n", + " yerr=1.96 * best_rgpe_all.std(axis=0) / math.sqrt(N_TRIALS),\n", + " label=\"RGPE - LogNEI\",\n", + " linewidth=3,\n", + " capsize=5,\n", + " capthick=3,\n", + ")\n", + "# Plot SingleTaskGP - LogNEI\n", + "ax.errorbar(\n", + " x,\n", + " best_vanilla_nei_all.mean(axis=0),\n", + " yerr=1.96 * best_vanilla_nei_all.std(axis=0) / math.sqrt(N_TRIALS),\n", + " label=\"SingleTaskGP - LogNEI\",\n", + " linewidth=3,\n", + " capsize=5,\n", + " capthick=3,\n", + ")\n", + "# Plot Random\n", + "ax.errorbar(\n", + " x,\n", + " best_random_all.mean(axis=0),\n", + " yerr=1.96 * best_random_all.std(axis=0) / math.sqrt(N_TRIALS),\n", + " label=\"Random\",\n", + " linewidth=3,\n", + " capsize=5,\n", + " capthick=3,\n", + ")\n", + "ax.set_ylim(bottom=0)\n", + "ax.set_xlabel(\"Iteration\", fontsize=12)\n", + "ax.set_ylabel(\"Best Observed Value\", fontsize=12)\n", + "ax.set_title(\"Best Observed Value by Iteration\", fontsize=12)\n", + "ax.legend(loc=\"lower right\", fontsize=10)\n", + "plt.tight_layout()" + ], + "execution_count": 19, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "bc7cf5ae-bdf2-465c-b918-a69c8f2e3e8f", + "showInput": true, + "customInput": null, + "language": "python" + }, + "source": [ + "" + ], + "execution_count": null, + "outputs": [] + } + ] +} diff --git a/website-old/static/files/meta_learning_with_rgpe.py b/website-old/static/files/meta_learning_with_rgpe.py new file mode 100644 index 0000000000..8d643bf237 --- /dev/null +++ b/website-old/static/files/meta_learning_with_rgpe.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Meta-Learning with the Rank-Weighted GP Ensemble (RGPE) +# +# BoTorch is designed in to be model-agnostic and only requries that a model conform to a minimal interface. This tutorial walks through an example of implementing the rank-weighted Gaussian process ensemble (RGPE) [Feurer, Letham, Bakshy ICML 2018 AutoML Workshop] and using the RGPE in BoTorch to do meta-learning across related optimization tasks. +# +# * Original paper: https://arxiv.org/pdf/1802.02219.pdf + +# In[1]: + + +import os +import torch +import math + + +torch.manual_seed(29) +device = torch.device("cuda:2" if torch.cuda.is_available() else "cpu") +dtype = torch.double +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Toy Problem +# * We consider optimizing the following 1-D synthetic function +# $$f(x, s_i) = \frac{1}{10}\bigg(x-1\bigg)\bigg(\sin(x+s_i)+\frac{1}{10}\bigg)$$ +# where +# $$s_i = \frac{(i+9)\pi}{8}$$ +# is a task-dependent shift parameter and $i$ is the task index $i \in [1, t]$. +# +# * In this tutorial, we will consider the scenario where we have collected data from 5 prior tasks (referred to as base tasks), which with a different task dependent shift parameter $s_i$. +# +# * The goal now is use meta-learning to improve sample efficiency when optimizing a 6th task. + +# #### Toy Problem Setup +# +# First let's define a function for compute the shift parameter $s_i$ and set the shift amount for the target task. + +# In[2]: + + +NUM_BASE_TASKS = 5 if not SMOKE_TEST else 2 + + +def task_shift(task): + """ + Fetch shift amount for task. + """ + return math.pi * task / 12.0 + + +# set shift for target task + +TARGET_SHIFT = 0.0 + + +# Then, let's define our function $f(x, s_i)$ and set bounds on $x$. + +# In[3]: + + +BOUNDS = torch.tensor([[-10.0], [10.0]], dtype=dtype, device=device) + + +def f(X, shift=TARGET_SHIFT): + """ + Torch-compatible objective function for the target_task + """ + f_X = X * torch.sin(X + math.pi + shift) + X / 10.0 + return f_X + + +# #### Sample training data for prior base tasks + +# We sample data from a Sobol sequence to help ensure numerical stability when using a small amount of 1-D data. Sobol sequences help prevent us from sampling a bunch of training points that are close together. + +# In[4]: + + +from botorch.utils.sampling import draw_sobol_samples +from botorch.utils.transforms import normalize, unnormalize + + +noise_std = 0.05 + +# Sample data for each base task +data_by_task = {} +for task in range(NUM_BASE_TASKS): + num_training_points = 20 + # draw points from a sobol sequence + raw_x = draw_sobol_samples( + bounds=BOUNDS, + n=num_training_points, + q=1, + seed=task + 5397923, + ).squeeze(1) + # get observed values + f_x = f(raw_x, task_shift(task + 1)) + train_y = f_x + noise_std * torch.randn_like(f_x) + train_yvar = torch.full_like(train_y, noise_std**2) + # store training data + data_by_task[task] = { + # scale x to [0, 1] + "train_x": normalize(raw_x, bounds=BOUNDS), + "train_y": train_y, + "train_yvar": train_yvar, + } + + +# #### Let's plot the base tasks and the target task function along with the observed points + +# In[5]: + + +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +fig, ax = plt.subplots(1, 1, figsize=(12, 8)) +x = torch.linspace(-10, 10, 51) +for task in data_by_task: + # plot true function and observed values for base runs + t = ax.plot( + unnormalize(data_by_task[task]["train_x"], bounds=BOUNDS).cpu().numpy(), + data_by_task[task]["train_y"].cpu().numpy(), + ".", + markersize=10, + label=f"Observed task {task}", + ) + ax.plot( + x.detach().numpy(), + f(x, task_shift(task + 1)).cpu().numpy(), + label=f"Base task {task}", + color=t[0].get_color(), + ) +# plot true target function +ax.plot( + x.detach().numpy(), + f(x, TARGET_SHIFT).detach().numpy(), + "--", + label="Target task", +) +ax.legend(loc="lower right", fontsize=10) +plt.tight_layout() + + +# ### Fit base task models + +# First, let's define a helper function to fit a SingleTaskGP with an fixed observed noise level. + +# In[8]: + + +from gpytorch.mlls import ExactMarginalLogLikelihood +from botorch.models import SingleTaskGP +from botorch.fit import fit_gpytorch_mll + + +def get_fitted_model(train_X, train_Y, train_Yvar, state_dict=None): + """ + Get a single task GP. The model will be fit unless a state_dict with model + hyperparameters is provided. + """ + model = SingleTaskGP(train_X=train_X, train_Y=train_Y, train_Yvar=train_Yvar) + if state_dict is None: + mll = ExactMarginalLogLikelihood(model.likelihood, model).to(train_X) + fit_gpytorch_mll(mll) + else: + model.load_state_dict(state_dict) + return model + + +# #### Now let's fit a SingleTaskGP for each base task + +# In[9]: + + +# Fit base model +base_model_list = [] +for task in range(NUM_BASE_TASKS): + print(f"Fitting base model {task}") + model = get_fitted_model( + data_by_task[task]["train_x"], + data_by_task[task]["train_y"], + data_by_task[task]["train_yvar"], + ) + base_model_list.append(model) + + +# ### Implement the RGPE +# +# The main idea of the RGPE is to estimate the target function as weighted sum of the target model and the base models: +# $$\bar f(\mathbf x | \mathcal D) = +# \sum_{i=1}^{t} w_if^i(\mathbf x |\mathcal D_i)$$ +# Importantly, the ensemble model is also a GP: +# $$\bar f(\mathbf x | \mathcal D) \sim \mathcal N\bigg(\sum_{i=1}^{t} w_i\mu_i(\mathbf x), \sum_{i=1}^{t}w_i^2\sigma_i^2\bigg)$$ +# +# The weights $w_i$ for model $i$ are based on the the ranking loss between a draw from the model's posterior and the targets. Specifically, the ranking loss for model $i$ is: +# $$\mathcal L(f^i, \mathcal D_t) = \sum_{j=1}^{n_t}\sum_{k=1}^{n_t}\mathbb 1\bigg[\bigg(f^i\big(\mathbf x^t_j\big) < f^i\big(\mathbf x_k^t\big)\bigg)\oplus \big(y_j^t < y_k^t\big)\bigg]$$ +# where $\oplus$ is exclusive-or. +# +# The loss for the target model is computing using leave-one-out cross-validation (LOOCV) and is given by: +# $$\mathcal L(f^t, \mathcal D_t) = \sum_{j=1}^{n_t}\sum_{k=1}^{n_t}\mathbb 1\bigg[\bigg(f^t_{-j}\big(\mathbf x^t_j\big) < f^t_{-j}\big(\mathbf x_k^t\big)\bigg)\oplus \big(y_j^t < y_k^t\big)\bigg]$$ +# where $f^t_{-j}$ model fitted to all data from the target task except training example $j$. +# +# The weights are then computed as: +# $$w_i = \frac{1}{S}\sum_{s=1}^S\mathbb 1\big(i = \text{argmin}_{i'}l_{i', s}\big)$$ + +# In[10]: + + +def roll_col(X, shift): + """ + Rotate columns to right by shift. + """ + return torch.cat((X[..., -shift:], X[..., :-shift]), dim=-1) + + +# In[11]: + + +def compute_ranking_loss(f_samps, target_y): + """ + Compute ranking loss for each sample from the posterior over target points. + + Args: + f_samps: `n_samples x (n) x n`-dim tensor of samples + target_y: `n x 1`-dim tensor of targets + Returns: + Tensor: `n_samples`-dim tensor containing the ranking loss across each sample + """ + n = target_y.shape[0] + if f_samps.ndim == 3: + # Compute ranking loss for target model + # take cartesian product of target_y + cartesian_y = torch.cartesian_prod( + target_y.squeeze(-1), + target_y.squeeze(-1), + ).view(n, n, 2) + # the diagonal of f_samps are the out-of-sample predictions + # for each LOO model, compare the out of sample predictions to each in-sample prediction + rank_loss = ( + ( + (f_samps.diagonal(dim1=1, dim2=2).unsqueeze(-1) < f_samps) + ^ (cartesian_y[..., 0] < cartesian_y[..., 1]) + ) + .sum(dim=-1) + .sum(dim=-1) + ) + else: + rank_loss = torch.zeros( + f_samps.shape[0], dtype=torch.long, device=target_y.device + ) + y_stack = target_y.squeeze(-1).expand(f_samps.shape) + for i in range(1, target_y.shape[0]): + rank_loss += ( + (roll_col(f_samps, i) < f_samps) ^ (roll_col(y_stack, i) < y_stack) + ).sum(dim=-1) + return rank_loss + + +# Define a function to: +# 1. Create a batch mode-gp LOOCV GP using the hyperparameters from `target_model` +# 2. Draw a joint sample across all points from the target task (in-sample and out-of-sample) + +# In[12]: + + +def get_target_model_loocv_sample_preds( + train_x, train_y, train_yvar, target_model, num_samples +): + """ + Create a batch-mode LOOCV GP and draw a joint sample across all points from the target task. + + Args: + train_x: `n x d` tensor of training points + train_y: `n x 1` tensor of training targets + target_model: fitted target model + num_samples: number of mc samples to draw + + Return: `num_samples x n x n`-dim tensor of samples, where dim=1 represents the `n` LOO models, + and dim=2 represents the `n` training points. + """ + batch_size = len(train_x) + masks = torch.eye(len(train_x), dtype=torch.uint8, device=device).bool() + train_x_cv = torch.stack([train_x[~m] for m in masks]) + train_y_cv = torch.stack([train_y[~m] for m in masks]) + train_yvar_cv = torch.stack([train_yvar[~m] for m in masks]) + state_dict = target_model.state_dict() + # expand to batch size of batch_mode LOOCV model + state_dict_expanded = { + name: t.expand(batch_size, *[-1 for _ in range(t.ndim)]) + for name, t in state_dict.items() + } + model = get_fitted_model( + train_x_cv, train_y_cv, train_yvar_cv, state_dict=state_dict_expanded + ) + with torch.no_grad(): + posterior = model.posterior(train_x) + # Since we have a batch mode gp and model.posterior always returns an output dimension, + # the output from `posterior.sample()` here `num_samples x n x n x 1`, so let's squeeze + # the last dimension. + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples])) + return sampler(posterior).squeeze(-1) + + +# In[13]: + + +def compute_rank_weights(train_x, train_y, base_models, target_model, num_samples): + """ + Compute ranking weights for each base model and the target model (using + LOOCV for the target model). Note: This implementation does not currently + address weight dilution, since we only have a small number of base models. + + Args: + train_x: `n x d` tensor of training points (for target task) + train_y: `n` tensor of training targets (for target task) + base_models: list of base models + target_model: target model + num_samples: number of mc samples + + Returns: + Tensor: `n_t`-dim tensor with the ranking weight for each model + """ + ranking_losses = [] + # compute ranking loss for each base model + for task in range(len(base_models)): + model = base_models[task] + # compute posterior over training points for target task + posterior = model.posterior(train_x) + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([num_samples])) + base_f_samps = sampler(posterior).squeeze(-1).squeeze(-1) + # compute and save ranking loss + ranking_losses.append(compute_ranking_loss(base_f_samps, train_y)) + # compute ranking loss for target model using LOOCV + # f_samps + target_f_samps = get_target_model_loocv_sample_preds( + train_x, + train_y, + train_yvar, + target_model, + num_samples, + ) + ranking_losses.append(compute_ranking_loss(target_f_samps, train_y)) + ranking_loss_tensor = torch.stack(ranking_losses) + # compute best model (minimum ranking loss) for each sample + best_models = torch.argmin(ranking_loss_tensor, dim=0) + # compute proportion of samples for which each model is best + rank_weights = ( + best_models.bincount(minlength=len(ranking_losses)).type_as(train_x) + / num_samples + ) + return rank_weights + + +# In[14]: + + +from botorch.models.gpytorch import GPyTorchModel +from gpytorch.models import GP +from gpytorch.distributions import MultivariateNormal +from gpytorch.lazy import PsdSumLazyTensor +from gpytorch.likelihoods import LikelihoodList +from torch.nn import ModuleList + + +class RGPE(GP, GPyTorchModel): + """ + Rank-weighted GP ensemble. Note: this class inherits from GPyTorchModel which provides an + interface for GPyTorch models in botorch. + """ + + _num_outputs = 1 # metadata for botorch + + def __init__(self, models, weights): + super().__init__() + self.models = ModuleList(models) + for m in models: + if not hasattr(m, "likelihood"): + raise ValueError( + "RGPE currently only supports models that have a likelihood (e.g. ExactGPs)" + ) + self.likelihood = LikelihoodList(*[m.likelihood for m in models]) + self.weights = weights + self.to(weights) + + def forward(self, x): + weighted_means = [] + weighted_covars = [] + # filter model with zero weights + # weights on covariance matrices are weight**2 + non_zero_weight_indices = (self.weights**2 > 0).nonzero() + non_zero_weights = self.weights[non_zero_weight_indices] + # re-normalize + non_zero_weights /= non_zero_weights.sum() + + for non_zero_weight_idx in range(non_zero_weight_indices.shape[0]): + raw_idx = non_zero_weight_indices[non_zero_weight_idx].item() + model = self.models[raw_idx] + posterior = model.posterior(x) + # unstandardize predictions + posterior_mean = posterior.mean.squeeze(-1) + posterior_cov = posterior.mvn.lazy_covariance_matrix + # apply weight + weight = non_zero_weights[non_zero_weight_idx] + weighted_means.append(weight * posterior_mean) + weighted_covars.append(posterior_cov * weight**2) + # set mean and covariance to be the rank-weighted sum the means and covariances of the + # base models and target model + mean_x = torch.stack(weighted_means).sum(dim=0) + covar_x = PsdSumLazyTensor(*weighted_covars) + return MultivariateNormal(mean_x, covar_x) + + +# ### Optimize target function using RGPE + qNEI + +# In[18]: + + +# suppress GPyTorch warnings about adding jitter +import warnings + +from botorch.acquisition.logei import qLogNoisyExpectedImprovement +from botorch.optim.optimize import optimize_acqf +from botorch.sampling.normal import SobolQMCNormalSampler + + +warnings.filterwarnings("ignore", "^.*jitter.*", category=RuntimeWarning) + + +best_rgpe_all = [] +best_random_all = [] +best_vanilla_nei_all = [] +N_BATCH = 10 if not SMOKE_TEST else 2 +NUM_POSTERIOR_SAMPLES = 256 if not SMOKE_TEST else 16 +RANDOM_INITIALIZATION_SIZE = 3 +N_TRIALS = 10 if not SMOKE_TEST else 2 +MC_SAMPLES = 512 if not SMOKE_TEST else 32 +N_RESTART_CANDIDATES = 512 if not SMOKE_TEST else 8 +N_RESTARTS = 10 if not SMOKE_TEST else 2 +Q_BATCH_SIZE = 1 + + +# Average over multiple trials +for trial in range(N_TRIALS): + print(f"Trial {trial + 1} of {N_TRIALS}") + best_rgpe = [] + best_random = [] + best_vanilla_nei = [] + # Initial random observations + raw_x = draw_sobol_samples( + bounds=BOUNDS, n=RANDOM_INITIALIZATION_SIZE, q=1, seed=trial + ).squeeze(1) + train_x = normalize(raw_x, bounds=BOUNDS) + train_y_noiseless = f(raw_x) + train_y = train_y_noiseless + noise_std * torch.randn_like(train_y_noiseless) + train_yvar = torch.full_like(train_y, noise_std**2) + vanilla_nei_train_x = train_x.clone() + vanilla_nei_train_y = train_y.clone() + vanilla_nei_train_yvar = train_yvar.clone() + # keep track of the best observed point at each iteration + best_value = train_y.max().item() + best_rgpe.append(best_value) + best_random.append(best_value) + vanilla_nei_best_value = best_value + best_vanilla_nei.append(vanilla_nei_best_value) + + # Run N_BATCH rounds of BayesOpt after the initial random batch + for iteration in range(N_BATCH): + target_model = get_fitted_model(train_x, train_y, train_yvar) + model_list = base_model_list + [target_model] + rank_weights = compute_rank_weights( + train_x, + train_y, + base_model_list, + target_model, + NUM_POSTERIOR_SAMPLES, + ) + + # create model and acquisition function + rgpe_model = RGPE(model_list, rank_weights) + sampler_qnei = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + qNEI = qLogNoisyExpectedImprovement( + model=rgpe_model, + X_baseline=train_x, + sampler=sampler_qnei, + prune_baseline=False, + ) + + # optimize + candidate, _ = optimize_acqf( + acq_function=qNEI, + bounds=torch.tensor([[0.0], [1.0]], dtype=dtype, device=device), + q=Q_BATCH_SIZE, + num_restarts=N_RESTARTS, + raw_samples=N_RESTART_CANDIDATES, + ) + + # fetch the new values + new_x = candidate.detach() + new_y_noiseless = f(unnormalize(new_x, bounds=BOUNDS)) + new_y = new_y_noiseless + noise_std * torch.randn_like(new_y_noiseless) + new_yvar = torch.full_like(new_y, noise_std**2) + + # update training points + train_x = torch.cat((train_x, new_x)) + train_y = torch.cat((train_y, new_y)) + train_yvar = torch.cat((train_yvar, new_yvar)) + random_candidate = torch.rand(1, dtype=dtype, device=device) + next_random_noiseless = f(unnormalize(random_candidate, bounds=BOUNDS)) + next_random = next_random_noiseless + noise_std * torch.randn_like( + next_random_noiseless + ) + next_random_best = next_random.max().item() + best_random.append(max(best_random[-1], next_random_best)) + + # get the new best observed value + best_value = train_y.max().item() + best_rgpe.append(best_value) + + # Run Vanilla NEI for comparison + vanilla_nei_model = get_fitted_model( + vanilla_nei_train_x, + vanilla_nei_train_y, + vanilla_nei_train_yvar, + ) + vanilla_nei_sampler = SobolQMCNormalSampler( + sample_shape=torch.Size([MC_SAMPLES]) + ) + vanilla_qNEI = qLogNoisyExpectedImprovement( + model=vanilla_nei_model, + X_baseline=vanilla_nei_train_x, + sampler=vanilla_nei_sampler, + ) + vanilla_nei_candidate, _ = optimize_acqf( + acq_function=vanilla_qNEI, + bounds=torch.tensor([[0.0], [1.0]], dtype=dtype, device=device), + q=Q_BATCH_SIZE, + num_restarts=N_RESTARTS, + raw_samples=N_RESTART_CANDIDATES, + ) + # fetch the new values + vanilla_nei_new_x = vanilla_nei_candidate.detach() + vanilla_nei_new_y_noiseless = f(unnormalize(vanilla_nei_new_x, bounds=BOUNDS)) + vanilla_nei_new_y = vanilla_nei_new_y_noiseless + noise_std * torch.randn_like( + new_y_noiseless + ) + vanilla_nei_new_yvar = torch.full_like(vanilla_nei_new_y, noise_std**2) + + # update training points + vanilla_nei_train_x = torch.cat([vanilla_nei_train_x, vanilla_nei_new_x]) + vanilla_nei_train_y = torch.cat([vanilla_nei_train_y, vanilla_nei_new_y]) + vanilla_nei_train_yvar = torch.cat( + [vanilla_nei_train_yvar, vanilla_nei_new_yvar] + ) + + # get the new best observed value + vanilla_nei_best_value = vanilla_nei_train_y.max().item() + best_vanilla_nei.append(vanilla_nei_best_value) + + best_rgpe_all.append(best_rgpe) + best_random_all.append(best_random) + best_vanilla_nei_all.append(best_vanilla_nei) + + +# #### Plot best observed value vs iteration + +# In[19]: + + +import numpy as np + + +best_rgpe_all = np.array(best_rgpe_all) +best_random_all = np.array(best_random_all) +best_vanilla_nei_all = np.array(best_vanilla_nei_all) + +x = range(RANDOM_INITIALIZATION_SIZE, RANDOM_INITIALIZATION_SIZE + N_BATCH + 1) + +fig, ax = plt.subplots(1, 1, figsize=(10, 6)) +# Plot RGPE - LogNEI +ax.errorbar( + x, + best_rgpe_all.mean(axis=0), + yerr=1.96 * best_rgpe_all.std(axis=0) / math.sqrt(N_TRIALS), + label="RGPE - LogNEI", + linewidth=3, + capsize=5, + capthick=3, +) +# Plot SingleTaskGP - LogNEI +ax.errorbar( + x, + best_vanilla_nei_all.mean(axis=0), + yerr=1.96 * best_vanilla_nei_all.std(axis=0) / math.sqrt(N_TRIALS), + label="SingleTaskGP - LogNEI", + linewidth=3, + capsize=5, + capthick=3, +) +# Plot Random +ax.errorbar( + x, + best_random_all.mean(axis=0), + yerr=1.96 * best_random_all.std(axis=0) / math.sqrt(N_TRIALS), + label="Random", + linewidth=3, + capsize=5, + capthick=3, +) +ax.set_ylim(bottom=0) +ax.set_xlabel("Iteration", fontsize=12) +ax.set_ylabel("Best Observed Value", fontsize=12) +ax.set_title("Best Observed Value by Iteration", fontsize=12) +ax.legend(loc="lower right", fontsize=10) +plt.tight_layout() + + +# In[ ]: + + + + diff --git a/website-old/static/files/multi_fidelity_bo.ipynb b/website-old/static/files/multi_fidelity_bo.ipynb new file mode 100644 index 0000000000..5fc95ca26f --- /dev/null +++ b/website-old/static/files/multi_fidelity_bo.ipynb @@ -0,0 +1,625 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Continuous Multi-Fidelity BO in BoTorch with Knowledge Gradient\n", + "\n", + "In this tutorial, we show how to perform continuous multi-fidelity Bayesian optimization (BO) in BoTorch using the multi-fidelity Knowledge Gradient (qMFKG) acquisition function [1, 2].\n", + "\n", + "[1] [J. Wu, P.I. Frazier. Continuous-Fidelity Bayesian Optimization with Knowledge Gradient. NIPS Workshop on Bayesian Optimization, 2017.](https://bayesopt.github.io/papers/2017/20.pdf)\n", + "\n", + "[2] [J. Wu, S. Toscano-Palmerin, P.I. Frazier, A.G. Wilson. Practical Multi-fidelity Bayesian Optimization for Hyperparameter Tuning. Conference on Uncertainty in Artificial Intelligence (UAI), 2019](https://arxiv.org/pdf/1903.04703.pdf)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set dtype and device" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "\n", + "\n", + "tkwargs = {\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", + "}\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Problem setup\n", + "\n", + "We'll consider the Augmented Hartmann multi-fidelity synthetic test problem. This function is a version of the Hartmann6 test function with an additional dimension representing the fidelity parameter; details are in [2]. The function takes the form $f(x,s)$ where $x \\in [0,1]^6$ and $s \\in [0,1]$. The target fidelity is 1.0, which means that our goal is to solve $\\max_x f(x,1.0)$ by making use of cheaper evaluations $f(x,s)$ for $s < 1.0$. In this example, we'll assume that the cost function takes the form $5.0 + s$, illustrating a situation where the fixed cost is $5.0$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.test_functions.multi_fidelity import AugmentedHartmann\n", + "\n", + "\n", + "problem = AugmentedHartmann(negate=True).to(**tkwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Model initialization\n", + "\n", + "We use a `SingleTaskMultiFidelityGP` as the surrogate model, which uses a kernel from [2] that is well-suited for multi-fidelity applications." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "from botorch.utils.transforms import unnormalize\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "\n", + "\n", + "def generate_initial_data(n=16):\n", + " # generate training data\n", + " train_x = torch.rand(n, 7, **tkwargs)\n", + " train_obj = problem(train_x).unsqueeze(-1) # add output dimension\n", + " return train_x, train_obj\n", + "\n", + "\n", + "def initialize_model(train_x, train_obj):\n", + " # define a surrogate model suited for a \"training data\"-like fidelity parameter\n", + " # in dimension 6, as in [2]\n", + " model = SingleTaskMultiFidelityGP(\n", + " train_x, train_obj, outcome_transform=Standardize(m=1), data_fidelities=[6]\n", + " )\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " return mll, model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define a helper function to construct the MFKG acquisition function\n", + "The helper function illustrates how one can initialize a $q$MFKG acquisition function. In this example, we assume that the affine cost is known. We then use the notion of a `CostAwareUtility` in BoTorch to scalarize the competing objectives of information gain and cost. The MFKG acquisition function optimizes the ratio of information gain to cost, which is captured by the `InverseCostWeightedUtility`.\n", + "\n", + "In order for MFKG to evaluate the information gain, it uses the model to predict the function value at the highest fidelity after conditioning on the observation. This is handled by the `project` argument, which specifies how to transform a tensor `X` to its target fidelity. We use a default helper function called `project_to_target_fidelity` to achieve this.\n", + "\n", + "An important point to keep in mind: in the case of standard KG, one can ignore the current value and simply optimize the expected maximum posterior mean of the next stage. However, for MFKG, since the goal is optimize information *gain* per cost, it is important to first compute the current value (i.e., maximum of the posterior mean at the target fidelity). To accomplish this, we use a `FixedFeatureAcquisitionFunction` on top of a `PosteriorMean`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch import fit_gpytorch_mll\n", + "from botorch.models.cost import AffineFidelityCostModel\n", + "from botorch.acquisition.cost_aware import InverseCostWeightedUtility\n", + "from botorch.acquisition import PosteriorMean\n", + "from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient\n", + "from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction\n", + "from botorch.optim.optimize import optimize_acqf\n", + "from botorch.acquisition.utils import project_to_target_fidelity\n", + "\n", + "\n", + "bounds = torch.tensor([[0.0] * problem.dim, [1.0] * problem.dim], **tkwargs)\n", + "target_fidelities = {6: 1.0}\n", + "\n", + "cost_model = AffineFidelityCostModel(fidelity_weights={6: 1.0}, fixed_cost=5.0)\n", + "cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)\n", + "\n", + "\n", + "def project(X):\n", + " return project_to_target_fidelity(X=X, target_fidelities=target_fidelities)\n", + "\n", + "\n", + "def get_mfkg(model):\n", + "\n", + " curr_val_acqf = FixedFeatureAcquisitionFunction(\n", + " acq_function=PosteriorMean(model),\n", + " d=7,\n", + " columns=[6],\n", + " values=[1],\n", + " )\n", + "\n", + " _, current_value = optimize_acqf(\n", + " acq_function=curr_val_acqf,\n", + " bounds=bounds[:, :-1],\n", + " q=1,\n", + " num_restarts=10 if not SMOKE_TEST else 2,\n", + " raw_samples=1024 if not SMOKE_TEST else 4,\n", + " options={\"batch_limit\": 10, \"maxiter\": 200},\n", + " )\n", + "\n", + " return qMultiFidelityKnowledgeGradient(\n", + " model=model,\n", + " num_fantasies=128 if not SMOKE_TEST else 2,\n", + " current_value=current_value,\n", + " cost_aware_utility=cost_aware_utility,\n", + " project=project,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define a helper function that performs the essential BO step\n", + "This helper function optimizes the acquisition function and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.optim.initializers import gen_one_shot_kg_initial_conditions\n", + "\n", + "torch.set_printoptions(precision=3, sci_mode=False)\n", + "\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "\n", + "\n", + "def optimize_mfkg_and_get_observation(mfkg_acqf):\n", + " \"\"\"Optimizes MFKG and returns a new candidate, observation, and cost.\"\"\"\n", + "\n", + " X_init = gen_one_shot_kg_initial_conditions(\n", + " acq_function=mfkg_acqf,\n", + " bounds=bounds,\n", + " q=4,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=mfkg_acqf,\n", + " bounds=bounds,\n", + " q=4,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " batch_initial_conditions=X_init,\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + " # observe new values\n", + " cost = cost_model(candidates).sum()\n", + " new_x = candidates.detach()\n", + " new_obj = problem(new_x).unsqueeze(-1)\n", + " print(f\"candidates:\\n{new_x}\\n\")\n", + " print(f\"observations:\\n{new_obj}\\n\\n\")\n", + " return new_x, new_obj, cost" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perform a few steps of multi-fidelity BO\n", + "First, let's generate some initial random data and fit a surrogate model." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "train_x, train_obj = generate_initial_data(n=16)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now use the helper functions above to run a few iterations of BO." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.005, 0.185, 0.708, 0.670, 0.472, 0.796, 0.000],\n", + " [0.000, 0.335, 0.670, 0.584, 0.301, 0.733, 0.000],\n", + " [0.066, 0.127, 0.583, 0.555, 0.302, 0.734, 0.000],\n", + " [0.023, 0.210, 0.606, 0.756, 0.236, 0.807, 0.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[0.427],\n", + " [1.045],\n", + " [1.396],\n", + " [0.416]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.024, 0.137, 0.466, 0.545, 0.236, 0.654, 0.000],\n", + " [0.220, 0.175, 0.597, 0.537, 0.269, 0.681, 0.000],\n", + " [0.045, 0.088, 0.644, 0.520, 0.234, 0.818, 0.013],\n", + " [0.024, 0.117, 0.613, 0.496, 0.330, 0.638, 0.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[1.372],\n", + " [1.640],\n", + " [1.259],\n", + " [1.728]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.162, 0.180, 0.608, 0.453, 0.377, 0.667, 0.010],\n", + " [0.180, 0.138, 0.505, 0.444, 0.293, 0.554, 0.751],\n", + " [0.185, 0.046, 0.631, 0.491, 0.384, 0.585, 0.002],\n", + " [0.151, 0.167, 0.698, 0.474, 0.240, 0.580, 0.024]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.165],\n", + " [2.315],\n", + " [1.676],\n", + " [1.693]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.156, 0.163, 0.527, 0.376, 0.290, 0.618, 0.000],\n", + " [0.208, 0.148, 0.480, 0.403, 0.399, 0.589, 0.004],\n", + " [0.131, 0.213, 0.527, 0.401, 0.377, 0.502, 0.009],\n", + " [0.240, 0.241, 0.519, 0.408, 0.306, 0.564, 0.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.882],\n", + " [2.431],\n", + " [2.120],\n", + " [2.504]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.215, 0.081, 0.494, 0.335, 0.243, 0.620, 0.000],\n", + " [0.198, 0.180, 0.539, 0.310, 0.293, 0.655, 0.016],\n", + " [0.440, 0.558, 0.028, 0.675, 0.168, 0.008, 0.000],\n", + " [0.153, 0.201, 0.453, 0.338, 0.252, 0.656, 0.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.878],\n", + " [3.178],\n", + " [1.162],\n", + " [2.952]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.232, 0.170, 0.469, 0.256, 0.312, 0.629, 0.037],\n", + " [0.126, 0.141, 0.519, 0.245, 0.308, 0.671, 0.016],\n", + " [0.654, 0.372, 0.777, 0.420, 0.574, 0.380, 0.341],\n", + " [0.218, 0.144, 0.481, 0.280, 0.318, 0.710, 0.031]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[3.235],\n", + " [3.161],\n", + " [0.170],\n", + " [3.209]], dtype=torch.float64)\n", + "\n", + "\n" + ] + } + ], + "source": [ + "cumulative_cost = 0.0\n", + "N_ITER = 6 if not SMOKE_TEST else 2\n", + "\n", + "\n", + "for _ in range(N_ITER):\n", + " mll, model = initialize_model(train_x, train_obj)\n", + " fit_gpytorch_mll(mll)\n", + " mfkg_acqf = get_mfkg(model)\n", + " new_x, new_obj, cost = optimize_mfkg_and_get_observation(mfkg_acqf)\n", + " train_x = torch.cat([train_x, new_x])\n", + " train_obj = torch.cat([train_obj, new_obj])\n", + " cumulative_cost += cost" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Make a final recommendation\n", + "In multi-fidelity BO, there are usually fewer observations of the function at the target fidelity, so it is important to use a recommendation function that uses the correct fidelity. Here, we maximize the posterior mean with the fidelity dimension fixed to the target fidelity of 1.0." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def get_recommendation(model):\n", + " rec_acqf = FixedFeatureAcquisitionFunction(\n", + " acq_function=PosteriorMean(model),\n", + " d=7,\n", + " columns=[6],\n", + " values=[1],\n", + " )\n", + "\n", + " final_rec, _ = optimize_acqf(\n", + " acq_function=rec_acqf,\n", + " bounds=bounds[:, :-1],\n", + " q=1,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + "\n", + " final_rec = rec_acqf._construct_X_full(final_rec)\n", + "\n", + " objective_value = problem(final_rec)\n", + " print(f\"recommended point:\\n{final_rec}\\n\\nobjective value:\\n{objective_value}\")\n", + " return final_rec" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recommended point:\n", + "tensor([[0.208, 0.164, 0.514, 0.280, 0.301, 0.664, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "objective value:\n", + "tensor([3.298], dtype=torch.float64)\n", + "\n", + "total cost: 121.25572809899545\n", + "\n" + ] + } + ], + "source": [ + "final_rec = get_recommendation(model)\n", + "print(f\"\\ntotal cost: {cumulative_cost}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Comparison to standard EI (always use target fidelity)\n", + "Let's now repeat the same steps using a standard EI acquisition function (note that this is not a rigorous comparison as we are only looking at one trial in order to keep computational requirements low)." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.acquisition import qExpectedImprovement\n", + "\n", + "\n", + "def get_ei(model, best_f):\n", + "\n", + " return FixedFeatureAcquisitionFunction(\n", + " acq_function=qExpectedImprovement(model=model, best_f=best_f),\n", + " d=7,\n", + " columns=[6],\n", + " values=[1],\n", + " )\n", + "\n", + "\n", + "def optimize_ei_and_get_observation(ei_acqf):\n", + " \"\"\"Optimizes EI and returns a new candidate, observation, and cost.\"\"\"\n", + "\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=ei_acqf,\n", + " bounds=bounds[:, :-1],\n", + " q=4,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + "\n", + " # add the fidelity parameter\n", + " candidates = ei_acqf._construct_X_full(candidates)\n", + "\n", + " # observe new values\n", + " cost = cost_model(candidates).sum()\n", + " new_x = candidates.detach()\n", + " new_obj = problem(new_x).unsqueeze(-1)\n", + " print(f\"candidates:\\n{new_x}\\n\")\n", + " print(f\"observations:\\n{new_obj}\\n\\n\")\n", + " return new_x, new_obj, cost" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "candidates:\n", + "tensor([[0.284, 0.692, 0.351, 0.840, 0.487, 0.058, 1.000],\n", + " [0.571, 0.227, 0.556, 0.254, 0.208, 0.771, 1.000],\n", + " [0.475, 0.811, 0.448, 0.853, 0.403, 0.000, 1.000],\n", + " [0.625, 0.141, 0.299, 0.163, 0.171, 0.854, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[0.895],\n", + " [1.644],\n", + " [1.248],\n", + " [0.905]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.580, 0.206, 0.677, 0.320, 0.163, 0.809, 1.000],\n", + " [0.538, 0.242, 0.613, 0.248, 0.152, 0.667, 1.000],\n", + " [0.453, 0.231, 0.634, 0.252, 0.290, 0.771, 1.000],\n", + " [0.619, 0.325, 0.576, 0.301, 0.226, 0.767, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[1.357],\n", + " [1.445],\n", + " [2.271],\n", + " [1.486]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.416, 0.189, 0.617, 0.265, 0.331, 0.728, 1.000],\n", + " [0.757, 0.521, 0.077, 0.687, 0.779, 0.473, 1.000],\n", + " [0.416, 0.243, 0.699, 0.191, 0.315, 0.793, 1.000],\n", + " [0.753, 0.544, 0.275, 0.703, 0.266, 0.637, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.547],\n", + " [0.010],\n", + " [2.088],\n", + " [0.134]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.057, 0.684, 1.000, 0.133, 0.647, 0.573, 1.000],\n", + " [0.339, 0.169, 0.558, 0.284, 0.349, 0.719, 1.000],\n", + " [0.430, 0.141, 0.663, 0.284, 0.367, 0.703, 1.000],\n", + " [0.734, 0.006, 0.873, 0.563, 0.275, 0.925, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[0.065],\n", + " [2.879],\n", + " [2.321],\n", + " [0.384]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.286, 0.174, 0.514, 0.281, 0.354, 0.746, 1.000],\n", + " [0.388, 0.494, 0.511, 0.892, 0.814, 0.650, 1.000],\n", + " [0.311, 0.700, 0.253, 0.139, 0.203, 0.086, 1.000],\n", + " [0.323, 0.109, 0.950, 0.702, 0.221, 0.896, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[2.944],\n", + " [0.026],\n", + " [0.350],\n", + " [0.451]], dtype=torch.float64)\n", + "\n", + "\n", + "candidates:\n", + "tensor([[0.694, 0.341, 0.325, 0.928, 0.077, 0.603, 1.000],\n", + " [0.758, 0.194, 0.803, 0.440, 0.016, 0.814, 1.000],\n", + " [0.252, 0.168, 0.529, 0.280, 0.329, 0.698, 1.000],\n", + " [0.438, 0.572, 0.395, 0.611, 0.429, 0.559, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "observations:\n", + "tensor([[0.011],\n", + " [0.574],\n", + " [3.203],\n", + " [0.413]], dtype=torch.float64)\n", + "\n", + "\n" + ] + } + ], + "source": [ + "cumulative_cost = 0.0\n", + "\n", + "train_x, train_obj = generate_initial_data(n=16)\n", + "\n", + "for _ in range(N_ITER):\n", + " mll, model = initialize_model(train_x, train_obj)\n", + " fit_gpytorch_mll(mll)\n", + " ei_acqf = get_ei(model, best_f=train_obj.max())\n", + " new_x, new_obj, cost = optimize_ei_and_get_observation(ei_acqf)\n", + " train_x = torch.cat([train_x, new_x])\n", + " train_obj = torch.cat([train_obj, new_obj])\n", + " cumulative_cost += cost" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recommended point:\n", + "tensor([[0.288, 0.175, 0.520, 0.283, 0.351, 0.735, 1.000]],\n", + " dtype=torch.float64)\n", + "\n", + "objective value:\n", + "tensor([2.990], dtype=torch.float64)\n", + "\n", + "total cost: 144.0\n", + "\n" + ] + } + ], + "source": [ + "final_rec = get_recommendation(model)\n", + "print(f\"\\ntotal cost: {cumulative_cost}\\n\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/multi_fidelity_bo.py b/website-old/static/files/multi_fidelity_bo.py new file mode 100644 index 0000000000..063f6108fb --- /dev/null +++ b/website-old/static/files/multi_fidelity_bo.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Continuous Multi-Fidelity BO in BoTorch with Knowledge Gradient +# +# In this tutorial, we show how to perform continuous multi-fidelity Bayesian optimization (BO) in BoTorch using the multi-fidelity Knowledge Gradient (qMFKG) acquisition function [1, 2]. +# +# [1] [J. Wu, P.I. Frazier. Continuous-Fidelity Bayesian Optimization with Knowledge Gradient. NIPS Workshop on Bayesian Optimization, 2017.](https://bayesopt.github.io/papers/2017/20.pdf) +# +# [2] [J. Wu, S. Toscano-Palmerin, P.I. Frazier, A.G. Wilson. Practical Multi-fidelity Bayesian Optimization for Hyperparameter Tuning. Conference on Uncertainty in Artificial Intelligence (UAI), 2019](https://arxiv.org/pdf/1903.04703.pdf) + +# ### Set dtype and device + +# In[1]: + + +import os +import torch + + +tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), +} +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Problem setup +# +# We'll consider the Augmented Hartmann multi-fidelity synthetic test problem. This function is a version of the Hartmann6 test function with an additional dimension representing the fidelity parameter; details are in [2]. The function takes the form $f(x,s)$ where $x \in [0,1]^6$ and $s \in [0,1]$. The target fidelity is 1.0, which means that our goal is to solve $\max_x f(x,1.0)$ by making use of cheaper evaluations $f(x,s)$ for $s < 1.0$. In this example, we'll assume that the cost function takes the form $5.0 + s$, illustrating a situation where the fixed cost is $5.0$. + +# In[2]: + + +from botorch.test_functions.multi_fidelity import AugmentedHartmann + + +problem = AugmentedHartmann(negate=True).to(**tkwargs) + + +# #### Model initialization +# +# We use a `SingleTaskMultiFidelityGP` as the surrogate model, which uses a kernel from [2] that is well-suited for multi-fidelity applications. + +# In[3]: + + +from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP +from botorch.models.transforms.outcome import Standardize +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood +from botorch.utils.transforms import unnormalize +from botorch.utils.sampling import draw_sobol_samples + + +def generate_initial_data(n=16): + # generate training data + train_x = torch.rand(n, 7, **tkwargs) + train_obj = problem(train_x).unsqueeze(-1) # add output dimension + return train_x, train_obj + + +def initialize_model(train_x, train_obj): + # define a surrogate model suited for a "training data"-like fidelity parameter + # in dimension 6, as in [2] + model = SingleTaskMultiFidelityGP( + train_x, train_obj, outcome_transform=Standardize(m=1), data_fidelities=[6] + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + return mll, model + + +# #### Define a helper function to construct the MFKG acquisition function +# The helper function illustrates how one can initialize a $q$MFKG acquisition function. In this example, we assume that the affine cost is known. We then use the notion of a `CostAwareUtility` in BoTorch to scalarize the competing objectives of information gain and cost. The MFKG acquisition function optimizes the ratio of information gain to cost, which is captured by the `InverseCostWeightedUtility`. +# +# In order for MFKG to evaluate the information gain, it uses the model to predict the function value at the highest fidelity after conditioning on the observation. This is handled by the `project` argument, which specifies how to transform a tensor `X` to its target fidelity. We use a default helper function called `project_to_target_fidelity` to achieve this. +# +# An important point to keep in mind: in the case of standard KG, one can ignore the current value and simply optimize the expected maximum posterior mean of the next stage. However, for MFKG, since the goal is optimize information *gain* per cost, it is important to first compute the current value (i.e., maximum of the posterior mean at the target fidelity). To accomplish this, we use a `FixedFeatureAcquisitionFunction` on top of a `PosteriorMean`. + +# In[4]: + + +from botorch import fit_gpytorch_mll +from botorch.models.cost import AffineFidelityCostModel +from botorch.acquisition.cost_aware import InverseCostWeightedUtility +from botorch.acquisition import PosteriorMean +from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient +from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction +from botorch.optim.optimize import optimize_acqf +from botorch.acquisition.utils import project_to_target_fidelity + + +bounds = torch.tensor([[0.0] * problem.dim, [1.0] * problem.dim], **tkwargs) +target_fidelities = {6: 1.0} + +cost_model = AffineFidelityCostModel(fidelity_weights={6: 1.0}, fixed_cost=5.0) +cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) + + +def project(X): + return project_to_target_fidelity(X=X, target_fidelities=target_fidelities) + + +def get_mfkg(model): + + curr_val_acqf = FixedFeatureAcquisitionFunction( + acq_function=PosteriorMean(model), + d=7, + columns=[6], + values=[1], + ) + + _, current_value = optimize_acqf( + acq_function=curr_val_acqf, + bounds=bounds[:, :-1], + q=1, + num_restarts=10 if not SMOKE_TEST else 2, + raw_samples=1024 if not SMOKE_TEST else 4, + options={"batch_limit": 10, "maxiter": 200}, + ) + + return qMultiFidelityKnowledgeGradient( + model=model, + num_fantasies=128 if not SMOKE_TEST else 2, + current_value=current_value, + cost_aware_utility=cost_aware_utility, + project=project, + ) + + +# #### Define a helper function that performs the essential BO step +# This helper function optimizes the acquisition function and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. + +# In[5]: + + +from botorch.optim.initializers import gen_one_shot_kg_initial_conditions + +torch.set_printoptions(precision=3, sci_mode=False) + +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 + + +def optimize_mfkg_and_get_observation(mfkg_acqf): + """Optimizes MFKG and returns a new candidate, observation, and cost.""" + + X_init = gen_one_shot_kg_initial_conditions( + acq_function=mfkg_acqf, + bounds=bounds, + q=4, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + candidates, _ = optimize_acqf( + acq_function=mfkg_acqf, + bounds=bounds, + q=4, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + batch_initial_conditions=X_init, + options={"batch_limit": 5, "maxiter": 200}, + ) + # observe new values + cost = cost_model(candidates).sum() + new_x = candidates.detach() + new_obj = problem(new_x).unsqueeze(-1) + print(f"candidates:\n{new_x}\n") + print(f"observations:\n{new_obj}\n\n") + return new_x, new_obj, cost + + +# ### Perform a few steps of multi-fidelity BO +# First, let's generate some initial random data and fit a surrogate model. + +# In[6]: + + +train_x, train_obj = generate_initial_data(n=16) + + +# We can now use the helper functions above to run a few iterations of BO. + +# In[7]: + + +cumulative_cost = 0.0 +N_ITER = 6 if not SMOKE_TEST else 2 + + +for _ in range(N_ITER): + mll, model = initialize_model(train_x, train_obj) + fit_gpytorch_mll(mll) + mfkg_acqf = get_mfkg(model) + new_x, new_obj, cost = optimize_mfkg_and_get_observation(mfkg_acqf) + train_x = torch.cat([train_x, new_x]) + train_obj = torch.cat([train_obj, new_obj]) + cumulative_cost += cost + + +# ### Make a final recommendation +# In multi-fidelity BO, there are usually fewer observations of the function at the target fidelity, so it is important to use a recommendation function that uses the correct fidelity. Here, we maximize the posterior mean with the fidelity dimension fixed to the target fidelity of 1.0. + +# In[8]: + + +def get_recommendation(model): + rec_acqf = FixedFeatureAcquisitionFunction( + acq_function=PosteriorMean(model), + d=7, + columns=[6], + values=[1], + ) + + final_rec, _ = optimize_acqf( + acq_function=rec_acqf, + bounds=bounds[:, :-1], + q=1, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + options={"batch_limit": 5, "maxiter": 200}, + ) + + final_rec = rec_acqf._construct_X_full(final_rec) + + objective_value = problem(final_rec) + print(f"recommended point:\n{final_rec}\n\nobjective value:\n{objective_value}") + return final_rec + + +# In[9]: + + +final_rec = get_recommendation(model) +print(f"\ntotal cost: {cumulative_cost}\n") + + +# ### Comparison to standard EI (always use target fidelity) +# Let's now repeat the same steps using a standard EI acquisition function (note that this is not a rigorous comparison as we are only looking at one trial in order to keep computational requirements low). + +# In[10]: + + +from botorch.acquisition import qExpectedImprovement + + +def get_ei(model, best_f): + + return FixedFeatureAcquisitionFunction( + acq_function=qExpectedImprovement(model=model, best_f=best_f), + d=7, + columns=[6], + values=[1], + ) + + +def optimize_ei_and_get_observation(ei_acqf): + """Optimizes EI and returns a new candidate, observation, and cost.""" + + candidates, _ = optimize_acqf( + acq_function=ei_acqf, + bounds=bounds[:, :-1], + q=4, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + options={"batch_limit": 5, "maxiter": 200}, + ) + + # add the fidelity parameter + candidates = ei_acqf._construct_X_full(candidates) + + # observe new values + cost = cost_model(candidates).sum() + new_x = candidates.detach() + new_obj = problem(new_x).unsqueeze(-1) + print(f"candidates:\n{new_x}\n") + print(f"observations:\n{new_obj}\n\n") + return new_x, new_obj, cost + + +# In[11]: + + +cumulative_cost = 0.0 + +train_x, train_obj = generate_initial_data(n=16) + +for _ in range(N_ITER): + mll, model = initialize_model(train_x, train_obj) + fit_gpytorch_mll(mll) + ei_acqf = get_ei(model, best_f=train_obj.max()) + new_x, new_obj, cost = optimize_ei_and_get_observation(ei_acqf) + train_x = torch.cat([train_x, new_x]) + train_obj = torch.cat([train_obj, new_obj]) + cumulative_cost += cost + + +# In[12]: + + +final_rec = get_recommendation(model) +print(f"\ntotal cost: {cumulative_cost}\n") + diff --git a/website-old/static/files/multi_objective_bo.ipynb b/website-old/static/files/multi_objective_bo.ipynb new file mode 100644 index 0000000000..f7411f662a --- /dev/null +++ b/website-old/static/files/multi_objective_bo.ipynb @@ -0,0 +1,772 @@ +{ + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "61330204-a407-449f-af77-fcd868546651", + "showInput": false + }, + "source": [ + "## Noisy, Parallel, Multi-Objective BO in BoTorch with qEHVI, qNEHVI, and qNParEGO\n", + "\n", + "In this tutorial, we illustrate how to implement a simple multi-objective (MO) Bayesian Optimization (BO) closed loop in BoTorch.\n", + "\n", + "In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. See [here](https://ax.dev/tutorials/multiobjective_optimization.html) for an Ax tutorial on MOBO. If desired, you can use a custom BoTorch model in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. Given a `MultiObjective`, Ax will default to the $q$NEHVI acquisiton function. If desired, this can also be customized by adding `\"botorch_acqf_class\": ,` to the `model_kwargs`.\n", + "\n", + "We use the parallel ParEGO ($q$ParEGO) [1], parallel Expected Hypervolume Improvement ($q$EHVI) [1], and parallel Noisy Expected Hypervolume Improvement ($q$NEHVI) [2] acquisition functions to optimize a synthetic BraninCurrin problem test function with additive Gaussian observation noise over a 2-parameter search space [0,1]^2. See `botorch/test_functions/multi_objective.py` for details on BraninCurrin. The noise standard deviations are 15.19 and 0.63 for each objective, respectively.\n", + "\n", + "Since botorch assumes a maximization of all objectives, we seek to find the Pareto frontier, the set of optimal trade-offs where improving one metric means deteriorating another.\n", + "\n", + "[1] [S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2006.05078)\n", + "\n", + "[2] [S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances in Neural Information Processing Systems 34, 2021.](https://arxiv.org/abs/2105.08195)\n", + "\n", + "**For batch optimization (or in noisy settings), we strongly recommend using $q$NEHVI rather than $q$EHVI because it is far more efficient than $q$EHVI and mathematically equivalent in the noiseless setting.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f01416cb-e83e-4263-b599-b2a3221d144d", + "showInput": false + }, + "source": [ + "### Set dtype and device\n", + "Note: $q$EHVI and $q$NEHVI aggressively exploit parallel hardware and are both much faster when run on a GPU. See [1, 2] for details." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649461840, + "executionStopTime": 1668649461848, + "originalKey": "41c30177-379b-4e63-9996-41bc17d70769", + "requestMsgId": "7e4820a5-df3b-45a6-9826-42541ee0f4f4", + "collapsed": false, + "customOutput": null + }, + "source": [ + "import os\n", + "import torch\n", + "\n", + "\n", + "tkwargs = {\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", + "}\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ], + "execution_count": 8, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8d895a93-397c-4d2c-b6f5-96f589312538", + "showInput": false + }, + "source": [ + "### Problem setup\n", + "" + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649462081, + "executionStopTime": 1668649462087, + "originalKey": "a8741d41-72b7-42e8-bd5d-3972be9995f7", + "requestMsgId": "bceaa0af-ffe5-415a-a5b1-93fc9cf7f71a", + "collapsed": false, + "customOutput": null + }, + "source": [ + "from botorch.test_functions.multi_objective import BraninCurrin\n", + "\n", + "\n", + "problem = BraninCurrin(negate=True).to(**tkwargs)" + ], + "execution_count": 9, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "fc047039-c2a9-4ea8-920e-c057547cfb11", + "showInput": false + }, + "source": [ + "#### Model initialization\n", + "\n", + "We use a list of `SingleTaskGP`s to model the two objectives with known noise variances. If no noise variances were provided, `SingleTaskGP` would infer (homoskedastic) noise levels instead.\n", + "\n", + "The models are initialized with $2(d+1)=6$ points drawn randomly from $[0,1]^2$." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649462312, + "executionStopTime": 1668649462318, + "originalKey": "47170c31-65e4-4f2d-949c-d91544e065fc", + "requestMsgId": "8a44b7cb-cc5f-419e-849f-2b4d1dc3abe1", + "collapsed": false, + "customOutput": null + }, + "source": [ + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.model_list_gp_regression import ModelListGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood\n", + "from botorch.utils.transforms import unnormalize, normalize\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "\n", + "NOISE_SE = torch.tensor([15.19, 0.63], **tkwargs)\n", + "\n", + "\n", + "def generate_initial_data(n=6):\n", + " # generate training data\n", + " train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1)\n", + " train_obj_true = problem(train_x)\n", + " train_obj = train_obj_true + torch.randn_like(train_obj_true) * NOISE_SE\n", + " return train_x, train_obj, train_obj_true\n", + "\n", + "\n", + "def initialize_model(train_x, train_obj):\n", + " # define models for objective and constraint\n", + " train_x = normalize(train_x, problem.bounds)\n", + " models = []\n", + " for i in range(train_obj.shape[-1]):\n", + " train_y = train_obj[..., i : i + 1]\n", + " train_yvar = torch.full_like(train_y, NOISE_SE[i] ** 2)\n", + " models.append(\n", + " SingleTaskGP(\n", + " train_x, train_y, train_yvar, outcome_transform=Standardize(m=1)\n", + " )\n", + " )\n", + " model = ModelListGP(*models)\n", + " mll = SumMarginalLogLikelihood(model.likelihood, model)\n", + " return mll, model" + ], + "execution_count": 10, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6bfaef9a-3f34-4d51-9700-fbddc79eccf1", + "showInput": false + }, + "source": [ + "#### Define a helper functions that performs the essential BO step for $q$EHVI and $q$NEHVI\n", + "The helper function below initializes the $q$EHVI acquisition function, optimizes it, and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. \n", + "\n", + "For this example, we'll use a relatively small batch of optimization ($q=4$). For batch optimization ($q>1$), passing the keyword argument `sequential=True` to the function `optimize_acqf`specifies that candidates should be optimized in a sequential greedy fashion (see [1] for details why this is important). A simple initialization heuristic is used to select the 10 restart initial locations from a set of 512 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation.\n", + "\n", + "**Reference Point**\n", + "\n", + "$q$EHVI requires specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy.\n", + "\n", + "**Partitioning the Non-dominated Space into disjoint rectangles**\n", + "\n", + "$q$EHVI requires partitioning the non-dominated space into disjoint rectangles (see [1] for details). \n", + "\n", + "*Note:* `FastNondominatedPartitioning` *will be very slow when 1) there are a lot of points on the pareto frontier and 2) there are >5 objectives.*" + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649462539, + "executionStopTime": 1668649462641, + "originalKey": "b7effe94-1327-405d-9148-c8e93470b846", + "requestMsgId": "40a5edda-3bee-43c9-b84b-c669c288eb80", + "collapsed": false, + "customOutput": null + }, + "source": [ + "from botorch.optim.optimize import optimize_acqf, optimize_acqf_list\n", + "from botorch.acquisition.objective import GenericMCObjective\n", + "from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization\n", + "from botorch.utils.multi_objective.box_decompositions.non_dominated import (\n", + " FastNondominatedPartitioning,\n", + ")\n", + "from botorch.acquisition.multi_objective.monte_carlo import (\n", + " qExpectedHypervolumeImprovement,\n", + " qNoisyExpectedHypervolumeImprovement,\n", + ")\n", + "from botorch.utils.sampling import sample_simplex\n", + "\n", + "\n", + "BATCH_SIZE = 4\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "\n", + "standard_bounds = torch.zeros(2, problem.dim, **tkwargs)\n", + "standard_bounds[1] = 1\n", + "\n", + "\n", + "def optimize_qehvi_and_get_observation(model, train_x, train_obj, sampler):\n", + " \"\"\"Optimizes the qEHVI acquisition function, and returns a new candidate and observation.\"\"\"\n", + " # partition non-dominated space into disjoint rectangles\n", + " with torch.no_grad():\n", + " pred = model.posterior(normalize(train_x, problem.bounds)).mean\n", + " partitioning = FastNondominatedPartitioning(\n", + " ref_point=problem.ref_point,\n", + " Y=pred,\n", + " )\n", + " acq_func = qExpectedHypervolumeImprovement(\n", + " model=model,\n", + " ref_point=problem.ref_point,\n", + " partitioning=partitioning,\n", + " sampler=sampler,\n", + " )\n", + " # optimize\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=standard_bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " sequential=True,\n", + " )\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=problem.bounds)\n", + " new_obj_true = problem(new_x)\n", + " new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE\n", + " return new_x, new_obj, new_obj_true" + ], + "execution_count": 11, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ccaf56e2-f0e0-4d1c-bf07-247d984535a5", + "showInput": false + }, + "source": [ + "**Integrating over function values at in-sample designs**\n", + "\n", + "$q$NEHVI integrates over the unknown function values at the previously evaluated designs (see [2] for details). Therefore, we need to provide the previously evaluated designs (`train_x`, *normalized* to be within $[0,1]^d$) to the acquisition function.\n", + "\n", + "**Efficient batch generation with Cached Box Decomposition (CBD)**\n", + "\n", + "$q$NEHVI leveraged CBD to efficiently generate large batches of candidates. CBD scales polynomially with respect to the batch size where as the inclusion-exclusion principle used by qEHVI scales exponentially with the batch size.\n", + "\n", + "**Pruning baseline designs**\n", + "To speed up integration over the function values at the previously evaluated designs, we prune the set of previously evaluated designs (by setting `prune_baseline=True`) to only include those which have positive probability of being on the current in-sample Pareto frontier." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649462860, + "executionStopTime": 1668649462867, + "originalKey": "f2749e5f-eb94-4150-8f52-057f90e08b39", + "requestMsgId": "f9e71b3c-2eb6-4b79-92c5-72d16d368233", + "collapsed": false, + "customOutput": null + }, + "source": [ + "def optimize_qnehvi_and_get_observation(model, train_x, train_obj, sampler):\n", + " \"\"\"Optimizes the qEHVI acquisition function, and returns a new candidate and observation.\"\"\"\n", + " # partition non-dominated space into disjoint rectangles\n", + " acq_func = qNoisyExpectedHypervolumeImprovement(\n", + " model=model,\n", + " ref_point=problem.ref_point.tolist(), # use known reference point\n", + " X_baseline=normalize(train_x, problem.bounds),\n", + " prune_baseline=True, # prune baseline points that have estimated zero probability of being Pareto optimal\n", + " sampler=sampler,\n", + " )\n", + " # optimize\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=standard_bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " sequential=True,\n", + " )\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=problem.bounds)\n", + " new_obj_true = problem(new_x)\n", + " new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE\n", + " return new_x, new_obj, new_obj_true" + ], + "execution_count": 12, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ad78607d-d910-441c-903b-b15dc77a432f", + "showInput": false + }, + "source": [ + "#### Define a helper function that performs the essential BO step for $q$NParEGO\n", + "The helper function below similarly initializes $q$NParEGO, optimizes it, and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. \n", + "\n", + "$q$NParEGO uses random augmented chebyshev scalarization with the `qNoisyExpectedImprovement` acquisition function. In the parallel setting ($q>1$), each candidate is optimized in sequential greedy fashion using a different random scalarization (see [1] for details).\n", + "\n", + "To do this, we create a list of `qNoisyExpectedImprovement` acquisition functions, each with different random scalarization weights. The `optimize_acqf_list` method sequentially generates one candidate per acquisition function and conditions the next candidate (and acquisition function) on the previously selected pending candidates." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649463087, + "executionStopTime": 1668649463185, + "originalKey": "806b115f-a15f-44df-b7f9-d2f098969e02", + "requestMsgId": "514d162f-78e0-447a-a483-c923cba18b80", + "collapsed": false, + "customOutput": null + }, + "source": [ + "from botorch.acquisition.monte_carlo import qNoisyExpectedImprovement\n", + "\n", + "\n", + "def optimize_qnparego_and_get_observation(model, train_x, train_obj, sampler):\n", + " \"\"\"Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization\n", + " of the qNParEGO acquisition function, and returns a new candidate and observation.\"\"\"\n", + " train_x = normalize(train_x, problem.bounds)\n", + " with torch.no_grad():\n", + " pred = model.posterior(train_x).mean\n", + " acq_func_list = []\n", + " for _ in range(BATCH_SIZE):\n", + " weights = sample_simplex(problem.num_objectives, **tkwargs).squeeze()\n", + " objective = GenericMCObjective(\n", + " get_chebyshev_scalarization(weights=weights, Y=pred)\n", + " )\n", + " acq_func = qNoisyExpectedImprovement( # pyre-ignore: [28]\n", + " model=model,\n", + " objective=objective,\n", + " X_baseline=train_x,\n", + " sampler=sampler,\n", + " prune_baseline=True,\n", + " )\n", + " acq_func_list.append(acq_func)\n", + " # optimize\n", + " candidates, _ = optimize_acqf_list(\n", + " acq_function_list=acq_func_list,\n", + " bounds=standard_bounds,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES, # used for intialization heuristic\n", + " options={\"batch_limit\": 5, \"maxiter\": 200},\n", + " )\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=problem.bounds)\n", + " new_obj_true = problem(new_x)\n", + " new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE\n", + " return new_x, new_obj, new_obj_true" + ], + "execution_count": 13, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f8ffd86a-f4bb-4984-9330-04ff4d62f189", + "showInput": false + }, + "source": [ + "### Perform Bayesian Optimization loop with $q$NEHVI, $q$EHVI, and $q$NParEGO\n", + "The Bayesian optimization \"loop\" for a batch size of $q$ simply iterates the following steps:\n", + "1. given a surrogate model, choose a batch of points $\\{x_1, x_2, \\ldots x_q\\}$\n", + "2. observe $f(x)$ for each $x$ in the batch \n", + "3. update the surrogate model. \n", + "\n", + "\n", + "Just for illustration purposes, we run one trial with `N_BATCH=20` rounds of optimization. The acquisition function is approximated using `MC_SAMPLES=128` samples.\n", + "\n", + "*Note*: Running this may take a little while." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649463513, + "executionStopTime": 1668649856754, + "originalKey": "c29b731a-64e7-401d-a5b2-3879d8d39327", + "requestMsgId": "43e1021f-9ecd-4d2c-a7ba-21e26051ce66", + "collapsed": false, + "customOutput": null + }, + "source": [ + "import time\n", + "import warnings\n", + "\n", + "from botorch import fit_gpytorch_mll\n", + "from botorch.exceptions import BadInitialCandidatesWarning\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "from botorch.utils.multi_objective.box_decompositions.dominated import (\n", + " DominatedPartitioning,\n", + ")\n", + "from botorch.utils.multi_objective.pareto import is_non_dominated\n", + "\n", + "\n", + "warnings.filterwarnings(\"ignore\", category=BadInitialCandidatesWarning)\n", + "warnings.filterwarnings(\"ignore\", category=RuntimeWarning)\n", + "\n", + "N_BATCH = 20 if not SMOKE_TEST else 5\n", + "MC_SAMPLES = 128 if not SMOKE_TEST else 16\n", + "\n", + "verbose = True\n", + "\n", + "hvs_qparego, hvs_qehvi, hvs_qnehvi, hvs_random = [], [], [], []\n", + "\n", + "# call helper functions to generate initial training data and initialize model\n", + "train_x_qparego, train_obj_qparego, train_obj_true_qparego = generate_initial_data(\n", + " n=2 * (problem.dim + 1)\n", + ")\n", + "mll_qparego, model_qparego = initialize_model(train_x_qparego, train_obj_qparego)\n", + "\n", + "train_x_qehvi, train_obj_qehvi, train_obj_true_qehvi = (\n", + " train_x_qparego,\n", + " train_obj_qparego,\n", + " train_obj_true_qparego,\n", + ")\n", + "train_x_qnehvi, train_obj_qnehvi, train_obj_true_qnehvi = (\n", + " train_x_qparego,\n", + " train_obj_qparego,\n", + " train_obj_true_qparego,\n", + ")\n", + "train_x_random, train_obj_random, train_obj_true_random = (\n", + " train_x_qparego,\n", + " train_obj_qparego,\n", + " train_obj_true_qparego,\n", + ")\n", + "mll_qehvi, model_qehvi = initialize_model(train_x_qehvi, train_obj_qehvi)\n", + "mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi, train_obj_qnehvi)\n", + "\n", + "# compute hypervolume\n", + "bd = DominatedPartitioning(ref_point=problem.ref_point, Y=train_obj_true_qparego)\n", + "volume = bd.compute_hypervolume().item()\n", + "\n", + "hvs_qparego.append(volume)\n", + "hvs_qehvi.append(volume)\n", + "hvs_qnehvi.append(volume)\n", + "hvs_random.append(volume)\n", + "\n", + "# run N_BATCH rounds of BayesOpt after the initial random batch\n", + "for iteration in range(1, N_BATCH + 1):\n", + "\n", + " t0 = time.monotonic()\n", + "\n", + " # fit the models\n", + " fit_gpytorch_mll(mll_qparego)\n", + " fit_gpytorch_mll(mll_qehvi)\n", + " fit_gpytorch_mll(mll_qnehvi)\n", + "\n", + " # define the qEI and qNEI acquisition modules using a QMC sampler\n", + " qparego_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + " qehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + " qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES]))\n", + "\n", + " # optimize acquisition functions and get new observations\n", + " (\n", + " new_x_qparego,\n", + " new_obj_qparego,\n", + " new_obj_true_qparego,\n", + " ) = optimize_qnparego_and_get_observation(\n", + " model_qparego, train_x_qparego, train_obj_qparego, qparego_sampler\n", + " )\n", + " new_x_qehvi, new_obj_qehvi, new_obj_true_qehvi = optimize_qehvi_and_get_observation(\n", + " model_qehvi, train_x_qehvi, train_obj_qehvi, qehvi_sampler\n", + " )\n", + " (\n", + " new_x_qnehvi,\n", + " new_obj_qnehvi,\n", + " new_obj_true_qnehvi,\n", + " ) = optimize_qnehvi_and_get_observation(\n", + " model_qnehvi, train_x_qnehvi, train_obj_qnehvi, qnehvi_sampler\n", + " )\n", + " new_x_random, new_obj_random, new_obj_true_random = generate_initial_data(\n", + " n=BATCH_SIZE\n", + " )\n", + "\n", + " # update training points\n", + " train_x_qparego = torch.cat([train_x_qparego, new_x_qparego])\n", + " train_obj_qparego = torch.cat([train_obj_qparego, new_obj_qparego])\n", + " train_obj_true_qparego = torch.cat([train_obj_true_qparego, new_obj_true_qparego])\n", + "\n", + " train_x_qehvi = torch.cat([train_x_qehvi, new_x_qehvi])\n", + " train_obj_qehvi = torch.cat([train_obj_qehvi, new_obj_qehvi])\n", + " train_obj_true_qehvi = torch.cat([train_obj_true_qehvi, new_obj_true_qehvi])\n", + "\n", + " train_x_qnehvi = torch.cat([train_x_qnehvi, new_x_qnehvi])\n", + " train_obj_qnehvi = torch.cat([train_obj_qnehvi, new_obj_qnehvi])\n", + " train_obj_true_qnehvi = torch.cat([train_obj_true_qnehvi, new_obj_true_qnehvi])\n", + "\n", + " train_x_random = torch.cat([train_x_random, new_x_random])\n", + " train_obj_random = torch.cat([train_obj_random, new_obj_random])\n", + " train_obj_true_random = torch.cat([train_obj_true_random, new_obj_true_random])\n", + "\n", + " # update progress\n", + " for hvs_list, train_obj in zip(\n", + " (hvs_random, hvs_qparego, hvs_qehvi, hvs_qnehvi),\n", + " (\n", + " train_obj_true_random,\n", + " train_obj_true_qparego,\n", + " train_obj_true_qehvi,\n", + " train_obj_true_qnehvi,\n", + " ),\n", + " ):\n", + " # compute hypervolume\n", + " bd = DominatedPartitioning(ref_point=problem.ref_point, Y=train_obj)\n", + " volume = bd.compute_hypervolume().item()\n", + " hvs_list.append(volume)\n", + "\n", + " # reinitialize the models so they are ready for fitting on next iteration\n", + " # Note: we find improved performance from not warm starting the model hyperparameters\n", + " # using the hyperparameters from the previous iteration\n", + " mll_qparego, model_qparego = initialize_model(train_x_qparego, train_obj_qparego)\n", + " mll_qehvi, model_qehvi = initialize_model(train_x_qehvi, train_obj_qehvi)\n", + " mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi, train_obj_qnehvi)\n", + "\n", + " t1 = time.monotonic()\n", + "\n", + " if verbose:\n", + " print(\n", + " f\"\\nBatch {iteration:>2}: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = \"\n", + " f\"({hvs_random[-1]:>4.2f}, {hvs_qparego[-1]:>4.2f}, {hvs_qehvi[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), \"\n", + " f\"time = {t1-t0:>4.2f}.\",\n", + " end=\"\",\n", + " )\n", + " else:\n", + " print(\".\", end=\"\")" + ], + "execution_count": 14, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\nBatch 1: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 0.00, 0.32, 2.37), time = 29.48.", + "\nBatch 2: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.64, 27.56, 34.87), time = 19.98.", + "\nBatch 3: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.64, 39.05, 48.44), time = 19.73.", + "\nBatch 4: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.77, 47.75, 48.44), time = 17.50.", + "\nBatch 5: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 31.77, 52.17, 48.44), time = 17.87.", + "\nBatch 6: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 39.70, 53.12, 50.71), time = 13.42.", + "\nBatch 7: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 45.20, 53.12, 53.03), time = 17.19.", + "\nBatch 8: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 45.20, 53.93, 55.20), time = 18.76.", + "\nBatch 9: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 47.48, 54.05, 55.48), time = 18.70.", + "\nBatch 10: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 54.26, 55.61), time = 16.88.", + "\nBatch 11: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 54.39, 56.15), time = 17.31.", + "\nBatch 12: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 54.56, 56.63), time = 17.12.", + "\nBatch 13: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 50.86, 55.72, 57.04), time = 19.59.", + "\nBatch 14: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 51.15, 55.80, 57.12), time = 16.62.", + "\nBatch 15: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 51.15, 55.86, 57.12), time = 23.50.", + "\nBatch 16: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 51.86, 55.91, 57.12), time = 17.27.", + "\nBatch 17: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 52.02, 55.96, 57.42), time = 20.15.", + "\nBatch 18: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.00, 53.68, 55.98, 57.50), time = 22.07.", + "\nBatch 19: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.64, 53.95, 56.02, 57.57), time = 20.06.", + "\nBatch 20: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = (0.64, 54.32, 56.03, 57.77), time = 26.89." + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "72232578-0908-4292-aca4-d52197890dd6", + "showInput": false + }, + "source": [ + "#### Plot the results\n", + "The plot below shows the a common metric of multi-objective optimization performance, the log hypervolume difference: the log difference between the hypervolume of the true pareto front and the hypervolume of the approximate pareto front identified by each algorithm. The log hypervolume difference is plotted at each step of the optimization for each of the algorithms.\n", + "\n", + "The plot shows that $q$NEHVI outperforms $q$EHVI, $q$ParEGO, and Sobol." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649857076, + "executionStopTime": 1668649858100, + "originalKey": "c9560130-5c74-4b2b-b24a-fd7618ee7822", + "requestMsgId": "d6978d53-90e4-476b-bb1d-75010f65dcd6", + "customOutput": null, + "collapsed": false + }, + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "iters = np.arange(N_BATCH + 1) * BATCH_SIZE\n", + "log_hv_difference_qparego = np.log10(problem.max_hv - np.asarray(hvs_qparego))\n", + "log_hv_difference_qehvi = np.log10(problem.max_hv - np.asarray(hvs_qehvi))\n", + "log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi))\n", + "log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random))\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n", + "ax.errorbar(\n", + " iters,\n", + " log_hv_difference_rnd,\n", + " label=\"Sobol\",\n", + " linewidth=1.5,\n", + ")\n", + "ax.errorbar(\n", + " iters,\n", + " log_hv_difference_qparego,\n", + " label=\"qNParEGO\",\n", + " linewidth=1.5,\n", + ")\n", + "ax.errorbar(\n", + " iters,\n", + " log_hv_difference_qehvi,\n", + " label=\"qEHVI\",\n", + " linewidth=1.5,\n", + ")\n", + "ax.errorbar(\n", + " iters,\n", + " log_hv_difference_qnehvi,\n", + " label=\"qNEHVI\",\n", + " linewidth=1.5,\n", + ")\n", + "ax.set(\n", + " xlabel=\"number of observations (beyond initial points)\",\n", + " ylabel=\"Log Hypervolume Difference\",\n", + ")\n", + "ax.legend(loc=\"lower left\")" + ], + "execution_count": 15, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "" + }, + "metadata": { + "bento_obj_id": "140541294158704" + }, + "execution_count": 15 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "bento_obj_id": "140541423428512", + "needs_background": "light" + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "originalKey": "824c329c-6c02-4f67-bce0-a4c57ea655d8", + "showInput": false + }, + "source": [ + "#### Plot the true objectives at the evaluated designs colored by iteration\n", + "\n", + "To examine optimization process from another perspective, we plot the true function values at the designs selected under each algorithm where the color corresponds to the BO iteration at which the point was collected. The plot on the right for $q$NEHVI shows that the $q$NEHVI quickly identifies the pareto front and most of its evaluations are very close to the pareto front. $q$NParEGO also identifies has many observations close to the pareto front, but relies on optimizing random scalarizations, which is a less principled way of optimizing the pareto front compared to $q$NEHVI, which explicitly attempts focuses on improving the pareto front. $q$EHVI uses the posterior mean as a plug-in estimator for the true function values at the in-sample points, whereas $q$NEHVI than integrating over the uncertainty at the in-sample designs Sobol generates random points and has few points close to the Pareto front." + ] + }, + { + "cell_type": "code", + "metadata": { + "executionStartTime": 1668649858641, + "executionStopTime": 1668649859522, + "originalKey": "cf610a77-3e82-4c30-ac7b-f7a97fa91fa5", + "requestMsgId": "afb07e9c-ebaf-4a8e-90e3-ca426532de20", + "customOutput": null, + "collapsed": false + }, + "source": [ + "from matplotlib.cm import ScalarMappable\n", + "\n", + "\n", + "fig, axes = plt.subplots(1, 4, figsize=(23, 7), sharex=True, sharey=True)\n", + "algos = [\"Sobol\", \"qNParEGO\", \"qEHVI\", \"qNEHVI\"]\n", + "cm = plt.get_cmap(\"viridis\")\n", + "\n", + "batch_number = torch.cat(\n", + " [\n", + " torch.zeros(2 * (problem.dim + 1)),\n", + " torch.arange(1, N_BATCH + 1).repeat(BATCH_SIZE, 1).t().reshape(-1),\n", + " ]\n", + ").numpy()\n", + "for i, train_obj in enumerate(\n", + " (\n", + " train_obj_true_random,\n", + " train_obj_true_qparego,\n", + " train_obj_true_qehvi,\n", + " train_obj_true_qnehvi,\n", + " )\n", + "):\n", + " sc = axes[i].scatter(\n", + " train_obj[:, 0].cpu().numpy(),\n", + " train_obj[:, 1].cpu().numpy(),\n", + " c=batch_number,\n", + " alpha=0.8,\n", + " )\n", + " axes[i].set_title(algos[i])\n", + " axes[i].set_xlabel(\"Objective 1\")\n", + "axes[0].set_ylabel(\"Objective 2\")\n", + "norm = plt.Normalize(batch_number.min(), batch_number.max())\n", + "sm = ScalarMappable(norm=norm, cmap=cm)\n", + "sm.set_array([])\n", + "fig.subplots_adjust(right=0.9)\n", + "cbar_ax = fig.add_axes([0.93, 0.15, 0.01, 0.7])\n", + "cbar = fig.colorbar(sm, cax=cbar_ax)\n", + "cbar.ax.set_title(\"Iteration\")" + ], + "execution_count": 16, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "Text(0.5, 1.0, 'Iteration')" + }, + "metadata": { + "bento_obj_id": "140541356926672" + }, + "execution_count": 16 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "bento_obj_id": "140541327761568", + "needs_background": "light" + } + } + ] + } + ] +} diff --git a/website-old/static/files/multi_objective_bo.py b/website-old/static/files/multi_objective_bo.py new file mode 100644 index 0000000000..a004478bcd --- /dev/null +++ b/website-old/static/files/multi_objective_bo.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Noisy, Parallel, Multi-Objective BO in BoTorch with qEHVI, qNEHVI, and qNParEGO +# +# In this tutorial, we illustrate how to implement a simple multi-objective (MO) Bayesian Optimization (BO) closed loop in BoTorch. +# +# In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. See [here](https://ax.dev/tutorials/multiobjective_optimization.html) for an Ax tutorial on MOBO. If desired, you can use a custom BoTorch model in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. Given a `MultiObjective`, Ax will default to the $q$NEHVI acquisiton function. If desired, this can also be customized by adding `"botorch_acqf_class": ,` to the `model_kwargs`. +# +# We use the parallel ParEGO ($q$ParEGO) [1], parallel Expected Hypervolume Improvement ($q$EHVI) [1], and parallel Noisy Expected Hypervolume Improvement ($q$NEHVI) [2] acquisition functions to optimize a synthetic BraninCurrin problem test function with additive Gaussian observation noise over a 2-parameter search space [0,1]^2. See `botorch/test_functions/multi_objective.py` for details on BraninCurrin. The noise standard deviations are 15.19 and 0.63 for each objective, respectively. +# +# Since botorch assumes a maximization of all objectives, we seek to find the Pareto frontier, the set of optimal trade-offs where improving one metric means deteriorating another. +# +# [1] [S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2006.05078) +# +# [2] [S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances in Neural Information Processing Systems 34, 2021.](https://arxiv.org/abs/2105.08195) +# +# **For batch optimization (or in noisy settings), we strongly recommend using $q$NEHVI rather than $q$EHVI because it is far more efficient than $q$EHVI and mathematically equivalent in the noiseless setting.** + +# ### Set dtype and device +# Note: $q$EHVI and $q$NEHVI aggressively exploit parallel hardware and are both much faster when run on a GPU. See [1, 2] for details. + +# In[8]: + + +import os +import torch + + +tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), +} +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ### Problem setup +# + +# In[9]: + + +from botorch.test_functions.multi_objective import BraninCurrin + + +problem = BraninCurrin(negate=True).to(**tkwargs) + + +# #### Model initialization +# +# We use a list of `SingleTaskGP`s to model the two objectives with known noise variances. If no noise variances were provided, `SingleTaskGP` would infer (homoskedastic) noise levels instead. +# +# The models are initialized with $2(d+1)=6$ points drawn randomly from $[0,1]^2$. + +# In[10]: + + +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize +from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood +from botorch.utils.transforms import unnormalize, normalize +from botorch.utils.sampling import draw_sobol_samples + +NOISE_SE = torch.tensor([15.19, 0.63], **tkwargs) + + +def generate_initial_data(n=6): + # generate training data + train_x = draw_sobol_samples(bounds=problem.bounds, n=n, q=1).squeeze(1) + train_obj_true = problem(train_x) + train_obj = train_obj_true + torch.randn_like(train_obj_true) * NOISE_SE + return train_x, train_obj, train_obj_true + + +def initialize_model(train_x, train_obj): + # define models for objective and constraint + train_x = normalize(train_x, problem.bounds) + models = [] + for i in range(train_obj.shape[-1]): + train_y = train_obj[..., i : i + 1] + train_yvar = torch.full_like(train_y, NOISE_SE[i] ** 2) + models.append( + SingleTaskGP( + train_x, train_y, train_yvar, outcome_transform=Standardize(m=1) + ) + ) + model = ModelListGP(*models) + mll = SumMarginalLogLikelihood(model.likelihood, model) + return mll, model + + +# #### Define a helper functions that performs the essential BO step for $q$EHVI and $q$NEHVI +# The helper function below initializes the $q$EHVI acquisition function, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. +# +# For this example, we'll use a relatively small batch of optimization ($q=4$). For batch optimization ($q>1$), passing the keyword argument `sequential=True` to the function `optimize_acqf`specifies that candidates should be optimized in a sequential greedy fashion (see [1] for details why this is important). A simple initialization heuristic is used to select the 10 restart initial locations from a set of 512 random points. Multi-start optimization of the acquisition function is performed using LBFGS-B with exact gradients computed via auto-differentiation. +# +# **Reference Point** +# +# $q$EHVI requires specifying a reference point, which is the lower bound on the objectives used for computing hypervolume. In this tutorial, we assume the reference point is known. In practice the reference point can be set 1) using domain knowledge to be slightly worse than the lower bound of objective values, where the lower bound is the minimum acceptable value of interest for each objective, or 2) using a dynamic reference point selection strategy. +# +# **Partitioning the Non-dominated Space into disjoint rectangles** +# +# $q$EHVI requires partitioning the non-dominated space into disjoint rectangles (see [1] for details). +# +# *Note:* `FastNondominatedPartitioning` *will be very slow when 1) there are a lot of points on the pareto frontier and 2) there are >5 objectives.* + +# In[11]: + + +from botorch.optim.optimize import optimize_acqf, optimize_acqf_list +from botorch.acquisition.objective import GenericMCObjective +from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization +from botorch.utils.multi_objective.box_decompositions.non_dominated import ( + FastNondominatedPartitioning, +) +from botorch.acquisition.multi_objective.monte_carlo import ( + qExpectedHypervolumeImprovement, + qNoisyExpectedHypervolumeImprovement, +) +from botorch.utils.sampling import sample_simplex + + +BATCH_SIZE = 4 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 + +standard_bounds = torch.zeros(2, problem.dim, **tkwargs) +standard_bounds[1] = 1 + + +def optimize_qehvi_and_get_observation(model, train_x, train_obj, sampler): + """Optimizes the qEHVI acquisition function, and returns a new candidate and observation.""" + # partition non-dominated space into disjoint rectangles + with torch.no_grad(): + pred = model.posterior(normalize(train_x, problem.bounds)).mean + partitioning = FastNondominatedPartitioning( + ref_point=problem.ref_point, + Y=pred, + ) + acq_func = qExpectedHypervolumeImprovement( + model=model, + ref_point=problem.ref_point, + partitioning=partitioning, + sampler=sampler, + ) + # optimize + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=standard_bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + sequential=True, + ) + # observe new values + new_x = unnormalize(candidates.detach(), bounds=problem.bounds) + new_obj_true = problem(new_x) + new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE + return new_x, new_obj, new_obj_true + + +# **Integrating over function values at in-sample designs** +# +# $q$NEHVI integrates over the unknown function values at the previously evaluated designs (see [2] for details). Therefore, we need to provide the previously evaluated designs (`train_x`, *normalized* to be within $[0,1]^d$) to the acquisition function. +# +# **Efficient batch generation with Cached Box Decomposition (CBD)** +# +# $q$NEHVI leveraged CBD to efficiently generate large batches of candidates. CBD scales polynomially with respect to the batch size where as the inclusion-exclusion principle used by qEHVI scales exponentially with the batch size. +# +# **Pruning baseline designs** +# To speed up integration over the function values at the previously evaluated designs, we prune the set of previously evaluated designs (by setting `prune_baseline=True`) to only include those which have positive probability of being on the current in-sample Pareto frontier. + +# In[12]: + + +def optimize_qnehvi_and_get_observation(model, train_x, train_obj, sampler): + """Optimizes the qEHVI acquisition function, and returns a new candidate and observation.""" + # partition non-dominated space into disjoint rectangles + acq_func = qNoisyExpectedHypervolumeImprovement( + model=model, + ref_point=problem.ref_point.tolist(), # use known reference point + X_baseline=normalize(train_x, problem.bounds), + prune_baseline=True, # prune baseline points that have estimated zero probability of being Pareto optimal + sampler=sampler, + ) + # optimize + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=standard_bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + sequential=True, + ) + # observe new values + new_x = unnormalize(candidates.detach(), bounds=problem.bounds) + new_obj_true = problem(new_x) + new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE + return new_x, new_obj, new_obj_true + + +# #### Define a helper function that performs the essential BO step for $q$NParEGO +# The helper function below similarly initializes $q$NParEGO, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. +# +# $q$NParEGO uses random augmented chebyshev scalarization with the `qNoisyExpectedImprovement` acquisition function. In the parallel setting ($q>1$), each candidate is optimized in sequential greedy fashion using a different random scalarization (see [1] for details). +# +# To do this, we create a list of `qNoisyExpectedImprovement` acquisition functions, each with different random scalarization weights. The `optimize_acqf_list` method sequentially generates one candidate per acquisition function and conditions the next candidate (and acquisition function) on the previously selected pending candidates. + +# In[13]: + + +from botorch.acquisition.monte_carlo import qNoisyExpectedImprovement + + +def optimize_qnparego_and_get_observation(model, train_x, train_obj, sampler): + """Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization + of the qNParEGO acquisition function, and returns a new candidate and observation.""" + train_x = normalize(train_x, problem.bounds) + with torch.no_grad(): + pred = model.posterior(train_x).mean + acq_func_list = [] + for _ in range(BATCH_SIZE): + weights = sample_simplex(problem.num_objectives, **tkwargs).squeeze() + objective = GenericMCObjective( + get_chebyshev_scalarization(weights=weights, Y=pred) + ) + acq_func = qNoisyExpectedImprovement( # pyre-ignore: [28] + model=model, + objective=objective, + X_baseline=train_x, + sampler=sampler, + prune_baseline=True, + ) + acq_func_list.append(acq_func) + # optimize + candidates, _ = optimize_acqf_list( + acq_function_list=acq_func_list, + bounds=standard_bounds, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, # used for intialization heuristic + options={"batch_limit": 5, "maxiter": 200}, + ) + # observe new values + new_x = unnormalize(candidates.detach(), bounds=problem.bounds) + new_obj_true = problem(new_x) + new_obj = new_obj_true + torch.randn_like(new_obj_true) * NOISE_SE + return new_x, new_obj, new_obj_true + + +# ### Perform Bayesian Optimization loop with $q$NEHVI, $q$EHVI, and $q$NParEGO +# The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps: +# 1. given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$ +# 2. observe $f(x)$ for each $x$ in the batch +# 3. update the surrogate model. +# +# +# Just for illustration purposes, we run one trial with `N_BATCH=20` rounds of optimization. The acquisition function is approximated using `MC_SAMPLES=128` samples. +# +# *Note*: Running this may take a little while. + +# In[14]: + + +import time +import warnings + +from botorch import fit_gpytorch_mll +from botorch.exceptions import BadInitialCandidatesWarning +from botorch.sampling.normal import SobolQMCNormalSampler +from botorch.utils.multi_objective.box_decompositions.dominated import ( + DominatedPartitioning, +) +from botorch.utils.multi_objective.pareto import is_non_dominated + + +warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning) +warnings.filterwarnings("ignore", category=RuntimeWarning) + +N_BATCH = 20 if not SMOKE_TEST else 5 +MC_SAMPLES = 128 if not SMOKE_TEST else 16 + +verbose = True + +hvs_qparego, hvs_qehvi, hvs_qnehvi, hvs_random = [], [], [], [] + +# call helper functions to generate initial training data and initialize model +train_x_qparego, train_obj_qparego, train_obj_true_qparego = generate_initial_data( + n=2 * (problem.dim + 1) +) +mll_qparego, model_qparego = initialize_model(train_x_qparego, train_obj_qparego) + +train_x_qehvi, train_obj_qehvi, train_obj_true_qehvi = ( + train_x_qparego, + train_obj_qparego, + train_obj_true_qparego, +) +train_x_qnehvi, train_obj_qnehvi, train_obj_true_qnehvi = ( + train_x_qparego, + train_obj_qparego, + train_obj_true_qparego, +) +train_x_random, train_obj_random, train_obj_true_random = ( + train_x_qparego, + train_obj_qparego, + train_obj_true_qparego, +) +mll_qehvi, model_qehvi = initialize_model(train_x_qehvi, train_obj_qehvi) +mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi, train_obj_qnehvi) + +# compute hypervolume +bd = DominatedPartitioning(ref_point=problem.ref_point, Y=train_obj_true_qparego) +volume = bd.compute_hypervolume().item() + +hvs_qparego.append(volume) +hvs_qehvi.append(volume) +hvs_qnehvi.append(volume) +hvs_random.append(volume) + +# run N_BATCH rounds of BayesOpt after the initial random batch +for iteration in range(1, N_BATCH + 1): + + t0 = time.monotonic() + + # fit the models + fit_gpytorch_mll(mll_qparego) + fit_gpytorch_mll(mll_qehvi) + fit_gpytorch_mll(mll_qnehvi) + + # define the qEI and qNEI acquisition modules using a QMC sampler + qparego_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + qehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + qnehvi_sampler = SobolQMCNormalSampler(sample_shape=torch.Size([MC_SAMPLES])) + + # optimize acquisition functions and get new observations + ( + new_x_qparego, + new_obj_qparego, + new_obj_true_qparego, + ) = optimize_qnparego_and_get_observation( + model_qparego, train_x_qparego, train_obj_qparego, qparego_sampler + ) + new_x_qehvi, new_obj_qehvi, new_obj_true_qehvi = optimize_qehvi_and_get_observation( + model_qehvi, train_x_qehvi, train_obj_qehvi, qehvi_sampler + ) + ( + new_x_qnehvi, + new_obj_qnehvi, + new_obj_true_qnehvi, + ) = optimize_qnehvi_and_get_observation( + model_qnehvi, train_x_qnehvi, train_obj_qnehvi, qnehvi_sampler + ) + new_x_random, new_obj_random, new_obj_true_random = generate_initial_data( + n=BATCH_SIZE + ) + + # update training points + train_x_qparego = torch.cat([train_x_qparego, new_x_qparego]) + train_obj_qparego = torch.cat([train_obj_qparego, new_obj_qparego]) + train_obj_true_qparego = torch.cat([train_obj_true_qparego, new_obj_true_qparego]) + + train_x_qehvi = torch.cat([train_x_qehvi, new_x_qehvi]) + train_obj_qehvi = torch.cat([train_obj_qehvi, new_obj_qehvi]) + train_obj_true_qehvi = torch.cat([train_obj_true_qehvi, new_obj_true_qehvi]) + + train_x_qnehvi = torch.cat([train_x_qnehvi, new_x_qnehvi]) + train_obj_qnehvi = torch.cat([train_obj_qnehvi, new_obj_qnehvi]) + train_obj_true_qnehvi = torch.cat([train_obj_true_qnehvi, new_obj_true_qnehvi]) + + train_x_random = torch.cat([train_x_random, new_x_random]) + train_obj_random = torch.cat([train_obj_random, new_obj_random]) + train_obj_true_random = torch.cat([train_obj_true_random, new_obj_true_random]) + + # update progress + for hvs_list, train_obj in zip( + (hvs_random, hvs_qparego, hvs_qehvi, hvs_qnehvi), + ( + train_obj_true_random, + train_obj_true_qparego, + train_obj_true_qehvi, + train_obj_true_qnehvi, + ), + ): + # compute hypervolume + bd = DominatedPartitioning(ref_point=problem.ref_point, Y=train_obj) + volume = bd.compute_hypervolume().item() + hvs_list.append(volume) + + # reinitialize the models so they are ready for fitting on next iteration + # Note: we find improved performance from not warm starting the model hyperparameters + # using the hyperparameters from the previous iteration + mll_qparego, model_qparego = initialize_model(train_x_qparego, train_obj_qparego) + mll_qehvi, model_qehvi = initialize_model(train_x_qehvi, train_obj_qehvi) + mll_qnehvi, model_qnehvi = initialize_model(train_x_qnehvi, train_obj_qnehvi) + + t1 = time.monotonic() + + if verbose: + print( + f"\nBatch {iteration:>2}: Hypervolume (random, qNParEGO, qEHVI, qNEHVI) = " + f"({hvs_random[-1]:>4.2f}, {hvs_qparego[-1]:>4.2f}, {hvs_qehvi[-1]:>4.2f}, {hvs_qnehvi[-1]:>4.2f}), " + f"time = {t1-t0:>4.2f}.", + end="", + ) + else: + print(".", end="") + + +# #### Plot the results +# The plot below shows the a common metric of multi-objective optimization performance, the log hypervolume difference: the log difference between the hypervolume of the true pareto front and the hypervolume of the approximate pareto front identified by each algorithm. The log hypervolume difference is plotted at each step of the optimization for each of the algorithms. +# +# The plot shows that $q$NEHVI outperforms $q$EHVI, $q$ParEGO, and Sobol. + +# In[15]: + + +import numpy as np +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +iters = np.arange(N_BATCH + 1) * BATCH_SIZE +log_hv_difference_qparego = np.log10(problem.max_hv - np.asarray(hvs_qparego)) +log_hv_difference_qehvi = np.log10(problem.max_hv - np.asarray(hvs_qehvi)) +log_hv_difference_qnehvi = np.log10(problem.max_hv - np.asarray(hvs_qnehvi)) +log_hv_difference_rnd = np.log10(problem.max_hv - np.asarray(hvs_random)) + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +ax.errorbar( + iters, + log_hv_difference_rnd, + label="Sobol", + linewidth=1.5, +) +ax.errorbar( + iters, + log_hv_difference_qparego, + label="qNParEGO", + linewidth=1.5, +) +ax.errorbar( + iters, + log_hv_difference_qehvi, + label="qEHVI", + linewidth=1.5, +) +ax.errorbar( + iters, + log_hv_difference_qnehvi, + label="qNEHVI", + linewidth=1.5, +) +ax.set( + xlabel="number of observations (beyond initial points)", + ylabel="Log Hypervolume Difference", +) +ax.legend(loc="lower left") + + +# #### Plot the true objectives at the evaluated designs colored by iteration +# +# To examine optimization process from another perspective, we plot the true function values at the designs selected under each algorithm where the color corresponds to the BO iteration at which the point was collected. The plot on the right for $q$NEHVI shows that the $q$NEHVI quickly identifies the pareto front and most of its evaluations are very close to the pareto front. $q$NParEGO also identifies has many observations close to the pareto front, but relies on optimizing random scalarizations, which is a less principled way of optimizing the pareto front compared to $q$NEHVI, which explicitly attempts focuses on improving the pareto front. $q$EHVI uses the posterior mean as a plug-in estimator for the true function values at the in-sample points, whereas $q$NEHVI than integrating over the uncertainty at the in-sample designs Sobol generates random points and has few points close to the Pareto front. + +# In[16]: + + +from matplotlib.cm import ScalarMappable + + +fig, axes = plt.subplots(1, 4, figsize=(23, 7), sharex=True, sharey=True) +algos = ["Sobol", "qNParEGO", "qEHVI", "qNEHVI"] +cm = plt.get_cmap("viridis") + +batch_number = torch.cat( + [ + torch.zeros(2 * (problem.dim + 1)), + torch.arange(1, N_BATCH + 1).repeat(BATCH_SIZE, 1).t().reshape(-1), + ] +).numpy() +for i, train_obj in enumerate( + ( + train_obj_true_random, + train_obj_true_qparego, + train_obj_true_qehvi, + train_obj_true_qnehvi, + ) +): + sc = axes[i].scatter( + train_obj[:, 0].cpu().numpy(), + train_obj[:, 1].cpu().numpy(), + c=batch_number, + alpha=0.8, + ) + axes[i].set_title(algos[i]) + axes[i].set_xlabel("Objective 1") +axes[0].set_ylabel("Objective 2") +norm = plt.Normalize(batch_number.min(), batch_number.max()) +sm = ScalarMappable(norm=norm, cmap=cm) +sm.set_array([]) +fig.subplots_adjust(right=0.9) +cbar_ax = fig.add_axes([0.93, 0.15, 0.01, 0.7]) +cbar = fig.colorbar(sm, cax=cbar_ax) +cbar.ax.set_title("Iteration") + diff --git a/website-old/static/files/one_shot_kg.ipynb b/website-old/static/files/one_shot_kg.ipynb new file mode 100644 index 0000000000..d2d511611b --- /dev/null +++ b/website-old/static/files/one_shot_kg.ipynb @@ -0,0 +1,317 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The one-shot Knowledge Gradient acquisition function\n", + "\n", + "The *Knowledge Gradient* (KG) (see [2, 3]) is a look-ahead acquisition function that quantifies the expected increase in the maximum of the modeled black-box function $f$ from obtaining additional (random) observations collected at the candidate set $\\mathbf{x}$. KG often shows improved Bayesian Optimization performance relative to simpler acquisition functions such as Expected Improvement, but in its traditional form it is computationally expensive and hard to implement.\n", + "\n", + "BoTorch implements a generalized variant of parallel KG [3] given by\n", + "$$ \\alpha_{\\text{KG}}(\\mathbf{x}) =\n", + " \\mathbb{E}_{\\mathcal{D}_{\\mathbf{x}}}\n", + " \\Bigl[\\, \\max_{x' \\in \\mathbb{X}} \\mathbb{E} \\left[ g(\\xi)\\right] \\Bigr] - \\mu,\n", + "$$\n", + "where $\\xi \\sim \\mathcal{P}(f(x') \\mid \\mathcal{D} \\cup \\mathcal{D}_{\\mathbf{x}})$ is the posterior at $x'$ conditioned on $\\mathcal{D}_{\\mathbf{x}}$, the (random) dataset observed at $\\mathbf{x}$, and $\\mu := \\max_{x}\\mathbb{E}[g(f(x)) \\mid \\mathcal{D}]$.\n", + "\n", + "In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. You can use a custom BoTorch model and acquisition function in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. To use the KG acquisition function, it is sufficient to add `\"botorch_acqf_class\": qKnowledgeGradient,` to `model_kwargs`. The linked tutorial shows how to use a custom BoTorch model. If you'd like to let Ax choose which model to use based on the properties of the search space, you can skip the `surrogate` argument in `model_kwargs`.\n", + "\n", + "\n", + "#### Optimizing KG\n", + "\n", + "The conventional approach for optimizing parallel KG (where $g(\\xi) = \\xi$) is to apply stochastic gradient ascent, with each gradient observation potentially being an average over multiple samples. For each sample $i$, the inner optimization problem $\\max_{x_i \\in \\mathbb{X}} \\mathbb{E} \\left[ \\xi^i \\mid \\mathcal{D}_{\\mathbf{x}}^i \\right]$ for the posterior mean is solved numerically. An unbiased stochastic gradient of KG can then be computed by leveraging the envelope theorem and the optimal points $\\{x_i^*\\}$. In this approach, every iteration requires solving numerous inner optimization problems, one for each outer sample, in order to estimate just one stochastic gradient.\n", + "\n", + "The \"one-shot\" formulation of KG in BoTorch treats optimizing $\\alpha_{\\text{KG}}(\\mathbf{x})$ as an entirely deterministic optimization problem. It involves drawing $N_{\\!f} = $ `num_fantasies` fixed base samples $\\mathbf{Z}_f:= \\{ \\mathbf{Z}^i_f \\}_{1\\leq i \\leq N_{\\!f}}$ for the outer expectation, sampling fantasy data $\\{\\mathcal{D}_{\\mathbf{x}}^i(\\mathbf{Z}_f^i)\\}_{1\\leq i \\leq N_{\\!f}}$, and constructing associated fantasy models $\\{\\mathcal{M}^i(\\mathbf{Z}_f^i)\\}_{1 \\leq i \\leq N_{\\!f}}$. The inner maximization can then be moved outside of the sample average, resulting in the following optimization problem:\n", + "$$\n", + "\\max_{\\mathbf{x} \\in \\mathbb{X}}\\alpha_{\\text{KG}}(\\mathbf{x}) \\approx \\max_{\\mathbf{x}\\in \\mathbb{X}, \\mathbf{X}' \\in \\mathbb{X}^{N_{\\!f}} } %=1}^{\\!N_{\\!f}}}\n", + "\\sum_{i=1}^{N_{\\!f}} \\mathbb{E}\\left[g(\\xi^i)\\right],\n", + "$$\n", + "where $\\xi^i \\sim \\mathcal{P}(f(x'^i) \\mid \\mathcal{D} \\cup \\mathcal{D}_{\\mathbf{x}}^i(\\mathbf{Z}_f^i))$ and $\\mathbf{X}' := \\{x'^i\\}_{1 \\leq i \\leq N_{\\!f}}$.\n", + "\n", + "If the inner expectation does not have an analytic expression, one can also draw fixed base samples $\\mathbf{Z}_I:= \\{ \\mathbf{Z}^i_I \\}_{1\\leq i\\leq N_{\\!I}}$ and use an MC approximation as with the standard MC acquisition functions of type `MCAcquisitionFunction`. In either case one is left with a deterministic optimization problem. \n", + "\n", + "The key difference from the envelope theorem approach is that we do not solve the inner optimization problem to completion for every fantasy point for every gradient step with respect to $\\mathbf{x}$. Instead, we solve the nested optimization problem jointly over $\\mathbf{x}$ and the fantasy points $\\mathbf{X}'$. The resulting optimization problem is of higher dimension, namely $(q + N_{\\!f})d$ instead of $qd$, but unlike the envelope theorem formulation it can be solved as a single optimization problem, which can be solved using standard methods for deterministic optimization. \n", + "\n", + "\n", + "[1] M. Balandat, B. Karrer, D. R. Jiang, S. Daulton, B. Letham, A. G. Wilson, and E. Bakshy. BoTorch: A Framework for Efficient Monte-Carlo Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020.\n", + "\n", + "[2] P. Frazier, W. Powell, and S. Dayanik. A Knowledge-Gradient policy for sequential information collection. SIAM Journal on Control and Optimization, 2008.\n", + "\n", + "[3] J. Wu and P. Frazier. The parallel knowledge gradient method for batch bayesian optimization. NIPS 2016." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setting up a toy model\n", + "\n", + "We'll fit a standard `SingleTaskGP` model on noisy observations of the synthetic function $f(x) = \\sin(2 \\pi x_1) * \\cos(2 \\pi x_2)$ in `d=2` dimensions on the hypercube $[0, 1]^2$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import math\n", + "import torch\n", + "\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.utils import standardize\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "bounds = torch.stack([torch.zeros(2), torch.ones(2)])\n", + "\n", + "train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(20, 2)\n", + "train_Y = torch.sin(2 * math.pi * train_X[:, [0]]) * torch.cos(\n", + " 2 * math.pi * train_X[:, [1]]\n", + ")\n", + "\n", + "train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y))\n", + "\n", + "model = SingleTaskGP(train_X, train_Y)\n", + "mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "fit_gpytorch_mll(mll);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Defining the qKnowledgeGradient acquisition function\n", + "\n", + "The `qKnowledgeGradient` complies with the standard `MCAcquisitionFunction` API. The only mandatory argument in addition to the model is `num_fantasies` the number of fantasy samples. More samples result in a better approximation of KG, at the expense of both memory and wall time. \n", + "\n", + "`qKnowledgeGradient` also supports the other parameters of `MCAcquisitionFunction`, such as a generic objective `objective` and pending points `X_pending`. It also accepts a `current_value` argument that is the maximum posterior mean of the current model (which can be obtained by maximizing `PosteriorMean` acquisition function). This does not change the optimizer so it is not required, but it means that the acquisition value is some constant shift of the actual \"Knowledge Gradient\" value. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.acquisition import qKnowledgeGradient\n", + "\n", + "\n", + "NUM_FANTASIES = 128 if not SMOKE_TEST else 4\n", + "qKG = qKnowledgeGradient(model, num_fantasies=NUM_FANTASIES)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Optimizing qKG\n", + "\n", + "`qKnowledgeGradient` subclasses `OneShotAcquisitionFunction`, which makes sure that the fantasy parameterization $\\mathbf{X}'$ is automatically generated and optimized when calling `optimize_acqf` on the acquisition function. This means that optimizing one-shot KG in BoTorch is just a easy as optimizing any other acquisition function (from an API perspective, at least). It turns out that a careful initialization of the fantasy points can significantly help with the optimization (see the logic in `botorch.optim.initializers.gen_one_shot_kg_initial_conditions` for more information).\n", + "\n", + "\n", + "Here we use `num_restarts=10` random initial `q`-batches with `q=2` in parallel, with the intialization heuristic starting from `raw_samples = 512` raw points (note that since `qKnowledgeGradient` is significantly more expensive to evaluate than other acquisition functions, large values of `num_restarts` and `raw_samples`, which are typically feasible in other settings, can result in long wall times and potential memory issues). \n", + "\n", + "Finally, since we do not pass a `current_value` argument, this value is not actually the KG value, but offset by the constant (w.r.t. the candidates) $\\mu := \\max_{x}\\mathbb{E}[g(f(x)) \\mid \\mathcal{D}]$." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.optim import optimize_acqf\n", + "from botorch.utils.sampling import manual_seed\n", + "\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "\n", + "\n", + "with manual_seed(1234):\n", + " candidates, acq_value = optimize_acqf(\n", + " acq_function=qKG,\n", + " bounds=bounds,\n", + " q=2,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([[0.1488, 1.0000],\n", + " [0.1084, 0.0012]])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "candidates" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(2.4176)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "acq_value" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Computing the actual KG value\n", + "\n", + "We first need to find the maximum posterior mean - we can use a large number of random restarts and raw_samples to increase the likelihood that we do indeed find it (this is a non-convex optimization problem, after all). " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.acquisition import PosteriorMean\n", + "\n", + "NUM_RESTARTS = 20 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 2048 if not SMOKE_TEST else 4\n", + "\n", + "\n", + "argmax_pmean, max_pmean = optimize_acqf(\n", + " acq_function=PosteriorMean(model),\n", + " bounds=bounds,\n", + " q=1,\n", + " num_restarts=20 if not SMOKE_TEST else 2,\n", + " raw_samples=2048 if not SMOKE_TEST else 4,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can optimize KG after passing the current value. We also pass in the `sampler` from the original `qKG` above, which containst the fixed base samples $\\mathbf{Z}_f$. This is to ensure that we optimize the same approximation and so our values are an apples-to-apples comparison (as `num_fantasies` increases, the effect of this randomness will get less and less important)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "qKG_proper = qKnowledgeGradient(\n", + " model,\n", + " num_fantasies=NUM_FANTASIES,\n", + " sampler=qKG.sampler,\n", + " current_value=max_pmean,\n", + ")\n", + "\n", + "with manual_seed(1234):\n", + " candidates_proper, acq_value_proper = optimize_acqf(\n", + " acq_function=qKG_proper,\n", + " bounds=bounds,\n", + " q=2,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([[0.0000, 0.1795],\n", + " [0.1480, 0.0015]])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "candidates_proper" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(0.1131)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "acq_value_proper" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/one_shot_kg.py b/website-old/static/files/one_shot_kg.py new file mode 100644 index 0000000000..203303e254 --- /dev/null +++ b/website-old/static/files/one_shot_kg.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## The one-shot Knowledge Gradient acquisition function +# +# The *Knowledge Gradient* (KG) (see [2, 3]) is a look-ahead acquisition function that quantifies the expected increase in the maximum of the modeled black-box function $f$ from obtaining additional (random) observations collected at the candidate set $\mathbf{x}$. KG often shows improved Bayesian Optimization performance relative to simpler acquisition functions such as Expected Improvement, but in its traditional form it is computationally expensive and hard to implement. +# +# BoTorch implements a generalized variant of parallel KG [3] given by +# $$ \alpha_{\text{KG}}(\mathbf{x}) = +# \mathbb{E}_{\mathcal{D}_{\mathbf{x}}} +# \Bigl[\, \max_{x' \in \mathbb{X}} \mathbb{E} \left[ g(\xi)\right] \Bigr] - \mu, +# $$ +# where $\xi \sim \mathcal{P}(f(x') \mid \mathcal{D} \cup \mathcal{D}_{\mathbf{x}})$ is the posterior at $x'$ conditioned on $\mathcal{D}_{\mathbf{x}}$, the (random) dataset observed at $\mathbf{x}$, and $\mu := \max_{x}\mathbb{E}[g(f(x)) \mid \mathcal{D}]$. +# +# In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one, since this will simplify your setup (including the amount of code you need to write) considerably. You can use a custom BoTorch model and acquisition function in Ax, following the [Using BoTorch with Ax](./custom_botorch_model_in_ax) tutorial. To use the KG acquisition function, it is sufficient to add `"botorch_acqf_class": qKnowledgeGradient,` to `model_kwargs`. The linked tutorial shows how to use a custom BoTorch model. If you'd like to let Ax choose which model to use based on the properties of the search space, you can skip the `surrogate` argument in `model_kwargs`. +# +# +# #### Optimizing KG +# +# The conventional approach for optimizing parallel KG (where $g(\xi) = \xi$) is to apply stochastic gradient ascent, with each gradient observation potentially being an average over multiple samples. For each sample $i$, the inner optimization problem $\max_{x_i \in \mathbb{X}} \mathbb{E} \left[ \xi^i \mid \mathcal{D}_{\mathbf{x}}^i \right]$ for the posterior mean is solved numerically. An unbiased stochastic gradient of KG can then be computed by leveraging the envelope theorem and the optimal points $\{x_i^*\}$. In this approach, every iteration requires solving numerous inner optimization problems, one for each outer sample, in order to estimate just one stochastic gradient. +# +# The "one-shot" formulation of KG in BoTorch treats optimizing $\alpha_{\text{KG}}(\mathbf{x})$ as an entirely deterministic optimization problem. It involves drawing $N_{\!f} = $ `num_fantasies` fixed base samples $\mathbf{Z}_f:= \{ \mathbf{Z}^i_f \}_{1\leq i \leq N_{\!f}}$ for the outer expectation, sampling fantasy data $\{\mathcal{D}_{\mathbf{x}}^i(\mathbf{Z}_f^i)\}_{1\leq i \leq N_{\!f}}$, and constructing associated fantasy models $\{\mathcal{M}^i(\mathbf{Z}_f^i)\}_{1 \leq i \leq N_{\!f}}$. The inner maximization can then be moved outside of the sample average, resulting in the following optimization problem: +# $$ +# \max_{\mathbf{x} \in \mathbb{X}}\alpha_{\text{KG}}(\mathbf{x}) \approx \max_{\mathbf{x}\in \mathbb{X}, \mathbf{X}' \in \mathbb{X}^{N_{\!f}} } %=1}^{\!N_{\!f}}} +# \sum_{i=1}^{N_{\!f}} \mathbb{E}\left[g(\xi^i)\right], +# $$ +# where $\xi^i \sim \mathcal{P}(f(x'^i) \mid \mathcal{D} \cup \mathcal{D}_{\mathbf{x}}^i(\mathbf{Z}_f^i))$ and $\mathbf{X}' := \{x'^i\}_{1 \leq i \leq N_{\!f}}$. +# +# If the inner expectation does not have an analytic expression, one can also draw fixed base samples $\mathbf{Z}_I:= \{ \mathbf{Z}^i_I \}_{1\leq i\leq N_{\!I}}$ and use an MC approximation as with the standard MC acquisition functions of type `MCAcquisitionFunction`. In either case one is left with a deterministic optimization problem. +# +# The key difference from the envelope theorem approach is that we do not solve the inner optimization problem to completion for every fantasy point for every gradient step with respect to $\mathbf{x}$. Instead, we solve the nested optimization problem jointly over $\mathbf{x}$ and the fantasy points $\mathbf{X}'$. The resulting optimization problem is of higher dimension, namely $(q + N_{\!f})d$ instead of $qd$, but unlike the envelope theorem formulation it can be solved as a single optimization problem, which can be solved using standard methods for deterministic optimization. +# +# +# [1] M. Balandat, B. Karrer, D. R. Jiang, S. Daulton, B. Letham, A. G. Wilson, and E. Bakshy. BoTorch: A Framework for Efficient Monte-Carlo Bayesian Optimization. Advances in Neural Information Processing Systems 33, 2020. +# +# [2] P. Frazier, W. Powell, and S. Dayanik. A Knowledge-Gradient policy for sequential information collection. SIAM Journal on Control and Optimization, 2008. +# +# [3] J. Wu and P. Frazier. The parallel knowledge gradient method for batch bayesian optimization. NIPS 2016. + +# ### Setting up a toy model +# +# We'll fit a standard `SingleTaskGP` model on noisy observations of the synthetic function $f(x) = \sin(2 \pi x_1) * \cos(2 \pi x_2)$ in `d=2` dimensions on the hypercube $[0, 1]^2$. + +# In[1]: + + +import os +import math +import torch + +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.utils import standardize +from gpytorch.mlls import ExactMarginalLogLikelihood + + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# In[2]: + + +bounds = torch.stack([torch.zeros(2), torch.ones(2)]) + +train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(20, 2) +train_Y = torch.sin(2 * math.pi * train_X[:, [0]]) * torch.cos( + 2 * math.pi * train_X[:, [1]] +) + +train_Y = standardize(train_Y + 0.05 * torch.randn_like(train_Y)) + +model = SingleTaskGP(train_X, train_Y) +mll = ExactMarginalLogLikelihood(model.likelihood, model) +fit_gpytorch_mll(mll); + + +# ### Defining the qKnowledgeGradient acquisition function +# +# The `qKnowledgeGradient` complies with the standard `MCAcquisitionFunction` API. The only mandatory argument in addition to the model is `num_fantasies` the number of fantasy samples. More samples result in a better approximation of KG, at the expense of both memory and wall time. +# +# `qKnowledgeGradient` also supports the other parameters of `MCAcquisitionFunction`, such as a generic objective `objective` and pending points `X_pending`. It also accepts a `current_value` argument that is the maximum posterior mean of the current model (which can be obtained by maximizing `PosteriorMean` acquisition function). This does not change the optimizer so it is not required, but it means that the acquisition value is some constant shift of the actual "Knowledge Gradient" value. + +# In[3]: + + +from botorch.acquisition import qKnowledgeGradient + + +NUM_FANTASIES = 128 if not SMOKE_TEST else 4 +qKG = qKnowledgeGradient(model, num_fantasies=NUM_FANTASIES) + + +# ### Optimizing qKG +# +# `qKnowledgeGradient` subclasses `OneShotAcquisitionFunction`, which makes sure that the fantasy parameterization $\mathbf{X}'$ is automatically generated and optimized when calling `optimize_acqf` on the acquisition function. This means that optimizing one-shot KG in BoTorch is just a easy as optimizing any other acquisition function (from an API perspective, at least). It turns out that a careful initialization of the fantasy points can significantly help with the optimization (see the logic in `botorch.optim.initializers.gen_one_shot_kg_initial_conditions` for more information). +# +# +# Here we use `num_restarts=10` random initial `q`-batches with `q=2` in parallel, with the intialization heuristic starting from `raw_samples = 512` raw points (note that since `qKnowledgeGradient` is significantly more expensive to evaluate than other acquisition functions, large values of `num_restarts` and `raw_samples`, which are typically feasible in other settings, can result in long wall times and potential memory issues). +# +# Finally, since we do not pass a `current_value` argument, this value is not actually the KG value, but offset by the constant (w.r.t. the candidates) $\mu := \max_{x}\mathbb{E}[g(f(x)) \mid \mathcal{D}]$. + +# In[4]: + + +from botorch.optim import optimize_acqf +from botorch.utils.sampling import manual_seed + +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 + + +with manual_seed(1234): + candidates, acq_value = optimize_acqf( + acq_function=qKG, + bounds=bounds, + q=2, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + + +# In[5]: + + +candidates + + +# In[6]: + + +acq_value + + +# ### Computing the actual KG value +# +# We first need to find the maximum posterior mean - we can use a large number of random restarts and raw_samples to increase the likelihood that we do indeed find it (this is a non-convex optimization problem, after all). + +# In[7]: + + +from botorch.acquisition import PosteriorMean + +NUM_RESTARTS = 20 if not SMOKE_TEST else 2 +RAW_SAMPLES = 2048 if not SMOKE_TEST else 4 + + +argmax_pmean, max_pmean = optimize_acqf( + acq_function=PosteriorMean(model), + bounds=bounds, + q=1, + num_restarts=20 if not SMOKE_TEST else 2, + raw_samples=2048 if not SMOKE_TEST else 4, +) + + +# Now we can optimize KG after passing the current value. We also pass in the `sampler` from the original `qKG` above, which containst the fixed base samples $\mathbf{Z}_f$. This is to ensure that we optimize the same approximation and so our values are an apples-to-apples comparison (as `num_fantasies` increases, the effect of this randomness will get less and less important). + +# In[8]: + + +qKG_proper = qKnowledgeGradient( + model, + num_fantasies=NUM_FANTASIES, + sampler=qKG.sampler, + current_value=max_pmean, +) + +with manual_seed(1234): + candidates_proper, acq_value_proper = optimize_acqf( + acq_function=qKG_proper, + bounds=bounds, + q=2, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + + +# In[9]: + + +candidates_proper + + +# In[10]: + + +acq_value_proper + diff --git a/website-old/static/files/optimize_stochastic.ipynb b/website-old/static/files/optimize_stochastic.ipynb new file mode 100644 index 0000000000..abf02f2958 --- /dev/null +++ b/website-old/static/files/optimize_stochastic.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8cc0284e-16f0-48c1-8b74-37449dcd2fb4", + "showInput": false + }, + "source": [ + "## Optimize acquisition functions using torch.optim\n", + "\n", + "In this tutorial, we show how to use PyTorch's `optim` module for optimizing BoTorch MC acquisition functions. This is useful if the acquisition function is stochastic in nature (caused by re-sampling the base samples when using the reparameterization trick, or if the model posterior itself is stochastic).\n", + "\n", + "*Note:* A pre-packaged, more user-friendly version of the optimization loop we will develop below is contained in the `gen_candidates_torch` function in the `botorch.gen` module. This tutorial should be quite useful if you would like to implement custom optimizers beyond what is contained in `gen_candidates_torch`.\n", + "\n", + "As discussed in the [CMA-ES tutorial](./optimize_with_cmaes), for deterministic acquisition functions BoTorch uses quasi-second order methods (such as L-BFGS-B or SLSQP) by default, which provide superior convergence speed in this situation. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "876ccfae-63ae-403e-85c7-7e279b6405ea", + "showInput": false + }, + "source": [ + "### Set up a toy model\n", + "\n", + "We'll fit a `SingleTaskGP` model on noisy observations of the function $f(x) = 1 - \\|x\\|_2$ in `d=5` dimensions on the hypercube $[-1, 1]^d$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668651600271, + "executionStopTime": 1668651601948, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "ab41b75c-bd1f-45a3-a10d-760b93eaf9af", + "requestMsgId": "9fb7ecfc-4c8c-4e5e-9cfe-44f8f73a2d45" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "I1116 182000.166 _utils_internal.py:179] NCCL_DEBUG env var is set to None\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "I1116 182000.167 _utils_internal.py:188] NCCL_DEBUG is INFO from /etc/nccl.conf\n" + ] + } + ], + "source": [ + "import torch\n", + "\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668651602257, + "executionStopTime": 1668651602610, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "fa81436e-7e13-4521-8542-ca813ae08884", + "requestMsgId": "7cae57f9-5eaf-4362-9fc6-63e8e200b63e" + }, + "outputs": [], + "source": [ + "d = 5\n", + "\n", + "bounds = torch.stack([-torch.ones(d), torch.ones(d)])\n", + "\n", + "train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(50, d)\n", + "train_Y = 1 - torch.linalg.norm(train_X, dim=-1, keepdim=True)\n", + "\n", + "model = SingleTaskGP(train_X, train_Y)\n", + "mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "fit_gpytorch_mll(mll);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e2966b4e-bf48-4fa2-a2bd-96970b803026", + "showInput": false + }, + "source": [ + "### Define acquisition function\n", + "\n", + "We'll use `qExpectedImprovement` with a `StochasticSampler` that uses a small number of MC samples. This results in a stochastic acquisition function that one should not attempt to optimize with the quasi-second order methods that are used by default in BoTorch's `optimize_acqf` function." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668651603261, + "executionStopTime": 1668651603264, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "bee44c18-723f-4eb3-a446-dc091b80305d", + "requestMsgId": "eda81892-99db-479a-9cad-a620c9c7fbf5" + }, + "outputs": [], + "source": [ + "from botorch.acquisition import qExpectedImprovement\n", + "from botorch.sampling.stochastic_samplers import StochasticSampler\n", + "\n", + "sampler = StochasticSampler(sample_shape=torch.Size([128]))\n", + "qEI = qExpectedImprovement(model, best_f=train_Y.max(), sampler=sampler)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "a952bcb8-745f-4164-8796-a85e7b9ee40c", + "showInput": false + }, + "source": [ + "### Optimizing the acquisition function\n", + "\n", + "We will perform optimization over `N=5` random initial `q`-batches with `q=2` in parallel. We use `N` random restarts because the acquisition function is non-convex and as a result we may get stuck in local minima." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668651603737, + "executionStopTime": 1668651603828, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "608b95ac-4445-41e6-ad5f-c64a77653a3e", + "requestMsgId": "5a4dfcc2-2643-46ce-abdf-7d170965aaee" + }, + "outputs": [], + "source": [ + "N = 5\n", + "q = 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "fbf016a1-1c6e-4c87-881b-be870163a306", + "showInput": false + }, + "source": [ + "#### Choosing initial conditions via a heuristic\n", + "\n", + "Using random initial conditions in conjunction with gradient-based optimizers can be problematic because qEI values and their corresponding gradients are often zero in large parts of the feature space. To mitigate this issue, BoTorch provides a heuristic for generating promising initial conditions (this dirty and not-so-little secret of Bayesian Optimization is actually very important for overall closed-loop performance).\n", + "\n", + "Given a set of `q`-batches $X'$ and associated acquisiton function values $Y'$, the `initialize_q_batch_nonneg` samples promising initial conditions $X$ (without replacement) from the multinomial distribution\n", + "\n", + "$$ \\mathbb{P}(X = X'_i) \\sim \\exp (\\eta \\tilde{Y}_i), \\qquad \\text{where} \\;\\; \\tilde{Y}_i = \\frac{Y'_i - \\mu(Y)}{\\sigma(Y)} \\;\\; \\text{if} \\;\\; Y'_i >0 $$\n", + "\n", + "and $\\mathbb{P}(X = X'_j) = 0$ for all $j$ such that $Y'_j = 0$. \n", + "\n", + "Fortunately, thanks to the high degree of parallelism in BoTorch, evaluating the acquisition function at a large number of randomly chosen points is quite cheap." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668651604094, + "executionStopTime": 1668651604159, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "60ec6384-1820-4ba3-a4ac-a75e3ac2a1e8", + "requestMsgId": "b38d8d63-745d-4297-b99b-f4b4c5a15eae" + }, + "outputs": [], + "source": [ + "from botorch.optim.initializers import initialize_q_batch_nonneg\n", + "\n", + "# generate a large number of random q-batches\n", + "Xraw = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(100 * N, q, d)\n", + "Yraw = qEI(Xraw) # evaluate the acquisition function on these q-batches\n", + "\n", + "# apply the heuristic for sampling promising initial conditions\n", + "X, _ = initialize_q_batch_nonneg(Xraw, Yraw, N)\n", + "\n", + "# we'll want gradients for the input\n", + "X.requires_grad_(True);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "c48909f8-747b-42d7-88e0-dd97b4ae9d87", + "showInput": false + }, + "source": [ + "#### Optimizing the acquisition function\n", + "\n", + "If you have used PyTorch, the basic optimization loop should be quite familiar. However, it is important to note that there is a **key difference** here compared to training ML models: When training ML models, one typically computes the gradient of an empirical loss function w.r.t. the model's parameters, while here we take the gradient of the acquisition function w.r.t. to the candidate set.\n", + "\n", + "Thus, when setting the optimizer from `torch.optim`, we **do not** add the acquisition function's parameters as parameters to optimize (that would be quite bad!).\n", + "\n", + "In this example, we use a vanilla `Adam` optimizer with fixed learning rate for a fixed number of iterations in order to keep things simple. But you can get as fancy as you want with learning rate scheduling, early termination, etc.\n", + "\n", + "A couple of things to note:\n", + "1. Evaluating the acquisition function on the `N x q x d`-dim inputs means evaluating `N` `q`-batches in `t`-batch mode. The result of this is an `N`-dim tensor of acquisition function values, evaluated independently. To compute the gradient of the full input `X` via back-propagation, we can for convenience just compute the gradient of the sum of the losses. \n", + "2. `torch.optim` does not have good built in support for constraints (general constrained stochastic optimization is hard and still an open research area). Here we do something simple and project the value obtained after taking the gradient step to the feasible set - that is, we perform \"projected stochastic gradient descent\". Since the feasible set here is a hyperrectangle, this can be done by simple clamping. Another approach would be to transform the feasible interval for each dimension to the real line, e.g. by using a sigmoid function, and then optimizing in the unbounded transformed space. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668651604492, + "executionStopTime": 1668651604767, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "5bc4484c-9f7e-478b-990a-4614e05238df", + "requestMsgId": "e4eae94a-20a2-49bc-8abd-2322681bf1fa" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iteration 15/75 - Loss: -0.924\n", + "Iteration 30/75 - Loss: -1.281\n", + "Iteration 45/75 - Loss: -1.374\n", + "Iteration 60/75 - Loss: -1.363\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iteration 75/75 - Loss: -1.361\n" + ] + } + ], + "source": [ + "# set up the optimizer, make sure to only pass in the candidate set here\n", + "optimizer = torch.optim.Adam([X], lr=0.01)\n", + "X_traj = [] # we'll store the results\n", + "\n", + "# run a basic optimization loop\n", + "for i in range(75):\n", + " optimizer.zero_grad()\n", + " # this performs batch evaluation, so this is an N-dim tensor\n", + " losses = -qEI(X) # torch.optim minimizes\n", + " loss = losses.sum()\n", + "\n", + " loss.backward() # perform backward pass\n", + " optimizer.step() # take a step\n", + "\n", + " # clamp values to the feasible set\n", + " for j, (lb, ub) in enumerate(zip(*bounds)):\n", + " X.data[..., j].clamp_(lb, ub) # need to do this on the data not X itself\n", + "\n", + " # store the optimization trajecatory\n", + " X_traj.append(X.detach().clone())\n", + "\n", + " if (i + 1) % 15 == 0:\n", + " print(f\"Iteration {i+1:>3}/75 - Loss: {loss.item():>4.3f}\")\n", + "\n", + " # use your favorite convergence criterion here..." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false, + "customOutput": null, + "executionStartTime": 1668651605000, + "executionStopTime": 1668651605005, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "76b4392a-d688-498a-9205-d95afbb0aca9", + "requestMsgId": "26a3b8ad-350d-4890-886b-7b38f7842e74" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/website-old/static/files/optimize_stochastic.py b/website-old/static/files/optimize_stochastic.py new file mode 100644 index 0000000000..05227ef266 --- /dev/null +++ b/website-old/static/files/optimize_stochastic.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Optimize acquisition functions using torch.optim +# +# In this tutorial, we show how to use PyTorch's `optim` module for optimizing BoTorch MC acquisition functions. This is useful if the acquisition function is stochastic in nature (caused by re-sampling the base samples when using the reparameterization trick, or if the model posterior itself is stochastic). +# +# *Note:* A pre-packaged, more user-friendly version of the optimization loop we will develop below is contained in the `gen_candidates_torch` function in the `botorch.gen` module. This tutorial should be quite useful if you would like to implement custom optimizers beyond what is contained in `gen_candidates_torch`. +# +# As discussed in the [CMA-ES tutorial](./optimize_with_cmaes), for deterministic acquisition functions BoTorch uses quasi-second order methods (such as L-BFGS-B or SLSQP) by default, which provide superior convergence speed in this situation. + +# ### Set up a toy model +# +# We'll fit a `SingleTaskGP` model on noisy observations of the function $f(x) = 1 - \|x\|_2$ in `d=5` dimensions on the hypercube $[-1, 1]^d$. + +# In[1]: + + +import torch + +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from gpytorch.mlls import ExactMarginalLogLikelihood + + +# In[2]: + + +d = 5 + +bounds = torch.stack([-torch.ones(d), torch.ones(d)]) + +train_X = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(50, d) +train_Y = 1 - torch.linalg.norm(train_X, dim=-1, keepdim=True) + +model = SingleTaskGP(train_X, train_Y) +mll = ExactMarginalLogLikelihood(model.likelihood, model) +fit_gpytorch_mll(mll); + + +# ### Define acquisition function +# +# We'll use `qExpectedImprovement` with a `StochasticSampler` that uses a small number of MC samples. This results in a stochastic acquisition function that one should not attempt to optimize with the quasi-second order methods that are used by default in BoTorch's `optimize_acqf` function. + +# In[3]: + + +from botorch.acquisition import qExpectedImprovement +from botorch.sampling.stochastic_samplers import StochasticSampler + +sampler = StochasticSampler(sample_shape=torch.Size([128])) +qEI = qExpectedImprovement(model, best_f=train_Y.max(), sampler=sampler) + + +# ### Optimizing the acquisition function +# +# We will perform optimization over `N=5` random initial `q`-batches with `q=2` in parallel. We use `N` random restarts because the acquisition function is non-convex and as a result we may get stuck in local minima. + +# In[4]: + + +N = 5 +q = 2 + + +# #### Choosing initial conditions via a heuristic +# +# Using random initial conditions in conjunction with gradient-based optimizers can be problematic because qEI values and their corresponding gradients are often zero in large parts of the feature space. To mitigate this issue, BoTorch provides a heuristic for generating promising initial conditions (this dirty and not-so-little secret of Bayesian Optimization is actually very important for overall closed-loop performance). +# +# Given a set of `q`-batches $X'$ and associated acquisiton function values $Y'$, the `initialize_q_batch_nonneg` samples promising initial conditions $X$ (without replacement) from the multinomial distribution +# +# $$ \mathbb{P}(X = X'_i) \sim \exp (\eta \tilde{Y}_i), \qquad \text{where} \;\; \tilde{Y}_i = \frac{Y'_i - \mu(Y)}{\sigma(Y)} \;\; \text{if} \;\; Y'_i >0 $$ +# +# and $\mathbb{P}(X = X'_j) = 0$ for all $j$ such that $Y'_j = 0$. +# +# Fortunately, thanks to the high degree of parallelism in BoTorch, evaluating the acquisition function at a large number of randomly chosen points is quite cheap. + +# In[5]: + + +from botorch.optim.initializers import initialize_q_batch_nonneg + +# generate a large number of random q-batches +Xraw = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(100 * N, q, d) +Yraw = qEI(Xraw) # evaluate the acquisition function on these q-batches + +# apply the heuristic for sampling promising initial conditions +X, _ = initialize_q_batch_nonneg(Xraw, Yraw, N) + +# we'll want gradients for the input +X.requires_grad_(True); + + +# #### Optimizing the acquisition function +# +# If you have used PyTorch, the basic optimization loop should be quite familiar. However, it is important to note that there is a **key difference** here compared to training ML models: When training ML models, one typically computes the gradient of an empirical loss function w.r.t. the model's parameters, while here we take the gradient of the acquisition function w.r.t. to the candidate set. +# +# Thus, when setting the optimizer from `torch.optim`, we **do not** add the acquisition function's parameters as parameters to optimize (that would be quite bad!). +# +# In this example, we use a vanilla `Adam` optimizer with fixed learning rate for a fixed number of iterations in order to keep things simple. But you can get as fancy as you want with learning rate scheduling, early termination, etc. +# +# A couple of things to note: +# 1. Evaluating the acquisition function on the `N x q x d`-dim inputs means evaluating `N` `q`-batches in `t`-batch mode. The result of this is an `N`-dim tensor of acquisition function values, evaluated independently. To compute the gradient of the full input `X` via back-propagation, we can for convenience just compute the gradient of the sum of the losses. +# 2. `torch.optim` does not have good built in support for constraints (general constrained stochastic optimization is hard and still an open research area). Here we do something simple and project the value obtained after taking the gradient step to the feasible set - that is, we perform "projected stochastic gradient descent". Since the feasible set here is a hyperrectangle, this can be done by simple clamping. Another approach would be to transform the feasible interval for each dimension to the real line, e.g. by using a sigmoid function, and then optimizing in the unbounded transformed space. + +# In[6]: + + +# set up the optimizer, make sure to only pass in the candidate set here +optimizer = torch.optim.Adam([X], lr=0.01) +X_traj = [] # we'll store the results + +# run a basic optimization loop +for i in range(75): + optimizer.zero_grad() + # this performs batch evaluation, so this is an N-dim tensor + losses = -qEI(X) # torch.optim minimizes + loss = losses.sum() + + loss.backward() # perform backward pass + optimizer.step() # take a step + + # clamp values to the feasible set + for j, (lb, ub) in enumerate(zip(*bounds)): + X.data[..., j].clamp_(lb, ub) # need to do this on the data not X itself + + # store the optimization trajecatory + X_traj.append(X.detach().clone()) + + if (i + 1) % 15 == 0: + print(f"Iteration {i+1:>3}/75 - Loss: {loss.item():>4.3f}") + + # use your favorite convergence criterion here... + + +# In[7]: + + + + diff --git a/website-old/static/files/optimize_with_cmaes.ipynb b/website-old/static/files/optimize_with_cmaes.ipynb new file mode 100644 index 0000000000..2be84627ef --- /dev/null +++ b/website-old/static/files/optimize_with_cmaes.ipynb @@ -0,0 +1,149 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimize acquisition functions using CMA-ES\n", + "\n", + "In this tutorial, we show how to use an external optimizer (in this case [CMA-ES](https://en.wikipedia.org/wiki/CMA-ES)) for optimizing BoTorch acquisition functions. CMA-ES is a zero-th order optimizer, meaning that it only uses function evaluations and does not require gradient information. This is of course very useful if gradient information about the function to be optimized is unavailable. \n", + "\n", + "In BoTorch, we typically do have gradient information available (thanks, autograd!). One is also generally better off using this information, rather than just ignoring it. However, for certain custom models or acquisition functions, we may not be able to backprop through the acquisition function and/or model. In such instances, using a zero-th order optimizer is appropriate.\n", + "\n", + "For this example we use the [PyCMA](https://github.com/CMA-ES/pycma) implementation of CMA-ES. PyCMA is easily installed via pip by running `pip install cma`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setting up the acquisition function\n", + "\n", + "For the purpose of this tutorial, we'll use a basic `UpperConfidenceBound` acquisition function on a basic model fit on synthetic data. Please see the documentation for [Models](../docs/models) and [Acquisition Functions](../docs/acquisition) for more information." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import math\n", + "import torch\n", + "\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models import SingleTaskGP\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "X = torch.rand(20, 2) - 0.5\n", + "Y = (torch.sin(2 * math.pi * X[:, 0]) + torch.cos(2 * math.pi * X[:, 1])).unsqueeze(-1)\n", + "Y += 0.1 * torch.randn_like(Y)\n", + "\n", + "gp = SingleTaskGP(X, Y)\n", + "mll = ExactMarginalLogLikelihood(gp.likelihood, gp)\n", + "fit_gpytorch_mll(mll);" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from botorch.acquisition import UpperConfidenceBound\n", + "\n", + "UCB = UpperConfidenceBound(gp, beta=0.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Optimizing the acquisition function using CMA-ES\n", + "\n", + "**Note:** Relative to sequential evaluations, parallel evaluations of the acqusition function are extremely fast in botorch (due to automatic parallelization across batch dimensions). In order to exploit this, we use the \"ask/tell\" interface to `cma` - this way we can batch-evaluate the whole CMA-ES population in parallel.\n", + "\n", + "In this examle we use an initial standard deviation $\\sigma_0 = 0.2$ and a population size $\\lambda = 50$. \n", + "We also constrain the input `X` to the unit cube $[0, 1]^d$.\n", + "See `cma`'s [API Reference](http://cma.gforge.inria.fr/apidocs-pycma/cma.evolution_strategy.CMAEvolutionStrategy.html) for more information on these options.\n", + "\n", + "With this, we can optimize this acquistition function as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(25_w,50)-aCMA-ES (mu_w=14.0,w_1=14%) in dimension 2 (seed=374178, Thu Aug 8 09:33:08 2019)\n" + ] + }, + { + "data": { + "text/plain": [ + "tensor([0.2642, 0.0255])" + ] + }, + "execution_count": 6, + "metadata": { + "bento_obj_id": "140190506026760" + }, + "output_type": "execute_result" + } + ], + "source": [ + "import cma\n", + "import numpy as np\n", + "\n", + "# get initial condition for CMAES in numpy form\n", + "# note that CMAES expects a different shape (no explicit q-batch dimension)\n", + "x0 = np.random.rand(2)\n", + "\n", + "# create the CMA-ES optimizer\n", + "es = cma.CMAEvolutionStrategy(\n", + " x0=x0,\n", + " sigma0=0.2,\n", + " inopts={\"bounds\": [0, 1], \"popsize\": 50},\n", + ")\n", + "\n", + "# speed up things by telling pytorch not to generate a compute graph in the background\n", + "with torch.no_grad():\n", + "\n", + " # Run the optimization loop using the ask/tell interface -- this uses\n", + " # PyCMA's default settings, see the PyCMA documentation for how to modify these\n", + " while not es.stop():\n", + " xs = es.ask() # as for new points to evaluate\n", + " # convert to Tensor for evaluating the acquisition function\n", + " X = torch.tensor(xs, device=X.device, dtype=X.dtype)\n", + " # evaluate the acquisition function (optimizer assumes we're minimizing)\n", + " Y = -UCB(\n", + " X.unsqueeze(-2)\n", + " ) # acquisition functions require an explicit q-batch dimension\n", + " y = Y.view(-1).double().numpy() # convert result to numpy array\n", + " es.tell(xs, y) # return the result to the optimizer\n", + "\n", + "# convert result back to a torch tensor\n", + "best_x = torch.from_numpy(es.best.x).to(X)\n", + "\n", + "best_x" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/optimize_with_cmaes.py b/website-old/static/files/optimize_with_cmaes.py new file mode 100644 index 0000000000..7ca80b6406 --- /dev/null +++ b/website-old/static/files/optimize_with_cmaes.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Optimize acquisition functions using CMA-ES +# +# In this tutorial, we show how to use an external optimizer (in this case [CMA-ES](https://en.wikipedia.org/wiki/CMA-ES)) for optimizing BoTorch acquisition functions. CMA-ES is a zero-th order optimizer, meaning that it only uses function evaluations and does not require gradient information. This is of course very useful if gradient information about the function to be optimized is unavailable. +# +# In BoTorch, we typically do have gradient information available (thanks, autograd!). One is also generally better off using this information, rather than just ignoring it. However, for certain custom models or acquisition functions, we may not be able to backprop through the acquisition function and/or model. In such instances, using a zero-th order optimizer is appropriate. +# +# For this example we use the [PyCMA](https://github.com/CMA-ES/pycma) implementation of CMA-ES. PyCMA is easily installed via pip by running `pip install cma`. + +# ### Setting up the acquisition function +# +# For the purpose of this tutorial, we'll use a basic `UpperConfidenceBound` acquisition function on a basic model fit on synthetic data. Please see the documentation for [Models](../docs/models) and [Acquisition Functions](../docs/acquisition) for more information. + +# In[4]: + + +import math +import torch + +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from gpytorch.mlls import ExactMarginalLogLikelihood + +X = torch.rand(20, 2) - 0.5 +Y = (torch.sin(2 * math.pi * X[:, 0]) + torch.cos(2 * math.pi * X[:, 1])).unsqueeze(-1) +Y += 0.1 * torch.randn_like(Y) + +gp = SingleTaskGP(X, Y) +mll = ExactMarginalLogLikelihood(gp.likelihood, gp) +fit_gpytorch_mll(mll); + + +# In[5]: + + +from botorch.acquisition import UpperConfidenceBound + +UCB = UpperConfidenceBound(gp, beta=0.1) + + +# ### Optimizing the acquisition function using CMA-ES +# +# **Note:** Relative to sequential evaluations, parallel evaluations of the acqusition function are extremely fast in botorch (due to automatic parallelization across batch dimensions). In order to exploit this, we use the "ask/tell" interface to `cma` - this way we can batch-evaluate the whole CMA-ES population in parallel. +# +# In this examle we use an initial standard deviation $\sigma_0 = 0.2$ and a population size $\lambda = 50$. +# We also constrain the input `X` to the unit cube $[0, 1]^d$. +# See `cma`'s [API Reference](http://cma.gforge.inria.fr/apidocs-pycma/cma.evolution_strategy.CMAEvolutionStrategy.html) for more information on these options. +# +# With this, we can optimize this acquistition function as follows: + +# In[6]: + + +import cma +import numpy as np + +# get initial condition for CMAES in numpy form +# note that CMAES expects a different shape (no explicit q-batch dimension) +x0 = np.random.rand(2) + +# create the CMA-ES optimizer +es = cma.CMAEvolutionStrategy( + x0=x0, + sigma0=0.2, + inopts={"bounds": [0, 1], "popsize": 50}, +) + +# speed up things by telling pytorch not to generate a compute graph in the background +with torch.no_grad(): + + # Run the optimization loop using the ask/tell interface -- this uses + # PyCMA's default settings, see the PyCMA documentation for how to modify these + while not es.stop(): + xs = es.ask() # as for new points to evaluate + # convert to Tensor for evaluating the acquisition function + X = torch.tensor(xs, device=X.device, dtype=X.dtype) + # evaluate the acquisition function (optimizer assumes we're minimizing) + Y = -UCB( + X.unsqueeze(-2) + ) # acquisition functions require an explicit q-batch dimension + y = Y.view(-1).double().numpy() # convert result to numpy array + es.tell(xs, y) # return the result to the optimizer + +# convert result back to a torch tensor +best_x = torch.from_numpy(es.best.x).to(X) + +best_x + diff --git a/website-old/static/files/preference_bo.ipynb b/website-old/static/files/preference_bo.ipynb new file mode 100644 index 0000000000..30c85efc3b --- /dev/null +++ b/website-old/static/files/preference_bo.ipynb @@ -0,0 +1,532 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "a0534c61-7c52-41ec-bd7a-c4c617eb6d4d", + "showInput": false + }, + "source": [ + "## Bayesian optimization with pairwise comparison data\n", + "\n", + "In many real-world problems, people are faced with making multi-objective decisions. While it is often hard write down the exact utility function over those objectives, it is much easier for people to make pairwise comparisons. Drawing from utility theory and discrete choice models in economics, one can assume the user makes comparisons based on some intrinsic utility function and model the latent utility function using only the observed attributes and pairwise comparisons. \n", + "In machine learning terms, we are concerned with [object ranking](https://en.wikipedia.org/wiki/Preference_learning) here.\n", + "This [book](https://link.springer.com/book/10.1007/978-3-642-14125-6) has some more general discussions on this topic.\n", + "\n", + "In this tutorial, we illustrate how to implement a simple Bayesian Optimization (BO) closed loop in BoTorch when we only observe (noisy) pairwise comparisons of the latent function values." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e61ee00b-9faf-470b-884e-f992b02b95e9", + "showInput": false + }, + "source": [ + "### Data generation\n", + "\n", + "Let's first generate some data that we are going to model.\n", + "\n", + "In this tutorial, the latent function we aim to fit is the weighted sum of the input vector, where for dimension $i$, the weight is $\\sqrt{i}$.\n", + "The input tensor X is randomly sampled within the d-dimensional unit cube.\n", + "\n", + "\n", + "Specifically,\n", + "$$\n", + "y = f(X) = \\sum_{i=1}^{d} \\sqrt{i} X_i ~~\\text{where}~~X \\in [0, 1]^d\n", + "$$\n", + "\n", + "This function is monotonically increasing in each individual dimension and has different weights for each input dimension, which are some properties that many real-world utility functions possess.\n", + "\n", + "We generate the data using following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1679377485172, + "executionStopTime": 1679377496184, + "originalKey": "3a0316c5-1648-4839-a717-59cd65c96a96", + "requestMsgId": "548b0126-7321-403b-bb01-d3f26ab165a0", + "showInput": true + }, + "outputs": [], + "source": [ + "import os\n", + "import warnings\n", + "from itertools import combinations\n", + "\n", + "import numpy as np\n", + "import torch\n", + "\n", + "# Suppress potential optimization warnings for cleaner notebook\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "customOutput": null, + "executionStartTime": 1679377496266, + "executionStopTime": 1679377496271, + "originalKey": "bdd3d4a5-2e45-4048-88c3-b008aab65179", + "requestMsgId": "5c82b088-2071-455b-a563-c3586ef4d476" + }, + "outputs": [], + "source": [ + "# data generating helper functions\n", + "def utility(X):\n", + " \"\"\"Given X, output corresponding utility (i.e., the latent function)\"\"\"\n", + " # y is weighted sum of X, with weight sqrt(i) imposed on dimension i\n", + " weighted_X = X * torch.sqrt(torch.arange(X.size(-1), dtype=torch.float) + 1)\n", + " y = torch.sum(weighted_X, dim=-1)\n", + " return y\n", + "\n", + "\n", + "def generate_data(n, dim=2):\n", + " \"\"\"Generate data X and y\"\"\"\n", + " # X is randomly sampled from dim-dimentional unit cube\n", + " # we recommend using double as opposed to float tensor here for\n", + " # better numerical stability\n", + " X = torch.rand(n, dim, dtype=torch.float64)\n", + " y = utility(X)\n", + " return X, y\n", + "\n", + "\n", + "def generate_comparisons(y, n_comp, noise=0.1, replace=False):\n", + " \"\"\"Create pairwise comparisons with noise\"\"\"\n", + " # generate all possible pairs of elements in y\n", + " all_pairs = np.array(list(combinations(range(y.shape[0]), 2)))\n", + " # randomly select n_comp pairs from all_pairs\n", + " comp_pairs = all_pairs[\n", + " np.random.choice(range(len(all_pairs)), n_comp, replace=replace)\n", + " ]\n", + " # add gaussian noise to the latent y values\n", + " c0 = y[comp_pairs[:, 0]] + np.random.standard_normal(len(comp_pairs)) * noise\n", + " c1 = y[comp_pairs[:, 1]] + np.random.standard_normal(len(comp_pairs)) * noise\n", + " reverse_comp = (c0 < c1).numpy()\n", + " comp_pairs[reverse_comp, :] = np.flip(comp_pairs[reverse_comp, :], 1)\n", + " comp_pairs = torch.tensor(comp_pairs).long()\n", + "\n", + " return comp_pairs\n", + "\n", + "\n", + "torch.manual_seed(123)\n", + "n = 50 if not SMOKE_TEST else 5\n", + "m = 100 if not SMOKE_TEST else 10\n", + "dim = 4\n", + "noise = 0.1\n", + "train_X, train_y = generate_data(n, dim=dim)\n", + "train_comp = generate_comparisons(train_y, m, noise=noise)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ccf32bac-a38d-4b26-b18f-0cfaf21cd5cd", + "showInput": false + }, + "source": [ + "`train_X` is a `n x dim` tensor;\n", + "\n", + "`train_y` is a `n`-dimensional vector, representing the noise-free latent function value $y$;\n", + "\n", + "`train_comp` is a `m x 2` tensor, representing the noisy comparisons based on $\\tilde{y} = y + N(0, \\sigma^2)$, where `train_comp[k, :] = (i, j)` indicates $\\tilde{y_i} > \\tilde{y_j}$.\n", + "\n", + "If y is the utility function value for a set of `n` items for a specific user, $\\tilde{y_i} > \\tilde{y_j}$ indicates (with some noise) the user prefers item i over item j.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "b00d6851-2ef7-4897-b77b-f33d81aacc40", + "showInput": false + }, + "source": [ + "### PairwiseGP model fitting\n", + "\n", + "In this problem setting, we never observe the actual function value.\n", + "Therefore, instead of fitting the model using (`train_X`, `train_y`) pair, we will fit the model with (`train_X`, `train_comp`).\n", + "\n", + "`PairwiseGP` from BoTorch is designed to work with such pairwise comparison input.\n", + "We use `PairwiseLaplaceMarginalLogLikelihood` as the marginal log likelihood that we aim to maximize for optimizing the hyperparameters." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1679377496379, + "executionStopTime": 1679377503021, + "hidden_ranges": [], + "originalKey": "a41e5119-e06c-488b-b6e1-91431b399b11", + "requestMsgId": "1efc0e48-14cf-4d74-8c40-6d83785429b5" + }, + "outputs": [], + "source": [ + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.models.pairwise_gp import PairwiseGP, PairwiseLaplaceMarginalLogLikelihood\n", + "from botorch.models.transforms.input import Normalize\n", + "\n", + "\n", + "model = PairwiseGP(\n", + " train_X,\n", + " train_comp,\n", + " input_transform=Normalize(d=train_X.shape[-1]),\n", + ")\n", + "mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model)\n", + "mll = fit_gpytorch_mll(mll)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "368a5c39-3fd4-4840-86bd-2fa6ae17694b", + "showInput": false + }, + "source": [ + "Because the we never observe the latent function value, output values from the model are only meaningful on a relative scale.\n", + "Hence, given a test pair (`test_X`, `test_y`), we can evaluate the model using Kendall-Tau rank correlation." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "customOutput": null, + "executionStartTime": 1679377503118, + "executionStopTime": 1679377503474, + "originalKey": "9aae3cd3-7e35-4250-84d2-64c08d198535", + "requestMsgId": "a680776e-3541-47aa-9f8c-cd5496f23039" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test Kendall-Tau rank correlation: 0.8885\n" + ] + } + ], + "source": [ + "from scipy.stats import kendalltau\n", + "\n", + "\n", + "# Kendall-Tau rank correlation\n", + "def eval_kt_cor(model, test_X, test_y):\n", + " pred_y = model.posterior(test_X).mean.squeeze().detach().numpy()\n", + " return kendalltau(pred_y, test_y).correlation\n", + "\n", + "\n", + "n_kendall = 1000 if not SMOKE_TEST else 10\n", + "\n", + "test_X, test_y = generate_data(n_kendall, dim=dim)\n", + "kt_correlation = eval_kt_cor(model, test_X, test_y)\n", + "\n", + "print(f\"Test Kendall-Tau rank correlation: {kt_correlation:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "c0b823a5-6f8e-4a0e-b700-80dc4e0e4f27", + "showInput": false + }, + "source": [ + "### Perform Bayesian Optimization loop with EUBO\n", + "\n", + "Now, we demonstrate how to implement a full Bayesian optimization with `AnalyticExpectedUtilityOfBestOption` (EUBO) acquisition function [4, 5].\n", + "\n", + "The Bayesian optimization loop for a batch size of `q` simply iterates the following steps:\n", + "1. given a surrogate model, choose a batch of points $X_{next} = \\{x_1, x_2, ..., x_q\\}$\n", + "2. observe `q_comp` randomly selected pairs of (noisy) comparisons between elements in $X_{next}$\n", + "3. update the surrogate model with $X_{next}$ and the observed pairwise comparisons\n", + "\n", + "We start off by defining a few helper functions." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1679377503538, + "executionStopTime": 1679377503567, + "hidden_ranges": [], + "originalKey": "e11ce86a-39a5-4155-a5c3-6caf75037c13", + "requestMsgId": "0119bb16-7952-4a66-b5ff-c0c53a009533" + }, + "outputs": [], + "source": [ + "from botorch.acquisition.preference import AnalyticExpectedUtilityOfBestOption\n", + "from botorch.optim import optimize_acqf\n", + "\n", + "\n", + "def init_and_fit_model(X, comp):\n", + " \"\"\"Model fitting helper function\"\"\"\n", + " model = PairwiseGP(\n", + " X,\n", + " comp,\n", + " input_transform=Normalize(d=X.shape[-1]),\n", + " )\n", + " mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + " return mll, model\n", + "\n", + "\n", + "def make_new_data(X, next_X, comps, q_comp):\n", + " \"\"\"Given X and next_X,\n", + " generate q_comp new comparisons between next_X\n", + " and return the concatenated X and comparisons\n", + " \"\"\"\n", + " # next_X is float by default; cast it to the dtype of X (i.e., double)\n", + " next_X = next_X.to(X)\n", + " next_y = utility(next_X)\n", + " next_comps = generate_comparisons(next_y, n_comp=q_comp, noise=noise)\n", + " comps = torch.cat([comps, next_comps + X.shape[-2]])\n", + " X = torch.cat([X, next_X])\n", + " return X, comps" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "71133aee-de87-4329-986b-6e43b964b4fc", + "showInput": false + }, + "source": [ + "The Bayesian optimization loop is as follows (running the code may take a while)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1679377503648, + "executionStopTime": 1679377735278, + "hidden_ranges": [], + "originalKey": "520ac886-fb13-4148-839c-2f1197d97628", + "requestMsgId": "d044bdc0-0516-4138-991d-7f7f5c6e597a" + }, + "outputs": [], + "source": [ + "algos = [\"EUBO\", \"rand\"]\n", + "\n", + "NUM_TRIALS = 3 if not SMOKE_TEST else 2\n", + "NUM_BATCHES = 30 if not SMOKE_TEST else 2\n", + "\n", + "dim = 4\n", + "NUM_RESTARTS = 3\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 8\n", + "q = 2 # number of points per query\n", + "q_comp = 1 # number of comparisons per query\n", + "\n", + "# initial evals\n", + "best_vals = {} # best observed values\n", + "for algo in algos:\n", + " best_vals[algo] = []\n", + "\n", + "# average over multiple trials\n", + "for i in range(NUM_TRIALS):\n", + " torch.manual_seed(i)\n", + " np.random.seed(i)\n", + " data = {}\n", + " models = {}\n", + "\n", + " # Create initial data\n", + " init_X, init_y = generate_data(q, dim=dim)\n", + " comparisons = generate_comparisons(init_y, q_comp, noise=noise)\n", + " # X are within the unit cube\n", + " bounds = torch.stack([torch.zeros(dim), torch.ones(dim)])\n", + "\n", + " for algo in algos:\n", + " best_vals[algo].append([])\n", + " data[algo] = (init_X, comparisons)\n", + " _, models[algo] = init_and_fit_model(init_X, comparisons)\n", + "\n", + " best_next_y = utility(init_X).max().item()\n", + " best_vals[algo][-1].append(best_next_y)\n", + "\n", + " # we make additional NUM_BATCHES comparison queries after the initial observation\n", + " for j in range(1, NUM_BATCHES + 1):\n", + " for algo in algos:\n", + " model = models[algo]\n", + " if algo == \"EUBO\":\n", + " # create the acquisition function object\n", + " acq_func = AnalyticExpectedUtilityOfBestOption(pref_model=model)\n", + " # optimize and get new observation\n", + " next_X, acq_val = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=bounds,\n", + " q=q,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + " else:\n", + " # randomly sample data\n", + " next_X, _ = generate_data(q, dim=dim)\n", + "\n", + " # update data\n", + " X, comps = data[algo]\n", + " X, comps = make_new_data(X, next_X, comps, q_comp)\n", + " data[algo] = (X, comps)\n", + "\n", + " # refit models\n", + " _, models[algo] = init_and_fit_model(X, comps)\n", + "\n", + " # record the best observed values so far\n", + " max_val = utility(X).max().item()\n", + " best_vals[algo][-1].append(max_val)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "91928bb9-e627-42dc-93b5-446944a9532d", + "showInput": false + }, + "source": [ + "### Plot the results\n", + "\n", + "The plot below shows the best objective value observed at each step of the optimization for each of the acquisition functions. The error bars represent the 95% confidence intervals for the sample mean at that step in the optimization across the trial runs." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1679377735395, + "executionStopTime": 1679377738125, + "originalKey": "55da8504-af9b-4e88-a69e-2253ea614ce0", + "requestMsgId": "640b62b5-042f-44fd-a6ad-14f2a7129067", + "showInput": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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soUhQ4YQHBzMvn372LFjB4mJiXTu3Jnp06enuxRcly5d2L9/PyNGjOC2227jyJEj/P333xQpUoT+/funO3525cqV9OnTB29vb7Zt22Z9bW80YsQIfv/9d+699168vb3ZuXMnV69e5Y477uC7775j9uzZDlub+EZTp07lueeeo1GjRsTFxbF9+3aOHz9OjRo1GD16NHv27EkzDtnd3Z3ly5fz3HPPUb58ef755x9OnjzJ//73P3bs2GFd61ckPzMZhtYoERHJTd27d+fXX39l2rRp2d6oRERE7KOeYBGRXHT06FF+++03gHQ3TBARkdyhJFhExMGOHDnClClTbJa3g9Rl03r06EFCQgItW7a86a5yIiKSszQcQkTEwfbs2UPt2rUxm81UqVKFwMBAzpw5Y10yrVy5cqxevZpKlSo5N1ARkQJMSbCIiIPFxMQwZcoUVqxYweHDh7lw4QIeHh5UrlyZHj168Oyzz1KsWDFnhykiUqApCRYRERGRAkdjgkVERESkwHF3dgB5SUpKCidPnqRw4cK33EteRERERHKfYRhcvnyZ0qVL33RtbiXBWXDy5EnKlSvn7DBERERE5BaOHz9O2bJlMzyuJDgLChcuDKS+qI7e6UlEREREsi8mJoZy5cpZ87aMKAnOAssQCH9/fyXBIiIiIi7sVkNXNTFORERERAocJcEiIiIiUuAoCRYRERGRAselk+Aff/yRO+64g2LFiuHt7U2FChXo168fx48fv+W5a9aswWQyZXibNWtWzj8BEREREXFJLjkxzjAMHnvsMT755BMqVapE3759KVy4MCdPnmTt2rUcPXo000uVtWnThrZt26Ypr1evnmODFhEREZE8wyWT4Pfee49PPvmEJ554gvfeew83Nzeb40lJSZluq23btoSFhTk4QhERERHJy1xuOMTVq1cZP348FStWZNq0aWkSYAB3d5fM3UVEREQkj3C5bHL58uVcvHiRIUOGkJyczM8//8yBAwcIDAykY8eOVK5cOUvtRUREMHXqVK5evUrZsmVp3749ZcqUyaHoRURERCQvcLkkeNu2bQC4ublRp04dDhw4YD1mNpt59tlnmTx5cqbbmzt3LnPnzrU+dnd356mnnuLtt99Ot5f5RvHx8cTHx1sfx8TEZPq6IiIiIuK6XG44xNmzZwF45513CAgIYMuWLVy+fJl169ZRtWpV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w9ixY0lJSQHgyy+/pG7duvj4+FC+fHnefvvtNM/35MmTjBs3jqZNm1KiRAm8vLwIDQ3liSee4OzZs3a/jllled3Sk95rcOOQhblz51KvXj18fHwoVaoUTz/9NFevXk23rXXr1tGrVy+Cg4Px8vKiXLly3H333WzYsMF6LXveE0j9BuGdd96xvuYBAQG0a9eORYsWpal743u4fPlymjdvjq+vL8WKFWPQoEGcP38+My8bQ4YMsbaT0b5BX3zxBQAPPfQQAD/++CP9+vWjcuXK+Pr6EhAQQKtWrViwYEGmrgk3Hxc9ePBgTCYTR44cSXNs4cKFdOjQgSJFiuDt7U2tWrWYPHmy1v0VEbGDeoLFob788ktSUlJ45JFHCA4OzrDe2LFjmTt3Ll988QWvvvqqzbGEhAQ6duzIlStXGDBgALGxsXz77bf079+fqKgonnrqKSA1WQBYu3YtgwYNsiZagYGBmYr1xx9/ZPny5fTq1YsWLVqwePFiXn/9dQzDICAggNdff52ePXvStm1bFixYwAsvvEBwcDADBw60trFu3TqmTJlChw4daNKkCR4eHuzYsYMPP/yQZcuWsX37dgICAjL/AuayGTNmsHTpUnr27En79u1ZunQp7733HlFRUXz99dc2dadNm8azzz6Lj48PvXv3pnz58pw4cYINGzbw/fff07JlS7vfE8MwuOeee1i4cCFVq1blySefJDY2lm+++Ya77rqLd955x2ZCrMXPP//M4sWLufPOO2nevDnr1q1j9uzZHDx40JqY30xISAgdO3ZkxYoVrFmzhnbt2tkc37NnD3/++Sf169enfv36QOo3HZ6enrRs2ZJSpUpx7tw5fv75Z+655x7ee+896+fT0caMGcOkSZMoU6YMd999NwEBAaxfv56RI0fyxx9/8N133+XIdUVE8i1DMi06OtoAjOjo6AzrXL161di3b59x9erVNMdSUlKMK1euZOoWHX3JuBKx0bgSsTH1fibPS3OLiba2cyUmOlPnpKSkpPvckpJTjF3HLxq7jl80kpLTr9O2bVsDMFasWHHL17N06dIGYBw7dsxaFhISYgBG69atjfj4eGv58ePHjaCgIMPLy8v4559/rOXjxo0zAGP16tXpXqNNmzbGfz/mM2fONADDw8PD2LJli7U8JibGKFGihOHr62uULFnSOHjwoPXYsWPHDE9PT6N27do2bZ05c8a4fPlymut++eWXBmC8/vrrNuW3ijej+J977jlj3LhxNrcPP/zQWi8kJMQICQnJ9GtgiSMgIMAIDw+3lsfFxRlVq1Y1zGazceLECWv5zp07DbPZbJQuXdo4fPiwTVspKSk2de15TyyvV5s2bWze96NHjxpBQUGGu7u7zftheQ/d3d2NDRs2WMuTkpKsn8FNmzale/3/mj9/vgEYDzzwQJpjzz77rAEYM2bMsJbdGIfF5cuXjdq1axsBAQFGbGyszTHL87rVa2AxaNAgA7B5nZcvX24ARufOnY0rV65Yy1NSUozHHnvMAIzvv//+ls/1Zv8/iYjkF5nJ1wzDMDQcIhfFxcVRqFChTN0CAgIpVKUFhaq0SL2fyfPS3PwDrO0U8g/I1DkZTVpyM5uoUzaQOmUDcTOn/1Xu6dOnATKcZHQjS51Tp06lOfbGG2/g6elpfVy2bFmefvpp4uPjmT9//i3bzowHHnjAZq3owoUL06NHD+Li4nj88cepWLGiTawtW7Zk3759JCUlWctLlCiR7oS1AQMG4O/vz2+//eaQWKdMmcL48eNtbh999FG223366aepVq2a9bGPjw/9+vUjJSXFZq3tjz/+mJSUFF5//fU0wy5MJhOlS5fOVhxffvklAG+99ZbN+16+fHmeffZZkpKS0vRMA/Tv358WLVpYH7u5uTFo0CAAtm7dmqlr9+rVi2LFirFgwQJiYmKs5YmJicyZMwdvb2/69+9vLb/xc2FRqFAhBg8eTHR0dKavmxWWJSAtk00tTCYTkyZNwmQypRkuJCIiN6fhEOJy3N3dadasWZryVq1aAbBjxw6HXKdevXppykqVKnXTY8nJyZw5c8ZmrPMPP/zAxx9/zPbt27l48aLN+EzLLofZderUqRyZGHf77benKStbtiwAly5dspZt2bIFuL51uaPt2LEDX1/fdJcutAxR2LlzZ5pjmY3/Zry8vHjggQeYNm0a8+bN49FHHwVg0aJFnDt3jn79+lGkSBFr/bNnzzJp0iSWLFnC0aNH04yfdtR7fqPNmzfj5+dnHZ/8Xz4+PoSHhzv8uiIi+ZmS4Fzk6+vLlStXMlU3OTkJt7N7U++XqImbm51vVUoynNmTej+4FphvvUGIr6+vfdcCSpYsSXh4OMePH7fpYUzP8ePHgeuJp0VQUBBmc9ovKSxjjKOjo+2O70bprUrh7u5+y2OJiYnWsilTpvD8889TvHhxOnXqRNmyZa3rWE+dOpX4+HiHxJpTbvY8b0zmo6OjMZlMad4rR4mJicnw2wPLNW/spbXIbPy38tBDDzFt2jS++OILaxL83wlxABcuXKBRo0YcO3aMFi1a0LFjRwIDA3Fzc2Pnzp0sXLgwR97zCxcukJSUZJ10mJ7Y2FiHX1dEJD9TEpyLTCaTzVeZN5OcnISbb2oyleznl70k+N928PPLVBKcHc2bN2fNmjWsXLmSjh07ZlgvPDyckydPUqZMmTTJT1RUFCkpKWkS4TNnzgC4zESzpKQkXnvtNUqVKsXOnTspUaKE9ZhhGLz11lu5FovZbCYhISHdY474oyEwMBDDMDh16lSaFT8cwd/fP8PVNCxDbHJyKb3atWvTqFEjtmzZwt69eylatChLly6lQoUKtG/f3lrv888/59ixY7z22mu8/PLLNm1MmjSJhQsXZup6ls92UlKSNWm3SO/98vf3x2QypdkeXkRE7KcxweJQAwcOxGw28+mnn3Lu3LkM602YMAGABx98MM2xpKQkNm3alKZ8/fr1ANZZ+oB162tnLBEVFRVFdHQ0zZo1s0mAAf78888MlxnLCUWKFOHs2bM245UhtXcwIiIi2+1bhiksX778lnXteU/q169PXFycddjFjSw7z6U3RMWRLD2+n3/+ObNnzyY5Odm6hJrFwYMHAejZs2ea8y2fz8ywDK84ceKETXlKSgq7du1KU79JkyacP3/eIe+liIikUhIsDlWtWjWefvppzp8/z5133plm0ltKSgqvvfYac+bMoVKlSjz//PPptvPiiy/a9Gz+888/TJs2DS8vL/r27WstL1q0KHB9aEVuKlGiBD4+Pmzfvt1mMuHFixdzbJmsjDRq1IjExESbyWOGYTBmzBiHfE3+2GOP4ebmxssvv8zRo0dtjhmGYTMO1p73xDKZbcyYMTbDTY4fP84777yDu7s7999/f3aewi3169cPX19f5syZwxdffIHZbLYu+WYREhICkGb5tblz5/Lrr79m+lqWCZmzZs2yKX/nnXc4fPhwmvrDhw8HUv9oTG8N5NOnT7N///5MX19ERDQcQnLAW2+9RXR0NF988QVVqlShe/fuVKpUiZiYGJYvX05ERARVqlTh119/Tfcr7lKlShEbG0udOnW48847resEnz9/nvfee8/m63jLhgwvvvgie/fuJSAggMDAQIYNG5bjz9NsNvPEE08wZcoU6taty5133klMTAxLliwhJCQk2ysmZMWwYcOYOXMmDz/8MCtWrKB48eKsX7+eS5cuUbdu3XR7F7Oidu3aTJ06leHDh1OzZk169epFSEgIp0+fZt26dXTv3p2pU6cC9r0nAwYM4IcffmDhwoXUqVOHHj16WNcJvnDhAlOmTEl3VQZH8vf355577mH27NmcO3eOLl26pBmqM2DAAN58802eeuopVq9eTUhICLt27WLlypXcfffd/PDDD5m61pAhQ3jrrbcICwtj586dVKpUiT///JM9e/bQpk0b1q5da1O/S5cujB07ltdee43KlSvTpUsXQkJCOH/+PJGRkaxfv57XX3+d6tWrO+z1EBHJ79QTLA7n7u7O559/zooVK+jWrRsbNmxg8uTJfP311wQFBTFlyhR27dpF5cqV0z3f09OTFStW0KZNG7766iu++OILypYty9y5c9P0sNaoUYOZM2cSFBTE9OnTGTt2bJpd6HLSxIkTmTBhAiaTiQ8++IAVK1bQr18/li9fjoeHR67FUatWLZYuXcrtt9/O999/z1dffUWNGjX4/fffM715yK0MGzaMVatW0a5dO5YsWcLkyZNZvnw5devW5b777rPWs+c9MZlMfP/990yePBkPDw+mT5/OnDlzqF27NgsXLmTEiBEOeQ63cuMkuPSG6pQtW5a1a9fSoUMHfvvtNz7++GMSEhJYvnw5d955Z6avExwczOrVq+nQoQPLly/n008/JTAwkM2bN2e489+rr77KihUraNWqFStXruSdd97hl19+IT4+nrCwsBzvKRcRyW9MhpHBXqGSRkxMDAEBAURHR2c4SefatWscPnyYChUq4O3tbfe1kpOTcDuzO/V+cO3sTYw7/Vfq/ZJ1cnxiXHZZEoD0towVkexx1P9PIiKuLDP5GqgnWEREREQKICXBIiIiIlLgKAkWERERkQJHq0OIS9FYYBEREckN6gkWERERkQJHSbCIiIiIFDhKgkVERESkwFESLCIiIiIFjpJgERERESlwlASLiIiISIGjJFhEREREChwlwXlUXEISoaMXEzp6MXEJSc4OR0RERCRPURIsIiIiIgWOkmARERERKXCUBOd3ZjcoXT/1ZnbLlUseOXIEk8l001toaKi1/n8fpye9OmFhYWna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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "\n", + "%matplotlib inline\n", + "\n", + "plt.rcParams.update({\"font.size\": 14})\n", + "\n", + "algo_labels = {\n", + " \"rand\": \"Random Exploration\",\n", + " \"EUBO\": \"EUBO\",\n", + "}\n", + "\n", + "\n", + "def ci(y):\n", + " return 1.96 * y.std(axis=0) / np.sqrt(y.shape[0])\n", + "\n", + "\n", + "# the utility function is maximized at the full vector of 1\n", + "optimal_val = utility(torch.tensor([[1] * dim])).item()\n", + "iters = list(range(NUM_BATCHES + 1))\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n", + "# plot the optimal value\n", + "ax.plot(\n", + " iters,\n", + " [optimal_val] * len(iters),\n", + " label=\"Optimal Function Value\",\n", + " color=\"black\",\n", + " linewidth=1.5,\n", + ")\n", + "\n", + "# plot the the best observed value from each algorithm\n", + "for algo in algos:\n", + " ys = np.vstack(best_vals[algo])\n", + " ax.errorbar(\n", + " iters, ys.mean(axis=0), yerr=ci(ys), label=algo_labels[algo], linewidth=1.5\n", + " )\n", + "\n", + "ax.set(\n", + " xlabel=f\"Number of queries (q = {q}, num_comparisons = {q_comp})\",\n", + " ylabel=\"Best observed value\",\n", + " title=f\"{dim}-dim weighted vector sum\",\n", + ")\n", + "ax.legend(loc=\"best\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "hidden_ranges": [], + "originalKey": "37fbe9bf-e19b-43bb-9ada-5dbcd40a46e5", + "showInput": false + }, + "source": [ + "### References\n", + "\n", + "[1] Wei Chu, and Zoubin Ghahramani. 2005. “Preference Learning with Gaussian Processes.” In Proceedings of the 22Nd International Conference on Machine Learning, 137–44. ICML ’05. New York, NY, USA: ACM.\n", + "\n", + "[2] Eric Brochu, Vlad M. Cora, and Nando de Freitas. 2010. “A Tutorial on Bayesian Optimization of Expensive Cost Functions, with Application to Active User Modeling and Hierarchical Reinforcement Learning.” arXiv [cs.LG]. arXiv.\n", + "\n", + "[3] Javier González, Zhenwen Dai, Andreas Damianou, and Neil D. Lawrence. 2017. “Preferential Bayesian Optimization.” In Proceedings of the 34th International Conference on Machine Learning, edited by Doina Precup and Yee Whye Teh, 70:1282–91. Proceedings of Machine Learning Research. International Convention Centre, Sydney, Australia: PMLR.\n", + "\n", + "[4] Zhiyuan Jerry Lin, Raul Astudillo, Peter I. Frazier, and Eytan Bakshy, Preference Exploration for Efficient Bayesian Optimization with Multiple Outcomes. AISTATS, 2022. https://arxiv.org/abs/2203.11382\n", + "\n", + "[5] Raul Astudillo, Zhiyuan Jerry Lin, Eytan Bakshy, and Peter I. Frazier, qEUBO: A Decision-Theoretic Acquisition Function for Preferential Bayesian Optimization. AISTATS, 2023.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/preference_bo.py b/website-old/static/files/preference_bo.py new file mode 100644 index 0000000000..9bed4d486b --- /dev/null +++ b/website-old/static/files/preference_bo.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Bayesian optimization with pairwise comparison data +# +# In many real-world problems, people are faced with making multi-objective decisions. While it is often hard write down the exact utility function over those objectives, it is much easier for people to make pairwise comparisons. Drawing from utility theory and discrete choice models in economics, one can assume the user makes comparisons based on some intrinsic utility function and model the latent utility function using only the observed attributes and pairwise comparisons. +# In machine learning terms, we are concerned with [object ranking](https://en.wikipedia.org/wiki/Preference_learning) here. +# This [book](https://link.springer.com/book/10.1007/978-3-642-14125-6) has some more general discussions on this topic. +# +# In this tutorial, we illustrate how to implement a simple Bayesian Optimization (BO) closed loop in BoTorch when we only observe (noisy) pairwise comparisons of the latent function values. + +# ### Data generation +# +# Let's first generate some data that we are going to model. +# +# In this tutorial, the latent function we aim to fit is the weighted sum of the input vector, where for dimension $i$, the weight is $\sqrt{i}$. +# The input tensor X is randomly sampled within the d-dimensional unit cube. +# +# +# Specifically, +# $$ +# y = f(X) = \sum_{i=1}^{d} \sqrt{i} X_i ~~\text{where}~~X \in [0, 1]^d +# $$ +# +# This function is monotonically increasing in each individual dimension and has different weights for each input dimension, which are some properties that many real-world utility functions possess. +# +# We generate the data using following code: + +# In[1]: + + +import os +import warnings +from itertools import combinations + +import numpy as np +import torch + +# Suppress potential optimization warnings for cleaner notebook +warnings.filterwarnings("ignore") + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# In[2]: + + +# data generating helper functions +def utility(X): + """Given X, output corresponding utility (i.e., the latent function)""" + # y is weighted sum of X, with weight sqrt(i) imposed on dimension i + weighted_X = X * torch.sqrt(torch.arange(X.size(-1), dtype=torch.float) + 1) + y = torch.sum(weighted_X, dim=-1) + return y + + +def generate_data(n, dim=2): + """Generate data X and y""" + # X is randomly sampled from dim-dimentional unit cube + # we recommend using double as opposed to float tensor here for + # better numerical stability + X = torch.rand(n, dim, dtype=torch.float64) + y = utility(X) + return X, y + + +def generate_comparisons(y, n_comp, noise=0.1, replace=False): + """Create pairwise comparisons with noise""" + # generate all possible pairs of elements in y + all_pairs = np.array(list(combinations(range(y.shape[0]), 2))) + # randomly select n_comp pairs from all_pairs + comp_pairs = all_pairs[ + np.random.choice(range(len(all_pairs)), n_comp, replace=replace) + ] + # add gaussian noise to the latent y values + c0 = y[comp_pairs[:, 0]] + np.random.standard_normal(len(comp_pairs)) * noise + c1 = y[comp_pairs[:, 1]] + np.random.standard_normal(len(comp_pairs)) * noise + reverse_comp = (c0 < c1).numpy() + comp_pairs[reverse_comp, :] = np.flip(comp_pairs[reverse_comp, :], 1) + comp_pairs = torch.tensor(comp_pairs).long() + + return comp_pairs + + +torch.manual_seed(123) +n = 50 if not SMOKE_TEST else 5 +m = 100 if not SMOKE_TEST else 10 +dim = 4 +noise = 0.1 +train_X, train_y = generate_data(n, dim=dim) +train_comp = generate_comparisons(train_y, m, noise=noise) + + +# `train_X` is a `n x dim` tensor; +# +# `train_y` is a `n`-dimensional vector, representing the noise-free latent function value $y$; +# +# `train_comp` is a `m x 2` tensor, representing the noisy comparisons based on $\tilde{y} = y + N(0, \sigma^2)$, where `train_comp[k, :] = (i, j)` indicates $\tilde{y_i} > \tilde{y_j}$. +# +# If y is the utility function value for a set of `n` items for a specific user, $\tilde{y_i} > \tilde{y_j}$ indicates (with some noise) the user prefers item i over item j. +# + +# ### PairwiseGP model fitting +# +# In this problem setting, we never observe the actual function value. +# Therefore, instead of fitting the model using (`train_X`, `train_y`) pair, we will fit the model with (`train_X`, `train_comp`). +# +# `PairwiseGP` from BoTorch is designed to work with such pairwise comparison input. +# We use `PairwiseLaplaceMarginalLogLikelihood` as the marginal log likelihood that we aim to maximize for optimizing the hyperparameters. + +# In[3]: + + +from botorch.fit import fit_gpytorch_mll +from botorch.models.pairwise_gp import PairwiseGP, PairwiseLaplaceMarginalLogLikelihood +from botorch.models.transforms.input import Normalize + + +model = PairwiseGP( + train_X, + train_comp, + input_transform=Normalize(d=train_X.shape[-1]), +) +mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model) +mll = fit_gpytorch_mll(mll) + + +# Because the we never observe the latent function value, output values from the model are only meaningful on a relative scale. +# Hence, given a test pair (`test_X`, `test_y`), we can evaluate the model using Kendall-Tau rank correlation. + +# In[4]: + + +from scipy.stats import kendalltau + + +# Kendall-Tau rank correlation +def eval_kt_cor(model, test_X, test_y): + pred_y = model.posterior(test_X).mean.squeeze().detach().numpy() + return kendalltau(pred_y, test_y).correlation + + +n_kendall = 1000 if not SMOKE_TEST else 10 + +test_X, test_y = generate_data(n_kendall, dim=dim) +kt_correlation = eval_kt_cor(model, test_X, test_y) + +print(f"Test Kendall-Tau rank correlation: {kt_correlation:.4f}") + + +# ### Perform Bayesian Optimization loop with EUBO +# +# Now, we demonstrate how to implement a full Bayesian optimization with `AnalyticExpectedUtilityOfBestOption` (EUBO) acquisition function [4, 5]. +# +# The Bayesian optimization loop for a batch size of `q` simply iterates the following steps: +# 1. given a surrogate model, choose a batch of points $X_{next} = \{x_1, x_2, ..., x_q\}$ +# 2. observe `q_comp` randomly selected pairs of (noisy) comparisons between elements in $X_{next}$ +# 3. update the surrogate model with $X_{next}$ and the observed pairwise comparisons +# +# We start off by defining a few helper functions. + +# In[5]: + + +from botorch.acquisition.preference import AnalyticExpectedUtilityOfBestOption +from botorch.optim import optimize_acqf + + +def init_and_fit_model(X, comp): + """Model fitting helper function""" + model = PairwiseGP( + X, + comp, + input_transform=Normalize(d=X.shape[-1]), + ) + mll = PairwiseLaplaceMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + return mll, model + + +def make_new_data(X, next_X, comps, q_comp): + """Given X and next_X, + generate q_comp new comparisons between next_X + and return the concatenated X and comparisons + """ + # next_X is float by default; cast it to the dtype of X (i.e., double) + next_X = next_X.to(X) + next_y = utility(next_X) + next_comps = generate_comparisons(next_y, n_comp=q_comp, noise=noise) + comps = torch.cat([comps, next_comps + X.shape[-2]]) + X = torch.cat([X, next_X]) + return X, comps + + +# The Bayesian optimization loop is as follows (running the code may take a while). + +# In[6]: + + +algos = ["EUBO", "rand"] + +NUM_TRIALS = 3 if not SMOKE_TEST else 2 +NUM_BATCHES = 30 if not SMOKE_TEST else 2 + +dim = 4 +NUM_RESTARTS = 3 +RAW_SAMPLES = 512 if not SMOKE_TEST else 8 +q = 2 # number of points per query +q_comp = 1 # number of comparisons per query + +# initial evals +best_vals = {} # best observed values +for algo in algos: + best_vals[algo] = [] + +# average over multiple trials +for i in range(NUM_TRIALS): + torch.manual_seed(i) + np.random.seed(i) + data = {} + models = {} + + # Create initial data + init_X, init_y = generate_data(q, dim=dim) + comparisons = generate_comparisons(init_y, q_comp, noise=noise) + # X are within the unit cube + bounds = torch.stack([torch.zeros(dim), torch.ones(dim)]) + + for algo in algos: + best_vals[algo].append([]) + data[algo] = (init_X, comparisons) + _, models[algo] = init_and_fit_model(init_X, comparisons) + + best_next_y = utility(init_X).max().item() + best_vals[algo][-1].append(best_next_y) + + # we make additional NUM_BATCHES comparison queries after the initial observation + for j in range(1, NUM_BATCHES + 1): + for algo in algos: + model = models[algo] + if algo == "EUBO": + # create the acquisition function object + acq_func = AnalyticExpectedUtilityOfBestOption(pref_model=model) + # optimize and get new observation + next_X, acq_val = optimize_acqf( + acq_function=acq_func, + bounds=bounds, + q=q, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + else: + # randomly sample data + next_X, _ = generate_data(q, dim=dim) + + # update data + X, comps = data[algo] + X, comps = make_new_data(X, next_X, comps, q_comp) + data[algo] = (X, comps) + + # refit models + _, models[algo] = init_and_fit_model(X, comps) + + # record the best observed values so far + max_val = utility(X).max().item() + best_vals[algo][-1].append(max_val) + + +# ### Plot the results +# +# The plot below shows the best objective value observed at each step of the optimization for each of the acquisition functions. The error bars represent the 95% confidence intervals for the sample mean at that step in the optimization across the trial runs. + +# In[7]: + + +from matplotlib import pyplot as plt + + +get_ipython().run_line_magic('matplotlib', 'inline') + +plt.rcParams.update({"font.size": 14}) + +algo_labels = { + "rand": "Random Exploration", + "EUBO": "EUBO", +} + + +def ci(y): + return 1.96 * y.std(axis=0) / np.sqrt(y.shape[0]) + + +# the utility function is maximized at the full vector of 1 +optimal_val = utility(torch.tensor([[1] * dim])).item() +iters = list(range(NUM_BATCHES + 1)) + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +# plot the optimal value +ax.plot( + iters, + [optimal_val] * len(iters), + label="Optimal Function Value", + color="black", + linewidth=1.5, +) + +# plot the the best observed value from each algorithm +for algo in algos: + ys = np.vstack(best_vals[algo]) + ax.errorbar( + iters, ys.mean(axis=0), yerr=ci(ys), label=algo_labels[algo], linewidth=1.5 + ) + +ax.set( + xlabel=f"Number of queries (q = {q}, num_comparisons = {q_comp})", + ylabel="Best observed value", + title=f"{dim}-dim weighted vector sum", +) +ax.legend(loc="best") + + +# ### References +# +# [1] Wei Chu, and Zoubin Ghahramani. 2005. “Preference Learning with Gaussian Processes.” In Proceedings of the 22Nd International Conference on Machine Learning, 137–44. ICML ’05. New York, NY, USA: ACM. +# +# [2] Eric Brochu, Vlad M. Cora, and Nando de Freitas. 2010. “A Tutorial on Bayesian Optimization of Expensive Cost Functions, with Application to Active User Modeling and Hierarchical Reinforcement Learning.” arXiv [cs.LG]. arXiv. +# +# [3] Javier González, Zhenwen Dai, Andreas Damianou, and Neil D. Lawrence. 2017. “Preferential Bayesian Optimization.” In Proceedings of the 34th International Conference on Machine Learning, edited by Doina Precup and Yee Whye Teh, 70:1282–91. Proceedings of Machine Learning Research. International Convention Centre, Sydney, Australia: PMLR. +# +# [4] Zhiyuan Jerry Lin, Raul Astudillo, Peter I. Frazier, and Eytan Bakshy, Preference Exploration for Efficient Bayesian Optimization with Multiple Outcomes. AISTATS, 2022. https://arxiv.org/abs/2203.11382 +# +# [5] Raul Astudillo, Zhiyuan Jerry Lin, Eytan Bakshy, and Peter I. Frazier, qEUBO: A Decision-Theoretic Acquisition Function for Preferential Bayesian Optimization. AISTATS, 2023. +# diff --git a/website-old/static/files/risk_averse_bo_with_environmental_variables.ipynb b/website-old/static/files/risk_averse_bo_with_environmental_variables.ipynb new file mode 100644 index 0000000000..289810de00 --- /dev/null +++ b/website-old/static/files/risk_averse_bo_with_environmental_variables.ipynb @@ -0,0 +1,551 @@ +{ + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "480abf47-77be-4c72-819e-647600340428", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "# Risk averse Bayesian optimization with environmental variables\n", + "\n", + "This notebook considers risk averse Bayesian optimization of objectives $f(x, w)$, where $x$ denotes the design variable and $w$ denotes the environmental variable. \n", + "The design variable $x$ is fully controlled by the practitioner, however, the environmental variable $w$ is only controllable at the experimentation phase and is determined by the environment once the decision $x$ is implemented, according to some probability distribution.\n", + "In this setting, with the $W$ denoting the random environmental variable, the objective we want to optimize becomes a random function, written as $f(x, W)$, whose value is determined only once the environmental variable $W$ is realized.\n", + "This formulation is relevant whenever we need to make a decision to be implemented in an unknown future environment, and we can simulate the environment during the optimization phase.\n", + "\n", + "For this problem setting, [1] proposes to optimize a risk measure of the random function, written as $\\rho[f(x, W)]$, where $\\rho$ denotes a risk measure, which is a functional that maps a random variable (in this case $f(x, W)$ induced by $W$) to a real number. \n", + "They propose the $\\rho$KG acquisition function, which extends the well-known knowledge-gradient acquisition function, and requires access to posterior mean of the objective, i.e., $\\mathbb{E}_n[\\rho[f(x, W)]]$, where the expectation is taken over the sample paths of the GP model.\n", + "Unlike the posterior mean of the function $f(x, w)$, the posterior mean of the risk measure is not available in closed-form and needs to be estimated via sampling. \n", + "The procedure for estimating $\\mathbb{E}_n[\\rho[f(x, W)]]$ for a given $x$ is as follows:\n", + "- Draw a set of `n_w` samples of $W$ according to the probability distribution. Let's call this `w_set`.\n", + "- Append each $w$ in `w_set` to the given $x$ to get $(x, w)$ pairs. Note that for a single $x$, we now have `n_w` pairs of $(x, w)$.\n", + "- Draw samples from the joint posterior distribution of these `n_w` pairs of $(x, w)$. Note that the joint distribution here is an `n_w`-dimensional Gaussian distribution.\n", + "- Calculate the empirical risk measure corresponding to each sample, converting each `n_w`-dimensional posterior sample to a scalar sample of the risk measure.\n", + "- Take the average of these risk measure samples to get the Monte-Carlo estimate of the posterior mean of the risk measure.\n", + "\n", + "Now that the background is established, we are ready to implement a one-shot version of the $\\rho$KG acquisition function proposed in [1], in native BoTorch. We will:\n", + " - Use `AppendFeatures` input transform to add the set of $W$ samples to each given $x$;\n", + " - Calculate the joint posterior over these samples;\n", + " - Use `RiskMeasureMCObjective` to convert these joint samples into samples of the risk measure;\n", + " - And use the samples of the risk measure in `qMultiFidelityKnowledgeGradient` to define the $\\rho$KG acquisition function.\n", + "\n", + "We will use the (negated) Branin function as $f(x, w)$ with the first input dimension denoting $x$ and the second input dimension denoting $w$, and find the $x$ maximizing the CVaR risk measure at risk level $\\alpha=0.7$. We will assume that $W$ has a uniform distribution over $[0, 1]$ and approximate the risk measure using $16$ (qMC) samples of $W$ at a given time.\n", + "\n", + "CVaR, the Conditional Value-at-Risk, is a risk measure that measures the expectation of the worst outcomes (small rewards or large losses) with a total probability of $1 - \\alpha$. \n", + "It is commonly defined as the conditional expectation of the reward function, with the condition that the reward is smaller than the corresponding $1 - \\alpha$ quantile.\n", + "\n", + "Note: Risk measures are typically studied in the context of a minimization problem (including in [1]), since it makes more sense to minimize \"risk\", and treat the larger values as being undesirable. Since the default behavior in BoTorch is to maximize the objective, the `RiskMeasureMCObjective` (and its subclasses) is defined w.r.t. the lower tail of the random variable, i.e., by treating the smaller values as undesirable. With this implementation, all that is needed to minimize a risk measure (of the original objective) is to negate the objective, as is done in this notebook. \n", + "\n", + "[1] [S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of Risk Measures. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2007.05554)" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "originalKey": "9c1ca130-1b7d-45c2-a28a-6b4759277c0e", + "code_folding": [], + "hidden_ranges": [], + "requestMsgId": "9a845d78-cc55-4b30-8eb3-46c393cfe8e7", + "executionStartTime": 1648579007383, + "executionStopTime": 1648579009336 + }, + "source": [ + "import os\n", + "import warnings\n", + "from time import time\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import torch\n", + "from botorch import fit_gpytorch_mll\n", + "from botorch.acquisition import qMultiFidelityKnowledgeGradient, qSimpleRegret\n", + "from botorch.acquisition.risk_measures import CVaR\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.models.transforms import Standardize\n", + "from botorch.models.transforms.input import AppendFeatures\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "from botorch.utils.transforms import unnormalize\n", + "from botorch.test_functions import Branin\n", + "from gpytorch import ExactMarginalLogLikelihood\n", + "from torch import Tensor\n", + "\n", + "%matplotlib inline\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n", + "BATCH_SIZE = 2 if not SMOKE_TEST else 1\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 128 if not SMOKE_TEST else 4\n", + "N_W = 16 if not SMOKE_TEST else 2\n", + "NUM_ITERATIONS = 20 if not SMOKE_TEST else 2\n", + "NUM_FANTASIES = 16 if not SMOKE_TEST else 2\n", + "\n", + "tkwargs = {\"device\": \"cpu\", \"dtype\": torch.double}" + ], + "execution_count": 1, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "103a4fc5-8c7b-4e2f-a954-818a8486e976", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "## Problem setup\n", + "We will initialize the `Branin` test function and define a wrapper around it to normalize the domain to $[0, 1]^2$." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "d375377b-f596-48c3-869f-2bf7474d3e00", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "9d78eb42-21a4-4553-9be3-8741c08500d2", + "executionStartTime": 1648579009383, + "executionStopTime": 1648579009522 + }, + "source": [ + "test_function = Branin(negate=True)\n", + "dim = test_function.dim\n", + "\n", + "\n", + "def evaluate_function(X: Tensor) -> Tensor:\n", + " return test_function(unnormalize(X, test_function.bounds)).view(*X.shape[:-1], 1)" + ], + "execution_count": 2, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "3742d3d6-f77a-4013-9823-f797480c5d44", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Model initialization\n", + "We will initialize the `SingleTaskGP` model on $8$ Sobol points drawn from the $(x, w)$ space. \n", + "In doing so, we will also pass in the `AppendFeatures`. We will re-initialize `AppendFeatures` with a new `w_set` at every model training to ensure adequate coverage of the $W$ space." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "ba374159-5e99-4eb2-8cd2-25d34909236e", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "3454b737-0190-44ca-8908-b96e37c03cd7", + "executionStartTime": 1648579009594, + "executionStopTime": 1648579010111 + }, + "source": [ + "bounds = torch.stack([torch.zeros(dim), torch.ones(dim)]).to(**tkwargs)\n", + "train_X = draw_sobol_samples(bounds=bounds, n=8, q=1).squeeze(-2).to(**tkwargs)\n", + "train_Y = evaluate_function(train_X)\n", + "\n", + "\n", + "def train_model(train_X: Tensor, train_Y: Tensor) -> SingleTaskGP:\n", + " r\"\"\"Returns a `SingleTaskGP` model trained on the inputs\"\"\"\n", + " w_set = (\n", + " draw_sobol_samples(n=N_W, q=1, bounds=bounds[:, -1:]).squeeze(-2).to(**tkwargs)\n", + " )\n", + " model = SingleTaskGP(\n", + " train_X,\n", + " train_Y,\n", + " input_transform=AppendFeatures(feature_set=w_set),\n", + " outcome_transform=Standardize(m=1),\n", + " )\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + " return model\n", + "\n", + "\n", + "model = train_model(train_X, train_Y)" + ], + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "ffd3d6f1-32ed-496a-934b-e24868f41eae", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Define a helper function that performs the BO step\n", + "The helper function will initialize the `qMultiFidelityKnowledgeGradient` acquisition function with the risk measure objective, and optimize it to find the candidate to evaluate.\n", + "We use `qMultiFidelityKnowledgeGradient` instead of `qKnowledgeGraient` since it accepts a `project` callable, which we will use to ignore the $w$ present in the fantasy solutions before adding the `w_set` via the `AppendFeatures` input transform. " + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "ba856f61-ae91-4576-8f86-d9823a24621e", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "199fbf67-4eac-4d7b-9c06-f22c31474f30", + "executionStartTime": 1648579010160, + "executionStopTime": 1648579010307 + }, + "source": [ + "risk_measure = CVaR(alpha=0.7, n_w=N_W)\n", + "\n", + "\n", + "def ignore_w(X: Tensor) -> Tensor:\n", + " r\"\"\"Remove `w` from the input.\"\"\"\n", + " return X[..., :-1]\n", + "\n", + "\n", + "def optimize_rho_kg_and_get_observation():\n", + " r\"\"\"Optimizes the rhoKG acquisition function, and returns a new candidate and observation.\"\"\"\n", + " acqf = qMultiFidelityKnowledgeGradient(\n", + " model=model,\n", + " num_fantasies=NUM_FANTASIES,\n", + " objective=risk_measure,\n", + " project=ignore_w,\n", + " )\n", + "\n", + " candidate, _ = optimize_acqf(\n", + " acq_function=acqf,\n", + " bounds=bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + "\n", + " new_observations = evaluate_function(candidate)\n", + " return candidate, new_observations" + ], + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "47a684ed-2145-4068-a22e-641b265a01eb", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "## Perform the Bayesian optimization loop with $\\rho$KG\n", + "The BO loop iterates the following steps:\n", + "- Given the surrogate model, maximize the acquisition function to find the candidate(s) $(x, w)$ to evaluate;\n", + "- Observe $f(x, w)$ for each candidate;\n", + "- Update the surrogate model with the new observation.\n", + "\n", + "Note: Running this may take a while." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "99cc78f3-d6a7-405f-ad9d-fb244e7f3d25", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "d85f4fc2-1482-477a-b529-f21cc70292a9", + "executionStartTime": 1648579010377, + "executionStopTime": 1648579199976 + }, + "source": [ + "start_time = time()\n", + "\n", + "for i in range(NUM_ITERATIONS):\n", + " print(f\"Starting iteration {i}, total time: {time() - start_time:.3f} seconds.\")\n", + " # optimize the acquisition function and get the observations\n", + " candidate, observations = optimize_rho_kg_and_get_observation()\n", + "\n", + " # update the model with new observations\n", + " train_X = torch.cat([train_X, candidate], dim=0)\n", + " train_Y = torch.cat([train_Y, observations], dim=0)\n", + " model = train_model(train_X, train_Y)" + ], + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 0, total time: 0.000 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 1, total time: 7.142 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 2, total time: 18.505 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 3, total time: 29.673 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 4, total time: 50.260 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 5, total time: 61.387 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 6, total time: 82.665 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 7, total time: 105.007 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 8, total time: 115.363 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 9, total time: 124.725 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 10, total time: 131.432 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 11, total time: 139.990 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 12, total time: 147.682 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 13, total time: 150.100 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 14, total time: 167.178 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 15, total time: 171.254 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 16, total time: 173.408 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 17, total time: 176.923 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 18, total time: 180.522 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 19, total time: 183.080 seconds.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "a3c58456-524d-4cfd-8619-3dab53e7d8ac", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Find the solution to implement\n", + "We will choose the solution to implement as the point maximizing the posterior expectation of the risk measure. Since this expectation is not available in closed form, we will maximize its qMC estimate as a surrogate. We will use a larger `w_set` here to get a more precise estimate." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "f6d16156-0631-43f2-9766-9f7e3db9a142", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "fb2c0d5d-d81f-46b3-8d26-99ac0cc1f98b", + "executionStartTime": 1648579199994, + "executionStopTime": 1648579201028 + }, + "source": [ + "# update the input transform of the already trained model\n", + "w_set = draw_sobol_samples(n=128, q=1, bounds=bounds[:, -1:]).squeeze(-2).to(**tkwargs)\n", + "new_transform = AppendFeatures(feature_set=w_set).eval()\n", + "model.input_transform = new_transform\n", + "\n", + "risk_measure = CVaR(alpha=0.7, n_w=128)\n", + "expected_risk_measure = qSimpleRegret(model=model, objective=risk_measure)\n", + "\n", + "final_candidate, expected_objective = optimize_acqf(\n", + " acq_function=expected_risk_measure,\n", + " bounds=bounds[:, :1],\n", + " q=1,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + ")" + ], + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "649ac54a-bfe6-48f8-b33b-1dcad0609a26", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Let's plot the true risk measure and see how we did\n", + "We can use the input transform and the risk measure we previously defined to make this part easier!\n", + "\n", + "The plot shows that we found the global optimal solution and that our estimate of the risk measure at the optimal point is quite accurate. " + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "faf23624-8625-428b-99d4-fb0213679beb", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "cbcfb84b-f2a0-4fd2-a935-21d52cc0799a", + "executionStartTime": 1648579201051, + "executionStopTime": 1648579201691 + }, + "source": [ + "plot_x = torch.linspace(0, 1, 100, **tkwargs).view(-1, 1)\n", + "eval_X = new_transform(plot_x)\n", + "eval_Y = evaluate_function(eval_X)\n", + "plot_risk_measure = risk_measure(eval_Y)\n", + "\n", + "plt.figure(figsize=(12, 8))\n", + "plt.title(\"True Risk Measure Objective and Solution Found\")\n", + "plt.plot(plot_x, plot_risk_measure)\n", + "plt.scatter(final_candidate, expected_objective, marker=\"*\", color=\"red\", s=500)\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"$\\\\rho[f(x, w)]$\")\n", + "plt.show()" + ], + "execution_count": 7, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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66pOli1dmYb+oDxZGm9Z55DrxEx50nNSrFG36/NaEWcZtgI4Sm7El8m5yi/XxGcA9JRre/AcWavRWm2BQLp1bUlERHVJcq+DPRC9Yhr6lVV8hIiIUog17EREucfaQIq1NkREaY4BOxFR7rH2npAVj9iJiLIZe4khIsoR0puPXRpLBS2P8xEiIkoDDNiJiHLHQhnyPTii4m5Ln+lERJSmmBJDRJQ79kh3aTukF5QTTUPflOpCERFRfOwlpmopWUHbtmxE0+atUvHTVI+4nbMft3Fu4HbODdzOuSGF2zlilPEg1rCnKX+FP4G5KNNxOzrcM2cAACAASURBVGc/buPcwO2cG7idc0M6bmcG7EREREREaYwBOxERERFRGmPATkRERESUxhiwExERERGlMQbsRERERERpjAE7EREREVEaY8BORERERJTGGLATEREREaUxBuxERERERGmMATsRERERURpjwE5ERERElMYYsBMRERERpTEG7EREREREacyR6gIkg6JqawHsB6AibNKhpqGvkHlcAJ4AcA6ARgB+BnC7aegLUlNqIiIiIqKqZUXALoabhv5anOljARwHoA+AdQCuBjBHUbVDTUNfWY/lJCIiIiJKWDYF7DEpqtYUwMUAzjYN/Rd5e7SiahdJ4H5LiotIRERERBRVNgXs5yiqdgeANgBWAnjQNPTZMq2nLOvisM98A+DYFJSViIiIiCghaR+wK6rmANAg1nTT0HcAWAZgNYCrAOwGMArALEXVTjAN/UsALWT2f8I+vhVAq3i/v23LRvgr/ElZluooLy/D1o0b6v13qX5xO2c/buPcwO2cG7idc0OqtnOzVm1iTkv7gB1AbwBmrImKqrlMQx8Y9vZDiqoNkgD+yzjfbQMQiPfjTZvHjefrzNaNG+JuOMoOKdvOF1wATJ5c/7+bg3gs5wZu59zA7Zwb0nE7p33Abhr6PAmsq2sVgNbyeqP83xzAn5Z5WlimEeWG5csBwwBWrAA6d051aYiIiKgKGd8Pu6JqBymq9qKiak3CJh0suewAsASAT3qJsToewKJ6KipRehgzBti1q/J/IiIiSntpX8OegI0AzgTQSFG16yUwvxVAJwBDUFlLv1NRtVcAPKqo2i/SreMtANpJd49EuWPx4tD/iYiIKK1lfA27aeilAPpJw9SVAP6QvPfepqEvt8x6E4DZABYC2AKg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\n" + }, + "metadata": { + "bento_obj_id": "140404141818208", + "needs_background": "light" + } + } + ] + } + ] +} diff --git a/website-old/static/files/risk_averse_bo_with_environmental_variables.py b/website-old/static/files/risk_averse_bo_with_environmental_variables.py new file mode 100644 index 0000000000..ef80785c1e --- /dev/null +++ b/website-old/static/files/risk_averse_bo_with_environmental_variables.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# # Risk averse Bayesian optimization with environmental variables +# +# This notebook considers risk averse Bayesian optimization of objectives $f(x, w)$, where $x$ denotes the design variable and $w$ denotes the environmental variable. +# The design variable $x$ is fully controlled by the practitioner, however, the environmental variable $w$ is only controllable at the experimentation phase and is determined by the environment once the decision $x$ is implemented, according to some probability distribution. +# In this setting, with the $W$ denoting the random environmental variable, the objective we want to optimize becomes a random function, written as $f(x, W)$, whose value is determined only once the environmental variable $W$ is realized. +# This formulation is relevant whenever we need to make a decision to be implemented in an unknown future environment, and we can simulate the environment during the optimization phase. +# +# For this problem setting, [1] proposes to optimize a risk measure of the random function, written as $\rho[f(x, W)]$, where $\rho$ denotes a risk measure, which is a functional that maps a random variable (in this case $f(x, W)$ induced by $W$) to a real number. +# They propose the $\rho$KG acquisition function, which extends the well-known knowledge-gradient acquisition function, and requires access to posterior mean of the objective, i.e., $\mathbb{E}_n[\rho[f(x, W)]]$, where the expectation is taken over the sample paths of the GP model. +# Unlike the posterior mean of the function $f(x, w)$, the posterior mean of the risk measure is not available in closed-form and needs to be estimated via sampling. +# The procedure for estimating $\mathbb{E}_n[\rho[f(x, W)]]$ for a given $x$ is as follows: +# - Draw a set of `n_w` samples of $W$ according to the probability distribution. Let's call this `w_set`. +# - Append each $w$ in `w_set` to the given $x$ to get $(x, w)$ pairs. Note that for a single $x$, we now have `n_w` pairs of $(x, w)$. +# - Draw samples from the joint posterior distribution of these `n_w` pairs of $(x, w)$. Note that the joint distribution here is an `n_w`-dimensional Gaussian distribution. +# - Calculate the empirical risk measure corresponding to each sample, converting each `n_w`-dimensional posterior sample to a scalar sample of the risk measure. +# - Take the average of these risk measure samples to get the Monte-Carlo estimate of the posterior mean of the risk measure. +# +# Now that the background is established, we are ready to implement a one-shot version of the $\rho$KG acquisition function proposed in [1], in native BoTorch. We will: +# - Use `AppendFeatures` input transform to add the set of $W$ samples to each given $x$; +# - Calculate the joint posterior over these samples; +# - Use `RiskMeasureMCObjective` to convert these joint samples into samples of the risk measure; +# - And use the samples of the risk measure in `qMultiFidelityKnowledgeGradient` to define the $\rho$KG acquisition function. +# +# We will use the (negated) Branin function as $f(x, w)$ with the first input dimension denoting $x$ and the second input dimension denoting $w$, and find the $x$ maximizing the CVaR risk measure at risk level $\alpha=0.7$. We will assume that $W$ has a uniform distribution over $[0, 1]$ and approximate the risk measure using $16$ (qMC) samples of $W$ at a given time. +# +# CVaR, the Conditional Value-at-Risk, is a risk measure that measures the expectation of the worst outcomes (small rewards or large losses) with a total probability of $1 - \alpha$. +# It is commonly defined as the conditional expectation of the reward function, with the condition that the reward is smaller than the corresponding $1 - \alpha$ quantile. +# +# Note: Risk measures are typically studied in the context of a minimization problem (including in [1]), since it makes more sense to minimize "risk", and treat the larger values as being undesirable. Since the default behavior in BoTorch is to maximize the objective, the `RiskMeasureMCObjective` (and its subclasses) is defined w.r.t. the lower tail of the random variable, i.e., by treating the smaller values as undesirable. With this implementation, all that is needed to minimize a risk measure (of the original objective) is to negate the objective, as is done in this notebook. +# +# [1] [S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of Risk Measures. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2007.05554) + +# In[1]: + + +import os +import warnings +from time import time + +import matplotlib.pyplot as plt +import torch +from botorch import fit_gpytorch_mll +from botorch.acquisition import qMultiFidelityKnowledgeGradient, qSimpleRegret +from botorch.acquisition.risk_measures import CVaR +from botorch.models import SingleTaskGP +from botorch.models.transforms import Standardize +from botorch.models.transforms.input import AppendFeatures +from botorch.optim import optimize_acqf +from botorch.utils.sampling import draw_sobol_samples +from botorch.utils.transforms import unnormalize +from botorch.test_functions import Branin +from gpytorch import ExactMarginalLogLikelihood +from torch import Tensor + +get_ipython().run_line_magic('matplotlib', 'inline') + +warnings.filterwarnings("ignore") + +SMOKE_TEST = os.environ.get("SMOKE_TEST") +BATCH_SIZE = 2 if not SMOKE_TEST else 1 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 128 if not SMOKE_TEST else 4 +N_W = 16 if not SMOKE_TEST else 2 +NUM_ITERATIONS = 20 if not SMOKE_TEST else 2 +NUM_FANTASIES = 16 if not SMOKE_TEST else 2 + +tkwargs = {"device": "cpu", "dtype": torch.double} + + +# ## Problem setup +# We will initialize the `Branin` test function and define a wrapper around it to normalize the domain to $[0, 1]^2$. + +# In[2]: + + +test_function = Branin(negate=True) +dim = test_function.dim + + +def evaluate_function(X: Tensor) -> Tensor: + return test_function(unnormalize(X, test_function.bounds)).view(*X.shape[:-1], 1) + + +# ### Model initialization +# We will initialize the `SingleTaskGP` model on $8$ Sobol points drawn from the $(x, w)$ space. +# In doing so, we will also pass in the `AppendFeatures`. We will re-initialize `AppendFeatures` with a new `w_set` at every model training to ensure adequate coverage of the $W$ space. + +# In[3]: + + +bounds = torch.stack([torch.zeros(dim), torch.ones(dim)]).to(**tkwargs) +train_X = draw_sobol_samples(bounds=bounds, n=8, q=1).squeeze(-2).to(**tkwargs) +train_Y = evaluate_function(train_X) + + +def train_model(train_X: Tensor, train_Y: Tensor) -> SingleTaskGP: + r"""Returns a `SingleTaskGP` model trained on the inputs""" + w_set = ( + draw_sobol_samples(n=N_W, q=1, bounds=bounds[:, -1:]).squeeze(-2).to(**tkwargs) + ) + model = SingleTaskGP( + train_X, + train_Y, + input_transform=AppendFeatures(feature_set=w_set), + outcome_transform=Standardize(m=1), + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + return model + + +model = train_model(train_X, train_Y) + + +# ### Define a helper function that performs the BO step +# The helper function will initialize the `qMultiFidelityKnowledgeGradient` acquisition function with the risk measure objective, and optimize it to find the candidate to evaluate. +# We use `qMultiFidelityKnowledgeGradient` instead of `qKnowledgeGraient` since it accepts a `project` callable, which we will use to ignore the $w$ present in the fantasy solutions before adding the `w_set` via the `AppendFeatures` input transform. + +# In[4]: + + +risk_measure = CVaR(alpha=0.7, n_w=N_W) + + +def ignore_w(X: Tensor) -> Tensor: + r"""Remove `w` from the input.""" + return X[..., :-1] + + +def optimize_rho_kg_and_get_observation(): + r"""Optimizes the rhoKG acquisition function, and returns a new candidate and observation.""" + acqf = qMultiFidelityKnowledgeGradient( + model=model, + num_fantasies=NUM_FANTASIES, + objective=risk_measure, + project=ignore_w, + ) + + candidate, _ = optimize_acqf( + acq_function=acqf, + bounds=bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + + new_observations = evaluate_function(candidate) + return candidate, new_observations + + +# ## Perform the Bayesian optimization loop with $\rho$KG +# The BO loop iterates the following steps: +# - Given the surrogate model, maximize the acquisition function to find the candidate(s) $(x, w)$ to evaluate; +# - Observe $f(x, w)$ for each candidate; +# - Update the surrogate model with the new observation. +# +# Note: Running this may take a while. + +# In[5]: + + +start_time = time() + +for i in range(NUM_ITERATIONS): + print(f"Starting iteration {i}, total time: {time() - start_time:.3f} seconds.") + # optimize the acquisition function and get the observations + candidate, observations = optimize_rho_kg_and_get_observation() + + # update the model with new observations + train_X = torch.cat([train_X, candidate], dim=0) + train_Y = torch.cat([train_Y, observations], dim=0) + model = train_model(train_X, train_Y) + + +# ### Find the solution to implement +# We will choose the solution to implement as the point maximizing the posterior expectation of the risk measure. Since this expectation is not available in closed form, we will maximize its qMC estimate as a surrogate. We will use a larger `w_set` here to get a more precise estimate. + +# In[6]: + + +# update the input transform of the already trained model +w_set = draw_sobol_samples(n=128, q=1, bounds=bounds[:, -1:]).squeeze(-2).to(**tkwargs) +new_transform = AppendFeatures(feature_set=w_set).eval() +model.input_transform = new_transform + +risk_measure = CVaR(alpha=0.7, n_w=128) +expected_risk_measure = qSimpleRegret(model=model, objective=risk_measure) + +final_candidate, expected_objective = optimize_acqf( + acq_function=expected_risk_measure, + bounds=bounds[:, :1], + q=1, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, +) + + +# ### Let's plot the true risk measure and see how we did +# We can use the input transform and the risk measure we previously defined to make this part easier! +# +# The plot shows that we found the global optimal solution and that our estimate of the risk measure at the optimal point is quite accurate. + +# In[7]: + + +plot_x = torch.linspace(0, 1, 100, **tkwargs).view(-1, 1) +eval_X = new_transform(plot_x) +eval_Y = evaluate_function(eval_X) +plot_risk_measure = risk_measure(eval_Y) + +plt.figure(figsize=(12, 8)) +plt.title("True Risk Measure Objective and Solution Found") +plt.plot(plot_x, plot_risk_measure) +plt.scatter(final_candidate, expected_objective, marker="*", color="red", s=500) +plt.xlabel("x") +plt.ylabel("$\\rho[f(x, w)]$") +plt.show() + diff --git a/website-old/static/files/risk_averse_bo_with_input_perturbations.ipynb b/website-old/static/files/risk_averse_bo_with_input_perturbations.ipynb new file mode 100644 index 0000000000..6346adc3b2 --- /dev/null +++ b/website-old/static/files/risk_averse_bo_with_input_perturbations.ipynb @@ -0,0 +1,644 @@ +{ + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "11b753f3-27c5-4cb5-a259-a7a40912c7f3", + "showInput": false, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "# Risk averse Bayesian optimization with input perturbations\n", + "\n", + "This notebook considers risk averse Bayesian optimization of objectives $f(x + \\Delta_x)$, where $x$ denotes the design variable and $\\Delta_x$ denotes the perturbations to the inputs that are applied at the implementation phase, such as manufacturing errors. \n", + "The design variable $x$ is fully controlled by the practitioner, however, the input perturbation $\\Delta_x$ is only controllable at the experimentation phase and is determined by the environment once the decision $x$ is implemented, according to some probability distribution.\n", + "This means that while optimizing the design, we can simulate $f(x)$ for any given $x$, however, once the optimization is done, the actual implemented solution becomes $x + \\Delta_x$.\n", + "\n", + "In this setting, we want to find high-performing designs that are also robust to the effects of the input perturbations. \n", + "To do so, we will follow the Bayesian optimization of risk measures framework introduced in [1]. \n", + "Please refer to the [Risk averse Bayesian optimization with environmental variables](https://botorch.org/tutorials/risk_averse_bo_with_environmental_variables) notebook for additional background on this.\n", + "\n", + "In this notebook, we will use the `qNoisyExpectedImprovement` acquisition function to optimize the VaR risk measure at risk level $\\alpha=0.8$, computed w.r.t. the perturbations in the inputs. To do so, we will:\n", + " - Use `InputPerturbation` input transform to add a set of samples of $\\Delta_x$ to each given $x$;\n", + " - Calculate the joint posterior over these samples;\n", + " - Use the `RiskMeasureMCObjective` to convert these joint samples into samples of the risk measure;\n", + " - And use these risk measure samples to define the improvement in `qNoisyExpectedImprovement`.\n", + "\n", + "We will use the (negated) SixHumpCamel test function, and assume that the input perturbations follow a Gaussian distribution with standard deviation of 5% of the parameter space (truncated to the parameter bounds). \n", + "During optimization, we will use 16 (qMC) samples of $\\Delta_x$ to approximate the VaR risk measure.\n", + "\n", + "VaR, the Value-at-Risk, is a risk measure that measures the worst possible outcome (small rewards or large losses) after excluding the worst outcomes with a total probability of $1 - \\alpha$. \n", + "It is commonly used in finance for risk management, and corresponds to the $1 - \\alpha$ quantile of the random variable.\n", + "\n", + "Note: Risk measures are typically studied in the context of a minimization problem (including in [1]), since it makes more sense to minimize \"risk\", and treat the larger values as being undesirable. Since the default behavior in BoTorch is to maximize the objective, the `RiskMeasureMCObjective` (and its subclasses) is defined w.r.t. the lower tail of the random variable, i.e., by treating the smaller values as undesirable. With this implementation, all that is needed to minimize a risk measure (of the original objective) is to negate the objective, as is done in this notebook. \n", + "\n", + "[1] [S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of Risk Measures. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2007.05554)" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "originalKey": "ebf568b8-adcc-43be-a799-541163a1804f", + "code_folding": [], + "hidden_ranges": [], + "requestMsgId": "d691ea18-fc13-4dbf-b076-22de5df56f59", + "executionStartTime": 1648579041696, + "executionStopTime": 1648579043375 + }, + "source": [ + "import os\n", + "import warnings\n", + "from time import time\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "from botorch import fit_gpytorch_mll\n", + "from botorch.acquisition import qNoisyExpectedImprovement, qSimpleRegret\n", + "from botorch.acquisition.risk_measures import VaR\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.models.transforms import Standardize\n", + "from botorch.models.transforms.input import InputPerturbation\n", + "from botorch.sampling import SobolQMCNormalSampler\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.utils.sampling import draw_sobol_samples, draw_sobol_normal_samples\n", + "from botorch.utils.transforms import unnormalize\n", + "from botorch.test_functions import SixHumpCamel\n", + "from gpytorch import ExactMarginalLogLikelihood\n", + "from torch import Tensor\n", + "\n", + "%matplotlib inline\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n", + "BATCH_SIZE = 2 if not SMOKE_TEST else 1\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 128 if not SMOKE_TEST else 4\n", + "N_W = 16 if not SMOKE_TEST else 2\n", + "NUM_ITERATIONS = 25 if not SMOKE_TEST else 2\n", + "STD_DEV = 0.05\n", + "ALPHA = 0.8\n", + "\n", + "tkwargs = {\"device\": \"cpu\", \"dtype\": torch.double}" + ], + "execution_count": 1, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e7e62792-3280-40e9-ad9e-0bfb3c36e079", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "## Problem setup\n", + "We will initialize the `SixHumpCamel` test function and define a wrapper around it to normalize the domain to $[0, 1]^2$." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "e1103a91-8192-4ecc-a1a8-e79dd538750b", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "a0989add-2c98-48ea-8995-2d4cff84de75", + "executionStartTime": 1648579043418, + "executionStopTime": 1648579043545 + }, + "source": [ + "test_function = SixHumpCamel(negate=True)\n", + "dim = test_function.dim\n", + "\n", + "\n", + "def evaluate_function(X: Tensor) -> Tensor:\n", + " return test_function(unnormalize(X, test_function.bounds)).view(*X.shape[:-1], 1)" + ], + "execution_count": 2, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "0f810e1e-ca4b-4376-bd8f-ecd1a27af515", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Model initialization\n", + "We will initialize the `SingleTaskGP` model on $8$ Sobol points. \n", + "In doing so, we will also pass in the `InputPerturbation`. We will re-initialize `InputPerturbation` with a new set `perturbation_set` at every model training to ensure adequate coverage of the perturbation space." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "81b5fa3f-fa50-4e57-93fa-7353c9560c86", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "9707eaf5-0f67-4171-b871-c3ae291c2a4d", + "executionStartTime": 1648579043590, + "executionStopTime": 1648579043842 + }, + "source": [ + "bounds = torch.stack([torch.zeros(dim), torch.ones(dim)]).to(**tkwargs)\n", + "train_X = draw_sobol_samples(bounds=bounds, n=8, q=1).squeeze(-2).to(**tkwargs)\n", + "train_Y = evaluate_function(train_X)\n", + "\n", + "\n", + "def train_model(train_X: Tensor, train_Y: Tensor) -> SingleTaskGP:\n", + " r\"\"\"Returns a `SingleTaskGP` model trained on the inputs\"\"\"\n", + " intf = InputPerturbation(\n", + " perturbation_set=draw_sobol_normal_samples(d=dim, n=N_W, **tkwargs) * STD_DEV,\n", + " bounds=bounds,\n", + " )\n", + " model = SingleTaskGP(\n", + " train_X, train_Y, input_transform=intf, outcome_transform=Standardize(m=1)\n", + " )\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + " return model\n", + "\n", + "\n", + "model = train_model(train_X, train_Y)" + ], + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "a0c807ee-19f8-4c02-ae17-63f0d00a12b8", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Define a helper function that performs the BO step\n", + "The helper function will initialize the `qNoisyExpectedImprovement` acquisition function with the risk measure objective, and optimize it to find the candidate to evaluate." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "465569ac-b49e-4f9c-9f62-4a205b456697", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "df0c214b-e2a9-4fa8-b919-13e515800d9c", + "executionStartTime": 1648579043887, + "executionStopTime": 1648579044027 + }, + "source": [ + "risk_measure = VaR(alpha=ALPHA, n_w=N_W)\n", + "\n", + "\n", + "def optimize_acqf_and_get_observation():\n", + " r\"\"\"Optimizes the acquisition function, and returns a new candidate and observation.\"\"\"\n", + " acqf = qNoisyExpectedImprovement(\n", + " model=model,\n", + " X_baseline=train_X,\n", + " sampler=SobolQMCNormalSampler(sample_shape=torch.Size([128])),\n", + " objective=risk_measure,\n", + " prune_baseline=True,\n", + " )\n", + "\n", + " candidate, _ = optimize_acqf(\n", + " acq_function=acqf,\n", + " bounds=bounds,\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + "\n", + " new_observations = evaluate_function(candidate)\n", + " return candidate, new_observations" + ], + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "561849ea-4637-4128-9db0-599fe37c1234", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "## Perform the Bayesian optimization loop\n", + "The BO loop iterates the following steps:\n", + "- Given the surrogate model, maximize the acquisition function to find the candidate(s) to evaluate;\n", + "- Observe $f(x)$ for each candidate;\n", + "- Update the surrogate model with the new observation(s).\n", + "\n", + "Note: Running this may take a while." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "620f1dd9-d789-4448-b7f6-ce203bf8c61b", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "09cd5df2-50d1-4d73-b1a6-18d98975aeaf", + "executionStartTime": 1648579044081, + "executionStopTime": 1648579222644 + }, + "source": [ + "start_time = time()\n", + "\n", + "for i in range(NUM_ITERATIONS):\n", + " print(f\"Starting iteration {i}, total time: {time() - start_time:.3f} seconds.\")\n", + " # optimize the acquisition function and get the observations\n", + " candidate, observations = optimize_acqf_and_get_observation()\n", + "\n", + " # update the model with new observations\n", + " train_X = torch.cat([train_X, candidate], dim=0)\n", + " train_Y = torch.cat([train_Y, observations], dim=0)\n", + " model = train_model(train_X, train_Y)" + ], + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 0, total time: 0.000 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 1, total time: 2.604 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 2, total time: 6.995 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 3, total time: 9.517 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 4, total time: 12.045 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 5, total time: 14.884 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 6, total time: 17.721 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 7, total time: 21.850 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 8, total time: 33.036 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 9, total time: 40.206 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 10, total time: 46.402 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 11, total time: 63.250 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 12, total time: 71.446 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 13, total time: 79.062 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 14, total time: 87.870 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 15, total time: 99.159 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 16, total time: 106.404 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 17, total time: 115.135 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 18, total time: 124.253 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 19, total time: 137.365 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 20, total time: 144.159 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 21, total time: 147.352 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 22, total time: 152.641 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 23, total time: 161.821 seconds.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 24, total time: 165.349 seconds.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "3bcbd671-d272-4a89-8b25-f2af1220cfe1", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Find the solution to implement\n", + "We will choose the solution to implement as the previously evaluated point that maximizes the posterior expectation of the risk measure. Since this expectation is not available in closed form, we will use its qMC estimate as a surrogate. We will use a larger `perturbation_set` here to get a more precise estimate." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "71d05725-90a6-4c77-9881-09cede69e98f", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "f2aac029-9a23-41ca-ae94-406ad8fbd9e2", + "executionStartTime": 1648579222717, + "executionStopTime": 1648579223162 + }, + "source": [ + "# update the input transform of the already trained model\n", + "new_intf = InputPerturbation(\n", + " perturbation_set=draw_sobol_normal_samples(d=dim, n=128, **tkwargs) * STD_DEV,\n", + " bounds=bounds,\n", + ").eval()\n", + "model.input_transform = new_intf\n", + "\n", + "risk_measure = VaR(alpha=ALPHA, n_w=128)\n", + "expected_risk_measure = qSimpleRegret(model=model, objective=risk_measure)\n", + "\n", + "with torch.no_grad():\n", + " expected_rm_values = expected_risk_measure(train_X.unsqueeze(-2))\n", + "expected_final_rm, max_idx = expected_rm_values.max(dim=0)\n", + "final_candidate = train_X[max_idx]" + ], + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "9b547b03-1693-4aac-8259-3ec21af0f091", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Plotting the risk measure corresponding to the best observed point over iterations\n", + "As before, we define the best observed point as the previously evaluated point that maximizes the posterior expectation of the risk measure." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "8e81e7f5-2f0d-4afa-a083-41a7f9707a2d", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [], + "collapsed": false, + "requestMsgId": "7855cba5-5393-4a3d-bb16-e90cb5cdb4df", + "executionStartTime": 1648579223341, + "executionStopTime": 1648579223966 + }, + "source": [ + "best_observed = torch.zeros(NUM_ITERATIONS + 1, **tkwargs)\n", + "for i in range(NUM_ITERATIONS + 1):\n", + " best_observed[i] = expected_rm_values[: 6 + i * BATCH_SIZE].max()\n", + "\n", + "fig, ax = plt.subplots(figsize=(12, 8))\n", + "ax.plot(best_observed)\n", + "ax.set_xlabel(\"iterations\")\n", + "ax.set_ylabel(\"risk measure\")\n", + "ax.set_title(\"Best Observed Risk Measure\")\n", + "plt.show()" + ], + "execution_count": 7, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "bento_obj_id": "140106045051520", + "needs_background": "light" + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "f5662c6c-d4be-4e6c-8dda-3d00bfe0eb90", + "showInput": true, + "customInput": null, + "code_folding": [], + "hidden_ranges": [] + }, + "source": [ + "### Plotting the true risk measure to see how we did\n", + "We can use the input transform and the risk measure we previously defined to make this part easier!\n", + "\n", + "We plot both the response surface, $f(x)$, and the risk measure surface, $\\rho[f(x + \\Delta_x)]$, and mark the best risk averse solution found on both plots. \n", + "The plots are restricted to $[0.3, 0.7]^2$ to highlight more promising areas of the solution space." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "9b85d409-31c6-4de9-b108-c494bd4e4ba6", + "showInput": true, + "customInput": null, + "collapsed": false, + "code_folding": [], + "hidden_ranges": [], + "requestMsgId": "cae96c86-8c72-448d-8007-7a7965c0bcfe", + "executionStartTime": 1648579223976, + "executionStopTime": 1648579225433 + }, + "source": [ + "n_plot = 100\n", + "\n", + "fig, axes = plt.subplots(ncols=2, figsize=(24, 10))\n", + "\n", + "for i, ax in enumerate(axes):\n", + " # generate a grid of `x` points to evaluate for plotting\n", + " x_ = np.linspace(0.3, 0.7, n_plot)\n", + " x1, x2 = np.meshgrid(x_, x_)\n", + " eval_x_grid = torch.cat(\n", + " [torch.from_numpy(x1).unsqueeze(-1), torch.from_numpy(x2).unsqueeze(-1)], dim=-1\n", + " )\n", + " if i == 0:\n", + " plot_values = evaluate_function(eval_x_grid).view(n_plot, n_plot)\n", + " ax.set_title(\"Function $f(x)$ and Solution Found\")\n", + " else:\n", + " # add `delta_x` to each point, evalute the objective, and calculate the risk measure\n", + " eval_x_dx = new_intf(eval_x_grid)\n", + " eval_y = evaluate_function(eval_x_dx)\n", + " plot_values = risk_measure(eval_y).view(n_plot, n_plot)\n", + " ax.set_title(\"Objective $\\\\rho[f(x + \\Delta_x)]$ and Solution Found\")\n", + " contours = ax.contourf(x1, x2, plot_values, levels=40)\n", + " plt.colorbar(contours, ax=ax)\n", + " ax.scatter(final_candidate[0], final_candidate[1], marker=\"*\", color=\"red\", s=500)\n", + " ax.set_xlabel(\"$x_1$\")\n", + " ax.set_ylabel(\"$x_2$\")\n", + "plt.show()" + ], + "execution_count": 8, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "bento_obj_id": "140105842553136", + "needs_background": "light" + } + } + ] + } + ] +} diff --git a/website-old/static/files/risk_averse_bo_with_input_perturbations.py b/website-old/static/files/risk_averse_bo_with_input_perturbations.py new file mode 100644 index 0000000000..d31e116a6b --- /dev/null +++ b/website-old/static/files/risk_averse_bo_with_input_perturbations.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# # Risk averse Bayesian optimization with input perturbations +# +# This notebook considers risk averse Bayesian optimization of objectives $f(x + \Delta_x)$, where $x$ denotes the design variable and $\Delta_x$ denotes the perturbations to the inputs that are applied at the implementation phase, such as manufacturing errors. +# The design variable $x$ is fully controlled by the practitioner, however, the input perturbation $\Delta_x$ is only controllable at the experimentation phase and is determined by the environment once the decision $x$ is implemented, according to some probability distribution. +# This means that while optimizing the design, we can simulate $f(x)$ for any given $x$, however, once the optimization is done, the actual implemented solution becomes $x + \Delta_x$. +# +# In this setting, we want to find high-performing designs that are also robust to the effects of the input perturbations. +# To do so, we will follow the Bayesian optimization of risk measures framework introduced in [1]. +# Please refer to the [Risk averse Bayesian optimization with environmental variables](https://botorch.org/tutorials/risk_averse_bo_with_environmental_variables) notebook for additional background on this. +# +# In this notebook, we will use the `qNoisyExpectedImprovement` acquisition function to optimize the VaR risk measure at risk level $\alpha=0.8$, computed w.r.t. the perturbations in the inputs. To do so, we will: +# - Use `InputPerturbation` input transform to add a set of samples of $\Delta_x$ to each given $x$; +# - Calculate the joint posterior over these samples; +# - Use the `RiskMeasureMCObjective` to convert these joint samples into samples of the risk measure; +# - And use these risk measure samples to define the improvement in `qNoisyExpectedImprovement`. +# +# We will use the (negated) SixHumpCamel test function, and assume that the input perturbations follow a Gaussian distribution with standard deviation of 5% of the parameter space (truncated to the parameter bounds). +# During optimization, we will use 16 (qMC) samples of $\Delta_x$ to approximate the VaR risk measure. +# +# VaR, the Value-at-Risk, is a risk measure that measures the worst possible outcome (small rewards or large losses) after excluding the worst outcomes with a total probability of $1 - \alpha$. +# It is commonly used in finance for risk management, and corresponds to the $1 - \alpha$ quantile of the random variable. +# +# Note: Risk measures are typically studied in the context of a minimization problem (including in [1]), since it makes more sense to minimize "risk", and treat the larger values as being undesirable. Since the default behavior in BoTorch is to maximize the objective, the `RiskMeasureMCObjective` (and its subclasses) is defined w.r.t. the lower tail of the random variable, i.e., by treating the smaller values as undesirable. With this implementation, all that is needed to minimize a risk measure (of the original objective) is to negate the objective, as is done in this notebook. +# +# [1] [S. Cakmak, R. Astudillo, P. Frazier, and E. Zhou. Bayesian Optimization of Risk Measures. Advances in Neural Information Processing Systems 33, 2020.](https://arxiv.org/abs/2007.05554) + +# In[1]: + + +import os +import warnings +from time import time + +import matplotlib.pyplot as plt +import numpy as np +import torch +from botorch import fit_gpytorch_mll +from botorch.acquisition import qNoisyExpectedImprovement, qSimpleRegret +from botorch.acquisition.risk_measures import VaR +from botorch.models import SingleTaskGP +from botorch.models.transforms import Standardize +from botorch.models.transforms.input import InputPerturbation +from botorch.sampling import SobolQMCNormalSampler +from botorch.optim import optimize_acqf +from botorch.utils.sampling import draw_sobol_samples, draw_sobol_normal_samples +from botorch.utils.transforms import unnormalize +from botorch.test_functions import SixHumpCamel +from gpytorch import ExactMarginalLogLikelihood +from torch import Tensor + +get_ipython().run_line_magic('matplotlib', 'inline') + +warnings.filterwarnings("ignore") + +SMOKE_TEST = os.environ.get("SMOKE_TEST") +BATCH_SIZE = 2 if not SMOKE_TEST else 1 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 128 if not SMOKE_TEST else 4 +N_W = 16 if not SMOKE_TEST else 2 +NUM_ITERATIONS = 25 if not SMOKE_TEST else 2 +STD_DEV = 0.05 +ALPHA = 0.8 + +tkwargs = {"device": "cpu", "dtype": torch.double} + + +# ## Problem setup +# We will initialize the `SixHumpCamel` test function and define a wrapper around it to normalize the domain to $[0, 1]^2$. + +# In[2]: + + +test_function = SixHumpCamel(negate=True) +dim = test_function.dim + + +def evaluate_function(X: Tensor) -> Tensor: + return test_function(unnormalize(X, test_function.bounds)).view(*X.shape[:-1], 1) + + +# ### Model initialization +# We will initialize the `SingleTaskGP` model on $8$ Sobol points. +# In doing so, we will also pass in the `InputPerturbation`. We will re-initialize `InputPerturbation` with a new set `perturbation_set` at every model training to ensure adequate coverage of the perturbation space. + +# In[3]: + + +bounds = torch.stack([torch.zeros(dim), torch.ones(dim)]).to(**tkwargs) +train_X = draw_sobol_samples(bounds=bounds, n=8, q=1).squeeze(-2).to(**tkwargs) +train_Y = evaluate_function(train_X) + + +def train_model(train_X: Tensor, train_Y: Tensor) -> SingleTaskGP: + r"""Returns a `SingleTaskGP` model trained on the inputs""" + intf = InputPerturbation( + perturbation_set=draw_sobol_normal_samples(d=dim, n=N_W, **tkwargs) * STD_DEV, + bounds=bounds, + ) + model = SingleTaskGP( + train_X, train_Y, input_transform=intf, outcome_transform=Standardize(m=1) + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + return model + + +model = train_model(train_X, train_Y) + + +# ### Define a helper function that performs the BO step +# The helper function will initialize the `qNoisyExpectedImprovement` acquisition function with the risk measure objective, and optimize it to find the candidate to evaluate. + +# In[4]: + + +risk_measure = VaR(alpha=ALPHA, n_w=N_W) + + +def optimize_acqf_and_get_observation(): + r"""Optimizes the acquisition function, and returns a new candidate and observation.""" + acqf = qNoisyExpectedImprovement( + model=model, + X_baseline=train_X, + sampler=SobolQMCNormalSampler(sample_shape=torch.Size([128])), + objective=risk_measure, + prune_baseline=True, + ) + + candidate, _ = optimize_acqf( + acq_function=acqf, + bounds=bounds, + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + + new_observations = evaluate_function(candidate) + return candidate, new_observations + + +# ## Perform the Bayesian optimization loop +# The BO loop iterates the following steps: +# - Given the surrogate model, maximize the acquisition function to find the candidate(s) to evaluate; +# - Observe $f(x)$ for each candidate; +# - Update the surrogate model with the new observation(s). +# +# Note: Running this may take a while. + +# In[5]: + + +start_time = time() + +for i in range(NUM_ITERATIONS): + print(f"Starting iteration {i}, total time: {time() - start_time:.3f} seconds.") + # optimize the acquisition function and get the observations + candidate, observations = optimize_acqf_and_get_observation() + + # update the model with new observations + train_X = torch.cat([train_X, candidate], dim=0) + train_Y = torch.cat([train_Y, observations], dim=0) + model = train_model(train_X, train_Y) + + +# ### Find the solution to implement +# We will choose the solution to implement as the previously evaluated point that maximizes the posterior expectation of the risk measure. Since this expectation is not available in closed form, we will use its qMC estimate as a surrogate. We will use a larger `perturbation_set` here to get a more precise estimate. + +# In[6]: + + +# update the input transform of the already trained model +new_intf = InputPerturbation( + perturbation_set=draw_sobol_normal_samples(d=dim, n=128, **tkwargs) * STD_DEV, + bounds=bounds, +).eval() +model.input_transform = new_intf + +risk_measure = VaR(alpha=ALPHA, n_w=128) +expected_risk_measure = qSimpleRegret(model=model, objective=risk_measure) + +with torch.no_grad(): + expected_rm_values = expected_risk_measure(train_X.unsqueeze(-2)) +expected_final_rm, max_idx = expected_rm_values.max(dim=0) +final_candidate = train_X[max_idx] + + +# ### Plotting the risk measure corresponding to the best observed point over iterations +# As before, we define the best observed point as the previously evaluated point that maximizes the posterior expectation of the risk measure. + +# In[7]: + + +best_observed = torch.zeros(NUM_ITERATIONS + 1, **tkwargs) +for i in range(NUM_ITERATIONS + 1): + best_observed[i] = expected_rm_values[: 6 + i * BATCH_SIZE].max() + +fig, ax = plt.subplots(figsize=(12, 8)) +ax.plot(best_observed) +ax.set_xlabel("iterations") +ax.set_ylabel("risk measure") +ax.set_title("Best Observed Risk Measure") +plt.show() + + +# ### Plotting the true risk measure to see how we did +# We can use the input transform and the risk measure we previously defined to make this part easier! +# +# We plot both the response surface, $f(x)$, and the risk measure surface, $\rho[f(x + \Delta_x)]$, and mark the best risk averse solution found on both plots. +# The plots are restricted to $[0.3, 0.7]^2$ to highlight more promising areas of the solution space. + +# In[8]: + + +n_plot = 100 + +fig, axes = plt.subplots(ncols=2, figsize=(24, 10)) + +for i, ax in enumerate(axes): + # generate a grid of `x` points to evaluate for plotting + x_ = np.linspace(0.3, 0.7, n_plot) + x1, x2 = np.meshgrid(x_, x_) + eval_x_grid = torch.cat( + [torch.from_numpy(x1).unsqueeze(-1), torch.from_numpy(x2).unsqueeze(-1)], dim=-1 + ) + if i == 0: + plot_values = evaluate_function(eval_x_grid).view(n_plot, n_plot) + ax.set_title("Function $f(x)$ and Solution Found") + else: + # add `delta_x` to each point, evalute the objective, and calculate the risk measure + eval_x_dx = new_intf(eval_x_grid) + eval_y = evaluate_function(eval_x_dx) + plot_values = risk_measure(eval_y).view(n_plot, n_plot) + ax.set_title("Objective $\\rho[f(x + \Delta_x)]$ and Solution Found") + contours = ax.contourf(x1, x2, plot_values, levels=40) + plt.colorbar(contours, ax=ax) + ax.scatter(final_candidate[0], final_candidate[1], marker="*", color="red", s=500) + ax.set_xlabel("$x_1$") + ax.set_ylabel("$x_2$") +plt.show() + diff --git a/website-old/static/files/robust_multi_objective_bo.ipynb b/website-old/static/files/robust_multi_objective_bo.ipynb new file mode 100644 index 0000000000..ab7ab53ec5 --- /dev/null +++ b/website-old/static/files/robust_multi_objective_bo.ipynb @@ -0,0 +1,941 @@ +{ + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "df56a923-da1d-402b-9201-20e8fac34baf", + "showInput": false + }, + "source": [ + "## Robust Multi-Objective Bayesian Optimization Under Input Noise\n", + "\n", + "In this tutorial, we illustrate how to perform robust multi-objective Bayesian optimization (BO) under input noise.\n", + "\n", + "This is a simple tutorial; for support for constraints, batch sizes greater than 1, and many alternative methods, please see https://github.com/facebookresearch/robust_mobo.\n", + "\n", + "We consider the problem of optimizing (maximizing) a vector-valued objective function $\\mathbf f(\\mathbf x)$ where at implementation time $\\mathbf f(\\mathbf x)$ is subject to input noise $\\mathbf{f}(\\mathbf{x} \\diamond \\mathbf{\\xi})$ where $\\mathbf{\\xi} \\sim P(\\mathbf \\xi | \\mathbf x)$ is the random input noise and $\\diamond$ denotes the perturbation function (e.g. addition, multiplication, or any arbitrary function).\n", + "\n", + "We consider the scenario where:\n", + "1. We have access to a simulator during optimization such that $\\mathbf{f}$ can be queried at a given design $\\mathbf x$ without input noise.\n", + "2. Input noise is only present at implementation time. After optimization, the design that is chosen according to the decision maker's preferences will be subject to input noise.\n", + "3. The perturbation function is known.\n", + "4. We can sample from the generative process $P(\\mathbf \\xi | \\mathbf x)$.\n", + "\n", + "Quantifying risk is important to understand how the final selected design will perform under input noise.\n", + "\n", + "To quantify risk in the multi-objective setting, the MVaR set is an appealing option. For a given design $\\mathbf x$, MVaR is theis the set of points such that for every $\\mathbf z$ in the MVaR set, $\\mathbf z$ is Pareto dominated by the objectives under input noise $\\mathbf f (\\mathbf x \\diamond \\mathbf \\xi)$ with probability $\\alpha$. In other words, if $\\mathbf x$ is the chosen final design, the objectives will be better than $\\mathbf z$ with probability $\\alpha$ for all $\\mathbf z$ in the MVaR set.\n", + "\n", + "![MVaR](attachment:1d_toy_mvar_single_designs_combined.png \"MvaR\")\n", + "\n", + "However, during optimization we are interested in identifying the global MVaR set that is the optimal set of probabilistic lower bounds across all designs. The global MVaR set is the non-dominated set of points across the union of MVaR sets of all points in the design space. See [1] for a deeper discussion.\n", + "\n", + "In this tutorial, we will optimize the 2 1-dimensional functions shown above to identify an approximate global MVaR set. See [1] for a description of these functions.\n", + "\n", + "To do so, we will use Bayesian optimization with MARS (MVaR approximated via random scalarizations). MARS exploits the result in [1] that, under limited assumptions, there is a bijection between weights in the $M-1$-dimensional-simplex (where $M$ is the number of objectives) and points $\\mathbf z$ in the MVaR set based on the value-at-risk (VaR) of a Chebyshev scalarization. \n", + "\n", + "![bijection](attachment:bijection_plots.png \"bijection\")\n", + "\n", + "MARS leverages this result to efficiently identify the MVaR set using Bayesian optimization by, at each iteration, sampling a random Chebyshev scalarization and selecting the new design with maximum acquisition value with respect to the value-at-risk\n", + "of the sampled scalarization.\n", + "\n", + "[1] [S. Daulton, S. Cakmak, M. Balandat, M. A. Osborne, E. Zhou, and E. Bakshy. Robust Bayesian Optimziation Under Input Noise. ICML, 2022.](https://arxiv.org/abs/2202.07549)" + ], + "attachments": { + "1d_toy_mvar_single_designs_combined.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAEsAAAAXcCAYAAACCavxKAAABJ2lDQ1BrQ0dDb2xvclNwYWNlQWRvYmVSR0IxOTk4AAAokWNgYFJILCjIYRJgYMjNKykKcndSiIiMUmB/xsDOwMfAxWDGoJeYXFzgGBDgwwAEMBoVfLvGwAiiL+uCzMKUxwu4UlKLk4H0HyDOTi4oKmFgYMwAspXLSwpA7B4gWyQpG8xeAGIXAR0IZG8BsdMh7BNgNRD2HbCakCBnIPsDkM2XBGYzgeziS4ewBUBsqL0gIOiYkp+UqgDyvYahpaWFJol+IAhKUitKQLRzfkFlUWZ6RomCIzCkUhU885L1dBSMDIyMGBhA4Q5R/TkQHJ6MYmcQYgiAEJsjwcDgv5SBgeUPQsykl4FhgQ4DA/9UhJiaIQODgD4Dw745yaVFZVBjGJmMGRgI8QFubEp9WcQITwAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAASwKADAAQAAAABAAAF3AAAAACwWcdbAABAAElEQVR4AezdCXydVZ0//m/2tknT0oUutFLELrSlgAqUfRUYBaQjgsruMvxBcUYYcZ2fOvz0jywOwwijogwgo+jAKMgmhYERKPtiqZQWWlpK9z1p2uz55Qn2tre5SW7a7H0/vC455/uc5zznvO8l9JXe+0lOQ+MRDgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJdJJDbRfdxGwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJNAgKwvBAIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgS6VEAAVpdyuxkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIAALK8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBLhUQgNWl3G5GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAgAMtrgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoEsFBGB1KbebESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICMDyGiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhSAQFYXcrtZgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgKwvAYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgS6VEAAVpdyuxkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIAALK8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBLhUQgNWl3G5GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAgAMtrgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoEsFBGB1KbebESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICMDyGiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhSAQFYXcrtZgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgKwvAYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgS6VEAAVpdyuxkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIAALK8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBLhUQgNWl3G5GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAgAMtrgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoEsFBGB1KbebESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICMDyGiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhSAQFYXcrtZgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgKwvAYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgS6VEAAVpdyuxkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIAALK8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBLhUQgNWl3G5GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAgAMtrgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoEsFBGB1KbebESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICMDyGiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhSAQFYXcrtZgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgKwvAYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgS6VEAAVpdyuxkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIAALK8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBLhUQgNWl3G5GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAgAMtrgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoEsFBGB1KbebESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICMDyGiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhSAQFYXcrtZgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgKwvAYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgS6VEAAVpdyuxkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBvivQ0NAQa9eu7bsbtDMCBAgQIECAAAECBAgQIECAAAECBAgQ6DCB/A6byUQECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECOw2AgsWLIhXXnklXn311Zg9e3bMnz8/3n777Rg0aFCsWrWq2x2S9TzzzDPx5ptvxvr16yMnJyeGDh0aU6dOjaOOOipGjhzZ7Wu0AAIECBAgQIAAAQIECBAgQIAAAQIECOzOAjmNv1WhYXcGsHcCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNoW2LhxY9x7770xc+bMeOyxx2L58uVNFxUUFMQxxxwTJ510UlOw1AEHHBD9+/dve8JOGFFTUxN33HFHXH/99TF37twW75CbmxsnnnhifPvb325ac4sDnSBAgAABAgQIECBAgAABAgQIECBAgACBThMQgNVptCYmQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0PsF/vSnP8WNN94Y999/f1RVVaU2tN9++8WXvvSlOPvss2Po0KGpenc1nnzyybjgggvi7bffblpCv3794nOf+1zMmDEjxo4dG2VlZZGMuemmm2LBggWpZZ5//vnxb//2b1FaWpqqaRAgQIAAAQIECBAgQIAAAQIECBAgQIBA5wsIwOp8Y3cgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0OsEnn322bjyyiubQqO2X/yUKVPiBz/4QZx22mmRk5Oz/alua19zzTXxzW9+M+rq6prWkARePfjggzF16tRma0pCvL74xS/GL37xi9S5/fffPx5++OEYPXp0qqZBgAABAgQIECBAgAABAgQIECBAgAABAp0rIACrc33NToAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBXCZSXl8c3vvGN+Pd///eor69PrX3AgAFx1VVXxd///d9HXl5eqt7dje9///vx7W9/O7WMkpKSePHFF2PixImp2o6NhoaGOO+88+I///M/U6cmTJgQs2bNiqFDh6ZqGgQIECBAgAABAgQIECBAgAABAgQIECDQeQICsDrP1swECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEepXA3LlzY8aMGTFv3ry0dU+ZMiXuvvvumDRpUlq9uzs///nP4wtf+ELaMm6++ea45JJL0mqZOhUVFU37effdd1OnjzrqqHj88cd7VMBXanEaBAgQIECAAAECBAgQIECAAAECBAgQ6GMCuX1sP7ZDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMBOCPzhD3+IQw45pFn41SmnnBLPPfdcjwu/SsK6vvzlL6ftdMKECfF3f/d3abWWOsXFxXHVVVelnX7yySeb1dIG6BAgQIAAAQIECBAgQIAAAQIECBAgQIBAhwnkNDQeHTabiQgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6HUCv/rVr+KCCy6I2tratLWfeeaZkZwrKChIq3d3J/lIVBLW9eKLL6Yt5aabbopLL700rdZap6amJvbee+9Yvnx5alhhYWHMmTMnxo8fn6ppECBAgAABAgQIECBAgAABAgQIECBAgEDHC+R2/JRmJECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgtwjcdtttcd555zULv/roRz/aI8OvEte77767WfhVElz16U9/ul3sSbDXOeeck3ZNdXV1XHnllWk1HQIECBAgQIAAAQIECBAgQIAAAQIECBDoeIGcxt920NDx05qRAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGeLvDggw/Gxz/+8WbhV1OmTIlnnnkmBg4c2CO3MHny5Jg7d27a2k455ZR46KGH0mrZdJ577rmYPn162tCcnJx47bXXInFwECBAgAABAgQIECBAgAABAgQIECBAgEDnCOR2zrRmJUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgJwskAU9nnXVWs/Cr/v37x913391jw69mzZrVLPwqcT755JN3ivvggw+OIUOGpF3b0NAQN9xwQ1pNhwABAgQIECBAgAABAgQIECBAgAABAgQ6VkAAVsd6mo0AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAjxfYsmVLfOpTn4qKiopma/3+978fkyZNalbvKYU77rgj41KOPPLIjPW2irm5uXHEEUc0G/bb3/42EicHAQIECBAgQIAAAQIECBAgQIAAAQIECHSOgACsznE1KwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEeK/C1r30tXn/99Wbrmzx5clx22WXN6j2pcO+99zZbTlFRURxwwAHN6tkWDj300GZDy8rK4qGHHmpWVyBAgAABAgQIECBAgAABAgQIECBAgACBjhEQgNUxjmYhQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0CsEZs+eHTfffHPGtV511VWRn5+f8VxPKM6ZMydWrFjRbCkTJkyIgoKCZvVsC/vvv3/GoQ8++GDGuiIBAgQIECBAgAABAgQIECBAgAABAgQI7LqAAKxdNzQDAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgV4j8JWvfCXq6uqarTcJkTrjjDOa1XtS4X/+538yLmfSpEkZ69kW99tvv4xDH3vssYx1RQIECBAgQIAAAQIECBAgQIAAAQIECBDYdQEBWLtuaAYCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECvULgueeei5ZCpD7/+c9Hbm7P/rjRq6++mtF57733zljPtphcn5OT02z4okWLYtWqVc3qCgQIECBAgAABAgQIECBAgAABAgQIECCw6wI9+yfSu74/MxAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg8FeB6667LqNFEv70mc98JuO5pFheXh6bN29u8XxXnXjttdcy3mrs2LEZ69kWCwsLY+TIkRmHv/zyyxnrigQIECBAgAABAgQIECBAgAABAgQIECCwawICsHbNz9UECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEeoXA+vXr495778241gMPPDD22muvpnNJ0NXtt98e5513Xuyzzz7Rr1+/KC0tjeLi4hgwYEAccMABcckll8QjjzwS9fX1GefrrOK8efMyTr3nnntmrLenOGLEiIzD58+fn7GuSIAAAQIECBAgQIAAAQIECBAgQIAAAQK7JpC/a5e7mgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB3iDw+9//PmpqajIu9fjjj4+Kioq4+uqr4+abb45169ZlHLdly5aYPXt20+MnP/lJTJw4semaM844I+P4jiwm6ysvL8845dChQzPW21McPnx4xuELFizIWFckQIAAAQIECBAgQIAAAQIECBAgQIAAgV0TEIC1a36uJtDrBebMmRNb/yIm+Y0sY8aM6fV7sgECBAgQIECAAAECBAgQIECAAAECBDpeIHkT/OLFi1MTH3fccU2/4TlV0CBAgEAPENi0aVPa96qWPqTSA5ZqCQQIECBAgAABAgQIECBAgAABAgQIdLPA2rVro76+vmkVo0aNiiFDhnTzityeQNcI/OEPf2jxRkmw1bRp02LhwoUtjsl0Yt68eTFjxoy48MILIwnEKioqyjSsQ2rLly9vcZ7S0tIWz2V7YtCgQRmHrlixImO9M4vJ96i5c+embpGsrbCwMNXXIECAAAECBAgQIECAAAECBAgQIECAwFaBsrKyqKqqauomP0vsTfkxArC2Pou+EthNBaZPn970G1p20+3bNgECBAgQIECAAAECBAgQIECAAAECOynwj//4j3Httdfu5NUuI0CAQOcIXHfddfG9732vcyY3KwECBAgQIECAAAECBAgQIECAAAECfVbgzDPPjP/6r//qs/uzMQLbC8yaNWv7blr75ptvbuofccQRcdFFF0XyNfmQ1IABA2Ljxo1NYUwPP/xw3HLLLZEpEOq2225rqt93331RUFCQNndHddavX9/iVMXFxS2ey/ZESUlJxqGrV6/OWO/MYmI8derUzryFuQkQIECAAAECBAgQIECAAAECBAgQ6IMCyc91lyxZ0mt2JgCr1zxVFkqAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBg5wQWLlwYK1eubPHiYcOGxS9+8Ys4/fTTm43ZY4894vDDD296fO1rX4srrrgifvrTnzYblwRkfeUrX4kf//jHzc51RKGysrLFafr169fiuWxP9O/fP+PQ8vLyjPX2FIcMGRKtrX/Huerr63cs6RMgQIAAAQIECBAgQIAAAQIECBAgQKDPCQjA6nNPqQ0RIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQSBeYP39+emG7XmlpacycOTMOPPDA7aqZm8XFxfGTn/wkkmuuvfbaZoNuuumm+MQnPhHHHXdcs3O7Wqiqqmpxiry8vBbPZXsiPz/zR61au2+2c2/ZsqVdAVjZzmscAQIECBAgQIAAAQIECBAgQIAAAQIEerNA5p/K9uYdWTsBAu0SGDt2bLzxxhtN1xQUFMT111/frusNJkCAAAECBAgQIECAAAECBAgQIEBg9xB4/fXXm97EvnW3kydP3tr0lQABAj1GYNKkSWlreeSRR6KoqCit1ps7mysqo6a6Nqoqq6Kqqi7ycnMiNy+3N2/J2gkQIECAAAECBAgQaKdAXX1dbKwpj/rG68qrNzVdXZCbHzmN/zgIEOjbAhVllbFy8YbUJgsK82LsxOGpfkc05r34bix7c01qqr0mDI8JH9or1W9Po3JzTTxz719i4+rNUb5+S3su7ZaxM/7hiCgZ1K9b7u2m3SNQW18f37via1Gx6b3/n2YT+NM9K3VXAh0rsHDhwhYn/PrXv55V+NX2E1x99dUxa9asePrpp7cvN7WvvPLKeOGFF5rVd7VQV1fX4hSdGYBVU1PT4n276sQ111wThx56aFfdzn0IECBAgAABAruNwJIlS+Lcc89tdb8nnnhi/NM//VOrY5wkQIAAAQIECBAg0J0C3/nOd+KJJ55oWsKUKVO6cyntvrcArHaTuYBA3xIYOHBgakNJANZll12W6msQIECAAAECBAgQIECAAAECBAgQIEBgq8BDDz2UFoCV/DZnBwECBHqawPZ/75Gs7cgjj4z+/fv3tGXu9Ho2lW1uDL+qic0Vjb8hfktN9OtfGAOKfTBzp0FdSIAAAQIECBAgQKAXCtTU18aqzeuioaEh1mxZ37SD4QOG9MKdWDIBAu0VWDxvZTzyyrYQkf5DiuOE449r7zStju+3eU4UVCxKjZkyZVwcfvzUVL89jU0bt8SyF2pjRe36yKsqa7q0J4f1HX3E0bHHntveU9uevRrbOwUqtmyJ/IJtH6cYNGhQ79yIVRNop8CyZcsyXpEER1166aUZz7VWzM3Nbfol3NOnT2827MUXX2wKxzr88MObnduVQvK5h5aO+sZwu109WpqjsLBwV6eOW265JVoL8NrxBlsav1ddcsklqfL+++8fRx99dKqvQYAAAQIECBAg0DECSbDr9se4IflRWpQbC9fWxKbqhqZTzz//fFMYaV/6RWzb71mbAAECBAgQIECg9wsMH77tl+f0ts97bPsbm97/PNgBAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIZBCoqKjJUIw466KDY2SC4Qw89NA455JBIAgF2PH79619HRwdg9evX8i+GqK2t3XEJ7e63NEdHBB2ce+657VpPeXl5WgBWuy42mAABAgQIECBAIGuBe+65p2nstFGFcf3Hh8SkPd8LP62sbYjbXyiP///RDVFWVhYzZ86MU089Net5DSRAgAABAgQIECBAIDuB3OyGGUWAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQG8VaCkAa9y4cbu0pTPPPDPj9Y8++mjG+q4UWwvqqqqq2pWpm66trq7OOMeQIUMy1hUJECBAgAABAgR6t8A777wTL774Yuy9R3789oI9U+FXya765efExYeVxlV/s0fTJrcGZfXuHVs9AQIECBAgQIAAgZ4nIACr5z0nVkSAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgQwUKCgoyzrer4U7HHntsxnnnzZsXmzZtynhuZ4utrbWlgK/23Kul9Q4fPrw90xhLgAABAgQIECDQSwS2hlp956TBUVyY+WP35314YEwbVRj33Xdf1NbW9pKdWSYBAgQIECBAgACB3iOQ+U/ivWf9VkqAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEeJbB66YZ4+/UVqcf6VeU9an19dTENse2faGw7CBAgQCBdoLS0NL3w115OTk7GerbFadOmRV5eXrPhDQ0NsWjRomb1XSmMGDEi8vPzM05RXr7r/78tKyvLOPc+++yTsa5IgAABAgQIECDQuwWSAKypIwviIxMHtLqRK44dFOvWrYvHH3+81XFOEiBAgAAB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9773vfj6178eubl+BrG9jTYBAgQIECBAgACBREAAltcBAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQK9TmDTxi1RvmFLat39iwtj8LDmH0DcOuDF/5kX61aWb+22+HXzpqq0cxVlVfHIr19Mq7XU+fDxE2LIiNKWTvfoen5BXhT1L0ytMb/AW81TGBoECBDYDQSmTZsWTz/9dFMg1S9/+ct48MEHY9myZbF48eK4/fbb0wQKCwvj6KOPjrPOOqspYGrQoEFp57PtzJgxI2677bamIIAVK1Y0hQGccsop8dOf/nSng6imT58eScDVAw88EHfccUfMnDkzXnnllabH9utKggeSPX/yk5+Miy66KPbaa6/tT2sTIECAAAECBAgQyCiwoqwuY/2ECf1j2qjCmL28Ou38uHHj4te//nUkf051ECBAgAABAgQIECCQWcDfSmZ2USVAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoAcLzHtlSbz8+PzUCt8/ZXSccNYHU/0dG6ve3RBLF6zesdxmv6aqJha/saLNccmAKYeOy2pcTxx06Mn7RfLoqCO/MD9y83I7aroOmachGjLO8+hvXop+jQFqW4+ydZu3Npu+LvzLslizfENaLXMnJ0777GGZT6kSIECglwgkwVbJIznWrFkTb775ZmzcuDEqKyujuLi4KShq3333jaKiog7Z0QUXXBDnn39+JAFYAwcOjJKSlsMss71hTk5OnHrqqU2PhoaGWLhwYSxdujQ2bNjQtO4hQ4bEfvvt1yH3ynZNxhEgQIAAAQIECPQNgbr6zD9bSHZ3+TGD4sK7tv3s6eyzz24Kd93ZwNi+IWYXBAgQIECAAAECBNoWEIDVtpERBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJClQMmg/nHRt06Je295Op747z9neVX3DVu9bGMUFOa1uIDN5ZWRPNo6elrgV1vrdZ4AAQJtCQwbNiySR2cfSWDVqFGjOuU2ydxJYFfycBAgQIAAAQIECBDoTIETJvSPaaMKY0FZQdx4443x2c9+tjNvZ24CBAgQIECAAAECfUZAAFafeSpthAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBvigweFhJfPj4iamtFfYvSLU1ep/AupXlsWLx2tTCixsDw/aeOCLV1yBAgAABAgQIECBAgAABAgR6p8B3ThsbUy5/ICZO3PZznN65E6smQIAAAQIECBAg0HUCArC6ztqdCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDoJoGJB42N0e8f1ubd168si7dmL02N61dcFPsf/v5Uv7XGoKHFrZ3e6XPJvAcdM36nr3dhzxJY9vaaeOahv6QWNeYDwwVgpTQ0CBAgQKAlgTdWVke//JyWTndKfXlZXbvnraiqj2cWVbb7uo64YOHamnZPU1cf3bbeN9e0f73JBl9eWh2L19e2e6+7ekFlTcNOTfHGqpouf+1uXWh1bfvXnNh212t4w5bGF2Q7j1Wb6rptvb5HtPPJaudw3yPaCbYTw32P2Am0dlzSV79H1Lfxv5ZDRtbEniVV7ZAylAABAgQIECBAgAABAVheAwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI9HmBffcfndUeF81dkR6ANaAgDjxy36yuNahnC+RE5tCQQz4yKUoGD0gt/q0/vxvLF61N9ZPgtH333yvVb7HR0ManYFu80AkCBAgQINC2wA8e29j2oB4w4s01tXHWHat6wEqyW0JlY0BSb1pvsqtL7l6T3eZ6yKgfPLqhh6wku2Xc8eKmSB695Xj8rcpIHr3l8D2i858p3yM619j3iM717Q3fI15vDCVt6yh74oYYds6tbQ1zngABAgQIECBAgACBvwoIwPJSIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHdVmDfqaNjjz0Hpva/ZtmGtACsIXuWxqQPjk2d1yBAgAABAt0hMHbs2CgpKemOW7snAQIECBAgQKDXCdRuXBYN1RWtrrussj5WbapvdcyOJ59bXBW19Q2Rn5s5ZDsZXzn/saheOjsK95q24+X6BAgQIECAAAECBAhkEBCAlQFFiQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAj1F4Gc/+1mccsopPWU5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AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQawXu+e//jufeqWp67LiJk08+OX54++0xYsSIHU5duEO/9W7hmAOj5NALmgaVNP770qP/LkrvvDMuvfTSKC8vb3bx0BV/jB/9pDby832kvxmOAgECBAgQIECAAIG/CuSSIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECvVXg4Ycfbrb0goKCuPbaa+Ohhx7KEH7VbPhOFc4999x45ZVX4uCDD252/dq1a+OFF15oVlcgQIAAAQIECBAgQGCbgLjYbRZaBAgQIECAAIHdVqChoSH+8ycz0/Z/+qePjNLBA9JqOgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhpAv369Utb0gc+8IG466674kMf+lBavTM6++67bzz99NPxrW99K6677rpIPqu19dhxXVvrvhIgQIAAAQIECBAg8J6AACyvBAIECBAgQIBADxRY9s6auP3H237rQGFRQVxx1dmdttLkh6pz/7w4bf5T/vaQxr4ArDQUHQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgR4jMLDxl7uNGjc0tZ7Bw0tSbQ0CvVng+ZlvRF1dfZtbWLF4XdqYVUs3xjMPv55Wa6kz/aT9Iic3p6XT6gQIECBAgAABAgQIECBAoNcJXH311fHWW2/F/Pnz48ILL4wf/ehHUVLSdT8vKigoiGuuuSZOPPHE+OIXvxhLliyJK664Ig466KBeZ2nBBAgQIECAAAECBLpSQABWV2q7FwECBAgQ2M0FVi5bHy/Pmp9SGFBSFO97/4hUX2ObQPLmpU1lW1KFon51qXamRv/NG2PC3Kdi+Ym/ioYtm2N8WXUM29I4x8Chsej9H47yD0zNdJkaAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgV4rMOHAMZE8HAT6msCc596OuprW3zOWac/rV5ZF8sjmOOTESZEnACsbKmMIECBAgAABAgQIECBAoJcITJo0KebMmRMNDQ2Rk9N9oc8nnXRS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Yox+9dCVR2Hm0Ke0r18vh/GP9UBH3eQTAnXThwAChygQlpZ2iCMZhgACCCCAAAIIINBTBGzTpyvl6adVdPHFMgVZdN3SpSr6wQ+U+txz3iSpQcLpRgABBBBAwE/A8+lwL7/8srKzs72nX+dRqmzdulXLly/3/iJOVVWVIiMjlZqaqkmTJnlPzy/scCCAAAIIIIAAAggggEDXCNTV2LV7R1FIF6+ra/CLK8wvlTXc/4N6/AK+rURFRypjSEpbXbQhgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0G0FNm7c2GptKSkpeuqppzRv3rxWfUeyYbrx++WrVq3SNddco5deeslv6g0bNvjVqSCAAAIIIICAvwAJsPw9qCGAAAIIINBrBKrffTfoXuqmHB80hgAEEEAAAQQQQAABBBBAAIG2BWIuukhLF7+ukY/5/4CyreiqF16QdfhwJf32t21104YAAggggEArAbfb7U189Zvf/EaeX3556KGHjmoCrJqaGj322GP629/+Jk8CrPaONCPx9w+MxI633HKLEhMT2wujHQEEEEAAAQQQQAABBI6SwP7iA1rw708OafbcTzfIcwY70gen6Ps/Pi1YGP0IIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIdCuBadOmyfM7bnv37vWu6+STT9YzzzzjbQu20C2frFH+mp3thnk+zDLYER8frxdffFGnnnqqbrjhBnl+L89znHvuucGG0o8AAggggECfFiABVp9++tk8AggggEBvEnC53Kqvs/u2VPX2Ql+5rUJdZIwaRoxpq4s2BBBAAAEEEEAAAQQQQACBEAV2Xn6uovKKlPHfxUFHlP3ud7KOGKG4K64IGksAAggggEDfFfAkvnr99df161//WmvXru0UiOeee04333yziouLvdfz/BLO1VdfrZNOOkkJCQkqKCjQ22+/reeff977i0H33nuvHn30UT3yyCO69NJLO2WNXAQBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBQAKe333Lzc3VC8aHFw8ePFjnn3++zGZzoCG+vvrqOnnOI3FcddVV8iTfevnllzVq1CgSYB0JVOZAAAEEEOjVAiTA6tVPL5tDAAEEEOhLAlvX5+npR971btnSUK9LlyyRJQBAfsZYRYT4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tb7pLgltCus0EiAB7whgIf369esNDzIT1jAsHIDES5cuWdYSmYv+ixYtatgXTczIMNPPxOPHj7vUkCtXLilVqpRLeiATjCaEnB2EAtledKyradOm0q1bN7l+/bpD9zHxaiWApQlXvfzyyx5HbDpx4oRDG48fPw76NeDQIHdIgASiBQGMiYjyZWQcv12pwNEgWGY0fiOyGMfvYBH3vF5E3EKku1u3bjkcBGEqKwEsbfyGIOrzzz/vcKzZjvP4DaekYF8DZn2JrHQIgX777bequc6dO6t7pchqm+2QQHQjgO8ECOhCqDBVqlRB7T7a0du5c+cEv7X9dRLV18ltEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEjAjcPfuXZUV7GCXznMiaPfQoUOCYEU0EiABEiABkbUntkpYxH2/UCw9vF5KZC8q8WwiHDTPCfTo0cNUAGvu3LlqHSP8y9wZhJvGjBljL9alSxf7dlRupEmTRlq3bi1jx4417MaIESM8EsA6e/asEg3SKrHy59LKuHtHYDIjg/+/OwGsFClSuBwKvzPccyRNmtQlz9uE8+fPS79+/ZRgZ3h4uNy+fVsFWYZfx5EjRwwFxfRtoP8fffSREtZJZFsrGJts3759YiQuljFjxqBiqFy5sosAFgSIOnXqpNrF/wGuEQiXaeJXQe1QNKocomUQv9qxY4dDr+PFi6dE3ho2bOiQHmoiWHv37lXfcxDVg2AX+ov1JLt37xYI7+7Zs0f9D2snkTJlSiWY9vrrr6ty/q57SpAggWTIkEEuX76sNaHeITBIASwHJNwhARIgARIgARIgARIIAQIx/qnR999/Lw8fPnRBjUXjvXv3loULF8pvv/3mku9NwmuvvSaZM2e2H4I279/378GevTJukECIEYjvRtE8wiY6RyOBUCcQvmaNZRfj2dT1KX5liYiZJGBKAPdYZoaJloiICLPsgKc7CwfoG8DEUcGCBfVJQd0uUKCAiprg3AiiKBjdqzqX82X/ooFg5bVr13ypyqtjENXG2e7du+ecFKv3ManSpk0bFwaYvMBElpHh80Q0F1j79u2NihimOV8HWlREw8JMJAESiLUErMbvMzaRY33Un2BDshq/MambLxJFap977jnD00Xko2CZ8/c22uH4HSza3tWbLFkyMYoq/Ndff5lG54Nj1R9//KEa4vht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nKLJNS/T4mcJBiRiP//8s4ueISGUW2+9NUw4QUKwimzjZYpqkchkmR64n3322SYhEi3MjhZpIlLe5s2bvYaUFmnt2rWzQw45JGwsOlfZwQcf7CmGElbBI6FZs2auvkeW3XLLLQGBDq/8aGkSAX7++edNUXBSJWgXrX/yIACB1BHQd9I999zj2YEiwmlSOdkm55B169aFNSvn09yK9YU1GkdC6PW7YcOGYcKPcTQXc9FI12+JKum7OxFT1DYJgUqItH///lamTHYHM10vvK5F6ksOwc8991zc3aqvM88804mt6PqfmZkZsY2CeP2W0GibNm3Czskv5iZRmdatW4flx5Kgei1atPAsKiEpOaslYhLnevnll01R/u64445Emkiojr5T5FBXqlQp+/TTT50zWEINBVXSebz55psJCasFNcMuBAoFAQnnhgo0LlmyxC3OkBCfBLF0fW/SpIl7lfOlRPCeffZZU+CQnJzbKlWq5P52IzkAFgqInAQEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAJpT0DBm+67775s4/z999+duIMCAR133HEueLrmRCRQoTkSzb9rvloiAFr4Hs00j/7RRx+ZFtxjEIAABPZlAhOXTLVtWTuiIqhZtqq1qdU8YpnixTKta6POEfP9GaMXTLI9OXw/+8vy+j8CBx10UNSAkn7fJfnoLFq0yFWS35sCbBUUkw9EJF+zDz74wJYtW2YrV660UaNGBU4pkeCMgcpRduR/rgCe8pves2ePKymfdvktyQcrnU2BKyWYFsluu+22lPiDRuovXdJr1KjhORQvYSrPgrlIlK+Olw+uP2B2Lpp265u1lqZq1apO/FXBzhVUT/658h+Sr6PEY+WT6/8s56a/VNfVPbkEbeVHHGx+ESwF8Y7F8kL8avny5c63SuPp3bu3KRhxutnWrVvDhlShQoWo63LCKpAAAQhAAAIQgAAEIACBPCCQfnfTST7pCy+80C2umj17tvkf4iSji998Aj9H+YRSIi048wtvJaMv2oBAuhHI8ImIRLOsX3+Nlk0eBJJGYPa0xfbKk6MC2/hPwhcBr1+3yf5cu8mK/jTbMnbvitp36aOPjppPJgQgED8BCSSdd955YRW1wFYLbtesWROWl2iCIl94iSuVK1fOPVDWot38sDlz5jjhpuC+NXkRSeghuFxu988//3yLtEhZ4kV6f3Jy7AkegwSojvZ9V/7xxx/O0SfSpNjtt98eXC3bviIIKZJeTous/ZU0maSJgA8//NB69uxpmoyLZpGiUPya4P3Jzp07PbuTqEcy7eqrr47YnCadcmORxM4kYqZIiN99913Mzet980+GqdKNN97oJsNibiBFBQvCRFyKTp1mIZASAprwPuOMM8La1jVD1xYvsaqwwjEmfPHFF06AI7S4JlbloFK+fPnQrDw5njFjhot4FdyZRKAiOdYEl8vtvp5lRTpvRYnS+xPrdVRjmTJlios09+effzpngEjXhWjXbzkSDxgwIOZ+JRqvZ2NypJJ4vKJuRTOu3+F0+vXrF57oS5EDR/fu3U2f0VhNnxe9v35ncN0D1q5dO9bquSqn+3OJ8+hvZ+TIkabIzLkxnYvupeWgo/t8r++q3LRPXQgUVALPPPOMm/8oVqxYUk9Bjm/6rS2BawwCEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEI5DcBLepXsJzixYsndSjVq1c3+S8cfvjhSW2XxiAAAQgUNALbs3ba+MVTchx2z6ZHWpEiRaKW61y/nZUrkT1IX2iFdds22LTlc0KTOc6BQLQgimPGjDH5rsvPzG/yF0tHQRb/+EJf5dd01llnhSa7Y/kLKxDfkCFDbPfu3S6tSpUqzlfNs0IuEuUrfvLJJ7t7BH8zCsr9yiuv5Pj595fP79dHHnnEunTp4jkM8ZPvlQRF9yU78MADPU9348aNnunJSvzpp59MomNly5a1UP8eBUdVwPBETQHO27dv79Zo/PXXXxGbkVDTNddcY8cff7zJnzTdTX9vEgxT8Plgk5iT1kDkJByWF+JXGte7774bEBXr0KFD8FDTZt9L4K1WrVppMz4GAgEIQAACEIAABCAAAT+BQi+ApckI/4Kx/v3722OPPeY/94Re9cNHDyq04GPWrFmebbRo0cKOPPJIzzwSIVAYCGT6Fj5Fs90JCkxEa5M8CHgR2PDnZluyYGVgW71ivVcxl1Zz5cKIef6MUj5hQwwCEEg+ATmc6AFzqK1evdq6detmGzZsCM2K+3jevHlOXCFUzEliCorK1rJly7jb9KoQj9iE6qu8BCOCTZMGOYlABJfPzb6i0kmsKJJJIFYTGHPnzo1UxKVLaOHBBx9097gSLdMkqN7XUqVKedbT+y1hhkgmR6QjjjjCLaSOVEbvpd67du3a2SeffGIVK1a0QYMGRSoeSNcCbS9TFL9IJlHb0M+Ov6zXw37leUXB8NdJ5FUTiIq4Emp6D88999zQ5LiOVT/S38D69evd+yphipzOSZNjXbt2dZ8FDUBtSoQlHSzSJFy8f7PpcC6MAQLpQEDOSUOHDnV/86HjkbODvuOTMdkvR5vTTz89TFRJojb67m/WrFlo9wkdx/td4HX9luCf7lvywnT+0b5fFU1OY9H3cjTT9/q9994bEK+U04SeaUUS4ZRg1bHHHuvZpJjouZrE4KM5DcjBSCJHiqY7evRok3PRCy+84NlmcGIi128JQOX39VvRgnWPEmp6DyM5YYWWjXQsIbRIfwMSoZOQlD4Luk+LZnKc0fv6n//8xxVTJDc50uSF6X5R4qcy3R+uWrXKRV9+9dVX436VeKuc95o3b+7ur/VZ0z1OukdSzAvO9AEBPwEJFEv0MFliVWeeeaYLAKLosRgEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQSBcCffv2tZkzZ9qhhx6alCFpLlP+Y8lqLymDohEIQAAC+URg4pKpti1rR9Tea5atam1qNY9aRpnFi2Va10Y5B0obvWCS7fH5DGOxE5APVzSRlcsvv9y+/PJL16B8m+SHVNDM73PkNW75wL300kuBrAsuuCDp4phqXD74Et8JNvldRfKfDy6XLvsZGRk2fPhwq1GjhueQFBRbwakjBYz2rFTAE5s2bep5BmKRKhNf+bopePWLL77o/CuD+5IPoO5HE7GVK1c6/8DFixcHqkssNtrndMKECdarVy9LdkDwwACSuJOTCNbkyZM9e5Nv6cUXX+x8oUMLSBTwySefDE1O+FhCun6rVKmSfzetXrVmJNSaNGkSmsQxBCAAAQhAAAIQgAAE8p1ARr6PIA8GcM8999jHH39sEkbQQiktwHvggQfssMMOi7l33eS///77TiU8J4GARx99tMAoeccMgIIQCCKQEUFYwl9kr0/IZO+WLVbUJzqCQSBdCNRYOT/qUDIbNbKMOnWiliETAhBIjIBEFj788EP30H7EiBHZGpk9e7Ydc8wx7j4rUjSNbBU8DrTAV4INoUJalStXtlGjRtkhhxziUSuxpAULFphEA2I13Rd+9dVXgeI9e/bMFlEnkJHCHQluvffeexbp4b4mOCVkpOguEtPQpE758uWdGJKifHz33XdOiGrTpk2BUer+WkJI0UyCBxKvkmCWl33zzTdOREOCF+q7QYMGTsBg8+bNtmjRIsdNIgkyCW69/vrrpog+OZlEEVQ+VBDjnXfesfvuuy9MZEpsJF7x6aefep7TsmXLPLtMdrQb/Z3IOWvAgAHZ+tOic4lo5MY0ealIR4pQ6DVRpTS9p0888YT7W9LvJE0WqZ6iwOh3lCZm9Bnyi8hoTGKXLqITEkHxskjCWF5lSYMABLITKFmypHuWIhEfXceDTRPtEtTRc5JIwkXB5b329Z1y6qmnhglpVatWzV2/FY0qWabrt8TKYzWJPur+wm+6z5BoZF6aJtjFV5G9vOzzzz935yRnJl2Tdf3Wd7NEr3777bfA9VvXVb/pvI4++mj/oeerrhe6fktgycsmTZrkrtv169d3r7p/k6OE+lm4cKG7fkvkVKbr8ZtvvmkSp8/JIr0/w4YNMwlnhjoFvP3229anTx8TBzEItby6fuvcL7roIncNDR6D7i0kYpkby8zMdIJlEtn3iu4mpxgJpUnsX5/RTp06Wd26dd31W/fFen6p67fuufxWoUIFd/3W33eqTY5Tcqbzm4RNtSXTLrnkkmQ2R1sQKBQE5OCqwB26V1fE02hCwF4nrO9u/S6U6KFEizEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgkI4ENMeseX35fGnxusQpQv3Foo1bgcE01ywRi7wKhhVtPORBAAIQSAcC27N22vjF//hMRRpTz6ZHxrxmrnP9djZu0WTbtHNrpOZs3bYNNm35HOt0wMERyxTWDL9Prs4veD+W89X6SAXf9TL5fvtN/ju59WPyt5WXr61bt3Y+iuPHjw/rVr5qwX5xqfAhkv+Zl0DOSSedFDaedE+Q+JV8ueTzuXv37rDhTp8+3bTeYPDgwWF5hTFB6yYUTFQBGIPN7/cYnJas/TvuuMMUMFaCTPItlJiruAebApMmIsgqYS2tuajjWw8nf5+TTz7Z7attBbpVuwMHDrQxY8YEd+f8ChVUM1qw9WwV8vFAfsq635cv5a+//hoYiQKd9+jRw52b1kn4LS/Fr9Sn3k+/JSO4sL+tZL6uXbs2rDmtu8EgAAEIQAACEIAABCCQbgT2CQEsLer65JNP3EIw3axrkb82LbLXIkEt/pLph6sWYGqRt8qtWLHCpH4sYYaffvop7Iet15t55ZVXmqKAYBAozAQyfIIIOVmW74FCiTgWGOfUHvkQyA2BonuyrNqaJVGbKOWxaDpqBTIhAIG4CGgBvyZOJKighfrBEwY//PCDS5f4zvnnn+8W7cfSuB5YP/LIIyaRqdDJGD38f/fdd50IQCxthZaRqIOXXX311fbZZ58F7h+9yihND801Lk0i+E2TjG+88YabMPGn5cWrJmjEQg/1ly5d6tmlJk0l1BUs1uVZ0JfYr1+/mCY6JFYlkYPjjjvO9F5FMk1CiEs00wTiiSeeGK1IIE8CGXr/Qxd6SwhCE3cPP/ywEwmRgJVEuiSsochGkQS9IjEJnhwOdJ7LnSuuuMJ9boKdsS677LJctvq/6hKSUbQjiXREmiTXhMvQoUM9I50ED0ICaRKXa9y4cXByvu1L7OW1117z7F8T3+KphfwYBCAQPwGJ840cOdIeeughJ5QX/P0kEay2bds6YUdNyOt6E4tJUFHtPf7449nuB1RX1yrdL8QieOjVV6Trt57V6LmQvr+imc5PY5Pgkt/kHPDqq6/m+feI7p0kXiImErTyMn2fKxKXtpzs5ptvtttvvz2nYk4AUYJnui4qqlgk0z1FpPsKf53nnnsuZkdhiWTpWhUqaCghLl2/Jd6laFNiIUcbCVvqGimHZC/Ly+u3Pl+6Vwm+vibr+i1RKzmfRGtP9zj6jGqLZormqKAAiYrORms7NE/3CbqvD/7OCC2T22N9/2jDIACBcAK699XvT236rvaL4UkQUg6acgLdvn27i36quRGJ5x100EHumiOnsFq1aoU3SgoEIACBfYTA5x/NsBW//SMG2/GIptb84Hr7yNlzmhCAAAQgAAEIQAACEIAABCAAAQhAAAIQKHgE5FOmTes+xo0b5xbyz58/382RyD9B896af5e/gEQBJJyleVit90g04FfBo8SIIQABCMRGYOKSqbYta0fUwjXLVrU2tWIXzCheLNO6Nups7/00Nmq7oxdMso51W1mxfczXMzgoXrBvfVRY/5952mmnuXWRv/zyS8TiugZee+21EfPTPeOmm24yLwGs4HEruJeCRybbXnnllbCgheKp+4mCaApWrXUXEk7zMvnUK+haND81r3oFMU2CcK1atXIB5oLHH8lPM7hMIvvy25F/ocSGnn32WdeE3g/50AabhKoivT/B5YL3FWRVwlBao6N+/Guk/WV0D+y/X5Z/ocTign36FHzzxhtvLBAieX4RLPmMeolgjR071t3n6/wkNKY1EaGmwLRewnah5eI51jqGYHGpn3/+OZ7qeVJW15fgMfo7PeSQQ/y7vEIAAhCAAAQgAAEIQCBtCOwzq4C1uEuL37SYw296yPPCCy+4H/BK08J8PfiQGnffvn3dgsfXX3/dCWDF8iDp9NNPt2eeecbfPK8QKLQEMn3K2TnZ7iBF7ZzKkg+BVBOo6hO/yvCJYEWzUkcfHS2bPAhAIAkEFDVNEdOmTp1qHTt2zNaiBEj1oLlRo0Y2YMAAF9HC6/5Li3R1T3fDDTc4gQaJGQWLX5UtW9b0IF4TAMH3fdk6i+FAQkSKfhFqihrXrFkz++9//+sWDofmy2FGEwlykvGLX0k8ROUl1FC8ePHQKnlyXLNmTScAm5sJvoyMDCce9dRTT8UcuUkPxeVYVLVq1YTOs3Tp0k7M4brrrourfqTJ2h9//NHd6+tzdrTve1/iV927d7dBgwZ5ti+RCIl4eZmEcyUslkzTxIwWnPutTZs2lsyJhQsuuMCJhui9TNT0u0oTZcFRUhJtKxn1NNnYq1cvW7lypWdzcmjTBHiwk4BnQRIhAIGIBHT9vvPOO13kVE2SB9v69eutT58+ThBPkaAkihU8Oe4vq+ujvjv0fa7vOolEBl/ny5Ur54S0dI1PVPxKfen730uUXPcFch6Q2GbwxLd/fJqAHjFihLs/8YtfSUxdE91vvvmmc4b1l83LVznLSMBd161ETU43ug/RFqvpO17RviQqmYjJQUTilhKGisciXb8llionDHGQGJbuqfTsTsJQXvbxxx87wVKvPImFffDBB15ZCac1bNjQjj/++EB9Xbt1DU+WXXrppe75Zawic179aoz6+0rmfYVXP0pTP3pGmuprr7hgEIBAzgQkDqm/l9d8grH6PauIj5oH0XVYv691PG3aNBsyZIibE0H8KmemlIAABAo3gfXrNtnq3/8MbFs2bS/cJ8zZQQACEIAABCAAAQhAAAIQgAAEIAABCECgkBCQr4ECA0q8QX5dEsRSUBDNiezYscPWrFnjfBq0NkRz2YhfFZI3ntOAAASSRmB71k4bv3hKju31bHpkzD7E/sY6129n5UqU8R96vq7btsGmLZ/jmVeYE4MDFAbvx3LO8iWScE00O/PMMxMKghXsmx+t/VTnycdbfn/RLFU+RPLbCzX54smfMhZTUOLcmpcvZm7aVBBNCadFMvnvaZ3HvmBaRxBqS5YsCU3K9bF8bBWoW/6oCg6rtREyrV8O/SzpHjY4CGdw50r3+jzIL1QiVwp6Gip+FVxf+7pXli9wsEkwNieRueDy+b1fr14954u8//77ZxuK7vv1ffHNN9/kqfiVBqH3ONj0XqSbKXB86Pe6Pn9a84RBAAIQgAAEIAABCEAg3QjsMwJYAq8F/zNnznSL5pL5RuiGX2IO+iGam8XkyRwTbUEglQSK1ahhPgWPqF3s9okRYBBIFwI1Vy7McSilfCrwGAQgkDcEJJ6hhbcSmlCkkGBbtmyZE46SQNZ+++3nRK7atm3rIq9Vr17dpSlqg0SYNmzYEKiqB/e33367LVq0yInd5PaeTKJVEj6SwFHoGFevXm233nqraSFx5cqVXQQQldFxxYoV3YJ/LSAu6otC1Lt3b5PokiaMQicpAoPPox098Ne4JO4Vr4CCxBKmT58eEPWKZ8h6MC7hCglVxGOdO3d29TTpE69pwvbUU0/NsdpVV11lEsnwC5PNmzfPXnzxRfcZat++vRNRCRZoCW1Q/ejzqWgrL730kls8Hlom3mMJyfhNQm/JNgn9anJHkWviMf1NKOrJnDlz7OCDD46nalLL6rMkEWP9Deq7QH93OU286fuiQYMG7rOv/bfeess+//zzpI6LxiCwLxDQtVnXEYkP6bsv2DTxr+uwvjslfiRHUf/1u1q1aiaBSjkMKHqVRC/9puvmv//9b1u8eLH169cv7uuTvx3/q5wEPv30U3v77bddRCt/ul4llCdBPF0Pq1SpYq1bt3bXeB1rHGeccYZzdtX1W/tz585133vBbeTHvr6/dA1W5C2NLR477LDD3DnpPiRek4OFvnO9BMWitaXv5tmzZ9t5550XrZhnnupI6Conk5DayJEjA8/gdK+la4McuvS5kzCil7OH2pUjiJx49FnVtWTw4MHO0TmnPnPKP//88wNFUnH91v2bhNwUjTge09+E3n9dv1u2bBlP1YTKSgRP93xyIE+lyRHonHPOSWUXtA0BCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAT2GQITl0y1bVnR/T1qlq1qbWpFFyPyAla8WKZ1bdTZKytb2ugFk2zP3r3Z0gr7wZ9//hk4xVARlUBGlB0J2sgXLpLlJJAVqZ4CannZrl27vJJTlibf+2jnIMEfBepLhS1dujSsWYmUBa9fCCsQlCD/Qy+LJ6jgzp07vZpwaYm+F6+++mrEgJxqU2sf1q5dG7HfdMyI5usfabw9e/YMy9I6gmTbZZdd5oRZn376aTvooIMCzctnNdSfT5+tn376KVAmeEcB4kODu//8889OsOz+++83BVuNxbTmQX0H26xZs4IP035fPr8KyBsqgiUxry5dutjQoUPDzkFrIBQQNxUW6quqtesS5U0n0xqrUNP6p2jXj9DyHEMAAhCAAAQgAAEIQCCvCGTkVUfp0o9uzD/55BO3UO6uu+6y3P44lYCDFlFrgT4GgX2FQBHfotsM38OR3VHUzbN+/XVfwcF5FgACNVcuiDrKTJ9AYkbNmlHLkAkBCCSXgAQcNOGkTSIJEpoaO3as2/dHF9DC+eXLl7vNq/eavr9bPaRWG5qEKFWqlFexXKX961//Mm0LFy509496WK7IJn7xDk02Bk84SnhLAiESizj33HOtYcOGueo/p8rximpJgMQvEDFo0CAn/uT1QFv9SnDjyCOPdIIbuY3uUKtWLdeXhM8kFPXZZ595To5VrVrVjj32WBdlT+9toiYuEj+57777bODAgaYJDb/pPVKEDwm1HH744f5k9yoBDX0OZXXr1nWbO8jhPwm3aGvTpo3pc5kbkyiHhGMkynX22WfnpqmIdfUZlaiJRN4U1VBRirwiDUn0SgIh+vuS8Iren/w2CWuMGzfODUMTcKGTedHGp0iO2mR6f48//vhoxcmDAAQ8COj6LfE/bZr09l+/Ja7jdyLQ9fs3nyCyNi/TNUHXF12/db2UQE8yTdeAs846y23z58931++vvvrKXb/933VyHAp2HlJktkMPPTRw/T7wwAOTOaSwtuK9fkvoU0JNEvDSdVTPtXTd8TLdexzlE6FSZDt93+fG9F05atQoJ5yofkePHm1//PFHWJMSKdX1W8KSodfWsMJREvT5kkDq3Xff7QQpFZXLb3qPevTo4cQ49V4Fmz5//muDrqGxRurVPZA2Xet0Drkxiabp3kLvle4dU2G6H9O5fvDBB/bGG2/YhAkTst3j+PvU9VvOCRIT0/Vbgq15Zfqb02ch1abPQLly5VLdDe1DAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIACBQk9ge9ZOG794So7n2bPpkQkHA+5cv52NWzTZNu3cGrGfdds22LTlc6zTAfkXpDXi4FKUEexjJ5/5eE2+89dcc43de++9YVUVsFJ+zfGa/Pj9fvKhdSWMJT/BeAMxh7YTz7GCKirIpt//N7iu8lKxfkB9hIra+PuVn5r8J6OZ/NYlNOVl8r9r3LixV1ZYWqT3QQW3bo38txTWUFCCfK7ef/995y8pQa9QW7FihQsg+sUXXwQCVIaWyctjBbzMybzOI6c6ChCqNcbr1q0LFNU6AvUXr39noIGQnVdeecWx1udF/pyhprUS8gcMNgXJDBXG0toV/Q0oAHSw6bMo4SsF14zVFKBe/vPy/fWbl0+oPy9dXxVAW/6T8pUN/u70+rtNpfiV+IQKiilN78nq1atdkHX5c+a3eQmrxbMGI7/HT/8QgAAEIAABCEAAAvsWgX1OAMv/9mph+amnnuoWyGnR2JgxY7ItfvSX83rVwjEt0rzgggvyZFGX1xhIg0B+E8j0LSqNJoC1GwGs/H6L6P//CRTdvcuqrlkSlUcp30MvDAIQyD8CrVu3Nm0PPfSQm4xRxJMlPpHF33//3bZs2WKalJBoUZkyZdxEgx5YN2nSJGZxg2ScmSaaJGqgTabJDk06SphB0VU0eVatWjXT2CRclFcmMYhErFGjRqZIINoULeSXX35xAkiakKxUqZITcUqF2JGEG/xiWnp/f/XdL+j91WSa+CkiR7ImjTRZ8PDDD9s999xj+kxJqEyRfpo3b26lS5f2xOYXdfHMzKNEfdbfeecdNzms/VSZOJ9yyilu02SPPs96T/R+SChNnwO9H+kw6RLMoG/fvqYNgwAE8p+AnGO0PfLII+76rQlSRT3TZLKcK/R9ouuUvnPlKKBrZFOf8Gxo1KVUnon6u+OOO9ymfjRRr/EFX78lfKTvu7y6fkvkSVsipvN54okn3Kbrmu6XJOrlv35LXCwVEZkkaqVNzh26Vuia4b9++/klcj5edSSI9uijj5qikun6rfsUXb9btGgR0VlJoqPa8tP0+Rk+fLgbY6LvbyzjV9uKsqdNzmR6L+R05L9+65mlBMDy6/qdDu9FLBwpAwEIZCega8pRvmdjF154oSk6rJdjVvYaHEEAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIACBgkdAAb0OO+wwF7BLwYTkr4ZBAAIQgIDZxCVTbVvWjqgoapatam1qNY9aJlpm8WKZ1rVRZ3vvp/8FqY1UdvSCSdaxbisrlqB/VaR20zE9KyvL+Uf5xxYqhONPz+lVAljyt5L/ULDdeOONwYcx7y9btsz8waxDK8nfWL7fqQ4wGdyv/KCuvvpqF1QxOF37XqJCoWUSPdZ9gpfolsTGFFhYQkJeJh/FXr16OU5e+TNnzow5yKRYRzL5jCUq1iSBpRdffNH69Onj2bwCjt5yyy325JNPeubnZaLWkuRkWtchcbZ4xNDk26pA1c8++2ygeQmOKQBks2bNAmk57ejvTv2Grn9QwOdrr73WBSRX8FEvUyDZ5557LluWAnkrEKnf5O8qf3+tg1bQ2WDT56Nfv35x+74edNBB2QSw4uEW3H9+7+t7aKIvuL18noJFsILHlWrxK/XlXwezdu3aQNfy7bzrrrvs8ccfd4FM5Wtdo0YN970hH1D9LlEZ1ZVPtXyDUyksqM9jsOnzmt8+t8HjYR8CEIAABCAAAQhAAALBBFK3ojq4lzTd1816t27d3KaHMAsWLLDZs2e7xWP60aEfvzIt1pQAgBaQ6Ue+fsiG/jBN01NkWBBIGYEM399DNNvtW4SJQSAdCFRZudiK7d0ddSilfNEtMAhAID0ISOSqY8eObkuPEXmPQuISqRCY8O7NO1VCC8l40K3Fze3bt/fuJIWpijiiLdWmic+2bdumupuktn/ooYcmtb2cGtNEisRftGEQgAAEEiGg6/chhxzitkTq51UdPdtJhcBjPOOPJMIYTxsqK6FCbXlpehZWt25dt6W6XwlhtWvXLtXdJLV9iYTlpek+UMJy2jAIQAACoQQkSvntt986Z1k5vD344IMRvy/02/Lggw+2m266yTmMyolRW7KuWaFj4xgCEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIJIuAgjh98803pqBdEuzQfIfmPbxMc96a1/33v//tgjJJlODOO++08uXLexUnDQIQgMA+QWB71k4bv3hKjufas+mRuV5H17l+Oxu3aLJt2rk1Yn/rtm2wacvnWKcDvL/LI1YsgBmfffZZNtEqCWDpWhavL68C5inY1cCBAwMUFGhaIk2J2IQJE6JWGz9+fJ4KYGkwEgRSoE7/Ok+lab1Bq1attJsSky/3jz/+GNa2BJIkXCThJAWF9q8v1TrUt956y41T+xImevrpp8PqP//88y5AmYIVr1692m677TZXTkEiQ23q1KmhSYFjCejoM6Mg5InY+eef7/xKJITlZU899ZRbQ3vxxRd7ZedZmu7xcjIJgel+8LjjjsupaLZ8CaIGC2ApU+JfsQpg6XM5aNAg58eptiS2pICiEhvq0aOHaa2yguFlprYAAEAASURBVGpK5MjLunTpEpas7wV9xiSIJH+f448/3glceb1PCqaaiIV+1vJS0C6R8Uaro7HrO0siWPpdEGx5IX7l70/9v/vuu/7DwKtE1d588023BRI9diRCdsIJJ9gVV1wR9+fYo7mwJPmQBdsxxxwT97UmuD77EIAABCAAAQhAAAIQSCWBoqlsvCC1rUXf+oF61lln2a233mqPPfaYe/ijB0D//e9/XdqZZ55pzZs3DzycKEjnx1ghkGwCGT6F6WiWFUVpPlo98iAQK4G/d++2Yit/syJ790StUm35z1HzlVnK97AJgwAEIFDQCOS3gEdB48V4IQABCEAAAulAgOt3OrwLjAECEIBA4SSwYcMGe/jhh02OrHLwOu+885xjpRwsN2/eHPGkFZX09ddft6FDh7rogopWqjbGjRsXsQ4ZEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQCC/CGzZssW04F+iFwripPUfCgaixeV//vlnxGEpkKJEDj788EMnJKD1Io0aNbL33nsvYh0yIAABCBR2AhOXTLVtWTuinmbNslWtTa3mUcvEklm8WKZ1bdQ5x6KjF0yyPT7hmMJs69atc4Gpgs9RIj6XX3657dy5Mzg5pv0bb7wxW1BlCb/4hZliauD/C0msxUu0KbgNCVFJ4CkvTSJfF1xwQbYuL7300mzHyT6IJvw0c+ZMJ6qpccm/ombNmk746IYbbnD3IhIrevTRR02iNqEmcaMGDRq44M66j3njjTdMa1pDbePGjU5cKTQ9+FjrXfW5SdQkctWhQ4eI1S+77DJ74YUXIuanOmPhwoU2cuTImLq55557bOvWyOJ6Xo3oXvLYY4/NljV69Ohsx5EORo0a5dhI5Ep/zwMGDLADDjjACbFKPO2PP/5wf0vRgoBWq1bNiYwF97Hbt07u6KOPtj59+jhxM4lgvfPOOxYqWhVcJ979bdu2Zaty2GGHZTsuaAf6e5o4cWK2wOx5KX4lXtdcc02usEnc7/3333eCZ//6179MAnfJsiVLlpi2YJNoMAYBCEAAAhCAAAQgAIF0JZCRrgNjXBCAQHoTyPQ9mIlme1autL+zsqxIZma0YuRBIGECu3wPfmtecor1KVLUNpetYpvLV7OMhQ3tr90LLLNhQyvum5TOqF/fqucggFW8RQvL8D04xCAAAQikMwFNjoRavBF+QutzDAEIQAACEIBAaglw/U4tX1qHAAQgAIF/CLz66qvOOTbaoo5/SnvvyXlOESRPPPFEFxVR0SjvvvtutyXiGOvdC6kQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAIHECWhh+7bXX2qpVqxJupFevXjZlyhQ74YQT7LfffjMtMpdghUQkihUrlnC7VIQABCBQ0Ahsz9pp4xdPyXHYdcrXsO9+m51juVgKZPi+Z4v4/v3t+xfJ1m3bYNOWz7FOBxwcqUiBTFfgqrlz59rnn39uzz//vK1evTrsPBSoqmXLlnbdddc5YZ6GvnUxmTGsyVKQrN69e9u7775rXmJRYR0FJUh4ReMaP368G5eujdFs6dKlToTyqquusu7du7v9kiVLRquSlDxdqwcNGuQEnxToSwKYqTSJGF199dU2cODAiN0oUJk2v1WvXt0FIOvatatL6tmzp6fQpsSRtMkklFWuXDm3L2GiefPmxfxeDBs2zCSQdMkll1inTp1csLSMjNiXCkscdMSIEU6Ma/369W4Mwf/t2bPH9D4r8JoEx7p06eJEnlLpQyKh088++8wJGkkcTMex2Lfffut8Xs4++2wnQqVApW3atLEqVapErd6/f3/H219If4ObNm0KvCf+9NBXiXOFmsTrZs/+33el7lclaJeT6T70xx9/zFZM3w06d9nLL7/s3ttsBXJ5sHz58kAL+o5p3bp14Lig7kgEa8KECXbUUUe5e/snn3wyT0/liCOOMAnGvfTSS7nuV+K8+tuLVfwtpw4//fTTbEX0d6HvbgwCEIAABCAAAQhAAALpSiD2X7XpegaMCwIQyBcCGV4CWL4HZRm1a5vyJJC11/fwrVj58vkyPjot/ASyFi1yJ1n0771WftNat9nyn2zdhA8DJ7//4sVWeXV2pfJA5v/vlPI94MIgAAEIpDuBLJ+oZKg1b577aEqhbXIMAQhAAAIQgEDyCHD9Th5LWoIABCAAgcgE5PiqaOXJsI4dOzonQkWTVMTWe++91xRVVFHUMQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCCQnwTuu+8+N3eRjDG08AXPnTRpklskv2zZMtMieQUaee211yyVog7JGDttQAACEEgWgYlLptq2rB05Njf99x9NW17a6AWTrGPdVlasaNG87DZlfUngSAJYsdgi3zoZiefIdE26/vrrY5qzv/XWW50A1hVXXGGlSpWKpSsnfDNnzpyYygYXWrNmjd1zzz1uU/pRvjU5Ep9JpTVu3NhOOukk+/jjj00iRxLBSrU9/fTTJpGonHwmVEYiVLpXkQCZ3x588EEbO3as53uv8cvX48ILL3TFL7jgAiee5a8b6+vkyZNNm0yfl1NOOSUu4ZwDfGv/JKQlsS6vgJ9qN7gPnev06dOdUJvykm3z58+3M888M6Fmf//9dydo6q8sca/TTz/df+j5euyxx7rP78SJE13+jh073N+R3s9oJjGwor7vJy9mqvvUU09Fqx7Iu/LKK13ZUAEyibLqc9e3b99A2WTtzJo1K9CURN4Ki0nMa+bMmVatWrV8OaXnnnvOfS/pd8Xff0cWeYxlcB988IETgpNgb25Nf9/B9p///IffO8FA2IcABCAAgaQTaHpsG6t/SNOI7f61Yp1NGfp5xHwyIAABCCCAxWcAAhBIiEDxgw6ySg8/bJn77+8EryR6lVGrlhXxPWTBIJAXBPwCWJH6KlKmjGX5BLCK7t0TqYhLL3XMMVHzyYQABCCQDgQU0SXUFKkFgwAEIAABCEAgfQlw/U7f94aRQQACECgsBG6//fakiV/5mUhs+aOPPnIOfopOKcesGjVqmJxlMQhAAAIQgAAEIAABCOyLBDas22wrflsXOPXSZUrYgU1qBY7ZgQAEIAABCEAAAhCAAAQgAIHUE3jssceSJn7lH60EH0aNGmWHHnqoE6Z4/fXXrXr16vboo4/6i/AKAQhAoNAS2JG108YvnpK257du2wabtnyOdTrg4LQdYzwDGzhwYDzFs5Vt1KhRtuNIB+3atbP33nvPzfVHKhOarqBYW7ZsCU2O+zhY9CnuynFUuOmmm5wA1qWXXhpHrcSLSoTo8ccftz59+tigQYPs66+/tlWrVpkCY1apUsUO8q2rO/74451gU9WqVcM6atKkiX377bd25513ulcVqFevnp188slO2KhmzZqBOhI6Ou644wLHie4EtxlrG927d7fRo0ebhM1isdq1a8dSLKEy9evXT0gIzKuzDh06eCWHpek+UwHj/GJWEjLKSQBLf2+6d3zggQdswYIFJpG7tm3bOvG60047LayPSAn6HOl+tF+/fjZjxgwrWbKkHeNb43bXXXe5MUWql2i6hLamTp3qquuzkld/S4mON956+SV+pXFmZma674vLLrvMBg8e7L4v9NnYtGmTOw2NTZ9vfS9I0E+/OyRaJxHeH374wT755JNsYnnDhw+33Apg6TM1bdq0AMZevXq576xAAjsQgAAEIACBFBCotH81q9ehScSWV5crHTGPDAhAAAIigAAWnwMIQCAhAhm+H9qV+vdPqC6VIJAMAhK3imaZPvX27TlFkfA9LCp15JHRmiEPAhCAQFoQ2LhxY7Zx6GG3JpswCEAAAhCAAATSl0Do9VsOMV27dk3fATMyCEAAAhAoUATGjBljisqXCtNCj9tuu83uv/9+17ycMXv06JGyCJ6pOAfahAAEIOAnsGtnls3/8Tf/YdTXLZu2Z8tf9fufNmfGL9nSvA6Kl8i0pi3398oiDQIQgAAECgGB5cvW2mcjvgucSc26lRHACtBgBwIQgAAEIAABCEAAAhCAQOoJaNG2goKkwhQY5GFfQOhrr73WNS8BhJNOOsmOOOKIVHRHmxCAAATShsCMFT/ZtqwdaTMer4FMWjq90AhgnX/++V6nmPS03r17x9XmqaeeGlf5/C7cpUsX2759uxMJysuxtG7d2p5//vmEupRI1ocffphjXZ1bflq6+DVKTC2v/l78vCVmJSEoiZzJZs+e7YTWJFQWzc4991zTtmfPHpNvaKImHx2JUkmAq2jRook2E1O9YcOGufGq8IABA6xMmTIx1aNQ7AQkcKXfFH7T+/r333/n+BmREJa+w7/66itXddmyZf4mEn598MEHA3Ul0pYbMcZAQ+xAAAIQgAAEIAABCEAgxQQQwEoxYJqHAAQgAIHUEMhatChqw5m+SBc5CWAV9z2ILuZ7QIpBAAIQyC8CmvDYvHmzVahQIeoQ1q5dmy3/6KOPtkQitGRrhAMIQAACEIAABBIikOj1W9HevCK9JTQIKkEAAhCAwD5NQI5R119/fUoZ3HzzzS4y4datW1300quvvtomTZqU0j5pHAIQgEAqCGzbujObaEk8ffw8+1fTlpNVqLQfAlg5QSIfAhCAAAQgAAEIQAACEIAABCAAAQhAAAIJErjhhhsCC/UTbCJqtSuuuMK0OHzNmjVucbqOf/rpJ1OQSgwCEIBAYSXw21+r0v7UNEbNjfN9nPZvVZ4OsGTJknnaH53tGwQUgG706NH222//C6zUv39/FyguMzMzRwC5Eb8KbjzV4ldZWVn29NNPuy579uxpffr0Ce6e/RQRiPV9lfjb66+/bvXq1XPXvtx+10lI66OPPgqclQTeateuHThmBwIQgAAEIAABCEAAAulKILWywEk+ay20WLFihbuJj6VpPejasGFDvmwbN250quIaAwYBCEAAAsknsGvx4qiNZtataztnzoxappRPQAaDAAQgkF8EnnzySVMkhYoVK9phhx1mS5YsiTiUpUuXZsu76aabsh1zAAEIQAACEIBA3hB45JFHrGzZsu76rYiv0aIscf3Om/eEXiAAAQjsiwTGjh1rCxYsCDt1OUFdddVV9vbbb9vcuXNzFalR1zs5vPnt66+/tu+//95/yCsEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQSDmBGTNm2LfffhvWjxZvX3LJJfbGG2/Y7Nmzbf/99w8rE2tCRkaG9e7dO1B83rx5Nm7cuMAxOxCAAAQKI4ESGcXT/rRKZhZH/Crt3yUGCIHCQaB8+fI2dOhQ84tZ6X7w8ccfLxwn9/9n8cILL9gvv/ziRJCGDBlSqM6tsJyMftNUq1bNnU7Dhg0TPq3t27fbpZdeGqgvgd+zzjorcMwOBCAAAQhAAAIQgAAE0plARjoPzj+2HTt22HXXXWevvvqq7d6922rVqmXPPPNMtokGf9ngV4kI5OZmP7itRPc1IaIfHhpHy5YtTYsju3fvbvphjEEAAhCAQGIE9voexuzxCSJGs7/37DHfRSNaESuNAFZUPmRCAAKpIyCnG4lY+cVSp0yZ4u4Tp0+f7u51g3tetWqVrV69OpB0/PHH2wknnBA4ZgcCEIAABCAAgbwhMHXqVLvjjjsCnU2ePNm6dOli06ZNsxo1agTStfPrr7/a+vXrA2kSEDnuuOMCx+xAAAIQgAAEckPgww8/zFb94IMPNkWj1O/F4Oi3wfvZKsR4cMghh9i7774bKC0HuHbt2gWO2YEABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACqSQQOifSpEkTNydy4oknBgQK1H/RormLC685keeffz5wKq+88op169YtcMwOBCAAgcJGoHXNpjZ+8ZS0Pq1WNZqm9fgYHAQgULgIHHXUUaYAqbfeeqs7sXvvvdet2WjVqlWBP1EFer3zzjutZMmS9v777wdElgr8iRWyE9jjWwe5adMmd1bHHntswmd3ww032KJFi1z9Y445xp599tmE26IiBCAAAQhAAAIQgAAE8ppAgRDAkvjV4MGDA2xWrlxpZ555pn333XfWvn37QHroToMGDaxNmzY2a9as0Kw8O5Zgl8arbdKkSTZw4EArXry4nXbaaXb77bdb69at82wsdAQBCECgoBDYvnWnZWX5BKwi2O65P5lPNSZC7v+SdyxfGTXftxrQslq1s01/bYteLkpuuQqlo+SSBQEIQCAyAUWm84tf+UvpflHCVhMmTLCKFSv6k2348OGBfYlrEHEjgIMdCEAAAhCAQJ4SmDlzZlh/y5cvNznWjh8/PpvYefD1W5Fng59rhTVCAgQgAAEIQCBOApob8dtJJ51kI0eONAXjSLbVqVMnW5NfffVVtmMOIAABCBQEAhkZxaz2AVVjGuq6NRtt545dgbLlK5ax/crlPA+wX7lSgTrsQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAskjoEBVflMg8rFjx1qpUuHP43IbFCR0TkTrPjAIQAAChZlAw8r7W5f67W3S0hlpeZoVS5WzU1oQbDAt3xwGBYE4CEjQpyDZLbfcYnPnzrWhQ4fazp073fpl3Y+WK1euIJ1GtrFqbfP5559vW7dutREjRpiEX7H0JPDpp5/a9u3brXr16s4vOZFRvvbaazZo0CBXVUEOU+VTlsjYqAMBCEAAAhCAAAQgAIFYCCR/RUQsvcZRZtu2baYb71DTD+DXX389qgCW6lxwwQX5KoAVOm4d79q1y9555x0XOf2qq66yRx991HMixqsuaRCAAAT2BQIfvDnJ5s5aFvlU/95r+539kBX5dallrFpuVbavt8o7Nli1XRus4o6/rOjePbbyqylWOXILtq7K/vbqI6OilIiepcnyB1+4JHohciEAAQhEINC2bVvPnNmzZ9uhhx5qjz32mCla3ZgxY6x///6urMSvdBzqbOPZEIkQgAAEIAABCCSdgCaDvez7778PXL8bNWpkmoS+++67XdFatWo5B9yaNWt6VSUNAhCAAAQgkBCBJUuWuHply5Y1RSBPhfiVOihdOrvoy7x581ykwYLs2JcQcCpBAAIFmoDEqc6/qmtM5zD8lS9t6cJVgbLtDmtiHbs0CxyzAwEIQAACEIAABCAAAQhAAAIQgAAEIAABCOQtgV9++cV1mJmZ6YQIvMSvVCA0GGW8owydE1mzZo0tXbrU6tevH29TlIcABCBQYAic1bqn1S1f075eNsP+2Lre9uzdm+9j369EaWtRrZGd2OwoK1uiTL6PhwFAAAK5I7Bq1T9zr7lrKe9qv/zyy7Zhwwb7+OOPbf78+XbOOefYRx99ZMWKFcu7QSSxp+uvv94mT55sL7zwgp122mlJbJmmYiGwceNGe/HFF02+XlonJDEyLz+vZcuW2TXXXOOavOOOO6x48eKxNJ+tzMSJE+3yyy93aQcffLDzXS5fvny2Msk6kKAaBgEIQAACEIAABCAAgVQQSHsBLN3kZ2VleZ77unXrPNODE88++2y7+eabTWrF6WZ7fQ8Hn3vuOZsxY4aNHj3aKlSokG5DZDwQgAAEkk7g11/W2Ev//Thquyt+/cM2b9wetcz/Mqub1fBtQVbk77+txta1dt2PrwSlhu/Ozahp8+f8Fp7x/ynVa1W0ilXKRswnAwIQgEBuCLRp08aJXN16662me8JgW7hwoZ188snBSS6Cgx58165dO1s6BxCAAAQgAAEI5B2Bjh072sMPP2x33nln2PVbjg4nnnhitsGccsopzmlAIpYYBCAAAQhAIFkEtIBjy5YtrrkePXpY1apVk9V0WDuanwk29a0FHwhgBVNhHwIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAgVQR2Lx5s2v6iCOOSKkYVeiciDpdvXp1SvtMFTPahQAEIBAPgcPrtTVtGAQgAIFUEJAgz9q1a61atWqpaD4lbUqcaMSIEU746v3337dRo0bZFVdcYYMHD05Jf6ls9Omnn7aBAwfagAED3Dmksi/aDiewadMmU+Bdv6jvSy+95HyKhw0bZgq2K9u1a5e9+eabpnVFf/75p3Xo0MGuuuqq8MZySJk+fbpbg6T29Nvpk08+sVSJX2koX375ZQ4jIhsCEIAABCAAAQhAAAKJESiaWLW8q6VFgpEiZ7Rs2TLHgegHshaBpLN99913TkE5VPwgncfM2CAAAQiknsDfvi5y2sJH8XeRIlZh10Yr6uqG5/tTfil/QA7t+0vyCgEIQCA1BG666SabNWuW9e3b12rWrJmtkyK+77IGDRq4CAy6V9QDaMSvsiHiAAIQgAAEIJAvBPr37++EzC+66CILFbbS9VuT0ldeeaVNmzbNPvjgg7Ay+TJoOoUABCAAgUJFQNcbf1TJtm1T64g8b968MHbr168PSyMBAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCKSCgAQIZAo4mUr7+eefw5rXAnQMAhCAAAQgAAEIQCBxAlu3bnXrer/55hvbvXt34g3lcc3ixYvb8OHDA0JEL7/8sl1//fV5PIrcdSeRpRtuuMFuu+02t+WuNWonQuC9994LiF/560uoqkWLFnb00Udb586drXLlym49kX57aE2RxNf8v4H8dXJ6lb9yt27dTOLB5513no0bNy5l4lca5yOPPGKjR4/OaVjkQwACEIAABCAAAQhAICEC/5sRSKhq3lTSYo4XX3zRKdDu3Lkz0GmTJk3cgsJAQpQd3bhLNMBv1atXty5dulidOnVMP0hTbYqKLnErKejqh/uGDRts1apVtmjRIlu3bp3rfsKECfbMM88UuB/DqWZH+xCAAAQSIdBg069Rq+21IrasbN2oZciEAAQgkBcEWrVqZZoQkWkRse4TMzMzrWrVqlaqVKm8GAJ9QAACEIAABCAQJwE51g4ZMsTVCr5+S4S9ZMmScbZGcQhAAAIQgED8BCpVquQiZFaoUCH+ynHUGDt2bFhpAnmEISEBAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIACBFBHQnMjq1autYsWKUXvQmpPc2JgxY8Kqaw0IBgEIQAACEIAABCCQOwIzZ850Qj+6XytRokRYY5dccok9++yzYen5naDgdAMHDrTWrVvbddddZ08//bRpbbPSihYtmt/Di9q/1lFfeOGFLhj7gAEDopYlM3UENm7c6Nl4VlaWTZw4MVtevXr1nKjUAQcckC09pwOtSe/Vq5ft2LHDnnjiCSd6llOdnPIHDx7sPvOh5fzr40PTOYYABCAAAQhAAAIQgEAyCaS9AJZOtmvXrvb999/bK6+84hZ1tGzZ0q644oqYlWh79uzpBAS2b99uffr0Md2E54XwVSxv1C+//GJPPvmk+/H70EMPOVEvrx/zsbRFGQhAAAKFhUDNupWtRp3KCZ9O8wUro9ZdV/UA2791w6hlevQ+xA45snnUMmRCAAIQSCYBOetowyAAAQhAAAIQKDgEuH4XnPeKkUIAAhAoTAQaN27s5kr++uuvlJ3Wd9995+ZlQjtItehWaH8cQwACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgsO8S0JzIvHnzLKc5kdyIVS1cuNA+//zzMMjMiYQhIQECEIAABCAAAQhEJNCwYUM78cQTI+ZHymjWrFmkrLRIv+yyy6xTp0527rnn2osvvujEWd966620DXb+5Zdf2hlnnGH/+te/3HrltIC4jw6ic+fOMZ35qaeeaoMGDbKqVavGVN5faNiwYXbxxRfb/vvvb2+//ba1b9/en5Wr1zp16thxxx0Xdxvt2rWLuw4VIAABCEAAAhCAAAQgEEqgQAhgadAtWrRwKrShJxDLcZkyZaxbt2724Ycf2i233JI24lcae4MGDey5556z3377zaSuPGrUKDvttNNiOS3KQAACECiQBCRudd3dpydl7NO//tmmTJgbaKtB01p2wvHNbMsrVwbSvHbqnHGK3XDHGV5ZgbSy5UtZyVLFA8fsQAACEIAABCAAAQhAAAIQgAAEIAABCEAgHQgcfvjhNnnyZJs2bVpKhqNIg4peGWqKYKmIgxgEIAABCEAAAhCAAAQKA4FhL35uq1esz/FU9u79O1sZ1Xn8ruHZ0iIdnHfl8Va9FsFPIvEhHQIQgAAEIAABCEAAAhCAQE4ENCeiNSA5zYkUKVIkp6Y88yWcde2119revXvD8rXOA4MABCAAAQhAAAIQiI3A6aefbtoKo7Vs2dJmzJhhjz/+uA0ZMsS2bNmSlgJYU6dOtV69ejnxotdff93k5xOL6Z742Wef9fQViqU+ZbwJdOjQwR544AG7//77Tb5YoSZhtbvvvtu6d+8emhXTsX4nXXPNNa6P0qVLx1QnlkI9evQwbRgEIAABCEAAAhCAAATyg0CBEcDKLZzevXu7yY9Yf7jltr9460sVVwJY48ePRwArXniUhwAEChSB4sUzfE6+FZMy5v3KlcrWTomSxW2/BT/YFt/Dt2hW5eQeVjpJY4jWD3kQgAAEIAABCEAAAhCAAAQgAAEIQAACEEg2ATmr/ec//7HRo0fbypUrrVatWknt4uqrr7bp06eHtdm6deu0dOALGygJEIAABCAAAQhAAAIQiIHA7qw9lrVrdwwlsxf52yeIFWu9v8PXT2dvjCMIQAACEIAABCAAAQhAAAIQiEpAcyIKgP7NN9/Y/PnzrWnTpp7ltWg/Ebvjjjts3LhxYVXr1q2b9PmXsE5IgAAEIAABCEAAAhAoMASKFy9u/fv3t1tvvdWKFSuWduP+8ccfnWhR+/btbcSIEZaREfuy8RdeeMHTTyjtTrIADujOO++0vn372ttvv20LFixwn51GjRpZt27drHnz5rk6o+HDh8cscparjqgMAQhAAAIQgAAEIACBPCQQ+y+ZPBxUKro66aSTLDMz0zZu3JiK5nPd5ty5c10b/tdcN0gDEIAABPZRAtsnTox+5r4HjSV9EaEwCEAAAhCAAAQgAAEIQAACEIAABCAAAQgURAKKAHjwwQfbDz/8YFdccUXSgn9s377dLr30Uhs2bJgnFi0ywSAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIBAXhHQ4nAFGv/iiy/ssssus88//9xKlCiR6+6zsrLshhtusIEDB3q2xZyIJxYSIQABCEAAAhCAwD5PIB3FrxYtWmTHH3+8NWzY0D7++GMrWbJkzO/T+++/7+6LdZ+NpYZAzZo17cYbb0x640WLFk16mzQIAQhAAAIQgAAEIACB/CawzwhglS9f3m677TZLNLpHKt+or776yl599VXXxdq1a1PZFW1DAAIQKPQEtk+YEPUci7Rua0XLlIlahkwIQAACEIAABCAAAQhAAAIQgAAEIAABCKQzgUceecRFbvzkk0/s3HPPtVdeecVKly6d8JAnTZrkFo4o2qCXaTGJIhJiEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQCAvCTz00EP25Zdf2tdff22nnnqqvfXWW1ahQoWEhzBjxgw3JzJr1izPNrSQXAFIMAhAAAIQgAAEIAABCKQ7geXLlzvB2DVr1phEXOVPFItt2LDBpk+fbt9//701btzYunTpEks1ykAAAhCAAAQgAAEIQAACEEgpgX1GAEsUH3jggZTCTKTxMWPG2Omnn26KIiLbvXt3Is1QBwIQgAAEfAQytmy0XXPnRmVR5JDDouaTCQEIQAACEIAABCAAAQhAAAIQgAAEIACBdCfQvXt3u/DCC+21116zd955xzmkaQ6kd+/elpER29TPzp07bezYsfb888+712jnfP3111udOnWiFSEPAhCAAAQgAAEIQAACBYrACWccart37clxzIt/XmHfjP8xUK5K9fLW81+dAsfRdipXKxctmzwIQAACEIAABCAAAQhAAAIQiIFAx44d7cYbb7THHnvMRo8eba1atbL77rvPzjnnHFMAj1hMazTGjx9vL774on300UdRg6pr/qVFixaxNEsZCEAAAhCAAAQgAAEI5BuBtWvX2vHHH2+//fabG8NLL72U0FguueSShOpRCQIQgAAEIAABCEAAAhCAQLIJxLYKItm90p4j8Oabb7oFKnv2/ONQV7FiRehAAAIQgECCBMotmG2+WemotYscenjUfDIhAAEIQAACEIAABCAAAQhAAAIQgAAEIFAQCLzwwgu2cOFC+/bbb23RokV21llnWeXKlU3iWO3btw8E3Pjhhx9s5cqVtm7dOve6ZMkSm+sTkVcUx+3bt+d4qu3atbN77703x3IUgAAEIFDQCXTt1cF27vxf0CKdS7nypQv6KTF+CEAAAhCIQqBq9QpRcv/J+mPNX/8c+PYyi2dYzbqVs6VxAAEIQAACEIAABCAAAQhAAAKpJTBgwAA3tyEBrOXLl9vFF19sCt6hOZEOHTrY1q1b3QDmzZvn5kf+/PNPNyeydOlSU9qMGTNsy5YtOQ6ycePG9sQTT+RYjgIQgAAEIAABCEAAAhDITwJ//fWXdevWzRYsWJCrYWRmZtoFF1yQqzaoDAEIQAACEIAABCAAAQhAIFkEEMBKFsk421mxYoVJHTlY/EpNtGzZMs6WKA4BCEAAAn4C5Rb+4N/1fN1TtJhltG3vmUciBCAAAQhAAAIQgAAEIAABCEAAAhCAAAQKEoGSJUvamDFjrFevXjZhwgQ3dC3oGDZsmNv855IbR7XmzZvbp59+auoLgwAEIFDYCVSsUrawnyLnBwEIQAACEIAABCAAAQhAAAIQgAAEIACBAkmgWLFiNnLkSDv77LPtww8/dOewadMme/fdd93mP6nrrrvOvxv3a7169UwCW+XLl4+7LhUgAAEIQAACEIAABCCQlwQkCKuAeLk1+RxVq1Ytt81QHwIQgAAEIAABCEAAAhCAQFIIFE1KK4WsEUU814TIrl27UnZmkyZN8kUQ3hnWfs+ePcPSSIAABCAAgdgIlFswO2rBP6ofaEVKlopahkwIQAACEIAABCAAAQhAAAIQgAAEIAABCBQUAmXLlrVx48bZ7bffblr8kUzr0aOHff3111ajRo1kNktbEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQCBuAgrWIRGshx9+2IoXLx53/WgVOnfubN9++60deOCB0YqRBwEIQAACEIAABCAAgbQgICFYrX3O7TZ8+PC0OB8GAQEIQAACEIAABCAAAQhAQAT2eQGshQsX2hNPPGGnnXaaNWrUyEUxL126tIvcUaJECStTpoy1aNHCzjrrLHvmmWds6dKlSfnk1KpVK6wd9X/yySeHpZMAAQhAAAI5Eyi5bZOVWrM8asFVtZpEzScTAhCAAAQgAAEIQAACEIAABCAAAQhAAAIFjUBGRoY98sgjNmPGDOvWrVuuh6/5iyFDhtioUaOsUqVKuW6PBiAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIBAMggUKVLE+vfvb3PmzLFTTz3VdJwbq1KlilsjMnHiRKtZs2ZumqIuBCAAAQhAAAIQgAAE8oyAfIUyMzNzvRUtus8vL8+z94yOIAABCEAAAhCAAAQgAIGcCeyzv1A++ugjO/zww61JkyZ200032QcffGCLFy+2nTt3ZqO2bds2mzdvnknNuF+/fi6qxxFHHOHKZysY50GXLl3spJNOCtSqWLGivfXWW0mP0B7ogB0IQAAChZxAzZXzczxDBLByREQBCEAAAhCAAAQgAAEIQAACEIAABCAAgQJK4OCDD7YxY8bYDz/8YNdff31cUcpLlSpl3bt3tzfeeMMFArnoootyvWikgGJk2BCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCKQ5Aa0BGTlypP3888922223WdOmTWMecfHixe2YY46xwYMH26+//mrXXnstazhipkdBCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgEBqCGSkptn0bXXJkiXWt29fU5SORG3y5MmmrVOnTm7io0WLFnE3pWgjEuEaN26crV+/3rp27WqVK1eOux0qQAACENiXCSyet8KWLFzpEBy0YFZUFLuLZdi6agdGLUMmBCAAAQhAAAIQgAAEIAABCEAAAhCAAAQKOoHWrVvbk08+6bbly5fbrFmzTHMjq1evtq1bt9qePXusZMmSVqlSJatTp441b97cVKdEiRIF/dQZPwQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAvsQAQlhDRgwwG2rVq1ycyK//PKLrVy50s2J7N69281/KFh57dq1A3MipUuX3ococaoQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAgfQnsE8JYH322Wd29tln26ZNm5LyzkyZMsXat29vAwcOtIsvvjjuNiWC1a1bt7jrUQECEIAABP5HYOfOLNu1I8sd1F+/JCqWtdUb2J6MzKhlyIQABCAAAQhAAAIQgAAEIAABCEAAAhCAQGEiULduXdOGQQACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAIHCTKBmzZqmDYMABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQKDgESha8Iac2IhHjhxpvXr1Spr4lX8UO3bssL59+9pDDz3kT+IVAhCAAATymEDZXZutyo4NUXtdXatJ1HwyIQABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAgbwjkJF3XeVfT7Nnz7Zzzz3Xdu/eHfMgypYta+XKlTO9Fi9e3Alnbdy40TZs8BZYufPOO61SpUp25ZVXxtwHBSEAAQhAIDkEGm5clmNDqxDAypERBSAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQCC9CUyfPN8+/2C6rV31l1WuVs6OPbmdtTuMABDp/a4xOghAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQCASgUIvgLV371674IILbMeOHZEYuPRDDjnETjvtNGvbtq21atXKqlWr5ln+r7/+spkzZ9rkyZPtjTfesMWLFwfK9evXz9q0aWOHHnpoII0dCEAAAhBIPYEGG3+N2snujOK2rlr9qGXIhAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQikM4FXnhhlbw36/P+H+LctXbTSZnwz386/qruddPZh6Tx0xgYBCEAAAhAosAQqVSlnB7X9Z665YuWyBfZcGDgEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAIF0JFDoBbBGjBhhs2fP9mRfrFgx69u3r/Xv39/q1avnWSY0sUKFCnbMMce47e6777bPPvvMrr32WluyZIllZWW59n744QfLzMwMrcoxBCAAAQikiECDTcuitrymegPbW6zQX/KiMiATAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAECi6BWd8t8olffeF5AsNe/NxatKtnBzau5ZlPIgQgAAEIQAACiROoU6+qacMgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAIDUEiqam2fRpdciQIZ6DadasmUmoatCgQTGLX3k1dMIJJ9hPP/1kvXv3dtnz5s2zV155xasoaRCAAAQgkAICFXZutEq+LZqtrt0kWjZ5EIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAIG0JjDkqc8ijm/v3r327ssTIuaTAQEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAgXQlkpOvAkjGuXbt22VdffRXWVIcOHWzs2LFWsWLFsLxEEkqVKmXvvPOO9erVyz777DN77LHH7PLLL7ciRYok0hx1IAABCEAgDgINNi7LsfSugw+xmnUqu3KZJQr1pS9HFhSAAAQgAAEIQAACEIAABCAAAQhAAAIQSB8CWVlZtmbNGqtataqVKFEipoFt2rTJ9uzZE1PZZBbSnEdmZqZpTqRo0UIfXyWZ6GgLArkmMGPyfJs3a1nUdmZOWWi/zF9hDZrWjlqOTAhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAAC+UFAcxurVq2yKlWqWMmSJWMawubNm2337t0xlU1mIf+ciMZZrFixZDZNWxCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCHgQKNQqIL/88ovt3Lkz22mXLVvWhg8fnjTxK3/jGRkZ9tJLL1nTpk1N/U6ZMsUOO+wwfzavEIAABCCQAgLFihW1hpt/i9pykf32s/MG3mxFfN/TGAQgAAEIQAACEIAABCAAAQhAAAIQgAAE0oXAM888Y3fffbdt3LjRSpcubTfffLPde++9OQbXaNasma1cuTLfTkOLPipXrmz16tWzFi1aWKdOnaxHjx62//7759uY6BgChZ3A0OfGxnSKI4ZMtNsfPTemshSCAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIJBXBIYOHermQdatW+cCglx55ZX23//+17QGI5p17tzZ5syZE61ISvM0J6Kg6wcccIA1b97cDj30UDcn0qBBg5T2S+MQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAgX2NQPQZgwJO488//ww7gyuuuMLq168flp6MhNq1a9u5555rgwYNsjFjxiCAlQyotAEBCEAgCoGmrfa3Ctt+j1LCrNQRRyB+FZUQmRCAAAQgAAEIQAACEIAABCAAAQhAAAJ5TWDYsGHWr1+/QLfbtm2z+++/3wlLXXfddYF0r50zzjjDnnrqKa+sPEn7+++/TQtUtM2YMcO0aEXWpUsXt3jlpJNOypNx0AkE9hUCMybPt3mzlsV0ujOnLLTFP6+whs1qx1SeQhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEUk1A6youuugi0/yCTAHONc+xny/A7QMPPBC1e82J5KcAlsa8fv16t82aNcs0vyPr2LGj3XDDDXbmmWfmGNgk6gmSCQEIQAACEIAABCAAAQhAAAIQgAAEIACBQkLg+xGT7MdRUyOezZ6sPRHzyIAABCAgAkULM4ZixYqFnd4555wTlpbMhJNPPtk1N2XKlGQ2S1sQgAAEIOBBoNiq322/Les9cv5JKnXUUf8csAcBCEAAAhCAAAQgAAEIQAACEIAABCAAgTQgMHDgQM9RDBkyxDM9OLFPnz7Bh2mzP2nSJNMcyXHHHWe//x5dtD5tBs1AIFAACAx9bmxco3zv1YlxlacwBCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAIFUEnj++ecD4lfB/cQyJ3L++eenpcDUtGnT7Oyzz3YB0xctWhR8WuxDAAIQgAAEIAABCEAAAhCAAAQgAAEIQGCfJLBzy3bbvPaviNu2DZv3SS6cNAQgEDuBQi2AVb169TASTZo0CUtLZkKDBg1cc0xkJJMqbUEAAhDwJlBizgzvjKDUUkcfHXTELgQgAAEIQAACEIAABCAAAQhAAAIQgAAE8p/AH3/84TmISOnBhdu0aWMtW7YMTkqr/fHjx1uHDh3yNSJ7WgFhMBDIBYEZk+fbvFnL4mph5pSF9sv8FXHVoTAEIAABCECgIBP4ec6v9s34nwLbil/XFeTTYewQgAAEIAABCEAAAhCAAAQKHYF167x/p0VKDwaw//7721FpHAj3u+++s44dO9o333wTPGz2IQABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQiJNARpzlC1Tx+vXrW/ny5W3jxo1u3KVKlTJtqbQqVaq45mNZpJLKcewLba9Zs8Zmzpxpy5Ytc+/x3r17rWzZsqaJLi0A0mte2N9//20///yzW8yzcuVK2759uxtH3bp13SKfOnXq5MUw6AMC+ySBkrOnRz3vIuXKWYm2baOWIRMCEIAABCAAAQhAAAIQgAAEIAABCEAAAnlN4LDDDrPFixeHdduqVauwNK+EPn362C233OKVlRZpq1evtu7du7tn+DVq1EiLMTEICBREAkOfG5vQsEcMmWi3P3puQnWpBAEIQAACEChoBCQWuWje74FhFy1axGof8D/fnUAiOxCAAAQgAAEIQAACEIAABCCQbwQ6depkU6ZMCes/1mAfmhOZMGFCWP10Sfjrr7/sxBNPtOnTp1vDhg3TZViMAwIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCBQoAgUagGsIkWKWM+ePe2tt95yb4qEibZt22alS5dO2Zu0c+fOQF8p62QfblhiZoMHD7Y33ngjx+jxjRs3tvPPP9+uvPJKq1y5ctKpSeTsiSeesNdff90kfBXJFOn+6quvtvPOO8+KFSsWqRjpEIBAAgRK/Dgjaq1SXbpYEf7uojIiEwIQgAAEIAABCEAAAhCAAAQgAAEIQCDvCTz00EP25Zdf2u+//7NQf7/99rMHH3wwpsGcc845dtttt5kCQ8g076EI6A0aNHCBQDQ/kmpT31lZWW7eRc/uFbRi6dKl9uuvv7quV61aZVdddZWNHPl/7J0FmJRV+8YfYHuXLmkB6ZIS6Raku0FK5BMQQSVEUAxKUZQPQVBCPiQEREJaUkJackmRlK5lG/5zH/8zTrwzO7k7M3s/1zXM+54+v5nlvHPifpZ7uiksnwT8ksD+nacEgh7O2MHdp+XcqStSuHgeZ7IzDwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAm4jcC7774ra9askcjISEOZwcHBMmnSJMO9rYt27dqp8wAxMTEqWVBQkNSuXVtwViA8PFySc00E51GwJnLjxg3lxBvrInCmDRGs3r17y/bt2211hXEkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAJWCPi1ABb6DPEjvQAW7vfu3St169bFpUdMf1glJCTEI+Wn5kLnzp2rPNrfunXLLgynT5+W0aNHy+TJk2X8+PHSv39/u/IllQiHenA4acKECepgD9LnzZtX+vbtK5UqVRIsqqHuxYsXy44dO5Q3l549eyqxLAh3lS1bNqkqGE8CJGAHgTjd31m62zdtpgz14P/3NitmJAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAnYIIA55UOHDsnXX3+tDnzg/tVXX7XbM3ju3LkFHtN/++03KVeunGzcuFGyZ89uo8bki8IcPtZlIND1008/ycGDB6VChQrJ1wDWRAJ+QmDef9e71JMfZ2+VEZO6ulQGM5MACZAACZAACZAACZAACZAACZAACZAACZAACZCAqwTgyPr333+X6dOny9GjRyVHjhxKLKp06dJ2FQ0HIo0aNZKff/5ZOQLZsmWL5MuXz668nk4E4atly5bJkCFD1LmBDRs2yEsvveTpalk+CZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACfgdAb8XwKpRo4a0adPG4GF8zpw5HhXA2rdvn/qSpE+f3u++LCnVocTERCVe9e233zrVBCwsQQgNYlQQ0QoMDHSqHGSCt5ZOnToJFs701qNHD5kxY4aEhobqg6Rhw4YyYMAAJYLVq1cvgbeXP/74Q6pUqSILFy6UVq1aGdLyggRIwDkC0UZ/h9ZKCKMAljU0DCcBEiABEiABEiABEiABEiABEiABEiABEkhhAtmyZZMxY8Y43Yq2bdsqAax+/fp5jfgVOoN+vfHGGxIXF6ecWsybN48CWE5/ysyYWgns33lKThz606XuH9x9Ws6duiKFi+dxqRxmJgESIAESIAESIAESIAESIAESIAESIAESIAESIAFXCWTIkEE5znC2nHbt2ikBrO7du3uN+BX6kilTJunTp48EBAQIHGZjTYQCWM5+ysxHAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiSQmgmkTQ2dh3BSkSJFVFfhdXzv3r0e6/aPP/6oys6fP7/H6khtBQ8ePFicFb8yZoXP/pVXXjEOcuj6zp07Ur9+fRPxqw4dOihRLWPxK+NCO3bsqDzcp037z59aTEyMYAFuxYoVxsl4TQIk4ASB6K1bbeZKq1tUDipXzmYaRpIACZAACZAACZAACZAACZAACZAACZAACZCArxKA8w9YmjRpvLILcBQB27x5s1e2j40iAW8mMG/quiSbFxIalGSaH2dvTTINE5AACZAACZAACZAACZAACZAACZAACZAACZAACZCAtxNo3ry5coKt35Pvbe1t0KCBahLXRLztk2F7SIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEfIVAqhDAypw5sxItKlOmjCQmJioBovPnz7v9M9qwYYNs27ZNlVu0aFG3l58aC5w7d65MmzbNpOuFCxdWQlajR4+WSZMmybvvvqs8piA8KVu4cKF89dVXSSWziIdwVePGjeXYsWOGuFy5csmsWbOSPFzUqFEjGTZsmCEfvoNdunSRgwcPGsJ4QQIk4DiBpASwQuvUkTT/Lz7neOnMQQIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQALeTaBAgQJSqVIluX//vlc2VD+ffvr0aUlISPDKNrJRJOCNBPbvPCUnDl+02bSg4EB555NOkjaN7aXeg7tPy7lTV2yWxUgSIAESIAESIAESIAESIAESIAESIAESIAESIAES8HYCGTNmVI6svXVN5Pjx4wrh33//Lbdv3/Z2nGwfCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACXgdgQCva5FGg+7evasR6lhQWFiYrF+/XsaNG6dEiypWrCjvvfeewBtI9uzZHSvMLHV8fLxs3bpV+vfvb4ipUqWK4ZoXzhG4evWqDBw40JC5Zs2aMn78eKlevbohzPzi6NGj6jNetGiReZThftSoUdK+fXuBgJW9Nnz4cNm3b59J8rFjx0qGDBlMwqzd4Ls2e/ZsuXHjhkoSHR0tnTt3ViJY4eHh1rIxnAT8ksCTJ08l+nGsS32LP3VKEq9ft1lGuqrVJepRjM00tiKDggIkUPeikQAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkIC3Ehg6dKhERUV5XfOu6+buRo4cqdqFNRSs87i6FuN1nWSDSMBDBOZNXZdkyY3aVJbCJfNIlbolZfev/zpv0cq4dM42GT6xi1YUw0iABEiABEiABEiABEiABEiABEiABEiABEiABEjAZwjgXAGcbnib3bt3TwYPHmxoFs4LZM2a1XDPCxIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggaQJeL2yBw5u5MiRw+3ewWNjY+Xtt99Wr6QxOZ6iTp06jmdiDhMCEKrSH9x58803ZfLkyZI2rW1P1mXKlJGFCxdKhw4d5JVXXpGHDx+alImbR48eyZdffikTJkywiNMK+PXXX+Wrr74yicJ3skePHiZhtm4gcjVgwAB5//33DcmwAPfhhx/KxIkTDWH+fvFEJ/wFSxsa6u9dZf9sELh04YbM/HSljRRJRxU/vkWqJpHs+30P5e7b85NIZT26aYeqUq1eaesJGEMCJOASgcePH8uJEyfk8uXLarxOkyaNwEtb/vz5pWTJkhIYGOhS+cxMAiRAAiRAAiTgfgL4ja4fv/HbGuN3pkyZDON3QIDXTzO5HwpLJAESIAESIIEUJgBHC95m586dk8aNG8ulS5cMTUtISDBc84IESMA6gf07T8mJwxetJ9DFBAUHSovONVSaJu1elL1bTsiTp0+t5jmwK1LOnboihYvnsZqGESRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQx9tfAAABAAElEQVRAAiTg7QSaNm0qeHmTweE32nRK59hXb1wT0ZPgOwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAnYT8DrTyZCOKh27dqyefNm+3uVwimfe+45KVu2bAq3wrerP3PmjMybN091on379vL555+rg7X29qp169YSHBwszZo1k6cam/7nz58v48ePt6vM4cOHW1TbpUsXVb5FhI2AXr16yQcffGDSnilTpkj//v2lYMGCNnL6VlT8X39J/PnzknDhgnqPO3tOYnQHnp78+ac8vXlDwqf8V3INHuBbnWJrvY5AriuRNtsUExwud7PwMI9NSIwkgRQgANGMuXPnyg8//CB79+6VxMREzVZgDG/YsKH07t1bWrVqZdd4rVmQWeDJkyelRIkSZqH+fXv27FnJly+fw88t/k2FvSMBEiABEnCEAISuZs+ercSm9+3bZ3X8DgkJkZdeekmN3y1btnSkCptpsUm0ePHiNtP4WyTmRAoUKCBBQUH+1jX2hwRIgARIIBUQOHr0qMBByJ07dwy91YtmGgJ4QQIkYJXAvKnrrMbpI5q0f1EyZg6Xx1HRkj1XJqnWoIzs3PiHPlrzfemcbTJ8YhfNOAaSAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAk4TuBP3d746tWrC0SwjC1LlizGt7wmARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARKwg0BaO9KkeBIIH/iSdevWzZea65VtxeFaCFflypVLZsyY4ZTwRZMmTZS4lFYHsdBk7GlFKw3Cfv75Z9m/f79FdIcOHSzCkgqA+MSLL75okiwuLk4mTZpkEubrN1dq1JCrdevKDZ1oyd2PP5aoRQslcd/vSvwKfdu/aKOvd5HtT2kCuv8bnrl22mYrrucuJrr/OGymYSQJkEDyEli1apUULVpUBg4cKLt27TIRz0ifPr00aNBAiV6FhYVJbGysrF69Wtq0aSNVq1aVczohRVfs4sWLgucCjMPR0dGuFOVTeZ88eaLEQMuVKye//fabT7WdjSUBEiABEvAOAitWrJAiRYrI4MGDZc+ePSbjd4YMGdTYjTE8NDRUYmJiZOXKlUq8sobudyE2erpi53XCyhDUwmZRPBukFoNAaOPGjaV8+fKKeWrpN/tJAiRAAiTgHwQwpw/nEcbiV+hZ4cKF1fOCf/SSvSABzxHYv/OUnDh80WYFwSGB0qZHLZM0LTpXk7RpbS/5HtgVKecir5jk4w0JkAAJkAAJeDMBPFvGRMfZ9UpMfGLSlYSERLvyxcbEm+TjDQmQAAmQAAmQAAmQAAmQAAmQAAk4QqBnz54W4leZM2eWPHnowNcRjkxLAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiAQ4AsYWrRoIYMGDfKFpkpERIS8/vrrPtFWb20kDnt+//33qnnjxo0TV7ygjBgxQmbNmiUJCQkW3T127JiUKFHCItw44JtvvjG+VdeZMmWSF154wSLcngAcYt29e7dJ0rlz58onn3ziUj9NCkzhm4CCBSXh0iWrrQi/f8NqHCNIwB4Cme9ckZCYRzaTXoMAFo0ESMBrCEyfPl0GDBigxC3NG1WnTh356aefBOMr7NChQ1KhQgVDsr179yoRrO3bt0vx4sUN4fZezJ8/X9UNMaglS5akqgO3OPiI/rds2VJq164t77//vowaNSrJA5H2smU6EiABEiAB/ybw5ZdfypAhQzTHb4heLV26VDJmzKggYLw2FnyG8CJELDF+Q0DLUYMoNkS30uhEbVFPcHCwo0X4bPp06dLJggULlJBYzZo15cMPPxTMbYAFjQRIgARIgARcIQBnDBCsDAoKkpCQEFeKspoXjicw725uzZo1Mw/iPQmQgAaBeVPXaYSaBrXsWkMyZg4XY8GOHLkzS61G5WTr2kOmic3uls7eJsMndjEL5S0JkAAJkAAJeCeBh/ej5evxPznVuN82HRW8krIMmcLl9ZG+5ZAvqT4xngRIgARIgARIgARIgARIgARIQCQ+Pl45ygwMDPTYfkGsuWBPhLk1bdrUPIj3JEACJEACJEACJEACJEACJEACJEACJEACJJAqCOSvWERylyxgta8Pb96X4+v2WY1nBAmQAAn4hABW/vz5pVy5cnLkyBGv/8SmTJki2bNn9/p2enMDDx8+rLyhlCpVSnr06OFSU/HdqVatmuYC099//22zbMRv2LDBIk316tUFB1KdMYhPmBsWwBYtWuQ3wmmBOgGsGI0FPX2/w+/d1F/yPZUSyPtsdnn3s+5O9z5q+jR5sNR29uZfvCNtkhC4s12C6A4D+sQQmVQ3GE8CKU5g69atMnDgQE3xDDRuxowZBvEr3N+4YSmUePPmTenQoYMSx7J3DMb42q9fPyUABXEtjOnGwhyoKzVY5cqVZc+ePfLSSy/JmDFjZNeuXbJ48WLJkCFDaug++0gCJEACJOAkgY0bN1oVv0KREIvWi1/h/tatW3gzsevXr0unTp1k//79dos3PX78WPr27SsLFy5UItHr16+XSpUqmZSbGm7wzALx7IYNG8q7776rrn/44Qclup4a+s8+kgAJkAAJuE4Av6PXrFkjW7ZsEcy3X7hwQR4+fGgoGAc+8uXLp4SmMe68/PLLbhlzs2XLpgS2ILalNwhuYV6ARgIkYJvA/p2n5MThizYTBYcESse+9SzSPH0i0uaVWrJ9/RGBCLw1O7ArUs5FXpHCxfJYS8JwEiABEiABEiABEiABEiABEiABEiABEiABEiABEvApAvfu3VNrIr/++qvaX4g1EYTpLSAgQPLkySPFihWTKlWqSKNGjZRDLziXdMWw/oF9iXfv3jUUgzLffPNNwz0vSIAESIAESIAESIAESIAESIAESIAESIAESCA1EchbtpCUa1HVapevR16iAJZVOowgARIAAZ9R94CHcL0AFoQPICSEhYiIiAhxdQHCHV8FtAFtwqIIzTUCO3bsUAUMGzbMLZ9tnTp1NAWwEhMTbTb0559/Fq00L7zwgs18tiJxcBffX/NycZD19ddft5XVZ+ICCxWy2dbw+5bCJjYzMNLvCKRLl1bCI0Kc7teD3b/ZzJsuRw7JVLm8zTSMJAESSB4COHCHQ67WDt7hsC2e5/QGz2tDhgzR35q8Hz16VH755Rdp3ry5SbjWzdWrV6VFixZy4MABtclk8+bNUqFCBa2kqSIMgqB4vqpfv76sW7dOiYNCUAQbe2gkQAIkQAIkYE4Av1dtiVcWLlxYChn97oPAhbXx++DBg0qE0p65kkuXLqnxGyIdWbNmFYzfEENPrVZQJy69c+dOqVevnqxatUpq1KihxvFnnnkmtSJhv0mABEiABOwggHF03LhxsmLFCuXd3FoW/P4+f/68euG3NgSTn3vuORk0aJASkw4JcW7uLjg4WD766CMZPny4qhrrJtOmTRM8P9BIgARsE5g3dZ3tBLrYll1rSKYsEfI4KkalDQwKlJjoeImLi5dsOTNKrUblZOvaQzbLWTp7mwyf2MVmGkaSAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQgLcTOHXqlIwfP16WLFkicJZpzRISEuTixYvqBSeaWMfAvkXs2x8wYICkT5/eWlab4VgD+fTTT9W6in5/JNpTsWJFm/kYSQIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkoE3AZwSwIHbwySefqF789NNPdokfaHeZod5OAJ5XcufOLZ06dXJLU3FoVMsyZ86sFWwI27hxo+Ha+KJMmTLGtw5dh4eHq8M+p0+fNsm3Z88e5QEmqTaZZPLSmwArvPXNDXn8QJ48fixpw8L0QXwnAbsJPH36VKK3bbOZPrRuXZvxjCQBEkg+Aps2bZLjx49brfDZZ581iYM408mTJ03CjG8gApGUABbG2JdeekltWMG4i4O8qVn8Ss8ve/bsgmebWrVqqc8E7/B6V6BAAX0SvpMACZAACZCAIrB27Vox/81qjMZ8/IY405kzZ4yTmFxj/E5KAOvEiRMqzeXLl5XQOdqQmsWv9AAhdoXnKYzbEIXXj9958+bVJ+E7CZAACZAACSgCjx49krfeektmzZolmD9zxs6ePSuDBw+WyZMny9SpU5UwpTPlwLFFw4YNBULWL774ohQtWtSZYpiHBFIVgf07T8mJwxdt9jk4JFA69q2n0gSHBEnM4zgJDAyQ4OBAiY2Nl8ePYqTNK7Vk+/ojVsXokfnArkg5F3lFChejMLpN4IwkARIgARIgARIgARIgARIgARIgARIgARIgARLwSgKxsbEyatQo+fLLLwXiVs4YHHSNHDlSpkyZotZFunbt6kwx0qdPH6latars379fypcvL66cMXCqAcxEAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAn5EwGcEsCpXriw5cuSQxMTEJIUP/OjzSZVdwYIUPKIEBQW5pf9Zs2bVLCd//vya4fpAiEJoWYkSJbSC7Q4rWbKkxWFifK9RX9u2be0ux1sTBhYqlGTT4nUiZ8GlSiWZjglIIE53kP6uzttSYJEi6qUbBOTJnTs2wYTWqWMznpEkQALJRwDe1WwZBKqMDV7ZbNmdJP7+kb927dpy48YNgYe1hQsXqg0mtspMTXE5c+YUCIpUqVJFzp8/L/Xq1ZPffvtNIK5BIwESIAESIAE9geQevyGWWUf3DH/r1i1Jly6d/Pjjj4I5INo/BCAQDkFPCIhAaEw/fkPckkYCJEACJEACIADhqqZNm1rMOTtL56+//pKWLVsqr+eYq8f47KjhkAdeNBIgAfsIzJu6LsmELbvWkExZIlS6dOnSSmhYsDyOipHQ8BCJi0vQHfRKlMxZI6RWo3Kyde0hm+Utnb1Nhk/sYjMNI0mABEiABEggpQmkzxgqA99rY1cz1izeLRfOXDOkrVqvtFSslrQQa9o0aQx5eEECJEACJEACJEACJEACJEACJOD9BK5evSrNmjWTQ4dsz4Ha25O///5bunXrpvbUfffddzqHA8H2ZjWkw7kAvGgkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAKuEfAZASyIGDRp0kR5yHCty8ztCwTcJX6FvkJcytzS6DYyVqhQwTzYcH/x4kWxJrJRoEABQzpnLgoWLKiZbe/evf4hgGWlf8adTtCJblAAy5gIr60RiDtyRB7On28tWjM8tG5dzXAG+i6BTSv3y5WLt7yqA9lyZpSmHap6VZu8sTGbN2+22ayAANNH0YiIfw7xWctUuHBha1Hy559/Sv369ZX4FRKNGzeOoqkatArphCohLAJWEMF6+eWXZceOHZIUe42iGEQCJEACNgns+euI3H5812aa5I7MHJpRqhWgEENS3B0dv9OnT2+zSFvjNwQ7MCZB/AoGMezGjRvbLC81RhYtWlQWL14sjRo1UiJYmB/btm2bhIWFpUYc7DMJkAAJkIARgdOnT0vNmjUNv4WNoly+nDZtmmCefPny5RIYGOhyeSyABEhAm8D+nafkxOGL2pH/HxocEigd+9YzSRMaHiwxMXG6sCc6MawgnRhWrETrXq171JTt64/IkydPTNIb3xzYFSnnIq9I4WJ5jIN5TQIkQAIkQAJeRQB7OiLSh9rVpoBAU9HWoKAAu/PaVQETkQAJkAAJkAAJkAAJkAAJkAAJpDiBK1euSI0aNdQ+QXc3ZsGCBQIHIXAuae7U0911sTwSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAFtAqaqA9ppvCa0VatWFMDymk/Ddxpy//59i8ZWqlRJMmTIYBGuDzh69Kj+0uQ9W7ZsEhpq3yZLk4xGN9YEtA4cOGCUyncv0+XKJWlCQuRpbKyky51bEnPmlrMP08nDDNl0r+zy5Jk80rt2bd/tIFuerATidQfiHbG4DFkkqFgxR7IwrQ8Q2LzqgPy+/ZRXtbR4mfwUwEriE4GQJESpHLGWLVvKO++8I1FRURbZMP527drVIhwBDx48kKZNmwo8vMEgCjFs2DB1zX8sCdSpU0dGjRolH330kRw+fFh69eqlRLEsUzKEBEiABJwnsPfSEYm8ecH5AjyQs3CWfBTASoLr9evXDeNpEkkN0a1bt5YRI0ZIdHS0IUx/gY2hnTt31t+avN+9e1d5ZoVHVRieA4YMGWKShjf/EmjYsKF6vpk4caKaG+vbt6/88MMP/ybgFQmQAAmQQKoj8PDhQ/Vb+MaNG3b3PUQ3b5sxY0Y1Nw4hRfz+xvw5fsNrOZJYvXq19OjRQxYuXGh3HUxIAiTgGIF5U9clmaFl1xqSKYulcHxEhlB5cDdKQkKDJTYmQf0dZ8wULrUalZOtaw/ZLHfp7G0yfGIXm2kYSQIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQALeQCAuLk45w3RkP2JwcLBhTQR7Fx4/fqzWRLBXIT4+3qJbcCLZpk0b+eWXXyRdOlOhZYvEDCABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEnA7gbRuL9GDBeIwpDVhIg9Wy6J9nMClS5csetC+fXuLMOOAkydPGt8arnPkyGG4dvbimWee0cx6+vRpzXBfC4Qn1vwnTkhh3eHngpcvS5oFP8mOer3lcKUWcq5oVbmVr7iktSE+5mv9ZXs9SyDuzBmHKnhQrJxD6ZnYNwg8feob7WQrTQlYG0tNU5ne5dYJJy5dulTSp09vEoFDuf/73/8kX758JuH6m27duskJ3dgDy5Qpk3z33XeC8YhmncDo0aOlZMmSKgGYf/HFF9YTM4YESIAEnCDwlAO4E9RSPosz4zfG5yVLllh4QcUGUgg05dKJJJsbvh9dunSRyMhIFZU1a1aZNWuWeTLemxEYO3asFC1aVIVCiGTatGlmKXhLAiRAAiSQmgiMHDlSziYhHl+oUCElVLlixQo5f/68OtwBwUvMRUMQ+Yxu7g0CWhDC+v3332Xq1KlSrVo1E4yLFi2SKVOmmITxhgRIwD0E9u88JScOX7RZWHBIoHTsW08zTWBggAQFB6q4sPBg9R4bGy8tu1aXtGltLwEf2BUp5yKvaJbLQBIgARIgARIgARIgARIgARIgARIgARIgARIgARLwJgLjxo2TQ4dsi/7nzZtXhg4dKsuWLVPrIBC8gkMurIVgTQRrI7jHmsjBgwdlxowZUq9ePZN9hhs2bJAxY8Z4U9fZFhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARJINQQCUk1P2dFUS8BcNC0oKEgglGHLrl69qhmdLVs2zXBHAq2VceXKFYnWiUaFhoY6UpxXpg0sWNAr28VG+R6BeEcFsIo+73udZIuTJEAdoyQReWWCv/76y6l2NW7cWG06gSgTxsY8efIoz2pa4hmo4KuvvpJVq1YZ6ho/frxYE5s0JOKFBAYGypdffikNGzZUNHBwGtelS5cmHRIgARJwCwEKEboFY7IX4uz43axZM8P4fe3aNcHG0rZt20rOnDk1+zB58mRZt26dIW7SpEmSPXt2wz0vtAnAQy1EK5s2baoSvPPOO1K/fn0pXry4dgaGkgAJkAAJ+C0BOH345ptvrPYP48PHH38sL774otU0xhEYYypXrqxeAwcOlOPHj8ugQYNky5YtKhl+M7Zo0UIgqEUjARJwH4F5U/99JrZWasuuNSRTlghr0RKePlTi4xIkMChAgnViWBDAypAxTGo1Kidb19o+ELZ09jYZPrGL1bIZQQIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIpTeD+/fvy2WefWW1G1apV5ZNPPpE6deqYiFlZy4B9c+XLl1ev1157Tc6dOydDhgwx7EGcOHGitGvXTsVbKyO1hcPJGZyy4HX37l3FGY7OSpUqpfZ3+goPf+mHr/BmO0mABEiABEiABEiABEiABEiABEiABEiABEiABEjAUQKpRgArPj5ejh07phYp/vzzT7l9+7bydo7wkJAQCQsLU2IJBQoUkCJFikixYsXsWgRxFDjTJz+B3bt3m1QK8StrIhr6hNevX9dfmrxnzJjR5N6Zm8yZM2tmw6IK6i1I8ShNPgxMnQTidYuljtiDYuUcSc60PkJA998jzQcJwFuaswaxjAEDBiSZ/fz58zJixAhDOjy/9e3b13DPC9sEGjRoILVr15Zt27bpDkfGSu/evWXPnj2SNm1a2xkZSwIkQAJ2EMDvG5rvEXBl/MbvbAhlJGXwqvree+8ZkkF8sWfPnoZ7Xtgm0KRJE8EGXsx1QES7T58+snPnTs5h2cbGWBIgARLwOwLff/+9JCQkWPQL89fz5s2Tli1bWsQ5EoADC7/++qvgkAd+d8fExKjx+4cffnCkGKYlARKwQWD/zlNy4vBFGylEQkKDpGPfejbTpE2bRsLCgyXqUYyE6t7jdGJY8QmJ0qxTVdm+/og8efLEav4DuyLlXOQVKVwsj9U0jCABEiABEiABEiABEiABEiABEiABEiABEiABEiCBlCSwaNEiiYqKsmgCHE7DWUj37t0t4hwJKFy4sKxcuVJmzZol/fv3l8TERBk2bJhs3LjRkWL8Mu2JEyeUk64VK1bIrVu3NPuIczc4m4H9ItbOSGhmTMZAf+lHMiJjVSRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiSQIgT8WgALhyp/+uknWb16tezfv18d0rCXMg6KVKlSRZo3by6tWrWSvHnz2puV6byIwJEjR+TSpUuGFgUFBQm81SdlEEjTsvDwcK1gh8JslXHz5k0KYDlEk4n9mcCTR48k8do1u7sYFZ5JYrPntjs9E/oOgTRpfKetbOm/BO7du/fvjYeuXn/9dSX8oC/+nXfekYAAv3681XfVbe9vvvmmEsBCgfv27VObgv7zn/+4rXwWRAIkkHoJpOEA7pMffnKM39gwCuFFvWHjKMUX9TTse8f4rRf73rVrl8yePVsJYdmXm6lIgARIgAT8gYDWoQt42t6yZYuUKVPGbV0cPny4EtqCeOWSJUtk/PjxAiciNBIgAdcJzJu6LslCWnSpLpmyRCSZLiQsWLcGGi+i08UL1V0/joqRDBnDpOZLZWXbusM28y+dvU2GT+xiMw0jSYAESIAESIAESIAESIAESIAESIAESIAESIAESCClCGitiWAvPsLhPMpd9uqrryqHAtjTsGnTJjl8+LA8//zz7irep8qB87Q33nhDfvzxR9E7wCtfvrw6U5MvXz558OCBctT1888/y5kzZ+T999+Xzz//XCZNmiT9+vXzmr76Sz+8BigbQgIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIeJpDWw+Une/GYZF++fLnUrl1bihUrpryT79y50yHxKzT6/v37smHDBuWNAgc6IISFxQyabxGA1xdjGzx4sDz33HPGQZrX8GivZfAW46rZEsDC984Vw6FXfO8def3xxx+uVMm8JOAxAvFnzzpU9rXcxR1Kz8QkQAKeJRAdHe3RCrCBZf369YY6IF7atWtXwz0v7COAZ9zs2bMbEmMzyiOdACGNBEiABEggdRLw9Pi9Zs0aJcyhpwuhjo4dO+pv+W4ngdatW5t4ToUoiac/OzubxmQkQAIkQALJROD48eMWNc2cOdOt4lf6CkaMGCEVKlRQHs8XLlyoD+Y7CZCACwT27zwlJw5ftFlCSGiQdOxbz2Ya48jw9CHqFvkCAtLJE916aZP2VZIUmz2wK1LORV4xLorXJEACJEACJEACJEACJEACJEACJEACJEACJEACJOA1BLTWRCC25E7xK31nX3vtNWnQoIG6nT9/vj44Vb3D2QqEv+AYBedyQkJCZO7cuXLw4EEZM2aM9OrVS3AeA+JYJ06ckEqVKik+OAMBftjDGR+vc9iQwuYv/UhhjKyeBEiABEiABEiABEiABEiABEiABEiABEiABEiABJKVgF8JYGGiGpPobdu2le3bt7sN5JMnT2T16tXSsGFDqVevnhw6dMhtZbMgzxGAiNWsWbMMFUDIbPTo0YZ7WxfWBLDSpUtnK5tdcQEBAVbTxcXFWY2zJ+LOnTty+vRph16xsbH2FM00JJDsBBwXwCqW7G1khSRAAv8SwKaFhw8fGl5RUVH/Rlq5SkhIMKQ3zmvPeDhq1CiTUtu1a6c2W5gE8iZJAni2adWqlSHdzZs35auvvjLc84IESIAESMC/CTgzfpvn0Y/h9mxgNB+/O3ToIEFBQf4N2QO9CwwMlBYtWhhKvn79ukybNs1wzwsSIAESIAH/J4B5YGOrUqWKtGnTxjjIbdf43fjWW2+p8tauXeu2clkQCaRmAvOmrkuy+y26VJdMWSKSTKdPEBgYIMEh/zxbh4X/I4aVKWt6qdmwrD6J1felc7ZZjWMECZAACZAACZAACZAACZAACZAACZAACZAACZAACaQkgdu3b5tUX7hwYenXr59JmDtvhg0bpopbty7peVx31usNZWEdqFGjRoI9CLA0adIooatXXnlFs3lwSq4/v6NP8MMPP6g1q8TERH1Qsr/7Sz+SHRwrJAESIAESIAESIAESIAESIAESIAESIAESIAESIIEUJuAXAlgQWOjfv78Sp4J3CU8aJulfeOEF+eCDDwSiDTTvJfDtt9+KftELCzBz5syR9OnT29VgeCzRMncIYNkqwx7BD612MYwE/JFA3JkzDnXreh4KYDkEjIlJwM0EFi1aJBkyZDC87BFhwCYR4zz66+nTp9tsHYRO9+3bZ5KmefPmJve8sZ/Ayy+/bJJ4ypQpYk0M1CQhb0iABEiABHyewPfff28yFhuLSFvr3KpVq0zy6Mdv/Aa3ZZs2bZIjR46YJOH4bYLDoRvz8Rsebjmn4BBCJiYBEiABnyZgPsfcpUsXj/anWbNmkjZtWvn9998FDkNoJEACzhPYv/OUnDh80WYBIaFB0rFvPZtptCLDIkLUgaSAwHQ6ofhAwVpXo7aV1d+vVnp92IHfIuVc5BX9Ld9JgARIgARIgARIgARIgARIgARIgARIgARIgARIwGsImDue7ty5s0fbVrduXXXe4OTJk3L//n2P1uVNhe/cuVM5oTd2fjZkyBDBGpEti4iIUCJZ4eHhhmRwPD9w4EDDfXJe+Es/kpMZ6yIBEiABEiABEiABEiABEiABEiABEiABEiABEiABbyEQ4C0NcbYd586dExyYxCJDchmEr8aOHSvbtm2T5cuXS+bMmZOratZjJwEsOOEz0ts777wjWJCy18LCwjSTevpwT2BgoGa99gbWqFFDPvroI3uTq3QzZsyQK1d4sMEhaEycLATiz561u56HEVnlUfpsdqdnQhIgAd8mMHXqVJMO4OBvvXqOHwo0KUTj5vjx47J79245dOiQXL16VbJnzy5jxoyRvHnzaqQ2DYqOjhaIgv3666+CTSEtW7YUT2++MW2B/Xd4RoJYqF4A9ObNm7JgwQLp06eP/YUwJQmQAAmQAAkkQcB8/Mbv3zp16iSRy/HoY8eOya5du+Tw4cNy7do1yZkzpxq/c+fOnWRhjx8/loULFyoPpfBG2rp1a+nQoUOS+VIigfmzD/q6ePFi6d69e0o0h3WSAAmQAAkkM4EcOXKYzOkWL17coy2A4CV+E//9999y+fJlyZ8/v0frY+Ek4M8E5k1dl2T3nsmbRVYv2pVkOq0E8fEJEh+boJvnEYmNjhG4e8mSI73cum77oNbSOdtk+ATPiulptZdhJEACJEACJOAOAiXKFZAcuf7dt5OvYA53FMsySIAESIAESIAESIAESIAESIAEvIAA1kSwHq43T6+JQHAL6yDYO3hWt4+5YsWK+qr99v3OnTvSqVMnwZ5HvWXNmlXef/99/a3N92effVaGDRtmkh7nE7AvMTn3XPhLP2zCZiQJkAAJkAAJkAAJeIgAHKjjnDaet80d83moShZLAiRAAiRAAiRAAiRAAiRAAhYEfFoAC97GmzRpIrdv37boWHIEbN26VapVqyabN28Wew5SJkebWMc/BCBOcevWLXVTv359GTdunENorAlg4QCsqwYBNWsWHBxsLcqu8BdffFHwcsRWrlxpcljKkbxMSwKeJBB/5ozdxV/PU8zutExIAiTgGQLw4GV8APbu3bvy8OFDm5WFhoaqA7TmiXCw1pqh3FWrVplEly5dWnldMwl04gbjPAROf/zxR1mzZo1cunTJohTEQ1jDlmjlli1bpFevXnLx4kVDfghS4JAwRDm9zTJlyiTFihWTU6dOGZo2a9YsCmAZaPCCBEiABPyXADxxGo/f2Az46NEjmx22Nn6nT5/eaj78Pl+7dq1J/PPPPy8oy1XD+I2xVz9+awk879ixQ44cOSLmnmGN6964caMa+4zHf4hZQgTzzTffNE7qFdcQISlUqJCcP3/e0B6M3xTAMuDgBQmQAAn4NYHy5cubzOlmyZLF4/3FQQcIYN24ccPk+cHjFbMCEvAjAvt3npITh/+dL7LWtT/PXJc5X5o+P1tLqx0O2SuY/h3XafCPVTvwW6Sci7wihYvlsZqGESRAAiRAAiTgrQRKPv+stzaN7SIBEiABEiABEiABEiABEiABEnCRANZEsN6vt+RYE8mW7R+HvHAkmRrs9ddfN1l3Qp/79+8vtvZxmnMZNGiQTJo0SaKiogxRKLdhw4bJ5mzeX/phAMgLEiABEiABEiABEkgmAtg3jD29EMCCY1uclU6bNm0y1c5qSIAESIAESIAESIAESIAESOBfAj4rgHXgwAFp1KiR3Lt379/epMAVhAIaNGgg27dvF/1iRwo0w2aVEIqYOnWqzTTJHZkmTRoZPXq0R6rFwVd9f3EYFAdWHVWetnYINzY21uU2x8XFWS0jc+Z/vZJaTcQIEkglBBwRwLqWmwJYqeRrwW56MYE2bdoIXnqDUMSXX36pv9V8h0iluZiVZkKjwJ9//lnMx+MKFSoYpXDucu7cuTJixAh1kPe5556TfPnyqedMcxGv06dPy9KlS6Vz586aFc2ePVtee+010RK8nDNnjlcKYKEjlSpVMhHA2rt3r0RGRiphLM2OMpAESIAESMAvCHTs2FHw0hs2A06fPl1/q/n+8ssvy7JlyzTjrAUuX75c4uPjTaLdMX5/++238u677yphdIzfBQoUkPv371uIeJ04cULwDNG2bVuTNuhvvvnmGxkwYIBoiV7PnDnTKwWw0HaM38YCWBD6wj3mQmgkQAIkQAL+TaB58+ayevVqQyf1ziAMAR640P8WN/b+7YFqWCQJ+DWBef9dn8z9g+iVsQiW7eqXztkmwyd0sZ2IsSRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiSQjASwJoK9fXpLjjWRmJgYVV1qWBPBPkE49zS3vn37mgfZvMcZiHbt2sm8efMM6eDkfuzYsTJlyhRDmKcu/KUfnuLDckmABEiABEiABEjAFoGTJ08q8Suk2bp1q3L8buxc11ZexpEACZAACZAACZAACZAACZCAOwn4pADW5cuXBQcukxK/gshSqVKl1IFAHKzEAUCIGeTMmVPCwsJEL3KEgxt4QSjq+vXrcu3aNYG4wfHjx+XQoUOCH3G2DPGtW7cWCC8FBHgfUiwevP/++7a6kOxxUIH2hADWnTt3pGfPnvL06VPJnj27rFu3zilhsvTp02syMfZKopnAjkCoYlszbxJRgydyd9iN66YidfFxCeKusrNkzyCFiuV2RzNZhpcReKL7O0nU/X9sr12nAJa9qJiOBHyewJo1ayz6gOc9Vw2iGRCoqlatmmTMmFEVh80y1atXV8+FxuVbE8CCGMirr74qT548MU5uuMYzqLeaFkP0B6IiNBIgARIgARJwlYCnxu8iRYrI999/r8ZvvefRGzduSNWqVU2EodB+jN9aAlgQzf7Pf/6j5hG0+vnMM89oBXtFmNb4DbGxt99+2yvax0aQAAmQAAl4jgAELCHijDUN2J49e6Rx48YeqxDz7VevXlXlh4SEeKweFkwC/kzgz7PX5cShP5Opi8bCV/prCGHh2rod3HVa7t5+KJmzaq+RWc/JGBIgARIgARIgARIgARIgARIgARIgARIgARIgARLwDIFmzZqp8x+XLl1SFUDoqHv37p6p7P9LvXLlirpKDWsi7733ngXLF154QZ599lmL8KQCsH5lLICF9HBINnLkSHV+J6n8rsT7Sz9cYcC8JEACJEACJEACJOAsAWOxK5y5zpUrl7NFMR8JkAAJkAAJkAAJkAAJkAAJuETA+9SakuhOfHy8OrB48+ZNzZQQvWrQoIFgAr1p06Ziz0FFiFaFh4dLlixZpHDhwhblQhRr06ZNsnDhQlm/fr0kJiZapNm5c6cSCJg0aZJFHAOShwA+l06dOslff/2lPs/Vq1cLDsM6Y7lza4sq2RKvsreehw8faibF9zBPnjyacckd+OTJU/npfzs8Um1MdJzbyi5buTAFsDzyKaV8ofFnz9rdiAcZsktURBa70zMhCZCA7xLAgdvNmzdbdADiVa5ajRo1LIqAMCUEoCCuaWxoA0SuIOipt8OHD0uPHj3UWD5//nwpXbq0QOzj008/lWPHjilhTm9+TixWrJi+K4b3VatWUQDLQIMXJEACJEACzhLAmAnBcHNzx/hdu3Zt82IlR44cShCkX79+JnGY18GzBOaN9LZ//37p1auXFChQQDB+lyhRQlauXCmfffaZnDhxQm3AnDx5sj65171bG78pgOV1HxUbRAIkQAJuJwDh5g8++EAGDx6sysY4NmbMGJPfqe6sFM5C9F7O9aKT7iyfZZFAaiBwIfJaMndTL3xlXK1tESw8L1++cIMCWMbIeE0CJEACJEACJEACJEACJEACJEACJEACJEACJJCiBIKCgmTChAnStWtX1Y7FixerPXl6R+jubhzOjcBZO8yaQ21315lS5Z05c0adkTGvH47qnbE6deoIRMNiYmIM2XH93//+Vz766CNDmLsv/KUf7ubC8kiABEiABEiABEjAnADOZf/xxx9SpkwZwXO23uDovWzZsiquYcOGEhgYqI9S73/++afgmat+/foe25tkUiFvSIAESIAESIAESIAESIAEUi2Bf0/t+wgCHEL8/fffLVqbLl06ee211+TUqVOyYcMG6dOnj13iVxYFaQRARKtbt25KxAALGm+99ZZAzdjccCjy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gARIgARIgARMBCiAxa8BCZCA3whEOBHAwkCykpP9Nh52lLcE0taudTiAgwll5Up4hMMyzCQBEiABEiABEiABEiABEiABEiABEiABEiCBYCRQsGBB87APHDhgPg7Eg4yMDDl79qwamuW4A3Gs+WlMltFsMe8XX3zRpeknJCToOrHC8dUTmz9/vl01RA8OC+MSpB2YEEqIjY+WMJMQVlhYAZMYVpSaWdqlDIGoH40ESIAESIAESIAESIAESIAESIAESIAESIAESIAELAngubRmgb4mgmecx48fV8O1HLc2fr6TAAmQQKgTyDi8WTKSNzieZniUJDQZ6LiMKTcqqabEVGvluNyVHElZN9lxGebaEejfv78Yrd1D6Obdd9+VDz74wFxv6NCh5uNgOIDI1JAhQwyHOmHCBJk2bZo5v3nz5lKlShXzubcONm/eLD179jQLiaFdCBihv2CxunXrKjE0o/HOnj1bpkyZYpQdkunVqlXTnZcmcqab6YXEl19+WTZt2iRt2rQRW+GhZNNeyn/++cfjXiDydOutt8qvv/6q2ihdurTUr19fatSoIRER1vvy/vzzT7n99tsFQdeCwV544QUZM2aM3VAXLlwo3bt3F0ciWP4Qv8LAlixZYh5f5cqVzceBdJCSkmI3nIoVK9qlMYEESIAESIAESIAESIAE8poAvc/z+hNg/ySQjwhEuiKAZRGxPh+hyXdTzTI9KMt28rDsQKFy+Y4LJ0wCJEACJEACJEACJEACJEACJEACJEACJJA/CFx33XXmif7888/m40A8gANeTk6OGlrZsmUDcYj5bkznzp1TTsvaxBGxF1FnXbVhw4ZJYmKiVfHVq1dbRci1yjQ4+f333wVOv5YGR83evXtbJvE4RAkUTIxVM4uJjZbw8HDJMW0MgwgWjQRIgARIgARIgARIgARIgARIgARIgARIgARIgAQsCViuLfz000+WWQF3vHPnTjl//rwal+W4A26gHBAJkAAJ+IhAypqJTluOr9dNIgolOS2HAoktBjstl7pjkWSfPuS0HAv8R6BIkSLy6KOP/pdgczR8+HA5ffq0Sm3WrJnUq1fPpkTgnz700ENSrFgx3YHCx+Krr74y5/Xp08d87K2Dy5cvS69evQQBwyztkUcesTwNimOM2dG4IZC2YYMT4bugmKlrg7z++ut1C548eVI33RuJ+M6OHTtW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QCixQoUK8ueff1okHjWML/WV36RFw+L5SokSJSR58uRi/js7deqUOm8FYkwwLUYJQADL2vDboJEACXiXwOO/fzQVmKTgq64LYF3YJQ83jDeVEZMLAalzuCyA9ezOWZv21s8gUr/mvyJg0dn+8Gcv5Ms/7snsPY9lwO935OUciSV7akupnKbFgyVk8wM5e+dfEWBH7alTp478/PPPonft6Cg/00mABLxPwN/7RbJEEiABEogeAn6GGRuCCha0WzgEsGiuEXhhUMYP27TJbqZgw0Wcn+GhEI0ESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAE3CGwec0Bd7LFaJ7Nf/h+G2MUCCsjARIgARIgARIgARIgARIgARIgARIgARIgARIgARKIVwTSpk0rv/32m0yePFmSJEnik9vWqFEjOXjwoFD8ynL3ZMuWTdq1a2fpNFsbPXq02dq/QjwQPIJVq1ZNypUrZ5HuiyvvvvuupE6tPXj+77//lu3bt5uaDVGiixcvqnU/Pz/p3LmzKS06FvCdhBgSWEL8qkCBArJu3TpZtWqVTwntBAcHKzG0lClTamKAMFZ0s9KsWMMZFRWl4RW5ffu2pt8Vp97247viivXs2dMm/OnTp0oowToBomwQwIJ16NDB6WNsWFiYymP+78qVK+arXLZDQEvkZ8uWLXZyMCm6CEDIEIJk5obfy65du8xdXI4hAiVLlrQRO/znn38k3DCWmEYCJEACcZFA4kR+8nWDNFI1TxIJN1xGTtvx0GYz/A3Xer2rp7LxWzsgzDhq1Cj5448/KH5lDYfrJBDLBKhmEss7gNWTAAm4RiCwSBG7GSIpgGWXj1Zi2ObN8kLjYZl5bLDhIS2NBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABNwl8ODeE3ezxli+uNDGGIPBikiABEiABEiABEiABEiABEiABEiABEiABEiABEiABOItgR49eihBimLFivnMNiZOnFgmTpwov//+u6RLl85n2uVLDenfv7/oCfjs2bNH1q9fr5r74sULCQkJMTW9X79+pmVfXkiePLm89957uk0cP368KW3atGmm5VdffVXy5s1rWvfmwrNnz+TTTz+Vl156SdauXauKhhDZ/v37BfX6okGc68cff9Rt2rx58+Sbb77RTY+pBIh1adnVq1e13C75cDzRsmTJkmm5dX0NGjSQPHny2KRPnz7dxgdBtEuXLil/165dbdL1HFq/6VOnTumF029F4NGjR1YekU2bNtn4vOl4/PixTJ06VXCspf1HACJwoaGh/zn+v7TZMHbVWxYZGSkQOJsyZYoMGzZMhg8fLj/88IMSzuT+sKR89+5dCQoKsnBCcM+bgmQ4R+7YsUO+++47GTp0qIwYMUIJBJ49e9aiXq6QAAmQgLcI4Lqpa8UUqriNZ55qFtu0eLDkTZdIMw3OfPnyybZt22TAgAG691a6mZlAAiQQ7QT0f73RXjUrIAHPCERERAjU20+cOKGUvZ88eSJp0qSRTJkyKeV0nIBiwnCRjodWUHKHujZUifHALWfOnFK+fHkpWLBgTDQjwdQRVLSo2OsaH3HsWIJh4a0NDTUolDqy4Lp1HYUwnQRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgAR0CeQpmEV2bjyqm+4LCXkNbaSRAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQEIgULx4cSWC0LdvXyUkEZvbXKRIEYEoT8mSJWOzGT5fN/bZa6+9JqtWrdJs65gxY+SVV16RFStWiFFAJ3/+/NKoUSPNeF909urVS4kzQeTE2n777Tc5f/68EvRYuXKlKblLly6mZW8uQFinefPmAmEjo7Vt21bmzJnj84Pl0W4Ipo0dO9bYdIvPgQMHKlGvmjVrWvhjciVjxoya1Z0+fVrT74ozPDxcMzxr1qyafj2nv7+/dOvWTcDL3I4cOaKEE6pUqWJAhvqMAABAAElEQVRyz5w5Uy1XrlxZXBEXTJo0qakM48K1a9fk1q1bkiFDBqOLnxoEIKw0YcIEmxSIWkCMCfvPmwZxp8mTJ8vo0aPVeOZOnTpJokQcom9kjHMQxJCsDYJVgwYNsna7tH7v3j3FHUJLDx8+1MybO3duVQ/OCd7e95oV+rgT309wszb8bqpWrWrtdmkdY+ohpIi/GzduaOatUaOGOgeVK1dOM51OEogTBAxiSzTfI1AwY6Bq1LWHUZqN8zfst97VU0nv3+7YpLdv316+//57SZHiXxEtmwA6SIAEYp0Ar65jfRewAa4S+Oeff9TNCh7WQXFWz3DD0rNnT4FidsqUKfXC3PZfvnxZcFOGh1ZQw9UzPLBAO3DDYK2Yq5eHfn0CEMCyZ8/v35dnhoc8ibKwc7o9TuZpjgSwgkqUkEQuPuAzL5/LJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJNCobRVZOnerhD3R7nDuC4Rad6rlC81gG0iABEiABEiABEiABEiABEiABEiABEiABEiABEiABEggRghAeAUDgOvUqSOdO3fWFEqI7oZAqAICJsHBwdFdVbwo/6OPPtIVwFqzZo0cPHjQQhCmT58+cUoIJFu2bPLGG2/I7NmzbfZXVFSUhISESLp06QTiG7D06dNL06ZNbWI9dUREREjjxo1l48aNpqJy5swpM2bM8HnxK2ODR4wYIRiHCbETawO/1q1by969eyV79uzWyTGynjdvXs16Dh06pOl3xQnxMi2DIJyrhjGhX3zxhTx9+tQi67Rp08QogAWRmaVLl6r09957zyLO0UqmTJk0Q/Dda9WqlWYanSIPHjyQt956SwldpUqVSq0budw3jC89fPiw10QVse+nTJkiI0eO1BX7MdadUD9xLPnss88E+wICVS9evDCh8FSQDMcEHI8hgGjPkA7BuiVLlsj8+fMlderU9sLjdRq+r8uXL5c0adLYXNtBkMwTgzgf9sfOnTvtFrNp0yaBICDO523atLEby0QS8FUCASm0z9G+2t6E0q7AgH+FySKi/jvXWG970+LBErL5gZy98+89Q/LkyZWIJa4daCRAAr5NwLsStr69rWxdHCeAm1Jc6FaoUEEWL15sV/wKm4obFjzUgwDV+vXrvbb1UJCH4nC+fPlk4sSJSvwKyxDD2rBhg2zdulV+/PFHqV+/vqoTit49evRQN8y7du3yWjsSakGOBLDAJeKob88cHdv7LvxphIQ+ear+Hp0+KxGGBzr2LLhePXvJTCMBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABhwQyZU0jX33XSVKlSeYwNqYDAgMDpPeQllKuauGYrpr1kQAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkECsE2jevLkcOHBAqlWrFmNtgTDFggULBCIyFL9yHnvNmjWlfPnyuhkgvmMcRwfhi44dO+rG+mpCv379dJsGASp8Z4z29ttvS1BQkHHVa58QcjEXv0LBAwcOFIjGxRVLlCiREoHJnDmzZpMhYoLffnh47Exe89JLL2m2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v8LMBrg5c2S4gf7000+VSi1icaEOJVtcyFesWNFRdqZrEAgqWpQCWBpcXHa9eC5ZLx+zmy2p4fsL0TEaCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZBA/CWwePHiGNs41IXxXwnVMFbNaObLRp+9Twiy9O3bV3r27Kkb1qZNG8maNatuul7Cs2faA8z14qPLX79+fSlqGD+mJWRkrNNdsSFjfr3P9evX2yRBAEtrLKNNoMGB8Y2emlE0xNNyjPn79++vxHyWLFlidFl8QpimVKlSUqFCBQt/dK90795dUwBr0aJF8u2330qKFClcbsK9e/fk4MGDFvn8/f0FAlCeGH5vHTt2tCji8OHDSnQHToj0lChRwiLd2ZXXX3/dRgALeXGchMjTG2+84WxRFnEQwkGbsf0QAorrht9Fhw4d1G8MgmF58+ZVm4RjnbUo4NWrV5VIIPxaBoExo7iVVrr1vsyfP3bESbTa5iu+mzdvyrvvvivJkydX31+j0BQEsKwNgkt6ZhStsj7GjhkzRtKlSye//fabjfiVdVn4fZ89e1aQx2h3794ViDbVqVPH6IrXn9jWUaNGSbFixWTixImmbcX+wPHU3LA/9I4Jer8NjOvHcdCe+JWxDgjI4XwCETnY48eP5fbt25IhQwZjCD9JIE4RCEieXlLX/SRa2pysbFvBX1yxJHkqS+Zef+k2d7fhuqVTp07i6v2NboF2EqCz5UhcL+zQMoms8YEEpte+HrBTPJNIgARiiYB/LNXLaknAKQIDBw4UPAjATfbw4cOVYm+9evV0HxilSpVKPv/8c/njjz8kKCjIpo5169bZffBlk+H/jg8//NDmwcfQoUMFSuPOGLYjS5YsptCIiAhp166dPHz40OTjgvMEIIBlzyKO2Rd1spc3IaWlv3VBkthRmwWLYMPvjUYCJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJBC/CcSkANbx48fdGuMVX/bAnTt3TJsCkQ5X7Z133pH06dPrZoNAljsWFhammQ1j4WLSIIRibxtSp04tLVu2jJYmnTt3zqZcDOKHsJIzduTIEc2wyMhITb+W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mdGzOgx9W8OOrfasjHicG/fl5v1/U2RILenzZJbAJEGmrV69erWEUV/CxIML3iVAASzv8mRpPkAAB1XMGHD58mUJDAyUiRMnSkhIiEstO378uGZ8xowenhQNpWbKlEmz7JMnT2r66dQnkLh0af1EQ0r4/v120xNiYtjWrRIQGW5304Pr1dNMT3/ttGS8cVYKH90kVbbMkaiWDeRsihRyoVgxud6+vTycNUszH50kQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQALOE0Cnn/aGZ65Pnz6V1157zW7ncedL/Tfyp59+EpRvbfXr17d2ObWO9lkb3rGsWrXK2s11EiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABHyOwcuVKCTdMtO6MdevWTYlfQQTLkSVJkkQmTZoky5Ytk3Tp0jkKV+mLFy92Ko5BJEACJEACJEAC+gQgSGEUocSkewEBAfrB8SDl1KlTMnToUAkODlZims5ed8TUpmPc+MiRI1X7MKlgfLbs2bPLoEGDBH3HatSoYbGpWw3jmg8cOGDh4woJkED8I/AiKjJObFTUkztxop2x0UjjfXnaXJmk5diu8ua0PtLqm27S8aePpHLHuuIf4C+hoaECESwaCUQHAQpgRQdVlhlrBKKioqRHjx4yb948pda7e/du6dWrl8vtuXr1qmYea+VZzSAHzgwZMmhGoE4c8GnOE0hcqpRNsL9BHThp7dqSul8/SW+4QTdXmbQJToCO0D/+cLjVegJYaW5esM1r+M1FHj0qj3/5RZwp27YAekiABEiABEiABEiABEiABEiABEiABEiABEiABEiABMwJfPrpp7LfMMED3knMnDnTPMnjZcy8Z22pU6eWEiVKWLudWq9QoYIkTpzYJnYWJ0ywYUIHCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACfgaAePgVnvtSps2rRKU+P777yVp0qT2Qm3SGjdurMQOatasaZNm7di0aZPcvn3b2s11EiABEiABEiABJwk8e/ZM2rZtK8eOHZN27dpJs2bNnMwZN8OMkwzic8GCBVKmTBmf2pCbN2/K66+/rsaNQ4wsV65cPtW+6GoMRMiWL18uBQsWtKjir7/+sljnCgmQQPwj4JfIti+pL25lYAbL45MvtjE22nT9+nXZtm2b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YNS30NL4+g2BKQw+wwoUKCB9+/b1NUu3j3/nnXdU+21f5vLly5UI1ty5c6V9+/ZKDMs+0yFDhsigQYPsg33eN2q/K1as6HO+/szAqP0uUqSI5M6d25/FMC8SSPQEsPr8iRMnrByuXbsmYWFhSszqn3/+ETxr7NmzxxrvbGOrZYywfv366hkt1ET1nNWbcSRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQHwncOx8uEfPmO616tGVeb+TCRZKz1YtO0zGSBEiABEiABEiABEiABEiABEiABEggYRC4s22mXP9jsOnJJE2fQ/K+v8M0nhH/TwB+svaGufeDBw+Wjz/+WJIm9Uzsyj4vs/02bdoo/9zWrVsrXyt9uhs3big/rVq1aumDuU0CNgQCc2XaFMEdEiABEogjApaGONIisnHjm2/k3t9/S1I78SvUCgJY8dWSPYiR3OGHnVb/fIHS8igZtQ6dQmIkCZBAwAisWLHCMG88KNWuXdswLliBEDnKmjWrTXF4gAoPD7cJ83Vn586dMnDgQClRooR89913NtkZCWVs3LjRJo22c+XKFWnUqJEYiSloaeLDd48ePWTcuHEOVV21apUULlw4qOJXqAQERfQ2duxYuXDhgj4ozrePHz/uUIeGDRs6hDGABEiABPxFwKz9hnijUdvlr3LdyadGjRqSMWNGm6SXL18WfPxpEEEZOnSolCpVyqHdMmJg1n5DqKmxZRXKc+fO+VS9OXPmyKVLl1QeEywrX6ZJk8an/Dw9uE+fPjJmzBiHwzAgjvZ74cKFDnGBEr9CQfbt99dffy32olgOFQpyANvvIANncSTwLwEIV5UrV07w4mz06NGye/duOXv2rAwfPlzdr1yBwvNbq1atBOLENBIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggcARCLPMgYm1m8NpVFrYuPES+/ChURTDSIAESIAESIAESIAESIAESIAESIAESIAEDAikTJnSJrRQoULy559/yieffBIw8SutwKJFi8qmTZvkww8/lCRJkmjB6tu+XjaR3CEBCwEKYPEyIAESSLAE0Cimfvppp+d3Lx4LYOU5f1iSP3TukHeuYFmn589IEiABEggkgd9//90w+woVKkimTJkM44IVCBGupk2bOhS3b98+hzBv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YefOndZS8ufPrzp16mTZZUVHR+vo0aO6ePGi9TlHkSJFVK5cOT7zyLLfMSaGAAIIIIAAAggggAACCCCAAAIIIIAAArlJgACs3PTdZq0IOBW44bSFowaXx46Vbjge41DDTqrnyb96HDlShwACCCCAQEYI7AsJtoKvzNfUbKUKFFN3W/BVg1K1CL5KDVwWb+vp5aEe/Vuqa+9mmv/Ten0/fpnOnw11Ouurodf17acLNGPCCqt/7/vbqGhxf6f9aIAAAggggAACWV8gn3de3Vm7kzpVaaEl+9fq98N/KSom2unEz4Zd0KRNszR3zwp1tPVtUa6B8nh6O+1HAwQQQACB9AuYkKv58+dr+fLlWrNmja5evWoNGhAQoC5duujZZ59V06ZNVbt27TSd7Pr16xo/frzGjRunffv2pThG6dKl9cADD+iZZ55RoUKFUmxHBQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCORsgYcfflg//fSTtcjAwECtX78+Sy340qVLmjBhgmbPnq2///5bERERdvMzoV3t2rXToEGDdNddd8nDw8OungMEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBmyNACs3NceYsCOR4gdjQUIVOmeJwnTHunjpUv4PDNlQigAACCCCAQPoE9p49pAX//K4D54+maqAyBUtYwVf1S9bgiWapkstejb28PdVrUBt1vztQv/2wTj9+vUwXz//vhmlHKwm7Em6FYP0yeZU6BDVSvwfbq3KN0o66UIcAAggggAAC2UTAL4+v+tS5TZ2rtNTifWu1JvhvRcfGOJ19yLVL+mn7Qs3bs1JtKjZR+0rNVNAnv9N+NEAAAQQQSJ1AWFiYdWPG1KlTtXnz5vjOnrYHTfTr188KourcubPMcXq2H3/8UU899ZTOnDljDVOgQAE99NBD6tixo/z9/XXixAktWLBAP/zwg7X/n//8R19++aW++uorDRgwID2npi8CCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghkQwHz2UVc+FVWm354eLjee+89ff755wq13edgNvNAkb59+6ps2bJWmXnYyM6dOzVv3jzrVb16df33v/9V+/btrfb8DwIIIIAAAggggAACCCCAAAIIIIAAAggggMDNE0jfX8PfvHlyJgQQyOICoZMn68b/f9p8SlM9VKWpIn0LpFRNOQIIIIAA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c+42U48AAggggAACCCCAAAIIIIAAAggggAACCCCAAAJKoLKyUq688kqpr6+XuXPnyi233NJtXNwFVOkTcHfNoi0nF6huPHM1uat31dZd3RlnnCFtCdLSX5+vvvrKXXfUI4AAAggggAACCCCAAAIIIIAAAggggAACPUIguEecBSeBAAJ+FSh54okW+4+7+eYW29AAAQQQQACBjhTILSuQHcf3mRvf9xQcEn0TvLdTclSCTFShVzr4anCf/t52x/4IIOADgYCAABkzcbAp/33XhbJt00H5TIVhff7Bt1J8orxdR8janye6vP6/n0lkVJhMmj5Cpp01Rk47c6QkpsS1q092QgABBBBAAIGOEwgJCpZT+maaUqvCr7bn7pVNx7bL1rw97fq7wCIW2a8CdXV5Z8enEhceY4KwdODWyJQMCQ8O7biT40gIINAtBSoqKtyOe86cOW63edqgb+y46aabRAdhNZ9eUQ+10CFY9jdhuAvA0jdN+GJy109YWJhX3aelpcntt9/epj7ee+89ArDaJEZjBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgd4o8Mtf/lL27Nkjffr0kb///e+iP4fVXabQUPfXaPXDQryd/HXdQ4/ro48+atPwysrKJDY2tk370BgBBBBAAAEEEEAAAQQQQAABBBBAAAEEEOhuAgRgdbevGONFoIMFqtaulZpNmzweNXTMGImcPdtjGzYigAACCCDgb4GK2irZnX9AhV7tN6FXRVWlPjlk35jkk6FXo2VAXKpP+qQTBBDwj4C+ufuUKUNNuem3P5Rvv95nwrC+/Pg7KS2ubNdBKytq5MuPt5qiOxg2qr9MnTVKldEycny6BAW5ftpfuw7GTggggAACCCDgc4HQoBCZ2H+0KTX1tbI1d48Kw9om2/P2SX1j+z74XFJdJmuyNpsSFBAow5LSRYdhjVUlLSbJ5+dAhwgg4FrgwQcflNJS3/zt7/oIba+97rrrJD093WlHTwFYw4YNc2rf2opLL73UZQBWYWGhbFLv60+ZMsXWlbsArNraWlsbbxbc9aNvmmFCAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBriXwzjvvyIoVK8ygli9fLv37d68HgsbExLgFraurc7uttRvc9eHpuK3tm3YIIIAAAggggAACCCCAAAIIIIAAAggggAACzgIEYDmbUIMA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AggggAACCCCAAAIIIIAAAggggAACCCDgnQABWN75sTcCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgh0oMD555/v8mjbtm1zWd/ayu+++86p6dixY2Xw4MFO9VQggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgh4L0AAlveG9IAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIdJHDKKafIsGHDnI62fv16p7rWVmRlZUlubq5T80svvdSpjgoEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAHfCBCA5RtHekEAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIEOEvjJT37idKRNmzZJVVWVU31rKlavXu3ULDg4WK655hqneioQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQR8IxBgUZNvuqIXBBDojgKHDx+WY8eOmaFHR0fLuHHjuuNpMGYEEECgVwsUFxfLzp07bQZTp06VwEByTm0gLCCAAALdQED/ab5u3TrbSEeNGiXx8fG2dRYQQAABBLqHwLZt26SsrMwMtl+/fpKent49Bs4oEeiGAtnZ2TJkyBCpra11GP17770nCxYscKhrzcoVV1whr776qkNTXffyyy871LGCAAIIIIAAAt4LHDx4UHJzc01HcXFxMnr0aO87pQcEEEAAgU4T2LNnjxQWFprjJyUlyfDhwzttLBwYAQQQQMB7gR07dkhJSYnpKC0tzbwH532v9IAAAggggAACCCCAAAII+E+goKBA9u7daw6gP0OvP0vPhAACHSewb98+yc/PNwdMSEiQzMzMjjs4R0IAAdm4caPU1dUZiYyMDElNTUUFAQQ6SKCiokK+++4729EmTZokYWFhtnUWEEDAvwI6H0PnZOgpIiJCJkyY4N8D0jsCCPhdgAAsvxNzAAQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEPC1wM9+9jP5+9//7tDtVVdd5VTn0MDFiv4Qgv7gj57bT2vXrpVp06bZV7GMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCDgQ4FAH/ZFVwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAhwj87ne/k9DQUIdjvfHGG1JUVORQ19LKP//5T6fwq7lz5xJ+1RIc2xFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABLwUIwPISkN0RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECg4wWGDBkiS5cudThwRUWFPP744w51nlbq6+vloYcecmgSEhIijz76qEMdKwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAr4XIADL96b0iAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAh0gcM8998jUqVMdjvTggw/KwYMHHercrTz88MOyd+9eh81/+ctfZMyYMQ51rCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCPheIMCiJt93S48IIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAgP8FCgoK5Oyzz5Zt27bZDjZp0iT57LPPJDY21lbXfGHVqlVyzjnnSF1dnW3TLbfcIo8++qhtnQUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAH/CQT6r2t6RgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQT8K5CUlCQ6zOrcc8+1HWjz5s1y1llnyc6dO2111gX9XJAXXnhBzj//fFv4VVBQkNxzzz2EX1mRmCOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACHSAQoG70sHTAcTgEAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIOA3AX254+mnn5a7775bCgoKzHF0sJUOxpoxY4bExsbKkSNH5L333pPt27fbxjFlyhR5+OGHZebMmbY6FhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAA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F33W99fh/qXrH2O1t/WKBetNGwSwzvjTTz/Jd999F1xn1AMthxJAUORq1appz17YU3THHXe4HjACNu9w2AhnPXgGxJ5/BEdu3LhxaFciPkZAZtjhwC4dweL27NljqANOaBEYDnuksO+1QoUKhus8IQEvCCxcuFDbF+1F3aF1wj9HyZIlQ5OUx7CNQPAtPLvinQu/PQQNc1L++PHjmq8QrLv98ccflt+Z3iiCM+rvcU2aNJG6devql/hJAmmaAB1gpemvn4NPdALw8HjkyBFts33u3LmlYMGCAmO8ZBO/jCPZuLO/JEACiUEALyzYoA7FAzzvQhnHKJiJ8d2wFyRAAiTglACUvHAee+LECS2SGIy8nRgfOK2f+UiABEiABLwngAVIzOVYBITTQhjlYD5PRoHDdIwDm5CyZs0qWPyA03QKCZAACZAACZAACZAACZBA7ATwnI21TeiBYLSYP39+Pm/HjpU1kAAJkECqETh58qS2VotP6PWxVgvH5RQSIAESIAF7AmZdOiLX45mYQgIkQAIkQAIkQAIkQAIkQAIkkPoEYC+k70uArRAcauFfrAFcU39k7EFaJXDq1CltXU7X30IHAXu4ZBO/jCPZuLO/yUMATuP2798v2F93/vx5bb0Ge6WxbsPgU8nzPabFnl68eFFzXIN5Hn/H+LvFs1cy3qtU3x/8F+Af9kxhbPBhwCDMKlpMJ4HoCMAWDQHR8Qknc5hL8I+/teh4spT/CdABlv+/Y46QBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABBKKwOUJ1Rt2hgRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIwPcE6ADL918xB0gCJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACiUWADrAS6/tgb0iABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEjA9wToAMv3XzEHSAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAKJRYAOsBLr+2BvSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESMD3BOgAy/dfMQdIAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiTw/9i7E3ibqv//4x/zEEJESJlKRUIqKQpRfWlQkhAyZEgaVUolSqifBr5Kg0plCGVoIiqakBLKGAlRJPM8/Huv3//c3xn2Pvecc8+9ruu1Hg+/s/daa6+993Of2+P33euszwcBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEMhcAgTAylzPg6tBAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBLK8AAGwsvwj5gYRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgcwlQACszPU8uBoEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIMsLEAAryz9ibhABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA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AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIHMJZAzc10OV4MAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIpF1gz549tmvXLtu7d6/ly5fPTjjhBMubN2/aB45zBJ3/s88+sz///NM6dOgQ59HJ6b5lyxZ744037Pfff7eqVata69atLU+ePMkZnFGiChw+fNgmTpxoX3/9tRUrVszatGljZcuWjXoMjZlDQP8N0X87KAgggAACCCCAAAIIIIAAAggggAACCCCAQGYQ2L9/v5v32L17t+XKlcvNe2juI6PLkSNHbM6cOTZ9+nTr06dPRp/ene/AgQM2evRo++GHH6xUqVLWrl07O/nkk4/KtRyPJ/32229t0qRJli1bNrv++uvtggsuOB4Zjrl73rdvn+XOnds9t2Pu4rlgBBBAAAEEEMjyAtn+/R8aR7L8XXKDCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggkGUFfvnlF/v888/tm2++sZ9//tlWr15t27dvj7jfokWLWoUKFVwQqLp161rDhg2tdOnSEf3SWrFp0yb76KOPbPLkyfbpp5+6BSmXX365zZw5M61Dx338r7/+arrXP/74I+XYmjVrOq+CBQum1LGRfIFDhw7ZjTfeaB988EHK4DLXd+OSSy5JqWMjcwkoWN3AgQNt+PDh9tVXX5n+XigIIIAAAggggAACCCCAAAIIIIAAAggggEBGCmzcuNEl11ByhQULFtiqVavsr7/+iriE/PnzW/ny5e2ss85y7501F6FEGMkuShigZB9Tpkxx/3R9OXLksIMHDyb7VKmOp8QjjRs3tlmzZqX0VQIKzRNVqVIlpY6N9BEYPHiw9erVK2VwBcF67rnn7M4770ypYyNzCehvZsSIETZgwAD37O6+++7MdYFcDQIIIIAAAggg8K8AAbD4GiCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggMAxJ7Bt2zZ7+eWX7a233nJBrxK5Af0oX8GhOnbsaDfffLPlzJkzkWHcMQsXLrSpU6e6f8p8fvjw4ZCxjlYArCZNmtiHH34Yci3a6d27tz355JMR9VQkT+DNN990WefDR6xUqZItX748vJr9oyywefNmGzRokA0bNsx2797trmbu3LlWq1ato3xlnB4BBBBAAAEEEEAAAQQQQAABBBBAAAEEjgcBBZMaO3asvfbaa/bll19GzDPEaqBgWG3btrUuXbrYiSeeGOthEf3WrVvn5jw0xzBjxgxTEKzgcrQCYOk97gMPPBB8KW67Tp06LqlBRAMVSRNYuXKlVa5c2ZQAJLhofk0JWcqWLRtczfZRFti/f7+9+uqr9tRTT9n69evd1TzzzDN27733HuUr4/QIIIAAAggggECkQOK/2IocixoEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIF0FtMDi6aefdtmkt2/fHnGu0qVLW6NGjey8884zbefJk8c2bdpkWqjxxRdfuMUP+sG3ypEjR9wiEi0kefTRR61fv37WqlWriDGjVSxatMgUZOr333+P1u2otSnjuVfxq/fqS11iAvq+eZUVK1a4hQb6flKOvsCWLVtMCz5efPFF27lz59G/IK4AAQQQQAABBBBAAAEEEEAAAQQQQAABBI47gVGjRlmfPn1szZo1EfdeqFAha9iwoQvWX65cOStQoIBt3brVNm7caN9++6199tlnpqQhgbJkyRJ78MEHXdCbe+65x21rriTWogQfNWvWtAULFsR6SIb283v3/s0339iBAwcsV65cGXo9x9PJZs+eHRH8Svev4G1fffWV3XLLLccTR6a9V/0djBw50iXCyazzl5kWjwtDAAEEEEAAgaMmQACso0bPiRFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAeAQWqat++va1evTrisIsvvtgFsVLwq2zZskW0q0KLR7QIZNiwYTZkyBDbvHlzSj+N2bp1a3vjjTfcv1iDEyl7+uOPP+7G+eWXX+z11183BdTJLCV//vy2e/fuiMspWLBgRB0VyRXIly+f54D6fp5wwgmebVRmnIAWiOm/A88995x5BdPLuCvhTAgggAACCCCAAAIIIIAAAggggAACCCBwvAooOM2tt97qknWEG5x22mluXkNBhfzeN+sYBR8aM2aMC3il4FeBoveemr9455133LyH5lFiKXqH3aVLF9P8ggJyvf3227Zs2bJYDs2QPn4WefPmtZw5WTafng/Bz17nZN4pPeVjG1v/LXjrrbesf//+nnOpsY1CLwQQQAAB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EUAAAQQQQAABBBBAAAEEEEAAAQQQQACBpAvce++9tnr1as9xn3nmGcuRI4dnW1oq+/TpY8WLF48YYt++fXbrrbea1zxMRGcqsoyAkn4sW7Ysy9wPN+IvkDt3bv9GWhBAAAEEEEAAgUwoQACsTPhQuCQEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBI5ngREjRvhmHm/btq3nYo1keF100UXWtGlTz6GGDx9uP/74o2cblVlP4M4777SFCxdmvRvjjhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQyXOCnn36y1157zfO85557rjVu3NizLa2VhQoVsl69enkOozkPzX1Qjg+Bd955x1599dXj42a5SwQQQAABBBBAAIFjToAAWMfcI+OCEUAAAQQQQAABBBBAAIHkCuzevdvmzJljEydOtM8//9yOHDkS8wl27dpl06dPtwkTJtimTZtiPo6OCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgj4Cei9dd++ff2arVu3br5tyWjo3r275zB6f37//fd7tlGZtQS0COmVV17JWjfF3SCAAAIIIIAAAggggAACCCCAAAIIIIAAAggcNYGHHnrIDh8+7Hn+9J73uO222yxfvnye537iiSds+/btnm1UZh0BBWDr3Llz1rkh7gQBBBBAAAEEEEAgywkQACvLPVJuCAEEEEAAAQQQQAABBBCILqAFOgp41adPH6tVq5Ypq4sy2t9www1Wv359a9WqVfQB/m3ds2ePPfzww1aiRAlr1KiR3XjjjVapUiX74osvUj2WDggggAACCCCAAAIIIIAAAggggAACCCCAAAIIRBN49913fZMunHPOOXbWWWdFOzzNbXrvXbZsWc9xZsyYYfPnz/dsozJrCGiuI70XG2UNKe4CAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIBaBZcuW2SeffOLZNUeOHNasWTPPtmRVFi1a1Pccf//9tykpBCXrCmzYsMGaNm1qSkBDQQABBBBAAAEEEEAgswoQACuzPhmuCwEEEEAAAQQQQAABBBBIssDy5ctdZvrTTz/dBbzq37+/ff/993bo0KGQM40ePdo+//zzkLrgnXXr1lmdOnXsqaeesl27dqU0bdu2zXr27JmyzwYCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAokIDB8+3Pew6667zrctWQ3ZsmWza665xne4oUOH+rbRcGwLzJs3z6699lrbv3//sX0jXD0CCCCAAAIIIIAAAggggAACCCCAAAIIIIBAphF46aWXTEmsvcrFF19sxYsX92pKap3effuVYcOG+V6f3zHUHxsCmzdvdgnP165de2xcMFeJAAIIIIAAAgggcNwK5Dxu75wbRwABBBBAAAEEEEAAAQSOMwFlLP/9999dEKzzzz/fSpYsaTNnzrQePXpEZPMYNWqUXX755RFCf/31l6tfuXJlRJsqFASLggACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAogKrV6+2H374wffwyy67zLctmQ1NmjQxv0BXEyZMMC1WyZMnT1JO+d1337nEFEuWLLF//vnHcuXKZaVLl7YaNWrY1VdfbSVKlEjKeQKDbN++3caPH2965//ggw8GqhP61PUqqcb8+fNNz27Hjh2mbPUnnXSSVa5c2fS8NCehoGLJLj///LM7tz43btxoBw8etMKFC1upUqWsVq1aVq9evbgWDX3yySfWokULk0940cIkr3r1y507t+XNmzf8EM/9w4cP25dffmnvvfeePfvss5YvXz7Pfqr0O5/vAf82FCpUKFqzS2wSnhgl/ADdi+4pnvLLL7+Y5qH0qWexZ88ey58/v5UpU8bOO+88t7jmlFNOiWdI+iKAAAIIIIAAAggggAACCCCAAAIIIIAAAllGYOLEib73klHzHo0bN7acOXO6d+nhF/Prr7/a3Llz7cILLwxvSmhf43388cf2008/2aZNm1xwLQX5qlKlil155ZVu/iChgX0O0vzAp59+apMnT7aXX37Zp1ds1fv27bOvv/7avv32W1uxYoWbt9G7/RNPPNEqVqxotWvXdnMfyZojCr6qDRs22GeffebmyRSwSu/a9d5fdnrXXrduXXcNwcdE29aaj6ZNm9rSpUs9u+3cudNzLkLzPCeccILnMV6Vmqd55513rHnz5la9enWvLq5OyddTm6MIP1jXoevxK3pe+het6HuvOYt4ip7FjBkz3HdYa3BkpWd+8skn2znnnGMNGjSws88+O54h6YsAAggggAACCCCQigABsFIBohkBBBBAAAEEEEAAAQQQyCoCnTt3Nv0LLrfddpv9+eef1rt37+BqN+EUUvHvjiYGlPlFEyFt2rRxC1amTZtmH330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fkkjgvv5bSso2V133ZXSrGBdpUqVStmPttG1a1fLlStXRJeZM2fatGnTIuqDKxQI7JZbbrHDhw+76mrVqpkSgcRSlJTkiiuu8OyakfMe+fPnt+bNm3teR7yVunclPwkvY8aMiZowJ7y/9vfs2WMKLqaiYGslS5Z029H+jxLUaL5ERXOC2o+lKMCW5qy8Su/eveO+dq9xqEMAAQQQQAABBI5HAQJgHY9PnXtGAAEEEEAAAQQQQAABBMIESpQoYeXLlw+pVUaLe++912WZD57gCenEDgIIIIAAAggggAACCCCAAAIIIIAAAggggAACSRLYvXt31JHy5csXtT3Zjamdb+fOnXGf8tlnnzVly06t5MmTx5TRvHr16p5dlWV73rx5nm3hlQoElVrW8vBjAvuaKwgOoKXFFVWrVg00p/qpwEdejnrWWsDgV/755x/r1q1bSrM8+vXrl7Ify4bfAoxEnlss5/ProwBQySi33nqr54INLRpSdvl4yuuvv+66FyhQwDfDevB4WjTy6aefplRp/iiecv3113t2f++992z79u2ebVQigAACCCCAAAIIIIAAAggggAACCCCAAALHuoCC8hw5csT3NhRMKCNLaudL5P35zTffbA8//HBMt6HgQEq24FX++OMPe+mll7yaPOvOPfdcz/pYKpVwIjAnVbhwYevQoUMsh7k+ClqmJBFeJfDu3atNdZ07d3ZJTgLtTz31VMwJR3RMVpv3qFy5snklg1GgMCVxj6dMmDDBtm3b5g7xC04VPJ4C0wUHZGvXrp1v4prg4wLbzZo1C2yGfK5atco+//zzkDp2EEAAAQQQQAABBGITIABWbE70QgABBBBAAAEEEEAAAQSyvIBXBnJlZXnxxRfjmljJ8lDcIAIIIIAAAggggAACCCCAAAIIIIAAAggggEC6CGghSLSSN2/eaM1Jb1PQpWglsDgiWp/gtiJFiljHjh2Dq6JuayHK0KFDffsMGzbMty28QZmrEylvv/22rVmzJuXQG2+8MWU7lg0Z+i0EmThxou8QChT2559/prQrgFK8Qbx69OhhCvAUXPQduummm4Kr0n07UfvwC5PlHXfcEV7tspqPGjUqot6vQt/bsWPHumZlmg838jquf//+KdX6Xl511VUp+7FsNGjQwLOb/uaDA2t5dqISAQQQQAABBBBAAAEEEEAAAQQQQAABBBA4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fX2xv78f5XK54X8tECBAgAABAgQIECBAgAABAgQIECBAgAABAgT+lsB39z3qs6yvr8fCwkI8Pj4Wo52ensbS0lJx3NraWtwHmZiYKM69ESBAgAABAgQIECDQvAK//nzR6aN5xzc5AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEfpbA9fV1TE5OxtXV1bcX3t3dHaVSKe7v74t6fW+lUomhoaFv91skQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECPwtgbe3t5iZmYmjo6NvR+jo6Iienp64ubkp6gMDA7G1tRXT09Pf7rdIgAABAgQIECBAgEBzCZSaa1zTEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBA4GcL1H/YUavVYnFxMcrlcgPGw8ND1P+mpqZiZ2cnjo+PhV81KFkgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE/g8CLS0tUa1WY3V1NQYHBxtGenl5KcKvRkZGij0XFxfCrxqULBAgQIAAAQIECBBoXoFfH39ezTu+yQkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIPBzBV5fX4swrMvLy3h6eorOzs7ixyHj4+PR1dX1c2FcOQECBAgQIECAAAECBAgQIECAAAECBAgQINCUAmdnZ3F+fl487KOtrS36+vpidHQ0+vv7m/J6DE2AAAECBAgQIECAQFpAAFbaR5UAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCCzQClzP+0IECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIJAUEYCV5FAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBHILCMDKLaofAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAUkAAVpJHkQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAILeAAKzcovoRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECcuzCDgAACo5JREFUBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgkBQRgJXkUCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEcgsIwMotqh8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBSQABWkkeRAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgt4AArNyi+hEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECCQFBGAleRQJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRyCwjAyi2qHwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQFJAAFaSR5EAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCC3gACs3KL6ESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIJAUEYCV5FAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBHILCMDKLaofAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAUkAAVpJHkQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAILeAAKzcovoRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgkBQRgJXkUCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEcgsIwMotqh8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBSQABWkkeRAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgt4AArNyi+hEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECCQFBGAleRQJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRyCwjAyi2qHwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQFJAAFaSR5EAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCC3gACs3KL6ESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIJAUEYCV5FAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBHILCMDKLaofAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAUkAAVpJHkQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAILeAAKzcovoRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgkBQRgJXkUCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEcgsIwMotqh8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBSQABWkkeRAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgt4AArNyi+hEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECCQFBGAleRQJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRyCwjAyi2qHwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQFLgN/604UDWWAJYAAAAAElFTkSuQmCC" + }, + "bijection_plots.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAEZQAAAZUCAYAAABofDBRAAABJ2lDQ1BrQ0dDb2xvclNwYWNlQWRvYmVSR0IxOTk4AAAokWNgYFJILCjIYRJgYMjNKykKcndSiIiMUmB/xsDOwMfAzcDKwJmYXFzgGBDgwwAEMBoVfLvGwAiiL+uCzMKUxwu4UlKLk4H0HyDOTi4oKmFgYMwAspXLSwpA7B4gWyQpG8xeAGIXAR0IZG8BsdMh7BNgNRD2HbCakCBnIPsDkM2XBGYzgeziS4ewBUBsqL0gIOiYkp+UqgDyvYahpaWFJol+IAhKUitKQLRzfkFlUWZ6RomCIzCkUhU885L1dBSMDIyMGBhA4Q5R/TkQHJ6MYmcQYgiAEJsjwcDgv5SBgeUPQsykl4FhgQ4DA/9UhJiaIQODgD4Dw745yaVFZVBjGJmMGRgI8QHE/koovb5cHQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAARlKADAAQAAAABAAAGVAAAAABkwkiCAABAAElEQVR4AezdB7jsVL2w8SjYEKQjUgSkKyAgXTgoICBSRXpHQDoI6qUjvUhHPBQVKSK9KEiRIiAKovRepCpVENGrFNlf3njXfGtnJ5lM22f2nHc9zzkze2aSrPwmk2S1/3rfUJoSkwIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooMCYF3j/mN8Dd0ABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFAgEzCgjAeCAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCigwIAIGlBmQL9LdUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFDCjjMaCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCgyIgAFlBuSLdDcUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEDyngMKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACAyJgQJkB+SLdDQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQwIAyHgMKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooMCACBhQZkC+SHdDAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRQwoIzHgAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgooMCACBpQZkC/S3VBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBQwo4zGggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoMiIABZQbki3Q3FFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABA8p4DCiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgMiYECZAfki3Q0FFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUMCAMh4DCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKDAgAgYUGZAvkh3QwEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUMKCMx4ACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKDAgAgaUGZAv0t1QQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUMKOMxoIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKDIiAAWUG5It0NxRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQPKeAwooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIDImBAmQH5It0NBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFDAgDIeAwoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiigwIAIGFBmQL5Id0MBBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFDCgjMeAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCigwIAIGlBmQL9LdUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFDCjjMaCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCgyIgAFlBuSLdDcUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEDyngMKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACAyJgQJkB+SLdDQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQwIAyHgMKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooMCACBhQZkC+SHdDAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRQwoIzHgAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgooMCACBpQZkC/S3VBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBQwo4zGggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoMiIABZQbki3Q3FFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABA8p4DCiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgMiYECZAfki3Q0FFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUMCAMh4DCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKDAgAgYUGZAvkh3QwEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUMKCMx4ACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKDAgAgaUGZAv0t1QQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUMKOMxoIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKDIiAAWUG5It0NxRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQPKeAwooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIDImBAmQH5It0NBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFDAgDIeAwoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiigwIAIGFBmQL5Id0MBBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFDCgjMeAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCigwIAIGlBmQL9LdUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFDCjjMaCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCgyIgAFlBuSLdDcUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEDyngMKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACAyJgQJkB+SLdDQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQwIAyHgMKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooMCACEw6IPvhbiiggAIKKKCAAgoooIACCiiggAIKKKBAHwm8+eabyQMPPJA8++yzyRtvvJG8/fbbyRRTTJFMNdVUyfzzz5/MPffcyfve974+yrFZGQsC//u//5vceuutycc+9rFk6aWXHgtZNo8KKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCoy6gAFlRp3cDSqggAIKKKCAAgoooIACCiiggAIKKDCYAq+88kpyxhlnJJdccklyzz33JO+9917pjhIQZM0110y23HLLZMUVVyz9XLtvEHiEIDYEsDGNbYF33nknueKKK5Jzzjknueaaa7LvdfHFF09+//vfd23Hnn/++WSWWWbp2vpckQIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACE1LgfUNpmpAZcNsKKKCAAgoooIACCiiggAIKKKCAAgoo0F2Bl19+Obn//vvbXumcc86ZzD777LWX/89//pMcddRRyWGHHZYQyKUofeADH0imnnrq5LXXXkvefffdYR9ZYYUVklNPPTWZe+65h73e7h8EHdlxxx2TL3/5y8kpp5zS7mpcbgILUHX94x//ODn00EOTp59+upEbjqWtt946OfnkkxOed5qee+65ZKGFFkrGjRuXjB8/Pplpppk6XaXL91iA88jdd9/d9lZmm222ZK655mp7eRdUQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEAB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Wy655ZZbmn6umx8IwWTiWVBOP/30bBP9GFQmf7x9+MMf7iZH2+taZZVVhgWU4V4Xx+985zttr7NXC/7hD39IvvCFL3S0+tNOOy3ZbrvtGutgfX/9618bf7fzhBlBQlAeft9l54j8urkXmGKKKfIv+7cCCihQKkC979FHH528/fbbhZ+58sork8cffzyZe+65C99v58VwbQ3LbrvttuGpjwooEAn89re/zQLHRi/5NCdQJ4jfW2+9lVvKPxUYPYF8WX30tlxvSwRpOfLIIys/vPPOO1e+38s3aSMmoAyJjtvNAu72Mi+uWwEFFOiFAAGFqY8sSgT+JKjXxz/+8aK3W35tgw02yALKOFt9y3QuoEBfC1AP/ZWvfKXj+ui+3kkzp4ACCiiggAIK9Fig2aQHTFIzFlK/10WOBUPzqIACvRe48MILEwKjlqXPfvazyaKLLlr2ds9fp02EsXX/+te/sn8936AbUKDHAkwwud5669WezK7H2XH1CiigQN8LvL/vc2gGFVBAAQUU6EDgiiuuSKo6jVAgn3feeTvYQmeLMhCNgAt0DC4b2NDZFlxaAQUUmLACc801V2kGOD8zmLTO4IjSlURvrLvuulkAG4NORCg+VWCMC7z00kvJSiutlDzzzDNjfE/MvgLlAk8++WTyox/9KJsBYtNNN03o/P6tb30rOffccxPeyyeCV9CB9ve//33+rYH8++STTx6xX1/60pdGvFbnBQZnLb300lkZrM7niz7zqU99KllzzTVHBMIh8NW4ceOSKaecsmixYa9NN910WT6GvdjjP+gERB7jYDJhkwx833777RMG2/VTYsb2OFU1uIfPEdTnlFNOSbbeeuuE44RASOz35ptvnhx//PHJQw89FD7a9iMBjfKJ45SAtv2WMPzEJz7RdrYWWGCBZM455xy2PJ4zzzzzsNfq/sGxv/zyyydLLbVUYxECBa2++urJjDPO2Hit6MnnPve55AMf+EDRW76mgAIKlApwbuH+qiy99957yQknnFD2dsuv//vf/07OOeecxnKcR5dccsnG3z5RQIH/ChD0jjINvxlTucBrr71W/ub/vTPZZJM1/YwfUKBXAnQ47ud0/fXXJ48++mhpFmm7WHzxxUvf7/UbBOsMwXKdCbTX2q5fAQUmhEC+TinOA0EiNt544671kSHYN4MkmY3WpIACgyHArNL0qXvk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13izab976KGHvHWmWy7b6X/4wx+8t4+Xl5fbXXfdZd27d882CeYPEYhGoyFTGI0AAggggAACCCCAAAIIIIBA+wvcfPPN1tTU5GVk5513th133LH9M0UOEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQ6jAABZTrMT8mGIIAAAggggECxCqxYscJmzJgRy/7EiRNjn/mAAAIIIIAAAggggEA+BebOnZvP5Notrb59+9pTTz1le+21ly1atMj0Nucf/vCH9vTTT5sCfzB0PIHLL7/czj33XNu8eXPcxo0YMcKuuOIKO+SQQ+LG+1/WrFnjBWvRd+0bBx54oD8p5d9nn33Wfv7zn3vzVFdX23333Wf77LNPymWynahgJ+ecc45ddtllpqA46kQ2bty4bJNh/hQCet5mQAABBBBAAAEEEEAAAQQQQKAQBdavX2/Bl45QR1yIvxJ5QgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBIpbIPWrW4t728g9AggggAACCCBQFAJTp041dXLUMGTIEDv00EOLIt9kEgEEEEAAAQQQQKD4BO68807bsGFD8WU8SY6HDRtmL730kg0dOtSb+sorr9iRRx5pjY2NSeZmVDEL3H777TZ58uRmwWS0TZ988on3DHXqqafaxo0b4zZTwWd++tOfekGHNOG4446zbt26xc2T7Murr77qpanle/bs6QUvmjBhQrJZcx63fPlyLwiOgslUVFSYtlF5ZcivwJNPPpnfBEkNAQQQQAABBBBAAAEEEEAAgTwJXHvttVZXV+elVltba0cffXSeUiYZBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEvhYgoAx7AgIIIIAAAggg0I4C77//vl111VWxHPzP//yPlZaWxr7zAQEEEEAAAQQQQACBfArMmzfPC5Tx4Ycf5jPZdktLQWVefvll23nnnb08PPTQQ17QEILKtNtP0iorvvjii9Ome8MNN9g3vvENLzDL/Pnz7fHHH7fvfve7dv/993vLVldX229/+9u06SiYzAEHHGBr1661UaNG2d/+9jfbY4890i6XzQxNTU02duxYe+SRR2ybbbax1157zY466qhskmDeNAINDQ129913mwL2MCCAAAIIIIAAAggggAACCCBQaAIqo7v00ktj2frF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23bbbW3ChAl27LHHWk1NDVQIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAh1E4IUXXrAbbrjB/vGPf9jKlSu9wDFjxoyx/fbbz4477jirra3tIFvKZiCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIFIMAAWWK4VcijwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQAYCJRnMwywIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACRSBAQJki+JHIIgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggkIkAAWUyUWIeBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECgCAQIKFMEPxJZRAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEMhEgoEwmSsyDAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUgQABZYrgRyKLCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAJgJlmczEPAgggEBLBCZMmGBPPvlkS5JgWQQQQAABBBBAAAEEEEAAAQQQQCClwIEHHmiPPvpoynk64sS6ujqrra3tiJsW26YRI0bYnDlzYt/5gAACCCCAAAItFxg8eLAtWLCg5QmRAgIIIIAAAggggAACCCCAAAII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vfPOO+2www6z1atXB8byEQEEEEAAAQQQQAABBIpBIBrdbCUlXYshq+QRAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGCEWhoaLA//elPdumllxZMnrLJyNy5c23ChAk2e/bsbBZjXgQQQAABBBBAAAEEEEAAAQQQQAABBOIEyuK+8QUBBDyBuro6u+yyy2z69Om2YcMG69u3r+29995tpnPrrbfaE088kfP66uvr7cMPP7QPPvjADjroIBs+fHjOabEgAggggAACCCCAAAIItJ9AWUkP29S0pv0ywJoRQAABBIpOoClq1hR1/yvyIWrFvw1F/hOQfQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSKTqCxsdFuv/12u/jii01BWXbeeWf7zW9+02bb8fHHH9sFF1yQ8/qU/6+++speeeUVKy0ttZNOOinntFgQAQQQQAABBBBAAAEEEEAAAQQQQAABAsqwDyAQEFi2bJldfvnl9r//+7+2bt26wJS2+6gANpMnT7alS5fmZaWnnXZaXtIhEQQQQAABBBBAAAEEEGh7gZKSzm2/UtaIAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEARCTQ1Ndldd91lF110kSmoS3sNU6dO9fKRj/Ufc8wx1qdPn3wkRRoIIIAAAggggAACCCCAAAIIIIAAAluoAAFlttAfns2OF1ixYoVdeeWVds0119iaNWviJ7bxt1tuuSVvwWRGjhxpEyZMaOMtYHUIIIAAAggggAACCCCQL4FIpDJfSZEOAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIdSiAajdpf/vIXu/DCC+2DDz5o121bvHixzZw5M295mDRpUt7SIiEEEEAAAQQQQAABBBBAAAEEEEAAgS1TgIAyW+bvzlb/W2D16tV21VVXef9WrVrV7i6NjY1eYJt8ZWTixIkWiUTylRzpIIAAAggggAACCCCAQBsLrNs030qt3KK2uY3XzOoQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEChMAQWSuf/++71AMu+8805BZFIvt62vr89LXnbbbTfbZZdd8pIWiSCAAAIIILClCJSVldn06dNt+YYF3iav27TUtb9tss6damzQkAFbCgPbiQACCCCAAAIIIIBAnAABZeI4+LKlCKxdu9ZUYHvFFVfYihUrCmaz//rXv9qnn36al/x07tzZjj/++LykRSIIIIAAAggggAACCCDQPgINTSutc+U3bUP9W+2TAdaKAAIIIFB0AlGvGUS06PKdmOHi34LELeI7AggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjkQ+Dhhx+2Cy64wN544418JJeXNNasWWM33nhjXtJSIqeddlre0iIhBBBAAAEEthSBkpISO+WUU2zh6n95m7xs/WcunEyj1VQNstJIpy2Fge1EAAEEEEAAAQQQQCBOgIAycRx86egC69ats+uvv96mTp1q69evt3Hjxtl2221nn3zyiT344IPtvvnTpk3LWx4mT55s3bt3z1t6JIQAAggggAACCCCAAALtI7Cufp6VRbq48ACb2icDrBUBBBAoaIFIQeeOzCGAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5Efgscces/PPP9/++c9/2o477uh1GFfKd9xxh61atSo/K8kxlT/+8Y+2cuXKHJeOX2z77be3I444In4k3xBAAAEEEEAAAQQQQAABBBBAAAEEEMhBgIAyOaCxSPEKTJw40RYsWGBXXHGFHXbYYdalS5fYxkyaNMmuu+662Pe2/vDcc895hdtab48ePeyuu+4yRUbNdtAyAwcOtNGjR2e7KPMjgAACCCCAAAIIIIBAAQo0RFdbQ7QAM0aWEEAAgQIQiEY5QRbAz0AWEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgVYVUDAZ9QX4yU9+YnfffbcNGzYstr5jjz3W9txzz9j3tv6wefNmu/rqq2OrnTJliu28886x79l86Nq1q+20007WqVOnbBZjXgQQQAABBBBAAAEEEEAAAQQQQAABBJIKEFAmKQsjO6rADTfcYFVVVUk37+c//3m7BpSZOnVqLF+nnnqqTZgwIfadDwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIbIkC48ePt7lz51okEmm2+XvssYdtu+229v777zeb1hYj7rzzTvv888+9VY0YMcJ+/etf5/Ri2bbIK+tAAAEEEEAAAQQQQAAB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QAABBBBAwBeg95ovwV8E2lBg+vTpduGFF5oilK9bt84WLVpkCxYssPfee89ef/11++c//2n19fUZ5ygajdrZZ59tmzZtsvPOOy/j5ZgRAQQQQAABBBBAAAEEWksgmjbhSKQ07TzMgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAoUhMH78eKurq7M1a9bYhg0bvM8LFy60OXPmeP0AFHBG07MZnn/+eZswYYI9+eST1q1bt2wWZV4EEEAAAQQQQAABBBBAAAEEEEAAAQRSChBQJiUPExFoHYGysjLr379/LPEddtgh9lkfFGTmueees7vvvtvuu+8+L/BM3AwhX84//3zr3r27/eIXvwiZg9EIIIAAAggggAACCCBQKAIRI6BMofwW5AMBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgUwEevfubfqXbNCLYvWC2QcffNBuu+02mz9/frLZmo1TIJqDDz7Ynn76aSsvL282nREIIIAAAggggAACCCCAAAIIIIAAAgjkIlCSy0IsgwACrSvQuXNn+/73v2//93//Z19++aVNnTrVttpqq4xW+qtf/cqeeeaZjOZlJgQQQAABBBBAAMW5uacAAEAASURBVAEEEGhHgQiP5O2oz6oRQACBDivQ5LassQP803YwIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAsUkEIlEbOedd7aLL77YPvvsM3vkkUds7NixGW3Ciy++aJMmTcpoXmZCAAEEEEAAAQQQQAABBBBAAAEEEEAgE4GyTGZiHgQQaD+Bbt262eTJk23ixIk2ZcoUmzZtmtXX14dmqLGx0Y466ij76KOPrKamJnS+XCd8/vnn9vbbb2e1uPLCgAACCCCAAAIIIIDAlicQcZscTbHZBJRJgcMkBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSKVkDBZQ466CDv30MPPWS//OUvvSAzqTbopptusvHjx9uRRx6ZaracpqmfwRNPPJHVskuWLMlqfmZGAAEEEEAAAQQQQAABBBBAAAEEECgsAQLKFNbvQW4QCBWorq62Sy65xH784x/b0UcfbW+++WbovEuXLrXzzjvPrr/++tB5cp2gQuSTTjop18VZDgEE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c+KyUlIZscqh4XUkCjTT/dsVccu88PibtmLZmrhxfEEAAQQQQAABBBBAIF8CV199dWg7/bq6Ovvb3/7W4lUtXrw4NA0FiFEedt99d5szJ/7+OXGh1atXe3WLN954Y2ySXl5x6623WiSSPGhjbEY+IIAAAggggEBKAb0gaurUqXbD1X/2/t16/QP25+sfshuvnmmLv6xLuSwTEUAAAQQQQAABBBDoqAIElOmovyzblbGAArmow8c777wTuszcuXPttNNOCy1oDl0wMGHAgAHWr1+/wJjkH6dPn27bbrutzZw50zZs2BA308cff2yTJk2y3XbbzebNm+dN22uvvey1116z8ePHx83LFwQQQAABBBBAAAEEEGg9gWi0wao6DW69FZAyAggggAACCBS0gBqf+MOZZ55pZWVl/lf+IoAAAggggAACCCCAAAIIIIAAAh1K4PWEgDKVQ8pNQWNSDV13rrBIoLikYXOjPXr3q6kWYRoCCCCAAAIIIIAAAlkLNDU1mdreX3LJJSmX/dnPfmbz589POU+6ibvssku6Weytt96yb37zm3b22WfbwoUL4+ZXIBkFkVE/gb/85S/etD59+niff//73xNMJk6LLwgggAACCOQmoIAyZ511lk254H+9f9f87na79nd32GUXTrcvFn6ZW6IshQACCCCAAAIIIIBAkQsEqm2LfEvIPgIZCDQ0NNh7771nX375pX3wwQf2yiuv2KxZs5oFbkmW1E033WRPPvmkF3xGBcJbb721de3a1VSQ27t372SLxI0rKSmxG264wX70ox+lDUyjtxsfd9xxdvLJJ9uYMWOsc+fOXiF2sCB76NChdtFFF9mxxx5LAXKcNF8QQAABBBBAAAEEEGgbgZJI57ZZEWtBAAEEEEAAgYISePbZZ+1f//qXl6fa2lo78cQTCyp/ZAYBBBBAAAEEEEAAAQQQQAABBBDIp8Abs+fEJVc1rDzue7IvpdUl1uWbnWzN65tikx++6292zMnfM7WhYkAAAQQQQAABBBBAIBcBvZBV7enV1l71dQ8++KB9/vnnaZP68MMPbZtttrEjjjjCxo0bZ8OGDbNevXpZdXW19zltAm6Gc8891+t3MGdO/P1x4rIbN260yy67zPSCitGjR9tWW21ly5cvN+Vh8+bN3uyVlZU2ceJEL82amprEJPiOAAIIIIAAAggggAACCCCA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k9FCFxYW7OMf/7j95E/+pP3Hf/zHkdf+8ssv2y//8i9bFEWhTblctq985Sv2tre97ch9UBEBBBBAAAEEEEAAAQQQQAABBBAYtEC73bH5myt93WanTzbYJTvdH1jTbrXtxpWFvjlwggACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBwPAECZY7nR2sEEEAAAQQQQAABBBBAAAEEEEAAAQQQQODMCLR9Jhfh0TkDovPz84fO4r/+67/sueees8985jOH1tPNL3/5y/ZzP/dztrS0FOo+/fTT9q//+q/2Uz/1U3dtSwUEEEAAAQQQQAABBBBAAAEEEEDgJAVazVZ/9wmzzGR/4Et/heOfJXMJS4/2v3Xx6g8O34s5/qj0gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCDxcAv1/lX241s5qEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDYV+Cd73ynZTKZfe/FF6Most///d+3Z5991r72ta9Zu604n9vlxRdftF//9V+397///ba6uhpufPjDH7Zvfetb9va3v/12RY4QQAABBBBAAAEEEEAAAQQQQACBByTQ3BMokx5PWSLtqTInXLIz/W9dvPoagTInTE73CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCDwkAmkH7L1slwEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBO4q8Oijj9qnPvUp+6M/+qO71v33f/93e9/73meTk5P2tre9zVKplL366qs2Nze32/YnfuIn7NOf/rT90i/90u41DhBAAAEEEEAAAQQQQAABBBBAAIEHLdCMWn1TyE6n+s5P6iQ7k7LKq83d7q8RKLNrwQECCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAwCAECZQahSB8IIIAAAggggAACCCCAAAIIIIAAAggggAACF07gE5/4RAiIUajM97///buub2lpyf7lX/5lt16xWLQPfOAD9rGPfcze+9737l7nAAEEEEAAAQQQQAABBBBAAAEEEDgrAq1mf6BMZup03lKoQJnecvUH872nHCOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIHFPgdP76e8xJ0hwBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEDg7gIdS1i7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ReMUAREQAREQAREQAREQAREQgXtEAHnMxkbN1ja23O4wbgRJDCKYQR8Li8tium2rtdrW2OwEkcz2Wsta211LuDimcqFgF67MWWNm4orZ3b4mUzNBLjNfKNmoWLLmds8aVZ/YdaFMY6trs/NZF7MMbHuzZVefHBt7z2KYWYQ3UVDBmHw+aCfVdx2wudile2qhDH2av5jziSafqG4NbW4pZ1WXygzWOgFZtz2y9RstW7pcsLrnn/G2kVjAr+MSmZGLbUL4xNSsy27KvhD5YtouPlEIj/VDBB4nAntztHsPzn70e5O0u/PB97Cps++8MoqACIiACIiACIiACIiACIiACIiACIiACIiACIiACDz0BFKVhH/Odctmvr66/dD3WR0UAREQAREQAREQAREQAREQAREQAREQAREQAREQgdMQGAwGtr6+HgQr8dpc5A2ITxDL8Bh5A8IY5CkIYtj23HPPhWYQqCBEmZ2dDeUOto0whYX95KxWq0HIsrm5GdaIXmgfUQuClrMM5DH1ej20Q36kLwhbELoghUFsc5z45aR+zM/PBwEOUppKpRLYRUZRFkN7iHXYDlc40yZrgu0IbRgzj+Fw+fLlIxme1BftE4HHgQCvESKu7+WYYxtxfS/bUm4ROM8EJJQ5z0dXYxMBERABERABERABERABEXioCHRbfdu4VrNCIh9kMjxvueyl13b5yq4DJXQ41bBr1zZtNNxxOcrI+i6byeTSPjHpZvCZHWs2urZT2jlxAoYJk9Jc1nKFlF379HbIs7XSGsteXL7S3O4HwcxZAcrkktZpDSyTT/rFvD5ZnWlYop+yfncQpC/IbE4T9H/xUt6q624MdzHOaDiyykIW346l0kzkBveO1TZ7h9IimCmUUj7+jJcd29RnncXSlfyJzA4l0gYROCcEksnx6yC1u0ZsddYx3BU5JZPjCeLE7vqs21E+ERABERABERABERABERABERABERABERABERABERCBx5NAquxCmdVb89sSyjyevwcatQiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAicVwIIUW7evBnEMYyx1WoF6QrilyiXiWNnH3KWRCIRxCipVMpYuAafstPIFxDLXLx4MUhlyIWgBtEMkhoeX716NTZ3JmtkMbRDfiQwCGUQ4yBy4fHW1pYtLy+HMZ22wYWFhZALfuREDEN7tMUaXkcF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6yDVmAicAwL77w46BwPSEERABERABERABERABERABB5fAkwQbq5s2fqzW3sQkMv0mj3r99z27UKEUTZp1e6WJZIJm3GBSavacYlJylrbPnFbbdlccWjpxYTNXSq50OR42Qp1WAoVN3BvuUCm3rWm5xoNdmzuQs62XDCTyadt6LIZgseTEyB7HZx4EPcndid2D074ThT1fKkglEFmQwy93eFw5BMmR0tYki6NKJQz1nUWpdmCdapu8O0NrbHds4uTiad4PBiNJ2xqLqRpu1yCmFvKh/EVZscyipimPdy27kzSLub/uDV7z1nXnyOrYFGIgAg8GAJLVwr+FQpmvIaRPOUKfl5cdzmWC54aVRdt+cJ+zhucl0Yu2mLZC99WqqRtdj4bZFmIZS49r+RimePPmXt19UAEREAEREAEREAEREAEREAEREAEREAEREAEREAEREAE7pBAupxwocz4szFSrK9u32EmVRMBERABERABERABERABERABERABERABERABEXicCCA4WF9fD/IWxo2gBNkBwhVEM0hdCOQF6fT4S1aRmSA1uH79eiiLBAUhytzcXBAfhApH/ECcwEJ5BDa0w5o22IZUJkoSokhhWmFLvN8gNnvc/QYIGRA6ME4irmO949b0j6D/9BHpA31HoIPs4bRBuxsbG0EEQ44olYDh7YL26U/s0+3Ka78IiMCDIYBQhkAqw+uV81m1Wg3n10ajYSycu+J5j/NWlGpRj32cHzi/cs6lHDIZzhcKERCBsyFw+v/Bz6ZdZREBERABERABERABERABERCBMyWALGbjORfF7E7gdhoIYtouWhlP7u415pMPOyOfgBgNrb7SDFIVygy6A8sU0i5QSPr+sXzGThDKxHyJxIzNLhYs5SKF+oZbdL1dukBOnrOdSLnI5naRTI1lMAlf4wrf8a4jcUi4+OZgBNkNm73McDAM/abN44Qy1J+dy9rGjYYlXBJRns/5pLQb1DtDl/C4SOdi3idfDrdzsF2e94dta2y4QMfHSlSWci64SYb6i5dcVHEgRjtDW2l9whIzKVt2sUyj96yLZWoHSumpCIjA/SSwdLlgmVzSzwlty7qgavnJlHWaA2s1BtZpD2zHzz3DPhKZXZGMT9SmswnLuwyqUPZz5e55KV9M28UnCn4OOlpmdT/HpLZEQAREQAREQAREQAREQAREQAREQARE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lxG+pLMpK18s2cDFNghTGptu1l6cCduDVObK9Abx4nze++MT2C5nYKzmcxq93f4d6vwRG3KljJnPEWdc1NKuu2zHRS4sQTRzoPxoV+6SiMKcuD5QLj6tb8LcbG7RreG5lm2vt60452IIX9q1vjVcgjMc7Lg4pmetXjdW27dGKFGspO3q8kWr96/5sTG7cKXo27zfp4zRztC6O/VT1lJxERCBe0LAT5fFJRb/BoJhIghmOB/woVgi6edDF2ulMvFCff9Aa9S7J91QUhE4LQH//pHTVlF5ERABERABERABERABERABERABERABERABETjXBLqd8yyU2X8x8fpq9VwfSw1OBERABERABERABETgdASeffZZ+9f/+l/bj/3Yj4Wbak9X++5Kc2Pwm970pvBFttwwF+NFL3qRveIVr7CFhYXwLe0f/OAH7ROf+ES4F+BHf/RHQ19f+9rX2vd///eHG3VjPa1FQAREQAREQARE4HEigKyFeyURpfD4uECeEqUmyGKQECB+OUqmghilWh3PH0aZDLIC3pedJILhfR3t0A/EKIhnsln/klnfTtvkQEhDf5GqcO/ntIFY5ebNm0bfyDM5VtqcJmKb9I1+dTqd0C+ENZPB9b8xP9IGIq4ny00+RvpAPgK5A2MjT+TGtlqtFspQluWooDySCNjRX4K6y8vLuvf1KGDaJgLnkADnZc63LJyLEE7FcyvnIvYfde4+hyg0JBF4KAhM/27loeiuOiEC947Ar/3arwWRzP/8n//z3jVyQuZ3vvOdwTA+OYH8FV/xFfYDP/AD9oIXvGBfTUyN3/d93xf6yxtv/lj6R//oH9nP//zP20/+5E/awT8A9lXWExEQAREQAREQARF4gASaW01rN5rW6bZDL/oucOm2ey538cmB/shlLiMb5YdW61Ut6TKYdC5l2ULWBTHJII5pbbftUuaWlXrQG/j2sQW7vtGw0WBoyXzShSllS6Si+ODwgBG8EH2vT5vBbFvwyUpfZpqIXMya1bZVloouhjGXrXQtX84eTnTElqS3my241MbXvfbAJ1JdzODj7LvAxuz2OXJet9+cCfWRynRbA2u56KW8lHd5y62LZJHiBB+Pb0qmxnZ12jouyNNxeY7PwlrZpTdNS9nzX7IYBDqtet8KLoRh6XeGlh0VbKeZcamEHxNPmPR2EUlkcknL+jEhUom05bx/i1cKlsmebHcPFfRDBETgkSGAQCab15TRI3PA1FEREAEREAEREAEREAEREAEREAEREAEREAEREAERmCDQ88/dzmukyvs/C1v3L7FQiIAIiIAIiIAIiIAIiMD169ft3/ybf2M/8iM/cuxNrfeS0u///u/bV3/1V9vv/u7v7jVz9epVe9vb3mbcD3AwfvmXf9n+9t/+28aX0XKD7rve9S771V/91XAfwJd8yZccLK7nIiACIiACIiACInCuCXAvJZKVKFRBlIJwBbEJAoJJKQryAQQlCGFYIzep1+u2uLi4735K3mOtra0FbuznXkwkJxcuXAgSlpOAUpagffLQJvdq0idy0V/aZjvCmqUl/zbHKQMBTJTTMM5kMhnGzZp7RBHVTBOU435S1ohu4kLuGLAjyM3YWXh8XDBWxsM6n8+HMXKPxfOe97zAOQp0GC/HCj70mcfUIT8CGrhQP8praJPjg1xGIQIi8HgS4HwweX56PClsaZU6AABAAElEQVRo1CLwYAno7qAHy1+tPwQEfuM3fiOIWT7wgQ88sN78y3/5L+2Nb3zjvva/+Zu/2X7oh34ovJnet8Of8Kb69a9/vX3RF31RmGSOEpr3v//99vKXv9yYZMbYqBABERABERABERCBe0Gg799qiMhl2EfG4roRn/xLJBMuHEEAk8Y+cqhZJgm3b2xbp9Gxcq5kPRestKpNF8kgWTkcI8878nZoC4lMJp+x4lwhFLz57Lo1c00rLRatsYlMZselK14WSYzLVRaW52xj0D2cdGILgheCcRCp7K0/jUpzRat3toLgpt3ouWQl533oWG7WZTM+1mkCoUwqnQgCl6KLaMjT8z7GydKTciCNKVXy5j4bo+6gO7SBy3Ya1a7NLuT2qsKQSGXGE7uJ1FhCs1dg4sGgN7Stm87KDxfSmIwfp3bPDevLhSCu6TT7tr3R8ePT92OYtNlswWYKt9qaSBUeZl0kc/mpRWundZHuQTZ6LgIiIAIiIAIPmsCOJWy0c/j92IPu12nb94+YT1tF5UVABERABERABERABERABERABERABERABJxAt3P0t8KeBzipyv55r45/cUWj3rbSbP48DE9jEAEREAEREAEREAEROCWBlZUVe/Ob32w//MM/HG5oPWX1Myn+4Q9/2F71qleFG3pjwhe+8IXGl91evnw5btq3fuUrX2kf+chH7C/8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sQgggAACCCCAAAKRI3DvvfeKujZWUlLi65TH45H77rtPnn76aV9eazbUaoSlpaWGqlarVR599FFDHjsIIBB/AhaLRa6++mr529/+5jd49R00evRov/zWZNRrfyyr45sndb4rrriieTb7YRD48MMP5eeff/a1ZDab5fbbb/fts4EAAggggAACCCCAQCCBCnuzgDLacnGmRNaMC2QVrjxLpjFgT3GBPVxN006MCuRuNgaUyejrkQTjj1FMjDw9xyV5v9l8YykuIKCMD4MNBBBAAAEEQhBI0H5ROOmkk2Ty5Mny+OOP69f/HQ5H0BbUvPsZM2bI888/H7ROWwtqa2tl4sSJIR2ujiEhgAACCCCAAAIIIIAAAggggAACCESvAAFlovezo+dxKKBWJX311Vf1C8pXXXWVVFVVBVW47rrrZO3atWIyhXdSSX5+vnz99ddBz0sBAggggAACCCDQ0QIJ0vJsPIc7+A33ju4b7SOAAAIIIIAAAgjErkC/fv3khRdekJNPPllUYIbGNGfOHJk+fbqMGjWqMavFdzUJ9O677/ar88ADD8hBBx3kl08GAgjEn8Dll18u//d//yeVlZWGwf/3v/+Vu+66y5DX2p2vvvpKAgV/V8Hp99hjj9Y2o9dzOp3y6aefyubNm2Xw4MEyadIkUUGxSLsEvF6v32d12WWXyf7777+rElsIIIAAAggggAACCAQQqCw33ucy2cI77yfAKeM+y5phNLaXVovH7RGzxRz3NgAEFsjdVGAoyOznNuzHyk56d+O4ivIJthQrny3jQAABBBDoGgEV4P3666+X448/Xs455xxZvXp10I6oxWXVswIjR44MWqctBWqxDJ4DaIscxyCAAAIIIIAAAggggAACCCCAAALRK2C8Gxq946DnCMSVwPnnny8LFy6UHj16BB33unXr5IsvvghaTgECCCCAAAIIIBCrAiU1RWJK4E+dWP18GRcCCCCAAAIIINCVAlOmTJHZs2cbuqCCBqhJn4WFxpWJDZV+36mpqZGzzjrLL0jElVdeKSpANAkBBBBQAtnZ2fKXv/zFD2Pjxo3y+eef++W3JuPpp5/2q5aUlCR33nmnX35LGWvWrJFhw4aJ+j689tpr9fcDDzxQfvvtt5YOa3NZS6u0trnRAAf++OOPMnfuXJk3b5589913hsBhAarvNmvWrFmyfPlyX719991XZs6c6dtnAwEEEEAAAQQQQACBYAIVdgLKBLPpqHxLpnExCxVIuKSooqNOR7sxILBtk/E6YEaMBpTJyHEZPq1iAsoYPNhBAAEEEECgrQLqevGSJUtk4sSJQZtQv5M2vycZtDIFCCCAAAIIIIAAAggggAACCCCAAAIItCBgaaGMIgQQiGABFXH8k08+kaOOOkqqqqoC9lRNfj7uuOMClrU1U62eec0114R0+LfffivLli0L6RgqI4AAAggggAACbRXw1Huluy1HSmqME/na2h7HIYAAAggggAACUSdQH3U9jqoOX3HFFZKcnCwqCExtba3e982bN8vRRx8tb7/9tuyzzz4Bx1NQUCBnn322fPPNN4byv/71r3L//fcb8thBAAEEbrzxRnnhhRdEfb80Tffee69MmDChadZutzds2CD/+9///OrddNNNsueee/rlB8soKyuTSZMmSX5+vqGKCnB/wgkniArKkpKSYihr705eXl7QJtxu40rpQSvupuCSSy6ROXPmGGoNHDhQ1PfzpZdeKlar1VC2u50VK1bI7bff7qvWrVs3eeONNyQ1NdWXxwYCCCCAAAIIIIAAAsEEyu3VhiKTzRjsxFDITlgELKkJkmAWqffsaq6ksEJ69um2K4MtBJoI5G423ofO7GcMvNKkalRvpjcLKFNEQJmo/jzpPAIIIIBAZAmo68XvvvuuPs9/8eLFATunriuroDI2my1geVsy1fXuUJ8DqKiokJdeeqktp+MYBBBAAAEEEEAAAQQQQAABBBBAAIEIECCgTAR8CHQBgbYKHHzwwfLss8/KueeeG7AJFXAm3Omwww4T9Qol3XfffQSUCQWMuggggAACCCDQboFUa4bYa0vFUx+eh8va3SEaQACBuBVQq0Z5XB7xerXoDtr/TOYEMVvNkpDAQwBx+0PBwBFAICYEpk2bJocccohccMEFsnr1an1MKqCCul43ffp0OfPMM2XYsGFisVhk06ZNMn/+fHn88celtLTUN/5evXrJM888IyeddJIvjw0EEECgUUBNJn/11VflyCOPFJdr14NpX375pbz11lty2mmnNVbd7fv1118vHk+TJyO1I0aNGiV33nnnbo9tWuG5557zCybTWK6+61QAnKuuuqoxKyzvX3/9ddB2mge2CVqxhYJVq1b5BZNR1bdt2yZXX321fg/mlVdekeHDh7fQyq4i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gAACCCCAAALDEWhytWqHmx0pcH1HG1HyZGz5JvHUqYtsnanO6e76yHsaCmzfUqONOr+0V7QhbU9qZ3JH6wFlvKF7X431zZJf1LlSbmqPntEhgEAiC8yYMUOee+45Iwi4WjBWpU2bNskdd9xhvHr3XQUKVwvNqqDhQ01ms1kefPBBOeyww4ZaBcchgAACCCCAQBgCi476ntz35kuyo6FOer6hh3HgEIrYbRky66DDpLRw1BCO5hAEEEAAAQQQQCA2AipAmnr5fD4jkK4KMKOC5KoYCMFgUNQ1CjXnTgWaUeWysgiOF5szQa0IpIZA2geUUadx6dKlMnHiRGPlSvULdaCkfumqaOTDSVOnTpWXX35ZVFAZEgIIIIAAAggggAACCAwsoG7+FGSOIqDMwDxsRQCBJBGwZdpEvUgIIIAAAggggAACgwsEAgHpHahl8JL6njFjxshVV10ll19+uRQWxn6V+AMOOEDvQCinVr798ssvZf/99++3jw0IIIAAAggggAACCCCAQKQCbleLdojZwYrJGkgcM9Y+AWVqq1kEI478CdfU9i1OrU95JekZUCanMDRuU+hOfq9A4LWhYEsElNF+PMgggMAICRx//PHyxhtvyLx582TLli2D9kIFCh9OUg9oPfzwwzJ//vzhVMOxCCCAAAIIIBCGwLj8IvnvMxaEUZIiCCCAAAIIIIBAagtYrVbJz883Xqk9UkaHAAKxFODO8ze6V199tbz77rsyffr0mHiff/758sEHH8ikSZNiUj+VIoAAAggggAACCCCQSgIV7kqxmTNSaUiMBQEEEEAAAQQQQAABBBBAYJgC6h7LvffeK9u2bZObb745LsFkVJc//PDDAXv+9NNPD7idjQgggAACCCCAAAIIIIBApAIElIlULHblbaGAMr2TCppBSl+B7WXV2uDzStMzoIzZIpKd79cs6mv4b0MDIYMAAiMqcMghh8jHH38sF110kZhM+t/yaHRsv/32kzVr1hBMJhqY1IEAAggggAACCCCAAAIIIIAAAggggAACcRUgoEwv7kMPPdSYFPzII4/I1KlTe+0Z+seZM2fKP//5T3n00UclNzd36BVxJAIIIIAAAggggAACaSTgCXikJGePNBoxQ0UAAQQQQAABBBBAAAEEEBhM4MADD5THHntMNm3aJFdccYU4HI7BikZ9e3l5ubzwwgsD1vve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ZRLhm7H2GHKHi2QUiHirW+bxwdsr5eIrT26psNDavJeWa6N1lASkaCTX4jWUFCw4C/UwLld527+nKcjClBFISYF77rlH5s6d2+HcVYjMFVdcIWeccYYMHjy4w3072qheQFtaWmp+1DMFoWXt2rXy3HPPmc8CbNq0KVSt/VQvn1UvwP3000/F4XBo2ygggAACCCCAAAIIIIAAAggggAACCCCAAAKJKmBP1IF1dlz333+/fPbZZ1F3Vydrb731Vtm+fbs8++yzMmPGjB6FyUTtxKhUqebqpPITTzwhu3btEjWmAQMGRN3997//facT1KM2QCUCCCCAAAIIIIBArwv4An4pzSvu9X7oAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEErCtQVdHqxWPG693smZa/Jc+6X0iSjFw97F54hE2bzRIjUMaKi6fJK+++8ak29MEn+MSYIkuKCziLfJqAq4JAGQ2EAgIpIrBmzRr59a9/3e5sTz/9dFm6dKl88skncvXVV/coTKbdTowNo0aNkttvv102btwo//nPf2TChAlRd1+3bp3cfffdUbdRiQACCCCAAAIIIIAAAggggAACCCCAAAIIJKKApa9eb9myRe66666orpdccomsX79efvOb35gp4lF36oXK3Nxc+d///V9RY7v++uuNC5/6lU+/3y/XXnttL/RMkwgggAACCCCAAAKxFMjN4k0ysfSkLQQQQAABBBBAAIE4CQSNfpLlEycyukEAAQQQQAABBBBAAAEEEEAAAQS6K+Aq1wMQ0hxp3W2K4xDQBIpaBcqs+WKbVJbVaPtYofDBgpVSX+tuGaotKIOP14NEWjaylkoCea0DZcpbBXSlEgZzRSCFBVRIjMfjaSOgAl7eeecdmTNnjkycOLHN9t6smDZtmixbtkyee+456d+/f5uufve735nBM202UIEAAggggAACCCCAAAIIIIAAAggggAACCCSggKUDZe69915xuyMuNhrAKtDlySeflH//+9+9lkLeme9RjePhhx+Wd999V/r166cdsmTJErNeq6SAAAIIIIAAAgggkFACZbVVkp+Vm1BjYjAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJA4AlWVegCC3WHp2/ESB5aRSN5Im6Rl6xBL3lmlV1ig9OaLy7RR9js0IDn9VCI2S6oL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1mJ3jYaGhjjzzDPjmc98ZjzjGc+ImpqaovdVkQABAgQIECBAgAABAgQIECheoL01G2iQr8sVv7OaBAgQIFC0QEPTcKZuZ1s20CuzUYEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGBWAYEys/LYSIAAAQIECBAgsNwC5Tl381tuc/0RIECAAAECBAgQILCyAoceemi8853vjCuvvDJuvvnmuPHGG+P222+PXbt2RW9vb4yMjERVVVVs2rQpduzYEccdd1zc//73jwc84AGRz/s31Mq+e3onQIAAAQIECBAgQIAAgY0g0DktUCYnUGYjvO3mSIDACgg0NGcDZYaHR6Onqy8aNtWuwGh0SYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBY2wICZdb2+2f0BAgQIECAAIF1J3BPZ0eUJbMaX3czMyECBAgQIECAAAECBAjMLpCGwzzwgQ8sPGavaSsBAgQIECBAgAABAgQIECCwnALtMwJlBLwup7++CBDYOAL1m0dmTLajrUegzAwVKwgQIECAAAECBAgQIECAAAECBAgQIECAAAECcwvk5q6iBgECBAgQIECAAIHlE9g72B+Hbd66fB3qiQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABArMIpGEG+y/5Oqfd7e/hNQECBEolUF4xHjX12VCZjmmhXqXqSzsECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgfUuUL7eJ2h+BAgQIECAAAECa0+gvrJm7Q3aiAkQIECAAAECBAisFoHx1TKQRYxjPcxhEdO3KwECBAgQIECAAAECBAgQILC6BDp2d2cGlBMok/FQIECAQCkFGpqGo3/vvtObO6eFepWyL20RIECAAAECBAgQIECAAAECBAgQIECAAAECBNazgFulrOd319wIECBAgAABAmtUoH1v9oTMNToNwyZAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAYB0IdLRlv7/M1eXXwaxMgQABAqtToKFpJDOwjtbsMTizUYEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQOCgAgJlDkpjAwECBAgQIECAwEoJ7O7pji11jSvVvX4JECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQEOjbOxCDA8MZjVyd0+4yIAoECBAooUBDc/aY29neU8LWNUWAAAECBAgQIECAAAECBAgQIECAAAECBAgQ2DgCvtneOO+1mRIgQIAAAQIE1pTA1rpNa2q8BkuAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwPoTaG/tnjGpfG1+xjorCBAgQKA0Ag1N2UCZ9t0zj8Ol6UkrBAgQIECAAAECBAgQIECAAAECBAgQIECAAIH1LVC+vqdndgQIECBAgAABAmtVoHdwYK0O3bgJECBAgAABAgQIrJzAeNJ1+rAQIECAAAECBAgQIECAAAECBAiURKCzrSfTTlllWZRVlGXWKRAgQIBA6QQat2QDZdp2d5WucS0RIECAAAECBAgQIECAAAECBAgQIECAAAECBDaQQG4DzdVUCRAgQIAAAQIE1pDA7zvboq6yeg2N2FAJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEFhvAtODDHJ1Trlbb++x+RAgsLoEpgfKdOzuXl0DNBoCBAgQIECAAAECBAgQIECAAAECBAgQIECAwBoR8O32GnmjDJMAAQIECBAgsB4E8mXF//o5Ph5RW1EThzRsXg9TNwcCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNagQNuuPZlR5+vzmbICAQIECJRWoKF5ONNge6tAmQyIAgECBAgQIECAAAECBAgQIECAAAECBAgQIECgSIHir+gtskHVCBAgQIAAAQIECBxMoCI/v5MrW7r3RGt3T5xwyJEHa9J6AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQILJlAW0tXpu2cQJmMhwIBAgRKLdA4LVBmb3d/DA2NlLob7REgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE1r2AQJl1/xabIAECBAgQIEBg9Qjkc/MLlElHPjo2Fr/ceXcctXl7NFbXrp7JGAkBAgQIECBAgACB1SgwngxqvTxWo68xESBAgAABAgQIECBAgAABAhtOYGagjFPuNtwPgQkTILCsAtMDZdLO26eFey3rgHRGgAABAgQIECBAgAABAgQIECBAgAABAgQIEFijAuVrdNyGTYAAAQIECBAgsAYFcmVlCx7179p3R21FVRx/yJGx8FYW3L0dCRAgQIAAAQIEVqFARX7+gYWrcBqGRIAAAQIECBAgQIAAAQIECBAgsIoFWqeFGOQbfCa1it8uQyNAYB0IVNeNRXnlWIwM7Qvw6mjtjh1HblkHszMFAgQIECBAgAABAgQIECBAgAABAgQIECBAgMDyCQiUWT5rPREgQIAAAQIENrxA2SKjYPqGB+NXO+/e8I4ACBAgQIAAAQIEJgSGR0dRECBAgAABAgQIECBAgAABAgQIEFhSgbZdezLt5+oEymRAFAgQILAEApu2DEX7zuqpltuTQBkLAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDA/AT2xffPbz+1CRAgQIAAAQIECMxbYDzG572PHQgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQILASAuPj4zE9xCBf75S7lXgv9EmAwMYSaNwykplw++6uTFmBAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgbgHfbs9tpAYBAgQIECBAgECJBMaSEy4tBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQWAsCne17Y3RkLDPUXH0+U1YgQIAAgdILNDQPZxpt392dKSsQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjMLVA+dxU1CBAgQIAAAQIECJRGYHQ0e7JlaVrVCgECBAgQIECAAAECUwJphqMcxykOLwgQIECAAAECBAgQIECAAAECixFoa9mT3b0sIlfnHm5ZFCUCBAiUXqBxWqBMR6tAmdIra5EAAQIECBAgQIAAAQIECBAgQIAAAQIECBBY7wK+3V7v77D5ESBAgAABAgRWkcDw2MgqGo2hECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBA4OACbbu6MhvTMJmysiRVxkKAAAECSyowPVCmbbdAmSUF1zgBAgQIECBAgAABAgQIECBAgAABAgQIECCwLgUEyqzLt9WkCBAgQIAAAQKrU2BoRKDM6nxnjIoAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgekCbS3ZQJl8fX56FWUCBAgQWAKBhubhTKsdrQJlMiAKBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBATKFIGkCgECBAgQIECAQGkEBoYHS9OQVggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQILDEAm0tezI95ATKZDwUCBAgsFQCjVuygTLtAmWWilq7BAgQIECAAAECBAgQIECAAAECBAgQIECAwDoWECizjt9cUyNAgAABAgQIrDaBPX19cXTzIattWMZDgAABAgQIECBAYH0JjCfTWQ+P9fWumA0BAgQIECBAgAABAgQIECCwBgXaWroyo843ON0uA6JAgACBJRJobM4GynR39sbI8OgS9aZZAgQIECBAgAABAgQIECBAgAABAgQIECBAgMD6FPAN9/p8X82KAAECBAgQILBqBUbH0itbLQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEFjdAq27soEyufr86h6w0REgQGCdCDRMC5RJp9Xe2r1OZmcaBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHlERAoszzOeiFAgAABAgQIEPgfgd93tMZRzYfwIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwKoWaGvZkxmfQJkMhwIBAgSWTKC2YTTyFWOZ9jsEymQ8FAgQIECAAAECBAgQIECAAAECBAgQIECAAAECcwkIlJlLyHYCBAgQIECAAIGSC4yPjZe8TQ0SIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAJtLV2Z5vL1TrfLgCgQIEBgCQUam4czrbftzh6TMxsVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEZgj4hnsGiRUECBAgQIAAAQJLLXBXR2sc1bxtqbvRPgECBAgQIECAAIENJzCeZDeuh8eGe+NMmAABAgQIECBAgAABAgQIEFh1Av29g9GXPPZfcvX5/YteEyBAgMASCjRuyQbKdOzuXsLeNE2AAAECBAgQIECAAAECBAgQIECAAAECBAgQWH8CAmXW33tqRgQIECBAgACBNSEwPl62JsZpkAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIbDyB1l17Zkw6L1BmhokVBAgQWCqBxuZsoEx7q0CZpbLWLgECBAgQIECAAAECBAgQIECAAAECBAgQILA+BQTKrM/31awIECBAgAABAqte4K723XFk07ZVP04DJECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBg4wm0tXRlJl1WVRZl5W6akUFRIECAwBIKNG6ZFiizO3tcXsKuNU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQWBcCAmXWxdtoEgQIECBAgACBtSmQnHK5Ngdu1AQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIrGuBtmnB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673i/gIvvPCC7NixQ0nRitb83//9n2hFVwK9tMJW999/v1e6VkDltttuk507d3r1+QpkZ2fLE0884dX95ptvysaNG73i/gIHDhyQ5cuXe6XcfffdohXN8XdpRXpOO+00efLJJ+XTTz+Vjz76SJYtWybnnHOOTJgwQT755BN/w3X7NKN169Z5+rS/f/UK3ngSuEEAAQQQQAABBBDoFYHqygapr21S1orLoKCMAkIDAQQQiGABr4IyJYcLvUbwsdg6AggggAACCCCAAAIIIIAAAggggAACCCCAAAIIINAtAQrKdIuPwQgggAACCCCAQOgEXO/j8Xt19VOR/U5KJwIIIIAAAggggAACCESVgOs9/RIND3GdI1av5557TsaOHevz+BaLxV0c5vbbbxetEMKhq7CwUCZPniz/+te/DoXcxWm2bNkiEydO9MS4QQCByBFYunSpjBkzRtlwWVmZ3HPPPUrMX6OqqkruvPNOr5Rf//rXctZZZ3nF/QW0wjS+LofDIc8++6yvbt34dddd51U0R/v514IFC6S1tVV3TMegln/zzTdLS0uL0jV16lT5zW9+o8T0Gp999plof3/qXdrfn1OmTBGtMI3dbtdL8Yr9/ve/l9/97nee+IwZM2TNmjWSkJDgiXGDAAIIIIAAAgggcGQECvZZ1IVNIsY01//jQgABBBCICoHMXKtyDktJtdKmgQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjEogAFZWLxWefMCCCAAAIIIBCRAo62tojcN5tGAAEEEEAAAQQQQAABBBAIXMBsNsu7776deuGnAABAAElEQVQrw4YN8zlIK6CwaNEiGTdunLz99tvy17/+1X2/ceNG95i8vDz55z//Ka+++qrk5OT4nIcOBBAIb4G+ffvK2rVrZdSoUcpGtUIzTz/9tBLTa2hFVi677DIpLi5Wum+44QZ58MEHlVggjcrKSr9pHdfxm/z/O5cvXy4XXnihkvr111/LjTfeKG0B/CxMK4yj/T3Y/jrxxBPl3//+t5hMnb85uLMcbQ+PPvqou2DXrl272i+j3GuFey6//HL51a9+5Yn//Oc/lzfffFMSExM9MW4QQAABBBBAAAEEjpxA/t4yZfG4zHgxdPaJH8oIGggggAAC4SyQ0aGgTHlJTThvl70hgAACCCCAAAIIIIAAAggggAACCCCAAAII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SQIECBAgQIAAAQIECBAgkBSBDS/GDWVS07xdLinXVh0ECBDIJ1A/uz1KN2soE3kICBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgfIS8Bvy8rreqiVAgAABAgSKWKCjq6uId2drBAgQIECAAAECBAgQKD+Bv/qrvxqVop999tnQled7wtNPP31I619zzTXhy1/+ctiwYUOfeZ5//vk+OQkCBAgQIECAAAECBAgQIJAkgY72znBsI4EqDWWSdInVQoAAgT4C9Q1xQ5mtTbv6HCNBgAABAgQIECBAgAABAgQIECBAgAABAgQIECgXgcpyKVSdBAgQIECAAIFiF9BQptivkP0RIECAAAECBAgQIEBgZATWrFmTd+KpU6fmzQ80WVVVFa6++uq8h+/cuTNvXpIAAQIECBAgQIAAAQIECCRFYFPj9pDNZqNyUnU+fy0CERAgQCBhAtOPaShzqK0j7NqxP2FVKocAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgMDABDWUG5uQoAgQIECBAgMCIC7R3do74GhYgQIAAAQIECBAgQIAAgeIT2Lt3b95NVVdX580PJnnJJZfkPbzQmnkPliRAgAABAgQIECBAgAABAiUosOHFbdGuKydVhYq0t8tFKAICBAgkTGByfWeoSmeiqpo3aa4dgQgIECBAgAABAgQIECBAgAABAgQIECBAgACBshHwG/KyudQKJUCAAAECBIpdoK2jI6R7Pj3egwABAgQIECBAgAABAgTKS+DYT0s/Uv2ePXuOvDzhr2eddVbec2tqavLmJQkQIECAAAECBAgQIECAQFIENqyNG8qkpqWSUpo6CBAgQKCAQGXPu6LrZ3VEo82bdkSxgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQLgIaypTLlVYnAQIECBAgUPQCmWw2nDR9eqisqCj6vdogAQIECBAgQIAAAQLFKpDt2VgSnsXqOzL7qq+vzzvx7t278+YHk6yrq8t7eENDQ968JAECBAgQIECAAAECBAgQSIrA+jXNUSlVdRrKRCACAgQIJFSgvqE9qmxr064oFhAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEykVAQ5lyudLqJECAAAECBEpC4MWt28KpsxuCljIlcblskgABAgQIECBAgAABAsMisHjx4rzzvPDCC3nzg0m2t8c3Txw598wzzzzy0lcCBAgQIECAAAECBAgQIJBIgRdXbInqStWno1hAgAABAskUqJ8d/0x0y6adySxUVQQIECBAgAABAgQIECBAgAABAgQIECBAgACB4whoKHMcIMMECBAgQIAAgdEWWL1lS1g0Z+5oL2s9AgQIECBAgAABAgQIEBgjgfPOOy9Mnjy5z+q/+93v+uQGm9jS8z1mvscll1ySLy1HgAABAgQIECBAgAABAgQSIXBgf1toboobCKRmaCiTiIurCAIECBxHoL6hIzqiWUOZyENAgAABAgQIECBAgAABAgQIECBAgAABAgQIlI+AhjLlc61VSoAAAQIECJSQwIrNm8PL584roR3bKgECBAgQIECAAAECBAicqEAqlQpXXXVVn9OfeOKJsHbt2j75wSQeffTRPodfeOGF4WUve1mfvAQBAgQIECBAgAABAgQIEEiKwNrlm+NSet4lV1WXinMiAgQIEEikQP3s9qguDWUiDgEBAgQIECBAgAABAgQIECBAgAABAgQIECBQRgIaypTRxVYqAQIECBAgUFoCy5uaNJUprUtmtwQIECBAgAABAgSKQyDbs40kPItDc9R28alPfSqk0/EnpWez2fD1r399SHu4/fbb+5z/6U9/uk9OggABAgQIECBAgAABAgQIJElgzbKmqJzU9HSoqKqIcgICBAgQSKbA9Ia4ocyObXtDZ0dXMotVFQECBAgQIECAAAECBAgQIECAAAECBAgQIECgHwENZfrBMUSAAAECBAgQGGuBXFOZxXPnjvU2rE+AAAECBAgQIECAAAECIyxw6qmnhs9+9rN9Vvn2t78dHnnkkT75gSQeeOCB8PDDD0eHXnbZZeHyyy+PcgICBAgQIECAAAECBAgQIJA0gTXL+zaUSVqN6iFAgACB/AL1xzSUyTXu3rp5V/6DZQkQIECAAAECBAgQIECAAAECBAgQIECAAAECCRbQUCbBF1dpBAgQIECAQDIEVjRtDqc3zElGMaogQIAAAQIECBAgQIAAgYICuYYyb3/726PxTCYT3vOe94Rly5ZF+eMFGzZsCFdffXV02KJFi8Jtt90W5QQECBAgQIAAAQIECBAgQCCJAmuPbSgzI53EMtVEgAABAnkEaiZkQu3kzmikedPOKBYQIECAAAECBAgQIECAAAECBAgQIECAAAECBMpBQEOZcrjKaiRAgAABAgRKXmB1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AAECBAgQIECAAAECJSew9Om4oUy6YVzJ1WDDBAgQIDD8AjPnxQ1ldrXsCwcPxLnhX9WMBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHiENBQpjiug10QIECAAAECZSywfFNTWDhzZhkLKJ0AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQInJtDW2h5eXLk5OjndUB3FAgIECBAoT4HpDe2hojIbFb+pcXsUCwgQIECAAAECBAgQIECAAAECBAgQIECAAAECSRVIJbUwdREgQIAAAQIESkUg97aVzTt2hYapdaWyZfskQIAAAQIECBAgUBQCVZX6ZRfFhbAJAgQIECBAgAABAgQIECBAgMAYCix7bn3IZI5qFtDzI6PUTA1lxvCSWJoAAQJFI5BKZ8P02e2hZUtN756a1reERWfP7429IECAAAECBAgQIECAAAECBAgQIECAAAECBAgkVUBDmaReWXURIECAAAECJSXQ3tUVtuzaXVJ7tlkCBAgQIECAAAECYy3QncmM9RaKb/1sz81TuWepP5JQQ6lfA/snQIAAAQIECBAgQIAAAQIlIrD06XXRTnPNZCpSFVFOQIAAAQLlKzBz7qGoocymxu3li6FyAgQIECBAgAABAgQIECBAgAABAgQIECBAoKwEfIRvWV1uxRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCA5AkueboyKSTdUR7GAAAECBMpbYNZJhyIADWUiDgEBAgQIECBAgAABAgQIECBAgAABAgQIECCQYAENZRJ8cZVGgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBpAp0d3WHlc9viMpLaSgTeQgIECBQ7gIz57VHBE2NLVEsIECAAAECBAgQIECAAAECBAgQIECAAAECBAgkVUBDmaReWXURIECAAAECoy4wvton3Y06ugUJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEChbgTXLN4dDbR1R/WkNZSIPAQECBMpdYNa8QxFB0/qWkM1mo5yAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJBEAQ1lknhV1USAAAECBAiMicDU2toxWdeiBAgQIECAAAECBAgQ6BXIvQc+k4Cn9/L3XlIvCBAgQIAAAQIECBAgQIAAgcICS59eFw1W1aVCZU1VlBMQIECAQHkLzDwpbiiTa0S2Y9ve8kZRPQECBAgQIECAAAECBAgQIECAAAECBAgQIFAWAhrKlMVlViQBAgQIECAwGgLtnV2jsYw1CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBHYMkxDWXSDdVcCBAgQIBAJDC1vjOMq+mOck2N26NYQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBIooCGMkm8qmoiQIAAAQIExkRgy85doWJMVrYoAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyk9g2TONUdGphnFRLCBAgAABAjmBmfMORRCbGluiWECAAAECBAgQIECAAAECBAgQIECAAAECBAgQSKKAhjJJvKpqIkCAAAECBMZE4GB7R2ioqxuTtS1KgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBchLYvKEl7N55ICo5Pbs6igUECBAgQCAnMKtPQ5ntYAgQIECAAAECBAgQIECAAAECBAgQIECAAAECiRdIJb5CBRIgQIAAAQIERlFgyoTasGX37lFc0VIECBAgQIAAAQIECBA4SiCbDSH39CBAgAABAgQIECBAgAABAgQIJFxgydONUYUVEypD1RRvh4tQBAQIECBwWGDmSYciiab1GspEIAICBAgQIECAAAECBAgQIECAAAECBAgQIEAgkQKViaxKUQQIECBAgACBMRLo6Owao5UtS4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgfIRWPr0uqjYdMO4KBYQIECAAIEjArPmxQ1lNq1rOTLkKwECBAgQIECAAAECBAgQIECAAAECBAgQIEAgsQIayiT20iqMAAECBAgQGAuBLbt2hYqxWNiaBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKCOBJX0aylSXUfVKJUCAAIHBCMyc1x4dvnXzrtDR4UOjIhQBAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkDgBDWUSd0kVRIAAAQIECIylwMH2jtBQVzeWW7A2AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEEi2wZ9eB0NTYEtWYbtBQJgIRECBAgECvwMx5h3pf515ks9mwZUP890h0gIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAABDWUScBGVQIAAAQIECBSXwNQJtcW1IbshQIAAAQIECBAgQKDMBLI99SbhWWaXTbkECBAgQIAAAQIECBAgQIDAgAWWPt0YH5uqCFX16TgnIkCAAAEC/0+gZnwmTKnviDw2rddQJgIRECBAgAABAgQIECBAgAABAgQIECBAgAABAokT0FAmcZdUQQQIECBAgMBYC7R3do31FqxPgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBxAosfXpdVFt6dnWoqKyIcgICBAgQIHC0wMx5h44OQ9O67VEsIECAAAECBAgQIECAAAECBAgQIECAAAECBAgkTUBDmaRdUfUQIECAAAECYy7QuH17mDllypjvwwYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQRIElxzaUaahOYplqIkCAAIFhFJh1TEOZTY0aygwjr6kIECBAgAABAgQIECBAgAABAgQIECBAgACBIhTQUKYIL4otESBAgAABAqUt0N2dCdWVqeDz70r7Oto9AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLFJ7B/78GweummaGOphnFRLCBAgAABAscKzDrpUJTatL4ligUECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaQJaCiTtCuqHgIECBAgQKAoBDbt2BlePm9eUezFJggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAUgad/uypkMtmXyqkKIT27+qXYKwIECBAgkEdg5rz2KNvUuD2KBQQIECBAgAABAgQIECBAgAABAgQIECBAgACBpAmkklaQeggQIECAAAECxSKwuqk5vHzu3HDUWxmLZWv2QYAAAQIECBAgQCARAr+q6rlbyCMWyPZ8B5J7lvojASWU+iWwfwIECBAgQIAAAQIECBAgUKwCTzyyItpaeu64UJGqiHICAgQIECBwrMCseYei1L49B8Pe3a1hSl1tlBcQIECAAAECBAgQIECAAAECBAgQIECAAAECBJIioKFMUq6kOggQIECAAIGiE+js7g7LN24uun3ZEAECBAgQIECAAIGkCHR2dSelFHUQIECAAAECBAgQIECAAAECBAgMQCDb00j3qd+sjI6snl8TxQICBAgQIJBPoH5We6hKZUJ3V2XvcNP6Fg1lejW8IECAAAECBAgQIECAAAECBAgQIECAAAECBJIm8NJvRZJWmXoIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIDECa5Y3hd079kf1pE/WUCYCERAgQIBAXoHKqhCmN7RHY5sat0exgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQJAENZZJ0NdVCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBhAo8+ciKqLLKKalQNTkV5QQECBAgQKCQwKyTDkVDGspEHAICBAgQIECAAAECBAgQIECAAAECBAgQIEAgYQJ+m56wC6ocAgQIECBAgAABAgQIECBAgAABAgTKWSAbQrbnWfKPJNRQ8hdBAQQIECBAgAABAgQIECBAoOgEnng0bihTffK4otujDREgQIBA8QrMmndMQ5l124p3s3ZGgAABAgQIECBAgAABAgQIECBAgAABAgQIEBiiQOUQz3c6AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAYUYH9ew+Glc9viNZIz6+JYgEBAgQIEOhPYPb8uKFM4+qt/R1ujAABAgQIECBAgAABAgQIECBAgAABAgQIECBQ0gIaypT05bN5AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIJF/gqd+sDJlM9qVCUxUhPWfcS7FXBAgQIEDgOAJzTzkYHdG8aWc41NYR5QQECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaQIaCiTlCupDgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECCRU4MlHVkSVpedUh4qqiignIECAAAEC/QnMnn8oVFS+1Jwsm82G9Wu29neKMQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAyQpoKFOyl87GCRAgQIAAgdEWqJ80cbSXtB4BAgQIECBAgAABAgQGJZDNhJCI50vv5x9U/Q4mQIAAAQIECBAgQIAAAQIEkimQu+H/qf9dGRVXfXJNFAsIECBAgMDxBNLV2TBzzqHosMbVzVEsIECAAAECBAgQIECAAAECBAgQIECAAAECBAgkRUBDmaRcSXUQIECAAAECIy4wfdKkEV/DAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAQC6xeuins2XUgSqbnaygTgQgIECBAYEACcxa2Rcc1rt4SxQICBAgQIECAAAECBAgQIECAAAECBAgQIECAQFIENJRJypVUBwECBAgQIDDiApWV/uk04sgWIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwDECTz66IspUTk2FqsmpKCcgQIAAAQIDEZh7TEOZdaubB3KaYwgQIECAAAECBAgQIECAAAECBAgQIECAAAECJSfgruiSu2Q2TIAAAQIECIyVwP6D8ScUjdU+rEuAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFyEji2oUz1/HHlVL5aCRAgQGAYBeYuPBjN1rhKQ5kIRECAAAECBAgQIECAAAECBAgQIECAAAECBAgkRsDHtCTmUiqEAAECBAgQGGmBLbt2h1RVVejq7h7ppcxPgAABAgQIECBAgACBExKoyGZD7lnqj4rSL6HUL4H9EyBAgAABAgQIECBAgACBohHYt6c1rHh+Y7Sf9PyaKBYQIECAAIGBCsxZGH+g1J5dB8LunftDXf2kgU7hOAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBASQhUlsQubZIAAQIECBAgUAQC3ZlsaKibWgQ7sQUCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUh8Dvf7MqZI9uoJuqCOk548qjeFUSIECAwLALTG9oD+lx8YdJNa5qHvZ1TEiAAAECBAgQIECAAAECBAgQIECAAAECBAgQGGsBDWXG+gpYnwABAgQIECgpgYk1PumupC6YzRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUNICD//i2Wj/6bnjQkVVRZQTECBAgACBgQpU9rxzes7JbdHh61ZrKBOBCAgQIECAAAECBAgQIECAAAECBAgQIECAAIFECGgok4jLqAgCBAgQIEBgtAS6M5nRWso6BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKGuBA/vawhOPrIgMqhf6EJAIRECAAAECgxaYuzBuKNO4esug53ACAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKDYBVLFvkH7I0CAAAECBAgUk0DL3v3FtB17IUCAAAECBAgQIECAQF+BbLZvruQySaih5NBtmAABAgQIECBAgAABAgQIFJ3AI/c/F7o6u1/aV8/Hp1UvHP9S7BUBAgQIEDgBgTl9Gso0n8AsTiFAgAABAgQIECBAgAABAgQIECBAgAABAgQIFLdAz6/YPQgQIECAAAECBAYqsGv/gTB5vDcoDtTLcQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEDhRgV//7Ono1OqTa0LlOG95i1AEBAgQIDBogbkLD0bnrF+zNWQymSgnIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUuoDfrpf6FbR/AgQIECBAYNQFZk2dOuprWpAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLlJNCybU944al1UcnVp02IYgEBAgQIEDgRgTkL26LT2g91huZNO6OcgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQ6gKpUi/A/gkQIECAAAECoy2wenNzqKgY7VWtR4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgfIReOjeZ0I2m+0tuKK6IlSfXNMbe0GAAAECBE5UYHJdV5g0tTPs35PunaJx9dYw9+QZvbEXBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFSF9BQptSvoP0TIECAAAECYyJw1PsWx2R9ixIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCDJAr+65+movOpTxoeKKp/8EaEICBAgQOCEBeYsbAurnj26ocyW8No3n33C8zmRAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBsAhrKFNsVsR8CBAgQIECAAAECBAgQIECAAAECBAicoEA2kw25Z6k/jv708VKvxf4JECBAgAABAgQIECBAgACBwQtseHFbeHHllujEcadNiGIBAQIECBAYisDchQd7GspM7p2icXVz72svCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJJEKhMQhFqIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFkCPz6nt9HhVTWVobUnOooJyBAgAABAkMRmLOwLTp9nYYykYeAAAECBAgQIECAAAECBAgQIECAAAECBAgQKH0BDWVK/xqqgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAokR+PW9z0S1VJ86IVRUVEQ5AQECBAgQGIrA3AVxQ5nN61tCR3vnUKZ0LgECBAgQIECAAAECBAgQIECAAAECBAgQIECgqARSRbUbmyFQhAJPPPFEOOmkk8KcOXPGZHc7duwIq1atCs3NzaGtrS3U1taGWbNmhXPPPTdMnDhxTPZkUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIjIbDsmcawtWlXNPW408dHsYAAAQIECAxVoOHkXEOZbM/z/xqWZTLZsOHFbeG0M+YNdWrnEyBAgECRCGzdujU0NjaGiy66aEx21N7eHlasWBE2btwY9u3bF9LpdKirqwtnnHFGmDfP3zdjclEsSoAAAQIECBAgQIAAAQIECBAoMwENZcrsgit34AK5RjJ///d/H/7nf/4n3HDDDeFv/uZvBn7yEI9ct25duOWWW8Ldd98dVq5cmXe2ysrKcMEFF4T3vve94ZprrgkTJkzIe5wkAQIECBAgQIAAAQIECBAgQIBAOQnk3vyee3oQIECAAAECBAgQIECAAAECBEpT4Nf3PhNtvHJqKqSmV0c5AQECBAgQGKrAuPGZMKOhPbQ01/RO1bi6WUOZXg0vCBAgULoCLS0t4atf/Wr41re+FRYvXhyeeSb+HmMkKzt48GD4wQ9+EH70ox+Fxx57LOSayuR75D7w9k//9E/DtddeG0477bR8h8gRIECAAAECBAgQIECAAAECBAgQGLKAhjJDJjRB0gSefvrpw41k7rvvvlEvrbm5Ofzd3/3d4R8iZzKZw+tXVFSEV7ziFeGcc84J1dXVh7uk//a3vw1tbW3h8ccfP/z80pe+FK6//vrDjWVGfdMWJECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIDAMAl2d3eGh+56NZhr3svFRLCBAgAABAsMlMHdhW9RQZt2q5uGa2jwECBAgMAYCO3fuDP/0T/8Ubr755tDa2jqqO+ju7g7f/OY3wxe/+MWQ28eRR0NDQ3jta18b6uvrw44dOw43mdmyZUvYtGlT+Jd/+Zdw0003Hb4HILfvyZMnHznNVwIECBAgQIAAAQIECBAgQIAAAQLDIqChzLAwmiQJAs8991z4/Oc/H37605+OSTm5da+55pqwa9eu3vVzPzz+13/918PNZHqTPS/27NkTbrjhhsPP3A+ft23bFj70oQ+Fn/3sZ4eb0dTW1h59uNcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEil7goZ8/E/btjm/8rD5tQtHv2wYJECBAoDQFcg1lnnusrnfzjau39L72ggABAgRKR2D37t29zVn2798/6hvPNYe54oorDn9Q7JHFcw1kcs1i3vOe94TKysoj6ZDNZsNdd90VPvGJT4TNmzeH3L0At9xyS3jggQfCT37yk3Deeef1HusFAQIECBAgQIAAAQIECBAgQIAAgaEKvPSTqaHO5HwCJSqwdOnS8O53v/vwD1/HqplMrgv6O97xjqiZzJVXXhkeeuihPs1kcsxTp04N119/fbj33ntDTU1Nr/zdd98dLrnkkmie3kEvCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBSxwI9vfSTaXaqhOlRN9plpEYqAAAECBIZNYO7Cg9Fcjaubo1hAgAABAsUtsG/fvvCFL3whLFy4MHzpS18KY9FMZvny5eHVr3511Exm3rx54amnngrvfe97o2YyOc2Kiorwrne9Kzz77LPh7LPP7gXeuHFjeP3rXx8efPDB3pwXBAgQIECAAAECBAgQIECAAAECBIYqoKHMUAWdX7ICK1asCFddddXhhi3//d//fbjb91gU8+///u+HO4znuo0febzqVa8K3//+90Mq1f8bYt7ylreE73znO0dOO/w198PnP/mTPwmHDh2K8gICBAgQeOn/Z/NZdPV8ykPVUZ8Cke8YOQIECBAgQIAAAQIECBS9QO5nTEl5Fj22DRIgQIAAAQIECBAgQIAAAQLDKfD8k2vD2hWboylrzpkYxQICBAgQIDCcAnMXtkXT7dy+L+zb0xrlBAQIECBQfAK5xjFf/vKXw4IFC8LnP//5sHfv3jHZ5Isvvhje9KY3hW3btvWuX11dHe65557DTW56k3lezJgxI9x///0h9/XI48CBA+Hyyy8PzzzzzJGUrwQIECBAgAABAgQIECBAgAABAgSGJKChzJD4nFyKAmvWrAnve9/7wllnnRXuvPPOMH/+/DBhwoQxKeWBBx4IH/7wh6NmNlVVVeHWW28N6XR6QHt6//vff/gHx0cf/Nvf/jZce+21R6e8JkCAQNkLdGcy/RrsO9gWZk+dGmZMntTvcQYJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAYPgFfnzrI9GklZOrQvrkmignIECAAAECwykwc+6hkErH7ylat6p5OJcwFwECBAgMo0Bra2u44YYbDjdr+exnP3v4w1tnzZo1jCsMfKp9+/aFSy+9NDQ3x39v/O3f/m145StfOaCJ5syZE26++ebo2FxTmT/+4z8OO3bsiPICAgQIECBAgAABAgQIECBAgAABAicioKHMiag5p2QFbrzxxnD22WeHPXv2hB/+8Idh9+7dYf369Yd/kHvxxRePal0tLS3hAx/4QMgc0+Dg6quvPtzsZjCbydVVWRn/53zbbbeF//zP/xzMNI4lQIBAogW6M9nj1rd5x66w70BbWDx3znGPdQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIDI/Alo07wu8eXBZNVnP2xFBRURHlBAQIECBAYDgFKqtCmLOgLZpy9dKNUSwgQIAAgeIQeP7558Mpp5wSvve974VPfepTYe3atWH79u1h69at4frrrx/1TX7kIx8J69ati9atr68PuYYyg3lceeWV4YILLohO2bJlS/jQhz4U5QQECBAgQIAAAQIECBAgQIAAAQIETkQg7kBxIjM4h0AJCVx44YVhw4YN4d577w25H75Onjz58O5zX7/whS+MaiXXXntt2LZtW581//qv/7pP7niJ0047LVx++eV9DvvEJz4R9u7d2ycvQYAAgXIU6OruHlDZ7V1dYeWmLWFRzyc/jK9OD+gcBxEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAiQv8922Phmz2pQ8JqaiuCOMWTTjxCZ1JgAABAgQGKLBgUWt05Molm6JYQIAAAQLFIZB7v/99990Xli5derihzKmnntq7sVwTl9mzZ/fGI/3ixz/+8eEPtz12nb/4i78ItbW1x6aPG1933XV9jrnnnnvCXXfd1ScvQYAAAQIECBAgQIAAAQIECBAgQGAwAhrKDEbLsSUv8LrXvS7MmjUrbx2vfvWr8+ZHIvn444/n/QFvrrv4WWeddUJL5utC3tLSEr74xS+e0HxOIkCAQNIEOrsG1lDmSN2rmraECdXjwknT64+kfCVAgAABAgQIECBAgEBpCORuvir1Z2lIlkdbuQAAQABJREFU2yUBAgQIECBAgAABAgQIECAwDAIH9rWF++96Ippp3OLaUJH21rYIRUCAAAECIyJwbEOZVUs2jsg6JiVAgACBoQksXLgwvOpVr8o7SWVlZTjvvPPy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vx7lBhJs37493HbbbX0O/cAHPhDOP//8PnkJAgQIJF2gvbMz6SWqjwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAsMq0NnRFf71yz8Jn/vLfw+tBw7Fc6crDjeSqTl/Ssh9wJEHAQIECBAoF4FF5+6PSl29vCkcPPbvyegIAQECBAiMhUChewFG4j6A97znPXnfo/+1r30t5O4HGOzjxhtv7HPKzJkzw+c+97k+eQkCBAgQIECAAAECBAgQIECAAAECgxHQUGYwWo5NtMAzzzxTsL7du3cXHDvRgTPPPDN8+ctf7nP6zTffHNavX98n318i98Pitra26JCTTjop5PvhcnSQgAABAgkVaNy6I8ycOjmh1SmLAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECwyuweUNL+Ksrbwp3fe/RPhNXTk2FiZfNDOmTx/cZkyBAgAABAkkXOPWMA6EqlektM9OdCUufaeyNvSBAgACBsRfINY159tln825kJO4DyDXZ/MEPfhBqa2ujNXP3AHzzm9+McscLHn744XDvvff2Oew73/lOmDzZ+2D7wEgQIECAAAECBAgQIECAAAECBAgMSkBDmUFxOTjJArfeemvB8h577LGCY0MZuO6668IVV1wRTXHo0KHwZ3/2Z6G7uzvKFwoeeOCBcMstt0TD48ePD3feeWeYPn16lBcQIECgnASm+0VaOV1utRIgQIAAAQIECBAgcESg582SuTdMlvzzSD2+EiBAgAABAgQIECBAgAABAiMu8OufPR3+4h3/HNYsb+qzVnrh+DDxbTNC1ZR0nzEJAgQIECBQDgLV47LhlMWtUam//83KKBYQIECAwNgK3HPPPaFQ45gNGzaEzZs3D/sGFy1aFP7jP/4jVFVVRXPnPih2xYoVUa5QsHfv3vChD32oz/CnP/3p8Pa3v71PXoIAAQIECBAgQIAAAQIECBAgQIDAYAU0lBmsmOMTKfC9730v3HHHHQVre/TRR8Pdd99dcPxEB3LdyW+//fbw7ne/O5rikUceCR/84AeP21TmySefDFddddXhG4SOTJDrdP7zn/88XHjhhUdSvhIgQKAsBTZsbQnpY35RV5YQiiZAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBA4P9n7z7goygTN44/u5teCCSEXlUUERXF3lEExd7/ip56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9913B+STgQACCCCAAAIIIIAAAu4R0IFVGoREn6v7Jw1u6T+Dp2cbJwPKnD171njn4BmEpR1ERowYYfoOwnN8s58ffvihMXjcPwC997Y6A6lbZgvXIJ+eumiAi02bNkU8mM27rXxGwM0COiBEAyhpYFtNek7RwFAa2MCppINCdACKJ+nyr7/+6lmM20//QeLaqU9nBCUhgEB4AiNHjjSC4une2j9AJ8YJdrZDzxG3bNliBFTRoOBWSYM3LVy40Gp1zPO9B8i/99578sADD8S8DhwQgWgL6Kym2iFe369o0pmudaCSDgANNmnQKR0opfcadvMe/v7770YH/GDLjeZ2GljTM1DWbUExo9luykbAaQHvieU0eMy4ceOMwelOHUf7JupgSe9ziz6X6NGjh1OHCLucqVOnSvPmzY39CxcubDxvyJkzZ9jlsSMCCCCAQPIL6P2wBpExezan17f777/fFMHpgDL63Ez7teqzcu+k1/KtW7emGzjWex/PZ+/vz548vc9evny5aXs92/ATAQQQSDYBffapz039A3ZpoGn9vhRq0r6uGnhS3/N6pz59+shbb73lncVnBBBAAAGHBf79919jooRKlSqZllynTh1ZsmRJwLpoBJSxGkOgk5br5OWhJh2zrO/k/NNrr70m/fr1889mGQEEEkgg+Ld7CdQoqooAAggku4BGMTQLJqPt1kAzsUpffPFFQDAZPbZGVgwnmIzue8899+gPn6SdWf2j5/pswAICCCDgQgHtGG8WTEZnjAgnmIw20SwowOHDh9NmUnUhA1VCAAEEXCOgD2GtgsloJbUDQyySdr7QjtP+SWeq7NKli392UMs6C7bZ7JY686d2+CYhgAACCCCAAAIIIICAewV0Jh+zYDJaYw3orrPSRTt9/fXXaTN667MrDYJp9Q7Cri49e/aUN998024T0aD4bkmvv/56WlV0Fr9IZ0ZPK4wPCCSAgAZ88ASTGTBggAwcONDRARw7d+6UuXPn+khop2g3JA2YtXv3bqMq1atXl5YtW7qhWtQBgYQU0AHcnmed+nxz9OjRIQeT0YbrYHA9L5UtW9bSQQfXWc3IbrlTlFboIHENCqhJZ5w06+MQpUNTLAIxFfjoo4/Sgslo8LW33347pGAyWlk9N+js20888YRt3d3yPUEHdXl/p9EO8toGEgIIhC6gg9815cqVS6ZPn+5o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Kq8k94EAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACxV6AgDLF/i0AAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUFQECyhSVV5L7QAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFiL0BAmWL/FgAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBIqKAAFlisoryX0ggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQLEXIKBMsX8LAIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACRUWAgDJF5ZXkPhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECg2AsQUKbYvwUAQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAANhS2O8AAEAASURBVAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGiIkBAmaLySnIfCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIFDsBQgoU+zfAgAggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQFERIKBMUXkluQ8EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKPYCBJQp9m8BABBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECgqAgQUKaovJLcBwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUewECyhT7twAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIFBUBAgoU1ReSe4DAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBIq9AAFliv1bAAAEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKCoCBJQpKq8k94EAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACxV6AgDLF/i0AAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUFQECyhSVV5L7QAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFiL0BAmWL/FgAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBIqKwJ5F5Ua4DwQQQAABBBBAAIG8F/jnn39s9erVtnHjRvv9999tx44dVrJkSStTpoxVrFjRatasaXvumX8/OX/++Wd76623bNKkSfbUU09Z9erV8x4p7oqbN2+2Bx54wD744APba6+9rE2bNtanTx8rUaJEXE4281tA7+9nnnnGxo0b576/GzVqZDfeeKNVrlw5v6vG9XNJYMmSJVa7dm0rVapULl2BYhFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQTSFfj6669t9uzZ9tVXX9mqVats69at9vfff1v58uVt3333tbp169qJJ55oJ598su2+O2trpetbXPNv2LDBpk6dajNnzrR27dpZhw4diisF940AAggggAACCCCAAAIIIIAAAggggEBGBTS3YM2aNe74a80x2GOPPdz5BeXKlbODDjrISpcundHrpVPY9u3bbcaMGTZ+/Hg799xzC0S7YFZWlj333HP2xhtv2G+//WYnnXSSO2b9gAMOSOfWyJtHAu+9954NGzbM7a849NBD7frrr7d69erl0dW5TF4L6LtM32FVqlTJ60tzPQQQQAABBDImsJvzgzMrY6VREAIIIIAAAggggECRFvjxxx9t2rRp7qDdzz77zJYvX247d+4MvGcFk1Gj7wknnGCnnXaaNWvWzNRolptp8eLFkSAyc+fONe/nrgYbH3HEEbl56ZRlb9q0yRSUZNmyZTF55TJlypR8Db4TUyE23PdNx44dbezYsTEaNWrUsE8++aRABCeKqRgbORL44osv7Pbbb3c/hxMmTLC2bdvmqDxORgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEMiZwJ9//mlPP/20Pfnkk/bNN9+EKuzAAw+07t27uwPtK1SoEOqcsJnefPNNa9iwoVWrVi3sKeQroAJvv/22O9j/nXfesV27drm11ASNOXPmZKzGw4cPt65du7qTZDJWKAUhgAACCCCAAAIIIIAAAggggAACCCBQAAUUAFxtbbNmzXLb2DROXkFRkqWqVavaMccc484vaNKkiRssfLfddkt2So6OrV271h0jrIVqFWBai+gqPfLII3bttdfmqOxMnNylSxcbNWpUTFFq79aYdc3FIBUcAfVbXHnl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b7nBinwz/N/OZs2a2YwZM6xly5b20UcfWcOGDW3q1Kl22GGHJTuNY/kgoMBY+heUOnbs6PZ3q/+QlWmDlNiPAAIIIIAAAggggAACCCCAAAJFWWDixInWvXt327x5c8JtlihRwm1D7d+/f8pAzDp5zz33tBYtWrj/1A6rRXD/+9//xpS7detW69Onj02bNs2dg6A5CWHTY489FsmqxSf79evntvdFdhawJ2pzUiB1v8SYdT+V/Nt34oknunNd4tubVSMFJD/22GPzr3JcOW0BzT/SwtkPP/ywG4Aq7QI4AQEEEEAAgUIksHshqitVRQABBBBAAAEEEMhFgR07dlibNm3cFRb9GrnUILlo0aLQwWSiq7r77ru7jcgaeHnWWWdFH3Kfq7H2lltusQsvvNA0UDM7SQ3FajguyEmDjtVYGJ/q169vl19+efxutvNRQJ0e9erVS6iBAhndd999CfvZUTAFfvjhB3dlXE1qeOWVV2yvvfYqmBWlVggggAACCCCAAAIIIIAAAggggAACCCCAQCEU0ED8TAWTWbBgQeDKth5NnTp1vKeRx0GDBiUEC4kcdJ5MmDAhejPp82+++cZOPvlk84LJ3HTTTQSTSSpW8A++/PLLvkGKLrvssoTKa2Xlo446yu2zCwomo5PWrFkTul9P7ycFkSlTpox9//331rhxY7e/NeHi7CjwAnXr1rVLLrmkwNeTCiKAAAIIIIAAAggggAACCCCAAAKZFhgyZIi1a9fON5iMgi9/8skn7jyAUqVKpX1pBVP5/PPPbeDAgb6L0iqws4J4rFy5Mu2ydYLmMKidtyCniy++2A1GHV/H8uXL2/333x+/m+18FDjkkEPs2muvTaiBFlR+9NFHQwVUSjiZHXku8Ntvv7lzpmrXrm133HGHaR6VvitICCCAAAIIFGUB/qcryq8u94YAAggggAACCIQU2LZtm7s6ngY0+iU1AivC93777ed3OPS+KlWquIOBteqfX3rjjTesQ4cO9ueff/odTrnvyCOPTJknPzNooPO8efPcFewUgVqDSLWq5QcffGB77713flaNa8cJqFNDr8u///1vtyPi+OOPt969e9sXX3xhfgPW405nswAI6LVTIJkVK1a4kftXr17tNvguXLjQDjjggAJQQ6qAAAIIIIAAAggggAACCCCAAAIIIIAAAggUboHDDz88Yzfw0ksvpSxr3333jcmj/qRZs2bF7IvfWLdunf3xxx/xuxO2FdCmUaNGbpuyDirgyODBgxPysaNwCWixDL+kBQS8pMUutGhFy5Yt7bvvvvN2J31ctmxZ0uPRB0855RQbP368u/Ky3o9NmjSxZAFros/lecES0GtJQgABBBBAAAEEEEAAAQQQQAABBIqTwM0332z655dq1arlBudu0KCB3+HQ+/bcc08bOnSoPfnkk75BZRQIXIGaly9fHrrM6IxVq1Y1LVxbUJMWy3z33Xft1ltvdQPLHHfccdarVy93zLqCHJMKlsDDDz9szz//vJ155pl29NFHu8GWZs6cad26dStYFaU2vgKTJk0yBZJRMPr+/fu7AeDVh7RhwwY7++yzfc9hJwIIIIAAAkVBYM+icBPcAwIIIIAAAggggED2Bf7++2+78MIL7cMPP/QtRA2wr776qqmxMhNJjb6jR4+27du325QpUxKKnDx5snXp0sVef/1130bhhBOidhTkxl6vmopMrUZEUu4KdO3a1e677z5T5PvsJq2qes8997j/slsG5+WfgFYR/fbbb61mzZoxlVDj/Y033mjXX399zH42EEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBNITKFmyZHonJMkdtOhB9CnxfVUKKKN+rmRpjz32sBIlSiTLYl999ZU1a9bMNm7c6OY766yzbNiwYUnP4WDhEChdurRvRefPn2+a6KLXvG3btu4qyr4ZA3amu8hF06ZN7fHHH7crr7zSNm3aZM2bN3evedBBBwVcgd0FUWCfffYpiNWiTggggAACCCCAAAIIIIAAAggggECuCDz00EM2ZMgQ37K1SK2CoNSoUcP3eHZ2qu1sx44ddsMNNyScrsUl1W772WefWaVKlRKOp9qhOQZbtmxJlS3fjpcpU8buuusu91++VaIYXFhzOLQIcOfOnXN0tz169HAXGM5RIZycLwJly5Z15zA1bNgw5vpa0ODRRx81zT8gIYAAAgggUBQFdi+KN8U9IYAAAggggAACCIQXUFCFt99+2/cEBeNQYJdMDgjWhRRURlF94wM9eJV444037LbbbvM2Qz+qXBICw4cPt1deecWNFI1G8RVQY3/QdwwrSBbf9wV3jgACCCCAAAIIIIAAAggggAACCCCAAAIFT2DdunWmVWZTpd122y0mS/ny5VMGltegYAWVCUorV650V530gsloAoT6xuhzChIrXPvPP/983wprtV9NHjjttNPSDiaj96Emd6SbrrjiCtOCCEpr1qyxli1b2m+//ZZuMeRHAAEEEEAAAQQQQAABBBBAAAEEEEAg1wW0QOyAAQN8r7P77ru7i8sefPDBvsdzslPzGjp06OBbxPLly619+/a2c+dO3+PJdtLem0yneBxbvHix9e3b11atWlU8bpi79BVo0qSJxQeT8TIqkHxhWODaqy+PCCCAAAIIpCNAQJl0tMiLAAIIIIAAAggUMYEpU6bYY489FnhXOlalSpXA4zk5ULFiRRsxYkRgEffee69Nnz498DgHEPAT0Cqi1113nd8h9iEQEVAUcRICCCCAAAIIIIAAAggggAACCCCAAAIIIIBAwRD48ssvs12RQYMGJT333//+d+Dxbdu22bnnnms///yzm0eBZ8aMGWO0IQeSFboDWrV44MCBCfVev3699erVy5YuXZpwLNkOrRQ8atQoa968ebJsgceGDRtm3kSbJUuWWLdu3SwrKyswPwcQQAABBBBAAAEEEEAAAQQQQAABBBDIawG1l3bv3t3++ecf30v37NnTmjVr5nssEzufe+45q1y5sm9Rs2bNsmRtvr4nsbPYC/zxxx/WqVMn2759e7G3ACC5gOY4kRBAAAEEECiKAgSUKYqvKveEAAIIIIAAAgiEEPjll1/cgZJBWRs3bhwY4TvonHT3K8Jv27ZtfU/T4MkePXrY5s2bfY+zE4F4ATX2du7c2fRIQiCZgFZHICGAAAIIIIAAAggggAACCCCAAAIIIIAAAggUDIFly5ZluyLXXHON3XLLLRbf7luyZEl7/PHHrVWrVoFlK6DIwoULI8f79etnjRo1imzzpGgIDB061F5++eVIIJfs3FXZsmXt8ssvNwWB0cSD7CaVo6AyXpowYYI9/PDD3iaPCCCAAAIIIIAAAggggAACCCCAAAII5LvAZZddZps2bfKtR7ly5eyee+7xPZapnfvss4/deeedgcU99NBD9vHHHwce5wAC8QIDBgywxYsXx+9mG4EEgfi+poQM7EAAAQQQQKCQCjCLrpC+cFQbAQQQQAABBBDIqYAac70VF/3K8lutzy9fTvcNGTLEdtttN99iVq9ebbfddpvvMXYiEC9w7bXXxgz8jj/ONgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCBQ8gZUrV+aoUnfffbd9/fXX9uCDD9rNN9/sBuz4/vvv7eqrrw4sV6vcvvbaa5Hj1atXt7vuuiuyzZOiJdClSxfTe2Ly5MkJwYf87lR9l0cccYS7OMfYsWNt/fr1Nnz4cKtRo4Zf9rT2NW/e3Nq0aRM5R+9ZJjNEOHiCAAIIIIAAAggggAACCCCAAAIIIJCPAu+8847bhhZUhUsvvdT233//oMMZ26+gNgcffLBvef/8849dccUV9vfff/seZycC0QLjxo2zJ554InoXzxFAAAEEEEAAgWInsGexu2NuGAEEEEAAAQQQQMCWL19uTz75ZKDEIYccYi1btgw8nskDGox51lln2cyZM32L1eDMvn372qGHHup7nJ0ISGDEiBH2zDPPgIEAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKFTEDBOnKa6tSpY/oXJq1atcquv/76mKwKJlO6dOmYfWwULYEff/zRevfubZpw4qWzzz7bWrVq5U6CKVOmjJUtW9YUXKhWrVpWqlQpL1vGHxUEadKkSZaVlWV//vmnaSLO3LlzQwW7yXhlKBABBBBAAAEEEEAAAQQQQAABBBBAAAFHQO1myRakVRDmPn365InVnnvuaVdeeaUNGDDA93oKMP7CCy+YAs+QEAgSWLp0qfXo0SPoMPsRQAABBBBAAIFiI7B7sblTbhQBBBBAAAEEEEAgIvDII4/Yzp07I9vxTzp16mRq9M2rdNVVVwVe6q+//rLBgwcHHucAAp988okbaR4JBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKHwCv/76a55W+uqrr7bffvstcs2aNWta165dI9s8KZoCmjiwZs2ayM3df//9NmPGDLvuuuvs4osvtnbt2pkCzBx++OG5GkxGFTjmmGPca3mVmTdvnj311FPeJo8IIIAAAggggAACCCCAAAIIIIAAAgjkucA777xjCxcuDLxugwYN7LDDDgs8nukDas8rWbJkYLH33HOP/f3334HHOVC8BX755Rdr27atbd26tXhDcPcIIIAAAggggIAjQEAZ3gYIIIAAAggggEAxE9i2bZuNGDEi6V23b98+6fFMH9TKf1r1LyiNGTPGNm7cGHSY/cVYQJHD1dir1RtJCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggUPgE1HeVV+m9996ziRMnxlxOq+pqxVtS9gQ++ugj2759e/ZOzqOz9Lq///77kas1bNjQ+vfvH9nOjyeXX355zGVvvfVW27x5c8w+NhBAAAEEEEAAAQQQQAABBBBAAAEEEMgrgSeeeCLppfJ6fsG+++5rZ511VmCdVq5caRMmTAg8zoHiK6D2as0v+Oabb4ovAneOAAIIIIAAAghECRBQJgqDpwgggAACCCCAQHEQGD9+fNJIywcccIDVq1cvTykUPbxp06aB1/zjjz9s5MiRgcczdUDX0SqYf/31V6aKDF1ObkZI37FjhynKdn4HXdH15btz587QLskyLlmyxO0oyOtgQ7n5WiW73/w8lpWVZRrQn1+fj/y8d66NAAIIIIAAAggggAACCCCAAAIIIIAAAgggkLsCedkvM2jQoJib2WOPPaxr164x+9gILzBp0iS3j+/cc88t0EFlxo0bF3NTnTp1itnOj43WrVtb2bJlI5dWH8yDDz4Y2eYJAggggAACCCCAAAIIIIAAAggggAACeSXw888/2zvvvJP0clpANq+T2h2TpaeffjrZ4Ywc27Vrlzt+WvMM8jrl5ph1jedXm+Tvv/+e17cVcz35KtB2puqhctq0aWOzZs2KuU5ub+Tma5Xbdc9J+fpcaJ5Kfnw+clJvzkUAAQQQQKC4CbC8TnF7xblfBBBAAAEEECj2AvGrLsaDnHrqqfG78mS7RYsWSaOEa6DnDTfckLQuCnqRTvrpp5/slVdesWnTptmCBQsigXZ22203q1Gjhp144oludOrzzz/fSpUqlU7RofIqEMqrr77qBsupU6eOW5dQJybJpEHXkydPdhv1P/30U/v2229NAWW8tM8++9hRRx1lTZo0sY4dO1rdunW9Qxl//Oqrr+yNN96wDz/80BYtWmTRgV9Kly5tumcZa8Bsy5Yt01r9c8qUKe4AbzVkB6UZM2YERhavXLmynXHGGUGnJuxXI6cGRY8aNcoWL15s3333XUKe6B261+iVLqOPpXp+zDHHuDbJ8q1Zs8Zmz56dLEvk2DnnnGN77713ZDvMk61bt5o+czL84osvbPny5TGBlhR4SvVUIKjOnTvbgQceGKZY8iCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUcwEFnx88eHCMgtqgw6SFCxfa7bff7pv1xhtvtDJlyvge83ZOnz7dPv/8c2/TfVS/WNWqVWP2hdlQnUeMGBEma8o8Z555pp1++umRfHfeeaf9888/ke1UT44//nhr165dqmwZP65+E/WhaeLBe++9Z5rcoX3qAypoSf1l0Ul9VPmd1PfYvHlztz/Gq8ujjz7q9oeqP6+gJfUXvfXWWzmqVvfu3a127dpuGep7GzJkSI7K08lapbpx48ZuOatWrbJnn302ZZkK5DNgwICU+ciAAAIIIIAAAggggAACCCCAAAIIFBcBteslG4dfoUIFO/roo/OcQ/MLkiWN1dZY8ooVKybLltYxLcCpMdRaxFftyRqz7aV9993XjjvuOFO9unXrZlWqVPEOZexR7a2aC6DFeOOvn5OLaF6B2vc++ugj06KuCgLiJbVV/utf/zK1l7dt29aaNWtmmk+RG2ndunX25ptvRsaoq03Paw/fa6+93DkcJ5xwgjvX4cILL7R02ko1vl/naF5IUFI/w9ixY4MO23nnnWcKRB8m6TOj8fyaX6AyP/74YzviiCOSnqp7z07wGb3XvHbQoAuoXJUfJsm4Vq1aYbJG8qh8zbuZOnWqffLJJ+48FX1evFS+fHn3/jVH44ILLrD69et7h3hEAAEEEEAAgfwWcH64kBBAAAEEEEAAAQSKiYATbCTLGSCnqCuB/x5++OF80XAGIQbWSfV1GiWz1q5dm7RuzmDQwDK+/vrryLlOw3HWFVdckbXnnnsG5o822n///bOeeuqpLKexMlJGdp84A6WznIa6LGdgb1aJEiUi13caL7NbpHve9u3bs+67774sJ1BKpMzoewh67gxUzXICpOTo2vEnO0FIspwG5bTqIeMHHnggSz7JkhOF3H3tgu4n7P7TTjst2WXcY7qW7sUZ3JrlNHBG7kd1TZX0fnY6DSLnhK2XXr8XX3wxVfFZzsDsUK+103ic5TS8pyzPy6C8ffv2zXIGe4euuz5HPXr0yHJWJ/CKKfCPTodB4P1NmDChwNefCiKAAAIIIIAAAggggAACCCCAAAIIIIAAApkS2LRpU5baw5Ole+65x7dN1RmAnuw032NbtmzxLStsO3pQvjBt1OoTiT9ffSvZSe+++25CWfFlh912AsjEVCFs/5VX/qWXXhpzfl5sOBNMspzB/QkGTnCcLGcF2LyoQlrXOOWUU2Lq6kwGCX2++gedFXKz1L/oBEEJfV6YjMOHD4+pl17T7L4nw1wvJ3mcQC0JdfXeg2Ef1b/lJWfCSo7L03X1/eQlfSeFqYv6z7KTnIk8vuWrH5mEAAIIIIAAAggggAACCCCAAAIIFGaBNm3a+LZ7eG0tzsKh+XZ7lSpVSlq3l156KWXdDj74YN8yHnnkkci5agd0Aj5npbqeZ6J5ABpzrbbDTCS1bV111VUx48+dYCo5LlrzFpxgQL73791L/KMTXCYrnTbUMJXUPI+uXbvGzJ+Iv278thPoJuuyyy5LOYdE19f7wFmANa37jL+etjUnI1XS3Iubbrop66CDDoq53n//+99Up2Y5wc5jzvGrQ/w+J5i+O1Y/VeFqv3YCervzbuLLiN5WeXpfhE1OkKOsxx9/PKtatWpp1V3zNZygSGEvUyDyBX1XXHnllQWiflQCAQQQQACB7Ars7vwYICGAAAIIIIAAAggUEwGn8cqiowD73faxxx7rtzvX9ylqecmSJQOv4/zgtVmzZgUe14EwkbAXLVpk9erVM2eApjkDpJOW5x1cv369OY1AbjRxRTFPN2m1TUVudwb02gEHHOBGrnaCVpgT4Cfdonzzz5071/S6DRw40DZs2ODmcRoo7ZxzzrEuXbq4K0IeeuihvucqSrQ8nIGOvsfT2ekE/HFXwGzatGnktWrQoIHdf//9bsRtrdb5008/mSKc33zzzaZo5l6Scf/+/e2oo44y3U9QmjJlivvaBR3PxP558+bZddddZ9WrVzfdi1YY3bp1a1pFy3Tjxo2myPdhPlN6nVauXGly0OqMqVKTJk3cvF9++aVbx/j8ihiuazsD6M0JgBN/2Hf7tddec6OCP/bYY+Y0hrt56tata+3bt7eLL77Yff/r/Ruf9Dl64YUXTHlnzpwZf5htBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgTwQ+PHHH+2OO+4wJ6CHHXjggW4bvNrhDz/8cOvcubM98cQT9tVXX8XURKtmOoPj3bbkmANFcEM+06dPT7gzrZSZnaR+rbvuussOO+yw7Jxu6re59dZb7eWXX7aLLroopgy9Lupb6dChQ9K+M73OzoQF69WrV8z5ub2hvhrVTSvlxicnYIjbL+X1M8Qfz6/t+NfJGdgfWBX1Sc6ZM8f+/e9/W6NGjaxcuXLuKrha5VifKa1CrP4jZwC9r0FgwT4H/N5/+qxmZ4Van+Izuuv00093V/cN0x8bf2H1IQ0ZMsTtS/KOOYuguP1L6hdzJnp4u0M91qhRw6655hobNmyYnX/++ZFz9DqrD1irQzuTIiL7o5+o384J4hS9i+cIIIAAAggggAACCCCAAAIIIIBAsRfQ2O5k6Zhjjkl2OFePaSx6svTRRx8lOxzq2G+//WatWrWya6+91h3/HeYkzQPQmOvjjjvO5s+fH+aUhDyfffaZDRo0yJwALnbSSSe57V1OIOaEfNnZ4Swy6s4lOO+880zzJ5QqVKjgtm1qXLj2y3aPPfZIKN4J/uKOH7/kkktsx44dCcfT2aF25Ntuu82OPPJItz1cbk5gEnfew9tvv23ffPONrV692q2j5jOo7c9LToAUcwJduz7PPPOMtzvhUfNjnMVRc1zXhIKjdmgOxH/+8x/39fbaO1esWBGVI9zTpUuXmnzj+wX8ztbnzgnIYnp/Pv/8835ZYvZpLs4PP/xga9assQEDBsQc04aOq/1U7zHNDwiT9N7R+0TtsXqdlNQ3oc+L5j84wajc+Qd+ZX388cfu+1ptwyQEEEAAAQQQyGeB7Eai4TwEEEAAAQQQQACBwifw9NNPp4wKHGYFx9y681TRr52AI0kv/d133wXe39dff52laND77bdfYB7np3nKY07Akywn6EfSeuigVl8cO3ZsltPYl+UMNE1Z7oUXXpiyTL8Mo0ePznIa9yLlOwNPs5yALH5Zs2SQLIL87bff7ntemJ1aUdAJXBKph9OYmzV16tSkpzqBWiL5o+11PxMnTvQ91xlAm6UVC71/TiOlbxkqz+kgiOTz8nuPTsNqTPmKeq4VQJ2BpoHleXXUfaaTdC2nQTdpuWHeU0HXdBrqY6Lha+XShQsXBmX33e8E+ImpnzPQNuv777/3zfvBBx9kOQFzYvJ7Noq2P27cON/zCtLOZN8VTrCnglRV6oIAAggggAACCCCAAAIIIIAAAggggAACCCQVUBu0Vuh0Bn3HtNs6gceznODkWSeeeGJMP4UTODzrlFNOyXIGb7v5tWKnVl5Nlu65556Ysr02Ya2Ymm5yBnZnnXDCCTH/tBqmV2ayRyeQR8x50eVs2rQpaVWc4C8J19DqplpZMyfJGfyepT6eZPWOP+YE5Ai9au2YMWMSynYCcWQ5gTRyXPfs3LcTTCamXyr+3rxtJ7CR21eWnWvkxjnvvvtujKP6C53B7zGXUr+IE0Qmq2bNmjF5vXvye8xu/553YX32tMpwfNnORAovS4F7dCZwJNQ3vv7R207wnSz17yVLzmIPWbvvvnuoctUP5izqkKw495gzESVLn5XoujgLSmQ5CyWkPDcow+TJk2PK88p2JroEncJ+BBBAAAEEEEAAAQQQQAABBBBAoMALOMG4fds8vLYPPY4cOTLf7uOGG25IWj/NP0iVDj74YN8yHnnkEbcd8+STT/Y9Hm2Q7LnmCmjceqqk9kDlcwLXZGmsfbIydUxth9lJTnD5LGdx2kj5ev7qq6/6timrbV3jyOP7GLy6nXXWWVlOAPHsVCNr1apVWQ0bNozUQ3MEnGDqvvXwLqB22tKlS0fO8eqhx+uvv97LlvC4ZcuWmHkDQXMCnMD8Mfm8uQXeY3TB2ucEsslygm1nqV0/ui5+z51A5tGnp3zuLHictEwnIH3KMpJlcIIGxZSv93s6Sf0B0W2s6m/SPAK/PiUnkE2Ws7hBzPWija6++up0Lp1veYO+K/RakRBAAAEEECjMArs7/zGTEEAAAQQQQAABBIqJwJIlS5LeqVbYq1KlStI8uXkwOqK033UWLFjgtzuyz/lhHnke/8QJ8OKulug0ekYO6V6bNWtmHTt2dCNwOw1AkWNBT7R6Z+vWrU0Rr5MlJ1iKGzl65syZphULZZvp9Nprr7mRnf/880+3aEVF18qPTmOd76WOOOIIc4JlWO/evX2PazW+F154wfdYsp1vvvmmtWjRwpygKG42ZwC36bVq2bJlstPc1QG1Mmp80v3oNYlfKVX5nMGkrqdM9U+R0oNS+fLlY/J65+hRqx56Sas8KrL74MGDbevWrVa5cmXvUEYedS1FR0+2YmNOVprUapjOBIBIXRVhXSujhk2KbK97V1KUeUUwdzp+LOjzoBUoFTH81FNPTbiEosZrpVtFzCchgAACCCCAAAIIIIAAAggggAACCCCAAAII5K7Ar7/+6q7uqDZor535kEMOsdmzZ9vy5cvdPgMnCL3bfv/cc8+5bebOwgL2ySefmBOE3q3coYcemrT9OtN34ASPcduQ1Y7s/dNqnmGS+h28c+IfnWAzSYt4/fXXE447iwiYEyg9YX86O5zgFqY+obD3oLLVn+UExAh1mUqVKsXkU9+W7t0ZvJzjuscUHGJDq8Vq1VqvXyrZKeqvOvfcc82ZaJAsW54dcwLcWM+ePSPXU3+hMxHC/SxMmzbNXU1Vn4V7773XVq5cGcmX6oneV+pbym5S35ETxD/hdPXTFNTUq1cv3z6ioPqqL1b9e8lS/fr13f7XZHm8Y+pzdQICeZuBj+r7a968eeS4Pu9aQdhvxeVIJp4ggAACCCCAAAIIIIAAAggggAACxVAg1fwCkaitJb+SEwA66aXV1q3xy9lNffv2tTlz5kROVxv2aaedZueff77bHnr88cenbFNyAr+bs+irpbJ0gm5Y48aN3XHaTlAOcxY5jVw3U0+cRVZN7aErVqxwi3SC5dj8+fPdsfl+7eFqW1e7qBNwxrcKTrBuu/TSS32PJdvpLP5pjRo1MvVRKOk6H374oTkLDCdt29b4dbVB+qWHHnrInODOfocsft5AUDugE2Q/cH6B5hhEpwsuuMBti3cWNbYDDzwwZTtn9Llhnj/wwANWrVq1wKxev09ghhQHateuHcmh9u8+ffpEtlM9Ubt5+/btzVkkwM3qLFbr9j1pHoHfnAhda9SoUXbTTTf5Fv3EE0+47bO+B9mJAAIIIIAAArkukLy3NtcvzwUQQAABBBBAAAEE8lIg1QDIqlWr5mV1Eq6VqsHXiYCecE7YHY8++qgtW7bMzd6uXTu3cVKDptXYpQbQSZMmmRPR2tTg16VLl6TFfv755+ZEuE6ap2vXrrZjxw7bsGGD2yCrgcHal6nkrNTnBmRRY7KSswKiOVGjfRvooq+pBjzlcyKNR++OPFdDYTrOU6dOda/trODplqGB6hpQHGYgp+oS1MCsgD033nhjpF65+UQNxs4qhe7rtXbtWndgvYLkBDUkZ6cuCvKj911Q+uKLL4IOpdzvrKQYafTXgNwBAwakPMfLoEHu9913n7fpBpZRQJpUyYn8bi+++KLttddeCVk1kLxTp06hBpQnnMwOBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgdAC6huIDs6uge4a3H3KKafElKHA5Aqm4ax86g7ajj6oQcRFPal/TP0/8UkBJjKRNAD9pZdeMgWXCZs0eDpMis6ntnn1a2kBgbxO6vvR4HG/YDIKzKL3XnwqaEFlNNFAEwC8tHTpUndCgxZN0P0lW7jCO8fv0RtQ73cszD6/9+HkyZN9rcOUlxd5nFV8Q19GfZphUth+VAXS+umnn8IUadELjaj/LJ3PaKgLkAkBBBBAAAEEEEAAAQQQQAABBBAoAgJe4JFkt5KfcwxSzS9Q0I2w7UXx96g5AVqIU0lBwxVAWu1PaksfO3asjRs3zg3GovHld911l287qFfm5s2b3fZHzR8ISroXBajWNVatWmXr1q2LXD/onHT2K7DNOeec45ar8xTkQ22NqQKyK68C6ATNodAiuGoDD5tWr15tZ5xxRmR8uwLZvPXWW9awYcNQRSQby66AJTltkw1VCSeTAvVrUWPNA9HrpTblVO/HsGUrn9r8b7311sBTcjK/QIUquJCXbrjhhtDto5pXofeDNz+kadOm7ryBMO2r+pwELYx7880328KFC70q8YgAAggggAACeShAQJk8xOZSCCCAAAIIIIBAfgukaizNz8Ze2VSuXDkpkeqfbDCnX7Rjr8DRo0dbyZIlbcyYMTZ+/HhTgA+/pEGbL7/8sptHA4CDkgadqrE4KGl1vOgo3gq8oUAufgE4gsoI2q+G5osuuijSSKfgLVp9NGzSfV177bW+2RXIZeDAgb7H4ncqirrqER39Wg2nYYLJeGW1bt3ae5rw+M477+RoVceEApPsUOT86PePBkUrgnYmU7IGXzW0Zze9//77pg4ApVatWlnYwf8KoBQdafykk05KK4iPrhM98Dq6/oqg/9hjj0Xv4jkCCCCAAAIIIIAAAggggAACCCCAAAIIIIBABgUmTpxoM2fOjClRwb6DAsorowbERwco0b78XFlW18+LNH36dN/LHHbYYb77s7NTq9SmE/Bd/Uxqp0+WFNRfr7OXXnjhBTv22GO9zTx7VH9NUDCZ8847z3RcAVkKelAZ9dtpQogWoYjuE8oJpFYq1sq0OUl+AYI0IUL9PwU1KYhQ/fr1Q1VP7w1v4kGyE9RnqAkUYVKyPlrvfE3gmT17trtZoUIFd4EM7xiPCCCAAAIIIIAAAggggAACCCCAAAL/E0g1v0A583OOQaVKlf5X2YBn6SymGl2E5hUo9e3b1xS8Q+Oio8f/e3k1x0HjwBcsWJC0TV0B4O+9917vtIRHla05BtFJwVPig8RHH0/nue5DwUC8pHH9YYLJePkHDRrkPU14VCCX7du3J+yP36Gg5G3atDEFlfFS//793eDe3naqR7WDV6tWzTfbli1bLKjN3/eEHOysVauWO//EK0L9KdFj7739OXnU4rzVq1f3LUIBjaLnafhmCtip10oLECipfbRbt24BOWN363qaH+IF7VGb7ciRI02L3oZJCjoT1FeislMt6hzmGuRBAAEEEEAAgfQFwv1Pnn65nIEAAggggAACCCBQAAUU0TpZKl++fLLDuX4sWQAXXVyDDb3GqXQr888//7iRwtXAFSa1a9fOjSqerPFLkZrTSWqQzcRA38GDB8cMMlY90n3ttMpf0L2p8XHNmjVJb00NepdcckkkkIkyq0xFE08nKYBPUCOoXrPoxuR0ys1E3hNOOCETxUTK0GDyZs2aRbajn8hcDdzZSSNGjIicdsUVV0Sep3py9dVXxzTsKyJ4uql79+6Bpzz++OOm15CEAAIIIIAAAggggAACCCCAAAIIIIAAAgggkHkBDeCNT8ccc0z8roRtBZ1Re7WXwgYp9/IXxsc5c+b4VjtZ8B3fE1LsvP32261OnTopcv3/wxpUf+WVVwbm1YqnvXv3jhy/8cYbrWPHjpHtvHoybdq0wGAy6kt79dVX3VVNFViloAeV2bVrlymQj/rZki1gEdZWq7KqfyenSZMS/JLsC3IKWrwivs7qn1bQoVRJExOC+tHiz33vvffidyVsa4GRv/76y92v771U/dAJBbADAQQQQAABBBBAAAEEEEAAAQQQKCYC3qKWQberxVS1qGt+pTBBiH/55ZdsVU/jnDWeWkGowywaq4AiCgSdLMj0Aw88kHIcfnxlzzzzzPhdaW9//PHHFj2mXGP60y1X4/qDxs9rbsEbb7yRsl533nmnzZ8/P5JP7fDJFmWNZIx70qJFi7g9/9tcuXLl/zby+FmDBg0yekW976677jrfMmWudvfspLFjx9rvv//unqo5H2E+R8qs8f8KnOQl9WOkG1BKgZmCrqcgNwq8REIAAQQQQACBvBXYPW8vx9UQQAABBBBAAAEE8lNgx44dSS9fqlSppMdz+2CYgXxhIlv71VOBZM4991y/Q4H7WrZsackGQ86bNy9h9c/Awv7vQI0aNVJlSXp8w4YN9tBDD0XyaAVFBXJJNylae1CDpoLFjBo1KmmRw4cPt88//zwmT79+/WK2w25cfPHFvlnVQBoUXdz3hAzvTNbYn91LBUXc1vv6lVdeSbtYra745ptvuufVrFnT9J4Nk9RpED14V/eq1STTTRogHtTgqxULPvjgg3SLJD8CCCCAAAIIIIAAAggggAACCCCAAAIIIIBACAG/9le174dJ0cHJNQC+qKe5c+f63mJQwHvfzCF2alLF888/b+q7CZNmzJjhBjiJz6tgJxrg7QXdV5ANBUHJ66RVXhU05o8//ki4dNu2be3111+PWa23IAeVWbhwodWrV8+uuuoqW7duXeR+1BdVpUqVyHaqJ/vss4917tzZFNBEPlqZOKcpKLDR7Nmzc1p0rp6vAEdhVodWJbyVnlNVSCsXh0lhViBWsCMv9ezZ03vKIwIIIIAAAggggAACCCCAAAIIIIBAnECqsflFeX6BAmXcf//9cSLJNzW2/YUXXgjMpGDiDz/8cOBxvwM5nV+gMm+++eaYort16xazHXajefPmgVlfeumlwGM6sHTp0gTPyy+/PFvBnoPmF+g6Bx98sB7yJeXG3IbLLrvMKlSo4Hs/zzzzjO/+VDtffPHFSJbo4PWRnT5PFIDm3nvvjTmSnfeRvjNOP/30mHKiN1K9j6Lz8hwBBBBAAAEEMiNAQJnMOFIKAggggAACCCBQKAR27tyZtJ5hImsnLSCHB/fYY4+UJfgNWk15kpNBgzuzk2677Tbbb7/9Ak/VwOB0UpigOcnKGzZsmEU33GsV0ewOeD711FMDL6VBzEFJDd1DhgyJOXzccce5A3FjdobcGDhwoPkNllWE8vLly4csJfPZcvpa+dXo7LPPtqAVYp999lm/U5Lu0wBc7zOhwbC77x7uT7z4zo/WrVuHPje6QhocHxSYSPmSvY+iy+E5AggggAACCCCAAAIIIIAAAggggAACCCCAQHgBtQv/+uuvCScoEH6YdM4550SyFfWAMrt27bJvvvkmcr/RTzIRDCS6PD1v1KiR9enTJ3534PYNN9yQsBqngsdMnTrVPUcD4xUYI0wfWuBFsnFA7fsKGuP1QUQXoQUctLppiRIlone7zwtiUBkt1KBVdRctWhSpb61ateyxxx6ztWvX2s8//2wrVqywyZMnuxM9brzxRuvVq5e7oIMeta1B+1988YVt3LjRXZShSZMmkbJy+iRodVetAuvnn9PrZep89Ssr8FGYNHHiREu10rXKCRuMadWqVQmfm+h66DXVSrdKxx57bNK+rOjzeI4AAggggAACCCCAAAIIIIAAAggUR4HiPL/gvPPOs+wEzFHQlWSLgCpghtqmw6acjln/9NNPbdasWTGXS3chXu/kZPMLtKBpsjbLu+++O+a+1a7dvXt3r+i0HtUGe/755yecowVUo/s4EjLk8o6cvlZ+1StXrpwpqIxfUl+BF3ze77jfvu+//94+/PBD95D6LI466ii/bAn7FIRGbeBe0mK3QfMevDxBj8neR8wvCFJjPwIIIIAAArknEG62Ye5dn5IRQAABBBBAAAEE8lAgVYNnqgbh3K6qApWkStkNeqOBodlJWmlQA0WDkgaXpuMWdiCk3/W0IuWIESNiDtWvXz9mO52NZA18algOWslUK07GN0xqdczspooVK9qcOXPcAdZqtFTj/JQpU2zQoEHZLTIj54UNzpLuxfr16+d7ypdffmlBq6T6nuDs9CLcq8E97OqKGkTrDUT3ys3J+0gDcYNSQV89M6je7EcAAQQQQAABBBBAAAEEEEAAAQQQQAABBAqywF9//eVbvbfeesu2bt3qeyx654EHHmh16tRxVwXV86KcfvjhBwvyyo2AMrJUUH6/QPp+zjt27LALLrjAtPKn0vTp002LHSiVKVPGJkyYYOpHycs0c+ZMa9Omje/EAA3Uf+ONN3yDyXh1LChBZdTPpdVvFbTH68tTX+mDDz5oy5Yts2uuucb23Xdft9oaGK/g++rDue+++0yLAGjShx61rcH89erVy5XAPuqL3HPPPT2+yKMmnKieBTkFTXKIr7Pe5wpClCqls9pufF9XdNkKwuT1c4btP4s+n+cIIIAAAggggAACCCCAAAIIIIBAcRJIFSDDa1vLL5OCOL9AFvGLs0b7rF+/PiHAS/TxTD/3xpN75aq9s1KlSt5mWo/JxoXrvRAU2F7Bu1977bWYa9WtW9eCAmrHZAzYGD16tA0dOtTOOOMMa9q0qT366KP29ttvZ2sR1YBLpL07J3NBkl1M7dV+geXVzpnuAsgKDOOl3r17e09TPkafp8w5mV+QbJ7KwoULbdu2bSnrQwYEEEAAAQQQyJxAYm905sqmJAQQQAABBBBAAIECJpCqwTdZxOi8uJUwDb6lS5fOVlX8BmKGLejiiy92g5sooEt8UmOWgoBocGxuJ61E+OOPP8ZcRgOLGzduHLMv7MbmzZsDs2qVwOXLl9uhhx6akOeVV15J2KeVHXOS1Fj8xBNP5KSIQnNup06drH///vbLL78k1FkDZRs2bJiw32/H4sWLI43yGuRcrVo1v2wJ+yZNmhQZROsdlP2oUaO8zbQetQpkUFKQHBICCCCAAAIIIIAAAggggAACCCCAAAIIIIBAZgW0WqUCYcS3M6tt/7HHHrNbbrnFX/jvAABAAElEQVQl5QU1mFdB/HNr8HPKCuRRhvh+lejLli9fPnozY8/Lli1rTz/9tLVo0SJUmUuWLHEDlmgCQufOne2ff/5xz9Pg7aOPPjpUGZnKlCyYjPoixo0b575vUl3PCyqjVXq9YDneOe+9955pdVz1V2S3388rK+hRwVi0eu3EiRMjWbT4hIIu5bVppAIBT/QZ1OdZk0zik94bySZwxOfP620FplI/5UcffZTy0iNHjrQePXoE5lO/lxagCJsUUGbAgAG+2RUMSKlkyZKmfl4SAggggAACCCCAAAIIIIAAAggggECwQEGfXxBmfkN22xlLlCgRDJPiiNrt1Na4aNEi35xqa1UglNxOmt8Q3Q6q62mOQHbnF6Sqr8aGn3rqqQnZxowZkzA+PafzC/T6DBw40P2XcMEitkNBgBToffz48Ql3poAy6vcJs1iu+hfUFqukYOYKaB8mrVy50ubPnx+T9bPPPsv2+yi+XyC6YAXJUXvwSSedFL2b5wgggAACCCCQiwIElMlFXIpGAAEEEEAAAQQKmoAahZIlrQ6Xn2n79u1JL69GMK3GmNdJgTo08DVoMOQXX3yRJwFl1LAcn1asWGH6lxtJg6zjA8qocU8DfeNTQRt8G1+/grSt1S+1GuL999+fUC1FZn/44YctzCD26Gjy6UQPnzFjRsJ11SibG0kr4f766695vnpqbtwLZSKAAAIIIIAAAggggAACCCCAAAIIIIAAAgVJQIPBJ0yYkFAlrdbZtWtXO+iggxKORe/o16+fbdq0KXpXkXy+bt063/vSSp9qr8+t1Lx5c7vkkksiA7dTXUeD7dX/4r0mgwYNCj3QO1XZYY+/++677oB1v/5CBYYJG0zGu15+BpW5/PLLYyZRVK9e3fWtXbu2V70C9aggUX4BZXKrDzCTN68+qqA+1OjrKM/3339vh/w/9u4DTpKqThz420zYhSVtIEnOIDmJoHBEAcmCKAJHRhAURcVDxQVRBBXBQ4IkSQoHSBAkiBIkLqBIFiSzhMWFBRZ22fn3r+7fczPdVT3dMz2hd7/v81mn69WrV6++XTPHvfB7Sy/dMbv981lnndX+OYLs5G300V6g9OHOO+9MEUQr7DqmGPOKTToi7bjjjlmwno7nfSZAgAABAgQIECBAgAABAgQIEOgs0NX6gg8//DALgl1PMIvONTfnKK+/srLmyj6iyvO9dfy5z32uMKBMrC/oixQBXir7FmP+9u23394rty8K4h4BxCuT9QWVIrWPDz300NyAMhHs5cYbb0zRT99Vis2Ky5vFxlhRVwGjyvXlrS946aWXUvzrjRTvkYAyvSGrTgIECBAgkC8goEy+i1wCBAgQIECAwCwpsNhii9Xc2e2NN97o1+d+/fXXa95/3LhxaejQ/vlP2Jj4WzQZ8vHHH6/Z7madvP/++6uqignj8b32RsqbSH333Xen2NWxMsXOiVL9AgcddFA6+eST23cZLV8ZAXsuuuiidPDBB5ezcn9Onz49/eY3v8nOxfdf7y6ncUHeAMUOO+zQa8GaIlDUfPPNl/scMgkQIECAAAECBAgQIECAAAECBAgQIECgewJ77LFHbkCZ6Gfee++9UwQHqTXBf8MNN+zejVvsqqKxp3onUffkcSOA/A033JCKgtpU1l0uF33+xx9/fOXpXj2OYDbbbbddylucEe2JXVFHjBjRcBv6I6hM2HUMyh+72P7ud79LAzWYTKCOHDky1/bVV1/NzR9ImTvvvHM67LDDsg0GumrXueeemyZMmFBVLHaZvvDCC7P8GPeK9+biiy+uKtcxI8bK4u9cjHF1TBdccEH7YWzwIBEgQIAAAQIECBAgQIAAAQIECNQWqGceeqwxGDNmTO2KeulsUR9vx9stvvjiHQ/77HP0nX7nO9/Jvd8TTzyRm9/szLx54bGh7HrrrdfsW2X15fWzRtChe+65p+p+1hdUkdTM2GyzzdLyyy+f8t6dM888s66AMtEHW077779/+WOXP/PWqcQ7VLk5cZcV1Vlg3nnnrbOkYgQIECBAgEAzBPpnNW4zWq4OAgQIECBAgACBhgW66ix9+eWXG66zmRd0NSmyq900m9mWyrpWW221yqz2496KvNx+g///IXbUq0z77bdfttNoZX5vHU+cODG3ap16uSyFmUsttVTWqXvddddVlYkdGLsKKBNR3MsDJDEZNnZSrSdFcJdnn322qmhMrl5ppZWq8mUQIECAAAECBAgQIECAAAECBAgQIECAwMAU2GmnnbKA8y+88EJVA2+77bb0ve99Lx133HFV52a3jOgXz0v19qvnXVtvXgRbP/3009Muu+xS7yUpxlsikEatYEB1V1ZnwT/96U+FwWRiw4XuBpMp374vg8rErrvHHnts+dbZzwh2MtB3Os3b5CEa/+abb3Z6loF4EG3fa6+90s9//vMum3f++ednf5cq3+8I+PPWW29l18cih9VXX73LgDJROHbl7RhQ5qOPPmrfkGGJJZZIsQBDIkCAAAECBAgQIECAAAECBAgQqC3Q1fqCuDrWGPRXQJlXXnml9gOUztbzDF1W0o0CK6+8cjaHO/qlKlN/ri+IQCDljUsr29Ubx7E5b16w8tGjR/fG7WbpOg899NB0+OGHVz3jtddem+J3Yfz48VXnyhmTJ09OV199dXa4wQYbpFVXXbV8qsuf//jHP6rK7L777umII46oypdBgAABAgQItJ7A4NZrshYTIECAAAECBAh0VyA6LWulCFARu7n1V+qqw3fFFVfsr6alWgFl3nnnnT5p14svvlh1n3//+99Veb2Z8c9//rOq+kGDBqXhw4dX5cuoLRAdvnnpwQcfTPfdd1/eqfa88s6aMeG9kd0V4x1qa2trr6f8oa/fo/J9/SRAgAABAgQIECBAgAABAgQIECBAgACB7gkMHTo0ffe73y28eMKECenyyy8vPD+7nPjggw9yHzX8+iLtvPPOKYL/1JumTJmSbr755nqL97hcBB/adtttU17gnc033zxdddVVqSjYSSM3LweVmXvuuasuu/XWW7OANnltqCpcIyN2wY2NGGbOnNleKsZRvvGNb7QfD9QPRe9j3iKMgfgM9e50G+NUN910U9UjxO66keL72nfffdMWW2yR5plnnqpylRl//OMfO2VF3eXx5qgnxjAlAgQIECBAgAABAgQIECBAgACB2gJdrS+Iq/tz09pyf0/RUyyyyCJp1KhRRad7NT/6Tpdddtnce0R/ZVH/dO4F3czMCzrf1/PC89YXxONYX9D4l/qlL30p5fWjz5gxI5XXDxTVetFFF7W/cwcccEBRsdz8vPeoHAQ89wKZBAgQIECAQEsJCCjTUl+XxhIgQIAAAQIEeiaw9tpr16wgAk08+eSTNcv05sknnniiZvXrrrtuzfO9eTIilw8bNiz3FtHh29tp2rRpKS9wzWuvvdbbt+5Uf1609Hhv+qLDu1NDZoGDrbbaKi299NK5T3LWWWfl5kdmDIzccMMN2fmtt946LbroooVlK09E0Ki81NfvUV4b5BEgQIAAAQIECBAgQIAAAQIECBAgQIBAYwL77LNPWmeddXIvir77vffeO/3tb3/LPT+7ZHYMLtJfz3z66aen+eabr+7bH3bYYSl2Eu2LFMFrigK5RFCPZgSTKT9H7Ia61FJLlQ87/XzooYfSc8891ymv0YNf/epXVeOcsRPv2LFjG62qz8sXBZTpizHIZjxsLDracMMN66rq3HPP7VTuscceS3fccUeWt/3226dYADRixIgsyFCngjkHzzzzTOq4WOW8887LSg0ePDj7+5dziSwCBAgQIECAAAECBAgQIECAAIEKgfHjx6eFF164IrfzYVdz/DuXbu5RV/fuz/UF8aRFc8HjXF/07+XNDe/reeF56wvi+WP9g9SYQATa/uIXv5h70dlnn90poHploXLAmXnnnTfttttuladrHg+E96hmA50kQIAAAQIEeiQgoEyP+FxMgAABAgQIEGgtgRVXXLHL3dwefvjhfnmo559/PjdgSsfGrL/++h0P+/xzUfTyOeecs9fbUrQDYVEHbG81aOrUqblV53Ui5haU2S4QuyIefPDB7ccdP1xyySWFvw8XXHBB+uijj7LijUYPL3qP+nPngI7P7TMBAgQIECBAgAABAgQIECBAgAABAgQI1C8QQRPOOeecLPhC3lXvvvtu2nHHHdPbb7+dd3q2yIvAFHkpdvPsqzRu3Lh0yimn1H27SZMmpSOOOKLu8j0pOGHChPS1r30tt4qvf/3r6bTTTss912hm7Ii7+eabp7///e9Vl84///zplltuSTGO2d0U4yYnnXRS1eUrrLBCVd5AzCgKfFS02cVAfIZ6x6yuuuqqTgGTOm6ycOCBB7Y/2i677NL+udaHCIoUacqUKenqq6/OPkcwpMUWWyz77H8IECBAgAABAgQIECBAgAABAgS6Fuhqjn4EhO6v9I9//KPmrbtqe82Lm3AyAoAUpf5aY9DX88KL1he88cYbRTTyawgccsghuWf/9a9/pZtuuin33IMPPpjKv6df+MIX0lxzzZVbrigzb41BX79HRW2TT4AAAQIECPRcQECZnhuqgQABAgQIECDQMgJDhgxJW265Zc32RmdSf6SudsiM3ehWW221/mha+z2LAsoU5bdf2IQP5QAilVV13HWv8lxvHBdFCo+AQFLjArF7bN5gQXSsR1CZvFTeuTF+J7bZZpu8IoV5A+U9KmygEwQIECBAgAABAgQIECBAgAABAgQIECDQkMCqq66afvjDHxZe88wzz6SiyceFF81CJ/L64OPx+jKgTNxv7733ThHkot504YUXphtuuKHe4j0q95Of/CR99atfza3jsMMOS7/85S9zz9WbWQ4mc//991ddUg4ms/rqq1edayQjJtG/8MILVZfMPffcVXkDMaPofSwKiDQQn2HXXXdNsfNtV+mDDz5IF198cVYsPsdGCpGWXHLJTr8jW221VRo5cmR2rtb/lAPKXHbZZe07Hv/nf/5nrUucI0CAAAECBAgQIECAAAECBAgQqBDoaj5yOVBFxWW9fhhBLp566qma9+mq7TUvbsLJonUEc8wxRxo6dGgT7lC7iry54REIJC9ASO2aun+26F7PPfdc9yudja+McZ+NN944V+DMM8/MzS+vL4iT9Qb/LlfU1taW8oKe9/U6lXJ7/CRAgAABAgSaLyCgTPNN1UiAAAECBAgQGNAC22+/fc32/elPf6p5vrdO/vnPf65Z9Wc/+9k0aNCgmmV6+2TRpMXFF1+8t2+dolM5Lz322GN52b2WN3z48Ny683aUzC0os5NATJTeY489OuWVD/I6fO+88870xBNPZEViMmwEiWokFb1Hjz76aCPVKEuAAAECBAgQIECAAAECBAgQIECAAAECA0jgyCOPTDvttFNhiy666KJ0zTXXFJ6flU8stNBCuY9XNME9t3CTMqPfv5EAJwceeGAq2tm1SU1qr+bkk09O8R7lpS9/+cvpjDPOyDvVZV5fBJOJRhS936+//nqXbRwIBaZPn57bjFq7G+de0I+ZseNt7HxbTyovbrjiiivSm2++mV0Sixw6jgXHmNa2227bZXUxth0LHs4777ys7IILLpi6Gg/vslIFCBAgQIAAAQIECBAgQIAAAQKzmUD0w3Tsm6l8/Jgn3h99bTFvOi9gSrl9Sy+9dFpllVXKh/3ysz/XF8QD580Nj/6yxx9/vM88rC9oPvWhhx6aW+nvf//79Oqrr3Y6F4G7Yxwo0nrrrdfwJs7xu58X3Pzpp59ORX3XnRrggAABAgQIEBjwAgLKDPivSAMJECBAgAABAs0V2GGHHVJRJOy408SJE9OkSZOae9M6arv11ltrlvriF79Y83xfnCyaXLzMMsv0+u1jgvGwYcOq7hOd87G7aF+lonfn9ttv76smzHL3KerwfeCBB7Lfx44P/Otf/zo7HDx4cOrO7oqjR4/uWF3753vvvTebbNue4QMBAgQIECBAgAABAgQIECBAgAABAgQItJRABGioNV5xxBFHpA8//LClnqkZjR07dmxuNbHb5nvvvZd7rrcy33jjjZqLHyrv+/zzz6dvfvObldm9dnzKKaekeE8qUyw+OOSQQ9KvfvWrylM1j/sqmEw04q677sptyz333JObP9Ay33333dwmFb2/uYUHQOb+++9fVytiPPrhhx9OZ511VlY+xkD32Wefqmt33XXXqrzKjAhIc/nll6e//vWv2am99torFS1gqbzWMQECBAgQIECAAAECBAgQIECAwP8KRD/UFltsUcgRfYQ33nhj4fneOnHLLbfUrHp2X18QOEVzw+++++6ads08WRQYOwICxbsjNS6w4447pvHjx1ddOGPGjFQO2F0+efXVV6fJkydnhxG4uztp3nnnrbosAtU8+OCDVfkyCBAgQIAAgdYTEFCm9b4zLSZAgAABAgQI9EggomDX6jyNTruiXfx6dOMaF7/00kvpoYceKiyx9tprp/XXX7/wfF+dmDJlSu6t1l133dz8ZmZG5OeiSaPXX399M29Vs66FF14493wMEkQHpdS4wJprrln4fsdupeUUk4l/+9vfZodbbbVVWnzxxcun6v6Z17EcF0cncl8OHNTdYAUJECBAgAABAgQIECBAgAABAgQIECBAoC6BmLB96aWX5ganjwoiOH05aHldFc4ihZZYYonCJ3n77bcLzzX7RAS92GWXXdK0adMaqvqXv/xluuOOOxq6pieFf/rTn6avfOUrVVXE+OHBBx/cHgCkqkBFRl8Gk4lbP/XUUxUt+N/DZ599NrXCpghTp07NbX/RuE5u4QGQ+fGPfzzVO2767W9/O912221Zq2NxRN446NZbb51i042uUsd3tjsbMnRVv/MECBAgQIAAAQIECBAgQIAAgdlBIIJK10oRtKKv07XXXlt4ywhSfOCBBxae76sT/bm+IJ6xqA9xIKwveO2111Jseio1LhDvd1FwmLPPPrtToJ7y2E+ME+2+++6N36x0RdEakb58j7rVcBcRIECAAAECdQkIKFMXk0IECBAgQIAAgVlLICbVDR06tPChyrvBFRZo8onf/OY3KXaCLEpHHXVU0ak+y4+JsnkdvqNGjUoREKQv0nLLLZd7m/POOy83v6eZhx12WDrttNM6VVPUhthVszcCEU2aNKnT/WfVg0MPPTT30S6++OJU3pXyd7/7XSpPKC7qIM6tpEPmmDFjUl4E8SjSG+9RRCbfaaed0h/+8IcOrfCRAAECBAgQIECAAAECBAgQIECAAAECBHpDYK211krHHHNMYdWnnnpq4blZ9cQyyyxTOCb2+uuv98ljxxhTbPbwr3/9K7tffC7aNbayQXHtfvvt13Agmsp6Gjn+2c9+lg4//PCqS6ItsUAjJqvXSn0dTObDDz9sH0vJa1eMi8Z4xUBOYZaXll122bzsAZ23//7719W+jgsRDjrooNxr5pxzzvSZz3wm91zHzFdeeSU7jA1KVlpppY6nfCZAgAABAgQIECBAgAABAgQIEKhTIPphVlhhhcLSEVAmAoT0VXrwwQfTI488Uni76GcdN25c4fm+OvHWW2/l3upTn/pUbn6zM4vm9sdmseV+s2be86KLLkr77LNPp41oi9oQ9z333HObefusrtllfUGsF8hb8xMbCNx8882ZxYsvvphuuumm7POee+6Z5pprrm55F32HF154Yc11Pt26Wemi73znO+n444/v7uWuI0CAAAECBBoUEFCmQTDFCRAgQIAAAQKzgkB0+NTanS0iQU+cOLFPHjUmn9bqKFxnnXXSbrvt1idtqXWTJ598slPHZ7lsdJ4PHz68fNirP2NXv7z0wAMPtHcK5p3vTl50/kUwmXfeeafT5bWC5/zoRz/qVLanBxHgJCbKFnW097T+gXT9rrvumhZaaKGqJoX/JZdckuWXo4dHBPBtt922qmy9GUXvUXznL7/8cr3V1FXu6KOPTldeeWUaMmRIXeUVIkCAAAECBAgQIECAAAECBAgQIECAAIGeCXzrW9/K+tbzannsscdqTsDPu6bV82IXz6KgHH0VUGbChAntgdc32GCDFP39J554Yt20TzzxRDruuOPqLt+Mgj//+c9TbDxQmWJcLyaxl8csKs/3dTCZuH+0qVaKhScxDlMO4F+rbH+ci2A3leNx5XYsv/zy5Y8t8zN2wI0NOepNMW5da3HNLrvsUm9VNce/665EQQIECBAgQIAAAQIECBAgQIDAbCoQc31POOGEwqefPn16OueccwrPN/tEUR9k3CcCEfd1n2nR8z366KNVpxZccMG00UYbVeX3RkbRvPD4vn7605829Zb//Oc/08EHH5yeffbZToFOVlxxxTTHHHPk3uuCCy5Ir776au657mQ+9dRTKQLJX3fddd25vKWuiTUDO+64Y26bzzzzzCz//PPPbw/40t0Na6Oi1VZbLfc+EbwmNsVtZorvLoLJzA5rRJrppi4CBAgQINATAQFleqLnWgIECBAgQIBACwt873vfS/PNN1/hExx77LGF55p54vLLL08xETYvDR48OMUuiIMGDco73ad5EbQlL0WE7b5Km2yySeGtjjrqqNyAN4UX1Djx97//PR1yyCFpxIgR2a6XHYtuuOGGWSd8x7zy53vuuSdF1PFmpHgnYqLy9ttvX/M9rXWvriYQ17q2r8/lWZfbEB2+0fl9++23Z1n77rtvjwK0FL1H77//foqFBs1K0XkcE85jIvDmm2/erGrVQ4AAAQIECBAgQIAAAQIECBAgQIAAgdlK4Otf/3qKnVbrTREEv9aujnfeeWe9Vc0y5dZff/3cZ3nhhRdy85uZGbuExphcpPHjx6cYF4sdRWNid4y51JtOOumkFIFR+jKdeuqp6ctf/nLVLWP8Zf/990+xMUDH1B/BZOL+McYS/2qla665Jq2++urpqquuqlWsX84V7aY7ZsyYtMgii/RLm3py05EjR6bPf/7zdVdx4IEH1hwLjs096tlVd+65506f+9zn6r6vggQIECBAgAABAgQIECBAgAABAtUCEbyiaJ5xlD755JPTlClTqi9sck4EIKkVvCY2vBwIfWdvvvlmeu6556qe/ktf+lKP5npXVVgjI/qZo885L0UfbwSBaUZ67733UgR/juDYlf3Gcf+i9ybmp3/jG99oRhPStGnT0h577JFGjx6dNttss27V2UrrC+IBDz300NznvPrqq1P0LZc3dl533XWzPvDcwnVk1gr6HesL4ntsRopgRPH7EeuDIjiRRIAAAQIECPSNgIAyfePsLgQIECBAgACBAScwbty49Itf/KKwXRH597bbbis834wTH330Ufr+979fWFUESWlkMm1hRU04cccdd1TVssoqq6QtttiiKr+3MjbddNPCYC4PP/xw+sEPftDjW8dunBHEZerUqdlkz4UWWqhTnRE9fKuttuqU1/EgOojzOsY7lunq8+TJk9N2222XBcipNek96qkVbCh2dGyldNBBB+UOHtx3333piCOOyB4lgiztt99+PXqsmHhblCIK/O9///ui03XnT5w4Me29995Z+cMPP7zm91R3pQoSIECAAAECBAgQIECAAAECBAgQIEBgNhR46KGH0p/+9KeGnnznnXdOSy+9dO41L774Ym7+rJxZ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03v173//u60U+KStFEilrTRpvq0UHKXwVuuuu25hvaUO68Lrenoi2ljasbHTvUsd623PPvtsT6uuur4ULKrTfWp9N/WeK3Uitz311FNV9xpIGaWASIXP/eUvf3kgNVVbCBAgQIAAAQIECBAgQIAAAQIECBCYjQUuuOCCTn2ZpV0b69YoTV7vdG308Z500kl1X9+xYNw3r4+4tJNnx2Ld+hxjDTE+kVd/ZV4pWHpbKSh4t+4TF5Um01fd5+STT+52fZUXljZVaFtwwQXb7xHtLQW+ryyWe1wK8t620EILtV9b+eyVx6UdSdtKgd9z6+rtzBjHKe14mtvWUjCZtlKwld5uQm79Mb6yyCKL5Lar0q+nx/FdlQK25LajkczS4oOq9sYYVr3f7S9+8Yuq6yufLcacuju+28iz5JWNvxGV7Ynj+J0vBcjKu6QwL373S4uHqurbaaedCq9p9olrr7226v7xPP/93//d7FupjwABAgQIECBAgAABAgQIECAwoAR++tOftpUC++b2jZQ2N23rSV/xX//617bll18+t+6Pfexjbffcc0+PLUrBSXLrj7UB9fbFlRtxyy23dJrnXQq803b66aeXT3frZ+VYQLlPrRR4o6H6oh8w5nGXr2/Wz5h730g68cQTm9qGZZZZpq20eW4jTWgrBccpbEMp+H7bfffd117fjBkz2i655JKsjz76U5944on2c5UfSsHhC+stBZivLN7U41gXUfmdnnnmmU29R1QWv8+lzYmr7lV570aO4+9HKZhM09vazAqLxsLiOePvnESAAAECBFpZoHa4v9L/tZMIECBAgAABAgRmH4FSB1i2S11EkM6L/HvNNdekUmdItkviSy+9VBdMRKz+7W9/m9ZZZ5203377VUXtLXU2ZRGIb7jhhlSafFlXnUWFxo4dW7gDXUTULgWVqWunydjxM6Kcf/e7322/1WqrrZZKHdbZc7Rn9uOHLbbYIkV07lJQmaa0IiKCl4LUpFKAmLrri3tfeeWVXUYzr7fCUjCZVOoQTxHRvpEUuxsWOUSk+kUXXTT73koBYlLsvnrcccelcePGpauvvjqVJnPn3ip2JC11FOeei8wLL7yw8FxPT8R3seeee3aqJr6XiOTd7PT1r389xe6ozUrLLrtsip1RSx33zaqy6fXEDgC33357Yb0RcT8ijEsECBAgQIAAAQIECBAg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BHoiMGPGjNzLV1hhhdx8mQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAs0X+MxnPpOOOeaY9oqffvrp9M1vfrP92AcCeQKLLLJIe/YSSyzR/rmeD/fcc086/vjj24secMAB6fDDD28/btaHu+++O11wwQVZdT/+8Y+bVa16CBAgQIAAAQIECBAgQIAAAQIECBDoBYGPPvoot1brC3JZZBIgQIAAAQIECDQoIKBMg2CKEyBAUltcUgAAQABJREFUgAABAgQIEOiJQN7OlkOHDk1rrLFGT6p1LQECBAgQIECAAAECBAgQIECAAAECBAgQIECgTwXa2tr69H69cbPjjjsubbvttu1Vn3rqqem6665rP/aBQKXAyy+/3J615pprtn/u6sObb76ZdttttzR9+vSs6DbbbJNOP/30ri5r+Pwrr7yS3ScWoey3337pk5/8ZMN1uIAAAQIECBAgQIAAAQIECBAgQIAAgb4TyFtfEHdfZ511+q4R7kSAAAECBAgQIDDLCggoM8t+tR6MAAECBAgQIEBgIApMnTq1qllrr712mnvuuavyZRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEBqrAO++8M1CbVne7Bg8enC677LL0iU98IrsmguTsueee6Yknnqi7DgVbTyDe3QkTJmTf+8orr5w+/elPp+985zvpmWee6fJhrrzyyqzMoEGDsuu6vKBU4IMPPkg77rhjev7557Pim266abriiitSbDrRzBTBbjbbbLP0wgsvZItNfvGLXzSz+tm+rqKdomd7GAAECBAgQIAAAQIECBAgQIAAAQI9Esjrax8yZEjaaKONelSviwkQIECAAAECBAiEgIAy3gMCBAgQIECAAAECfSgwefLkqrvtsMMOVXkyCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIDWeCBBx4YyM2ru21zzTVXuvbaa9t3e50yZUrabrvt0uuvv153HQq2jsCkSZPSuuuum/7rv/4r3XXXXenRRx9Nt912Wzr++OPTcsstl4455pg0ffr03Ae67rrr0q233pqd++QnP5kWXXTR3HIdM2fOnJn22muvdPvtt2fZW2+9dbrmmmvSHHPM0bFYjz8//PDDacMNN0yPPfZY+vjHP56irc2+R48b2eIVRKAeiQABAgQIECBAgAABAgQIECBAgECzBd56662qKjfeeOM0//zzV+XLIECAAAECBAgQINCogIAyjYopT4AAAQIECBAgQKCDwLvvvptNyLz++uvTe++91+FM/sdXXnml04lhw4Zlk0g7ZTogQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAxwgdNOOy298cYbA7yV9TVv9OjR6ZZbbkmbbLJJdsFTTz2Vttpqq/T222/XV4FSLSOw3377pccffzy3vR999FE64YQTUgSLefLJJzuViYAwX/jCF9rzvva1r7V/LvrQ1taW4n6//e1vsyJ77rlnuvrqq1MEMWpm+vWvf50Fk3nuuefSlltumQXIWWihhZp5i9m+rggMdP7558/2DgAIECBAgAABAgQIECBAgAABAgS6Foh+wehP/J//+Z/08ssvd3lB5fqCuCD6FSUCBAgQIECAAAECzRAY2oxK1EGAAAECBAgQIEBgdhT4xz/+kTbffPNU7sQdO3ZsNiE0IoLnpalTp6bY9bBjih0Jx48f3zHLZwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAgBd46aWX0tprr52++tWvZj9HjhyZ2+aPfexjad555809N5AyR40alW688cZ0wAEHpAsuuCBNnDgxbbHFFumGG25IEXBGan2B1157LV177bVdPsg999yTVl111fT5z38+rbbaaunee+/NxgAjqEikzTbbLG2//fY164ngNPvuu2/2Lg0ePDj94Ac/SN/+9rdrXtOdk//+97/TV77ylTRkyJB0yimnpCOOOCINGjSoO1XNtte8+eabKf6e5aXp06enZ555Jp1xxhnpvvvuyysijwABAgQIECBAgAABAgQIECBAgEC7QGxQu/XWW6e//OUvWd7w4cOzINa1AlQ//fTT7dfHh6WWWirtuuuunfIcECBAgAABAgQIEOiugIAy3ZVzHQECBAgQIECAwGwvcNBBB7UHkwmMCBYTu1VGRPG11lqryuf+++/vlBeTj4877rhOeQ4IECBAgAABAgQIECBAgAABAgQIECBAgAABAq0i8Nxzz2XBLGq199JLL02f+9znahUZMOdGjBiRzj///LTGGmuko48+OkVgkQge8oc//CGNGTNmwLRTQ7onEIFB6k0ffvhhOu+886qKx3sQ70itNG3atLTnnntmOxAvuOCCWfltttmm1iXdPhfjjZdddlkW1Mk72j3G+Bv15S9/uXsXu4oAAQIECBAgQIAAAQIECBAgQIBAB4EI+lwOJhPZ0c941FFHZQGhIxh0ZYqAxg8//HCn7JNPPjkNGzasU54DAgQIECBAgAABAt0VEFCmu3KuI0CAAAECBAgQmO0FHnjggSqD999/P2233XbpjjvuyKKDdyxw+eWXtx/GzoBnnnlmWnjhhdvzfCBAgAABAgQIECBAgAABAgQIECBAgAABAgQIDDSB5ZZbLm2//fbdbtYiiyzS7Wv768KY2L/JJptkQUEmTpyY1l9//SyozPLLL99fTXLfJghEcJeepPnmmy/dcMMNqdY7/eabb2a/L3fddVcWjOiCCy7o9fHA3gpW0xOrVrp2ySWX7NHfuCWWWKKVHldbCRAgQIAAAQIECBAgQIAAAQIEelEgb31B3C6Cyiy++OJpp5126nT36G9899132/P222+/tMMOO7Qf+0CAAAECBAgQIECgpwKD2kqpp5W4ngABAgQIECBAgMDsKLD22munok7fcePGZbsNbrHFFmnGjBnp7LPPToceemiaOXNmimAyETn8yCOPnB3ZPDMBAgQIECBAgAABAgQIECBAgAABAgQIECBAoCUEPvjgg3TiiSdm/84///y02267tUS7NbJYIAIFddwhuLhk5zMrrbRSuvLKK1MEWKqVbrvttvTZz342/ehHP0oHHnhgNi5Yq7xzBAgQIECAAAECBAgQIECAAAECBAjMOgLHHHNMOuGEE3IfaOjQoVlf8+GHH56GDRuWbWAbfc6vvPJKVj4Cu8cGtnFOIkCAAAECBAgQINAsAQFlmiWpHgIECBAgQIAAgdlOICabRsCYmExclBZaaKE0bdq09M4772RFYufCX/3qV2nXXXctukQ+AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAABJ4/fXXU4z5SK0v8MILL2Tje48//nhdDzPHHHNkm0Qce+yxKT7Xk9588820wAIL1FNUGQIECBAgQIAAAQIECBAgQIAAAQIEZiGBN954I22wwQbp6aefLnyqkSNHprnmmiu99tprWZnBgweno446KgtEM2TIkMLrnCBAgAABAgQIECDQHQEBZbqj5hoCBAgQIECAAAEC/1/goYceyiaRxm6DtVIEktlnn33S0UcfncaMGVOrqHMECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECPSSwNSpU7OdgM8+++w0adKk3Lusssoq2QYRBxxwQBo3blxuGZkECBAgQIAAAQIECBAgQIAAAQIECBCoFJg8eXK2ZuD8889P06dPrzzdfjxs2LC03Xbbpf/6r/9Kq6++enu+DwQIECBAgAABAgSaKSCgTDM11UWAAAECBAgQIDDbCjz11FPpxhtvTI888kiKTuBIo0ePTksttVRab7310oYbbphGjBgx2/p4cAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIDSWDmzJnpiSeeyHYLfuedd9Lw4cPT2LFj00orrZQWWGCBgdRUbSFAgAABAgQIECBAgAABAgQIECBAoMUEYk3BH/7wh/TAAw+kV199NX3wwQdp1KhRafHFF88CyHzqU59K888/f4s9leYSIECAAAECBAi0moCAMq32jWkvAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECgQGF+TLJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEWExBQpsW+MM0lQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAkYCAMkUy8gkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBiAgLKtNgXprkECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoEhBQpkhGPgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFpMQECZFvvCNJcAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJFAgLKFMnIJ0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQIsJCCjTYl+Y5hIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBIQECZIhn5BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaDEBAWVa7AvTXAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBQJCChTJCOfAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLSYgoEyLfWGaS4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIBAWWKZOQTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgxQQElGmxL0xzCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCQgoEyRjHwCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0mICAMi32hWkuAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEigQElCmSkU+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEWExBQpsW+MM0lQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAkYCAMkUy8gkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBiAgLKtNgXprkECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoEhBQpkhGPgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFpMQECZFvvCNJcAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJFAgLKFMnIJ0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQIsJCCjTYl+Y5hIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBIQECZIhn5BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaDEBAWVa7AvTXAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBQJCChTJCOfAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLSYgoEyLfWGaS4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIBAWWKZOQTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgxQQElGmxL0xzCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCQgoEyRjHwCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0mICAMi32hWkuAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEigQElCmSkU+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEWExBQpsW+MM0lQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAkYCAMkUy8gkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBiAgLKtNgXprkECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoEhBQpkhGPgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFpMQECZFvvCNJcAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJFAgLKFMnIJ0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQIsJCCjTYl+Y5hIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBIQECZIhn5BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaDEBAWVa7AvTXAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBQJCChTJCOfAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLSYgoEyLfWGaS4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIBAWWKZOQTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgxQQElGmxL0xzCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCQgoEyRjHwCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0mICAMi32hWkuAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEigQElCmSkU+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEWExBQpsW+MM0lQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAkYCAMkUy8gkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBiAgLKtNgXprkECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoEhBQpkhGPgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFpMQECZFvvCNJcAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJFAkOLTrRC/vTp09MHH3yQ4mf8mzlzZtbstra2Vmi+NhIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoKTBo0KA0ePDg7N/QoUPT8OHD0xxzzJEd17zQydlWoOUCyrz33ntpypQpaerUqVkQmdn2m/PgBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECs63AnHPOmUaNGpXmnXfeNGzYsNnWwYNXCwxqK6Xq7IGX8+6776ZJkyaladOmDbzGaREBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBfhAYNGhQmmeeedKYMWMElukH/4F4ywEfUGbmzJnplVdeSVOmTBmIftpEgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoN8FBg8enMaOHZvmm2++fm+LBvSvwIAOKDNjxoz0/PPPp2nTphUqDRkyJI0YMSINHTo0xYsdUZMkAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABArOKQFtbW4o4HNOnT08ffvhhiuOiNP/886dx48YVnZY/GwgMHajPOHPmzMJgMsOHD8+iIY0cOTILJjNQn0G7CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECDRTIILJvP/+++ntt99OU6ZMSR999FGn6idPnpwGDRqUxo4d2ynfwewjMKj0khSHHOpHh5deeil7aTs2YciQIdnLOnr06I7ZPhMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCY7QQimMzrr7+eIohMZVpsscXSqFGjKrMdzwYCgwfiM7777rtVwWSGDx+ellxyySSYzED8xrSJAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgrwWGDBmSxo0blxZddNGqW7/yyitp5syZVfkyZn2BARlQZtKkSZ3k4+VdfPHFUwSVkQgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ+D+BeeaZJ40fP/7/MkqfZsyYkSZPntwpz8HsITDgAsq89957adq0aZ30x44dK5hMJxEHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBP5PYL755ksjR478v4zSp7feeiu1tbV1ynMw6wsMuIAyU6ZM6aQ+fPjwNHr06E55DggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6CwwZsyYThnTp09P7733Xqc8B7O+wIALKDN16tRO6hH9SCJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoLbAHHPMkeacc85OhSpjeXQ66WCWFBhQAWUiqlH865hGjhzZ8dBnAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQKBCpjdbz//vsFJWXPqgIDKqDMBx980Ml5yJAhacSIEZ3yHBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkC8w11xzdToxbdq0TscOZn2BARVQZvr06Z3EBZPpxOGAAAEC/4+98wCXpKjacCkLCAZEEBABSSogSk6SJeec85IFBMlZMksQJIhLzhlckkiWnCQsICA5KEgWUBYFdP56i7/Gmp7O0zPTc+93nufe6emurq7+uqf71AnfEQJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIARSEYjydfz3v/81//nPf1L30cahhUCtCWVGjBgxtNDW2QgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICIEuIjDeeOO19f7pp5+2rdOKoYtArQhlYDQKJe4GDbdrWQgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAiB/yHwhS98wXzxi62UIlFOj/+11tJQRKD16g/FM9Q5CQEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIgWGEAKQykuGLQK0IZRqNxvC9EjpzISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEh0AUExOnRBVBr3GWtCGVqjJOGJgSEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEKg9AiKUqf0l0gCFgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBAC+RAQoUw+nNRKCAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhUHsERChT+0ukAQoBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIATyISBCmXw4qZUQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkKg9giIUKb2l0gDFAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACORDQIQy+XBSKyEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhEDtERChTO0vkQYoBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQyIeACGXy4aRWQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIgdojIEKZ2l8iDVAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASGQDwERyuTDSa2EgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACtUdAhDK1v0QaoBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQiAfAiKUyYeTWgkBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIASEgBAQAkJACAgBISAEhIAQEAJCQAgIASEgBISAEBACQkAICAEhIARqj8CI2o9QAxQCA4xAo9Ewzz33nHn++efNm2++acaNG2dGjBhhvv71r5upp57azDnnnOarX/3qAJ+hht4vBJ555hlzzz33mJEjR/ZrCDquEBACQkAICAEhIASEQI0R+O9//2vQGV988cXmXGSCCSZwc5FpppnGzDHHHObLX/5yjc9AQ6srAk8++aR56KGHzGabbVbXIWpcQkAICAEhIAQGCgFsyM8++6x54YUXzBtvvGE+/vhjM/7445tJJpnEfPvb33Y25K985SsDdU4abD0Q+POf/2zuvfde2ZDrcTk0CiEgBIYwAu//669m3KfvDuEz7N6pTTz+ZObrX5qmewdQz0JACAgBISAESiDw6aefGuzg999/v1l88cXNrLPOmtnLAw88YO644w43h1922WUz26uBEBACQkAICAEhIASEgBAQAkJACAiBQUTgn//8p3n00UfNww8/bLbccsvMXCh84VdddZXLqVpxxRXN7LPPPoinrTELASEgBISAEBjyCLz00kvuff3666+7/OcvfOELLudgyimndHbvb3zjG0MeA51g9Qi88sor5pZbbjEbbrihmWiiiao/gHoUAkKgEAIilCkElxoLgWwEMHrccMMN5pxzzjE33nij+eCDDxJ3Qrmaa665zEYbbWQ233xz0w3lCkUO8hrJ4CNAYgn31UUXXWRQqJAZZpjBLLnkkpWc3L/+9S+DkW/yySevpD91IgSEgBAQAkJACAiBbiMgXbcVYUhkrr32WnPeeeeZm2++2fzjH/9obRB8++IXv2jmm28+NxeBGORrX/tasLWaRV2fanCsQy8EzzMXufjii81rr73mhoSDf5555qlkeMxD/vOf/7jE+Uo6VCdDAgHuNewZ2E4kQkAICIGhhgA25Ouvv96ce+65zob84YcfJp4ietvcc8/dtCFDVl61SG+rGtH+9QeJDPcVNuRXX33VDeS73/2uWXTRRSsZFIRH/HXDl1HJANWJEBACQqAvCDTMfxqf9eXIg3/QxuCfgs6g5wjgMyfwkKSVl19+2bzzzjtOPyEIEV15+umndzarZZZZJhcJRM9PQAesJQLMya6++mpnV6dYFLE+kuGDAMWceKY88cQTZuzYse7z3//+twNg2223NaNHj04F4/bbbzc/+clPDHN9ZPfddzfHHHNM6j7aKASEwNBBAP/W3Xff7f54jvzlL39x8aKQU1FocNJJJzWzzTabmXfeec1KK60km8rQufRdPxOKWJJ8je67xBJLmL333rvrx9QBhIAQEAJCQAgIASEQIoBOi40EPZc/iGSwzRGjiJBgvsEG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OeXcpP4IrL322mXXXXfturNPfOIT45bdcsstdVlGgtg17a7GrfD/JxLEuUnXXnttSUffxz3ucaU9yEyz3CsBAgQIECAwOgIpq04dYhMAPWVbqRdpgluPzpW6kn4KNG3Xss9NN920513/4Q9/KHvuuWd54IEH6m0SwPi0007reXsrEpgLAgLKzIW77BpHUiCNDtorb5LBao+gNpIX7aLqhitf/vKXy1vf+tbyT//0T3W0xt///vdTyiSYzOWXX16v94QnPKHnkYhvvfXW8sIXvrAebSkbp4P3v/zLv0x5PCsQIECAAAECBBZH4O1vf3vJyFlNynRGdxz21IxSOuzXkYAeGXm4SWeccUbprDBulnkdHYF8ftOhIc+h+ZvMM2hGkZkqZZ1f/vKX9WpPfvKT6wAgU22T5XnOefGLX9wavWa//fablWfe//mf/yk777xzueuuu+oRp3McqX8Czci4/dpjRjxvGsI1Ixj8/d//fb92bz8ECBBoCUw06lpr4RC9SYPh9sZ7n/nMZ1SUDtH9m+mp3nvvveVLX/rSuDLkVJpPlf785z+Xn/zkJ/VqW221VVlllVWm2qRe/qc//am86EUvKk1A85e85CXl9NNP72nb6ax09dVXl5122qksXLiw7L777uOCoE9nP9adWKDf+baJj2IuAQIECIyqQIKwJIhBk3/I7/Vee+014WjGg2yw4YYb1uVfK6ywQn2a6RSThq2dwSUG+Rqc25IVSB46+eGkDAw1nXTooYeWBExMWn/99es6kHTAkmZfIPVPzfdV59FuuummcfVRCXp+6aWXli233LLknj384Q/v3KSe/o//+I/WM9GFF15YfvOb39Tlzg95yEMmXL8fMz/72c+WU089td5VykoTxCbBQSQC0xVIsIgMLtakBFbK52mYUrdn2m7zh+naXvrSl5Zjjz22dcr//u//Xg+O0JrhDYEOgfwGNWk6+ZPU6e6xxx6ladOQEcPbA5Y3+1zc19/97nd1R/XksfPdk849EgECiyeQZ9f8/TZ1W8973vP6Xkb/xz/+saTsP/UPSS94wQvKBz/4wcU7cVsPlcBV3722jD0wtsg5b3vAluWNlx9aNthi/UWW9WtG9p1j5FidKeeUc5P6J5CAct3SV7/61dJe55jnhrvvvrsOprrPPvt026wkD9ukPG8kn37ggQc2s7wSIECAAAECEwg0z+cTLBqqWc985jNL+hkss8wy9XlfdNFFJYHSpdEWSH7vW9/6Vt3fIP0O0s9kwYKJA1S2S2S7H/zgB/WsBDvsdbDR/L2kLVkCpic99alPrdvOZdDS2UyjUP48mz72PXgCAsoM3j1xRgR6Fjj++ONLRsVqUqKJt49608z3OhoCaTC/xRZb1I3zk5l6y1veUjdgS8O2RHpPpMZu6eSTT647SGb5ZJGj27dP5ULWTQfLZruzzz7biFjtSN4TIECAAAECsyKQQsNPfvKTJY2UkhIpOB0Q/vu//3tWjrekdnrNNdeMzIhxGa0wI+A1KfnRjB4ijaZAAsJkZJg0QkpwywT0SBTvjDx91FFHlfZo4J0C73nPe1qdbnp9Fkln5DRAuu666+rdpdHBxz72sVaFQucxZjp9ySWXlKc//enlxhtvLKm0aEaanen+bLeoQNOhZtEl05+TUdXyucvoBCnkP/PMM+sKgOnvyRYECBCYWiCN7kel02iCUh922GGti04noVTYSqMpcNVVV5UEFE9gvvYy5OTbXv3qV9fBWLpd+fvf//5Wg/Ne821pJJpjXXnllfVuk19M4KJ0ZuxnSj4g+bbrr7++rvTP82LT2KSfx5nL++pnvm0uO7p2AgQIzGWBrbfeupx77rll/vz5NUMGO5msA8qgWj3taU8rCdLQ5GfSaLG9TcagnrfzmrlAGnumMfOTnvSk8qAHPagO7pKOzR/+8IdbbSy67T0BuJuRNKcT9Phd73pXSQeqpAc/+MHl29/+dnnkIx/Z7TDm91lgtdVWq8sZu+22PYB+PhtJBx98cP2aBskTpTRW/t73vlcv+tSnPlW/zmbnuIsvvrhVR5NnowwMlQ6+EoGZCuSz3tTLZh8HHXRQueyyy2a6u4HZ7rbbbhuYc1mcE3nnO99ZEji6Sccdd1y54IILmkmvIyiQ4BBpC/y3f/u3ZY011iiPeMQj6gF5vvnNb055tRkUpEl/93d/17yd9DXtRFPG19TNJi+UfE4zgvikG09j4W9/+9vyjGc8oy7jy/ND8ltS/wSaQB/926M9DYNA8qEpz097pKT8jeXZvJ+d5vIdkaDyTcCqPPt87nOf6/t3xDB4z+VzXP0hq467/BXXWKHs/9k9yss+9uKywir/VxY0boU+T+QYOVaOmWO3p9XXH39u7cu8n77AjjvuWB71qEdNuGF+a1IP2KSmnVWemTfaaKOSgcYmSj/60Y9K8uYpg0l5SMoPJwtAM9E+zCNAgAABAnNNIHn9tJEZhZSAsu973/tal5I+rmkTLo2mQAIQ7rDDDiXlUulvkH4HKdtMwP4Ez06b/W7prLPOaj175jl03rypw18kj5n9f//73693m2AyGci628AC3Y49k/kpw5MIDJPAMtUfzKKhYpfSFdxwww2l/Y8oI69kFBaJAIHJBY455pjy7ne/u14pP3ZpbNKtQGbyPVk6qAJ33XVXefzjH19+/etfdz3FZKwS8KXz3n/nO98piTifh4mMppaKv6lGQUqHmd1226185StfqY+Xhi4f+chHWg3nup6EBQTGY9cJAAAz5UlEQVQIECBAgACBPgok/5IRYNNYKelv/uZvSkZ5fNjDHtbHo8zOrtZaa61xz7fNUdJwI42xRiGlOOHwww9vjUyWxvZpMJ18qzQ6AqnQ32yzzUoa+HVLj3nMY+qONum83J4yMk0+7+lUseqqq9aVG/mcTJbSKTmNlvNcm5QOOyeddFLfOwyngX0+v2n89MIXvrA+/xVXXHGyU7NsmgIZYTDf150BGVZeeeXWaL297jJBjVLBkJHPsn1+F/KcKxEgQKAfAgm00YxO0b6/BK5KXnRUUkZ3aSrn03kuZYZptC+NjkCC/CXfdu213UdiTCfVdJDuvPfpjLLzzjvXo4smT5RRYdKxdbKURqPJ633961+vV0tj0Yxa3Etl/mT77VyW0Q0TwDLX99znPrd88YtfrPMDneuZnrlA8lgJXJ+8eGcaoGrkzlMzTYAAAQIDKnDeeefVgRpStpuURoIZKGXYUjrHpIFrU66R4HsJrCyNnkDucxMApPPq0nk7z4bbbbdd56KSgHwpD025afLAacuRNhtTpRwrwdqTz0oeLKN0p3xVWrICCTa+7bbbTnjQdL7NfU2Z9kMf+tC6fDtlnXmWvvTSS8s222wz4Xb5jkhQ17THybpp7N8Ep5pwgxnOzKBQCbiZRtk51wTAGaXyixmy2KwPAmmbluf8ZjC7tNtNvWwv3219OPxi7SJ1PhMFsktgnLR3G4WUurb99tuv1YE3QUZSL9tZNzcK1zrXr+Gmm24q22+/fdd2ohmUI52u1llnnUWo8veb8rOkLbfcsiRI81Qpn60M6NC0yXj+859fB4rod71pglSlI1DK4rfaaqv6u2aia5jqfC3vLpBOVxPlCRJ4PIOX9jul/jbPScOQMmDSVO0UhuE6Os8xg3PlnqfMP2mnnXaq/5ZXWmmlzlVnPJ3viAQuTFunJG0rZkw5Ehv++7u+Xy7+2OVl/c3WK7ue/Lyy9sZrLpXruvnaW8oXX/uNcuN//75se+ATy98f+/Slch6jfNATTzyxHH300RNe4uabb15+/vOfl4suuqgeBCLPp02A1XzfvvGNb5xwuy996Usl/QPT9ifBWj//+c9PuJ6ZBAgQIEBgLgokoFvze9p+/bP1PNd+jCX5/rTTTitHHHFEXT+SsvO0/8kzhjQ6Aqn7Sv5wskGK0x4tA4nlGbY9/dd//VedV2ziS6RcK+VbU6UMsJb2aknZZ/KZaWe+JNK+++5b0q6tMyWIctq6DWL61a9+1RosI+eXdv5LIvjOIFrMyXOq/kgHJlWVr2PViPOtf5mWCBDoTaDqDDBWNVJJgKixqvPqWFUB09uG1hoKgaoSsL63ub+T/asy1GPViMNjP/vZz+rv0je/+c1jVRTn1jZVId2U11t1qByrItbX2+QzVY1yMuU2ViBAgAABAgQIzJZA1WB/rCroaeVnqlG4xqrOjbN1uL7tt6oAbZ1ze/4t86vGHGNVh4q+HWtp76gqsB6rRsCsr3e99dar86FL+5wcv38CVTCXCT/L7Z/rvK8aro+97nWvG/vFL34xVhXqjlWd1seqUeta21YN6ac8qaqT8FjVyLHeJtuecsopU24zkxXy/JxzzrNS1ZhhrGpoNZPd2GYSgarAeawa0bt1/9s/L1VB/SRbLroon6mqE0a9ryqw2FjVaWPRlcwhQIDAYgjku6X9e6p5v/rqq49VI6SNpaxsVFIVlLxVhrz22muP/eQnPxmVS3MdlUDVQWrCz3LzmW5ek8+qGmiMVY0967z7scceW+flmuXVKNdTelaBR8aqRh318VKGnM/WbKSqgr0+RvKa73jHO8aqxuuzcZg5vc+rr756rBp1uutnZ07juHgCBAgQmLFA1ZF1LM//Tf7i1FNPnfG+luaGVUeXsWrAlvo6Uv5ZNW5cmqfj2LMgUHW6bX1Om89r52vyz29729vGPRtWHaHHnvSkJ7W2rUZU7OnszjnnnLG06cgxHve4xw1FXUdPFzakK1XB8Vv3sPO+pwz54x//eL28Cp457gqrxrUTbpf95Xsi+8pz1mykKnDRWBXkpj5GFfBmrBplczYOY59zWCDlYNUAZK3PeOplq0EiB17kjDPOaJ1z+99z1aF+pMq/Ui5y6KGHtq419bJXXnnlwN8fJzg9gT322KN1j9s/z+3v81vQ+RtQBakbqwINtbatBiac8sD5TFUdX1rbVKM5j1VBpKfcbrorfPCDH2zlq6ugMmPVgCbT3YX1pxBIu5oqEFHrXrZ/XtKeQxo9gbRxqAJVtu55FXSs73+/+Y7Ye++9W8d45StfOZbPmkSAwOgLVAHuxtUdtv+u5H3a7VQBeuvvh5StNKka9KL1ndG5Teomk9fI/PPPP7/ZxCsBAgQIECBQCeywww4T/oamfiJtnP/85z+PjFPyDlUQ2/p68ypfMDK3tr6QajDZCT/LnXnDTO+1115jF1988dhVV11Vf86rQP2tbVM+NlXKc3HymM2+099oNsq1JjqP9HdIH4emL39zDs1r2rsNaqoGLWjF70gsj4ULFw7qqTqvWRBYJvusPqgDkarKp3EjuCcCaUY6kAgQ6E3gggsuKNWPabn11ltLRqHIdLcRdXrbo7UGRaCqDC6nn376Yp3OpptuWo+WNFmUvYzyWjUMqD87icifKHm77LLLYh3XxgQIECBAgACBfgh89KMfraNSZ2TbjTfeuHznO98Z6BHxqiCP455vOw0yolfVyK9Uhb2di+r5GfFvmFJGI8qoeFWhUknk6G9+85tGxBumGzjJuWZEq4xmtjhp6623rkexrAK4dN3N7bffXjLi3fe///36M1Q1cizPetazuq6/OAsyWnBGxn77299eMsqwNH2BZzzjGfUIu51bppgx9/KPf/xj56LWdJ5J77jjjtb0ZG8yWmFGUrz55pvr0RgTOT4j/EoECBDop8AGG2xQj4rabZ9VB9I6f5ZRvztTymAvv/zyztkDPf31r3+9HrGyarRfjwKXUWurjpADfc5OrjeBjMSdEbkXJ2VUwUsuuaRMNoJpfsczYnny/HnuSRly56gxi3MO7dtef/31pQpUWOfbHvWoR7Uv8r5HgdybqnPZImsn31Y1Oip/+MMfFlnWPmOAqpHbT8t7AgQIEBgCgSp4Yak6i5YqOHipgrGUqhNpyQhxw5aqhox13ufGG28sVaPAUg0EU6oOM8N2Gc63i0DKI7/2ta91WTp+9kYbbVSPlJn2OOedd15dFp41Mmpf1eCyHsFv/Bbjp5Jvrjp81qP+veAFLyhnnnlmqRrHjl/J1BIVqIJd1fVOEx20CvhT3598B+T7bIsttmitlmeUKuBMa7p5k++6rJf1qwbQpQrE0Szqy+sVV1xRqmCQpRqgry7X/vKXv1w222yzvuzbTgi0C1QN8UsVbLZUwWPr2ZtsskldL1sFsGhfbaDepzwk5SITpXxPH3jggWW77bYrVQDpiVYpaVOX+udhSfkOetOb3lT/puS+pN4890kafoG77rqrbvdbdX7p6WKqACJlm222KRnB+Vvf+lY9wnc2zEjQE41s3r7TKihEXb+feuCMCv6e97ynVIOHtK/Sl/e5lsc85jF1/WEVMLocdNBBfdnvXNpJnkUyKvdEKfcxZahVcPzyox/9aKJVShVQphx//PETLjNzOAWqQC/1s0WeMfKc+s///M+lGmy0rxeTz9Y+++xTqgGz6u+I/PYceeSRfT2GnREgMNgCL3nJS0ra6EyUXvayl5UqEHNJ+5/f/va3JfXpTUq9c9r6dKbU8+U3Lf1Trrvuuvr7q3Md0wQIECBAYK4K7LjjjpM+x6evQdrNtv/mtlvltze/scOScr5pd5R8RNqVf+5zn6vrX4bl/J1nd4GUKR9zzDHdV+hhST7rqWeZrK14notTxlQNDFDSJyf10FUQ1B723tsqn/nMZ7qWpaSsK3EwJiu/qwLKlEMOOaS3gy3htaoBY+ty5eaw1SAKdV1nM+11tAUW7bk22tfr6giMtEA6Wf3sZz+rC3HTCS+d71KYm0Yw0nALpOHJ4qRkotKYZLJgMr///e/rz0oqlp761KfWnUbTKEoiQIAAAQIECAyCQAp9UuGY4BZpNJtGf+kM+8QnPnEQTm/a53D33XeX3/zmNxNul4Ziw5Z23XXXUo2+Wd+f5CfTSC1BZmYrIMiw+czl801BYzpYTBZMphrRt+6E/POf/7z+zKTh02wGGE5D/sUNkjOX72muPZ3BrrnmmlllyHd8NbpzyXfiP/7jP9aF82nQKhEgQGBJC1QjM9ffexMdN0Hxhy3tvPPO5ac//WldhvzDH/6wJEhYKuZnKyDIsPnM5fNNB6SUIU8WTKYakbAOQJ7GHU972tNKKtA33HDDWWPLvs8555xZ2/9c2HE6lFx99dVz4VJdIwECBAgMmMCWW25ZD3aS8tx0bj388MPrYGaL24hwSV9mBvBJ3mfPPfcsyT9XIynXgURyPdLwCySv1GtKeVgapHamBDFIGehkKdul82XKthLkOoEaFrcNyGTHs6w3gb333rscffTRdflj5xYJEpSUTvrtwWQyb/fdd58woEyCMaaRczWabN+DyaTeJc/zCeSdgESf+MQn6gCfOR+JQL8Fmo7p+S1P/WwamKfeL0GJH/nIR/b7cH3Z32TfqQmM+4EPfKD+1+1gJ5988lB1kn/9619fEhR43333rYNM5XunGs255J5Jwy2QTtaTdUbpvLrkT/OvPWXwlwSumyxl0MHkb7/61a/WdbJZ/5nPfOZkm8x4WQK1f+ELX6i/P7oFdZrxzufIhhmMKPkPiUAEUnfeBHJIu/C0rUjQwX6mfEckkETq61NvkHqAdHCVCBCYWwIJ2NgtoEzT5irBozs7tu+xxx4TBpRJ4NWko446SjCZufVRcrUECBAg0AeBBHxM8JVuKcE1hillkNK0XUvZY/oa7LbbbuVDH/qQILTDdBO7nOtk5bRdNhk3e9VVV60DF04WTCblvSnXyoARCRSevvMpK+1nykB92lr1U9S+BkVgmaoyc2xQTiaRmW655ZbW6aQx9mx2IGodyBsCIyaQkUJOOeWUurNVKpjSsCUVRdLwCiSKc6IvziRlBJJU/k01gmsanWRU2YxGkGj1OurNRNs2BAgQIECAwGwLpHFE8itp2JeGC7/+9a9LIm8PWlqcwtkUpqWx5jCmFFpn9LI0ik8AkTTIN8rqMN7Jv55zRjPbb7/9/jpjGu8SZCjPIlMFqkxD18svv7xkVLojjjhiaD//06AZ+lVT7rA4RYpTPW+ms/oGG2xQNy7NyN8JeioRIEBgtgQWJ9+Wc5rqO222zntx95vrPumkk+rRSfOdnnzbWmuttbi7tf1SFEijzjTynkl67GMfW+fbpuoY9rznPa9ceOGF5a1vfWtJ56Vh/fzPxGhYt5ntfNuwujhvAgQIEFhyAslrpiz32GOPLQnWeOihh5ZTTz116Mp/8puaUd9POOGEupPvG97whrocdHEbRi65O+FIEwkkQMc3vvGNiRZNOS/3Pp/t17zmNV3Xzec/n5X3v//95eEPf3jdufspT3lK1/UtWPIC+++/f/nUpz7V9cAZ3fIVr3jFuOW5rwki1C0gUcrU99lnn3HbLM5EAn+m03COe+KJJ5Z02JMILCmB6667rg5a8oMf/KBue5g6nyc/+clL6vA9H+eiiy6aMoDGZDtLp9cEXR62lMHjMvJu7ks6W6RsKAMSSsMrsHDhwrqMdjpBZdqvdp111qnbf3YGQ2tfp33QwQxUmd+6bCcNrkA62X30ox+d8QnusssuAsrPWG+wNkxwwfzdXnzxxfU9TZDBddddt68n2R5UPm3KU1fvO6KvxHZGYGgE8gyaAbu6DTaVcpErr7xykYCqWb9bfWO2yfKNN954aBycKAECBAgQWBICc7ldR4Jkph4l5RUZlLTfgUGWxP1zjL8KJOj+Vltt9dcZ03iXIDKpD0mg/8lS6uVOO+208trXvrburzLZwGmT7WeyZckL5+9ypin9gAa1DjkB5Nvby6a+a5VVVpnppdpuyAQElBmyG+Z0CUxHIKO8p1NeCnWl4RfYa6+96qh5vV5JMh6pNH7f+97XUyfeSy+9tGQUiASgkQgQIECAAAECgy6Q0SFvvvnmekS8QT/XuXh+aWB6xRVXlHQ4lYZbIAWieabM6FO9pnQsTqP2dLLppaA2o6olQFQ6VUgEGoGMVpgGjiuuuGIzyysBAgQIzIJARrBJZW4aIUvDLZDK7ASUyW9orykV2Bnx573vfW9PlcOXXHJJ3allquDlvR7fegQIECBAgMDcEUhg8MMPP7wOJJNypkFtRDfVHUmZ52GHHVZ3qOs2SvNU+7B8cATOP//8GXWufdCDHlR3vp5qUKDk0RMoOY1fE0xbg8zBuffNmaRsevvtt28mx72uscYa5Xe/+11ZeeWVx83PREZUT0ChzpRtMqBeL+Xindt2m05AzwT2POOMMxbpqNdtG/MJ9FMg32XpTH7ccceVs88+uzzrWc/q5+7tqw8CyZPkeymDfiQAlTTcAimry3f+dNOWW25Zlws+4hGPmHTTa6+9tv7te/e731323nvvSde1kACBwRLI4LLbbbddede73lUHfJuNs8sI7E9/+tPrQIZ+U2ZD2D4JDJfAO9/5zvLmN795wpN+9rOfXQeym2jhE5/4xPLjH/94kUWTbbPIymYQIECAAAECc0bg1ltvLamvSb9ZafgFjjnmmJJyp+mk1LedfvrpPQVNveqqq8qf/vSngQx8Pp1rXlrrCiiztOQH47gCygzGfXAWBAgQmFIgHTnTyCidMu+8886u66+wwgolGamjjz66pKJQIkCAAAECBAgQIECAwOIIZBS8448/vh5JNyNJd0tpJJ9RHPMs8rjHPa7bauYTIECAAAECBAjMkkBGEHn7299eV8zfddddXY+SgG0pQ37Tm95UnvCEJ3RdzwICBAgQIECAQL8FUraU+uxhT6NyHcN+H/px/ieddFI9+mUvIw0mENKee+5Zd9jfYIMNejr8PffcU+bPn9/TulZaOgIpy/7lL3+5yMGPOOKI8oEPfGCR+Zlx0UUX1cGCOhcecsgh5cMf/nDn7MWaTvn8csstN7SBuBbr4m08UAJ++wbqdixyMu7PIiRDOyPtQhM0utfBPjKA4LHHHlte//rXl+WXX76n65Y/6YnJSgQGUmBJ/P0uiWMMJK6TIkBgEYEETN1oo43Kfffdt8iyBDXcfffdF5mfGQl8lYCUnSkBKlOuIhEgQIAAAQIECIy2wEc+8pG6vOqWW27peqEZvPY5z3lOeeMb31h23HHHrutZ0F8BAWX66zlsexNQZtjumPMlQGDOC9x+++3la1/7Wrn88svLjTfeWFIhvOqqq5aHPexhJRGdk4lKRaFEgAABAgQIECBAgACBfgqkYPerX/1q+clPflJuuummkoZEefbYeOONy9Zbb1122GEHo+z2E9y+CBAgQIAAAQIzFLjtttvqMuSM/teUIa+22mrjypAzLREgQIAAAQIECBAgUOpRs9PZ6Rvf+MYig/skiMymm25adtlll3LAAQeURz/60chGTODkk08uRx111CJX9Ytf/KJr4PSxsbGy4YYblv/93/8dt91ll11Wt9sZN9MEAQIECBCYpkB+Z84888xy2mmnlUsvvbR0Br5LsOhtt9227sS9zz77aCs6TV+rEyBAgAABAr0L7LrrruW8884bt8FDHvKQsmDBgq7B7K666qpFyk/WWmut8rvf/W4kAk2PwzBBgAABAgQIECAwoUAGQrvgggvKJZdcUtelZHrllVeu61a22GKL8oxnPKOss846E25r5uwJCCgze7bDsGcBZYbhLjlHAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEJgVgfvvv7/8+te/LjfffHPJ+zXWWKM8/OEPL4Ixzgr3wOz0T3/6U3noQx9aD+bUnNT2229ffvCDHzSTE74eeeSR5QMf+EBr2ROe8ITy05/+tDXtDQECBAgQ6IfAHXfcUedPEkB6ueWWK+uuu24dNHr55Zfvx+7tgwABAgQIECAwqcD5559fdtppp3HrHHfcceUd73jHuHmdE+kk/LOf/aw1+zWveU055ZRTWtPeECBAgAABAgQIECCw5AUElFny5oN0xOUG6WScCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQWJICyy67bNlkk02W5CEdawAEMkr6brvtVs4666zW2Rx88MGt993e7L777uMCyhx44IHdVjWfAAECBAjMWGCVVVYpCVomESBAgAABAgSWhsBznvOcstFGG5UFCxbUh583b1551ateNeWp7LHHHuMCynhmnpLMCgQIECBAgAABAgQIEJhVgXmzunc7J0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgMoMBBBx3UOqu11167pOPbVOmpT31qWX/99evVVlhhhfLyl798qk0sJ0CAAAECBAgQIECAAAECQyWQADLtwWASYGbjjTee8hoShLVJW2+9dXn84x/fTHolQIAAAQIECBAgQIAAgaUgIKDMUkB3SAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGDpCuywww7l0Y9+dH0S++23X0mAmKlSOtXtuuuu9WovfvGLy1prrTXVJpYTIECAAAECBAgQIECAAIGhEzjggAPKsssuW5/3IYcc0tP5b7LJJmXzzTev120PSNPTxlYiQIAAAQIECBAgQIAAgb4LCCjTd1I7JECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBIZB4LDDDquDwhx88ME9n+5LX/rSeptXvepVPW9jRQIECBAgQIAAAQIECBAgMEwCG2ywQdltt93KYx/72LLzzjv3fOove9nLyvrrr1/22muvnrexIgECBAgQIECAAAECBAjMjsAyY1WanV1Pf6833HBDueWWW1obrrnmmvUDZGuGNwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQITCrwq1/9qtx///2tdR72sIeVVVZZpTXtzWgLzBvty3N1BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQmDsCAsrMnXvtSgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQGHEBAWVG/Aa7PAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE5o6AgDJz5167UgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIERlxAQJkRv8EujwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBuSMgoMzcudeulAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBERcQUGbEb7DLI0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBg7ggIKDN37rUrJUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgxAUElBnxG+zyCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCYOwICysyde+1KCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAYcQEBZUb8Brs8AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTmjoCAMnPnXrtSAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRGXEBAmRG/wS6PAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG5IyCgzNy5166UAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIERFxBQZsRvsMsjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGDuCAgoM3futSslQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGDEBQYqoMwyyywz4twujwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCSFRDTY8l6L+2jDVRAmaWN4fgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEhl1gbGxs2C/B+S+GwEAFlJk3b/zp3H///YtxaTYlQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgMLcEEkzmgQceGHfRnTE9xi00MXIC4yO4LOXLW3755cedwX333Tdu2gQBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAt0F7r///kUWdsb0WGQFM0ZKYKADyvzlL38ZKWwXQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGA2BTrjdcybN68su+yys3lI+x4wgYEKKLPCCiuM40nEo84P6bgVTBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0BK48847W+/zZsUVVxw3bWL0BQYqoMzyyy9f8q89LVy4sH3SewIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEugh0xupYaaWVuqxp9qgKDFRAmSCvuuqq46xvueWWcdMmCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBYVODuu+8ud91117gFnbE8xi00MZICAxdQZo011hgHfc8995Rbb7113DwTBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiMF/j9738/bsbyyy9fVl555XHzTIy+wMAFlMmHcMUVVxwnf9NNN5UElpEIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEFhU4JZbbikLFy4ct2DNNdcsyyyzzLh5JkZfYOACyoR8vfXWGyd///33lwULFggqM07FBAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFSbr/99nLDDTeMo1huueXKWmutNW6eibkhMJABZVZZZZWyxhprjLsD99xzT7n22mvLrbfeOm6+CQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJzUeD+++8vN954Y/ntb3+7yOWvv/76Zd68gQwtssi5mtFfgWXGqtTfXfZnbw888EC57rrryt13373IDufPn1/WXHPNsuqqq5YVVlhhkeVmECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBhFgYQKueuuu8rtt99ebr311pIYHZ1p7bXXLuutt17nbNNzRGBgA8rE/7777isLFiyYMKhMc3+WXXbZOqjMcsstV/JeIkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIDBKAgkikzgc9957b7nnnntKprultdZaqzzkIQ/pttj8OSAw0AFl4p8oSDfccEO57bbb5sDtcIkECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEpi8wb968st5665U111xz+hvbYqQEBj6gTKN9xx13lJtuuqncfffdzSyvBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBOa0wDLLLFNWX331su6665bll19+Tlu4+P8TGJqAMs0Nu/POO8ttt91WFi5cWO69995mtlcCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECc0ZgpZVWKquttlpZY401BJKZM3e9twsduoAy7ZeVgDJ/+ctf6sAyef/AAw/Ui8fGxtpX854AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAUAoss8wyZd68efW/5ZZbrsyfP7+suOKK9fRQXpCTnnWBoQ4oM+s6DkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEhEpg3ROfqVAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgEgEBZSbBsYgAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLDJCCgzDDdLedKgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBSQQElJkExyICBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgMk4CAMsN0t5wrAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEJhEQUGYSHIsIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAwTAICygzT3XKuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQmERAQJlJcCwiQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAMAkIKDNMd8u5EiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAYBIBAWUmwbGIAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECwyQgoMww3S3nSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUkEBJSZBMciAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIDJOAgDLDdLecKwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBCYREFBmEhyLCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgMEwCAsoM091yrgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEJhEQECZSXAsIkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwDAJCCgzTHfLuRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGASAQFlJsGxiAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAsMkIKDMMN0t50qAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFJBASUmQTHIgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAyTgIAyw3S3nCsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQmERBQZhIciwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIDBMAgLKDNPdcq4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCYREBAmUlwLCJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMAwCQgoM0x3y7kSIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgEgEBZSbBsYgAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLDJCCgzDDdLedKgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBSQQElJkExyICBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgMk4CAMsN0t5wrAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEJhEQUGYSHIsIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAwTAICygzT3XKuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQmERAQJlJcCwiQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAMAkIKDNMd8u5EiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAYBIBAWUmwbGIAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECwyTw/wCiWheO5VEScQAAAABJRU5ErkJggg==" + } + } + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "beb6652b-6fbb-4475-9283-d11fa2812848", + "customOutput": null, + "collapsed": false, + "requestMsgId": "12410103-c132-4be6-83c2-fd7781eeee45", + "executionStartTime": 1668650182436, + "executionStopTime": 1668650182448 + }, + "source": [ + "import torch\n", + "import numpy as np\n", + "import os\n", + "\n", + "tkwargs = {\n", + " \"dtype\": torch.double,\n", + " \"device\": torch.device(\"cuda:2\" if torch.cuda.is_available() else \"cpu\"),\n", + "}\n", + "seed = 0\n", + "torch.manual_seed(seed)\n", + "np.random.seed(seed)\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ], + "execution_count": 1, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "628d0f9c-dde6-4e07-9394-abf074df9144", + "showInput": false + }, + "source": [ + "## Configure the problem and optimization" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "5b2f0395-06e3-419b-ae7d-495ab050638b", + "customOutput": null, + "collapsed": false, + "requestMsgId": "3c295fbe-79b6-4b6a-8339-a4ab7b49c5c0", + "executionStartTime": 1668650182776, + "executionStopTime": 1668650186914 + }, + "source": [ + "from botorch.test_functions.multi_objective import ToyRobust\n", + "\n", + "base_function = ToyRobust(negate=True).to(**tkwargs) # define test function\n", + "bounds = base_function.bounds\n", + "n_w = (\n", + " 2 if SMOKE_TEST else 32\n", + ") # number of MC samples for approximating input noise distribution\n", + "alpha = 0.9 # probability level\n", + "std_dev = 0.1 # zero-mean quasi-Normal input noise, with a standard deviation of 0.1\n", + "search_space_range = bounds[1] - bounds[0]\n", + "# scale the specified std_dev to a unit cube search space\n", + "scaled_std_dev = (\n", + " torch.tensor(std_dev, dtype=bounds.dtype, device=bounds.device) / search_space_range\n", + ")\n", + "mc_samples = 2 if SMOKE_TEST else 256 # number of samples for MC acquisition functions\n", + "hv_n_w = (\n", + " 2 if SMOKE_TEST else 512\n", + ") # number of MC samples for approximating input noise distribution for omniscient evaluation\n", + "mvar_ref_point = torch.tensor(\n", + " [-14.1951, -3.1887], **tkwargs\n", + ") # reference point for the MVaR frontier\n", + "# options for acquisition optimization\n", + "options = {\n", + " \"batch_limit\": 5, # number of starting points to jointly optimize in L-BFGS-B\n", + " \"maxiter\": 2 if SMOKE_TEST else 200, # maximum number of L-BFGS-B iterations\n", + "}\n", + "optimization_kwargs = {\n", + " \"num_restarts\": 2 if SMOKE_TEST else 20, # number of random restarts for L-BFGS-B\n", + " \"raw_samples\": 10\n", + " if SMOKE_TEST\n", + " else 1024, # number of random samples for initialization heuristic\n", + " \"options\": options,\n", + "}\n", + "iterations = 1 if SMOKE_TEST else 5 # number of BO iterations\n", + "verbose = True\n", + "n_initial_points = 4 # number of initial sobol points" + ], + "execution_count": 2, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "79ac50d4-58c5-4fde-9c49-2c381ee8adad", + "showInput": false + }, + "source": [ + "## Create a function for evaluating the objectives.\n", + "We work in a search space that is normalized to the unit cube and only unnormalize the search space to evaluate the objectives." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "48f3a309-2347-4174-866a-ca1139208c8f", + "customOutput": null, + "collapsed": false, + "requestMsgId": "26d5287b-2bdb-4a42-bd37-a6311a19f092", + "executionStartTime": 1668650187152, + "executionStopTime": 1668650187160 + }, + "source": [ + "from botorch.utils.transforms import unnormalize\n", + "\n", + "# define function for evaluation\n", + "def eval_problem(X):\n", + " X = unnormalize(X, base_function.bounds)\n", + " return base_function(X)" + ], + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "dc795f3d-fea5-4641-b393-17de1d1cf40b", + "showInput": false + }, + "source": [ + "## Create a function for sampling initial quasi-random points from the unit cube" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "aed74cd7-ad5c-477c-ba27-554447e732a2", + "customOutput": null, + "collapsed": false, + "requestMsgId": "a8d7f75d-2c4d-46a1-b580-7d0040e7b82f", + "executionStartTime": 1668650187395, + "executionStopTime": 1668650187484 + }, + "source": [ + "from botorch.utils.sampling import draw_sobol_samples\n", + "\n", + "standard_bounds = torch.ones(2, base_function.dim, **tkwargs)\n", + "standard_bounds[0] = 0\n", + "\n", + "\n", + "def generate_initial_data(\n", + " n,\n", + " eval_problem,\n", + " bounds,\n", + " tkwargs,\n", + "):\n", + " r\"\"\"\n", + " Generates the initial data for the experiments.\n", + " Args:\n", + " n: Number of training points.\n", + " eval_problem: The callable used to evaluate the objective function.\n", + " bounds: The bounds to generate the training points from. `2 x d`-dim tensor.\n", + " tkwargs: Arguments for tensors, dtype and device.\n", + " Returns:\n", + " The train_X and train_Y. `n x d` and `n x m`.\n", + " \"\"\"\n", + " train_x = draw_sobol_samples(bounds=bounds, n=n, q=1).squeeze(-2).to(**tkwargs)\n", + " train_obj = eval_problem(train_x)\n", + " return train_x, train_obj" + ], + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "54209f83-8c8a-4889-b045-426c6bd730af", + "showInput": false + }, + "source": [ + "## Create a utility module for evaluating the hypervolume of the MVaR frontier\n", + "\n", + "We can evaluate the quality of an MVaR frontier by measuring the hypervolume dominated by the MVaR frontier and bounded from below by a reference point." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "40740209-b429-4e72-af5d-f458a68c3269", + "customOutput": null, + "collapsed": false, + "requestMsgId": "4f2fc6b7-94d1-43a2-90c5-94460de5b955", + "executionStartTime": 1668650187740, + "executionStopTime": 1668650187746 + }, + "source": [ + "from botorch.acquisition.multi_objective.multi_output_risk_measures import MVaR\n", + "from botorch.utils.multi_objective.box_decompositions.dominated import (\n", + " DominatedPartitioning,\n", + ")\n", + "from botorch.models.transforms.input import InputPerturbation\n", + "\n", + "\n", + "class MVaRHV(torch.nn.Module):\n", + " r\"\"\"A helper class that calculates the HV of the MVaR set.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " alpha,\n", + " eval_problem,\n", + " ref_point,\n", + " n_w,\n", + " perturbation_set,\n", + " ):\n", + " super().__init__()\n", + " self.hv = DominatedPartitioning(ref_point=ref_point)\n", + " self.mvar = MVaR(n_w=n_w, alpha=alpha)\n", + " self.perturbation = InputPerturbation(\n", + " perturbation_set=perturbation_set,\n", + " ).eval()\n", + " self.eval_problem = eval_problem\n", + "\n", + " def forward(self, new_X):\n", + " r\"\"\"Calculate the resulting HV by adding the MVaR corresponding to the new_X\n", + " to the Pareto set.\n", + " Args:\n", + " new_X: `q x dim`-dim tensor of candidate points.\n", + " Returns:\n", + " The cumulative MVaR HV of all points evaluated so far.\n", + " \"\"\"\n", + " # Get the corresponding MVaR set.\n", + " perturbed_X = self.perturbation(new_X)\n", + " perturbed_Y = self.eval_problem(perturbed_X)\n", + " new_mvar = self.mvar(perturbed_Y).view(-1, perturbed_Y.shape[-1])\n", + " # Update and return the new MVaR HV.\n", + " self.hv.update(new_mvar)\n", + " return self.hv.compute_hypervolume().item()" + ], + "execution_count": 5, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "34a89cbe-d10d-4e9b-b21e-8f4ebe830baf", + "showInput": false + }, + "source": [ + "## Create a method for initializing the surrogate model" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "c9c2c4a5-f72a-44ae-afb3-4907d1d1e6f4", + "customOutput": null, + "collapsed": false, + "requestMsgId": "e7f5fd2b-1cb9-4b24-b99d-cc88643ab7ec", + "executionStartTime": 1668650187968, + "executionStopTime": 1668650187975 + }, + "source": [ + "from botorch.models.gp_regression import SingleTaskGP\n", + "from botorch.models.model_list_gp_regression import ModelListGP\n", + "from gpytorch.mlls import SumMarginalLogLikelihood\n", + "from botorch.models.transforms.outcome import Standardize\n", + "\n", + "\n", + "def initialize_model(train_x, train_y, perturbation_set):\n", + " r\"\"\"Constructs the model and its MLL.\n", + " Args:\n", + " train_x: An `n x d`-dim tensor of training inputs.\n", + " train_y: An `n x m`-dim tensor of training outcomes.\n", + " perturbation_set: A `n_w x d`-dim tensor of perturbations\n", + " Returns:\n", + " The MLL and the model. Note: the model is not trained!\n", + " \"\"\"\n", + " train_Yvar = torch.full_like(train_y, 1e-7) * train_y.std(dim=0).pow(2)\n", + " models = []\n", + " for i in range(train_y.shape[-1]):\n", + " models.append(\n", + " SingleTaskGP(\n", + " train_X=train_x,\n", + " train_Y=train_y[..., i : i + 1],\n", + " train_Yvar=train_Yvar[..., i : i + 1],\n", + " outcome_transform=Standardize(m=1),\n", + " input_transform=InputPerturbation(perturbation_set=perturbation_set),\n", + " )\n", + " )\n", + " model = ModelListGP(*models)\n", + " mll = SumMarginalLogLikelihood(model.likelihood, model)\n", + "\n", + " return mll, model" + ], + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "5585c5c6-7652-44e2-b9d8-bb72505da7b8", + "showInput": false + }, + "source": [ + "## Create a method for initializing MARS-NEI\n", + "\n", + "We use the MARS approach with the NEI acquisition function as in [1]." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "8d6469ba-43db-475b-ac2c-815ecadaeadc", + "customOutput": null, + "collapsed": false, + "requestMsgId": "fca87ff5-d043-411d-a0ec-4c6bcd04d8bd", + "executionStartTime": 1668650188197, + "executionStopTime": 1668650188204 + }, + "source": [ + "from botorch.acquisition.multi_objective.multi_output_risk_measures import MARS\n", + "from botorch.acquisition.monte_carlo import qNoisyExpectedImprovement\n", + "from botorch.utils.sampling import sample_simplex\n", + "\n", + "\n", + "def get_MARS_NEI(\n", + " model,\n", + " n_w,\n", + " X_baseline,\n", + " sampler,\n", + " mvar_ref_point,\n", + "):\n", + " r\"\"\"Construct the NEI acquisition function with VaR of Chebyshev scalarizations.\n", + " Args:\n", + " model: A fitted multi-output GPyTorchModel.\n", + " n_w: the number of perturbation samples\n", + " X_baseline: An `r x d`-dim tensor of points already observed.\n", + " sampler: The sampler used to draw the base samples.\n", + " mvar_ref_point: The mvar reference point.\n", + " Returns:\n", + " The NEI acquisition function.\n", + " \"\"\"\n", + " # sample weights from the simplex\n", + " weights = sample_simplex(\n", + " d=mvar_ref_point.shape[0],\n", + " n=1,\n", + " dtype=X_baseline.dtype,\n", + " device=X_baseline.device,\n", + " ).squeeze(0)\n", + " # set up mars objective\n", + " mars = MARS(\n", + " alpha=alpha,\n", + " n_w=n_w,\n", + " chebyshev_weights=weights,\n", + " ref_point=mvar_ref_point,\n", + " )\n", + " # set normalization bounds for the scalarization\n", + " mars.set_baseline_Y(model=model, X_baseline=X_baseline)\n", + " # initial qNEI acquisition function with the MARS objective\n", + " acq_func = qNoisyExpectedImprovement(\n", + " model=model,\n", + " X_baseline=X_baseline,\n", + " objective=mars,\n", + " prune_baseline=True,\n", + " sampler=sampler,\n", + " )\n", + " return acq_func" + ], + "execution_count": 7, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "b0cafbb9-9e05-4049-87bc-4324f0629bc7", + "showInput": false + }, + "source": [ + "## Set up the optimization" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "fd893472-c1a0-441e-a8d1-e80f20400953", + "customOutput": null, + "collapsed": false, + "requestMsgId": "37168aae-a90e-4205-a752-c240e86a122d", + "executionStartTime": 1668650188436, + "executionStopTime": 1668650188443 + }, + "source": [ + "# Get the initial data.\n", + "X, Y = generate_initial_data(\n", + " n=n_initial_points,\n", + " eval_problem=eval_problem,\n", + " bounds=standard_bounds,\n", + " tkwargs=tkwargs,\n", + ")" + ], + "execution_count": 8, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "60e71e0d-5faf-4196-9c5d-0b48492dc5e0", + "customOutput": null, + "collapsed": false, + "requestMsgId": "b915d403-0139-463f-af8a-22ac984a344f", + "executionStartTime": 1668650188661, + "executionStopTime": 1668650188748 + }, + "source": [ + "from botorch.utils.sampling import draw_sobol_normal_samples\n", + "\n", + "# Ensure consistency of MVaRHV across seeds by using same perturbations.\n", + "# This sets the random seed and generates the perturbations on CPU.\n", + "# MVaR calculations are also moved to CPU.\n", + "old_state = torch.random.get_rng_state()\n", + "torch.manual_seed(0)\n", + "perturbations = (\n", + " draw_sobol_normal_samples(d=base_function.dim, n=hv_n_w, **tkwargs) * scaled_std_dev\n", + ")\n", + "mvar_hv = MVaRHV(\n", + " alpha=alpha,\n", + " eval_problem=eval_problem,\n", + " ref_point=torch.tensor(mvar_ref_point, **tkwargs),\n", + " n_w=hv_n_w,\n", + " perturbation_set=perturbations,\n", + ")\n", + "torch.random.set_rng_state(old_state)" + ], + "execution_count": 9, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "d860f8f2-1a99-4107-b8fb-becd358541fc", + "customOutput": null, + "collapsed": false, + "requestMsgId": "eb92bde8-ad9f-4514-9127-934e8754f056", + "executionStartTime": 1668650188994, + "executionStopTime": 1668650189070 + }, + "source": [ + "try:\n", + " all_mvar_hvs = torch.tensor([mvar_hv(X)], dtype=tkwargs[\"dtype\"])\n", + "except RuntimeError:\n", + " # Try to feed them one by one. This helps with memory.\n", + " initial_mvar_hv = 0.0\n", + " for j in range(X.shape[0]):\n", + " initial_mvar_hv = mvar_hv(X[j : j + 1])\n", + " all_mvar_hvs = torch.tensor([initial_mvar_hv], dtype=tkwargs[\"dtype\"])" + ], + "execution_count": 10, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "77044bd5-dd9a-466f-94d1-b05a06f0cf92", + "showInput": false + }, + "source": [ + "## Run BO with MARS" + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "d76adb33-3442-4a8f-9b48-0c80c09e2c51", + "customOutput": null, + "collapsed": false, + "requestMsgId": "d83b7a34-7b1a-45c4-aa30-84ab5055342f", + "executionStartTime": 1668650189383, + "executionStopTime": 1668650246736 + }, + "source": [ + "import gc\n", + "import gpytorch.settings as gpt_settings\n", + "from time import time\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "from botorch.optim.optimize import optimize_acqf\n", + "\n", + "start = time()\n", + "for i in range(iterations):\n", + " if verbose:\n", + " print(\n", + " f\"Starting iteration {i}, \"\n", + " f\"time: {time()-start}, current MVaR HV: {all_mvar_hvs[-1]}.\"\n", + " )\n", + "\n", + " # Generate the perturbations for evaluation\n", + " perturbation_set = (\n", + " draw_sobol_normal_samples(d=base_function.dim, n=n_w, **tkwargs)\n", + " * scaled_std_dev\n", + " )\n", + " # Fit the model.\n", + " mll, model = initialize_model(\n", + " train_x=X, train_y=Y, perturbation_set=perturbation_set\n", + " )\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " with gpt_settings.cholesky_max_tries(6):\n", + " # Construct the acqf.\n", + " sampler = SobolQMCNormalSampler(sample_shape=torch.Size([mc_samples]))\n", + " acq_func = get_MARS_NEI(\n", + " model=model,\n", + " n_w=n_w,\n", + " X_baseline=X,\n", + " sampler=sampler,\n", + " mvar_ref_point=mvar_ref_point,\n", + " )\n", + "\n", + " # Optimize the acqf.\n", + " while options[\"batch_limit\"] >= 1:\n", + " # Try to get around OOM by reducing batch_limit.\n", + " try:\n", + " torch.cuda.empty_cache()\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=standard_bounds,\n", + " q=1,\n", + " **optimization_kwargs,\n", + " )\n", + " torch.cuda.empty_cache()\n", + " break\n", + " except RuntimeError as e:\n", + " if options[\"batch_limit\"] > 1:\n", + " print(\n", + " \"Got a RuntimeError in `optimize_acqf`. \"\n", + " \"Trying with reduced `batch_limit`.\"\n", + " )\n", + " options[\"batch_limit\"] //= 2\n", + " continue\n", + " else:\n", + " raise e\n", + " # free memory\n", + " del acq_func, mll, model\n", + " gc.collect()\n", + " torch.cuda.empty_cache()\n", + "\n", + " # Get the new observations and update the data.\n", + " new_y = eval_problem(candidates)\n", + " X = torch.cat([X, candidates], dim=0)\n", + " Y = torch.cat([Y, new_y], dim=0)\n", + " new_mvar_hv = mvar_hv(candidates)\n", + " all_mvar_hvs = torch.cat(\n", + " [all_mvar_hvs, torch.tensor([new_mvar_hv], dtype=tkwargs[\"dtype\"])], dim=0\n", + " )" + ], + "execution_count": 11, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 0, time: 0.00027441978454589844, current MVaR HV: 42.430757642706055.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:\n\nVery small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 1, time: 9.476728200912476, current MVaR HV: 85.50895176532245.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:\n\nVery small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:306: RuntimeWarning:\n\nOptimization failed in `gen_candidates_scipy` with the following warning(s):\n[NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal')]\nTrying again with a new set of initial conditions.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 2, time: 24.17291235923767, current MVaR HV: 87.13964153247537.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:\n\nVery small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n\n/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/linear_operator/utils/cholesky.py:40: NumericalWarning:\n\nA not p.d., added jitter of 1.0e-08 to the diagonal\n\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 3, time: 29.630997896194458, current MVaR HV: 87.148383606772.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:\n\nVery small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/linear_operator/utils/cholesky.py:40: NumericalWarning:\n\nA not p.d., added jitter of 1.0e-08 to the diagonal\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:306: RuntimeWarning:\n\nOptimization failed in `gen_candidates_scipy` with the following warning(s):\n[NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal')]\nTrying again with a new set of initial conditions.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:328: RuntimeWarning:\n\nOptimization failed on the second try, after generating a new set of initial conditions.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Starting iteration 4, time: 43.48030400276184, current MVaR HV: 89.14242777378423.\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/gpytorch/likelihoods/noise_models.py:144: NumericalWarning:\n\nVery small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/linear_operator/utils/cholesky.py:40: NumericalWarning:\n\nA not p.d., added jitter of 1.0e-08 to the diagonal\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:306: RuntimeWarning:\n\nOptimization failed in `gen_candidates_scipy` with the following warning(s):\n[NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2.'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal'), NumericalWarning('A not p.d., added jitter of 1.0e-08 to the diagonal')]\nTrying again with a new set of initial conditions.\n\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/data/sandcastle/boxes/fbsource/buck-out/v2/gen/fbcode/f3b9a99e517e0a13/bento/kernels/__bento_kernel_axoptics__/bento_kernel_axoptics#link-tree/botorch/optim/optimize.py:328: RuntimeWarning:\n\nOptimization failed on the second try, after generating a new set of initial conditions.\n\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "97ef61bc-04a8-4d19-81ca-089b53eea058", + "showInput": false + }, + "source": [ + "## Evaluate Results" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "301bb536-78e9-4c8e-9b44-2b66d93184b4", + "showInput": false + }, + "source": [ + "First we evaluate the hypervolume dominated by the MvaR frontier and bounded from below by the reference point. A larger hypervolume means a better MVaR frontier." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "68905db7-f9cd-4e43-8705-2f9650d782bc", + "customOutput": null, + "collapsed": false, + "requestMsgId": "1ea755d5-be5f-447f-8c61-2477c569fc86", + "executionStartTime": 1668650247048, + "executionStopTime": 1668650247485 + }, + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "plt.plot(torch.arange(all_mvar_hvs.shape[0]), all_mvar_hvs)\n", + "plt.ylabel(\"MVaR HV\")\n", + "plt.xlabel(\"BO Iterations\")" + ], + "execution_count": 12, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "Text(0.5, 0, 'BO Iterations')" + }, + "metadata": { + "bento_obj_id": "139793369851552" + }, + "execution_count": 12 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "bento_obj_id": "139793427799920", + "needs_background": "light" + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "be6d2097-0494-4c32-b01b-2f64ddc5a2ad", + "showInput": false + }, + "source": [ + "Next, we plot the mvar frontier to see the possible probabilistic lower bounds. For each point $\\mathbf z$ in the MVaR set, there is a previously evaluated design that will be at least as good as $\\mathbf z$ with probability $\\alpha$." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "0b80468d-c17d-4817-935f-b7d6320a6aab", + "customOutput": null, + "collapsed": false, + "requestMsgId": "7f6c4f92-64b3-4f50-9dd7-02e2e63986be", + "executionStartTime": 1668650370360, + "executionStopTime": 1668650370435 + }, + "source": [ + "from botorch.utils.multi_objective.pareto import is_non_dominated\n", + "\n", + "# Evaluate true MVaR\n", + "# Perturb X\n", + "perturbed_X = mvar_hv.perturbation(X)\n", + "# Compute objectives at perturbed points\n", + "true_Y_under_noise = eval_problem(perturbed_X)\n", + "# calculate the MVaR frontier for each point X\n", + "mvar_points = mvar_hv.mvar(true_Y_under_noise)\n", + "# calculate the pareto frontier over the union of individual MVaR frontiers for each design\n", + "mvar_frontier = mvar_points[is_non_dominated(mvar_points)].cpu()" + ], + "execution_count": 15, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "45ac2c9a-1d19-4a17-838f-73d3acb6d5f3", + "customOutput": null, + "collapsed": false, + "requestMsgId": "81c30d71-221b-4c88-aaf5-9c8ba128465b", + "executionStartTime": 1668650373160, + "executionStopTime": 1668650373294 + }, + "source": [ + "plt.plot(\n", + " mvar_frontier[:, 0], mvar_frontier[:, 1], \".\", alpha=0.4, label=\"MVaR Frontier\"\n", + ")\n", + "plt.xlabel(\"Objective 1\")\n", + "plt.ylabel(\"Objective 2\")\n", + "plt.legend()" + ], + "execution_count": 16, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "" + }, + "metadata": { + "bento_obj_id": "139793366604336" + }, + "execution_count": 16 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "bento_obj_id": "139793366602320", + "needs_background": "light" + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "20a6fa62-d448-4a5f-b32e-0826e336e27c" + }, + "source": [ + "Finally, we can plot the MVaR frontier for each evaluated design. Clearly some designs are far more robust than others under input noise." + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "3839b063-33c9-456b-a66a-e9b5f5084369", + "customOutput": null, + "collapsed": false, + "requestMsgId": "b35cd8e9-efc6-44c1-981e-e595086a4790", + "executionStartTime": 1668650388086, + "executionStopTime": 1668650388283 + }, + "source": [ + "for i, y in enumerate(true_Y_under_noise.view(X.shape[0], hv_n_w, -1).cpu()):\n", + " plt.plot(y[:, 0], y[:, 1], \".\", color=f\"C{i}\", label=f\"x_{i}\", alpha=0.3)\n", + "plt.xlabel(\"Objective 1\")\n", + "plt.ylabel(\"Objective 2\")\n", + "plt.legend()" + ], + "execution_count": 18, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "" + }, + "metadata": { + "bento_obj_id": "139793366590848" + }, + "execution_count": 18 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "bento_obj_id": "139793366591424", + "needs_background": "light" + } + } + ] + }, + { + "cell_type": "code", + "metadata": { + "originalKey": "9a6504ac-11dc-4690-a8c7-bfd6c3cefc85", + "customOutput": null + }, + "source": [ + "" + ], + "execution_count": null, + "outputs": [] + } + ] +} diff --git a/website-old/static/files/robust_multi_objective_bo.py b/website-old/static/files/robust_multi_objective_bo.py new file mode 100644 index 0000000000..bd847c6159 --- /dev/null +++ b/website-old/static/files/robust_multi_objective_bo.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Robust Multi-Objective Bayesian Optimization Under Input Noise +# +# In this tutorial, we illustrate how to perform robust multi-objective Bayesian optimization (BO) under input noise. +# +# This is a simple tutorial; for support for constraints, batch sizes greater than 1, and many alternative methods, please see https://github.com/facebookresearch/robust_mobo. +# +# We consider the problem of optimizing (maximizing) a vector-valued objective function $\mathbf f(\mathbf x)$ where at implementation time $\mathbf f(\mathbf x)$ is subject to input noise $\mathbf{f}(\mathbf{x} \diamond \mathbf{\xi})$ where $\mathbf{\xi} \sim P(\mathbf \xi | \mathbf x)$ is the random input noise and $\diamond$ denotes the perturbation function (e.g. addition, multiplication, or any arbitrary function). +# +# We consider the scenario where: +# 1. We have access to a simulator during optimization such that $\mathbf{f}$ can be queried at a given design $\mathbf x$ without input noise. +# 2. Input noise is only present at implementation time. After optimization, the design that is chosen according to the decision maker's preferences will be subject to input noise. +# 3. The perturbation function is known. +# 4. We can sample from the generative process $P(\mathbf \xi | \mathbf x)$. +# +# Quantifying risk is important to understand how the final selected design will perform under input noise. +# +# To quantify risk in the multi-objective setting, the MVaR set is an appealing option. For a given design $\mathbf x$, MVaR is theis the set of points such that for every $\mathbf z$ in the MVaR set, $\mathbf z$ is Pareto dominated by the objectives under input noise $\mathbf f (\mathbf x \diamond \mathbf \xi)$ with probability $\alpha$. In other words, if $\mathbf x$ is the chosen final design, the objectives will be better than $\mathbf z$ with probability $\alpha$ for all $\mathbf z$ in the MVaR set. +# +# ![MVaR](attachment:1d_toy_mvar_single_designs_combined.png "MvaR") +# +# However, during optimization we are interested in identifying the global MVaR set that is the optimal set of probabilistic lower bounds across all designs. The global MVaR set is the non-dominated set of points across the union of MVaR sets of all points in the design space. See [1] for a deeper discussion. +# +# In this tutorial, we will optimize the 2 1-dimensional functions shown above to identify an approximate global MVaR set. See [1] for a description of these functions. +# +# To do so, we will use Bayesian optimization with MARS (MVaR approximated via random scalarizations). MARS exploits the result in [1] that, under limited assumptions, there is a bijection between weights in the $M-1$-dimensional-simplex (where $M$ is the number of objectives) and points $\mathbf z$ in the MVaR set based on the value-at-risk (VaR) of a Chebyshev scalarization. +# +# ![bijection](attachment:bijection_plots.png "bijection") +# +# MARS leverages this result to efficiently identify the MVaR set using Bayesian optimization by, at each iteration, sampling a random Chebyshev scalarization and selecting the new design with maximum acquisition value with respect to the value-at-risk +# of the sampled scalarization. +# +# [1] [S. Daulton, S. Cakmak, M. Balandat, M. A. Osborne, E. Zhou, and E. Bakshy. Robust Bayesian Optimziation Under Input Noise. ICML, 2022.](https://arxiv.org/abs/2202.07549) + +# In[1]: + + +import torch +import numpy as np +import os + +tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda:2" if torch.cuda.is_available() else "cpu"), +} +seed = 0 +torch.manual_seed(seed) +np.random.seed(seed) +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ## Configure the problem and optimization + +# In[2]: + + +from botorch.test_functions.multi_objective import ToyRobust + +base_function = ToyRobust(negate=True).to(**tkwargs) # define test function +bounds = base_function.bounds +n_w = ( + 2 if SMOKE_TEST else 32 +) # number of MC samples for approximating input noise distribution +alpha = 0.9 # probability level +std_dev = 0.1 # zero-mean quasi-Normal input noise, with a standard deviation of 0.1 +search_space_range = bounds[1] - bounds[0] +# scale the specified std_dev to a unit cube search space +scaled_std_dev = ( + torch.tensor(std_dev, dtype=bounds.dtype, device=bounds.device) / search_space_range +) +mc_samples = 2 if SMOKE_TEST else 256 # number of samples for MC acquisition functions +hv_n_w = ( + 2 if SMOKE_TEST else 512 +) # number of MC samples for approximating input noise distribution for omniscient evaluation +mvar_ref_point = torch.tensor( + [-14.1951, -3.1887], **tkwargs +) # reference point for the MVaR frontier +# options for acquisition optimization +options = { + "batch_limit": 5, # number of starting points to jointly optimize in L-BFGS-B + "maxiter": 2 if SMOKE_TEST else 200, # maximum number of L-BFGS-B iterations +} +optimization_kwargs = { + "num_restarts": 2 if SMOKE_TEST else 20, # number of random restarts for L-BFGS-B + "raw_samples": 10 + if SMOKE_TEST + else 1024, # number of random samples for initialization heuristic + "options": options, +} +iterations = 1 if SMOKE_TEST else 5 # number of BO iterations +verbose = True +n_initial_points = 4 # number of initial sobol points + + +# ## Create a function for evaluating the objectives. +# We work in a search space that is normalized to the unit cube and only unnormalize the search space to evaluate the objectives. + +# In[3]: + + +from botorch.utils.transforms import unnormalize + +# define function for evaluation +def eval_problem(X): + X = unnormalize(X, base_function.bounds) + return base_function(X) + + +# ## Create a function for sampling initial quasi-random points from the unit cube + +# In[4]: + + +from botorch.utils.sampling import draw_sobol_samples + +standard_bounds = torch.ones(2, base_function.dim, **tkwargs) +standard_bounds[0] = 0 + + +def generate_initial_data( + n, + eval_problem, + bounds, + tkwargs, +): + r""" + Generates the initial data for the experiments. + Args: + n: Number of training points. + eval_problem: The callable used to evaluate the objective function. + bounds: The bounds to generate the training points from. `2 x d`-dim tensor. + tkwargs: Arguments for tensors, dtype and device. + Returns: + The train_X and train_Y. `n x d` and `n x m`. + """ + train_x = draw_sobol_samples(bounds=bounds, n=n, q=1).squeeze(-2).to(**tkwargs) + train_obj = eval_problem(train_x) + return train_x, train_obj + + +# ## Create a utility module for evaluating the hypervolume of the MVaR frontier +# +# We can evaluate the quality of an MVaR frontier by measuring the hypervolume dominated by the MVaR frontier and bounded from below by a reference point. + +# In[5]: + + +from botorch.acquisition.multi_objective.multi_output_risk_measures import MVaR +from botorch.utils.multi_objective.box_decompositions.dominated import ( + DominatedPartitioning, +) +from botorch.models.transforms.input import InputPerturbation + + +class MVaRHV(torch.nn.Module): + r"""A helper class that calculates the HV of the MVaR set.""" + + def __init__( + self, + alpha, + eval_problem, + ref_point, + n_w, + perturbation_set, + ): + super().__init__() + self.hv = DominatedPartitioning(ref_point=ref_point) + self.mvar = MVaR(n_w=n_w, alpha=alpha) + self.perturbation = InputPerturbation( + perturbation_set=perturbation_set, + ).eval() + self.eval_problem = eval_problem + + def forward(self, new_X): + r"""Calculate the resulting HV by adding the MVaR corresponding to the new_X + to the Pareto set. + Args: + new_X: `q x dim`-dim tensor of candidate points. + Returns: + The cumulative MVaR HV of all points evaluated so far. + """ + # Get the corresponding MVaR set. + perturbed_X = self.perturbation(new_X) + perturbed_Y = self.eval_problem(perturbed_X) + new_mvar = self.mvar(perturbed_Y).view(-1, perturbed_Y.shape[-1]) + # Update and return the new MVaR HV. + self.hv.update(new_mvar) + return self.hv.compute_hypervolume().item() + + +# ## Create a method for initializing the surrogate model + +# In[6]: + + +from botorch.models.gp_regression import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from gpytorch.mlls import SumMarginalLogLikelihood +from botorch.models.transforms.outcome import Standardize + + +def initialize_model(train_x, train_y, perturbation_set): + r"""Constructs the model and its MLL. + Args: + train_x: An `n x d`-dim tensor of training inputs. + train_y: An `n x m`-dim tensor of training outcomes. + perturbation_set: A `n_w x d`-dim tensor of perturbations + Returns: + The MLL and the model. Note: the model is not trained! + """ + train_Yvar = torch.full_like(train_y, 1e-7) * train_y.std(dim=0).pow(2) + models = [] + for i in range(train_y.shape[-1]): + models.append( + SingleTaskGP( + train_X=train_x, + train_Y=train_y[..., i : i + 1], + train_Yvar=train_Yvar[..., i : i + 1], + outcome_transform=Standardize(m=1), + input_transform=InputPerturbation(perturbation_set=perturbation_set), + ) + ) + model = ModelListGP(*models) + mll = SumMarginalLogLikelihood(model.likelihood, model) + + return mll, model + + +# ## Create a method for initializing MARS-NEI +# +# We use the MARS approach with the NEI acquisition function as in [1]. + +# In[7]: + + +from botorch.acquisition.multi_objective.multi_output_risk_measures import MARS +from botorch.acquisition.monte_carlo import qNoisyExpectedImprovement +from botorch.utils.sampling import sample_simplex + + +def get_MARS_NEI( + model, + n_w, + X_baseline, + sampler, + mvar_ref_point, +): + r"""Construct the NEI acquisition function with VaR of Chebyshev scalarizations. + Args: + model: A fitted multi-output GPyTorchModel. + n_w: the number of perturbation samples + X_baseline: An `r x d`-dim tensor of points already observed. + sampler: The sampler used to draw the base samples. + mvar_ref_point: The mvar reference point. + Returns: + The NEI acquisition function. + """ + # sample weights from the simplex + weights = sample_simplex( + d=mvar_ref_point.shape[0], + n=1, + dtype=X_baseline.dtype, + device=X_baseline.device, + ).squeeze(0) + # set up mars objective + mars = MARS( + alpha=alpha, + n_w=n_w, + chebyshev_weights=weights, + ref_point=mvar_ref_point, + ) + # set normalization bounds for the scalarization + mars.set_baseline_Y(model=model, X_baseline=X_baseline) + # initial qNEI acquisition function with the MARS objective + acq_func = qNoisyExpectedImprovement( + model=model, + X_baseline=X_baseline, + objective=mars, + prune_baseline=True, + sampler=sampler, + ) + return acq_func + + +# ## Set up the optimization + +# In[8]: + + +# Get the initial data. +X, Y = generate_initial_data( + n=n_initial_points, + eval_problem=eval_problem, + bounds=standard_bounds, + tkwargs=tkwargs, +) + + +# In[9]: + + +from botorch.utils.sampling import draw_sobol_normal_samples + +# Ensure consistency of MVaRHV across seeds by using same perturbations. +# This sets the random seed and generates the perturbations on CPU. +# MVaR calculations are also moved to CPU. +old_state = torch.random.get_rng_state() +torch.manual_seed(0) +perturbations = ( + draw_sobol_normal_samples(d=base_function.dim, n=hv_n_w, **tkwargs) * scaled_std_dev +) +mvar_hv = MVaRHV( + alpha=alpha, + eval_problem=eval_problem, + ref_point=torch.tensor(mvar_ref_point, **tkwargs), + n_w=hv_n_w, + perturbation_set=perturbations, +) +torch.random.set_rng_state(old_state) + + +# In[10]: + + +try: + all_mvar_hvs = torch.tensor([mvar_hv(X)], dtype=tkwargs["dtype"]) +except RuntimeError: + # Try to feed them one by one. This helps with memory. + initial_mvar_hv = 0.0 + for j in range(X.shape[0]): + initial_mvar_hv = mvar_hv(X[j : j + 1]) + all_mvar_hvs = torch.tensor([initial_mvar_hv], dtype=tkwargs["dtype"]) + + +# ## Run BO with MARS + +# In[11]: + + +import gc +import gpytorch.settings as gpt_settings +from time import time +from botorch.fit import fit_gpytorch_mll +from botorch.sampling.normal import SobolQMCNormalSampler +from botorch.optim.optimize import optimize_acqf + +start = time() +for i in range(iterations): + if verbose: + print( + f"Starting iteration {i}, " + f"time: {time()-start}, current MVaR HV: {all_mvar_hvs[-1]}." + ) + + # Generate the perturbations for evaluation + perturbation_set = ( + draw_sobol_normal_samples(d=base_function.dim, n=n_w, **tkwargs) + * scaled_std_dev + ) + # Fit the model. + mll, model = initialize_model( + train_x=X, train_y=Y, perturbation_set=perturbation_set + ) + fit_gpytorch_mll(mll) + + with gpt_settings.cholesky_max_tries(6): + # Construct the acqf. + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([mc_samples])) + acq_func = get_MARS_NEI( + model=model, + n_w=n_w, + X_baseline=X, + sampler=sampler, + mvar_ref_point=mvar_ref_point, + ) + + # Optimize the acqf. + while options["batch_limit"] >= 1: + # Try to get around OOM by reducing batch_limit. + try: + torch.cuda.empty_cache() + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=standard_bounds, + q=1, + **optimization_kwargs, + ) + torch.cuda.empty_cache() + break + except RuntimeError as e: + if options["batch_limit"] > 1: + print( + "Got a RuntimeError in `optimize_acqf`. " + "Trying with reduced `batch_limit`." + ) + options["batch_limit"] //= 2 + continue + else: + raise e + # free memory + del acq_func, mll, model + gc.collect() + torch.cuda.empty_cache() + + # Get the new observations and update the data. + new_y = eval_problem(candidates) + X = torch.cat([X, candidates], dim=0) + Y = torch.cat([Y, new_y], dim=0) + new_mvar_hv = mvar_hv(candidates) + all_mvar_hvs = torch.cat( + [all_mvar_hvs, torch.tensor([new_mvar_hv], dtype=tkwargs["dtype"])], dim=0 + ) + + +# ## Evaluate Results + +# First we evaluate the hypervolume dominated by the MvaR frontier and bounded from below by the reference point. A larger hypervolume means a better MVaR frontier. + +# In[12]: + + +import matplotlib.pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') +plt.plot(torch.arange(all_mvar_hvs.shape[0]), all_mvar_hvs) +plt.ylabel("MVaR HV") +plt.xlabel("BO Iterations") + + +# Next, we plot the mvar frontier to see the possible probabilistic lower bounds. For each point $\mathbf z$ in the MVaR set, there is a previously evaluated design that will be at least as good as $\mathbf z$ with probability $\alpha$. + +# In[15]: + + +from botorch.utils.multi_objective.pareto import is_non_dominated + +# Evaluate true MVaR +# Perturb X +perturbed_X = mvar_hv.perturbation(X) +# Compute objectives at perturbed points +true_Y_under_noise = eval_problem(perturbed_X) +# calculate the MVaR frontier for each point X +mvar_points = mvar_hv.mvar(true_Y_under_noise) +# calculate the pareto frontier over the union of individual MVaR frontiers for each design +mvar_frontier = mvar_points[is_non_dominated(mvar_points)].cpu() + + +# In[16]: + + +plt.plot( + mvar_frontier[:, 0], mvar_frontier[:, 1], ".", alpha=0.4, label="MVaR Frontier" +) +plt.xlabel("Objective 1") +plt.ylabel("Objective 2") +plt.legend() + + +# Finally, we can plot the MVaR frontier for each evaluated design. Clearly some designs are far more robust than others under input noise. + +# In[18]: + + +for i, y in enumerate(true_Y_under_noise.view(X.shape[0], hv_n_w, -1).cpu()): + plt.plot(y[:, 0], y[:, 1], ".", color=f"C{i}", label=f"x_{i}", alpha=0.3) +plt.xlabel("Objective 1") +plt.ylabel("Objective 2") +plt.legend() + + +# In[ ]: + + + + diff --git a/website-old/static/files/saasbo.ipynb b/website-old/static/files/saasbo.ipynb new file mode 100644 index 0000000000..5909f22dcf --- /dev/null +++ b/website-old/static/files/saasbo.ipynb @@ -0,0 +1,791 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "501041cc-0473-4971-bff9-fd6e92e1eae4", + "showInput": false + }, + "source": [ + "## High-Dimensional sample-efficient Bayesian Optimization with SAASBO\n", + "\n", + "This tutorial shows how to use the Sparse Axis-Aligned Subspace Bayesian Optimization (SAASBO) \n", + "method for high-dimensional Bayesian optimization [1]. SAASBO places strong priors on the \n", + "inverse lengthscales to avoid overfitting in high-dimensional spaces. Specifically, SAASBO \n", + "uses a hierarchical sparsity prior consisting of a global shrinkage parameter \n", + "$\\tau \\sim \\mathcal{HC}(\\beta)$ and inverse lengthscales $\\rho_d \\sim \\mathcal{HC}(\\tau)$ \n", + "for $d=1, \\ldots, D$, where $\\mathcal{HC}$ is the half-Cauchy distribution. \n", + "While half-Cauchy priors favor values near zero they also have heavy tails, which allows the \n", + "inverse lengthscales of the most important parameters to escape zero. To perform inference in the \n", + "SAAS model we use Hamiltonian Monte Carlo (HMC) as we found that to outperform MAP inference.\n", + "\n", + "We find that SAASBO performs well on problems with hundreds of dimensions. As we rely on HMC \n", + "and in particular the No-U-Turn-Sampler (NUTS) for inference, the overhead of SAASBO scales \n", + "cubically with the number of datapoints. Depending on the problem, using more than a few hundred\n", + "evaluations may not be feasible as SAASBO is designed for problems with a limited evaluation budget.\n", + "\n", + "In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one. See [here](https://ax.dev/tutorials/saasbo.html) for a SAASBO tutorial in Ax, which uses the Log Noisy Expected Improvement acquisition function. Therefore, this tutorial shows a minimal illustrative example of how to use SAASBO with only BoTorch. To customize the acquisition function used with SAASBO in Ax, see the [custom acquisition tutorial](./custom_acquisition), where adding `\\\"surrogate\\\": Surrogate(SaasFullyBayesianSingleTaskGP),` to the `model_kwargs` of `BOTORCH_MODULAR` step is sufficient to enable the SAAS model.\n", + "\n", + "[1]: [D. Eriksson, M. Jankowiak. High-Dimensional Bayesian Optimization with Sparse Axis-Aligned Subspaces. Proceedings of the Thirty-Seventh Conference on Uncertainty in Artificial Intelligence, 2021.](https://proceedings.mlr.press/v161/eriksson21a.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "code_folding": [], + "customOutput": null, + "executionStartTime": 1668653404823, + "executionStopTime": 1668653404909, + "hidden_ranges": [], + "originalKey": "26933c08-82d6-439d-9fcb-6e358b080ab6", + "requestMsgId": "1806f0c7-d668-4248-a390-14add9bcb451" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import torch\n", + "from torch.quasirandom import SobolEngine\n", + "\n", + "from botorch import fit_fully_bayesian_model_nuts\n", + "from botorch.acquisition.logei import qLogExpectedImprovement\n", + "from botorch.models.fully_bayesian import SaasFullyBayesianSingleTaskGP\n", + "from botorch.models.transforms import Standardize\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.test_functions import Branin\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653405125, + "executionStopTime": 1668653405130, + "hidden_ranges": [], + "originalKey": "f1e3c7f0-1afc-42e2-af59-5f5fae755ce5", + "requestMsgId": "068ddee5-939e-4f5b-8210-2f6a490f6c4e", + "showInput": true + }, + "outputs": [], + "source": [ + "tkwargs = {\n", + " \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", + " \"dtype\": torch.double,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "08a3d790-52a5-4821-af21-1040f1a037f0", + "showInput": false + }, + "source": [ + "The time to fit the SAAS model can be decreased by lowering\n", + "`WARMUP_STEPS` and `NUM_SAMPLES`. \n", + "\n", + "We recommend using 512 warmup steps and 256 samples when\n", + "possible and to not use fewer than 256 warmup steps and 128 samples. By default, we only\n", + "keep each 16th sample which with 256 samples results in 32 hyperparameter samples.\n", + "\n", + "To make this tutorial run faster we use 256 warmup steps and 128 samples. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653405353, + "executionStopTime": 1668653405445, + "originalKey": "363224de-347c-46a7-9c84-970cbb8e825d", + "requestMsgId": "09e1ff1f-9c11-4053-8123-08aa3397dfc1", + "showInput": true + }, + "outputs": [], + "source": [ + "WARMUP_STEPS = 256 if not SMOKE_TEST else 32\n", + "NUM_SAMPLES = 128 if not SMOKE_TEST else 16\n", + "THINNING = 16" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "af8beafd-352c-421d-8797-7660ddfa39f3", + "showInput": false + }, + "source": [ + "## Simple model fitting\n", + "We generate a simple function that only depends on the first parameter and show that the SAAS\n", + "model sets all other lengthscales to large values." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653405681, + "executionStopTime": 1668653405771, + "hidden_ranges": [], + "originalKey": "f506aa6b-904c-4a7e-8a38-0443e983df06", + "requestMsgId": "a6b6bfcd-c30c-4339-a342-02dd398a8274", + "showInput": true + }, + "outputs": [], + "source": [ + "train_X = torch.rand(10, 4, **tkwargs)\n", + "test_X = torch.rand(5, 4, **tkwargs)\n", + "train_Y = torch.sin(train_X[:, :1])\n", + "test_Y = torch.sin(test_X[:, :1])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "cc9314b1-f255-4f7d-9f6d-eb349b34805e", + "showInput": false + }, + "source": [ + "By default, we infer the unknown noise variance in the data. You can also pass in a known \n", + "noise variance (`train_Yvar`) for each observation, which may be useful in cases where you for example\n", + "know that the problem is noise-free and can then set the noise variance to a small value such as `1e-6`.\n", + "\n", + "In this case you can construct a model as follows:\n", + "```\n", + "gp = SaasFullyBayesianSingleTaskGP(train_X=train_X, train_Y=train_Y, train_Yvar=torch.full_like(train_Y, 1e-6))\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653406085, + "executionStopTime": 1668653471282, + "hidden_ranges": [], + "originalKey": "148855fb-cf0c-4fc5-8431-06a5e61c5da5", + "requestMsgId": "0c13f0b6-28b9-43ea-8d1b-00871e5e4f02", + "showInput": true + }, + "outputs": [], + "source": [ + "gp = SaasFullyBayesianSingleTaskGP(\n", + " train_X=train_X,\n", + " train_Y=train_Y,\n", + " outcome_transform=Standardize(m=1)\n", + ")\n", + "fit_fully_bayesian_model_nuts(\n", + " gp,\n", + " warmup_steps=WARMUP_STEPS,\n", + " num_samples=NUM_SAMPLES,\n", + " thinning=THINNING,\n", + " disable_progbar=True,\n", + ")\n", + "with torch.no_grad():\n", + " posterior = gp.posterior(test_X)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "5f4fa168-2662-499b-ac82-3ab122dfe2ad", + "showInput": false + }, + "source": [ + "Computing the median lengthscales over the MCMC dimensions makes it clear that the first feature has the smallest lengthscale\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653471605, + "executionStopTime": 1668653471693, + "hidden_ranges": [], + "originalKey": "44a1f7c0-9649-4d89-8226-0405fdf88518", + "requestMsgId": "e815926b-5a2d-4b78-8b89-3c0720e45592", + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tensor([ 2.6688, 19.3581, 30.6755, 26.3881], dtype=torch.float64)\n" + ] + } + ], + "source": [ + "print(gp.median_lengthscale.detach())" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "cf15a6ca-3377-40d1-9821-fad8f6600657", + "showInput": false + }, + "source": [ + "### Make predictions with the model\n", + "\n", + "In the next cell we show how to make predictions with the SAAS model. You compute the mean\n", + "and variance for test points just like for any other BoTorch posteriors. Note that the mean \n", + "and posterior will have an extra batch dimension at -3 that corresponds to the number of MCMC\n", + "samples (which is 8 in this tutorial)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653471916, + "executionStopTime": 1668653472023, + "hidden_ranges": [], + "originalKey": "898039a4-6ec8-46bd-a583-5a1614a3ccf6", + "requestMsgId": "4328636f-fb02-44ee-8c48-842c3845297f", + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([8, 5, 1])\n", + "torch.Size([8, 5, 1])\n" + ] + } + ], + "source": [ + "print(posterior.mean.shape)\n", + "print(posterior.variance.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "02b33f7f-4f31-432a-bac7-8cad1831e9a1", + "showInput": false + }, + "source": [ + "We also provide several convenience methods for computing different statistics over the MCMC samples:\n", + "```\n", + "mixture_mean = posterior.mixture_mean\n", + "mixture_variance = posterior.mixture_variance\n", + "mixture_quantile = posterior.quantile(q=0.95)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653472240, + "executionStopTime": 1668653472326, + "hidden_ranges": [], + "originalKey": "b387d057-a497-401b-bfc2-ab427669c451", + "requestMsgId": "64e0ee73-6ffd-4ad5-b9b2-bd9ea4e637ee", + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ground truth: tensor([0.1842, 0.3531, 0.6900, 0.2710, 0.6056], dtype=torch.float64)\n", + "Mixture mean: tensor([0.1837, 0.3490, 0.6888, 0.2658, 0.6045], dtype=torch.float64)\n" + ] + } + ], + "source": [ + "print(f\"Ground truth: {test_Y.squeeze(-1)}\")\n", + "print(f\"Mixture mean: {posterior.mixture_mean.squeeze(-1)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "executionStartTime": 1644277314184, + "executionStopTime": 1644277314189, + "hidden_ranges": [], + "originalKey": "d9bec8be-2acd-40b8-aebb-612b62bbdfc3", + "requestMsgId": "d9bec8be-2acd-40b8-aebb-612b62bbdfc3", + "showInput": false + }, + "source": [ + "## Optimize Branin embedded in a 30D space\n", + "We take the standard 2D Branin problem and embed it in a 30D space. In particular,\n", + "we let dimensions 0 and 1 correspond to the true dimensions. We will show that\n", + "SAASBO is able to identify the important dimensions and efficiently optimize this function.\n", + "We work with the domain $[0, 1]^d$ and unnormalize the inputs to the true domain of Branin \n", + "before evaluating the function." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653472540, + "executionStopTime": 1668653472545, + "hidden_ranges": [], + "originalKey": "15baa08e-ca35-4da7-a495-c63fe5d5779d", + "requestMsgId": "6c3f8d91-9139-4c07-986f-77629b1887e5", + "showInput": true + }, + "outputs": [], + "source": [ + "branin = Branin().to(**tkwargs)\n", + "\n", + "\n", + "def branin_emb(x):\n", + " \"\"\"x is assumed to be in [0, 1]^d\"\"\"\n", + " lb, ub = branin.bounds\n", + " return branin(lb + (ub - lb) * x[..., :2])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653472768, + "executionStopTime": 1668653472776, + "hidden_ranges": [], + "originalKey": "98b6936b-2f06-4d1f-82c0-2f1bd660d0b2", + "requestMsgId": "1b5baaf2-e690-4b4d-9011-b6124e083410", + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using a total of 50 function evaluations\n" + ] + } + ], + "source": [ + "DIM = 30 if not SMOKE_TEST else 2\n", + "\n", + "# Evaluation budget\n", + "N_INIT = 10\n", + "N_ITERATIONS = 8 if not SMOKE_TEST else 1\n", + "BATCH_SIZE = 5 if not SMOKE_TEST else 1\n", + "print(f\"Using a total of {N_INIT + BATCH_SIZE * N_ITERATIONS} function evaluations\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "27fd793f-18ee-49cb-9aa5-c8cd78b0b807", + "showInput": false + }, + "source": [ + "### Run the optimization\n", + "We use 10 initial Sobol points followed by 8 iterations of BO using a batch size of 5, \n", + "which results in a total of 50 function evaluations. As our goal is to minimize Branin, we flip\n", + "the sign of the function values before fitting the SAAS model as the BoTorch acquisition\n", + "functions assume maximization." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668653473096, + "executionStopTime": 1668655621405, + "hidden_ranges": [], + "originalKey": "269287e0-500f-474d-891a-5439487e9a77", + "requestMsgId": "5117b535-1fe7-40be-9f68-361db9d9b51b", + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best initial point: 5.322\n", + "3) New best: 2.028 @ [1.000, 0.181]\n", + "4) New best: 2.019 @ [1.000, 0.219]\n", + "5) New best: 0.866 @ [0.129, 0.762]\n", + "6) New best: 0.415 @ [0.121, 0.831]\n", + "8) New best: 0.398 @ [0.542, 0.153]\n" + ] + } + ], + "source": [ + "X = SobolEngine(dimension=DIM, scramble=True, seed=0).draw(N_INIT).to(**tkwargs)\n", + "Y = branin_emb(X).unsqueeze(-1)\n", + "print(f\"Best initial point: {Y.min().item():.3f}\")\n", + "\n", + "for i in range(N_ITERATIONS):\n", + " train_Y = -1 * Y # Flip the sign since we want to minimize f(x)\n", + " gp = SaasFullyBayesianSingleTaskGP(\n", + " train_X=X,\n", + " train_Y=train_Y,\n", + " train_Yvar=torch.full_like(train_Y, 1e-6),\n", + " outcome_transform=Standardize(m=1),\n", + " )\n", + " fit_fully_bayesian_model_nuts(\n", + " gp,\n", + " warmup_steps=WARMUP_STEPS,\n", + " num_samples=NUM_SAMPLES,\n", + " thinning=THINNING,\n", + " disable_progbar=True,\n", + " )\n", + "\n", + " EI = qLogExpectedImprovement(model=gp, best_f=train_Y.max())\n", + " candidates, acq_values = optimize_acqf(\n", + " EI,\n", + " bounds=torch.cat((torch.zeros(1, DIM), torch.ones(1, DIM))).to(**tkwargs),\n", + " q=BATCH_SIZE,\n", + " num_restarts=10,\n", + " raw_samples=1024,\n", + " )\n", + "\n", + " Y_next = torch.cat([branin_emb(x).unsqueeze(-1) for x in candidates]).unsqueeze(-1)\n", + " if Y_next.min() < Y.min():\n", + " ind_best = Y_next.argmin()\n", + " x0, x1 = candidates[ind_best, :2].tolist()\n", + " print(\n", + " f\"{i + 1}) New best: {Y_next[ind_best].item():.3f} @ \"\n", + " f\"[{x0:.3f}, {x1:.3f}]\"\n", + " )\n", + " X = torch.cat((X, candidates))\n", + " Y = torch.cat((Y, Y_next))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "a9704a99-0712-40bb-a263-6798e0925291", + "showInput": false + }, + "source": [ + "## Plot the results\n", + "\n", + "We can see that we were able to get close to the global optimium of $\\approx 0.398$ after 50 function evaluations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668655621761, + "executionStopTime": 1668655621936, + "hidden_ranges": [], + "originalKey": "fd0d7aa7-8d55-4942-adc2-de356666ac84", + "requestMsgId": "4024717d-fc5c-4939-90ce-fb24b3e06ea3", + "showInput": true + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAsEAAAI5CAYAAABAcOcAAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAACJA0lEQVR4nOzddXhb1/3H8bckM2PYcZjBYWi44TIzbl15a7u16/Zbt3XQrlu3deu6rsyYpmlKadIwcxyOE8cBO3FiZlsG6feHbdWOZZJBlvR5PU+fxvde3XvkI8sfH33vOYaEhAQrIiIiIiIexOjsBoiIiIiItDeFYBERERHxOArBIiIiIuJxFIJFRERExOMoBIuIiIiIx1EIFhERERGPoxAsIiIiIh5HIVhEREREPI5CsIiIiIh4HIVgEREREfE4Xs5ugIiIu3r3g0/ZtmMXEeHh/PF3Tzq7OSIiUoNCsIi0uxdefIVjx5Ps7jMajfj5+RIZEU5sz55MGDeaPr1j272NrSEiPIzu3boSGhri7KbYZGZm8ds/Pmd3n8lkIsDfn+DgIGJ79mBA/76MGjkcb2/vdm9nc508lcyGTVtJOnGS7JxcrBYLgYEB9OjRnTGjRjJuTBxGY/0ffh48nMDmrds5efI0BQWF+Pn50aVLJ8aOjuOiSeMbfKyIuCZDQkKC1dmNEBHPUh2Cvb296BQdXWtfeXk5uXl5lJSYbdumTJ7AjdddhcFgcEJr3UvNEBwaGkJQYKBtX3l5OUXFxeTnF9i2BQUFsmDebGZMneyU9jbGarWy5MtvWL12I1arFYPBQGhoCEaDkZzcXCwWCwB9esdy/z13EhAQUOccH326hI2btwLg4+NNeFgYhUVFFBQU2h774H0/ws/Xt52fnYi0JY0Ei4jTdIqO5tdPPGJ336nTKXz17XIOHznKxs3biO0Zw+SJ49q9je5s9sxpzJoxtc72/IICDh85xuq1G0hOOcOixUs5duw4d99xMyaTySltrc/a9ZtYtWYDAJMnjuPyS+YTHBwEQHFJCd8u+57V6zaSdOIUny7+kjtvu7HW41etWc/GzVsxGAxcunAus6ZPwcfHB4ADh47w7vufkHTiFO9/uIgf33WrE56hiLQVfb4jIh1SbM8e/OTu2wkOqgw0O3btcXaTPEZwUBDjx47iicceYv6cWQDE7zvAos+/dHbTarFYLCz/fg0AQ4cM4pYbr7UFYAB/Pz+uueoyBg8aAMDu+H21PmEoKTHz3ferAZg2ZRLz58yyBWCAYUMGcfON1wCwZ+9+Tpw83W7PTUTankaCRaTD8vHxJqZHNw4dOUpubl6d/dU3ng0eNICf3H07S778hvh9B8jPL+CnD9zDgP59bcdmZGSydsNmjh47TkZmJmVl5fj5+dK1SxfGjBrBlMkT6oxylpjN/PyXvwXgR3feQtyIYWzcvI2t23eSlpZBWXk5EeFhjBg+lPlzZ+Hv52e3fRfeGLd2w2YWLV6Kj48P//zrH8nJyWX5yjUcPJxAbm4e3l5edOvWhelTJjNm9Mg2+M42jdFo5LJL5pFfUMCmLdvZsGkrkyaMJbZnjNPaVFNhUREDB/ajqLCI6VMm1Xvc4IH9OXzkKBUVFeTk5NClS2cADhw8TFFRMQCzZ02z+9iRw4cSFRlBRmYW23fupnevnm30bESkvSkEi0iHVlxSAlU3mTXksyVfsmnLdiLCw+kUHVUr0B44dITX33qfsrIyDAYDwcFBhIaEkJ2Ty/GkExxPOsGuPft46L4f4ePzw01g3l4/vEVarVZef+t99u4/SEhIMCEhwWTn5JKWnsHK1es4eiyRXzzyYJPKBWqe99y587zw0qsUFhYRFRlBWGgImVnZHE86yfGkk+Tm5dktWWhPV1y6gJ274zGbS1mxai333HVbkx536nQKH3z8WbOvd8uN1xLbs0ejxwUHBXHXbTc1epzF+sOtLwGBP9QEH044BkCn6CgiwsPtPtZgMDCgf18yMrM4fORoE5+BiLgChWAR6bDOnTvPqdMpAIweNaLe43Jz80g6cZIHfnIXQ4cMqrWvuKSEd97/mLKyMjp3iuYnd99mGwmsqKhg1ZoNLP16GceTTvD96rVcMn+O7bE1ZwRYs3YjWTk5PPLQvfTv1weAsrIyPlvyFRs3b+N08hn27N3P2NFxjT4vo7HyBj+r1cKb735E3969uOG6KwkJDgYgPSOT/7z8OhmZWXy7fBVTL5ro1BkaAgMDGDF8KDt27iEhIRGLxdKk2RLMZjNnzqY2+3pms7kJRzVNaWkpW7fvBKBP71627zFA6rnzAHTt2rnBc3Tr2gWAjMwsysrKXGK2DBFpnEKwiHQoFRUV5OXnc/jIMb7+djkWi4VRccOZOH5svY85m3qOBXMvrhOAARKOJtrqQK+6fKEtAFM1Jdjc2TM4ePgIicdPsHvPvlohuOZsFCdOneanD9xjC8AA3t7eXH3FpWzdvovy8nKOHjvepBAMlectKyvHbDbXueEsOiqSubNn8uEniykuLiY55azTp4nr0yuWHTv3UFxSQlpaeq3vY30G9O/LSy/Yn46tLVWWPeRy/MRJlq9cw7lzaXTp0onbb7m+1nFZ2dkAhIWGNni+6inurFYrWVk5dO4c3eDxIuIaFIJFxGnOnE3lwUd+We/+6KhIbr/lBsaPHdXo9Ghjx9gPn3EjhvGv5/9MYWERAQH+do+J7dmDxOMnyMjMqvf8PWN6MHBAvzrbfX196NwpmjNnU8nOyW2wjfbMmjHVbglFz5jutn9n5+QAzg3BNec6zi8opItTW1O/Xzz5O1sJDUDfPr244dormTRhbJ0R3OoRZ98aN8PZU3N/SSuOUouIcykEi4jT2JsnGMBcWkpOdg7pGZl8/sXXtpFePz/787R6e3vRuVP9o3NGo7HWrAEX8vWpPG95eXm9x1R/JG6PX9UNcWVlZfUeU+95u9k/b82b7MrK6m9Xe6lZx9ya5QqtrVu3LhQXl1BcXEx2Ti6nk1MwGo2Eh4UyfNiQWsdWf19NXg3XcXvVeO5l5c3vYxHpmBSCRcRpGponuLy8nAOHjvD5F1+zcvU6Dh85ymM/vd9uEA4ICGhwpLiiooKt23cSv+8gZ1PPUVhY1OzAGh5W/0fmxuprO7D0UHg9H8XXfD5Wq/PXNKq5gIZvPX+MdASP/fR+27/z8wtYv3EL332/mmOJScyaMZVrrrzUtt/b24vS0jLKyysaPGfNP458VA8s4jY0T7CIdEheXl7EjRjGzx66Fx8fb86cTeXb5SvtHmsy1j+SV1BYyN/++R8+/ORzDh1OICcnl8AAf7p06UT3bl3p3q1rg6PE1dpqtTpXWQWvZqlHVESEU9vSVMHBQVyyYA7XXnUZAKvXbuDoseO2/dUj+I2NbNcsgfC7YBo8EXFdGgkWkQ4tMiKcwQMHsHf/QeL37ufqKy5p1uM//+JrklPOAjBj2kXMnzvLtgBHtW+WfV9vwJZKBw4eBiAsNITwRqarq9bWU6Q11UWTxrPky28oKytnz979tvmjoyIjyMvLb7SWu3q/0WhsdKo+EXEdCsEi0uFV35SVY2fBjIZUVFSwO34/AIMHDeC6qy+3e1xBYWErtNJ9nTufxsnTyQDEjRze5Me19RRpOTm5nEk9R35+AWNHj6xVu1uTl5cXAQEB5ObmkV/wQ1lHt65dSDpxirOp5xq8TkrVH1FdOneq9xoi4nr00ywiHV71SFxQjYUOmiK/oNBW+9uvb2+7x1gsFg5pEYR6WSwWPvh4MVarFR8fb2ZOu6jJj23rKdIOJxzl/Y8qR5ojwsNqrRBYU2lpGQUFlX/o1PwUYMjggWzcvI3MzCzS0jPoFB1V57EVFRUkHEuEqmWURcR9qCZYRDq07OwcjlSt7NW/npBTH98aq78VFtgf7V2xai2ZNaZGc2SGB3dVVlbGux98StKJkwBcfcWlREVFOrtZNkMGD7It2rF67YZ6j9u2YxcVFZU3v9X8Y2jIoAG2Txm+X7XW7mO3bt9FXl4+RqOxwbmqRcT1KASLSIdksVg4nHCUF19+vWqVLi8WzL24Wefw9/ene7euAGzZtpPTyWds+3Jy8/h08VK+W7GKBfN+OG9i0olWfBZt41hiEg8/9isefuxXbNu+q9XPbzaXsmXbDp77+4vs2LUHgGlTJjH1oomtfq2WCA0JZkbVyPT+g4d578NPya1RMlNWXs7KNetZ/MVXUFXOEDdimG2/t7c3l18yH4DNW3fw7XcrKS2t/CPIYrGwc3c8ny2pfOz0KZO0SIaIm1E5hIg4TVp6Os/89YU628vLy8nOyaW0tBSq7vK/67ab6NK5U7Ovcfml8/nfa29TXFLCX//xIhHhYVisVnJycjGZTNxy47X07R3LdytWY7FY+N9r7xARHsZPH7yH8LCOeROU1WrFYrEAYGnB9Gkr16xn6wUhOr+ggPz8Atu0bH5+vtxw7ZWMHzu6ha1uG1detoCioiK2bt9l+y80JBhvb2+ysnNs36dO0VHcf8+ddRYmmTh+DOnpGSxfuYZvvvue71evIzwslIKCQgqLigCIGzmMq5p5Q6aIdHwKwSLiNGVl5XZvnPIymQgIDKBP71iGDRnExPFj8Pe3v9pbY4YNGcRPH7iH5SvXcOpUMrm5eQSHBDNuzChmzZhKTI9uAFx71WV8v2ot+QWFeHt7u8wNUCaT4x/o5ebm1Ro5pep7HxISTPeuXRgyeBATxo2ud6W9jsBkMnHbzdczccJYtmzdwYmTp8nOycVSWERgQADdu3Vh5IhhTBw/Fh8f+3P8XnbJPAYN7M+6jZtJOnGKzMws/P39GTxoAJMmjGXMqJHt/rxEpO0ZEhISnD8Lu4iINEt2dg6/efpZ7vvxHXVWQhMRkca5xlDHBXJy8/h+1VqOJBwjKzsbPz8/OkdHM23qJEaNHO4yk8+LiDjqxKnTAHTt0tnZTRERcUkuF4LPp6Xz9xf+S2lZGVMmTyCmezeKzWa279jNG29/wJTJE7np+quc3UwRkTa1YdNWunfr2qFmaxARcSUuF4KXf7+awqIibrzuqlp3Kl80cRx/fPbvbNy8lTmzpukXg4i4rfz8Avr16c3gQf2d3RQREZflclOkZVTN59m3T69a2729vekZU7nMZkZWtlPaJiLSHoKDg7hkwRz69O7VhKNFRMQelwvBXbt0ASAtLaPOvsysLIxGI106aS5HEREREamfy5VDzJ09g/0HDvLZki8xGAzExsZgLjGzaes2TiefYc7FMwgLC3V2M0VERESkA3PJKdKyc3J4+72PSTz+w8pO3t5eXHHpAmZOn9Lk8wT7+zS4/5m/hPGf/9adLD/p6Cn8/Fzu2yYiIiLicvKLS9vkvC43EpyRkcnLr71Nfn4Bly2cR4/uXSkpMbN3/0E+W/IVGZlZXHf15U0+X3TXHra15y8UHGp/qrVO3WJwcN5+cRKLxUJ6akqD/S3uQ/3tWdTfnkX97VksFgv5SUltcm6XC8Hvf/QZ59PSefzRB4ntGWPbPnZMHD4f+rB2/Sb69+1D3MhhDZ6nmtForPeH6ILVNWs+Cv3cuaaG+lvcj/rbs6i/PYv6W1rKpV49JWYziUkniIwIrxWAq40YXrlq0uGEo61yvfrW3Khail5EREREXJRLheCysjKsVivl5eX295eWAdS7v7nq+wPTqnJgEREREZfmUiE4OCiITtFR5OTmcfTY8Tr7d8fvAztzCDuqvhCskWARERER1+ZyNcHXXn05r7z+Di+/9jZTJk+ge7eumM1m9h04xJGEY/TpHcuEcWNa5VoKwSIiIiLuyeVC8NDBA3n80QdZuXo9u/fsZe36TXh5edEpOoorLp3PzOlTMNV/R1uzqCZYRERExD25XAgGiOnRnbtuv6nNr6OaYBERERH35FI1we1N5RAiIiIi7kkhuAEqhxARERFxTwrBDVA5hIiIiIh7UghugMohRERERNyTS94Y114UgkVEOh6z2UxeXh4lJSUAWK1WSs2lpKSkYKivjk3chvrb9fj5+RESEoKvr6+zm1KLQnADVBMsItKx5Obmkp+fT2RkJFFRURgMBqxWK2VlpXh7+ygUeQD1t2uxWq0UFxeTnp5OcHAwoaGhzm6SjUJwA1QTLCLScZjNZvLz8+nevbvCj4iLMBgMBAQE4O/vz5kzZ/Dz8+swI8KqCW6AyiFERDqOvLw8IiMjFYBFXJDBYCAiIoK8vDxnN8VGIbgBKocQEek4SkpK8PPzc3YzRMRB/v7+tlr+jkAhuAEqhxAR6Vg0Ciziujraz69CcANUDiEiIiLinhSCG6AQLCIiIuKeFIIboJpgEREREfekENwA1QSLiIiIuCeF4AaoHEJERETEPWmxjAaoHEJERNzVoSMJLF+xikOHE0hLS6fEbMbHx4eIiHAG9OvLjGlTmDxpfKN39H+3YiV/f+ElAGZMm8L/PfnzZrflscf/j/0HDwHwzB+eYtzY0W36HPbuO8AvnnzK7jm9vbwIDgmmV2xPLpo0gfnzZuPj7d1gO8rKyli1Zh3bd+zmWOJxcnLzqCgvJzgkmN69Ypk4fixzZs8kMCCgyd8TaXsKwQ1QOYSIiLgbq9XKS/97naVffQtAcFAQPWN6EBwSTH5+AaeTU1i7fiNr129k8sTxPPXrx/Hyqj8uLFu+0vbvTVu2kZefT0hwcJPbk5JyxhaAAZatWNloCLZarbz08mss/XpZi5/D+HFj8DKZbF8XFReTmnqO3Xv2snvPXr765jv+9pc/EFbPcr974vfxt3++SHp6BgBdu3Rm0MD+mEwm0tMz2LU7nl2743nvw0/45c9/xvhxY5r8vZG2pRDcAJVDiIiIu/ny62Us/epbfH19eOj+n3DxzGl41xjpNJvNfPnNd7z+5rts3rqd9z74hLvuuMXuuU6dTubQ4QQCAgKI7dmDw0eOsnrNeq68/JImt6c6RI8ZHceu3fFs3bqDvLw8QkJC6n3MN8tWsPTrZa3yHH71+KMEBQXW2b5j527+8MzfOHnqNK+98S6PP/ZwnWM2bt7Kn559noqKCiZNHMeP7ryN2J4xtY5JS0vnnfc/ZsXK1fzm93/mpw/ey6UL5zX5+yNtRzXBDVAIFhERd/P1t8sBuPaqK5g/9+Ja4RHA19eX666+gmuuugyAL776FrPZbPdcy777HoDx40Yza8Y0AL5bsarJbamoqOD71WsBuOaqy+nfrw9l5eWsXL2uwcd9+93KVnsO9Rk3djQ3XHsVVIXdC6WeO8/f/v5vKioquPLyS/jDb39dJwADdOoUzeOPPczdd9yK1Wrl5Vff5OSp081qi7QNheAGqCZYRETczZmzqQD0iu3Z4HHXX3sVzz3zNO+88V98fX3r7C8rK7OF1ZnTpzJtymSMRiPHk06QeDypSW3Zsm0H2dk5hIWGMDpuBDOmTQFg+ferG3zc2dRzrfIcGtOvb28AioqKKC0trbXvw48/o6i4mN69YrnvnrsaPddNN1zD6LiRlJaW8t9X3mh2W6T1KQQ3QDXBIiLibvz9/QA4ePhIg8eFhYYyOm5EvbWwm7duJzcvj5CQYMaNGUVERDij4kbABXXCDak+bsb0qZhMJmbNmIbRaCTpxEkSjibW/xz8Wuc5NCYnNw+A0JAQfHx8bNtLS0tZtabyD4BrrrocU42a4obccP3VUFVHfP58mkNtktajmuAGqBxCRMQ13H+/F4cONeFAFzRsGLz2Wuudb+zoUaxeu56lX32Ln68v1117ZbNuZKtWHWBnTp9qK0eYe/FMdu2OZ83aDdx7z10NzqqQkZHJzl17AJhz8UwAoqIiiRs5nN179rL8+1UMHNDP7mNHjxrB2vWbWvwcGrNuwyYAplw0sdb2wwlHKSsrg6pSkKYaOXwofn5+lJSUsGfvfubPvbiVWyzNoRDcAJVDiIi4hoMHDWzb1vBUXlLpR3feSvy+/WRlZfPxos9Z9PlShgwayIgRQxkxbChDBg/Cz6/h0oG0tHT2xO+DGgEW4KLJEwgICCC/oIBNm7cyc/rUes/x3YpVWCwWesXGMKB/X9v2ebNnsXvPXtas28B999xVawS22h233cS+A4da9BzqU1hYSMqZsyxavJSdu/YQ2zOGO269qdYxKSlnAQgPDyM8LKzJ5zaZTPTqGcORo8dIOXPGofZJ61EIboDKIURExN106hTNS/96ntfeeId1GzZRUVHB/oOH2H/wEB+wCG8vL0aMGMbsWdOZWVWmcKFlK1ZisViI7RlTa7TW19eX6VMns2z5Sr5bsareEGy1Wlm+srLut2aIpkaQLigoZOOmrcyaOa3uc4iO4qUX/sZrb77r8HOodtX1t9rdbjQaue6aK7j5huvqzB6Rn58PVVOzNVdwcFDVOQqa/VhpXaoJboDKIURExB1FRUbwqyce5aP3XufRn97PtCmTCQurrJstKy9n1+54nnv+X9z30KOcTU2t9ViLxcKK79eAnQALMHf2LADi9+4nLS3d7vX3xO/j3LnzGI1GLp41vda+6iAN8N339c80EdmC51DT+HFjmDxxvO2/iRPGMXzoEPz9/Vi85Ct++4dn6szmYKkaDbM4EAiqH9vYIiTS9jQS3ACFYBERcWfhYWEsnD+XhfPnApCccob4vftZuXodhw4f4eSpZH7zuz/z6n9fsC02sWt3PGnp6XYDLMCwoYPp1q0rZ8+msvz71dx2yw11jrHNDTxqJJEREXX2z509k2XLVxK/dz/nzqfRpXOnVn0ONdU3T3B5eTnfLFvBy6++yQM//QV/e/YPDB0yCKpulAPIzctr4LtrX/UIcGgD8yBL+1AIboBqgkVEXMPQoVYMBivgfqNrw4a137VienQnpkd3LrtkPp9/8RUvv/omySlnWLt+I7NnzYAaAdZisXDTbT9q8HwrVq7m1puvrzXqmZeXx6Yt2wDYsWsPcxZeVe/jrVYry79fVacmt6XPoSm8vLy44rKFnE09x+dffMVrb77DC88/C2CbDzg/v6DRkF5TRUUFp05XjirHNjK9m7Q9heAGqCZYRMQ1vPxyOd7ePvUOXkjzXX3lZSz96lvOpp7j1KlkAHJyc9mybQdU3RRmMtZfa5uZlcW582ns2buf0VVTpwGsXL2OsrIyvLy8Gpy6zFxqJj+/gBXfr+G2m2/AWN8v5WY+h+YaPWokn3/xFYePHKWiogKTycTAAf0I8PenqLiYTZu3cs1VlzfpXPF792M2l2I0Ghk1crhD7ZHWoxDcAJVDiIiIO9m0eRs7du0mMiLCbpnChUKCgzmbes42Bdr3q9ZSXl5OSEgw77/9aoNToD3x69+xJ34fy1esqhWCq0eSL1kwl4fuv6fex6elpXPb3feRll45E8WY0XGVz2HLNrbv2EVUVCS333Jjs59DcxUWFkHVyLfFYsFkMuHt7c3si2fw5dfL+PyLr1k4fw7+/v6NnuvTxV8AMGniOMLDmz6rhLQN3RjXAIVgERFxJ0cSjvLNshV88tnnpJ473+CxGRmZHE86AcDAgf0B+K4qwF48c3qDARhg/tzZULXkcEFBIQCHjxy13WTW2By5nTpF2xbfqLkU85GEYyxbvpJPP1vi0HNorvi9lVPBxfToXitI33HbTUREhJOWns7z//wPFRUVDZ7n08+WsHvPXgICArj/nrsdaou0LoXgBtT3sZrKIURExBVdc/XlhIeHYTaX8tjj/8fWbTvshrdDRxL41VN/oKy8nIED+jF2dBwHDh7mdHIKNCHAAkyZPIHgoCBKS0tZvXY9AMuWfw9Av7596Ne3T6PnmD+n8jqbtmyz3VB2zVWXERYW6tBzaA6LxcJ3K1baAvgVly2stT8kOJgnHvspvr4+rN+4mV/+3+85lni8znnS0jP4x79e4rU338XLy4vHfvoAnZtYQyxtS+UQDdBIsIiIuJOw0FD+8qff8fSf/8rZs6k89fQzhIQE061LF0JDQygtKyMl5QzpGZkADB0yiN/95peYTCZbGcOA/v3o07tXo9fy8fFh5oypfPn1Mr5bsYo5s2eybn3lCmxNXSlt8uQJBAcHkZ9fwKq167nysoWEhYbyp9//mmf++kKzn4M9z/7tn3hdsK+ouJiTp06Tk5MLwJWXX8Llly6o89gxo+N44flnefrPf2XvvgM88NNf0LlTNN26dcVkMpGensGp05W1yBER4Tz1q18wbOiQJj13aXsKwQ1QCBYREXfTp3cvXn/5X6xZt4EtW3eQdOIkJ0+dprSsDD9fXyKjIrh45nSmT7uIiePHYjAYKCoqZv3GzdCMAAuwYN5svvx6GccSj/Pd8pUUFRfj4+PDrBl1F8Cwx8fbm4tnTueLL79h+YpVXFk1Gtu7Vyyv/fcF1q7f2OTnUJ/tO3bV2ebr60N0VBTjx45hwbzZDBs6uN7H9+vbhzdeeZHVa9azees2Eo+f4MCBQ1iB0NAQxo0dzaQJ45h98Qz8/fya9LylfRgSEhI89sP9YH8fOnfvWe8dp+vWwQw7s6ksWQJXXtn27ZPWY7FYOH/mdIP9Le5D/e2ekpOTiYmJqbPdarVSVlZaNTuEpodwd+pv11bfz3F9LBYLiYmJbdIW/XZogKZIExEREXFPCsENUDmEiIiIiHtSCG6AQrCIiIiIe1IIboCmSBMRERFxTwrBDdBIsIiIiIh7UghugEKwiIiIiHtSCG5AfeUQCsEiIiIirk0huAGaIk1ERETEPSkEN0DlECIiIiLuyeWWTX7q6b+QlZ3d4DETxo3h9luub/G1FIJFRERE3JPLheAbr7sSc2mp3X3HEpNYv3EL3bt1aZVraYo0EREREffkciF46JBBdrcXl5Tw+RdfE9OjOzOmXdQq19JIsIiIiIh7cpua4C++XEZObh4333A1JpOpVc6pECwiIiLintwiBJ86ncKmLduYetFEesb0aLXzaoo0EREREffkcuUQ9iz+4mv8fH25ZMGcZj/W0miirft3QkWFRUHYxVT3c+P9Le5A/e2erFYr1kZuymhsv7gX9bfrsVqtzXpvbsv3cZcPwQcPHeF40gkWzptNUGBgsx+fnppS776sNC+g7shybnY258/kN/ta4nwN9be4H/W3eyk1l1JWZv/GaKDBfeJ+1N+uqdRcwvkzp53dDHCHEPz96nV4mUxMnTLRocdHd+2BsZ7i36J6fr6CQsLp3D3coeuJc1gsFtJTUxrsb3Ef6m/3lJKSgre3j919ZWWl9e4T13HufBq3330fAO+++T+6dO5k9zhn9vfcS64G4G/P/oGRI4Y5pQ32NPV752w+vn507t700lWLxUJ+UlKbtMWlQ/C582kcS0xidNwIQoKDHTqH0Wis95dkfffXWa3Gem+ak46tof4W96P+di8GgwGDnZs1an4kbm+/1O/8+TSWrVjJnvj9pJ47R35+AV4mE9Gdohg0cAAzp01h3NjR7dYePz9fJk8cb/t3R+7v+l6PzlKzLR2tbTUZDIYO877s0iF45+54AEaOGNom59eyySIi4o4sFgtvvfMBny35kvLycry8vOjdK5Z+fftQUlxCckoK369cw/cr1xA3Yhi//uXPCQ8Pa7Xrp2dkcPPt93DbzTdw+6032raHh4Xx9G9/1WrXEWmIS4fgQ4ePAjBoYP82Ob+mSBMREXdjtVr501+eZ8PGLfj5+nLHrTdy+aULCQjwtx1jsVjYHb+P/736JvH7DnDfw4/x4j+eo1On6FZpw6HDCa1yHpGW6Bjj0Q6oqKggOeUMYWGhDt0Q1xSaIk1ERNzNZ58vZcPGLfj4+PCXP/+eG6+/plYApqqUaOzoOP71j78wcEA/srKyee7v/2q1O/WPJBxrlfOItITLjgRnZGRhsViIjIhos2uoHEJERNxJUVExH37yGQC33XJDvauwVgsMCOBXTzzGPff/jH37D7J67Xpmz5ph23/NjbeTl5fP83/5I5ER4bzzwcfs23eQ/Px8wsLDmDRhHLffciOhoSEA7N13gF88+ZTt8e99+AnvffgJnTtF8/7br3LufBq33XVv5b63XrHd3PXqG2+zaPFSbrv5Bq69+nJef+s9tmzdTm5ePlGREcyaOY3bbr4Bk8nEocNHePeDT0g4eoyysjJ69+rF7bfeyLgxo+o8v7z8fD5f8hVbt+/kbGoqpaVlBAcHMWjgAK696vIW3/h2x4/u52zqOX50563ceP01do85fOQoP33slxgMBt578390rnrO6RkZLFq8lF2740lLS6e8ooKw0FCGDRvMDddeTb++vZvUhr/+4998v3INc2bP5InHflpnf80++f7bJXX2H0s8zmeff8n+AwfJycnF18+X2J4xzJg+lUvmz8Hb27uZ35WOw2VHgguLigDw9/drs2uoHEJERNzJhk1bKCgoxN/fj8svWdCkx3Tv1pVpUyYDsHzFqlr7fH0qZ2hITjnDTx97kvj4fcTGxjCgavT4y6+X8ejjv6agoBCAkJBgJk8cT0hI5c3sMT26M3nieMaMjmuwDT5VM0FYLBae/M3TrFu/iV6xPenerQup587zwUeLeO3Nd4nfu59fPPlbsrNzGNCvH/5+fhxJOMpTv/8zicdP1DpnZlYWDzz8cz74eBGnk1Po3asXcSOH4+vjw9ZtO3j8V7/luwueb3NNn3YRAJs2b6v3mA2bNgMwZPBAWwA+cfIU9z74KEuWfk1GRib9+/VlxLChWCwW1q7byMOPPmG7L6otffvdCh565AlWr12PwWBk+LChREdFcfjIUV56+TUe/9VvKS4ubvN2tBWXHQnu0zuWl154rk2voRAsIuJ60gvTHX5skE8Q/t7+dvdlFGU4vDhDgHcAgT72S/eyirOosFTU2R4d2Dr1tzXt238QgOHDhtYpgWjIhPFjWLVmHQcOHaGsrMw2+ld9l/9b737ARZMn8vCDP8Gnat/xpBP84smnSE45w0efLuaeu2+nd69Ynv7tr/j5L3/Dvv0HmTFtSq0b4+pTfZ3vvl9FbM8Y3nz1RUJCQjAYDHz0yWLefOd9vlm2gk2bt/LoT+9nzsUzoWrA7Ilf/Y6jxxL5ZtlyfvbQfbZzvvPeR5xPS6dLl8786/lniYionPrUYrHw+lvvsmjxUv732ptMnzoZf/+mf69qmjltKh99spiEY4lkZGQSFRVZ55gNm7YCMGvGNNu2/736Fvn5BQwa0J/nnnna1lelZWX87R//Zu26jbz40iu888bLDrWrKRKOJvKv/7yCwWDg8cceZu7sWbZ9SSdO8sdn/sbBQ0d4850PePC+H7dZO9qSy4bg9qCaYBER19PpecfnR/3Pgv/w4PgH7e4b/NJgMooyHDrv76b/jt/P+L3dfVPfmsqh9EN1tlt/1/q1dylnzgDQu1fPZj2uT+9eAJSXl5N67jw9Y2rP82oymWoFYIC+fXpzzZWX8877H7FqzTruufv2Frc/OzuH55/9Q61Pga+84hLefu9DSkpK6DKwvy0AU1XOMX/uxRw9llhnJLhH927MmT2TMaNG2gIwVYH79ltuYvGSrygsLOLQ4YRGR6rr07t3LLE9Yzh1OplNW7ZxxWULa+0/euw4586dx2QyMX3qRbbt/fv3ITw8jNkXz6j1x4qPtze333Ija9dt5GzqOVLOnKVH924Ota0xH336GRaLhWuvvqJWAKbq9fDzRx7k0cf/j++Wr+Su229p1h9VHYXLlkO0B9UEi4iIO8nPLwAgOKh5c+vXnIu/+hw1TZowrlYArjZubGUdbmZmFmnpjv0BUVPfPr3pfkHo8/fzs9UcT71oUp3HVNcV5+XXXun1+muv4onHfsrFM6fXeYyfny9hYaEAZGXntKjN1SURGzdvrbNvw8bKUogxo+NszwHgx3fdzpOPP8JYO+G75iIY2S1sW30qKirYuWsPABMnjLV7zLChQwgLDaHEbObwEdec7UMjwQ1QOYSIiLiT6tkdmjvLg8X6w/EGY92PSXv3irX7uK5du9j+ff58Gp2io5p13Qt1rmeKtuoAHm3n/D5VdcsV5eV19pnNZrZs3cGRhKNkZedQWlpqK3kpLKysY27pjBgzp03h3fc/Zv+BQ+Tl59f6g2LDpi0AzJoxtc7jCgsL2bh5G8cSj5Obm0dZWVmdcpzWmq3jQmfOpmI2Vy6b++FHn/H5kq/sHldW9T1NPnPW4dFyZ1IIboDKIURExJ2EhoZw5mwquXl5zXpczdHf0JCQOvuDguzXOwcGBNj+XdQKN1D5N/KRe0AzancTjh7j6T89R3pGZovb1ZAePbrTr28fEo8nsXXbDltpwfGkE5w5m4qfry+TJ02o9ZhtO3bx3N9eIL+g7qh7e6jZ37vj9zZ6fPUfDK5GIbgBKocQEXE9ab9Ic/ixQT5B9e47/ODhFt0YV58Nd22we2NcW4jtGcOhwwkcSzzerMcdT6qsp/X19an1cXw1k8lk93G1ljhudmvbTnFJCb/9w7NkZWXTrWsXfnTXbQwfNoSQ4GDbc7n1zp9wPs3xmyxrmj7tIhKPJ7Fp8zZbCN6wsXIUeNLE8fj7/VDjnJGRyZ+e+RslZjP9+/XhrttvYeDA/gQGBNjaNmfhVa3SrvrUHO1f9NHbhIWGtun1nEUhuAEqhxARcT1tMasCQFRAyz7Kr0+Ef9vNd3+hUXEjWLZ8JYcOJ5CdndPkpZC379gNQNyI4XYDb2Fhkd3HFRX9sD2wjRa2csSOnbvJysrGaDTy56d/Q48e3escU1xS0mrXmzltCm+89R679sRTXFKCv59fvaUQa9ZtoMRsJsDfn+f+/DTBwbX/MGvtKckurJXmghrw7Owctw3BujGuAQrBIiLiTiZPHE9oSAjl5eV8sujzJj3mbGqqLbAtXDDX7jGnk1Psbj9z9pzt391q1Ac729mzqVB1k5m9AJxy5ix5eXXDoaM6d+7E4EEDMJtL2bU7ntPJKZxOTiEkJJixFyzicaaqbQMH9q8TgKlaXKM5vKr+aDGXmO3uT009V2db1y6d8asanU446r6r+ykEN0A1wSIi4k58fX15oGpO1yVfftPgIg5UrTD3l7+9QFlZGZMnjmfyxPF2j9uydTsVFXVLOqpnGOjWrWutUWdD1S/YtrqxqzFBQZXhsrS0zO7+9z74xPZve8/LETOmTYGq78nmrdsBmDZlMl5etT+UD7a1rbTOOSwWC+9/+Gmz2lZdw10drmuqqKiwuyCIyWSyrbC3bPlKu+fNzy/g/od/zqtvvE25nZsOXYFCcANUEywiIu5m1oypzJ87G4vFwh+f/RtvvvN+nY/ErVYru/fs5Wc/f5LDR47SrVtXHvnp/fWeMyc3j/++8katMJR4PInFX3wJwLw5teeZrQ56iceTWvnZNU3fPpVLDmdkZtb6Q6C4uJiX/vc6R48lMmzoYKhaDa81TJt6EUajkV179rJt+064YIGMC9t2JOEYCUcTbdtzc/P401+eB0PlHxUAp5vQtkEDB0BVXXfN51paWspL/3udinr+ELnhuqswGo0cOpzAK6+/XStwZ2Vl8/SfnyPxeBJnz56rE+RdhWu2up2oHEJERNzRzx95kJ4x3Xnj7ff56JPFfPrZF/TpHUtYWBhms5nTySnk5OQCMHHCOH7585/VOwMEwI3XX82ixV+wfuNm+vXtQ3FxMYePHMVisdCvbx+uvuKyWscPGzqYjZu3sm3HLm654ycA/OOvf2rjZ/2DwYMGMG7MKHbs2sMfnvkrgwYOwNvbi2OJSRiNBv7yp9+xdv0mDhw8zJKlX3M86QS333IDw4YOcfiaUZERDBs6mH37D5KWlk6n6Ghb0K5p6pRJ9Pq4JydPnebRX/yKwYMHYrFYOHrsOKGhIfz9uT/x2hvvcPZsKm+89R5btm7npw/eW+/NiePHjaZXbAwnTyXz+z/9hW7duhIcFEhyyllMJiNP/epxnvj176BqpLl6db6BA/rz0P338J+XX+Ozz5eyes16+vTpRWFhIYnHT1BWVkav2J489MBPHP6eOJtCcAMUgkVExF1dd82VTJ0yme+Wr2R3/F5SUs5yPOkkPt7eRHeKYuL4scyaMY1RcSMaPVdMj+68+M+/8v6HnxK/dz/5+flER0Uy5aJJ3HbLDfj5+dY6/rJL5nPi5Ck2bdlGbl4uXbt0xsfXxzY3bXv4vyd/zutvvcfmLds5eiyR8LBQLpo8gZtvuJYe3bsRHRVFYuJxDiccJSXljC0ctsSMaVPYt/8gFouFGdOn2MpCajKZTDz7x9/y2pvvsnP3Hg4fTiAyKpIF82Zz8w3XEhERzp2338z5tHROnDzFuXPnG2ybt7c3f33mD7z5zgfs2LWb8+fTKC4OZvy40dxx6034+vrYji0tLavVV5ddMp8B/fvx+Rdfsf/AQfbu3Y9/gD+9e8UyY/oUFs6fU2saPFdjSEhI8NgP94P9fejcvWe9Lx6r1X4Qfuwx+Pvf27590nosFgvnz5xusL/Ffai/3VNycjIxMTF1tlutVsrKSvH29rEbKqTtVE8j9tSvH2falMntck31t2ur7+e4PhaLhcTExCYc2Xz67dAA3RgnIiIi4p4UghthbxBJIVhERETEtSkEN8LeaLBCsIiIiIhrUwhuhL2RYE2RJiIiIuLaNDtEI1QOISIiYt/7b7/q7CaIOEwjwY1QCBYRERFxPwrBjbBXE6xyCBERERHXphDcCI0Ei4iIiLgfheBGKASLiIiIuB+F4EZoijQRERER96MQ3AhNkSYi0nFY9QYs4rI62s+vQnAjVA4hItIx+Pn5UVJS4uxmiIiDiouL8fPzc3YzbBSCG6EQLCLSMYSEhJCZmdnhRpNEpHFWq5WsrCxCQkKc3RQbheBGaIo0EZGOwdfXl+DgYM6cOUNRUZHCsIgLsFqtFBUVcebMGYKDg/H19XV2k2y0YlwjNBIsItJxhIaG4ufnR15eHpmZmVD1S7bUXIKPrx8GeyMX4lbU367Hz8+P6OjoDhWAUQhunEKwiEjH4uvrS3R0tO1ri8XC+TOn6dy9B0Z7b9riVtTf0lr06mmEpkgTERERcT8KwY3QFGkiIiIi7kchuBEqhxARERFxPwrBjVAIFhEREXE/CsGN0BRpIiIiIu5HIbgRGgkWERERcT8KwY1QCBYRERFxPwrBjdAUaSIiIiLuRyG4EZoiTURERMT9KAQ3QuUQIiIiIu5HIbgRCsEiIiIi7kchuBGaIk1ERETE/SgEN0IjwSIiIiLuRyG4EQrBIiIiIu7Hy9kNcFTSiZMsW76Kk6eTKS+vIDoqkkkTxjFj2mQM9moYHKQQLCIiIuJ+XDIEx+87wOtvvU+3rl24bOE8vLy82L5zN58t+ZKMzEyuu/ryVruWaoJFRERE3I/LheCioiI++Pgzunfryi8eeQBvb28AJowbzd//9TJJJ05SUmLGz8+3Va6nkWARERER9+NyIXjbjt0UFRVz+83X2wIwgMlk4onHHmr16ykEi4iIiLgfl7sx7tDhBIxGI4MG9gfAarVSWlrWZtfTsskiIiIi7sflRoJTz50nPCyUrOwcliz9hsMJxygvLyc4KIjx40Zx6YJ5+Ph4N+FMTaNlk0VERETcj8uF4ILCIgIC/Pn3S68SN3I4d99xMyUlZjZt2caqNRs4cyaVh+7/cZNniLA0MqxrNBqA2ueyWKxYLErCrqS6nxvrb3EP6m/Pov72LOpvz9KW/exyIbiiooLc3DyuufJSZs2Yats+bkwcf/3Hixw5msjBQ0cYNnRwk86XnprS4P7yss6Af61tZnMp58+kOvgMxJka629xL+pvz6L+9izqb2kplwvBvj4+FJeUMG7sqFrbjUYjE8ePJTnlS44mHm9yCI7u2gOjvZqH6uv51R1R9vLyoXP3ng60XpzFYrGQnprSaH+Le1B/exb1t2dRf3sWi8VCflJSm5zb5UJwZGQEKWfOYrLzwg8JCQagpMTc5PMZjcYGf4jszw5hqCqTEFfTWH+Le1F/exb1t2dRf0tLudyrp2+fXgCcTjlbZ19mVjYAYaGhrXY9TZEmIiIi4n5cLgRPnjgOg8HAsuUraxVLl5aWsWnzNgCGD2taKURTaIo0EREREffjcuUQPbp3Y/6cWSxbsYp/v/QaE8aPpri4hC3bdpKekcn0qZOJ6dG91a6nKdJERERE3I/LhWCASxfOpVOnaNZt2MSni7/EarXStUtnbr7hGi6aNL5Vr6VyCBERERH345IhGGD82FGMv2CGiLagECwiIiLiflyuJri92asJVjmEiIiIiGtTCG6ERoJFRERE3I9CcCMUgkVERETcj0JwIzRFmoiIiIj7UQhuhKZIExEREXE/CsGNUDmEiIiIiPtRCG6EQrCIiIiI+2nSPMHvfbioxRcyGMBisVBeXs7dd9zS4vO1F02RJiIiIuJ+mhSCt+3Y1aoXvfuOVj1dm9JIsIiIiIj7aVII7tenN9gZEQU4n5ZOfn4BAKGhIYSGhODl5UVZWRnZ2TkUFBYCEBERTs+Y7vj5+rZi89ueQrCIiIiI+2lSCH7k4Xvtbl/+/RpOnDzN/LmzmDp5IqGhIXWOycjIZN2GzazfuIXuE8ayYO7FLW91O9IUaSIiIiLup0kh2J4DBw/z1bfLueHaK5l60cR6j4uKiuSaqy4jPDyMz5d+Q9cunYkbMczRy7Y7TZEmIiIi4n4cnh1i7YbN+Pr6MHniuCYdP23KJHx8vNmwaaujl3QKlUOIiIiIuB+HQ3BKylnCw8IwmUxNOt7Ly4vw8DBSzpx19JJOoRAsIiIi4n4cLocoMZfUe7NcfYqLiikpMTt6SafQFGkiIiIi7sfhkeDQkBAKCgrZu/9gk44/eOgIefkFhAQHO3pJp9BIsIiIiIj7cTgEDx0yCIC33/uI5d+vITMr2+5xubl5rF67gTff/RCAwYP6O3pJp1AIFhEREXE/DpdDLJh3MfF795OXX8BX3y7nq2+X4+fnR0hwEF5eXlRUVJBfUEBRUbHtMQH+/poiTURERESczuEQHBwUxM9/9gAffrKYhGPHASgpKaGkpMTu8TE9unPbzdcRHh7meGudQFOkiYiIiLgfh0MwQGRkBA8/cA+p585z6HACqefOk19QSFlZGd5eXgQGBtClcycGDuhHbM+Y1mt1O1I5hIiIiIj7aVEIrta1S2e6duncGqfqcBSCRURERNyPwzfGeQp7NcGoJEJERETEpbV4JLi8vJzklLNk5+RQVlbepHQ4YfyYll623dgbCaYqBNcXkEVERESkY2tRCF6zbiPLlq+iqLi4CUdXMhgMbhGCLZb694mIiIhIx+ZwCN61Zy+Lv/i62Y+zulgdQUMhWERERERck8MheN2GLVA1Q8S82TOI6dEDf38/DG5WI6CaYBERERH343AIPpt6DoPBwMP3/5ioyIjWbVUHopFgEREREffjcFVreXkZkRHhbh2AUQgWERERcUsOjwSHhoRg9IA7w+orhwgO1uwQrsUAxDq7EeIgPz+YMgVeeQV69XJ2a0RExB04HIKHDB7Ipi3bKSgoJCgosHVb1YE0NEWa6oJdif5icWVFRbBiBcyeDUePamYWERFpOYd/lcybPZOAAH8+/mwJFjeuDTCZnN0CEal2/Djs2uXsVoiIiDtweCQ4LCyURx66l7fe/Yg/PPM8s2ZMpVu3LoSHhmIwNjzqFhEe7uhl211cnLNbICI1JSfDuHHOboWIiLg6h0Pwz3/5WwAsVgtlZeV8unhpkx5nMBh48R/POnrZdjd7NkybBuvXO7slIgKQk+PsFoiIiDtwOASbS0sdepyrLZbh7w/LlsGSJbBzp2aFcFVWq5WignwCgoLdbi5rd1VQAG++WXd7drYzWiMiIu7G4RB83dWX4+3thcFgdPtQERAAt9xS+Z+4JovFyvkzWXTuHoSxkXId6Rjy8hSCRUSk7TgcgqdPndy6LRERqSEoqHIWiAs/fVE5hIiItAZNNCQiHZLRCGFhdbdrJFhERFqDQrCIdFj2JpJRCBYRkdbgcDnEb//4XLMfY7FYKC8v5y9/fMrRy4qIB9FIsIiItBWHQ3BWln4TiUjbsjcSrJpgERFpDQ6H4AnjRje432qFoqIiTienkJdfwID+fRkyeKCjlxMRD6RyCBERaSsOh+Dbbr6+ScdZLBa27djFp4uXEtszhisune/oJUXEw9QXgq1WcPOZGUVEpI05HIKbymg0MmnCOCwWKx99+jkxPboxOm5EW19WRNyAvZrg0lIoKalcyEZERMRRbR6Cq00YN5pPFy9l/cYtLQrB23bs5t0PPql3f9cunfnNk485fH4R6TjsjQRTNRqsECwiIi3RbiHYy8sLf38/zqaea9F5iouLAbh45jR6xcbU2e/v79ei84tIx9FQCO7Wrb1bIyIi7qTdQnBpaRlFRcUtXmK5qKgyBA8ZNIBBA/u3UutEpCOyVw6BZogQEZFW0G6LZSxbvhKLxUJoSHCLzlNUNRLsr89CRdxeQyPBIiIiLeHwSPB7Hy5q9Bir1UpxcTHJKWfIyc0DaPE0adUjwQEBlSHYYrFgsVjw8mq3QW0RaScKwSIi0lYcTo7bduxq9mPCw8JYMG+2o5eEGiPBW7btYE/8fjIys7BYLERGRjB54jjmzJqOyWRq8vksFkuL2iOuobqf1d+uJSQEux9YZWdbaKgr1d+eRf3tWdTfnqUt+9nhEBweFtb4PJ0GA95eXoSFhTKwf1+mTJ5oG8F1VPVI8M5d8UyZPIGuXbuQl5fPug2b+eqb5Zw8eZp7f3xHk2uP01NTWtQecS3qb9dSVmIEetbZnnwql/Nncht9vPrbs6i/PYv6W1rKkJCQYHV2I5oj8fgJSkpK6Nu3N/5+P8wEUVFRwXN/f5EzZ1P5yY9uZ+TwoY2eK9jfh+iuPTAa2600WpzEYrGQnpqi/nYx5eXg61u3v372Myv/+Ef9b13qb8+i/vYs6m/PYrFYSEpKapNzu1whbb++ve1uN5lMzJg2mQ8+XszhI0ebFIKpWsxDP0SeQ/3tWnx8ICgICgpqb8/NNWA0Nv5pj/rbs6i/PYv6W1qqVUNwbl4++fn5lJaW4uvrS2hICEFBga15iQaFBFfOPFFcUtJu1xSRthUeXjcE68Y4ERFpqRaH4IzMLFauXsfe/QfJzy+osz8iPIyxY+KYNX1qiwOx2VzKwcNHMBqNxI0YVmf/ubT0qmvWc0u5iLic8HBITq69TSFYRERaqkUheN+BQ7z93keUlpbVe0xWdg4rVq5l67ad3HfPnfSM6eHw9by8THy6eCklJWZ+/cQjdIqOsu0rKipm7bqNGAwGRscNd/gaItKx2PubViFYRERayuEQnJWdzVvvfkhZWTne3t4MHzqInjE9CA0NwdvLm9KyMnJyckg6eZrDhxPIyy/g5dfe5re/+rnDC12YTCauu/py3n7vY/7+r/8ydfJEoqOjyM7OYePmrWTn5LJw/mxienR39GmJSAdjb9U4rRgnIiIt5XAIXrNuE2Vl5fSM6cG9P76jwZXg0jMyefnVt0hLz2Ddhi3MnzvL0csyZtRIwsNCWbV2A1t37CI/Lx8fXx9iY3pw43VXMWzoYIfPLSIdj0aCRUSkLTgcghOOJgJwxy03NLoUcnRUJHfcegN/++dL7D94qEUhGKBP71706d2rRecQEddgLwQXFEBZGXh7O6NFIiLiDhyeWyQrO4fwsDA6d45u0vGxPWMICQ4iPT3T0UuKiAeyVw4BkNv4WhkiIiL1cjgEl5WW4uPbvGGYgIAAzKWljl5SRDxQfZO9qCRCRERawuEQHBgYQE52LhUVFU063mKxkJOT2+Jlk0XEsygEi4hIW3A4BMf06I65tJQt23Y26fjNW3dQYjZr5gYRaZb6yiE0Q4SIiLSEwyF4zOg4ABYtXsqKlWsoKiqye1xBQSHLlq9k0eKlAIwfO8rRS4qIB9JIsIiItAWHZ4cYNyaOLdt2cCwxiS+/Wc7Xy74nOjqS8LAwvL28KCsrIzsnl7T0DKxWKwCDBvZnbFV4FhFpCoVgERFpCw6HYIPBwL0/uoMPP/mM3fH7sVgsnD+fzvnz6XaPnzBuDDdce0VL2ioiHkjlECIi0hZatGyyn58vd99xC/PmpLI7fh+nT6eQm5dPaWkpPj4+hIWFEtuzB2NHxTV5KjURkZo0EiwiIm2hRSG4WvduXenerWtrnEpEpBZ/f/D1BbO59naFYBERaYkm3Rj35FN/5Pd/+mutbQ89+iR/ef7fbdUuEREbeyURKocQEZGWaFIILioqxmp3j/2tIiKtyV5JhEaCRUSkJZpUDhEUFEhmZhar125g6JBBeHtXPqy8vILs7ByszQzDEfUV+YmI2KEQLCIira1JIbhXzxj2HTjE50u/4fOl39i2nzufxlN/+EuzLmgwGHjxH882v6Ui4rEUgkVEpLU1qRziiksXEBQU2CoXrJ4zWESkqVQTLCIira1JI8GdO0fzh6eeJPF4EvkFhVitVt7/aBHhYWFcsmBO27dSRDyavZHgnBywWMDo8LqXIiLiyZo8RZqPjzdDBg+0ff3+R4sIDPRn4vgxbdU2ERGoJwRbLJCfD6GhzmiRiIi4OofnCb71pusIDPBv3daIiNjR0KpxCsEiIuIIh0OwRoBFpL00tGpcbGx7t0ZERNyBqulEpMPT0skiItLaFIJFpMNrqBxCRETEEQrBItLhaSRYRERam0KwiHR4CsEiItLaFIJFpMNTOYSIiLQ2hWAR6fCCg+0viqGRYBERcZRCsIh0eEaj/dFghWAREXGUw/ME25OWnkFuXh4GDPTr27s1Ty0iHi4sDLKyam9TCBYREUe1OASnZ2Sy/PvVxO89QInZDECP7l158hc/sx3z7XcriY6OZNyYUS29nIh4KHs3x6kmWEREHNWiELz/wCHefPcjysrKGjxu09bt5ObmkZaWwSUL5rTkkiLioeyFYI0Ei4iIoxyuCc7OzuHNdz+krKyMsNAQ5s+dxX333FnnuIqKCkYOHwrAshWrOHkquWUtFhGPpBAsIiKtyeGR4NVrN1BWVk7fPr148N678fHxsXucyWTi+muuwMfHh5Wr17F563Z6xca0pM0i4oHs3RincggREXGUwyPBR44mAnDT9VfXG4BrmnvxdIxGI8eTTjp6SRHxYPZGgs1mKC52RmtERMTVORyCs7KzCQwIoEvnTk06PiAggLDQEHLz8h29pIh4MK0aJyIircnhEFxRUUFQcGCzHuPl5UVFRbmjlxQRD6ZV40REpDU5HIKDg4LJysqmrLxpobaoqIiMzCyCg4MdvaSIeDCNBIuISGtyOAT36R1LWVk5a9ZuaNLxS7/+DovFQp/esY5eUkQ8mEKwiIi0JodD8NQpEwH46tsVfPXtcgoKC+0ed+p0Cq+++S6btmyvfNzkiY5eUkQ8mMohRESkNTk8RVq/Pr2ZMXUyazdsZvn3a1ixci3BQZU1wufT0vnjs38nJycXc2mp7THTp06mb59erdNyEfEoGgkWEZHW1KIV4669+nLCwkL5dvlKSkvLyMsvAKCsrJzzaem243x9fFgw72Iunjmt5S0WEY+kECwiIq2pRSEYYPas6UyZPJG9+w5w4tRpcnLzKCstxdfXl7CwUHrF9mTE8CH4+fq2TotFxCOFhtrfrnIIERFxRItDMICfny8Txo9hwvgxrXE6EZE6vL0hKAgKCmpv10iwiIg4wuEb40RE2pu9kgiFYBERcYTDI8HHjic1+zFWi5WysjKGDhnk6GVFxIOFhUFycu1tCsEiIuIIh0Pwv/7zqkOPMxgMvPiPZx29rIh4MHsjwaoJFhERR7RKTXBzWK3WVj/n4YSj/OflNwB46YXnWv38ItIxqBxCRERai8Mh+GcP/qTB/VarlaKiYk6eOs2WbTvp0b0rN1x3FQEB/o5e0q6SEjMffLy4Vc8pIh2TQrCIiLQWh0Nw/359mnRc3MhhXDxrGi/97w1ee/M9fv7IA45e0q4lX35DQUEhnTtF15qbWETcj71V4woKoLwcvNr9cy0REXFl7TI7RHBQEDffcA2p586zYuWaVjtvwtFENm3ZzsJ5swkJDm6184pIx1TfghmqCxYRkeZqtynSesb0wM/Pj/i9B1rlfCVmMx98/BkxPboze5ZWohPxBFo1TkREWku7foBoNBrIbqUhm6VfLSM3N4/77rkTo9HxLG+xWFqlPdKxVfez+tu1hYRg92/3rCwLNbtW/e1Z1N+eRf3tWdqyn9stBGdkZlFUVIyPj3eLz3X02HE2bNrKJfNn061rlxadKz01pcXtEdeh/nZtBos/0LnO9qTENHp2K6mzXf3tWdTfnkX9LS3V5iG4qKiYU8nJLP1qGQBdOndq0flKS0v54OPP6Na1C3Nnz2xx+6K79mjRSLK4BovFQnpqivrbxfXuV88OYyc6d//hS/W3Z1F/exb1t2exWCzkJzV/gbamcDgEP/Tokw49buL4sY5eEoAvvlpGVnYOTzz2ECaTqUXnAjAajfoh8iDqb9cWEWF/e16eEXvdqv72LOpvz6L+lpZqt3IIg8HAtCmTmDZlksPnSEw6wfqNW5g2ZRJBQYG16ovLy8sBbNvC7c2lJCIuTTfGiYhIa3E4BC+Yd3Gjxxgw4O3tRVhYKP379iEsLNTRywGQkJCI1Wpl3YbNrNuw2e4xv/l95ZLMWjlOxP0oBIuISGtxOARfMn9O67akCcaOiaNnzx5293359XecTT3Hfffc2e7tEpH24ecHPj5QWlp7u+YJFhGR5nKpNZY6d4qmc6dou/tWrV4PwPChg9u5VSLSXgyGytHg8+drb9dIsIiINJcqykXEpdgriVAIFhGR5mrSSPBv/9h69bUG4Omnftlq56v2yMP3tvo5RaTjsXfPq0KwiIg0V5NCcFaWfsOISMdgbyRYNcEiItJcTQrBE8aNbvuWiIg0gcohRESkNTQpBN928/Vt3xIRkSaobyTYYsHughkiIiL2tOuvjDXrNrFzV3x7XlJE3Iy9mmCLBQoKnNEaERFxVe06Rdra9RspLy9n7Ji49rysiLiRhhbMCAlp79aIiIiranEItlqtnE4+Q0ZmJuVl5XaPKSsvJzHpBJlZ2Xh7e7f0kiLiwRoKwbGx7d0aERFxVS0KwSdOnuLdDz4lPSOzyY/p3q1LSy4pIh7OXjkEmiFCRESayeEQnJmVzX/+9wZmc2kTjgY/Pz96xnTn2qsuc/SSIiINjgSLiIg0lcMhePXaDZjNpYSFhXLJ/DnE9uyBr48Pv/vTX+nSuRP3/+QuCgsL2X/wMGvWbmT6lElcsmAORt2+LSItoBAsIiKtweEQfOToMQB+dMct9O7Vs/ZJvUxERoQTGRFOz5gejBk1kn+++D/Op6Xz47tubXmrRcRjqRxCRERag8PDsllZOQQFBtYJwPZ06dyJa668jPh9B9i2fZejlxQR0UiwiIi0CodDcEVFBYGBAXW2GwwGSkvL6mwfM2oE3t7ebNm+09FLiogQHGx/UQyFYBERaQ6HQ3CAvz+5uXlYrdZa2319fMi3M2u9yWQiPCyUc+fSHL2kiAhGI4SG1t2uECwiIs3hcAju1q0LJWYz23fuqbU9PDyM4uISTien1NputVopKCikuKTE8daKiDSwdLKIiEhTORyC40YMA+D9jxbx+tvvU1JiBqBXbAwAH336OTm5ebbjly1fRVFxMaEhwS1vtYh4NHshWCPBIiLSHA7PDjF54jg2bdnOmbOpxO89wG03XW/bvmXbTpJTzvK7P/yFyMgICouKKCwsAmDokEGt13oR8Uj2ZohQCBYRkeZweCTYy8uLnz34EyZNGEtQYCC+vj4A9O4Vy7w5MwGosFhIS8+wBeDoqEguWTCntdouIh5K5RAiItJSLVo2OSDAn1tuvBaLxVJr+2UL5zFoQH927NpDVlY2vr6+9Ovbm0kTx+Hn69vSNouIh1M5hIiItFSLQnA1e6vA9e/Xh/79+rTG6UVEarEXgs1mKC4Gf39ntEhERFyNw+UQT//5byxbsYrMzKzWbZGISCO0apyIiLSUwyPB6RmZfLPse75Z9j19evdi4vjRjIobgb+fX+u2UETkAg2tGte1a3u3RkREXJHDIXjQwP4cPXYci8VC0omTJJ04yaeLv2TEsMGMHzuaIYMH2i2TEBFpKS2dLCIiLeVwCH7ovh9RWFhE/L4D7N6zl6OJSZSXl7M7fj+74/cTFBjI2DFxjB87mp4x3Vu31SLi0VQOISIiLdWiG+MCAwO4aNJ4Lpo0nvyCAvbE72f3nn0cP3GSgsJC1q7fxNr1m+jcOZqJ48Ywdkwc4fX99hIRaSKNBIuISEu1yuwQAMFBQUybMolpUyaRm5tXNSK8lxMnT3P+fDpLv/6OL79ZzoB+fXj4gXta67Ii4oEUgkVEpKVaLQTXFBoawszpFzFz+kXk5OQSv/8ge/cd4FhiEgnHjrfFJUXEg6gcQkREWqpN71zLLyjg0JGjJCYmkXLmbFteSkQ8SH0hWCPBIiLSVK0+Epybl8/efQfYHb+P40knsVqttn3RUZGMHR3X2pcUEQ/j7Q1BQVBQUHu7QrCIiDRVq4TgnNw84vfuZ8/e/SSdOFUr+AYFBTJm1EjGjRlFr9iY1riciAhhYQrBIiLiOIdDcHZODnvi97Nn7wFOnjpdK/j6+vgwYvhQxo2JY9DA/povWERaXXg4pKTU3qaaYBERaSqHQ/BTT/+l1tcmo5FBgwYwbkwcI4YNxcfHuzXaJyJil70ZIjQSLCIiTdXicojevWIZNyaO0aNGEBQY2DqtEhFphEKwiIi0hMMh+NKFcxk3Oo7IyIjWbZGISBPYmyFC5RAiItJUDofg+XNmtW5LRESawd5IcH4+lJeDbkMQEZHG6FeFiLik+laN02iwiIg0hUKwiLgkrRonIiItoRAsIi6pvpFg3RwnIiJNoRAsIi5JIVhERFpCIVhEXJLKIUREpCUUgkXEJWkkWEREWqLdQnB5eTnHjieReu58e11SRNyYQrCIiLSEwyH4oUef5C/P/7tZj/nXf17l7fc+dvSSIiI2KocQEZGWaOFIsLXpFzIaMRgMZGRktuySIiKAvz/4+NTdrpFgERFpimatGHfseFKtr83m0jrb7CkvK2fP3v1YrU0PzQ3Jys5m5er1HEk4RlZ2Dv7+fkRHRTJl8gTGjo7DqOWiRNyewVBZEnH+ggorhWAREWmKZoXgf/3n1Vpfp2dk1tnWmJge3Zt1/IVOJ5/hXy+9Ala4aPIEunfrSmFhIZu2bued9z/hSMIxbr/lhhZdQ0RcQ1iYQrCIiDimWSF41owpJB4/SXLKGYdGdTtFR3H9tVc0+3E1ff7F15SUmHn04fvo17e3bfukieP447N/Z9uO3cyfezGdoqNadB0R6fjs3RynmmAREWmKZoXgq6+4FIASs5lfPPk7oqMiufmGaxp9nMFgICQkmKjIiBaXKsTFDWfo0EG1AjCAv58fvWN7Er/vANnZOQrBIh7AXgjWSLCIiDRFs0JwNT9fX8LDwugUHUX/fn1av1UNmDF1st3tFRUVnE09h8lkokuXzu3aJhFxDoVgERFxlEMhGOCPv3uydVvigJISM2azmbT0DFasWktGZhbXXX05oSHBTT6HxWJp0zZKx1Ddz+pv9xIaagAMtbbl5FipqFB/exL9fHsW9bdnact+djgEV0vPyCQ3L49+fWqXJxQXF7Ni1TpSzpzF38+PuJHDGB03oqWXq+Uf/36ZM2dTAejerSs/feCeZo9Mp6emtGqbpGNTf7sXH68woPaEwRaLgRPHzhAcrP72NOpvz6L+lpZqUQhetWY9S7/+jrgRQ2uF4MzMLP754v/Iyc2zbdsdv4+jx45z43VXtazFNdxy47UUFBaSlZXNtu27+Pd/X2P+3FlcMn9Ok88R3bWHplTzABaLhfTUFPW3m+nR0/52k293QP3tKfTz7VnU357FYrGQn9T4dLyOcDgEnzqdzJIvvwWg4oKh6k8XLyUnNw+DwcCggf0pKysj8fgJNm7exqiRwxk4oF/LWw7E9uxh+/eUyRP43+vv8O13K4np0Z0Rw4Y06RxGo1E/RB5E/e1e6ls6OS/PSECU+tvTqL89i/pbWsrhV8+mLdsBuGjSBO656zbb9vSMTA4eTgDg1huv5cF77+aRh+7lyssXArBl286Wt9oOg8HAxPFjADhw8HCbXENEOpb6QrBujhMRkcY4HIKPnziJyWjkskvm1tq+b/9BACIjI5hQFUoBpl00CS8vEydOnXa4sRkZmfzm6Wd54cVX7O4vKysDFcuLeAyFYBERcZTDITgvL5/QsFCCAgNrbT+ScAygTjmCj483ISEh5OXl4aiIiHCMRiOJSSdIPH6i1j6r1crW7bsA6Ne3fadtExHnCAuzv10LZoiISGMcrgkuNZcSElx7KrKKigqOnzgJwCA7db/eXl62qYscYTQaufmGa3j51bd46ZU3mTJpPN27d6O4uIRdu+M5ceo0ffv0ZtyYOIevISKuo76RYIVgERFpjMMh2D/An9y8/FrbjiYep7S0DJPJZHeqsrz8fPz8fB29JFSF6//75aOsXL2Og4cT2LB5GwaDgc6dorji0gXMnDEFk8nUomuIiGuovxzCYH+HiIhIFYdDcPduXUk4mkj83gPEjRwGwKrV6wEYOKAfPj4+tY4/cfIUxcUl9OoZ09I20yk6qknLNYuIewsOBoMBrNba2zUSLCIijXE4BI+OG07C0UTefv9jBu3oT15eHqeTzwBw0cTxtY7Nzsnhw08+B2DQwP4tbbOICABGY2Vd8IU3wunGOBERaYzDN8ZNmjCOvn16UV5ezoGDh20BeOzoOEaOGGo7zmKx8Idnnif13HkCAvyZNnVS67RcRKSekgiFYBERaYzDI8FGo5GH7/8x6zdu4Whi5UoeQwYNYMrkCXWOi46Kwmw2c+dtN9W5mU5EpCXszRChcggREWlMi5ZN9vLyYtaMqcyaMbXB4350xy1ERUXohjURaXX2RoIVgkVEpDEtCsFN1blzdHtcRkQ8kMohRETEEa0SgktLSzl0+CinU1LIzc0nMDCAq6+4xLa/oKCQoKDABs8hIuIIhWAREXFEi0Kw1Wplxcq1rFi1BrO51La9R/eutY5798NPsVRUcMetNxIcHNSSS4qI1GKvJthsNlBSormCRUSkfg7PDgHw/keL+Orb5bYAHBgYUOeYiooKjiUmceRoIq+8/g7l5eUtuaSISC31LZiRm9uitzcREXFzDv+WOHg4gW07dmMwGJg5fQq//80TPPen39Y5zmQy8ejD9xIUFMjJ08ns2rOvpW0WEbFRCBYREUc4/Ftiy9YdAMydPYNrrryUqMiIeo/tGdODm66/GoAdO3c7ekkRkTrslUMA5OYpBIuISP0c/i1x4tRpDAYD82bPatLxI4YNwdfXh7T0DEcvKSJSh0aCRUTEEQ7/ligoKCQiIhwfH+8mHW8wGAgLDSUvv8DRS4qI1KEQLCIijnD4t4S3lxdYrc16TElJCV5e7TI1sYh4CJVDiIiIIxz+LREZGUFmVjYZmVlNOv7U6RRy8/KJjqq/dlhEpLk0EiwiIo5w+LfEkMEDAfjok8WUlZU1eGxObh7vffhprceJiLSGekeCFYJFRKQBDtcmzJoxhQ2btpJw7DjP/PUFpkyeQLeuXQAoLS3j5KlksrKzOZaYxPaduzGbS/Hz82PGtItas/0i4uG8vSEwEAoLa2/PUQgWEZEGOByCg4OC+Mndt/HKG++QnpHJki+/te1LS8/g+RdeqnW8r48P99x1K8FBWjFORFpXeHjdEJynmmAREWlAi35LDOjfl18//ghjx8TVe8Obt7cX48eO4ldPPMLAAf1acjkREbvs1QXn5pqc0RQREXERLZ6qITIygjtvvZHyG8tJOZNKTm4upaVl+Pr6EB4WRvduXTCZ9MtIRNqOvbpg1QSLiEhDWm2+Mi8vL3rFxgAxrXVKEZEmsTsSrHIIERFpgH5LiIjLs18Oobc3ERGpX6uvXGE2l7Js+Ur27N1PTm4eQYEBDOjfj/lzZ9G5U3RrX05ExG4ILigwUl5uwcfHGS0SEZGOrtkheMu2nezYtYfcvDwiwsIYPWokkyaMBaDEbOaFF/9HyplU2/G5efns2LWH+H37eeAnd9O/X5/WfQYi4vHqnysYovW3t4iI2NGsEPzxoiVs3LzN9vX58+kcTjjG4SNHufuOm/lu+SpSzqTi7e3N4EH9CQ0JIScnlyNHj1FWVs5b733Eb3/1C/z8fNviuYiIh6pv1bjsbIVgERGxr8kh+MTJU7YAHBQUSEyP7pSUlHDqdAq74/cxbOhgtmzfSWREOD976CdE1PitlJ2dw4svv05aegZbd+xixtTJbfNsRMQjNRSCRURE7GlyCN62YzcAgwcN4Cd334a3tzcA586d598vv86y5SspLCzi5huuqRWAAcLDw7jumst56X9vcuDgYYVgEWlV9ZVD5OS0d0tERMRVNPn26ZOnkgG4bOE8WwAG6NKlM5cumEt6RiZeXiaGDRlk9/GDBvTHx8ebs6nnWqPdIiI2GgkWEZHmanIIzsrKxs/Pj54x3evsGzJoAADhYWH1LoxhMBgICw2lqKioJe0VEalDIVhERJqrySG4xGwmNCTY7r6Qqu2+vg3PReTt7U15eUVz2ygi0qCGZocQERGxp8kh2GKx4O1tv4TYaNSk9CLiPPWPBBvauykiIuIilF5FxOX5+2N3UQyVQ4iISH0UgkXE5RkM9ksiFIJFRKQ+CsEi4hbslURoijQREalPs1aMO5+Wzl+e/1eL9ouItAWFYBERaY5mheCysnJSzqQ6vF9EpK2oHEJERJqjySG4X5/eoButRaSD0kiwiIg0R5ND8CMP39u2LRERaYH6QrDVWnnjnIiISE26MU5E3IK9EFxRYSA/3xmtERGRjk4hWETcQn2rxqkkQkRE7FEIFhG3UP+qce3dEhERcQUKwSLiFhSCRUSkORSCRcQtqBxCRESao1nzBHcURUXFrF67gb37D5KRmYXBAF27dGbyxPFMnjgOg24FF/E4GgkWEZHmcLkQnJObx/MvvERubh4Txo1m1oypFBcXs3HzNj78ZDHn09K4+opLnd1MEWlnCsEiItIcLheCv/z6O7Kzc7ju6suZMe0i2/aJ48fyh2efZ/XajcyeNZ2Q4GCntlNE2ld95RD//CcsWtTerZGaTCYIDISAgIb/X/3vgAAwNqNYz2KB7Aw/wqOa97i21qULDBnSsdokIj9wuRAcHh5K3MhhTJ44rtb2gAB/+vbuRfy+A5xNPa8QLOJhQkIqF8WwWmtvT0mp/E/cmRHo4uxG2DVmDHz1FXTt6uyWiMiFXC4EX7ZwXr37ioqLAQjw92/HFolIR2A0Vo4Gq/xBOpJdu+D3v4dXXnF2S0TkQm7zIc2Zs6kkHj9Bp+goYnp0c3ZzRMQJBg50dgtE6lqypO4nFCLifC43EmxPdnYOr77xLgaDgZtvuKZZs0NYLJY2bZt0DNX9rP52b/fdB1u3us3f9uIm0tPh7FmLSiJaid7PPUtb9rMhISHBpf8+PXU6hVdef5vCwiLuuO1GRseNaPJjg/192rRtItL+vvk2gM+/CCQtzeTspkiVsjIDxcUGioqNlf8vMlBS4ll/rLz/znlmzSx2djNEXFJ+cWmbnNelR4J37orn/Y8/w8fHmwfv+xED+vdt9jmiu/bAqFt33Z7FYiE9NUX97QHuvgfu/JH6u6OzWCwUF0NhIRQV1f5/cXHzygesViu5WRmERkQ5fZ745GR44IG6r7lTKdF07u6UJrkdvZ97FovFQn5SUpuc22VD8MrV61jy5bd069qFe398B1GREQ6dx2g06ofIg6i/PYv6u+MyGiE4uPK/lrJYLJw/U0zn7gan93dpKfzsZ1BWVnv7vn1GTZXWyvTzLS3lkq+e9Ru3sOTLbxk8sD8//9kDDgdgERGR1uTjA0OH1t0eH++M1ohIQ1wuBCedOMmiz7+kb59e3HvPnfj5+Tq7SSIiIjYjR9bddvRoZamHiHQcLlcOsejzr7BYLAwfOpj9Bw7ZPaZrl8507dK53dsmIiISFwfvvFN7m9UKBw7AhAnOapWIXMjlQvDp5Mqln774alm9xyycN5tLFsxpx1aJiIhUiouzvz0+XiFYpCNxuRD80gvPObsJIiIi9bJXDoHqgkU6HJerCRYREenIwsOhZ8+62/fudUZrRKQ+CsEiIiKtzF5JxL59UFHhjNaIiD0KwSIiIq3MXgguLITjx53RGhGxRyFYRESkldVXF6ySCJGOQyFYRESklTU0Q4SIdAwKwSIiIq2sVy8ICam7XSFYpONQCBYREWllRiOMGFF3u8ohRDoOhWAREZE2YK8k4swZSE93RmtE5EIKwSIiIm2gvrpgjQaLdAwKwSIiIm1AM0SIdGwKwSIiIm1g6FAwmepu181xIh2DQrCIiEgb8PeHQYPqblcIFukYFIJFRETaiL2SiCNHoKTEGa0RkZoUgkVERNqIvZvjysvh0CFntEZEalIIFhERaSNaOU6k41IIFhERaSOaIUKk41IIFhERaSOdOkHXrnW3ayRYxPkUgkVERNqQvZKIvXvBanVGa0SkmkKwiIhIG7JXEpGbC6dOOaM1IlJNIVhERKQN6eY4kY5JIVhERKQNKQSLdEwKwSIiIm2oX7/K1eMupBAs4lwKwSIiIm3IZIIRI+pu1zRpIs6lECwiItLG7JVEnDwJOTnOaI2IoBAsIiLS9uqrC9ZosIjzKASLiIi0Ma0cJ9LxKASLiIi0seHDwWCou103x4k4j0KwiIhIGwsKgv79625XCBZxHoVgERGRdmCvJOLgQSgrc0ZrREQhWEREpB3YuzmutBSOHHFGa0REIVhERKQdaOU4kY5FIVhERKQdaIYIkY5FIVhERKQddOsGUVF1t2skWMQ5FIJFRETagcFgvyQiPh6sVme0SMSzKQSLiIi0E3slEZmZcPasM1oj4tkUgkVERNqJbo4T6TgUgkVERNqJQrBIx6EQLCIi0k4GDgQfn7rbNUOESPtTCBYREWkn3t4wbFjd7RoJFml/CsEiIiLtyF5JRGIiFBQ4ozUinkshWEREpB3ZmyHCaoX9+53RGhHPpRAsIiLSjnRznEjHoBAsIiLSjupbPlkhWKR9uXQI3rJtJz9/8rc8+MgvyczMcnZzREREGhUaCr17192uGSJE2peXsxvgiPyCAj765HP2HTiEt7e3s5sjIiLSLCNHwokTtbft2wcVFWAyOatVIp7FJUeCn/v7i5w4dZoHfnIXsTE9nN0cERGRZrFXF1xcDMeOOaM1Ip7JJUNwn149+fUTjzBk8EBnN0VERKTZ6rs5TiURIu3HJUPw3XfcQnBQkLObISIi4hDdHCfifC5ZE9yaLBaLs5sg7aC6n9XfnkH97Vlcsb9jYiA01EBurqHW9j17rFgsVqe1yxW4Yn+L49qynz0+BKenpji7CdKO1N+eRf3tWVytv4cM7sKWrX61tu3ZXcH5M671PJzF1fpbOh6PD8HRXXtgNLpkVYg0g8ViIT01Rf3tIdTfnsVV+3vsOANbttbelpbuBV496dzZWa3q+Fy1v8UxFouF/KSkNjm3x4dgo9GoHyIPov72LOpvz+Jq/T1qlP3t+/cb6dq1vVvjelytv6Xj0atHRETECbR8sohzKQSLiIg4wZAh4GXn81hNkybSPhSCRUREnMDXFwYPrrtdI8Ei7cPlaoIzs7I5dTrZ9nV+YQEABw8nEBQUCEBkRASxPbWSnIiIdGxxcbB/f+1tR45Urh7n7++sVol4BpcLwUePHef9jxbV2f7JZ1/Y/j1h3Bhuv+X6dm6ZiIhI84wcCe+9V3ubxQKrVsGllzqrVSKeweVC8KQJY5k0YayzmyEiItJiY8bY3/7ccwrBIm1NNcEiIiJOctFF0L173e0bN8KGDc5okYjnUAgWERFxEm9v+MUv7O975pn2bo2IZ1EIFhERcaJ77oHIyLrbv/sOdu92RotEPINCsIiIiBMFBsIjj9jf9+yz7d0aEc+hECwiIuJkDz4IwcF1ty9eXDllmoi0PoVgERERJwsPhwceqLvdaq2cKUJEWp9CsIiISAfw6KPg51d3+/vvw6lTzmiRiHtTCBYREekAOneGH/2o7vbycnj+eWe0SMS9KQSLiIh0EI8/Dl52lrF6/XU4f94ZLRJxXwrBIiIiHURsLNx8c93tJSXwwgvOaJGI+1IIFhER6UCefBIMhrrb//tfyMlxRotE3JNCsIiISAcyeDBcdVXd7Xl5lUFYRFqHQrCIiEgH86tf2d/+z39CUVF7t0bEPSkEi4iIdDBjx8LcuXW3Z2RU3iQnIi2nECwiItIB/frX9rf/7W9QWtrerRFxPwrBIiIiHdC0aTB5ct3tKSmVC2iISMsoBIuIiHRABkP9o8F/+QtUVLR3i0Tci0KwiIhIB7VwIYwYUXf7sWOweLEzWiTiPhSCRUREOqiGRoOfeQas1vZukYj7UAgWERHpwK69Fvr1q7t9715YtswZLRJxDwrBIiIiHZjJVLmKnD1//rNGg0UcpRAsIiLSwd12G/ToUXf75s2wYYMzWiTi+ryc3QARERFpmI8P/OIX8Mgjdff9+McwerQzWuUcVquBkuJo/PwNGAzObo3jDAYID4eoqB/+i46u/bW/v7Nb6d48OgRnFmdiLPTHaGz+gHiQTxD+3vZfnRlFGVgd/HwqwDuAQJ9Au/uyirOosDg2J46flx/BvsF29+WU5FBWUebQeX1MPoT6hdrdl1uSS2mFYzO6e5u8CfMLs7sv35xPSXlJs85nsVjILM7EuziIqMAou8cUlhZSVObYeqQGg4GoAPvnLS4rpqC0wKHzAkQHRtvdbi43k2fOc/i8kQGRGA11X/ulFaXkluQ6fN5w/3C8jHXfWsot5WQXZzt83lC/UHxMPnW2W6wWMosya2+r6u+m/HyH+Ibg6+Vrd196YbrD7dV7RCVXeY+oZjKaiPCPsLvP2e8RV9wMf3gesrJqbz92LJpjx+w8wGQGX8ffIyiOBKudnx9TKfg6/h5BSThY7MQPYzn4Nec94oK+MIdCRd33CAwW8M+su72pzCFQYf89ggDH3yMoDYLyelJuQAZgJSAAIiIgIhIiIyAgoPHTmiwBeFntv0eUmrKw4th7hNHqh7fF/ntEqTEHq6H57xG/eBxGDW/4PaKkzLGf5aYwJCQkeGw10cCPBjr82P8s+A8Pjn/Q7r7ov0WTUZTh0Hl/N/13/H7G7+3uG/rfoRxKP+TQeR8Y+wAvXfKS3X0z3p7BulPrHDrvtUOuZdF1i+zuu27RdXx26DOHzjs9djpr71xrd9+D3zzIf3f+16HzDokawsEHD9rd9/u1v+fpdU87dN6ogCjSH7f/ZvjS9pd4aNlDDp0XwPo7+z+iiw4u4vrPrnf4vGm/SLMbsNeeXMvMd2Y6fN4D9x9gaKehdbYfTDvIsJeHOXzeNXesYUavGXW2pxem0+n5Tg6f99NrP+W6odfZ3Wd42vFhJr1HVGqP9wiLxcL5M6fp3L0nRqOxZe8R0UM4+IBrvUfw+3p+jQ9ZBNc7/h7BX9OgyM4f4b3Wwp2Ov0fw0gFIr/seQfRBeNDx9wjeXgMn675HEJAOTzj+HsGnn8Ih++8R/L4FQ9Hf/Ad22H+P4PFoCHTsPYK1v4O19t8jeGAodHLsPYLtD8C39t8juHMG9Gq794iEmxIcOndjVBMsIiIiIh5HIVhEREREPI5H1wSLiIi4uq5d7W8vDoecFpy3U2cw2SnHNEdAlr0HNFFUNHjbSR9l4eBgAQBQWTvra667vcIP0lpw3rBw8K/ne5zagvMGBUGxSctfO5NH1wSXkk901x66Mc4DbnqxWCykp6bQpXusboyr4u43xqWnpjTp51s3xlVy5feIC2uC3fXGuPp42ntEfT/fzXmPaI62fI/wNfmTmwsZGT/8l54Op9IzyMy0kpUFGZmQlVl5Q2RpE34MDeUBGMvtv0dU+GSB0bH3CEO5H8Zy++8RFp8crMbmv0e8/DJMm9z4jXG5qS24EbMBHh2Cg/19bG+a4t4u/CUp7k397VnU355F/e1ZLBYLiYmJbXJuvXpERERExOMoBIuIiIiIx1EIFhERERGPoxAsIiIiIh5HIVhEREREPI5CsIiIiIh4HIVgEREREfE4CsEiIiIi4nEUgkVERETE4ygEi4iIiIjHUQgWEREREY+jECwiIiIiHkchWEREREQ8jpezG+CIiooK1qzfxPYdu0lLz8BkMtKjezcunjmNEcOGOLt5IiIiItLBueRI8BvvfMCSpd8QHh7GDddeyZWXLcRsNvPK6++wYdNWZzdPRERERDo4lxsJjt93gL37DjJ2dBx33X6TbfuEcaN55q8v8PnSb4gbMYzg4CCntlNEREREOi6XGwnetn0XABfPnFZru4+PD1MmT6C0tJRd8fuc1DoRERERcQUuF4KTTp7C29ubHt271tnXp3ds5TFJJ53QMhERERFxFS5VDlFSYqagoJDoqEiMxrr5PTw8DID0jMwmn9NisbRqG6Vjqu5n9bdnUH97FvW3Z1F/e5a27GfXCsFmMwC+vr529/v6VG4vKSlp0vnyi0vJT0pqxRZKR6f+9izqb8+i/vYs6m9pKZcrh2iYFQCDweDshoiIiIhIB+ZSIdjfzw9qjAhfqHq7X9VxIiIiIiL2uFQI9vX1ITQkmJycXLs1IpmZ2QB06hTlhNaJiIiIiKtwqRAM0K9vb8rLyzl1OrnOvmOJlfVBA/r1cULLRERERMRVuFwInjxpAgArV6+vtb2oqJiNW7YRGBjAqJHDndQ6EREREXEFLjU7BMCgAf2YNGEsW7bt5OVX32JU3AjMZjPrNmwmLy+fH915C/7+/s5upoiIiIh0YIaEhASrsxvRXBaLhfWbtrJ5y3bS0tMxmUz0iu3J/Lmz6N9XpRAiIiIi0jCXDMEiIiIiIi3hcjXBIiIiIiIt5XI1wS1RUVHBmvWb2L5jN2npGZhMRnp078bFM6cxYtgQZzdPWsGWbTv5bMmXlJSY+cNTvyQyMqLOMXoduL6iomJWr93A3v0HycjMwmCArl06M3nieCZPHFdrwRz1t+vLys5m5er1HEk4RlZ2Dv7+fkRHRTJl8gTGjo7DaPxhPEf97X4OJxzlPy+/AcBLLzxXa5/627Vt27Gbdz/4pN79Xbt05jdPPmb7urX726PKIV5981327jvIsKGDiRsxjPLycjZt2UZyylluvO4qpl400dlNFAflFxTw0Sefs+/AIby9vSktLa03BOt14NpycvN4/oWXyM3NY8K40fTt05vi4mI2bt7G+bR0Lp45lauvuNR2vPrbtZ1OPsO/XnoFrHDR5Al079aVwsJCNm3dzrlzaUwYN5rbb7nBdrz6272UlJj503P/IDs7B+yEYPW3a1u7fhOLPv+Si2dOo1dsTJ39/v5+DB44wPZ1a/e3x4wEx+87wN59Bxk7Oo67br/Jtn3CuNE889cX+HzpN8SNGEZwcJBT2ymOee7vL1JRUcEDP7mLFSvXcuy4/TXl9TpwfV9+/R3Z2Tlcd/XlzJh2kW37xPFj+cOzz7N67UZmz5pOSHCw+tsNfP7F15SUmHn04fvo17e3bfukieP447N/Z9uO3cyfezGdoqPU325oyZffUFBQSOdO0ZxPS6+1T/3t+oqKigEYMmgAgwb2b/DYtuhvj6kJ3rZ9FwAXz5xWa7uPjw9TJk+gtLSUXfH7nNQ6aak+vXry6yceYcjggQ0ep9eB6wsPDyVu5DAmTxxXa3tAgD99e/fCarVyNvU8qL/dQlzccK68fGGtAAzg7+dH79ieALZRQvW3e0k4msimLdtZOG82IcHBdfarv11fUXFlCG7K1LZt0d8eE4KTTp7C29ubHt271tnXp3ds5TFJJ53QMmkNd99xC8FBjf/1p9eB67ts4Tzuues2fHx86uyrfkMNqHpDVX+7vhlTJzNn1vQ62ysqKjibeg6TyUSXLp1B/e1WSsxmPvj4M2J6dGf2rGl2j1F/u77qkeCAgMr3bIvFQnl5ud1j26K/PaIcoqTETEFBIdFRkbVuoKgWHh4GQHpGphNaJ+1FrwP3duZsKonHT9ApOoqYHt3U326opMSM2WwmLT2DFavWkpGZxXVXX05oSLD6280s/WoZubl53HfPnXb7U/3tHqoHLrZs28Ge+P1kZGZhsViIjIxg8sRxzJk1HZPJ1Gb97Rkh2GwGwNfX1+5+X5/K7SUlJe3aLmlfeh24r+zsHF59410MBgM333ANBoNB/e2G/vHvlzlzNhWA7t268tMH7qF/v8oFktTf7uPoseNs2LSVS+bPplvXLnaPUX+7h+qR4J274pkyeQJdu3YhLy+fdRs289U3yzl58jT3/viONutvjwjBjaucIKPmtEriifQ6cEWnTqfwyutvU1hYxJ233WgLRY1Tf7uaW268loLCQrKystm2fRf//u9rzJ87i0vmz2nCo9XfrqC0tJQPPv6Mbl27MHf2zBacSf3tCi6/ZB4lJSX07dsbfz8/2/aJ48fw3N9fZP/Bw+w7cIjYnnVnjqjNsf72iBBc/Y2t/kviQtXb/Wp0gLgfvQ7cz85d8bz/8Wf4+Hjz4H0/YkD/vrZ96m/3E9uzh+3fUyZP4H+vv8O3360kpkd3BvbvB+pvl/fFV8vIys7hiccewmQy1Xucfr7dw4U3vFYzmUzMmDaZDz5ezOEjRxk0oHLmiNbub4+4Mc7X14fQkGBycnKxWCx19mdmZgPQqVOUE1on7UWvA/eycvU63nrvI6KjInnisYdrBWDU327PYDAwcfwYAA4cPKz+dgOJSSdYv3ELUy+aSFBQINk5Obb/qm+Wqv5a/e3+qmcEKS4pabP+9oiRYKr+2ti1Zx+nTifTu1dsrX3HEivnlB3Q5I9RxVXpdeAe1m/cwpIvv2XwwP78+K7b8POzXyem/nZtGRmZvPDSq0RFRPDIw/fW2V9WVgZVd5Sj/nZ5CQmJWK1W1m3YzLoNm+0e85vfPwtVi2aov12b2VzKwcNHMBqNxI0YVmf/uap5oSPCw6GNfr49YiQYYPKkCQCsXL2+1vaiomI2btlGYGAAo0YOd1LrpL3odeD6kk6cZNHnX9K3Ty/uvefOegMw6m+XFxERjtFoJDHpBInHT9TaZ7Va2Vo1b2i/vpW/+NTfrm3smDjuu+dOu/9V3yBX/TXqb5fn5WXi08VLefu9j0lLz6i1r6iomLXrNmIwGBgdV9mHbdHfHjMSPGhAPyZNGMuWbTt5+dW3GBU3ArPZzLoNm8nLy+dHd97SpMmapePJzMrm1Olk29f5hQUAHDycQFBQIACRERHE9uyh14EbWPT5V1gsFoYPHcz+A4fsHtO1S2e6dums/nZxRqORm2+4hpdffYuXXnmTKZPG0717N4qLS9i1O54Tp07Tt09vxo2JA73Pu7zOnaLp3Cna7r5VVcFn+NDBtm3qb9dmMpm47urLefu9j/n7v/7L1MkTiY6OIjs7h42bt5Kdk8vC+bOJ6dEd2qi/DQkJCdY2en4djsViYf2mrWzesp209HRMJhO9Ynsyf+4s+vfVRyauasu2nbz/0aIGj5kwbgy333I96HXg8h585JeNHrNw3mwuWVA5Y4D62/WlpWewcvU6Eo+fICs7B4PBQOdOUYyOG8nMGVPw9vphPEf97Z5eePEVjh1P4qUXnqu1Xf3t+pJOnGTV2g2cOp1Cfl4+Pr4+xMb0YMa0ixhW448e2qC/PSoEi4iIiIjgSTXBIiIiIiLVFIJFRERExOMoBIuIiIiIx1EIFhERERGPoxAsIiIiIh5HIVhEREREPI5CsIiIiIh4HIVgEREREfE4HrNssoi4jnc/+JRtO3bRv28fHnn4Xmc3p91ZLBbWbdjMxi3byMzMBuDyS+Yxa8ZUZzet1R09dpx/vfQqAD978CcM6N/X2U1q1DfLvufb5SsB6qxgJiKuQyFYxIVULx0KMG3KJG649spGH/PJZ1+wfuMWjw2Urmj1uo0sWfoNAD4+PoSHhTq7SVJDSEgw3bt1dXYzRKSFFIJFXNSGTVsZP3YUvXvFOrsp0sr2xO8DICwslP974lECAvyd3SSPs3bDZhYtXsqtN13HpAlja+2betFEpl400WltE5HWoZpgERdkMBiwWq18+MnnVFRUOLs50sryCwoB6Ns7VgHYSRKrPnEREfelECzigiaMG4OPjw9nU8/x/ep1zm6OtDKrxQqAl5e3s5visRKPn3B2E0SkjakcQsQFRYSHcemCOXy+9Bu+W7GK0XEj6BQd1axzNPWGpAcf+SUAC+fN5pIFc2zbn/v7vzmdfIZLF85l/pxZrF2/mS3bdpCekYG3lzfdunZhzuwZDB08EIAzZ1NZ/v1qEo+foLCwiOCQYEYMG8JlC+fi79/waOeBQ0dYt34TySlnKS4uJig4iIH9+7Fg3sVER0XW+7h9Bw6xddtOTp5OprCgEG9vb6KiIhk6ZCAzp08hKDCwzmOq666nTJ7ApQvnsnjJ1xw+cpSCwkL+8NQviYyMaOJ3uPI5r9uwmWOJSeTk5mG1WggKCqJXzxgmTRjL0CGDah3/1NN/ISs72/b1th272LZjl93vf1OuvXb9Jo4mJpGbm4fBAGGhofTv14eZ06fQtUtn27HZOTk89fRfsFqtXLpgDgvmza73vDt3x/PWux8B8OhP76Nfn962fceOJ7Fpy3ZOnDxNbm4eFouFoMAAevaMYcqk8QwbOrjJ7c/MzOK3f6y86cxeSUK16u/ZhHFjuP2W6+vsb26batbdA7z/0SLe/2gRgK3/G7sxrqy8nC1bd7Bn737Opp6juLgEP19foqIiGDJoIDOmXURQUN3X3hP/9zSFhUVcdflCZs+azs5d8WzYvJXU1POYzWZCQ0MYPGggC+ddTGhoSJ3HFxYWsXbDJg4eOkJaeiZms5nAgADCwkIYNmQwkyeNIyI8vAnffRHPoBAs4qJmTp/Cjl3xJKec4aNPP+dnD/6kXa9fc5TyvQ8/ZduO3YSHhxEQEEBOTi7Hjidx/MRJ7r/nTry9vfnvq28BEB4WSlFxCdnZOazbsJnklLM89tP7MBgMdq+zas16Pl/6Dd7e3kSEh2E0GsjJyWXbjl3E79vPzx78CbE9Y2o9pqy8nHfe/5g98fsB8PP1JTIygty8PJJTzpCccoYNm7Zy/z13NlhT/c57H3M44RjRUZH4+/thMNpvoz1r1m1k8RdfY7VaMRgMRESE42UykZmVzZ69+9mzdz/jx47mtpuvw2is/FCuS5dO+Pv7ce58GhUVFQQE+BMeFgZVN2M11co16/niy2+xWq2YTCaiIiMoKi4mLT2DtPQMtmzbyfXXXGGraw0PC6Nfn94cO57E7vj9DYbg3VX1ypGREfTt3cu2vWYwNJlMhIaEYDAayM7OYf+BQ+w/cIiLZ07l6isubfLzaClH2hQdHUl+QQHnzqdB1es1ICCg8hxejf/KzMnN46X/vcHZ1HMA+Pv5ER0VSVFxMadOp3DqdArrN27hvnvupE/v2q897xrnX/T5l6xdv4nAwACCg4OwWK1kZmWzcfNWDh1O4FeP/6xWqUx6Rib/fPF/5ObmARAcFERodBRFxcUkp5wlOeUsq9dt4P577qJ/vz4t/M6KuAeFYBEXZTQaufmGq/nbP1/i6LHjbNm2s97Rsra5fmUg3LvvALl5+fz8Zw/YfqmfOZvKS/97g9y8fL78ZjkFBQVMmTyByy+Zh7e3NxaLhcVffM3a9ZtIOnGSY4lJdkei0zMy+PLr77jsknnMmj4VH5/K4H3kaCKvv/kexSUlvPvBp/zfLx+1BUmAz7/4mj3x+/H29uLaqy5n0oSxmEwmAJJOnOLDTxaTeu48r7zxLr958jG7I8KnTieTnZ3LE489VCdkN+bg4QQ+W/IVACOHD+X6a64grGqGhxKzme+Wr+L71evYvnM3XTp3Yt6cmQA8eO/dUGN0c/jQIXZHNxuyO36fbWaJ6VMnc+mCubawlJ2dw6eLl7LvwCE++ewLunbpTL++lSO548bGcex4EmdTz3E+LZ3OnaLrnLukxMyhwwkAjB87yvaHS+LxE7awOWrkcG647kqCg4KganTy40VL2B2/j1VrNjB86JB2CWGOtumWG6+tNQp9yYK5Tf65slgsvPH2+5xNPYefry+33HQtcSOG2V6bJ06e4u33PiYjM4vX3nqPp558zBawAQxVx8XvPUDq+fPcc/dtjBw+FIPBQEVFBStWreXrb1eQlV0ZhufOnml77OdffE1ubh6REeHcc/dtxPTobtt37nwa73+0iBMnT/PeR4v4/f89XuvnRcRT6adAxIX1jOnBtCmTAFjy5TfkFxS027UNVAag5JSz3HDNlbVGtbp362obZUxOOUNISDDXXHkp3t6VIdZoNHLl5QttofZ4kv36y5zcPCZNHMf8ObNsxwIMGtCPKy5bAFW/4I8eO27bl5aewYZNWwG49qrLmTJ5gi0AA/TpHcvD9/8YHx8f8vMLWLdhs91rJ6ec5YrLFjQ7AAN89c13AHTp3Ikf3XmLLQBTNSp95eULGTliKADfr15HWVlZs69hj8ViYcnSbwEYOzqO66+5otZoYXh4GPfcfRs9Y3pgtVr5etkK275RI4fjVfV9qh5Bv9C+AwcpKysHYPzY0bbt8fsOYDAYbH+YVYdNgMDAAG696Tpb/+3as7dVnmtjnNGmA4eOkHTiFAA3XHcVo+NG1AqbvXvF8uO7bsNgMJCXl8/GzdtqPb76Z+rEqdNcc+VlxI0YZvtDw2QyMX/OLNt0eTVf8zW/njZ1cq0ATI3XYb++venTO9Z246WIp1MIFnFxly2cR1hYKIWFRSxe8nW7Xz8sLNQW6Grq1vWHeVTtTSfl7eVFdFRlHXNuXn69558+dbLd7WNGjbAFhCNHE23bd+zag9Vqxd/Pr94RvNDQEOJGDIMGAp/RaGR03Ih621WftPQMklPOAtQJ4DVdNHE8AMXFxXUCjaOOJ5201RTPmjHF7jFGo9HWH4nHT9j+cAoICLDVKO/Za/97smtPZSlE7149a9WgX3vVZfzr+T/z56d/XWtks5qvrw9dOncCICMzq4XPsmmc0abdVd+fwMAAxo4eafeYmB7d6BnTA4D4fQftHhMSEszE8WPqbDcYDPSoCrjZObl2H1tdDnGh8LAwHn34Pu689UZCm1FaI+LOFIJFXJyfny/XX3MFVAXAw0eOtuv1u3SKtlvP6+fna/u3vY/WK4/xA6h3JNTPz7fWDVw1BQQE2EZYq+s3AU6ePF15zc7R9QZQgF6xMbbHlpaW1tkfFRlR6zk01enklBrX6FnvcT179rD9O/nM2WZfx54TVc8doFvXLvUeV/3crVYryclnbNvHjokDIOXMWdLSM2o9pqiomCNVr60J4+oGNJPJREhw/eHK16fye1leNZLcHtq7TdV9HxvTo8Fyg9iqvj+bmorFYqmzv0vnTvU+3r+en5nBgwZAVS36J599QXLKGaxWawuejYj7U02wiBsYOXwoI4cPZe/+g3y0aAm/+eWj+Pj4tMu1Q0Lq3qVO1aiV7Zh6goix6pj6fldX3xRW77WDg8nOzqGwsMi2LadqJOzkqWTbzBYNsVqt5OcX1Jn1wd7d+02RV2NUOzy8/vYHBwVhNBqxWCzk57dOGUtO7g+jg488/psmPabmKPzwoYPx8/OlpMRM/N79tWpO9+4/QHlFBV4mk90R8vyCAjZu2sqhI8dIS0+npLiEcifPYd3ebcrNq3ztNdTvAKFVPzNlZeWUlJTUGakOC61/hUBDPT8zN1x7JefT0jmbeo71G7ewfuMWgoODGNCvL4MH9Wf4sCF2a99FPJlCsIibuO6ay0k4mkhmZhbffLeSqy5f2C7XrW9Wh+YeY0/NOmB7vKrupi8v/2FUrLRqhMzPz5fIiKZNZ1ZhZzTOZKx/FLkh5hqjyt6NzCbg7e2F2VxqdyTaEdWjgwaDocGR4FptqNFGb29v4kYMZ+v2ney5IARXl0IMGzqYwMDaoe3U6WRe+t+bFBZV/jFiNBoJDw/D18fH1vfpGZmt9jybwhltKi2t/P57NdLvXt4/7DeXltYJwY78vAQHB/H4ow+xacs2tu/czenkM+TnF7Brz1527dmLl5cXF00az5WXLWi3P5BFOjqFYBE3ER4WxqUL5/LZkq9YvXYDY0fHEdOjm8MBlKpRUmcqL2941K46/Hp7//BL3bfqF3zvXrE8dN+P2riFdfnWCBiN3fBWfZOZbyuFkurzmEwmfv3EIw6dY9zYUWzdvpPTyWfIyMwiKjKCgsJCEqrqrmveEEfVzXhvvvMhhUVFeHl5cf01lzNuzOg6f8BcOP9ua7FS9zXqrDb5+HhTUmJuQr//sL+6JKO1rj9z+hRmTp9Cbl4+CUePcfjIMQ4cOkxRUTHrNmwmIzOLB35yV6tdU8SVqSZYxI1MnzqZnjE9sFgsfPjJYiwWS721hTXDcX1ht3oUzVnqu8mnWnXpQXDwDx/zVn8UnZ2d08ats6/mIgbZOfW3oXrhhgsf0xJhVc+9vLzc4RKLAf362NpTfYNc/N4DWCwWAgMDGDa09gIfJ06ett1Ydsn82Vw0aYLdEfyCwmbOSNCE1ydAcVFxnW1t1qZGVJcx1HfTWrXq16aPjw/+/n6t2oZqoSHBjB87mjtuvYFn/vAb2ywyBw8d4WDVNHcink4hWMSNVE8HZTQaOZ2cwtr1m+odZaz5kW1xcYndY06eSm6ztjZFQWFhvXfuFxQW2up/a948V33T1/m09AZDdHl529yg1avGlGrHq6bLsqfmTWyOTMNm99qxP5znaGL9M05YLBa7N2RR9RoaM6pyZoN9+w8BsKdqgYwxo0bWudmw5gp31XMOXygjI7PWzYtN4e1d8/VZN+gCpJ47T4nZXGd7W7WpMdX9ePJUMhUN1B5X933PmO4t+qTmQvVd09vLi+uuvhxf38r3gjOtdCOmiKtTCBZxMzE9ujOjalqxr5etqHc0N7zG3LWnTtcNu1arlZWr17VhS5tmY9WcvxfauXuvbYSwemlmqubHNRgMWK1Wvl+1tt7zvv72B/zuT39l/cYtrdreyMgIWxjdtHlbvWF73cbK+YnDQkPo26eX3WOaq0+vWCIjKpfFXbl6Xb2haM26jfzqqT/x0aef290/bswoAE6eOk16RiZHEytLBi4shQDw9f3h4/yCwrqvNYvFwieffWH7uqy8aXMiBwUG2oLwqdMpdo+pr39b2qaaKwM254+lMVXTohUXF7NjV7zdY6oXJKHqtdoadu3ey+/++BzP/PWFev+4qbBYqKio3Fc9K4uIp1MIFnFDly6cR3h4GGZzKVu27rB7TFhYqG2u1w2bttaaqzYnJ5e33v2QrOwcW6hqX5XhNiQkmFVrN7Bm3aZaYeTg4QS++rpyQYpesTG1piKLjoq0zYO7Zv0mvln2PWbzDzc/5eTk8t6Hn7L/wCEyMjKbfANZc1xx6QIMBgNp6Rm8+c6HtWZgKCws4sNPFtu+35ddMr/Bqdyao3oREoDTyWd44+0PyKnx0bzZXFq5pPJXyygoLKy1iEdNPWO607lTdNXiG99gsVjoFB1F7151p3zrHdvT1v5ly1eRl//Dc01OOcuLL79ORmaWbc7m1HPna83m0dBz6denchR3z9797I7fZwt4BYWFfPb5l+w7cJge3bu1eptCgoNtI7T7DhyitLSMioqKRm+iGzJogG3lw0WLlxK/94DtDzWr1cqRhGO89c6HUDUNmr2p5hzRo3s3srJzOHc+jTff+ZCMjMxa+3Nz8/jgo88oLy/Hy8urTkmLiKfSjXEibsjX14cbrr2S/732doNTQl12yTzefOdDiktK+NdLr+Lj44OPtzeFRUX4+vrw0H0/4oOPFwNgsdofYWoLlqrg0KNbV3rG9OCzJV/y5TffEREeRlFxsa0WOCgwkNtuvr7OR8pXX3EJefn5xO89wLfLV/L96rVEhIdjLi0lNzcPq9WK0Wjk+muuqPfj8pYY0L8vN1x7JZ989gV79x9k/8HDREVGYLVayczKtoW5BfMutrsoQkuMjhtB5mVZLP36O/buP8i+A4ds07/lZOfYXg+TJoxl7sUz6j3PuLGj+PrbFezdX7mgw/hxdUeBqZqV4OIZU1mxai2nk1N46vfPEhERTnFxCfkFBYSGhvDgvXeTciaVzVt3YDaX8rs//ZUe3bvyyEP3NvhcFs6fw7HEJMorKnjj7Q/w9vbGz9eXgsJCTEYjd99xC5u3biflzNlar8+WtsnLy4sB/fuScDSRQ4cTeOyXT2EwGLj5hmsaXELZYDBw1+038d9X3iQ55SyvvfUegYEBBAcHkZ9fYAva0VGR3PvjOxqd/aSpOneO5rqrL+fTxUvZs3c/e/buJyQkmIAAf0qKS8jNy7e95m+6/moiwp3xh61Ix6MQLOKmhg8dTNzIYcTvPVDvMaPjRuDv58fKNes5nZyC2VyKV4A/48eOYt7sWXTu/MMiF2XtuMiB7e55g4HLLplHt25d2Lh5G2fPnqOkpITw8DCGDh7EgnkXE2bnpjJvb2/uues29h04xNbtOzl5KpnMzCx8fH3o2qUzA/r35aJJ49tkFLja1Ism0rdPL9as28jRxCTbzVIR4WH07dOb6VMn2xZNaG1zLp7B4EEDWLdhM8cSk8jJzcNgMBAeHkbPmO5MmjDOtrhCfcaNjuPrbyuXVTYYDIyvKpGw54rLFhAZGcGmLdtIPZdGTm4e4WGhTBg/hotnTiUkOJgunTtxPOkE8XsPUF5R3qQ5a/v0juWxn93PdytWk3TyFMXFJRiMBkYOH8qci2fQKzaGzVu3g50FL1rapltvupaPF33B/7d3hyoNBlAYhj/DYIaJWtQibmoSxCaTn92QxeAdDmUgiNj+MGcaMoQJs05sgsFmO89zFW84nG/6Mst6/ZXdne1fJ0R/2er1cntznfvJQx6fnjOfv2WxeM9mt5vjQT8X52dpri7//U3ZqBmmf3SY8d0k09lrlsuPrFaf6XQ6Odjfy+nJIKNm+LOSByQbbdualAEAoBQ3wQAAlCOCAQAoRwQDAFCOCAYAoBwRDABAOSIYAIByRDAAAOWIYAAAyhHBAACUI4IBAChHBAMAUI4IBgCgHBEMAEA5IhgAgHJEMAAA5XwDXGl+dQgS9A0AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "%matplotlib inline\n", + "\n", + "Y_np = Y.cpu().numpy()\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "ax.plot(np.minimum.accumulate(Y_np), color=\"b\", label=\"SAASBO\")\n", + "ax.plot([0, len(Y_np)], [0.398, 0.398], \"--\", c=\"g\", lw=3, label=\"Optimal value\")\n", + "ax.grid(True)\n", + "ax.set_title(f\"Branin, D = {DIM}\", fontsize=20)\n", + "ax.set_xlabel(\"Number of evaluations\", fontsize=20)\n", + "ax.set_xlim([0, len(Y_np)])\n", + "ax.set_ylabel(\"Best value found\", fontsize=20)\n", + "ax.set_ylim([0, 8])\n", + "ax.legend(fontsize=18)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "d81134ff-cec6-45cb-92bf-4170f428af40", + "showInput": false + }, + "source": [ + "## Predict on some test points\n", + "We fit a model using the 50 datapoints collected by SAASBO and predict on 50 test \n", + "points in order to see how well the SAAS model predicts out-of-sample.\n", + "The plot shows the mean and a 95% confidence interval for each test point." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668655622271, + "executionStopTime": 1668655822584, + "hidden_ranges": [], + "originalKey": "970977ea-ee5e-46eb-b500-683673ce723e", + "requestMsgId": "2ae0c053-022f-4902-8bc5-b904bd85f90d", + "showInput": true + }, + "outputs": [], + "source": [ + "train_X = SobolEngine(dimension=DIM, scramble=True, seed=0).draw(50).to(**tkwargs)\n", + "test_X = SobolEngine(dimension=DIM, scramble=True, seed=1).draw(50).to(**tkwargs)\n", + "train_Y = branin_emb(train_X).unsqueeze(-1)\n", + "test_Y = branin_emb(test_X).unsqueeze(-1)\n", + "\n", + "gp = SaasFullyBayesianSingleTaskGP(\n", + " train_X=train_X,\n", + " train_Y=train_Y,\n", + " train_Yvar=torch.full_like(train_Y, 1e-6),\n", + " outcome_transform=Standardize(m=1),\n", + ")\n", + "fit_fully_bayesian_model_nuts(\n", + " gp,\n", + " warmup_steps=WARMUP_STEPS,\n", + " num_samples=NUM_SAMPLES,\n", + " thinning=THINNING,\n", + " disable_progbar=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668655921184, + "executionStopTime": 1668655921625, + "hidden_ranges": [], + "originalKey": "25139c91-a34c-4fa8-808f-70c1cf6952fd", + "requestMsgId": "ad9413e7-09aa-47f5-b435-bf37cf0180d1", + "showInput": true + }, + "outputs": [], + "source": [ + "with torch.no_grad():\n", + " posterior = gp.posterior(test_X)\n", + "median = posterior.quantile(value=torch.tensor([0.5], **tkwargs))\n", + "q1 = posterior.quantile(value=torch.tensor([0.025], **tkwargs))\n", + "q2 = posterior.quantile(value=torch.tensor([0.975], **tkwargs))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668655923525, + "executionStopTime": 1668655923743, + "hidden_ranges": [], + "originalKey": "39163b27-e252-4244-9712-f52503e00f74", + "requestMsgId": "7c819fcc-5f74-48b1-9fd4-286839fdd0b6", + "showInput": true + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAi4AAAIkCAYAAAA50aC5AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB8k0lEQVR4nO3deVxTZ74/8M9JQkLCLqggKO4VXFBBUaoWsXtrF1trqdB1Okun7dBZ78zcmXtn5v6mM73tXKadaaedVmrB4nTBWmuXqcW6i4oKVakbKK7IFrbsOfn9ERKJCRACISR83q+Xr+o5D+c88RT9+Jzn+T7C8ePHLSAiIiLyAxJfd4CIiIjIXQwuRERE5DcYXIiIiMhvMLgQERGR32BwISIiIr/B4EJERER+g8GFiIiI/AaDCxEREfkNBhciIiLyGwwuRERE5Ddkvu6AJ9Qtrfjyq6/x7fGTaGpuRnBwMEaPHIklixdiTspMCIJgb2s2m7F1+y7s238QV+obIJVKkBA/BsuWLsGsGck+/RxERETUN4K/7VVUd6UeL+W/CoPRiEUZ6RgbPwZavR779h/E2dpzWJSxANkP3Gtv/8aad1BReRQzpidh9qwZMJlM2LWnDOfOX8SDK+/F4usX+PTzEBERkfv8bsTliy9L0aHROIWO6xfMwx+efwk7d+/FTVlLEBMTjcOVR1BReRRpc2fjsYez7W3T583FH1/IR8nGzZg9awbCwkJ99GmIiIioL/xujktDYxMAYNLE8Q7Hg4KCMG5sgrVNUzMAoGxfOQBg2dIlDm3lcjkWZaTDYDCg/HDlIPWciIiI+svvgktcbCwA4MqVBqdzjU1NkEgkiB01EgBQfeYsgoKCkBAf59R24oREa5vqM17vMxEREQ0Mv3tVdPONmfjmyFF8sOFjCIKAxMSx0Ov02LW3DLXnLuCmZZmIjIyATqdHe3sHRsZEQyJxzmdRUZEAgPqGRh98CiIiIvKE3wWX6BFR+NmPn8bbhevxxpp37MeDgmS4/97lWHrDIgCATq8HACgUCpfXUcitx3U6nVv3DVPKB6D3REREgau2Vob7VsXiwgUZjh8/4ZV7+F1waWhoxGv/fBttbe1YfvstSIiPg06nR8U3R/HBhk1oaGzCyhV3uXEl62KqrkunezMyLsHl6A35F1EUUX/pPJ9nAOCzDCx8nv6tpgZ44CEBFy64//eqJ/wuuBQVf4C6K/X42XM/ROK4sfbjaamzIX9Xjq+378KUSRORNG0q0GXk5Vq248HBwW7fWyKR8JspgPB5Bg4+y8DC5+l/amqArCygttb666Qk71Va8av/M3R6PU5V1yB6RJRDaLGZNdNaUK7q+AkoFHJEhIdBrW6BKIpObRsbrSuPRo2KGYSeExERBa4rV4Bm61+rSEoCtmxhcAEAGI1GWCwWmEwm1+cNRgCwn588aQJMJhPO1p5zanvyVDUAYOrkiV7tMxERUaBLTwe++ML639JSoHMBsFf4VXAJCw3FqJExULe04sTJ007nD3bWZLHVeMlYmA4A2FK63aGdRqPFzj1lCAlRYU7KzEHpOxERUSBbuBDYs8e7oQX+OMfl/hV34fU31+K1f76NRRnpiB8TB71ej8ojx/Dt8ZOYOCER6fNSAQDTpk7GwvQ07Ck7gNfeKMCc2bOg1+uxbcdutLa24YlHV0OpVPr6IxEREfmVmhqguBj45S+Brmtc+rDexWN+F1ymJ12Hnz33Q2wp3Y6Dhyrw9fZdkMlkGDUyBnffeSuW3rAIUqnU3v6hVfchISEeu/fsw/r3SyCVSjE+cRyyV63AlEl8TURERNQXNTVAZqZ1Im5jI/Dii4MTWGz8bpNFXwlTyjE6fhxnugcAURRRd6GWzzMA8FkGFj7Poa9raEHnRNw9e4CICMd2oiji1KlTXukD/88gIiKiXrkKLaWlzqHF2xhciIiIqEfdhRZvT8R1hcGFiIiIujWUQgsYXIiIiKg7Qy20gMGFiIiIuvP440MrtMAfl0MTERHR4Hj7bWDpUiA4eGiEFjC4EBERUXcSE4Gvvwbk8qERWsDgQkRERDbnzwMjRwIKxdVj48b5skfOOMeFiIiIUFMDXH89sGIFoNf7ujfdY3AhIiIa5rquHvr0U+BnP/N1j7rH4EJERDSMuVry/Ktf+bpX3WNwISIiGqaGYp2W3jC4EBERDUP+GFrA4EJERDT8+GtoAYMLERHR8FJb67+hBQwuREREw0tU1NXaLP4WWsACdEREREObRqOBRqNxu71KpYJKper2fFiYdcnzj34E/PGP/hVawOBCREQ0tFVVVaG8vNzhmFarhcVigSAIUCqVDudSU1ORmpra4zXDwoA1a7zSXa9jcCEiIhrCkpKSkJiY6HBs06ZN0Gq1UCqVWL58ucO5a0dbamqAp58GCgqAUaMGpctexeBCREQ0hLl69SOXy2E0GiGXyxETE9Pt13ZdPZSVZZ3P4u/hhZNziYiIAtC1S55F0frD3zG4EBERBRh/rtPSGwYXIiKiABLIoQUMLkRERP5HFEXo9XqI17z7CfTQAk7OJSIi8h8VFRXIz89HUVERTCYTZDIZSktLkZeXh/DwlIAPLWBwISIi8g/FxcXIzc2FIAgwmUwAAJPJhKKiIhQWFuL22wtRW5sNBHBoAV8VERERDX0VFRXIzc2F2Wy2hxYbk8kEs9mMTz/NxV13VQR0aAGDCxER0dCXn58PQRB6bCMIAqKi8rFzZ+CGFjC4EBERDW2iKKK4uNhppOVaJpMJ69cXIyrKMmh98wUGFyIioiFMq9VCr9e71Vav10Or1Xq9T77E4EJERDSEKZVKKBQKt9oqFAqnTRcDDYMLERHRECaRSJCdnQ2ZrOeFwDKZDNnZ2b3OhfF3XA5NREQ0QDQaDTQajdvtXW2g6EpeXh7eeaewxzYWiwV5eXlu39tfMbgQERENkKqqKpSXlzsc02q1sFgsEATB6TVOamoqUlNTe71ueHgKoqIK0diYC0AAcHWirkwmg8ViQWFhIVJSUgbw0wxNDC5EREQDJCkpCYmJiQ7HNm3aBK1WC6VSieXLlzucc2e0xVbGv7ExG0AyIiLy0dFxtXJuTk4O8vLyhkVoAYMLERHRwHH16kcul8NoNEIulyMmJqZP12tru3bvoRSUlhbgyy+zoFarERUVhZycnIH8CEMeJ+cSERENUWFhwDPPWH/etSKuRCKBQqEI+Im4rnDEhYiIaABdO0HXYDDAZDLBYDCgoaHBqX1vE3R/+lNgxAjg9tsDuyKuuxhciIiIBtC1E3SbmppgMpkgCALWrVvX6wRdoxEICnK85uOPe7/f/oLBhYiIaABdO0F306ZNuHTpEgAgPDy8xwm6NTXALbcAf/kLcOed1mOullj3NIrj7hJrf8XgQkRENICuDQ4ymcy+AqinCbq21UO1tcCKFcDmzcBNN/W8xNpoNKKkpMThnLtLrP0VgwsREZEXVFRUID8/H0VF1qXLUqkUGRkZmDFjhtPS5a6hBQAmTwZmzrT+3NUS654E8mgLGFyIiIgGXnFxMXJzcyEIgn1XZ7PZjF27diE1NRWFhYXIzs4GXISWrquHMAxe/fSV3wWX3/zuT2hqbu6xTfq8VDy8+gGg83+Urdt3Yd/+g7hS3wCpVIKE+DFYtnQJZs1IHqReExHRcFFRUYHc3FyYzWanc6IoAgByc3ORnJyM8PCUHkMLOfO74PLgynugNxhcnjt5qhrbd+5B/JirT/yttetQUXkUM6YnYekNi2AymbBrTxlef3MtHlx5LxZfv2AQe09ERIEuPz/frfoq//M/+di3r4ChpY/8LrhMT57m8rhWp0PJR59gbEI8MpdcDwA4XHkEFZVHkTZ3Nh57ONveNn3eXPzxhXyUbNyM2bNmICwsdND6T0REgUsURRQXF9tfD3XHbDbjgw+KAKwBIDC09EHAVM796OPPoG5pxUOrVkAqlQIAyvZZZ2EvW7rEoa1cLseijHQYDAaUH670SX+JiMj/aDQaNDQ0dPvj3Llz0Ov1bl7NBKCMoaWP/G7ExZWzteexa08ZFl+/AOPGJtiPV585i6CgICTExzl9zcQJ1hna1dVnkLk4Y1D7S0RE/qm33Z8VCoV9+bM7IiP/gdLSBQwtfRAQweXDjz5BsEKBO267yX5Mp9Ojvb0DI2OiIZE4DyxFRUUCAOobGt2+j21SFfk323Pk8/R/fJaBxR+e53XXXYexY8c6HNu8ebN99+c77rgDW7duxXvvvefW9bTa9Rg58i2IYmDtOeTNZ+j3weXosW9xuroGt99yI0JDQuzHdZ1DdQqFwuXXKeTW4zqdzu171V863+/+0tDB5xk4+CwDi789T4tohkUUYRHNMOs1yH1oldvBRa/X48zpE1Bdsw0Adc/vg8uXpdsgk0qxeFFfVwdZAKBPO2uOjEtwOXpD/kUURdRfOs/nGQD4LAOLvz5PRbASJrMIRbASo+PH4da4BLdfFykUCoyfNDXgdnkWRRFt1dVeubZfB5fLdVdw8lQ15s6ehfCwMIdzyuBgoMvIy7Vsx4M727lDIpH41TcT9YzPM3DwWQYWf36etr7n5OSgsLDQZS0XG5lMhuzsbPuCEnKPf/6f0enAwcMAgJRZ053OKRRyRISHQa1ucfmurbHRWsRu1CjXe0YQERF5Ki8vDxZLz20sFgvy8vIGq0sBw6+Dy7GqEwCAaddNcXl+8qQJMJlMOFt7zuncyVPWIaypkyd6uZdERDTchIenICqqEIAUguD4V61MJoNUKkVhYaHTnkXUO78NLmazGefOX0BkZITDpNyuMhamAwC2lG53OK7RaLFzTxlCQlSYkzJzUPpLRESBSRRF6PV6++i+be+hxsZsAOWYMmWR/XWQTCZDTk4OysvL7XsVUd/47RyXhoYmiKKI6BEjum0zbepkLExPw56yA3jtjQLMmT0Ler0e23bsRmtrG554dDWUnMlNREQeuHb3Z5lMho0bS7FrVx4uX7aOpCQlpeCnP/0uLl++BSaTCRMnTkROTo6vu+7X/Da4dGg0AAClsufJtQ+tug8JCfHYvWcf1r9fAqlUivGJ45C9agWmTOJrIiIi6jtXuz+bTCZ8+GERgEIAhUhKykZpKfDVV9ZJu3K5POBWD/mC3waXiRMS8ff8P/faTiKRIHNxBqvjEhHRgOhp92drGX8AyMUrryQjNpZzWAaa3wYXIiIiX3Bn92epFHj11d9j3Lg/oaOjwx5yOjo6cPLkSXu74OBgREdHQ6VSeb3fgYLBhYiIyE192f1548aNmDt3LiwWi33i7pUrV/D+++/b26lUKixevBipqale73ugYHAhIiJyk1ardXv3Z7PZjOXLl/e4CMQ24kLuY3AhIiJyk1KphEKhcCu8KBQKzJw5kxNyB5jf1nEhIiIabBKJBHfemd3rv/tt5fwZWgYegwsREZGbamqAXbvy7Bv1dofl/L2HwYWIiMgNtoq41uJy1nL+UqnjyAvL+XsfgwsREVEvbKGlttb666SkbGzZUo7c3BzIZNbwwnL+g4OTc4mIiHpw5sy1oQUoLQViY1OwbFkBsrKyoFarERUVxXL+g4AjLkRERD1QqYCwMOvPr4aWq+clEgkUCgUn4g4SBhciIqIejBplDSv33+8cWmjw8VURERFRL0aNAroUvCUf4ogLERFRFzU1wEMPAW1tvu4JucIRFyIiok5dVw+dOwd8+unV+S00NDC4EBERuVjy3NgIaDSOwUWj0UCj0Th8ncFggMlkgsFgQENDg8M5lUrFnZ8HGIMLERENe851WqwTcUePdmxXVVWF8vJyh2NarRYWiwVGoxElJSUO51JTU7nz8wBjcCEiomGtu9DiavVQUlISEhMT3b42R1sGHoMLERENW30JLeCrnyGBq4qIiGhY6mtooaGBwYWIiIalP/2JocUf8VURERENSy+/DJw/bx15YWjxHwwuREQ0LCkUQEkJ0NoKjBzp696Qu/iqiIiIhoWaGuDsWcdjCgVDi79hcCEiooBnm4ibmekcXsi/MLgQEVFA67p66MwZ4KmnfN0j6g8GFyIiCliuljy/9Zave0X9weBCREQBiXVaAhODCxERBRyGlsDF4EJERAGFoSWwMbgQEVHAaGhgaAl0DC5ERBQwoqOB+++3/pyhJTCxci4REQUMQQBefBGIiwNychhaAhGDCxER+TVRBCRd3h8IAvDTn/qyR+RNfFVERER+q6YGSEkBdu/2dU9osDC4EBGRX7KtHjpyBLj1VmDfPl/3iAYDgwsREfmda5c8JyQA48b5ulc0GBhciIjIr7BOy/DG4EJERH6DoYUYXIiIyC8wtBAYXIiIyB8wtJANgwsREQ15ZWXAuXPWnzO0DG8sQEdEREPegw8CGg3wl78AW7YwtAxnfhtcqmvO4LMvvsKZ2nMwmcwYGRONhenzkLkkA4Ig2NuZzWZs3b4L+/YfxJX6BkilEiTEj8GypUswa0ayTz8DERG57/HHrWX85XJf94R8yS+Dy+HKI3izoAhj4mKx/PZbIJPJsO/AQXyw4WM0NDZi5Yq77G3fWrsOFZVHMWN6EpbesAgmkwm79pTh9TfX4sGV92Lx9Qt8+lmIiIYDjUYDjUbTYxtRFKFWt0CqaEBjYyiOHFFh5UrHNgwt5HfBRaPRYN36DxA/Jg4/zXsKQUFBAID0eXPx0l9fQ3XNGeh0egQHK3C48ggqKo8ibe5sPPZwtv0a6fPm4o8v5KNk42bMnjUDYWGhPvxERESBr6qqCuXl5Q7HtFotLBYLBEGAUqkEABgNBogWOTZtSsWWLanQ662jLEQ2fhdcyvYfhEajxcMPPWAPLQAglUrx8x8/7dh2n/WbZNnSJQ7H5XI5FmWkY8PHn6L8cCUyF2cMUu+JiIanpKQkJCYmOhzbtGkTtFotlEolli9fDlEU8c3hK3jup6Nx+nQILBbgz38GVq0CuvxxT8Oc3wWXY1XHIZFIMO26KQAAi8UCo9EEudz5/+rqM2cRFBSEhPg4p3MTJ1i/gaqrzzC4EBF5mUqlgkqlcjgml8thNBohl8sRExOD06dFPPL4CFy4YP2rKSkJ+PJLhhZy5HfB5dLlOkRFRqCpWY0NGzej6vhJmEwmhIWGYv68ObjztlsglwdBp9Ojvb0DI2OiIZE4r/qOiooEANQ3NPrgUxARkSiK0Ov1UCqVqKkBsrIEh9DCJc/kit8Fl/YODVQqJV7++xuYnTITjz/yEHQ6PXbtKcNXW3fgwoVLePoH34FOrwcAKBQKl9dRyK3HdTqd2/cWRXGAPgX5ku058nn6Pz5L/1RRUYG//vWvWLduHUwmE2QyGeTyr6DRPAcgBUlJFmzZYsGoUQAfrX/y5vek3wUXs9mMlpZW3HfPncjKXGw/Pi91Nl74yyv49sQpHD32LRIS4nu5kgUAHJZO96b+0nmP+01DD59n4OCz9B8bNn6MZ3/0HCAIMJvNAACTyQSTaR2AIsSOLkBxYSYEsxl1F3zdWxqK/C64KORyaHU6zEub43BcIpFgwfw0nDv/MU6cOo0pkycBgH3k5Vq248HBwW7fe2RcgsvXTuRfRFFE/aXzfJ4BgM/Sv1RUVODZvB/D7PJf4yYAwJX6R2HCPoyOn+OiDfkLURTRVl3tlWv7XXCJjh6B8xcuQuriD6nw8DAAgE6nh0IhR0R4GNTqFoii6PSHWmNjMwBg1KgYt+8tkUj4h2MA4fMMHHyW/uHll1/udZRbEAT87W9/Q0FBwaD1i/yL332nT5o4HgBQe/6i07nGJmsYiYyIAABMnjQBJpMJZ2vPObU9ecqaBKdOnujlHhMRkSiKKC4uhslk6rGd2WxGcXExLBbLoPWN/IvfBZeMBfMgCAI++2KLw+Qfg8GIXbvLAAAzZyRZ2y5MBwBsKd3ucA2NRoude8oQEqLCnJSZg9p/IqLhSKvVQt/Nq/tr6fV6aLVar/eJ/JPfvSpKiB+DW2/Kwmf//gov//2fSJ8/F1qtDnvKDqC+oRE3LM7A2M6JudOmTsbC9DTsKTuA194owJzZs6DX67Ftx260trbhiUdX26s1EhGR9yiVSsjlChgMvYcXhULBP5upW34XXADgzttvxqhRI7Ftxy689+HHsFgsiIsdjYdW3YfrF853aPvQqvuQkBCP3Xv2Yf37JZBKpRifOA7Zq1ZgyiS+JiIiGgxnz0ogk2XDYCiyT8R1RSqVIjs7u08rPml4EY4fP84XiW4IU8oxOn4cJwAGAFEUUXehls8zAPBZ+oeaGiAzE6itrQCQCsDcbVupRIL9Bw5gzhyuKvJnoiji1KlTXrk2v9OJiMhrroYWAEjBmDGFkEqlkMkcB/xlMhmkUile/uv/ISUlxVfdJT/A4EJERF7hGFqsZfzLy7NRXl6OnJwce3iRyWTIycnB/v37ce/dd/m20zTk+eUcFyIiGtpchRbb3kOxsSkoKChAVlYW1Go1oqKikJOTY3/1R9QTBhciIhpQrkLL5s0ayGQaNDRcbWcymSCVSmE0GtHQ0ABRFKFWt0CqaEBoaKjTbtJEYHAhIqKBZrFYf6DLSMuFC1X48styh3ZarRYWiwVGoxElJSUAAKPBgCC5HKmpqUhNTfVF92mIY3AhIqIBNXEi8PXXwFNPAW+/bX09FB6ehMTExB6/ThRFNNZdQvToOISGhg5af8m/MLgQEdGAmzgR+Pzzq79WqVS9vvoRRRFmvQYxMTFc3k7d4v8ZRETULzU11tEVg8HXPaHhgCMuRETksa4TcS9eBN57D5DLfd0rCmQccSEiIo9cu3roxAmgpcXXvaJAx+BCRER91l2dlpEjfd0zCnR8VURENExpNBpoNBq329sm2PZUXI7I2xhciIiGqaqqKpSXu66tIggClEqlw7nU1FSMGJHK0EI+xeBCRDRMJSU511bZtGkTtFotlEolli9f7nCuvl7F0EI+NyDB5dTpGhw8XIlz5y+gpbUN0SOi8KMfftd+/nT1GYwZEwtlcPBA3I6IiAaAq9oqcrkcRqMRcrkcMTExDud++EOGFvK9fgWX9vYOvF20Ht8eP+lwXKV0DCglGz9BY2Mzvv/koxifOLY/tyQiIh954w1rcGlpYWgh3/F4VZHZbMbfX3/LHlqiIiMxO2WGy3bqlla0d3TgjbfWoq29vX89JiIin4iIsFbD3bqVoYV8x+Pgsnvvfpw7fxEqlRLfe+Jh/OG//gPfeTTHqZ1UKsVvfvkTJI5LQGtbO/aWHehvn4mIaBCcOQOH3ZzRGV5Gj/ZVj4j6EVwOVXwDAFi54i7MnJHcY9tghQKr7r8XAHC48ointyQiIi8TRRF6vR4dHSJuuAG48Ubn8ELkSx7Pcblw8RJkMhnS5s52q/24sfEIDQlBc7Pa01sSEZGXVFRUID8/H0VFRTCZTJBIZBDFUtTW5uHpp1Owfr2ve0hk5fGIi06rw4ioSAiC4PbXhISq0KHRenpLIiLyguLiYqSmptpDCwCIoglAEYBU3HBDsa+7SGTn8YhLsDIYWp3O7faiKKK1tY1LoomIvKiv1XBPnTqF3NxcmM1mF2etIeaZZ3KRkZGMlJSUAewpkWc8Di5xsaNx6nQNTp2uweRJE3ptf7jyCLRaHaZMnujpLYmIqBd9rYa7adOmXkfOBUFAfn4+CgoKvNJnor7wOLjMTpmJU6dr8M677+GH330co0d3v7NW5ZFjeHf9hwCAOSkzPb0lERH1oi/VcEVRxLPPPmt/PdQdk8mE4uJirFmzpk/TA4i8wePgsmjhfOzctReX667gjy/8H5KmTUVcnHVhf1t7B7786ms0Natx4tRp1NXVAwBiR4/C9QvnD1zviYjIQV+q4XZ0dECv17t1Xb1eD61W63RtosHmcXCRyWT44fcfx+tvrsX5C5dw5Ni3OHLsWwBAS0srNn7yuUP7hPg4fPeJhyGVSvvfayIi6jelUgmFQuFWeFEoFE6vmYh8oV8l/6MiI/Gz555G2f5y7C8/jLO152AwGO3nFQo5xo8bi7TU2ZifNpehhYjoGn2dTOtqRKU3ttos1wYPiUSC7Oxsh9VErshkMmRnZ/M1EQ0J/d5kUSqVImPBfGQsmA+LxQKtVguDwQiFQs50TkTUi75Opk1NTUVqaqpb1762NotMJkNpaSl+9KM8zJ5tXSGUl5eHwsLCHq9jsViQl5fX589G5A0Dsju0jSAInf8aGMirEhEFrr5MpkXniIs7iouLkZubC0EQ7KMpJpMJhYVFePvtQrzwQiF+9rNspKSkoLCw0KktOkdaLBYLCgsLuRSahgyPC9AREVH/qVQqxMTEOPyQy+WQyWT2ybRdf7gTXCoqKuy1Wa59BWQ2mwCY8fOf5+KjjyoAANnZ2SgvL0dOTg5kMuu/Z2UyGXJyclBeXo7s7GwvfXqivvN4xOW3f/hzn79GFEWYTCb86Q+/8fS2RETUi/z8fDfmowhYvz4f99xjrc2SkpKCgoICZGVlQa1WIyoqCjk5zhvnEvmax8Glqal5YHtCRET9JooiiouLe63NApjw0UfFsFgca7NIJBIoFApOxKUhy+Pgkj5vbo/nLRbrbPnac+fR2taOqVMmITnpOk9vR0REbtBqtazNQgHN4+CS+9ADbrUTRRFl+8vx3ocbkThuLO6+81ZPb0lENCz1Zcm0KIqszUIBbUBXFbkikUiwMH0eRNGC4vdKMDZhDObOnuXt2xIRBYy+Lpm+6aab8Nlnn3dOxHVNJpPhgQceQGNjo8Nxg8EAk8kEg8GAhoYGh3Oe1JAhGmheDy426fPm4r0PN2L7zj0MLkREfdDXJdOzZs3C5s2f9XhNi8WC2267DSUlJQ7HbYHIaDQ6netLDRkibxm04CKTyaBUBuPipcuDdUsiooDQl/2HACAmJgbr1hVi9epcWCwCANe1We6+++4+V+0l8rVBCy4GgxEajZYz1YmIBkF2djaSk5Px4ov5WL/+auXcnJwc5OXl2QvKMYyQvxm04PLZF1sgiiJGREUO1i2JiIaNmhrg9GngxhuvHrNWxS3AzTezNgsFDo+DS+G77/faxrZ30bnzF6BuaQUALokmIhoAXTdOrKkBMjOBujrgo4+AW69ZvMnaLBRIPA4uZfvL3WjlKCoyErfdcqMbLYmIyBVXGyfK5aXQaPIApOBXvwJuvhmQcEMXClAeB5eoyEj0XlFaQJBMhsjICFw3ZRIWZSyASsWaAUREnuhu40STqQhAIcaMKcSHH96NpibHCbdc4kyBxOPg8of/+o+B7QkREXWr68aJzqwhpq4uF3v3FqKtrc3hLJc4UyAZtMm5A6Vs/0G8s+5f3Z6Pix2N//yPH9t/bTabsXX7LuzbfxBX6hsglUqQED8Gy5YuwawZyYPUayKi/nFn40RBEPDZZ5/hL3/5i9vX5WgL+Ru/Cy5arRYAsGzpEoxPHOt0XqkMdvj1W2vXoaLyKGZMT8LSGxbBZDJh154yvP7mWjy48l4svn7BoPWdiMgT7m6caDKZ8N5772Ht2rWciEsBy63g8unnWwb0prff6vkEXY3GGlySp03FtOum9Nj2cOURVFQeRdrc2Xjs4Wz78fR5c/HHF/JRsnEzZs+agbCwUI/7Q0Q00LquGAI3TiRy4F5w+WIIBZfOERd3NgYr22dd+bRs6RKH43K5HIsy0rHh409RfrgSmYszPO4PEdFAcbViqLS0FM8++yw3TiTq5HcL5mwjLrbVSaIodjt8Wn3mLIKCgpAQH+d0buIE674f1dVnvNpfIiJ3FBcXIzU11R5a0Pnqp6ioCPPmzUN6ejpksp7/rSmTyZCdnc3XRBTQ3Bpx+dv//cn7PXGTbcRlT9l+HDr8DRoamyCKIqKjRyBjwTzclHUDpFIpdDo92ts7MDImGhIXBQ2iOiv41jc0Op3rjiiKA/hJyFdsz5HP0/8FyrPsacWQLcTs3Lmz1+tYLBY8++yzfvv7ESjPk7z7DAd1cq66pRUymRShISEeX8M24nKg/DAWZaQjLi4Wra1t2LZjNzZt/gJnztTie995BLrOIVWFQuHyOgq59bhOp3P73vWXznvcbxp6+DwDh78/yz89//96bSMIAualpWL//gOAIDiEHKlUClgseDn/L4iNiULdhVov99i7/P15kncNanB5+e9vQC4Pwn/89EceX+OuO26BTqfDpEkToAy+uoJowfxU/PmlV/DN0SpUHjmGxHHOK44cWYDOPwzcNTIuweXoDfkXURRRf+k8n2cACIRnKYoiNn78STf1Wa4ym804dLgC+/bvxyuvvOIwDyYnJwfPPvusfeNEfxUIz5OsRFFEW3W1V649KMHFZDLhVPUZNDU3A+jfu9fJkya4PC6VSpG5JAPr1n+Iqm9PYNpU64ojXTeT2WzHg4ODXZ53RSKR8JspgPB5Bg5/fpZ9XTE0bdo0FBQUICsrcDdO9OfnSd7Xr+BiNJmw5auvUX6oEg2NTb3WGEDnVgHeEh4WBgDQ6nRQKOSICA+DWt0CURSdvgkaG5sBAKNGxXitP0REvVEqlR6tGOLGiTRceRxpRVHE3157E5s/34LLdVfcCi2CIOCmZTd4ekvo9QYcPFyJw5VHXJ6/fKUeADAiKgroHJ0xmUw4W3vOqe3JU9YhrKmTJ3rcHyKi/pJIJMjOzuaKISI3eTzisnN3GU53LiWePHECxo1LgEIhx2dffIXwsFBcn5GOjg4NjlUdR0NjE+649UbMn5eK6BFRnndWJsV7H26ETqfHr36eh1Ejr46WaDRafL1tJwRBwNzZMwEAGQvTUX6oEltKt+PJx3Md2u7cU4aQEBXmpMz0uD9ERNfSaDTQaDRutLRSqVTIy8vDO+8U9tjOYrEgLy9vAHpI5N88Di7lhyoAAHcvvw03ZV0dRfnsi68QHh6GO269Cej8Zvtiy1Z88WUpRo6M6VdwkUqlWLniLrxduB4v/fVVLM5YgJEjY9DcrMbO3XvRrG7B7bfeiLEJ8QCAaVMnY2F6GvaUHcBrbxRgzuxZ0Ov12LZjN1pb2/DEo6tZqImIBlRVVRXKy8sdjtk2ORQEwenPnNTUVIwYkYqoqEI0NuZ2zgO8OoItk8lgsVjw6quvIj4+3r67M3d8puHK4+By6VIdgoKCsCxzcY/tBEHArTdlobW1DUXFHyAudjTixzgXhHNX6pwUREVG4Kuvd2Dv/nK0tbZBrpAjcWwCHlx5L2ZMT3Jo/9Cq+5CQEI/de/Zh/fslkEqlGJ84DtmrVmDKJL4mIqKBlZSUhMTERIdjmzZtglarhVKpxPLlyx3OqVQqnDwJWCzZAJIREZGPjg7HFUM333wz2traHHZ25o7PNFx5HFy0Ol23xd3MZufCM7fcuBQ7du1F6dc7kPvQA57eFgAwccJ4TJww3q22EokEmYszWNafiAaFq9EOuVwOo9EIuVyOmBjnBQEpKcCWLcCzz6bg/fcL8OWXjiuGPHn9RBSoPA4u8qAgl0uNg4Jk9h2cu4qICEdkRIR9UiwREV01Zw6wfTsgCM4rhvjqh+gqj1cVRUePQFtbO+o6V/LYhKhUULe0or2jw+lrBImA1rY2T29JROSXbLs928qg19QA//mfwLVV0blgiKh3Ho+4TJ0yCRcvXcYba97BvXfdgeRpUyGRSDBmTBzULa344t+luO/eq+9yz52/gKamZiiV7hd8IyLyZ652e964sRS7duXh8uUUXLkC/OMfAGutEbnP4+CSueR67NpThrq6erz+5lo8//v/RGhoCFJmJuNY1XFs3b4Ll6/UY8qkCWjv6MCesgMA4EYpfiIi/1dcXIzc3FwIguCw2/OHHxYBKARQiJ07s9HSAkR5vtiSaNjxOOfHRI/AE4/mQKkMhiAICA21bpy4YH4aEuKtq4aqvj2Bjzd/gdKvd0Kr1UEQBNxy49KB6z0R0RDUdbdn5+KcJgBmALl45ZUKhhaiPurXAOWM5Gn4w29/iSceWW0/JpVK8cwPnsT8tDkICro6oBM/Jg7f/84jmMJKtUQU4PLz83utcCuVCigqyh+0PhEFin5vshgcrEDKrOkOx0JCVHh49SqsfvB+tLd3QK6QO+zkTEQUqERRRHFxca/boJjNJhQXF2PNmjUs40/UBx6PuHz+ZWnnbs/dk0qliIgIZ2ghomGjr7s9uyofQUTd83jE5ZNP/43Nn32JSRPHI33eXMxJmYXgYMXA9o6IyM94utszEbmnX6+KLBYLTp2uwanTNXjvw42YNXM65qfOQVLn0mgiouHGttuzbQl0d7jbM5FnPA4u/+93v8ahw5UoP1SJmjNnYTSaUH6wAuUHKxAWFoq0ubMxP20uxiaMGdgeExENcXl5eSgs5G7PRN7gcXCJCA9D5pLrkbnkejSr1Th4yBpias+dR1tbO7Zu24mt23YiLnY00ufNRVrqHERGhA9s74mIhpDaWiA6GkhJSUFhYaFTHRd02e25sLAQKSkpPu0vkT/q96oiAIiKjMSypUuwbOkSNDY2ofxQJQ4ersD5C5dw6XIdPtr0GTZ+8jmmTpmE9HmpmJ82ZyBuS0Q0ZNTUAJmZwMSJwCefANnZ2UhOTnaqnJuTk4O8vDyGFiIPDUhw6So6egRuvjETN9+YiSv1DTh4qBIV3xzBufMXcfzEKRw/cYrBhYgCii201NZafzz3HPDGG9aRl4KCAmRlOe72TESe8+oM2lEjY5CVuRi333oTZs1M9uatiIh8omtoAYCkJOD3v3dsc+1uz0TkuQEfcUFnHYOKb47hUMU3OH7ilMP73REjWN+aiAKDq9BSWgrExvq6Z0SBa8CCi0ajQcU3R3Hw8Dc4cfI0zGaz/VyISoU5s2diXuocTJo4fqBuSUTkM92FlvBwDRoaNA5tDQYDTCYTDAYDGhoaer12T+1VKhVUKtXAfhgiP9Kv4NLe0YGKyqM4dLgSJ09VwyyK9nNBQTLMnJ6MeWlzkDxtKqRS6UD0l4jI53oaaSkvr0J5eblDe61WC4vFAqPRiJKSEodzMTExTuGkp/apqalITU311kcjGvI8Di6vvPpPnDxdA7FLWBEEAddNnYx5qXOQMms6ghWspEtEgeXcuZ5fDyUlJSExMdFr9+doCw13HgeX4ydP238+bmwC5qXOQdrcFISFhQ5U34iIhpyYGGDaNGtwcTWnha9yiLzL4+AyMiYaaamzMS91DkaNjBnYXhERDVFKJfDRR8BPfgL89reciEs02DwOLv/1658NbE+IiPyEUgm8+qqve0E0PHEnRCKiHtTUADfeCJw/7+ueEBEYXIiIumdbPfTVV9b/MrwQ+R6DCxGRC9cueZbLAZlXSnYSUV8wuBARXYMVcYmGLgYXIqIuGFqIhjYGFyKiTgwtREMfgwsREUMLkd9gcCEiAlBYyNBC5A/cmiN/8nR1v28kQIAoijAajZiePK3f1yMiGki/+Q1QX29d+szQQjR0uRVc/vq3NwbshoIg4JW/PD9g1yMiGgiCALz8MtDSAkRG+ro3RNQdvioiomGppgY4cMDxmCAwtBANdW6NuPzuN79wefzipctY/14J5HI5FqanYdy4sYgMD4NMJoPBaERzsxrVZ86ibF85goKC8PDqBzBhvPe2eyci/6TRaKDRaNxu398dmG0TcVtagH//G5g/3+NLEdEgcyu4RI+IcjrW3KxGUfH7mDRhPJ54dDWkUqlTmzFxsZiePA233pSF199ci9feeBs//8kziIkeMTC9J6KAUFVVhfLycodjWq0WFosFgiBAqVQ6nEtNTUVqaqpH97p29VBeHrBrl3W0hYiGPo8LWH+xZSs0Gi1WrbzXZWjpKigoCKsfvB+/+f2f8MWXpVj94P2e3paIAlBSUhISEx1HYzdt2gStVgulUonly5cDnWFGp9MhODgYDQ0NEEURanULpIoGSCTOb76vHZlxteS5pIShhcifeBxcjn17AtEjohARHuZW+6ioSERHj8DxE6c8vSURBShXr37kcjmMRiPkcjliYmIAAOXl5U4jM+1tbZBIpb2OzLBOC1Fg8Di4tLa2IqqPs9ikEgmaWlo8vSURDXPXjsyIoogNJR/CLFocRmZsbGGIoYUocHgcXBRyBRoam9DcrEZUVO8BRt3SivqGRiiDgz29JRENc9eOzIiiiKCgIAhm0WFkpiuGFqLA4vFy6MTEsbBYLHjr7XVoblb32La1rQ0F77wLi8WCsWPjPb0lEVGfaDTA0qUMLUSBxOMRl2WZi3Gs6jjO1J7Df//PC5gwIREJ8WMQHhYGmUwKs9mMtvZ2XLx4Gaeqz8BkMgEAMhdnDGT/iYi6pVIBv/wl8P3vM7QQBQqPg8t1Uydj1f334IMNm2A2m3HqdA1Ona7ptr0gCLj91hsxc0ayp7ckIuqz730PCA+3jrwwtBD5P4+DCwAsvn4BkqZNxY5de3Gs6jiuXKmHWRTt5wVBwMiYaFw3dTIWZaQjfkzcQPSZiMhOFEXo9Xr7iiK9HlAoHNtkZ/umb0Q08PoVXAAgJnoE7r3rdtx71+0QRREarRZGoxFBsiAolcG91ngZCFXHT+Bvr70FAPh7/p8dzpnNZmzdvgv79h/ElfoGSKUSJMSPwbKlSzCLoz9EfquiogL/93//h3Xr1sFkMkEmk2HjxlLs3ZuHl15KwapVvu4hEXlDv4NLVxKJBKEhIQN5yV7pdHqsW/9ht+ffWrsOFZVHMWN6EpbesAgmkwm79pTh9TfX4sGV92Lx9QsGtb9E1DuNRgODwQCTyQSDwYCGhgaH8yUlJXjqqaeAzn+cAIDJZMKHHxYBKER2diFUqmxcszqaiALAgAWX2nMXcO78ebS0tCE4WIGszMX2c2az2WsjLxs+3oz29g6MHjUSdVfqHc4drjyCisqjSJs7G489fHWsOH3eXPzxhXyUbNyM2bNmICws1Ct9IyLPVFVVoa6uDqIoorW1FSUlJfYtAC5evIiXXnoJYpfX0ldZFwFYLLlQqZIBpAx634nIu/q9O/SB8sP47/95AS/85RUUv7cBn36xBfsOHHRos7ZoPd7914fQ6w39vZ2D4ydOYdeefbj9lhsRHuZcwbdsn7XC5rKlSxyOy+VyLMpIh8FgQPnhygHtExH1X1JSEkaPHo3IyEiMHj0aK1asQHh4OORyOXbv3g2hlxr9UqmAoqL8QesvEQ2efgWXTZ9+gbeL1qOhsanbNmazGceqTmD33v32Wi4DQafXY936DzA2IR43Zi1x2ab6zFkEBQUhId55UvDECdbqm9XVZwakP0Q0cFQqFeRyOWQymb2wnFwuh0QiQVlZmf31UHfMZhOKi4sH7M8bIho6PH5VVHPmLL74cisAIGXmdNywOAPjxiXgp//xXw7tpFIpcrJX4p13/4Ujx75FxTdHMXvWjH53fOOmz9DS0orvP/moy83VdDo92ts7MDIm2uV5W7Xf+oZGt+/pemia/I3tOfJ5+g/bszIajfaaUL3R6/Xo6Ohw2gOJhi5+bwYObz5Dj4PL9p17AQAZC+bhoVX39dh2dsoMaHU6rFv/Acr2lfc7uJw4eRo7du3FHbfeiDFxrgsz6PR6AIDi2nWRnRRy63GdTuf2fesvnfeovzQ08XkObXqdFkaDAXqpBHUXaqHVdECv00EqlfY64oLOV8KtTfVoa+bWz/6G35vUE4+Dy+maMxAEAfcsv82t9vPT5uC9Dz/Cxct1nt4SAGAwGLBu/QcYExeLm29c2o8rWYeQe3tX3tXIuASXozfkX0RRRP2l83yeQ5wiWAmTWcTluiv45W/+277s2fo9K9i/h12RyWR46KGHEJuQ2G0bGnr4vRk4RFFEW3W1V67dj92h2xAREe72MKxUKkVUZCSa1T3va9SbjzZ9hqZmNX7+46d7XKlk28zRNvJyLdvx4D5s+iiRSPjNFED4PIe+ffv2oaCgABKJxP6KyJ15KxaLBXl5eXy+forfm9QTj4OLVCJBkKxvX24ymfr1P+Op6hps37kHSxYtRGhoiEMIsv2hZjsWFRmJiPAwqNUtEEXR6b6Njc0AgFGjnHeTJSLfO3PmDNasWQOLxeL2+3KZTAaLxYLCwkKkpHApNFEg8ji4REZF4MqVBrS2tblcinytuiv1aGpWIy52tKe3xPHjp2CxWLBtx25s27HbZZv//O/ngc4KupMnTUD5oUqcrT2HCeMdh4xPnrIOYU2dPNHj/hDRwKuoqEB+fj7Wrl3b6+iK7VWvxWKBTCZDTk4O8vLyGFqIApjnmyxOmYy6unp8uOETh+Juruj11nkp6Nyc0VNpqbMxblyCy3Mff/I5Ll66jO8/+aj9WMbCdJQfqsSW0u148vFc+3GNRoude8oQEqLCnJSZHveHiAZWcXExcnOt36vuvhKSyWR46aWXMGLECOTk5AxCL4nIlzwOLssyF2P33n0oP1QBdUsLli5ZhLg462iKKFrQ1taOpmY1Tp46ja937IZa3QKZTIqlNyzyuLOjR43E6FEjXZ77qnQ7AGDm9CT7sWlTJ2Nhehr2lB3Aa28UYM7sWdDr9di2YzdaW9vwxKOr7RuzEZFvVVRUIDc3160VQ13ZXkH3ZaI9Efkvj4NLdPQI5GSvxNqif+F09Rmc7lLI7eKly/jlb//Hob0gCMh5cCWiR0T1r8d99NCq+5CQEI/de/Zh/fslkEqlGJ84DtmrVmDKJL4mIvIGjUYDjUbjdnuVSoX8/HyPwodMJkNQUFCfv46I/FO/9ipKmzsb0SOisOHjT1Fdc7bbdpMmjsc9y29zmmcykPKe+Z7L4xKJBJmLM5C5OMNr9yYiR1VVVSgvL3c4ZttrSBAEp5HOOXPmoLi42O3icjYymQzp6fM52kI0jPR7k8UJ4xPx42d/gKbmZtScqUVLSysMBiMUCjmiIiORmJiAqMjIgektEfmFpKQkJCY6/kNl06ZN0Gq1UCqVWH7Nts0WiwX6bkoX9MRisWBpZma3u0irVCpWziUKMAO2O/SIqCiMiOr+NZAoilC3tEAulyM0JGSgbktEQ5CrwCCXy2E0Gu17D3UliiIUCoXb4cVW3uDRRx9FVNQIGAwGGI1GlJSUOLRLTU1Fampqvz8PEQ0dHgeX3/7hz4iLHY0fdFnF0xOTyYzf/v7PGDc2AT//8dOe3paIApBEIsGDD2Zj7doiAD2/LhIEAatWrcJ3v/tdJCcno7HuEqJHx7msEcXRFqLA43FwaWpqhkrpftXZoCAZJBIJrtQ3uNGaiIab557LQ2FhIXqqNScIAv7whz/g17/+NdA5UmPWaxATE8NKq0TDRJ+CS1Nzs8OvTSYzmpvVsPSwZwgAGI0mHDr8jfUPGXPfJt8R0fCQkpKCoqJC5OTkQhAEhz8rZDIZRFHEY489hvHjx/u0n0TkW30KLr/9/Z8dfn257gp+8/s/9emGsaM9r5xLRIHl4kVg5EjAtpo5OzsbycnJyM/PR1FREUwmk70i7pQpU5zmxhDR8NOn4JIyazpOV59Be3uHRzdTqZS47947PfpaIvJvoihCr9fbl0LX1ACZmcD8+cC7714NLykpKSgoKEBWVhbUajWioqKQk5ODdevWoaPDsz97iChw9Cm4PPmYtRT35ct1+J8//x+iIiNxx2039fp1giAgIjwM4xPHIThY4Xlvicjv2PYe6jqCsnFjKXbtysPlyymorQXGjwf+93+vfo1Go4HJZIJUKoXRaERDQwMMBoPTsmdRFKFWtyAsSoPQ0FDffUgiGjQeTc6N7dwoMSREiQXzudSQiFyz7T0kCIK9uJzJZMKHHxYBKARQiKSkbPzkJ45fV1VVhbq6OoiiiNbWVpSUlNgL2F277NloMCBdb0RaWtpgfzwi8gGPVxX97f/6NreFiIaXnvcesk28zcUrryQjNtZxN+ekpCRUVlZ2W7DORhRFNNZdwtgJk7zwCYhoKOp3AbrGxiZs27kbs2fNxMQJziX9P/+yFG1t7Vi65HrExET393ZE5Cfc2XtIKhVQVJSPZcsKHI6rVKoeC9bZ2JZDs14L0fDRr+BS8c1RrC1aD4PBiDFxcS6DS2NjE/aUHcDuvfvx2MPZmDUjuT+3JCIf6cvGiaIourX3kNlsQnFxMdasWcP9hojILR4Hl/qGRhS8U9w52U4KmUzqsl18/BioVEpoNFoUvPMufvXz5zCSIy9EfqcvGyfq9Xq3y/fr9XpotVqOmhCRWzwOLltKt8FkMmHK5Il44pHVCA11vf9Q5uIMLJyfhjcLClF1/CS2lG5H9gP39qfPROQDfdk4URRF/OxnP3MrvCgUCqfdoomIuuNxjexvT5yCRCLBIzkPdhtabBQKOVY/eD8EQcC3x094eksiGkQajQYNDQ32H729JlKpVIiJiUFMTAxGjRqF7OxsyGQ9/9tIJpMhOzubr4mIyG0ej7io1S2IiopEZES4W+0jIyMwYkQU1OoWT29JRIPInVdDTU1N9iXLVVVVDjsx5+VZ9x7qicViQV5entc+AxEFHo+DS5BMBlh63qPoWmaz2fp1RDTkufNqqOuvk5KSgM6KuD/5CfDWWykoLCx0quOCzpEWi8WCwsJCpKSkON2biKg7Hr8qGjkyGo1NzahvaHSr/dnac1CrWzByJCfmEvmDrq9+bD/kcjlkMpl9ibJMJoPZbIZMJoNKpbKX8d+wAbjxRuDWW7NRXl6OnJwc+2sj295D5eXlyM7O9vXHJCI/4/Hwx6wZyTh3/iIK3nkXTz6Wi6ioyG7b1pypxZq171q/buZ0T29JREPE2bNn8dhjj3Vbxh8AtFrAYOh+7yEiIk94HFyW3rAYu/bsQ+25C/jv//e/mDRxPOLHxCE8LLRzfxETmtVqnDlbi/MXLgGd81yW3rBoIPtPRINs3759KCgogEQi6bGMf2kp0HUzeIlEAoVCwYm4RNQvHgeX4GAFnv7Bd/D6m2tR39CIEydP48TJ0922HzUyBt974hEEK7jJIpG/Onv2LAoKCiCKIkRRvOZsz2X8iYgGQr9mysaOHoVf/eI57Nm7HwcPVaL2/HkYDEb7ebk8COMSEjB3ziwsTE9DkG3feiIactypjLt58+Zer9NdGX8iooHQ7yU+QTIZlixaiCWLFsJisUCr1cFgMEAul0OpDOawMJGf6Gn588WLF7Fjxw7s3bu31+uwjD8RedOArk0WBAEqlRIqFatgEg1FPY2qjBo1CkuXLnU4tm3bNuzevRtr167t031Yxp+IvIVFVYiGkb7sNwQAZ86cwdq1a13MZ+mZQqFAR0eHQ0gyGAwwmUwwGAxoaGjo9Ro9tVepVAxFRMOUW8Hlt3/4M+JiR+MHTz7qcMwTAoDf/eYXHn0tEfVPYmIiwsMdq11v3boVOp0OwcHBTiMujz76KPpKJpPh5ptvxoYNGxyO2wKS0WhESUmJw7mYmBincNJT+9TUVIcqvUQ0fLgVXJqamqFSBjsdIyL/cvbsWacRl/r6elgsFrS1tWHr1q0AgHPnzmHLli1uzWm5lsViwa9+9StMnjx5wPp9LY62EA1fbgWX+WlzMWKEY4G59HlzvdUnIvISV2X833rrLXsRuRUrVqCkpATPP/88LH3c0qNrGf8FCxYMcM+JiKzcCi4Pr37A6VjuQ87HiGhoczU3RCKRQBAESCQSXLhwAU899RTMZnOfr52Tk4O8vDzuPUREXsXJuURkl5+f3+clzIIgYNGiRSgoYN0WIvI+jzdZJKLAIooiiouLHXZxdocgCLj11lu91i8ioq7cGnEpfPf9ft9IEKx/MJpMJjz+yOp+X4+IBoYoijAYDJBKpdDr9W5/nUwmgyiKeOyxx5zmzRAReYtbwaVsf7kbrdz3+CMDejki8kBFRQXy8/NRWFgIs9kMiUQCiUTids2WnJwcTJkyBTExMV7vKxGRjVvBZfLECdYCLC7UXalHW1s7ACAiIhwR4eGQyWQwGo1oblajvaMDADBiRBTGjY3nJotEQ0BxcTFycnJgsVjsq4fcDSyCIGDhwoX43//9X2zatAlarZZF4oho0LgVXPKe+Z7L4198uRU1Z2px681ZWJyxABER4U5tGhoasW3HbmzfuQfx6Wm47eZl/e81EXmsoqICq1ev7vNy567S0tJQUlLCInFENOg8XlV05GgVNn36BVbdfw8WX999zYaYmGjcd+9yREVFomTjZsTFjsbsWTM8vS0R9dNvf/tbj0KLbU7LI488gtmzZ2P58uU9tudoCxF5g8fB5esdu6FQyJGxYJ5b7ZcsWohPPvs3duzay+BC5COiKOKTTz5xu71UKoXZbIZMJnOY0yKXyzm3hYh8wuPgcv78RURFRkIqlbp3I5kMUVGROH/hoqe3JKJ+6ujo6NOGia+99hp0Oh2ioqKQk5ODdevWoaNz3hoRkS94HFx0el23E3a7o9VoodO5v9ySiHzLZDJBKpXCaDSioaHBYcdmjUbD10FENOg8Di4R4eFobGpGxTdHkTJzeq/tjx77Fq1t7YiKjOy1LRF5R0hIiNtLngVBgEajAQDodDq89957UKvVsFgsUKvV2L9/P6ZPv/q9z1VERDQYPA4u05OnYfvOPXi7sBi33rQMaamzET0iyqldS0sryg9VYPPnXwIAkqZN6V+PichjEokEWVl3YsuWj3ttm5GRAaVS6XAsODgYFosFgiCgqqoKx48ft5/jKiIiGgweB5fbblmGwxXfoLWtHZs+/QKbPv0CwcHBCA8LhUwmg9lsRlt7OzQarf1rVEoll0MT+VBNDXDkyO8BfAKg+1EXiUSCF198EZMnT3b72hxtIaLB4HFwCQsNxU9+9BTe/deHOH7yNNA5nKzT6Vy2H5sQj9yHViIqiq+KiHyhpgbIzAQuX04BUAQgB4JgcVgaLQgCBEFAUVERFizovswBEZGv9Gt36OjoEXjmqSdx6XIdjlUdx6XLdWhr74DRaESQTIaQEBViR4/CdVMnI3Hc2AHrdFNzM7aUbse3x0+iqVkNpTIYI2OisSgjHWlzZ0Miubp3pNlsxtbtu7Bv/0FcqW+AVCpBQvwYLFu6BLNmJA9Yn4iGMltoqa21/jopKRuvvJKMoqKrJf9tS57z8vKQkpLi6y4TEbnUr+BiExc7GnGxowfiUr2qPXcBf/3764AFuD4jHfFj4tDR0YFde/dhbdG/8O3xk3h49Sp7+7fWrkNF5VHMmJ6EpTcsgslkwq49ZXj9zbV4cOW9PRbPIwoUFRXAhQvWnyclAaWlQGxsCpYtK8C0adOg1WqhUqnw85//3NddJSLq0YAEl8FU8tEn0On0eO6Z72PypAn24wsXzMMfnn8JZfsP4tabl2HUyBgcrjyCisqjSJs7G489nG1vmz5vLv74Qj5KNm7G7FkzEBYW6qNPQzQ47rkHePdd4H/+B/j3v4HY2KvnJBIJ5HI5BKGP9Q2IiHxgQILLqdM1OHi4EufOX0BLaxuiR0ThRz/8rv386eozGDMmFsrg4H7fa/bsmZg+fZpDaAEAZXAwJiSOw+HKI2huVmPUyBiU7bPuar1s6RKHtnK5HIsy0rHh409RfrgSmYsz+t0voqHugQeAFSsAmd/9c4WI6Kp+/RHW3t6Bt4vW49vjJx2Oq5SOAaVk4ydobGzG9598FOMT+zfXpbuQYTabcfHSZUilUsR2vraqPnMWQUFBSIiPc2o/cUKitU31GQYXGpI0Go29joo7utZRqakBtm8HHnnEsQ1DCxH5O4//GDObzfj762/h3HlrCf+oyEgkJibgcMURp3bqlla0d3TgjbfW4pc/z0NY6MC8mtHp9NDr9bhS34B/f/U1GhqbsHLFXYgID4NOp0d7ewdGxkQ7TNa1sa1uqm9odPt+fSmVTkOX7Tn68nm6E0qOHTuGY8eOAZ2vcyQSiX03ZkEQnGqszJ07F3PnzkVNDZCVJaC2VkB7u4gf/MD9fvnb/+ND4VnSwOHzDBzefIYeB5fde/fj3PmLUKmUyM1eiZmdK3Sefu4/HNpJpVL85pc/wSuv/hNna89jb9kB3LQss/89B/CXl1/DhYuXAADxY+Lw7FNPYsrkiQAAnd66tYBCoXD5tQq59Xh3y7ddqb90fgB6TUOFL59n1bcnUHX8hMMxvV5vDyUKhQKiKELsXKo8afx4TJo4Htt37YZOb0CwQo7FC9Mdvj44WIH9ey7ivlWxuHDB+q398l9NuOOWi+jm2wAAIIpmWEQRomhG3YVab3xcr+P3ZmDh86SeeBxcDlV8AwBYueIue2jpTrBCgVX334sX/vIKDlceGbDgsvrB+9He0YGmpmaU7SvHy6/+E7fenIU7br3Jja+2/oXQlwmJI+MSXI7ekH8RRRH1l8779HmGRcVgRspsh2ObN2+GVquFUqnEHXfc4XDO9hroYOURCJIOhISE4LrpMx3a1NQADzwk4MIF6//TSUkWbNkiQ2zsOHsb1yM9AiAIAARIFY5F5IZ6Gf+h8Cxp4PB5Bg5RFNFWXe2Va3scXC5cvASZTIa0ubPdaA2MGxuP0JAQNDerPb2lk8RxCfafL8pIxz/eXItPP9+CsQnxuG6KteKnbeTlWrbjwX2YMGwbrqfA4MvnGRoaitBrXpnK5XIYjUbI5XKMGjWq12t07bv19VDXOi1AaamA2FjHYP7NN9/g0KFDDseMRiMsFguMRiM++OADh3Nz5szBwoULPfmIg4rfm4GFz5N64vH/GTqtDiOiIvs0YhESqkJHly0ABpIgCFgw37pPypGjVVAo5IgID4Na3eLyXVtjYzMAYNSoGK/0h2iwOBeXs9Vpce/rbaMqQ3lkhYjIxuMRl2BlMLR9mB8iiiJaW9v6tSS6oaER+X9/AzEjRiDvme85nTcajfZ7AcDkSRNQfqgSZ2vPYcL4RIe2J09Zh7Cmds6JIfJHfQ0tKSkpmDLF/Y1OGWaIaKjxeMQlLnY02tracep0jVvtD1cegVarQ1yc5xV2R4yIgkQiwanqGqf7WiwW7O2s2zJ5kjWMZHROXtxSut2hrUajxc49ZQgJUWFOiuM8AaKhTBRF6PV668RdEbj33r6NtKhUKsTExLj9g8GFiIYaj0dcZqfMxKnTNXjn3ffww+8+jtGjR3bbtvLIMby7/kMA6FdQkEgkeGjVfXjtjQL8/fU1WLRwPuLjx0Cr1aH84GHUnK3FpIkTMC/VOu9m2tTJWJiehj1lB/DaGwWYM3sW9Ho9tu3YjdbWNjzx6GqnJaVEQ1FFRQXy8/NRVFQEk8kEmUyG0tJS/PzneXjqqRSMGdO310NERP5KOH78uMWNdk5MJhP+9OLLuFx3BVKJBEnTpiIuLhZffvU1IiLCkbk4A03Napw4dRp1dfUAgNjRo/DLn/0IUqm0X52+Ut+ALaXbcOp0DZqa1RAEAaNHxWDu7BQszVyEoC5VtkRRxPZde7F7zz5cqa+HVCrF+MRxuPXmLEyZ5P5rojClHKPjx3HCWAAQRRF1F2qH3PNct24dOjqsK4ZWr15tP15cXIzc3FwIggCTyWQ/LpPJYLFY8PvfF+Lxx7OHZWgZqs+SPMPnGThEUcSpU6e8cm2PgwsANKvVeP3NtTh/4VKvbRPi4/DdJx7GiKgoT2/nUwwugWOo/uHoKrhUVFQgNTUVZrO526+TSqUoLy8fljs6D9VnSZ7h8wwc3gwu/SoAHhUZiZ899zTK9pdjf/lhnK09B4PBaD+vUMgxftxYpKXOxvy0uf0eaSEabvLz83tduScIAvLz81FQUDBo/SIi8pV+71wilUqRsWA+MhbMh8VigVarhcFghEIh5/wRon4QRRHFxcUOr4dcMZlMKC4uxpo1a7jDMxEFPI+Dy8nT1ZBKpPbNCtH5Lz9rPYiB6h7R8KXVaqHvpoDitfR6PbRaLVcBEVHA8/gl4l//9gbW/esDN1oSkSeUSiXk8h42GepCoVBwhJOIhgWPg0t4WCiMxp6HsInIc2fPSiCTZfc6MCqTyZCdnc3XREQ0LHgcXFJmzUBTUzOqvj3hRmsi6gtbRVyNJs++IWh3LBYL8vLyBq1vRES+5HFwWXH3Hbh+4Xy8+XYRNn/+Jequ1A9sz4iGKa22axn/FIwZUwipVAqZzHHkRSaTQSqVorCwcFguhSai4cnjyblvrCkEAESPGIHPvvgKn33xFYKCZFCpVD0uexYA/O43v/D0tkQBy1bOPzhYCbncesxaxj8bdXXJTpVzc3JykJeXx9BCRMOKx8HF1Ssio9GElpbW/vaJaFhxVc7/3ntLER+fh/XrUxAbC8TGpqCgoABZWVlQq9WIiopCTk6Or7tORDToPA4u89PmgnMBifrHVTl/k8mEDRuKYLEUYuvWQmRnZ9vbSyQSKBQKTsQlomHL4+Dy8OoHBrYnRMNMRUUFcnNzXZbzt4WY3NxcJCcn83UQEVEnbgZB5CP5+fmds766ZyvnT0REVgwuRD4giiLefbcYZrN75fwtFo/3QiUiCih9flV0tvYcvtiyFdXVZ6HT6xEZEY5ZM5Nx841LERoS4p1eEgWYqiotDAb3y/mfO3cOKpUKBoMBJpMJBoMBDQ0NDu2s222w5D8RBbY+BZfygxVYu+5fEEXRfqyhsQmlX+/EwcPfIO+H30VMTLQ3+kkUMGpqgNtuUwJQAOg9vMhkMnz22WcQBAFarRYWiwVGoxElJSUO7VJTU5GamurFnhMR+Z7bwUXd0op3//UhRFFEsEKB5KSpCAkJQUNjE46fOAW1ugVvrX0Xv/jJM97tMZGPaTQaaDQat9t3HQmxVcQ9d04CIBtAEYDuXxdJpVKsXLkS9913n1v3ISIKdG4Hl127y6A3GDAmLhbPPPUdhIWG2s+dO38Rr7z2T5w7fwFHj32L6cnTvNVfIp+rqqpCeXm5wzHbSIggCE6bHdpGQmyhxVoRF5gwIQ+1tYVwsajIwS9+8QvExMQM+OcgIvJHbgeXb0+cAgCsXHGXQ2gBgLEJY3DbzTfiw4824QiDCwW4pKQkJCYmOhzbtGkTtFotlEolli9f7nDONhLy3/99NbRYK+KmYOvWQqc6Luh8PWSxWFjOn4joGm6vKrpypR4KhRxTJk90eX7mjCQA4J5FFPBUKhViYmIcfsjlcshkMsjlcqdztuDy2mvA0qW20ALExgLZ2dkoLy9HTk6OfS8iWzn/8vJyh+JzRETUhxEXjVaLUSO7H66OiowAAGi1uoHpGVGAUamATz4B2tuBUaOuHk9JYTl/IiJ3uR1cLBYLgoK6b27bWNFiEbttQzSc1NQAwcFAXNzVYyqV9YcrLOdPRNQ7FqAj8gLbRNzMTODSJV/3hogocDC4EA2wrquHTpwAnnrK1z0iIgocHm+ySERXiaIIvV4PQOmw5DkpyTopl4iIBgaDC1E/VFRUID8/H0VFRTCZTJBIZBDFUgB5SEpKsa8eIiKigdGn4FJ3pR5/evGv/Wwj4D9++mxfbks0JBUXFzvVYBFFU2c13EI880whYmO5nJmIaCD1KbgYjSacv9DzTEN32hD5u4qKCuTm5sLssuytNcQ880wuMjKSWUCOiGgAuR1cJk+cAHCVJgUYT/cdys/P73XZsiAIyM/PR0FBwQD0lIiI0JfgkvfM97zbEyIf8GTfoTlz5qC4uNihRL8rJpMJxcXFWLNmDWuzEBENEE7OpWHNk32HtFpt5wqi3un1emi1Wu7cTEQ0QBhcaFjo+kpIFEWo1S2QKhogkbguZSSRSOz7Dl3LbBahUCjcCi8KhcJp1IaIiDzH4ELDwrWvhIwGA0xms8tXQk1NTQgODkZYWJjTdWpqgBUrJLj55mx89llRj6+LZDIZsrOz+ZqIiGgAMbjQsND1lZAoimisu4S9Bw66fCW0adMml6MpXSvinj6dB1Es7PGeFosFeXl5Xvg0RETDF4MLkRu6hhYASEhIwTPPFOKZZxzruKBzpMVisaCwsLDbpdCuVjMZDAaYTCYYDAY0NDQ4nLOtZiIiGu4YXGhYqKiowKFDh+y/NhmNaG1rs78qeu+99+znmpubIZFIEBISArgILUlJ6KyIm42MjGSHyrkymQw5OTnIy8vrsX5LT6uZjEYjSkpKHM6lpqYiNTV1oH47iIj8FoMLDUsWwKl43Pnz51FaWooDBw7AbDZDJpNh48ZS7NqVh8uXrSHkamixfk1KSgoKCgqQlZUFtVqNqKgo5OTk9Hp/V6uZesLRFiIiKwYXGhZSUlIwZcoUoMscl/c3bLSPuMhkMrzwwgtAl0BjMpnw4YfW8v1AIZKSsrvde0gikUChULg9EZevfoiIPMPgQsNC16AgiiLMeo09ZNTV1eH3v/99j+X7gVy88koyYmNZvp+IyJdcF7EgGkZ2797d60iJVCqgqCh/0PpERESuMbjQsCaKIr755ptey/ebzdby/RaLZdD6RkREzviqiAJST5sn2irnms1mdHR0dPOKyBnL9xMR+R6DCwWknpYbX7x4Edu3b8f+/fshiqLb17SV72cNFiIi32FwoYDU3eaJO3bsQEFBQZ+v17V8P2uwEBH5jl8GF41Gi9Kvd6Dim6NoaGyCIABxsaORsWA+MhbMc5hoaTabsXX7LuzbfxBX6hsglUqQED8Gy5YuwawZyT79HOSenl77uGIb4bh2lOPSpUsehRZcU76fNViIiHzH74KLuqUVL+b/HS0trUifNxdZmYuh1Wqxc3cZ3v3Xh6i7cgUr7r7T3v6ttetQUXkUM6YnYekNi2AymbBrTxlef3MtHlx5LxZfv8Cnn4d613WEQxRFiKIInU5nr8ESHBzs0H769OmYPn06cM1rmnfeeafP93ZVvp+vfoiIfMfvgsvHn3yO5mY1Vq64C5lLrrcfXzA/Db9//kWUfr0TN2bdgPCwMByuPIKKyqNImzsbjz2cbW+bPm8u/vhCPko2bsbsWTMQFhbqo09D7ug6wnH06FEcPXoUer0eoihCIrEujOsaYqqqqnD8+HGgy2saURTtx9zlbvl+IiIaPH4XXKKiIjA7ZQYyFsxzOK5SKTFpwngcrjyCi5fqEB4WhrJ91n+lL1u6xKGtXC7Hoox0bPj4U5QfrkTm4oxB/QzUN11HOObNm4fp06dj06ZN9p2d0TnH5Npdnm1fW1FRYa+K664XXngBcXFxbpXvJyKiweN3wWX57bd0e06j1QIAVJ1/mVWfOYugoCAkxMc5tZ04wfov+OrqMwwufsQWYuRyOYxGI+RyOQDYfx4TE+PQvri4GLm5uX2+j0Qicbt8PxERDR6/Cy7duXDxEk6drsGokTEYmzAGOp0e7e0dGBkTbX+d0FVUVCQAoL6h0e179GXpLPlG12dUUVGB3Nxct+u0dGXbGZrPfGizPR8+p8DA5xk4vPkMAyK4NDer8cZb70AQBDy06j4IggCdXg901t5wRSG3HtfpdG7fp/7S+QHqMfWXXqeF0WCAXmoNpbaf112otbf50/P/z6NrR0VFwWwyQa/TOlyPhi5+bwYWPk/qid8Hl7O15/H6m2+jo0ODR3MfxJTJE938Smvp9r68DhgZl+By9IYGnyJYCZNZhCLY+lrQ9vPR8eOAzrS/8eNPPBptWbVqFQSJBIJECqnCcfUQVxQNLaIoov7SeX5vBgg+z8AhiiLaqqu9cm2/Di4Hyg+jaP0HkMuD8MPvP4GpUybZzyk7l8jaRl6uZTt+7VLankgkEn4zDXG256PVaqHv5tn3JDMzE9OmTYPBYIDRaMRHH33kcJ7F5IYmfm8GFj5P6onfBpctpduw4eNPMSYuFt/7ziOIiR7hcF6hkCMiPAxqdYvDslmbxsZmAMCoUY6TOSkwKJVKyOUKGAzuhZfx48fj17/+Ne65554e23G0hYjIt/wyuGzfuQcbPv4USddNwXcey0VwsOt5LJMnTUD5oUqcrT2HCeMdK52ePGUdwprq9qslGqpEUYRer7cvjQaAs2clkMmyYTAUAeh+52dBEJCSkoJDhw4NUm+JiKg//C64VNecwfslH2PSxPH43pOPIkjW/UfIWJiO8kOV2FK6HU8+fnVJrEajxc49ZQgJUWFOysxB6jkNtHPnzmHbtm3Ys2cPTCYTZDIZSktLsXJlHn7wgxRoNHkACnu9TkYGl8MTEfkLvwsu75dsgiiKmDk9Cd8cOeayTVzsaMTFjsa0qZOxMD0Ne8oO4LU3CjBn9izo9Xps27Ebra1teOLR1Q7/Sif/sWvXLvzjH/+AIAj2ZXcmkwmFhUV4++3CzsCSjTFjClFXlwtBEGAyXR15kUqlEEUR9913H2JjY334SYiIqC/8LrjUnrMuk/to02fdtrn9lhtxx203AQAeWnUfEhLisXvPPqx/vwRSqRTjE8che9UKTJnE10T+pqKiAr/97W/x8ccfA52l/rsym23hJBcTJiRj9+5s1NUlIz8/H0VFRfaRmfvuvQfRI0dh9OjRPvgURETkKb8LLn/P/3Of2kskEmQuzmB13ABgq4LrXmEjAfPn5yM2tgCxsSkoKChAVlYW1Go1oqKisOyGRXjz7UKn4ENEREOb3wUXGp76XgXXhI8+KobFsgaCIECj0cBkMkEqlcJoNEKtboHFYrEHl4aGBoevZr0WIqKhicGF/EJ+fn6f9w7S6/XQarVQqVSoqqpCXV0dRFFEa2srSrftcGhbUlLi8GvWayEiGpoYXGjIE0URxcXFDpNr3aFQKOyTr5OSklBZWWnfRXpB2lx8/Onn9jkvK1ascPhajrYQEQ1NDC405HlSBdcWRhobrZtoNjc3O1zn4uXLMJvNEEURZrMZly5dcqqizPBCRDT0MLjQkKdUKqFQKPoUXkRRxKRJk+yvgNRqNbRarf18fX29Q9uNGzc6VFeeNm2a0ygMERH5HoMLDXkSiQTZ2dn25cw9EQQBEokEb775Ju6880778ebmZrS0tACdS6hb1U0IjxxhnzejUCgcRlyioqK89nmIiMhzDC7kF/Ly8vDOO71Xwb3rrrvwu9/9DikpKQ7HY2Ku7kkliiLqLtRidPw4buRGRORnGFxo0Gg0Gmg0Grfbd12SHB6egqioQjQ25gIQHPYfso2afP/738err77qhZ4TEdFQweBCg6aqqgrl5eUOx7RaLSwWCwRBcNp+wbYkuaYGyMwEGhuzASQjIiIfHR1Xq+BmZGRgyZIlmDZt2iB/IiIiGmwMLjRokpKSkJjouEv3pk2b7EuUly9f7nBOpVLZQ0ttre0aKSgtLcCXX16tgisIAjo6OgbzoxARkY8wuNCgcVWNVi6Xw2g0Qi6XO8xDAeAitAClpUBsrHXCrkKh6HNROiIi8m+cmUhD1nPPuQ4tREQ0fHHEhbzC3Ym4BoMBJpPJ5R5Eb70FnD0L6PUMLUREZMXgQl7h7kTchoYGGAwGlzs+R0cDW7YARiNDCxERWTG4kFf0NhF3woQJeP311/HBBx/YVwdt21aOH/0oD4sWXa3BEh3tg84TEdGQxeBCXtHTRNwDBw7gySefhCAI9kq4JpMJH3xQhA8+KMSbbxbiiSeyXV7X9grK9orJYDDYv95gMKChoaHXfhARkf9icKFBde7cOfzjH/9w+WrIVlTuySdzkZaW7FT9Fl1eQTU1NUEURbS2tkKpVMJiscBoNNr3JrKx1YIhIqLAwOBCg+qrr77qtY1EIiA/Px8FBQVO52yvoHqq/9IVR1uIiAILgwsNGlEUsX///m5GW64ym00oLi7GmjVrnOq02F799FT/hYiIAheDCw0a27wUd+j1emi1WqhUKpdLq7vOceG8FiKi4YPBhQaNTCaDVCp1WbPlWgqFwr5kuqel1ZzXQkQ0vDC4kNdVVFQgPz8fRUVFboUWmUyG7Oxs+2siV0ure8LRFiKiwMXgQl5VXFyM3Nxch6XPvbFYLMjLy7P/mq9+iIjIhsGFvKaiogK5ublujbKgc6TFYrGgsLDQ5VJoIiIibrJIXpOfn+/27s0ymQw5OTkoLy9Hdrbr4nNEREQccSG3uLtpok1wcDCKi4vdej0kk8mwZs0a5Obm9rOXREQU6BhcyC3ubppok5ycDL1e79a1TSYTjEbjgPaXiIgCE4MLuaW3TROvrV5bVxcMQAGg9/Aik8kgl8sHvM9ERBR4GFzILT1tmnht9dqaGuD22wEgG0CRfQ8iVyQSCebPn+/2XBgiIhreGFyGub7OXeltaXJNDZCZCdTWAkAegMJer5mVleX2/YmIaHhjcBnm+jp3pWtVWlEUodfr7W0cQwuQlJSCZ54pxDPPONdxkUqlAIAnnngCcXFxLN1PRERuYXAZ5vo6d0WlUjlUwjWZTJDJZNi4sRS7duXh8uWUzusCpaVAbGw2MjKSHdpLpVKkp6cjKysLMTExMBgMLN1PRERuYXAZ5voydwXdVMI1mUzYsKEIolgIoBBJSdmdocX6NSkpKSgoKMANN9yA5uZmRERE4K677nKrb0RERF0xuJDbeqqEK4q210C5eOWVZMTGOle+DQoKQkhICBQKhVMgIiIicgcr55Lb3KmEK5MJKCrKH7Q+ERHR8MLgQm4RRdGtSrgmkwnFxcWwWCyD1jciIho+GFzILVqt1u1KuHq9Hlqt1ut9IiKi4YfBhdyiVCqhUCjcaqtQKJyWURMREQ0ETs4lt0gkEtx5ZzY+/LDnSrgymQzZ2dnQarVOhe0MBgNMJhNrthARkccYXMjJtYXl0Flcbteu3ivhWiwW5OXl9VjYjjVbiIjIUwwuZOeqsFxpaSlWrszDD36Q0llcrhBALqRSAWbz1ZEXmUwGi8WCwsJCpKSkQKPROBW26wlHW4iIyB0MLgT0UFiusLAIb79d2BlYspGUlI1XXklGUZFjwMnJyUFeXh5SUqz1W/jqh4iIvMGvg8uesgP4YMPH0On0+P1vfoHo6BFObcxmM7Zu34V9+w/iSn0DpFIJEuLHYNnSJZg1I9kn/R5qeiosd3VUJRcTJiSjtDQFsbEpWLasAFlZWVCr1YiKikJOTs6g95uIiIYfv1xV1Nbejjfeegfr1n8AUey5Xshba9dhw8bNiIqKxKr778E9y2+HXq/H62+uxY5dewetz0OZO4XlAAHz5+fby/ijc8KuQqFw42uJiIgGhl8Glz+/9Apqztbiqe8+hsSxCd22O1x5BBWVR5E2dzZ+8OSjWJiehsXXL8CPn/0BRsZEo2TjZrS1tQ9q34cadwvLASZ89BELyxERkW/55auiiePHYeV9dyMsNBT/3vJ1t+3K9llXtSxbusThuFwux6KMdGz4+FOUH65E5uIMr/e5OxqNxmnZcE8Geu6IJ4XlOHeFiIh8xS+Dy+OPrHarXfWZswgKCkJCfJzTuYkTrCteqqvP+DS49LRsWBAEp0JuA71s2FZYzp3wIpfL0dHRYQ9arMtCRESDzS+Dizt0Oj3a2zswMiYaEonzG7GoqEgAQH1Do9vXFEVxQPsIANdddx3Gjh3rcGzz5s3QarVQKpW44447HM6pVKoB78eDDz6IoqJ1DsubryWVSpGWloYNGzbYj/VUl2Xu3LmYO3fugPZzoNh+/7zxPGlw8VkGFj7PwOHNZxi4waVzBKG7MvUKufW4Tqdz+5r1l84PUO96ZhHNsIgiLKIZZr3ja6Q2vQZtzQN7vxszH8LatUU998liwU+f+xGmXXedW9cMDlag7kLtAPXQOwbreZL38VkGFj5P6knABpfeWSeZ9mVFzMi4BJejNwNNEayEySxCEazE6PhxXr1XTQ3w6/9KtBeWAwSHkv62wnJr167F3Svu92pfBosoiqi/dH7Qnid5D59lYOHzDByiKKKtutor1w7Y4KIMDga6jLxcy3Y8uLOdOyQSyaB/M3nzfjU1QFYWUFsLANmYMCEZ8+fn48MPuy8sF0h88TzJO/gsAwufJ/UkYIOLQiFHRHgY1OoWiKLo9E3Q2Gh93zJqVIyPeuhbNTVAZqYttABJSegsLleAwkIWliMioqEpoCPt5EkTYDKZcLb2nNO5k6esQ1hTJ0/0Qc98y3Vogb24HAvLERHRUBXQwSVjYToAYEvpdofjGo0WO/eUISREhTkpM33UO9+5cgVo7pzge21oISIiGsr87lVRY1OzwwhKW4e18u3RquMIDQ0BAESPGIHEcQmYNnUyFqanYU/ZAbz2RgHmzJ4FvV6PbTt2o7W1DU88utqpTspwkJ4O/PvfQF4e8NFHDC1EROQ//C64nDh5GkXF7zsd/9cHH9l/nj4vFQ+vfgAA8NCq+5CQEI/de/Zh/fslkEqlGJ84DtmrVmDKpOH3mshmwQJgzx6Ab4OIiMif+F1wWZiehoXpaW63l0gkyFyc4dPquL5WUwO8+y7wq185BhWGFiIi8jd+F1yGM0/2NaqrU9kn4jY2Ai+9xMBCRET+i8HFj/R1X6PExFR897up9tVDn38O/Nd/ARERg9lrIiKigcPg4keSkpKQmJjocGzTpk32fY2WL19uP37+PPDAAyqnJc/XhhZXozjcPJGIiIYqBhc/4io0yOVyGI1GyOVyxMRYi+nV1AB33919nZauehrFcbV54kDvTk1ERNQXDC4BprfictdyNYrTE462EBGRLzG4+DlRFKHX66FUKvscWsBXP0RE5GcYXPxURUUF8vPzUVR0dUPEmJhSXL6cByCFFXGJiCggMbj4oeLiYuTm5kIQBJhMJgCAyWRCfX0RgEKMGVOI0tJshhYiIgo4Ab1XUSCqqKhAbm4uzGazPbTYmM0mAGbU1eWirq7CZ30kIiLyFgYXP5Ofn9/rrs2CICA/P3/Q+kRERDRYGFz8iCiKKC4udhppuZbJZEJxcTEsFsug9Y2IiGgwMLj4Ea1WC71e71ZbvV4PrVbr9T4RERENJgYXP6JUKiGXK9xqq1AonLYAICIi8ncMLkOQrTaLKIoOx8+elUAmy+51MZhMJkN2dnavc2GIiIj8DZdDDyGuarOUlpYiLy8P4eEpyMwENJo8AIU9XsdisSAvL2/Q+k1ERDRYOOIyRBQXFyM1NdUeWtA5ybaoqAhz56Zi3rzizoq4KRgzphBSqRQymWPulMlkkEqlKCwsREpKim8+CBERkRdxxMVLXO263J0jR47Ya7NcyxZiGhtzASQjKSkFpaXZqKtLdhqdycnJQV5eHkMLEREFLAYXL+lp12VBEBwmzr799ttuXFFAREQ+SksLEBsLxMamoKCgAFlZWVCr1YiKikJOTo4XPgkREdHQweDiJa52Xd60aRO0Wi2USiWWL18OdE7EfeaZZ1yOtjgyQastxujRawBcnXQrkUigUCg4EZeIiIYFBhcvcbXrslwuh9FohFwuR0xMDACgo6MDBoPBrWsaDNbaLNzNmYiIhisGFx9TKpVQKBRuFZZTKBTo6OhwmDtjMBhgMplgMBjQ0NDg0N5VeCIiIvJnDC4+JpFIkJ2d7bCayBWZTIabb74ZGzZscDhumzdjNBpRUlLicC41NRWpqale6zsREdFgY3AZAvLy8vDOO73XZvnVr36FyZMnu31djrYQEVGgYXAZAO4ufba91rl2Im54eAqiogo7lzwLAK6OvMhkMlgsFhQWFmLBggVe6T8REZG/YHAZAO4ufW5qanIq419TA2RmAo2N2QCSERGRj44O1mYhIiJyhcFlALi79Nl2LDQ0FOgSWqwVcdFZXK4AX37J2ixERESuMLgMAHeXPtuOSaVSF6EFKC0FYmNZm4WIiKg73KtoEHXd9fmNN1yHFiIiIuoeR1wGgatdn1evLsXdd+fhxIkUhhYiIiI3Mbh42e7du/HII49AEASHXZ/XrSuCxVKI114rRGxstq+7SURE5BcYXNykVrdAqmiARNL72zXbfJdz587hH//4h9NKInTZ9fkHP8jF/PnJXDVERETkBgYXN5Vu2wGpTGYPITqdzr7cOTg42KHt9OnTodPp8MUXX8BisfR4XUEQkJ+fj4KCAq/2n4iIKBAwuLgp64bFqGtsxrFjx4AuwcX2wxZgzp8/j8LCQuzfv9+NHZ+tIy/FxcVYs2YNVxERERH1gsHFTZGRERg7YRJmzJgBdKnJ0tbWBqVSCaVSCaPRiBdeeAEA3AotNno9d30mIiJyB4NLH6hUKnvxOFtNFolEAplMhkuXLuG3v/1tnwKLjUKhsFfXJSIiou6xjssA+fzzzz161SOTyZCdnc3XRERERG5gcBkAoihiz5499pVCfWGxWJCXl+eVfhEREQUavirqJ1EU0d7e3ufQ0nXXZy6FJiIicg+Di4dqamqwadMmlJeX93lei1Qq5a7PREREHmBw6aOKigo8++yz2L59e5+/ViKRYN68eXj66ae56zMREZEHGFz6oLi4GA8//LDLSrjuuummmzgRl4iIyEMMLm46euwYHnnkEY9Ci20+y/e+9z2MHTvWK/0jIiIaDriqyE3/fMuzyrYymQw5OTkoLy9HRkaGV/pGREQ0XAT8iIvZbMbW7buwb/9BXKlvgFQqQUL8GCxbugSzZiS7fZ2PNm7q88qhF198EaNHj7bPZzly5Eif+09ERERXBfyIy1tr12HDxs2IiorEqvvvwT3Lb4der8frb67Fjl173b6OwWDo031lMhlCQ0M5n4WIiGgABfSIy+HKI6ioPIq0ubPx2MPZ9uPp8+bijy/ko2TjZsyeNQNhYaG9Xksul7sdXqRSKTIyMhhaiIiIBlhAj7iU7SsHACxbusThuFwux6KMdBgMBpQfrnTrWjctu7tPOe+WW25xq51Go0FDQ4PDD4PBAJPJBIPB4HROo9G43QciIqJAE9AjLtVnziIoKAgJ8XFO5yZOSLS2qT6DzMW9T5rdX/4LACW9tpNIJCgsLIQoiujo6Oi1fVVVFcrLyx2OabVaWCwWGI1GlJQ43jM1NRWpqam9XpeIiCgQBWxw0en0aG/vwMiYaEgkzgNLUVGRAID6hka3rqdWzwVQCCAXUqkAs9l5ou6SJUuQn5+PlJQUFBUV2UdNrly5AnTOk7n2WExMDG644QYEBwe7tUO0SqXqVx0Zgv33j7+P/o/PMrDweQYObz7DwA0uej0AQKFQuDyvkFuP63Q6t673zTcnAaTi229LsHbtWnzyyScwGAyQy+W46aabkJ2djeuuuw5msxkHDx5ESEgImpqaYDAY8O677zpcy2g04l//+pfDsUmTJmHy5Mm99kOtVrvVX+pdW3W1r7tAA4TPMrDweVJPAja49M4CAH2eQDtt2jQ8//zzeP7553tsN3PmTMycObNfPSQiIiJHATs5VxkcDHQZebmW7XhwZzsiIiIa+gI2uCgUckSEh0GtbnH5rq2xsRkAMGpUjA96R0RERJ4I2OACAJMnTYDJZMLZ2nNO506esr5DnTp5og96RkRERJ4I6OCSsTAdALCldLvDcY1Gi517yhASosKcFM5DISIi8hcBPTl32tTJWJiehj1lB/DaGwWYM3sW9Ho9tu3YjdbWNjzx6Gq3liATERHR0CAcP37c4utOeJMoiti+ay9279mHK/X1kEqlGJ84DrfenIUpk/iaiIiIyJ8EfHAhIiKiwBHQc1yIiIgosAT0HJf+MpvN2Lp9F/btP4gr9Q2QSiVIiB+DZUuXYNaMZF93j7qh0WhR+vUOVHxzFA2NTRAEIC52NDIWzEfGgnkORQf5jP1L1fET+NtrbwEA/p7/Z4dzfJb+obrmDD774iucqT0Hk8mMkTHRWJg+D5lLMvi96UfULa348quv8e3xk2hqbkZwcDBGjxyJJYsXYk7KTK8+S74q6sEba95BReVRzJiehNmzZsBkMmHXnjKcO38RD668F4uvX+DrLtI11C2teDH/72hpaUX6vLmYNHECtFotdu4uQ92Veixbuhgr7r7T3p7P2H/odHr8z5//guZm67YX1wYXPsuh73DlEbxZUIQxcbFYlJEOmUyGfQcO4uSpamQuuR4rV9xlb8vnOXTVXanHS/mvwmA0YlFGOsbGj4FWr8e+/QdxtvYcFmUsQPYD99rbD/Sz5IhLNw5XHkFF5VGkzZ2Nxx7Oth9PnzcXf3whHyUbN2P2rBkICwv1aT/J0ceffI7mZjVWrrgLmUuutx9fMD8Nv3/+RZR+vRM3Zt2A8LAwPmM/s+HjzWhv78DoUSNRd6Xe4Ryf5dCn0Wiwbv0HiB8Th5/mPYWgoCCg8xm99NfXUF1zBjqdHsHBCj7PIe6LL0vRodE4hY7rF8zDH55/CTt378VNWUsQExPtlWfJOS7dKNtXDgBYtnSJw3G5XI5FGekwGAwoP1zpo95Rd6KiIjA7ZQYyFsxzOK5SKTFpwnhYLBZcvFQH8Bn7leMnTmHXnn24/ZYbER4W5nSez3LoK9t/EBqNFnfedpM9tACAVCrFz3/8NH7xk2cRHGzd/JbPc2hraGwCAEyaON7heFBQEMaNTbC2abJWp/fGs2Rw6Ub1mbMICgpCQnyc07mJExKtbarP+KBn1JPlt9+CJx/LhVwudzqn0WoBAKrO2j18xv5Bp9dj3foPMDYhHjdmLXHZhs9y6DtWdRwSiQTTrpsCALBYLDAYjC7b8nkObXGxsQCAK1canM41NjVBIpEgdtRIwEvPkq+KXNDp9Ghv78DImGhIJM7ZLioqEgBQ39Dog96RJy5cvIRTp2swamQMxiaM4TP2Ixs3fYaWllZ8/8lHXT4rPkv/cOlyHaIiI9DUrMaGjZtRdfwkTCYTwkJDMX/eHNx52y2Qy4P4PP3AzTdm4psjR/HBho8hCAISE8dCr9Nj194y1J67gJuWZSIyMsJrz5LBxQXbztEKhcLleYXcelyn0w1qv8gzzc1qvPHWOxAEAQ+tug+CIPAZ+4kTJ09jx669uOPWGzEmLtZlGz5L/9DeoYFKpcTLf38Ds1Nm4vFHHoJOp8euPWX4ausOXLhwCU//4Dt8nn4gekQUfvbjp/F24Xq8seYd+/GgIBnuv3c5lt6wCPDi9yaDi0esC7G6Lveioels7Xm8/ubb6OjQ4NHcBzHF7U01+Yx9zWAwYN36DzAmLhY337i0H1fisxwKzGYzWlpacd89dyIrc7H9+LzU2XjhL6/g2xOncPTYt0hIiO/lSnyevtbQ0IjX/vk22trasfz2W5AQHwedTo+Kb47igw2b0NDY5LBCrHuePUsGFxeUwcFAl7R4Ldvx4M52NDQdKD+MovUfQC4Pwg+//wSmTplkP8dnPPR9tOkzNDWr8fMfPw2pVNptOz5L/6CQy6HV6TAvbY7DcYlEggXz03Du/Mc4ceo0pky2fp/yeQ5dRcUfoO5KPX723A+ROG6s/Xha6mzI35Xj6+27MGXSRCRNmwp44Vlycq4LCoUcEeFhUKtbIIqi0/nGRuts6VGjYnzQO3LHltJtKCgsxsiYaPz8x884hBbwGQ95p6prsH3nHiy+fgFCQ0PQrFbbf5hMJgCw/5rP0j9ER48AAEhdzHUID7euFNPp9HyeQ5xOr8ep6hpEj4hyCC02s2ZaC8pVHT/htWfJEZduTJ40AeWHKnG29hwmjE90OHfyVDUAYKrbrx1oMG3fuQcbPv4USddNwXcey7UvsbwWn/HQdfz4KVgsFmzbsRvbdux22eY///t5oLMQHZ/l0Ddp4nicv3ARtecvYtrUyQ7nGjuXzkZGRAD83hzSjEYjLBaL/R8QTuc7V4rZznvjWXLEpRsZC9MBAFtKtzsc12i02LmnDCEhKsxJmemj3lF3qmvO4P2SjzFp4nh878lHuw0t4DMe0tJSZ+P7Tz7q8odtkq7t1+Cz9Au27TY++2KLw7++DQYjdu0uAwDMnJFkbcvnOWSFhYZi1MgYqFtaceLkaafzBztrsthqvHjjWXLEpRvTpk7GwvQ07Ck7gNfeKMCc2bOg1+uxbcdutLa24YlHV0PZWQ+Eho73SzZBFEXMnJ6Eb44cc9kmLnY04mJH8xkPYaNHjcTozjoQ1/qq8w/AmdOT7Mf4LIe+hPgxuPWmLHz276/w8t//ifT5c6HV6rCn7ADqGxpxw+IMjO2cmMvnObTdv+IuvP7mWrz2z7exKCMd8WPioNfrUXnkGL49fhITJyQifV4q4KVnyb2KeiCKIrbv2ovde/bhSn09pFIpxieOw603Z2HKJA5TDkU/zPtFr21uv+VG3HHbTQCfsV/Kf+V1nDxd7bRXEZ+lf9h34BC27diFi5fqYLFYEBc7Gosy0nH9wvkO7fg8h7Zz5y9gS+l2nDpdjda2dshkMowaGYPUObOw9IZFDtWRB/pZMrgQERGR3+AcFyIiIvIbDC5ERETkNxhciIiIyG8wuBAREZHfYHAhIiIiv8HgQkRERH6DwYWIiIj8BoMLERER+Q2W/Cci6qPGxib89g/Wyr052SuxMD3N110iGjYYXIj82J6yAygqft/jr+dfukTkbxhciPxYSIgK8WPiXJ67Ul8Po9EEuVyOkTHR3X49EZE/YXAh8mOzZiRj1oxkl+f++EI+Lly8hMSxCch75nuD3jciIm/g5FwiIiLyGxxxIRrmuk40/WneU9DrDfh48+e4eKkO48eNtY/W/DDvFwCA22+5EXfcdpPLa+W/8jpOnq7GlEkTXY7y6HR6fL1jF745cgxX6hug1xsQEqLC2IR4zE+dg9S5KRAEwa1+GwxG/PI3f4BOr8fC9DTkZK/stm3NmbN4Mf9VAMAjOaswP22u/dyFi5ewbcdunDpdg2a1GiaTGSqVEgnxY5A+LxXzUme73ScMwd8nokDD4EJEdg2NTVi3/kMIAhAdHYXQsJABu/aFi5fw6utroG5pBQBERoQjRKVCY1Mzjh77FkePfYu9+8vx5GO5UCjkvV5PLg9CyqwZKNtfjspvjsH8gBlSqdRl24OHKgEACoUcKTNn2I/v3rsPxe9tgCiKEAQBEeFhCAoKQnOzGt8eP4lvj5/EN0eO4bGHsyGRDM4A9UD/PhEFGgYXIrL7aut2XDd1Mh7NfRDK4OABu65Go8GrbxRA3dKK+DFxyH3oAYxNGAMAMJvN2LZjNz7e/Dmqvj2BDzZ8jNUP3u/WdeelzUbZ/nJ0aDQ4cfI0kqZNdWpjsVhwsOIbAMDsWTPsf9k3NDRi/fsfQRRFTJ40AY/krMKIqCgAgMFgwMZPPsfX23fh4OFKTE+ehgXzUwfs96M73vp9IgoknONCRHb19Y14ZPWqAQ0tAPDV1zugVrdAqVTi6R88Yf/LGACkUimyMhfj7jtvBzqXeDc2Nrl13eumTEZ4eBgA4ODhSpdtTtecgVrdAgAOr4gqjxyDKIoAgAfuu9seWgBALpfj/nuXI3qE9Vj5oQoPPnXfeev3iSiQMLgQkd305OugUikH/Lr7DxwCAMxLnY3wsDCXba5fOA9BQTJYLBYc6hwh6Y1EIkHqnBQAQOU3V4NIV7bXRJER4Zg6ZZL9eFbmYrz80h/xx9/92uWSckEQMHZsPNA5D2gweOv3iSiQMLgQkV38mDFutOqb1rY2NDY1AwDGxMV2204ulyMu1nq+9twFt68/L3UOAKC9owMnTlU7nBNF0f6X+7y0uU7zVCQSCSIiwru9tkKuAAAYjSa3++Mpb/8+EQUKznEhIrvQ0IEvSKdWt9p/vv79DVj//oZev6a1tc3t6yeOS8CokTG4Ut+AQ4crMW3qZPu5U6dr7NeanzbH6Wt1ej127S7DkWPf4nLdFWg0WphM3g8prnj794koUDC4EJGdROJ6VU5/GIwG+8+jR0Qh2I35M7Z5K+6alzoHmz//EhWVR7Hq/nvsIyu2uSljE+KdRjEaGhrx8qv/tI9yCIKAyMgIKIOD7UuNm9VqaDTaPvXFU4Px+0QUCBhciGjAWGBxOmZ73QIAd915K9Lmzh7w+85LswaXtvZ2nDpdg6lTJsFsNuNwxRGgm9GWwuL30djUDEEQcOftN+OGxRlOk5LfWfceyvaXD3h/ffX7RBQIOMeFiPrEYnH+S9fG1ehEVFSE/efNzWqv9GlkTDTGJ44FAPuclhMnT6O9owMSicQ+D8ZGrW7BqdM1AIDrF87HrTdluVxJ1dHR4XGfhuLvE1EgYHAhIrcEBVkHaDVa169ONBot6q7UOx0PDQlBTOcmj9dOnr1Wf+aX2MJJ5ZFjQJfl0cnTpiIsLNShbVNzs/3nkydNcHk9rU6HU9U1fe7HUP99IvJ3DC5E5JbICOuIQG3teZfnS7ftgNlsdnluXudrj6pvT6D2nOuvb1ar8R//+Qfkv/I6Ll+u63P/UuekQCKRQK1uwbnzF+0BpmvtFhuF4uprmfZ2jcvrbdi4GXq9dd6J0WR0ux9D/feJyN8xuBCRW2w1UGrO1uLr7bvsf/nq9Hp88eVWbCndhokTxrv82qzMRYiMCIfFYsE/3lyLb4+fdDh/8lQ1Xv77P6HV6dDa1oaRI2P63L+wsFD7iqLPvtiC9vYOBAcrMGum8+7ZsaNHISTEuoJq67YdqG9otJ+7Ut+At95eh8OVR5CVuRgA0N7e4XKUxJWh/vtE5O84OZeI3HLzskwcPFQJrU6H90s+xoaNm6FUKtGh0cBisWDlirtwue4KqmvOQLQ4FoJTqVT4wXcfw6tvFKClpRWvvPYmQkNDEBoSgpbWNmg7X6uMjInG9554pNs9h3ozL20Ojn17AhXfHAUAzJ09C0FBQU7tpFIplt9+C9a/vwGNTc34/R9fRPSIKBiNRrS0tiFYocB3Hs+FTCrFV1u3w2Kx4E8vvozIiHD8169/5ve/T0T+jMGFiNwSExONnz33ND799xacPHka7R0aWGDBdVMnY1nmYiRNm4p/ffAR0E3BtoT4MfjNL3+C7Tv24Jsjx1BXX4/6hkaEhoZgTNxozJ09C+nzU/u13cCsmdMhlwfBYLC+2pk/z/k1kc3i6xcgNDQEpV/vxIWLF6FuaUFERAQWZaRj2dIlGNk53+SWm5Zi99790On0UCp7ryrsD79PRP5MOH78ePdT34mIiIiGEM5xISIiIr/B4EJERER+g8GFiIiI/AaDCxEREfkNBhciIiLyGwwuRERE5DcYXIiIiMhvMLgQERGR32BwISIiIr/B4EJERER+g8GFiIiI/AaDCxEREfkNBhciIiLyGwwuRERE5DcYXIiIiMhv/H++7bykYrwQzgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n", + "ax.plot([0, 80], [0, 80], \"b--\", lw=2)\n", + "\n", + "yerr1, yerr2 = median - q1, q2 - median\n", + "yerr = torch.cat((yerr1.unsqueeze(0), yerr2.unsqueeze(0)), dim=0).squeeze(-1)\n", + "markers, caps, bars = ax.errorbar(\n", + " test_Y.squeeze(-1).cpu().numpy(),\n", + " median.squeeze(-1).cpu().numpy(),\n", + " yerr=yerr.cpu().numpy(),\n", + " fmt=\".\",\n", + " capsize=4,\n", + " elinewidth=2.0,\n", + " ms=14,\n", + " c=\"k\",\n", + " ecolor=\"gray\",\n", + ")\n", + "ax.set_xlim([0, 80])\n", + "ax.set_ylim([0, 80])\n", + "[bar.set_alpha(0.8) for bar in bars]\n", + "[cap.set_alpha(0.8) for cap in caps]\n", + "ax.set_xlabel(\"True value\", fontsize=20)\n", + "ax.set_ylabel(\"Predicted value\", fontsize=20)\n", + "ax.set_aspect(\"equal\")\n", + "ax.grid(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "code_folding": [], + "customInput": null, + "hidden_ranges": [], + "originalKey": "34e976cd-7d09-40d2-8987-aecdefa7c0fd", + "requestMsgId": "34e976cd-7d09-40d2-8987-aecdefa7c0fd", + "showInput": false + }, + "source": [ + "## Look a the lengthscales from the final model\n", + "\n", + "As SAASBO places strong priors on the inverse lengthscales, we only expect parameters \n", + "0 and 1 to be identified as important by the model since the other parameters have no effect.\n", + "We can confirm that this is the case below as the lengthscales of parameters 0 and 1 are \n", + "small with all other lengthscales being large." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "code_folding": [], + "customInput": null, + "customOutput": null, + "executionStartTime": 1668655927129, + "executionStopTime": 1668655927142, + "hidden_ranges": [], + "originalKey": "33147b57-ea6b-4c67-9c7d-796bb54d5c84", + "requestMsgId": "b32e63df-16ee-45f1-af94-7f6f4bb78173", + "showInput": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter 0) Median lengthscale = 7.38e-01\n", + "Parameter 1) Median lengthscale = 2.35e+00\n", + "Parameter 12) Median lengthscale = 5.04e+02\n", + "Parameter 29) Median lengthscale = 7.27e+02\n", + "Parameter 27) Median lengthscale = 7.72e+02\n", + "Parameter 7) Median lengthscale = 9.16e+02\n", + "Parameter 3) Median lengthscale = 9.53e+02\n", + "Parameter 16) Median lengthscale = 9.84e+02\n", + "Parameter 8) Median lengthscale = 1.04e+03\n", + "Parameter 9) Median lengthscale = 1.05e+03\n" + ] + } + ], + "source": [ + "median_lengthscales = gp.median_lengthscale\n", + "for i in median_lengthscales.argsort()[:10]:\n", + " print(f\"Parameter {i:2}) Median lengthscale = {median_lengthscales[i].item():.2e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "fileHeader": "", + "kernelspec": { + "display_name": "python3", + "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.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/saasbo.py b/website-old/static/files/saasbo.py new file mode 100644 index 0000000000..390b4c1edd --- /dev/null +++ b/website-old/static/files/saasbo.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## High-Dimensional sample-efficient Bayesian Optimization with SAASBO +# +# This tutorial shows how to use the Sparse Axis-Aligned Subspace Bayesian Optimization (SAASBO) +# method for high-dimensional Bayesian optimization [1]. SAASBO places strong priors on the +# inverse lengthscales to avoid overfitting in high-dimensional spaces. Specifically, SAASBO +# uses a hierarchical sparsity prior consisting of a global shrinkage parameter +# $\tau \sim \mathcal{HC}(\beta)$ and inverse lengthscales $\rho_d \sim \mathcal{HC}(\tau)$ +# for $d=1, \ldots, D$, where $\mathcal{HC}$ is the half-Cauchy distribution. +# While half-Cauchy priors favor values near zero they also have heavy tails, which allows the +# inverse lengthscales of the most important parameters to escape zero. To perform inference in the +# SAAS model we use Hamiltonian Monte Carlo (HMC) as we found that to outperform MAP inference. +# +# We find that SAASBO performs well on problems with hundreds of dimensions. As we rely on HMC +# and in particular the No-U-Turn-Sampler (NUTS) for inference, the overhead of SAASBO scales +# cubically with the number of datapoints. Depending on the problem, using more than a few hundred +# evaluations may not be feasible as SAASBO is designed for problems with a limited evaluation budget. +# +# In general, we recommend using [Ax](https://ax.dev) for a simple BO setup like this one. See [here](https://ax.dev/tutorials/saasbo.html) for a SAASBO tutorial in Ax, which uses the Log Noisy Expected Improvement acquisition function. Therefore, this tutorial shows a minimal illustrative example of how to use SAASBO with only BoTorch. To customize the acquisition function used with SAASBO in Ax, see the [custom acquisition tutorial](./custom_acquisition), where adding `\"surrogate\": Surrogate(SaasFullyBayesianSingleTaskGP),` to the `model_kwargs` of `BOTORCH_MODULAR` step is sufficient to enable the SAAS model. +# +# [1]: [D. Eriksson, M. Jankowiak. High-Dimensional Bayesian Optimization with Sparse Axis-Aligned Subspaces. Proceedings of the Thirty-Seventh Conference on Uncertainty in Artificial Intelligence, 2021.](https://proceedings.mlr.press/v161/eriksson21a.html) + +# In[1]: + + +import os + +import torch +from torch.quasirandom import SobolEngine + +from botorch import fit_fully_bayesian_model_nuts +from botorch.acquisition.logei import qLogExpectedImprovement +from botorch.models.fully_bayesian import SaasFullyBayesianSingleTaskGP +from botorch.models.transforms import Standardize +from botorch.optim import optimize_acqf +from botorch.test_functions import Branin + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# In[2]: + + +tkwargs = { + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), + "dtype": torch.double, +} + + +# The time to fit the SAAS model can be decreased by lowering +# `WARMUP_STEPS` and `NUM_SAMPLES`. +# +# We recommend using 512 warmup steps and 256 samples when +# possible and to not use fewer than 256 warmup steps and 128 samples. By default, we only +# keep each 16th sample which with 256 samples results in 32 hyperparameter samples. +# +# To make this tutorial run faster we use 256 warmup steps and 128 samples. + +# In[3]: + + +WARMUP_STEPS = 256 if not SMOKE_TEST else 32 +NUM_SAMPLES = 128 if not SMOKE_TEST else 16 +THINNING = 16 + + +# ## Simple model fitting +# We generate a simple function that only depends on the first parameter and show that the SAAS +# model sets all other lengthscales to large values. + +# In[4]: + + +train_X = torch.rand(10, 4, **tkwargs) +test_X = torch.rand(5, 4, **tkwargs) +train_Y = torch.sin(train_X[:, :1]) +test_Y = torch.sin(test_X[:, :1]) + + +# By default, we infer the unknown noise variance in the data. You can also pass in a known +# noise variance (`train_Yvar`) for each observation, which may be useful in cases where you for example +# know that the problem is noise-free and can then set the noise variance to a small value such as `1e-6`. +# +# In this case you can construct a model as follows: +# ``` +# gp = SaasFullyBayesianSingleTaskGP(train_X=train_X, train_Y=train_Y, train_Yvar=torch.full_like(train_Y, 1e-6)) +# ``` + +# In[5]: + + +gp = SaasFullyBayesianSingleTaskGP( + train_X=train_X, + train_Y=train_Y, + outcome_transform=Standardize(m=1) +) +fit_fully_bayesian_model_nuts( + gp, + warmup_steps=WARMUP_STEPS, + num_samples=NUM_SAMPLES, + thinning=THINNING, + disable_progbar=True, +) +with torch.no_grad(): + posterior = gp.posterior(test_X) + + +# Computing the median lengthscales over the MCMC dimensions makes it clear that the first feature has the smallest lengthscale +# + +# In[6]: + + +print(gp.median_lengthscale.detach()) + + +# ### Make predictions with the model +# +# In the next cell we show how to make predictions with the SAAS model. You compute the mean +# and variance for test points just like for any other BoTorch posteriors. Note that the mean +# and posterior will have an extra batch dimension at -3 that corresponds to the number of MCMC +# samples (which is 8 in this tutorial). + +# In[7]: + + +print(posterior.mean.shape) +print(posterior.variance.shape) + + +# We also provide several convenience methods for computing different statistics over the MCMC samples: +# ``` +# mixture_mean = posterior.mixture_mean +# mixture_variance = posterior.mixture_variance +# mixture_quantile = posterior.quantile(q=0.95) +# ``` + +# In[8]: + + +print(f"Ground truth: {test_Y.squeeze(-1)}") +print(f"Mixture mean: {posterior.mixture_mean.squeeze(-1)}") + + +# ## Optimize Branin embedded in a 30D space +# We take the standard 2D Branin problem and embed it in a 30D space. In particular, +# we let dimensions 0 and 1 correspond to the true dimensions. We will show that +# SAASBO is able to identify the important dimensions and efficiently optimize this function. +# We work with the domain $[0, 1]^d$ and unnormalize the inputs to the true domain of Branin +# before evaluating the function. + +# In[9]: + + +branin = Branin().to(**tkwargs) + + +def branin_emb(x): + """x is assumed to be in [0, 1]^d""" + lb, ub = branin.bounds + return branin(lb + (ub - lb) * x[..., :2]) + + +# In[10]: + + +DIM = 30 if not SMOKE_TEST else 2 + +# Evaluation budget +N_INIT = 10 +N_ITERATIONS = 8 if not SMOKE_TEST else 1 +BATCH_SIZE = 5 if not SMOKE_TEST else 1 +print(f"Using a total of {N_INIT + BATCH_SIZE * N_ITERATIONS} function evaluations") + + +# ### Run the optimization +# We use 10 initial Sobol points followed by 8 iterations of BO using a batch size of 5, +# which results in a total of 50 function evaluations. As our goal is to minimize Branin, we flip +# the sign of the function values before fitting the SAAS model as the BoTorch acquisition +# functions assume maximization. + +# In[11]: + + +X = SobolEngine(dimension=DIM, scramble=True, seed=0).draw(N_INIT).to(**tkwargs) +Y = branin_emb(X).unsqueeze(-1) +print(f"Best initial point: {Y.min().item():.3f}") + +for i in range(N_ITERATIONS): + train_Y = -1 * Y # Flip the sign since we want to minimize f(x) + gp = SaasFullyBayesianSingleTaskGP( + train_X=X, + train_Y=train_Y, + train_Yvar=torch.full_like(train_Y, 1e-6), + outcome_transform=Standardize(m=1), + ) + fit_fully_bayesian_model_nuts( + gp, + warmup_steps=WARMUP_STEPS, + num_samples=NUM_SAMPLES, + thinning=THINNING, + disable_progbar=True, + ) + + EI = qLogExpectedImprovement(model=gp, best_f=train_Y.max()) + candidates, acq_values = optimize_acqf( + EI, + bounds=torch.cat((torch.zeros(1, DIM), torch.ones(1, DIM))).to(**tkwargs), + q=BATCH_SIZE, + num_restarts=10, + raw_samples=1024, + ) + + Y_next = torch.cat([branin_emb(x).unsqueeze(-1) for x in candidates]).unsqueeze(-1) + if Y_next.min() < Y.min(): + ind_best = Y_next.argmin() + x0, x1 = candidates[ind_best, :2].tolist() + print( + f"{i + 1}) New best: {Y_next[ind_best].item():.3f} @ " + f"[{x0:.3f}, {x1:.3f}]" + ) + X = torch.cat((X, candidates)) + Y = torch.cat((Y, Y_next)) + + +# ## Plot the results +# +# We can see that we were able to get close to the global optimium of $\approx 0.398$ after 50 function evaluations. +# + +# In[12]: + + +import matplotlib.pyplot as plt +import numpy as np + +get_ipython().run_line_magic('matplotlib', 'inline') + +Y_np = Y.cpu().numpy() +fig, ax = plt.subplots(figsize=(8, 6)) +ax.plot(np.minimum.accumulate(Y_np), color="b", label="SAASBO") +ax.plot([0, len(Y_np)], [0.398, 0.398], "--", c="g", lw=3, label="Optimal value") +ax.grid(True) +ax.set_title(f"Branin, D = {DIM}", fontsize=20) +ax.set_xlabel("Number of evaluations", fontsize=20) +ax.set_xlim([0, len(Y_np)]) +ax.set_ylabel("Best value found", fontsize=20) +ax.set_ylim([0, 8]) +ax.legend(fontsize=18) +plt.show() + + +# ## Predict on some test points +# We fit a model using the 50 datapoints collected by SAASBO and predict on 50 test +# points in order to see how well the SAAS model predicts out-of-sample. +# The plot shows the mean and a 95% confidence interval for each test point. + +# In[13]: + + +train_X = SobolEngine(dimension=DIM, scramble=True, seed=0).draw(50).to(**tkwargs) +test_X = SobolEngine(dimension=DIM, scramble=True, seed=1).draw(50).to(**tkwargs) +train_Y = branin_emb(train_X).unsqueeze(-1) +test_Y = branin_emb(test_X).unsqueeze(-1) + +gp = SaasFullyBayesianSingleTaskGP( + train_X=train_X, + train_Y=train_Y, + train_Yvar=torch.full_like(train_Y, 1e-6), + outcome_transform=Standardize(m=1), +) +fit_fully_bayesian_model_nuts( + gp, + warmup_steps=WARMUP_STEPS, + num_samples=NUM_SAMPLES, + thinning=THINNING, + disable_progbar=True, +) + + +# In[14]: + + +with torch.no_grad(): + posterior = gp.posterior(test_X) +median = posterior.quantile(value=torch.tensor([0.5], **tkwargs)) +q1 = posterior.quantile(value=torch.tensor([0.025], **tkwargs)) +q2 = posterior.quantile(value=torch.tensor([0.975], **tkwargs)) + + +# In[15]: + + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +ax.plot([0, 80], [0, 80], "b--", lw=2) + +yerr1, yerr2 = median - q1, q2 - median +yerr = torch.cat((yerr1.unsqueeze(0), yerr2.unsqueeze(0)), dim=0).squeeze(-1) +markers, caps, bars = ax.errorbar( + test_Y.squeeze(-1).cpu().numpy(), + median.squeeze(-1).cpu().numpy(), + yerr=yerr.cpu().numpy(), + fmt=".", + capsize=4, + elinewidth=2.0, + ms=14, + c="k", + ecolor="gray", +) +ax.set_xlim([0, 80]) +ax.set_ylim([0, 80]) +[bar.set_alpha(0.8) for bar in bars] +[cap.set_alpha(0.8) for cap in caps] +ax.set_xlabel("True value", fontsize=20) +ax.set_ylabel("Predicted value", fontsize=20) +ax.set_aspect("equal") +ax.grid(True) + + +# ## Look a the lengthscales from the final model +# +# As SAASBO places strong priors on the inverse lengthscales, we only expect parameters +# 0 and 1 to be identified as important by the model since the other parameters have no effect. +# We can confirm that this is the case below as the lengthscales of parameters 0 and 1 are +# small with all other lengthscales being large. + +# In[16]: + + +median_lengthscales = gp.median_lengthscale +for i in median_lengthscales.argsort()[:10]: + print(f"Parameter {i:2}) Median lengthscale = {median_lengthscales[i].item():.2e}") + + +# In[17]: + + + + diff --git a/website-old/static/files/scalable_constrained_bo.ipynb b/website-old/static/files/scalable_constrained_bo.ipynb new file mode 100644 index 0000000000..727df821ab --- /dev/null +++ b/website-old/static/files/scalable_constrained_bo.ipynb @@ -0,0 +1,568 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Scalable Constrained Bayesian Optimization (SCBO)\n", + "In this tutorial, we show how to implement Scalable Constrained Bayesian Optimization (SCBO) [1] in a closed loop in BoTorch.\n", + "\n", + "We optimize the 10𝐷 Ackley function on the domain $[−5,10]^{10}$. This implementation uses two simple constraint functions $c1$ and $c2$. Our goal is to find an $x$ that maximizes the Ackley function subject to the constraints $c1(x) \\leq 0$ and $c2(x) \\leq 0$.\n", + "\n", + "[1]: David Eriksson and Matthias Poloczek. Scalable constrained Bayesian optimization. In International Conference on Artificial Intelligence and Statistics, pages 730–738. PMLR, 2021.\n", + "(https://doi.org/10.48550/arxiv.2002.08526)\n", + "\n", + "Since SCBO is essentially a constrained version of Trust Region Bayesian Optimization (TuRBO), this tutorial shares much of the same code as the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1) with small modifications made to implement SCBO." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "import warnings\n", + "from dataclasses import dataclass\n", + "\n", + "import gpytorch\n", + "import torch\n", + "from gpytorch.constraints import Interval\n", + "from gpytorch.kernels import MaternKernel, ScaleKernel\n", + "from gpytorch.likelihoods import GaussianLikelihood\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "from torch import Tensor\n", + "from torch.quasirandom import SobolEngine\n", + "\n", + "from botorch.fit import fit_gpytorch_mll\n", + "# Constrained Max Posterior Sampling s a new sampling class, similar to MaxPosteriorSampling,\n", + "# which implements the constrained version of Thompson Sampling described in [1].\n", + "from botorch.generation.sampling import ConstrainedMaxPosteriorSampling\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.models.model_list_gp_regression import ModelListGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.test_functions import Ackley\n", + "from botorch.utils.transforms import unnormalize\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "tkwargs = {\"device\": device, \"dtype\": dtype}\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demonstration with 10-dimensional Ackley function and Two Simple Constraint Functions" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Here we define the example 10D Ackley function\n", + "fun = Ackley(dim=10, negate=True).to(**tkwargs)\n", + "fun.bounds[0, :].fill_(-5)\n", + "fun.bounds[1, :].fill_(10)\n", + "dim = fun.dim\n", + "lb, ub = fun.bounds\n", + "\n", + "batch_size = 4\n", + "n_init = 10\n", + "max_cholesky_size = float(\"inf\") # Always use Cholesky\n", + "\n", + "# When evaluating the function, we must first unnormalize the inputs since\n", + "# we will use normalized inputs x in the main optimizaiton loop\n", + "def eval_objective(x):\n", + " \"\"\"This is a helper function we use to unnormalize and evalaute a point\"\"\"\n", + " return fun(unnormalize(x, fun.bounds))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Defining two simple constraint functions\n", + "\n", + "#### We'll use two constraints functions: c1 and c2 \n", + "We want to find solutions which maximize the above Ackley objective subject to the constraint that \n", + "c1(x) <= 0 and c2(x) <= 0 \n", + "Note that SCBO expects all constraints to be of the for c(x) <= 0, so any other desired constraints must be modified to fit this form. \n", + "\n", + "Note also that while the below constraints are very simple functions, the point of this tutorial is to show how to use SCBO, and this same implementation could be applied in the same way if c1, c2 were actually complex black-box functions. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def c1(x): # Equivalent to enforcing that sum(x) <= 0\n", + " return x.sum()\n", + "\n", + "\n", + "def c2(x): # Equivalent to enforcing that ||x||_2 <= 5\n", + " return torch.norm(x, p=2) - 5\n", + "\n", + "\n", + "# We assume c1, c2 have same bounds as the Ackley function above\n", + "def eval_c1(x):\n", + " \"\"\"This is a helper function we use to unnormalize and evalaute a point\"\"\"\n", + " return c1(unnormalize(x, fun.bounds))\n", + "\n", + "\n", + "def eval_c2(x):\n", + " \"\"\"This is a helper function we use to unnormalize and evalaute a point\"\"\"\n", + " return c2(unnormalize(x, fun.bounds))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define TuRBO Class\n", + "\n", + "Just as in the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1), we'll define a class to hold the turst region state and a method update_state() to update the side length of the trust region hyper-cube during optimization. We'll update the side length according to the number of sequential successes or failures as discussed in the original TuRBO paper. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ScboState(dim=10, batch_size=4, length=0.8, length_min=0.0078125, length_max=1.6, failure_counter=0, failure_tolerance=3, success_counter=0, success_tolerance=10, best_value=-inf, best_constraint_values=tensor([inf, inf], dtype=torch.float64), restart_triggered=False)\n" + ] + } + ], + "source": [ + "@dataclass\n", + "class ScboState:\n", + " dim: int\n", + " batch_size: int\n", + " length: float = 0.8\n", + " length_min: float = 0.5**7\n", + " length_max: float = 1.6\n", + " failure_counter: int = 0\n", + " failure_tolerance: int = float(\"nan\") # Note: Post-initialized\n", + " success_counter: int = 0\n", + " success_tolerance: int = 10 # Note: The original paper uses 3\n", + " best_value: float = -float(\"inf\")\n", + " best_constraint_values: Tensor = torch.ones(2, **tkwargs) * torch.inf\n", + " restart_triggered: bool = False\n", + "\n", + " def __post_init__(self):\n", + " self.failure_tolerance = math.ceil(max([4.0 / self.batch_size, float(self.dim) / self.batch_size]))\n", + "\n", + "\n", + "def update_tr_length(state: ScboState):\n", + " # Update the length of the trust region according to\n", + " # success and failure counters\n", + " # (Just as in original TuRBO paper)\n", + " if state.success_counter == state.success_tolerance: # Expand trust region\n", + " state.length = min(2.0 * state.length, state.length_max)\n", + " state.success_counter = 0\n", + " elif state.failure_counter == state.failure_tolerance: # Shrink trust region\n", + " state.length /= 2.0\n", + " state.failure_counter = 0\n", + "\n", + " if state.length < state.length_min: # Restart when trust region becomes too small\n", + " state.restart_triggered = True\n", + "\n", + " return state\n", + "\n", + "\n", + "def get_best_index_for_batch(Y: Tensor, C: Tensor):\n", + " \"\"\"Return the index for the best point.\"\"\"\n", + " is_feas = (C <= 0).all(dim=-1)\n", + " if is_feas.any(): # Choose best feasible candidate\n", + " score = Y.clone()\n", + " score[~is_feas] = -float(\"inf\")\n", + " return score.argmax()\n", + " return C.clamp(min=0).sum(dim=-1).argmin()\n", + "\n", + "\n", + "def update_state(state, Y_next, C_next):\n", + " \"\"\"Method used to update the TuRBO state after each step of optimization.\n", + "\n", + " Success and failure counters are updated according to the objective values\n", + " (Y_next) and constraint values (C_next) of the batch of candidate points\n", + " evaluated on the optimization step.\n", + "\n", + " As in the original TuRBO paper, a success is counted whenver any one of the\n", + " new candidate points improves upon the incumbent best point. The key difference\n", + " for SCBO is that we only compare points by their objective values when both points\n", + " are valid (meet all constraints). If exactly one of the two points being compared\n", + " violates a constraint, the other valid point is automatically considered to be better.\n", + " If both points violate some constraints, we compare them inated by their constraint values.\n", + " The better point in this case is the one with minimum total constraint violation\n", + " (the minimum sum of constraint values)\"\"\"\n", + "\n", + " # Pick the best point from the batch\n", + " best_ind = get_best_index_for_batch(Y=Y_next, C=C_next)\n", + " y_next, c_next = Y_next[best_ind], C_next[best_ind]\n", + "\n", + " if (c_next <= 0).all():\n", + " # At least one new candidate is feasible\n", + " improvement_threshold = state.best_value + 1e-3 * math.fabs(state.best_value)\n", + " if y_next > improvement_threshold or (state.best_constraint_values > 0).any():\n", + " state.success_counter += 1\n", + " state.failure_counter = 0\n", + " state.best_value = y_next.item()\n", + " state.best_constraint_values = c_next\n", + " else:\n", + " state.success_counter = 0\n", + " state.failure_counter += 1\n", + " else:\n", + " # No new candidate is feasible\n", + " total_violation_next = c_next.clamp(min=0).sum(dim=-1)\n", + " total_violation_center = state.best_constraint_values.clamp(min=0).sum(dim=-1)\n", + " if total_violation_next < total_violation_center:\n", + " state.success_counter += 1\n", + " state.failure_counter = 0\n", + " state.best_value = y_next.item()\n", + " state.best_constraint_values = c_next\n", + " else:\n", + " state.success_counter = 0\n", + " state.failure_counter += 1\n", + "\n", + " # Update the length of the trust region according to the success and failure counters\n", + " state = update_tr_length(state)\n", + " return state\n", + "\n", + "\n", + "# Define example state\n", + "state = ScboState(dim=dim, batch_size=batch_size)\n", + "print(state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generate Initial Points\n", + "\n", + "Here we define a simple method to generate a set of random initial datapoints that we will use to kick-off optimization. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def get_initial_points(dim, n_pts, seed=0):\n", + " sobol = SobolEngine(dimension=dim, scramble=True, seed=seed)\n", + " X_init = sobol.draw(n=n_pts).to(dtype=dtype, device=device)\n", + " return X_init" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generating a batch of candidates for SCBO \n", + "\n", + "Just as in the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1), we'll define a method generate_batch to generate a new batch of candidate points within the TuRBO trust region using Thompson sampling. \n", + "\n", + "The key difference here from TuRBO is that, instead of using MaxPosteriorSampling to simply grab the candidates within the trust region with the maximum posterior values, we use ConstrainedMaxPosteriorSampling to instead grab the candidates within the trust region with the maximum posterior values subject to the constraint that the posteriors for the constraint models for c1(x) and c2(x) must be less than or equal to 0 for both candidates. \n", + "\n", + "We use additional GPs ('constraint models') to model each black-box constraint (c1 and c2), and throw out all candidates for which the sampled value for these constraint models is greater than 0. According to [1], in the special case when all of the candidaates are predicted to be constraint violators, we select the candidate with the minimum predicted violation. (See botorch.generation.sampling.ConstrainedMaxPosteriorSampling for implementation details)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def generate_batch(\n", + " state,\n", + " model, # GP model\n", + " X, # Evaluated points on the domain [0, 1]^d\n", + " Y, # Function values\n", + " C, # Constraint values\n", + " batch_size,\n", + " n_candidates, # Number of candidates for Thompson sampling\n", + " constraint_model,\n", + " sobol: SobolEngine,\n", + "):\n", + " assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))\n", + "\n", + " # Create the TR bounds\n", + " best_ind = get_best_index_for_batch(Y=Y, C=C)\n", + " x_center = X[best_ind, :].clone()\n", + " tr_lb = torch.clamp(x_center - state.length / 2.0, 0.0, 1.0)\n", + " tr_ub = torch.clamp(x_center + state.length / 2.0, 0.0, 1.0)\n", + "\n", + " # Thompson Sampling w/ Constraints (SCBO)\n", + " dim = X.shape[-1]\n", + " pert = sobol.draw(n_candidates).to(dtype=dtype, device=device)\n", + " pert = tr_lb + (tr_ub - tr_lb) * pert\n", + "\n", + " # Create a perturbation mask\n", + " prob_perturb = min(20.0 / dim, 1.0)\n", + " mask = torch.rand(n_candidates, dim, **tkwargs) <= prob_perturb\n", + " ind = torch.where(mask.sum(dim=1) == 0)[0]\n", + " mask[ind, torch.randint(0, dim - 1, size=(len(ind),), device=device)] = 1\n", + "\n", + " # Create candidate points from the perturbations and the mask\n", + " X_cand = x_center.expand(n_candidates, dim).clone()\n", + " X_cand[mask] = pert[mask]\n", + "\n", + " # Sample on the candidate points using Constrained Max Posterior Sampling\n", + " constrained_thompson_sampling = ConstrainedMaxPosteriorSampling(\n", + " model=model, constraint_model=constraint_model, replacement=False\n", + " )\n", + " with torch.no_grad():\n", + " X_next = constrained_thompson_sampling(X_cand, num_samples=batch_size)\n", + "\n", + " return X_next" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Main Optimization Loop" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "14) No feasible point yet! Smallest total violation: 1.61e+01, TR length: 8.00e-01\n", + "18) No feasible point yet! Smallest total violation: 8.45e+00, TR length: 8.00e-01\n", + "22) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 8.00e-01\n", + "26) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 8.00e-01\n", + "30) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 8.00e-01\n", + "34) No feasible point yet! Smallest total violation: 2.11e+00, TR length: 4.00e-01\n", + "38) No feasible point yet! Smallest total violation: 8.84e-01, TR length: 4.00e-01\n", + "42) Best value: -6.17e+00, TR length: 4.00e-01\n", + "46) Best value: -6.17e+00, TR length: 4.00e-01\n", + "50) Best value: -5.94e+00, TR length: 4.00e-01\n", + "54) Best value: -5.94e+00, TR length: 4.00e-01\n", + "58) Best value: -5.81e+00, TR length: 4.00e-01\n", + "62) Best value: -4.58e+00, TR length: 4.00e-01\n", + "66) Best value: -4.58e+00, TR length: 4.00e-01\n", + "70) Best value: -4.58e+00, TR length: 4.00e-01\n", + "74) Best value: -4.58e+00, TR length: 2.00e-01\n", + "78) Best value: -4.19e+00, TR length: 2.00e-01\n", + "82) Best value: -2.97e+00, TR length: 2.00e-01\n", + "86) Best value: -2.97e+00, TR length: 2.00e-01\n", + "90) Best value: -2.97e+00, TR length: 2.00e-01\n", + "94) Best value: -2.97e+00, TR length: 1.00e-01\n", + "98) Best value: -2.41e+00, TR length: 1.00e-01\n", + "102) Best value: -2.41e+00, TR length: 1.00e-01\n", + "106) Best value: -2.41e+00, TR length: 1.00e-01\n", + "110) Best value: -2.36e+00, TR length: 1.00e-01\n", + "114) Best value: -2.36e+00, TR length: 1.00e-01\n", + "118) Best value: -2.36e+00, TR length: 1.00e-01\n", + "122) Best value: -2.36e+00, TR length: 5.00e-02\n", + "126) Best value: -1.57e+00, TR length: 5.00e-02\n", + "130) Best value: -1.57e+00, TR length: 5.00e-02\n", + "134) Best value: -1.16e+00, TR length: 5.00e-02\n", + "138) Best value: -1.16e+00, TR length: 5.00e-02\n", + "142) Best value: -1.16e+00, TR length: 5.00e-02\n", + "146) Best value: -1.05e+00, TR length: 5.00e-02\n", + "150) Best value: -1.05e+00, TR length: 5.00e-02\n", + "154) Best value: -1.05e+00, TR length: 5.00e-02\n", + "158) Best value: -1.05e+00, TR length: 2.50e-02\n", + "162) Best value: -4.22e-01, TR length: 2.50e-02\n", + "166) Best value: -4.22e-01, TR length: 2.50e-02\n", + "170) Best value: -4.22e-01, TR length: 2.50e-02\n", + "174) Best value: -4.22e-01, TR length: 1.25e-02\n", + "178) Best value: -3.24e-01, TR length: 1.25e-02\n", + "182) Best value: -3.24e-01, TR length: 1.25e-02\n", + "186) Best value: -3.24e-01, TR length: 1.25e-02\n", + "190) Best value: -3.24e-01, TR length: 6.25e-03\n" + ] + } + ], + "source": [ + "# Generate initial data\n", + "train_X = get_initial_points(dim, n_init)\n", + "train_Y = torch.tensor([eval_objective(x) for x in train_X], **tkwargs).unsqueeze(-1)\n", + "C1 = torch.tensor([eval_c1(x) for x in train_X], **tkwargs).unsqueeze(-1)\n", + "C2 = torch.tensor([eval_c2(x) for x in train_X], **tkwargs).unsqueeze(-1)\n", + "\n", + "# Initialize TuRBO state\n", + "state = ScboState(dim, batch_size=batch_size)\n", + "\n", + "# Note: We use 2000 candidates here to make the tutorial run faster.\n", + "# SCBO actually uses min(5000, max(2000, 200 * dim)) candidate points by default.\n", + "N_CANDIDATES = 2000 if not SMOKE_TEST else 4\n", + "sobol = SobolEngine(dim, scramble=True, seed=1)\n", + "\n", + "\n", + "def get_fitted_model(X, Y):\n", + " likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))\n", + " covar_module = ScaleKernel( # Use the same lengthscale prior as in the TuRBO paper\n", + " MaternKernel(nu=2.5, ard_num_dims=dim, lengthscale_constraint=Interval(0.005, 4.0))\n", + " )\n", + " model = SingleTaskGP(\n", + " X,\n", + " Y,\n", + " covar_module=covar_module,\n", + " likelihood=likelihood,\n", + " outcome_transform=Standardize(m=1),\n", + " )\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "\n", + " with gpytorch.settings.max_cholesky_size(max_cholesky_size):\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " return model\n", + "\n", + "\n", + "while not state.restart_triggered: # Run until TuRBO converges\n", + " # Fit GP models for objective and constraints\n", + " model = get_fitted_model(train_X, train_Y)\n", + " c1_model = get_fitted_model(train_X, C1)\n", + " c2_model = get_fitted_model(train_X, C2)\n", + "\n", + " # Generate a batch of candidates\n", + " with gpytorch.settings.max_cholesky_size(max_cholesky_size):\n", + " X_next = generate_batch(\n", + " state=state,\n", + " model=model,\n", + " X=train_X,\n", + " Y=train_Y,\n", + " C=torch.cat((C1, C2), dim=-1),\n", + " batch_size=batch_size,\n", + " n_candidates=N_CANDIDATES,\n", + " constraint_model=ModelListGP(c1_model, c2_model),\n", + " sobol=sobol,\n", + " )\n", + "\n", + " # Evaluate both the objective and constraints for the selected candidaates\n", + " Y_next = torch.tensor([eval_objective(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1)\n", + " C1_next = torch.tensor([eval_c1(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1)\n", + " C2_next = torch.tensor([eval_c2(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1)\n", + " C_next = torch.cat([C1_next, C2_next], dim=-1)\n", + "\n", + " # Update TuRBO state\n", + " state = update_state(state=state, Y_next=Y_next, C_next=C_next)\n", + "\n", + " # Append data. Note that we append all data, even points that violate\n", + " # the constraints. This is so our constraint models can learn more\n", + " # about the constraint functions and gain confidence in where violations occur.\n", + " train_X = torch.cat((train_X, X_next), dim=0)\n", + " train_Y = torch.cat((train_Y, Y_next), dim=0)\n", + " C1 = torch.cat((C1, C1_next), dim=0)\n", + " C2 = torch.cat((C2, C2_next), dim=0)\n", + "\n", + " # Print current status. Note that state.best_value is always the best\n", + " # objective value found so far which meets the constraints, or in the case\n", + " # that no points have been found yet which meet the constraints, it is the\n", + " # objective value of the point with the minimum constraint violation.\n", + " if (state.best_constraint_values <= 0).all():\n", + " print(f\"{len(train_X)}) Best value: {state.best_value:.2e}, TR length: {state.length:.2e}\")\n", + " else:\n", + " violation = state.best_constraint_values.clamp(min=0).sum()\n", + " print(\n", + " f\"{len(train_X)}) No feasible point yet! Smallest total violation: \"\n", + " f\"{violation:.2e}, TR length: {state.length:.2e}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot Results" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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q1b49NNPdc7aqd721KlTGDFiBCpUqAAnJye4uLigatWqePfdd3Hp0iUMGjTI5NjyM2HCBGzYsAGjRo1C48aNUbZsWTg6OsLR0RHlypVDr169sG3bNmzevFnndS44OBjHjx/HsGHDULFiRY0+tebo2LEjTpw4gQEDBkjHtbo9f/jhB+zZs8di37KZy5hzsG/fvti+fTvGjh2L5s2bIzw8HM7OznBwcEBISAi6d++OX3/9FQcOHNAY7agkkwlRAgbDI6IidfXqVWk0iDlz5pg9OQQREVFxZlu3SIjIKOrRCOzt7Qv1bg8REVFxwISYiDQkJibihx9+AJDTdUI9DSsREVFpxVEmiAgJCQlITk7GvXv3MH36dDx+/BgymUwa/o6IiKg0Y0JMRPjggw+wYsUKjWWjR4+WZnwiIiIqzZgQE5HEwcEBFSpUwIgRI/KdZIGIiKi04CgTRERERGTTeIfYRNnZ2bh37x7c3Nyk2a6IiIiIqPgQQuDp06cICgoyOM46E2IT3bt3D6GhodYOg4iIiIjycfv2bYSEhOhdz4TYRG5ubgCA2NhYeHt7WzmakkmlUmHXrl3o0KFDocx5byvYjuZjG1oG29F8bEPLYDtaRmlox+TkZISGhkp5mz5MiE2k7ibh5uYGd3d3K0dTMqlUKjg7O8Pd3b3EnmjFAdvRfGxDy2A7mo9taBlsR8soTe2YX/dWTsxBRERERDaNCTERERER2TQmxERERERk05gQExEREZFNY0JMRERERDaNCTERERER2TQmxERERERk05gQExEREZFNY0JMRERERDaNCTERERER2TQmxERERERk05gQExEREZFNY0JMRERERDaNCTERERER2TQmxERERERk05gQExEREZFNY0JMRERERDaNCTERERER2TQmxERERERk05gQExEREZFNY0JMRERERDaNCTERERER2TSbTogXLVqEcuXKwdHREY0aNcLx48etHRIRERERFTF7awdgLWvWrMG4ceOwePFiNGrUCF9//TU6duyIy5cvw9/f3+h6Hj58iOzs7ALv39XVFU5OTnrrFEIUuE4AcHZ2houLi851jx8/RlZWlkn1Ojo6ws3NTee6xMREqFSqAtepUqnw/PlzveuTkpKQkZFR4HoBQKFQwNPTU+e6p0+fIi0tzaR67ezs4O3trXPd8+fPkZKSYlK9MpkMvr6+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UneHOD9HjhxBzZo1ERAQIC3r2LEjRo0ahfPnz6Nu3bo6t0tPT0d6err0Ojk5GQCgUqmgUqkKN+hSSt1ubD/zsB3Nxza0DLaj+diGlsF2tIzS0I7Gxm5zCXF8fLxGMgxAeh0fH693u88//xwzZszQWh4dHQ1nZ2fLBmljoqKirB1CqcB2NB/b0DLYjuZjG1oG29EySnI7pqSkGFWuRCTEkyZNwpw5cwyWuXjxIqpUqVJoMUyePBnjxo2TXicnJyM0NBStW7eGj49Poe23NFOpVIiKikL79u2hUCisHU6JxXY0H9vQMtiO5mMbWgbb0TJKQzuqv9HPT4ES4vv378POzk7vekPrAEAmk5k0msP48eMxZMgQg2XKly9vVF2BgYE4fvy4xrL79+9L6/RRKpVQKpVayxUKRYk9SIoLtqFlsB3Nxza0DLaj+diGlsF2tIyS3I7Gxm2RcYgLm5+fH/z8/CxSV5MmTTBr1iwkJCTA398fQM5XAe7u7qhWrZpF9kFEREREJYfRCfG0adMKMw6LuXXrFh4/foxbt24hKysLp0+fBgBUrFgRrq6u6NChA6pVq4aBAwfiiy++QHx8PKZMmYIxY8bovANMRERERKVbqUuIp06dihUrVkiv1aNGREdHIzIyEnZ2dti2bRtGjRqFJk2awMXFBYMHD8bMmTOtFTIRERERWVGJeKiuIJYvX47ly5cbLBMWFobt27cXTUBEREREVKyVuok5iIiIiIgKggkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsREREREZNOYEBMRERGRTWNCTEREREQ2jQkxEREREdk0JsRE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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from matplotlib import rc\n", + "\n", + "%matplotlib inline\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "\n", + "score = train_Y.clone()\n", + "# Set infeasible to -inf\n", + "score[~(torch.cat((C1, C2), dim=-1) <= 0).all(dim=-1)] = float(\"-inf\")\n", + "fx = np.maximum.accumulate(score.cpu())\n", + "plt.plot(fx, marker=\"\", lw=3)\n", + "\n", + "plt.plot([0, len(train_Y)], [fun.optimal_value, fun.optimal_value], \"k--\", lw=3)\n", + "plt.ylabel(\"Function value\", fontsize=18)\n", + "plt.xlabel(\"Number of evaluations\", fontsize=18)\n", + "plt.title(\"10D Ackley with 2 outcome constraints\", fontsize=20)\n", + "plt.xlim([0, len(train_Y)])\n", + "plt.ylim([-15, 1])\n", + "\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.9.13" + }, + "vscode": { + "interpreter": { + "hash": "9beb4c3e6521665a47c2b1e65f245d1b2309f4194f15ed6955f5e52622a9d29e" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/scalable_constrained_bo.py b/website-old/static/files/scalable_constrained_bo.py new file mode 100644 index 0000000000..9b80248aeb --- /dev/null +++ b/website-old/static/files/scalable_constrained_bo.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# # Scalable Constrained Bayesian Optimization (SCBO) +# In this tutorial, we show how to implement Scalable Constrained Bayesian Optimization (SCBO) [1] in a closed loop in BoTorch. +# +# We optimize the 10𝐷 Ackley function on the domain $[−5,10]^{10}$. This implementation uses two simple constraint functions $c1$ and $c2$. Our goal is to find an $x$ that maximizes the Ackley function subject to the constraints $c1(x) \leq 0$ and $c2(x) \leq 0$. +# +# [1]: David Eriksson and Matthias Poloczek. Scalable constrained Bayesian optimization. In International Conference on Artificial Intelligence and Statistics, pages 730–738. PMLR, 2021. +# (https://doi.org/10.48550/arxiv.2002.08526) +# +# Since SCBO is essentially a constrained version of Trust Region Bayesian Optimization (TuRBO), this tutorial shares much of the same code as the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1) with small modifications made to implement SCBO. + +# In[ ]: + + +import math +import os +import warnings +from dataclasses import dataclass + +import gpytorch +import torch +from gpytorch.constraints import Interval +from gpytorch.kernels import MaternKernel, ScaleKernel +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.mlls import ExactMarginalLogLikelihood +from torch import Tensor +from torch.quasirandom import SobolEngine + +from botorch.fit import fit_gpytorch_mll +# Constrained Max Posterior Sampling s a new sampling class, similar to MaxPosteriorSampling, +# which implements the constrained version of Thompson Sampling described in [1]. +from botorch.generation.sampling import ConstrainedMaxPosteriorSampling +from botorch.models import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize +from botorch.test_functions import Ackley +from botorch.utils.transforms import unnormalize + +warnings.filterwarnings("ignore") + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +dtype = torch.double +tkwargs = {"device": device, "dtype": dtype} + +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ## Demonstration with 10-dimensional Ackley function and Two Simple Constraint Functions + +# In[2]: + + +# Here we define the example 10D Ackley function +fun = Ackley(dim=10, negate=True).to(**tkwargs) +fun.bounds[0, :].fill_(-5) +fun.bounds[1, :].fill_(10) +dim = fun.dim +lb, ub = fun.bounds + +batch_size = 4 +n_init = 10 +max_cholesky_size = float("inf") # Always use Cholesky + +# When evaluating the function, we must first unnormalize the inputs since +# we will use normalized inputs x in the main optimizaiton loop +def eval_objective(x): + """This is a helper function we use to unnormalize and evalaute a point""" + return fun(unnormalize(x, fun.bounds)) + + +# ### Defining two simple constraint functions +# +# #### We'll use two constraints functions: c1 and c2 +# We want to find solutions which maximize the above Ackley objective subject to the constraint that +# c1(x) <= 0 and c2(x) <= 0 +# Note that SCBO expects all constraints to be of the for c(x) <= 0, so any other desired constraints must be modified to fit this form. +# +# Note also that while the below constraints are very simple functions, the point of this tutorial is to show how to use SCBO, and this same implementation could be applied in the same way if c1, c2 were actually complex black-box functions. +# + +# In[3]: + + +def c1(x): # Equivalent to enforcing that sum(x) <= 0 + return x.sum() + + +def c2(x): # Equivalent to enforcing that ||x||_2 <= 5 + return torch.norm(x, p=2) - 5 + + +# We assume c1, c2 have same bounds as the Ackley function above +def eval_c1(x): + """This is a helper function we use to unnormalize and evalaute a point""" + return c1(unnormalize(x, fun.bounds)) + + +def eval_c2(x): + """This is a helper function we use to unnormalize and evalaute a point""" + return c2(unnormalize(x, fun.bounds)) + + +# ## Define TuRBO Class +# +# Just as in the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1), we'll define a class to hold the turst region state and a method update_state() to update the side length of the trust region hyper-cube during optimization. We'll update the side length according to the number of sequential successes or failures as discussed in the original TuRBO paper. + +# In[4]: + + +@dataclass +class ScboState: + dim: int + batch_size: int + length: float = 0.8 + length_min: float = 0.5**7 + length_max: float = 1.6 + failure_counter: int = 0 + failure_tolerance: int = float("nan") # Note: Post-initialized + success_counter: int = 0 + success_tolerance: int = 10 # Note: The original paper uses 3 + best_value: float = -float("inf") + best_constraint_values: Tensor = torch.ones(2, **tkwargs) * torch.inf + restart_triggered: bool = False + + def __post_init__(self): + self.failure_tolerance = math.ceil(max([4.0 / self.batch_size, float(self.dim) / self.batch_size])) + + +def update_tr_length(state: ScboState): + # Update the length of the trust region according to + # success and failure counters + # (Just as in original TuRBO paper) + if state.success_counter == state.success_tolerance: # Expand trust region + state.length = min(2.0 * state.length, state.length_max) + state.success_counter = 0 + elif state.failure_counter == state.failure_tolerance: # Shrink trust region + state.length /= 2.0 + state.failure_counter = 0 + + if state.length < state.length_min: # Restart when trust region becomes too small + state.restart_triggered = True + + return state + + +def get_best_index_for_batch(Y: Tensor, C: Tensor): + """Return the index for the best point.""" + is_feas = (C <= 0).all(dim=-1) + if is_feas.any(): # Choose best feasible candidate + score = Y.clone() + score[~is_feas] = -float("inf") + return score.argmax() + return C.clamp(min=0).sum(dim=-1).argmin() + + +def update_state(state, Y_next, C_next): + """Method used to update the TuRBO state after each step of optimization. + + Success and failure counters are updated according to the objective values + (Y_next) and constraint values (C_next) of the batch of candidate points + evaluated on the optimization step. + + As in the original TuRBO paper, a success is counted whenver any one of the + new candidate points improves upon the incumbent best point. The key difference + for SCBO is that we only compare points by their objective values when both points + are valid (meet all constraints). If exactly one of the two points being compared + violates a constraint, the other valid point is automatically considered to be better. + If both points violate some constraints, we compare them inated by their constraint values. + The better point in this case is the one with minimum total constraint violation + (the minimum sum of constraint values)""" + + # Pick the best point from the batch + best_ind = get_best_index_for_batch(Y=Y_next, C=C_next) + y_next, c_next = Y_next[best_ind], C_next[best_ind] + + if (c_next <= 0).all(): + # At least one new candidate is feasible + improvement_threshold = state.best_value + 1e-3 * math.fabs(state.best_value) + if y_next > improvement_threshold or (state.best_constraint_values > 0).any(): + state.success_counter += 1 + state.failure_counter = 0 + state.best_value = y_next.item() + state.best_constraint_values = c_next + else: + state.success_counter = 0 + state.failure_counter += 1 + else: + # No new candidate is feasible + total_violation_next = c_next.clamp(min=0).sum(dim=-1) + total_violation_center = state.best_constraint_values.clamp(min=0).sum(dim=-1) + if total_violation_next < total_violation_center: + state.success_counter += 1 + state.failure_counter = 0 + state.best_value = y_next.item() + state.best_constraint_values = c_next + else: + state.success_counter = 0 + state.failure_counter += 1 + + # Update the length of the trust region according to the success and failure counters + state = update_tr_length(state) + return state + + +# Define example state +state = ScboState(dim=dim, batch_size=batch_size) +print(state) + + +# ### Generate Initial Points +# +# Here we define a simple method to generate a set of random initial datapoints that we will use to kick-off optimization. + +# In[5]: + + +def get_initial_points(dim, n_pts, seed=0): + sobol = SobolEngine(dimension=dim, scramble=True, seed=seed) + X_init = sobol.draw(n=n_pts).to(dtype=dtype, device=device) + return X_init + + +# ### Generating a batch of candidates for SCBO +# +# Just as in the TuRBO Tutorial (https://botorch.org/tutorials/turbo_1), we'll define a method generate_batch to generate a new batch of candidate points within the TuRBO trust region using Thompson sampling. +# +# The key difference here from TuRBO is that, instead of using MaxPosteriorSampling to simply grab the candidates within the trust region with the maximum posterior values, we use ConstrainedMaxPosteriorSampling to instead grab the candidates within the trust region with the maximum posterior values subject to the constraint that the posteriors for the constraint models for c1(x) and c2(x) must be less than or equal to 0 for both candidates. +# +# We use additional GPs ('constraint models') to model each black-box constraint (c1 and c2), and throw out all candidates for which the sampled value for these constraint models is greater than 0. According to [1], in the special case when all of the candidaates are predicted to be constraint violators, we select the candidate with the minimum predicted violation. (See botorch.generation.sampling.ConstrainedMaxPosteriorSampling for implementation details). + +# In[6]: + + +def generate_batch( + state, + model, # GP model + X, # Evaluated points on the domain [0, 1]^d + Y, # Function values + C, # Constraint values + batch_size, + n_candidates, # Number of candidates for Thompson sampling + constraint_model, + sobol: SobolEngine, +): + assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y)) + + # Create the TR bounds + best_ind = get_best_index_for_batch(Y=Y, C=C) + x_center = X[best_ind, :].clone() + tr_lb = torch.clamp(x_center - state.length / 2.0, 0.0, 1.0) + tr_ub = torch.clamp(x_center + state.length / 2.0, 0.0, 1.0) + + # Thompson Sampling w/ Constraints (SCBO) + dim = X.shape[-1] + pert = sobol.draw(n_candidates).to(dtype=dtype, device=device) + pert = tr_lb + (tr_ub - tr_lb) * pert + + # Create a perturbation mask + prob_perturb = min(20.0 / dim, 1.0) + mask = torch.rand(n_candidates, dim, **tkwargs) <= prob_perturb + ind = torch.where(mask.sum(dim=1) == 0)[0] + mask[ind, torch.randint(0, dim - 1, size=(len(ind),), device=device)] = 1 + + # Create candidate points from the perturbations and the mask + X_cand = x_center.expand(n_candidates, dim).clone() + X_cand[mask] = pert[mask] + + # Sample on the candidate points using Constrained Max Posterior Sampling + constrained_thompson_sampling = ConstrainedMaxPosteriorSampling( + model=model, constraint_model=constraint_model, replacement=False + ) + with torch.no_grad(): + X_next = constrained_thompson_sampling(X_cand, num_samples=batch_size) + + return X_next + + +# ## Main Optimization Loop + +# In[7]: + + +# Generate initial data +train_X = get_initial_points(dim, n_init) +train_Y = torch.tensor([eval_objective(x) for x in train_X], **tkwargs).unsqueeze(-1) +C1 = torch.tensor([eval_c1(x) for x in train_X], **tkwargs).unsqueeze(-1) +C2 = torch.tensor([eval_c2(x) for x in train_X], **tkwargs).unsqueeze(-1) + +# Initialize TuRBO state +state = ScboState(dim, batch_size=batch_size) + +# Note: We use 2000 candidates here to make the tutorial run faster. +# SCBO actually uses min(5000, max(2000, 200 * dim)) candidate points by default. +N_CANDIDATES = 2000 if not SMOKE_TEST else 4 +sobol = SobolEngine(dim, scramble=True, seed=1) + + +def get_fitted_model(X, Y): + likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3)) + covar_module = ScaleKernel( # Use the same lengthscale prior as in the TuRBO paper + MaternKernel(nu=2.5, ard_num_dims=dim, lengthscale_constraint=Interval(0.005, 4.0)) + ) + model = SingleTaskGP( + X, + Y, + covar_module=covar_module, + likelihood=likelihood, + outcome_transform=Standardize(m=1), + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + + with gpytorch.settings.max_cholesky_size(max_cholesky_size): + fit_gpytorch_mll(mll) + + return model + + +while not state.restart_triggered: # Run until TuRBO converges + # Fit GP models for objective and constraints + model = get_fitted_model(train_X, train_Y) + c1_model = get_fitted_model(train_X, C1) + c2_model = get_fitted_model(train_X, C2) + + # Generate a batch of candidates + with gpytorch.settings.max_cholesky_size(max_cholesky_size): + X_next = generate_batch( + state=state, + model=model, + X=train_X, + Y=train_Y, + C=torch.cat((C1, C2), dim=-1), + batch_size=batch_size, + n_candidates=N_CANDIDATES, + constraint_model=ModelListGP(c1_model, c2_model), + sobol=sobol, + ) + + # Evaluate both the objective and constraints for the selected candidaates + Y_next = torch.tensor([eval_objective(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1) + C1_next = torch.tensor([eval_c1(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1) + C2_next = torch.tensor([eval_c2(x) for x in X_next], dtype=dtype, device=device).unsqueeze(-1) + C_next = torch.cat([C1_next, C2_next], dim=-1) + + # Update TuRBO state + state = update_state(state=state, Y_next=Y_next, C_next=C_next) + + # Append data. Note that we append all data, even points that violate + # the constraints. This is so our constraint models can learn more + # about the constraint functions and gain confidence in where violations occur. + train_X = torch.cat((train_X, X_next), dim=0) + train_Y = torch.cat((train_Y, Y_next), dim=0) + C1 = torch.cat((C1, C1_next), dim=0) + C2 = torch.cat((C2, C2_next), dim=0) + + # Print current status. Note that state.best_value is always the best + # objective value found so far which meets the constraints, or in the case + # that no points have been found yet which meet the constraints, it is the + # objective value of the point with the minimum constraint violation. + if (state.best_constraint_values <= 0).all(): + print(f"{len(train_X)}) Best value: {state.best_value:.2e}, TR length: {state.length:.2e}") + else: + violation = state.best_constraint_values.clamp(min=0).sum() + print( + f"{len(train_X)}) No feasible point yet! Smallest total violation: " + f"{violation:.2e}, TR length: {state.length:.2e}" + ) + + +# ### Plot Results + +# In[8]: + + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import rc + +get_ipython().run_line_magic('matplotlib', 'inline') + +fig, ax = plt.subplots(figsize=(8, 6)) + +score = train_Y.clone() +# Set infeasible to -inf +score[~(torch.cat((C1, C2), dim=-1) <= 0).all(dim=-1)] = float("-inf") +fx = np.maximum.accumulate(score.cpu()) +plt.plot(fx, marker="", lw=3) + +plt.plot([0, len(train_Y)], [fun.optimal_value, fun.optimal_value], "k--", lw=3) +plt.ylabel("Function value", fontsize=18) +plt.xlabel("Number of evaluations", fontsize=18) +plt.title("10D Ackley with 2 outcome constraints", fontsize=20) +plt.xlim([0, len(train_Y)]) +plt.ylim([-15, 1]) + +plt.grid(True) +plt.show() + + +# In[ ]: + + + + diff --git a/website-old/static/files/thompson_sampling.ipynb b/website-old/static/files/thompson_sampling.ipynb new file mode 100644 index 0000000000..571da7adb3 --- /dev/null +++ b/website-old/static/files/thompson_sampling.ipynb @@ -0,0 +1,530 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tutorial on large-scale Thompson sampling\n", + "\n", + "This demo currently considers four approaches to discrete Thompson sampling on `m` candidates points:\n", + "\n", + "1. **Exact sampling with Cholesky:** Computing a Cholesky decomposition of the corresponding `m x m` covariance matrix which reuqires `O(m^3)` computational cost and `O(m^2)` space. This is the standard approach to sampling from a Gaussian process, but the quadratic memory usage and cubic compliexity limits the number of candidate points.\n", + "\n", + "2. **Contour integral quadrature (CIQ):** CIQ [1] is a Krylov subspace method combined with a rational approximation that can be used for computing matrix square roots of covariance matrices, which is the main bottleneck when sampling from a Gaussian process. CIQ relies on computing matrix vector multiplications with the exact kernel matrix which requires `O(m^2)` computational complexity and space. Note that the space complexity can be further lowered to `O(m)` by using [KeOps](https://github.com/getkeops/keops), but this is not covered as part of the tutorial.\n", + "\n", + "3. **Lanczos:** Rather than using CIQ, we can solve the linear systems `K^(1/2) v = b` using Lanczos and the conjugate gradient (CG) method. This will be faster than CIQ, but will generally produce samples of worse quality. Similarly to CIQ, [KeOps](https://github.com/getkeops/keops) can be used to improve space complexity of Lanczos.\n", + "\n", + "4. **Random Fourier features (RFFs):** The RFF kernel was originally proposed in [2] and we use it as implemented in GPyTorch. RFFs are computationally cheap to work with as the computational cost and space are both `O(km)` where `k` is the number of Fourier features. Note that while Cholesky and CIQ are able to generate exact samples from the GP model, RFFs are an unbiased approximation and the resulting samples often aren't perfectly calibrated. \n", + "\n", + "\n", + "[1] [Pleiss, Geoff, et al. \"Fast matrix square roots with applications to Gaussian processes and Bayesian optimization.\", Advances in neural information processing systems (2020)](https://proceedings.neurips.cc/paper/2020/file/fcf55a303b71b84d326fb1d06e332a26-Paper.pdf)\n", + "\n", + "[2] [Rahimi, Ali, and Benjamin Recht. \"Random features for large-scale kernel machines.\", Advances in neural information processing systems (2007)](https://people.eecs.berkeley.edu/~brecht/papers/07.rah.rec.nips.pdf)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "from contextlib import ExitStack\n", + "\n", + "import gpytorch\n", + "import gpytorch.settings as gpts\n", + "import torch\n", + "from gpytorch.constraints import Interval\n", + "from gpytorch.distributions import MultivariateNormal\n", + "from gpytorch.kernels import RBFKernel, RFFKernel, ScaleKernel\n", + "from gpytorch.likelihoods import GaussianLikelihood\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "from torch.quasirandom import SobolEngine\n", + "from torch import Tensor\n", + "\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.generation import MaxPosteriorSampling\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.models.transforms.outcome import Standardize\n", + "from botorch.test_functions import Hartmann\n", + "from botorch.utils.sampling import draw_sobol_samples\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will use 6 dimensional Hartmann test function, which is typically evaluated on the unit hypercube." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "hart6 = Hartmann(dim=6, negate=True).to(device=device, dtype=dtype)\n", + "dim = hart6.dim" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def generate_batch(\n", + " X: Tensor,\n", + " Y: Tensor,\n", + " batch_size: int,\n", + " n_candidates: int,\n", + " sampler: str, # \"cholesky\", \"ciq\", \"rff\", \"lanczos\"\n", + " seed: int,\n", + ") -> Tensor:\n", + " assert sampler in (\"cholesky\", \"ciq\", \"rff\", \"lanczos\")\n", + " assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))\n", + "\n", + " if sampler == \"rff\":\n", + " base_kernel = RFFKernel(ard_num_dims=X.shape[-1], num_samples=1024)\n", + " else:\n", + " base_kernel = RBFKernel(ard_num_dims=X.shape[-1])\n", + " covar_module = ScaleKernel(base_kernel)\n", + "\n", + " # Fit a GP model\n", + " model = SingleTaskGP(train_X=X, train_Y=Y, covar_module=covar_module, outcome_transform=Standardize(m=1))\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " # Draw samples on a Sobol sequence\n", + " X_cand = draw_sobol_samples(bounds=hart6.bounds, n=n_candidates, q=1, seed=seed).squeeze(-2)\n", + "\n", + " # Thompson sample\n", + " with ExitStack() as es:\n", + " if sampler == \"cholesky\":\n", + " es.enter_context(gpts.max_cholesky_size(float(\"inf\")))\n", + " elif sampler == \"ciq\":\n", + " es.enter_context(gpts.fast_computations(covar_root_decomposition=True))\n", + " es.enter_context(gpts.max_cholesky_size(0))\n", + " es.enter_context(gpts.ciq_samples(True))\n", + " es.enter_context(\n", + " gpts.minres_tolerance(2e-3)\n", + " ) # Controls accuracy and runtime\n", + " es.enter_context(gpts.num_contour_quadrature(15))\n", + " elif sampler == \"lanczos\":\n", + " es.enter_context(\n", + " gpts.fast_computations(\n", + " covar_root_decomposition=True, log_prob=True, solves=True\n", + " )\n", + " )\n", + " es.enter_context(gpts.max_lanczos_quadrature_iterations(10))\n", + " es.enter_context(gpts.max_cholesky_size(0))\n", + " es.enter_context(gpts.ciq_samples(False))\n", + " elif sampler == \"rff\":\n", + " es.enter_context(gpts.fast_computations(covar_root_decomposition=True))\n", + " es.enter_context(torch.no_grad())\n", + " \n", + " thompson_sampling = MaxPosteriorSampling(model=model, replacement=False)\n", + " X_next = thompson_sampling(X_cand, num_samples=batch_size)\n", + "\n", + " return X_next" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def run_optimization(\n", + " sampler: str,\n", + " n_candidates: int,\n", + " n_init: int,\n", + " max_evals: int,\n", + " batch_size: int,\n", + " seed: int,\n", + ") -> tuple[Tensor, Tensor]:\n", + " X = draw_sobol_samples(bounds=hart6.bounds, n=n_init, q=1, seed=seed).squeeze(-2)\n", + " Y = torch.tensor(\n", + " [hart6(x) for x in X], dtype=dtype, device=device\n", + " ).unsqueeze(-1)\n", + " print(f\"{len(X)}) Best value: {Y.max().item():.2e}\")\n", + "\n", + " inner_seed = seed\n", + " while len(X) < max_evals:\n", + " # Create a batch\n", + " start = time.monotonic()\n", + " inner_seed += 1\n", + " X_next = generate_batch(\n", + " X=X,\n", + " Y=Y,\n", + " batch_size=min(batch_size, max_evals - len(X)),\n", + " n_candidates=n_candidates,\n", + " seed=inner_seed,\n", + " sampler=sampler,\n", + " )\n", + " end = time.monotonic()\n", + " print(f\"Generated batch in {end - start:.1f} seconds\")\n", + " Y_next = torch.tensor(\n", + " [hart6(x) for x in X_next], dtype=dtype, device=device\n", + " ).unsqueeze(-1)\n", + "\n", + " # Append data\n", + " X = torch.cat((X, X_next), dim=0)\n", + " Y = torch.cat((Y, Y_next), dim=0)\n", + "\n", + " print(f\"{len(X)}) Best value: {Y.max().item():.2e}\")\n", + " return X, Y" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "batch_size = 5\n", + "n_init = 10\n", + "max_evals = 50\n", + "seed = 12345 # To get the same Sobol points\n", + "N_CAND = 10_000 if not SMOKE_TEST else 10\n", + "\n", + "shared_args = {\n", + " \"n_candidates\": N_CAND,\n", + " \"n_init\": n_init,\n", + " \"max_evals\": max_evals,\n", + " \"batch_size\": batch_size,\n", + " \"seed\": seed,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Track memory footprint" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext memory_profiler" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cholesky" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10) Best value: 6.72e-01\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated batch in 16.0 seconds\n", + "15) Best value: 6.72e-01\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated batch in 14.1 seconds\n", + "20) Best value: 6.72e-01\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated batch in 18.6 seconds\n", + "25) Best value: 1.94e+00\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/anaconda3/envs/botorch/lib/python3.10/site-packages/linear_operator/utils/cholesky.py:40: NumericalWarning: A not p.d., added jitter of 1.0e-08 to the diagonal\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated batch in 14.8 seconds\n", + "30) Best value: 1.94e+00\n", + "Generated batch in 13.9 seconds\n", + "35) Best value: 2.11e+00\n", + "Generated batch in 14.0 seconds\n", + "40) Best value: 2.68e+00\n", + "Generated batch in 14.6 seconds\n", + "45) Best value: 2.98e+00\n", + "Generated batch in 14.7 seconds\n", + "50) Best value: 2.98e+00\n", + "peak memory: 7941.41 MiB, increment: 7674.36 MiB\n" + ] + } + ], + "source": [ + "%memit X_chol, Y_chol = run_optimization(\"cholesky\", **shared_args)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## RFFs" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10) Best value: 6.72e-01\n", + "Generated batch in 1.4 seconds\n", + "15) Best value: 6.72e-01\n", + "Generated batch in 2.6 seconds\n", + "20) Best value: 6.72e-01\n", + "Generated batch in 2.0 seconds\n", + "25) Best value: 1.00e+00\n", + "Generated batch in 2.0 seconds\n", + "30) Best value: 1.36e+00\n", + "Generated batch in 2.5 seconds\n", + "35) Best value: 2.00e+00\n", + "Generated batch in 2.2 seconds\n", + "40) Best value: 2.43e+00\n", + "Generated batch in 2.2 seconds\n", + "45) Best value: 2.43e+00\n", + "Generated batch in 2.1 seconds\n", + "50) Best value: 2.65e+00\n", + "peak memory: 1709.20 MiB, increment: 1349.06 MiB\n" + ] + } + ], + "source": [ + "%memit X_rff, Y_rff = run_optimization(\"rff\", **shared_args)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lanczos" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10) Best value: 6.72e-01\n", + "Generated batch in 1.9 seconds\n", + "15) Best value: 6.72e-01\n", + "Generated batch in 2.5 seconds\n", + "20) Best value: 1.83e+00\n", + "Generated batch in 2.4 seconds\n", + "25) Best value: 1.93e+00\n", + "Generated batch in 2.6 seconds\n", + "30) Best value: 2.39e+00\n", + "Generated batch in 2.5 seconds\n", + "35) Best value: 2.41e+00\n", + "Generated batch in 2.5 seconds\n", + "40) Best value: 2.96e+00\n", + "Generated batch in 2.6 seconds\n", + "45) Best value: 2.98e+00\n", + "Generated batch in 2.5 seconds\n", + "50) Best value: 2.98e+00\n", + "peak memory: 2981.11 MiB, increment: 1271.91 MiB\n" + ] + } + ], + "source": [ + "%memit X_lanczos, Y_lanczos = run_optimization(\"lanczos\", **shared_args)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CIQ" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10) Best value: 6.72e-01\n", + "Generated batch in 9.6 seconds\n", + "15) Best value: 6.72e-01\n", + "Generated batch in 12.5 seconds\n", + "20) Best value: 6.72e-01\n", + "Generated batch in 16.8 seconds\n", + "25) Best value: 6.72e-01\n", + "Generated batch in 18.7 seconds\n", + "30) Best value: 2.19e+00\n", + "Generated batch in 18.2 seconds\n", + "35) Best value: 2.48e+00\n", + "Generated batch in 14.8 seconds\n", + "40) Best value: 2.61e+00\n", + "Generated batch in 15.2 seconds\n", + "45) Best value: 2.98e+00\n", + "Generated batch in 15.9 seconds\n", + "50) Best value: 2.98e+00\n", + "peak memory: 2674.38 MiB, increment: 908.34 MiB\n" + ] + } + ], + "source": [ + "%memit X_ciq, Y_ciq = run_optimization(\"ciq\", **shared_args)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA8AAAAL4CAYAAAC0iJ7vAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gU5d7G8e/upoeSEAgQOioCiqiIooiA0kS6gNgQLICCwvG89gb2rlgRQcEKghQLHhFFqgoIIkWkGLpAQiqpm+y8f4xZEtNDNrObvT/XlSuzu8/O3IljyC9PsxmGYSAiIiIiIiJSzdmtDiAiIiIiIiJSFVQAi4iIiIiIiF9QASwiIiIiIiJ+QQWwiIiIiIiI+AUVwCIiIiIiIuIXVACLiIiIiIiIX1ABLCIiIiIiIn5BBbCIiIiIiIj4BRXAIiIiIiIi4hdUAIuIiIiIiIhfUAEsIiKVrlu3bthsNmw2G5MnTy7XeydPnux+b7du3TySTyRPfHw8b7/9Nr1796Zly5aEhYURGRlJmzZtuOqqq3j++efZsGGD1TFFRKSSBFgdQERExFd169aNFStWALB8+XIV7D7EMAymTZvGAw88QHJycoHXMjIySEpKYseOHSxZsoTw8HBOnDhhUVIREalMKoBFRETErxiGwZgxY5gxY4b7uYiICC655BIaNGgAwJEjR9i8eTOHDh2yKqaIiHiACmARERHxK/fcc4+7+I2OjuaFF17g+uuvx+FwFGq7Y8cOPv/886qOKCIiHqICWERERPzG6tWrefnllwFo0KABK1eu5Iwzzii2fevWrXnooYeqKp6IiHiYFsESERERv3HnnXdiGAYA06ZNK7H4FRGR6kcFsIiI+KR9+/bx9ttvc+2113L22WdTu3ZtAgMDiYqKol27dtx+++38/PPPZTrXqFGj3CtPz5o1C4CkpCSmTp3KZZddRqNGjQgICMBms5GUlORum7cAFkD37t3dz+f/yDtfnubNm7tf27t3LwC7d+/mnnvucX8doaGhtG/fnqeffpr09PRCef/8808mTJhAu3btqFWrFhEREXTq1Ik333yT3NzcMn3Nv/76K8888wz9+vWjZcuW1KhRg6CgIOrXr88ll1zCQw89xP79+8t0rqK+poMHD/LII4/Qvn17IiIiCA8Pp3Xr1tx5553s27ev1HPmX0n8xx9/BCAhIYHnnnuOjh07UrduXUJDQ2nZsiW33HILW7duLfWcP//8M7/99htg9uwOHDiwTF+fiIhUI4aIiEgl69q1qwEYgPHYY4+V672PPfaY+71du3Ytss3//d//GTabzd2upI8RI0YYaWlpJV7zpptucrd///33jdWrVxtNmjQp8nyJiYllum7+8+XXrFkz92uxsbHGhx9+aISFhRX7/vPOO89ISEhwv/+JJ54w7HZ7se27detW6tfbsWPHMmUPDAw0nnvuuVL/m/37a1q4cKFRu3btYs8bGhpqfPXVVyWeM/89tHz5cmP16tVGo0aNij2nw+Ewpk+fXuI5x48f727/0EMPlfp1iYhI9aM5wCIi4nMOHDiAYRjYbDbOPPNMzjzzTKKioggMDOT48eNs2rSJPXv2ADBnzhxSUlL46quvsNlspZ579+7dTJo0ieTkZGrWrMlll11GTEwMiYmJrFy5EoDx48cDsHDhQg4fPgzAoEGDaNSoUaHztWnTpthrffPNN0yYMAGXy8UZZ5zBhRdeSEhICL///jvr168HYNOmTYwYMYJvv/2WZ555hkceeQSAc845h/bt2xMQEMC6devYtm0bAD/++CN3330306ZNK/a6eT27wcHBnHXWWZx++unUrl0bwzD4+++/+eWXX4iPj8fpdHLfffcBcO+995b6vQNYtmwZ48aNIzc3l6ZNm3LxxRdTq1YtYmNj+fHHH8nJySEjI4Phw4ezdetWWrRoUeo5t27dygMPPMCJEyeIjo6mS5cuREVFcejQIX744QcyMjLIzc1l3LhxtGvXjk6dOhV5njVr1riPO3ToAMD69euZNm0aP/74I4cPHyYsLIymTZvSo0cPbr/9dlq2bFmmr1tERHyE1RW4iIhUP57uAX7++eeN999/34iLiyv2PCtXrjROP/1097k+/PDDYtvm7wEOCAgwAGP8+PFGampqgXbZ2dlGbm5ukV/n8uXLy/T15e8tDQ4ONmrWrGnMmzevULs5c+YYDofD3faVV14xHA6HERMTY/z444+F2r/00kvutna73YiNjS02w+233258/fXXRnp6epGv5+TkGO+//74RHh7u7gn+66+/yvw1hYeHGx9++KHhcrkKtNu6dWuBXtzRo0cXe87839vg4GDD4XAYL730kuF0Ogu0279/v3H22We723bv3r3I82VkZLj/2wLG+vXrjfvuu6/E3vTAwEDjySefLDajiIj4Hpth/LMShIiISCXp1q2be35sx44dufDCC8v83nXr1rl7P7t27eqe/1kRe/fupU2bNmRmZnLhhRfyyy+/FNlu1KhRzJ492/341ltv5d133y31/Pm/zuXLl9OtW7dS39O8eXP3HFibzcbSpUvp0aNHkW1vu+22AnvVhoaG8uuvvxbbq9yzZ0+WLVsGwHPPPVfmXtvizJ07lxEjRgBmD/Bzzz1XZLt/f01LliyhT58+Rbb9+uuv6devHwA1atQgMTGRgIDCA9Lyf28B3nnnHcaMGVPkObdu3co555zjHhVw6NAhGjZsWKBNbGxsgd7ca665hrlz5wJQu3ZtunfvTr169Th06BDLly8nIyPD3bakr11ERHyLhkCLiIhHrV+/3l3QVrXmzZvTvXt3vvnmG9avX09KSgq1atUq8T0hISE8//zzVZJvwIABxRa/ANdee22BAnjs2LElDqm+9tpr3QXwunXrTjnf0KFDqVGjBidOnHCftzT9+vUrtvgF6Nu3Lw0aNODIkSOcOHGCP/74g3bt2pV4znbt2hVb/AKcffbZdOzYkXXr1mEYBhs2bKB///4F2iQlJRV4nFf83nzzzUydOpUaNWq4Xzt27BijRo3im2++AeCFF16gV69eXHHFFSXmFBER76cCWEREfNr+/ftZt24dO3fuJCkpiYyMDPIPboqNjQXAMAw2b95Mly5dSjxfr169iIyM9GjmPEOHDi3x9X8XhqW1P/vss93HeV93aX7//Xc2bdrE3r17SUlJISsrq8DrefOmt2zZgsvlwm4veQOJYcOGlfi6zWajffv2HDlyBDB76UsrgEs7J8B5553nLvrzVqLOLy0trdBzAwcOZObMmYWej46OZtGiRVx44YVs3rwZwzB4/PHHVQCLiFQDKoBFRMSjHnvsMSZPnlzm9pMnT2bKlCmltvvpp5+4//77WbVqFWWdzRMfH19qm7zFkapC/oK1KP8uxM8666wS29epU8d9nJKSUmLb2bNn8/TTT7Nz585SUpqcTifJycml/nGgtGIWICoqqsw5K+ucISEhhZ4raVhzUFAQTz31lHu49sqVKzly5AgNGjQoNYuIiHgv7QMsIiI+57333qNz586sXLmyzMUvQGpqaqlt6tWrdyrRyqV27dolvv7vubHlae90OotsYxgGN998M6NGjSpz8Zun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BRXAIiIiIiIi4hdUAIuIiIiIiIhfUAEsIiIiIiIifkEFsIiIiIiIiPiFAKsDeIP4+HhcLle531ejRg1CQ0OLPadhGBXKExYWRnh4eJGvJSQkkJubW6HzhoSEULNmzSJfS0pKwul0Vui8QUFB1K5du8jXkpOTyc7OrtB5AwMDiYiIKPK11NRUMjMzK3Reh8NBnTp1inwtLS2N9PT0Cp3XZrNRt27dIl/LyMjgxIkTFTovQL169Yp8Pisri5SUlAqfNyoqCru98N/BsrOzSU5OrvB5IyMjCQgo/OMlJyeHxMTECp+3du3aBAUFFXre5XJx/PjxCp+3Vq1aBAcHF/laXFxchc+rnxEmm81W7Gv6GWHSzwiTN/2McDqdJCcnExcXR1RUlH5GoN8j8njzz4j8921gYKD7ef2MMOn3CJM3/YxISEio0LVOieHHkpOTDaDCH2+88Uax565bt26Fz/vYY48Ve962bdtW+Lx33HFHseft2rVrhc87dOjQYs87dOjQCp+3a9euxZ73jjvuqPB527ZtW+x5H3vssQqft27dusWe94033jiley2/7OxsY9GiRUZ2drbx2WefndJ5jx07VmTe5cuXn9J5t27dWuR5t27dekrnXb58eZHnPXbs2Cmd97PPPiv2v92pnFc/I8yPIUOGuO/Zf9PPCFNl/ozITz8jTPoZcZI3/ozQ7xHmh35GnPzQzwjzw59+RiQnJxf73sqmIdAiIiIiIiLiF1QAi4iIiIiIiF9QASwiIiIiIiJ+wWYYFZw9XQ2kpKRQu3ZtduzYUexiBiXxh4npZaHFK0xVtcCN0+lkyZIl9O3bF5fLpcUr0OIV+XnjzwibzcaaNWvo27dvgUVZQD8j8mgRLJM3/YxwOp0sW7aMHj16aBGsf+j3CJM3/4zIf99qEazC9HuEyZt+RiQkJNC6dWuSk5OpVatWha5bXiqAa9cmPj6eqKgoq+OIlEn+AvjfxYSIN9I9K75I9634It234muOHz9O3bp1q7QA1hBoERERERER8QsqgEVERERERMQvqAAWERERERERv6ACWERERERERPyCCmARERERERHxCyqARURERERExC+oABYRERERERG/oAJYRERERERE/IIKYBEREREREfELKoBFRERERETEL6gAFhEREREREb+gAlhERERERET8ggpgERERERER8QsqgEVERERERMQvqAAWERERERERv6ACWERERERERPyCCmARERERERHxCyqARURERERExC+oABYRERERERG/oAJYRERERERE/IIKYBEREREREfELKoBFRERERETEL6gAFhEREREREb+gAlhERERERET8ggpgERERERER8QsqgEVERERERMQvqAAWERERERERv6ACWERERERERPyCCmARERERERHxCyqARURERERExC+oABYRERERERG/oAJYRERERERE/IIKYBEREREREfELKoBFRERERETEL6gAFhEREREREb+gAlhERERERET8ggpgERERERER8QsqgEVERERERMQvqAAWERERERERv6ACWERERERERPyCCmARERERERHxCyqARURERERExC+oABYRERERERG/oAJYRERERERE/IIKYBEREREREfEL5S6AU1JSmDNnDv/973/p2rUrp59+OrVr1yYoKIjo6Gi6devG888/z/Hjxyst5DfffMPgwYNp3LgxwcHBNG7cmMGDB/PNN99U2jVERERERESkegso7xvWrVvHtddeW+RrcXFxrFixghUrVvDCCy/w0Ucf0bt37wqHc7lcjBkzhpkzZxZ4/tChQxw6dIhFixZx66238s4772C3qzNbREREREREilfuAhigSZMmdO/enQ4dOtCkSRMaNmyIy+Xi4MGDzJ8/nwULFhAfH8+AAQNYt24d7du3r1C4hx56yF38nnfeedx7772cdtpp7Nmzh+eff55NmzYxY8YM6tWrx9NPP12ha4iIiIiIiIh/KHcB3L17d/bv31/s68OHD2fRokUMHjyY7OxspkyZwoIFC8odbOfOnbz44osAXHDBBaxcuZLQ0FAAOnbsyIABA+jatSsbNmzghRde4Oabb+b0008v93VERERERETEP5R73LDD4Si1zaBBgzjzzDMBWLVqVflTAa+++io5OTkAvP766+7iN09YWBivv/46ADk5ObzyyisVuo6IiIiIiIj4B49NnK1ZsyYAmZmZ5X6vYRgsXrwYgNatW9OpU6ci23Xq1MldaC9evBjDMCqYVkREREREpLC1B9bSaUYn1h5Ya3UUZakEHimA//zzT3777TfALGDLKzY2lsOHDwPQtWvXEtvmvX7o0CH27t1b7muJiIiIiIgU5/VfXueXQ7/wxro3rI6iLJWg0grg9PR0du3axcsvv0zXrl3d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nI4RbliSJcQeQqYrE4CmIU25vsH11A2qW2KW3FxITQ0mIiKryNdFysKelUWj1atp8b//Eblrl/v5lMaNCTt2DEd2NiWNrjdsNpxhYXz7/vu4goI8H7gSpaenc91115GcnEytWrWq5JqVVgBv27aNLl26kJiYSEhICN9++22VbH906623MnPmTAA++ugjrr/++mLbFtUD3KRJE/7++2+ioirnh6yIpzmdTr777jt69uxJYGCg1XFESqV7VnyRL963l82+jJ8P/VzoeRs2DEr/de/ixhezYuSKUtspi/fyxfvWr+3cif3dd7F/8AG2f9Y1MgIDMYYMwTV2LEbnzti+/hrH1VcDYCuibDP+mXie+/nnGP36VV32SnL8+HEaNmxYpQVwpQyBjo2NpVevXiQmJuJwOJgzZ06V7f07duxYdwG8YsWKEgvg4OBggoMLzysJDAzUDwnxObpvxdfonhVf5Ev37S3n38KGvzeQ68otUNiVVuTZsGG32bmw0YUs27usUrJc2OhC1h9ej8tw+XQWh93BLefdUuAeuOceCAiARx+F0NBKiVjpfOm+9TtOJyxeDG+/DT/8cPL55s1h7FhsN9+MLTr65EJNgwebq0GPGmVuiWS3mwte/fPZFhEBs2cT0L9/VX8llcKK+/SUC+DDhw/To0cPDh8+jM1m47333mPgwIGVka1M2rZt6z4+dOhQlV1XRERExJvcev6tnB19NgM+HUBiRiI5Rk6Z3mdgkGvkMvWXqUz9ZaqHU/pGFofNQVRYFItHLKZT407u59etg5deAsOAHj3giissiyi+5sABc9nwGTPg77/N52w2uOoquP126N0bits+dsAAc9W1+fPNrY4SEswFrwYPNvfeKmEKqBR2SgVwfHw8PXv25K+//gLg9ddfZ+TIkZUSrKxsWm9eREREBIBOjTuxedxmBs8d7O71LEl4YDinRZ5GoMMzvTDOXCd7EveQ5kwruaEB4UHekcWGjY6NOrLwmoU0qNHg5Pud5p6/hgHXX6/iV8rA5YLvvjN7e7/88uSWRvXrw623wm23QbNmZTtXSAjccIP5IaekwgVwcnIyvXv3Zvv27QA8++yzjB8/vtKClVXe9cFcCEtERETEnzWs2ZCVo1fS84OerNy/sth21519He8Pep8gh2cXzcnOzWb0otF8svWTYtsMqT2ET+/61CuyXHv2tUV+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mUwMHz6cAwcOaOW/+uorrl+/rt3/5ZdfuH79unbbu3dvnp9L//796datGxcuXCA6OpqYmBjGjBkDwNKlS/n44495+eWXefnll7l+/TrR0dFERkYydOhQAD777DNOnz6d5bjVqlXjs88+48iRIyQlJREREUFKSgpHjx5lwIABpKSkMGTIEK5evZrnmG0ZPHgwYO5JcPLkSatlLl68yLZt2zKVt2fMubFp0yYGDx6M0Wjk7bffJjQ0lKSkJBISEjh58iRPP/00cXFxPP300yWzJbjIUm0HJC3ARefBYTUUk1D9unW0dyjFnrSmieJGrllhT3lpjSBDK1Reb9OnT7d53FKlSuX7uNm18NSpU8dqncIwduxY7fhXr1696+Nl1wLcoUMHBahq1aqphISELPsPHDigtaJ99tlnmfZlbImsUaOG1ZbaTZs2aWWWLVuWp7j37t2rAOXp6Wn1mrIcN68twEuXLtXqWmt1S0tLU48++qgCVL169bLst7QAc7vl+86eC4mJiVrLcKdOnfIVd25bgNu2batMJlOW+i1atNDKPP/881n2p6enqypVqihAvf/++zbjsKVz58426+a3BVgppWrWrKkANWbMGKv7P/zwQwWoSpUqWX3ehRHz3bQAG41G7VqYNWuWzfpPPfWUAtRrr72Wy2djJi3AQgBbZszjUPBpnIxOjBo60d7hCCGEECKPIiIitO2AgACrZc6ePUtQUJDV265du3J1nujoaDZs2ADAW2+9hYeHR5YyDRo0oGfPngAsWbLE5rHeeust3N3dszz+5JNP8thjjwHmFsm8ePjhhyldujQJCQnZjsnNK8tM2U2bNqVdu3ZZ9js5OTFxovkz1NGjRzly5IjV4zRr1oyWLVtmedzd3Z233noLgD/++IOYmJiCCj2Ld955J9O4Yov27dtr25YW4YwMBgNt2rQBzGOI86pz584A7NixI891szNo0CAAFi9ebHV88qJFiwAYMGCA1eedncKKOTt//fUXZ86coVSpUjz//PM2y1lasy2/jyWJJMAlyK5Lu2gytwm7LuXun0xRxfLp7pkAdAlrzCPdH7VzZEIIIYQoDOnp6dy4ccPqLTU1NVfHOHDggJZkPPnkkzbLtW3bFjAnSrYmpXriiSds1rfs27dvX5Z9qampzJw5k3bt2lGuXDlcXV0zTQx18+ZNAC5fvpyr55Qbljiy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "\n", + "fig = plt.figure(figsize=(10, 8))\n", + "matplotlib.rcParams.update({\"font.size\": 20})\n", + "\n", + "results = [\n", + " (Y_chol.cpu(), f\"Cholesky-{N_CAND}\", \"b\", \"\", 14, \"--\"),\n", + " (Y_rff.cpu(), f\"RFF-{N_CAND}\", \"r\", \".\", 16, \"-\"),\n", + " (Y_lanczos.cpu(), f\"Lanczos-{N_CAND}\", \"m\", \"^\", 9, \"-\"),\n", + " (Y_ciq.cpu(), f\"CIQ-{N_CAND}\", \"g\", \"*\", 12, \"-\"),\n", + "]\n", + "\n", + "optimum = hart6.optimal_value\n", + "\n", + "ax = fig.add_subplot(1, 1, 1)\n", + "names = []\n", + "for res, name, c, m, ms, ls in results:\n", + " names.append(name)\n", + " fx = res.cummax(dim=0)[0]\n", + " t = 1 + np.arange(len(fx))\n", + " plt.plot(t[0::2], fx[0::2], c=c, marker=m, linestyle=ls, markersize=ms)\n", + "\n", + "plt.plot([0, max_evals], [hart6.optimal_value, hart6.optimal_value], \"k--\", lw=3)\n", + "plt.xlabel(\"Function value\", fontsize=18)\n", + "plt.xlabel(\"Number of evaluations\", fontsize=18)\n", + "plt.title(\"Hartmann6\", fontsize=24)\n", + "plt.xlim([0, max_evals])\n", + "plt.ylim([0, 3.5])\n", + "\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.legend(\n", + " names + [\"Global optimal value\"],\n", + " loc=\"lower right\",\n", + " ncol=1,\n", + " fontsize=18,\n", + ")\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/website-old/static/files/thompson_sampling.py b/website-old/static/files/thompson_sampling.py new file mode 100644 index 0000000000..c9d363a529 --- /dev/null +++ b/website-old/static/files/thompson_sampling.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## Tutorial on large-scale Thompson sampling +# +# This demo currently considers four approaches to discrete Thompson sampling on `m` candidates points: +# +# 1. **Exact sampling with Cholesky:** Computing a Cholesky decomposition of the corresponding `m x m` covariance matrix which reuqires `O(m^3)` computational cost and `O(m^2)` space. This is the standard approach to sampling from a Gaussian process, but the quadratic memory usage and cubic compliexity limits the number of candidate points. +# +# 2. **Contour integral quadrature (CIQ):** CIQ [1] is a Krylov subspace method combined with a rational approximation that can be used for computing matrix square roots of covariance matrices, which is the main bottleneck when sampling from a Gaussian process. CIQ relies on computing matrix vector multiplications with the exact kernel matrix which requires `O(m^2)` computational complexity and space. Note that the space complexity can be further lowered to `O(m)` by using [KeOps](https://github.com/getkeops/keops), but this is not covered as part of the tutorial. +# +# 3. **Lanczos:** Rather than using CIQ, we can solve the linear systems `K^(1/2) v = b` using Lanczos and the conjugate gradient (CG) method. This will be faster than CIQ, but will generally produce samples of worse quality. Similarly to CIQ, [KeOps](https://github.com/getkeops/keops) can be used to improve space complexity of Lanczos. +# +# 4. **Random Fourier features (RFFs):** The RFF kernel was originally proposed in [2] and we use it as implemented in GPyTorch. RFFs are computationally cheap to work with as the computational cost and space are both `O(km)` where `k` is the number of Fourier features. Note that while Cholesky and CIQ are able to generate exact samples from the GP model, RFFs are an unbiased approximation and the resulting samples often aren't perfectly calibrated. +# +# +# [1] [Pleiss, Geoff, et al. "Fast matrix square roots with applications to Gaussian processes and Bayesian optimization.", Advances in neural information processing systems (2020)](https://proceedings.neurips.cc/paper/2020/file/fcf55a303b71b84d326fb1d06e332a26-Paper.pdf) +# +# [2] [Rahimi, Ali, and Benjamin Recht. "Random features for large-scale kernel machines.", Advances in neural information processing systems (2007)](https://people.eecs.berkeley.edu/~brecht/papers/07.rah.rec.nips.pdf) + +# In[1]: + + +import os +import time +from contextlib import ExitStack + +import gpytorch +import gpytorch.settings as gpts +import torch +from gpytorch.constraints import Interval +from gpytorch.distributions import MultivariateNormal +from gpytorch.kernels import RBFKernel, RFFKernel, ScaleKernel +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.mlls import ExactMarginalLogLikelihood +from torch.quasirandom import SobolEngine +from torch import Tensor + +from botorch.fit import fit_gpytorch_mll +from botorch.generation import MaxPosteriorSampling +from botorch.models import SingleTaskGP +from botorch.models.transforms.outcome import Standardize +from botorch.test_functions import Hartmann +from botorch.utils.sampling import draw_sobol_samples + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +dtype = torch.double +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# We will use 6 dimensional Hartmann test function, which is typically evaluated on the unit hypercube. + +# In[2]: + + +hart6 = Hartmann(dim=6, negate=True).to(device=device, dtype=dtype) +dim = hart6.dim + + +# In[3]: + + +def generate_batch( + X: Tensor, + Y: Tensor, + batch_size: int, + n_candidates: int, + sampler: str, # "cholesky", "ciq", "rff", "lanczos" + seed: int, +) -> Tensor: + assert sampler in ("cholesky", "ciq", "rff", "lanczos") + assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y)) + + if sampler == "rff": + base_kernel = RFFKernel(ard_num_dims=X.shape[-1], num_samples=1024) + else: + base_kernel = RBFKernel(ard_num_dims=X.shape[-1]) + covar_module = ScaleKernel(base_kernel) + + # Fit a GP model + model = SingleTaskGP(train_X=X, train_Y=Y, covar_module=covar_module, outcome_transform=Standardize(m=1)) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + + # Draw samples on a Sobol sequence + X_cand = draw_sobol_samples(bounds=hart6.bounds, n=n_candidates, q=1, seed=seed).squeeze(-2) + + # Thompson sample + with ExitStack() as es: + if sampler == "cholesky": + es.enter_context(gpts.max_cholesky_size(float("inf"))) + elif sampler == "ciq": + es.enter_context(gpts.fast_computations(covar_root_decomposition=True)) + es.enter_context(gpts.max_cholesky_size(0)) + es.enter_context(gpts.ciq_samples(True)) + es.enter_context( + gpts.minres_tolerance(2e-3) + ) # Controls accuracy and runtime + es.enter_context(gpts.num_contour_quadrature(15)) + elif sampler == "lanczos": + es.enter_context( + gpts.fast_computations( + covar_root_decomposition=True, log_prob=True, solves=True + ) + ) + es.enter_context(gpts.max_lanczos_quadrature_iterations(10)) + es.enter_context(gpts.max_cholesky_size(0)) + es.enter_context(gpts.ciq_samples(False)) + elif sampler == "rff": + es.enter_context(gpts.fast_computations(covar_root_decomposition=True)) + es.enter_context(torch.no_grad()) + + thompson_sampling = MaxPosteriorSampling(model=model, replacement=False) + X_next = thompson_sampling(X_cand, num_samples=batch_size) + + return X_next + + +# In[4]: + + +def run_optimization( + sampler: str, + n_candidates: int, + n_init: int, + max_evals: int, + batch_size: int, + seed: int, +) -> tuple[Tensor, Tensor]: + X = draw_sobol_samples(bounds=hart6.bounds, n=n_init, q=1, seed=seed).squeeze(-2) + Y = torch.tensor( + [hart6(x) for x in X], dtype=dtype, device=device + ).unsqueeze(-1) + print(f"{len(X)}) Best value: {Y.max().item():.2e}") + + inner_seed = seed + while len(X) < max_evals: + # Create a batch + start = time.monotonic() + inner_seed += 1 + X_next = generate_batch( + X=X, + Y=Y, + batch_size=min(batch_size, max_evals - len(X)), + n_candidates=n_candidates, + seed=inner_seed, + sampler=sampler, + ) + end = time.monotonic() + print(f"Generated batch in {end - start:.1f} seconds") + Y_next = torch.tensor( + [hart6(x) for x in X_next], dtype=dtype, device=device + ).unsqueeze(-1) + + # Append data + X = torch.cat((X, X_next), dim=0) + Y = torch.cat((Y, Y_next), dim=0) + + print(f"{len(X)}) Best value: {Y.max().item():.2e}") + return X, Y + + +# In[5]: + + +batch_size = 5 +n_init = 10 +max_evals = 50 +seed = 12345 # To get the same Sobol points +N_CAND = 10_000 if not SMOKE_TEST else 10 + +shared_args = { + "n_candidates": N_CAND, + "n_init": n_init, + "max_evals": max_evals, + "batch_size": batch_size, + "seed": seed, +} + + +# ## Track memory footprint + +# In[6]: + + +get_ipython().run_line_magic('load_ext', 'memory_profiler') + + +# ## Cholesky + +# In[7]: + + +get_ipython().run_line_magic('memit', 'X_chol, Y_chol = run_optimization("cholesky", **shared_args)') + + +# ## RFFs + +# In[8]: + + +get_ipython().run_line_magic('memit', 'X_rff, Y_rff = run_optimization("rff", **shared_args)') + + +# ## Lanczos + +# In[9]: + + +get_ipython().run_line_magic('memit', 'X_lanczos, Y_lanczos = run_optimization("lanczos", **shared_args)') + + +# ## CIQ + +# In[10]: + + +get_ipython().run_line_magic('memit', 'X_ciq, Y_ciq = run_optimization("ciq", **shared_args)') + + +# ## Plot + +# In[12]: + + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np + + +fig = plt.figure(figsize=(10, 8)) +matplotlib.rcParams.update({"font.size": 20}) + +results = [ + (Y_chol.cpu(), f"Cholesky-{N_CAND}", "b", "", 14, "--"), + (Y_rff.cpu(), f"RFF-{N_CAND}", "r", ".", 16, "-"), + (Y_lanczos.cpu(), f"Lanczos-{N_CAND}", "m", "^", 9, "-"), + (Y_ciq.cpu(), f"CIQ-{N_CAND}", "g", "*", 12, "-"), +] + +optimum = hart6.optimal_value + +ax = fig.add_subplot(1, 1, 1) +names = [] +for res, name, c, m, ms, ls in results: + names.append(name) + fx = res.cummax(dim=0)[0] + t = 1 + np.arange(len(fx)) + plt.plot(t[0::2], fx[0::2], c=c, marker=m, linestyle=ls, markersize=ms) + +plt.plot([0, max_evals], [hart6.optimal_value, hart6.optimal_value], "k--", lw=3) +plt.xlabel("Function value", fontsize=18) +plt.xlabel("Number of evaluations", fontsize=18) +plt.title("Hartmann6", fontsize=24) +plt.xlim([0, max_evals]) +plt.ylim([0, 3.5]) + +plt.grid(True) +plt.tight_layout() +plt.legend( + names + ["Global optimal value"], + loc="lower right", + ncol=1, + fontsize=18, +) +plt.show() + diff --git a/website-old/static/files/turbo_1.ipynb b/website-old/static/files/turbo_1.ipynb new file mode 100644 index 0000000000..d1dbf9f977 --- /dev/null +++ b/website-old/static/files/turbo_1.ipynb @@ -0,0 +1,1115 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "1bc3d568-b16a-4fe5-9667-c0e187f9a366", + "showInput": false + }, + "source": [ + "## BO with TuRBO-1 and TS/qEI\n", + "\n", + "In this tutorial, we show how to implement Trust Region Bayesian Optimization (TuRBO) [1] in a closed loop in BoTorch.\n", + "\n", + "This implementation uses one trust region (TuRBO-1) and supports either parallel expected improvement (qEI) or Thompson sampling (TS). We optimize the $20D$ Ackley function on the domain $[-5, 10]^{20}$ and show that TuRBO-1 outperforms qEI as well as Sobol.\n", + "\n", + "Since botorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\\max_x -f(x)=0$.\n", + "\n", + "[1]: [Eriksson, David, et al. Scalable global optimization via local Bayesian optimization. Advances in Neural Information Processing Systems. 2019](https://proceedings.neurips.cc/paper/2019/file/6c990b7aca7bc7058f5e98ea909e924b-Paper.pdf)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921563794, + "executionStopTime": 1674921566438, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "c11881c9-13f5-4e35-bdc8-b8f817089713", + "requestMsgId": "b21eda64-89d8-461f-a9d1-57117892e0c9" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[KeOps] Warning : omp.h header is not in the path, disabling OpenMP. To fix this, you can set the environment\n", + " variable OMP_PATH to the location of the header before importing keopscore or pykeops,\n", + " e.g. using os.environ: import os; os.environ['OMP_PATH'] = '/path/to/omp/header'\n", + "[KeOps] Warning : Cuda libraries were not detected on the system or could not be loaded ; using cpu only mode\n" + ] + } + ], + "source": [ + "import os\n", + "import math\n", + "import warnings\n", + "from dataclasses import dataclass\n", + "\n", + "import torch\n", + "from botorch.acquisition import qExpectedImprovement, qLogExpectedImprovement\n", + "from botorch.exceptions import BadInitialCandidatesWarning\n", + "from botorch.fit import fit_gpytorch_mll\n", + "from botorch.generation import MaxPosteriorSampling\n", + "from botorch.models import SingleTaskGP\n", + "from botorch.optim import optimize_acqf\n", + "from botorch.test_functions import Ackley\n", + "from botorch.utils.transforms import unnormalize\n", + "from torch.quasirandom import SobolEngine\n", + "\n", + "import gpytorch\n", + "from gpytorch.constraints import Interval\n", + "from gpytorch.kernels import MaternKernel, ScaleKernel\n", + "from gpytorch.likelihoods import GaussianLikelihood\n", + "from gpytorch.mlls import ExactMarginalLogLikelihood\n", + "\n", + "\n", + "warnings.filterwarnings(\"ignore\", category=BadInitialCandidatesWarning)\n", + "warnings.filterwarnings(\"ignore\", category=RuntimeWarning)\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "5be02873-2895-4451-8bf6-35e3cd0e6f99", + "showInput": false + }, + "source": [ + "## Optimize the 20-dimensional Ackley function\n", + "\n", + "The goal is to minimize the popular Ackley function:\n", + "\n", + "$f(x_1,\\ldots,x_d) = -20\\exp\\left(-0.2 \\sqrt{\\frac{1}{d} \\sum_{j=1}^d x_j^2} \\right) -\\exp \\left( \\frac{1}{d} \\sum_{j=1}^d \\cos(2 \\pi x_j) \\right) + 20 + e$\n", + "\n", + "over the domain $[-5, 10]^{20}$. The global optimal value of $0$ is attained at $x_1 = \\ldots = x_d = 0$.\n", + "\n", + "As mentioned above, since botorch assumes a maximization problem, we instead maximize $-f(x)$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921566576, + "executionStopTime": 1674921566582, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "069fba29-e308-4a40-b92e-b1a5bdc8dcd8", + "requestMsgId": "40b2ab4c-067e-4e9f-9330-93dcda5f3e8c" + }, + "outputs": [], + "source": [ + "fun = Ackley(dim=20, negate=True).to(dtype=dtype, device=device)\n", + "fun.bounds[0, :].fill_(-5)\n", + "fun.bounds[1, :].fill_(10)\n", + "dim = fun.dim\n", + "lb, ub = fun.bounds\n", + "\n", + "batch_size = 4\n", + "n_init = 2 * dim\n", + "max_cholesky_size = float(\"inf\") # Always use Cholesky\n", + "\n", + "\n", + "def eval_objective(x):\n", + " \"\"\"This is a helper function we use to unnormalize and evalaute a point\"\"\"\n", + " return fun(unnormalize(x, fun.bounds))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6e19c4b3-1364-4789-833d-c7ae648e7a78", + "showInput": false + }, + "source": [ + "## Maintain the TuRBO state\n", + "TuRBO needs to maintain a state, which includes the length of the trust region, success and failure counters, success and failure tolerance, etc. \n", + "\n", + "In this tutorial we store the state in a dataclass and update the state of TuRBO after each batch evaluation. \n", + "\n", + "**Note**: These settings assume that the domain has been scaled to $[0, 1]^d$ and that the same batch size is used for each iteration." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921566718, + "executionStopTime": 1674921566731, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "4c419a40-d6cf-43de-8c60-e8445c3ca473", + "requestMsgId": "5fb06df5-5815-47f9-bfa5-73155751345f" + }, + "outputs": [], + "source": [ + "@dataclass\n", + "class TurboState:\n", + " dim: int\n", + " batch_size: int\n", + " length: float = 0.8\n", + " length_min: float = 0.5**7\n", + " length_max: float = 1.6\n", + " failure_counter: int = 0\n", + " failure_tolerance: int = float(\"nan\") # Note: Post-initialized\n", + " success_counter: int = 0\n", + " success_tolerance: int = 10 # Note: The original paper uses 3\n", + " best_value: float = -float(\"inf\")\n", + " restart_triggered: bool = False\n", + "\n", + " def __post_init__(self):\n", + " self.failure_tolerance = math.ceil(\n", + " max([4.0 / self.batch_size, float(self.dim) / self.batch_size])\n", + " )\n", + "\n", + "\n", + "def update_state(state, Y_next):\n", + " if max(Y_next) > state.best_value + 1e-3 * math.fabs(state.best_value):\n", + " state.success_counter += 1\n", + " state.failure_counter = 0\n", + " else:\n", + " state.success_counter = 0\n", + " state.failure_counter += 1\n", + "\n", + " if state.success_counter == state.success_tolerance: # Expand trust region\n", + " state.length = min(2.0 * state.length, state.length_max)\n", + " state.success_counter = 0\n", + " elif state.failure_counter == state.failure_tolerance: # Shrink trust region\n", + " state.length /= 2.0\n", + " state.failure_counter = 0\n", + "\n", + " state.best_value = max(state.best_value, max(Y_next).item())\n", + " if state.length < state.length_min:\n", + " state.restart_triggered = True\n", + " return state" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e03f6fa1-83d1-4f7e-8dfd-0a0a53a9ad1c", + "showInput": false + }, + "source": [ + "## Take a look at the state" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921566859, + "executionStopTime": 1674921566868, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "e06a71f5-ab79-4c11-a798-2dd5f3cf40e1", + "requestMsgId": "af20e76d-b6b3-4f59-82ae-e1d3a3159b8d" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TurboState(dim=20, batch_size=4, length=0.8, length_min=0.0078125, length_max=1.6, failure_counter=0, failure_tolerance=5, success_counter=0, success_tolerance=10, best_value=-inf, restart_triggered=False)\n" + ] + } + ], + "source": [ + "state = TurboState(dim=dim, batch_size=batch_size)\n", + "print(state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "9fc2a1a5-1b3e-429a-933f-49739c0e9a6b", + "showInput": false + }, + "source": [ + "## Generate initial points\n", + "This generates an initial set of Sobol points that we use to start of the BO loop." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921567266, + "executionStopTime": 1674921567271, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "f0a7d80a-efba-4b9d-b5bc-64fdf62d0e99", + "requestMsgId": "890e6347-f465-428c-a332-f6e3bbe34aa6" + }, + "outputs": [], + "source": [ + "def get_initial_points(dim, n_pts, seed=0):\n", + " sobol = SobolEngine(dimension=dim, scramble=True, seed=seed)\n", + " X_init = sobol.draw(n=n_pts).to(dtype=dtype, device=device)\n", + " return X_init" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "d7ed19a9-4662-496c-880b-c2e0717c4117", + "showInput": false + }, + "source": [ + "## Generate new batch\n", + "Given the current `state` and a probabilistic (GP) `model` built from observations `X` and `Y`, we generate a new batch of points. \n", + "\n", + "This method works on the domain $[0, 1]^d$, so make sure to not pass in observations from the true domain. `unnormalize` is called before the true function is evaluated which will first map the points back to the original domain.\n", + "\n", + "We support either TS and qEI which can be specified via the `acqf` argument." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921567409, + "executionStopTime": 1674921567429, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "f4a1f540-1959-4f95-92b1-696525a50347", + "requestMsgId": "90e9fc43-786b-4027-b89e-f76dc8e472f0" + }, + "outputs": [], + "source": [ + "def generate_batch(\n", + " state,\n", + " model, # GP model\n", + " X, # Evaluated points on the domain [0, 1]^d\n", + " Y, # Function values\n", + " batch_size,\n", + " n_candidates=None, # Number of candidates for Thompson sampling\n", + " num_restarts=10,\n", + " raw_samples=512,\n", + " acqf=\"ts\", # \"ei\" or \"ts\"\n", + "):\n", + " assert acqf in (\"ts\", \"ei\")\n", + " assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y))\n", + " if n_candidates is None:\n", + " n_candidates = min(5000, max(2000, 200 * X.shape[-1]))\n", + "\n", + " # Scale the TR to be proportional to the lengthscales\n", + " x_center = X[Y.argmax(), :].clone()\n", + " weights = model.covar_module.base_kernel.lengthscale.squeeze().detach()\n", + " weights = weights / weights.mean()\n", + " weights = weights / torch.prod(weights.pow(1.0 / len(weights)))\n", + " tr_lb = torch.clamp(x_center - weights * state.length / 2.0, 0.0, 1.0)\n", + " tr_ub = torch.clamp(x_center + weights * state.length / 2.0, 0.0, 1.0)\n", + "\n", + " if acqf == \"ts\":\n", + " dim = X.shape[-1]\n", + " sobol = SobolEngine(dim, scramble=True)\n", + " pert = sobol.draw(n_candidates).to(dtype=dtype, device=device)\n", + " pert = tr_lb + (tr_ub - tr_lb) * pert\n", + "\n", + " # Create a perturbation mask\n", + " prob_perturb = min(20.0 / dim, 1.0)\n", + " mask = torch.rand(n_candidates, dim, dtype=dtype, device=device) <= prob_perturb\n", + " ind = torch.where(mask.sum(dim=1) == 0)[0]\n", + " mask[ind, torch.randint(0, dim - 1, size=(len(ind),), device=device)] = 1\n", + "\n", + " # Create candidate points from the perturbations and the mask\n", + " X_cand = x_center.expand(n_candidates, dim).clone()\n", + " X_cand[mask] = pert[mask]\n", + "\n", + " # Sample on the candidate points\n", + " thompson_sampling = MaxPosteriorSampling(model=model, replacement=False)\n", + " with torch.no_grad(): # We don't need gradients when using TS\n", + " X_next = thompson_sampling(X_cand, num_samples=batch_size)\n", + "\n", + " elif acqf == \"ei\":\n", + " ei = qExpectedImprovement(model, train_Y.max())\n", + " X_next, acq_value = optimize_acqf(\n", + " ei,\n", + " bounds=torch.stack([tr_lb, tr_ub]),\n", + " q=batch_size,\n", + " num_restarts=num_restarts,\n", + " raw_samples=raw_samples,\n", + " )\n", + "\n", + " return X_next" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "6b3dceba-35f5-4678-b21a-3b4ca22d3190", + "showInput": false + }, + "source": [ + "## Optimization loop\n", + "This simple loop runs one instance of TuRBO-1 with Thompson sampling until convergence.\n", + "\n", + "TuRBO-1 is a local optimizer that can be used for a fixed evaluation budget in a multi-start fashion. Once TuRBO converges, `state[\"restart_triggered\"]` will be set to true and the run should be aborted. If you want to run more evaluations with TuRBO, you simply generate a new set of initial points and then keep generating batches until convergence or when the evaluation budget has been exceeded. It's important to note that evaluations from previous instances are discarded when TuRBO restarts.\n", + "\n", + "NOTE: We use a `SingleTaskGP` with a noise constraint to keep the noise from getting too large as the problem is noise-free. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921567583, + "executionStopTime": 1674921663734, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "89258ea0-2a0c-4b88-8606-79ed531f0d97", + "requestMsgId": "98ebf52b-fddf-485c-a250-d857b501eb19" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "44) Best value: -1.17e+01, TR length: 8.00e-01\n", + "48) Best value: -1.17e+01, TR length: 8.00e-01\n", + "52) Best value: -1.12e+01, TR length: 8.00e-01\n", + "56) Best value: -1.04e+01, TR length: 8.00e-01\n", + "60) Best value: -1.04e+01, TR length: 8.00e-01\n", + "64) Best value: -9.42e+00, TR length: 8.00e-01\n", + "68) Best value: -9.42e+00, TR length: 8.00e-01\n", + "72) Best value: -9.42e+00, TR length: 8.00e-01\n", + "76) Best value: -9.42e+00, TR length: 8.00e-01\n", + "80) Best value: -8.75e+00, TR length: 8.00e-01\n", + "84) Best value: -8.75e+00, TR length: 8.00e-01\n", + "88) Best value: -8.75e+00, TR length: 8.00e-01\n", + "92) Best value: -8.75e+00, TR length: 8.00e-01\n", + "96) Best value: -8.27e+00, TR length: 8.00e-01\n", + "100) Best value: -8.27e+00, TR length: 8.00e-01\n", + "104) Best value: -8.27e+00, TR length: 8.00e-01\n", + "108) Best value: -8.27e+00, TR length: 8.00e-01\n", + "112) Best value: -8.27e+00, TR length: 8.00e-01\n", + "116) Best value: -8.27e+00, TR length: 4.00e-01\n", + "120) Best value: -6.45e+00, TR length: 4.00e-01\n", + "124) Best value: -6.45e+00, TR length: 4.00e-01\n", + "128) Best value: -6.45e+00, TR length: 4.00e-01\n", + "132) Best value: -6.45e+00, TR length: 4.00e-01\n", + "136) Best value: -5.85e+00, TR length: 4.00e-01\n", + "140) Best value: -5.85e+00, TR length: 4.00e-01\n", + "144) Best value: -5.85e+00, TR length: 4.00e-01\n", + "148) Best value: -5.70e+00, TR length: 4.00e-01\n", + "152) Best value: -5.70e+00, TR length: 4.00e-01\n", + "156) Best value: -5.70e+00, TR length: 4.00e-01\n", + "160) Best value: -5.70e+00, TR length: 4.00e-01\n", + "164) Best value: -5.70e+00, TR length: 4.00e-01\n", + "168) Best value: -5.70e+00, TR length: 2.00e-01\n", + "172) Best value: -4.70e+00, TR length: 2.00e-01\n", + "176) Best value: -4.45e+00, TR length: 2.00e-01\n", + "180) Best value: -4.03e+00, TR length: 2.00e-01\n", + "184) Best value: -4.03e+00, TR length: 2.00e-01\n", + "188) Best value: -4.03e+00, TR length: 2.00e-01\n", + "192) Best value: -4.03e+00, TR length: 2.00e-01\n", + "196) Best value: -4.03e+00, TR length: 2.00e-01\n", + "200) Best value: -3.97e+00, TR length: 2.00e-01\n", + "204) Best value: -3.97e+00, TR length: 2.00e-01\n", + "208) Best value: -3.97e+00, TR length: 2.00e-01\n", + "212) Best value: -3.97e+00, TR length: 2.00e-01\n", + "216) Best value: -3.77e+00, TR length: 2.00e-01\n", + "220) Best value: -3.77e+00, TR length: 2.00e-01\n", + "224) Best value: -3.71e+00, TR length: 2.00e-01\n", + "228) Best value: -3.67e+00, TR length: 2.00e-01\n", + "232) Best value: -3.67e+00, TR length: 2.00e-01\n", + "236) Best value: -3.67e+00, TR length: 2.00e-01\n", + "240) Best value: -3.67e+00, TR length: 2.00e-01\n", + "244) Best value: -3.67e+00, TR length: 2.00e-01\n", + "248) Best value: -3.67e+00, TR length: 1.00e-01\n", + "252) Best value: -3.23e+00, TR length: 1.00e-01\n", + "256) Best value: -3.23e+00, TR length: 1.00e-01\n", + "260) Best value: -3.23e+00, TR length: 1.00e-01\n", + "264) Best value: -2.73e+00, TR length: 1.00e-01\n", + "268) Best value: -2.73e+00, TR length: 1.00e-01\n", + "272) Best value: -2.39e+00, TR length: 1.00e-01\n", + "276) Best value: -2.39e+00, TR length: 1.00e-01\n", + "280) Best value: -2.39e+00, TR length: 1.00e-01\n", + "284) Best value: -2.39e+00, TR length: 1.00e-01\n", + "288) Best value: -2.39e+00, TR length: 1.00e-01\n", + "292) Best value: -2.39e+00, TR length: 5.00e-02\n", + "296) Best value: -2.15e+00, TR length: 5.00e-02\n", + "300) Best value: -2.15e+00, TR length: 5.00e-02\n", + "304) Best value: -1.83e+00, TR length: 5.00e-02\n", + "308) Best value: -1.83e+00, TR length: 5.00e-02\n", + "312) Best value: -1.83e+00, TR length: 5.00e-02\n", + "316) Best value: -1.83e+00, TR length: 5.00e-02\n", + "320) Best value: -1.83e+00, TR length: 5.00e-02\n", + "324) Best value: -1.73e+00, TR length: 5.00e-02\n", + "328) Best value: -1.73e+00, TR length: 5.00e-02\n", + "332) Best value: -1.73e+00, TR length: 5.00e-02\n", + "336) Best value: -1.73e+00, TR length: 5.00e-02\n", + "340) Best value: -1.66e+00, TR length: 5.00e-02\n", + "344) Best value: -1.66e+00, TR length: 5.00e-02\n", + "348) Best value: -1.66e+00, TR length: 5.00e-02\n", + "352) Best value: -1.66e+00, TR length: 5.00e-02\n", + "356) Best value: -1.62e+00, TR length: 5.00e-02\n", + "360) Best value: -1.28e+00, TR length: 5.00e-02\n", + "364) Best value: -1.28e+00, TR length: 5.00e-02\n", + "368) Best value: -1.28e+00, TR length: 5.00e-02\n", + "372) Best value: -1.28e+00, TR length: 5.00e-02\n", + "376) Best value: -1.28e+00, TR length: 5.00e-02\n", + "380) Best value: -1.28e+00, TR length: 2.50e-02\n", + "384) Best value: -1.05e+00, TR length: 2.50e-02\n", + "388) Best value: -1.05e+00, TR length: 2.50e-02\n", + "392) Best value: -1.05e+00, TR length: 2.50e-02\n", + "396) Best value: -1.05e+00, TR length: 2.50e-02\n", + "400) Best value: -1.04e+00, TR length: 2.50e-02\n", + "404) Best value: -1.04e+00, TR length: 2.50e-02\n", + "408) Best value: -1.04e+00, TR length: 2.50e-02\n", + "412) Best value: -1.04e+00, TR length: 2.50e-02\n", + "416) Best value: -9.62e-01, TR length: 2.50e-02\n", + "420) Best value: -9.62e-01, TR length: 2.50e-02\n", + "424) Best value: -9.62e-01, TR length: 2.50e-02\n", + "428) Best value: -9.62e-01, TR length: 2.50e-02\n", + "432) Best value: -9.62e-01, TR length: 2.50e-02\n", + "436) Best value: -8.91e-01, TR length: 2.50e-02\n", + "440) Best value: -8.91e-01, TR length: 2.50e-02\n", + "444) Best value: -7.98e-01, TR length: 2.50e-02\n", + "448) Best value: -7.98e-01, TR length: 2.50e-02\n", + "452) Best value: -7.98e-01, TR length: 2.50e-02\n", + "456) Best value: -7.98e-01, TR length: 2.50e-02\n", + "460) Best value: -7.98e-01, TR length: 2.50e-02\n", + "464) Best value: -6.43e-01, TR length: 2.50e-02\n", + "468) Best value: -6.43e-01, TR length: 2.50e-02\n", + "472) Best value: -6.43e-01, TR length: 2.50e-02\n", + "476) Best value: -6.43e-01, TR length: 2.50e-02\n", + "480) Best value: -6.43e-01, TR length: 2.50e-02\n", + "484) Best value: -6.43e-01, TR length: 1.25e-02\n", + "488) Best value: -6.43e-01, TR length: 1.25e-02\n", + "492) Best value: -6.06e-01, TR length: 1.25e-02\n", + "496) Best value: -5.59e-01, TR length: 1.25e-02\n", + "500) Best value: -3.93e-01, TR length: 1.25e-02\n", + "504) Best value: -3.53e-01, TR length: 1.25e-02\n", + "508) Best value: -3.53e-01, TR length: 1.25e-02\n", + "512) Best value: -3.02e-01, TR length: 1.25e-02\n", + "516) Best value: -2.70e-01, TR length: 1.25e-02\n", + "520) Best value: -2.27e-01, TR length: 1.25e-02\n", + "524) Best value: -1.81e-01, TR length: 1.25e-02\n", + "528) Best value: -1.81e-01, TR length: 1.25e-02\n", + "532) Best value: -1.81e-01, TR length: 1.25e-02\n", + "536) Best value: -1.81e-01, TR length: 1.25e-02\n", + "540) Best value: -1.81e-01, TR length: 1.25e-02\n", + "544) Best value: -1.81e-01, TR length: 6.25e-03\n" + ] + } + ], + "source": [ + "X_turbo = get_initial_points(dim, n_init)\n", + "Y_turbo = torch.tensor(\n", + " [eval_objective(x) for x in X_turbo], dtype=dtype, device=device\n", + ").unsqueeze(-1)\n", + "\n", + "state = TurboState(dim, batch_size=batch_size, best_value=max(Y_turbo).item())\n", + "\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 512 if not SMOKE_TEST else 4\n", + "N_CANDIDATES = min(5000, max(2000, 200 * dim)) if not SMOKE_TEST else 4\n", + "\n", + "torch.manual_seed(0)\n", + "\n", + "while not state.restart_triggered: # Run until TuRBO converges\n", + " # Fit a GP model\n", + " train_Y = (Y_turbo - Y_turbo.mean()) / Y_turbo.std()\n", + " likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))\n", + " covar_module = ScaleKernel( # Use the same lengthscale prior as in the TuRBO paper\n", + " MaternKernel(\n", + " nu=2.5, ard_num_dims=dim, lengthscale_constraint=Interval(0.005, 4.0)\n", + " )\n", + " )\n", + " model = SingleTaskGP(\n", + " X_turbo, train_Y, covar_module=covar_module, likelihood=likelihood\n", + " )\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + "\n", + " # Do the fitting and acquisition function optimization inside the Cholesky context\n", + " with gpytorch.settings.max_cholesky_size(max_cholesky_size):\n", + " # Fit the model\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " # Create a batch\n", + " X_next = generate_batch(\n", + " state=state,\n", + " model=model,\n", + " X=X_turbo,\n", + " Y=train_Y,\n", + " batch_size=batch_size,\n", + " n_candidates=N_CANDIDATES,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " acqf=\"ts\",\n", + " )\n", + "\n", + " Y_next = torch.tensor(\n", + " [eval_objective(x) for x in X_next], dtype=dtype, device=device\n", + " ).unsqueeze(-1)\n", + "\n", + " # Update state\n", + " state = update_state(state=state, Y_next=Y_next)\n", + "\n", + " # Append data\n", + " X_turbo = torch.cat((X_turbo, X_next), dim=0)\n", + " Y_turbo = torch.cat((Y_turbo, Y_next), dim=0)\n", + "\n", + " # Print current status\n", + " print(\n", + " f\"{len(X_turbo)}) Best value: {state.best_value:.2e}, TR length: {state.length:.2e}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "518bbb5e-84f6-4062-bf28-25ccf7650c01", + "showInput": false + }, + "source": [ + "## GP-LogEI\n", + "We compare TuRBO to qLogEI [2], a recent improvement to the expected improvement (EI) acquisition functions.\n", + "\n", + "[2]: [Ament, Sebastian, et al., Unexpected Improvements to Expected Improvement for Bayesian Optimization. Advances in Neural Information Processing Systems. 2023](https://proceedings.neurips.cc/paper_files/paper/2023/file/419f72cbd568ad62183f8132a3605a2a-Paper-Conference.pdf)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921663896, + "executionStopTime": 1674921754833, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "8cc7262f-36ac-427f-b7a1-d94b0ceeae5e", + "requestMsgId": "20905f90-c4bf-4073-9f15-9ac77e1f9c22" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "44) Best value: -1.15e+01\n", + "48) Best value: -1.04e+01\n", + "52) Best value: -1.02e+01\n", + "56) Best value: -9.98e+00\n", + "60) Best value: -9.62e+00\n", + "64) Best value: -9.10e+00\n", + "68) Best value: -9.10e+00\n", + "72) Best value: -8.87e+00\n", + "76) Best value: -8.87e+00\n", + "80) Best value: -8.75e+00\n", + "84) Best value: -8.18e+00\n", + "88) Best value: -7.58e+00\n", + "92) Best value: -7.24e+00\n", + "96) Best value: -6.86e+00\n", + "100) Best value: -6.75e+00\n", + "104) Best value: -6.35e+00\n", + "108) Best value: -5.74e+00\n", + "112) Best value: -5.43e+00\n", + "116) Best value: -5.25e+00\n", + "120) Best value: -4.66e+00\n", + "124) Best value: -4.66e+00\n", + "128) Best value: -4.66e+00\n", + "132) Best value: -4.66e+00\n", + "136) Best value: -4.55e+00\n", + "140) Best value: -4.36e+00\n", + "144) Best value: -4.24e+00\n", + "148) Best value: -4.22e+00\n", + "152) Best value: -4.22e+00\n", + "156) Best value: -3.97e+00\n", + "160) Best value: -3.86e+00\n", + "164) Best value: -3.63e+00\n", + "168) Best value: -3.63e+00\n", + "172) Best value: -3.59e+00\n", + "176) Best value: -3.59e+00\n", + "180) Best value: -3.59e+00\n", + "184) Best value: -3.59e+00\n", + "188) Best value: -3.20e+00\n", + "192) Best value: -3.20e+00\n", + "196) Best value: -3.20e+00\n", + "200) Best value: -3.20e+00\n", + "204) Best value: -3.20e+00\n", + "208) Best value: -3.20e+00\n", + "212) Best value: -2.64e+00\n", + "216) Best value: -2.64e+00\n", + "220) Best value: -2.64e+00\n", + "224) Best value: -2.62e+00\n", + "228) Best value: -2.62e+00\n", + "232) Best value: -2.62e+00\n", + "236) Best value: -2.62e+00\n", + "240) Best value: -2.49e+00\n", + "244) Best value: -2.49e+00\n", + "248) Best value: -2.49e+00\n", + "252) Best value: -2.49e+00\n", + "256) Best value: -2.49e+00\n", + "260) Best value: -2.49e+00\n", + "264) Best value: -2.49e+00\n", + "268) Best value: -2.49e+00\n", + "272) Best value: -2.12e+00\n", + "276) Best value: -2.12e+00\n", + "280) Best value: -2.11e+00\n", + "284) Best value: -2.11e+00\n", + "288) Best value: -2.11e+00\n", + "292) Best value: -2.11e+00\n", + "296) Best value: -2.11e+00\n", + "300) Best value: -2.11e+00\n", + "304) Best value: -2.11e+00\n", + "308) Best value: -2.11e+00\n", + "312) Best value: -2.11e+00\n", + "316) Best value: -2.11e+00\n", + "320) Best value: -2.11e+00\n", + "324) Best value: -2.11e+00\n", + "328) Best value: -2.11e+00\n", + "332) Best value: -2.11e+00\n", + "336) Best value: -2.11e+00\n", + "340) Best value: -2.11e+00\n", + "344) Best value: -2.11e+00\n", + "348) Best value: -2.11e+00\n", + "352) Best value: -2.11e+00\n", + "356) Best value: -2.11e+00\n", + "360) Best value: -2.11e+00\n", + "364) Best value: -2.11e+00\n", + "368) Best value: -2.11e+00\n", + "372) Best value: -2.11e+00\n", + "376) Best value: -2.11e+00\n", + "380) Best value: -2.11e+00\n", + "384) Best value: -2.11e+00\n", + "388) Best value: -2.11e+00\n", + "392) Best value: -2.11e+00\n", + "396) Best value: -2.11e+00\n", + "400) Best value: -2.11e+00\n", + "404) Best value: -2.11e+00\n", + "408) Best value: -2.11e+00\n", + "412) Best value: -2.11e+00\n", + "416) Best value: -2.11e+00\n", + "420) Best value: -2.11e+00\n", + "424) Best value: -2.11e+00\n", + "428) Best value: -2.11e+00\n", + "432) Best value: -2.11e+00\n", + "436) Best value: -2.11e+00\n", + "440) Best value: -2.11e+00\n", + "444) Best value: -2.11e+00\n", + "448) Best value: -2.11e+00\n", + "452) Best value: -2.11e+00\n", + "456) Best value: -2.11e+00\n", + "460) Best value: -2.11e+00\n", + "464) Best value: -2.11e+00\n", + "468) Best value: -2.11e+00\n", + "472) Best value: -2.11e+00\n", + "476) Best value: -2.11e+00\n", + "480) Best value: -2.11e+00\n", + "484) Best value: -2.11e+00\n", + "488) Best value: -2.11e+00\n", + "492) Best value: -2.11e+00\n", + "496) Best value: -2.11e+00\n", + "500) Best value: -2.11e+00\n", + "504) Best value: -2.11e+00\n", + "508) Best value: -2.11e+00\n", + "512) Best value: -2.11e+00\n", + "516) Best value: -2.11e+00\n", + "520) Best value: -2.11e+00\n", + "524) Best value: -2.11e+00\n", + "528) Best value: -2.11e+00\n", + "532) Best value: -2.11e+00\n", + "536) Best value: -2.11e+00\n", + "540) Best value: -2.11e+00\n", + "544) Best value: -2.11e+00\n" + ] + } + ], + "source": [ + "torch.manual_seed(0)\n", + "\n", + "X_logei = get_initial_points(dim, n_init)\n", + "Y_logei = torch.tensor(\n", + " [eval_objective(x) for x in X_logei], dtype=dtype, device=device\n", + ").unsqueeze(-1)\n", + "\n", + "# Cap the number of evals when running smoke test\n", + "max_evals = min(len(Y_turbo), n_init + 2 * batch_size) if SMOKE_TEST else len(Y_turbo)\n", + "while len(Y_logei) < max_evals:\n", + " train_Y = (Y_logei - Y_logei.mean()) / Y_logei.std()\n", + " likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))\n", + " model = SingleTaskGP(X_logei, train_Y, likelihood=likelihood)\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " # Create a batch\n", + " log_ei = qLogExpectedImprovement(model, train_Y.max())\n", + " candidate, acq_value = optimize_acqf(\n", + " log_ei,\n", + " bounds=torch.stack(\n", + " [\n", + " torch.zeros(dim, dtype=dtype, device=device),\n", + " torch.ones(dim, dtype=dtype, device=device),\n", + " ]\n", + " ),\n", + " q=batch_size,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + " Y_next = torch.tensor(\n", + " [eval_objective(x) for x in candidate], dtype=dtype, device=device\n", + " ).unsqueeze(-1)\n", + "\n", + " # Append data\n", + " X_logei = torch.cat((X_logei, candidate), axis=0)\n", + " Y_logei = torch.cat((Y_logei, Y_next), axis=0)\n", + "\n", + " # Print current status\n", + " print(f\"{len(X_logei)}) Best value: {Y_logei.max().item():.2e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GP-EI" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "44) Best value: -1.13e+01\n", + "48) Best value: -1.04e+01\n", + "52) Best value: -9.96e+00\n", + "56) Best value: -8.97e+00\n", + "60) Best value: -8.73e+00\n", + "64) Best value: -8.73e+00\n", + "68) Best value: -8.73e+00\n", + "72) Best value: -8.68e+00\n", + "76) Best value: -8.68e+00\n", + "80) Best value: -8.68e+00\n", + "84) Best value: -8.68e+00\n", + "88) Best value: -8.68e+00\n", + "92) Best value: -8.68e+00\n", + "96) Best value: -8.68e+00\n", + "100) Best value: -8.68e+00\n", + "104) Best value: -8.68e+00\n", + "108) Best value: -8.68e+00\n", + "112) Best value: -8.68e+00\n", + "116) Best value: -8.68e+00\n", + "120) Best value: -8.68e+00\n", + "124) Best value: -8.68e+00\n", + "128) Best value: -8.68e+00\n", + "132) Best value: -8.68e+00\n", + "136) Best value: -8.68e+00\n", + "140) Best value: -8.68e+00\n", + "144) Best value: -8.68e+00\n", + "148) Best value: -8.68e+00\n", + "152) Best value: -8.68e+00\n", + "156) Best value: -8.68e+00\n", + "160) Best value: -8.68e+00\n", + "164) Best value: -8.68e+00\n", + "168) Best value: -8.68e+00\n", + "172) Best value: -8.68e+00\n", + "176) Best value: -8.68e+00\n", + "180) Best value: -8.68e+00\n", + "184) Best value: -8.68e+00\n", + "188) Best value: -8.68e+00\n", + "192) Best value: -8.68e+00\n", + "196) Best value: -8.68e+00\n", + "200) Best value: -8.68e+00\n", + "204) Best value: -8.68e+00\n", + "208) Best value: -8.68e+00\n", + "212) Best value: -8.68e+00\n", + "216) Best value: -8.68e+00\n", + "220) Best value: -8.68e+00\n", + "224) Best value: -8.68e+00\n", + "228) Best value: -8.68e+00\n", + "232) Best value: -8.68e+00\n", + "236) Best value: -8.68e+00\n", + "240) Best value: -8.68e+00\n", + "244) Best value: -8.68e+00\n", + "248) Best value: -8.68e+00\n", + "252) Best value: -8.68e+00\n", + "256) Best value: -8.68e+00\n", + "260) Best value: -8.68e+00\n", + "264) Best value: -8.68e+00\n", + "268) Best value: -8.68e+00\n", + "272) Best value: -8.68e+00\n", + "276) Best value: -8.68e+00\n", + "280) Best value: -8.68e+00\n", + "284) Best value: -8.68e+00\n", + "288) Best value: -8.68e+00\n", + "292) Best value: -8.68e+00\n", + "296) Best value: -8.68e+00\n", + "300) Best value: -8.68e+00\n", + "304) Best value: -8.68e+00\n", + "308) Best value: -8.68e+00\n", + "312) Best value: -8.68e+00\n", + "316) Best value: -8.68e+00\n", + "320) Best value: -8.68e+00\n", + "324) Best value: -8.68e+00\n", + "328) Best value: -8.68e+00\n", + "332) Best value: -8.68e+00\n", + "336) Best value: -8.68e+00\n", + "340) Best value: -8.68e+00\n", + "344) Best value: -8.68e+00\n", + "348) Best value: -8.68e+00\n", + "352) Best value: -8.68e+00\n", + "356) Best value: -8.68e+00\n", + "360) Best value: -8.68e+00\n", + "364) Best value: -8.68e+00\n", + "368) Best value: -8.68e+00\n", + "372) Best value: -8.68e+00\n", + "376) Best value: -8.68e+00\n", + "380) Best value: -8.68e+00\n", + "384) Best value: -8.68e+00\n", + "388) Best value: -8.68e+00\n", + "392) Best value: -8.68e+00\n", + "396) Best value: -8.68e+00\n", + "400) Best value: -8.68e+00\n", + "404) Best value: -8.68e+00\n", + "408) Best value: -8.68e+00\n", + "412) Best value: -8.68e+00\n", + "416) Best value: -8.68e+00\n", + "420) Best value: -8.68e+00\n", + "424) Best value: -8.68e+00\n", + "428) Best value: -8.68e+00\n", + "432) Best value: -8.68e+00\n", + "436) Best value: -8.68e+00\n", + "440) Best value: -8.68e+00\n", + "444) Best value: -8.68e+00\n", + "448) Best value: -8.68e+00\n", + "452) Best value: -8.68e+00\n", + "456) Best value: -8.68e+00\n", + "460) Best value: -8.68e+00\n", + "464) Best value: -8.68e+00\n", + "468) Best value: -8.68e+00\n", + "472) Best value: -8.68e+00\n", + "476) Best value: -8.68e+00\n", + "480) Best value: -8.68e+00\n", + "484) Best value: -8.68e+00\n", + "488) Best value: -8.68e+00\n", + "492) Best value: -8.68e+00\n", + "496) Best value: -8.68e+00\n", + "500) Best value: -8.68e+00\n", + "504) Best value: -8.68e+00\n", + "508) Best value: -8.68e+00\n", + "512) Best value: -8.68e+00\n", + "516) Best value: -8.68e+00\n", + "520) Best value: -8.68e+00\n", + "524) Best value: -8.68e+00\n", + "528) Best value: -8.68e+00\n", + "532) Best value: -8.68e+00\n", + "536) Best value: -8.68e+00\n", + "540) Best value: -8.68e+00\n", + "544) Best value: -8.68e+00\n" + ] + } + ], + "source": [ + "torch.manual_seed(0)\n", + "\n", + "X_ei = get_initial_points(dim, n_init)\n", + "Y_ei = torch.tensor(\n", + " [eval_objective(x) for x in X_ei], dtype=dtype, device=device\n", + ").unsqueeze(-1)\n", + "\n", + "while len(Y_ei) < len(Y_turbo):\n", + " train_Y = (Y_ei - Y_ei.mean()) / Y_ei.std()\n", + " likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3))\n", + " model = SingleTaskGP(X_ei, train_Y, likelihood=likelihood)\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " fit_gpytorch_mll(mll)\n", + "\n", + " # Create a batch\n", + " ei = qExpectedImprovement(model, train_Y.max())\n", + " candidate, acq_value = optimize_acqf(\n", + " ei,\n", + " bounds=torch.stack(\n", + " [\n", + " torch.zeros(dim, dtype=dtype, device=device),\n", + " torch.ones(dim, dtype=dtype, device=device),\n", + " ]\n", + " ),\n", + " q=batch_size,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + " Y_next = torch.tensor(\n", + " [eval_objective(x) for x in candidate], dtype=dtype, device=device\n", + " ).unsqueeze(-1)\n", + "\n", + " # Append data\n", + " X_ei = torch.cat((X_ei, candidate), axis=0)\n", + " Y_ei = torch.cat((Y_ei, Y_next), axis=0)\n", + "\n", + " # Print current status\n", + " print(f\"{len(X_ei)}) Best value: {Y_ei.max().item():.2e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "38f8ac21-d9ae-41f7-ba42-0ff6abde0a2c", + "showInput": false + }, + "source": [ + "## Sobol" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921754972, + "executionStopTime": 1674921755010, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "a6333e87-1fcf-4174-9cb8-111598dd7780", + "requestMsgId": "629a258e-fa1d-44f8-848c-3335a98e3421" + }, + "outputs": [], + "source": [ + "X_Sobol = (\n", + " SobolEngine(dim, scramble=True, seed=0)\n", + " .draw(len(X_turbo))\n", + " .to(dtype=dtype, device=device)\n", + ")\n", + "Y_Sobol = torch.tensor(\n", + " [eval_objective(x) for x in X_Sobol], dtype=dtype, device=device\n", + ").unsqueeze(-1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "e20c8975-af02-4308-a1ef-3f12afb85ffd", + "showInput": false + }, + "source": [ + "## Compare the methods" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921755158, + "executionStopTime": 1674921757156, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "b57b38e5-da03-4511-a301-7252eb6c7013", + "requestMsgId": "7c4c1c41-4852-4498-a42e-14e08dc88afb" + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+IAAAKBCAYAAADTDRELAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACk+UlEQVR4nOzdd5hU1f3H8c/M7mxn+7K0pQlSBZEmEgNIsUeNBREFrFE00aBG/GlEEhWNJcZuEgU7tphEbCDFiihN6Yj0snS2lyn398dmx71Tts5O2Xm/nmcf55577rnfGc5e57vn3HMthmEYAgAAAAAAQWENdQAAAAAAAEQTEnEAAAAAAIKIRBwAAAAAgCAiEQcAAAAAIIhIxAEAAAAACCIScQAAAAAAgohEHAAAAACAICIRBwAAAAAgiEjEAQAAAAAIIhJxAAAQEnPmzJHFYpHFYtGUKVOa3F51WxaLpenBAQDQjEjEAQBhb/v27frHP/6hyy+/XP3791dGRoZsNpsyMzPVr18//eY3v9Fnn33W6Pa/++47TZ06Vb1791ZqaqpSU1PVu3dvTZ06Vd999129Y6yZCNb8sVqtatWqlTp06KA+ffro3HPP1b333qt58+apvLy80XE31uWXX26K76GHHgp6DAAARDOLYRhGqIMAAMCXVatW6frrr9e3335br/ojR47USy+9pI4dO9arfmVlpW6//XY9+eST8ve/Q4vFoptvvll/+ctfZLPZ/La1fft2denSpV7nrSkzM1OTJk3StGnTlJeX1+DjG6qoqEht2rRRaWmpu6xXr15av359s5/b05w5c3TllVdKkiZPnqw5c+Y0qb2aI+F8vQEAhLPYUAcAAIA/mzZt8krCjz/+ePXt21fZ2dk6duyYvv76a+3evVuStGTJEg0bNkxffPGFunbtWmf71157rV5++WX3dteuXXXyySdLkr755htt3bpVhmHo8ccfV2FhoV544YV6xz5p0iS1atXKvV1ZWamjR4/qwIEDWrVqlYqKiiRJR44c0eOPP67Zs2fr6aef1sSJE+t9jsZ4++23TUm4JG3YsEHfffedBg8e3KznBgAAVUjEAQBhr1u3brrmmmt0+eWXq3379qZ9LpdLc+bM0W9/+1uVlpZq7969mjhxor7++uta7xV+8cUX3Um41WrVo48+qt/97neyWq3udp944gndeuutcrlcevHFFzVixAhNmjSpXjHPnDlTnTt39rnP5XJp5cqVeuqpp/T666/LbreroKBAl19+ufbt26fbbrutXudojJdeesn9OjExUWVlZe5yEnEAAIKDe8QBAGGrbdu2mj17tjZu3Kg77rjDKwmXqpLoq666Sq+++qq77JtvvtH8+fP9tltRUaF7773Xvf2HP/xBt9xyizsJr273lltu0e233+4uu+eee1RZWdnEd1XV9qBBgzRnzhx99dVXpqn0d9xxhz744IMmn8OXbdu26YsvvpBUNY37kUcece974403AvLeAABA3UjEAQBha8SIEZoyZYpiYmLqrHvBBRdoyJAh7u3aktn//ve/2rVrlyQpLS1Nf/zjH/3Wveeee5SamipJ2rFjR8CT5MGDB2vRokXuc7hcLt1yyy1yOp0BPY8kvfzyy+57p0eMGKHrrrtOOTk5kqqmyM+bNy/g5wQAAN5IxAEALcbw4cPdr7dv3+633r///W/36/HjxyspKclv3aSkJF1yySXu7ffee69JMfpy3HHHmUant2zZorfeeiug5zAMw3Q//BVXXKHY2Fhdeuml7rKa09bry+l06q233tKkSZPUo0cP94r2WVlZGjp0qG6++WYtXLiwyYun7dy5Uz179nSv9D506FAdPny4SW2WlJTo2Wef1bnnnqtOnTopKSlJrVq1Uvfu3XXVVVdp0aJFfo9dtWqVO5aMjIx6r35fVFSklJQU97E//PBDk94DACAykYgDAFqMmveE1zaivHjxYvfrkSNH1tnuqFGj3K9rS86aYtKkSe7RaUmaO3duQNv/8ssvtXXrVklSQkKCLrroIklVCXm1jz76SAcPHqx3m1988YV69+6t8ePH65VXXtHmzZt17NgxORwOHTlyRN9++62eeOIJjRkzRnfeeWejY1+3bp1OOeUUbdq0SZI0duxYLVq0SFlZWY1u8+2331a3bt00depUzZs3Tzt37lRZWZmKi4u1ZcsWzZ49W6NHj9a5556rgoICr+MHDBiggQMHSpKOHTumd999t17nffPNN1VSUiKpajZEv379Gv0eAACRi0QcANBirFmzxv3a36PACgoKtG/fPvf2SSedVGe7Nevs2bNHhYWFTYjSt/j4eJ177rnu7S+//DKgj+CqOdp93nnnuafCDx48WD179pQk2e12vf766/Vqb+7cuRo9erQ2b97sLjv+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XrRoUZNi9FSdjP/mN7+RVDUVPJDJeM17zqv7USD5GhWvOTp+5ZVXNuk56ACAFsQAACDMVVZWGmeddZYhyZBkxMfHGwsWLGh0e2+99Za7rfT0dKO0tNRv3dLSUiM9Pd1d/9133/VZb9u2be46koxt27bVO54tW7YYqamp7mN79eplOJ3Ohr4tk1/96lfu9s4999wGHdu3b1/3sXfddZdp37Jly9z7LBaLsX79+kbHOHv2bHdbkydPdpe7XC7jN7/5jXtfbGys8dZbb9XZXs3P35c333zTvb9t27ZGWVlZo2P35ejRo0ZiYqIhyUhJSTF27dplJCQkGJIMq9Vq7Ny5M6DnAwBELv4sCwAIa9X3C1c/Azo2NlZvvfVWk6b4/upXv1KHDh0kSceOHdP999/vt+6f//xn96JlnTp1Mt1HHQjLly/XaaedpsLCQklSTEyMHn/88SaNnB48eFAfffSRe/vyyy9v0PE167/yyisyDMO9PWTIEA0fPlxS1f3PkyZNCui94lLVyPizzz6r6667TlLVyPhll12mt99+u0ntXnjhherWrZukqlX3p06danpvtSkuLlZJSUmtddLT03XJJZe461900UXuxQDHjRunvLy8JkQPAGhJSMQBAGHLMAxdffXVeueddyRJVqtVr7zyin71q181qd34+HjNnDnTvT1r1iw98cQT7pWtJcnlcumJJ57QQw895C7705/+pLi4uCadu7rt5cuX68orr9Tw4cNNi8H99a9/1bhx45rU/uuvvy673S6panG1c889t0HHT5gwQRaLRVLVQnWLFy827X/iiScUHx8vqeoPCb/85S9NC83VlJ+fr0ceeUQPP/xwg2KwWCx67rnnApqMx8TE6Nlnn1VMTIwkafbs2Tr77LPdK8v7snr1at1xxx3Ky8ur1z3xNaen1/xMWKQNAFCTxajvn4IBAAiyZ555RjfeeKN7u3v37g1KUp966qla90+aNEmvvPKKe/u4445z3wP9zTffuB+PJlXd3/viiy/6bWv79u2mldsnTZpkWmG8srJSx44d08GDB7Vy5Ur3CHi1jIwMPfvss+7HYDXFSSed5F6EbvLkyZozZ06D2xgxYoQ+//xzSVXvxXPhttdee01TpkwxPdKrR48eGjBggNLS0lRQUKD169dr7dq1crlcuvnmm/X444+b2pgzZ46uvPLKWuM0DEO/+c1v9I9//ENS1YyIN954QxdddJFX3eo/HlQf588//vEP3XDDDXI6ne7jevfurX79+ik1NVWlpaXat2+fvv/+ex08eNB93Jo1a9S3b1+/7Vbr27eve1V/SWrdurV2795tekQbACDKhW5WPAAAtZsxY4bpvt+G/tSloqLCuOmmmwyLxeK3DYvFYvzud78zKisra23L8x7x+v5kZWUZ06ZNM3bv3h2Qz+yHH34wtd/Ye+n//ve/u9tITk42ioqKvOosXLjQ6NKlS73ep+e95obh/x5xTy6Xy7j22mtN94y//fbbXvUa8m+/aNEio3v37vX+d+rTp4+xZ8+eOts1DMN4/PHHTcfedttt9ToOABA9YpuWxgMAELni4uL05JNP6oorrtCLL76oJUuWaM+ePZKqHlM2cuRIXX311aaVxBsrOTlZaWlpSktLU9euXTVo0CANGTJEo0ePdk/zDoSaI9dt27Zt9CrvF110kX7729+qoqJCJSUleueddzRlyhRTndNOO02bNm3S3LlzNW/ePC1fvlwHDhxQRUWF0tLS1K1bNw0bNkwXXHBBkx7HZrFY9Pzzz0uqGs12OByaMGGCO87GGDVqlDZs2KB///vf+uCDD/TNN98oPz9fhYWFSkpKUm5urnr27KlTTjlFZ555pk488cR6t/3rX/9at9xyi3ubaekAAE9MTQcAAAigl156yf1Hi1/84hf64osvQhsQACDssFgbAABAANV8dnjNxdsAAKjGiDgAAECArFq1SieddJIkKTMzU3v27FFCQkKIowIAhBtGxAEAAAKgvLxcv/3tb93b119/PUk4AMAnRsQBAAAa6amnntKWLVt07NgxLVy4ULt375YkZWdna9OmTcrMzAxxhACAcMSq6QAAAI30zjvv6LPPPjOVxcTE6IUXXiAJBwD4RSKOZuFyubR37161atVKFosl1OEAANAsnE6n+3V6erqGDh2qadOm6eSTT1ZhYWEIIwPQkhmGoaKiIrVr105WK3cbRyKmpqNZ7N69W3l5eaEOAwAAAGixdu3apQ4dOoQ6DDQCI+JoFq1atZIkbdu2jal5MLHb7Zo/f77GjRsnm80W6nAQJugX8IV+AX/oG/AlmvpFYWGh8vLy3N+5EXlIxNEsqqejt2rVSqmpqSGOBuHEbrcrKSlJqampLf5/kqg/+gV8oV/AH/oGfInGfsEtoJGL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ajzxxBPuxOLuu+820tLSjPHjx7uTvaKiIuOuu+5yt7FgwQKvc1cnXGlpaUZ8fLzx3HPPGWVlZYZhVCVGF110kfv4//znP17H//Of/3Qnnffff7+xb98+wzAMw+FwGMuXLzdOO+00Q5LRoUMHo6ioyOfnXJ2k3HHHHcaBAwcMwzCM8vJyr6TVH4fDYQwdOtT9Pl599VWjoqLCMAzD+Omnn4xzzjnHnYx9+OGHXsfPnj3b9O/dGHfffbchyWjdurXx9NNPG4cPHzYMwzAqKyuNxYsXGwMGDDAkGSeddJLhdDrdxy1btsz9+W7YsMFn2zt27HAngIsWLTLt+/Of/2w8/PDDxpo1awy73W4YRtUfBdauXWtMnDjRkGQkJycbe/bs8Wq3ORPx559/3pgxY4axfPly97+Fy+Uytm7datx8882GxWIxYmJi6vwDTW1qS8SffPJJ9+d63XXXuftlcXGx8de//tX9xwpff6CqPn96erphtVqNO++80zh48KBhGIZRUFBg3HPPPe62X3jhBdOx27dvd/9Ratq0aabP/dixY8YXX3xhTJ061Vi+fHmt7w0A4B+JOAC0UJ6JuGEYxgUXXGBIMk488UTD5XK5y4OViPfp08coLy/3OvaKK65w1xk7dqwptmrVI91XX321177qhMtXUmEYVUndL3/5S3cMNRUWFrpHzj/++GOf781utxsDBw40JBl//etfTftqjppOmzbN5/H1MXfuXHc7n3zyic8YqhP1vn37eu1vaiK+bds2IyYmxkhMTDRWr17ts05hYaHRoUMHQ5Lx3nvvmfb16NHDPSvBlwceeMCQZHTs2NHnv29tzj77bEOS8ec//9lrX3Mm4nWpngngq082NREvLS01MjMzDUnGhAkTfB77xBNPuPuMZ1Jcs1/6e/+//vWvDUnGmDFjTOVvvvmmIck4/vjja40dANB43CMOAFHkgQceUExMjFavXq033ngj6Of//e9/r/j4eK/y008/3f16+vTpslgsfuv88MMPftvPy8vTlVde6VVutVp19913S5LWrVvnXmFcqrqn+9ixYxowYIApjppiY2M1YcIESVX3zfpitVp1xx13+I2tLm+++aYkadiwYRo3bpzPGGbMmCGp6l72mu8hEObMmSOn06kzzjhD/fv391mnVatWOv/88yV5fw5XXHGFJOm1116TYRhex77yyiuSpIkTJ/r8963N2WefLUn68ssvG3Rcc2vOuBYsWOC+B9vfPe5Tp051P/bs9ddf91knPj5et912m8995513niTv36n09HRJUlFRkWntBgBA4JCIA0AU6dmzpztR/eMf/1ivxc8CaciQIT7Lc3Nz3a8HDx5ca52jR4/6bb96cS5fTj31VMXGxkqSli9f7i7/6quvJEkbNmxQmzZt/P786U9/klS1GJov3bp1U+vWrf3GVpfqmMaMGeO3zqhRoxQTE+P1HgKh+nOYP39+rZ/D7NmzJXl/DldccYUsFot27typzz77zLRvxYoV2rBhgyRp0qRJPs///fffa+rUqerXr59SU1NltVrdi5tNnTpVkrR79+6Avuf62Lp1q2677TYNHDhQ6enpiomJccd11llnNVtc1f++eXl5Ov74433WiYmJ0WmnnWaq76lPnz5+V9Jv166dJHktujZkyBBlZ2dr3759Gjp0qJ566ilt3LjR5x9YAACNExvqAAAAwXXvvffqtdde09atW/Xcc8/pt7/9bdDO3apVK5/l1QlyferU9seD9u3b+92XkJCgrKws7d+/XwcOHHCX7927V1LVauW1rSBdreZq3zU1JQmX5I6prveQnZ3t9R4CofpzKCkpqdcoqOfn0LFjR40YMUJLlizRK6+8YlqFvHo0fPDgwerZs6dXW0899ZRuvvlmuVwuSZLFYlFaWpp79kRZWZkKCwuDPjr73nvvacKECaqoqHCXpaamKiEhQRaLRZWVlTp69GizxFWf/iBJHTp0MNX35O/3Sfr5d8rhcJjK09PT9cYbb+iyyy7TunXr3NeItLQ0/fKXv9Qll1yi8ePHy2az1e/NAAC8MCIOAFGmffv27i/W9913n9fjqKJN9eOfxo8fL6Nq7ZRaf/w9wq16pDpSVX8Od9xxR70+hyVLlni1UT3a/c4776isrExSVZJXfRtE9fT1mjZs2KBbbrlFLpdLF198sb799luVl5fr6NGjys/PV35+vh577DFJCuqI7OHDhzVlyhRVVFTotNNO05IlS1RaWqqCggLt379f+fn5evvtt4MWT7CNGTNG27Zt08svv6zJkyere/fuKigo0Pvvv68rrrhCAwYM0J49e0IdJgBELBJxAIhC06dPV0ZGhg4cOKBHH3201ro1R6trGzEuKCgIWHyNVVtiUFFRocOHD0syj163adNGkv8p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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from matplotlib import rc\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "names = [\"TuRBO-1\", \"LogEI\", \"EI\", \"Sobol\"]\n", + "runs = [Y_turbo, Y_logei, Y_ei, Y_Sobol]\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "\n", + "for name, run in zip(names, runs):\n", + " fx = np.maximum.accumulate(run.cpu())\n", + " plt.plot(fx, marker=\"\", lw=3)\n", + "\n", + "plt.plot([0, len(Y_turbo)], [fun.optimal_value, fun.optimal_value], \"k--\", lw=3)\n", + "plt.xlabel(\"Function value\", fontsize=18)\n", + "plt.xlabel(\"Number of evaluations\", fontsize=18)\n", + "plt.title(\"20D Ackley\", fontsize=24)\n", + "plt.xlim([0, len(Y_turbo)])\n", + "plt.ylim([-15, 1])\n", + "\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.legend(\n", + " names + [\"Global optimal value\"],\n", + " loc=\"lower center\",\n", + " bbox_to_anchor=(0, -0.08, 1, 1),\n", + " bbox_transform=plt.gcf().transFigure,\n", + " ncol=5,\n", + " fontsize=16,\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1674921757397, + "executionStopTime": 1674921757407, + "jupyter": { + "outputs_hidden": false + }, + "originalKey": "81817f68-6383-4446-abc2-7ac698325684", + "requestMsgId": "f058303d-23d4-4c3a-b5e5-0042b9f1cc05" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/website-old/static/files/turbo_1.py b/website-old/static/files/turbo_1.py new file mode 100644 index 0000000000..810dfe9184 --- /dev/null +++ b/website-old/static/files/turbo_1.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## BO with TuRBO-1 and TS/qEI +# +# In this tutorial, we show how to implement Trust Region Bayesian Optimization (TuRBO) [1] in a closed loop in BoTorch. +# +# This implementation uses one trust region (TuRBO-1) and supports either parallel expected improvement (qEI) or Thompson sampling (TS). We optimize the $20D$ Ackley function on the domain $[-5, 10]^{20}$ and show that TuRBO-1 outperforms qEI as well as Sobol. +# +# Since botorch assumes a maximization problem, we will attempt to maximize $-f(x)$ to achieve $\max_x -f(x)=0$. +# +# [1]: [Eriksson, David, et al. Scalable global optimization via local Bayesian optimization. Advances in Neural Information Processing Systems. 2019](https://proceedings.neurips.cc/paper/2019/file/6c990b7aca7bc7058f5e98ea909e924b-Paper.pdf) +# + +# In[1]: + + +import os +import math +import warnings +from dataclasses import dataclass + +import torch +from botorch.acquisition import qExpectedImprovement, qLogExpectedImprovement +from botorch.exceptions import BadInitialCandidatesWarning +from botorch.fit import fit_gpytorch_mll +from botorch.generation import MaxPosteriorSampling +from botorch.models import SingleTaskGP +from botorch.optim import optimize_acqf +from botorch.test_functions import Ackley +from botorch.utils.transforms import unnormalize +from torch.quasirandom import SobolEngine + +import gpytorch +from gpytorch.constraints import Interval +from gpytorch.kernels import MaternKernel, ScaleKernel +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.mlls import ExactMarginalLogLikelihood + + +warnings.filterwarnings("ignore", category=BadInitialCandidatesWarning) +warnings.filterwarnings("ignore", category=RuntimeWarning) + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +dtype = torch.double +SMOKE_TEST = os.environ.get("SMOKE_TEST") + + +# ## Optimize the 20-dimensional Ackley function +# +# The goal is to minimize the popular Ackley function: +# +# $f(x_1,\ldots,x_d) = -20\exp\left(-0.2 \sqrt{\frac{1}{d} \sum_{j=1}^d x_j^2} \right) -\exp \left( \frac{1}{d} \sum_{j=1}^d \cos(2 \pi x_j) \right) + 20 + e$ +# +# over the domain $[-5, 10]^{20}$. The global optimal value of $0$ is attained at $x_1 = \ldots = x_d = 0$. +# +# As mentioned above, since botorch assumes a maximization problem, we instead maximize $-f(x)$. + +# In[2]: + + +fun = Ackley(dim=20, negate=True).to(dtype=dtype, device=device) +fun.bounds[0, :].fill_(-5) +fun.bounds[1, :].fill_(10) +dim = fun.dim +lb, ub = fun.bounds + +batch_size = 4 +n_init = 2 * dim +max_cholesky_size = float("inf") # Always use Cholesky + + +def eval_objective(x): + """This is a helper function we use to unnormalize and evalaute a point""" + return fun(unnormalize(x, fun.bounds)) + + +# ## Maintain the TuRBO state +# TuRBO needs to maintain a state, which includes the length of the trust region, success and failure counters, success and failure tolerance, etc. +# +# In this tutorial we store the state in a dataclass and update the state of TuRBO after each batch evaluation. +# +# **Note**: These settings assume that the domain has been scaled to $[0, 1]^d$ and that the same batch size is used for each iteration. + +# In[3]: + + +@dataclass +class TurboState: + dim: int + batch_size: int + length: float = 0.8 + length_min: float = 0.5**7 + length_max: float = 1.6 + failure_counter: int = 0 + failure_tolerance: int = float("nan") # Note: Post-initialized + success_counter: int = 0 + success_tolerance: int = 10 # Note: The original paper uses 3 + best_value: float = -float("inf") + restart_triggered: bool = False + + def __post_init__(self): + self.failure_tolerance = math.ceil( + max([4.0 / self.batch_size, float(self.dim) / self.batch_size]) + ) + + +def update_state(state, Y_next): + if max(Y_next) > state.best_value + 1e-3 * math.fabs(state.best_value): + state.success_counter += 1 + state.failure_counter = 0 + else: + state.success_counter = 0 + state.failure_counter += 1 + + if state.success_counter == state.success_tolerance: # Expand trust region + state.length = min(2.0 * state.length, state.length_max) + state.success_counter = 0 + elif state.failure_counter == state.failure_tolerance: # Shrink trust region + state.length /= 2.0 + state.failure_counter = 0 + + state.best_value = max(state.best_value, max(Y_next).item()) + if state.length < state.length_min: + state.restart_triggered = True + return state + + +# ## Take a look at the state + +# In[4]: + + +state = TurboState(dim=dim, batch_size=batch_size) +print(state) + + +# ## Generate initial points +# This generates an initial set of Sobol points that we use to start of the BO loop. + +# In[5]: + + +def get_initial_points(dim, n_pts, seed=0): + sobol = SobolEngine(dimension=dim, scramble=True, seed=seed) + X_init = sobol.draw(n=n_pts).to(dtype=dtype, device=device) + return X_init + + +# ## Generate new batch +# Given the current `state` and a probabilistic (GP) `model` built from observations `X` and `Y`, we generate a new batch of points. +# +# This method works on the domain $[0, 1]^d$, so make sure to not pass in observations from the true domain. `unnormalize` is called before the true function is evaluated which will first map the points back to the original domain. +# +# We support either TS and qEI which can be specified via the `acqf` argument. + +# In[6]: + + +def generate_batch( + state, + model, # GP model + X, # Evaluated points on the domain [0, 1]^d + Y, # Function values + batch_size, + n_candidates=None, # Number of candidates for Thompson sampling + num_restarts=10, + raw_samples=512, + acqf="ts", # "ei" or "ts" +): + assert acqf in ("ts", "ei") + assert X.min() >= 0.0 and X.max() <= 1.0 and torch.all(torch.isfinite(Y)) + if n_candidates is None: + n_candidates = min(5000, max(2000, 200 * X.shape[-1])) + + # Scale the TR to be proportional to the lengthscales + x_center = X[Y.argmax(), :].clone() + weights = model.covar_module.base_kernel.lengthscale.squeeze().detach() + weights = weights / weights.mean() + weights = weights / torch.prod(weights.pow(1.0 / len(weights))) + tr_lb = torch.clamp(x_center - weights * state.length / 2.0, 0.0, 1.0) + tr_ub = torch.clamp(x_center + weights * state.length / 2.0, 0.0, 1.0) + + if acqf == "ts": + dim = X.shape[-1] + sobol = SobolEngine(dim, scramble=True) + pert = sobol.draw(n_candidates).to(dtype=dtype, device=device) + pert = tr_lb + (tr_ub - tr_lb) * pert + + # Create a perturbation mask + prob_perturb = min(20.0 / dim, 1.0) + mask = torch.rand(n_candidates, dim, dtype=dtype, device=device) <= prob_perturb + ind = torch.where(mask.sum(dim=1) == 0)[0] + mask[ind, torch.randint(0, dim - 1, size=(len(ind),), device=device)] = 1 + + # Create candidate points from the perturbations and the mask + X_cand = x_center.expand(n_candidates, dim).clone() + X_cand[mask] = pert[mask] + + # Sample on the candidate points + thompson_sampling = MaxPosteriorSampling(model=model, replacement=False) + with torch.no_grad(): # We don't need gradients when using TS + X_next = thompson_sampling(X_cand, num_samples=batch_size) + + elif acqf == "ei": + ei = qExpectedImprovement(model, train_Y.max()) + X_next, acq_value = optimize_acqf( + ei, + bounds=torch.stack([tr_lb, tr_ub]), + q=batch_size, + num_restarts=num_restarts, + raw_samples=raw_samples, + ) + + return X_next + + +# ## Optimization loop +# This simple loop runs one instance of TuRBO-1 with Thompson sampling until convergence. +# +# TuRBO-1 is a local optimizer that can be used for a fixed evaluation budget in a multi-start fashion. Once TuRBO converges, `state["restart_triggered"]` will be set to true and the run should be aborted. If you want to run more evaluations with TuRBO, you simply generate a new set of initial points and then keep generating batches until convergence or when the evaluation budget has been exceeded. It's important to note that evaluations from previous instances are discarded when TuRBO restarts. +# +# NOTE: We use a `SingleTaskGP` with a noise constraint to keep the noise from getting too large as the problem is noise-free. + +# In[7]: + + +X_turbo = get_initial_points(dim, n_init) +Y_turbo = torch.tensor( + [eval_objective(x) for x in X_turbo], dtype=dtype, device=device +).unsqueeze(-1) + +state = TurboState(dim, batch_size=batch_size, best_value=max(Y_turbo).item()) + +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 512 if not SMOKE_TEST else 4 +N_CANDIDATES = min(5000, max(2000, 200 * dim)) if not SMOKE_TEST else 4 + +torch.manual_seed(0) + +while not state.restart_triggered: # Run until TuRBO converges + # Fit a GP model + train_Y = (Y_turbo - Y_turbo.mean()) / Y_turbo.std() + likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3)) + covar_module = ScaleKernel( # Use the same lengthscale prior as in the TuRBO paper + MaternKernel( + nu=2.5, ard_num_dims=dim, lengthscale_constraint=Interval(0.005, 4.0) + ) + ) + model = SingleTaskGP( + X_turbo, train_Y, covar_module=covar_module, likelihood=likelihood + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + + # Do the fitting and acquisition function optimization inside the Cholesky context + with gpytorch.settings.max_cholesky_size(max_cholesky_size): + # Fit the model + fit_gpytorch_mll(mll) + + # Create a batch + X_next = generate_batch( + state=state, + model=model, + X=X_turbo, + Y=train_Y, + batch_size=batch_size, + n_candidates=N_CANDIDATES, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + acqf="ts", + ) + + Y_next = torch.tensor( + [eval_objective(x) for x in X_next], dtype=dtype, device=device + ).unsqueeze(-1) + + # Update state + state = update_state(state=state, Y_next=Y_next) + + # Append data + X_turbo = torch.cat((X_turbo, X_next), dim=0) + Y_turbo = torch.cat((Y_turbo, Y_next), dim=0) + + # Print current status + print( + f"{len(X_turbo)}) Best value: {state.best_value:.2e}, TR length: {state.length:.2e}" + ) + + +# ## GP-LogEI +# We compare TuRBO to qLogEI [2], a recent improvement to the expected improvement (EI) acquisition functions. +# +# [2]: [Ament, Sebastian, et al., Unexpected Improvements to Expected Improvement for Bayesian Optimization. Advances in Neural Information Processing Systems. 2023](https://proceedings.neurips.cc/paper_files/paper/2023/file/419f72cbd568ad62183f8132a3605a2a-Paper-Conference.pdf) +# +# + +# In[8]: + + +torch.manual_seed(0) + +X_logei = get_initial_points(dim, n_init) +Y_logei = torch.tensor( + [eval_objective(x) for x in X_logei], dtype=dtype, device=device +).unsqueeze(-1) + +# Cap the number of evals when running smoke test +max_evals = min(len(Y_turbo), n_init + 2 * batch_size) if SMOKE_TEST else len(Y_turbo) +while len(Y_logei) < max_evals: + train_Y = (Y_logei - Y_logei.mean()) / Y_logei.std() + likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3)) + model = SingleTaskGP(X_logei, train_Y, likelihood=likelihood) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + + # Create a batch + log_ei = qLogExpectedImprovement(model, train_Y.max()) + candidate, acq_value = optimize_acqf( + log_ei, + bounds=torch.stack( + [ + torch.zeros(dim, dtype=dtype, device=device), + torch.ones(dim, dtype=dtype, device=device), + ] + ), + q=batch_size, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + Y_next = torch.tensor( + [eval_objective(x) for x in candidate], dtype=dtype, device=device + ).unsqueeze(-1) + + # Append data + X_logei = torch.cat((X_logei, candidate), axis=0) + Y_logei = torch.cat((Y_logei, Y_next), axis=0) + + # Print current status + print(f"{len(X_logei)}) Best value: {Y_logei.max().item():.2e}") + + +# ## GP-EI + +# In[9]: + + +torch.manual_seed(0) + +X_ei = get_initial_points(dim, n_init) +Y_ei = torch.tensor( + [eval_objective(x) for x in X_ei], dtype=dtype, device=device +).unsqueeze(-1) + +while len(Y_ei) < len(Y_turbo): + train_Y = (Y_ei - Y_ei.mean()) / Y_ei.std() + likelihood = GaussianLikelihood(noise_constraint=Interval(1e-8, 1e-3)) + model = SingleTaskGP(X_ei, train_Y, likelihood=likelihood) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + + # Create a batch + ei = qExpectedImprovement(model, train_Y.max()) + candidate, acq_value = optimize_acqf( + ei, + bounds=torch.stack( + [ + torch.zeros(dim, dtype=dtype, device=device), + torch.ones(dim, dtype=dtype, device=device), + ] + ), + q=batch_size, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + Y_next = torch.tensor( + [eval_objective(x) for x in candidate], dtype=dtype, device=device + ).unsqueeze(-1) + + # Append data + X_ei = torch.cat((X_ei, candidate), axis=0) + Y_ei = torch.cat((Y_ei, Y_next), axis=0) + + # Print current status + print(f"{len(X_ei)}) Best value: {Y_ei.max().item():.2e}") + + +# ## Sobol + +# In[10]: + + +X_Sobol = ( + SobolEngine(dim, scramble=True, seed=0) + .draw(len(X_turbo)) + .to(dtype=dtype, device=device) +) +Y_Sobol = torch.tensor( + [eval_objective(x) for x in X_Sobol], dtype=dtype, device=device +).unsqueeze(-1) + + +# ## Compare the methods + +# In[11]: + + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import rc + +get_ipython().run_line_magic('matplotlib', 'inline') + + +names = ["TuRBO-1", "LogEI", "EI", "Sobol"] +runs = [Y_turbo, Y_logei, Y_ei, Y_Sobol] +fig, ax = plt.subplots(figsize=(8, 6)) + +for name, run in zip(names, runs): + fx = np.maximum.accumulate(run.cpu()) + plt.plot(fx, marker="", lw=3) + +plt.plot([0, len(Y_turbo)], [fun.optimal_value, fun.optimal_value], "k--", lw=3) +plt.xlabel("Function value", fontsize=18) +plt.xlabel("Number of evaluations", fontsize=18) +plt.title("20D Ackley", fontsize=24) +plt.xlim([0, len(Y_turbo)]) +plt.ylim([-15, 1]) + +plt.grid(True) +plt.tight_layout() +plt.legend( + names + ["Global optimal value"], + loc="lower center", + bbox_to_anchor=(0, -0.08, 1, 1), + bbox_transform=plt.gcf().transFigure, + ncol=5, + fontsize=16, +) +plt.show() + + +# In[ ]: + + + + diff --git a/website-old/static/files/vae_mnist.ipynb b/website-old/static/files/vae_mnist.ipynb new file mode 100644 index 0000000000..71cb4c0887 --- /dev/null +++ b/website-old/static/files/vae_mnist.ipynb @@ -0,0 +1,502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## VAE MNIST example: BO in a latent space" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this tutorial, we use the MNIST dataset and some standard PyTorch examples to show a synthetic problem where the input to the objective function is a `28 x 28` image. The main idea is to train a [variational auto-encoder (VAE)](https://arxiv.org/abs/1312.6114) on the MNIST dataset and run Bayesian Optimization in the latent space. We also refer readers to [this tutorial](http://krasserm.github.io/2018/04/07/latent-space-optimization/), which discusses [the method](https://arxiv.org/abs/1610.02415) of jointly training a VAE with a predictor (e.g., classifier), and shows a similar tutorial for the MNIST setting." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "import torch.optim as optim\n", + "from torchvision import datasets # transforms\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "dtype = torch.double\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\", False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Problem setup\n", + "\n", + "Let's first define our synthetic expensive-to-evaluate objective function. We assume that it takes the following form:\n", + "\n", + "$$\\text{image} \\longrightarrow \\text{image classifier} \\longrightarrow \\text{scoring function} \n", + "\\longrightarrow \\text{score}.$$\n", + "\n", + "The classifier is a convolutional neural network (CNN) trained using the architecture of the [PyTorch CNN example](https://github.com/pytorch/examples/tree/master/mnist)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "class Net(nn.Module):\n", + " def __init__(self):\n", + " super(Net, self).__init__()\n", + " self.conv1 = nn.Conv2d(1, 20, 5, 1)\n", + " self.conv2 = nn.Conv2d(20, 50, 5, 1)\n", + " self.fc1 = nn.Linear(4 * 4 * 50, 500)\n", + " self.fc2 = nn.Linear(500, 10)\n", + "\n", + " def forward(self, x):\n", + " x = F.relu(self.conv1(x))\n", + " x = F.max_pool2d(x, 2, 2)\n", + " x = F.relu(self.conv2(x))\n", + " x = F.max_pool2d(x, 2, 2)\n", + " x = x.view(-1, 4 * 4 * 50)\n", + " x = F.relu(self.fc1(x))\n", + " x = self.fc2(x)\n", + " return F.log_softmax(x, dim=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def get_pretrained_dir() -> str:\n", + " \"\"\"\n", + " Get the directory of pretrained models, which are in the BoTorch repo.\n", + "\n", + " Returns the location specified by PRETRAINED_LOCATION if that env\n", + " var is set; otherwise checks if we are in a likely part of the BoTorch\n", + " repo (botorch/botorch or botorch/tutorials) and returns the right path.\n", + " \"\"\"\n", + " if \"PRETRAINED_LOCATION\" in os.environ.keys():\n", + " return os.environ[\"PRETRAINED_LOCATION\"]\n", + " cwd = os.getcwd()\n", + " folder = os.path.basename(cwd)\n", + " # automated tests run from botorch folder\n", + " if folder == \"botorch\": \n", + " return os.path.join(cwd, \"tutorials/pretrained_models/\")\n", + " # typical case (running from tutorial folder)\n", + " elif folder == \"tutorials\":\n", + " return os.path.join(cwd, \"pretrained_models/\")\n", + " raise FileNotFoundError(\"Could not figure out location of pretrained models.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "cnn_weights_path = os.path.join(get_pretrained_dir(), \"mnist_cnn.pt\")\n", + "cnn_model = Net().to(dtype=dtype, device=device)\n", + "cnn_state_dict = torch.load(cnn_weights_path, map_location=device, weights_only=True)\n", + "cnn_model.load_state_dict(cnn_state_dict);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our VAE model follows the [PyTorch VAE example](https://github.com/pytorch/examples/tree/master/vae), except that we use the same data transform from the CNN tutorial for consistency. We then instantiate the model and again load a pre-trained model. To train these models, we refer readers to the PyTorch Github repository. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "class VAE(nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.fc1 = nn.Linear(784, 400)\n", + " self.fc21 = nn.Linear(400, 20)\n", + " self.fc22 = nn.Linear(400, 20)\n", + " self.fc3 = nn.Linear(20, 400)\n", + " self.fc4 = nn.Linear(400, 784)\n", + "\n", + " def encode(self, x):\n", + " h1 = F.relu(self.fc1(x))\n", + " return self.fc21(h1), self.fc22(h1)\n", + "\n", + " def reparameterize(self, mu, logvar):\n", + " std = torch.exp(0.5 * logvar)\n", + " eps = torch.randn_like(std)\n", + " return mu + eps * std\n", + "\n", + " def decode(self, z):\n", + " h3 = F.relu(self.fc3(z))\n", + " return torch.sigmoid(self.fc4(h3))\n", + "\n", + " def forward(self, x):\n", + " mu, logvar = self.encode(x.view(-1, 784))\n", + " z = self.reparameterize(mu, logvar)\n", + " return self.decode(z), mu, logvar\n", + "\n", + "vae_weights_path = os.path.join(get_pretrained_dir(), \"mnist_vae.pt\")\n", + "vae_model = VAE().to(dtype=dtype, device=device)\n", + "vae_state_dict = torch.load(vae_weights_path, map_location=device, weights_only=True)\n", + "vae_model.load_state_dict(vae_state_dict);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now define the scoring function that maps digits to scores. The function below prefers the digit '3'." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def score(y):\n", + " \"\"\"Returns a 'score' for each digit from 0 to 9. It is modeled as a squared exponential\n", + " centered at the digit '3'.\n", + " \"\"\"\n", + " return torch.exp(-2 * (y - 3) ** 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Given the scoring function, we can now write our overall objective, which as discussed above, starts with an image and outputs a score. Let's say the objective computes the expected score given the probabilities from the classifier." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def score_image(x):\n", + " \"\"\"The input x is an image and an expected score \n", + " based on the CNN classifier and the scoring \n", + " function is returned.\n", + " \"\"\"\n", + " with torch.no_grad():\n", + " probs = torch.exp(cnn_model(x)) # b x 10\n", + " scores = score(\n", + " torch.arange(10, device=device, dtype=dtype)\n", + " ).expand(probs.shape)\n", + " return (probs * scores).sum(dim=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we define a helper function `decode` that takes as input the parameters `mu` and `logvar` of the variational distribution and performs reparameterization and the decoding. We use batched Bayesian optimization to search over the parameters `mu` and `logvar`" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def decode(train_x):\n", + " with torch.no_grad():\n", + " decoded = vae_model.decode(train_x)\n", + " return decoded.view(train_x.shape[0], 1, 28, 28)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Model initialization and initial random batch\n", + "\n", + "We use a `SingleTaskGP` to model the score of an image generated by a latent representation. The model is initialized with points drawn from $[-6, 6]^{20}$." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":1:10: fatal error: 'omp.h' file not found\n", + "#include \n", + " ^~~~~~~\n", + "1 error generated.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[KeOps] Warning : omp.h header is not in the path, disabling OpenMP.\n", + "[KeOps] Warning : Cuda libraries were not detected on the system ; using cpu only mode\n" + ] + } + ], + "source": [ + "from botorch.models import SingleTaskGP\n", + "from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood\n", + "from botorch.utils.transforms import normalize, unnormalize\n", + "from botorch.models.transforms import Standardize, Normalize\n", + "\n", + "d = 20\n", + "bounds = torch.tensor([[-6.0] * d, [6.0] * d], device=device, dtype=dtype)\n", + "\n", + "\n", + "def gen_initial_data(n=5):\n", + " # generate training data\n", + " train_x = unnormalize(\n", + " torch.rand(n, d, device=device, dtype=dtype), \n", + " bounds=bounds\n", + " )\n", + " train_obj = score_image(decode(train_x)).unsqueeze(-1)\n", + " best_observed_value = train_obj.max().item()\n", + " return train_x, train_obj, best_observed_value\n", + "\n", + "\n", + "def get_fitted_model(train_x, train_obj, state_dict=None):\n", + " # initialize and fit model\n", + " model = SingleTaskGP(\n", + " train_X=normalize(train_x, bounds), \n", + " train_Y=train_obj,\n", + " outcome_transform=Standardize(m=1)\n", + " )\n", + " if state_dict is not None:\n", + " model.load_state_dict(state_dict)\n", + " mll = ExactMarginalLogLikelihood(model.likelihood, model)\n", + " mll.to(train_x)\n", + " fit_gpytorch_mll(mll)\n", + " return model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define a helper function that performs the essential BO step\n", + "The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\\{x_1, x_2, \\ldots x_q\\}$ along with the observed function values. For this example, we'll use a small batch of $q=3$." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.optim import optimize_acqf\n", + "\n", + "\n", + "BATCH_SIZE = 3 if not SMOKE_TEST else 2\n", + "NUM_RESTARTS = 10 if not SMOKE_TEST else 2\n", + "RAW_SAMPLES = 256 if not SMOKE_TEST else 4\n", + "\n", + "\n", + "def optimize_acqf_and_get_observation(acq_func):\n", + " \"\"\"Optimizes the acquisition function, and returns a\n", + " new candidate and a noisy observation\"\"\"\n", + "\n", + " # optimize\n", + " candidates, _ = optimize_acqf(\n", + " acq_function=acq_func,\n", + " bounds=torch.stack(\n", + " [\n", + " torch.zeros(d, dtype=dtype, device=device),\n", + " torch.ones(d, dtype=dtype, device=device),\n", + " ]\n", + " ),\n", + " q=BATCH_SIZE,\n", + " num_restarts=NUM_RESTARTS,\n", + " raw_samples=RAW_SAMPLES,\n", + " )\n", + "\n", + " # observe new values\n", + " new_x = unnormalize(candidates.detach(), bounds=bounds)\n", + " new_obj = score_image(decode(new_x)).unsqueeze(-1)\n", + " return new_x, new_obj" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perform Bayesian Optimization loop with qEI\n", + "The Bayesian optimization \"loop\" for a batch size of $q$ simply iterates the following steps: (1) given a surrogate model, choose a batch of points $\\{x_1, x_2, \\ldots x_q\\}$, (2) observe $f(x)$ for each $x$ in the batch, and (3) update the surrogate model. We run `N_BATCH=75` iterations. The acquisition function is approximated using `MC_SAMPLES=2048` samples. We also initialize the model with 5 randomly drawn points." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[W NNPACK.cpp:64] Could not initialize NNPACK! Reason: Unsupported hardware.\n" + ] + } + ], + "source": [ + "from botorch import fit_gpytorch_mll\n", + "from botorch.acquisition.monte_carlo import qExpectedImprovement\n", + "from botorch.sampling.normal import SobolQMCNormalSampler\n", + "\n", + "seed = 1\n", + "torch.manual_seed(seed)\n", + "\n", + "N_BATCH = 25 if not SMOKE_TEST else 3\n", + "best_observed = []\n", + "\n", + "# call helper function to initialize model\n", + "train_x, train_obj, best_value = gen_initial_data(n=5)\n", + "best_observed.append(best_value)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are now ready to run the BO loop (this make take a few minutes, depending on your machine)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Running BO ........................." + ] + } + ], + "source": [ + "import warnings\n", + "from matplotlib import pyplot as plt\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "\n", + "print(f\"\\nRunning BO \", end=\"\")\n", + "\n", + "state_dict = None\n", + "# run N_BATCH rounds of BayesOpt after the initial random batch\n", + "for iteration in range(N_BATCH):\n", + "\n", + " # fit the model\n", + " model = get_fitted_model(\n", + " train_x=train_x,\n", + " train_obj=train_obj,\n", + " state_dict=state_dict,\n", + " )\n", + "\n", + " # define the qNEI acquisition function\n", + " qEI = qExpectedImprovement(\n", + " model=model, best_f=train_obj.max()\n", + " )\n", + "\n", + " # optimize and get new observation\n", + " new_x, new_obj = optimize_acqf_and_get_observation(qEI)\n", + "\n", + " # update training points\n", + " train_x = torch.cat((train_x, new_x))\n", + " train_obj = torch.cat((train_obj, new_obj))\n", + "\n", + " # update progress\n", + " best_value = train_obj.max().item()\n", + " best_observed.append(best_value)\n", + "\n", + " state_dict = model.state_dict()\n", + "\n", + " print(\".\", end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "EI recommends the best point observed so far. We can visualize what the images corresponding to recommended points *would have* been if the BO process ended at various times. Here, we show the progress of the algorithm by examining the images at 0%, 10%, 25%, 50%, 75%, and 100% completion. The first image is the best image found through the initial random batch." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "\n", + "from matplotlib import pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "\n", + "fig, ax = plt.subplots(1, 6, figsize=(14, 14))\n", + "percentages = np.array([0, 10, 25, 50, 75, 100], dtype=np.float32)\n", + "inds = (N_BATCH * BATCH_SIZE * percentages / 100 + 4).astype(int)\n", + "\n", + "for i, ax in enumerate(ax.flat):\n", + " b = torch.argmax(score_image(decode(train_x[: inds[i], :])), dim=0)\n", + " img = decode(train_x[b].view(1, -1)).squeeze().cpu()\n", + " ax.imshow(img, alpha=0.8, cmap=\"gray\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "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.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website-old/static/files/vae_mnist.py b/website-old/static/files/vae_mnist.py new file mode 100644 index 0000000000..03bca7e436 --- /dev/null +++ b/website-old/static/files/vae_mnist.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +# ## VAE MNIST example: BO in a latent space + +# In this tutorial, we use the MNIST dataset and some standard PyTorch examples to show a synthetic problem where the input to the objective function is a `28 x 28` image. The main idea is to train a [variational auto-encoder (VAE)](https://arxiv.org/abs/1312.6114) on the MNIST dataset and run Bayesian Optimization in the latent space. We also refer readers to [this tutorial](http://krasserm.github.io/2018/04/07/latent-space-optimization/), which discusses [the method](https://arxiv.org/abs/1610.02415) of jointly training a VAE with a predictor (e.g., classifier), and shows a similar tutorial for the MNIST setting. + +# In[1]: + + +import os +import torch + +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from torchvision import datasets # transforms + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +dtype = torch.double +SMOKE_TEST = os.environ.get("SMOKE_TEST", False) + + +# ### Problem setup +# +# Let's first define our synthetic expensive-to-evaluate objective function. We assume that it takes the following form: +# +# $$\text{image} \longrightarrow \text{image classifier} \longrightarrow \text{scoring function} +# \longrightarrow \text{score}.$$ +# +# The classifier is a convolutional neural network (CNN) trained using the architecture of the [PyTorch CNN example](https://github.com/pytorch/examples/tree/master/mnist). + +# In[2]: + + +class Net(nn.Module): + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(1, 20, 5, 1) + self.conv2 = nn.Conv2d(20, 50, 5, 1) + self.fc1 = nn.Linear(4 * 4 * 50, 500) + self.fc2 = nn.Linear(500, 10) + + def forward(self, x): + x = F.relu(self.conv1(x)) + x = F.max_pool2d(x, 2, 2) + x = F.relu(self.conv2(x)) + x = F.max_pool2d(x, 2, 2) + x = x.view(-1, 4 * 4 * 50) + x = F.relu(self.fc1(x)) + x = self.fc2(x) + return F.log_softmax(x, dim=1) + + +# In[4]: + + +def get_pretrained_dir() -> str: + """ + Get the directory of pretrained models, which are in the BoTorch repo. + + Returns the location specified by PRETRAINED_LOCATION if that env + var is set; otherwise checks if we are in a likely part of the BoTorch + repo (botorch/botorch or botorch/tutorials) and returns the right path. + """ + if "PRETRAINED_LOCATION" in os.environ.keys(): + return os.environ["PRETRAINED_LOCATION"] + cwd = os.getcwd() + folder = os.path.basename(cwd) + # automated tests run from botorch folder + if folder == "botorch": + return os.path.join(cwd, "tutorials/pretrained_models/") + # typical case (running from tutorial folder) + elif folder == "tutorials": + return os.path.join(cwd, "pretrained_models/") + raise FileNotFoundError("Could not figure out location of pretrained models.") + + +# In[5]: + + +cnn_weights_path = os.path.join(get_pretrained_dir(), "mnist_cnn.pt") +cnn_model = Net().to(dtype=dtype, device=device) +cnn_state_dict = torch.load(cnn_weights_path, map_location=device, weights_only=True) +cnn_model.load_state_dict(cnn_state_dict); + + +# Our VAE model follows the [PyTorch VAE example](https://github.com/pytorch/examples/tree/master/vae), except that we use the same data transform from the CNN tutorial for consistency. We then instantiate the model and again load a pre-trained model. To train these models, we refer readers to the PyTorch Github repository. + +# In[6]: + + +class VAE(nn.Module): + def __init__(self): + super().__init__() + self.fc1 = nn.Linear(784, 400) + self.fc21 = nn.Linear(400, 20) + self.fc22 = nn.Linear(400, 20) + self.fc3 = nn.Linear(20, 400) + self.fc4 = nn.Linear(400, 784) + + def encode(self, x): + h1 = F.relu(self.fc1(x)) + return self.fc21(h1), self.fc22(h1) + + def reparameterize(self, mu, logvar): + std = torch.exp(0.5 * logvar) + eps = torch.randn_like(std) + return mu + eps * std + + def decode(self, z): + h3 = F.relu(self.fc3(z)) + return torch.sigmoid(self.fc4(h3)) + + def forward(self, x): + mu, logvar = self.encode(x.view(-1, 784)) + z = self.reparameterize(mu, logvar) + return self.decode(z), mu, logvar + +vae_weights_path = os.path.join(get_pretrained_dir(), "mnist_vae.pt") +vae_model = VAE().to(dtype=dtype, device=device) +vae_state_dict = torch.load(vae_weights_path, map_location=device, weights_only=True) +vae_model.load_state_dict(vae_state_dict); + + +# We now define the scoring function that maps digits to scores. The function below prefers the digit '3'. + +# In[7]: + + +def score(y): + """Returns a 'score' for each digit from 0 to 9. It is modeled as a squared exponential + centered at the digit '3'. + """ + return torch.exp(-2 * (y - 3) ** 2) + + +# Given the scoring function, we can now write our overall objective, which as discussed above, starts with an image and outputs a score. Let's say the objective computes the expected score given the probabilities from the classifier. + +# In[8]: + + +def score_image(x): + """The input x is an image and an expected score + based on the CNN classifier and the scoring + function is returned. + """ + with torch.no_grad(): + probs = torch.exp(cnn_model(x)) # b x 10 + scores = score( + torch.arange(10, device=device, dtype=dtype) + ).expand(probs.shape) + return (probs * scores).sum(dim=1) + + +# Finally, we define a helper function `decode` that takes as input the parameters `mu` and `logvar` of the variational distribution and performs reparameterization and the decoding. We use batched Bayesian optimization to search over the parameters `mu` and `logvar` + +# In[9]: + + +def decode(train_x): + with torch.no_grad(): + decoded = vae_model.decode(train_x) + return decoded.view(train_x.shape[0], 1, 28, 28) + + +# #### Model initialization and initial random batch +# +# We use a `SingleTaskGP` to model the score of an image generated by a latent representation. The model is initialized with points drawn from $[-6, 6]^{20}$. + +# In[10]: + + +from botorch.models import SingleTaskGP +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood +from botorch.utils.transforms import normalize, unnormalize +from botorch.models.transforms import Standardize, Normalize + +d = 20 +bounds = torch.tensor([[-6.0] * d, [6.0] * d], device=device, dtype=dtype) + + +def gen_initial_data(n=5): + # generate training data + train_x = unnormalize( + torch.rand(n, d, device=device, dtype=dtype), + bounds=bounds + ) + train_obj = score_image(decode(train_x)).unsqueeze(-1) + best_observed_value = train_obj.max().item() + return train_x, train_obj, best_observed_value + + +def get_fitted_model(train_x, train_obj, state_dict=None): + # initialize and fit model + model = SingleTaskGP( + train_X=normalize(train_x, bounds), + train_Y=train_obj, + outcome_transform=Standardize(m=1) + ) + if state_dict is not None: + model.load_state_dict(state_dict) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + mll.to(train_x) + fit_gpytorch_mll(mll) + return model + + +# #### Define a helper function that performs the essential BO step +# The helper function below takes an acquisition function as an argument, optimizes it, and returns the batch $\{x_1, x_2, \ldots x_q\}$ along with the observed function values. For this example, we'll use a small batch of $q=3$. + +# In[11]: + + +from botorch.optim import optimize_acqf + + +BATCH_SIZE = 3 if not SMOKE_TEST else 2 +NUM_RESTARTS = 10 if not SMOKE_TEST else 2 +RAW_SAMPLES = 256 if not SMOKE_TEST else 4 + + +def optimize_acqf_and_get_observation(acq_func): + """Optimizes the acquisition function, and returns a + new candidate and a noisy observation""" + + # optimize + candidates, _ = optimize_acqf( + acq_function=acq_func, + bounds=torch.stack( + [ + torch.zeros(d, dtype=dtype, device=device), + torch.ones(d, dtype=dtype, device=device), + ] + ), + q=BATCH_SIZE, + num_restarts=NUM_RESTARTS, + raw_samples=RAW_SAMPLES, + ) + + # observe new values + new_x = unnormalize(candidates.detach(), bounds=bounds) + new_obj = score_image(decode(new_x)).unsqueeze(-1) + return new_x, new_obj + + +# ### Perform Bayesian Optimization loop with qEI +# The Bayesian optimization "loop" for a batch size of $q$ simply iterates the following steps: (1) given a surrogate model, choose a batch of points $\{x_1, x_2, \ldots x_q\}$, (2) observe $f(x)$ for each $x$ in the batch, and (3) update the surrogate model. We run `N_BATCH=75` iterations. The acquisition function is approximated using `MC_SAMPLES=2048` samples. We also initialize the model with 5 randomly drawn points. + +# In[12]: + + +from botorch import fit_gpytorch_mll +from botorch.acquisition.monte_carlo import qExpectedImprovement +from botorch.sampling.normal import SobolQMCNormalSampler + +seed = 1 +torch.manual_seed(seed) + +N_BATCH = 25 if not SMOKE_TEST else 3 +best_observed = [] + +# call helper function to initialize model +train_x, train_obj, best_value = gen_initial_data(n=5) +best_observed.append(best_value) + + +# We are now ready to run the BO loop (this make take a few minutes, depending on your machine). + +# In[13]: + + +import warnings +from matplotlib import pyplot as plt + +warnings.filterwarnings("ignore") + + +print(f"\nRunning BO ", end="") + +state_dict = None +# run N_BATCH rounds of BayesOpt after the initial random batch +for iteration in range(N_BATCH): + + # fit the model + model = get_fitted_model( + train_x=train_x, + train_obj=train_obj, + state_dict=state_dict, + ) + + # define the qNEI acquisition function + qEI = qExpectedImprovement( + model=model, best_f=train_obj.max() + ) + + # optimize and get new observation + new_x, new_obj = optimize_acqf_and_get_observation(qEI) + + # update training points + train_x = torch.cat((train_x, new_x)) + train_obj = torch.cat((train_obj, new_obj)) + + # update progress + best_value = train_obj.max().item() + best_observed.append(best_value) + + state_dict = model.state_dict() + + print(".", end="") + + +# EI recommends the best point observed so far. We can visualize what the images corresponding to recommended points *would have* been if the BO process ended at various times. Here, we show the progress of the algorithm by examining the images at 0%, 10%, 25%, 50%, 75%, and 100% completion. The first image is the best image found through the initial random batch. + +# In[14]: + + +import numpy as np + +from matplotlib import pyplot as plt + +get_ipython().run_line_magic('matplotlib', 'inline') + + +fig, ax = plt.subplots(1, 6, figsize=(14, 14)) +percentages = np.array([0, 10, 25, 50, 75, 100], dtype=np.float32) +inds = (N_BATCH * BATCH_SIZE * percentages / 100 + 4).astype(int) + +for i, ax in enumerate(ax.flat): + b = torch.argmax(score_image(decode(train_x[: inds[i], :])), dim=0) + img = decode(train_x[b].view(1, -1)).squeeze().cpu() + ax.imshow(img, alpha=0.8, cmap="gray") + diff --git a/website-old/static/img/botorch.ico b/website-old/static/img/botorch.ico new file mode 100644 index 0000000000..dad1c4d4a4 Binary files /dev/null and b/website-old/static/img/botorch.ico differ diff --git a/website-old/static/img/botorch.png b/website-old/static/img/botorch.png new file mode 100644 index 0000000000..30631bf9ef Binary files /dev/null and b/website-old/static/img/botorch.png differ diff --git a/website-old/static/img/botorch_logo_lockup.png b/website-old/static/img/botorch_logo_lockup.png new file mode 100755 index 0000000000..895766a462 Binary files /dev/null and b/website-old/static/img/botorch_logo_lockup.png differ diff --git a/website-old/static/img/botorch_logo_lockup.svg b/website-old/static/img/botorch_logo_lockup.svg new file mode 100755 index 0000000000..77484777ce --- /dev/null +++ b/website-old/static/img/botorch_logo_lockup.svg @@ -0,0 +1 @@ +01_FullColor \ No newline at end of file diff --git a/website-old/static/img/botorch_logo_lockup_top.png b/website-old/static/img/botorch_logo_lockup_top.png new file mode 100644 index 0000000000..3c541ee6c7 Binary files /dev/null and b/website-old/static/img/botorch_logo_lockup_top.png differ diff --git a/website-old/static/img/botorch_logo_lockup_white.png b/website-old/static/img/botorch_logo_lockup_white.png new file mode 100644 index 0000000000..36c7c76ad3 Binary files /dev/null and b/website-old/static/img/botorch_logo_lockup_white.png differ diff --git a/website-old/static/img/expanding_arrows.svg b/website-old/static/img/expanding_arrows.svg new file mode 100644 index 0000000000..500f19a74a --- /dev/null +++ b/website-old/static/img/expanding_arrows.svg @@ -0,0 +1,17 @@ + + + + +exp_arrows_grey + + + + + diff --git a/website-old/static/img/oss_logo.png b/website-old/static/img/oss_logo.png new file mode 100644 index 0000000000..750c3c3551 Binary files /dev/null and b/website-old/static/img/oss_logo.png differ diff --git a/website-old/static/img/puzzle_pieces.svg b/website-old/static/img/puzzle_pieces.svg new file mode 100644 index 0000000000..f19f16126d --- /dev/null +++ b/website-old/static/img/puzzle_pieces.svg @@ -0,0 +1,21 @@ + + + + +puzze_grey + + + + diff --git a/website-old/static/img/pytorch_logo.svg b/website-old/static/img/pytorch_logo.svg new file mode 100644 index 0000000000..f4e4a19f2f --- /dev/null +++ b/website-old/static/img/pytorch_logo.svg @@ -0,0 +1,13 @@ + + + + +pytorch_logo + + + diff --git a/website-old/static/js/code_block_buttons.js b/website-old/static/js/code_block_buttons.js new file mode 100644 index 0000000000..e4fb913d98 --- /dev/null +++ b/website-old/static/js/code_block_buttons.js @@ -0,0 +1,47 @@ +// Turn off ESLint for this file because it's sent down to users as-is. +/* eslint-disable */ +window.addEventListener('load', function() { + function button(label, ariaLabel, icon, className) { + const btn = document.createElement('button'); + btn.classList.add('btnIcon', className); + btn.setAttribute('type', 'button'); + btn.setAttribute('aria-label', ariaLabel); + btn.innerHTML = + '
' + + icon + + '' + + label + + '' + + '
'; + return btn; + } + + function addButtons(codeBlockSelector, btn) { + document.querySelectorAll(codeBlockSelector).forEach(function(code) { + code.parentNode.appendChild(btn.cloneNode(true)); + }); + } + + const copyIcon = + ''; + + addButtons( + '.hljs', + button('Copy', 'Copy code to clipboard', copyIcon, 'btnClipboard'), + ); + + const clipboard = new ClipboardJS('.btnClipboard', { + target: function(trigger) { + return trigger.parentNode.querySelector('code'); + }, + }); + + clipboard.on('success', function(event) { + event.clearSelection(); + const textEl = event.trigger.querySelector('.btnIcon__label'); + textEl.textContent = 'Copied'; + setTimeout(function() { + textEl.textContent = 'Copy'; + }, 2000); + }); +}); diff --git a/website-old/static/js/doctools.js b/website-old/static/js/doctools.js new file mode 100644 index 0000000000..0398ebb9f0 --- /dev/null +++ b/website-old/static/js/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/website-old/static/js/documentation_options.js b/website-old/static/js/documentation_options.js new file mode 100644 index 0000000000..7e4c114f21 --- /dev/null +++ b/website-old/static/js/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/website-old/static/js/language_data.js b/website-old/static/js/language_data.js new file mode 100644 index 0000000000..c7fe6c6faf --- /dev/null +++ b/website-old/static/js/language_data.js @@ -0,0 +1,192 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/website/static/js/mathjax.js b/website-old/static/js/mathjax.js similarity index 100% rename from website/static/js/mathjax.js rename to website-old/static/js/mathjax.js diff --git a/website-old/static/js/searchindex.js b/website-old/static/js/searchindex.js new file mode 100644 index 0000000000..45dc885b02 --- /dev/null +++ b/website-old/static/js/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"API Reference": [[5, null]], "Abstract Acquisition Function APIs": [[0, "module-botorch.acquisition.acquisition"]], "Abstract Box Decompositions": [[14, "module-botorch.utils.multi_objective.box_decompositions.box_decomposition"]], "Abstract Posterior API": [[9, "module-botorch.posteriors.posterior"]], "Abstract Test Function API": [[12, "module-botorch.test_functions.base"]], "Acquisition Function APIs": [[0, "acquisition-function-apis"]], "Acquisition Function Optimization": [[8, "module-botorch.optim.optimize"]], "Acquisition Function Optimization with Homotopy": [[8, "module-botorch.optim.optimize_homotopy"]], "Acquisition Function Optimization with Mixed Integer Variables": [[8, "module-botorch.optim.optimize_mixed"]], "Acquisition Functions": [[0, "acquisition-functions"]], "Acquisition Optimization Utilities": [[8, "module-botorch.optim.utils.acquisition_utils"]], "Active Learning Acquisition Functions": [[0, "module-botorch.acquisition.active_learning"]], "Analytic Acquisition Function API": [[0, "analytic-acquisition-function-api"]], "Analytic Acquisition Functions": [[0, "module-botorch.acquisition.analytic"]], "Base Classes for Multi-Objective Acquisition Function API": [[0, "module-botorch.acquisition.multi_objective.base"]], "Base Model API": [[7, "module-botorch.models.model"]], "Base Samples": [[9, "module-botorch.posteriors.base_samples"]], "Bayesian Active Learning Acquisition Functions": [[0, "module-botorch.acquisition.bayesian_active_learning"]], "Bivariate Normal Probabilities and Statistics": [[14, "module-botorch.utils.probability.bvn"]], "BoTorch API Reference": [[5, null]], "Box Decomposition List": [[14, "module-botorch.utils.multi_objective.box_decompositions.box_decomposition_list"]], "Box Decomposition Utilities": [[14, "module-botorch.utils.multi_objective.box_decompositions.utils"]], "Cached Cholesky Acquisition Function API": [[0, "module-botorch.acquisition.cached_cholesky"]], "Candidate Generation Utilities for Acquisition Functions": [[4, "module-botorch.generation.gen"]], "Closures": [[8, "closures"]], "Constants": [[14, "module-botorch.utils.constants"]], "Constraints": [[14, "module-botorch.utils.constraints"]], "Constructors for Acquisition Function Input Arguments": [[0, "module-botorch.acquisition.input_constructors"]], "Containers": [[14, "module-botorch.utils.containers"]], "Context Managers": [[14, "module-botorch.utils.context_managers"]], "Contextual GP Models with Aggregate Rewards": [[7, "module-botorch.models.contextual"]], "Contextual GP Models with Context Rewards": [[7, "module-botorch.models.contextual_multioutput"]], "Core": [[8, "module-botorch.optim.core"], [8, "id2"]], "Cost Models (for cost-aware optimization)": [[7, "module-botorch.models.cost"]], "Cost-Aware Utility": [[0, "module-botorch.acquisition.cost_aware"]], "Datasets": [[14, "module-botorch.utils.datasets"]], "Decoupled Acquisition Function API": [[0, "module-botorch.acquisition.decoupled"]], "Deterministic Model API": [[7, "module-botorch.models.deterministic"]], "Dispatcher": [[14, "module-botorch.utils.dispatcher"]], "Dominated Partitionings": [[14, "module-botorch.utils.multi_objective.box_decompositions.dominated"]], "Elliptic Slice Sampler with Linear Constraints": [[14, "module-botorch.utils.probability.lin_ess"]], "Ensemble Model API": [[7, "module-botorch.models.ensemble"]], "Ensemble Posterior": [[9, "module-botorch.posteriors.ensemble"]], "Errors": [[2, "module-botorch.exceptions.errors"]], "Factory Functions for Acquisition Functions": [[0, "module-botorch.acquisition.factory"]], "Feasible Volume": [[14, "module-botorch.utils.feasible_volume"]], "Feature Map Generators": [[10, "module-botorch.sampling.pathwise.features.generators"]], "Feature Maps": [[10, "module-botorch.sampling.pathwise.features.maps"]], "Fixed Feature Acquisition Function": [[0, "module-botorch.acquisition.fixed_feature"]], "Fully Bayesian GP Models": [[7, "module-botorch.models.fully_bayesian"]], "Fully Bayesian Multitask GP Models": [[7, "module-botorch.models.fully_bayesian_multitask"]], "Fully Bayesian Posterior": [[9, "module-botorch.posteriors.fully_bayesian"]], "GP Regression Models": [[7, "module-botorch.models.gp_regression"]], "GP Regression Models for Mixed Parameter Spaces": [[7, "module-botorch.models.gp_regression_mixed"]], "GPyTorch Model API": [[7, "module-botorch.models.gpytorch"]], "GPyTorch Module Constructors": [[7, "module-botorch.models.utils.gpytorch_modules"]], "GPyTorch Posterior": [[9, "module-botorch.posteriors.gpytorch"]], "Gaussian Monte-Carlo Samplers": [[10, "module-botorch.sampling.normal"]], "General Optimization Utilities": [[8, "module-botorch.optim.utils.common"]], "General Utilities for Acquisition Functions": [[0, "module-botorch.acquisition.utils"]], "Get Sampler Helper": [[10, "module-botorch.sampling.get_sampler"]], "Higher Order GP Models": [[7, "module-botorch.models.higher_order_gp"]], "Higher Order GP Posterior": [[9, "module-botorch.posteriors.higher_order"]], "Homotopy Utilities": [[8, "module-botorch.optim.homotopy"]], "Hypervolume": [[14, "module-botorch.utils.multi_objective.hypervolume"]], "Index Sampler": [[10, "module-botorch.sampling.index_sampler"]], "Indices and Tables": [[5, "indices-and-tables"]], "Inducing Point Allocators": [[7, "module-botorch.models.utils.inducing_point_allocators"]], "Initialization Helpers": [[8, "module-botorch.optim.initializers"]], "Input Transforms": [[7, "module-botorch.models.transforms.input"]], "Joint Entropy Search Acquisition Functions": [[0, "module-botorch.acquisition.joint_entropy_search"]], "Kernels": [[7, "module-botorch.models.kernels.categorical"]], "Likelihoods": [[7, "module-botorch.models.likelihoods.pairwise"]], "Linear Algebra Helpers": [[14, "module-botorch.utils.probability.linalg"]], "List Sampler": [[10, "module-botorch.sampling.list_sampler"]], "Low-Rank Cholesky Update Utils": [[14, "module-botorch.utils.low_rank"]], "Max-value Entropy Search Acquisition Functions": [[0, "module-botorch.acquisition.max_value_entropy_search"]], "Mock": [[13, "module-botorch.test_utils.mock"]], "Model APIs": [[7, "model-apis"]], "Model Components": [[7, "model-components"]], "Model Conversion": [[7, "module-botorch.models.converter"]], "Model Fitting Closures": [[8, "module-botorch.optim.closures.model_closures"]], "Model Fitting Optimization": [[8, "module-botorch.optim.fit"]], "Model Fitting Utilities": [[8, "module-botorch.optim.utils.model_utils"]], "Model List GP Regression Models": [[7, "module-botorch.models.model_list_gp_regression"]], "Models": [[7, "models"]], "Monte-Carlo Acquisition Function API": [[0, "monte-carlo-acquisition-function-api"]], "Monte-Carlo Acquisition Functions": [[0, "module-botorch.acquisition.monte_carlo"]], "Monte-Carlo Sampler API": [[10, "module-botorch.sampling.base"]], "Multi-Fidelity GP Regression Models": [[7, "module-botorch.models.gp_regression_fidelity"]], "Multi-Fidelity Synthetic Test Functions": [[12, "module-botorch.test_functions.multi_fidelity"]], "Multi-Objective Analytic Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.analytic"]], "Multi-Objective Hypervolume Knowledge Gradient Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.hypervolume_knowledge_gradient"]], "Multi-Objective Joint Entropy Search Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.joint_entropy_search"]], "Multi-Objective Max-value Entropy Search Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.max_value_entropy_search"]], "Multi-Objective Monte-Carlo Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.monte_carlo"]], "Multi-Objective Multi-Fidelity Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.multi_fidelity"]], "Multi-Objective Multi-Fidelity Synthetic Test Functions": [[12, "module-botorch.test_functions.multi_objective_multi_fidelity"]], "Multi-Objective Objectives": [[0, "module-botorch.acquisition.multi_objective.objective"]], "Multi-Objective Predictive Entropy Search Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.predictive_entropy_search"]], "Multi-Objective Synthetic Test Functions": [[12, "module-botorch.test_functions.multi_objective"]], "Multi-Objective Utilities": [[14, "multi-objective-utilities"]], "Multi-Objective Utilities for Acquisition Functions": [[0, "module-botorch.acquisition.multi_objective.utils"]], "Multi-Output Risk Measures": [[0, "module-botorch.acquisition.multi_objective.multi_output_risk_measures"]], "Multi-Step Lookahead Acquisition Functions": [[0, "module-botorch.acquisition.multi_step_lookahead"]], "Multi-Task Distribution Utils": [[14, "module-botorch.utils.multitask"]], "Multitask GP Models": [[7, "module-botorch.models.multitask"]], "Multitask GP Posterior": [[9, "module-botorch.posteriors.multitask"]], "Multivariate Gaussian Probabilities via Bivariate Conditioning": [[14, "module-botorch.utils.probability.mvnxpb"]], "Non-dominated Partitionings": [[14, "module-botorch.utils.multi_objective.box_decompositions.non_dominated"]], "Numpy - Torch Conversion Tools": [[8, "module-botorch.optim.utils.numpy_utils"]], "Objective": [[14, "module-botorch.utils.objective"]], "Objectives": [[0, "module-botorch.acquisition.objective"]], "Objectives and Cost-Aware Utilities": [[0, "objectives-and-cost-aware-utilities"]], "Optimization": [[8, "optimization"]], "Optimization with Timeouts": [[8, "module-botorch.optim.utils.timeout"]], "Other Utilties": [[7, "module-botorch.models.utils.assorted"]], "Outcome Transforms": [[7, "module-botorch.models.transforms.outcome"]], "Pairwise GP Models": [[7, "module-botorch.models.pairwise_gp"]], "Pairwise Monte-Carlo Samplers": [[10, "module-botorch.sampling.pairwise_samplers"]], "ParEGO: Multi-Objective Acquisition Function with Chebyshev Scalarization": [[0, "module-botorch.acquisition.multi_objective.parego"]], "Parameter Constraint Utilities": [[8, "module-botorch.optim.parameter_constraints"]], "Pareto": [[14, "module-botorch.utils.multi_objective.pareto"]], "Pathwise Posterior Samplers": [[10, "module-botorch.sampling.pathwise.posterior_samplers"]], "Pathwise Prior Samplers": [[10, "module-botorch.sampling.pathwise.prior_samplers"]], "Pathwise Sampling": [[10, "pathwise-sampling"]], "Pathwise Update Strategies": [[10, "module-botorch.sampling.pathwise.update_strategies"]], "Penalized Acquisition Function Wrapper": [[0, "module-botorch.acquisition.penalized"]], "Posterior APIs": [[9, "posterior-apis"]], "Posterior List API": [[9, "module-botorch.posteriors.posterior_list"]], "Posteriors": [[9, "posteriors"]], "Predictive Entropy Search Acquisition Functions": [[0, "module-botorch.acquisition.predictive_entropy_search"]], "Preference Acquisition Functions": [[0, "module-botorch.acquisition.preference"]], "Prior-Guided Acquisition Function Wrapper": [[0, "module-botorch.acquisition.prior_guided"]], "Probability Helpers": [[14, "module-botorch.utils.probability.utils"]], "Probability Utilities": [[14, "probability-utilities"]], "Proximal Acquisition Function Wrapper": [[0, "module-botorch.acquisition.proximal"]], "QMC Base Functionality": [[10, "module-botorch.sampling.qmc"]], "Risk Measures": [[0, "module-botorch.acquisition.risk_measures"]], "Rounding": [[14, "module-botorch.utils.rounding"]], "Safe Math": [[14, "module-botorch.utils.safe_math"]], "Sample Paths": [[10, "module-botorch.sampling.pathwise.paths"]], "Sampling": [[14, "module-botorch.utils.sampling"]], "Sampling Strategies": [[4, "module-botorch.generation.sampling"]], "Sampling from GP priors": [[14, "module-botorch.utils.gp_sampling"]], "Scalarization": [[14, "module-botorch.utils.multi_objective.scalarization"]], "Sensitivity Analysis Test Functions": [[12, "module-botorch.test_functions.sensitivity_analysis"]], "Stochastic Samplers": [[10, "module-botorch.sampling.stochastic_samplers"]], "Stopping Criteria": [[8, "module-botorch.optim.stopping"]], "Synthetic Test Functions": [[12, "module-botorch.test_functions.synthetic"]], "Test Helpers": [[14, "module-botorch.utils.test_helpers"]], "Testing": [[14, "module-botorch.utils.testing"]], "The One-Shot Knowledge Gradient": [[0, "module-botorch.acquisition.knowledge_gradient"]], "Thompson Sampling": [[0, "module-botorch.acquisition.thompson_sampling"]], "Torch": [[14, "module-botorch.utils.torch"]], "Torch Posterior": [[9, "module-botorch.posteriors.torch"]], "Transform Factory Methods": [[7, "module-botorch.models.transforms.factory"]], "Transform Utilities": [[7, "module-botorch.models.transforms.utils"]], "Transformations": [[14, "module-botorch.utils.transforms"]], "Transformed Posterior": [[9, "module-botorch.posteriors.transformed"]], "Transforms": [[7, "transforms"]], "Truncated Multivariate Normal Distribution": [[14, "module-botorch.utils.probability.truncated_multivariate_normal"]], "Types and Type Hints": [[14, "module-botorch.utils.types"]], "Unified Skew Normal Distribution": [[14, "module-botorch.utils.probability.unified_skew_normal"]], "Utilities": [[0, "utilities"], [4, "module-botorch.generation.utils"], [7, "utilities"], [8, "utilities"], [9, "utilities"], [10, "module-botorch.sampling.pathwise.utils"]], "Utilities For Test Functions": [[12, "module-botorch.test_functions.utils"]], "Variational GP Models": [[7, "module-botorch.models.approximate_gp"]], "Warnings": [[2, "module-botorch.exceptions.warnings"]], "botorch.acquisition": [[0, null]], "botorch.cross_validation": [[1, null]], "botorch.exceptions": [[2, null]], "botorch.fit": [[3, null]], "botorch.generation": [[4, null]], "botorch.logging": [[6, null]], "botorch.models": [[7, null]], "botorch.optim": [[8, null]], "botorch.posteriors": [[9, null]], "botorch.sampling": [[10, null]], "botorch.settings": [[11, null]], "botorch.test_functions": [[12, null]], "botorch.test_utils": [[13, null]], "botorch.utils": [[14, null]]}, "docnames": ["acquisition", "cross_validation", "exceptions", "fit", "generation", "index", "logging", "models", "optim", "posteriors", "sampling", "settings", "test_functions", "test_utils", "utils"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.todo": 2, "sphinx.ext.viewcode": 1}, "filenames": ["acquisition.rst", "cross_validation.rst", "exceptions.rst", "fit.rst", "generation.rst", "index.rst", "logging.rst", "models.rst", "optim.rst", "posteriors.rst", "sampling.rst", "settings.rst", "test_functions.rst", "test_utils.rst", "utils.rst"], "indexentries": {"_default_sample_shape (botorch.acquisition.acquisition.mcsamplermixin attribute)": [[0, "botorch.acquisition.acquisition.MCSamplerMixin._default_sample_shape", false]], "_default_sample_shape (botorch.acquisition.multi_objective.max_value_entropy_search.qmultiobjectivemaxvalueentropy attribute)": [[0, "botorch.acquisition.multi_objective.max_value_entropy_search.qMultiObjectiveMaxValueEntropy._default_sample_shape", false]], "_has_transformed_inputs (botorch.models.model.model attribute)": [[7, "botorch.models.model.Model._has_transformed_inputs", false]], "_is_ensemble (botorch.models.model.model attribute)": [[7, "botorch.models.model.Model._is_ensemble", false]], "_is_fully_bayesian (botorch.models.model.model attribute)": [[7, "botorch.models.model.Model._is_fully_bayesian", false]], "_original_train_inputs (botorch.models.model.model attribute)": [[7, "botorch.models.model.Model._original_train_inputs", false]], "_pivoted_cholesky_init() (in module botorch.models.utils.inducing_point_allocators)": [[7, "botorch.models.utils.inducing_point_allocators._pivoted_cholesky_init", false]], "ackley (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Ackley", false]], "acq_function (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.acq_function", false]], "acqf_input_constructor() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.acqf_input_constructor", false]], "acquisitionfunction (class in botorch.acquisition.acquisition)": [[0, "botorch.acquisition.acquisition.AcquisitionFunction", false]], "add() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.add", false]], "add_output_dim() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.add_output_dim", false]], "affinedeterministicmodel (class in botorch.models.deterministic)": [[7, "botorch.models.deterministic.AffineDeterministicModel", false]], "affinefidelitycostmodel (class in botorch.models.cost)": [[7, "botorch.models.cost.AffineFidelityCostModel", false]], "affineinputtransform (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.AffineInputTransform", false]], "allocate_inducing_points() (botorch.models.utils.inducing_point_allocators.inducingpointallocator method)": [[7, "botorch.models.utils.inducing_point_allocators.InducingPointAllocator.allocate_inducing_points", false]], "allow_only_specific_variable_kwargs() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.allow_only_specific_variable_kwargs", false]], "alpha (botorch.test_functions.multi_objective.dh1 attribute)": [[12, "botorch.test_functions.multi_objective.DH1.alpha", false]], "alpha_1 (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.alpha_1", false]], "alpha_2 (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.alpha_2", false]], "alpha_3 (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.alpha_3", false]], "analyticacquisitionfunction (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.AnalyticAcquisitionFunction", false]], "analyticexpectedutilityofbestoption (class in botorch.acquisition.preference)": [[0, "botorch.acquisition.preference.AnalyticExpectedUtilityOfBestOption", false]], "append() (botorch.utils.multi_objective.hypervolume.multilist method)": [[14, "botorch.utils.multi_objective.hypervolume.MultiList.append", false]], "appendfeatures (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.AppendFeatures", false]], "apply_constraints() (in module botorch.utils.objective)": [[14, "botorch.utils.objective.apply_constraints", false]], "apply_constraints_nonnegative_soft() (in module botorch.utils.objective)": [[14, "botorch.utils.objective.apply_constraints_nonnegative_soft", false]], "approximate_round() (in module botorch.utils.rounding)": [[14, "botorch.utils.rounding.approximate_round", false]], "approximategpytorchmodel (class in botorch.models.approximate_gp)": [[7, "botorch.models.approximate_gp.ApproximateGPyTorchModel", false]], "arg_constraints (botorch.utils.probability.unified_skew_normal.unifiedskewnormal attribute)": [[14, "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal.arg_constraints", false]], "as_ndarray() (in module botorch.optim.utils.numpy_utils)": [[8, "botorch.optim.utils.numpy_utils.as_ndarray", false]], "asdict() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.asdict", false]], "assertallclose() (botorch.utils.testing.botorchtestcase method)": [[14, "botorch.utils.testing.BotorchTestCase.assertAllClose", false]], "augment() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.augment", false]], "augment_cholesky() (in module botorch.utils.probability.linalg)": [[14, "botorch.utils.probability.linalg.augment_cholesky", false]], "augmentedbranin (class in botorch.test_functions.multi_fidelity)": [[12, "botorch.test_functions.multi_fidelity.AugmentedBranin", false]], "augmentedhartmann (class in botorch.test_functions.multi_fidelity)": [[12, "botorch.test_functions.multi_fidelity.AugmentedHartmann", false]], "augmentedrosenbrock (class in botorch.test_functions.multi_fidelity)": [[12, "botorch.test_functions.multi_fidelity.AugmentedRosenbrock", false]], "backward() (botorch.utils.rounding.identitystefunction static method)": [[14, "botorch.utils.rounding.IdentitySTEFunction.backward", false]], "badinitialcandidateswarning": [[2, "botorch.exceptions.warnings.BadInitialCandidatesWarning", false]], "base_sample_shape (botorch.posteriors.gpytorch.gpytorchposterior property)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.base_sample_shape", false]], "base_sample_shape (botorch.posteriors.higher_order.higherordergpposterior property)": [[9, "botorch.posteriors.higher_order.HigherOrderGPPosterior.base_sample_shape", false]], "base_sample_shape (botorch.posteriors.multitask.multitaskgpposterior property)": [[9, "botorch.posteriors.multitask.MultitaskGPPosterior.base_sample_shape", false]], "base_sample_shape (botorch.posteriors.posterior.posterior property)": [[9, "botorch.posteriors.posterior.Posterior.base_sample_shape", false]], "base_sample_shape (botorch.posteriors.transformed.transformedposterior property)": [[9, "botorch.posteriors.transformed.TransformedPosterior.base_sample_shape", false]], "base_sample_shape (botorch.utils.testing.mockposterior property)": [[14, "botorch.utils.testing.MockPosterior.base_sample_shape", false]], "baseline_y (botorch.acquisition.multi_objective.multi_output_risk_measures.mars property)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MARS.baseline_Y", false]], "basetestproblem (class in botorch.test_functions.base)": [[12, "botorch.test_functions.base.BaseTestProblem", false]], "basetestproblemtestcasemixin (class in botorch.utils.testing)": [[14, "botorch.utils.testing.BaseTestProblemTestCaseMixIn", false]], "batch_cross_validation() (in module botorch.cross_validation)": [[1, "botorch.cross_validation.batch_cross_validation", false]], "batch_initial_conditions (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.batch_initial_conditions", false]], "batch_range (botorch.posteriors.fully_bayesian.gaussianmixtureposterior property)": [[9, "botorch.posteriors.fully_bayesian.GaussianMixturePosterior.batch_range", false]], "batch_range (botorch.posteriors.gpytorch.gpytorchposterior property)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.batch_range", false]], "batch_range (botorch.posteriors.higher_order.higherordergpposterior property)": [[9, "botorch.posteriors.higher_order.HigherOrderGPPosterior.batch_range", false]], "batch_range (botorch.posteriors.multitask.multitaskgpposterior property)": [[9, "botorch.posteriors.multitask.MultitaskGPPosterior.batch_range", false]], "batch_range (botorch.posteriors.posterior.posterior property)": [[9, "botorch.posteriors.posterior.Posterior.batch_range", false]], "batch_range (botorch.posteriors.transformed.transformedposterior property)": [[9, "botorch.posteriors.transformed.TransformedPosterior.batch_range", false]], "batch_range (botorch.utils.testing.mockposterior property)": [[14, "botorch.utils.testing.MockPosterior.batch_range", false]], "batch_shape (botorch.models.approximate_gp.singletaskvariationalgp property)": [[7, "botorch.models.approximate_gp.SingleTaskVariationalGP.batch_shape", false]], "batch_shape (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp property)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.batch_shape", false]], "batch_shape (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp property)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.batch_shape", false]], "batch_shape (botorch.models.gpytorch.batchedmultioutputgpytorchmodel property)": [[7, "botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel.batch_shape", false]], "batch_shape (botorch.models.gpytorch.gpytorchmodel property)": [[7, "botorch.models.gpytorch.GPyTorchModel.batch_shape", false]], "batch_shape (botorch.models.gpytorch.modellistgpytorchmodel property)": [[7, "botorch.models.gpytorch.ModelListGPyTorchModel.batch_shape", false]], "batch_shape (botorch.models.model.model property)": [[7, "botorch.models.model.Model.batch_shape", false]], "batch_shape (botorch.models.model.modellist property)": [[7, "botorch.models.model.ModelList.batch_shape", false]], "batch_shape (botorch.models.pairwise_gp.pairwisegp property)": [[7, "botorch.models.pairwise_gp.PairwiseGP.batch_shape", false]], "batch_shape (botorch.models.transforms.input.inputperturbation property)": [[7, "botorch.models.transforms.input.InputPerturbation.batch_shape", false]], "batch_shape (botorch.sampling.pathwise.features.maps.featuremap attribute)": [[10, "botorch.sampling.pathwise.features.maps.FeatureMap.batch_shape", false]], "batch_shape (botorch.sampling.pathwise.features.maps.kernelevaluationmap property)": [[10, "botorch.sampling.pathwise.features.maps.KernelEvaluationMap.batch_shape", false]], "batch_shape (botorch.sampling.pathwise.features.maps.kernelfeaturemap property)": [[10, "botorch.sampling.pathwise.features.maps.KernelFeatureMap.batch_shape", false]], "batch_shape (botorch.utils.testing.mockmodel property)": [[14, "botorch.utils.testing.MockModel.batch_shape", false]], "batch_shape (botorch.utils.testing.mockposterior property)": [[14, "botorch.utils.testing.MockPosterior.batch_shape", false]], "batchbroadcastedinputtransform (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.BatchBroadcastedInputTransform", false]], "batched_bisect() (in module botorch.posteriors.fully_bayesian)": [[9, "botorch.posteriors.fully_bayesian.batched_bisect", false]], "batched_multi_output_to_single_output() (in module botorch.models.converter)": [[7, "botorch.models.converter.batched_multi_output_to_single_output", false]], "batched_multinomial() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.batched_multinomial", false]], "batched_to_model_list() (in module botorch.models.converter)": [[7, "botorch.models.converter.batched_to_model_list", false]], "batchedmultioutputgpytorchmodel (class in botorch.models.gpytorch)": [[7, "botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel", false]], "beale (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Beale", false]], "beta (botorch.test_functions.multi_objective.dh1 attribute)": [[12, "botorch.test_functions.multi_objective.DH1.beta", false]], "beta (botorch.test_functions.multi_objective.dh2 attribute)": [[12, "botorch.test_functions.multi_objective.DH2.beta", false]], "bilog (class in botorch.models.transforms.outcome)": [[7, "botorch.models.transforms.outcome.Bilog", false]], "block_matrix_concat() (in module botorch.utils.probability.linalg)": [[14, "botorch.utils.probability.linalg.block_matrix_concat", false]], "bnh (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.BNH", false]], "boltzmannsampling (class in botorch.generation.sampling)": [[4, "botorch.generation.sampling.BoltzmannSampling", false]], "botorch.acquisition": [[0, "module-botorch.acquisition", false]], "botorch.acquisition.acquisition": [[0, "module-botorch.acquisition.acquisition", false]], "botorch.acquisition.active_learning": [[0, "module-botorch.acquisition.active_learning", false]], "botorch.acquisition.analytic": [[0, "module-botorch.acquisition.analytic", false]], "botorch.acquisition.bayesian_active_learning": [[0, "module-botorch.acquisition.bayesian_active_learning", false]], "botorch.acquisition.cached_cholesky": [[0, "module-botorch.acquisition.cached_cholesky", false]], "botorch.acquisition.cost_aware": [[0, "module-botorch.acquisition.cost_aware", false]], "botorch.acquisition.decoupled": [[0, "module-botorch.acquisition.decoupled", false]], "botorch.acquisition.factory": [[0, "module-botorch.acquisition.factory", false]], "botorch.acquisition.fixed_feature": [[0, "module-botorch.acquisition.fixed_feature", false]], "botorch.acquisition.input_constructors": [[0, "module-botorch.acquisition.input_constructors", false]], "botorch.acquisition.joint_entropy_search": [[0, "module-botorch.acquisition.joint_entropy_search", false]], "botorch.acquisition.knowledge_gradient": [[0, "module-botorch.acquisition.knowledge_gradient", false]], "botorch.acquisition.logei": [[0, "module-botorch.acquisition.logei", false]], "botorch.acquisition.max_value_entropy_search": [[0, "module-botorch.acquisition.max_value_entropy_search", false]], "botorch.acquisition.monte_carlo": [[0, "module-botorch.acquisition.monte_carlo", false]], "botorch.acquisition.multi_objective.analytic": [[0, "module-botorch.acquisition.multi_objective.analytic", false]], "botorch.acquisition.multi_objective.base": [[0, "module-botorch.acquisition.multi_objective.base", false]], "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient": [[0, "module-botorch.acquisition.multi_objective.hypervolume_knowledge_gradient", false]], "botorch.acquisition.multi_objective.joint_entropy_search": [[0, "module-botorch.acquisition.multi_objective.joint_entropy_search", false]], "botorch.acquisition.multi_objective.logei": [[0, "module-botorch.acquisition.multi_objective.logei", false]], "botorch.acquisition.multi_objective.max_value_entropy_search": [[0, "module-botorch.acquisition.multi_objective.max_value_entropy_search", false]], "botorch.acquisition.multi_objective.monte_carlo": [[0, "module-botorch.acquisition.multi_objective.monte_carlo", false]], "botorch.acquisition.multi_objective.multi_fidelity": [[0, "module-botorch.acquisition.multi_objective.multi_fidelity", false]], "botorch.acquisition.multi_objective.multi_output_risk_measures": [[0, "module-botorch.acquisition.multi_objective.multi_output_risk_measures", false]], "botorch.acquisition.multi_objective.objective": [[0, "module-botorch.acquisition.multi_objective.objective", false]], "botorch.acquisition.multi_objective.parego": [[0, "module-botorch.acquisition.multi_objective.parego", false]], "botorch.acquisition.multi_objective.predictive_entropy_search": [[0, "module-botorch.acquisition.multi_objective.predictive_entropy_search", false]], "botorch.acquisition.multi_objective.utils": [[0, "module-botorch.acquisition.multi_objective.utils", false]], "botorch.acquisition.multi_step_lookahead": [[0, "module-botorch.acquisition.multi_step_lookahead", false]], "botorch.acquisition.objective": [[0, "module-botorch.acquisition.objective", false]], "botorch.acquisition.penalized": [[0, "module-botorch.acquisition.penalized", false]], "botorch.acquisition.predictive_entropy_search": [[0, "module-botorch.acquisition.predictive_entropy_search", false]], "botorch.acquisition.preference": [[0, "module-botorch.acquisition.preference", false]], "botorch.acquisition.prior_guided": [[0, "module-botorch.acquisition.prior_guided", false]], "botorch.acquisition.proximal": [[0, "module-botorch.acquisition.proximal", false]], "botorch.acquisition.risk_measures": [[0, "module-botorch.acquisition.risk_measures", false]], "botorch.acquisition.thompson_sampling": [[0, "module-botorch.acquisition.thompson_sampling", false]], "botorch.acquisition.utils": [[0, "module-botorch.acquisition.utils", false]], "botorch.cross_validation": [[1, "module-botorch.cross_validation", false]], "botorch.exceptions": [[2, "module-botorch.exceptions", false]], "botorch.exceptions.errors": [[2, "module-botorch.exceptions.errors", false]], "botorch.exceptions.warnings": [[2, "module-botorch.exceptions.warnings", false]], "botorch.fit": [[3, "module-botorch.fit", false]], "botorch.generation": [[4, "module-botorch.generation", false]], "botorch.generation.gen": [[4, "module-botorch.generation.gen", false]], "botorch.generation.sampling": [[4, "module-botorch.generation.sampling", false]], "botorch.generation.utils": [[4, "module-botorch.generation.utils", false]], "botorch.logging": [[6, "module-botorch.logging", false]], "botorch.models": [[7, "module-botorch.models", false]], "botorch.models.approximate_gp": [[7, "module-botorch.models.approximate_gp", false]], "botorch.models.contextual": [[7, "module-botorch.models.contextual", false]], "botorch.models.contextual_multioutput": [[7, "module-botorch.models.contextual_multioutput", false]], "botorch.models.converter": [[7, "module-botorch.models.converter", false]], "botorch.models.cost": [[7, "module-botorch.models.cost", false]], "botorch.models.deterministic": [[7, "module-botorch.models.deterministic", false]], "botorch.models.ensemble": [[7, "module-botorch.models.ensemble", false]], "botorch.models.fully_bayesian": [[7, "module-botorch.models.fully_bayesian", false]], "botorch.models.fully_bayesian_multitask": [[7, "module-botorch.models.fully_bayesian_multitask", false]], "botorch.models.gp_regression": [[7, "module-botorch.models.gp_regression", false]], "botorch.models.gp_regression_fidelity": [[7, "module-botorch.models.gp_regression_fidelity", false]], "botorch.models.gp_regression_mixed": [[7, "module-botorch.models.gp_regression_mixed", false]], "botorch.models.gpytorch": [[7, "module-botorch.models.gpytorch", false]], "botorch.models.higher_order_gp": [[7, "module-botorch.models.higher_order_gp", false]], "botorch.models.kernels.categorical": [[7, "module-botorch.models.kernels.categorical", false]], "botorch.models.kernels.contextual_lcea": [[7, "module-botorch.models.kernels.contextual_lcea", false]], "botorch.models.kernels.contextual_sac": [[7, "module-botorch.models.kernels.contextual_sac", false]], "botorch.models.kernels.downsampling": [[7, "module-botorch.models.kernels.downsampling", false]], "botorch.models.kernels.exponential_decay": [[7, "module-botorch.models.kernels.exponential_decay", false]], "botorch.models.kernels.infinite_width_bnn": [[7, "module-botorch.models.kernels.infinite_width_bnn", false]], "botorch.models.kernels.linear_truncated_fidelity": [[7, "module-botorch.models.kernels.linear_truncated_fidelity", false]], "botorch.models.kernels.orthogonal_additive_kernel": [[7, "module-botorch.models.kernels.orthogonal_additive_kernel", false]], "botorch.models.likelihoods.pairwise": [[7, "module-botorch.models.likelihoods.pairwise", false]], "botorch.models.model": [[7, "module-botorch.models.model", false]], "botorch.models.model_list_gp_regression": [[7, "module-botorch.models.model_list_gp_regression", false]], "botorch.models.multitask": [[7, "module-botorch.models.multitask", false]], "botorch.models.pairwise_gp": [[7, "module-botorch.models.pairwise_gp", false]], "botorch.models.transforms.factory": [[7, "module-botorch.models.transforms.factory", false]], "botorch.models.transforms.input": [[7, "module-botorch.models.transforms.input", false]], "botorch.models.transforms.outcome": [[7, "module-botorch.models.transforms.outcome", false]], "botorch.models.transforms.utils": [[7, "module-botorch.models.transforms.utils", false]], "botorch.models.utils.assorted": [[7, "module-botorch.models.utils.assorted", false]], "botorch.models.utils.gpytorch_modules": [[7, "module-botorch.models.utils.gpytorch_modules", false]], "botorch.models.utils.inducing_point_allocators": [[7, "module-botorch.models.utils.inducing_point_allocators", false]], "botorch.optim": [[8, "module-botorch.optim", false]], "botorch.optim.closures.core": [[8, "module-botorch.optim.closures.core", false]], "botorch.optim.closures.model_closures": [[8, "module-botorch.optim.closures.model_closures", false]], "botorch.optim.core": [[8, "module-botorch.optim.core", false]], "botorch.optim.fit": [[8, "module-botorch.optim.fit", false]], "botorch.optim.homotopy": [[8, "module-botorch.optim.homotopy", false]], "botorch.optim.initializers": [[8, "module-botorch.optim.initializers", false]], "botorch.optim.optimize": [[8, "module-botorch.optim.optimize", false]], "botorch.optim.optimize_homotopy": [[8, "module-botorch.optim.optimize_homotopy", false]], "botorch.optim.optimize_mixed": [[8, "module-botorch.optim.optimize_mixed", false]], "botorch.optim.parameter_constraints": [[8, "module-botorch.optim.parameter_constraints", false]], "botorch.optim.stopping": [[8, "module-botorch.optim.stopping", false]], "botorch.optim.utils.acquisition_utils": [[8, "module-botorch.optim.utils.acquisition_utils", false]], "botorch.optim.utils.common": [[8, "module-botorch.optim.utils.common", false]], "botorch.optim.utils.model_utils": [[8, "module-botorch.optim.utils.model_utils", false]], "botorch.optim.utils.numpy_utils": [[8, "module-botorch.optim.utils.numpy_utils", false]], "botorch.optim.utils.timeout": [[8, "module-botorch.optim.utils.timeout", false]], "botorch.posteriors": [[9, "module-botorch.posteriors", false]], "botorch.posteriors.base_samples": [[9, "module-botorch.posteriors.base_samples", false]], "botorch.posteriors.ensemble": [[9, "module-botorch.posteriors.ensemble", false]], "botorch.posteriors.fully_bayesian": [[9, "module-botorch.posteriors.fully_bayesian", false]], "botorch.posteriors.gpytorch": [[9, "module-botorch.posteriors.gpytorch", false]], "botorch.posteriors.higher_order": [[9, "module-botorch.posteriors.higher_order", false]], "botorch.posteriors.multitask": [[9, "module-botorch.posteriors.multitask", false]], "botorch.posteriors.posterior": [[9, "module-botorch.posteriors.posterior", false]], "botorch.posteriors.posterior_list": [[9, "module-botorch.posteriors.posterior_list", false]], "botorch.posteriors.torch": [[9, "module-botorch.posteriors.torch", false]], "botorch.posteriors.transformed": [[9, "module-botorch.posteriors.transformed", false]], "botorch.sampling": [[10, "module-botorch.sampling", false]], "botorch.sampling.base": [[10, "module-botorch.sampling.base", false]], "botorch.sampling.get_sampler": [[10, "module-botorch.sampling.get_sampler", false]], "botorch.sampling.index_sampler": [[10, "module-botorch.sampling.index_sampler", false]], "botorch.sampling.list_sampler": [[10, "module-botorch.sampling.list_sampler", false]], "botorch.sampling.normal": [[10, "module-botorch.sampling.normal", false]], "botorch.sampling.pairwise_samplers": [[10, "module-botorch.sampling.pairwise_samplers", false]], "botorch.sampling.pathwise.features.generators": [[10, "module-botorch.sampling.pathwise.features.generators", false]], "botorch.sampling.pathwise.features.maps": [[10, "module-botorch.sampling.pathwise.features.maps", false]], "botorch.sampling.pathwise.paths": [[10, "module-botorch.sampling.pathwise.paths", false]], "botorch.sampling.pathwise.posterior_samplers": [[10, "module-botorch.sampling.pathwise.posterior_samplers", false]], "botorch.sampling.pathwise.prior_samplers": [[10, "module-botorch.sampling.pathwise.prior_samplers", false]], "botorch.sampling.pathwise.update_strategies": [[10, "module-botorch.sampling.pathwise.update_strategies", false]], "botorch.sampling.pathwise.utils": [[10, "module-botorch.sampling.pathwise.utils", false]], "botorch.sampling.qmc": [[10, "module-botorch.sampling.qmc", false]], "botorch.sampling.stochastic_samplers": [[10, "module-botorch.sampling.stochastic_samplers", false]], "botorch.settings": [[11, "module-botorch.settings", false]], "botorch.test_functions": [[12, "module-botorch.test_functions", false]], "botorch.test_functions.base": [[12, "module-botorch.test_functions.base", false]], "botorch.test_functions.multi_fidelity": [[12, "module-botorch.test_functions.multi_fidelity", false]], "botorch.test_functions.multi_objective": [[12, "module-botorch.test_functions.multi_objective", false]], "botorch.test_functions.multi_objective_multi_fidelity": [[12, "module-botorch.test_functions.multi_objective_multi_fidelity", false]], "botorch.test_functions.sensitivity_analysis": [[12, "module-botorch.test_functions.sensitivity_analysis", false]], "botorch.test_functions.synthetic": [[12, "module-botorch.test_functions.synthetic", false]], "botorch.test_functions.utils": [[12, "module-botorch.test_functions.utils", false]], "botorch.test_utils": [[13, "module-botorch.test_utils", false]], "botorch.test_utils.mock": [[13, "module-botorch.test_utils.mock", false]], "botorch.utils": [[14, "module-botorch.utils", false]], "botorch.utils.constants": [[14, "module-botorch.utils.constants", false]], "botorch.utils.constraints": [[14, "module-botorch.utils.constraints", false]], "botorch.utils.containers": [[14, "module-botorch.utils.containers", false]], "botorch.utils.context_managers": [[14, "module-botorch.utils.context_managers", false]], "botorch.utils.datasets": [[14, "module-botorch.utils.datasets", false]], "botorch.utils.dispatcher": [[14, "module-botorch.utils.dispatcher", false]], "botorch.utils.feasible_volume": [[14, "module-botorch.utils.feasible_volume", false]], "botorch.utils.gp_sampling": [[14, "module-botorch.utils.gp_sampling", false]], "botorch.utils.low_rank": [[14, "module-botorch.utils.low_rank", false]], "botorch.utils.multi_objective.box_decompositions.box_decomposition": [[14, "module-botorch.utils.multi_objective.box_decompositions.box_decomposition", false]], "botorch.utils.multi_objective.box_decompositions.box_decomposition_list": [[14, "module-botorch.utils.multi_objective.box_decompositions.box_decomposition_list", false]], "botorch.utils.multi_objective.box_decompositions.dominated": [[14, "module-botorch.utils.multi_objective.box_decompositions.dominated", false]], "botorch.utils.multi_objective.box_decompositions.non_dominated": [[14, "module-botorch.utils.multi_objective.box_decompositions.non_dominated", false]], "botorch.utils.multi_objective.box_decompositions.utils": [[14, "module-botorch.utils.multi_objective.box_decompositions.utils", false]], "botorch.utils.multi_objective.hypervolume": [[14, "module-botorch.utils.multi_objective.hypervolume", false]], "botorch.utils.multi_objective.pareto": [[14, "module-botorch.utils.multi_objective.pareto", false]], "botorch.utils.multi_objective.scalarization": [[14, "module-botorch.utils.multi_objective.scalarization", false]], "botorch.utils.multitask": [[14, "module-botorch.utils.multitask", false]], "botorch.utils.objective": [[14, "module-botorch.utils.objective", false]], "botorch.utils.probability.bvn": [[14, "module-botorch.utils.probability.bvn", false]], "botorch.utils.probability.lin_ess": [[14, "module-botorch.utils.probability.lin_ess", false]], "botorch.utils.probability.linalg": [[14, "module-botorch.utils.probability.linalg", false]], "botorch.utils.probability.mvnxpb": [[14, "module-botorch.utils.probability.mvnxpb", false]], "botorch.utils.probability.truncated_multivariate_normal": [[14, "module-botorch.utils.probability.truncated_multivariate_normal", false]], "botorch.utils.probability.unified_skew_normal": [[14, "module-botorch.utils.probability.unified_skew_normal", false]], "botorch.utils.probability.utils": [[14, "module-botorch.utils.probability.utils", false]], "botorch.utils.rounding": [[14, "module-botorch.utils.rounding", false]], "botorch.utils.safe_math": [[14, "module-botorch.utils.safe_math", false]], "botorch.utils.sampling": [[14, "module-botorch.utils.sampling", false]], "botorch.utils.test_helpers": [[14, "module-botorch.utils.test_helpers", false]], "botorch.utils.testing": [[14, "module-botorch.utils.testing", false]], "botorch.utils.torch": [[14, "module-botorch.utils.torch", false]], "botorch.utils.transforms": [[14, "module-botorch.utils.transforms", false]], "botorch.utils.types": [[14, "module-botorch.utils.types", false]], "botorchcontainer (class in botorch.utils.containers)": [[14, "botorch.utils.containers.BotorchContainer", false]], "botorcherror": [[2, "botorch.exceptions.errors.BotorchError", false]], "botorchtensordimensionerror": [[2, "botorch.exceptions.errors.BotorchTensorDimensionError", false]], "botorchtensordimensionwarning": [[2, "botorch.exceptions.warnings.BotorchTensorDimensionWarning", false]], "botorchtestcase (class in botorch.utils.testing)": [[14, "botorch.utils.testing.BotorchTestCase", false]], "botorchwarning": [[2, "botorch.exceptions.warnings.BotorchWarning", false]], "bounds (botorch.models.transforms.input.normalize property)": [[7, "botorch.models.transforms.input.Normalize.bounds", false]], "bounds (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.bounds", false]], "bounds (botorch.utils.probability.mvnxpb.mvnxpbstate attribute)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState.bounds", false]], "boxdecomposition (class in botorch.utils.multi_objective.box_decompositions.box_decomposition)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition", false]], "boxdecompositionlist (class in botorch.utils.multi_objective.box_decompositions.box_decomposition_list)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList", false]], "branin (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Branin", false]], "branincurrin (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.BraninCurrin", false]], "bufferdict (class in botorch.utils.torch)": [[14, "botorch.utils.torch.BufferDict", false]], "build() (botorch.utils.probability.mvnxpb.mvnxpb class method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.build", false]], "build_positional_indices() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.build_positional_indices", false]], "bukin (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Bukin", false]], "bvn() (in module botorch.utils.probability.bvn)": [[14, "botorch.utils.probability.bvn.bvn", false]], "bvnmom() (in module botorch.utils.probability.bvn)": [[14, "botorch.utils.probability.bvn.bvnmom", false]], "bvnu() (in module botorch.utils.probability.bvn)": [[14, "botorch.utils.probability.bvn.bvnu", false]], "c2dtlz2 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.C2DTLZ2", false]], "cachedcholeskymcsamplermixin (class in botorch.acquisition.cached_cholesky)": [[0, "botorch.acquisition.cached_cholesky.CachedCholeskyMCSamplerMixin", false]], "candidategenerationerror": [[2, "botorch.exceptions.errors.CandidateGenerationError", false]], "carsideimpact (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.CarSideImpact", false]], "case_dispatcher() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.case_dispatcher", false]], "categoricalkernel (class in botorch.models.kernels.categorical)": [[7, "botorch.models.kernels.categorical.CategoricalKernel", false]], "cauchy() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.cauchy", false]], "chainedinputtransform (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.ChainedInputTransform", false]], "chainedoutcometransform (class in botorch.models.transforms.outcome)": [[7, "botorch.models.transforms.outcome.ChainedOutcomeTransform", false]], "chainedtransform (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.ChainedTransform", false]], "chebyshev_objective (botorch.acquisition.multi_objective.multi_output_risk_measures.mars property)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MARS.chebyshev_objective", false]], "chebyshev_weights (botorch.acquisition.multi_objective.multi_output_risk_measures.mars property)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MARS.chebyshev_weights", false]], "check_dtype_float32_or_float64() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.check_dtype_float32_or_float64", false]], "check_min_max_scaling() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.check_min_max_scaling", false]], "check_negative_info_gain() (in module botorch.acquisition.bayesian_active_learning)": [[0, "botorch.acquisition.bayesian_active_learning.check_negative_info_gain", false]], "check_no_nans() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.check_no_nans", false]], "check_standardization() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.check_standardization", false]], "check_tau() (in module botorch.acquisition.logei)": [[0, "botorch.acquisition.logei.check_tau", false]], "clear() (botorch.utils.torch.bufferdict method)": [[14, "botorch.utils.torch.BufferDict.clear", false]], "clone() (botorch.utils.containers.densecontainer method)": [[14, "botorch.utils.containers.DenseContainer.clone", false]], "clone() (botorch.utils.containers.slicecontainer method)": [[14, "botorch.utils.containers.SliceContainer.clone", false]], "clone() (botorch.utils.datasets.contextualdataset method)": [[14, "botorch.utils.datasets.ContextualDataset.clone", false]], "clone() (botorch.utils.datasets.multitaskdataset method)": [[14, "botorch.utils.datasets.MultiTaskDataset.clone", false]], "clone() (botorch.utils.datasets.superviseddataset method)": [[14, "botorch.utils.datasets.SupervisedDataset.clone", false]], "clone() (botorch.utils.probability.linalg.pivotedcholesky method)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.clone", false]], "clone() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.clone", false]], "coefficient (botorch.models.transforms.input.affineinputtransform property)": [[7, "botorch.models.transforms.input.AffineInputTransform.coefficient", false]], "columnwise_clamp() (in module botorch.optim.utils.acquisition_utils)": [[8, "botorch.optim.utils.acquisition_utils.columnwise_clamp", false]], "comparisons (botorch.models.pairwise_gp.pairwisegp property)": [[7, "botorch.models.pairwise_gp.PairwiseGP.comparisons", false]], "complement_indices() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.complement_indices", false]], "complement_indices_like() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.complement_indices_like", false]], "compute() (botorch.utils.multi_objective.hypervolume.hypervolume method)": [[14, "botorch.utils.multi_objective.hypervolume.Hypervolume.compute", false]], "compute_best_f() (botorch.acquisition.logei.qlognoisyexpectedimprovement method)": [[0, "botorch.acquisition.logei.qLogNoisyExpectedImprovement.compute_best_f", false]], "compute_best_f() (botorch.acquisition.monte_carlo.qnoisyexpectedimprovement method)": [[0, "botorch.acquisition.monte_carlo.qNoisyExpectedImprovement.compute_best_f", false]], "compute_best_feasible_objective() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.compute_best_feasible_objective", false]], "compute_dgsm() (botorch.test_functions.sensitivity_analysis.ishigami method)": [[12, "botorch.test_functions.sensitivity_analysis.Ishigami.compute_dgsm", false]], "compute_dists() (in module botorch.models.fully_bayesian)": [[7, "botorch.models.fully_bayesian.compute_dists", false]], "compute_dominated_hypercell_bounds_2d() (in module botorch.utils.multi_objective.box_decompositions.utils)": [[14, "botorch.utils.multi_objective.box_decompositions.utils.compute_dominated_hypercell_bounds_2d", false]], "compute_feasibility_indicator() (in module botorch.utils.objective)": [[14, "botorch.utils.objective.compute_feasibility_indicator", false]], "compute_hypervolume() (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.compute_hypervolume", false]], "compute_hypervolume() (botorch.utils.multi_objective.box_decompositions.box_decomposition_list.boxdecompositionlist method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList.compute_hypervolume", false]], "compute_local_upper_bounds() (in module botorch.utils.multi_objective.box_decompositions.utils)": [[14, "botorch.utils.multi_objective.box_decompositions.utils.compute_local_upper_bounds", false]], "compute_log_prob_feas_from_bounds() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.compute_log_prob_feas_from_bounds", false]], "compute_non_dominated_hypercell_bounds_2d() (in module botorch.utils.multi_objective.box_decompositions.utils)": [[14, "botorch.utils.multi_objective.box_decompositions.utils.compute_non_dominated_hypercell_bounds_2d", false]], "compute_q_subset_indices() (botorch.utils.multi_objective.hypervolume.subsetindexcachingmixin method)": [[14, "botorch.utils.multi_objective.hypervolume.SubsetIndexCachingMixin.compute_q_subset_indices", false]], "compute_sample_box_decomposition() (in module botorch.acquisition.multi_objective.utils)": [[0, "botorch.acquisition.multi_objective.utils.compute_sample_box_decomposition", false]], "compute_smoothed_feasibility_indicator() (in module botorch.utils.objective)": [[14, "botorch.utils.objective.compute_smoothed_feasibility_indicator", false]], "compute_subset_indices() (in module botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.compute_subset_indices", false]], "concat() (botorch.utils.probability.linalg.pivotedcholesky method)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.concat", false]], "concat() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.concat", false]], "concatenate_pending_points() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.concatenate_pending_points", false]], "condition_on_observations() (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp method)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.condition_on_observations", false]], "condition_on_observations() (botorch.models.gpytorch.batchedmultioutputgpytorchmodel method)": [[7, "botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel.condition_on_observations", false]], "condition_on_observations() (botorch.models.gpytorch.gpytorchmodel method)": [[7, "botorch.models.gpytorch.GPyTorchModel.condition_on_observations", false]], "condition_on_observations() (botorch.models.gpytorch.modellistgpytorchmodel method)": [[7, "botorch.models.gpytorch.ModelListGPyTorchModel.condition_on_observations", false]], "condition_on_observations() (botorch.models.higher_order_gp.higherordergp method)": [[7, "botorch.models.higher_order_gp.HigherOrderGP.condition_on_observations", false]], "condition_on_observations() (botorch.models.model.fantasizemixin method)": [[7, "botorch.models.model.FantasizeMixin.condition_on_observations", false]], "condition_on_observations() (botorch.models.model.model method)": [[7, "botorch.models.model.Model.condition_on_observations", false]], "condition_on_observations() (botorch.models.model_list_gp_regression.modellistgp method)": [[7, "botorch.models.model_list_gp_regression.ModelListGP.condition_on_observations", false]], "condition_on_observations() (botorch.models.pairwise_gp.pairwisegp method)": [[7, "botorch.models.pairwise_gp.PairwiseGP.condition_on_observations", false]], "consolidate_duplicates() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.consolidate_duplicates", false]], "constr (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.CONSTR", false]], "constrainedbasetestproblem (class in botorch.test_functions.base)": [[12, "botorch.test_functions.base.ConstrainedBaseTestProblem", false]], "constrainedbranincurrin (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.ConstrainedBraninCurrin", false]], "constrainedexpectedimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.ConstrainedExpectedImprovement", false]], "constrainedgramacy (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.ConstrainedGramacy", false]], "constrainedhartmann (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.ConstrainedHartmann", false]], "constrainedhartmannsmooth (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.ConstrainedHartmannSmooth", false]], "constrainedmaxposteriorsampling (class in botorch.generation.sampling)": [[4, "botorch.generation.sampling.ConstrainedMaxPosteriorSampling", false]], "constrainedmcobjective (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.ConstrainedMCObjective", false]], "constrainedsynthetictestfunction (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.ConstrainedSyntheticTestFunction", false]], "constrainedtestproblemtestcasemixin (class in botorch.utils.testing)": [[14, "botorch.utils.testing.ConstrainedTestProblemTestCaseMixin", false]], "constraint_noise_std (botorch.test_functions.base.constrainedbasetestproblem attribute)": [[12, "botorch.test_functions.base.ConstrainedBaseTestProblem.constraint_noise_std", false]], "construct_evaluation_mask() (botorch.acquisition.decoupled.decoupledacquisitionfunction method)": [[0, "botorch.acquisition.decoupled.DecoupledAcquisitionFunction.construct_evaluation_mask", false]], "construct_inputs() (botorch.models.contextual.lceagp class method)": [[7, "botorch.models.contextual.LCEAGP.construct_inputs", false]], "construct_inputs() (botorch.models.contextual.sacgp class method)": [[7, "botorch.models.contextual.SACGP.construct_inputs", false]], "construct_inputs() (botorch.models.contextual_multioutput.lcemgp class method)": [[7, "botorch.models.contextual_multioutput.LCEMGP.construct_inputs", false]], "construct_inputs() (botorch.models.gp_regression.singletaskgp class method)": [[7, "botorch.models.gp_regression.SingleTaskGP.construct_inputs", false]], "construct_inputs() (botorch.models.gp_regression_fidelity.singletaskmultifidelitygp class method)": [[7, "botorch.models.gp_regression_fidelity.SingleTaskMultiFidelityGP.construct_inputs", false]], "construct_inputs() (botorch.models.gp_regression_mixed.mixedsingletaskgp class method)": [[7, "botorch.models.gp_regression_mixed.MixedSingleTaskGP.construct_inputs", false]], "construct_inputs() (botorch.models.model.model class method)": [[7, "botorch.models.model.Model.construct_inputs", false]], "construct_inputs() (botorch.models.multitask.multitaskgp class method)": [[7, "botorch.models.multitask.MultiTaskGP.construct_inputs", false]], "construct_inputs() (botorch.models.pairwise_gp.pairwisegp class method)": [[7, "botorch.models.pairwise_gp.PairwiseGP.construct_inputs", false]], "construct_inputs_analytic_eubo() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_analytic_eubo", false]], "construct_inputs_bald() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_BALD", false]], "construct_inputs_best_f() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_best_f", false]], "construct_inputs_ehvi() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_EHVI", false]], "construct_inputs_mf_base() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_mf_base", false]], "construct_inputs_nipv() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_NIPV", false]], "construct_inputs_noisy_ei() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_noisy_ei", false]], "construct_inputs_posterior_mean() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_posterior_mean", false]], "construct_inputs_qehvi() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qEHVI", false]], "construct_inputs_qei() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qEI", false]], "construct_inputs_qeubo() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qeubo", false]], "construct_inputs_qhvkg() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qHVKG", false]], "construct_inputs_qjes() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qJES", false]], "construct_inputs_qkg() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qKG", false]], "construct_inputs_qlogei() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qLogEI", false]], "construct_inputs_qlognehvi() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qLogNEHVI", false]], "construct_inputs_qlognei() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qLogNEI", false]], "construct_inputs_qlognparego() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qLogNParEGO", false]], "construct_inputs_qmes() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qMES", false]], "construct_inputs_qmfhvkg() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qMFHVKG", false]], "construct_inputs_qmfkg() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qMFKG", false]], "construct_inputs_qmfmes() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qMFMES", false]], "construct_inputs_qnehvi() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qNEHVI", false]], "construct_inputs_qnei() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qNEI", false]], "construct_inputs_qpi() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qPI", false]], "construct_inputs_qsimpleregret() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qSimpleRegret", false]], "construct_inputs_qucb() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_qUCB", false]], "construct_inputs_ucb() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.construct_inputs_ucb", false]], "contextualdataset (class in botorch.utils.datasets)": [[14, "botorch.utils.datasets.ContextualDataset", false]], "continuous_step() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.continuous_step", false]], "convert_to_target_pre_hook() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.convert_to_target_pre_hook", false]], "cosine8 (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Cosine8", false]], "cost_sampler (botorch.acquisition.knowledge_gradient.qmultifidelityknowledgegradient property)": [[0, "botorch.acquisition.knowledge_gradient.qMultiFidelityKnowledgeGradient.cost_sampler", false]], "cost_sampler (botorch.acquisition.max_value_entropy_search.qmultifidelitymaxvalueentropy property)": [[0, "botorch.acquisition.max_value_entropy_search.qMultiFidelityMaxValueEntropy.cost_sampler", false]], "cost_sampler (botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qhypervolumeknowledgegradient property)": [[0, "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qHypervolumeKnowledgeGradient.cost_sampler", false]], "costawareutility (class in botorch.acquisition.cost_aware)": [[0, "botorch.acquisition.cost_aware.CostAwareUtility", false]], "costawarewarning": [[2, "botorch.exceptions.warnings.CostAwareWarning", false]], "covariance_matrix (botorch.utils.probability.unified_skew_normal.unifiedskewnormal property)": [[14, "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal.covariance_matrix", false]], "cvar (class in botorch.acquisition.risk_measures)": [[0, "botorch.acquisition.risk_measures.CVaR", false]], "cvfolds (class in botorch.cross_validation)": [[1, "botorch.cross_validation.CVFolds", false]], "cvresults (class in botorch.cross_validation)": [[1, "botorch.cross_validation.CVResults", false]], "datapoints (botorch.models.pairwise_gp.pairwisegp property)": [[7, "botorch.models.pairwise_gp.PairwiseGP.datapoints", false]], "decoupledacquisitionfunction (class in botorch.acquisition.decoupled)": [[0, "botorch.acquisition.decoupled.DecoupledAcquisitionFunction", false]], "default (class in botorch.utils.types)": [[14, "botorch.utils.types.DEFAULT", false]], "delattr_ctx() (in module botorch.utils.context_managers)": [[14, "botorch.utils.context_managers.delattr_ctx", false]], "delaunaypolytopesampler (class in botorch.utils.sampling)": [[14, "botorch.utils.sampling.DelaunayPolytopeSampler", false]], "densecontainer (class in botorch.utils.containers)": [[14, "botorch.utils.containers.DenseContainer", false]], "density() (botorch.posteriors.gpytorch.gpytorchposterior method)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.density", false]], "density() (botorch.posteriors.posterior.posterior method)": [[9, "botorch.posteriors.posterior.Posterior.density", false]], "density() (botorch.posteriors.torch.torchposterior method)": [[9, "botorch.posteriors.torch.TorchPosterior.density", false]], "deprecationerror": [[2, "botorch.exceptions.errors.DeprecationError", false]], "detach() (botorch.utils.probability.linalg.pivotedcholesky method)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.detach", false]], "detach() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.detach", false]], "detect_duplicates() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.detect_duplicates", false]], "deterministicmodel (class in botorch.models.deterministic)": [[7, "botorch.models.deterministic.DeterministicModel", false]], "device (botorch.optim.utils.model_utils.torchattr attribute)": [[8, "botorch.optim.utils.model_utils.TorchAttr.device", false]], "device (botorch.posteriors.ensemble.ensembleposterior property)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.device", false]], "device (botorch.posteriors.posterior.posterior property)": [[9, "botorch.posteriors.posterior.Posterior.device", false]], "device (botorch.posteriors.posterior_list.posteriorlist property)": [[9, "botorch.posteriors.posterior_list.PosteriorList.device", false]], "device (botorch.posteriors.torch.torchposterior property)": [[9, "botorch.posteriors.torch.TorchPosterior.device", false]], "device (botorch.posteriors.transformed.transformedposterior property)": [[9, "botorch.posteriors.transformed.TransformedPosterior.device", false]], "device (botorch.utils.containers.botorchcontainer property)": [[14, "botorch.utils.containers.BotorchContainer.device", false]], "device (botorch.utils.containers.densecontainer property)": [[14, "botorch.utils.containers.DenseContainer.device", false]], "device (botorch.utils.containers.slicecontainer property)": [[14, "botorch.utils.containers.SliceContainer.device", false]], "device (botorch.utils.context_managers.tensorcheckpoint attribute)": [[14, "botorch.utils.context_managers.TensorCheckpoint.device", false]], "device (botorch.utils.testing.botorchtestcase attribute)": [[14, "botorch.utils.testing.BotorchTestCase.device", false]], "device (botorch.utils.testing.mockposterior property)": [[14, "botorch.utils.testing.MockPosterior.device", false]], "dh (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DH", false]], "dh1 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DH1", false]], "dh2 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DH2", false]], "dh3 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DH3", false]], "dh4 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DH4", false]], "diag (botorch.utils.probability.linalg.pivotedcholesky attribute)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.diag", false]], "dim (botorch.test_functions.base.basetestproblem attribute)": [[12, "botorch.test_functions.base.BaseTestProblem.dim", false]], "dim (botorch.test_functions.multi_fidelity.augmentedbranin attribute)": [[12, "botorch.test_functions.multi_fidelity.AugmentedBranin.dim", false]], "dim (botorch.test_functions.multi_fidelity.augmentedhartmann attribute)": [[12, "botorch.test_functions.multi_fidelity.AugmentedHartmann.dim", false]], "dim (botorch.test_functions.multi_objective.bnh attribute)": [[12, "botorch.test_functions.multi_objective.BNH.dim", false]], "dim (botorch.test_functions.multi_objective.branincurrin attribute)": [[12, "botorch.test_functions.multi_objective.BraninCurrin.dim", false]], "dim (botorch.test_functions.multi_objective.carsideimpact attribute)": [[12, "botorch.test_functions.multi_objective.CarSideImpact.dim", false]], "dim (botorch.test_functions.multi_objective.constr attribute)": [[12, "botorch.test_functions.multi_objective.CONSTR.dim", false]], "dim (botorch.test_functions.multi_objective.constrainedbranincurrin attribute)": [[12, "botorch.test_functions.multi_objective.ConstrainedBraninCurrin.dim", false]], "dim (botorch.test_functions.multi_objective.discbrake attribute)": [[12, "botorch.test_functions.multi_objective.DiscBrake.dim", false]], "dim (botorch.test_functions.multi_objective.gmm attribute)": [[12, "botorch.test_functions.multi_objective.GMM.dim", false]], "dim (botorch.test_functions.multi_objective.osy attribute)": [[12, "botorch.test_functions.multi_objective.OSY.dim", false]], "dim (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.dim", false]], "dim (botorch.test_functions.multi_objective.srn attribute)": [[12, "botorch.test_functions.multi_objective.SRN.dim", false]], "dim (botorch.test_functions.multi_objective.toyrobust attribute)": [[12, "botorch.test_functions.multi_objective.ToyRobust.dim", false]], "dim (botorch.test_functions.multi_objective.vehiclesafety attribute)": [[12, "botorch.test_functions.multi_objective.VehicleSafety.dim", false]], "dim (botorch.test_functions.multi_objective.weldedbeam attribute)": [[12, "botorch.test_functions.multi_objective.WeldedBeam.dim", false]], "dim (botorch.test_functions.multi_objective_multi_fidelity.momfbranincurrin attribute)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFBraninCurrin.dim", false]], "dim (botorch.test_functions.multi_objective_multi_fidelity.momfpark attribute)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFPark.dim", false]], "dim (botorch.test_functions.synthetic.beale attribute)": [[12, "botorch.test_functions.synthetic.Beale.dim", false]], "dim (botorch.test_functions.synthetic.branin attribute)": [[12, "botorch.test_functions.synthetic.Branin.dim", false]], "dim (botorch.test_functions.synthetic.bukin attribute)": [[12, "botorch.test_functions.synthetic.Bukin.dim", false]], "dim (botorch.test_functions.synthetic.constrainedgramacy attribute)": [[12, "botorch.test_functions.synthetic.ConstrainedGramacy.dim", false]], "dim (botorch.test_functions.synthetic.cosine8 attribute)": [[12, "botorch.test_functions.synthetic.Cosine8.dim", false]], "dim (botorch.test_functions.synthetic.dropwave attribute)": [[12, "botorch.test_functions.synthetic.DropWave.dim", false]], "dim (botorch.test_functions.synthetic.eggholder attribute)": [[12, "botorch.test_functions.synthetic.EggHolder.dim", false]], "dim (botorch.test_functions.synthetic.holdertable attribute)": [[12, "botorch.test_functions.synthetic.HolderTable.dim", false]], "dim (botorch.test_functions.synthetic.pressurevessel attribute)": [[12, "botorch.test_functions.synthetic.PressureVessel.dim", false]], "dim (botorch.test_functions.synthetic.shekel attribute)": [[12, "botorch.test_functions.synthetic.Shekel.dim", false]], "dim (botorch.test_functions.synthetic.sixhumpcamel attribute)": [[12, "botorch.test_functions.synthetic.SixHumpCamel.dim", false]], "dim (botorch.test_functions.synthetic.speedreducer attribute)": [[12, "botorch.test_functions.synthetic.SpeedReducer.dim", false]], "dim (botorch.test_functions.synthetic.tensioncompressionstring attribute)": [[12, "botorch.test_functions.synthetic.TensionCompressionString.dim", false]], "dim (botorch.test_functions.synthetic.threehumpcamel attribute)": [[12, "botorch.test_functions.synthetic.ThreeHumpCamel.dim", false]], "dim (botorch.test_functions.synthetic.weldedbeamso attribute)": [[12, "botorch.test_functions.synthetic.WeldedBeamSO.dim", false]], "discbrake (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DiscBrake", false]], "discrete_step() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.discrete_step", false]], "discretemaxvaluebase (class in botorch.acquisition.max_value_entropy_search)": [[0, "botorch.acquisition.max_value_entropy_search.DiscreteMaxValueBase", false]], "dispatch() (botorch.utils.dispatcher.dispatcher method)": [[14, "botorch.utils.dispatcher.Dispatcher.dispatch", false]], "dispatcher (class in botorch.utils.dispatcher)": [[14, "botorch.utils.dispatcher.Dispatcher", false]], "distribution (botorch.posteriors.gpytorch.gpytorchposterior attribute)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.distribution", false]], "div() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.div", false]], "dixonprice (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.DixonPrice", false]], "doc (botorch.utils.dispatcher.dispatcher attribute)": [[14, "botorch.utils.dispatcher.Dispatcher.doc", false]], "dominatedpartitioning (class in botorch.utils.multi_objective.box_decompositions.dominated)": [[14, "botorch.utils.multi_objective.box_decompositions.dominated.DominatedPartitioning", false]], "downsamplingkernel (class in botorch.models.kernels.downsampling)": [[7, "botorch.models.kernels.downsampling.DownsamplingKernel", false]], "draw() (botorch.sampling.qmc.multivariatenormalqmcengine method)": [[10, "botorch.sampling.qmc.MultivariateNormalQMCEngine.draw", false]], "draw() (botorch.sampling.qmc.normalqmcengine method)": [[10, "botorch.sampling.qmc.NormalQMCEngine.draw", false]], "draw() (botorch.utils.probability.lin_ess.linearellipticalslicesampler method)": [[14, "botorch.utils.probability.lin_ess.LinearEllipticalSliceSampler.draw", false]], "draw() (botorch.utils.sampling.delaunaypolytopesampler method)": [[14, "botorch.utils.sampling.DelaunayPolytopeSampler.draw", false]], "draw() (botorch.utils.sampling.hitandrunpolytopesampler method)": [[14, "botorch.utils.sampling.HitAndRunPolytopeSampler.draw", false]], "draw() (botorch.utils.sampling.polytopesampler method)": [[14, "botorch.utils.sampling.PolytopeSampler.draw", false]], "draw_kernel_feature_paths() (in module botorch.sampling.pathwise.prior_samplers)": [[10, "botorch.sampling.pathwise.prior_samplers.draw_kernel_feature_paths", false]], "draw_matheron_paths() (in module botorch.sampling.pathwise.posterior_samplers)": [[10, "botorch.sampling.pathwise.posterior_samplers.draw_matheron_paths", false]], "draw_sobol_normal_samples() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.draw_sobol_normal_samples", false]], "draw_sobol_samples() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.draw_sobol_samples", false]], "dropwave (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.DropWave", false]], "dtlz (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DTLZ", false]], "dtlz1 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DTLZ1", false]], "dtlz2 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DTLZ2", false]], "dtlz3 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DTLZ3", false]], "dtlz4 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DTLZ4", false]], "dtlz5 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DTLZ5", false]], "dtlz7 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.DTLZ7", false]], "dtype (botorch.optim.utils.model_utils.torchattr attribute)": [[8, "botorch.optim.utils.model_utils.TorchAttr.dtype", false]], "dtype (botorch.posteriors.ensemble.ensembleposterior property)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.dtype", false]], "dtype (botorch.posteriors.posterior.posterior property)": [[9, "botorch.posteriors.posterior.Posterior.dtype", false]], "dtype (botorch.posteriors.posterior_list.posteriorlist property)": [[9, "botorch.posteriors.posterior_list.PosteriorList.dtype", false]], "dtype (botorch.posteriors.torch.torchposterior property)": [[9, "botorch.posteriors.torch.TorchPosterior.dtype", false]], "dtype (botorch.posteriors.transformed.transformedposterior property)": [[9, "botorch.posteriors.transformed.TransformedPosterior.dtype", false]], "dtype (botorch.utils.containers.botorchcontainer property)": [[14, "botorch.utils.containers.BotorchContainer.dtype", false]], "dtype (botorch.utils.containers.densecontainer property)": [[14, "botorch.utils.containers.DenseContainer.dtype", false]], "dtype (botorch.utils.containers.slicecontainer property)": [[14, "botorch.utils.containers.SliceContainer.dtype", false]], "dtype (botorch.utils.context_managers.tensorcheckpoint attribute)": [[14, "botorch.utils.context_managers.TensorCheckpoint.dtype", false]], "dtype (botorch.utils.testing.mockposterior property)": [[14, "botorch.utils.testing.MockPosterior.dtype", false]], "dtypes_of_buffers (botorch.models.model.model property)": [[7, "botorch.models.model.Model.dtypes_of_buffers", false]], "dummynonscalarizingposteriortransform (class in botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.DummyNonScalarizingPosteriorTransform", false]], "e_d (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.E_d", false]], "e_g (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.E_g", false]], "eggholder (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.EggHolder", false]], "encode_args() (botorch.utils.dispatcher.dispatcher method)": [[14, "botorch.utils.dispatcher.Dispatcher.encode_args", false]], "encoder (botorch.utils.dispatcher.dispatcher property)": [[14, "botorch.utils.dispatcher.Dispatcher.encoder", false]], "ensemble_size (botorch.posteriors.ensemble.ensembleposterior property)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.ensemble_size", false]], "ensemblemodel (class in botorch.models.ensemble)": [[7, "botorch.models.ensemble.EnsembleModel", false]], "ensembleposterior (class in botorch.posteriors.ensemble)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior", false]], "equality_constraints (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.equality_constraints", false]], "equals() (botorch.models.transforms.input.affineinputtransform method)": [[7, "botorch.models.transforms.input.AffineInputTransform.equals", false]], "equals() (botorch.models.transforms.input.batchbroadcastedinputtransform method)": [[7, "botorch.models.transforms.input.BatchBroadcastedInputTransform.equals", false]], "equals() (botorch.models.transforms.input.chainedinputtransform method)": [[7, "botorch.models.transforms.input.ChainedInputTransform.equals", false]], "equals() (botorch.models.transforms.input.filterfeatures method)": [[7, "botorch.models.transforms.input.FilterFeatures.equals", false]], "equals() (botorch.models.transforms.input.inputtransform method)": [[7, "botorch.models.transforms.input.InputTransform.equals", false]], "equals() (botorch.models.transforms.input.onehottonumeric method)": [[7, "botorch.models.transforms.input.OneHotToNumeric.equals", false]], "equals() (botorch.models.transforms.input.reversibleinputtransform method)": [[7, "botorch.models.transforms.input.ReversibleInputTransform.equals", false]], "equals() (botorch.models.transforms.input.round method)": [[7, "botorch.models.transforms.input.Round.equals", false]], "estimate_feasible_volume() (in module botorch.utils.feasible_volume)": [[14, "botorch.utils.feasible_volume.estimate_feasible_volume", false]], "eval() (botorch.models.approximate_gp.approximategpytorchmodel method)": [[7, "botorch.models.approximate_gp.ApproximateGPyTorchModel.eval", false]], "eval() (botorch.models.model.model method)": [[7, "botorch.models.model.Model.eval", false]], "eval_lin_constraint() (in module botorch.optim.parameter_constraints)": [[8, "botorch.optim.parameter_constraints.eval_lin_constraint", false]], "evaluate() (botorch.acquisition.knowledge_gradient.qknowledgegradient method)": [[0, "botorch.acquisition.knowledge_gradient.qKnowledgeGradient.evaluate", false]], "evaluate() (botorch.acquisition.objective.expectationposteriortransform method)": [[0, "botorch.acquisition.objective.ExpectationPosteriorTransform.evaluate", false]], "evaluate() (botorch.acquisition.objective.posteriortransform method)": [[0, "botorch.acquisition.objective.PosteriorTransform.evaluate", false]], "evaluate() (botorch.acquisition.objective.scalarizedposteriortransform method)": [[0, "botorch.acquisition.objective.ScalarizedPosteriorTransform.evaluate", false]], "evaluate() (botorch.optim.stopping.expmastoppingcriterion method)": [[8, "botorch.optim.stopping.ExpMAStoppingCriterion.evaluate", false]], "evaluate() (botorch.optim.stopping.stoppingcriterion method)": [[8, "botorch.optim.stopping.StoppingCriterion.evaluate", false]], "evaluate() (botorch.utils.test_helpers.dummynonscalarizingposteriortransform method)": [[14, "botorch.utils.test_helpers.DummyNonScalarizingPosteriorTransform.evaluate", false]], "evaluate_slack() (botorch.test_functions.base.constrainedbasetestproblem method)": [[12, "botorch.test_functions.base.ConstrainedBaseTestProblem.evaluate_slack", false]], "evaluate_slack_true() (botorch.test_functions.base.constrainedbasetestproblem method)": [[12, "botorch.test_functions.base.ConstrainedBaseTestProblem.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.bnh method)": [[12, "botorch.test_functions.multi_objective.BNH.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.c2dtlz2 method)": [[12, "botorch.test_functions.multi_objective.C2DTLZ2.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.constr method)": [[12, "botorch.test_functions.multi_objective.CONSTR.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.constrainedbranincurrin method)": [[12, "botorch.test_functions.multi_objective.ConstrainedBraninCurrin.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.discbrake method)": [[12, "botorch.test_functions.multi_objective.DiscBrake.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.mw7 method)": [[12, "botorch.test_functions.multi_objective.MW7.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.osy method)": [[12, "botorch.test_functions.multi_objective.OSY.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.srn method)": [[12, "botorch.test_functions.multi_objective.SRN.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.multi_objective.weldedbeam method)": [[12, "botorch.test_functions.multi_objective.WeldedBeam.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.synthetic.constrainedgramacy method)": [[12, "botorch.test_functions.synthetic.ConstrainedGramacy.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.synthetic.constrainedhartmann method)": [[12, "botorch.test_functions.synthetic.ConstrainedHartmann.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.synthetic.constrainedhartmannsmooth method)": [[12, "botorch.test_functions.synthetic.ConstrainedHartmannSmooth.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.synthetic.pressurevessel method)": [[12, "botorch.test_functions.synthetic.PressureVessel.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.synthetic.speedreducer method)": [[12, "botorch.test_functions.synthetic.SpeedReducer.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.synthetic.tensioncompressionstring method)": [[12, "botorch.test_functions.synthetic.TensionCompressionString.evaluate_slack_true", false]], "evaluate_slack_true() (botorch.test_functions.synthetic.weldedbeamso method)": [[12, "botorch.test_functions.synthetic.WeldedBeamSO.evaluate_slack_true", false]], "evaluate_true() (botorch.test_functions.base.basetestproblem method)": [[12, "botorch.test_functions.base.BaseTestProblem.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_fidelity.augmentedbranin method)": [[12, "botorch.test_functions.multi_fidelity.AugmentedBranin.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_fidelity.augmentedhartmann method)": [[12, "botorch.test_functions.multi_fidelity.AugmentedHartmann.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_fidelity.augmentedrosenbrock method)": [[12, "botorch.test_functions.multi_fidelity.AugmentedRosenbrock.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.bnh method)": [[12, "botorch.test_functions.multi_objective.BNH.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.branincurrin method)": [[12, "botorch.test_functions.multi_objective.BraninCurrin.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.carsideimpact method)": [[12, "botorch.test_functions.multi_objective.CarSideImpact.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.constr method)": [[12, "botorch.test_functions.multi_objective.CONSTR.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.dh1 method)": [[12, "botorch.test_functions.multi_objective.DH1.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.dh3 method)": [[12, "botorch.test_functions.multi_objective.DH3.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.discbrake method)": [[12, "botorch.test_functions.multi_objective.DiscBrake.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.dtlz1 method)": [[12, "botorch.test_functions.multi_objective.DTLZ1.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.dtlz2 method)": [[12, "botorch.test_functions.multi_objective.DTLZ2.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.dtlz3 method)": [[12, "botorch.test_functions.multi_objective.DTLZ3.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.dtlz5 method)": [[12, "botorch.test_functions.multi_objective.DTLZ5.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.dtlz7 method)": [[12, "botorch.test_functions.multi_objective.DTLZ7.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.gmm method)": [[12, "botorch.test_functions.multi_objective.GMM.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.mw7 method)": [[12, "botorch.test_functions.multi_objective.MW7.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.osy method)": [[12, "botorch.test_functions.multi_objective.OSY.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.penicillin method)": [[12, "botorch.test_functions.multi_objective.Penicillin.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.srn method)": [[12, "botorch.test_functions.multi_objective.SRN.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.toyrobust method)": [[12, "botorch.test_functions.multi_objective.ToyRobust.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.vehiclesafety method)": [[12, "botorch.test_functions.multi_objective.VehicleSafety.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.weldedbeam method)": [[12, "botorch.test_functions.multi_objective.WeldedBeam.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.zdt1 method)": [[12, "botorch.test_functions.multi_objective.ZDT1.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.zdt2 method)": [[12, "botorch.test_functions.multi_objective.ZDT2.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective.zdt3 method)": [[12, "botorch.test_functions.multi_objective.ZDT3.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective_multi_fidelity.momfbranincurrin method)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFBraninCurrin.evaluate_true", false]], "evaluate_true() (botorch.test_functions.multi_objective_multi_fidelity.momfpark method)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFPark.evaluate_true", false]], "evaluate_true() (botorch.test_functions.sensitivity_analysis.gsobol method)": [[12, "botorch.test_functions.sensitivity_analysis.Gsobol.evaluate_true", false]], "evaluate_true() (botorch.test_functions.sensitivity_analysis.ishigami method)": [[12, "botorch.test_functions.sensitivity_analysis.Ishigami.evaluate_true", false]], "evaluate_true() (botorch.test_functions.sensitivity_analysis.morris method)": [[12, "botorch.test_functions.sensitivity_analysis.Morris.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.ackley method)": [[12, "botorch.test_functions.synthetic.Ackley.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.beale method)": [[12, "botorch.test_functions.synthetic.Beale.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.branin method)": [[12, "botorch.test_functions.synthetic.Branin.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.bukin method)": [[12, "botorch.test_functions.synthetic.Bukin.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.constrainedgramacy method)": [[12, "botorch.test_functions.synthetic.ConstrainedGramacy.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.cosine8 method)": [[12, "botorch.test_functions.synthetic.Cosine8.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.dixonprice method)": [[12, "botorch.test_functions.synthetic.DixonPrice.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.dropwave method)": [[12, "botorch.test_functions.synthetic.DropWave.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.eggholder method)": [[12, "botorch.test_functions.synthetic.EggHolder.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.griewank method)": [[12, "botorch.test_functions.synthetic.Griewank.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.hartmann method)": [[12, "botorch.test_functions.synthetic.Hartmann.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.holdertable method)": [[12, "botorch.test_functions.synthetic.HolderTable.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.levy method)": [[12, "botorch.test_functions.synthetic.Levy.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.michalewicz method)": [[12, "botorch.test_functions.synthetic.Michalewicz.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.powell method)": [[12, "botorch.test_functions.synthetic.Powell.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.pressurevessel method)": [[12, "botorch.test_functions.synthetic.PressureVessel.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.rastrigin method)": [[12, "botorch.test_functions.synthetic.Rastrigin.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.rosenbrock method)": [[12, "botorch.test_functions.synthetic.Rosenbrock.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.shekel method)": [[12, "botorch.test_functions.synthetic.Shekel.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.sixhumpcamel method)": [[12, "botorch.test_functions.synthetic.SixHumpCamel.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.speedreducer method)": [[12, "botorch.test_functions.synthetic.SpeedReducer.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.styblinskitang method)": [[12, "botorch.test_functions.synthetic.StyblinskiTang.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.tensioncompressionstring method)": [[12, "botorch.test_functions.synthetic.TensionCompressionString.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.threehumpcamel method)": [[12, "botorch.test_functions.synthetic.ThreeHumpCamel.evaluate_true", false]], "evaluate_true() (botorch.test_functions.synthetic.weldedbeamso method)": [[12, "botorch.test_functions.synthetic.WeldedBeamSO.evaluate_true", false]], "event_shape (botorch.utils.containers.botorchcontainer attribute)": [[14, "botorch.utils.containers.BotorchContainer.event_shape", false]], "event_shape (botorch.utils.containers.densecontainer attribute)": [[14, "botorch.utils.containers.DenseContainer.event_shape", false]], "event_shape (botorch.utils.containers.slicecontainer attribute)": [[14, "botorch.utils.containers.SliceContainer.event_shape", false]], "exp() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.exp", false]], "expand() (botorch.utils.probability.linalg.pivotedcholesky method)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.expand", false]], "expand() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.expand", false]], "expand() (botorch.utils.probability.truncated_multivariate_normal.truncatedmultivariatenormal method)": [[14, "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal.expand", false]], "expand() (botorch.utils.probability.unified_skew_normal.unifiedskewnormal method)": [[14, "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal.expand", false]], "expand_and_copy_tensor() (in module botorch.models.transforms.utils)": [[7, "botorch.models.transforms.utils.expand_and_copy_tensor", false]], "expand_trace_observations() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.expand_trace_observations", false]], "expectation (class in botorch.acquisition.risk_measures)": [[0, "botorch.acquisition.risk_measures.Expectation", false]], "expectationposteriortransform (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.ExpectationPosteriorTransform", false]], "expectedhypervolumeimprovement (class in botorch.acquisition.multi_objective.analytic)": [[0, "botorch.acquisition.multi_objective.analytic.ExpectedHypervolumeImprovement", false]], "expectedimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.ExpectedImprovement", false]], "expectedimprovementqualityfunction (class in botorch.models.utils.inducing_point_allocators)": [[7, "botorch.models.utils.inducing_point_allocators.ExpectedImprovementQualityFunction", false]], "expmastoppingcriterion (class in botorch.optim.stopping)": [[8, "botorch.optim.stopping.ExpMAStoppingCriterion", false]], "exponentialdecaykernel (class in botorch.models.kernels.exponential_decay)": [[7, "botorch.models.kernels.exponential_decay.ExponentialDecayKernel", false]], "extend() (botorch.utils.multi_objective.hypervolume.multilist method)": [[14, "botorch.utils.multi_objective.hypervolume.MultiList.extend", false]], "extra_repr() (botorch.utils.torch.bufferdict method)": [[14, "botorch.utils.torch.BufferDict.extra_repr", false]], "extract_batch_covar() (in module botorch.utils.low_rank)": [[14, "botorch.utils.low_rank.extract_batch_covar", false]], "extract_candidates() (botorch.acquisition.acquisition.oneshotacquisitionfunction method)": [[0, "botorch.acquisition.acquisition.OneShotAcquisitionFunction.extract_candidates", false]], "extract_candidates() (botorch.acquisition.knowledge_gradient.qknowledgegradient method)": [[0, "botorch.acquisition.knowledge_gradient.qKnowledgeGradient.extract_candidates", false]], "extract_candidates() (botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qhypervolumeknowledgegradient method)": [[0, "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qHypervolumeKnowledgeGradient.extract_candidates", false]], "extract_candidates() (botorch.acquisition.multi_step_lookahead.qmultisteplookahead method)": [[0, "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead.extract_candidates", false]], "f_1() (botorch.test_functions.multi_objective.toyrobust method)": [[12, "botorch.test_functions.multi_objective.ToyRobust.f_1", false]], "f_2() (botorch.test_functions.multi_objective.toyrobust method)": [[12, "botorch.test_functions.multi_objective.ToyRobust.f_2", false]], "failure (botorch.optim.core.optimizationstatus attribute)": [[8, "botorch.optim.core.OptimizationStatus.FAILURE", false]], "fantasize (class in botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.fantasize", false]], "fantasize() (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp method)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.fantasize", false]], "fantasize() (botorch.models.model.fantasizemixin method)": [[7, "botorch.models.model.FantasizeMixin.fantasize", false]], "fantasize() (botorch.models.model.modellist method)": [[7, "botorch.models.model.ModelList.fantasize", false]], "fantasizemixin (class in botorch.models.model)": [[7, "botorch.models.model.FantasizeMixin", false]], "fastnondominatedpartitioning (class in botorch.utils.multi_objective.box_decompositions.non_dominated)": [[14, "botorch.utils.multi_objective.box_decompositions.non_dominated.FastNondominatedPartitioning", false]], "fastpartitioning (class in botorch.utils.multi_objective.box_decompositions.box_decomposition)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.FastPartitioning", false]], "fatmax() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.fatmax", false]], "fatmaximum() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.fatmaximum", false]], "fatmin() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.fatmin", false]], "fatminimum() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.fatminimum", false]], "fatmoid() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.fatmoid", false]], "fatplus() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.fatplus", false]], "feasibilityweightedmcmultioutputobjective (class in botorch.acquisition.multi_objective.objective)": [[0, "botorch.acquisition.multi_objective.objective.FeasibilityWeightedMCMultiOutputObjective", false]], "feasible() (botorch.utils.sampling.polytopesampler method)": [[14, "botorch.utils.sampling.PolytopeSampler.feasible", false]], "featuremap (class in botorch.sampling.pathwise.features.maps)": [[10, "botorch.sampling.pathwise.features.maps.FeatureMap", false]], "featureselector (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.FeatureSelector", false]], "filterfeatures (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.FilterFeatures", false]], "find_interior_point() (botorch.utils.sampling.polytopesampler method)": [[14, "botorch.utils.sampling.PolytopeSampler.find_interior_point", false]], "find_interior_point() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.find_interior_point", false]], "fit_fully_bayesian_model_nuts() (in module botorch.fit)": [[3, "botorch.fit.fit_fully_bayesian_model_nuts", false]], "fit_gpytorch_mll() (in module botorch.fit)": [[3, "botorch.fit.fit_gpytorch_mll", false]], "fit_gpytorch_mll_scipy() (in module botorch.optim.fit)": [[8, "botorch.optim.fit.fit_gpytorch_mll_scipy", false]], "fit_gpytorch_mll_torch() (in module botorch.optim.fit)": [[8, "botorch.optim.fit.fit_gpytorch_mll_torch", false]], "fix_features() (in module botorch.optim.utils.acquisition_utils)": [[8, "botorch.optim.utils.acquisition_utils.fix_features", false]], "fixed_features (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.fixed_features", false]], "fixedcostmodel (class in botorch.models.cost)": [[7, "botorch.models.cost.FixedCostModel", false]], "fixedfeatureacquisitionfunction (class in botorch.acquisition.fixed_feature)": [[0, "botorch.acquisition.fixed_feature.FixedFeatureAcquisitionFunction", false]], "fixedhomotopyschedule (class in botorch.optim.homotopy)": [[8, "botorch.optim.homotopy.FixedHomotopySchedule", false]], "fixedsinglesamplemodel (class in botorch.models.deterministic)": [[7, "botorch.models.deterministic.FixedSingleSampleModel", false]], "flattenedstandardize (class in botorch.models.higher_order_gp)": [[7, "botorch.models.higher_order_gp.FlattenedStandardize", false]], "forkedrngsampler (class in botorch.sampling.stochastic_samplers)": [[10, "botorch.sampling.stochastic_samplers.ForkedRNGSampler", false]], "forward() (botorch.acquisition.acquisition.acquisitionfunction method)": [[0, "botorch.acquisition.acquisition.AcquisitionFunction.forward", false]], "forward() (botorch.acquisition.active_learning.pairwisemcposteriorvariance method)": [[0, "botorch.acquisition.active_learning.PairwiseMCPosteriorVariance.forward", false]], "forward() (botorch.acquisition.active_learning.qnegintegratedposteriorvariance method)": [[0, "botorch.acquisition.active_learning.qNegIntegratedPosteriorVariance.forward", false]], "forward() (botorch.acquisition.analytic.constrainedexpectedimprovement method)": [[0, "botorch.acquisition.analytic.ConstrainedExpectedImprovement.forward", false]], "forward() (botorch.acquisition.analytic.expectedimprovement method)": [[0, "botorch.acquisition.analytic.ExpectedImprovement.forward", false]], "forward() (botorch.acquisition.analytic.logconstrainedexpectedimprovement method)": [[0, "botorch.acquisition.analytic.LogConstrainedExpectedImprovement.forward", false]], "forward() (botorch.acquisition.analytic.logexpectedimprovement method)": [[0, "botorch.acquisition.analytic.LogExpectedImprovement.forward", false]], "forward() (botorch.acquisition.analytic.lognoisyexpectedimprovement method)": [[0, "botorch.acquisition.analytic.LogNoisyExpectedImprovement.forward", false]], "forward() (botorch.acquisition.analytic.logprobabilityofimprovement method)": [[0, "botorch.acquisition.analytic.LogProbabilityOfImprovement.forward", false]], "forward() (botorch.acquisition.analytic.noisyexpectedimprovement method)": [[0, "botorch.acquisition.analytic.NoisyExpectedImprovement.forward", false]], "forward() (botorch.acquisition.analytic.posteriormean method)": [[0, "botorch.acquisition.analytic.PosteriorMean.forward", false]], "forward() (botorch.acquisition.analytic.posteriorstandarddeviation method)": [[0, "botorch.acquisition.analytic.PosteriorStandardDeviation.forward", false]], "forward() (botorch.acquisition.analytic.probabilityofimprovement method)": [[0, "botorch.acquisition.analytic.ProbabilityOfImprovement.forward", false]], "forward() (botorch.acquisition.analytic.qanalyticprobabilityofimprovement method)": [[0, "botorch.acquisition.analytic.qAnalyticProbabilityOfImprovement.forward", false]], "forward() (botorch.acquisition.analytic.scalarizedposteriormean method)": [[0, "botorch.acquisition.analytic.ScalarizedPosteriorMean.forward", false]], "forward() (botorch.acquisition.analytic.upperconfidencebound method)": [[0, "botorch.acquisition.analytic.UpperConfidenceBound.forward", false]], "forward() (botorch.acquisition.bayesian_active_learning.qbayesianactivelearningbydisagreement method)": [[0, "botorch.acquisition.bayesian_active_learning.qBayesianActiveLearningByDisagreement.forward", false]], "forward() (botorch.acquisition.cost_aware.costawareutility method)": [[0, "botorch.acquisition.cost_aware.CostAwareUtility.forward", false]], "forward() (botorch.acquisition.cost_aware.genericcostawareutility method)": [[0, "botorch.acquisition.cost_aware.GenericCostAwareUtility.forward", false]], "forward() (botorch.acquisition.cost_aware.inversecostweightedutility method)": [[0, "botorch.acquisition.cost_aware.InverseCostWeightedUtility.forward", false]], "forward() (botorch.acquisition.fixed_feature.fixedfeatureacquisitionfunction method)": [[0, "botorch.acquisition.fixed_feature.FixedFeatureAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.joint_entropy_search.qjointentropysearch method)": [[0, "botorch.acquisition.joint_entropy_search.qJointEntropySearch.forward", false]], "forward() (botorch.acquisition.knowledge_gradient.projectedacquisitionfunction method)": [[0, "botorch.acquisition.knowledge_gradient.ProjectedAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.knowledge_gradient.qknowledgegradient method)": [[0, "botorch.acquisition.knowledge_gradient.qKnowledgeGradient.forward", false]], "forward() (botorch.acquisition.knowledge_gradient.qmultifidelityknowledgegradient method)": [[0, "botorch.acquisition.knowledge_gradient.qMultiFidelityKnowledgeGradient.forward", false]], "forward() (botorch.acquisition.max_value_entropy_search.maxvaluebase method)": [[0, "botorch.acquisition.max_value_entropy_search.MaxValueBase.forward", false]], "forward() (botorch.acquisition.max_value_entropy_search.qmultifidelitymaxvalueentropy method)": [[0, "botorch.acquisition.max_value_entropy_search.qMultiFidelityMaxValueEntropy.forward", false]], "forward() (botorch.acquisition.monte_carlo.mcacquisitionfunction method)": [[0, "botorch.acquisition.monte_carlo.MCAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.monte_carlo.samplereducingmcacquisitionfunction method)": [[0, "botorch.acquisition.monte_carlo.SampleReducingMCAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.multi_objective.analytic.expectedhypervolumeimprovement method)": [[0, "botorch.acquisition.multi_objective.analytic.ExpectedHypervolumeImprovement.forward", false]], "forward() (botorch.acquisition.multi_objective.base.multiobjectiveanalyticacquisitionfunction method)": [[0, "botorch.acquisition.multi_objective.base.MultiObjectiveAnalyticAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.multi_objective.base.multiobjectivemcacquisitionfunction method)": [[0, "botorch.acquisition.multi_objective.base.MultiObjectiveMCAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qhypervolumeknowledgegradient method)": [[0, "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qHypervolumeKnowledgeGradient.forward", false]], "forward() (botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qmultifidelityhypervolumeknowledgegradient method)": [[0, "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qMultiFidelityHypervolumeKnowledgeGradient.forward", false]], "forward() (botorch.acquisition.multi_objective.joint_entropy_search.lowerboundmultiobjectiveentropysearch method)": [[0, "botorch.acquisition.multi_objective.joint_entropy_search.LowerBoundMultiObjectiveEntropySearch.forward", false]], "forward() (botorch.acquisition.multi_objective.joint_entropy_search.qlowerboundmultiobjectivejointentropysearch method)": [[0, "botorch.acquisition.multi_objective.joint_entropy_search.qLowerBoundMultiObjectiveJointEntropySearch.forward", false]], "forward() (botorch.acquisition.multi_objective.logei.qlogexpectedhypervolumeimprovement method)": [[0, "botorch.acquisition.multi_objective.logei.qLogExpectedHypervolumeImprovement.forward", false]], "forward() (botorch.acquisition.multi_objective.logei.qlognoisyexpectedhypervolumeimprovement method)": [[0, "botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement.forward", false]], "forward() (botorch.acquisition.multi_objective.max_value_entropy_search.qlowerboundmultiobjectivemaxvalueentropysearch method)": [[0, "botorch.acquisition.multi_objective.max_value_entropy_search.qLowerBoundMultiObjectiveMaxValueEntropySearch.forward", false]], "forward() (botorch.acquisition.multi_objective.max_value_entropy_search.qmultiobjectivemaxvalueentropy method)": [[0, "botorch.acquisition.multi_objective.max_value_entropy_search.qMultiObjectiveMaxValueEntropy.forward", false]], "forward() (botorch.acquisition.multi_objective.monte_carlo.qexpectedhypervolumeimprovement method)": [[0, "botorch.acquisition.multi_objective.monte_carlo.qExpectedHypervolumeImprovement.forward", false]], "forward() (botorch.acquisition.multi_objective.monte_carlo.qnoisyexpectedhypervolumeimprovement method)": [[0, "botorch.acquisition.multi_objective.monte_carlo.qNoisyExpectedHypervolumeImprovement.forward", false]], "forward() (botorch.acquisition.multi_objective.multi_fidelity.momf method)": [[0, "botorch.acquisition.multi_objective.multi_fidelity.MOMF.forward", false]], "forward() (botorch.acquisition.multi_objective.multi_output_risk_measures.independentcvar method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentCVaR.forward", false]], "forward() (botorch.acquisition.multi_objective.multi_output_risk_measures.independentvar method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentVaR.forward", false]], "forward() (botorch.acquisition.multi_objective.multi_output_risk_measures.multioutputexpectation method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputExpectation.forward", false]], "forward() (botorch.acquisition.multi_objective.multi_output_risk_measures.multioutputriskmeasuremcobjective method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputRiskMeasureMCObjective.forward", false]], "forward() (botorch.acquisition.multi_objective.multi_output_risk_measures.multioutputworstcase method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputWorstCase.forward", false]], "forward() (botorch.acquisition.multi_objective.multi_output_risk_measures.mvar method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MVaR.forward", false]], "forward() (botorch.acquisition.multi_objective.objective.feasibilityweightedmcmultioutputobjective method)": [[0, "botorch.acquisition.multi_objective.objective.FeasibilityWeightedMCMultiOutputObjective.forward", false]], "forward() (botorch.acquisition.multi_objective.objective.identitymcmultioutputobjective method)": [[0, "botorch.acquisition.multi_objective.objective.IdentityMCMultiOutputObjective.forward", false]], "forward() (botorch.acquisition.multi_objective.objective.mcmultioutputobjective method)": [[0, "botorch.acquisition.multi_objective.objective.MCMultiOutputObjective.forward", false]], "forward() (botorch.acquisition.multi_objective.objective.weightedmcmultioutputobjective method)": [[0, "botorch.acquisition.multi_objective.objective.WeightedMCMultiOutputObjective.forward", false]], "forward() (botorch.acquisition.multi_objective.predictive_entropy_search.qmultiobjectivepredictiveentropysearch method)": [[0, "botorch.acquisition.multi_objective.predictive_entropy_search.qMultiObjectivePredictiveEntropySearch.forward", false]], "forward() (botorch.acquisition.multi_step_lookahead.qmultisteplookahead method)": [[0, "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead.forward", false]], "forward() (botorch.acquisition.objective.constrainedmcobjective method)": [[0, "botorch.acquisition.objective.ConstrainedMCObjective.forward", false]], "forward() (botorch.acquisition.objective.expectationposteriortransform method)": [[0, "botorch.acquisition.objective.ExpectationPosteriorTransform.forward", false]], "forward() (botorch.acquisition.objective.genericmcobjective method)": [[0, "botorch.acquisition.objective.GenericMCObjective.forward", false]], "forward() (botorch.acquisition.objective.identitymcobjective method)": [[0, "botorch.acquisition.objective.IdentityMCObjective.forward", false]], "forward() (botorch.acquisition.objective.learnedobjective method)": [[0, "botorch.acquisition.objective.LearnedObjective.forward", false]], "forward() (botorch.acquisition.objective.linearmcobjective method)": [[0, "botorch.acquisition.objective.LinearMCObjective.forward", false]], "forward() (botorch.acquisition.objective.mcacquisitionobjective method)": [[0, "botorch.acquisition.objective.MCAcquisitionObjective.forward", false]], "forward() (botorch.acquisition.objective.posteriortransform method)": [[0, "botorch.acquisition.objective.PosteriorTransform.forward", false]], "forward() (botorch.acquisition.objective.scalarizedposteriortransform method)": [[0, "botorch.acquisition.objective.ScalarizedPosteriorTransform.forward", false]], "forward() (botorch.acquisition.penalized.gaussianpenalty method)": [[0, "botorch.acquisition.penalized.GaussianPenalty.forward", false]], "forward() (botorch.acquisition.penalized.grouplassopenalty method)": [[0, "botorch.acquisition.penalized.GroupLassoPenalty.forward", false]], "forward() (botorch.acquisition.penalized.l1penalty method)": [[0, "botorch.acquisition.penalized.L1Penalty.forward", false]], "forward() (botorch.acquisition.penalized.l1penaltyobjective method)": [[0, "botorch.acquisition.penalized.L1PenaltyObjective.forward", false]], "forward() (botorch.acquisition.penalized.l2penalty method)": [[0, "botorch.acquisition.penalized.L2Penalty.forward", false]], "forward() (botorch.acquisition.penalized.penalizedacquisitionfunction method)": [[0, "botorch.acquisition.penalized.PenalizedAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.penalized.penalizedmcobjective method)": [[0, "botorch.acquisition.penalized.PenalizedMCObjective.forward", false]], "forward() (botorch.acquisition.predictive_entropy_search.qpredictiveentropysearch method)": [[0, "botorch.acquisition.predictive_entropy_search.qPredictiveEntropySearch.forward", false]], "forward() (botorch.acquisition.preference.analyticexpectedutilityofbestoption method)": [[0, "botorch.acquisition.preference.AnalyticExpectedUtilityOfBestOption.forward", false]], "forward() (botorch.acquisition.preference.pairwisebayesianactivelearningbydisagreement method)": [[0, "botorch.acquisition.preference.PairwiseBayesianActiveLearningByDisagreement.forward", false]], "forward() (botorch.acquisition.preference.qexpectedutilityofbestoption method)": [[0, "botorch.acquisition.preference.qExpectedUtilityOfBestOption.forward", false]], "forward() (botorch.acquisition.prior_guided.priorguidedacquisitionfunction method)": [[0, "botorch.acquisition.prior_guided.PriorGuidedAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.proximal.proximalacquisitionfunction method)": [[0, "botorch.acquisition.proximal.ProximalAcquisitionFunction.forward", false]], "forward() (botorch.acquisition.risk_measures.cvar method)": [[0, "botorch.acquisition.risk_measures.CVaR.forward", false]], "forward() (botorch.acquisition.risk_measures.expectation method)": [[0, "botorch.acquisition.risk_measures.Expectation.forward", false]], "forward() (botorch.acquisition.risk_measures.riskmeasuremcobjective method)": [[0, "botorch.acquisition.risk_measures.RiskMeasureMCObjective.forward", false]], "forward() (botorch.acquisition.risk_measures.var method)": [[0, "botorch.acquisition.risk_measures.VaR.forward", false]], "forward() (botorch.acquisition.risk_measures.worstcase method)": [[0, "botorch.acquisition.risk_measures.WorstCase.forward", false]], "forward() (botorch.acquisition.thompson_sampling.pathwisethompsonsampling method)": [[0, "botorch.acquisition.thompson_sampling.PathwiseThompsonSampling.forward", false]], "forward() (botorch.generation.sampling.boltzmannsampling method)": [[4, "botorch.generation.sampling.BoltzmannSampling.forward", false]], "forward() (botorch.generation.sampling.constrainedmaxposteriorsampling method)": [[4, "botorch.generation.sampling.ConstrainedMaxPosteriorSampling.forward", false]], "forward() (botorch.generation.sampling.maxposteriorsampling method)": [[4, "botorch.generation.sampling.MaxPosteriorSampling.forward", false]], "forward() (botorch.generation.sampling.samplingstrategy method)": [[4, "botorch.generation.sampling.SamplingStrategy.forward", false]], "forward() (botorch.models.approximate_gp.approximategpytorchmodel method)": [[7, "botorch.models.approximate_gp.ApproximateGPyTorchModel.forward", false]], "forward() (botorch.models.cost.affinefidelitycostmodel method)": [[7, "botorch.models.cost.AffineFidelityCostModel.forward", false]], "forward() (botorch.models.cost.fixedcostmodel method)": [[7, "botorch.models.cost.FixedCostModel.forward", false]], "forward() (botorch.models.deterministic.affinedeterministicmodel method)": [[7, "botorch.models.deterministic.AffineDeterministicModel.forward", false]], "forward() (botorch.models.deterministic.deterministicmodel method)": [[7, "botorch.models.deterministic.DeterministicModel.forward", false]], "forward() (botorch.models.deterministic.fixedsinglesamplemodel method)": [[7, "botorch.models.deterministic.FixedSingleSampleModel.forward", false]], "forward() (botorch.models.deterministic.genericdeterministicmodel method)": [[7, "botorch.models.deterministic.GenericDeterministicModel.forward", false]], "forward() (botorch.models.deterministic.posteriormeanmodel method)": [[7, "botorch.models.deterministic.PosteriorMeanModel.forward", false]], "forward() (botorch.models.ensemble.ensemblemodel method)": [[7, "botorch.models.ensemble.EnsembleModel.forward", false]], "forward() (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp method)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.forward", false]], "forward() (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp method)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.forward", false]], "forward() (botorch.models.gp_regression.singletaskgp method)": [[7, "botorch.models.gp_regression.SingleTaskGP.forward", false]], "forward() (botorch.models.higher_order_gp.flattenedstandardize method)": [[7, "botorch.models.higher_order_gp.FlattenedStandardize.forward", false]], "forward() (botorch.models.higher_order_gp.higherordergp method)": [[7, "botorch.models.higher_order_gp.HigherOrderGP.forward", false]], "forward() (botorch.models.likelihoods.pairwise.pairwiselikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLikelihood.forward", false]], "forward() (botorch.models.multitask.kroneckermultitaskgp method)": [[7, "botorch.models.multitask.KroneckerMultiTaskGP.forward", false]], "forward() (botorch.models.multitask.multitaskgp method)": [[7, "botorch.models.multitask.MultiTaskGP.forward", false]], "forward() (botorch.models.pairwise_gp.pairwisegp method)": [[7, "botorch.models.pairwise_gp.PairwiseGP.forward", false]], "forward() (botorch.models.pairwise_gp.pairwiselaplacemarginalloglikelihood method)": [[7, "botorch.models.pairwise_gp.PairwiseLaplaceMarginalLogLikelihood.forward", false]], "forward() (botorch.models.transforms.input.inputtransform method)": [[7, "botorch.models.transforms.input.InputTransform.forward", false]], "forward() (botorch.models.transforms.outcome.bilog method)": [[7, "botorch.models.transforms.outcome.Bilog.forward", false]], "forward() (botorch.models.transforms.outcome.chainedoutcometransform method)": [[7, "botorch.models.transforms.outcome.ChainedOutcomeTransform.forward", false]], "forward() (botorch.models.transforms.outcome.log method)": [[7, "botorch.models.transforms.outcome.Log.forward", false]], "forward() (botorch.models.transforms.outcome.outcometransform method)": [[7, "botorch.models.transforms.outcome.OutcomeTransform.forward", false]], "forward() (botorch.models.transforms.outcome.power method)": [[7, "botorch.models.transforms.outcome.Power.forward", false]], "forward() (botorch.models.transforms.outcome.standardize method)": [[7, "botorch.models.transforms.outcome.Standardize.forward", false]], "forward() (botorch.sampling.base.mcsampler method)": [[10, "botorch.sampling.base.MCSampler.forward", false]], "forward() (botorch.sampling.index_sampler.indexsampler method)": [[10, "botorch.sampling.index_sampler.IndexSampler.forward", false]], "forward() (botorch.sampling.list_sampler.listsampler method)": [[10, "botorch.sampling.list_sampler.ListSampler.forward", false]], "forward() (botorch.sampling.normal.normalmcsampler method)": [[10, "botorch.sampling.normal.NormalMCSampler.forward", false]], "forward() (botorch.sampling.pairwise_samplers.pairwisemcsampler method)": [[10, "botorch.sampling.pairwise_samplers.PairwiseMCSampler.forward", false]], "forward() (botorch.sampling.pathwise.features.maps.kernelevaluationmap method)": [[10, "botorch.sampling.pathwise.features.maps.KernelEvaluationMap.forward", false]], "forward() (botorch.sampling.pathwise.features.maps.kernelfeaturemap method)": [[10, "botorch.sampling.pathwise.features.maps.KernelFeatureMap.forward", false]], "forward() (botorch.sampling.pathwise.paths.generalizedlinearpath method)": [[10, "botorch.sampling.pathwise.paths.GeneralizedLinearPath.forward", false]], "forward() (botorch.sampling.pathwise.paths.pathdict method)": [[10, "botorch.sampling.pathwise.paths.PathDict.forward", false]], "forward() (botorch.sampling.pathwise.paths.pathlist method)": [[10, "botorch.sampling.pathwise.paths.PathList.forward", false]], "forward() (botorch.sampling.pathwise.utils.chainedtransform method)": [[10, "botorch.sampling.pathwise.utils.ChainedTransform.forward", false]], "forward() (botorch.sampling.pathwise.utils.featureselector method)": [[10, "botorch.sampling.pathwise.utils.FeatureSelector.forward", false]], "forward() (botorch.sampling.pathwise.utils.inverselengthscaletransform method)": [[10, "botorch.sampling.pathwise.utils.InverseLengthscaleTransform.forward", false]], "forward() (botorch.sampling.pathwise.utils.outcomeuntransformer method)": [[10, "botorch.sampling.pathwise.utils.OutcomeUntransformer.forward", false]], "forward() (botorch.sampling.pathwise.utils.outputscaletransform method)": [[10, "botorch.sampling.pathwise.utils.OutputscaleTransform.forward", false]], "forward() (botorch.sampling.pathwise.utils.sinecosinetransform method)": [[10, "botorch.sampling.pathwise.utils.SineCosineTransform.forward", false]], "forward() (botorch.sampling.pathwise.utils.tensortransform method)": [[10, "botorch.sampling.pathwise.utils.TensorTransform.forward", false]], "forward() (botorch.sampling.stochastic_samplers.forkedrngsampler method)": [[10, "botorch.sampling.stochastic_samplers.ForkedRNGSampler.forward", false]], "forward() (botorch.sampling.stochastic_samplers.stochasticsampler method)": [[10, "botorch.sampling.stochastic_samplers.StochasticSampler.forward", false]], "forward() (botorch.test_functions.base.basetestproblem method)": [[12, "botorch.test_functions.base.BaseTestProblem.forward", false]], "forward() (botorch.utils.gp_sampling.gpdraw method)": [[14, "botorch.utils.gp_sampling.GPDraw.forward", false]], "forward() (botorch.utils.gp_sampling.randomfourierfeatures method)": [[14, "botorch.utils.gp_sampling.RandomFourierFeatures.forward", false]], "forward() (botorch.utils.rounding.onehotargmaxste static method)": [[14, "botorch.utils.rounding.OneHotArgmaxSTE.forward", false]], "forward() (botorch.utils.rounding.roundste static method)": [[14, "botorch.utils.rounding.RoundSTE.forward", false]], "forward() (botorch.utils.test_helpers.dummynonscalarizingposteriortransform method)": [[14, "botorch.utils.test_helpers.DummyNonScalarizingPosteriorTransform.forward", false]], "forward() (botorch.utils.test_helpers.simplegpytorchmodel method)": [[14, "botorch.utils.test_helpers.SimpleGPyTorchModel.forward", false]], "forwardbackwardclosure (class in botorch.optim.closures.core)": [[8, "botorch.optim.closures.core.ForwardBackwardClosure", false]], "from_joint_dataset() (botorch.utils.datasets.multitaskdataset class method)": [[14, "botorch.utils.datasets.MultiTaskDataset.from_joint_dataset", false]], "full_tree (botorch.optim.optimize.optimizeacqfinputs property)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.full_tree", false]], "fullybayesianacquisitionfunction (class in botorch.acquisition.bayesian_active_learning)": [[0, "botorch.acquisition.bayesian_active_learning.FullyBayesianAcquisitionFunction", false]], "fullybayesianposterior (class in botorch.posteriors.fully_bayesian)": [[9, "botorch.posteriors.fully_bayesian.FullyBayesianPosterior", false]], "funcs (botorch.utils.dispatcher.dispatcher attribute)": [[14, "botorch.utils.dispatcher.Dispatcher.funcs", false]], "functions (botorch.utils.testing.basetestproblemtestcasemixin property)": [[14, "botorch.utils.testing.BaseTestProblemTestCaseMixIn.functions", false]], "fval (botorch.optim.core.optimizationresult attribute)": [[8, "botorch.optim.core.OptimizationResult.fval", false]], "gaussian_update() (in module botorch.sampling.pathwise.update_strategies)": [[10, "botorch.sampling.pathwise.update_strategies.gaussian_update", false]], "gaussianmixtureposterior (class in botorch.posteriors.fully_bayesian)": [[9, "botorch.posteriors.fully_bayesian.GaussianMixturePosterior", false]], "gaussianpenalty (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.GaussianPenalty", false]], "gen_batch_initial_conditions() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.gen_batch_initial_conditions", false]], "gen_candidates (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.gen_candidates", false]], "gen_candidates_scipy() (in module botorch.generation.gen)": [[4, "botorch.generation.gen.gen_candidates_scipy", false]], "gen_candidates_torch() (in module botorch.generation.gen)": [[4, "botorch.generation.gen.gen_candidates_torch", false]], "gen_kernel_features() (in module botorch.sampling.pathwise.features.generators)": [[10, "botorch.sampling.pathwise.features.generators.gen_kernel_features", false]], "gen_loo_cv_folds() (in module botorch.cross_validation)": [[1, "botorch.cross_validation.gen_loo_cv_folds", false]], "gen_multi_task_dataset() (in module botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.gen_multi_task_dataset", false]], "gen_one_shot_hvkg_initial_conditions() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.gen_one_shot_hvkg_initial_conditions", false]], "gen_one_shot_kg_initial_conditions() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.gen_one_shot_kg_initial_conditions", false]], "gen_pareto_front() (botorch.test_functions.base.multiobjectivetestproblem method)": [[12, "botorch.test_functions.base.MultiObjectiveTestProblem.gen_pareto_front", false]], "gen_pareto_front() (botorch.test_functions.multi_objective.dtlz1 method)": [[12, "botorch.test_functions.multi_objective.DTLZ1.gen_pareto_front", false]], "gen_pareto_front() (botorch.test_functions.multi_objective.dtlz2 method)": [[12, "botorch.test_functions.multi_objective.DTLZ2.gen_pareto_front", false]], "gen_pareto_front() (botorch.test_functions.multi_objective.zdt1 method)": [[12, "botorch.test_functions.multi_objective.ZDT1.gen_pareto_front", false]], "gen_pareto_front() (botorch.test_functions.multi_objective.zdt2 method)": [[12, "botorch.test_functions.multi_objective.ZDT2.gen_pareto_front", false]], "gen_pareto_front() (botorch.test_functions.multi_objective.zdt3 method)": [[12, "botorch.test_functions.multi_objective.ZDT3.gen_pareto_front", false]], "gen_positional_indices() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.gen_positional_indices", false]], "gen_value_function_initial_conditions() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.gen_value_function_initial_conditions", false]], "generalizedlinearpath (class in botorch.sampling.pathwise.paths)": [[10, "botorch.sampling.pathwise.paths.GeneralizedLinearPath", false]], "generate_starting_points() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.generate_starting_points", false]], "genericcostawareutility (class in botorch.acquisition.cost_aware)": [[0, "botorch.acquisition.cost_aware.GenericCostAwareUtility", false]], "genericdeterministicmodel (class in botorch.models.deterministic)": [[7, "botorch.models.deterministic.GenericDeterministicModel", false]], "genericmcmultioutputobjective (class in botorch.acquisition.multi_objective.objective)": [[0, "botorch.acquisition.multi_objective.objective.GenericMCMultiOutputObjective", false]], "genericmcobjective (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.GenericMCObjective", false]], "get_acqf_input_constructor() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.get_acqf_input_constructor", false]], "get_acquisition_function() (in module botorch.acquisition.factory)": [[0, "botorch.acquisition.factory.get_acquisition_function", false]], "get_acquisition_function() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.get_acquisition_function", false]], "get_all_tasks() (botorch.models.multitask.multitaskgp class method)": [[7, "botorch.models.multitask.MultiTaskGP.get_all_tasks", false]], "get_attribute() (in module botorch.models.converter)": [[7, "botorch.models.converter.get_attribute", false]], "get_augmented_q_batch_size() (botorch.acquisition.acquisition.oneshotacquisitionfunction method)": [[0, "botorch.acquisition.acquisition.OneShotAcquisitionFunction.get_augmented_q_batch_size", false]], "get_augmented_q_batch_size() (botorch.acquisition.knowledge_gradient.qknowledgegradient method)": [[0, "botorch.acquisition.knowledge_gradient.qKnowledgeGradient.get_augmented_q_batch_size", false]], "get_augmented_q_batch_size() (botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qhypervolumeknowledgegradient method)": [[0, "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qHypervolumeKnowledgeGradient.get_augmented_q_batch_size", false]], "get_augmented_q_batch_size() (botorch.acquisition.multi_step_lookahead.qmultisteplookahead method)": [[0, "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead.get_augmented_q_batch_size", false]], "get_batch_dimensions() (botorch.models.gpytorch.batchedmultioutputgpytorchmodel static method)": [[7, "botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel.get_batch_dimensions", false]], "get_best_candidates() (in module botorch.generation.gen)": [[4, "botorch.generation.gen.get_best_candidates", false]], "get_best_f_analytic() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.get_best_f_analytic", false]], "get_best_f_mc() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.get_best_f_mc", false]], "get_bounds_as_ndarray() (in module botorch.optim.utils.numpy_utils)": [[8, "botorch.optim.utils.numpy_utils.get_bounds_as_ndarray", false]], "get_chebyshev_scalarization() (in module botorch.utils.multi_objective.scalarization)": [[14, "botorch.utils.multi_objective.scalarization.get_chebyshev_scalarization", false]], "get_constants() (in module botorch.utils.constants)": [[14, "botorch.utils.constants.get_constants", false]], "get_constants() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.get_constants", false]], "get_constants_like() (in module botorch.utils.constants)": [[14, "botorch.utils.constants.get_constants_like", false]], "get_constants_like() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.get_constants_like", false]], "get_covar_module_with_dim_scaled_prior() (in module botorch.models.utils.gpytorch_modules)": [[7, "botorch.models.utils.gpytorch_modules.get_covar_module_with_dim_scaled_prior", false]], "get_data_loader() (in module botorch.optim.utils.model_utils)": [[8, "botorch.optim.utils.model_utils.get_data_loader", false]], "get_dataset_without_task_feature() (botorch.utils.datasets.multitaskdataset method)": [[14, "botorch.utils.datasets.MultiTaskDataset.get_dataset_without_task_feature", false]], "get_default_partitioning_alpha() (in module botorch.acquisition.multi_objective.utils)": [[0, "botorch.acquisition.multi_objective.utils.get_default_partitioning_alpha", false]], "get_deterministic_model() (in module botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.get_deterministic_model", false]], "get_deterministic_model_list() (in module botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.get_deterministic_model_list", false]], "get_deterministic_model_multi_samples() (in module botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.get_deterministic_model_multi_samples", false]], "get_device_of_sequence() (in module botorch.acquisition.fixed_feature)": [[0, "botorch.acquisition.fixed_feature.get_device_of_sequence", false]], "get_dtype_of_sequence() (in module botorch.acquisition.fixed_feature)": [[0, "botorch.acquisition.fixed_feature.get_dtype_of_sequence", false]], "get_eval_gp_sample_callable() (in module botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.get_eval_gp_sample_callable", false]], "get_fantasy_model() (botorch.models.higher_order_gp.higherordergp method)": [[7, "botorch.models.higher_order_gp.HigherOrderGP.get_fantasy_model", false]], "get_feasible_samples() (in module botorch.utils.feasible_volume)": [[14, "botorch.utils.feasible_volume.get_feasible_samples", false]], "get_fully_bayesian_model() (in module botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.get_fully_bayesian_model", false]], "get_fully_bayesian_model_list() (in module botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.get_fully_bayesian_model_list", false]], "get_gaussian_likelihood_with_gamma_prior() (in module botorch.models.utils.gpytorch_modules)": [[7, "botorch.models.utils.gpytorch_modules.get_gaussian_likelihood_with_gamma_prior", false]], "get_gaussian_likelihood_with_lognormal_prior() (in module botorch.models.utils.gpytorch_modules)": [[7, "botorch.models.utils.gpytorch_modules.get_gaussian_likelihood_with_lognormal_prior", false]], "get_gp_samples() (in module botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.get_gp_samples", false]], "get_hypercell_bounds() (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.get_hypercell_bounds", false]], "get_hypercell_bounds() (botorch.utils.multi_objective.box_decompositions.box_decomposition.fastpartitioning method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.FastPartitioning.get_hypercell_bounds", false]], "get_hypercell_bounds() (botorch.utils.multi_objective.box_decompositions.box_decomposition_list.boxdecompositionlist method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList.get_hypercell_bounds", false]], "get_hypercell_bounds() (botorch.utils.multi_objective.box_decompositions.non_dominated.nondominatedpartitioning method)": [[14, "botorch.utils.multi_objective.box_decompositions.non_dominated.NondominatedPartitioning.get_hypercell_bounds", false]], "get_ic_generator() (botorch.optim.optimize.optimizeacqfinputs method)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.get_ic_generator", false]], "get_index_tensors() (in module botorch.utils.probability.lin_ess)": [[14, "botorch.utils.probability.lin_ess.get_index_tensors", false]], "get_induced_fantasy_model() (botorch.acquisition.multi_step_lookahead.qmultisteplookahead method)": [[0, "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead.get_induced_fantasy_model", false]], "get_infeasible_cost() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.get_infeasible_cost", false]], "get_init_args() (botorch.models.transforms.input.normalize method)": [[7, "botorch.models.transforms.input.Normalize.get_init_args", false]], "get_init_args() (botorch.models.transforms.input.round method)": [[7, "botorch.models.transforms.input.Round.get_init_args", false]], "get_input_transform() (in module botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.get_input_transform", false]], "get_loss_closure() (in module botorch.optim.closures.model_closures)": [[8, "botorch.optim.closures.model_closures.get_loss_closure", false]], "get_loss_closure_with_grads() (in module botorch.optim.closures.model_closures)": [[8, "botorch.optim.closures.model_closures.get_loss_closure_with_grads", false]], "get_matern_kernel_with_gamma_prior() (in module botorch.models.utils.gpytorch_modules)": [[7, "botorch.models.utils.gpytorch_modules.get_matern_kernel_with_gamma_prior", false]], "get_matheron_path_model() (in module botorch.sampling.pathwise.posterior_samplers)": [[10, "botorch.sampling.pathwise.posterior_samplers.get_matheron_path_model", false]], "get_model() (in module botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.get_model", false]], "get_monotonicity_constraints() (in module botorch.utils.constraints)": [[14, "botorch.utils.constraints.get_monotonicity_constraints", false]], "get_multi_step_tree_input_representation() (botorch.acquisition.multi_step_lookahead.qmultisteplookahead method)": [[0, "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead.get_multi_step_tree_input_representation", false]], "get_mvar_set_vectorized() (botorch.acquisition.multi_objective.multi_output_risk_measures.mvar method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MVaR.get_mvar_set_vectorized", false]], "get_mvar_set_via_counting() (botorch.acquisition.multi_objective.multi_output_risk_measures.mvar method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MVaR.get_mvar_set_via_counting", false]], "get_name_filter() (in module botorch.optim.utils.model_utils)": [[8, "botorch.optim.utils.model_utils.get_name_filter", false]], "get_nearest_neighbors() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.get_nearest_neighbors", false]], "get_objective_weights_transform() (in module botorch.utils.objective)": [[14, "botorch.utils.objective.get_objective_weights_transform", false]], "get_optimal_samples() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.get_optimal_samples", false]], "get_outcome_constraint_transforms() (in module botorch.utils.constraints)": [[14, "botorch.utils.constraints.get_outcome_constraint_transforms", false]], "get_outcome_feasibility_probability() (in module botorch.utils.feasible_volume)": [[14, "botorch.utils.feasible_volume.get_outcome_feasibility_probability", false]], "get_output_transform() (in module botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.get_output_transform", false]], "get_parameters() (in module botorch.optim.utils.model_utils)": [[8, "botorch.optim.utils.model_utils.get_parameters", false]], "get_parameters_and_bounds() (in module botorch.optim.utils.model_utils)": [[8, "botorch.optim.utils.model_utils.get_parameters_and_bounds", false]], "get_partition_bounds() (in module botorch.utils.multi_objective.box_decompositions.utils)": [[14, "botorch.utils.multi_objective.box_decompositions.utils.get_partition_bounds", false]], "get_polytope_samples() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.get_polytope_samples", false]], "get_posterior_samples() (botorch.acquisition.acquisition.mcsamplermixin method)": [[0, "botorch.acquisition.acquisition.MCSamplerMixin.get_posterior_samples", false]], "get_pvar_expected() (in module botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.get_pvar_expected", false]], "get_rounding_input_transform() (in module botorch.models.transforms.factory)": [[7, "botorch.models.transforms.factory.get_rounding_input_transform", false]], "get_sample_moments() (in module botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.get_sample_moments", false]], "get_sampler() (in module botorch.sampling.get_sampler)": [[10, "botorch.sampling.get_sampler.get_sampler", false]], "get_split_shapes() (botorch.acquisition.multi_step_lookahead.qmultisteplookahead method)": [[0, "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead.get_split_shapes", false]], "get_spray_points() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.get_spray_points", false]], "get_task_value_remapping() (in module botorch.models.multitask)": [[7, "botorch.models.multitask.get_task_value_remapping", false]], "get_tensors_as_ndarray_1d() (in module botorch.optim.utils.numpy_utils)": [[8, "botorch.optim.utils.numpy_utils.get_tensors_as_ndarray_1d", false]], "get_train_inputs() (in module botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.get_train_inputs", false]], "get_train_targets() (in module botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.get_train_targets", false]], "get_weights_posterior() (in module botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.get_weights_posterior", false]], "get_x_baseline() (in module botorch.optim.utils.acquisition_utils)": [[8, "botorch.optim.utils.acquisition_utils.get_X_baseline", false]], "gmm (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.GMM", false]], "gpdraw (class in botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.GPDraw", false]], "gpt_posterior_settings() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.gpt_posterior_settings", false]], "gpytorchmodel (class in botorch.models.gpytorch)": [[7, "botorch.models.gpytorch.GPyTorchModel", false]], "gpytorchposterior (class in botorch.posteriors.gpytorch)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior", false]], "greedyimprovementreduction (class in botorch.models.utils.inducing_point_allocators)": [[7, "botorch.models.utils.inducing_point_allocators.GreedyImprovementReduction", false]], "greedyvariancereduction (class in botorch.models.utils.inducing_point_allocators)": [[7, "botorch.models.utils.inducing_point_allocators.GreedyVarianceReduction", false]], "griewank (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Griewank", false]], "group_lasso_regularizer() (in module botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.group_lasso_regularizer", false]], "grouplassopenalty (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.GroupLassoPenalty", false]], "gsobol (class in botorch.test_functions.sensitivity_analysis)": [[12, "botorch.test_functions.sensitivity_analysis.Gsobol", false]], "hartmann (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Hartmann", false]], "help() (botorch.utils.dispatcher.dispatcher method)": [[14, "botorch.utils.dispatcher.Dispatcher.help", false]], "higherordergp (class in botorch.models.higher_order_gp)": [[7, "botorch.models.higher_order_gp.HigherOrderGP", false]], "higherordergpposterior (class in botorch.posteriors.higher_order)": [[9, "botorch.posteriors.higher_order.HigherOrderGPPosterior", false]], "hitandrunpolytopesampler (class in botorch.utils.sampling)": [[14, "botorch.utils.sampling.HitAndRunPolytopeSampler", false]], "holdertable (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.HolderTable", false]], "homotopy (class in botorch.optim.homotopy)": [[8, "botorch.optim.homotopy.Homotopy", false]], "homotopyparameter (class in botorch.optim.homotopy)": [[8, "botorch.optim.homotopy.HomotopyParameter", false]], "hypervolume (class in botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.Hypervolume", false]], "ic_gen_kwargs (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.ic_gen_kwargs", false]], "ic_generator (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.ic_generator", false]], "identitymcmultioutputobjective (class in botorch.acquisition.multi_objective.objective)": [[0, "botorch.acquisition.multi_objective.objective.IdentityMCMultiOutputObjective", false]], "identitymcobjective (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.IdentityMCObjective", false]], "identitystefunction (class in botorch.utils.rounding)": [[14, "botorch.utils.rounding.IdentitySTEFunction", false]], "iidnormalsampler (class in botorch.sampling.normal)": [[10, "botorch.sampling.normal.IIDNormalSampler", false]], "independentcvar (class in botorch.acquisition.multi_objective.multi_output_risk_measures)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentCVaR", false]], "independentvar (class in botorch.acquisition.multi_objective.multi_output_risk_measures)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentVaR", false]], "indexsampler (class in botorch.sampling.index_sampler)": [[10, "botorch.sampling.index_sampler.IndexSampler", false]], "indices (botorch.utils.containers.slicecontainer attribute)": [[14, "botorch.utils.containers.SliceContainer.indices", false]], "inducingpointallocator (class in botorch.models.utils.inducing_point_allocators)": [[7, "botorch.models.utils.inducing_point_allocators.InducingPointAllocator", false]], "inequality_constraints (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.inequality_constraints", false]], "infer_reference_point() (in module botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.infer_reference_point", false]], "infinitewidthbnnkernel (class in botorch.models.kernels.infinite_width_bnn)": [[7, "botorch.models.kernels.infinite_width_bnn.InfiniteWidthBNNKernel", false]], "init_inducing_points() (botorch.models.approximate_gp.singletaskvariationalgp method)": [[7, "botorch.models.approximate_gp.SingleTaskVariationalGP.init_inducing_points", false]], "initialize_q_batch() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.initialize_q_batch", false]], "initialize_q_batch_nonneg() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.initialize_q_batch_nonneg", false]], "input_transform (botorch.sampling.pathwise.features.maps.featuremap attribute)": [[10, "botorch.sampling.pathwise.features.maps.FeatureMap.input_transform", false]], "input_transform (botorch.sampling.pathwise.utils.transformedmodulemixin attribute)": [[10, "botorch.sampling.pathwise.utils.TransformedModuleMixin.input_transform", false]], "inputdataerror": [[2, "botorch.exceptions.errors.InputDataError", false]], "inputdatawarning": [[2, "botorch.exceptions.warnings.InputDataWarning", false]], "inputperturbation (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.InputPerturbation", false]], "inputstandardize (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.InputStandardize", false]], "inputtransform (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.InputTransform", false]], "interaction_features() (in module botorch.models.transforms.utils)": [[7, "botorch.models.transforms.utils.interaction_features", false]], "interactionfeatures (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.InteractionFeatures", false]], "inversecostweightedutility (class in botorch.acquisition.cost_aware)": [[0, "botorch.acquisition.cost_aware.InverseCostWeightedUtility", false]], "inverselengthscaletransform (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.InverseLengthscaleTransform", false]], "is_ensemble() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.is_ensemble", false]], "is_feasible() (botorch.test_functions.base.constrainedbasetestproblem method)": [[12, "botorch.test_functions.base.ConstrainedBaseTestProblem.is_feasible", false]], "is_fully_bayesian() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.is_fully_bayesian", false]], "is_non_dominated() (in module botorch.utils.multi_objective.pareto)": [[14, "botorch.utils.multi_objective.pareto.is_non_dominated", false]], "is_nonnegative() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.is_nonnegative", false]], "is_one_to_many (botorch.models.transforms.input.appendfeatures attribute)": [[7, "botorch.models.transforms.input.AppendFeatures.is_one_to_many", false]], "is_one_to_many (botorch.models.transforms.input.inputperturbation attribute)": [[7, "botorch.models.transforms.input.InputPerturbation.is_one_to_many", false]], "is_one_to_many (botorch.models.transforms.input.inputtransform attribute)": [[7, "botorch.models.transforms.input.InputTransform.is_one_to_many", false]], "ishigami (class in botorch.test_functions.sensitivity_analysis)": [[12, "botorch.test_functions.sensitivity_analysis.Ishigami", false]], "items() (botorch.sampling.pathwise.paths.pathdict method)": [[10, "botorch.sampling.pathwise.paths.PathDict.items", false]], "items() (botorch.utils.torch.bufferdict method)": [[14, "botorch.utils.torch.BufferDict.items", false]], "k (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.K", false]], "k_1 (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.K_1", false]], "k_2 (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.K_2", false]], "k_d (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.k_d", false]], "k_g (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.k_g", false]], "k_i (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.K_I", false]], "k_p (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.K_p", false]], "k_x (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.K_X", false]], "kernelevaluationmap (class in botorch.sampling.pathwise.features.maps)": [[10, "botorch.sampling.pathwise.features.maps.KernelEvaluationMap", false]], "kernelfeaturemap (class in botorch.sampling.pathwise.features.maps)": [[10, "botorch.sampling.pathwise.features.maps.KernelFeatureMap", false]], "keys() (botorch.sampling.pathwise.paths.pathdict method)": [[10, "botorch.sampling.pathwise.paths.PathDict.keys", false]], "keys() (botorch.utils.torch.bufferdict method)": [[14, "botorch.utils.torch.BufferDict.keys", false]], "kroneckermultitaskgp (class in botorch.models.multitask)": [[7, "botorch.models.multitask.KroneckerMultiTaskGP", false]], "l0approximation (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.L0Approximation", false]], "l0penaltyapprox (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.L0PenaltyApprox", false]], "l0penaltyapproxobjective (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.L0PenaltyApproxObjective", false]], "l1penalty (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.L1Penalty", false]], "l1penaltyobjective (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.L1PenaltyObjective", false]], "l2penalty (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.L2Penalty", false]], "la2() (botorch.test_functions.multi_objective.mw7 method)": [[12, "botorch.test_functions.multi_objective.MW7.LA2", false]], "lambd (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.lambd", false]], "last_fantasize_flag (botorch.utils.test_helpers.simplegpytorchmodel attribute)": [[14, "botorch.utils.test_helpers.SimpleGPyTorchModel.last_fantasize_flag", false]], "lceagp (class in botorch.models.contextual)": [[7, "botorch.models.contextual.LCEAGP", false]], "lceakernel (class in botorch.models.kernels.contextual_lcea)": [[7, "botorch.models.kernels.contextual_lcea.LCEAKernel", false]], "lcemgp (class in botorch.models.contextual_multioutput)": [[7, "botorch.models.contextual_multioutput.LCEMGP", false]], "learn_bounds (botorch.models.transforms.input.normalize property)": [[7, "botorch.models.transforms.input.Normalize.learn_bounds", false]], "learn_coefficients (botorch.models.transforms.input.affineinputtransform property)": [[7, "botorch.models.transforms.input.AffineInputTransform.learn_coefficients", false]], "learnedobjective (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.LearnedObjective", false]], "legacy_ei_numerics_warning() (in module botorch.exceptions.warnings)": [[2, "botorch.exceptions.warnings.legacy_ei_numerics_warning", false]], "leggauss() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.leggauss", false]], "level (botorch.settings.log_level attribute)": [[11, "botorch.settings.log_level.level", false]], "levy (botorch.test_functions.multi_objective.toyrobust attribute)": [[12, "botorch.test_functions.multi_objective.ToyRobust.levy", false]], "levy (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Levy", false]], "lifetime_samples (botorch.utils.probability.lin_ess.linearellipticalslicesampler property)": [[14, "botorch.utils.probability.lin_ess.LinearEllipticalSliceSampler.lifetime_samples", false]], "likelihood (botorch.models.gpytorch.gpytorchmodel attribute)": [[7, "botorch.models.gpytorch.GPyTorchModel.likelihood", false]], "lin_constraint_jac() (in module botorch.optim.parameter_constraints)": [[8, "botorch.optim.parameter_constraints.lin_constraint_jac", false]], "linearellipticalslicesampler (class in botorch.utils.probability.lin_ess)": [[14, "botorch.utils.probability.lin_ess.LinearEllipticalSliceSampler", false]], "linearhomotopyschedule (class in botorch.optim.homotopy)": [[8, "botorch.optim.homotopy.LinearHomotopySchedule", false]], "linearmcobjective (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.LinearMCObjective", false]], "lineartruncatedfidelitykernel (class in botorch.models.kernels.linear_truncated_fidelity)": [[7, "botorch.models.kernels.linear_truncated_fidelity.LinearTruncatedFidelityKernel", false]], "listsampler (class in botorch.sampling.list_sampler)": [[10, "botorch.sampling.list_sampler.ListSampler", false]], "load_mcmc_samples() (botorch.models.fully_bayesian.pyromodel method)": [[7, "botorch.models.fully_bayesian.PyroModel.load_mcmc_samples", false]], "load_mcmc_samples() (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp method)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.load_mcmc_samples", false]], "load_mcmc_samples() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.load_mcmc_samples", false]], "load_mcmc_samples() (botorch.models.fully_bayesian_multitask.multitasksaaspyromodel method)": [[7, "botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel.load_mcmc_samples", false]], "load_mcmc_samples() (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp method)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.load_mcmc_samples", false]], "load_state_dict() (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp method)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.load_state_dict", false]], "load_state_dict() (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp method)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.load_state_dict", false]], "load_state_dict() (botorch.models.model.modellist method)": [[7, "botorch.models.model.ModelList.load_state_dict", false]], "load_state_dict() (botorch.models.pairwise_gp.pairwisegp method)": [[7, "botorch.models.pairwise_gp.PairwiseGP.load_state_dict", false]], "load_state_dict() (botorch.utils.testing.mockmodel method)": [[14, "botorch.utils.testing.MockModel.load_state_dict", false]], "log (class in botorch.models.transforms.outcome)": [[7, "botorch.models.transforms.outcome.Log", false]], "log() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.log", false]], "log10 (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.Log10", false]], "log1mexp() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.log1mexp", false]], "log1pexp() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.log1pexp", false]], "log_cdf_robust() (in module botorch.acquisition.multi_objective.predictive_entropy_search)": [[0, "botorch.acquisition.multi_objective.predictive_entropy_search.log_cdf_robust", false]], "log_erfc() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.log_erfc", false]], "log_erfcx() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.log_erfcx", false]], "log_fatmoid() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.log_fatmoid", false]], "log_fatplus() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.log_fatplus", false]], "log_level (class in botorch.settings)": [[11, "botorch.settings.log_level", false]], "log_ndtr() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.log_ndtr", false]], "log_p() (botorch.models.likelihoods.pairwise.pairwiselikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLikelihood.log_p", false]], "log_p() (botorch.models.likelihoods.pairwise.pairwiselogitlikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLogitLikelihood.log_p", false]], "log_partition (botorch.utils.probability.truncated_multivariate_normal.truncatedmultivariatenormal property)": [[14, "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal.log_partition", false]], "log_phi() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.log_phi", false]], "log_prob (botorch.utils.probability.mvnxpb.mvnxpbstate attribute)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState.log_prob", false]], "log_prob() (botorch.utils.probability.truncated_multivariate_normal.truncatedmultivariatenormal method)": [[14, "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal.log_prob", false]], "log_prob() (botorch.utils.probability.unified_skew_normal.unifiedskewnormal method)": [[14, "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal.log_prob", false]], "log_prob_extra (botorch.utils.probability.mvnxpb.mvnxpbstate attribute)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState.log_prob_extra", false]], "log_prob_normal_in() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.log_prob_normal_in", false]], "log_softplus() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.log_softplus", false]], "logconstrainedexpectedimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.LogConstrainedExpectedImprovement", false]], "logdiffexp() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.logdiffexp", false]], "logexpectedimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.LogExpectedImprovement", false]], "logexpit() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.logexpit", false]], "logimprovementmcacquisitionfunction (class in botorch.acquisition.logei)": [[0, "botorch.acquisition.logei.LogImprovementMCAcquisitionFunction", false]], "loglinearhomotopyschedule (class in botorch.optim.homotopy)": [[8, "botorch.optim.homotopy.LogLinearHomotopySchedule", false]], "logmeanexp() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.logmeanexp", false]], "lognoisyexpectedimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.LogNoisyExpectedImprovement", false]], "lognorm_to_norm() (in module botorch.models.transforms.utils)": [[7, "botorch.models.transforms.utils.lognorm_to_norm", false]], "logplusexp() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.logplusexp", false]], "logprobabilityofimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.LogProbabilityOfImprovement", false]], "logsumexp() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.logsumexp", false]], "lowerboundmultiobjectiveentropysearch (class in botorch.acquisition.multi_objective.joint_entropy_search)": [[0, "botorch.acquisition.multi_objective.joint_entropy_search.LowerBoundMultiObjectiveEntropySearch", false]], "m_x (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.m_X", false]], "make_best_f() (in module botorch.acquisition.multi_step_lookahead)": [[0, "botorch.acquisition.multi_step_lookahead.make_best_f", false]], "make_differentiable() (botorch.acquisition.multi_objective.multi_output_risk_measures.mvar method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MVaR.make_differentiable", false]], "make_posterior_variances() (botorch.models.higher_order_gp.higherordergp method)": [[7, "botorch.models.higher_order_gp.HigherOrderGP.make_posterior_variances", false]], "make_scipy_bounds() (in module botorch.optim.parameter_constraints)": [[8, "botorch.optim.parameter_constraints.make_scipy_bounds", false]], "make_scipy_linear_constraints() (in module botorch.optim.parameter_constraints)": [[8, "botorch.optim.parameter_constraints.make_scipy_linear_constraints", false]], "make_scipy_nonlinear_inequality_constraints() (in module botorch.optim.parameter_constraints)": [[8, "botorch.optim.parameter_constraints.make_scipy_nonlinear_inequality_constraints", false]], "manual_seed() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.manual_seed", false]], "mars (class in botorch.acquisition.multi_objective.multi_output_risk_measures)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MARS", false]], "match_batch_shape() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.match_batch_shape", false]], "matern52_kernel() (in module botorch.models.fully_bayesian)": [[7, "botorch.models.fully_bayesian.matern52_kernel", false]], "matheronpath (class in botorch.sampling.pathwise.posterior_samplers)": [[10, "botorch.sampling.pathwise.posterior_samplers.MatheronPath", false]], "max_hv (botorch.test_functions.base.multiobjectivetestproblem property)": [[12, "botorch.test_functions.base.MultiObjectiveTestProblem.max_hv", false]], "maximize_samples() (botorch.generation.sampling.maxposteriorsampling method)": [[4, "botorch.generation.sampling.MaxPosteriorSampling.maximize_samples", false]], "maxposteriorsampling (class in botorch.generation.sampling)": [[4, "botorch.generation.sampling.MaxPosteriorSampling", false]], "maxvaluebase (class in botorch.acquisition.max_value_entropy_search)": [[0, "botorch.acquisition.max_value_entropy_search.MaxValueBase", false]], "mcacquisitionfunction (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.MCAcquisitionFunction", false]], "mcacquisitionobjective (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.MCAcquisitionObjective", false]], "mcmultioutputobjective (class in botorch.acquisition.multi_objective.objective)": [[0, "botorch.acquisition.multi_objective.objective.MCMultiOutputObjective", false]], "mcsampler (class in botorch.sampling.base)": [[10, "botorch.sampling.base.MCSampler", false]], "mcsamplermixin (class in botorch.acquisition.acquisition)": [[0, "botorch.acquisition.acquisition.MCSamplerMixin", false]], "mean (botorch.posteriors.ensemble.ensembleposterior property)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.mean", false]], "mean (botorch.posteriors.gpytorch.gpytorchposterior property)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.mean", false]], "mean (botorch.posteriors.posterior_list.posteriorlist property)": [[9, "botorch.posteriors.posterior_list.PosteriorList.mean", false]], "mean (botorch.posteriors.transformed.transformedposterior property)": [[9, "botorch.posteriors.transformed.TransformedPosterior.mean", false]], "mean (botorch.utils.testing.mockposterior property)": [[14, "botorch.utils.testing.MockPosterior.mean", false]], "means (botorch.models.transforms.input.inputstandardize property)": [[7, "botorch.models.transforms.input.InputStandardize.means", false]], "median_lengthscale (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp property)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.median_lengthscale", false]], "median_lengthscale (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp property)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.median_lengthscale", false]], "message (botorch.optim.core.optimizationresult attribute)": [[8, "botorch.optim.core.OptimizationResult.message", false]], "michalewicz (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Michalewicz", false]], "minimize_with_timeout() (in module botorch.optim.utils.timeout)": [[8, "botorch.optim.utils.timeout.minimize_with_timeout", false]], "mins (botorch.models.transforms.input.normalize property)": [[7, "botorch.models.transforms.input.Normalize.mins", false]], "mixedsingletaskgp (class in botorch.models.gp_regression_mixed)": [[7, "botorch.models.gp_regression_mixed.MixedSingleTaskGP", false]], "mixture_covariance_matrix (botorch.posteriors.fully_bayesian.gaussianmixtureposterior property)": [[9, "botorch.posteriors.fully_bayesian.GaussianMixturePosterior.mixture_covariance_matrix", false]], "mixture_mean (botorch.posteriors.fully_bayesian.gaussianmixtureposterior property)": [[9, "botorch.posteriors.fully_bayesian.GaussianMixturePosterior.mixture_mean", false]], "mixture_variance (botorch.posteriors.fully_bayesian.gaussianmixtureposterior property)": [[9, "botorch.posteriors.fully_bayesian.GaussianMixturePosterior.mixture_variance", false]], "mock_optimize() (in module botorch.test_utils.mock)": [[13, "botorch.test_utils.mock.mock_optimize", false]], "mock_optimize_context_manager() (in module botorch.test_utils.mock)": [[13, "botorch.test_utils.mock.mock_optimize_context_manager", false]], "mockacquisitionfunction (class in botorch.utils.testing)": [[14, "botorch.utils.testing.MockAcquisitionFunction", false]], "mockmodel (class in botorch.utils.testing)": [[14, "botorch.utils.testing.MockModel", false]], "mockposterior (class in botorch.utils.testing)": [[14, "botorch.utils.testing.MockPosterior", false]], "mod_batch_shape() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.mod_batch_shape", false]], "model (botorch.cross_validation.cvresults attribute)": [[1, "botorch.cross_validation.CVResults.model", false]], "model (class in botorch.models.model)": [[7, "botorch.models.model.Model", false]], "model_list_to_batched() (in module botorch.models.converter)": [[7, "botorch.models.converter.model_list_to_batched", false]], "modeldict (class in botorch.models.model)": [[7, "botorch.models.model.ModelDict", false]], "modelfittingerror": [[2, "botorch.exceptions.errors.ModelFittingError", false]], "modellist (class in botorch.models.model)": [[7, "botorch.models.model.ModelList", false]], "modellistgp (class in botorch.models.model_list_gp_regression)": [[7, "botorch.models.model_list_gp_regression.ModelListGP", false]], "modellistgpytorchmodel (class in botorch.models.gpytorch)": [[7, "botorch.models.gpytorch.ModelListGPyTorchModel", false]], "module": [[0, "module-botorch.acquisition", false], [0, "module-botorch.acquisition.acquisition", false], [0, "module-botorch.acquisition.active_learning", false], [0, "module-botorch.acquisition.analytic", false], [0, "module-botorch.acquisition.bayesian_active_learning", false], [0, "module-botorch.acquisition.cached_cholesky", false], [0, "module-botorch.acquisition.cost_aware", false], [0, "module-botorch.acquisition.decoupled", false], [0, "module-botorch.acquisition.factory", false], [0, "module-botorch.acquisition.fixed_feature", false], [0, "module-botorch.acquisition.input_constructors", false], [0, "module-botorch.acquisition.joint_entropy_search", false], [0, "module-botorch.acquisition.knowledge_gradient", false], [0, "module-botorch.acquisition.logei", false], [0, "module-botorch.acquisition.max_value_entropy_search", false], [0, "module-botorch.acquisition.monte_carlo", false], [0, "module-botorch.acquisition.multi_objective.analytic", false], [0, "module-botorch.acquisition.multi_objective.base", false], [0, "module-botorch.acquisition.multi_objective.hypervolume_knowledge_gradient", false], [0, "module-botorch.acquisition.multi_objective.joint_entropy_search", false], [0, "module-botorch.acquisition.multi_objective.logei", false], [0, "module-botorch.acquisition.multi_objective.max_value_entropy_search", false], [0, "module-botorch.acquisition.multi_objective.monte_carlo", false], [0, "module-botorch.acquisition.multi_objective.multi_fidelity", false], [0, "module-botorch.acquisition.multi_objective.multi_output_risk_measures", false], [0, "module-botorch.acquisition.multi_objective.objective", false], [0, "module-botorch.acquisition.multi_objective.parego", false], [0, "module-botorch.acquisition.multi_objective.predictive_entropy_search", false], [0, "module-botorch.acquisition.multi_objective.utils", false], [0, "module-botorch.acquisition.multi_step_lookahead", false], [0, "module-botorch.acquisition.objective", false], [0, "module-botorch.acquisition.penalized", false], [0, "module-botorch.acquisition.predictive_entropy_search", false], [0, "module-botorch.acquisition.preference", false], [0, "module-botorch.acquisition.prior_guided", false], [0, "module-botorch.acquisition.proximal", false], [0, "module-botorch.acquisition.risk_measures", false], [0, "module-botorch.acquisition.thompson_sampling", false], [0, "module-botorch.acquisition.utils", false], [1, "module-botorch.cross_validation", false], [2, "module-botorch.exceptions", false], [2, "module-botorch.exceptions.errors", false], [2, "module-botorch.exceptions.warnings", false], [3, "module-botorch.fit", false], [4, "module-botorch.generation", false], [4, "module-botorch.generation.gen", false], [4, "module-botorch.generation.sampling", false], [4, "module-botorch.generation.utils", false], [6, "module-botorch.logging", false], [7, "module-botorch.models", false], [7, "module-botorch.models.approximate_gp", false], [7, "module-botorch.models.contextual", false], [7, "module-botorch.models.contextual_multioutput", false], [7, "module-botorch.models.converter", false], [7, "module-botorch.models.cost", false], [7, "module-botorch.models.deterministic", false], [7, "module-botorch.models.ensemble", false], [7, "module-botorch.models.fully_bayesian", false], [7, "module-botorch.models.fully_bayesian_multitask", false], [7, "module-botorch.models.gp_regression", false], [7, "module-botorch.models.gp_regression_fidelity", false], [7, "module-botorch.models.gp_regression_mixed", false], [7, "module-botorch.models.gpytorch", false], [7, "module-botorch.models.higher_order_gp", false], [7, "module-botorch.models.kernels.categorical", false], [7, "module-botorch.models.kernels.contextual_lcea", false], [7, "module-botorch.models.kernels.contextual_sac", false], [7, "module-botorch.models.kernels.downsampling", false], [7, "module-botorch.models.kernels.exponential_decay", false], [7, "module-botorch.models.kernels.infinite_width_bnn", false], [7, "module-botorch.models.kernels.linear_truncated_fidelity", false], [7, "module-botorch.models.kernels.orthogonal_additive_kernel", false], [7, "module-botorch.models.likelihoods.pairwise", false], [7, "module-botorch.models.model", false], [7, "module-botorch.models.model_list_gp_regression", false], [7, "module-botorch.models.multitask", false], [7, "module-botorch.models.pairwise_gp", false], [7, "module-botorch.models.transforms.factory", false], [7, "module-botorch.models.transforms.input", false], [7, "module-botorch.models.transforms.outcome", false], [7, "module-botorch.models.transforms.utils", false], [7, "module-botorch.models.utils.assorted", false], [7, "module-botorch.models.utils.gpytorch_modules", false], [7, "module-botorch.models.utils.inducing_point_allocators", false], [8, "module-botorch.optim", false], [8, "module-botorch.optim.closures.core", false], [8, "module-botorch.optim.closures.model_closures", false], [8, "module-botorch.optim.core", false], [8, "module-botorch.optim.fit", false], [8, "module-botorch.optim.homotopy", false], [8, "module-botorch.optim.initializers", false], [8, "module-botorch.optim.optimize", false], [8, "module-botorch.optim.optimize_homotopy", false], [8, "module-botorch.optim.optimize_mixed", false], [8, "module-botorch.optim.parameter_constraints", false], [8, "module-botorch.optim.stopping", false], [8, "module-botorch.optim.utils.acquisition_utils", false], [8, "module-botorch.optim.utils.common", false], [8, "module-botorch.optim.utils.model_utils", false], [8, "module-botorch.optim.utils.numpy_utils", false], [8, "module-botorch.optim.utils.timeout", false], [9, "module-botorch.posteriors", false], [9, "module-botorch.posteriors.base_samples", false], [9, "module-botorch.posteriors.ensemble", false], [9, "module-botorch.posteriors.fully_bayesian", false], [9, "module-botorch.posteriors.gpytorch", false], [9, "module-botorch.posteriors.higher_order", false], [9, "module-botorch.posteriors.multitask", false], [9, "module-botorch.posteriors.posterior", false], [9, "module-botorch.posteriors.posterior_list", false], [9, "module-botorch.posteriors.torch", false], [9, "module-botorch.posteriors.transformed", false], [10, "module-botorch.sampling", false], [10, "module-botorch.sampling.base", false], [10, "module-botorch.sampling.get_sampler", false], [10, "module-botorch.sampling.index_sampler", false], [10, "module-botorch.sampling.list_sampler", false], [10, "module-botorch.sampling.normal", false], [10, "module-botorch.sampling.pairwise_samplers", false], [10, "module-botorch.sampling.pathwise.features.generators", false], [10, "module-botorch.sampling.pathwise.features.maps", false], [10, "module-botorch.sampling.pathwise.paths", false], [10, "module-botorch.sampling.pathwise.posterior_samplers", false], [10, "module-botorch.sampling.pathwise.prior_samplers", false], [10, "module-botorch.sampling.pathwise.update_strategies", false], [10, "module-botorch.sampling.pathwise.utils", false], [10, "module-botorch.sampling.qmc", false], [10, "module-botorch.sampling.stochastic_samplers", false], [11, "module-botorch.settings", false], [12, "module-botorch.test_functions", false], [12, "module-botorch.test_functions.base", false], [12, "module-botorch.test_functions.multi_fidelity", false], [12, "module-botorch.test_functions.multi_objective", false], [12, "module-botorch.test_functions.multi_objective_multi_fidelity", false], [12, "module-botorch.test_functions.sensitivity_analysis", false], [12, "module-botorch.test_functions.synthetic", false], [12, "module-botorch.test_functions.utils", false], [13, "module-botorch.test_utils", false], [13, "module-botorch.test_utils.mock", false], [14, "module-botorch.utils", false], [14, "module-botorch.utils.constants", false], [14, "module-botorch.utils.constraints", false], [14, "module-botorch.utils.containers", false], [14, "module-botorch.utils.context_managers", false], [14, "module-botorch.utils.datasets", false], [14, "module-botorch.utils.dispatcher", false], [14, "module-botorch.utils.feasible_volume", false], [14, "module-botorch.utils.gp_sampling", false], [14, "module-botorch.utils.low_rank", false], [14, "module-botorch.utils.multi_objective.box_decompositions.box_decomposition", false], [14, "module-botorch.utils.multi_objective.box_decompositions.box_decomposition_list", false], [14, "module-botorch.utils.multi_objective.box_decompositions.dominated", false], [14, "module-botorch.utils.multi_objective.box_decompositions.non_dominated", false], [14, "module-botorch.utils.multi_objective.box_decompositions.utils", false], [14, "module-botorch.utils.multi_objective.hypervolume", false], [14, "module-botorch.utils.multi_objective.pareto", false], [14, "module-botorch.utils.multi_objective.scalarization", false], [14, "module-botorch.utils.multitask", false], [14, "module-botorch.utils.objective", false], [14, "module-botorch.utils.probability.bvn", false], [14, "module-botorch.utils.probability.lin_ess", false], [14, "module-botorch.utils.probability.linalg", false], [14, "module-botorch.utils.probability.mvnxpb", false], [14, "module-botorch.utils.probability.truncated_multivariate_normal", false], [14, "module-botorch.utils.probability.unified_skew_normal", false], [14, "module-botorch.utils.probability.utils", false], [14, "module-botorch.utils.rounding", false], [14, "module-botorch.utils.safe_math", false], [14, "module-botorch.utils.sampling", false], [14, "module-botorch.utils.test_helpers", false], [14, "module-botorch.utils.testing", false], [14, "module-botorch.utils.torch", false], [14, "module-botorch.utils.transforms", false], [14, "module-botorch.utils.types", false]], "module_rollback_ctx() (in module botorch.utils.context_managers)": [[14, "botorch.utils.context_managers.module_rollback_ctx", false]], "momf (class in botorch.acquisition.multi_objective.multi_fidelity)": [[0, "botorch.acquisition.multi_objective.multi_fidelity.MOMF", false]], "momfbranincurrin (class in botorch.test_functions.multi_objective_multi_fidelity)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFBraninCurrin", false]], "momfpark (class in botorch.test_functions.multi_objective_multi_fidelity)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFPark", false]], "morris (class in botorch.test_functions.sensitivity_analysis)": [[12, "botorch.test_functions.sensitivity_analysis.Morris", false]], "mu_p (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.mu_p", false]], "mu_x (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.mu_X", false]], "mul() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.mul", false]], "multilist (class in botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.MultiList", false]], "multimodelacquisitionfunction (class in botorch.acquisition.acquisition)": [[0, "botorch.acquisition.acquisition.MultiModelAcquisitionFunction", false]], "multiobjectiveanalyticacquisitionfunction (class in botorch.acquisition.multi_objective.base)": [[0, "botorch.acquisition.multi_objective.base.MultiObjectiveAnalyticAcquisitionFunction", false]], "multiobjectivemcacquisitionfunction (class in botorch.acquisition.multi_objective.base)": [[0, "botorch.acquisition.multi_objective.base.MultiObjectiveMCAcquisitionFunction", false]], "multiobjectivetestproblem (class in botorch.test_functions.base)": [[12, "botorch.test_functions.base.MultiObjectiveTestProblem", false]], "multiobjectivetestproblemtestcasemixin (class in botorch.utils.testing)": [[14, "botorch.utils.testing.MultiObjectiveTestProblemTestCaseMixin", false]], "multioutput_to_batch_mode_transform() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.multioutput_to_batch_mode_transform", false]], "multioutputexpectation (class in botorch.acquisition.multi_objective.multi_output_risk_measures)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputExpectation", false]], "multioutputriskmeasuremcobjective (class in botorch.acquisition.multi_objective.multi_output_risk_measures)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputRiskMeasureMCObjective", false]], "multioutputworstcase (class in botorch.acquisition.multi_objective.multi_output_risk_measures)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputWorstCase", false]], "multitaskdataset (class in botorch.utils.datasets)": [[14, "botorch.utils.datasets.MultiTaskDataset", false]], "multitaskgp (class in botorch.models.multitask)": [[7, "botorch.models.multitask.MultiTaskGP", false]], "multitaskgpposterior (class in botorch.posteriors.multitask)": [[9, "botorch.posteriors.multitask.MultitaskGPPosterior", false]], "multitaskgpytorchmodel (class in botorch.models.gpytorch)": [[7, "botorch.models.gpytorch.MultiTaskGPyTorchModel", false]], "multitasksaaspyromodel (class in botorch.models.fully_bayesian_multitask)": [[7, "botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel", false]], "multivariatenormalqmcengine (class in botorch.sampling.qmc)": [[10, "botorch.sampling.qmc.MultivariateNormalQMCEngine", false]], "mvar (class in botorch.acquisition.multi_objective.multi_output_risk_measures)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MVaR", false]], "mvn (botorch.posteriors.gpytorch.gpytorchposterior property)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.mvn", false]], "mvnxpb (class in botorch.utils.probability.mvnxpb)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB", false]], "mvnxpbstate (class in botorch.utils.probability.mvnxpb)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState", false]], "mw7 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.MW7", false]], "name (botorch.utils.dispatcher.dispatcher attribute)": [[14, "botorch.utils.dispatcher.Dispatcher.name", false]], "narrow_gaussian() (in module botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.narrow_gaussian", false]], "ndarrayoptimizationclosure (class in botorch.optim.closures.core)": [[8, "botorch.optim.closures.core.NdarrayOptimizationClosure", false]], "ndtr() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.ndtr", false]], "negative_log_gradient_sum() (botorch.models.likelihoods.pairwise.pairwiselikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLikelihood.negative_log_gradient_sum", false]], "negative_log_gradient_sum() (botorch.models.likelihoods.pairwise.pairwiselogitlikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLogitLikelihood.negative_log_gradient_sum", false]], "negative_log_gradient_sum() (botorch.models.likelihoods.pairwise.pairwiseprobitlikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseProbitLikelihood.negative_log_gradient_sum", false]], "negative_log_hessian_sum() (botorch.models.likelihoods.pairwise.pairwiselikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLikelihood.negative_log_hessian_sum", false]], "negative_log_hessian_sum() (botorch.models.likelihoods.pairwise.pairwiselogitlikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLogitLikelihood.negative_log_hessian_sum", false]], "negative_log_hessian_sum() (botorch.models.likelihoods.pairwise.pairwiseprobitlikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseProbitLikelihood.negative_log_hessian_sum", false]], "nnz_approx() (in module botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.nnz_approx", false]], "node (class in botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.Node", false]], "noisyexpectedhypervolumemixin (class in botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.NoisyExpectedHypervolumeMixin", false]], "noisyexpectedimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.NoisyExpectedImprovement", false]], "nondominatedpartitioning (class in botorch.utils.multi_objective.box_decompositions.non_dominated)": [[14, "botorch.utils.multi_objective.box_decompositions.non_dominated.NondominatedPartitioning", false]], "nonlinear_constraint_is_feasible() (in module botorch.optim.parameter_constraints)": [[8, "botorch.optim.parameter_constraints.nonlinear_constraint_is_feasible", false]], "nonlinear_inequality_constraints (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.nonlinear_inequality_constraints", false]], "norm_to_lognorm() (in module botorch.models.transforms.utils)": [[7, "botorch.models.transforms.utils.norm_to_lognorm", false]], "norm_to_lognorm_mean() (in module botorch.models.transforms.utils)": [[7, "botorch.models.transforms.utils.norm_to_lognorm_mean", false]], "norm_to_lognorm_variance() (in module botorch.models.transforms.utils)": [[7, "botorch.models.transforms.utils.norm_to_lognorm_variance", false]], "normalize (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.Normalize", false]], "normalize() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.normalize", false]], "normalize_dense_linear_constraints() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.normalize_dense_linear_constraints", false]], "normalize_indices() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.normalize_indices", false]], "normalize_sparse_linear_constraints() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.normalize_sparse_linear_constraints", false]], "normalmcsampler (class in botorch.sampling.normal)": [[10, "botorch.sampling.normal.NormalMCSampler", false]], "normalqmcengine (class in botorch.sampling.qmc)": [[10, "botorch.sampling.qmc.NormalQMCEngine", false]], "nu() (botorch.acquisition.multi_objective.analytic.expectedhypervolumeimprovement method)": [[0, "botorch.acquisition.multi_objective.analytic.ExpectedHypervolumeImprovement.nu", false]], "num_constraints (botorch.test_functions.base.constrainedbasetestproblem attribute)": [[12, "botorch.test_functions.base.ConstrainedBaseTestProblem.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.bnh attribute)": [[12, "botorch.test_functions.multi_objective.BNH.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.c2dtlz2 attribute)": [[12, "botorch.test_functions.multi_objective.C2DTLZ2.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.constr attribute)": [[12, "botorch.test_functions.multi_objective.CONSTR.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.constrainedbranincurrin attribute)": [[12, "botorch.test_functions.multi_objective.ConstrainedBraninCurrin.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.discbrake attribute)": [[12, "botorch.test_functions.multi_objective.DiscBrake.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.mw7 attribute)": [[12, "botorch.test_functions.multi_objective.MW7.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.osy attribute)": [[12, "botorch.test_functions.multi_objective.OSY.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.srn attribute)": [[12, "botorch.test_functions.multi_objective.SRN.num_constraints", false]], "num_constraints (botorch.test_functions.multi_objective.weldedbeam attribute)": [[12, "botorch.test_functions.multi_objective.WeldedBeam.num_constraints", false]], "num_constraints (botorch.test_functions.synthetic.constrainedgramacy attribute)": [[12, "botorch.test_functions.synthetic.ConstrainedGramacy.num_constraints", false]], "num_constraints (botorch.test_functions.synthetic.constrainedhartmann attribute)": [[12, "botorch.test_functions.synthetic.ConstrainedHartmann.num_constraints", false]], "num_constraints (botorch.test_functions.synthetic.constrainedhartmannsmooth attribute)": [[12, "botorch.test_functions.synthetic.ConstrainedHartmannSmooth.num_constraints", false]], "num_constraints (botorch.test_functions.synthetic.pressurevessel attribute)": [[12, "botorch.test_functions.synthetic.PressureVessel.num_constraints", false]], "num_constraints (botorch.test_functions.synthetic.speedreducer attribute)": [[12, "botorch.test_functions.synthetic.SpeedReducer.num_constraints", false]], "num_constraints (botorch.test_functions.synthetic.tensioncompressionstring attribute)": [[12, "botorch.test_functions.synthetic.TensionCompressionString.num_constraints", false]], "num_constraints (botorch.test_functions.synthetic.weldedbeamso attribute)": [[12, "botorch.test_functions.synthetic.WeldedBeamSO.num_constraints", false]], "num_mcmc_samples (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp property)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.num_mcmc_samples", false]], "num_mcmc_samples (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp property)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.num_mcmc_samples", false]], "num_objectives (botorch.test_functions.base.multiobjectivetestproblem attribute)": [[12, "botorch.test_functions.base.MultiObjectiveTestProblem.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.bnh attribute)": [[12, "botorch.test_functions.multi_objective.BNH.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.branincurrin attribute)": [[12, "botorch.test_functions.multi_objective.BraninCurrin.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.carsideimpact attribute)": [[12, "botorch.test_functions.multi_objective.CarSideImpact.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.constr attribute)": [[12, "botorch.test_functions.multi_objective.CONSTR.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.constrainedbranincurrin attribute)": [[12, "botorch.test_functions.multi_objective.ConstrainedBraninCurrin.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.dh attribute)": [[12, "botorch.test_functions.multi_objective.DH.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.discbrake attribute)": [[12, "botorch.test_functions.multi_objective.DiscBrake.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.mw7 attribute)": [[12, "botorch.test_functions.multi_objective.MW7.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.osy attribute)": [[12, "botorch.test_functions.multi_objective.OSY.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.srn attribute)": [[12, "botorch.test_functions.multi_objective.SRN.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.toyrobust attribute)": [[12, "botorch.test_functions.multi_objective.ToyRobust.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.vehiclesafety attribute)": [[12, "botorch.test_functions.multi_objective.VehicleSafety.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective.weldedbeam attribute)": [[12, "botorch.test_functions.multi_objective.WeldedBeam.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective_multi_fidelity.momfbranincurrin attribute)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFBraninCurrin.num_objectives", false]], "num_objectives (botorch.test_functions.multi_objective_multi_fidelity.momfpark attribute)": [[12, "botorch.test_functions.multi_objective_multi_fidelity.MOMFPark.num_objectives", false]], "num_objectives (botorch.test_functions.synthetic.constrainedgramacy attribute)": [[12, "botorch.test_functions.synthetic.ConstrainedGramacy.num_objectives", false]], "num_objectives (botorch.test_functions.synthetic.synthetictestfunction attribute)": [[12, "botorch.test_functions.synthetic.SyntheticTestFunction.num_objectives", false]], "num_outputs (botorch.models.approximate_gp.approximategpytorchmodel property)": [[7, "botorch.models.approximate_gp.ApproximateGPyTorchModel.num_outputs", false]], "num_outputs (botorch.models.ensemble.ensemblemodel property)": [[7, "botorch.models.ensemble.EnsembleModel.num_outputs", false]], "num_outputs (botorch.models.gpytorch.gpytorchmodel property)": [[7, "botorch.models.gpytorch.GPyTorchModel.num_outputs", false]], "num_outputs (botorch.models.model.model property)": [[7, "botorch.models.model.Model.num_outputs", false]], "num_outputs (botorch.models.model.modellist property)": [[7, "botorch.models.model.ModelList.num_outputs", false]], "num_outputs (botorch.models.pairwise_gp.pairwisegp property)": [[7, "botorch.models.pairwise_gp.PairwiseGP.num_outputs", false]], "num_outputs (botorch.sampling.pathwise.features.maps.featuremap attribute)": [[10, "botorch.sampling.pathwise.features.maps.FeatureMap.num_outputs", false]], "num_outputs (botorch.sampling.pathwise.features.maps.kernelevaluationmap property)": [[10, "botorch.sampling.pathwise.features.maps.KernelEvaluationMap.num_outputs", false]], "num_outputs (botorch.sampling.pathwise.features.maps.kernelfeaturemap property)": [[10, "botorch.sampling.pathwise.features.maps.KernelFeatureMap.num_outputs", false]], "num_outputs (botorch.utils.testing.mockmodel property)": [[14, "botorch.utils.testing.MockModel.num_outputs", false]], "num_restarts (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.num_restarts", false]], "num_steps (botorch.optim.homotopy.fixedhomotopyschedule property)": [[8, "botorch.optim.homotopy.FixedHomotopySchedule.num_steps", false]], "numericswarning": [[2, "botorch.exceptions.warnings.NumericsWarning", false]], "observed_y (botorch.cross_validation.cvresults attribute)": [[1, "botorch.cross_validation.CVResults.observed_Y", false]], "observed_yvar (botorch.cross_validation.cvresults attribute)": [[1, "botorch.cross_validation.CVResults.observed_Yvar", false]], "offset (botorch.models.transforms.input.affineinputtransform property)": [[7, "botorch.models.transforms.input.AffineInputTransform.offset", false]], "onehotargmaxste (class in botorch.utils.rounding)": [[14, "botorch.utils.rounding.OneHotArgmaxSTE", false]], "onehottonumeric (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.OneHotToNumeric", false]], "oneshotacquisitionfunction (class in botorch.acquisition.acquisition)": [[0, "botorch.acquisition.acquisition.OneShotAcquisitionFunction", false]], "optimal_sobol_indicies() (botorch.test_functions.sensitivity_analysis.gsobol method)": [[12, "botorch.test_functions.sensitivity_analysis.Gsobol.optimal_sobol_indicies", false]], "optimal_value (botorch.test_functions.synthetic.synthetictestfunction property)": [[12, "botorch.test_functions.synthetic.SyntheticTestFunction.optimal_value", false]], "optimizationgradienterror": [[2, "botorch.exceptions.errors.OptimizationGradientError", false]], "optimizationresult (class in botorch.optim.core)": [[8, "botorch.optim.core.OptimizationResult", false]], "optimizationstatus (class in botorch.optim.core)": [[8, "botorch.optim.core.OptimizationStatus", false]], "optimizationtimeouterror": [[2, "botorch.exceptions.errors.OptimizationTimeoutError", false]], "optimizationwarning": [[2, "botorch.exceptions.warnings.OptimizationWarning", false]], "optimize_acqf() (in module botorch.optim.optimize)": [[8, "botorch.optim.optimize.optimize_acqf", false]], "optimize_acqf_cyclic() (in module botorch.optim.optimize)": [[8, "botorch.optim.optimize.optimize_acqf_cyclic", false]], "optimize_acqf_discrete() (in module botorch.optim.optimize)": [[8, "botorch.optim.optimize.optimize_acqf_discrete", false]], "optimize_acqf_discrete_local_search() (in module botorch.optim.optimize)": [[8, "botorch.optim.optimize.optimize_acqf_discrete_local_search", false]], "optimize_acqf_homotopy() (in module botorch.optim.optimize_homotopy)": [[8, "botorch.optim.optimize_homotopy.optimize_acqf_homotopy", false]], "optimize_acqf_list() (in module botorch.optim.optimize)": [[8, "botorch.optim.optimize.optimize_acqf_list", false]], "optimize_acqf_mixed() (in module botorch.optim.optimize)": [[8, "botorch.optim.optimize.optimize_acqf_mixed", false]], "optimize_acqf_mixed_alternating() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.optimize_acqf_mixed_alternating", false]], "optimize_objective() (in module botorch.acquisition.input_constructors)": [[0, "botorch.acquisition.input_constructors.optimize_objective", false]], "optimize_posterior_samples() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.optimize_posterior_samples", false]], "optimizeacqfinputs (class in botorch.optim.optimize)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs", false]], "optimizers (botorch.test_functions.synthetic.hartmann property)": [[12, "botorch.test_functions.synthetic.Hartmann.optimizers", false]], "optimizers (botorch.test_functions.synthetic.michalewicz property)": [[12, "botorch.test_functions.synthetic.Michalewicz.optimizers", false]], "options (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.options", false]], "orthogonaladditivekernel (class in botorch.models.kernels.orthogonal_additive_kernel)": [[7, "botorch.models.kernels.orthogonal_additive_kernel.OrthogonalAdditiveKernel", false]], "osy (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.OSY", false]], "outcometransform (class in botorch.models.transforms.outcome)": [[7, "botorch.models.transforms.outcome.OutcomeTransform", false]], "outcomeuntransformer (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.OutcomeUntransformer", false]], "output_transform (botorch.sampling.pathwise.features.maps.featuremap attribute)": [[10, "botorch.sampling.pathwise.features.maps.FeatureMap.output_transform", false]], "output_transform (botorch.sampling.pathwise.utils.transformedmodulemixin attribute)": [[10, "botorch.sampling.pathwise.utils.TransformedModuleMixin.output_transform", false]], "outputscaletransform (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.OutputscaleTransform", false]], "p() (botorch.models.likelihoods.pairwise.pairwiselikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLikelihood.p", false]], "p() (botorch.models.likelihoods.pairwise.pairwiselogitlikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLogitLikelihood.p", false]], "p() (botorch.models.likelihoods.pairwise.pairwiseprobitlikelihood method)": [[7, "botorch.models.likelihoods.pairwise.PairwiseProbitLikelihood.p", false]], "pairwisebayesianactivelearningbydisagreement (class in botorch.acquisition.preference)": [[0, "botorch.acquisition.preference.PairwiseBayesianActiveLearningByDisagreement", false]], "pairwisegp (class in botorch.models.pairwise_gp)": [[7, "botorch.models.pairwise_gp.PairwiseGP", false]], "pairwiseiidnormalsampler (class in botorch.sampling.pairwise_samplers)": [[10, "botorch.sampling.pairwise_samplers.PairwiseIIDNormalSampler", false]], "pairwiselaplacemarginalloglikelihood (class in botorch.models.pairwise_gp)": [[7, "botorch.models.pairwise_gp.PairwiseLaplaceMarginalLogLikelihood", false]], "pairwiselikelihood (class in botorch.models.likelihoods.pairwise)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLikelihood", false]], "pairwiselogitlikelihood (class in botorch.models.likelihoods.pairwise)": [[7, "botorch.models.likelihoods.pairwise.PairwiseLogitLikelihood", false]], "pairwisemcposteriorvariance (class in botorch.acquisition.active_learning)": [[0, "botorch.acquisition.active_learning.PairwiseMCPosteriorVariance", false]], "pairwisemcsampler (class in botorch.sampling.pairwise_samplers)": [[10, "botorch.sampling.pairwise_samplers.PairwiseMCSampler", false]], "pairwiseprobitlikelihood (class in botorch.models.likelihoods.pairwise)": [[7, "botorch.models.likelihoods.pairwise.PairwiseProbitLikelihood", false]], "pairwisesobolqmcnormalsampler (class in botorch.sampling.pairwise_samplers)": [[10, "botorch.sampling.pairwise_samplers.PairwiseSobolQMCNormalSampler", false]], "parameter (botorch.optim.homotopy.homotopyparameter attribute)": [[8, "botorch.optim.homotopy.HomotopyParameter.parameter", false]], "parameter_rollback_ctx() (in module botorch.utils.context_managers)": [[14, "botorch.utils.context_managers.parameter_rollback_ctx", false]], "pareto_y (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition property)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.pareto_Y", false]], "pareto_y (botorch.utils.multi_objective.box_decompositions.box_decomposition_list.boxdecompositionlist property)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList.pareto_Y", false]], "partition_space() (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.partition_space", false]], "partition_space() (botorch.utils.multi_objective.box_decompositions.box_decomposition.fastpartitioning method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.FastPartitioning.partition_space", false]], "pathdict (class in botorch.sampling.pathwise.paths)": [[10, "botorch.sampling.pathwise.paths.PathDict", false]], "pathlist (class in botorch.sampling.pathwise.paths)": [[10, "botorch.sampling.pathwise.paths.PathList", false]], "pathwisethompsonsampling (class in botorch.acquisition.thompson_sampling)": [[0, "botorch.acquisition.thompson_sampling.PathwiseThompsonSampling", false]], "penalizedacquisitionfunction (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.PenalizedAcquisitionFunction", false]], "penalizedmcobjective (class in botorch.acquisition.penalized)": [[0, "botorch.acquisition.penalized.PenalizedMCObjective", false]], "penicillin (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.Penicillin", false]], "penicillin_vectorized() (botorch.test_functions.multi_objective.penicillin class method)": [[12, "botorch.test_functions.multi_objective.Penicillin.penicillin_vectorized", false]], "percentile_of_score() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.percentile_of_score", false]], "perm (botorch.utils.probability.linalg.pivotedcholesky attribute)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.perm", false]], "perm (botorch.utils.probability.mvnxpb.mvnxpbstate attribute)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState.perm", false]], "phi() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.phi", false]], "piv_chol (botorch.utils.probability.mvnxpb.mvnxpbstate attribute)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState.piv_chol", false]], "pivot_() (botorch.utils.probability.linalg.pivotedcholesky method)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.pivot_", false]], "pivot_() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.pivot_", false]], "pivotedcholesky (class in botorch.utils.probability.linalg)": [[14, "botorch.utils.probability.linalg.PivotedCholesky", false]], "plug_ins (botorch.utils.probability.mvnxpb.mvnxpbstate attribute)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState.plug_ins", false]], "polytopesampler (class in botorch.utils.sampling)": [[14, "botorch.utils.sampling.PolytopeSampler", false]], "pop() (botorch.utils.torch.bufferdict method)": [[14, "botorch.utils.torch.BufferDict.pop", false]], "post_processing_func (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.post_processing_func", false]], "posterior (botorch.cross_validation.cvresults attribute)": [[1, "botorch.cross_validation.CVResults.posterior", false]], "posterior (class in botorch.posteriors.posterior)": [[9, "botorch.posteriors.posterior.Posterior", false]], "posterior() (botorch.models.approximate_gp.approximategpytorchmodel method)": [[7, "botorch.models.approximate_gp.ApproximateGPyTorchModel.posterior", false]], "posterior() (botorch.models.ensemble.ensemblemodel method)": [[7, "botorch.models.ensemble.EnsembleModel.posterior", false]], "posterior() (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp method)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.posterior", false]], "posterior() (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp method)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.posterior", false]], "posterior() (botorch.models.gpytorch.batchedmultioutputgpytorchmodel method)": [[7, "botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel.posterior", false]], "posterior() (botorch.models.gpytorch.gpytorchmodel method)": [[7, "botorch.models.gpytorch.GPyTorchModel.posterior", false]], "posterior() (botorch.models.gpytorch.modellistgpytorchmodel method)": [[7, "botorch.models.gpytorch.ModelListGPyTorchModel.posterior", false]], "posterior() (botorch.models.gpytorch.multitaskgpytorchmodel method)": [[7, "botorch.models.gpytorch.MultiTaskGPyTorchModel.posterior", false]], "posterior() (botorch.models.higher_order_gp.higherordergp method)": [[7, "botorch.models.higher_order_gp.HigherOrderGP.posterior", false]], "posterior() (botorch.models.model.fantasizemixin method)": [[7, "botorch.models.model.FantasizeMixin.posterior", false]], "posterior() (botorch.models.model.model method)": [[7, "botorch.models.model.Model.posterior", false]], "posterior() (botorch.models.model.modellist method)": [[7, "botorch.models.model.ModelList.posterior", false]], "posterior() (botorch.models.multitask.kroneckermultitaskgp method)": [[7, "botorch.models.multitask.KroneckerMultiTaskGP.posterior", false]], "posterior() (botorch.models.pairwise_gp.pairwisegp method)": [[7, "botorch.models.pairwise_gp.PairwiseGP.posterior", false]], "posterior() (botorch.utils.testing.mockmodel method)": [[14, "botorch.utils.testing.MockModel.posterior", false]], "posteriorlist (class in botorch.posteriors.posterior_list)": [[9, "botorch.posteriors.posterior_list.PosteriorList", false]], "posteriormean (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.PosteriorMean", false]], "posteriormeanmodel (class in botorch.models.deterministic)": [[7, "botorch.models.deterministic.PosteriorMeanModel", false]], "posteriorstandarddeviation (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.PosteriorStandardDeviation", false]], "posteriortransform (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.PosteriorTransform", false]], "postprocess_mcmc_samples() (botorch.models.fully_bayesian.pyromodel method)": [[7, "botorch.models.fully_bayesian.PyroModel.postprocess_mcmc_samples", false]], "postprocess_mcmc_samples() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.postprocess_mcmc_samples", false]], "powell (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Powell", false]], "power (class in botorch.models.transforms.outcome)": [[7, "botorch.models.transforms.outcome.Power", false]], "predictive_mean_cache (botorch.models.multitask.kroneckermultitaskgp property)": [[7, "botorch.models.multitask.KroneckerMultiTaskGP.predictive_mean_cache", false]], "preprocess_transform() (botorch.models.transforms.input.batchbroadcastedinputtransform method)": [[7, "botorch.models.transforms.input.BatchBroadcastedInputTransform.preprocess_transform", false]], "preprocess_transform() (botorch.models.transforms.input.chainedinputtransform method)": [[7, "botorch.models.transforms.input.ChainedInputTransform.preprocess_transform", false]], "preprocess_transform() (botorch.models.transforms.input.inputtransform method)": [[7, "botorch.models.transforms.input.InputTransform.preprocess_transform", false]], "pressurevessel (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.PressureVessel", false]], "priorguidedacquisitionfunction (class in botorch.acquisition.prior_guided)": [[0, "botorch.acquisition.prior_guided.PriorGuidedAcquisitionFunction", false]], "probabilityofimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.ProbabilityOfImprovement", false]], "project_to_sample_points() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.project_to_sample_points", false]], "project_to_target_fidelity() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.project_to_target_fidelity", false]], "projectedacquisitionfunction (class in botorch.acquisition.knowledge_gradient)": [[0, "botorch.acquisition.knowledge_gradient.ProjectedAcquisitionFunction", false]], "propagate_grads (class in botorch.settings)": [[11, "botorch.settings.propagate_grads", false]], "proximalacquisitionfunction (class in botorch.acquisition.proximal)": [[0, "botorch.acquisition.proximal.ProximalAcquisitionFunction", false]], "prune_candidates() (in module botorch.optim.optimize_homotopy)": [[8, "botorch.optim.optimize_homotopy.prune_candidates", false]], "prune_inferior_points() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.prune_inferior_points", false]], "prune_inferior_points_multi_objective() (in module botorch.acquisition.multi_objective.utils)": [[0, "botorch.acquisition.multi_objective.utils.prune_inferior_points_multi_objective", false]], "psi() (botorch.acquisition.multi_objective.analytic.expectedhypervolumeimprovement method)": [[0, "botorch.acquisition.multi_objective.analytic.ExpectedHypervolumeImprovement.psi", false]], "pyromodel (class in botorch.models.fully_bayesian)": [[7, "botorch.models.fully_bayesian.PyroModel", false]], "q (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.q", false]], "qanalyticprobabilityofimprovement (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.qAnalyticProbabilityOfImprovement", false]], "qbayesianactivelearningbydisagreement (class in botorch.acquisition.bayesian_active_learning)": [[0, "botorch.acquisition.bayesian_active_learning.qBayesianActiveLearningByDisagreement", false]], "qexpectedhypervolumeimprovement (class in botorch.acquisition.multi_objective.monte_carlo)": [[0, "botorch.acquisition.multi_objective.monte_carlo.qExpectedHypervolumeImprovement", false]], "qexpectedimprovement (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.qExpectedImprovement", false]], "qexpectedutilityofbestoption (class in botorch.acquisition.preference)": [[0, "botorch.acquisition.preference.qExpectedUtilityOfBestOption", false]], "qhypervolumeknowledgegradient (class in botorch.acquisition.multi_objective.hypervolume_knowledge_gradient)": [[0, "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qHypervolumeKnowledgeGradient", false]], "qjointentropysearch (class in botorch.acquisition.joint_entropy_search)": [[0, "botorch.acquisition.joint_entropy_search.qJointEntropySearch", false]], "qknowledgegradient (class in botorch.acquisition.knowledge_gradient)": [[0, "botorch.acquisition.knowledge_gradient.qKnowledgeGradient", false]], "qlogexpectedhypervolumeimprovement (class in botorch.acquisition.multi_objective.logei)": [[0, "botorch.acquisition.multi_objective.logei.qLogExpectedHypervolumeImprovement", false]], "qlogexpectedimprovement (class in botorch.acquisition.logei)": [[0, "botorch.acquisition.logei.qLogExpectedImprovement", false]], "qlognoisyexpectedhypervolumeimprovement (class in botorch.acquisition.multi_objective.logei)": [[0, "botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement", false]], "qlognoisyexpectedimprovement (class in botorch.acquisition.logei)": [[0, "botorch.acquisition.logei.qLogNoisyExpectedImprovement", false]], "qlognparego (class in botorch.acquisition.multi_objective.parego)": [[0, "botorch.acquisition.multi_objective.parego.qLogNParEGO", false]], "qlowerboundmaxvalueentropy (class in botorch.acquisition.max_value_entropy_search)": [[0, "botorch.acquisition.max_value_entropy_search.qLowerBoundMaxValueEntropy", false]], "qlowerboundmultiobjectivejointentropysearch (class in botorch.acquisition.multi_objective.joint_entropy_search)": [[0, "botorch.acquisition.multi_objective.joint_entropy_search.qLowerBoundMultiObjectiveJointEntropySearch", false]], "qlowerboundmultiobjectivemaxvalueentropysearch (class in botorch.acquisition.multi_objective.max_value_entropy_search)": [[0, "botorch.acquisition.multi_objective.max_value_entropy_search.qLowerBoundMultiObjectiveMaxValueEntropySearch", false]], "qlowerconfidencebound (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.qLowerConfidenceBound", false]], "qmaxvalueentropy (class in botorch.acquisition.max_value_entropy_search)": [[0, "botorch.acquisition.max_value_entropy_search.qMaxValueEntropy", false]], "qmultifidelityhypervolumeknowledgegradient (class in botorch.acquisition.multi_objective.hypervolume_knowledge_gradient)": [[0, "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qMultiFidelityHypervolumeKnowledgeGradient", false]], "qmultifidelityknowledgegradient (class in botorch.acquisition.knowledge_gradient)": [[0, "botorch.acquisition.knowledge_gradient.qMultiFidelityKnowledgeGradient", false]], "qmultifidelitylowerboundmaxvalueentropy (class in botorch.acquisition.max_value_entropy_search)": [[0, "botorch.acquisition.max_value_entropy_search.qMultiFidelityLowerBoundMaxValueEntropy", false]], "qmultifidelitymaxvalueentropy (class in botorch.acquisition.max_value_entropy_search)": [[0, "botorch.acquisition.max_value_entropy_search.qMultiFidelityMaxValueEntropy", false]], "qmultiobjectivemaxvalueentropy (class in botorch.acquisition.multi_objective.max_value_entropy_search)": [[0, "botorch.acquisition.multi_objective.max_value_entropy_search.qMultiObjectiveMaxValueEntropy", false]], "qmultiobjectivepredictiveentropysearch (class in botorch.acquisition.multi_objective.predictive_entropy_search)": [[0, "botorch.acquisition.multi_objective.predictive_entropy_search.qMultiObjectivePredictiveEntropySearch", false]], "qmultisteplookahead (class in botorch.acquisition.multi_step_lookahead)": [[0, "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead", false]], "qnegintegratedposteriorvariance (class in botorch.acquisition.active_learning)": [[0, "botorch.acquisition.active_learning.qNegIntegratedPosteriorVariance", false]], "qnoisyexpectedhypervolumeimprovement (class in botorch.acquisition.multi_objective.monte_carlo)": [[0, "botorch.acquisition.multi_objective.monte_carlo.qNoisyExpectedHypervolumeImprovement", false]], "qnoisyexpectedimprovement (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.qNoisyExpectedImprovement", false]], "qpredictiveentropysearch (class in botorch.acquisition.predictive_entropy_search)": [[0, "botorch.acquisition.predictive_entropy_search.qPredictiveEntropySearch", false]], "qprobabilityofimprovement (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.qProbabilityOfImprovement", false]], "qsimpleregret (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.qSimpleRegret", false]], "qualityfunction (class in botorch.models.utils.inducing_point_allocators)": [[7, "botorch.models.utils.inducing_point_allocators.QualityFunction", false]], "quantile() (botorch.posteriors.fully_bayesian.gaussianmixtureposterior method)": [[9, "botorch.posteriors.fully_bayesian.GaussianMixturePosterior.quantile", false]], "quantile() (botorch.posteriors.gpytorch.gpytorchposterior method)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.quantile", false]], "quantile() (botorch.posteriors.posterior.posterior method)": [[9, "botorch.posteriors.posterior.Posterior.quantile", false]], "quantile() (botorch.posteriors.torch.torchposterior method)": [[9, "botorch.posteriors.torch.TorchPosterior.quantile", false]], "qupperconfidencebound (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.qUpperConfidenceBound", false]], "r (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.R", false]], "random_search_optimizer() (in module botorch.acquisition.multi_objective.utils)": [[0, "botorch.acquisition.multi_objective.utils.random_search_optimizer", false]], "randomfourierfeatures (class in botorch.utils.gp_sampling)": [[14, "botorch.utils.gp_sampling.RandomFourierFeatures", false]], "ranges (botorch.models.transforms.input.normalize property)": [[7, "botorch.models.transforms.input.Normalize.ranges", false]], "rankingdataset (class in botorch.utils.datasets)": [[14, "botorch.utils.datasets.RankingDataset", false]], "rastrigin (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Rastrigin", false]], "raw_samples (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.raw_samples", false]], "redraw() (botorch.acquisition.thompson_sampling.pathwisethompsonsampling method)": [[0, "botorch.acquisition.thompson_sampling.PathwiseThompsonSampling.redraw", false]], "ref_point (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition property)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.ref_point", false]], "ref_point (botorch.utils.multi_objective.box_decompositions.box_decomposition_list.boxdecompositionlist property)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList.ref_point", false]], "ref_point (botorch.utils.multi_objective.hypervolume.hypervolume property)": [[14, "botorch.utils.multi_objective.hypervolume.Hypervolume.ref_point", false]], "reinsert() (botorch.utils.multi_objective.hypervolume.multilist method)": [[14, "botorch.utils.multi_objective.hypervolume.MultiList.reinsert", false]], "remove() (botorch.utils.multi_objective.hypervolume.multilist method)": [[14, "botorch.utils.multi_objective.hypervolume.MultiList.remove", false]], "repeat_to_match_aug_dim() (in module botorch.acquisition.utils)": [[0, "botorch.acquisition.utils.repeat_to_match_aug_dim", false]], "reset() (botorch.optim.homotopy.homotopy method)": [[8, "botorch.optim.homotopy.Homotopy.reset", false]], "reset() (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.reset", false]], "reshape_and_detach() (in module botorch.models.fully_bayesian)": [[7, "botorch.models.fully_bayesian.reshape_and_detach", false]], "restart() (botorch.optim.homotopy.fixedhomotopyschedule method)": [[8, "botorch.optim.homotopy.FixedHomotopySchedule.restart", false]], "restart() (botorch.optim.homotopy.homotopy method)": [[8, "botorch.optim.homotopy.Homotopy.restart", false]], "retry_on_optimization_warning (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.retry_on_optimization_warning", false]], "return_best_only (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.return_best_only", false]], "return_full_tree (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.return_full_tree", false]], "reverse (botorch.models.transforms.input.reversibleinputtransform attribute)": [[7, "botorch.models.transforms.input.ReversibleInputTransform.reverse", false]], "reversibleinputtransform (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.ReversibleInputTransform", false]], "riskmeasuremcobjective (class in botorch.acquisition.risk_measures)": [[0, "botorch.acquisition.risk_measures.RiskMeasureMCObjective", false]], "rosenbrock (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Rosenbrock", false]], "round (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.Round", false]], "round_nearest() (in module botorch.test_functions.utils)": [[12, "botorch.test_functions.utils.round_nearest", false]], "roundste (class in botorch.utils.rounding)": [[14, "botorch.utils.rounding.RoundSTE", false]], "rsample() (botorch.posteriors.ensemble.ensembleposterior method)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.rsample", false]], "rsample() (botorch.posteriors.gpytorch.gpytorchposterior method)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.rsample", false]], "rsample() (botorch.posteriors.higher_order.higherordergpposterior method)": [[9, "botorch.posteriors.higher_order.HigherOrderGPPosterior.rsample", false]], "rsample() (botorch.posteriors.multitask.multitaskgpposterior method)": [[9, "botorch.posteriors.multitask.MultitaskGPPosterior.rsample", false]], "rsample() (botorch.posteriors.posterior.posterior method)": [[9, "botorch.posteriors.posterior.Posterior.rsample", false]], "rsample() (botorch.posteriors.posterior_list.posteriorlist method)": [[9, "botorch.posteriors.posterior_list.PosteriorList.rsample", false]], "rsample() (botorch.posteriors.torch.torchposterior method)": [[9, "botorch.posteriors.torch.TorchPosterior.rsample", false]], "rsample() (botorch.posteriors.transformed.transformedposterior method)": [[9, "botorch.posteriors.transformed.TransformedPosterior.rsample", false]], "rsample() (botorch.utils.probability.truncated_multivariate_normal.truncatedmultivariatenormal method)": [[14, "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal.rsample", false]], "rsample() (botorch.utils.probability.unified_skew_normal.unifiedskewnormal method)": [[14, "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal.rsample", false]], "rsample() (botorch.utils.testing.mockposterior method)": [[14, "botorch.utils.testing.MockPosterior.rsample", false]], "rsample_from_base_samples() (botorch.posteriors.ensemble.ensembleposterior method)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.rsample_from_base_samples", false]], "rsample_from_base_samples() (botorch.posteriors.gpytorch.gpytorchposterior method)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.rsample_from_base_samples", false]], "rsample_from_base_samples() (botorch.posteriors.higher_order.higherordergpposterior method)": [[9, "botorch.posteriors.higher_order.HigherOrderGPPosterior.rsample_from_base_samples", false]], "rsample_from_base_samples() (botorch.posteriors.multitask.multitaskgpposterior method)": [[9, "botorch.posteriors.multitask.MultitaskGPPosterior.rsample_from_base_samples", false]], "rsample_from_base_samples() (botorch.posteriors.posterior.posterior method)": [[9, "botorch.posteriors.posterior.Posterior.rsample_from_base_samples", false]], "rsample_from_base_samples() (botorch.posteriors.transformed.transformedposterior method)": [[9, "botorch.posteriors.transformed.TransformedPosterior.rsample_from_base_samples", false]], "rsample_from_base_samples() (botorch.utils.testing.mockposterior method)": [[14, "botorch.utils.testing.MockPosterior.rsample_from_base_samples", false]], "running (botorch.optim.core.optimizationstatus attribute)": [[8, "botorch.optim.core.OptimizationStatus.RUNNING", false]], "runtime (botorch.optim.core.optimizationresult attribute)": [[8, "botorch.optim.core.OptimizationResult.runtime", false]], "saasfullybayesianmultitaskgp (class in botorch.models.fully_bayesian_multitask)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP", false]], "saasfullybayesiansingletaskgp (class in botorch.models.fully_bayesian)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP", false]], "saaspyromodel (class in botorch.models.fully_bayesian)": [[7, "botorch.models.fully_bayesian.SaasPyroModel", false]], "sacgp (class in botorch.models.contextual)": [[7, "botorch.models.contextual.SACGP", false]], "sackernel (class in botorch.models.kernels.contextual_sac)": [[7, "botorch.models.kernels.contextual_sac.SACKernel", false]], "sample() (botorch.models.fully_bayesian.pyromodel method)": [[7, "botorch.models.fully_bayesian.PyroModel.sample", false]], "sample() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.sample", false]], "sample() (botorch.models.fully_bayesian_multitask.multitasksaaspyromodel method)": [[7, "botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel.sample", false]], "sample() (botorch.posteriors.posterior.posterior method)": [[9, "botorch.posteriors.posterior.Posterior.sample", false]], "sample_all_priors() (in module botorch.optim.utils.model_utils)": [[8, "botorch.optim.utils.model_utils.sample_all_priors", false]], "sample_cached_cholesky() (in module botorch.utils.low_rank)": [[14, "botorch.utils.low_rank.sample_cached_cholesky", false]], "sample_feasible_points() (in module botorch.optim.optimize_mixed)": [[8, "botorch.optim.optimize_mixed.sample_feasible_points", false]], "sample_hypersphere() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.sample_hypersphere", false]], "sample_latent_features() (botorch.models.fully_bayesian_multitask.multitasksaaspyromodel method)": [[7, "botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel.sample_latent_features", false]], "sample_lengthscale() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.sample_lengthscale", false]], "sample_mean() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.sample_mean", false]], "sample_noise() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.sample_noise", false]], "sample_optimal_points() (in module botorch.acquisition.multi_objective.utils)": [[0, "botorch.acquisition.multi_objective.utils.sample_optimal_points", false]], "sample_outputscale() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.sample_outputscale", false]], "sample_perturbed_subset_dims() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.sample_perturbed_subset_dims", false]], "sample_points_around_best() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.sample_points_around_best", false]], "sample_polytope() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.sample_polytope", false]], "sample_q_batches_from_polytope() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.sample_q_batches_from_polytope", false]], "sample_shape (botorch.acquisition.acquisition.mcsamplermixin property)": [[0, "botorch.acquisition.acquisition.MCSamplerMixin.sample_shape", false]], "sample_shape (botorch.sampling.list_sampler.listsampler property)": [[10, "botorch.sampling.list_sampler.ListSampler.sample_shape", false]], "sample_simplex() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.sample_simplex", false]], "sample_task_lengthscale() (botorch.models.fully_bayesian_multitask.multitasksaaspyromodel method)": [[7, "botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel.sample_task_lengthscale", false]], "sample_truncated_normal_perturbations() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.sample_truncated_normal_perturbations", false]], "samplepath (class in botorch.sampling.pathwise.paths)": [[10, "botorch.sampling.pathwise.paths.SamplePath", false]], "sampler (botorch.utils.probability.truncated_multivariate_normal.truncatedmultivariatenormal property)": [[14, "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal.sampler", false]], "samplereducingmcacquisitionfunction (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.SampleReducingMCAcquisitionFunction", false]], "samplereductionprotocol (class in botorch.acquisition.monte_carlo)": [[0, "botorch.acquisition.monte_carlo.SampleReductionProtocol", false]], "samplingstrategy (class in botorch.generation.sampling)": [[4, "botorch.generation.sampling.SamplingStrategy", false]], "samplingwarning": [[2, "botorch.exceptions.warnings.SamplingWarning", false]], "scalarize (botorch.acquisition.objective.scalarizedposteriortransform attribute)": [[0, "botorch.acquisition.objective.ScalarizedPosteriorTransform.scalarize", false]], "scalarize (botorch.utils.test_helpers.dummynonscalarizingposteriortransform attribute)": [[14, "botorch.utils.test_helpers.DummyNonScalarizingPosteriorTransform.scalarize", false]], "scalarize_posterior() (in module botorch.posteriors.gpytorch)": [[9, "botorch.posteriors.gpytorch.scalarize_posterior", false]], "scalarize_posterior_gpytorch() (in module botorch.posteriors.gpytorch)": [[9, "botorch.posteriors.gpytorch.scalarize_posterior_gpytorch", false]], "scalarizedposteriormean (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.ScalarizedPosteriorMean", false]], "scalarizedposteriortransform (class in botorch.acquisition.objective)": [[0, "botorch.acquisition.objective.ScalarizedPosteriorTransform", false]], "scale_tril (botorch.utils.probability.unified_skew_normal.unifiedskewnormal property)": [[14, "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal.scale_tril", false]], "schedule (botorch.optim.homotopy.homotopyparameter attribute)": [[8, "botorch.optim.homotopy.HomotopyParameter.schedule", false]], "scipy_minimize() (in module botorch.optim.core)": [[8, "botorch.optim.core.scipy_minimize", false]], "select_pivot() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.select_pivot", false]], "separate_mtmvn() (in module botorch.utils.multitask)": [[14, "botorch.utils.multitask.separate_mtmvn", false]], "sequential (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.sequential", false]], "set_attribute() (in module botorch.models.converter)": [[7, "botorch.models.converter.set_attribute", false]], "set_baseline_y() (botorch.acquisition.multi_objective.multi_output_risk_measures.mars method)": [[0, "botorch.acquisition.multi_objective.multi_output_risk_measures.MARS.set_baseline_Y", false]], "set_inputs() (botorch.models.fully_bayesian.pyromodel method)": [[7, "botorch.models.fully_bayesian.PyroModel.set_inputs", false]], "set_inputs() (botorch.models.fully_bayesian.saaspyromodel method)": [[7, "botorch.models.fully_bayesian.SaasPyroModel.set_inputs", false]], "set_inputs() (botorch.models.fully_bayesian_multitask.multitasksaaspyromodel method)": [[7, "botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel.set_inputs", false]], "set_tensors_from_ndarray_1d() (in module botorch.optim.utils.numpy_utils)": [[8, "botorch.optim.utils.numpy_utils.set_tensors_from_ndarray_1d", false]], "set_train_data() (botorch.models.pairwise_gp.pairwisegp method)": [[7, "botorch.models.pairwise_gp.PairwiseGP.set_train_data", false]], "set_x_pending() (botorch.acquisition.acquisition.acquisitionfunction method)": [[0, "botorch.acquisition.acquisition.AcquisitionFunction.set_X_pending", false]], "set_x_pending() (botorch.acquisition.analytic.analyticacquisitionfunction method)": [[0, "botorch.acquisition.analytic.AnalyticAcquisitionFunction.set_X_pending", false]], "set_x_pending() (botorch.acquisition.decoupled.decoupledacquisitionfunction method)": [[0, "botorch.acquisition.decoupled.DecoupledAcquisitionFunction.set_X_pending", false]], "set_x_pending() (botorch.acquisition.max_value_entropy_search.maxvaluebase method)": [[0, "botorch.acquisition.max_value_entropy_search.MaxValueBase.set_X_pending", false]], "set_x_pending() (botorch.acquisition.max_value_entropy_search.qmaxvalueentropy method)": [[0, "botorch.acquisition.max_value_entropy_search.qMaxValueEntropy.set_X_pending", false]], "set_x_pending() (botorch.acquisition.multi_objective.base.multiobjectiveanalyticacquisitionfunction method)": [[0, "botorch.acquisition.multi_objective.base.MultiObjectiveAnalyticAcquisitionFunction.set_X_pending", false]], "set_x_pending() (botorch.acquisition.multi_objective.max_value_entropy_search.qmultiobjectivemaxvalueentropy method)": [[0, "botorch.acquisition.multi_objective.max_value_entropy_search.qMultiObjectiveMaxValueEntropy.set_X_pending", false]], "set_x_pending() (botorch.acquisition.penalized.penalizedacquisitionfunction method)": [[0, "botorch.acquisition.penalized.PenalizedAcquisitionFunction.set_X_pending", false]], "set_x_pending() (botorch.utils.multi_objective.hypervolume.noisyexpectedhypervolumemixin method)": [[14, "botorch.utils.multi_objective.hypervolume.NoisyExpectedHypervolumeMixin.set_X_pending", false]], "set_x_pending() (botorch.utils.testing.mockacquisitionfunction method)": [[14, "botorch.utils.testing.MockAcquisitionFunction.set_X_pending", false]], "setup() (botorch.utils.testing.botorchtestcase method)": [[14, "botorch.utils.testing.BotorchTestCase.setUp", false]], "shape (botorch.optim.utils.model_utils.torchattr attribute)": [[8, "botorch.optim.utils.model_utils.TorchAttr.shape", false]], "shape (botorch.utils.containers.botorchcontainer property)": [[14, "botorch.utils.containers.BotorchContainer.shape", false]], "shape (botorch.utils.containers.densecontainer property)": [[14, "botorch.utils.containers.DenseContainer.shape", false]], "shape (botorch.utils.containers.slicecontainer property)": [[14, "botorch.utils.containers.SliceContainer.shape", false]], "shape_to_str() (in module botorch.logging)": [[6, "botorch.logging.shape_to_str", false]], "shekel (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.Shekel", false]], "should_stop (botorch.optim.homotopy.fixedhomotopyschedule property)": [[8, "botorch.optim.homotopy.FixedHomotopySchedule.should_stop", false]], "should_stop (botorch.optim.homotopy.homotopy property)": [[8, "botorch.optim.homotopy.Homotopy.should_stop", false]], "sigmoid() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.sigmoid", false]], "simplegpytorchmodel (class in botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.SimpleGPyTorchModel", false]], "sinecosinetransform (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.SineCosineTransform", false]], "singletaskgp (class in botorch.models.gp_regression)": [[7, "botorch.models.gp_regression.SingleTaskGP", false]], "singletaskmultifidelitygp (class in botorch.models.gp_regression_fidelity)": [[7, "botorch.models.gp_regression_fidelity.SingleTaskMultiFidelityGP", false]], "singletaskvariationalgp (class in botorch.models.approximate_gp)": [[7, "botorch.models.approximate_gp.SingleTaskVariationalGP", false]], "sixhumpcamel (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.SixHumpCamel", false]], "slicecontainer (class in botorch.utils.containers)": [[14, "botorch.utils.containers.SliceContainer", false]], "smooth_amax() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.smooth_amax", false]], "smooth_amin() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.smooth_amin", false]], "sobolqmcnormalsampler (class in botorch.sampling.normal)": [[10, "botorch.sampling.normal.SobolQMCNormalSampler", false]], "solve() (botorch.utils.probability.mvnxpb.mvnxpb method)": [[14, "botorch.utils.probability.mvnxpb.MVNXPB.solve", false]], "solver (botorch.utils.probability.truncated_multivariate_normal.truncatedmultivariatenormal property)": [[14, "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal.solver", false]], "sort_by_dimension() (in module botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.sort_by_dimension", false]], "source() (botorch.utils.dispatcher.dispatcher method)": [[14, "botorch.utils.dispatcher.Dispatcher.source", false]], "sparse_to_dense_constraints() (in module botorch.utils.sampling)": [[14, "botorch.utils.sampling.sparse_to_dense_constraints", false]], "speedreducer (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.SpeedReducer", false]], "srn (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.SRN", false]], "standard_normal_log_hazard() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.standard_normal_log_hazard", false]], "standardize (class in botorch.models.transforms.outcome)": [[7, "botorch.models.transforms.outcome.Standardize", false]], "standardize() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.standardize", false]], "standardize_moments() (in module botorch.utils.test_helpers)": [[14, "botorch.utils.test_helpers.standardize_moments", false]], "state (botorch.optim.closures.core.ndarrayoptimizationclosure property)": [[8, "botorch.optim.closures.core.NdarrayOptimizationClosure.state", false]], "state_dict() (botorch.utils.testing.mockmodel method)": [[14, "botorch.utils.testing.MockModel.state_dict", false]], "status (botorch.optim.core.optimizationresult attribute)": [[8, "botorch.optim.core.OptimizationResult.status", false]], "stds (botorch.models.transforms.input.inputstandardize property)": [[7, "botorch.models.transforms.input.InputStandardize.stds", false]], "step (botorch.optim.core.optimizationresult attribute)": [[8, "botorch.optim.core.OptimizationResult.step", false]], "step (botorch.utils.probability.linalg.pivotedcholesky attribute)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.step", false]], "step (botorch.utils.probability.mvnxpb.mvnxpbstate attribute)": [[14, "botorch.utils.probability.mvnxpb.mvnxpbState.step", false]], "step() (botorch.optim.homotopy.fixedhomotopyschedule method)": [[8, "botorch.optim.homotopy.FixedHomotopySchedule.step", false]], "step() (botorch.optim.homotopy.homotopy method)": [[8, "botorch.optim.homotopy.Homotopy.step", false]], "step() (botorch.utils.probability.lin_ess.linearellipticalslicesampler method)": [[14, "botorch.utils.probability.lin_ess.LinearEllipticalSliceSampler.step", false]], "stochasticsampler (class in botorch.sampling.stochastic_samplers)": [[10, "botorch.sampling.stochastic_samplers.StochasticSampler", false]], "stopped (botorch.optim.core.optimizationstatus attribute)": [[8, "botorch.optim.core.OptimizationStatus.STOPPED", false]], "stoppingcriterion (class in botorch.optim.stopping)": [[8, "botorch.optim.stopping.StoppingCriterion", false]], "styblinskitang (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.StyblinskiTang", false]], "sub() (in module botorch.utils.safe_math)": [[14, "botorch.utils.safe_math.sub", false]], "subset_output() (botorch.models.deterministic.affinedeterministicmodel method)": [[7, "botorch.models.deterministic.AffineDeterministicModel.subset_output", false]], "subset_output() (botorch.models.deterministic.genericdeterministicmodel method)": [[7, "botorch.models.deterministic.GenericDeterministicModel.subset_output", false]], "subset_output() (botorch.models.gpytorch.batchedmultioutputgpytorchmodel method)": [[7, "botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel.subset_output", false]], "subset_output() (botorch.models.gpytorch.multitaskgpytorchmodel method)": [[7, "botorch.models.gpytorch.MultiTaskGPyTorchModel.subset_output", false]], "subset_output() (botorch.models.model.model method)": [[7, "botorch.models.model.Model.subset_output", false]], "subset_output() (botorch.models.model.modellist method)": [[7, "botorch.models.model.ModelList.subset_output", false]], "subset_output() (botorch.models.transforms.outcome.bilog method)": [[7, "botorch.models.transforms.outcome.Bilog.subset_output", false]], "subset_output() (botorch.models.transforms.outcome.chainedoutcometransform method)": [[7, "botorch.models.transforms.outcome.ChainedOutcomeTransform.subset_output", false]], "subset_output() (botorch.models.transforms.outcome.log method)": [[7, "botorch.models.transforms.outcome.Log.subset_output", false]], "subset_output() (botorch.models.transforms.outcome.outcometransform method)": [[7, "botorch.models.transforms.outcome.OutcomeTransform.subset_output", false]], "subset_output() (botorch.models.transforms.outcome.power method)": [[7, "botorch.models.transforms.outcome.Power.subset_output", false]], "subset_output() (botorch.models.transforms.outcome.standardize method)": [[7, "botorch.models.transforms.outcome.Standardize.subset_output", false]], "subset_transform() (in module botorch.models.transforms.utils)": [[7, "botorch.models.transforms.utils.subset_transform", false]], "subsetindexcachingmixin (class in botorch.utils.multi_objective.hypervolume)": [[14, "botorch.utils.multi_objective.hypervolume.SubsetIndexCachingMixin", false]], "success (botorch.optim.core.optimizationstatus attribute)": [[8, "botorch.optim.core.OptimizationStatus.SUCCESS", false]], "superviseddataset (class in botorch.utils.datasets)": [[14, "botorch.utils.datasets.SupervisedDataset", false]], "supports_cache_root() (in module botorch.acquisition.cached_cholesky)": [[0, "botorch.acquisition.cached_cholesky.supports_cache_root", false]], "swap_along_dim_() (in module botorch.utils.probability.utils)": [[14, "botorch.utils.probability.utils.swap_along_dim_", false]], "synthetictestfunction (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.SyntheticTestFunction", false]], "synthetictestfunctiontestcasemixin (class in botorch.utils.testing)": [[14, "botorch.utils.testing.SyntheticTestFunctionTestCaseMixin", false]], "t_batch_mode_transform() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.t_batch_mode_transform", false]], "t_o (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.T_o", false]], "t_v (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.T_v", false]], "task_covar_module() (botorch.models.contextual_multioutput.lcemgp method)": [[7, "botorch.models.contextual_multioutput.LCEMGP.task_covar_module", false]], "tensioncompressionstring (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.TensionCompressionString", false]], "tensorcheckpoint (class in botorch.utils.context_managers)": [[14, "botorch.utils.context_managers.TensorCheckpoint", false]], "tensortransform (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.TensorTransform", false]], "test_attributes() (botorch.utils.testing.multiobjectivetestproblemtestcasemixin method)": [[14, "botorch.utils.testing.MultiObjectiveTestProblemTestCaseMixin.test_attributes", false]], "test_evaluate_slack() (botorch.utils.testing.constrainedtestproblemtestcasemixin method)": [[14, "botorch.utils.testing.ConstrainedTestProblemTestCaseMixin.test_evaluate_slack", false]], "test_forward_and_evaluate_true() (botorch.utils.testing.basetestproblemtestcasemixin method)": [[14, "botorch.utils.testing.BaseTestProblemTestCaseMixIn.test_forward_and_evaluate_true", false]], "test_max_hv() (botorch.utils.testing.multiobjectivetestproblemtestcasemixin method)": [[14, "botorch.utils.testing.MultiObjectiveTestProblemTestCaseMixin.test_max_hv", false]], "test_num_constraints() (botorch.utils.testing.constrainedtestproblemtestcasemixin method)": [[14, "botorch.utils.testing.ConstrainedTestProblemTestCaseMixin.test_num_constraints", false]], "test_optimal_value() (botorch.utils.testing.synthetictestfunctiontestcasemixin method)": [[14, "botorch.utils.testing.SyntheticTestFunctionTestCaseMixin.test_optimal_value", false]], "test_optimizer() (botorch.utils.testing.synthetictestfunctiontestcasemixin method)": [[14, "botorch.utils.testing.SyntheticTestFunctionTestCaseMixin.test_optimizer", false]], "test_ref_point() (botorch.utils.testing.multiobjectivetestproblemtestcasemixin method)": [[14, "botorch.utils.testing.MultiObjectiveTestProblemTestCaseMixin.test_ref_point", false]], "test_x (botorch.cross_validation.cvfolds attribute)": [[1, "botorch.cross_validation.CVFolds.test_X", false]], "test_y (botorch.cross_validation.cvfolds attribute)": [[1, "botorch.cross_validation.CVFolds.test_Y", false]], "test_yvar (botorch.cross_validation.cvfolds attribute)": [[1, "botorch.cross_validation.CVFolds.test_Yvar", false]], "threehumpcamel (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.ThreeHumpCamel", false]], "timeout_sec (botorch.optim.optimize.optimizeacqfinputs attribute)": [[8, "botorch.optim.optimize.OptimizeAcqfInputs.timeout_sec", false]], "torch_minimize() (in module botorch.optim.core)": [[8, "botorch.optim.core.torch_minimize", false]], "torchattr (class in botorch.optim.utils.model_utils)": [[8, "botorch.optim.utils.model_utils.TorchAttr", false]], "torchposterior (class in botorch.posteriors.torch)": [[9, "botorch.posteriors.torch.TorchPosterior", false]], "toyrobust (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.ToyRobust", false]], "train() (botorch.models.approximate_gp.approximategpytorchmodel method)": [[7, "botorch.models.approximate_gp.ApproximateGPyTorchModel.train", false]], "train() (botorch.models.fully_bayesian.saasfullybayesiansingletaskgp method)": [[7, "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP.train", false]], "train() (botorch.models.fully_bayesian_multitask.saasfullybayesianmultitaskgp method)": [[7, "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP.train", false]], "train() (botorch.models.model.model method)": [[7, "botorch.models.model.Model.train", false]], "train() (botorch.models.multitask.kroneckermultitaskgp method)": [[7, "botorch.models.multitask.KroneckerMultiTaskGP.train", false]], "train_full_covar (botorch.models.multitask.kroneckermultitaskgp property)": [[7, "botorch.models.multitask.KroneckerMultiTaskGP.train_full_covar", false]], "train_x (botorch.cross_validation.cvfolds attribute)": [[1, "botorch.cross_validation.CVFolds.train_X", false]], "train_y (botorch.cross_validation.cvfolds attribute)": [[1, "botorch.cross_validation.CVFolds.train_Y", false]], "train_yvar (botorch.cross_validation.cvfolds attribute)": [[1, "botorch.cross_validation.CVFolds.train_Yvar", false]], "transform() (botorch.models.transforms.input.appendfeatures method)": [[7, "botorch.models.transforms.input.AppendFeatures.transform", false]], "transform() (botorch.models.transforms.input.batchbroadcastedinputtransform method)": [[7, "botorch.models.transforms.input.BatchBroadcastedInputTransform.transform", false]], "transform() (botorch.models.transforms.input.chainedinputtransform method)": [[7, "botorch.models.transforms.input.ChainedInputTransform.transform", false]], "transform() (botorch.models.transforms.input.filterfeatures method)": [[7, "botorch.models.transforms.input.FilterFeatures.transform", false]], "transform() (botorch.models.transforms.input.inputperturbation method)": [[7, "botorch.models.transforms.input.InputPerturbation.transform", false]], "transform() (botorch.models.transforms.input.inputtransform method)": [[7, "botorch.models.transforms.input.InputTransform.transform", false]], "transform() (botorch.models.transforms.input.onehottonumeric method)": [[7, "botorch.models.transforms.input.OneHotToNumeric.transform", false]], "transform() (botorch.models.transforms.input.reversibleinputtransform method)": [[7, "botorch.models.transforms.input.ReversibleInputTransform.transform", false]], "transform() (botorch.models.transforms.input.round method)": [[7, "botorch.models.transforms.input.Round.transform", false]], "transform_constraints() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.transform_constraints", false]], "transform_inputs() (botorch.models.model.fantasizemixin method)": [[7, "botorch.models.model.FantasizeMixin.transform_inputs", false]], "transform_inputs() (botorch.models.model.model method)": [[7, "botorch.models.model.Model.transform_inputs", false]], "transform_inputs() (botorch.models.model.modellist method)": [[7, "botorch.models.model.ModelList.transform_inputs", false]], "transform_inter_point_constraint() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.transform_inter_point_constraint", false]], "transform_intra_point_constraint() (in module botorch.optim.initializers)": [[8, "botorch.optim.initializers.transform_intra_point_constraint", false]], "transform_on_eval (botorch.models.transforms.input.inputtransform attribute)": [[7, "botorch.models.transforms.input.InputTransform.transform_on_eval", false]], "transform_on_fantasize (botorch.models.transforms.input.inputtransform attribute)": [[7, "botorch.models.transforms.input.InputTransform.transform_on_fantasize", false]], "transform_on_train (botorch.models.transforms.input.inputtransform attribute)": [[7, "botorch.models.transforms.input.InputTransform.transform_on_train", false]], "transformedmodulemixin (class in botorch.sampling.pathwise.utils)": [[10, "botorch.sampling.pathwise.utils.TransformedModuleMixin", false]], "transformedposterior (class in botorch.posteriors.transformed)": [[9, "botorch.posteriors.transformed.TransformedPosterior", false]], "tril (botorch.utils.probability.linalg.pivotedcholesky attribute)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.tril", false]], "truncatedmultivariatenormal (class in botorch.utils.probability.truncated_multivariate_normal)": [[14, "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal", false]], "type_bypassing_encoder() (in module botorch.utils.dispatcher)": [[14, "botorch.utils.dispatcher.type_bypassing_encoder", false]], "unconsolidated_utility (botorch.models.pairwise_gp.pairwisegp property)": [[7, "botorch.models.pairwise_gp.PairwiseGP.unconsolidated_utility", false]], "unifiedskewnormal (class in botorch.utils.probability.unified_skew_normal)": [[14, "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal", false]], "unitqualityfunction (class in botorch.models.utils.inducing_point_allocators)": [[7, "botorch.models.utils.inducing_point_allocators.UnitQualityFunction", false]], "unnormalize() (in module botorch.utils.transforms)": [[14, "botorch.utils.transforms.unnormalize", false]], "unsupportederror": [[2, "botorch.exceptions.errors.UnsupportedError", false]], "untransform() (botorch.models.higher_order_gp.flattenedstandardize method)": [[7, "botorch.models.higher_order_gp.FlattenedStandardize.untransform", false]], "untransform() (botorch.models.transforms.input.batchbroadcastedinputtransform method)": [[7, "botorch.models.transforms.input.BatchBroadcastedInputTransform.untransform", false]], "untransform() (botorch.models.transforms.input.chainedinputtransform method)": [[7, "botorch.models.transforms.input.ChainedInputTransform.untransform", false]], "untransform() (botorch.models.transforms.input.inputtransform method)": [[7, "botorch.models.transforms.input.InputTransform.untransform", false]], "untransform() (botorch.models.transforms.input.onehottonumeric method)": [[7, "botorch.models.transforms.input.OneHotToNumeric.untransform", false]], "untransform() (botorch.models.transforms.input.reversibleinputtransform method)": [[7, "botorch.models.transforms.input.ReversibleInputTransform.untransform", false]], "untransform() (botorch.models.transforms.outcome.bilog method)": [[7, "botorch.models.transforms.outcome.Bilog.untransform", false]], "untransform() (botorch.models.transforms.outcome.chainedoutcometransform method)": [[7, "botorch.models.transforms.outcome.ChainedOutcomeTransform.untransform", false]], "untransform() (botorch.models.transforms.outcome.log method)": [[7, "botorch.models.transforms.outcome.Log.untransform", false]], "untransform() (botorch.models.transforms.outcome.outcometransform method)": [[7, "botorch.models.transforms.outcome.OutcomeTransform.untransform", false]], "untransform() (botorch.models.transforms.outcome.power method)": [[7, "botorch.models.transforms.outcome.Power.untransform", false]], "untransform() (botorch.models.transforms.outcome.standardize method)": [[7, "botorch.models.transforms.outcome.Standardize.untransform", false]], "untransform_posterior() (botorch.models.higher_order_gp.flattenedstandardize method)": [[7, "botorch.models.higher_order_gp.FlattenedStandardize.untransform_posterior", false]], "untransform_posterior() (botorch.models.transforms.outcome.bilog method)": [[7, "botorch.models.transforms.outcome.Bilog.untransform_posterior", false]], "untransform_posterior() (botorch.models.transforms.outcome.chainedoutcometransform method)": [[7, "botorch.models.transforms.outcome.ChainedOutcomeTransform.untransform_posterior", false]], "untransform_posterior() (botorch.models.transforms.outcome.log method)": [[7, "botorch.models.transforms.outcome.Log.untransform_posterior", false]], "untransform_posterior() (botorch.models.transforms.outcome.outcometransform method)": [[7, "botorch.models.transforms.outcome.OutcomeTransform.untransform_posterior", false]], "untransform_posterior() (botorch.models.transforms.outcome.power method)": [[7, "botorch.models.transforms.outcome.Power.untransform_posterior", false]], "untransform_posterior() (botorch.models.transforms.outcome.standardize method)": [[7, "botorch.models.transforms.outcome.Standardize.untransform_posterior", false]], "update() (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.update", false]], "update() (botorch.utils.multi_objective.box_decompositions.box_decomposition.fastpartitioning method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.FastPartitioning.update", false]], "update() (botorch.utils.multi_objective.box_decompositions.box_decomposition_list.boxdecompositionlist method)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList.update", false]], "update() (botorch.utils.torch.bufferdict method)": [[14, "botorch.utils.torch.BufferDict.update", false]], "update_() (botorch.utils.probability.linalg.pivotedcholesky method)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.update_", false]], "update_local_upper_bounds_incremental() (in module botorch.utils.multi_objective.box_decompositions.utils)": [[14, "botorch.utils.multi_objective.box_decompositions.utils.update_local_upper_bounds_incremental", false]], "upperconfidencebound (class in botorch.acquisition.analytic)": [[0, "botorch.acquisition.analytic.UpperConfidenceBound", false]], "userinputwarning": [[2, "botorch.exceptions.warnings.UserInputWarning", false]], "v_max (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.V_max", false]], "validate_init (botorch.utils.probability.linalg.pivotedcholesky attribute)": [[14, "botorch.utils.probability.linalg.PivotedCholesky.validate_init", false]], "validate_input_scaling (class in botorch.settings)": [[11, "botorch.settings.validate_input_scaling", false]], "validate_input_scaling() (in module botorch.models.utils.assorted)": [[7, "botorch.models.utils.assorted.validate_input_scaling", false]], "value (botorch.optim.homotopy.fixedhomotopyschedule property)": [[8, "botorch.optim.homotopy.FixedHomotopySchedule.value", false]], "values (botorch.utils.containers.densecontainer attribute)": [[14, "botorch.utils.containers.DenseContainer.values", false]], "values (botorch.utils.containers.slicecontainer attribute)": [[14, "botorch.utils.containers.SliceContainer.values", false]], "values (botorch.utils.context_managers.tensorcheckpoint attribute)": [[14, "botorch.utils.context_managers.TensorCheckpoint.values", false]], "values() (botorch.sampling.pathwise.paths.pathdict method)": [[10, "botorch.sampling.pathwise.paths.PathDict.values", false]], "values() (botorch.utils.torch.bufferdict method)": [[14, "botorch.utils.torch.BufferDict.values", false]], "var (class in botorch.acquisition.risk_measures)": [[0, "botorch.acquisition.risk_measures.VaR", false]], "variance (botorch.posteriors.ensemble.ensembleposterior property)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.variance", false]], "variance (botorch.posteriors.gpytorch.gpytorchposterior property)": [[9, "botorch.posteriors.gpytorch.GPyTorchPosterior.variance", false]], "variance (botorch.posteriors.posterior_list.posteriorlist property)": [[9, "botorch.posteriors.posterior_list.PosteriorList.variance", false]], "variance (botorch.posteriors.transformed.transformedposterior property)": [[9, "botorch.posteriors.transformed.TransformedPosterior.variance", false]], "variance (botorch.utils.testing.mockposterior property)": [[14, "botorch.utils.testing.MockPosterior.variance", false]], "vehiclesafety (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.VehicleSafety", false]], "warmstart_multistep() (in module botorch.acquisition.multi_step_lookahead)": [[0, "botorch.acquisition.multi_step_lookahead.warmstart_multistep", false]], "warp (class in botorch.models.transforms.input)": [[7, "botorch.models.transforms.input.Warp", false]], "weightedmcmultioutputobjective (class in botorch.acquisition.multi_objective.objective)": [[0, "botorch.acquisition.multi_objective.objective.WeightedMCMultiOutputObjective", false]], "weights (botorch.posteriors.ensemble.ensembleposterior property)": [[9, "botorch.posteriors.ensemble.EnsemblePosterior.weights", false]], "weldedbeam (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.WeldedBeam", false]], "weldedbeamso (class in botorch.test_functions.synthetic)": [[12, "botorch.test_functions.synthetic.WeldedBeamSO", false]], "worstcase (class in botorch.acquisition.risk_measures)": [[0, "botorch.acquisition.risk_measures.WorstCase", false]], "x (botorch.utils.datasets.contextualdataset property)": [[14, "botorch.utils.datasets.ContextualDataset.X", false]], "x (botorch.utils.datasets.multitaskdataset property)": [[14, "botorch.utils.datasets.MultiTaskDataset.X", false]], "x (botorch.utils.datasets.superviseddataset property)": [[14, "botorch.utils.datasets.SupervisedDataset.X", false]], "x_baseline (botorch.utils.multi_objective.hypervolume.noisyexpectedhypervolumemixin property)": [[14, "botorch.utils.multi_objective.hypervolume.NoisyExpectedHypervolumeMixin.X_baseline", false]], "x_evaluation_mask (botorch.acquisition.decoupled.decoupledacquisitionfunction property)": [[0, "botorch.acquisition.decoupled.DecoupledAcquisitionFunction.X_evaluation_mask", false]], "x_pending (botorch.acquisition.fixed_feature.fixedfeatureacquisitionfunction property)": [[0, "botorch.acquisition.fixed_feature.FixedFeatureAcquisitionFunction.X_pending", false]], "x_pending (botorch.acquisition.penalized.penalizedacquisitionfunction property)": [[0, "botorch.acquisition.penalized.PenalizedAcquisitionFunction.X_pending", false]], "xs (botorch.utils.gp_sampling.gpdraw property)": [[14, "botorch.utils.gp_sampling.GPDraw.Xs", false]], "y (botorch.utils.datasets.contextualdataset property)": [[14, "botorch.utils.datasets.ContextualDataset.Y", false]], "y (botorch.utils.datasets.multitaskdataset property)": [[14, "botorch.utils.datasets.MultiTaskDataset.Y", false]], "y (botorch.utils.datasets.superviseddataset property)": [[14, "botorch.utils.datasets.SupervisedDataset.Y", false]], "y (botorch.utils.multi_objective.box_decompositions.box_decomposition.boxdecomposition property)": [[14, "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition.Y", false]], "y_ps (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.Y_ps", false]], "y_xs (botorch.test_functions.multi_objective.penicillin attribute)": [[12, "botorch.test_functions.multi_objective.Penicillin.Y_xs", false]], "ys (botorch.utils.gp_sampling.gpdraw property)": [[14, "botorch.utils.gp_sampling.GPDraw.Ys", false]], "yvar (botorch.utils.datasets.contextualdataset property)": [[14, "botorch.utils.datasets.ContextualDataset.Yvar", false]], "yvar (botorch.utils.datasets.multitaskdataset property)": [[14, "botorch.utils.datasets.MultiTaskDataset.Yvar", false]], "yvar (botorch.utils.datasets.superviseddataset property)": [[14, "botorch.utils.datasets.SupervisedDataset.Yvar", false]], "zdt (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.ZDT", false]], "zdt1 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.ZDT1", false]], "zdt2 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.ZDT2", false]], "zdt3 (class in botorch.test_functions.multi_objective)": [[12, "botorch.test_functions.multi_objective.ZDT3", false]], "zero_grad_ctx() (in module botorch.utils.context_managers)": [[14, "botorch.utils.context_managers.zero_grad_ctx", false]]}, "objects": {"botorch": [[0, 0, 0, "-", "acquisition"], [1, 0, 0, "-", "cross_validation"], [2, 0, 0, "-", "exceptions"], [3, 0, 0, "-", "fit"], [4, 0, 0, "-", "generation"], [6, 0, 0, "-", "logging"], [7, 0, 0, "-", "models"], [8, 0, 0, "-", "optim"], [9, 0, 0, "-", "posteriors"], [10, 0, 0, "-", "sampling"], [11, 0, 0, "-", "settings"], [12, 0, 0, "-", "test_functions"], [13, 0, 0, "-", "test_utils"], [14, 0, 0, "-", "utils"]], "botorch.acquisition": [[0, 0, 0, "-", "acquisition"], [0, 0, 0, "-", "active_learning"], [0, 0, 0, "-", "analytic"], [0, 0, 0, "-", "bayesian_active_learning"], [0, 0, 0, "-", "cached_cholesky"], [0, 0, 0, "-", "cost_aware"], [0, 0, 0, "-", "decoupled"], [0, 0, 0, "-", "factory"], [0, 0, 0, "-", "fixed_feature"], [0, 0, 0, "-", "input_constructors"], [0, 0, 0, "-", "joint_entropy_search"], [0, 0, 0, "-", "knowledge_gradient"], [0, 0, 0, "-", "logei"], [0, 0, 0, "-", "max_value_entropy_search"], [0, 0, 0, "-", "monte_carlo"], [0, 0, 0, "-", "multi_step_lookahead"], [0, 0, 0, "-", "objective"], [0, 0, 0, "-", "penalized"], [0, 0, 0, "-", "predictive_entropy_search"], [0, 0, 0, "-", "preference"], [0, 0, 0, "-", "prior_guided"], [0, 0, 0, "-", "proximal"], [0, 0, 0, "-", "risk_measures"], [0, 0, 0, "-", "thompson_sampling"], [0, 0, 0, "-", "utils"]], "botorch.acquisition.acquisition": [[0, 1, 1, "", "AcquisitionFunction"], [0, 1, 1, "", "MCSamplerMixin"], [0, 1, 1, "", "MultiModelAcquisitionFunction"], [0, 1, 1, "", "OneShotAcquisitionFunction"]], "botorch.acquisition.acquisition.AcquisitionFunction": [[0, 2, 1, "", "forward"], [0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.acquisition.MCSamplerMixin": [[0, 3, 1, "", "_default_sample_shape"], [0, 2, 1, "", "get_posterior_samples"], [0, 4, 1, "", "sample_shape"]], "botorch.acquisition.acquisition.OneShotAcquisitionFunction": [[0, 2, 1, "", "extract_candidates"], [0, 2, 1, "", "get_augmented_q_batch_size"]], "botorch.acquisition.active_learning": [[0, 1, 1, "", "PairwiseMCPosteriorVariance"], [0, 1, 1, "", "qNegIntegratedPosteriorVariance"]], "botorch.acquisition.active_learning.PairwiseMCPosteriorVariance": [[0, 2, 1, "", "forward"]], "botorch.acquisition.active_learning.qNegIntegratedPosteriorVariance": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic": [[0, 1, 1, "", "AnalyticAcquisitionFunction"], [0, 1, 1, "", "ConstrainedExpectedImprovement"], [0, 1, 1, "", "ExpectedImprovement"], [0, 1, 1, "", "LogConstrainedExpectedImprovement"], [0, 1, 1, "", "LogExpectedImprovement"], [0, 1, 1, "", "LogNoisyExpectedImprovement"], [0, 1, 1, "", "LogProbabilityOfImprovement"], [0, 1, 1, "", "NoisyExpectedImprovement"], [0, 1, 1, "", "PosteriorMean"], [0, 1, 1, "", "PosteriorStandardDeviation"], [0, 1, 1, "", "ProbabilityOfImprovement"], [0, 1, 1, "", "ScalarizedPosteriorMean"], [0, 1, 1, "", "UpperConfidenceBound"], [0, 1, 1, "", "qAnalyticProbabilityOfImprovement"]], "botorch.acquisition.analytic.AnalyticAcquisitionFunction": [[0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.analytic.ConstrainedExpectedImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.ExpectedImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.LogConstrainedExpectedImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.LogExpectedImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.LogNoisyExpectedImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.LogProbabilityOfImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.NoisyExpectedImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.PosteriorMean": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.PosteriorStandardDeviation": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.ProbabilityOfImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.ScalarizedPosteriorMean": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.UpperConfidenceBound": [[0, 2, 1, "", "forward"]], "botorch.acquisition.analytic.qAnalyticProbabilityOfImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.bayesian_active_learning": [[0, 1, 1, "", "FullyBayesianAcquisitionFunction"], [0, 5, 1, "", "check_negative_info_gain"], [0, 1, 1, "", "qBayesianActiveLearningByDisagreement"]], "botorch.acquisition.bayesian_active_learning.qBayesianActiveLearningByDisagreement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.cached_cholesky": [[0, 1, 1, "", "CachedCholeskyMCSamplerMixin"], [0, 5, 1, "", "supports_cache_root"]], "botorch.acquisition.cost_aware": [[0, 1, 1, "", "CostAwareUtility"], [0, 1, 1, "", "GenericCostAwareUtility"], [0, 1, 1, "", "InverseCostWeightedUtility"]], "botorch.acquisition.cost_aware.CostAwareUtility": [[0, 2, 1, "", "forward"]], "botorch.acquisition.cost_aware.GenericCostAwareUtility": [[0, 2, 1, "", "forward"]], "botorch.acquisition.cost_aware.InverseCostWeightedUtility": [[0, 2, 1, "", "forward"]], "botorch.acquisition.decoupled": [[0, 1, 1, "", "DecoupledAcquisitionFunction"]], "botorch.acquisition.decoupled.DecoupledAcquisitionFunction": [[0, 4, 1, "", "X_evaluation_mask"], [0, 2, 1, "", "construct_evaluation_mask"], [0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.factory": [[0, 5, 1, "", "get_acquisition_function"]], "botorch.acquisition.fixed_feature": [[0, 1, 1, "", "FixedFeatureAcquisitionFunction"], [0, 5, 1, "", "get_device_of_sequence"], [0, 5, 1, "", "get_dtype_of_sequence"]], "botorch.acquisition.fixed_feature.FixedFeatureAcquisitionFunction": [[0, 4, 1, "", "X_pending"], [0, 2, 1, "", "forward"]], "botorch.acquisition.input_constructors": [[0, 5, 1, "", "acqf_input_constructor"], [0, 5, 1, "", "allow_only_specific_variable_kwargs"], [0, 5, 1, "", "construct_inputs_BALD"], [0, 5, 1, "", "construct_inputs_EHVI"], [0, 5, 1, "", "construct_inputs_NIPV"], [0, 5, 1, "", "construct_inputs_analytic_eubo"], [0, 5, 1, "", "construct_inputs_best_f"], [0, 5, 1, "", "construct_inputs_mf_base"], [0, 5, 1, "", "construct_inputs_noisy_ei"], [0, 5, 1, "", "construct_inputs_posterior_mean"], [0, 5, 1, "", "construct_inputs_qEHVI"], [0, 5, 1, "", "construct_inputs_qEI"], [0, 5, 1, "", "construct_inputs_qHVKG"], [0, 5, 1, "", "construct_inputs_qJES"], [0, 5, 1, "", "construct_inputs_qKG"], [0, 5, 1, "", "construct_inputs_qLogEI"], [0, 5, 1, "", "construct_inputs_qLogNEHVI"], [0, 5, 1, "", "construct_inputs_qLogNEI"], [0, 5, 1, "", "construct_inputs_qLogNParEGO"], [0, 5, 1, "", "construct_inputs_qMES"], [0, 5, 1, "", "construct_inputs_qMFHVKG"], [0, 5, 1, "", "construct_inputs_qMFKG"], [0, 5, 1, "", "construct_inputs_qMFMES"], [0, 5, 1, "", "construct_inputs_qNEHVI"], [0, 5, 1, "", "construct_inputs_qNEI"], [0, 5, 1, "", "construct_inputs_qPI"], [0, 5, 1, "", "construct_inputs_qSimpleRegret"], [0, 5, 1, "", "construct_inputs_qUCB"], [0, 5, 1, "", "construct_inputs_qeubo"], [0, 5, 1, "", "construct_inputs_ucb"], [0, 5, 1, "", "get_acqf_input_constructor"], [0, 5, 1, "", "get_best_f_analytic"], [0, 5, 1, "", "get_best_f_mc"], [0, 5, 1, "", "optimize_objective"]], "botorch.acquisition.joint_entropy_search": [[0, 1, 1, "", "qJointEntropySearch"]], "botorch.acquisition.joint_entropy_search.qJointEntropySearch": [[0, 2, 1, "", "forward"]], "botorch.acquisition.knowledge_gradient": [[0, 1, 1, "", "ProjectedAcquisitionFunction"], [0, 1, 1, "", "qKnowledgeGradient"], [0, 1, 1, "", "qMultiFidelityKnowledgeGradient"]], "botorch.acquisition.knowledge_gradient.ProjectedAcquisitionFunction": [[0, 2, 1, "", "forward"]], "botorch.acquisition.knowledge_gradient.qKnowledgeGradient": [[0, 2, 1, "", "evaluate"], [0, 2, 1, "", "extract_candidates"], [0, 2, 1, "", "forward"], [0, 2, 1, "", "get_augmented_q_batch_size"]], "botorch.acquisition.knowledge_gradient.qMultiFidelityKnowledgeGradient": [[0, 4, 1, "", "cost_sampler"], [0, 2, 1, "", "forward"]], "botorch.acquisition.logei": [[0, 1, 1, "", "LogImprovementMCAcquisitionFunction"], [0, 5, 1, "", "check_tau"], [0, 1, 1, "", "qLogExpectedImprovement"], [0, 1, 1, "", "qLogNoisyExpectedImprovement"]], "botorch.acquisition.logei.qLogNoisyExpectedImprovement": [[0, 2, 1, "", "compute_best_f"]], "botorch.acquisition.max_value_entropy_search": [[0, 1, 1, "", "DiscreteMaxValueBase"], [0, 1, 1, "", "MaxValueBase"], [0, 1, 1, "", "qLowerBoundMaxValueEntropy"], [0, 1, 1, "", "qMaxValueEntropy"], [0, 1, 1, "", "qMultiFidelityLowerBoundMaxValueEntropy"], [0, 1, 1, "", "qMultiFidelityMaxValueEntropy"]], "botorch.acquisition.max_value_entropy_search.MaxValueBase": [[0, 2, 1, "", "forward"], [0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.max_value_entropy_search.qMaxValueEntropy": [[0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.max_value_entropy_search.qMultiFidelityMaxValueEntropy": [[0, 4, 1, "", "cost_sampler"], [0, 2, 1, "", "forward"]], "botorch.acquisition.monte_carlo": [[0, 1, 1, "", "MCAcquisitionFunction"], [0, 1, 1, "", "SampleReducingMCAcquisitionFunction"], [0, 1, 1, "", "SampleReductionProtocol"], [0, 1, 1, "", "qExpectedImprovement"], [0, 1, 1, "", "qLowerConfidenceBound"], [0, 1, 1, "", "qNoisyExpectedImprovement"], [0, 1, 1, "", "qProbabilityOfImprovement"], [0, 1, 1, "", "qSimpleRegret"], [0, 1, 1, "", "qUpperConfidenceBound"]], "botorch.acquisition.monte_carlo.MCAcquisitionFunction": [[0, 2, 1, "", "forward"]], "botorch.acquisition.monte_carlo.SampleReducingMCAcquisitionFunction": [[0, 2, 1, "", "forward"]], "botorch.acquisition.monte_carlo.qNoisyExpectedImprovement": [[0, 2, 1, "", "compute_best_f"]], "botorch.acquisition.multi_objective": [[0, 0, 0, "-", "analytic"], [0, 0, 0, "-", "base"], [0, 0, 0, "-", "hypervolume_knowledge_gradient"], [0, 0, 0, "-", "joint_entropy_search"], [0, 0, 0, "-", "logei"], [0, 0, 0, "-", "max_value_entropy_search"], [0, 0, 0, "-", "monte_carlo"], [0, 0, 0, "-", "multi_fidelity"], [0, 0, 0, "-", "multi_output_risk_measures"], [0, 0, 0, "-", "objective"], [0, 0, 0, "-", "parego"], [0, 0, 0, "-", "predictive_entropy_search"], [0, 0, 0, "-", "utils"]], "botorch.acquisition.multi_objective.analytic": [[0, 1, 1, "", "ExpectedHypervolumeImprovement"]], "botorch.acquisition.multi_objective.analytic.ExpectedHypervolumeImprovement": [[0, 2, 1, "", "forward"], [0, 2, 1, "", "nu"], [0, 2, 1, "", "psi"]], "botorch.acquisition.multi_objective.base": [[0, 1, 1, "", "MultiObjectiveAnalyticAcquisitionFunction"], [0, 1, 1, "", "MultiObjectiveMCAcquisitionFunction"]], "botorch.acquisition.multi_objective.base.MultiObjectiveAnalyticAcquisitionFunction": [[0, 2, 1, "", "forward"], [0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.multi_objective.base.MultiObjectiveMCAcquisitionFunction": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient": [[0, 1, 1, "", "qHypervolumeKnowledgeGradient"], [0, 1, 1, "", "qMultiFidelityHypervolumeKnowledgeGradient"]], "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qHypervolumeKnowledgeGradient": [[0, 4, 1, "", "cost_sampler"], [0, 2, 1, "", "extract_candidates"], [0, 2, 1, "", "forward"], [0, 2, 1, "", "get_augmented_q_batch_size"]], "botorch.acquisition.multi_objective.hypervolume_knowledge_gradient.qMultiFidelityHypervolumeKnowledgeGradient": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.joint_entropy_search": [[0, 1, 1, "", "LowerBoundMultiObjectiveEntropySearch"], [0, 1, 1, "", "qLowerBoundMultiObjectiveJointEntropySearch"]], "botorch.acquisition.multi_objective.joint_entropy_search.LowerBoundMultiObjectiveEntropySearch": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.joint_entropy_search.qLowerBoundMultiObjectiveJointEntropySearch": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.logei": [[0, 1, 1, "", "qLogExpectedHypervolumeImprovement"], [0, 1, 1, "", "qLogNoisyExpectedHypervolumeImprovement"]], "botorch.acquisition.multi_objective.logei.qLogExpectedHypervolumeImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.max_value_entropy_search": [[0, 1, 1, "", "qLowerBoundMultiObjectiveMaxValueEntropySearch"], [0, 1, 1, "", "qMultiObjectiveMaxValueEntropy"]], "botorch.acquisition.multi_objective.max_value_entropy_search.qLowerBoundMultiObjectiveMaxValueEntropySearch": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.max_value_entropy_search.qMultiObjectiveMaxValueEntropy": [[0, 3, 1, "", "_default_sample_shape"], [0, 2, 1, "", "forward"], [0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.multi_objective.monte_carlo": [[0, 1, 1, "", "qExpectedHypervolumeImprovement"], [0, 1, 1, "", "qNoisyExpectedHypervolumeImprovement"]], "botorch.acquisition.multi_objective.monte_carlo.qExpectedHypervolumeImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.monte_carlo.qNoisyExpectedHypervolumeImprovement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.multi_fidelity": [[0, 1, 1, "", "MOMF"]], "botorch.acquisition.multi_objective.multi_fidelity.MOMF": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.multi_output_risk_measures": [[0, 1, 1, "", "IndependentCVaR"], [0, 1, 1, "", "IndependentVaR"], [0, 1, 1, "", "MARS"], [0, 1, 1, "", "MVaR"], [0, 1, 1, "", "MultiOutputExpectation"], [0, 1, 1, "", "MultiOutputRiskMeasureMCObjective"], [0, 1, 1, "", "MultiOutputWorstCase"]], "botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentCVaR": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.multi_output_risk_measures.IndependentVaR": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.multi_output_risk_measures.MARS": [[0, 4, 1, "", "baseline_Y"], [0, 4, 1, "", "chebyshev_objective"], [0, 4, 1, "", "chebyshev_weights"], [0, 2, 1, "", "set_baseline_Y"]], "botorch.acquisition.multi_objective.multi_output_risk_measures.MVaR": [[0, 2, 1, "", "forward"], [0, 2, 1, "", "get_mvar_set_vectorized"], [0, 2, 1, "", "get_mvar_set_via_counting"], [0, 2, 1, "", "make_differentiable"]], "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputExpectation": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputRiskMeasureMCObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.multi_output_risk_measures.MultiOutputWorstCase": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.objective": [[0, 1, 1, "", "FeasibilityWeightedMCMultiOutputObjective"], [0, 1, 1, "", "GenericMCMultiOutputObjective"], [0, 1, 1, "", "IdentityMCMultiOutputObjective"], [0, 1, 1, "", "MCMultiOutputObjective"], [0, 1, 1, "", "WeightedMCMultiOutputObjective"]], "botorch.acquisition.multi_objective.objective.FeasibilityWeightedMCMultiOutputObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.objective.IdentityMCMultiOutputObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.objective.MCMultiOutputObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.objective.WeightedMCMultiOutputObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.parego": [[0, 1, 1, "", "qLogNParEGO"]], "botorch.acquisition.multi_objective.predictive_entropy_search": [[0, 5, 1, "", "log_cdf_robust"], [0, 1, 1, "", "qMultiObjectivePredictiveEntropySearch"]], "botorch.acquisition.multi_objective.predictive_entropy_search.qMultiObjectivePredictiveEntropySearch": [[0, 2, 1, "", "forward"]], "botorch.acquisition.multi_objective.utils": [[0, 5, 1, "", "compute_sample_box_decomposition"], [0, 5, 1, "", "get_default_partitioning_alpha"], [0, 5, 1, "", "prune_inferior_points_multi_objective"], [0, 5, 1, "", "random_search_optimizer"], [0, 5, 1, "", "sample_optimal_points"]], "botorch.acquisition.multi_step_lookahead": [[0, 5, 1, "", "make_best_f"], [0, 1, 1, "", "qMultiStepLookahead"], [0, 5, 1, "", "warmstart_multistep"]], "botorch.acquisition.multi_step_lookahead.qMultiStepLookahead": [[0, 2, 1, "", "extract_candidates"], [0, 2, 1, "", "forward"], [0, 2, 1, "", "get_augmented_q_batch_size"], [0, 2, 1, "", "get_induced_fantasy_model"], [0, 2, 1, "", "get_multi_step_tree_input_representation"], [0, 2, 1, "", "get_split_shapes"]], "botorch.acquisition.objective": [[0, 1, 1, "", "ConstrainedMCObjective"], [0, 1, 1, "", "ExpectationPosteriorTransform"], [0, 1, 1, "", "GenericMCObjective"], [0, 1, 1, "", "IdentityMCObjective"], [0, 1, 1, "", "LearnedObjective"], [0, 1, 1, "", "LinearMCObjective"], [0, 1, 1, "", "MCAcquisitionObjective"], [0, 1, 1, "", "PosteriorTransform"], [0, 1, 1, "", "ScalarizedPosteriorTransform"]], "botorch.acquisition.objective.ConstrainedMCObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.objective.ExpectationPosteriorTransform": [[0, 2, 1, "", "evaluate"], [0, 2, 1, "", "forward"]], "botorch.acquisition.objective.GenericMCObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.objective.IdentityMCObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.objective.LearnedObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.objective.LinearMCObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.objective.MCAcquisitionObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.objective.PosteriorTransform": [[0, 2, 1, "", "evaluate"], [0, 2, 1, "", "forward"]], "botorch.acquisition.objective.ScalarizedPosteriorTransform": [[0, 2, 1, "", "evaluate"], [0, 2, 1, "", "forward"], [0, 3, 1, "", "scalarize"]], "botorch.acquisition.penalized": [[0, 1, 1, "", "GaussianPenalty"], [0, 1, 1, "", "GroupLassoPenalty"], [0, 1, 1, "", "L0Approximation"], [0, 1, 1, "", "L0PenaltyApprox"], [0, 1, 1, "", "L0PenaltyApproxObjective"], [0, 1, 1, "", "L1Penalty"], [0, 1, 1, "", "L1PenaltyObjective"], [0, 1, 1, "", "L2Penalty"], [0, 1, 1, "", "PenalizedAcquisitionFunction"], [0, 1, 1, "", "PenalizedMCObjective"], [0, 5, 1, "", "group_lasso_regularizer"], [0, 5, 1, "", "narrow_gaussian"], [0, 5, 1, "", "nnz_approx"]], "botorch.acquisition.penalized.GaussianPenalty": [[0, 2, 1, "", "forward"]], "botorch.acquisition.penalized.GroupLassoPenalty": [[0, 2, 1, "", "forward"]], "botorch.acquisition.penalized.L1Penalty": [[0, 2, 1, "", "forward"]], "botorch.acquisition.penalized.L1PenaltyObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.penalized.L2Penalty": [[0, 2, 1, "", "forward"]], "botorch.acquisition.penalized.PenalizedAcquisitionFunction": [[0, 4, 1, "", "X_pending"], [0, 2, 1, "", "forward"], [0, 2, 1, "", "set_X_pending"]], "botorch.acquisition.penalized.PenalizedMCObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.predictive_entropy_search": [[0, 1, 1, "", "qPredictiveEntropySearch"]], "botorch.acquisition.predictive_entropy_search.qPredictiveEntropySearch": [[0, 2, 1, "", "forward"]], "botorch.acquisition.preference": [[0, 1, 1, "", "AnalyticExpectedUtilityOfBestOption"], [0, 1, 1, "", "PairwiseBayesianActiveLearningByDisagreement"], [0, 1, 1, "", "qExpectedUtilityOfBestOption"]], "botorch.acquisition.preference.AnalyticExpectedUtilityOfBestOption": [[0, 2, 1, "", "forward"]], "botorch.acquisition.preference.PairwiseBayesianActiveLearningByDisagreement": [[0, 2, 1, "", "forward"]], "botorch.acquisition.preference.qExpectedUtilityOfBestOption": [[0, 2, 1, "", "forward"]], "botorch.acquisition.prior_guided": [[0, 1, 1, "", "PriorGuidedAcquisitionFunction"]], "botorch.acquisition.prior_guided.PriorGuidedAcquisitionFunction": [[0, 2, 1, "", "forward"]], "botorch.acquisition.proximal": [[0, 1, 1, "", "ProximalAcquisitionFunction"]], "botorch.acquisition.proximal.ProximalAcquisitionFunction": [[0, 2, 1, "", "forward"]], "botorch.acquisition.risk_measures": [[0, 1, 1, "", "CVaR"], [0, 1, 1, "", "Expectation"], [0, 1, 1, "", "RiskMeasureMCObjective"], [0, 1, 1, "", "VaR"], [0, 1, 1, "", "WorstCase"]], "botorch.acquisition.risk_measures.CVaR": [[0, 2, 1, "", "forward"]], "botorch.acquisition.risk_measures.Expectation": [[0, 2, 1, "", "forward"]], "botorch.acquisition.risk_measures.RiskMeasureMCObjective": [[0, 2, 1, "", "forward"]], "botorch.acquisition.risk_measures.VaR": [[0, 2, 1, "", "forward"]], "botorch.acquisition.risk_measures.WorstCase": [[0, 2, 1, "", "forward"]], "botorch.acquisition.thompson_sampling": [[0, 1, 1, "", "PathwiseThompsonSampling"]], "botorch.acquisition.thompson_sampling.PathwiseThompsonSampling": [[0, 2, 1, "", "forward"], [0, 2, 1, "", "redraw"]], "botorch.acquisition.utils": [[0, 5, 1, "", "compute_best_feasible_objective"], [0, 5, 1, "", "expand_trace_observations"], [0, 5, 1, "", "get_acquisition_function"], [0, 5, 1, "", "get_infeasible_cost"], [0, 5, 1, "", "get_optimal_samples"], [0, 5, 1, "", "project_to_sample_points"], [0, 5, 1, "", "project_to_target_fidelity"], [0, 5, 1, "", "prune_inferior_points"], [0, 5, 1, "", "repeat_to_match_aug_dim"]], "botorch.cross_validation": [[1, 1, 1, "", "CVFolds"], [1, 1, 1, "", "CVResults"], [1, 5, 1, "", "batch_cross_validation"], [1, 5, 1, "", "gen_loo_cv_folds"]], "botorch.cross_validation.CVFolds": [[1, 3, 1, "", "test_X"], [1, 3, 1, "", "test_Y"], [1, 3, 1, "", "test_Yvar"], [1, 3, 1, "", "train_X"], [1, 3, 1, "", "train_Y"], [1, 3, 1, "", "train_Yvar"]], "botorch.cross_validation.CVResults": [[1, 3, 1, "", "model"], [1, 3, 1, "", "observed_Y"], [1, 3, 1, "", "observed_Yvar"], [1, 3, 1, "", "posterior"]], "botorch.exceptions": [[2, 0, 0, "-", "errors"], [2, 0, 0, "-", "warnings"]], "botorch.exceptions.errors": [[2, 6, 1, "", "BotorchError"], [2, 6, 1, "", "BotorchTensorDimensionError"], [2, 6, 1, "", "CandidateGenerationError"], [2, 6, 1, "", "DeprecationError"], [2, 6, 1, "", "InputDataError"], [2, 6, 1, "", "ModelFittingError"], [2, 6, 1, "", "OptimizationGradientError"], [2, 6, 1, "", "OptimizationTimeoutError"], [2, 6, 1, "", "UnsupportedError"]], "botorch.exceptions.warnings": [[2, 6, 1, "", "BadInitialCandidatesWarning"], [2, 6, 1, "", "BotorchTensorDimensionWarning"], [2, 6, 1, "", "BotorchWarning"], [2, 6, 1, "", "CostAwareWarning"], [2, 6, 1, "", "InputDataWarning"], [2, 6, 1, "", "NumericsWarning"], [2, 6, 1, "", "OptimizationWarning"], [2, 6, 1, "", "SamplingWarning"], [2, 6, 1, "", "UserInputWarning"], [2, 5, 1, "", "legacy_ei_numerics_warning"]], "botorch.fit": [[3, 5, 1, "", "fit_fully_bayesian_model_nuts"], [3, 5, 1, "", "fit_gpytorch_mll"]], "botorch.generation": [[4, 0, 0, "-", "gen"], [4, 0, 0, "-", "sampling"], [4, 0, 0, "-", "utils"]], "botorch.generation.gen": [[4, 5, 1, "", "gen_candidates_scipy"], [4, 5, 1, "", "gen_candidates_torch"], [4, 5, 1, "", "get_best_candidates"]], "botorch.generation.sampling": [[4, 1, 1, "", "BoltzmannSampling"], [4, 1, 1, "", "ConstrainedMaxPosteriorSampling"], [4, 1, 1, "", "MaxPosteriorSampling"], [4, 1, 1, "", "SamplingStrategy"]], "botorch.generation.sampling.BoltzmannSampling": [[4, 2, 1, "", "forward"]], "botorch.generation.sampling.ConstrainedMaxPosteriorSampling": [[4, 2, 1, "", "forward"]], "botorch.generation.sampling.MaxPosteriorSampling": [[4, 2, 1, "", "forward"], [4, 2, 1, "", "maximize_samples"]], "botorch.generation.sampling.SamplingStrategy": [[4, 2, 1, "", "forward"]], "botorch.logging": [[6, 5, 1, "", "shape_to_str"]], "botorch.models": [[7, 0, 0, "-", "approximate_gp"], [7, 0, 0, "-", "contextual"], [7, 0, 0, "-", "contextual_multioutput"], [7, 0, 0, "-", "converter"], [7, 0, 0, "-", "cost"], [7, 0, 0, "-", "deterministic"], [7, 0, 0, "-", "ensemble"], [7, 0, 0, "-", "fully_bayesian"], [7, 0, 0, "-", "fully_bayesian_multitask"], [7, 0, 0, "-", "gp_regression"], [7, 0, 0, "-", "gp_regression_fidelity"], [7, 0, 0, "-", "gp_regression_mixed"], [7, 0, 0, "-", "gpytorch"], [7, 0, 0, "-", "higher_order_gp"], [7, 0, 0, "-", "model"], [7, 0, 0, "-", "model_list_gp_regression"], [7, 0, 0, "-", "multitask"], [7, 0, 0, "-", "pairwise_gp"]], "botorch.models.approximate_gp": [[7, 1, 1, "", "ApproximateGPyTorchModel"], [7, 1, 1, "", "SingleTaskVariationalGP"]], "botorch.models.approximate_gp.ApproximateGPyTorchModel": [[7, 2, 1, "", "eval"], [7, 2, 1, "", "forward"], [7, 4, 1, "", "num_outputs"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "train"]], "botorch.models.approximate_gp.SingleTaskVariationalGP": [[7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "init_inducing_points"]], "botorch.models.contextual": [[7, 1, 1, "", "LCEAGP"], [7, 1, 1, "", "SACGP"]], "botorch.models.contextual.LCEAGP": [[7, 2, 1, "", "construct_inputs"]], "botorch.models.contextual.SACGP": [[7, 2, 1, "", "construct_inputs"]], "botorch.models.contextual_multioutput": [[7, 1, 1, "", "LCEMGP"]], "botorch.models.contextual_multioutput.LCEMGP": [[7, 2, 1, "", "construct_inputs"], [7, 2, 1, "", "task_covar_module"]], "botorch.models.converter": [[7, 5, 1, "", "batched_multi_output_to_single_output"], [7, 5, 1, "", "batched_to_model_list"], [7, 5, 1, "", "get_attribute"], [7, 5, 1, "", "model_list_to_batched"], [7, 5, 1, "", "set_attribute"]], "botorch.models.cost": [[7, 1, 1, "", "AffineFidelityCostModel"], [7, 1, 1, "", "FixedCostModel"]], "botorch.models.cost.AffineFidelityCostModel": [[7, 2, 1, "", "forward"]], "botorch.models.cost.FixedCostModel": [[7, 2, 1, "", "forward"]], "botorch.models.deterministic": [[7, 1, 1, "", "AffineDeterministicModel"], [7, 1, 1, "", "DeterministicModel"], [7, 1, 1, "", "FixedSingleSampleModel"], [7, 1, 1, "", "GenericDeterministicModel"], [7, 1, 1, "", "PosteriorMeanModel"]], "botorch.models.deterministic.AffineDeterministicModel": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"]], "botorch.models.deterministic.DeterministicModel": [[7, 2, 1, "", "forward"]], "botorch.models.deterministic.FixedSingleSampleModel": [[7, 2, 1, "", "forward"]], "botorch.models.deterministic.GenericDeterministicModel": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"]], "botorch.models.deterministic.PosteriorMeanModel": [[7, 2, 1, "", "forward"]], "botorch.models.ensemble": [[7, 1, 1, "", "EnsembleModel"]], "botorch.models.ensemble.EnsembleModel": [[7, 2, 1, "", "forward"], [7, 4, 1, "", "num_outputs"], [7, 2, 1, "", "posterior"]], "botorch.models.fully_bayesian": [[7, 1, 1, "", "PyroModel"], [7, 1, 1, "", "SaasFullyBayesianSingleTaskGP"], [7, 1, 1, "", "SaasPyroModel"], [7, 5, 1, "", "compute_dists"], [7, 5, 1, "", "matern52_kernel"], [7, 5, 1, "", "reshape_and_detach"]], "botorch.models.fully_bayesian.PyroModel": [[7, 2, 1, "", "load_mcmc_samples"], [7, 2, 1, "", "postprocess_mcmc_samples"], [7, 2, 1, "", "sample"], [7, 2, 1, "", "set_inputs"]], "botorch.models.fully_bayesian.SaasFullyBayesianSingleTaskGP": [[7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "condition_on_observations"], [7, 2, 1, "", "forward"], [7, 2, 1, "", "load_mcmc_samples"], [7, 2, 1, "", "load_state_dict"], [7, 4, 1, "", "median_lengthscale"], [7, 4, 1, "", "num_mcmc_samples"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "train"]], "botorch.models.fully_bayesian.SaasPyroModel": [[7, 2, 1, "", "load_mcmc_samples"], [7, 2, 1, "", "postprocess_mcmc_samples"], [7, 2, 1, "", "sample"], [7, 2, 1, "", "sample_lengthscale"], [7, 2, 1, "", "sample_mean"], [7, 2, 1, "", "sample_noise"], [7, 2, 1, "", "sample_outputscale"], [7, 2, 1, "", "set_inputs"]], "botorch.models.fully_bayesian_multitask": [[7, 1, 1, "", "MultitaskSaasPyroModel"], [7, 1, 1, "", "SaasFullyBayesianMultiTaskGP"]], "botorch.models.fully_bayesian_multitask.MultitaskSaasPyroModel": [[7, 2, 1, "", "load_mcmc_samples"], [7, 2, 1, "", "sample"], [7, 2, 1, "", "sample_latent_features"], [7, 2, 1, "", "sample_task_lengthscale"], [7, 2, 1, "", "set_inputs"]], "botorch.models.fully_bayesian_multitask.SaasFullyBayesianMultiTaskGP": [[7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "fantasize"], [7, 2, 1, "", "forward"], [7, 2, 1, "", "load_mcmc_samples"], [7, 2, 1, "", "load_state_dict"], [7, 4, 1, "", "median_lengthscale"], [7, 4, 1, "", "num_mcmc_samples"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "train"]], "botorch.models.gp_regression": [[7, 1, 1, "", "SingleTaskGP"]], "botorch.models.gp_regression.SingleTaskGP": [[7, 2, 1, "", "construct_inputs"], [7, 2, 1, "", "forward"]], "botorch.models.gp_regression_fidelity": [[7, 1, 1, "", "SingleTaskMultiFidelityGP"]], "botorch.models.gp_regression_fidelity.SingleTaskMultiFidelityGP": [[7, 2, 1, "", "construct_inputs"]], "botorch.models.gp_regression_mixed": [[7, 1, 1, "", "MixedSingleTaskGP"]], "botorch.models.gp_regression_mixed.MixedSingleTaskGP": [[7, 2, 1, "", "construct_inputs"]], "botorch.models.gpytorch": [[7, 1, 1, "", "BatchedMultiOutputGPyTorchModel"], [7, 1, 1, "", "GPyTorchModel"], [7, 1, 1, "", "ModelListGPyTorchModel"], [7, 1, 1, "", "MultiTaskGPyTorchModel"]], "botorch.models.gpytorch.BatchedMultiOutputGPyTorchModel": [[7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "condition_on_observations"], [7, 2, 1, "", "get_batch_dimensions"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "subset_output"]], "botorch.models.gpytorch.GPyTorchModel": [[7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "condition_on_observations"], [7, 3, 1, "", "likelihood"], [7, 4, 1, "", "num_outputs"], [7, 2, 1, "", "posterior"]], "botorch.models.gpytorch.ModelListGPyTorchModel": [[7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "condition_on_observations"], [7, 2, 1, "", "posterior"]], "botorch.models.gpytorch.MultiTaskGPyTorchModel": [[7, 2, 1, "", "posterior"], [7, 2, 1, "", "subset_output"]], "botorch.models.higher_order_gp": [[7, 1, 1, "", "FlattenedStandardize"], [7, 1, 1, "", "HigherOrderGP"]], "botorch.models.higher_order_gp.FlattenedStandardize": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "untransform"], [7, 2, 1, "", "untransform_posterior"]], "botorch.models.higher_order_gp.HigherOrderGP": [[7, 2, 1, "", "condition_on_observations"], [7, 2, 1, "", "forward"], [7, 2, 1, "", "get_fantasy_model"], [7, 2, 1, "", "make_posterior_variances"], [7, 2, 1, "", "posterior"]], "botorch.models.kernels": [[7, 0, 0, "-", "categorical"], [7, 0, 0, "-", "contextual_lcea"], [7, 0, 0, "-", "contextual_sac"], [7, 0, 0, "-", "downsampling"], [7, 0, 0, "-", "exponential_decay"], [7, 0, 0, "-", "infinite_width_bnn"], [7, 0, 0, "-", "linear_truncated_fidelity"], [7, 0, 0, "-", "orthogonal_additive_kernel"]], "botorch.models.kernels.categorical": [[7, 1, 1, "", "CategoricalKernel"]], "botorch.models.kernels.contextual_lcea": [[7, 1, 1, "", "LCEAKernel"]], "botorch.models.kernels.contextual_sac": [[7, 1, 1, "", "SACKernel"]], "botorch.models.kernels.downsampling": [[7, 1, 1, "", "DownsamplingKernel"]], "botorch.models.kernels.exponential_decay": [[7, 1, 1, "", "ExponentialDecayKernel"]], "botorch.models.kernels.infinite_width_bnn": [[7, 1, 1, "", "InfiniteWidthBNNKernel"]], "botorch.models.kernels.linear_truncated_fidelity": [[7, 1, 1, "", "LinearTruncatedFidelityKernel"]], "botorch.models.kernels.orthogonal_additive_kernel": [[7, 1, 1, "", "OrthogonalAdditiveKernel"]], "botorch.models.likelihoods": [[7, 0, 0, "-", "pairwise"]], "botorch.models.likelihoods.pairwise": [[7, 1, 1, "", "PairwiseLikelihood"], [7, 1, 1, "", "PairwiseLogitLikelihood"], [7, 1, 1, "", "PairwiseProbitLikelihood"]], "botorch.models.likelihoods.pairwise.PairwiseLikelihood": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "log_p"], [7, 2, 1, "", "negative_log_gradient_sum"], [7, 2, 1, "", "negative_log_hessian_sum"], [7, 2, 1, "", "p"]], "botorch.models.likelihoods.pairwise.PairwiseLogitLikelihood": [[7, 2, 1, "", "log_p"], [7, 2, 1, "", "negative_log_gradient_sum"], [7, 2, 1, "", "negative_log_hessian_sum"], [7, 2, 1, "", "p"]], "botorch.models.likelihoods.pairwise.PairwiseProbitLikelihood": [[7, 2, 1, "", "negative_log_gradient_sum"], [7, 2, 1, "", "negative_log_hessian_sum"], [7, 2, 1, "", "p"]], "botorch.models.model": [[7, 1, 1, "", "FantasizeMixin"], [7, 1, 1, "", "Model"], [7, 1, 1, "", "ModelDict"], [7, 1, 1, "", "ModelList"]], "botorch.models.model.FantasizeMixin": [[7, 2, 1, "", "condition_on_observations"], [7, 2, 1, "", "fantasize"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "transform_inputs"]], "botorch.models.model.Model": [[7, 3, 1, "", "_has_transformed_inputs"], [7, 3, 1, "", "_is_ensemble"], [7, 3, 1, "", "_is_fully_bayesian"], [7, 3, 1, "", "_original_train_inputs"], [7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "condition_on_observations"], [7, 2, 1, "", "construct_inputs"], [7, 4, 1, "", "dtypes_of_buffers"], [7, 2, 1, "", "eval"], [7, 4, 1, "", "num_outputs"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "train"], [7, 2, 1, "", "transform_inputs"]], "botorch.models.model.ModelList": [[7, 4, 1, "", "batch_shape"], [7, 2, 1, "", "fantasize"], [7, 2, 1, "", "load_state_dict"], [7, 4, 1, "", "num_outputs"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "transform_inputs"]], "botorch.models.model_list_gp_regression": [[7, 1, 1, "", "ModelListGP"]], "botorch.models.model_list_gp_regression.ModelListGP": [[7, 2, 1, "", "condition_on_observations"]], "botorch.models.multitask": [[7, 1, 1, "", "KroneckerMultiTaskGP"], [7, 1, 1, "", "MultiTaskGP"], [7, 5, 1, "", "get_task_value_remapping"]], "botorch.models.multitask.KroneckerMultiTaskGP": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "posterior"], [7, 4, 1, "", "predictive_mean_cache"], [7, 2, 1, "", "train"], [7, 4, 1, "", "train_full_covar"]], "botorch.models.multitask.MultiTaskGP": [[7, 2, 1, "", "construct_inputs"], [7, 2, 1, "", "forward"], [7, 2, 1, "", "get_all_tasks"]], "botorch.models.pairwise_gp": [[7, 1, 1, "", "PairwiseGP"], [7, 1, 1, "", "PairwiseLaplaceMarginalLogLikelihood"]], "botorch.models.pairwise_gp.PairwiseGP": [[7, 4, 1, "", "batch_shape"], [7, 4, 1, "", "comparisons"], [7, 2, 1, "", "condition_on_observations"], [7, 2, 1, "", "construct_inputs"], [7, 4, 1, "", "datapoints"], [7, 2, 1, "", "forward"], [7, 2, 1, "", "load_state_dict"], [7, 4, 1, "", "num_outputs"], [7, 2, 1, "", "posterior"], [7, 2, 1, "", "set_train_data"], [7, 4, 1, "", "unconsolidated_utility"]], "botorch.models.pairwise_gp.PairwiseLaplaceMarginalLogLikelihood": [[7, 2, 1, "", "forward"]], "botorch.models.transforms": [[7, 0, 0, "-", "factory"], [7, 0, 0, "-", "input"], [7, 0, 0, "-", "outcome"], [7, 0, 0, "-", "utils"]], "botorch.models.transforms.factory": [[7, 5, 1, "", "get_rounding_input_transform"]], "botorch.models.transforms.input": [[7, 1, 1, "", "AffineInputTransform"], [7, 1, 1, "", "AppendFeatures"], [7, 1, 1, "", "BatchBroadcastedInputTransform"], [7, 1, 1, "", "ChainedInputTransform"], [7, 1, 1, "", "FilterFeatures"], [7, 1, 1, "", "InputPerturbation"], [7, 1, 1, "", "InputStandardize"], [7, 1, 1, "", "InputTransform"], [7, 1, 1, "", "InteractionFeatures"], [7, 1, 1, "", "Log10"], [7, 1, 1, "", "Normalize"], [7, 1, 1, "", "OneHotToNumeric"], [7, 1, 1, "", "ReversibleInputTransform"], [7, 1, 1, "", "Round"], [7, 1, 1, "", "Warp"]], "botorch.models.transforms.input.AffineInputTransform": [[7, 4, 1, "", "coefficient"], [7, 2, 1, "", "equals"], [7, 4, 1, "", "learn_coefficients"], [7, 4, 1, "", "offset"]], "botorch.models.transforms.input.AppendFeatures": [[7, 3, 1, "", "is_one_to_many"], [7, 2, 1, "", "transform"]], "botorch.models.transforms.input.BatchBroadcastedInputTransform": [[7, 2, 1, "", "equals"], [7, 2, 1, "", "preprocess_transform"], [7, 2, 1, "", "transform"], [7, 2, 1, "", "untransform"]], "botorch.models.transforms.input.ChainedInputTransform": [[7, 2, 1, "", "equals"], [7, 2, 1, "", "preprocess_transform"], [7, 2, 1, "", "transform"], [7, 2, 1, "", "untransform"]], "botorch.models.transforms.input.FilterFeatures": [[7, 2, 1, "", "equals"], [7, 2, 1, "", "transform"]], "botorch.models.transforms.input.InputPerturbation": [[7, 4, 1, "", "batch_shape"], [7, 3, 1, "", "is_one_to_many"], [7, 2, 1, "", "transform"]], "botorch.models.transforms.input.InputStandardize": [[7, 4, 1, "", "means"], [7, 4, 1, "", "stds"]], "botorch.models.transforms.input.InputTransform": [[7, 2, 1, "", "equals"], [7, 2, 1, "", "forward"], [7, 3, 1, "", "is_one_to_many"], [7, 2, 1, "", "preprocess_transform"], [7, 2, 1, "", "transform"], [7, 3, 1, "", "transform_on_eval"], [7, 3, 1, "", "transform_on_fantasize"], [7, 3, 1, "", "transform_on_train"], [7, 2, 1, "", "untransform"]], "botorch.models.transforms.input.Normalize": [[7, 4, 1, "", "bounds"], [7, 2, 1, "", "get_init_args"], [7, 4, 1, "", "learn_bounds"], [7, 4, 1, "", "mins"], [7, 4, 1, "", "ranges"]], "botorch.models.transforms.input.OneHotToNumeric": [[7, 2, 1, "", "equals"], [7, 2, 1, "", "transform"], [7, 2, 1, "", "untransform"]], "botorch.models.transforms.input.ReversibleInputTransform": [[7, 2, 1, "", "equals"], [7, 3, 1, "", "reverse"], [7, 2, 1, "", "transform"], [7, 2, 1, "", "untransform"]], "botorch.models.transforms.input.Round": [[7, 2, 1, "", "equals"], [7, 2, 1, "", "get_init_args"], [7, 2, 1, "", "transform"]], "botorch.models.transforms.outcome": [[7, 1, 1, "", "Bilog"], [7, 1, 1, "", "ChainedOutcomeTransform"], [7, 1, 1, "", "Log"], [7, 1, 1, "", "OutcomeTransform"], [7, 1, 1, "", "Power"], [7, 1, 1, "", "Standardize"]], "botorch.models.transforms.outcome.Bilog": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "untransform"], [7, 2, 1, "", "untransform_posterior"]], "botorch.models.transforms.outcome.ChainedOutcomeTransform": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "untransform"], [7, 2, 1, "", "untransform_posterior"]], "botorch.models.transforms.outcome.Log": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "untransform"], [7, 2, 1, "", "untransform_posterior"]], "botorch.models.transforms.outcome.OutcomeTransform": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "untransform"], [7, 2, 1, "", "untransform_posterior"]], "botorch.models.transforms.outcome.Power": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "untransform"], [7, 2, 1, "", "untransform_posterior"]], "botorch.models.transforms.outcome.Standardize": [[7, 2, 1, "", "forward"], [7, 2, 1, "", "subset_output"], [7, 2, 1, "", "untransform"], [7, 2, 1, "", "untransform_posterior"]], "botorch.models.transforms.utils": [[7, 5, 1, "", "expand_and_copy_tensor"], [7, 5, 1, "", "interaction_features"], [7, 5, 1, "", "lognorm_to_norm"], [7, 5, 1, "", "norm_to_lognorm"], [7, 5, 1, "", "norm_to_lognorm_mean"], [7, 5, 1, "", "norm_to_lognorm_variance"], [7, 5, 1, "", "subset_transform"]], "botorch.models.utils": [[7, 0, 0, "-", "assorted"], [7, 0, 0, "-", "gpytorch_modules"], [7, 0, 0, "-", "inducing_point_allocators"]], "botorch.models.utils.assorted": [[7, 5, 1, "", "add_output_dim"], [7, 5, 1, "", "check_min_max_scaling"], [7, 5, 1, "", "check_no_nans"], [7, 5, 1, "", "check_standardization"], [7, 5, 1, "", "consolidate_duplicates"], [7, 5, 1, "", "detect_duplicates"], [7, 1, 1, "", "fantasize"], [7, 5, 1, "", "gpt_posterior_settings"], [7, 5, 1, "", "mod_batch_shape"], [7, 5, 1, "", "multioutput_to_batch_mode_transform"], [7, 5, 1, "", "validate_input_scaling"]], "botorch.models.utils.gpytorch_modules": [[7, 5, 1, "", "get_covar_module_with_dim_scaled_prior"], [7, 5, 1, "", "get_gaussian_likelihood_with_gamma_prior"], [7, 5, 1, "", "get_gaussian_likelihood_with_lognormal_prior"], [7, 5, 1, "", "get_matern_kernel_with_gamma_prior"]], "botorch.models.utils.inducing_point_allocators": [[7, 1, 1, "", "ExpectedImprovementQualityFunction"], [7, 1, 1, "", "GreedyImprovementReduction"], [7, 1, 1, "", "GreedyVarianceReduction"], [7, 1, 1, "", "InducingPointAllocator"], [7, 1, 1, "", "QualityFunction"], [7, 1, 1, "", "UnitQualityFunction"], [7, 5, 1, "", "_pivoted_cholesky_init"]], "botorch.models.utils.inducing_point_allocators.InducingPointAllocator": [[7, 2, 1, "", "allocate_inducing_points"]], "botorch.optim": [[8, 0, 0, "-", "core"], [8, 0, 0, "-", "fit"], [8, 0, 0, "-", "homotopy"], [8, 0, 0, "-", "initializers"], [8, 0, 0, "-", "optimize"], [8, 0, 0, "-", "optimize_homotopy"], [8, 0, 0, "-", "optimize_mixed"], [8, 0, 0, "-", "parameter_constraints"], [8, 0, 0, "-", "stopping"]], "botorch.optim.closures": [[8, 0, 0, "-", "core"], [8, 0, 0, "-", "model_closures"]], "botorch.optim.closures.core": [[8, 1, 1, "", "ForwardBackwardClosure"], [8, 1, 1, "", "NdarrayOptimizationClosure"]], "botorch.optim.closures.core.NdarrayOptimizationClosure": [[8, 4, 1, "", "state"]], "botorch.optim.closures.model_closures": [[8, 5, 1, "", "get_loss_closure"], [8, 5, 1, "", "get_loss_closure_with_grads"]], "botorch.optim.core": [[8, 1, 1, "", "OptimizationResult"], [8, 1, 1, "", "OptimizationStatus"], [8, 5, 1, "", "scipy_minimize"], [8, 5, 1, "", "torch_minimize"]], "botorch.optim.core.OptimizationResult": [[8, 3, 1, "", "fval"], [8, 3, 1, "", "message"], [8, 3, 1, "", "runtime"], [8, 3, 1, "", "status"], [8, 3, 1, "", "step"]], "botorch.optim.core.OptimizationStatus": [[8, 3, 1, "", "FAILURE"], [8, 3, 1, "", "RUNNING"], [8, 3, 1, "", "STOPPED"], [8, 3, 1, "", "SUCCESS"]], "botorch.optim.fit": [[8, 5, 1, "", "fit_gpytorch_mll_scipy"], [8, 5, 1, "", "fit_gpytorch_mll_torch"]], "botorch.optim.homotopy": [[8, 1, 1, "", "FixedHomotopySchedule"], [8, 1, 1, "", "Homotopy"], [8, 1, 1, "", "HomotopyParameter"], [8, 1, 1, "", "LinearHomotopySchedule"], [8, 1, 1, "", "LogLinearHomotopySchedule"]], "botorch.optim.homotopy.FixedHomotopySchedule": [[8, 4, 1, "", "num_steps"], [8, 2, 1, "", "restart"], [8, 4, 1, "", "should_stop"], [8, 2, 1, "", "step"], [8, 4, 1, "", "value"]], "botorch.optim.homotopy.Homotopy": [[8, 2, 1, "", "reset"], [8, 2, 1, "", "restart"], [8, 4, 1, "", "should_stop"], [8, 2, 1, "", "step"]], "botorch.optim.homotopy.HomotopyParameter": [[8, 3, 1, "", "parameter"], [8, 3, 1, "", "schedule"]], "botorch.optim.initializers": [[8, 5, 1, "", "gen_batch_initial_conditions"], [8, 5, 1, "", "gen_one_shot_hvkg_initial_conditions"], [8, 5, 1, "", "gen_one_shot_kg_initial_conditions"], [8, 5, 1, "", "gen_value_function_initial_conditions"], [8, 5, 1, "", "initialize_q_batch"], [8, 5, 1, "", "initialize_q_batch_nonneg"], [8, 5, 1, "", "is_nonnegative"], [8, 5, 1, "", "sample_perturbed_subset_dims"], [8, 5, 1, "", "sample_points_around_best"], [8, 5, 1, "", "sample_q_batches_from_polytope"], [8, 5, 1, "", "sample_truncated_normal_perturbations"], [8, 5, 1, "", "transform_constraints"], [8, 5, 1, "", "transform_inter_point_constraint"], [8, 5, 1, "", "transform_intra_point_constraint"]], "botorch.optim.optimize": [[8, 1, 1, "", "OptimizeAcqfInputs"], [8, 5, 1, "", "optimize_acqf"], [8, 5, 1, "", "optimize_acqf_cyclic"], [8, 5, 1, "", "optimize_acqf_discrete"], [8, 5, 1, "", "optimize_acqf_discrete_local_search"], [8, 5, 1, "", "optimize_acqf_list"], [8, 5, 1, "", "optimize_acqf_mixed"]], "botorch.optim.optimize.OptimizeAcqfInputs": [[8, 3, 1, "", "acq_function"], [8, 3, 1, "", "batch_initial_conditions"], [8, 3, 1, "", "bounds"], [8, 3, 1, "", "equality_constraints"], [8, 3, 1, "", "fixed_features"], [8, 4, 1, "", "full_tree"], [8, 3, 1, "", "gen_candidates"], [8, 2, 1, "", "get_ic_generator"], [8, 3, 1, "", "ic_gen_kwargs"], [8, 3, 1, "", "ic_generator"], [8, 3, 1, "", "inequality_constraints"], [8, 3, 1, "", "nonlinear_inequality_constraints"], [8, 3, 1, "", "num_restarts"], [8, 3, 1, "", "options"], [8, 3, 1, "", "post_processing_func"], [8, 3, 1, "", "q"], [8, 3, 1, "", "raw_samples"], [8, 3, 1, "", "retry_on_optimization_warning"], [8, 3, 1, "", "return_best_only"], [8, 3, 1, "", "return_full_tree"], [8, 3, 1, "", "sequential"], [8, 3, 1, "", "timeout_sec"]], "botorch.optim.optimize_homotopy": [[8, 5, 1, "", "optimize_acqf_homotopy"], [8, 5, 1, "", "prune_candidates"]], "botorch.optim.optimize_mixed": [[8, 5, 1, "", "complement_indices"], [8, 5, 1, "", "complement_indices_like"], [8, 5, 1, "", "continuous_step"], [8, 5, 1, "", "discrete_step"], [8, 5, 1, "", "generate_starting_points"], [8, 5, 1, "", "get_nearest_neighbors"], [8, 5, 1, "", "get_spray_points"], [8, 5, 1, "", "optimize_acqf_mixed_alternating"], [8, 5, 1, "", "sample_feasible_points"]], "botorch.optim.parameter_constraints": [[8, 5, 1, "", "eval_lin_constraint"], [8, 5, 1, "", "lin_constraint_jac"], [8, 5, 1, "", "make_scipy_bounds"], [8, 5, 1, "", "make_scipy_linear_constraints"], [8, 5, 1, "", "make_scipy_nonlinear_inequality_constraints"], [8, 5, 1, "", "nonlinear_constraint_is_feasible"]], "botorch.optim.stopping": [[8, 1, 1, "", "ExpMAStoppingCriterion"], [8, 1, 1, "", "StoppingCriterion"]], "botorch.optim.stopping.ExpMAStoppingCriterion": [[8, 2, 1, "", "evaluate"]], "botorch.optim.stopping.StoppingCriterion": [[8, 2, 1, "", "evaluate"]], "botorch.optim.utils": [[8, 0, 0, "-", "acquisition_utils"], [8, 0, 0, "-", "common"], [8, 0, 0, "-", "model_utils"], [8, 0, 0, "-", "numpy_utils"], [8, 0, 0, "-", "timeout"]], "botorch.optim.utils.acquisition_utils": [[8, 5, 1, "", "columnwise_clamp"], [8, 5, 1, "", "fix_features"], [8, 5, 1, "", "get_X_baseline"]], "botorch.optim.utils.model_utils": [[8, 1, 1, "", "TorchAttr"], [8, 5, 1, "", "get_data_loader"], [8, 5, 1, "", "get_name_filter"], [8, 5, 1, "", "get_parameters"], [8, 5, 1, "", "get_parameters_and_bounds"], [8, 5, 1, "", "sample_all_priors"]], "botorch.optim.utils.model_utils.TorchAttr": [[8, 3, 1, "", "device"], [8, 3, 1, "", "dtype"], [8, 3, 1, "", "shape"]], "botorch.optim.utils.numpy_utils": [[8, 5, 1, "", "as_ndarray"], [8, 5, 1, "", "get_bounds_as_ndarray"], [8, 5, 1, "", "get_tensors_as_ndarray_1d"], [8, 5, 1, "", "set_tensors_from_ndarray_1d"]], "botorch.optim.utils.timeout": [[8, 5, 1, "", "minimize_with_timeout"]], "botorch.posteriors": [[9, 0, 0, "-", "base_samples"], [9, 0, 0, "-", "ensemble"], [9, 0, 0, "-", "fully_bayesian"], [9, 0, 0, "-", "gpytorch"], [9, 0, 0, "-", "higher_order"], [9, 0, 0, "-", "multitask"], [9, 0, 0, "-", "posterior"], [9, 0, 0, "-", "posterior_list"], [9, 0, 0, "-", "torch"], [9, 0, 0, "-", "transformed"]], "botorch.posteriors.ensemble": [[9, 1, 1, "", "EnsemblePosterior"]], "botorch.posteriors.ensemble.EnsemblePosterior": [[9, 4, 1, "", "device"], [9, 4, 1, "", "dtype"], [9, 4, 1, "", "ensemble_size"], [9, 4, 1, "", "mean"], [9, 2, 1, "", "rsample"], [9, 2, 1, "", "rsample_from_base_samples"], [9, 4, 1, "", "variance"], [9, 4, 1, "", "weights"]], "botorch.posteriors.fully_bayesian": [[9, 1, 1, "", "FullyBayesianPosterior"], [9, 1, 1, "", "GaussianMixturePosterior"], [9, 5, 1, "", "batched_bisect"]], "botorch.posteriors.fully_bayesian.GaussianMixturePosterior": [[9, 4, 1, "", "batch_range"], [9, 4, 1, "", "mixture_covariance_matrix"], [9, 4, 1, "", "mixture_mean"], [9, 4, 1, "", "mixture_variance"], [9, 2, 1, "", "quantile"]], "botorch.posteriors.gpytorch": [[9, 1, 1, "", "GPyTorchPosterior"], [9, 5, 1, "", "scalarize_posterior"], [9, 5, 1, "", "scalarize_posterior_gpytorch"]], "botorch.posteriors.gpytorch.GPyTorchPosterior": [[9, 4, 1, "", "base_sample_shape"], [9, 4, 1, "", "batch_range"], [9, 2, 1, "", "density"], [9, 3, 1, "", "distribution"], [9, 4, 1, "", "mean"], [9, 4, 1, "", "mvn"], [9, 2, 1, "", "quantile"], [9, 2, 1, "", "rsample"], [9, 2, 1, "", "rsample_from_base_samples"], [9, 4, 1, "", "variance"]], "botorch.posteriors.higher_order": [[9, 1, 1, "", "HigherOrderGPPosterior"]], "botorch.posteriors.higher_order.HigherOrderGPPosterior": [[9, 4, 1, "", "base_sample_shape"], [9, 4, 1, "", "batch_range"], [9, 2, 1, "", "rsample"], [9, 2, 1, "", "rsample_from_base_samples"]], "botorch.posteriors.multitask": [[9, 1, 1, "", "MultitaskGPPosterior"]], "botorch.posteriors.multitask.MultitaskGPPosterior": [[9, 4, 1, "", "base_sample_shape"], [9, 4, 1, "", "batch_range"], [9, 2, 1, "", "rsample"], [9, 2, 1, "", "rsample_from_base_samples"]], "botorch.posteriors.posterior": [[9, 1, 1, "", "Posterior"]], "botorch.posteriors.posterior.Posterior": [[9, 4, 1, "", "base_sample_shape"], [9, 4, 1, "", "batch_range"], [9, 2, 1, "", "density"], [9, 4, 1, "", "device"], [9, 4, 1, "", "dtype"], [9, 2, 1, "", "quantile"], [9, 2, 1, "", "rsample"], [9, 2, 1, "", "rsample_from_base_samples"], [9, 2, 1, "", "sample"]], "botorch.posteriors.posterior_list": [[9, 1, 1, "", "PosteriorList"]], "botorch.posteriors.posterior_list.PosteriorList": [[9, 4, 1, "", "device"], [9, 4, 1, "", "dtype"], [9, 4, 1, "", "mean"], [9, 2, 1, "", "rsample"], [9, 4, 1, "", "variance"]], "botorch.posteriors.torch": [[9, 1, 1, "", "TorchPosterior"]], "botorch.posteriors.torch.TorchPosterior": [[9, 2, 1, "", "density"], [9, 4, 1, "", "device"], [9, 4, 1, "", "dtype"], [9, 2, 1, "", "quantile"], [9, 2, 1, "", "rsample"]], "botorch.posteriors.transformed": [[9, 1, 1, "", "TransformedPosterior"]], "botorch.posteriors.transformed.TransformedPosterior": [[9, 4, 1, "", "base_sample_shape"], [9, 4, 1, "", "batch_range"], [9, 4, 1, "", "device"], [9, 4, 1, "", "dtype"], [9, 4, 1, "", "mean"], [9, 2, 1, "", "rsample"], [9, 2, 1, "", "rsample_from_base_samples"], [9, 4, 1, "", "variance"]], "botorch.sampling": [[10, 0, 0, "-", "base"], [10, 0, 0, "-", "get_sampler"], [10, 0, 0, "-", "index_sampler"], [10, 0, 0, "-", "list_sampler"], [10, 0, 0, "-", "normal"], [10, 0, 0, "-", "pairwise_samplers"], [10, 0, 0, "-", "qmc"], [10, 0, 0, "-", "stochastic_samplers"]], "botorch.sampling.base": [[10, 1, 1, "", "MCSampler"]], "botorch.sampling.base.MCSampler": [[10, 2, 1, "", "forward"]], "botorch.sampling.get_sampler": [[10, 5, 1, "", "get_sampler"]], "botorch.sampling.index_sampler": [[10, 1, 1, "", "IndexSampler"]], "botorch.sampling.index_sampler.IndexSampler": [[10, 2, 1, "", "forward"]], "botorch.sampling.list_sampler": [[10, 1, 1, "", "ListSampler"]], "botorch.sampling.list_sampler.ListSampler": [[10, 2, 1, "", "forward"], [10, 4, 1, "", "sample_shape"]], "botorch.sampling.normal": [[10, 1, 1, "", "IIDNormalSampler"], [10, 1, 1, "", "NormalMCSampler"], [10, 1, 1, "", "SobolQMCNormalSampler"]], "botorch.sampling.normal.NormalMCSampler": [[10, 2, 1, "", "forward"]], "botorch.sampling.pairwise_samplers": [[10, 1, 1, "", "PairwiseIIDNormalSampler"], [10, 1, 1, "", "PairwiseMCSampler"], [10, 1, 1, "", "PairwiseSobolQMCNormalSampler"]], "botorch.sampling.pairwise_samplers.PairwiseMCSampler": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise": [[10, 0, 0, "-", "paths"], [10, 0, 0, "-", "posterior_samplers"], [10, 0, 0, "-", "prior_samplers"], [10, 0, 0, "-", "update_strategies"], [10, 0, 0, "-", "utils"]], "botorch.sampling.pathwise.features": [[10, 0, 0, "-", "generators"], [10, 0, 0, "-", "maps"]], "botorch.sampling.pathwise.features.generators": [[10, 5, 1, "", "gen_kernel_features"]], "botorch.sampling.pathwise.features.maps": [[10, 1, 1, "", "FeatureMap"], [10, 1, 1, "", "KernelEvaluationMap"], [10, 1, 1, "", "KernelFeatureMap"]], "botorch.sampling.pathwise.features.maps.FeatureMap": [[10, 3, 1, "", "batch_shape"], [10, 3, 1, "", "input_transform"], [10, 3, 1, "", "num_outputs"], [10, 3, 1, "", "output_transform"]], "botorch.sampling.pathwise.features.maps.KernelEvaluationMap": [[10, 4, 1, "", "batch_shape"], [10, 2, 1, "", "forward"], [10, 4, 1, "", "num_outputs"]], "botorch.sampling.pathwise.features.maps.KernelFeatureMap": [[10, 4, 1, "", "batch_shape"], [10, 2, 1, "", "forward"], [10, 4, 1, "", "num_outputs"]], "botorch.sampling.pathwise.paths": [[10, 1, 1, "", "GeneralizedLinearPath"], [10, 1, 1, "", "PathDict"], [10, 1, 1, "", "PathList"], [10, 1, 1, "", "SamplePath"]], "botorch.sampling.pathwise.paths.GeneralizedLinearPath": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.paths.PathDict": [[10, 2, 1, "", "forward"], [10, 2, 1, "", "items"], [10, 2, 1, "", "keys"], [10, 2, 1, "", "values"]], "botorch.sampling.pathwise.paths.PathList": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.posterior_samplers": [[10, 1, 1, "", "MatheronPath"], [10, 5, 1, "", "draw_matheron_paths"], [10, 5, 1, "", "get_matheron_path_model"]], "botorch.sampling.pathwise.prior_samplers": [[10, 5, 1, "", "draw_kernel_feature_paths"]], "botorch.sampling.pathwise.update_strategies": [[10, 5, 1, "", "gaussian_update"]], "botorch.sampling.pathwise.utils": [[10, 1, 1, "", "ChainedTransform"], [10, 1, 1, "", "FeatureSelector"], [10, 1, 1, "", "InverseLengthscaleTransform"], [10, 1, 1, "", "OutcomeUntransformer"], [10, 1, 1, "", "OutputscaleTransform"], [10, 1, 1, "", "SineCosineTransform"], [10, 1, 1, "", "TensorTransform"], [10, 1, 1, "", "TransformedModuleMixin"], [10, 5, 1, "", "get_input_transform"], [10, 5, 1, "", "get_output_transform"], [10, 5, 1, "", "get_train_inputs"], [10, 5, 1, "", "get_train_targets"]], "botorch.sampling.pathwise.utils.ChainedTransform": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.utils.FeatureSelector": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.utils.InverseLengthscaleTransform": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.utils.OutcomeUntransformer": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.utils.OutputscaleTransform": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.utils.SineCosineTransform": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.utils.TensorTransform": [[10, 2, 1, "", "forward"]], "botorch.sampling.pathwise.utils.TransformedModuleMixin": [[10, 3, 1, "", "input_transform"], [10, 3, 1, "", "output_transform"]], "botorch.sampling.qmc": [[10, 1, 1, "", "MultivariateNormalQMCEngine"], [10, 1, 1, "", "NormalQMCEngine"]], "botorch.sampling.qmc.MultivariateNormalQMCEngine": [[10, 2, 1, "", "draw"]], "botorch.sampling.qmc.NormalQMCEngine": [[10, 2, 1, "", "draw"]], "botorch.sampling.stochastic_samplers": [[10, 1, 1, "", "ForkedRNGSampler"], [10, 1, 1, "", "StochasticSampler"]], "botorch.sampling.stochastic_samplers.ForkedRNGSampler": [[10, 2, 1, "", "forward"]], "botorch.sampling.stochastic_samplers.StochasticSampler": [[10, 2, 1, "", "forward"]], "botorch.settings": [[11, 1, 1, "", "log_level"], [11, 1, 1, "", "propagate_grads"], [11, 1, 1, "", "validate_input_scaling"]], "botorch.settings.log_level": [[11, 3, 1, "", "level"]], "botorch.test_functions": [[12, 0, 0, "-", "base"], [12, 0, 0, "-", "multi_fidelity"], [12, 0, 0, "-", "multi_objective"], [12, 0, 0, "-", "multi_objective_multi_fidelity"], [12, 0, 0, "-", "sensitivity_analysis"], [12, 0, 0, "-", "synthetic"], [12, 0, 0, "-", "utils"]], "botorch.test_functions.base": [[12, 1, 1, "", "BaseTestProblem"], [12, 1, 1, "", "ConstrainedBaseTestProblem"], [12, 1, 1, "", "MultiObjectiveTestProblem"]], "botorch.test_functions.base.BaseTestProblem": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "forward"]], "botorch.test_functions.base.ConstrainedBaseTestProblem": [[12, 3, 1, "", "constraint_noise_std"], [12, 2, 1, "", "evaluate_slack"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "is_feasible"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.base.MultiObjectiveTestProblem": [[12, 2, 1, "", "gen_pareto_front"], [12, 4, 1, "", "max_hv"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_fidelity": [[12, 1, 1, "", "AugmentedBranin"], [12, 1, 1, "", "AugmentedHartmann"], [12, 1, 1, "", "AugmentedRosenbrock"]], "botorch.test_functions.multi_fidelity.AugmentedBranin": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_fidelity.AugmentedHartmann": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_fidelity.AugmentedRosenbrock": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_objective": [[12, 1, 1, "", "BNH"], [12, 1, 1, "", "BraninCurrin"], [12, 1, 1, "", "C2DTLZ2"], [12, 1, 1, "", "CONSTR"], [12, 1, 1, "", "CarSideImpact"], [12, 1, 1, "", "ConstrainedBraninCurrin"], [12, 1, 1, "", "DH"], [12, 1, 1, "", "DH1"], [12, 1, 1, "", "DH2"], [12, 1, 1, "", "DH3"], [12, 1, 1, "", "DH4"], [12, 1, 1, "", "DTLZ"], [12, 1, 1, "", "DTLZ1"], [12, 1, 1, "", "DTLZ2"], [12, 1, 1, "", "DTLZ3"], [12, 1, 1, "", "DTLZ4"], [12, 1, 1, "", "DTLZ5"], [12, 1, 1, "", "DTLZ7"], [12, 1, 1, "", "DiscBrake"], [12, 1, 1, "", "GMM"], [12, 1, 1, "", "MW7"], [12, 1, 1, "", "OSY"], [12, 1, 1, "", "Penicillin"], [12, 1, 1, "", "SRN"], [12, 1, 1, "", "ToyRobust"], [12, 1, 1, "", "VehicleSafety"], [12, 1, 1, "", "WeldedBeam"], [12, 1, 1, "", "ZDT"], [12, 1, 1, "", "ZDT1"], [12, 1, 1, "", "ZDT2"], [12, 1, 1, "", "ZDT3"]], "botorch.test_functions.multi_objective.BNH": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.BraninCurrin": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.C2DTLZ2": [[12, 2, 1, "", "evaluate_slack_true"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.multi_objective.CONSTR": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.CarSideImpact": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.ConstrainedBraninCurrin": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.DH": [[12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.DH1": [[12, 3, 1, "", "alpha"], [12, 3, 1, "", "beta"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_objective.DH2": [[12, 3, 1, "", "beta"]], "botorch.test_functions.multi_objective.DH3": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_objective.DTLZ1": [[12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "gen_pareto_front"]], "botorch.test_functions.multi_objective.DTLZ2": [[12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "gen_pareto_front"]], "botorch.test_functions.multi_objective.DTLZ3": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_objective.DTLZ5": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_objective.DTLZ7": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_objective.DiscBrake": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.GMM": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.multi_objective.MW7": [[12, 2, 1, "", "LA2"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.OSY": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.Penicillin": [[12, 3, 1, "", "E_d"], [12, 3, 1, "", "E_g"], [12, 3, 1, "", "K"], [12, 3, 1, "", "K_1"], [12, 3, 1, "", "K_2"], [12, 3, 1, "", "K_I"], [12, 3, 1, "", "K_X"], [12, 3, 1, "", "K_p"], [12, 3, 1, "", "R"], [12, 3, 1, "", "T_o"], [12, 3, 1, "", "T_v"], [12, 3, 1, "", "V_max"], [12, 3, 1, "", "Y_ps"], [12, 3, 1, "", "Y_xs"], [12, 3, 1, "", "alpha_1"], [12, 3, 1, "", "alpha_2"], [12, 3, 1, "", "alpha_3"], [12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "k_d"], [12, 3, 1, "", "k_g"], [12, 3, 1, "", "lambd"], [12, 3, 1, "", "m_X"], [12, 3, 1, "", "mu_X"], [12, 3, 1, "", "mu_p"], [12, 3, 1, "", "num_objectives"], [12, 2, 1, "", "penicillin_vectorized"]], "botorch.test_functions.multi_objective.SRN": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.ToyRobust": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "f_1"], [12, 2, 1, "", "f_2"], [12, 3, 1, "", "levy"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.VehicleSafety": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.WeldedBeam": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective.ZDT1": [[12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "gen_pareto_front"]], "botorch.test_functions.multi_objective.ZDT2": [[12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "gen_pareto_front"]], "botorch.test_functions.multi_objective.ZDT3": [[12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "gen_pareto_front"]], "botorch.test_functions.multi_objective_multi_fidelity": [[12, 1, 1, "", "MOMFBraninCurrin"], [12, 1, 1, "", "MOMFPark"]], "botorch.test_functions.multi_objective_multi_fidelity.MOMFBraninCurrin": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.multi_objective_multi_fidelity.MOMFPark": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.sensitivity_analysis": [[12, 1, 1, "", "Gsobol"], [12, 1, 1, "", "Ishigami"], [12, 1, 1, "", "Morris"]], "botorch.test_functions.sensitivity_analysis.Gsobol": [[12, 2, 1, "", "evaluate_true"], [12, 2, 1, "", "optimal_sobol_indicies"]], "botorch.test_functions.sensitivity_analysis.Ishigami": [[12, 2, 1, "", "compute_dgsm"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.sensitivity_analysis.Morris": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic": [[12, 1, 1, "", "Ackley"], [12, 1, 1, "", "Beale"], [12, 1, 1, "", "Branin"], [12, 1, 1, "", "Bukin"], [12, 1, 1, "", "ConstrainedGramacy"], [12, 1, 1, "", "ConstrainedHartmann"], [12, 1, 1, "", "ConstrainedHartmannSmooth"], [12, 1, 1, "", "ConstrainedSyntheticTestFunction"], [12, 1, 1, "", "Cosine8"], [12, 1, 1, "", "DixonPrice"], [12, 1, 1, "", "DropWave"], [12, 1, 1, "", "EggHolder"], [12, 1, 1, "", "Griewank"], [12, 1, 1, "", "Hartmann"], [12, 1, 1, "", "HolderTable"], [12, 1, 1, "", "Levy"], [12, 1, 1, "", "Michalewicz"], [12, 1, 1, "", "Powell"], [12, 1, 1, "", "PressureVessel"], [12, 1, 1, "", "Rastrigin"], [12, 1, 1, "", "Rosenbrock"], [12, 1, 1, "", "Shekel"], [12, 1, 1, "", "SixHumpCamel"], [12, 1, 1, "", "SpeedReducer"], [12, 1, 1, "", "StyblinskiTang"], [12, 1, 1, "", "SyntheticTestFunction"], [12, 1, 1, "", "TensionCompressionString"], [12, 1, 1, "", "ThreeHumpCamel"], [12, 1, 1, "", "WeldedBeamSO"]], "botorch.test_functions.synthetic.Ackley": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Beale": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Branin": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Bukin": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.ConstrainedGramacy": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"], [12, 3, 1, "", "num_objectives"]], "botorch.test_functions.synthetic.ConstrainedHartmann": [[12, 2, 1, "", "evaluate_slack_true"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.synthetic.ConstrainedHartmannSmooth": [[12, 2, 1, "", "evaluate_slack_true"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.synthetic.Cosine8": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.DixonPrice": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.DropWave": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.EggHolder": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Griewank": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Hartmann": [[12, 2, 1, "", "evaluate_true"], [12, 4, 1, "", "optimizers"]], "botorch.test_functions.synthetic.HolderTable": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Levy": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Michalewicz": [[12, 2, 1, "", "evaluate_true"], [12, 4, 1, "", "optimizers"]], "botorch.test_functions.synthetic.Powell": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.PressureVessel": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.synthetic.Rastrigin": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Rosenbrock": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.Shekel": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.SixHumpCamel": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.SpeedReducer": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.synthetic.StyblinskiTang": [[12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.SyntheticTestFunction": [[12, 3, 1, "", "num_objectives"], [12, 4, 1, "", "optimal_value"]], "botorch.test_functions.synthetic.TensionCompressionString": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.synthetic.ThreeHumpCamel": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_true"]], "botorch.test_functions.synthetic.WeldedBeamSO": [[12, 3, 1, "", "dim"], [12, 2, 1, "", "evaluate_slack_true"], [12, 2, 1, "", "evaluate_true"], [12, 3, 1, "", "num_constraints"]], "botorch.test_functions.utils": [[12, 5, 1, "", "round_nearest"]], "botorch.test_utils": [[13, 0, 0, "-", "mock"]], "botorch.test_utils.mock": [[13, 5, 1, "", "mock_optimize"], [13, 5, 1, "", "mock_optimize_context_manager"]], "botorch.utils": [[14, 0, 0, "-", "constants"], [14, 0, 0, "-", "constraints"], [14, 0, 0, "-", "containers"], [14, 0, 0, "-", "context_managers"], [14, 0, 0, "-", "datasets"], [14, 0, 0, "-", "dispatcher"], [14, 0, 0, "-", "feasible_volume"], [14, 0, 0, "-", "gp_sampling"], [14, 0, 0, "-", "low_rank"], [14, 0, 0, "-", "multitask"], [14, 0, 0, "-", "objective"], [14, 0, 0, "-", "rounding"], [14, 0, 0, "-", "safe_math"], [14, 0, 0, "-", "sampling"], [14, 0, 0, "-", "test_helpers"], [14, 0, 0, "-", "testing"], [14, 0, 0, "-", "torch"], [14, 0, 0, "-", "transforms"], [14, 0, 0, "-", "types"]], "botorch.utils.constants": [[14, 5, 1, "", "get_constants"], [14, 5, 1, "", "get_constants_like"]], "botorch.utils.constraints": [[14, 5, 1, "", "get_monotonicity_constraints"], [14, 5, 1, "", "get_outcome_constraint_transforms"]], "botorch.utils.containers": [[14, 1, 1, "", "BotorchContainer"], [14, 1, 1, "", "DenseContainer"], [14, 1, 1, "", "SliceContainer"]], "botorch.utils.containers.BotorchContainer": [[14, 4, 1, "", "device"], [14, 4, 1, "", "dtype"], [14, 3, 1, "", "event_shape"], [14, 4, 1, "", "shape"]], "botorch.utils.containers.DenseContainer": [[14, 2, 1, "", "clone"], [14, 4, 1, "", "device"], [14, 4, 1, "", "dtype"], [14, 3, 1, "", "event_shape"], [14, 4, 1, "", "shape"], [14, 3, 1, "", "values"]], "botorch.utils.containers.SliceContainer": [[14, 2, 1, "", "clone"], [14, 4, 1, "", "device"], [14, 4, 1, "", "dtype"], [14, 3, 1, "", "event_shape"], [14, 3, 1, "", "indices"], [14, 4, 1, "", "shape"], [14, 3, 1, "", "values"]], "botorch.utils.context_managers": [[14, 1, 1, "", "TensorCheckpoint"], [14, 5, 1, "", "delattr_ctx"], [14, 5, 1, "", "module_rollback_ctx"], [14, 5, 1, "", "parameter_rollback_ctx"], [14, 5, 1, "", "zero_grad_ctx"]], "botorch.utils.context_managers.TensorCheckpoint": [[14, 3, 1, "", "device"], [14, 3, 1, "", "dtype"], [14, 3, 1, "", "values"]], "botorch.utils.datasets": [[14, 1, 1, "", "ContextualDataset"], [14, 1, 1, "", "MultiTaskDataset"], [14, 1, 1, "", "RankingDataset"], [14, 1, 1, "", "SupervisedDataset"]], "botorch.utils.datasets.ContextualDataset": [[14, 4, 1, "", "X"], [14, 4, 1, "", "Y"], [14, 4, 1, "", "Yvar"], [14, 2, 1, "", "clone"]], "botorch.utils.datasets.MultiTaskDataset": [[14, 4, 1, "", "X"], [14, 4, 1, "", "Y"], [14, 4, 1, "", "Yvar"], [14, 2, 1, "", "clone"], [14, 2, 1, "", "from_joint_dataset"], [14, 2, 1, "", "get_dataset_without_task_feature"]], "botorch.utils.datasets.SupervisedDataset": [[14, 4, 1, "", "X"], [14, 4, 1, "", "Y"], [14, 4, 1, "", "Yvar"], [14, 2, 1, "", "clone"]], "botorch.utils.dispatcher": [[14, 1, 1, "", "Dispatcher"], [14, 5, 1, "", "type_bypassing_encoder"]], "botorch.utils.dispatcher.Dispatcher": [[14, 2, 1, "", "dispatch"], [14, 3, 1, "", "doc"], [14, 2, 1, "", "encode_args"], [14, 4, 1, "", "encoder"], [14, 3, 1, "", "funcs"], [14, 2, 1, "", "help"], [14, 3, 1, "", "name"], [14, 2, 1, "", "source"]], "botorch.utils.feasible_volume": [[14, 5, 1, "", "estimate_feasible_volume"], [14, 5, 1, "", "get_feasible_samples"], [14, 5, 1, "", "get_outcome_feasibility_probability"]], "botorch.utils.gp_sampling": [[14, 1, 1, "", "GPDraw"], [14, 1, 1, "", "RandomFourierFeatures"], [14, 5, 1, "", "get_deterministic_model"], [14, 5, 1, "", "get_deterministic_model_list"], [14, 5, 1, "", "get_deterministic_model_multi_samples"], [14, 5, 1, "", "get_eval_gp_sample_callable"], [14, 5, 1, "", "get_gp_samples"], [14, 5, 1, "", "get_weights_posterior"]], "botorch.utils.gp_sampling.GPDraw": [[14, 4, 1, "", "Xs"], [14, 4, 1, "", "Ys"], [14, 2, 1, "", "forward"]], "botorch.utils.gp_sampling.RandomFourierFeatures": [[14, 2, 1, "", "forward"]], "botorch.utils.low_rank": [[14, 5, 1, "", "extract_batch_covar"], [14, 5, 1, "", "sample_cached_cholesky"]], "botorch.utils.multi_objective": [[14, 0, 0, "-", "hypervolume"], [14, 0, 0, "-", "pareto"], [14, 0, 0, "-", "scalarization"]], "botorch.utils.multi_objective.box_decompositions": [[14, 0, 0, "-", "box_decomposition"], [14, 0, 0, "-", "box_decomposition_list"], [14, 0, 0, "-", "dominated"], [14, 0, 0, "-", "non_dominated"], [14, 0, 0, "-", "utils"]], "botorch.utils.multi_objective.box_decompositions.box_decomposition": [[14, 1, 1, "", "BoxDecomposition"], [14, 1, 1, "", "FastPartitioning"]], "botorch.utils.multi_objective.box_decompositions.box_decomposition.BoxDecomposition": [[14, 4, 1, "", "Y"], [14, 2, 1, "", "compute_hypervolume"], [14, 2, 1, "", "get_hypercell_bounds"], [14, 4, 1, "", "pareto_Y"], [14, 2, 1, "", "partition_space"], [14, 4, 1, "", "ref_point"], [14, 2, 1, "", "reset"], [14, 2, 1, "", "update"]], "botorch.utils.multi_objective.box_decompositions.box_decomposition.FastPartitioning": [[14, 2, 1, "", "get_hypercell_bounds"], [14, 2, 1, "", "partition_space"], [14, 2, 1, "", "update"]], "botorch.utils.multi_objective.box_decompositions.box_decomposition_list": [[14, 1, 1, "", "BoxDecompositionList"]], "botorch.utils.multi_objective.box_decompositions.box_decomposition_list.BoxDecompositionList": [[14, 2, 1, "", "compute_hypervolume"], [14, 2, 1, "", "get_hypercell_bounds"], [14, 4, 1, "", "pareto_Y"], [14, 4, 1, "", "ref_point"], [14, 2, 1, "", "update"]], "botorch.utils.multi_objective.box_decompositions.dominated": [[14, 1, 1, "", "DominatedPartitioning"]], "botorch.utils.multi_objective.box_decompositions.non_dominated": [[14, 1, 1, "", "FastNondominatedPartitioning"], [14, 1, 1, "", "NondominatedPartitioning"]], "botorch.utils.multi_objective.box_decompositions.non_dominated.NondominatedPartitioning": [[14, 2, 1, "", "get_hypercell_bounds"]], "botorch.utils.multi_objective.box_decompositions.utils": [[14, 5, 1, "", "compute_dominated_hypercell_bounds_2d"], [14, 5, 1, "", "compute_local_upper_bounds"], [14, 5, 1, "", "compute_non_dominated_hypercell_bounds_2d"], [14, 5, 1, "", "get_partition_bounds"], [14, 5, 1, "", "update_local_upper_bounds_incremental"]], "botorch.utils.multi_objective.hypervolume": [[14, 1, 1, "", "Hypervolume"], [14, 1, 1, "", "MultiList"], [14, 1, 1, "", "Node"], [14, 1, 1, "", "NoisyExpectedHypervolumeMixin"], [14, 1, 1, "", "SubsetIndexCachingMixin"], [14, 5, 1, "", "compute_subset_indices"], [14, 5, 1, "", "infer_reference_point"], [14, 5, 1, "", "sort_by_dimension"]], "botorch.utils.multi_objective.hypervolume.Hypervolume": [[14, 2, 1, "", "compute"], [14, 4, 1, "", "ref_point"]], "botorch.utils.multi_objective.hypervolume.MultiList": [[14, 2, 1, "", "append"], [14, 2, 1, "", "extend"], [14, 2, 1, "", "reinsert"], [14, 2, 1, "", "remove"]], "botorch.utils.multi_objective.hypervolume.NoisyExpectedHypervolumeMixin": [[14, 4, 1, "", "X_baseline"], [14, 2, 1, "", "set_X_pending"]], "botorch.utils.multi_objective.hypervolume.SubsetIndexCachingMixin": [[14, 2, 1, "", "compute_q_subset_indices"]], "botorch.utils.multi_objective.pareto": [[14, 5, 1, "", "is_non_dominated"]], "botorch.utils.multi_objective.scalarization": [[14, 5, 1, "", "get_chebyshev_scalarization"]], "botorch.utils.multitask": [[14, 5, 1, "", "separate_mtmvn"]], "botorch.utils.objective": [[14, 5, 1, "", "apply_constraints"], [14, 5, 1, "", "apply_constraints_nonnegative_soft"], [14, 5, 1, "", "compute_feasibility_indicator"], [14, 5, 1, "", "compute_smoothed_feasibility_indicator"], [14, 5, 1, "", "get_objective_weights_transform"]], "botorch.utils.probability": [[14, 0, 0, "-", "bvn"], [14, 0, 0, "-", "lin_ess"], [14, 0, 0, "-", "linalg"], [14, 0, 0, "-", "mvnxpb"], [14, 0, 0, "-", "truncated_multivariate_normal"], [14, 0, 0, "-", "unified_skew_normal"], [14, 0, 0, "-", "utils"]], "botorch.utils.probability.bvn": [[14, 5, 1, "", "bvn"], [14, 5, 1, "", "bvnmom"], [14, 5, 1, "", "bvnu"]], "botorch.utils.probability.lin_ess": [[14, 1, 1, "", "LinearEllipticalSliceSampler"], [14, 5, 1, "", "get_index_tensors"]], "botorch.utils.probability.lin_ess.LinearEllipticalSliceSampler": [[14, 2, 1, "", "draw"], [14, 4, 1, "", "lifetime_samples"], [14, 2, 1, "", "step"]], "botorch.utils.probability.linalg": [[14, 1, 1, "", "PivotedCholesky"], [14, 5, 1, "", "augment_cholesky"], [14, 5, 1, "", "block_matrix_concat"]], "botorch.utils.probability.linalg.PivotedCholesky": [[14, 2, 1, "", "clone"], [14, 2, 1, "", "concat"], [14, 2, 1, "", "detach"], [14, 3, 1, "", "diag"], [14, 2, 1, "", "expand"], [14, 3, 1, "", "perm"], [14, 2, 1, "", "pivot_"], [14, 3, 1, "", "step"], [14, 3, 1, "", "tril"], [14, 2, 1, "", "update_"], [14, 3, 1, "", "validate_init"]], "botorch.utils.probability.mvnxpb": [[14, 1, 1, "", "MVNXPB"], [14, 1, 1, "", "mvnxpbState"]], "botorch.utils.probability.mvnxpb.MVNXPB": [[14, 2, 1, "", "asdict"], [14, 2, 1, "", "augment"], [14, 2, 1, "", "build"], [14, 2, 1, "", "clone"], [14, 2, 1, "", "concat"], [14, 2, 1, "", "detach"], [14, 2, 1, "", "expand"], [14, 2, 1, "", "pivot_"], [14, 2, 1, "", "select_pivot"], [14, 2, 1, "", "solve"]], "botorch.utils.probability.mvnxpb.mvnxpbState": [[14, 3, 1, "", "bounds"], [14, 3, 1, "", "log_prob"], [14, 3, 1, "", "log_prob_extra"], [14, 3, 1, "", "perm"], [14, 3, 1, "", "piv_chol"], [14, 3, 1, "", "plug_ins"], [14, 3, 1, "", "step"]], "botorch.utils.probability.truncated_multivariate_normal": [[14, 1, 1, "", "TruncatedMultivariateNormal"]], "botorch.utils.probability.truncated_multivariate_normal.TruncatedMultivariateNormal": [[14, 2, 1, "", "expand"], [14, 4, 1, "", "log_partition"], [14, 2, 1, "", "log_prob"], [14, 2, 1, "", "rsample"], [14, 4, 1, "", "sampler"], [14, 4, 1, "", "solver"]], "botorch.utils.probability.unified_skew_normal": [[14, 1, 1, "", "UnifiedSkewNormal"]], "botorch.utils.probability.unified_skew_normal.UnifiedSkewNormal": [[14, 3, 1, "", "arg_constraints"], [14, 4, 1, "", "covariance_matrix"], [14, 2, 1, "", "expand"], [14, 2, 1, "", "log_prob"], [14, 2, 1, "", "rsample"], [14, 4, 1, "", "scale_tril"]], "botorch.utils.probability.utils": [[14, 5, 1, "", "build_positional_indices"], [14, 5, 1, "", "case_dispatcher"], [14, 5, 1, "", "compute_log_prob_feas_from_bounds"], [14, 5, 1, "", "gen_positional_indices"], [14, 5, 1, "", "get_constants"], [14, 5, 1, "", "get_constants_like"], [14, 5, 1, "", "leggauss"], [14, 5, 1, "", "log_erfc"], [14, 5, 1, "", "log_erfcx"], [14, 5, 1, "", "log_ndtr"], [14, 5, 1, "", "log_phi"], [14, 5, 1, "", "log_prob_normal_in"], [14, 5, 1, "", "ndtr"], [14, 5, 1, "", "percentile_of_score"], [14, 5, 1, "", "phi"], [14, 5, 1, "", "standard_normal_log_hazard"], [14, 5, 1, "", "swap_along_dim_"]], "botorch.utils.rounding": [[14, 1, 1, "", "IdentitySTEFunction"], [14, 1, 1, "", "OneHotArgmaxSTE"], [14, 1, 1, "", "RoundSTE"], [14, 5, 1, "", "approximate_round"]], "botorch.utils.rounding.IdentitySTEFunction": [[14, 2, 1, "", "backward"]], "botorch.utils.rounding.OneHotArgmaxSTE": [[14, 2, 1, "", "forward"]], "botorch.utils.rounding.RoundSTE": [[14, 2, 1, "", "forward"]], "botorch.utils.safe_math": [[14, 5, 1, "", "add"], [14, 5, 1, "", "cauchy"], [14, 5, 1, "", "check_dtype_float32_or_float64"], [14, 5, 1, "", "div"], [14, 5, 1, "", "exp"], [14, 5, 1, "", "fatmax"], [14, 5, 1, "", "fatmaximum"], [14, 5, 1, "", "fatmin"], [14, 5, 1, "", "fatminimum"], [14, 5, 1, "", "fatmoid"], [14, 5, 1, "", "fatplus"], [14, 5, 1, "", "log"], [14, 5, 1, "", "log1mexp"], [14, 5, 1, "", "log1pexp"], [14, 5, 1, "", "log_fatmoid"], [14, 5, 1, "", "log_fatplus"], [14, 5, 1, "", "log_softplus"], [14, 5, 1, "", "logdiffexp"], [14, 5, 1, "", "logexpit"], [14, 5, 1, "", "logmeanexp"], [14, 5, 1, "", "logplusexp"], [14, 5, 1, "", "logsumexp"], [14, 5, 1, "", "mul"], [14, 5, 1, "", "sigmoid"], [14, 5, 1, "", "smooth_amax"], [14, 5, 1, "", "smooth_amin"], [14, 5, 1, "", "sub"]], "botorch.utils.sampling": [[14, 1, 1, "", "DelaunayPolytopeSampler"], [14, 1, 1, "", "HitAndRunPolytopeSampler"], [14, 1, 1, "", "PolytopeSampler"], [14, 5, 1, "", "batched_multinomial"], [14, 5, 1, "", "draw_sobol_normal_samples"], [14, 5, 1, "", "draw_sobol_samples"], [14, 5, 1, "", "find_interior_point"], [14, 5, 1, "", "get_polytope_samples"], [14, 5, 1, "", "manual_seed"], [14, 5, 1, "", "normalize_dense_linear_constraints"], [14, 5, 1, "", "normalize_sparse_linear_constraints"], [14, 5, 1, "", "optimize_posterior_samples"], [14, 5, 1, "", "sample_hypersphere"], [14, 5, 1, "", "sample_polytope"], [14, 5, 1, "", "sample_simplex"], [14, 5, 1, "", "sparse_to_dense_constraints"]], "botorch.utils.sampling.DelaunayPolytopeSampler": [[14, 2, 1, "", "draw"]], "botorch.utils.sampling.HitAndRunPolytopeSampler": [[14, 2, 1, "", "draw"]], "botorch.utils.sampling.PolytopeSampler": [[14, 2, 1, "", "draw"], [14, 2, 1, "", "feasible"], [14, 2, 1, "", "find_interior_point"]], "botorch.utils.test_helpers": [[14, 1, 1, "", "DummyNonScalarizingPosteriorTransform"], [14, 1, 1, "", "SimpleGPyTorchModel"], [14, 5, 1, "", "gen_multi_task_dataset"], [14, 5, 1, "", "get_fully_bayesian_model"], [14, 5, 1, "", "get_fully_bayesian_model_list"], [14, 5, 1, "", "get_model"], [14, 5, 1, "", "get_pvar_expected"], [14, 5, 1, "", "get_sample_moments"], [14, 5, 1, "", "standardize_moments"]], "botorch.utils.test_helpers.DummyNonScalarizingPosteriorTransform": [[14, 2, 1, "", "evaluate"], [14, 2, 1, "", "forward"], [14, 3, 1, "", "scalarize"]], "botorch.utils.test_helpers.SimpleGPyTorchModel": [[14, 2, 1, "", "forward"], [14, 3, 1, "", "last_fantasize_flag"]], "botorch.utils.testing": [[14, 1, 1, "", "BaseTestProblemTestCaseMixIn"], [14, 1, 1, "", "BotorchTestCase"], [14, 1, 1, "", "ConstrainedTestProblemTestCaseMixin"], [14, 1, 1, "", "MockAcquisitionFunction"], [14, 1, 1, "", "MockModel"], [14, 1, 1, "", "MockPosterior"], [14, 1, 1, "", "MultiObjectiveTestProblemTestCaseMixin"], [14, 1, 1, "", "SyntheticTestFunctionTestCaseMixin"]], "botorch.utils.testing.BaseTestProblemTestCaseMixIn": [[14, 4, 1, "", "functions"], [14, 2, 1, "", "test_forward_and_evaluate_true"]], "botorch.utils.testing.BotorchTestCase": [[14, 2, 1, "", "assertAllClose"], [14, 3, 1, "", "device"], [14, 2, 1, "", "setUp"]], "botorch.utils.testing.ConstrainedTestProblemTestCaseMixin": [[14, 2, 1, "", "test_evaluate_slack"], [14, 2, 1, "", "test_num_constraints"]], "botorch.utils.testing.MockAcquisitionFunction": [[14, 2, 1, "", "set_X_pending"]], "botorch.utils.testing.MockModel": [[14, 4, 1, "", "batch_shape"], [14, 2, 1, "", "load_state_dict"], [14, 4, 1, "", "num_outputs"], [14, 2, 1, "", "posterior"], [14, 2, 1, "", "state_dict"]], "botorch.utils.testing.MockPosterior": [[14, 4, 1, "", "base_sample_shape"], [14, 4, 1, "", "batch_range"], [14, 4, 1, "", "batch_shape"], [14, 4, 1, "", "device"], [14, 4, 1, "", "dtype"], [14, 4, 1, "", "mean"], [14, 2, 1, "", "rsample"], [14, 2, 1, "", "rsample_from_base_samples"], [14, 4, 1, "", "variance"]], "botorch.utils.testing.MultiObjectiveTestProblemTestCaseMixin": [[14, 2, 1, "", "test_attributes"], [14, 2, 1, "", "test_max_hv"], [14, 2, 1, "", "test_ref_point"]], "botorch.utils.testing.SyntheticTestFunctionTestCaseMixin": [[14, 2, 1, "", "test_optimal_value"], [14, 2, 1, "", "test_optimizer"]], "botorch.utils.torch": [[14, 1, 1, "", "BufferDict"]], "botorch.utils.torch.BufferDict": [[14, 2, 1, "", "clear"], [14, 2, 1, "", "extra_repr"], [14, 2, 1, "", "items"], [14, 2, 1, "", "keys"], [14, 2, 1, "", "pop"], [14, 2, 1, "", "update"], [14, 2, 1, "", "values"]], "botorch.utils.transforms": [[14, 5, 1, "", "concatenate_pending_points"], [14, 5, 1, "", "convert_to_target_pre_hook"], [14, 5, 1, "", "is_ensemble"], [14, 5, 1, "", "is_fully_bayesian"], [14, 5, 1, "", "match_batch_shape"], [14, 5, 1, "", "normalize"], [14, 5, 1, "", "normalize_indices"], [14, 5, 1, "", "standardize"], [14, 5, 1, "", "t_batch_mode_transform"], [14, 5, 1, "", "unnormalize"]], "botorch.utils.types": [[14, 1, 1, "", "DEFAULT"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "attribute", "Python attribute"], "4": ["py", "property", "Python property"], "5": ["py", "function", "Python function"], "6": ["py", "exception", "Python exception"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:attribute", "4": "py:property", "5": "py:function", "6": "py:exception"}, "terms": {"": [0, 4, 7, 8, 9, 10, 12, 14], "0": [0, 1, 4, 7, 8, 9, 10, 12, 14], "000": 7, "000000000000001e": 12, "0000034868717194": 14, "0001": [0, 7, 8, 12, 14], "0002": 12, "00025": 12, "001": [0, 7, 12, 14], "005": 12, "00601": 12, "00798": 14, "01": [0, 7, 12], "014": 12, "019": 14, "025": 7, "02785": 10, "03": 12, "04": 12, "05": [7, 8, 12, 14], "05135": 7, "05502": 12, "06": [0, 7, 9], "07": [7, 12], "08": [7, 14], "08526": 4, "092": 12, "0th": [0, 14], "1": [0, 1, 4, 7, 8, 9, 10, 12, 14], "10": [0, 1, 4, 7, 8, 12, 14], "100": [4, 8, 12, 14], "1000": [0, 10, 12, 14], "10000": [8, 14], "1007": 14, "1012": 7, "10123": 7, "1015": 7, "1024": [0, 8, 10, 14], "10449": 14, "105": 12, "108": 12, "1083": 7, "10x10": 7, "11": 12, "1157": 14, "1163": 14, "1164": 0, "12": [7, 12], "123": 10, "1234": [10, 14], "128": 0, "12997": 7, "1309": 7, "13901": [0, 12], "13th": 14, "14": 12, "143": 12, "145": 12, "15": [7, 8, 12], "150": 12, "150011": 12, "1506": 10, "16": [0, 3, 7, 8, 10, 12, 14], "16341639895667773": 0, "164": 12, "166166": 12, "1709": 7, "173": 12, "180": 12, "19": [0, 7, 12], "1900": 12, "193": 12, "195": 12, "1990": 14, "1994": 14, "1995": 12, "1d": [7, 12, 14], "1e": [0, 7, 8, 9, 12, 14], "1e100": 14, "1e20": 14, "1st": 0, "2": [0, 1, 4, 7, 8, 9, 10, 12, 14], "20": [0, 7, 8, 10, 12, 14], "200": 14, "2000": [0, 12], "2002": [4, 12], "2004": [12, 14], "2005": [7, 12], "2006": [12, 14], "2007": [7, 10], "2008": 0, "2009": 7, "2010": [7, 12], "2011": [0, 14], "2012": [0, 14], "2013": [0, 7, 8], "2014": [0, 12, 14], "2015": [10, 14], "2016": [0, 12, 14], "20169": 12, "2017": [0, 14], "2018": [7, 10], "2019": [0, 7, 12], "2020": [0, 7, 10, 12, 14], "2021": [0, 7, 10, 12], "2022": [0, 7, 12, 14], "2023": [0, 7], "2024": [7, 14], "203": 12, "2048": [0, 8], "20708": 0, "2085": 12, "2092": 12, "20x20": 7, "21": 12, "2106": 7, "2112": [0, 12], "22": [7, 12], "2262": 12, "2272": 12, "2287706461": 14, "22nd": 7, "23": 12, "2300": 12, "2301": 7, "2310": 0, "2407": 14, "2451": 7, "2468": 14, "25": [0, 4, 8, 9, 10, 12, 14], "250": [0, 12], "256": [3, 8], "259": 12, "2599": 7, "25th": 7, "26": 7, "273": 12, "275": 12, "275332": 12, "29th": 7, "2a": 14, "2d": [7, 14], "2m": 12, "2pi": 14, "2rxy": 14, "2x": 3, "3": [0, 1, 4, 7, 8, 10, 12, 14], "30": 7, "30x30": 7, "311652": 12, "3136": 3, "31945": 14, "32": [0, 7, 9, 12, 14], "32237": 12, "32nd": 7, "33": [0, 7, 12], "34": 0, "35": [0, 12, 14], "36": [0, 7], "373": 12, "37th": 0, "39": 12, "397887": 12, "39th": [0, 7, 12], "3d": 0, "3k": 12, "3pi": 12, "4": [0, 1, 7, 8, 12, 14], "400": 7, "4000": 12, "4096": 8, "40th": 0, "42478": 12, "44": 12, "45": 12, "47": 12, "475": 12, "476874": 12, "48550": 4, "495": 12, "4e": 12, "4i": 12, "4x_i": 12, "5": [0, 1, 4, 7, 8, 9, 12, 14], "50": [11, 12, 14], "500": [4, 8, 12], "50000": 12, "5100": 12, "512": [0, 3, 8, 12, 14], "519": 12, "521": 12, "5363": 12, "549": 12, "5e": 14, "5mb": 14, "5th": 14, "5w_1": 12, "6": [0, 3, 7, 8, 12, 14], "60": 12, "600": 12, "64": [0, 8], "6573": 12, "66": 14, "66459": 12, "6835": 7, "7": [7, 12, 14], "70": 14, "7000": 12, "75": [0, 14], "768": 12, "7733": 7, "79": 14, "8": [0, 7, 8, 12, 14], "8348668001940709": 14, "85": 12, "89": [7, 12], "9": [1, 7, 12], "903534": 12, "925": 14, "9287": 12, "9303": 12, "94": 12, "945": 12, "956": 12, "972": 12, "986": 12, "9872": 12, "99": 12, "A": [0, 1, 2, 3, 4, 7, 8, 9, 10, 12, 13, 14], "As": [0, 4, 7, 8, 9, 10, 14], "At": 9, "By": [0, 4, 7, 8, 10, 14], "For": [0, 4, 7, 8, 9, 10, 11, 14], "If": [0, 3, 4, 7, 8, 9, 10, 12, 13, 14], "In": [0, 4, 7, 8, 9, 12, 14], "It": [0, 7, 8, 9, 10, 12, 14], "NOT": [0, 7], "No": [3, 7, 11], "Not": [0, 7, 8], "On": [0, 10], "One": [7, 8], "Or": 14, "Such": 14, "That": [0, 8], "The": [2, 3, 4, 7, 8, 9, 10, 11, 12, 14], "Then": [0, 7, 14], "There": [0, 7, 12], "These": [0, 7, 8, 12, 14], "To": [0, 7, 8, 9, 10, 14], "Will": 7, "With": 7, "_": [0, 7, 14], "_0": 0, "_2": 12, "_______________________________________": 10, "__call__": [0, 10, 14], "__eq__": 7, "__getitem__": 14, "__hash__": 7, "__init__": [7, 14], "_aug_batch_shap": 7, "_c": 8, "_compute_information_gain": 0, "_construct_base_sampl": 10, "_construct_x_ful": 0, "_default_sample_shap": 0, "_defaulttyp": 7, "_evaluation_ma": 0, "_expanded_perturb": 7, "_extended_shap": [9, 10, 14], "_fast_solv": 7, "_flag": [7, 11], "_has_transformed_input": 7, "_i": 7, "_incompatiblekei": 7, "_input_batch_shap": 7, "_instanc": 14, "_is_ensembl": 7, "_is_fully_bayesian": 7, "_is_linear": 7, "_is_mo": 0, "_load_from_state_dict": 7, "_lrschedul": 8, "_original_train_input": 7, "_output_task": 7, "_pend": 0, "_pivoted_cholesky_init": 7, "_revert_to_original_input": 7, "_sampl": 14, "_sample_forward": 0, "_sample_max_valu": 0, "_scalartype_co": [2, 8, 14], "_singletaskvariationalgp": 7, "_updat": 7, "_update_damp": 0, "_variationaldistribut": 7, "_variationalstrategi": 7, "_verify_output_shap": 0, "a_": 12, "a_1j": 12, "a_eq": 14, "a_i": 12, "a_ij": 12, "a_nlz": 14, "ab": [0, 7, 8], "abc": [0, 4, 7, 8, 9, 10, 12, 14], "abil": 7, "abl": [0, 7], "abort": 8, "about": [0, 7, 14], "abov": [0, 11, 14], "abraham": 12, "absolut": [7, 8, 12, 14], "abstract": [4, 7, 8, 10], "ac": 7, "acceler": 12, "accept": [0, 4, 7, 14], "access": [0, 7, 9, 14], "accommod": 0, "accord": [0, 4, 7, 8, 14], "accordingli": [7, 8], "account": [0, 7, 14], "accross": 0, "accur": [0, 7, 14], "accuraci": [8, 14], "achiev": [8, 14], "acklei": 12, "acm": 0, "aco": 7, "acos_ep": 7, "acq": 0, "acq_func": 4, "acq_funct": [0, 8], "acq_function_list": 8, "acq_init": 8, "acq_val": 8, "acq_valu": 8, "acq_value_list": 8, "acqf": [0, 14], "acqf_cl": 0, "acqf_input_constructor": 0, "acqfn": 0, "acquir": 8, "acquisit": [2, 5, 7, 9, 10, 14], "acquisition_funct": 4, "acquisition_function_nam": 0, "acquisition_util": 8, "acquisitionfunct": [0, 4, 8, 14], "acquisiton": 8, "acquist": 0, "acqusit": [7, 14], "acqval": 0, "across": [0, 4, 7, 8, 9, 10, 14], "act": [7, 8, 10], "activ": [7, 12, 14], "active_dim": 7, "active_learn": 0, "actual": 0, "ad": [0, 7, 8, 10, 12, 14], "adam": [4, 7, 8, 12], "adapt": [0, 12, 14], "add": [0, 7, 8, 12, 14], "add_output_dim": 7, "added_batch_shap": 14, "addit": [0, 7, 8, 10, 12, 14], "addition": [0, 8, 14], "additon": 0, "address": 0, "adher": 14, "adjust": [10, 14], "advanc": [0, 7, 10, 12, 14], "advantag": [0, 14], "advis": 0, "af": 0, "affin": [0, 7, 9], "affinecostmodel": 0, "affinedeterministicmodel": 7, "affinefidelitycostmodel": 7, "affineinputtransform": 7, "after": [0, 2, 4, 7, 8, 13, 14], "afterward": [7, 10, 14], "ag": 7, "again": [0, 4, 8, 14], "against": [0, 7], "agnost": 7, "agre": [0, 14], "agreement": 14, "ahead": 0, "ai": 12, "aim": 7, "aistat": [0, 14], "akedo": 14, "al": 0, "alg": 14, "algebra": 7, "algorithm": [0, 3, 7, 8, 12, 14], "alia": [1, 7, 8, 14], "align": [0, 7, 14], "all": [0, 2, 4, 7, 8, 9, 10, 11, 12, 14], "all_task": 7, "allclos": 14, "allevi": 0, "alloc": 14, "allocate_inducing_point": 7, "allow": [0, 7, 8, 9, 14], "allow_only_specific_variable_kwarg": 0, "almost": 0, "almost_zero": 7, "along": [0, 1, 7, 8, 10, 14], "alpha": [0, 7, 8, 12, 14], "alpha_1": 12, "alpha_2": 12, "alpha_3": 12, "alpha_i": 12, "alpig": 14, "alreadi": [0, 7, 8, 12, 14], "also": [0, 3, 7, 8, 9, 14], "altern": [0, 7, 8, 14], "although": [7, 10, 14], "alwai": [0, 7, 8, 14], "amax": [0, 14], "ament": 0, "ament2023logei": 0, "amersfoort": 0, "amin": 14, "amount": [0, 3, 8, 14], "an": [0, 1, 4, 7, 8, 9, 10, 12, 13, 14], "anal": 12, "analog": 0, "analysi": 0, "analyt": [4, 7], "analyticacquisitionfunct": 0, "analyticexpectedutilityofbestopt": 0, "andez": 12, "andrea": 0, "angl": 14, "ani": [0, 1, 2, 3, 4, 7, 8, 9, 10, 12, 14], "annal": 0, "anneal": 12, "annual": [0, 14], "anoth": [7, 14], "anyoptim": 12, "anyth": 13, "api": [8, 14], "appear": [0, 7, 8], "append": [0, 7, 14], "appendfeatur": [0, 7], "appendix": 0, "appli": [0, 7, 8, 9, 10, 11, 12, 14], "applic": [0, 7, 10, 14], "apply_constraint": [0, 14], "apply_constraints_nonnegative_soft": 14, "approach": [0, 4, 7, 10, 14], "appropri": [0, 7, 8, 9, 14], "approx": 0, "approxim": [0, 7, 9, 10, 12, 14], "approximate_gp": 7, "approximate_round": 14, "approximategp": 7, "approximategpytorchmodel": 7, "apr": [7, 12], "ar": [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14], "arang": 0, "arbitrari": [0, 7, 8, 12, 14], "arbitrarili": 7, "arbitrary_q_method": 14, "architectur": 7, "ard": 7, "ard_num_dim": 7, "area": 8, "aren": [7, 8], "arg": [0, 2, 4, 7, 8, 10, 14], "arg_constraint": 14, "argmax": 14, "argmin_": 14, "argument": [1, 3, 4, 7, 8, 10, 14], "ari": 14, "arithmet": [10, 14], "arlington": 12, "arm": 7, "around": [0, 8, 14], "arrai": [0, 2, 8, 14], "articl": 0, "artifici": [0, 7, 12], "arxiv": [0, 4, 7, 10, 12, 14], "as_arrai": 8, "as_ndarrai": 8, "asdict": 14, "assert": 14, "assert_clos": 14, "assert_output_shap": 14, "assertallclos": 14, "assertionerror": [13, 14], "assess": [0, 14], "assign": [0, 8, 14], "assist": 7, "associ": [0, 2, 4, 7, 8, 12, 14], "assort": 7, "asssum": 7, "assum": [0, 4, 7, 8, 9, 10, 12, 14], "assumpt": 0, "astudillo": 0, "astudillo2023qeubo": 0, "asymptot": [0, 14], "asynchron": 0, "atol": [7, 14], "atol_mean": 7, "atol_std": 7, "attach": 0, "attain": 14, "attempt": [2, 8], "attr": [7, 14], "attribut": [0, 3, 7, 8, 9, 12, 14], "auai": 12, "augment": [0, 7, 12, 14], "augment_choleski": 14, "augmented_sampl": 0, "augmentedbranin": 12, "augmentedhartmann": 12, "augmentedrosenbrock": 12, "auto": 0, "autograd": [8, 14], "automat": [0, 7, 8, 9, 14], "auxiliari": 14, "avail": [0, 14], "averag": [0, 7, 8, 9, 10, 12, 14], "avers": 0, "avoid": [0, 7, 8, 13, 14], "awar": 2, "ax": [0, 14], "axi": [0, 7, 14], "b": [0, 4, 7, 8, 9, 10, 12, 14], "b1": [0, 7, 9], "b_1": 7, "b_eq": 14, "b_k": 7, "b_min": 12, "b_nlz": 14, "bach": 8, "back": [0, 7, 14], "backpropag": 0, "backshi": 7, "backward": [0, 3, 7, 8, 9, 14], "bad": [2, 8], "badinitialcandidateswarn": 2, "bahri": 7, "bakshi": [0, 7, 12, 14], "balandat": [0, 7, 12, 14], "balandat2020botorch": 0, "bald": 0, "bandit": 0, "bank": 8, "bar": 3, "barbosa": 12, "base": [1, 2, 4, 8, 11, 12, 14], "base_kernel": 7, "base_sampl": [9, 14], "base_sample_shap": [9, 14], "base_shap": 14, "base_value_funct": 0, "baselin": [0, 7, 8, 14], "baseline_i": 0, "baseline_l": 14, "basemodel": 7, "basetestproblem": [12, 14], "basetestproblemtestcasemixin": 14, "basi": [0, 8, 10, 14], "basic": [0, 14], "batch": [0, 1, 4, 7, 8, 9, 10, 14], "batch_acq_valu": 4, "batch_candid": 4, "batch_cross_valid": 1, "batch_dim": 10, "batch_gp": 7, "batch_initial_condit": [0, 8], "batch_limit": 8, "batch_mo_gp": 7, "batch_mo_model": 7, "batch_model": 7, "batch_rang": [9, 14], "batch_shap": [0, 1, 4, 7, 8, 9, 10, 12, 14], "batch_shape_i": 14, "batch_shape_input": 14, "batch_shape_x": 14, "batch_siz": [0, 7, 8, 14], "batch_so_gp": 7, "batch_valu": 4, "batch_x": 7, "batchbald": 0, "batchbroadcastedinputtransform": 7, "batchbroadcastedtransformlist": 7, "batched_bisect": 9, "batched_mo_model": 7, "batched_multi_output_to_single_output": 7, "batched_multinomi": 14, "batched_to_model_list": 7, "batchedmultioutputgpytorchmodel": [7, 14], "bayesian": [3, 10, 12, 14], "bayesian_active_learn": 0, "bd": 14, "bd1": 14, "bd2": 14, "beal": 12, "beam": 12, "becaus": [0, 7, 9, 12, 14], "becom": [0, 14], "been": [0, 7, 8, 14], "befor": [0, 4, 7, 8, 14], "beforehand": 7, "begin": 7, "behav": [0, 7, 10, 14], "behavior": [0, 7, 8, 10, 14], "behind": 0, "being": [0, 7, 8, 12, 14], "belakaria": 0, "belakaria2019": 0, "belief": 0, "belong": [7, 14], "below": [0, 7, 14], "benchmark": [2, 12], "benefici": 0, "beneifici": 8, "berlin": 12, "bernardino": 0, "bernoulli": 7, "besid": 0, "best": [0, 4, 7, 8, 10, 14], "best_candid": 4, "best_f": [0, 4, 8], "best_pct": 8, "beta": [0, 4, 7, 8, 12], "beta_i": 12, "beta_ij": 12, "beta_ijl": 12, "better": [0, 4, 7, 14], "between": [0, 7, 8, 9, 12, 14], "beyond": 14, "bfg": [4, 8, 14], "bia": [0, 7, 10, 14], "bias": [0, 7, 10], "bias_modul": 10, "big": 7, "bilog": 7, "binari": [0, 8, 14], "bingham": 12, "bingham2013virtu": 12, "binoi": 0, "binois2017repexp": 0, "biomass": 12, "bisect": 9, "bivari": 0, "bk": [0, 7, 9], "black": [4, 8, 12], "blackbox": 12, "blob": [7, 12, 14], "block": [0, 7, 14], "block_matrix_concat": 14, "bmucb": 4, "bnh": 12, "bnn": 7, "bo": [0, 7, 8], "bogu": 14, "boltzmann": 4, "boltzmannsampl": 4, "bonilla": 7, "bonilla2007mtgp": 7, "bool": [0, 1, 3, 4, 7, 8, 10, 11, 12, 13, 14], "boolean": [0, 3, 4, 7, 8, 12, 14], "booltensor": 14, "bootstrap": 7, "bope": 0, "borg": 12, "borovitskii": 10, "boschresearch": 12, "both": [0, 4, 7, 8, 9, 10, 12, 14], "botorchcontain": [7, 14], "botorcherror": 2, "botorchtensordimensionerror": 2, "botorchtensordimensionwarn": 2, "botorchtestcas": 14, "botorchwarn": 2, "boukouvala": 7, "bound": [0, 4, 7, 8, 9, 12, 14], "boundari": [7, 8, 14], "box": [0, 4, 8, 10, 12], "box_decomposit": [0, 14], "box_decomposition_list": 14, "boxdecomposit": [0, 14], "boxdecompositionlist": 14, "brake": 12, "branch": 14, "branin": 12, "branincurrin": 12, "break": 14, "breakdown": [7, 14], "bridg": 10, "brisban": 14, "british": 14, "broadcast": [0, 4, 7, 8, 14], "broadcast_index": 7, "broader": 0, "brochu": 7, "brochu2010tutori": 7, "brown": 14, "buffer": [7, 8, 14], "bufferdict": 14, "build": [0, 7, 8, 14], "build_positional_indic": 14, "built": [0, 7, 8], "bukin": 12, "burn": [3, 8, 14], "burnin": 14, "burt": 7, "burt2020svgp": 7, "bvn": 14, "bvnmom": 14, "bvnu": 14, "bxd": 0, "byproduct": 12, "b\u00e4ck": 12, "c": [0, 4, 7, 8, 12, 14], "c1": [4, 7], "c2": [4, 7], "c2dtlz2": 12, "c_": 12, "c_1": [0, 7, 12], "c_2": [0, 7], "c_3": [0, 7], "c_i": 12, "c_n": 0, "ca": [12, 14], "cach": [7, 14], "cache_pend": [0, 14], "cache_root": [0, 14], "cached_choleski": 0, "cachedcholeskymcsamplermixin": [0, 14], "cakmak": [0, 12], "cakmak2020risk": [0, 7], "calcul": [0, 7, 8, 14], "calibr": 7, "call": [0, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14], "callabl": [0, 3, 4, 7, 8, 9, 10, 13, 14], "callback": [4, 8], "can": [0, 4, 7, 8, 9, 10, 12, 13, 14], "canada": 14, "candid": [0, 2, 7, 8, 9, 10, 14], "candidate_s": 0, "candidate_set": 0, "candidategenerationerror": 2, "cannot": [0, 7], "canon": [0, 14], "car": 12, "cardin": [0, 7], "care": [0, 7, 10, 14], "carl": 7, "carlo": [4, 14], "carsideimpact": 12, "case": [0, 2, 4, 7, 8, 9, 10, 14], "case_dispatch": 14, "cast": 7, "cat": 7, "cat_0": 7, "cat_dim": 7, "cat_feature_dict": 7, "cat_k": 7, "catch": 10, "categor": [7, 8, 14], "categorical_featur": 7, "categoricalkernel": 7, "cauchi": 14, "caus": [0, 8], "cbd": [0, 14], "cdf": [0, 7, 14], "cdot": 7, "cei": 0, "ceil": 0, "cell": [0, 14], "censor": 14, "center": [0, 7, 10], "certain": [0, 7], "chai": 7, "chain": [7, 8, 14], "chainedinputtransform": 7, "chainedoutcometransform": 7, "chainedtransform": 10, "chang": [0, 7, 14], "cheap": 0, "cheaper": 7, "cheapli": 14, "chebyshev": 14, "chebyshev_object": 0, "chebyshev_weight": 0, "check": [0, 7, 8, 11, 14], "check_dtype_float32_or_float64": 14, "check_feas": 14, "check_if_fit": 7, "check_min_max_sc": 7, "check_negative_info_gain": 0, "check_no_nan": 7, "check_standard": 7, "check_tau": 0, "checkpoint": 14, "chen": [0, 7], "chen2014seqexpdesign": 0, "chen2018dpp": 7, "child": 14, "cho": 7, "cho2009kernel": 7, "choic": [0, 7, 8, 14], "choleski": 7, "choleskyvariationaldistribut": 7, "choos": [7, 12, 14], "chosen": [0, 8, 14], "chu": 7, "chu2005prefer": 7, "ci": 7, "circular": [0, 13], "circumv": 0, "clamp": [0, 4, 7, 8], "class": [1, 4, 7, 8, 9, 10, 11, 12, 14], "classic": 0, "classif": 0, "classmethod": [7, 12, 14], "clear": [3, 8, 14], "clip": [7, 14], "clone": 14, "close": [0, 8, 12, 14], "closest": [0, 12], "closur": 3, "closure_kwarg": [3, 8], "cm": 4, "cmodel1": 4, "cmodel2": 4, "cmp": 4, "co": [7, 12], "co2": 12, "code": [0, 7, 12], "codifi": 7, "coeff": 8, "coeff_constraint": 7, "coeffici": [4, 7, 8, 14], "coeffs_1_prior": 7, "coeffs_2_prior": 7, "coello": 12, "coellocoello2002constraint": 12, "coinlab": [12, 14], "collapse_batch_dim": 0, "collapse_fantasy_base_sampl": 0, "collect": [0, 8], "column": [0, 4, 7, 8, 14], "columnwise_clamp": 8, "com": [0, 3, 7, 12, 14], "combin": [0, 7, 8, 10, 12, 14], "come": [0, 7, 12, 14], "comment": 0, "common": [0, 7, 8, 12, 14], "commonli": [0, 12], "comp": 7, "compar": [0, 14], "comparison": [7, 10, 12], "compat": [0, 4, 7, 9, 10, 14], "compil": 14, "complement": 8, "complement_indic": 8, "complement_indices_lik": 8, "complementari": 14, "complet": [8, 12], "complex": [0, 7, 14], "compli": 2, "compon": [0, 9, 14], "compos": [12, 14], "composit": [0, 10], "comppon": 7, "compress": 12, "compris": 0, "comput": [0, 4, 7, 8, 9, 10, 12, 14], "computacion": 12, "computation": [0, 7], "compute_": 0, "compute_best_f": 0, "compute_best_feasible_object": 0, "compute_dgsm": 12, "compute_dist": 7, "compute_dominated_hypercell_bounds_2d": 14, "compute_feasibility_ind": 14, "compute_hypervolum": 14, "compute_local_upper_bound": 14, "compute_log_prob_feas_from_bound": 14, "compute_non_dominated_hypercell_bounds_2d": 14, "compute_q_subset_indic": 14, "compute_sample_box_decomposit": 0, "compute_smoothed_feasibility_ind": [0, 14], "compute_subset_indic": 14, "con_both": 14, "con_both_ind": 14, "con_low": 14, "con_lower_ind": 14, "con_upp": 14, "con_upper_ind": 14, "concat": 14, "concaten": [0, 8, 10, 14], "concatenate_pending_point": 14, "concav": 12, "concentr": [7, 12], "concentration0": 7, "concentration0_prior": 7, "concentration1": 7, "concentration1_prior": 7, "condit": [0, 7, 8, 10], "condition_noiseless": 0, "condition_on_observ": 7, "conditional_entropi": 0, "conf": 14, "confer": [0, 7, 10, 12], "confid": 0, "configur": [0, 7], "congress": 14, "conjunct": [7, 8, 9, 14], "connect": 7, "consecut": [0, 14], "consequ": 0, "conserv": [0, 7], "consid": [0, 4, 7, 8, 12, 14], "consist": [0, 7, 9, 12, 14], "consolid": 7, "consolidate_atol": 7, "consolidate_dupl": 7, "consolidate_rtol": 7, "consolidated_i": 7, "consolidated_x": 7, "constant": [7, 12], "constantmean": 7, "constitut": 7, "constr": 12, "constrain": [0, 4, 7, 8, 12, 14], "constrained_ei": 0, "constrained_object": 0, "constrainedbasetestproblem": 12, "constrainedbranincurrin": 12, "constrainedexpectedimprov": 0, "constrainedgramaci": 12, "constrainedhartmann": 12, "constrainedhartmannsmooth": 12, "constrainedmaxposteriorsampl": 4, "constrainedmcacquisitionfunct": 0, "constrainedmcobject": 0, "constrainedsynthetictestfunct": 12, "constrainedtestproblemtestcasemixin": 14, "constraint": [0, 4, 7, 12], "constraint_i": 0, "constraint_idc": 0, "constraint_ind": 0, "constraint_model": 4, "constraint_noise_std": 12, "construct": [0, 7, 8, 9, 12, 14], "construct_evaluation_mask": 0, "construct_input": 7, "construct_inputs_analytic_eubo": 0, "construct_inputs_bald": 0, "construct_inputs_best_f": 0, "construct_inputs_ehvi": 0, "construct_inputs_mf_bas": 0, "construct_inputs_nipv": 0, "construct_inputs_noisy_ei": 0, "construct_inputs_posterior_mean": 0, "construct_inputs_qehvi": 0, "construct_inputs_qei": 0, "construct_inputs_qeubo": 0, "construct_inputs_qhvkg": 0, "construct_inputs_qj": 0, "construct_inputs_qkg": 0, "construct_inputs_qlogei": 0, "construct_inputs_qlognehvi": 0, "construct_inputs_qlognei": 0, "construct_inputs_qlognparego": 0, "construct_inputs_qm": 0, "construct_inputs_qmfhvkg": 0, "construct_inputs_qmfkg": 0, "construct_inputs_qmfm": 0, "construct_inputs_qnehvi": 0, "construct_inputs_qnei": 0, "construct_inputs_qpi": 0, "construct_inputs_qsimpleregret": 0, "construct_inputs_qucb": 0, "construct_inputs_ucb": 0, "constructor": [1, 4, 12], "constuct": 7, "consum": 9, "cont_dim": 8, "cont_kernel_factori": 7, "contain": [0, 4, 7, 8, 9, 11], "containt": 2, "content": [0, 14], "context": [0, 2, 8, 10, 13], "context1": 14, "context2": 14, "context_cat_featur": 7, "context_emb_featur": 7, "context_manag": [8, 14], "context_nam": 7, "context_weight_dict": 7, "contextmanag": [8, 14], "contextu": [0, 14], "contextual_lcea": 7, "contextual_multioutput": 7, "contextual_sac": 7, "contextualdataset": 14, "contigu": [7, 14], "continu": [7, 8, 12, 14], "continuous_relax": 8, "continuous_step": 8, "contrast": [0, 7, 14], "contribut": 14, "control": [0, 4, 14], "conveni": [0, 7, 14], "convent": [2, 14], "converg": [0, 7, 8], "convert": [4, 7, 8, 14], "convert_to_target_pre_hook": 14, "convex": [12, 14], "coordin": [8, 14], "copi": [0, 7, 14], "cora": 7, "core": 12, "cornelliu": 7, "correct": 7, "correctli": 9, "correl": [7, 14], "correspond": [0, 4, 7, 8, 9, 12, 14], "correspondin": 8, "cosin": [10, 12], "cosine8": 12, "cost": 2, "cost_awar": [0, 7], "cost_aware_util": [0, 7], "cost_cal": 0, "cost_func": 0, "cost_intercept": 0, "cost_model": [0, 7], "cost_object": 0, "cost_sampl": 0, "costawareutil": 0, "costawarewarn": 2, "costli": 14, "couckuyt": 14, "couckuyt2012": 14, "could": [0, 7, 14], "count": 0, "cousin": 0, "cousin2013mvar": 0, "cov": [7, 9, 10, 14], "cov_": 7, "cov_f_new": 0, "cov_f_old": 0, "cov_ln": 7, "cov_ln_": 7, "cov_n": 7, "cov_n_": 7, "covar": 7, "covar_modul": 7, "covar_module_bias": 7, "covar_module_unbias": 7, "covari": [0, 7, 9, 10, 14], "covariance_matrix": 14, "covariance_root": 14, "cover": 0, "cpu": [0, 14], "crash": 12, "creas": 14, "creat": [0, 1, 7, 8, 14], "creation": 7, "creteria": 7, "criteria": 0, "criterion": [7, 8, 12, 14], "cross": [1, 7, 14], "cross_covariance_matrix": 14, "cross_valid": 5, "crucial": 14, "ctx": 14, "cube": [7, 11, 12, 14], "cuda": [0, 7], "cultur": 12, "cumul": 0, "current": [0, 1, 2, 4, 7, 8, 13, 14], "current_model": 8, "current_valu": 0, "current_x": [2, 8], "currin": 12, "custom": [0, 7, 8, 12, 14], "cv": 1, "cv_fold": 1, "cv_result": 1, "cvar": 0, "cvfold": 1, "cvresult": 1, "cycl": [7, 8], "cyclic": [0, 8], "cyclic_opt": 8, "d": [0, 1, 4, 7, 8, 10, 12, 14], "d_f": [0, 7], "da": 14, "damp": 0, "daniel": 12, "data": [0, 2, 7, 8, 9, 10, 11, 14], "data_covar_modul": 7, "data_fidel": 7, "data_load": 8, "dataclass": 14, "dataload": 8, "datapoint": 7, "dataset": [0, 7], "dateset": 14, "daulton": [0, 12, 14], "daulton2020qehvi": [0, 14], "daulton2021nehvi": 0, "daulton2022": 12, "daulton2022bopr": [7, 14], "daulton2022mar": 0, "daulton2023hvkg": 0, "david": 7, "dayanik": 0, "de": [7, 14], "deb": 12, "deb2005dtlz": 12, "deb2005robust": 12, "decai": [0, 7, 8, 14], "decemb": 12, "decis": [0, 7, 14], "decompos": 7, "decomposit": [0, 7], "decor": [0, 7, 13, 14], "decoupledacquisitionfunct": 0, "decreas": [8, 14], "decres": 0, "dedic": 14, "dedup": 7, "dedupl": 14, "deep": [0, 7, 9, 14], "deepcopi": [7, 14], "def": [7, 14], "default": [0, 4, 7, 8, 9, 10, 11, 12, 14], "default_bound": 8, "defin": [0, 7, 8, 9, 10, 12, 14], "definin": 14, "defininig": 14, "definit": 0, "deg": 14, "degener": 7, "deisenroth": [0, 10], "delattr_ctx": 14, "delaunai": 14, "delaunaypolytopesampl": 14, "delet": 14, "delta": [0, 7, 12], "delta_0": 12, "delta_i": 12, "denomin": 7, "denot": [0, 7, 8, 10, 14], "dens": [12, 14], "densecontain": 14, "densiti": [0, 9, 14], "depend": [0, 7, 12, 13], "deprec": [0, 2, 7, 9, 10, 14], "deprecationerror": 2, "depth": [3, 7], "der": 7, "deriv": [0, 12, 14], "descend": 14, "descent": 8, "deschrijv": 14, "describ": [0, 4, 7, 9, 14], "descript": [0, 7], "deshwal": 0, "design": [0, 7, 8, 12, 14], "desir": [0, 7, 10, 14], "destin": 14, "detach": [7, 8, 14], "detail": [0, 7, 8, 10, 12, 14], "detect": 2, "detect_dupl": 7, "determin": [0, 7, 8, 14], "determinant": 7, "determinist": [0, 9, 10, 14], "deterministicmodel": [0, 7], "determist": 7, "deutz": [0, 12], "develop": 0, "deviat": [0, 7, 8, 12, 14], "devic": [7, 8, 9, 14], "dgsm": 12, "dgsm_gradient": 12, "dgsm_gradient_ba": 12, "dgsm_gradient_squar": 12, "dh": 12, "dh1": 12, "dh2": 12, "dh3": 12, "dh4": 12, "dhaen": 14, "di": 0, "diag": 14, "diaglinearoper": 14, "diagnost": 3, "diagon": [0, 7, 14], "dickstein": 7, "dict": [0, 1, 3, 4, 7, 8, 10, 14], "dictionari": [0, 3, 4, 7, 8, 10, 14], "diffenti": 14, "differ": [0, 4, 7, 8, 9, 10, 12, 14], "differenti": [0, 7, 14], "difficult": [9, 14], "digabel": 12, "dim": [0, 4, 7, 8, 9, 10, 12, 14], "dimens": [0, 4, 7, 8, 9, 10, 12, 14], "dimension": [0, 4, 7, 8, 10, 12, 14], "dimesion": 8, "dimsension": 14, "direct": 14, "directli": [0, 7, 10, 14], "directori": 13, "disabl": [0, 7], "disable_pivot": 14, "disable_progbar": 3, "disagr": 0, "disc": 12, "discard": 14, "discbrak": 12, "disconnect": 12, "discontinu": 12, "discret": [0, 7, 8, 9, 12, 14], "discrete_choic": 8, "discrete_dim": 8, "discrete_step": 8, "discretemaxvaluebas": 0, "discuss": [0, 7], "disk": 12, "dispatch": [3, 8], "dist": 7, "distanc": [7, 8, 12], "distinct": [0, 14], "distribut": [0, 4, 7, 9, 10, 12], "div": 14, "divers": [0, 7, 14], "divid": [10, 14], "divis": [7, 14], "dixonpric": 12, "dklv17": 0, "dltz1": 12, "dltz2": 12, "do": [0, 4, 7, 8, 12, 14], "doc": [0, 14], "docstr": [0, 4, 8, 14], "document": [7, 8], "doe": [0, 2, 7, 8, 9, 10, 14], "doesn": 7, "doi": 4, "domain": [0, 8, 12, 14], "domin": [0, 12], "dominatedpartit": [0, 14], "don": [7, 8], "done": [0, 8, 14], "doppa": 0, "doubl": [0, 7, 14], "doubli": 14, "doucet": 7, "doucet2010sampl": [7, 9], "doucet_simulationconditionalgaussian": 7, "down": [3, 7, 14], "downsampl": 7, "downsamplingkernel": 7, "downstream": [7, 8], "dpp": 7, "draw": [0, 4, 7, 9, 10, 14], "draw_kernel_feature_path": 10, "draw_matheron_path": 10, "draw_sobol_normal_sampl": 14, "draw_sobol_sampl": [8, 14], "drawn": [0, 9, 10, 14], "driven": 10, "drop": [7, 14], "dropwav": 12, "dtlz": 12, "dtlz1": 12, "dtlz2": 12, "dtlz3": 12, "dtlz4": 12, "dtlz5": 12, "dtlz7": 12, "dtype": [0, 2, 7, 8, 9, 10, 12, 14], "dtypes_of_buff": 7, "due": [0, 2, 7, 14], "dummi": [0, 7, 14], "dummynonscalarizingposteriortransform": 14, "duplic": 7, "dure": [0, 2, 3, 7, 8, 9, 14], "dx": 14, "dy": 14, "dynam": 8, "d\u00f6pp": [0, 12], "e": [0, 3, 4, 7, 8, 9, 10, 12, 14], "e_d": 12, "e_g": 12, "each": [0, 4, 7, 8, 9, 10, 12, 14], "eagerli": 9, "eas": 7, "easi": [9, 12], "easier": 7, "easiest": 7, "edg": 12, "edward": 7, "effect": [0, 7, 14], "effici": [0, 7, 10, 12, 14], "egghold": 12, "ehvi": 0, "ei": [0, 2], "ei_proxim": 0, "eip": 0, "either": [0, 4, 7, 8, 9, 14], "eleg": 0, "element": [0, 4, 7, 8, 9, 14], "eleven": 12, "elimin": 7, "elment": 8, "els": [0, 7, 8, 10, 14], "elsewher": 7, "elston": 14, "embed": 7, "embedding_dim": 14, "embs_dim_list": 7, "embs_feature_dict": 7, "emit": [0, 7], "emmerich": [0, 12], "empir": [0, 12], "empti": [7, 14], "enabl": [0, 7, 9, 10, 14], "enable_grad": 0, "encapsul": 14, "encod": [4, 7, 8, 14], "encode_arg": 14, "encompass": 0, "encount": [7, 14], "encourag": 7, "end": [0, 7, 8, 9, 14], "enforc": [8, 14], "enforce_hasattr": 14, "engin": [7, 8, 10, 12], "ensembl": [10, 14], "ensemble_s": 9, "ensemblemodel": 7, "ensembleposterior": [7, 9, 10], "ensur": [0, 9, 14], "entir": [9, 14], "entri": [0, 7, 8, 14], "entropi": 12, "enum": 8, "enumer": [8, 14], "environment": 7, "ep": [0, 7, 14], "ep_jitt": 0, "epsilon": [7, 10], "epsneg": 14, "eq": 8, "equal": [0, 4, 7, 8, 9, 10, 14], "equal_nan": 14, "equality_constraint": [4, 8, 14], "equally_spac": 8, "equat": [0, 7, 12, 14], "equival": [7, 14], "erfc": 14, "erfcx": 14, "eric": 7, "eriksson": [0, 7, 14], "eriksson2021saasbo": 7, "eriksson2021scal": 7, "eriksson21a": 7, "error": [0, 3, 7, 8, 10, 14], "especi": 14, "ess": 14, "essenti": 7, "estim": [0, 3, 4, 7, 12, 14], "estimate_feasible_volum": 14, "estimation_typ": 0, "et": 0, "eta": [0, 4, 7, 8, 14], "etc": 0, "eubo": 0, "eval": [7, 14], "eval_lin_constraint": 8, "evalaut": 0, "evalu": [0, 1, 3, 4, 7, 8, 10, 12, 14], "evaluate_slack": 12, "evaluate_slack_tru": 12, "evaluate_tru": 12, "evaluation_mask": [0, 7], "evalut": [8, 10], "even": [0, 7], "event": 14, "event_shap": 14, "everi": [0, 3, 7, 8, 10, 14], "everyth": 0, "everywher": 14, "evid": 7, "evol": 14, "evolutionari": [12, 14], "exact": [0, 7, 10, 12, 14], "exact_gp": 7, "exactgp": [0, 7, 14], "exactli": 14, "exactmarginalloglikelihood": [1, 7], "examin": 14, "exampl": [0, 1, 3, 4, 7, 8, 9, 10, 14], "exampleacquisitionfunct": 14, "exampleclass": 14, "exce": 0, "except": [0, 5, 7, 8, 12, 14], "excess": 8, "exclud": [0, 8], "execut": [4, 8, 14], "exercis": 14, "exhibit": 12, "exist": [0, 10, 14], "exit": 14, "exp": [4, 7, 8, 12, 14], "expand": [0, 7, 9, 14], "expand_and_copy_tensor": 7, "expand_dim": 0, "expand_trace_observ": 0, "expans": [0, 14], "expect": [0, 7, 8, 9, 10, 12, 14], "expectationposteriortransform": 0, "expected_q": 14, "expectedhypervolumeimprov": 0, "expectedimprov": 0, "expectedimprovementqualityfunct": 7, "expecxt": 14, "expedit": 14, "expens": [0, 7, 8, 14], "experi": [0, 12], "experiment": [0, 7], "expit": 14, "explan": 8, "explicit": [2, 7, 8], "explicitli": [0, 9], "explod": 8, "exploit": [0, 7, 9], "explor": 0, "expmastoppingcriterion": 8, "expon": [0, 12, 14], "exponenti": [0, 7, 8, 14], "exponential_decai": 7, "exponentialdecaykernel": 7, "expos": 9, "express": [7, 8, 12], "extend": [0, 9, 14], "extens": 0, "extra": [7, 14], "extra_repr": 14, "extract": [0, 4, 8, 14], "extract_batch_covar": 14, "extract_candid": 0, "extrem": [7, 9, 14], "f": [0, 7, 9, 10, 12, 13, 14], "f1": 7, "f2": 7, "f_": 12, "f_0": 12, "f_1": [0, 7, 12], "f_2": [0, 7, 12], "f_i": [0, 7, 12], "f_i1": 0, "f_ij": 0, "f_ik": 0, "f_j": 7, "f_k": 0, "f_m": 12, "f_map": 7, "f_mean": 7, "f_np_wrapper": 8, "f_opt": 14, "f_stddev": 7, "face": 0, "fact": 8, "factor": [0, 8, 14], "factori": [8, 14], "fail": [0, 7, 8], "failur": [0, 8], "fake": 7, "fals": [0, 1, 3, 4, 7, 8, 10, 12, 13, 14], "famili": 0, "fanci": 8, "fant_x": 8, "fantas": [0, 7, 8], "fantasi": [0, 7, 8, 11], "fantasizemixin": [7, 14], "fantasy_model": 8, "far": [0, 4, 7, 8, 14], "fashion": [0, 7, 8], "fast": [7, 14], "faster": [0, 3, 14], "fastnondominatedpartit": [0, 14], "fastpartit": 14, "fat": [0, 14], "fatmax": 14, "fatmaximum": 14, "fatmin": 14, "fatminimum": 14, "fatmoid": 14, "fatplu": 14, "favor": [0, 7], "feasibilityweightedmcmultioutputobject": 0, "feasibilti": 0, "feasibl": [0, 8, 12], "feasible_volum": 14, "featur": [1, 2, 4, 7, 8, 14], "feature_index": [0, 8], "feature_indic": 7, "feature_map": 10, "feature_nam": 14, "feature_set": [0, 7], "featuremap": 10, "featureselector": 10, "feb": 14, "fed": 7, "feed": [12, 14], "feng": 7, "feng2020hdcp": 7, "ferment": 12, "few": 7, "fewer": [0, 7, 8], "fidelity_1": 7, "fidelity_2": 7, "fidelity_dim": [0, 7], "fidelity_featur": 7, "fidelity_weight": [0, 7], "field": [0, 1, 8, 14], "figur": 14, "file": [9, 14], "fill": [0, 8], "fill_valu": 8, "filter": [0, 7, 8, 12, 14], "filter_domin": 0, "filterfeatur": 7, "final": [0, 7, 8, 10], "final_opt": 8, "financ": 10, "financi": 0, "find": [0, 7, 8, 14], "find_interior_point": 14, "finfo": 14, "finit": [0, 4, 9, 10, 14], "first": [0, 4, 7, 8, 12, 14], "fit": [0, 2, 4, 5, 7, 14], "fit_arg": 1, "fit_fully_bayesian_model_nut": [3, 7], "fit_gpytorch_": 7, "fit_gpytorch_ml": [1, 3, 7], "fit_gpytorch_mll_scipi": 8, "fit_gpytorch_mll_torch": [7, 8], "fix": [4, 7, 8, 9, 12, 14], "fix_featur": 8, "fixed_cost": 7, "fixed_featur": [0, 4, 8], "fixed_features_list": 8, "fixed_indic": 14, "fixed_x_fantasi": 8, "fixedcostmodel": 7, "fixedfeatureacquisitionfunct": 0, "fixedhomotopyschedul": 8, "fixednoisegaussianlikelihood": 7, "fixedsinglesamplemodel": 7, "fixtur": 14, "fkwarg": 7, "flag": [0, 7, 11, 14], "flat_idxr": 8, "flatten": 7, "flattenedstandard": 7, "flexibl": 7, "flip": 14, "float": [0, 2, 4, 7, 8, 9, 12, 14], "float32": [0, 14], "float64": [0, 7, 12, 14], "floatortensor": 0, "fly": [0, 7], "focus": 0, "fold": 1, "follow": [0, 1, 4, 7, 8, 10, 14], "fonseca": 14, "fonseca2006": 14, "footprint": 14, "forc": [13, 14], "forest": 9, "fork_rng": 10, "forkedrngsampl": [0, 10], "form": [0, 4, 7, 8, 10, 12, 14], "formal": 0, "format": [0, 7, 14], "former": [7, 10, 14], "formul": [0, 7], "formula": 0, "forward": [0, 3, 4, 7, 8, 10, 12, 14], "forwardbackwardclosur": 8, "found": [4, 7, 8, 12], "four": [7, 12], "fourier": [0, 10, 14], "frac": 7, "frac_random": 8, "fraction": [0, 8, 14], "framework": 0, "frazier": [0, 7], "frazier2008knowledg": 0, "free": [7, 12], "freita": 7, "frohlich": 12, "frohlich2020": 12, "from": [0, 1, 4, 7, 8, 9, 10, 12], "from_joint_dataset": 14, "fronit": 14, "front": [0, 12, 14], "frontier": [0, 8, 12, 14], "fsolv": 7, "fukuoka": 0, "fukushima": 12, "fulfil": 8, "full": [0, 7, 8, 10, 14], "full_lik": 7, "full_optim": 0, "full_tre": 8, "fulli": [0, 3, 8, 14], "fully_bayesian": [7, 9], "fully_bayesian_multitask": 7, "fullybayesianacquisitionfunct": 0, "fullybayesianposterior": 9, "fun": 8, "func": 14, "function": [1, 2, 7, 9, 14], "funtion": 8, "further": [0, 7, 8], "fuse": 8, "fusi": 7, "futur": [0, 14], "fval": 8, "g": [0, 3, 7, 8, 10, 12, 14], "gain": 0, "gal": 0, "galsbei": 14, "gamma": 7, "gammaprior": 7, "gandi": 0, "gardner": [0, 14], "garnett": 0, "garrido": [0, 12], "garridomerchan2020": 12, "gauss": [7, 14], "gaussian": [0, 7, 8, 9, 12], "gaussian_upd": 10, "gaussianlikelihood": 7, "gaussianmixtureposterior": [7, 9], "gaussianpenalti": 0, "gd": 0, "gelbart": 12, "gelbart2014": 12, "gen": 4, "gen_batch_initial_condit": [4, 8], "gen_candid": 8, "gen_candidates_scipi": [4, 8], "gen_candidates_torch": [4, 8, 10], "gen_kernel_featur": 10, "gen_loo_cv_fold": 1, "gen_multi_task_dataset": 14, "gen_one_shot_hvkg_initial_condit": 8, "gen_one_shot_kg_initial_condit": 8, "gen_pareto_front": 12, "gen_positional_indic": 14, "gen_value_function_initial_condit": 8, "gener": [1, 2, 5, 7, 9, 12, 13, 14], "generalizedlinearpath": 10, "generate_starting_point": 8, "generic_object": 0, "genericcostawareutil": 0, "genericdeterministicmodel": [0, 7, 10, 14], "genericmcmultioutputobject": 0, "genericmcobject": 0, "genet": [12, 14], "genz": 14, "genz2004bvnt": 14, "genz2016numer": 14, "gertvv": 14, "gessner": 14, "gessner2020": 14, "get": [0, 7, 8, 14], "get_acqf_input_constructor": 0, "get_acquisition_funct": 0, "get_all_task": 7, "get_attribut": 7, "get_aug_chebyshev_scalar": 14, "get_augmented_q_batch_s": 0, "get_batch_dimens": 7, "get_best_candid": 4, "get_best_f_analyt": 0, "get_best_f_mc": 0, "get_bounds_as_ndarrai": 8, "get_chebyshev_scalar": 14, "get_const": 14, "get_constants_lik": 14, "get_covar_module_with_dim_scaled_prior": 7, "get_data_load": 8, "get_dataset_without_task_featur": 14, "get_default_dtyp": 10, "get_default_partitioning_alpha": 0, "get_deterministic_model": 14, "get_deterministic_model_list": 14, "get_deterministic_model_multi_sampl": 14, "get_device_of_sequ": 0, "get_dtype_of_sequ": 0, "get_eval_gp_sample_cal": 14, "get_fantasy_model": 7, "get_feasible_sampl": 14, "get_fully_bayesian_model": 14, "get_fully_bayesian_model_list": 14, "get_gaussian_likelihood_with_gamma_prior": 7, "get_gaussian_likelihood_with_lognormal_prior": 7, "get_gp_sampl": 14, "get_hypercell_bound": 14, "get_ic_gener": 8, "get_index_tensor": 14, "get_induced_fantasy_model": 0, "get_infeasible_cost": 0, "get_init_arg": 7, "get_input_transform": 10, "get_loss_closur": 8, "get_loss_closure_with_grad": [3, 8], "get_matern_kernel_with_gamma_prior": 7, "get_matheron_path_model": [7, 10], "get_model": 14, "get_monotonicity_constraint": 14, "get_multi_step_tree_input_represent": 0, "get_mvar_set_": 0, "get_mvar_set_vector": 0, "get_mvar_set_via_count": 0, "get_name_filt": 8, "get_nearest_neighbor": 8, "get_objective_weights_transform": 14, "get_optimal_sampl": 0, "get_outcome_constraint_transform": 14, "get_outcome_feasibility_prob": 14, "get_output_transform": 10, "get_paramet": 8, "get_parameters_and_bound": 8, "get_partition_bound": 14, "get_polytope_sampl": 14, "get_posterior_sampl": 0, "get_pvar_expect": 14, "get_rounding_input_transform": 7, "get_sampl": [0, 10, 14], "get_sample_mo": 14, "get_split_shap": 0, "get_spray_point": 8, "get_stat": 8, "get_swap_module_params_on_convers": 14, "get_task_value_remap": 7, "get_tensors_as_ndarray_1d": 8, "get_train_input": 10, "get_train_target": 10, "get_weights_posterior": 14, "get_x_baselin": 8, "getattr": 7, "getlogg": 11, "getlossclosur": 8, "getlossclosurewithgrad": 8, "gge": 14, "ghahramani": [0, 7], "gibbon": 0, "gibson": 14, "gibson1994mont": 14, "github": [3, 7, 12, 14], "give": [0, 7], "given": [0, 4, 7, 8, 10, 11, 12, 14], "gj": 14, "glob": 0, "global": [0, 7, 12], "glucos": 12, "gmm": 12, "go": [0, 8], "goal": [7, 12, 14], "goe": 14, "goldberg": 12, "good": 7, "govern": [0, 14], "gp": [0, 1, 3, 4, 10], "gp1": 7, "gp2": 7, "gp_model": 7, "gp_regress": [0, 7], "gp_regression_fidel": 7, "gp_regression_mix": 7, "gp_sampl": 14, "gpdraw": 14, "gpflow": 14, "gpflowopt": 14, "gpt_posterior_set": 7, "gpytorch": [0, 1, 3, 8], "gpytorch_modul": 7, "gpytorchmodel": [0, 1, 7, 8, 10, 14], "gpytorchposterior": [0, 1, 7, 9, 14], "grad": [3, 8], "grad_output": 14, "gradf": 2, "gradient": [2, 3, 4, 7, 8, 9, 11, 12, 14], "graepel": 0, "grai": 12, "gramaci": [0, 12], "gramacy2016": 12, "great": 7, "greater": [0, 7], "greatli": 0, "greedi": [0, 7, 8, 14], "greedili": 7, "greedyimprovementreduct": 7, "greedyvariancereduct": 7, "grid": [0, 7], "griewank": 12, "ground": 14, "group": 0, "group_lasso_regular": 0, "grouplassopenalti": 0, "grow": 9, "gsobol": 12, "guarante": [7, 14], "gumbel": 0, "guoxin": 7, "gupta": 12, "h": [0, 7, 12, 14], "h_min": 12, "ha": [0, 1, 4, 7, 8, 9, 12, 13, 14], "halv": 0, "ham": 7, "han": 7, "hand": [8, 14], "handl": [0, 7, 8, 9, 12, 14], "happen": [0, 14], "hard": 0, "harder": 0, "harryql": 12, "hartmann": 12, "hash": 7, "hashabl": 0, "hasten": 14, "have": [0, 2, 4, 7, 8, 9, 10, 12, 14], "hazard": 14, "heavi": 0, "heavili": 7, "heavisid": 14, "hedar": 12, "hedar2006derivfre": 12, "hellsten": 7, "help": [0, 7, 10, 14], "helper": [0, 7, 9], "henc": 0, "hennig": 14, "henri": 7, "hensman": 7, "hensman2013svgp": 7, "here": [0, 7, 8, 12, 14], "hern": 12, "hernandez": 0, "hess": 8, "hessian": 7, "hessianupdatestrategi": 8, "hessp": 8, "heterogen": [7, 14], "heteroscedast": 7, "heurist": [0, 8, 12], "hierarch": 7, "high": [0, 7, 8, 14], "higher": [0, 4, 14], "higher_ord": 9, "higher_order_gp": 7, "higherordergp": [7, 9], "higherordergpposterior": [7, 9], "highest": 4, "highli": [0, 7, 9, 14], "hisao": 12, "hit": 14, "hitandrunpolytopesampl": 14, "hogp": 9, "hold": [0, 14], "holder": 12, "holdert": 12, "homotopy_paramet": 8, "homotopyparamet": 8, "hong": 0, "hong2014review": 0, "honor": 7, "hood": 10, "hook": [7, 10, 14], "hope": 0, "hopefulli": 7, "hot": [7, 14], "houlsbi": 0, "houlsby2011bald": 0, "hous": [3, 8, 14], "how": [0, 7, 8], "howev": [0, 7, 14], "html": [7, 12], "http": [0, 3, 4, 7, 12, 14], "hu": 0, "huang": 0, "hull": 14, "hundr": 7, "husz\u00e1r": 0, "hutter": 0, "hv": 14, "hvarfner": [0, 7], "hvarfner2022": 0, "hvarfner2022joint": 0, "hvarfner2024vanilla": 7, "hvkg": [0, 8], "hybrid": 14, "hyper": [0, 7, 14], "hyperbol": [7, 14], "hypercel": [0, 14], "hypercell_bound": 0, "hypercub": 12, "hyperparam": 7, "hyperparamet": [0, 7, 8, 14], "hyperplan": 12, "hyperrectangl": 14, "hyperspher": [12, 14], "hypertriangl": 14, "hypervolum": [8, 12], "hypervolume_knowledge_gradi": 0, "i": [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14], "i1": 7, "i2": 7, "i_d": [10, 14], "ibanez": 14, "ic_gen_kwarg": 8, "ic_gener": 8, "iclr": 0, "icm": [7, 9], "icml": 0, "idc": 7, "idea": [0, 14], "ideal": 12, "ident": [0, 7, 9, 12, 14], "identifi": [0, 7, 9, 14], "identity_object": 0, "identitymcmultioutputobject": [0, 14], "identitymcobject": [0, 4], "identitymultioutputobject": 0, "identitystefunct": 14, "ieee": [12, 14], "iff": 12, "ignor": [0, 3, 7, 8, 10, 14], "ignore_dim": 7, "ignore_x_dim": 7, "ii": [0, 7, 11, 14], "iid": [10, 14], "iidnormalsampl": 10, "iii": [7, 11], "ij": 7, "ijcnn": 0, "imag": 7, "imagin": 7, "impact": [7, 12], "implement": [0, 7, 8, 9, 10, 12, 14], "impli": [0, 14], "implicitli": [7, 9, 14], "import": [0, 1, 7, 14], "importantli": 7, "impos": [8, 14], "imposs": 0, "improv": [0, 7, 8, 12, 14], "inaccess": [0, 7], "incl": [0, 14], "includ": [0, 4, 7, 8, 10, 12, 14], "incompat": 8, "incorpor": [0, 7], "increas": [0, 3, 14], "increment": [0, 12, 14], "incremental_nehvi": [0, 14], "incur": 14, "independ": [0, 7, 8, 9, 14], "independentcvar": 0, "independentmodellist": 7, "independentvar": 0, "index": [0, 5, 7, 8, 14], "index_sampl": 10, "indexsampl": [9, 10], "indic": [0, 3, 4, 7, 8, 9, 10, 12, 14], "indictor": 7, "individu": [0, 4, 7, 8, 9, 14], "induc": 0, "inducing_point": 7, "inducing_point_alloc": 7, "inducingpointalloc": 7, "ineffici": [0, 1], "ineq": 8, "inequ": [4, 8, 14], "inequality_constraint": [4, 8, 14], "inf": [0, 7, 8, 14], "infeas": [0, 7, 8, 14], "infeasible_cost": [0, 14], "infeasible_obj": 0, "infer": [0, 7, 14], "infer_nois": 14, "infer_reference_point": [12, 14], "inferred_noise_model": 7, "infin": 14, "infinit": [7, 12], "infinite_width_bnn": 7, "infinitewidthbnnkernel": 7, "influenc": 14, "info": 11, "info_gain": 0, "inform": [0, 4, 7, 8, 9, 10, 14], "informat": 12, "infti": [4, 14], "inherif": 7, "inherit": [0, 7], "init_batch_limit": 8, "init_inducing_point": 7, "init_point": 0, "initi": [0, 1, 2, 4, 7, 10, 14], "initial_condit": 4, "initialization_strategi": 8, "initialize_q_batch": 8, "initialize_q_batch_nonneg": 8, "initit": 8, "initvar": 14, "inject": 8, "inlin": 0, "inner": [0, 8], "inner_mc_sampl": 0, "inner_sampl": 0, "inplac": 8, "input": [1, 2, 4, 8, 9, 10, 11, 12, 14], "input_batch_shap": 7, "input_constructor": 0, "input_dim": 14, "input_transform": [0, 1, 7, 10, 14], "inputdataerror": [2, 7], "inputdatawarn": 2, "inputperturb": [0, 7], "inputstandard": 7, "inputtransform": [7, 10], "insert": [0, 7, 14], "insid": [0, 7, 14], "instanc": [0, 1, 3, 7, 8, 10, 11, 14], "instanti": [0, 7, 14], "instead": [0, 7, 8, 10, 14], "int": [0, 3, 4, 7, 8, 9, 10, 11, 12, 14], "int_": 14, "integ": [0, 7, 14], "integer_indic": 7, "integr": [0, 7, 12, 14], "intellig": [0, 7, 12], "intend": [0, 7, 9, 14], "intens": [0, 7, 14], "inter": [4, 7, 8, 14], "interact": 7, "interaction_featur": 7, "interactionfeatur": 7, "interfac": [7, 8], "interior": [7, 14], "interior_point": 14, "intern": [0, 1, 4, 7, 8, 9, 10, 12, 14], "interpret": 0, "intersect": 14, "interv": [7, 14], "intra": [4, 8], "intrins": 7, "introduc": [0, 7, 12], "introduct": 10, "intut": 8, "inv_transform": 10, "invalid": 14, "invers": [0, 7, 10, 14], "inversecostweightedutil": [0, 7], "inverselengthscaletransform": 10, "invert": 0, "invok": [8, 9, 14], "involv": [0, 7, 14], "irshad": [0, 12], "irshad2021": 12, "irshad2021momf": 0, "is_ensembl": 14, "is_feas": 12, "is_fully_bayesian": 14, "is_intrapoint": 8, "is_non_domin": 14, "is_nonneg": 8, "is_one_to_mani": 7, "ishibuchi": [12, 14], "ishibuchi2011": 14, "ishigami": 12, "isn": [7, 8], "issu": [0, 2, 3, 7, 14], "issuecom": 14, "item": [0, 7, 8, 10, 14], "item_0": 14, "item_1": 14, "iter": [0, 2, 4, 7, 8, 10, 13, 14], "iteration_fidel": 7, "ith": [7, 14], "its": [0, 4, 7, 8, 9, 10, 12, 13, 14], "itself": [7, 14], "j": [0, 7, 8, 10, 12, 14], "jac": 8, "jacobian": 8, "jain": 12, "jame": 7, "jankowiak": 7, "jasper": 12, "je": 0, "jegelka": 0, "ji": 12, "jiang": 0, "jiang2020multistep": 0, "jit": 3, "jit_compil": 3, "jitter": [0, 7, 14], "jj": 7, "jmlr": 7, "join": 10, "joint": [4, 7, 8, 9, 14], "joint_covariance_matrix": [7, 9], "joint_dim": 10, "joint_entropy_search": 0, "jointli": [0, 4, 7, 8, 14], "joost": 0, "journal": [0, 7, 10, 12, 14], "jul": 7, "juli": 14, "jun": 7, "just": [0, 4, 7, 8, 13], "justif": 7, "k": [0, 7, 10, 12, 14], "k_0": 7, "k_1": [7, 12], "k_2": [7, 12], "k_3": 7, "k_cat_1": 7, "k_cat_2": 7, "k_cont_1": 7, "k_cont_2": 7, "k_d": 12, "k_f": 7, "k_g": 12, "k_i": [7, 8, 12], "k_m": 7, "k_p": 12, "k_x": 12, "kaa": 14, "kab": 14, "kanjil": 14, "kanta": 0, "karasuyama": 0, "karrer": [0, 12], "karsch": [0, 12], "kayween": 14, "kba": 14, "kbb": 14, "keep": [7, 8, 14], "keep_dim": 7, "keep_var": 14, "keepdim": [0, 7, 14], "kei": [4, 7, 8, 10, 14], "kept": [7, 14], "kernel": [9, 10, 14], "kernel_batch_shap": 14, "kernel_matrix": 7, "kernelevaluationmap": 10, "kernelfeaturemap": 10, "keyword": [0, 1, 3, 7, 8, 14], "kg": [0, 8], "kind": 14, "kirbi": 7, "kirsch": 0, "kirsch2019batchbald": 0, "klamroth": 14, "klensk": 12, "know": 7, "knowl": 14, "knowledge_gradi": 0, "knowledgegradi": 0, "knowles2005": [0, 14], "known": [0, 2, 7, 8], "koyama": 0, "krige": 0, "kroneck": [7, 9], "kroneckermultitaskgp": [7, 9], "kumaraswami": 7, "kundu": 12, "kw_onli": 14, "kwarg": [0, 2, 3, 4, 7, 8, 10, 14], "l": [0, 4, 7, 8, 12, 14], "l0": 0, "l0approxim": 0, "l0penaltyapprox": 0, "l0penaltyapproxobject": 0, "l1": 0, "l1_penalized_object": 0, "l1_penalty_object": 0, "l1penalti": 0, "l1penaltyobject": 0, "l2": 0, "l2penalti": 0, "l_": 14, "l_i": 8, "la2": 12, "laa": 14, "label": [0, 7, 14], "lacour": 14, "lacour17": 14, "lagrangian": 12, "lai": 12, "lambd": 12, "lambda": [0, 7], "lame": 7, "landscap": 14, "laplac": [0, 7], "larg": [0, 7, 8, 9, 10, 12, 14], "larger": [0, 7, 8, 12, 14], "largest": 0, "lasso": 0, "last": [0, 7, 8, 12, 14], "last_fantasize_flag": 14, "latent": [0, 7], "latent_init": 7, "later": [0, 4, 8], "latham": 7, "latter": [7, 10, 14], "lattic": 8, "laumann": 12, "lawrenc": 7, "layer": [0, 7], "lazi": 14, "lb": 0, "lb2": 0, "lba": 14, "lce": 7, "lcea": 14, "lceagp": 7, "lceakernel": 7, "lcem": 14, "lcemgp": 7, "ldot": 7, "le": 12, "lead": [0, 7, 8, 14], "leaf_modul": 7, "learn": [7, 8, 10, 12], "learn_bound": 7, "learn_coeffici": 7, "learn_inducing_point": 7, "learn_latent_par": 7, "learnabl": 7, "learned_pref_obj": 0, "learnedobject": 0, "learning_r": 14, "least": [7, 8, 9, 12, 14], "lee": [7, 12], "lee2018deep": 7, "left": [7, 10, 14], "legaci": 2, "legacy_ei_numerics_warn": 2, "legacy_nam": 2, "legendr": 7, "leggauss": 14, "lemong": 12, "lemonge2010constrain": 12, "len": [7, 8, 14], "length": [0, 7, 8, 12, 14], "lengthscal": [0, 7, 10], "lengthscale_constraint": 7, "lengthscale_constraint_bias": 7, "lengthscale_constraint_unbias": 7, "lengthscale_prior": 7, "lengthscale_prior_bias": 7, "lengthscale_prior_unbias": 7, "lengyel": 0, "lenthscal": 7, "less": [0, 8, 12, 14], "lesser": 7, "let": [0, 8, 14], "letham": [0, 12], "letham2019": 12, "level": [0, 7, 8, 9, 11, 14], "leverag": 0, "levi": 12, "li": 12, "liang": 12, "liang2021": 12, "librari": 12, "lie": 14, "lifetim": 14, "lifetime_sampl": 14, "lightweight": 7, "like": [0, 7, 8, 14], "likelihood": [0, 1, 8, 10, 14], "likelihood_rank": 7, "limit": [0, 7, 8, 14], "lin": 0, "lin2022prefer": 0, "lin_constraint_jac": 8, "lin_ess": 14, "linalg": 14, "lincongauss": 14, "lindauer": 0, "line": [0, 12, 14], "linear": [0, 7, 8, 10, 12], "linear_constraint": 0, "linear_object": 0, "linear_oper": 7, "linear_trunc": 7, "linear_truncated_fidel": 7, "linearellipticalslicesampl": 14, "linearhomotopyschedul": 8, "linearli": [10, 14], "linearmcobject": 0, "linearoper": [7, 9, 10, 14], "lineartruncatedfidelitykernel": 7, "link": [0, 14], "linspac": 8, "list": [0, 4, 8, 12], "list_gp": 7, "list_sampl": 10, "listsampl": [0, 7, 10], "liu": 0, "lkf17": 0, "lkj": 7, "lkjcovarianceprior": 7, "ln": 14, "load": 7, "load_mcmc_sampl": 7, "load_state_dict": [7, 14], "lobato": [0, 12], "loc": 14, "local": [0, 8, 12, 14], "locat": [0, 7, 9, 14], "log": [0, 5, 7, 8, 11, 14], "log10": 7, "log1mexp": 14, "log1pexp": 14, "log_": 0, "log_a": 14, "log_b": 14, "log_cdf_robust": 0, "log_erfc": 14, "log_erfcx": 14, "log_fatmoid": 14, "log_fatplu": 14, "log_level": 11, "log_level_default": 11, "log_ndtr": 14, "log_p": 7, "log_partit": 14, "log_phi": 14, "log_pi": 0, "log_prob": 14, "log_prob_extra": 14, "log_prob_normal_in": 14, "log_softplu": 14, "logarithm": [0, 14], "logcei": 0, "logconstrainedei": 0, "logconstrainedexpectedimprov": 0, "logdiffexp": 14, "logei": [0, 2], "logei_nam": 2, "logexpectedimprov": 0, "logexpit": 14, "logger": 11, "logic": 7, "logimprovementmcacquisitionfunct": 0, "logist": [0, 7], "loglinearhomotopyschedul": 8, "logmeanexp": 14, "lognei": 0, "lognoisyexpectedimprov": 0, "lognorm": 7, "lognorm_to_norm": 7, "lognparego": 0, "logpi": 0, "logplusexp": 14, "logprobabilityofimprov": 0, "logsumexp": 14, "long": [7, 8], "longtensor": [0, 7, 10, 14], "loo": 1, "look": 0, "lookup": [0, 14], "loop": [0, 7, 8, 14], "lopez": 14, "lorentzian": 14, "loss": [0, 4, 8, 14], "lot": 7, "low": [0, 7], "low_rank": 14, "lower": [0, 7, 8, 9, 12, 14], "lower_": 0, "lower_bound": [4, 8], "lower_i": 0, "lowerboundmultiobjectiveentropysearch": 0, "lr_schedul": 8, "lrschedul": 8, "lu": 7, "lu2022addit": 7, "ludkovski": 0, "m": [0, 1, 7, 8, 9, 10, 12, 14], "m1": [0, 7], "m1_c1": 14, "m1_c2": 14, "m2": [0, 7], "m_1": 7, "m_12": 7, "m_2": 7, "m_d": 7, "m_x": 12, "ma": [8, 12], "ma2019": 12, "machin": [0, 7, 10, 12], "maddox": 7, "maddox2021bohdo": 7, "maechler2012accur": 14, "magnifi": 7, "magnitud": 7, "mai": [0, 3, 7, 8, 9, 10, 12, 14], "main": [7, 10], "maintain": 7, "mainten": 14, "make": [0, 7, 8, 10, 14], "make_best_f": 0, "make_differenti": 0, "make_posterior_vari": 7, "make_scipy_bound": 8, "make_scipy_linear_constraint": 8, "make_scipy_nonlinear_inequality_constraint": 8, "maker": 7, "manag": [0, 7, 13], "manhattan": 8, "mani": [0, 4, 7, 12, 14], "manipul": 8, "manner": [0, 14], "manual": [0, 7, 9, 14], "manual_se": 14, "mao": 7, "map": [0, 7, 8, 14], "mapper": 7, "mar": 0, "margin": [0, 7, 9, 14], "marginalize_dim": [0, 14], "marginalloglikelihood": [1, 3, 7, 8], "mark": 7, "markov": [8, 14], "mask": [0, 7, 14], "mass": 9, "master": [7, 12, 14], "match": [0, 7, 8, 9, 10, 14], "match_batch_shap": 14, "materi": 0, "matern": 7, "matern52_kernel": 7, "maternkernel": 7, "math": [7, 12], "mathbb": 10, "mathbf": 7, "mathcal": 10, "mathemat": [7, 14], "matheron": [7, 9, 10], "matheronpath": 10, "matric": [0, 7, 14], "matrix": [0, 7, 9, 10, 14], "max": [4, 7, 8, 10, 14], "max_batch_s": 8, "max_discrete_valu": 8, "max_ep_iter": 0, "max_frac": 0, "max_hv": 12, "max_i": 14, "max_iep": [0, 14], "max_length": 7, "max_num_comparison": 10, "max_plate_nest": 7, "max_ref_point": 14, "max_retri": 8, "max_step": 9, "max_tree_depth": 3, "max_tri": [0, 14], "max_value_entropy_search": 0, "maxfev": 7, "maxim": [0, 4, 7, 8, 12, 14], "maximium": 0, "maximize_sampl": 4, "maximum": [0, 3, 4, 7, 8, 9, 12, 14], "maximun": 7, "maxit": [4, 8], "maxiter_altern": 8, "maxiter_continu": 8, "maxiter_discret": 8, "maxposteriorsampl": 4, "maxvaluebas": 0, "mc": [0, 10, 14], "mc_acq": 0, "mc_obj": 0, "mc_point": 0, "mc_reduct": 0, "mc_sampl": [0, 14], "mcacquisitionfunct": 0, "mcacquisitionobject": [0, 4, 14], "mcmc": [3, 7, 9], "mcmc_dim": 9, "mcmc_sampl": 7, "mcmultioutputobject": [0, 14], "mcobject": 0, "mcsampler": [0, 7, 10, 14], "mcsamplermixin": 0, "me": 0, "mean": [0, 1, 4, 7, 8, 9, 10, 11, 12, 14], "mean_cost": 0, "mean_modul": 7, "mean_transform": 9, "meant": 7, "measur": [1, 7, 12, 14], "mechan": 12, "mec\u00e1nica": 12, "median": 7, "median_lengthscal": 7, "memori": [0, 1, 3, 8, 14], "merch": 12, "merchan": 0, "merchan2019": 0, "merg": 14, "mesmo": 0, "messag": 8, "metadata": 14, "method": [0, 4, 8, 9, 10, 12, 13, 14], "methodnam": 14, "methodologi": [0, 14], "metric": [0, 14], "metric_decomposit": 14, "mezura": 12, "mf": 0, "mf_gibbon": 0, "mf_me": 0, "mf_qgibbon": 0, "michael": 12, "michalewicz": 12, "might": [0, 14], "million": 7, "mimic": 10, "min": [0, 7, 8, 14], "min_cost": 0, "min_inferred_noise_level": 7, "min_rang": 7, "min_std": 7, "min_stddv": 7, "min_stdv": 7, "min_x": 0, "minibatch": 7, "minim": [0, 4, 7, 8, 12, 14], "minima": 12, "minimize_with_timeout": 8, "minimum": [0, 4, 7, 8, 12, 14], "mininimum": 12, "miss": [7, 8, 10, 14], "missing_kei": [7, 14], "mitig": 0, "mix": [0, 14], "mixedsingletaskgp": 7, "mixin": [0, 7, 10, 14], "mixtur": [0, 9, 12], "mixture_covariance_matrix": 9, "mixture_mean": 9, "mixture_vari": 9, "mll": [1, 3, 7, 8], "mll_cl": 1, "mlr": 7, "mm": 14, "mo": 7, "mock": 14, "mock_optim": 13, "mock_optimize_context_manag": 13, "mockacquisitionfunct": 14, "mockmodel": 14, "mockposterior": 14, "mod_batch_shap": 7, "mode": [0, 1, 3, 4, 7, 8, 14], "model": [0, 1, 2, 3, 4, 5, 9, 10, 11, 12, 14], "model1": 7, "model2": 7, "model_1": 9, "model_2": 9, "model_batch_shap": [7, 14], "model_cl": 1, "model_closur": 8, "model_dict": 0, "model_init_kwarg": 1, "model_list": 7, "model_list_gp_regress": 7, "model_list_to_batch": 7, "model_util": 8, "modeldict": [0, 7], "modelfittingerror": 2, "modellist": [0, 7, 9, 10, 14], "modellistgp": [0, 4, 7, 14], "modellistgpytorchmodel": 7, "modelthatcanfantas": 7, "moder": [0, 7, 14], "modif": [7, 8], "modifi": [0, 7, 12, 14], "modul": [0, 4, 5, 8, 9, 10, 12, 14], "module_rollback_ctx": 14, "moduledict": 7, "moham": 0, "moment": [0, 14], "momf": 0, "momf_val": 0, "momfbranincurrin": 12, "momfpark": 12, "monoton": [0, 14], "mont": [4, 12, 14], "monte_carlo": 0, "more": [0, 4, 7, 8, 10, 12, 14], "moreov": 7, "moriconi": 0, "morri": 12, "moss": [0, 7], "moss2021gibbon": 0, "moss2023ipa": 7, "most": [0, 7, 8, 12, 14], "mostowski": 10, "move": [7, 8, 14], "mp": 4, "msu": [12, 14], "mt_mvn": 14, "mtgp": 9, "mtmvn": 14, "mtsaas_gp": 7, "mu": [0, 7, 9, 10], "mu_": 7, "mu_k": 0, "mu_ln": 7, "mu_ln_": 7, "mu_n": 7, "mu_n_": 7, "mu_p": 12, "mu_x": 12, "much": [0, 14], "mul": 14, "muller": 10, "multi": [1, 8, 9], "multi_fidel": [0, 12], "multi_obj": 0, "multi_object": [0, 12, 14], "multi_objective_multi_fidel": 12, "multi_output_risk_measur": 0, "multi_step_lookahead": 0, "multicriteria": 12, "multifidel": 0, "multilist": 14, "multimodelacquisitionfunct": 0, "multinomi": [4, 14], "multiobject": [7, 12, 14], "multiobjectiveanalyticacquisitionfunct": 0, "multiobjectivemcacquisitionfunct": 0, "multiobjectivetestproblem": 12, "multiobjectivetestproblemtestcasemixin": 14, "multioutput": [7, 10], "multioutput_to_batch_mode_transform": 7, "multioutputexpect": 0, "multioutputmcacquisitionobject": 0, "multioutputriskmeasuremcobject": 0, "multioutputworstcas": 0, "multipl": [0, 7, 8, 12, 14], "multipledispatch": 14, "multipli": [0, 7, 10, 14], "multistart": [0, 8], "multitask": 14, "multitaskdataset": [7, 14], "multitaskgaussianlikelihood": 7, "multitaskgp": [4, 7, 14], "multitaskgpposterior": [7, 9], "multitaskgpytorchmodel": 7, "multitaskmultivariatenorm": [7, 9, 14], "multitasksaaspyromodel": 7, "multivari": [0, 7, 9, 10], "multivariant": 14, "multivariatenorm": [7, 9, 14], "multivariatenormalqmcengin": 10, "must": [0, 1, 4, 7, 8, 10, 12, 14], "muthen": 14, "muthen1990mo": 14, "mutual": 0, "mv": 0, "mvar": [0, 14], "mve": 0, "mvn": [7, 9, 14], "mvnxpb": [0, 14], "mvnxpbstate": 14, "mw": 12, "mw7": 12, "mymodul": 14, "m\u00e4chler": 14, "n": [0, 1, 4, 7, 8, 9, 10, 12, 14], "n0": 14, "n_0": 0, "n_box_decomposit": 14, "n_burnin": [8, 14], "n_c": 12, "n_con": 14, "n_constraint": 14, "n_context": 7, "n_discrete_point": 8, "n_eq": 14, "n_eq_con": 14, "n_eval": 14, "n_f": 7, "n_i": 7, "n_ineq": 14, "n_ineq_con": 14, "n_k": 0, "n_nz": 0, "n_p": 7, "n_pareto": [0, 14], "n_pareto_i": 14, "n_sampl": 14, "n_thin": [8, 14], "n_train": [0, 9], "n_w": [0, 7], "n_window": 8, "nadir": [12, 14], "naiv": 14, "name": [0, 2, 7, 8, 14], "name_filt": [8, 14], "named_paramet": 8, "namedtupl": [1, 8, 14], "nan": [0, 2, 7, 8, 11, 14], "nan_polici": 14, "nando": 7, "nardi": [0, 7], "narrow_gaussian": 0, "nativ": 8, "ndarrai": [2, 8, 14], "ndarrayoptimizationclosur": 8, "ndtr": 14, "nearbi": 8, "nearest": [8, 12, 14], "necessari": [0, 7, 8, 9, 14], "necessarili": 0, "need": [0, 7, 8, 9, 10, 14], "neg": [0, 7, 8, 12, 14], "negat": [0, 8, 12, 14], "negative_log_gradient_sum": 7, "negative_log_hessian_sum": 7, "nehvi": [0, 14], "nei": 0, "neighbor": 8, "neighbour": 8, "neil": 7, "network": 7, "neural": [0, 7, 10, 14], "neurip": [0, 7, 12], "neurocomput": 0, "never": 14, "new": [0, 1, 7, 8, 12, 14], "new_batch_shap": 7, "new_i": 7, "new_indic": 7, "new_pareto_i": 14, "new_posterior": 9, "new_valu": 7, "new_x": [7, 14], "next": [0, 14], "nicolo": 7, "nip": 0, "nn": [0, 4, 7, 10, 14], "nnz_approx": 0, "no_grad": 8, "node": [7, 14], "nois": [0, 1, 4, 7, 8, 9, 12, 14], "noise_free_model": 7, "noise_std": 12, "noiseless": 0, "noisi": [0, 7, 12, 14], "noisyexpectedhypervolumemixin": [0, 14], "noisyexpectedimprov": 0, "noisyinputentropysearch": 12, "nojima": 14, "nomin": 12, "non": [0, 7, 8], "non_domin": 14, "nondominatedpartit": [0, 14], "none": [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14], "nonlinear": [4, 7, 8, 12], "nonlinear_constraint_is_feas": 8, "nonlinear_inequality_constraint": [4, 8], "nonmonoton": 12, "nonmyop": 0, "nonneg": [7, 8], "noreturn": [4, 7], "norm": [0, 14], "norm_to_lognorm": 7, "norm_to_lognorm_mean": 7, "norm_to_lognorm_vari": 7, "normal": [0, 1, 7, 8, 9, 10, 11], "normalize_dense_linear_constraint": 14, "normalize_indic": 14, "normalize_sparse_linear_constraint": 14, "normalize_tf": 7, "normalmcsampl": 10, "normalqmcengin": 10, "notabl": 14, "note": [0, 1, 3, 4, 7, 8, 9, 10, 12, 14], "notebook": 7, "notic": 14, "novak": 7, "now": [0, 7], "np": 10, "npt": 8, "nsample_featur": 14, "nsample_outcom": 14, "nth": 3, "nu": [0, 7], "nugget": 14, "nullcontext": 8, "nullop": 8, "num": 7, "num_categori": 14, "num_cel": [0, 14], "num_chain": 14, "num_choic": 8, "num_constraint": [0, 12], "num_data": 7, "num_dimens": 7, "num_fantasi": [0, 8], "num_induc": 7, "num_inner_restart": 8, "num_input": 10, "num_latent_dim": 7, "num_mc_point": 0, "num_mcmc_sampl": 7, "num_model": [0, 14], "num_mv_sampl": 0, "num_neighbor": 8, "num_object": [0, 12], "num_optima": [0, 14], "num_outcom": [0, 14], "num_output": [7, 9, 10, 14], "num_pareto": [0, 8], "num_pareto_point": 0, "num_pareto_sampl": 0, "num_point": [0, 8], "num_pseudo_point": 8, "num_restart": [0, 4, 8, 14], "num_rff_featur": 14, "num_sampl": [0, 3, 4, 7, 14], "num_spray_point": 8, "num_step": [8, 14], "num_trace_ob": 0, "num_trace_observ": 0, "num_y_sampl": 0, "number": [0, 1, 3, 4, 7, 8, 9, 10, 12, 14], "numer": [0, 2, 7, 8, 10, 14], "numericswarn": 2, "nummer": 7, "numpi": [2, 4, 14], "numpy_util": 8, "nut": [3, 7], "o": [7, 8, 9, 12, 14], "oak": 7, "ober": 7, "obermay": 0, "obersev": 7, "obj": [0, 7, 14], "object": [3, 4, 7, 8, 9, 10, 11], "objective_index": 0, "objective_threshold": 0, "observ": [0, 1, 4, 7, 8, 9, 11, 12, 14], "observation_nois": [1, 4, 7, 14], "observed_i": 1, "observed_noise_model": 7, "observed_yvar": 1, "obtain": [0, 3, 7, 8, 9, 12, 14], "occasion": 0, "occur": [0, 14], "odd": 14, "off": [0, 8, 14], "offer": 14, "offset": [0, 7, 9, 10], "offset_constraint": 7, "offset_prior": 7, "often": [0, 7, 8], "omit": [0, 3, 7, 8, 10, 14], "onc": [0, 14], "one": [0, 3, 4, 7, 8, 9, 10, 12, 13, 14], "one_hot_bound": 7, "onehotargmaxst": 14, "onehottonumer": 7, "ones": [0, 7, 14], "ones_lik": 7, "onesampleposteriordrawmodel": 0, "oneshotacquisitionfunct": 0, "onli": [0, 1, 7, 8, 9, 10, 14], "onlin": 7, "onto": 0, "oper": [0, 7, 8, 14], "oppos": 0, "opt": 7, "opt_input": 8, "optim": [0, 2, 3, 4, 5, 9, 10, 12, 13, 14], "optimal_input": 0, "optimal_output": 0, "optimal_sobol_indici": 12, "optimal_valu": 12, "optimi": 14, "optimis": 0, "optimist": 0, "optimizationgradienterror": 2, "optimizationresult": 8, "optimizationstatu": 8, "optimizationtimeouterror": 2, "optimizationwarn": [2, 8], "optimize_acqf": [0, 4, 8, 10, 14], "optimize_acqf_cycl": 8, "optimize_acqf_discret": 8, "optimize_acqf_discrete_local_search": 8, "optimize_acqf_homotopi": 8, "optimize_acqf_list": 8, "optimize_acqf_mix": [7, 8], "optimize_acqf_mixed_altern": 8, "optimize_homotopi": 8, "optimize_mix": 8, "optimize_object": 0, "optimize_objective_kwarg": 0, "optimize_posterior_sampl": 14, "optimizeacqfinput": 8, "optimizer_kwarg": [0, 3], "optimizer_opt": 0, "optimizeresult": 8, "optimum": 0, "optimzi": [2, 8], "option": [0, 4, 7, 8, 9, 10, 12, 14], "ord": 14, "order": [0, 12, 14], "ordereddict": 14, "ordin": 7, "org": [0, 4, 7], "origin": [0, 7, 8, 9, 14], "original_batch_shap": 7, "orthogon": 7, "orthogonal_additive_kernel": 7, "orthogonaladditivekernel": 7, "osborn": [0, 12, 14], "osi": 12, "osyczka": 12, "oszycka1995": 12, "other": [0, 8, 9, 11, 14], "otherwis": [0, 3, 7, 8, 10, 12, 14], "ottoni": 12, "ouf": 12, "ought": 7, "our": [0, 7], "out": [0, 2, 7, 8, 10, 14], "outcom": [0, 1, 8, 9, 10, 12, 14], "outcomd": 0, "outcome_constraint": 14, "outcome_model": 0, "outcome_nam": 14, "outcome_names_per_task": 14, "outcome_transform": [0, 1, 7, 10, 14], "outcometransform": [7, 10], "outcomeuntransform": 10, "outer": [0, 8], "output": [7, 8, 9, 10, 12, 14], "output_indic": [7, 14], "output_shap": [7, 9, 14], "output_task": 7, "output_transform": 10, "outputscal": [7, 10], "outputscaletransform": 10, "outsid": 7, "over": [0, 4, 7, 8, 9, 10, 12, 14], "overal": [7, 14], "overhead": 14, "overrid": [0, 7, 14], "overridden": [0, 7, 10, 14], "overwrit": [9, 14], "overwritten": [7, 10], "owen": 14, "own": [7, 13, 14], "ox": 7, "p": [0, 7, 8, 10, 12, 14], "p1": 12, "p2": 12, "p_1": 9, "p_12": 9, "p_1j": 12, "p_2": 9, "p_ij": 12, "packag": [0, 7, 8, 14], "pad": 0, "pad_to_n_w": 0, "page": [5, 10, 14], "pages2018numprob": 10, "pai": 14, "pair": [0, 7, 12, 14], "pairwis": 0, "pairwise_gp": 7, "pairwise_sampl": 10, "pairwisebayesianactivelearningbydisagr": 0, "pairwisegp": [0, 7, 10], "pairwiseiidnormalsampl": 10, "pairwiselaplacemarginalloglikelihood": 7, "pairwiselikelihood": 7, "pairwiselogitlikelihood": 7, "pairwisemcposteriorvari": 0, "pairwisemcsampl": 10, "pairwiseprobitlikelihood": 7, "pairwisesobolqmcnormalsampl": 10, "palmerin": 7, "paper": [0, 7, 12], "paquet": 14, "parallel": [0, 12, 14], "param": 7, "param_new": 0, "param_old": 0, "paramet": [0, 1, 2, 3, 4, 6, 9, 10, 11, 12, 13, 14], "parameter": 0, "parameter_1": 7, "parameter_2": 7, "parameter_constraint": 8, "parameter_decomposit": 14, "parameter_rollback_ctx": 14, "paranthes": 14, "parego": 14, "parent": 7, "pareto": [0, 8, 12], "pareto_front": 0, "pareto_i": 14, "pareto_set": 0, "pareto_y_sort": 14, "park": 12, "parmet": 7, "parsimoni": 7, "part": [0, 7, 12, 14], "partial": 0, "particular": [0, 7, 8, 11, 14], "particularli": [0, 14], "partit": [0, 7], "partition_spac": 14, "pass": [0, 1, 3, 4, 7, 8, 9, 10, 14], "past": 8, "path": [0, 7, 14], "pathdict": 10, "pathlist": 10, "pathwis": [0, 14], "pathwisethompsonsampl": 0, "pattern": [0, 8], "pbo": 0, "pdf": [0, 7, 14], "pe": 0, "peculiar": 12, "penalizedacquisitionfunct": 0, "penalizedmcobject": 0, "penalti": 0, "penalty_func": 0, "penalty_object": 0, "pend": [0, 14], "penicillin": 12, "penicillin_vector": 12, "pennington": 7, "penultim": 10, "per": [0, 7, 8, 9], "percent": 0, "percentag": 8, "percentil": 14, "percentile_of_scor": 14, "percentileofscor": 14, "perform": [0, 1, 4, 7, 8, 10, 11, 12, 14], "perhap": 7, "perm": 14, "permit": 14, "persist": [8, 14], "perspect": [7, 14], "perturb": [0, 7, 8, 12], "perturbation_set": [0, 7], "pessimist": 0, "pg": 10, "phase": 0, "phi": [10, 14], "pi": [0, 12], "pibo": 0, "picheni": 7, "pick": [4, 8], "piec": [7, 10, 14], "piecewis": [7, 14], "pii": 0, "piv_chol": 14, "pivot": [7, 14], "pivot_": 14, "pivotedcholeski": 14, "place": [0, 7, 8, 14], "placehold": 0, "plai": 7, "plain": 14, "plan": 0, "plane": 12, "pleas": [0, 7, 12, 14], "plu": [0, 8], "plug": 14, "plug_in": 14, "pm": 0, "pmlr": [7, 12], "point": [0, 4, 8, 9, 10, 11, 12, 14], "pointwis": 8, "polar": 14, "polici": [0, 7], "poloczek": 7, "polynomi": 7, "polyop": 14, "polytop": [8, 14], "polytopesampl": 14, "poor": 0, "pop": 14, "pop_siz": 0, "popul": [7, 14], "posit": [0, 7, 8, 9, 12, 14], "possibl": [0, 7, 8, 10, 14], "possibli": [0, 2, 7, 8, 10], "post": [0, 7, 8], "post_processing_func": [0, 8], "posterior": [0, 1, 4, 5, 7, 8, 14], "posterior_list": 9, "posterior_sampl": [7, 10], "posterior_transform": [0, 4, 7, 14], "posteriorlist": [0, 7, 9, 10], "posteriormean": [0, 8], "posteriormeanmodel": 7, "posteriorstandarddevi": 0, "posteriortransform": [0, 4, 7, 14], "postprocess_mcmc_sampl": 7, "potenti": [0, 2, 7, 12, 14], "pow": 7, "powel": [0, 12], "power": [7, 8, 10, 14], "power_constraint": 7, "power_prior": 7, "pp": [12, 14], "ppl": 3, "practic": [0, 7, 10, 14], "pre": [7, 14], "precis": [0, 14], "precision_matrix": 14, "precomput": 14, "pred": 14, "predecessor": 14, "predefin": 7, "predict": [4, 7, 9, 12, 14], "predictive_entropy_search": 0, "predictive_mean_cach": 7, "preemptiv": 0, "pref_model": 0, "prefer": 7, "preferenti": 0, "prefix": 14, "prekopa": 0, "prekopa2012mvar": 0, "prepared_sampl": 0, "prepend": 10, "preprint": [0, 7, 10, 12], "preprocess": [0, 7, 14], "preprocess_transform": 7, "preprocessing_funct": 0, "present": [0, 4, 7, 14], "preserv": 14, "press": [7, 12], "pressur": 12, "pressurevessel": 12, "prevent": [0, 14], "previou": [0, 7, 8, 14], "previous": [0, 7, 14], "previous_winn": 0, "primari": [0, 13, 14], "principl": 0, "print": [3, 11, 12, 14], "prior": [7, 8], "prior_config": 7, "prior_expon": 0, "prior_guid": 0, "prior_modul": 0, "prior_path": 10, "prior_sampl": 10, "priorguidedacquisitionfunct": 0, "priorit": 14, "prob_perturb": 8, "probabilist": [0, 14], "probabilityofimprov": 0, "probabilti": 0, "probabl": [0, 7, 8, 9, 10], "probit": [0, 7], "probl": 12, "problem": [0, 1, 4, 7, 8, 12, 14], "proc": 14, "procedur": [0, 4], "proceed": [0, 7, 12], "process": [0, 7, 8, 9, 10, 12, 14], "prod_": 12, "prod_i": 0, "produc": [0, 7, 9, 10, 14], "product": [7, 12], "product_i": 0, "program": 14, "programmat": 0, "progress": [3, 14], "project": [0, 8], "project_to_sample_point": 0, "project_to_target_fidel": 0, "projectedacquisitionfunct": 0, "promis": 7, "propag": [0, 11, 14], "propagate_grad": 11, "propat": 11, "proper": 8, "properli": [7, 14], "properti": [0, 7, 8, 9, 10, 12, 14], "proport": [8, 14], "propos": [0, 7, 12, 14], "protocol": 0, "provid": [0, 2, 3, 4, 7, 8, 9, 10, 12, 14], "proxi": 0, "proximal_weight": 0, "proximalacquisitionfunct": 0, "prune": [0, 8, 14], "prune_baselin": [0, 14], "prune_candid": 8, "prune_inferior_point": 0, "prune_inferior_points_multi_object": [0, 14], "prune_toler": 8, "psd_safe_choleski": 7, "pseudorandom": 14, "psi": 0, "pstd": 0, "psychologi": 14, "public": 8, "publish": 10, "pull": 7, "pure": 0, "purpos": [0, 8], "put": [7, 10], "py": [7, 12, 14], "pymoo": [12, 14], "pyro": [3, 7], "pyro_model": 7, "pyromodel": 7, "python": 14, "pytorch": [4, 7, 9, 14], "pytorch_ess": 14, "q": [0, 4, 7, 8, 9, 12, 14], "q_0": 0, "q_1": 0, "q_2": 0, "q_aug": 0, "q_e": 0, "q_i": 0, "q_k": 0, "q_out": 14, "q_reduct": 0, "q_subset_indic": 14, "q_term": 0, "qanalyticprobabilityofimprov": 0, "qbayesianactivelearningbydisagr": 0, "qehvi": 0, "qei": [0, 4, 8], "qei_ff": 0, "qeubo": 0, "qexpectedhypervolumeimprov": 0, "qexpectedimprov": [0, 4, 8, 14], "qexpectedutilityofbestopt": 0, "qgibbon": 0, "qhvkg": 8, "qhypervolumeknowledgegradi": [0, 8], "qjointentropysearch": 0, "qknowledgegradi": [0, 8, 10], "qld": 14, "qlogei": 0, "qlogexpectedhypervolumeimprov": 0, "qlogexpectedimprov": 0, "qlognehvi": 14, "qlognei": 0, "qlognoisyexpectedhypervolumeimprov": 0, "qlognoisyexpectedimprov": 0, "qlognparego": 0, "qlowerboundmaxvalueentropi": 0, "qlowerboundmultiobjectivejointentropysearch": 0, "qlowerboundmultiobjectivemaxvalueentropysearch": 0, "qlowerconfidencebound": 0, "qmaxvalueentropi": 0, "qmc": [0, 8, 14], "qmultifidelityhypervolumeknowledgegradi": 0, "qmultifidelityknowledgegradi": 0, "qmultifidelitylowerboundmaxvalueentropi": 0, "qmultifidelitymaxvalueentropi": 0, "qmultiobjectivemaxvalueentropi": 0, "qmultiobjectivepredictiveentropysearch": 0, "qmultisteplookahead": 0, "qnegintegratedposteriorvari": 0, "qnehvi": [0, 8, 14], "qnei": 0, "qnoisyexpectedhypervolumeimprov": 0, "qnoisyexpectedimprov": [0, 7], "qnparego": 0, "qpi": 0, "qpredictiveentropysearch": 0, "qprobabilityofimprov": 0, "qsimpleregret": 0, "qsr": 0, "quad_deg": 7, "quadrat": 7, "quadratur": [7, 14], "qualiti": 7, "quality_scor": 7, "qualityfunct": 7, "qualnam": 8, "quantifi": 0, "quantil": [0, 9], "quasi": [0, 10, 14], "qucb": [0, 8], "queri": [0, 14], "quickli": 8, "quit": 0, "qupperconfidencebound": [0, 8], "r": [0, 1, 7, 8, 10, 12, 14], "r1": 12, "r2": 12, "radial": 8, "rahimi": 10, "rahimi2007random": 10, "rais": [0, 2, 7, 8, 13, 14], "raise_on_fail": 7, "raise_on_viol": 8, "rand": [0, 1, 4, 7, 8, 9, 14], "rand_lik": 1, "randint": 7, "randn": [7, 14], "random": [0, 7, 8, 9, 10, 12, 14], "random_search_optim": 0, "randomfourierfeatur": 14, "randomli": [0, 12], "randperm": 14, "rang": [7, 8, 9, 12, 14], "ranjan": 12, "rank": [0, 7], "rankingdataset": [7, 14], "rare": 8, "rasmussen": 7, "rastrigin": 12, "rate": [0, 7, 12], "rather": [0, 4, 7, 8, 9, 14], "raw": [0, 7, 8, 14], "raw_acqf": 0, "raw_inner_sampl": 8, "raw_sampl": [0, 4, 8, 14], "rbf": 7, "rbfkernel": 7, "re": [7, 14], "reach": 8, "real": [0, 12], "realiz": 0, "reason": [0, 7, 11], "receiv": 8, "recent": 0, "recevi": 0, "recht": 10, "recip": [7, 10, 14], "recommend": [0, 7, 14], "recomput": [0, 14], "rectangl": [0, 14], "rectangular": 14, "recurs": 7, "redraw": 0, "reduc": [0, 8, 12, 14], "ref": 14, "ref_point": [0, 8, 14], "refer": [0, 7, 8, 10, 12, 14], "reference_tensor": 0, "refit": 7, "regardless": [0, 9, 14], "regener": 14, "regi": 8, "region": [0, 7, 12], "regist": [0, 7, 8, 10, 14], "registri": 0, "regress": [0, 14], "regret": 0, "regular": [0, 7, 8, 14], "regularization_paramet": 0, "regularli": 12, "reinforc": 7, "reiniti": [7, 8], "reinsert": 14, "reject": 0, "rel": [0, 7, 8, 14], "rel_tol": 8, "relat": [0, 2, 7], "relationship": [7, 12], "relax": [0, 8, 14], "releas": 14, "relev": [0, 8, 14], "reli": [0, 14], "relu": [0, 7, 14], "remain": [8, 14], "remedi": 14, "rememb": 14, "remov": [0, 7, 8, 14], "reparameter": [0, 14], "repeat": [0, 8, 10, 14], "repeat_to_match_aug_dim": 0, "repeatedli": 14, "replac": [0, 2, 4, 8, 10, 14], "replic": [0, 10], "report": 14, "repres": [0, 2, 4, 7, 8, 9, 10, 12, 14], "represent": [0, 7, 8, 10, 14], "request": [0, 7], "requir": [0, 7, 8, 14], "require_grad": 8, "requires_grad": [8, 14], "rescal": [10, 12], "research": [0, 7, 10, 14], "reserv": 14, "reset": [7, 8, 14], "reshap": [0, 7, 10], "reshape_and_detach": 7, "resolv": 14, "resp": 0, "respect": [0, 4, 7, 8, 9, 12, 14], "respons": [3, 7, 8, 9], "responsibli": 13, "rest": [7, 13], "restart": 8, "restrict": [0, 7], "result": [0, 3, 7, 8, 10, 12, 14], "retain": [0, 3, 8, 14], "retri": 8, "retriev": [0, 14], "retry_on_optimization_warn": 8, "return": [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 12, 13, 14], "return_best_onli": [0, 8], "return_full_tre": [0, 8], "return_numer": 7, "return_transform": [0, 14], "revers": 7, "reversibleinputtransform": 7, "revert": 7, "review": 0, "revisit": 7, "reward": 0, "reweight": 0, "rework": 14, "rezend": 0, "rezende2014reparam": 0, "rff": [0, 14], "rh": [4, 8, 14], "rhokg": 7, "right": [7, 8, 10, 14], "risk": 7, "risk_averse_bo_with_environmental_vari": 7, "risk_averse_bo_with_input_perturb": 7, "risk_measur": [0, 7], "risk_measure_sampl": 7, "riskmeasuremcobject": [0, 7], "rmpfr": 14, "rng": [10, 14], "robust": [0, 12, 14], "robustli": 0, "roll": 14, "rollback": 14, "root": [0, 10, 14], "rosenbrock": 12, "rough": 3, "round": [0, 7, 8, 12], "round_nearest": 12, "round_tf": 7, "roundst": 14, "routin": [3, 4, 8, 14], "row": [7, 8, 14], "rsampl": [7, 9, 10, 14], "rsample_from_base_sampl": [9, 10, 14], "rtol": [7, 14], "rubric": 7, "rule": [7, 9, 10, 14], "run": [0, 7, 8, 10, 14], "runtest": 14, "runtim": [2, 3, 8, 14], "runtimeerror": [2, 14], "ryan": 12, "ryoji": 12, "s0925231219308525": 0, "s10898": 14, "s_i": 12, "saa": [7, 10], "saas_gp": 7, "saasfullybayesianmultitaskgp": [3, 7], "saasfullybayesiansingletaskgp": [0, 3, 7, 14], "saaspyromodel": 7, "sac": 7, "sacgp": 7, "sackernel": 7, "safe_math": 14, "sai": 7, "said": 14, "sake": 0, "same": [0, 7, 8, 9, 12, 14], "sampl": [2, 3, 5, 7, 8, 12], "sample_all_prior": 8, "sample_around_best": 8, "sample_cached_choleski": 14, "sample_dim": 10, "sample_feasible_point": 8, "sample_hyperspher": 14, "sample_latent_featur": 7, "sample_lengthscal": 7, "sample_mean": 7, "sample_multipli": 0, "sample_nois": 7, "sample_optimal_point": 0, "sample_outputscal": 7, "sample_pareto_fronti": 0, "sample_perturbed_subset_dim": 8, "sample_pf": 0, "sample_point": 0, "sample_points_around_best": 8, "sample_polytop": 14, "sample_q_batches_from_polytop": 8, "sample_reduct": 0, "sample_s": 0, "sample_shap": [0, 9, 10, 14], "sample_simplex": 14, "sample_task_lengthscal": 7, "sample_transform": [9, 14], "sample_truncated_normal_perturb": 8, "sample_valu": 10, "sampled_x": 4, "samplepath": 10, "sampler": [0, 3, 7, 8, 9], "samplereducingacquisitionfunct": 0, "samplereducingmcacquisitionfunct": 0, "samplereductionprotocol": 0, "samplerlist": 10, "samplingstrategi": 4, "samplingwarn": 2, "saniti": 14, "santo": 12, "satisfi": [0, 4, 8, 9, 10, 14], "saul": 7, "save": 14, "scalabl": [0, 7, 12], "scalar": [4, 7, 8, 9], "scalarization_weight": 0, "scalarize_posterior": 9, "scalarize_posterior_gpytorch": 9, "scalarizedposteriormean": 0, "scalarizedposteriortransform": [0, 14], "scale": [0, 7, 10, 14], "scale_max_ref_point": 14, "scale_tril": 14, "scalekernel": [7, 10], "schedul": 8, "scheme": [0, 14], "schneider": 10, "schoenholz": 7, "scienc": [0, 12], "sciencedirect": 0, "scipi": [0, 4, 7, 8, 13, 14], "scipy_bound": 8, "scipy_minim": 8, "scipy_opt": 0, "score": [7, 14], "scrambl": 14, "scratch": 7, "script": 7, "scriptmodul": [0, 4, 7, 10, 14], "sd_prior": 7, "search": [4, 5, 7, 8, 12, 14], "sebastian": 7, "second": [0, 2, 4, 7, 8, 10, 12, 14], "second_ord": 7, "section": [12, 14], "see": [0, 4, 7, 8, 10, 12, 14], "seed": [0, 8, 10, 14], "seed_inn": 0, "seek": 0, "seem": 0, "select": [0, 4, 7, 8, 12, 14], "select_pivot": 14, "self": [0, 7, 9, 14], "semant": [4, 7], "semi": 0, "sens": 0, "sensit": [0, 14], "sensitivity_analysi": 12, "seo": 0, "seo2014activedata": 0, "separ": [7, 12, 14], "separate_mtmvn": 14, "seper": 4, "sequenc": [0, 3, 7, 8, 10, 14], "sequenti": [0, 7, 8, 14], "serv": [0, 14], "set": [0, 2, 3, 4, 5, 7, 8, 9, 10, 12, 14], "set_attribut": 7, "set_baseline_i": 0, "set_input": 7, "set_stat": 8, "set_tensors_from_ndarray_1d": 8, "set_train_data": 7, "set_x_pend": [0, 14], "setattr": 7, "setup": 14, "seven": 12, "seventh": 7, "sever": [7, 12, 14], "sfu": 12, "shafei": 0, "shallow": 14, "shape": [0, 1, 4, 6, 7, 8, 9, 10, 14], "shape_to_str": 6, "shapex": 8, "share": [0, 4, 7, 8, 10, 14], "sharp": 0, "sharper": 14, "shekel": 12, "shift": [0, 14], "shiga": 0, "shoemak": 8, "shot": 8, "should": [0, 2, 7, 8, 9, 10, 12, 14], "should_stop": 8, "shown": 8, "shrinkag": 7, "siam": 0, "sibl": 10, "side": [8, 12, 14], "sigma": [0, 7, 8, 9, 10, 14], "sigma_k": 0, "sigma_sq": 14, "sigmoid": [0, 7, 14], "sign": 14, "signal": 14, "signatur": [0, 7, 8, 14], "significantli": [0, 14], "silent": [7, 10, 14], "silva": 12, "sim": 10, "similar": [0, 4, 7, 8, 12, 14], "similarli": 14, "simon": 14, "simpl": [0, 7, 12, 14], "simplegpytorchmodel": 14, "simplex": [0, 14], "simpli": [7, 8, 10, 14], "simplic": 0, "simplifi": [7, 12], "simul": [0, 7, 12], "simulaten": 4, "simultan": [0, 12], "sin": [0, 7, 12], "sinc": [0, 7, 8, 10, 14], "sine": 10, "sinecosinetransform": 10, "sing": 7, "singl": [0, 4, 7, 8, 9, 10, 12, 14], "single_q_method": 14, "singletaskgp": [0, 1, 7, 14], "singletaskmultifidelitygp": 7, "singletaskvariationalgp": 7, "sinusoid": 12, "situat": 14, "six": 12, "sixhumpcamel": 12, "size": [0, 1, 4, 6, 7, 8, 9, 10, 14], "skip": 14, "skip_expand": 7, "skip_task_features_in_dataset": 14, "slack": [12, 14], "slicecontain": 14, "slow": 14, "slsqp": [4, 8], "small": [0, 1, 7, 8, 14], "smaller": [0, 7, 8, 14], "smallest": 0, "smart": 8, "smooth": [0, 7, 12, 14], "smooth_amax": 14, "smooth_amin": 14, "smoothed_constraint_ind": 0, "smoother": 12, "smoothli": [0, 14], "snoek": [7, 12], "so": [0, 4, 7, 8, 9, 12, 14], "sobol": [8, 10, 12, 14], "sobolengin": 10, "sobolqmcnormalsampl": [0, 8, 10], "soft": [0, 12], "softmax": [4, 8], "softplu": [0, 14], "softwar": 7, "sole": [7, 8, 14], "solut": [0, 4, 8, 12], "solv": [0, 7, 8, 12, 14], "solver": [4, 8, 14], "some": [0, 7, 14], "someth": 7, "sometim": [0, 3], "somewhat": [0, 12], "sort": [7, 14], "sort_by_dimens": 14, "sourc": [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14], "souza": 0, "space": [0, 4, 8, 9, 12, 14], "spars": [7, 14], "sparse_to_dense_constraint": 14, "sparsiti": 7, "spatial": 7, "special": [0, 7, 14], "specif": [0, 7, 8, 12], "specifi": [0, 3, 7, 8, 9, 10, 12, 14], "speed": [7, 12, 13, 14], "speedreduc": 12, "sphere": 14, "split": [0, 7], "sprai": 8, "springer": [10, 12, 14], "sqrt": [0, 7, 12], "squar": [0, 10, 12, 14], "squeez": 7, "srn": 12, "ssurjano": 12, "stabil": [7, 14], "stabl": 14, "stack": [7, 14], "stage": [0, 8, 14], "standard": [0, 1, 2, 4, 7, 8, 9, 10, 11, 12, 14], "standard_normal_log_hazard": 14, "standardize_mo": 14, "standardize_model": 14, "standardized_rang": 14, "start": [0, 4, 7, 8, 14], "stat": [7, 14], "state": [0, 4, 7, 8, 10, 11, 12, 14], "state_dict": [7, 14], "statement": [11, 14], "static": [0, 7, 14], "stationari": 10, "statist": [0, 7, 12], "statu": 8, "std": [0, 7, 8], "std_cont_perturb": 8, "std_nois": 0, "std_normal_cdf": 7, "stderr": 11, "steadi": 12, "step": [3, 7, 8, 9, 14], "step_limit": 8, "still": [0, 7], "stipul": 14, "stochast": [0, 7], "stochastic_sampl": 10, "stochasticsampl": [0, 10], "stoll": 0, "stop": [4, 7, 13], "stopping_criterion": 8, "stoppingcriterion": 8, "store": [0, 7, 14], "str": [0, 1, 2, 3, 4, 6, 7, 8, 10, 14], "straight": [7, 14], "strategi": [0, 2, 7, 8, 14], "stratifi": 7, "stream": 7, "strict": [7, 14], "strictli": 14, "string": [0, 8, 10, 12, 14], "strong": [0, 7, 12], "stronger": 7, "strongli": 0, "structur": [7, 9, 12, 14], "styblinski": 12, "styblinskitang": 12, "style": [7, 8], "sub": [0, 7, 14], "sub_modul": 7, "subclass": [0, 7, 10, 14], "subject": [0, 12, 14], "submit": [0, 14], "submodel": [4, 7], "subroutin": 3, "subset": [0, 7, 8, 10, 14], "subset_model": 7, "subset_output": 7, "subset_sigma": 8, "subset_transform": 7, "subsetindexcachingmixin": [0, 14], "subspac": 7, "substanti": 0, "substitut": 7, "substrat": 12, "subtre": 0, "succeed": 3, "success": [0, 8], "successor": 14, "suffici": 7, "suffix": 7, "suggest": [0, 7, 14], "suit": [7, 12], "suitabl": [7, 10], "sum": [0, 7, 8, 12, 14], "sum_": [0, 7, 12], "sum_i": [0, 4, 8, 12, 14], "sum_j": [7, 8], "summar": 8, "super": [7, 14], "superviseddataset": [0, 7, 14], "supplementari": 0, "support": [0, 1, 7, 8, 9, 10, 12, 14], "supports_cache_root": 0, "suppress": 14, "suppress_input_warn": 14, "surfac": 0, "surjanov": 12, "surrog": [0, 8], "sutherland": 10, "sutherland2015error": 10, "svgp": 7, "swap": [7, 14], "swap_along_dim_": 14, "swarm": 12, "sweep": 14, "swerski": 7, "swersky2013mtbo": 7, "switch": 14, "synthetictestfunct": 12, "synthetictestfunctiontestcasemixin": 14, "synthtet": 12, "system": [0, 7, 10, 14], "t": [0, 1, 4, 7, 8, 9, 10, 12, 14], "t1": 12, "t2": 12, "t_1": 12, "t_2": 12, "t_batch_mode_transform": 14, "t_o": 12, "t_v": 12, "tabl": [0, 12], "tacqfargconstructor": 0, "tactic": 13, "tail": [0, 14], "take": [0, 4, 7, 8, 9, 10, 14], "taken": [0, 7, 12, 14], "takeno": 0, "takeno2020mfmv": 0, "takeuchi": 0, "tanab": 12, "tanabe2020": 12, "tang": 12, "tangent": [7, 14], "target": [0, 7, 8, 9, 11, 14], "target_batch_shap": 7, "target_fidel": 0, "target_outcome_nam": 14, "target_point": 0, "target_task_valu": 14, "target_tensor": 0, "task": [0, 1, 4, 7, 8, 9], "task_": 14, "task_covar_modul": 7, "task_covar_prior": 7, "task_featur": [7, 14], "task_feature_index": 14, "task_idc": 7, "task_rank": 7, "task_valu": [7, 14], "tau": [0, 7, 14], "tau_max": 0, "tau_relu": 0, "tausq": 7, "taylor": 14, "technic": 14, "technometr": 12, "temper": 4, "temperatur": [0, 4, 7, 8, 12, 14], "temporari": 7, "temporarili": 14, "tension": 12, "tensioncompressionstr": 12, "tensor": [0, 1, 2, 3, 4, 7, 8, 9, 10, 12, 14], "tensorbas": 8, "tensorcheckpoint": 14, "tensorflow": 14, "tensoro": 14, "tensortransform": 10, "terenin": 10, "term": [0, 7, 14], "termin": [0, 2, 8], "termniat": 9, "tessel": 14, "tesselampl": 14, "test": [0, 1, 7, 8, 9, 13], "test_attribut": 14, "test_batch_shap": [7, 14], "test_evaluate_slack": 14, "test_forward_and_evaluate_tru": 14, "test_funct": 5, "test_help": 14, "test_i": 1, "test_jitt": 0, "test_max_hv": 14, "test_mean": 9, "test_nois": 9, "test_num_constraint": 14, "test_optim": 14, "test_optimal_valu": 14, "test_ref_point": 14, "test_train_covar": 9, "test_util": 5, "test_x": [0, 1, 7, 9, 10], "test_yvar": 1, "testcas": 14, "tf": 7, "tf1": 7, "tf2": 7, "tf3": 7, "th": [0, 4, 7, 8, 14], "than": [0, 4, 7, 8, 9, 11, 12, 14], "thee": 0, "thei": [0, 7, 8, 14], "them": [0, 7, 9, 10, 14], "themselv": 7, "theoret": 0, "theori": 0, "therefor": 0, "thereof": 14, "theta": [7, 12], "theta_0": 12, "theta_i": 12, "thi": [0, 1, 3, 4, 7, 8, 9, 10, 11, 12, 14], "thiel": 12, "thin": [3, 8, 14], "thing": 7, "thinn": 14, "third": [7, 12], "thirti": 7, "thirtieth": 12, "thompson_sampl": 0, "thoroughli": 8, "those": [0, 7, 14], "though": [0, 7], "thought": 0, "three": [7, 8, 12], "threehumpcamel": 12, "threshold": [0, 8, 14], "thresold": 14, "through": [0, 3, 7, 8, 11, 12, 14], "throughput": 7, "thu": [0, 8, 14], "ti": 14, "tie": 14, "tiger": 7, "tight": 14, "tighter": 14, "time": [0, 2, 7, 8, 9, 12, 14], "timeout": 4, "timeout_sec": [4, 8], "tini": 14, "tinputtransform": 10, "tkwarg": [0, 7, 14], "todo": [0, 7, 8, 12, 14], "togeth": [7, 10], "toggl": [0, 7, 14], "tol": [8, 9], "toler": [7, 8, 11], "too": [0, 7], "top": [7, 9, 10], "topic": 0, "torch": [0, 1, 4, 7, 10, 12], "torch_minim": 8, "torchattr": 8, "torchposterior": [9, 10, 14], "toscano": 7, "total": [0, 2, 7, 8, 14], "tournament": 12, "toutputtransform": 10, "toward": [0, 14], "toyrobust": 12, "trace": 0, "track": [0, 8, 14], "trade": 0, "tradeoff": 0, "trail": [0, 7], "train": [0, 1, 3, 7, 8, 9, 11, 14], "train_comp": 0, "train_diff": 9, "train_embed": 7, "train_full_covar": 7, "train_i": [0, 1, 3, 7, 11, 14], "train_input": [0, 7, 8], "train_nois": 9, "train_obj": 0, "train_target": 9, "train_train_covar": 9, "train_x": [0, 1, 3, 7, 11, 14], "train_x1": 7, "train_x2": 7, "train_y1": 7, "train_y2": 7, "train_y_var": 11, "train_yvar": [0, 1, 7, 14], "training_data": [0, 7], "transact": [0, 12, 14], "transfer": 7, "transfomr": 7, "transform": [0, 1, 8, 10], "transform_constraint": 8, "transform_input": [7, 14], "transform_inter_point_constraint": 8, "transform_intra_point_constraint": 8, "transform_on_ev": 7, "transform_on_fantas": 7, "transform_on_train": 7, "transformed_weight": 0, "transformedmodulemixin": 10, "transformedposterior": [7, 9], "translat": 7, "travers": 14, "treat": [0, 7, 14], "treatment": 0, "tree": [0, 3, 8], "tri": [0, 8, 14], "triangl": 14, "triangul": 14, "triangular": 14, "trick": [0, 14], "trikalino": 14, "trikalinos2014polytop": 14, "tril": 14, "tring": 14, "trinh": 14, "trinh2015bivari": [0, 14], "trip": [0, 8], "tripl": 14, "trivari": 14, "trivial": 0, "true": [0, 4, 7, 8, 10, 11, 12, 13, 14], "trunc": 14, "truncat": [7, 8], "truncated_multivariate_norm": 14, "truncatedmultivariatenorm": 14, "trust": 0, "try": [0, 4], "tsukada": 0, "tu": 0, "tu2022": 0, "tu2022joint": 0, "tunabl": 7, "tune": 7, "tupl": [0, 1, 3, 4, 7, 8, 9, 10, 12, 14], "turbo": 12, "turn": [3, 7], "tutori": 7, "twenti": 12, "twice": 14, "two": [0, 4, 7, 8, 9, 12, 14], "type": [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 12, 13], "type_bypassing_encod": 14, "typeddict": 14, "typic": [0, 7, 8, 9, 12, 14], "u": [0, 3, 7, 14], "u_": 0, "u_1": 0, "u_2": 0, "u_3": 0, "u_idx": 14, "u_n": 0, "uai": 12, "ucb": [0, 4], "uk": 7, "un": [0, 7, 14], "unalt": 0, "unbound": 14, "unbserver": 7, "uncertainti": [0, 7, 12], "unclaim": 14, "unconsolid": 7, "unconsolidated_util": 7, "unconstrain": 0, "uncorrel": 7, "undefin": 14, "under": [0, 4, 8, 10, 12, 14], "underflow": 14, "underli": [0, 4, 7, 10, 13, 14], "undesir": [0, 7], "unexpect": [0, 3], "unexpected_kei": [7, 14], "unexpectedli": 7, "unified_skew_norm": 14, "unifiedskewnorm": 14, "uniform": [4, 7, 14], "uniformli": [12, 14], "uninform": 7, "union": [0, 8], "uniqu": [7, 8, 12, 14], "unit": [0, 4, 7, 11, 12, 14], "unitqualityfunct": 7, "univers": 14, "universitext": 10, "unknown": [0, 7, 8, 12], "unless": [0, 7, 9, 12, 14], "unlik": [0, 7, 14], "unnorm": [7, 14], "unnormalize_tf": 7, "unord": 14, "unreach": 7, "unsqueez": 7, "unstabl": 0, "unsuccessfulli": 2, "unsupport": 2, "unsupportederror": 2, "until": [0, 7, 8], "untransform": [7, 10], "untransform_posterior": 7, "unus": [0, 7], "up": [0, 7, 8, 11, 12, 13, 14], "updat": [0, 7, 8], "update_": 14, "update_local_upper_bounds_increment": 14, "update_model": 7, "update_path": 10, "update_strategi": 10, "upon": [0, 8, 14], "upper": [0, 7, 8, 9, 12, 14], "upper_": 0, "upper_bound": [4, 8], "upper_i": 0, "upperconfidencebound": [0, 4], "upstream": 7, "us": [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14], "usa": 12, "usag": [0, 3, 7, 8], "use_count": 0, "use_gumbel": 0, "use_lkj_prior": 7, "use_mean": 0, "use_model_list": 14, "use_posterior_mean": 0, "use_rbf_kernel": 7, "user": [0, 2, 3, 7, 8, 9, 14], "userinputwarn": 2, "usual": [0, 7, 10, 12], "util": [1, 5, 13], "util_diff": 7, "v": [4, 7, 8, 10, 12], "v130": 7, "v21": 7, "v_i": 12, "v_max": 12, "val": 7, "valfunc_argfac": 0, "valfunc_cl": 0, "valid": [0, 1, 7, 8, 11, 14], "validate_arg": 14, "validate_init": 14, "validate_input_sc": [7, 11], "valkenhoef": 14, "valu": [3, 4, 7, 8, 9, 10, 11, 12, 14], "value_funct": 8, "valueerror": [8, 14], "van": [0, 7, 14], "vancouv": 14, "vanilla": 7, "vanish": 7, "var": [0, 7, 11, 14], "vari": [0, 12], "variabl": [0, 7, 9, 10, 14], "varianc": [0, 4, 7, 9, 11, 14], "variance_transform": 9, "variant": [0, 14], "variat": [9, 14], "variational_distribut": 7, "variational_strategi": 7, "variationalelbo": 7, "variationalstrategi": 7, "varieti": 7, "variou": 7, "vector": [0, 7, 8, 9, 10, 12, 14], "vectorof": 7, "vehicl": 12, "vehiclesafeti": 12, "vendor": 14, "verbatim": 8, "verbos": [11, 14], "veri": [0, 1, 7, 14], "verlag": 12, "version": [0, 2, 7, 10, 12, 14], "vertex": 14, "vertic": 14, "vessel": 12, "via": [0, 4, 7, 8, 9, 10], "victor": 7, "vinogradska": 12, "violat": [2, 4, 12], "virginia": 12, "virtual": [0, 2, 12], "visibl": 14, "visual": 14, "vlad": 7, "vol": [12, 14], "volum": [7, 12], "w": [0, 1, 4, 7, 8, 9, 12, 14], "w_1": 12, "w_2": 12, "w_3": 12, "w_4": 12, "w_d": 12, "w_i": [12, 14], "w_j": 12, "w_l": 12, "w_set": 0, "wa": [0, 8, 12, 14], "wai": [0, 7, 14], "wait": 8, "wall": 0, "wallat": 0, "wan": 14, "wang": [0, 12], "wang2017mv": 0, "want": [0, 7, 8, 14], "warm": [0, 14], "warmstart_multistep": 0, "warmup_step": 3, "warn": [0, 1, 7, 14], "warp": 7, "we": [0, 4, 7, 8, 9, 14], "wei": 7, "weigh": 0, "weight": [0, 4, 7, 8, 9, 10, 14], "weighted_object": 0, "weighted_util": 0, "weightedmcmultioutputobject": 0, "weld": 12, "welded_beam": 12, "weldedbeam": 12, "weldedbeamso": 12, "well": [0, 7, 8, 9, 12, 14], "were": [0, 7, 12], "wess": 14, "wether": 0, "what": [0, 7, 8], "when": [0, 2, 3, 4, 7, 8, 9, 10, 11, 14], "whenev": 8, "where": [0, 1, 4, 7, 8, 9, 10, 12, 14], "whether": [0, 3, 7, 8, 12, 14], "which": [0, 2, 4, 7, 8, 9, 10, 12, 14], "while": [0, 4, 7, 8, 9, 10, 12, 14], "whiten": 7, "whole": [0, 7, 8, 14], "whose": [0, 8, 10, 14], "widespread": 12, "width": 7, "wierstra": 0, "wild": 12, "wilk": 7, "william": 7, "wilson": [0, 7, 10], "wilson2017reparam": 0, "wilson2020sampl": 10, "wilson2021pathwis": 10, "window": 8, "winner": 0, "winter": 0, "wise": [0, 7, 8, 14], "with_current_valu": 0, "with_grad": 4, "within": [0, 7, 8, 9, 10, 12, 14], "without": [0, 7, 8, 9, 14], "wjmaddox": 14, "word": [0, 14], "work": [0, 7, 9, 14], "workshop": [0, 12], "world": 12, "worst": 0, "worstcas": 0, "worthi": 12, "would": [0, 7, 14], "wrap": [0, 8, 10, 13, 14], "wrapper": [7, 8, 14], "write": 14, "written": [7, 14], "wrt": 14, "wu": [0, 7, 14], "wu2016parallelkg": 0, "wu2019mf": 7, "wu2024": 14, "www": [0, 7, 12], "x": [0, 1, 4, 7, 8, 9, 10, 12, 14], "x0": [8, 14], "x1": [7, 12], "x2": [7, 12], "x3": 12, "x4": 12, "x_": [7, 12], "x_0": 12, "x_1": [0, 7, 12], "x_11": 7, "x_1d": 7, "x_2": [7, 12], "x_21": 7, "x_2d": 7, "x_3": 12, "x_7": 12, "x_actual": 0, "x_avoid": 8, "x_base": 0, "x_baselin": [0, 7, 8, 14], "x_d": 12, "x_evaluation_mask": 0, "x_expand": 0, "x_fantasi": 0, "x_full": 0, "x_i": [0, 7, 12], "x_init": 8, "x_input": 12, "x_j": [0, 7, 12], "x_k": 0, "x_m": 12, "x_m1": 7, "x_match": 14, "x_md": 7, "x_n": 0, "x_normal": 14, "x_observ": 0, "x_opt": 14, "x_p": 0, "x_pend": [0, 8, 14], "x_pending_evaluation_mask": 0, "x_proj": 0, "x_q": 0, "x_rnd": 8, "x_shape": 0, "xception": 14, "xdoctest": 14, "xi": 7, "xing": 7, "xinit": [4, 8], "xl": 14, "xtol": 7, "xu": 14, "xxix": 12, "y": [0, 4, 7, 8, 10, 12, 14], "y1": 14, "y2": 14, "y_base": 0, "y_baselin": 0, "y_i": [0, 14], "y_p": 12, "y_pmean": 0, "y_sampl": 0, "y_standard": 14, "y_tild": 0, "y_x": 12, "yang": [0, 12], "yang2019": [0, 12, 14], "yang2019a": 12, "yarin": 0, "yet": [0, 7, 14], "yield": [0, 12, 14], "yl": 14, "you": [0, 7, 8, 14], "your": [0, 7, 14], "yu": 14, "yvar": [7, 14], "z": [0, 7, 8, 10, 12, 14], "z_1": [12, 14], "z_2": 12, "z_3": 12, "z_4": 12, "z_i": 12, "zdt": 12, "zdt1": 12, "zdt2": 12, "zdt3": 12, "zeiling": 12, "zero": [0, 4, 7, 8, 11, 12, 14], "zero_grad_ctx": [8, 14], "zero_on_ent": 14, "zero_on_exit": 14, "zeta": 0, "zhang": 7, "zhe": 7, "zhe2019hogp": [7, 9], "zhou": [0, 7, 12], "zitzler": 12, "zitzler2000": 12, "zoo": 7, "zoubin": 7, "\u03b5": 10, "\u03c3": 14}, "titles": ["botorch.acquisition", "botorch.cross_validation", "botorch.exceptions", "botorch.fit", "botorch.generation", "BoTorch API Reference", "botorch.logging", "botorch.models", "botorch.optim", "botorch.posteriors", "botorch.sampling", "botorch.settings", "botorch.test_functions", "botorch.test_utils", "botorch.utils"], "titleterms": {"For": 12, "One": 0, "The": 0, "abstract": [0, 9, 12, 14], "acquisit": [0, 4, 8], "activ": 0, "aggreg": 7, "algebra": 14, "alloc": 7, "analysi": 12, "analyt": 0, "api": [0, 5, 7, 9, 10, 12], "argument": 0, "awar": [0, 7], "base": [0, 7, 9, 10], "bayesian": [0, 7, 9], "bivari": 14, "botorch": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "box": 14, "cach": 0, "candid": 4, "carlo": [0, 10], "chebyshev": 0, "choleski": [0, 14], "class": 0, "closur": 8, "compon": 7, "condit": 14, "constant": 14, "constraint": [8, 14], "constructor": [0, 7], "contain": 14, "context": [7, 14], "contextu": 7, "convers": [7, 8], "core": 8, "cost": [0, 7], "criteria": 8, "cross_valid": 1, "dataset": 14, "decomposit": 14, "decoupl": 0, "determinist": 7, "dispatch": 14, "distribut": 14, "domin": 14, "ellipt": 14, "ensembl": [7, 9], "entropi": 0, "error": 2, "except": 2, "factori": [0, 7], "feasibl": 14, "featur": [0, 10], "fidel": [0, 7, 12], "fit": [3, 8], "fix": 0, "from": 14, "fulli": [7, 9], "function": [0, 4, 8, 10, 12], "gaussian": [10, 14], "gener": [0, 4, 8, 10], "get": 10, "gp": [7, 9, 14], "gpytorch": [7, 9], "gradient": 0, "guid": 0, "helper": [8, 10, 14], "higher": [7, 9], "hint": 14, "homotopi": 8, "hypervolum": [0, 14], "index": 10, "indic": 5, "induc": 7, "initi": 8, "input": [0, 7], "integ": 8, "joint": 0, "kernel": 7, "knowledg": 0, "learn": 0, "likelihood": 7, "linear": 14, "list": [7, 9, 10, 14], "log": 6, "lookahead": 0, "low": 14, "manag": 14, "map": 10, "math": 14, "max": 0, "measur": 0, "method": 7, "mix": [7, 8], "mock": 13, "model": [7, 8], "modul": 7, "mont": [0, 10], "multi": [0, 7, 12, 14], "multitask": [7, 9], "multivari": 14, "non": 14, "normal": 14, "numpi": 8, "object": [0, 12, 14], "optim": [7, 8], "order": [7, 9], "other": 7, "outcom": 7, "output": 0, "pairwis": [7, 10], "paramet": [7, 8], "parego": 0, "pareto": 14, "partit": 14, "path": 10, "pathwis": 10, "penal": 0, "point": 7, "posterior": [9, 10], "predict": 0, "prefer": 0, "prior": [0, 10, 14], "probabl": 14, "proxim": 0, "qmc": 10, "rank": 14, "refer": 5, "regress": 7, "reward": 7, "risk": 0, "round": 14, "safe": 14, "sampl": [0, 4, 9, 10, 14], "sampler": [10, 14], "scalar": [0, 14], "search": 0, "sensit": 12, "set": 11, "shot": 0, "skew": 14, "slice": 14, "space": 7, "statist": 14, "step": 0, "stochast": 10, "stop": 8, "strategi": [4, 10], "synthet": 12, "tabl": 5, "task": 14, "test": [12, 14], "test_funct": 12, "test_util": 13, "thompson": 0, "timeout": 8, "tool": 8, "torch": [8, 9, 14], "transform": [7, 9, 14], "truncat": 14, "type": 14, "unifi": 14, "updat": [10, 14], "util": [0, 4, 7, 8, 9, 10, 12, 14], "utilti": 7, "valu": 0, "variabl": 8, "variat": 7, "via": 14, "volum": 14, "warn": 2, "wrapper": 0}}) \ No newline at end of file diff --git a/website-old/static/js/searchtools.js b/website-old/static/js/searchtools.js new file mode 100644 index 0000000000..2c774d17af --- /dev/null +++ b/website-old/static/js/searchtools.js @@ -0,0 +1,632 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/website-old/static/pygments.css b/website-old/static/pygments.css new file mode 100644 index 0000000000..320f0cb803 --- /dev/null +++ b/website-old/static/pygments.css @@ -0,0 +1,68 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + .highlight .hll { background-color: #ffffcc } +.highlight .c { color: #60a0b0; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #007020; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #007020 } /* Comment.Preproc */ +.highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #808080 } /* Generic.Output */ +.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0040D0 } /* Generic.Traceback */ +.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #007020 } /* Keyword.Pseudo */ +.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #902000 } /* Keyword.Type */ +.highlight .m { color: #40a070 } /* Literal.Number */ +.highlight .s { color: #4070a0 } /* Literal.String */ +.highlight .na { color: #4070a0 } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ +.highlight .no { color: #60add5 } /* Name.Constant */ +.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #007020 } /* Name.Exception */ +.highlight .nf { color: #06287e } /* Name.Function */ +.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ +.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #bb60d5 } /* Name.Variable */ +.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mf { color: #40a070 } /* Literal.Number.Float */ +.highlight .mh { color: #40a070 } /* Literal.Number.Hex */ +.highlight .mi { color: #40a070 } /* Literal.Number.Integer */ +.highlight .mo { color: #40a070 } /* Literal.Number.Oct */ +.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ +.highlight .sc { color: #4070a0 } /* Literal.String.Char */ +.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4070a0 } /* Literal.String.Double */ +.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ +.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #c65d09 } /* Literal.String.Other */ +.highlight .sr { color: #235388 } /* Literal.String.Regex */ +.highlight .s1 { color: #4070a0 } /* Literal.String.Single */ +.highlight .ss { color: #517918 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ +.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ +.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ +.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ diff --git a/website/tutorials.json b/website-old/tutorials.json similarity index 100% rename from website/tutorials.json rename to website-old/tutorials.json diff --git a/website/versioned_sidebars/.gitkeep b/website-old/versioned_docs/.gitkeep similarity index 100% rename from website/versioned_sidebars/.gitkeep rename to website-old/versioned_docs/.gitkeep diff --git a/website-old/versioned_sidebars/.gitkeep b/website-old/versioned_sidebars/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/website-old/yarn.lock b/website-old/yarn.lock new file mode 100644 index 0000000000..0b79c8c2f2 --- /dev/null +++ b/website-old/yarn.lock @@ -0,0 +1,7692 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/code-frame@^7.22.5", "@babel/code-frame@^7.5.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + dependencies: + "@babel/highlight" "^7.22.5" + +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.5.tgz#b1f6c86a02d85d2dd3368a2b67c09add8cd0c255" + integrity sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA== + +"@babel/core@^7.12.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.5.tgz#d67d9747ecf26ee7ecd3ebae1ee22225fe902a89" + integrity sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helpers" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.5.tgz#1e7bf768688acfb05cf30b2369ef855e82d984f7" + integrity sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA== + dependencies: + "@babel/types" "^7.22.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz#a3f4758efdd0190d8927fcffd261755937c71878" + integrity sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz#fc7319fc54c5e2fa14b2909cf3c5fd3046813e02" + integrity sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw== + dependencies: + "@babel/compat-data" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz#2192a1970ece4685fbff85b48da2c32fcb130b7c" + integrity sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + semver "^6.3.0" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz#bb2bf0debfe39b831986a4efbf4066586819c6e4" + integrity sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.0" + +"@babel/helper-define-polyfill-provider@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz#487053f103110f25b9755c5980e031e93ced24d8" + integrity sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg== + dependencies: + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" + integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz#0f65daa0716961b6e96b164034e737f60a80d2ef" + integrity sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-remap-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz#14a38141a7bf2165ad38da61d61cf27b43015da2" + integrity sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-wrap-function" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-replace-supers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz#71bc5fb348856dea9fdc4eafd7e2e49f585145dc" + integrity sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz#88cf11050edb95ed08d596f7a044462189127a08" + integrity sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + +"@babel/helper-wrap-function@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz#44d205af19ed8d872b4eefb0d2fa65f45eb34f06" + integrity sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helpers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.5.tgz#74bb4373eb390d1ceed74a15ef97767e63120820" + integrity sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + +"@babel/parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" + integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" + integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz#fef09f9499b1f1c930da8a0c419db42167d792ca" + integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.5" + +"@babel/plugin-proposal-class-properties@^7.12.1": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-object-rest-spread@^7.12.1": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.7" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" + integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" + integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz#7336356d23380eda9a56314974f053a020dab0c3" + integrity sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" + integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-block-scoping@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz#8bfc793b3a4b2742c0983fadc1480d843ecea31b" + integrity sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz#3e40c46f048403472d6f4183116d5e46b1bff5ba" + integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz#635d4e98da741fad814984639f4c0149eb0135e1" + integrity sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + +"@babel/plugin-transform-destructuring@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz#d3aca7438f6c26c78cdd0b0ba920a336001b27cc" + integrity sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dotall-regex@^7.22.5", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" + integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-duplicate-keys@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" + integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz#d6908a8916a810468c4edff73b5b75bda6ad393e" + integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" + integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz#57c41cb1d0613d22f548fddd8b288eedb9973a5b" + integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" + integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz#14b64352fdf7e1f737eed68de1a1468bd2a77ec0" + integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz#66ae5f068fd5a9a5dc570df16f56c2a8462a9d6c" + integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-amd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" + integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-commonjs@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa" + integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz#18c31410b5e579a0092638f95c896c2a98a5d496" + integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + +"@babel/plugin-transform-modules-umd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" + integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" + integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz#f8872c65776e0b552e0849d7596cddd416c3e381" + integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58" + integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz#9686dc3447df4753b0b2a2fae7e8bc33cdc1f2e1" + integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ== + dependencies: + "@babel/compat-data" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.5" + +"@babel/plugin-transform-object-super@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz#842080be3076703be0eaf32ead6ac8174edee333" + integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz#1003762b9c14295501beb41be72426736bedd1e0" + integrity sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" + integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" + integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz#07a77f28cbb251546a43d175a1dda4cf3ef83e32" + integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-display-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz#3c4326f9fce31c7968d6cb9debcaf32d9e279a2b" + integrity sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-jsx-development@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" + integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.22.5" + +"@babel/plugin-transform-react-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz#932c291eb6dd1153359e2a90cb5e557dcf068416" + integrity sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/plugin-transform-react-pure-annotations@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz#1f58363eef6626d6fa517b95ac66fe94685e32c0" + integrity sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-regenerator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz#cd8a68b228a5f75fa01420e8cc2fc400f0fc32aa" + integrity sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + regenerator-transform "^0.15.1" + +"@babel/plugin-transform-reserved-words@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" + integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-shorthand-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" + integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-template-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typeof-symbol@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" + integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-escapes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz#ce0c248522b1cb22c7c992d88301a5ead70e806c" + integrity sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" + integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" + integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" + integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/polyfill@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.12.1.tgz#1f2d6371d1261bbd961f3c5d5909150e12d0bd96" + integrity sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.4" + +"@babel/preset-env@^7.12.1": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.5.tgz#3da66078b181f3d62512c51cf7014392c511504e" + integrity sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A== + dependencies: + "@babel/compat-data" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.22.5" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.22.5" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.5" + "@babel/plugin-transform-classes" "^7.22.5" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.22.5" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.5" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.5" + "@babel/plugin-transform-for-of" "^7.22.5" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.5" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.5" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.5" + "@babel/plugin-transform-modules-systemjs" "^7.22.5" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5" + "@babel/plugin-transform-numeric-separator" "^7.22.5" + "@babel/plugin-transform-object-rest-spread" "^7.22.5" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.5" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.5" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.5" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.22.5" + babel-plugin-polyfill-corejs2 "^0.4.3" + babel-plugin-polyfill-corejs3 "^0.8.1" + babel-plugin-polyfill-regenerator "^0.5.0" + core-js-compat "^3.30.2" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.12.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.5.tgz#c4d6058fbf80bccad02dd8c313a9aaa67e3c3dd6" + integrity sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + "@babel/plugin-transform-react-display-name" "^7.22.5" + "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/plugin-transform-react-jsx-development" "^7.22.5" + "@babel/plugin-transform-react-pure-annotations" "^7.22.5" + +"@babel/register@^7.12.1": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.22.5.tgz#e4d8d0f615ea3233a27b5c6ada6750ee59559939" + integrity sha512-vV6pm/4CijSQ8Y47RH5SopXzursN35RQINfGJkmOlcpAtGuf94miFvIPhCKGQN7WGIcsgG1BHEX2KVdTYwTwUQ== + dependencies: + clone-deep "^4.0.1" + find-cache-dir "^2.0.0" + make-dir "^2.1.0" + pirates "^4.0.5" + source-map-support "^0.5.16" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.8.4": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" + integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/template@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.12.5", "@babel/traverse@^7.22.5": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.12.6", "@babel/types@^7.22.5", "@babel/types@^7.4.4": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + +"@babel/types@^7.22.15", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.stat@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" + integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@sindresorhus/is@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" + integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== + +"@types/cheerio@^0.22.8": + version "0.22.31" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.31.tgz#b8538100653d6bb1b08a1e46dec75b4f2a5d5eb6" + integrity sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.0.tgz#719498898d5defab83c3560f45d8498f58d11938" + integrity sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ== + +"@types/q@^1.5.1": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" + integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +address@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" + integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== + +address@^1.0.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== + +airbnb-prop-types@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" + integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== + dependencies: + array.prototype.find "^2.1.1" + function.prototype.name "^1.1.2" + is-regex "^1.1.0" + object-is "^1.1.2" + object.assign "^4.1.0" + object.entries "^1.1.2" + prop-types "^15.7.2" + prop-types-exact "^1.2.0" + react-is "^16.13.1" + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ== + +ansi-red@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" + integrity sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow== + dependencies: + ansi-wrap "0.1.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + +ansi-regex@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-wrap@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw== + +arch@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + +archive-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/archive-type/-/archive-type-4.0.0.tgz#f92e72233056dfc6969472749c267bdb046b1d70" + integrity sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA== + dependencies: + file-type "^4.2.0" + +argparse@^1.0.10, argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== + +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== + +array.prototype.filter@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.2.tgz#5f90ca6e3d01c31ea8db24c147665541db28bb4c" + integrity sha512-us+UrmGOilqttSOgoWZTpOvHu68vZT2YCjc/H4vhu56vzZpaDFBhB+Se2UwqWzMKbDv7Myq5M5pcZLAtUvTQdQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + +array.prototype.find@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.2.1.tgz#769b8182a0b535c3d76ac025abab98ba1e12467b" + integrity sha512-I2ri5Z9uMpMvnsNrHre9l3PaX+z9D0/z6F7Yt2u15q7wt0I62g5kX6xUKR1SJiefgG+u2/gJUmM8B47XRvQR6w== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.flat@^1.2.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.reduce@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac" + integrity sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== + +async@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autolinker@^3.11.0: + version "3.16.2" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-3.16.2.tgz#6bb4f32432fc111b65659336863e653973bfbcc9" + integrity sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA== + dependencies: + tslib "^2.3.0" + +autolinker@~0.28.0: + version "0.28.1" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47" + integrity sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ== + dependencies: + gulp-header "^1.7.1" + +autoprefixer@^9.7.5: + version "9.8.8" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" + integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + picocolors "^0.2.1" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" + integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + +babel-plugin-polyfill-corejs2@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz#75044d90ba5043a5fb559ac98496f62f3eb668fd" + integrity sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-define-polyfill-provider" "^0.4.0" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz#39248263c38191f0d226f928d666e6db1b4b3a8a" + integrity sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.0" + core-js-compat "^3.30.1" + +babel-plugin-polyfill-regenerator@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz#e7344d88d9ef18a3c47ded99362ae4a757609380" + integrity sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.0" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +big-integer@^1.6.17: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +bin-build@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bin-build/-/bin-build-3.0.0.tgz#c5780a25a8a9f966d8244217e6c1f5082a143861" + integrity sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA== + dependencies: + decompress "^4.0.0" + download "^6.2.2" + execa "^0.7.0" + p-map-series "^1.0.0" + tempfile "^2.0.0" + +bin-check@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bin-check/-/bin-check-4.1.0.tgz#fc495970bdc88bb1d5a35fc17e65c4a149fc4a49" + integrity sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA== + dependencies: + execa "^0.7.0" + executable "^4.1.0" + +bin-version-check@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/bin-version-check/-/bin-version-check-4.0.0.tgz#7d819c62496991f80d893e6e02a3032361608f71" + integrity sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ== + dependencies: + bin-version "^3.0.0" + semver "^5.6.0" + semver-truncate "^1.1.2" + +bin-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bin-version/-/bin-version-3.1.0.tgz#5b09eb280752b1bd28f0c9db3f96f2f43b6c0839" + integrity sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ== + dependencies: + execa "^1.0.0" + find-versions "^3.0.0" + +bin-wrapper@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bin-wrapper/-/bin-wrapper-4.1.0.tgz#99348f2cf85031e3ef7efce7e5300aeaae960605" + integrity sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q== + dependencies: + bin-check "^4.1.0" + bin-version-check "^4.0.0" + download "^7.1.0" + import-lazy "^3.1.0" + os-filter-obj "^2.0.0" + pify "^4.0.1" + +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bl@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" + integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +bl@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.0.2.tgz#b45308efeb30d3d453d5c67e47c5f0ff1fa237ac" + integrity sha512-/ivXMGCGDI0EB4JI4zCqppp79j03vUgZz/zakw7TworE2NVjIuPxpL1Ti0InSsarKqFG5NLFreCBcCCSjtrTQw== + dependencies: + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^4.2.0" + +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +body@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069" + integrity sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ== + dependencies: + continuable-cache "^0.3.1" + error "^7.0.0" + raw-body "~1.1.0" + safe-json-parse "~1.0.1" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@4.14.2: + version "4.14.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.2.tgz#1b3cec458a1ba87588cc5e9be62f19b6d48813ce" + integrity sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw== + dependencies: + caniuse-lite "^1.0.30001125" + electron-to-chromium "^1.3.564" + escalade "^3.0.2" + node-releases "^1.1.61" + +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.21.3, browserslist@^4.21.5: + version "4.21.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.7.tgz#e2b420947e5fb0a58e8f4668ae6e23488127e551" + integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA== + dependencies: + caniuse-lite "^1.0.30001489" + electron-to-chromium "^1.4.411" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +buffer@^5.2.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + +bytes@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" + integrity sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cacheable-request@^2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" + integrity sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ== + dependencies: + clone-response "1.0.2" + get-stream "3.0.0" + http-cache-semantics "3.8.1" + keyv "3.0.0" + lowercase-keys "1.0.0" + normalize-url "2.0.1" + responselike "1.0.2" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ== + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001489: + version "1.0.30001502" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001502.tgz#f7e4a76eb1d2d585340f773767be1fefc118dca8" + integrity sha512-AZ+9tFXw1sS0o0jcpJQIXvFTOB/xGiQ4OQ2t98QX3NDn2EZTSRBC801gxrsGgViuq2ak/NLkNgSNEPtCr5lfKg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +caw@^2.0.0, caw@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/caw/-/caw-2.0.1.tgz#6c3ca071fc194720883c2dc5da9b074bfc7e9e95" + integrity sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA== + dependencies: + get-proxy "^2.0.0" + isurl "^1.0.0-alpha5" + tunnel-agent "^0.6.0" + url-to-options "^1.0.1" + +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + +chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" + integrity sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA== + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash.assignin "^4.0.9" + lodash.bind "^4.1.4" + lodash.defaults "^4.0.1" + lodash.filter "^4.4.0" + lodash.flatten "^4.2.0" + lodash.foreach "^4.3.0" + lodash.map "^4.4.0" + lodash.merge "^4.4.0" + lodash.pick "^4.2.1" + lodash.reduce "^4.4.0" + lodash.reject "^4.4.0" + lodash.some "^4.4.0" + +cheerio@^1.0.0-rc.3: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +classnames@^2.2.6: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone-response@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q== + dependencies: + mimic-response "^1.0.0" + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +coffee-script@^1.12.4: + version "1.12.7" + resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" + integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw== + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw== + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.19.0, commander@^2.8.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-with-sourcemaps@*: + version "1.1.0" + resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" + integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== + dependencies: + source-map "^0.6.1" + +config-chain@^1.1.11: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +console-stream@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/console-stream/-/console-stream-0.1.1.tgz#a095fe07b20465955f2fafd28b5d72bccd949d44" + integrity sha512-QC/8l9e6ofi6nqZ5PawlDgzmMw3OxIXtvolBzap/F4UDBJlDaZRSNbL/lb41C29FcbSJncBFlJFj2WJoNyZRfQ== + +content-disposition@0.5.4, content-disposition@^0.5.2: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +continuable-cache@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" + integrity sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA== + +convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== + +core-js-compat@^3.30.1, core-js-compat@^3.30.2: + version "3.31.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.31.0.tgz#4030847c0766cc0e803dcdfb30055d7ef2064bf1" + integrity sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw== + dependencies: + browserslist "^4.21.5" + +core-js@^2.6.5: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cross-spawn@7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A== + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crowdin-cli@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/crowdin-cli/-/crowdin-cli-0.3.0.tgz#eac9989a6fe7feaaf33090397afc187c67b46191" + integrity sha512-s1vSRqWalCqd+vW7nF4oZo1a2pMpEgwIiwVlPRD0HmGY3HjJwQKXqZ26NpX5qCDVN8UdEsScy+2jle0PPQBmAg== + dependencies: + request "^2.53.0" + yamljs "^0.2.1" + yargs "^2.3.0" + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q== + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA== + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff" + integrity sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ== + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.2" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.3" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw== + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw== + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== + +cssnano@^4.1.10: + version "4.1.11" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.11.tgz#c7b5f5b81da269cb1fd982cb960c1200910c9a99" + integrity sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g== + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.8" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^4.0.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng== + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +debug@^3.1.0, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +decompress-response@^3.2.0, decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== + dependencies: + mimic-response "^1.0.0" + +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.0.0, decompress@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" + integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA== + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA== + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-port-alt@1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +diacritics-map@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/diacritics-map/-/diacritics-map-0.1.0.tgz#6dfc0ff9d01000a2edf2865371cac316e94977af" + integrity sha512-3omnDTYrGigU0i4cJjvaKwD52B8aoqyX/NEIkukFFkogBemsIbhSa1O414fpTp5nuszJG6lvQ5vBvDVNCbSsaQ== + +dir-glob@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag== + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== + +docusaurus@^1.14.7: + version "1.14.7" + resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-1.14.7.tgz#f51858ab643b29ec52264d6dd85e0d629e5b3a4a" + integrity sha512-UWqar4ZX0lEcpLc5Tg+MwZ2jhF/1n1toCQRSeoxDON/D+E9ToLr+vTRFVMP/Tk84NXSVjZFRlrjWwM2pXzvLsQ== + dependencies: + "@babel/core" "^7.12.3" + "@babel/plugin-proposal-class-properties" "^7.12.1" + "@babel/plugin-proposal-object-rest-spread" "^7.12.1" + "@babel/polyfill" "^7.12.1" + "@babel/preset-env" "^7.12.1" + "@babel/preset-react" "^7.12.5" + "@babel/register" "^7.12.1" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.6" + autoprefixer "^9.7.5" + babylon "^6.18.0" + chalk "^3.0.0" + classnames "^2.2.6" + commander "^4.0.1" + crowdin-cli "^0.3.0" + cssnano "^4.1.10" + enzyme "^3.10.0" + enzyme-adapter-react-16 "^1.15.1" + escape-string-regexp "^2.0.0" + express "^4.17.1" + feed "^4.2.1" + fs-extra "^9.0.1" + gaze "^1.1.3" + github-slugger "^1.3.0" + glob "^7.1.6" + highlight.js "^9.16.2" + imagemin "^6.0.0" + imagemin-gifsicle "^6.0.1" + imagemin-jpegtran "^6.0.0" + imagemin-optipng "^6.0.0" + imagemin-svgo "^7.0.0" + lodash "^4.17.20" + markdown-toc "^1.2.0" + mkdirp "^0.5.1" + portfinder "^1.0.28" + postcss "^7.0.23" + prismjs "^1.22.0" + react "^16.8.4" + react-dev-utils "^11.0.1" + react-dom "^16.8.4" + remarkable "^2.0.0" + request "^2.88.0" + shelljs "^0.8.4" + sitemap "^3.2.2" + tcp-port-used "^1.0.1" + tiny-lr "^1.1.1" + tree-node-cli "^1.2.5" + truncate-html "^1.0.3" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +dom-serializer@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1, domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1, domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +download@^6.2.2: + version "6.2.5" + resolved "https://registry.yarnpkg.com/download/-/download-6.2.5.tgz#acd6a542e4cd0bb42ca70cfc98c9e43b07039714" + integrity sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA== + dependencies: + caw "^2.0.0" + content-disposition "^0.5.2" + decompress "^4.0.0" + ext-name "^5.0.0" + file-type "5.2.0" + filenamify "^2.0.0" + get-stream "^3.0.0" + got "^7.0.0" + make-dir "^1.0.0" + p-event "^1.0.0" + pify "^3.0.0" + +download@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/download/-/download-7.1.0.tgz#9059aa9d70b503ee76a132897be6dec8e5587233" + integrity sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ== + dependencies: + archive-type "^4.0.0" + caw "^2.0.1" + content-disposition "^0.5.2" + decompress "^4.2.0" + ext-name "^5.0.0" + file-type "^8.1.0" + filenamify "^2.0.0" + get-stream "^3.0.0" + got "^8.3.1" + make-dir "^1.2.0" + p-event "^2.1.0" + pify "^3.0.0" + +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + +duplexer3@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" + integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + +duplexer@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.3.564, electron-to-chromium@^1.4.411: + version "1.4.427" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz#67e8069f7a864fc092fe2e09f196e68af5cb88a1" + integrity sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +enzyme-adapter-react-16@^1.15.1: + version "1.15.7" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.7.tgz#a737e6d8e2c147e9da5acf957755be7634f76201" + integrity sha512-LtjKgvlTc/H7adyQcj+aq0P0H07LDL480WQl1gU512IUyaDo/sbOaNDdZsJXYW2XaoPqrLLE9KbZS+X2z6BASw== + dependencies: + enzyme-adapter-utils "^1.14.1" + enzyme-shallow-equal "^1.0.5" + has "^1.0.3" + object.assign "^4.1.4" + object.values "^1.1.5" + prop-types "^15.8.1" + react-is "^16.13.1" + react-test-renderer "^16.0.0-0" + semver "^5.7.0" + +enzyme-adapter-utils@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz#f30db15dafc22e0ccd44f5acc8d93be29218cdcf" + integrity sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ== + dependencies: + airbnb-prop-types "^2.16.0" + function.prototype.name "^1.1.5" + has "^1.0.3" + object.assign "^4.1.4" + object.fromentries "^2.0.5" + prop-types "^15.8.1" + semver "^5.7.1" + +enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz#5528a897a6ad2bdc417c7221a7db682cd01711ba" + integrity sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg== + dependencies: + has "^1.0.3" + object-is "^1.1.5" + +enzyme@^3.10.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" + integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== + dependencies: + array.prototype.flat "^1.2.3" + cheerio "^1.0.0-rc.3" + enzyme-shallow-equal "^1.0.1" + function.prototype.name "^1.1.2" + has "^1.0.3" + html-element-map "^1.2.0" + is-boolean-object "^1.0.1" + is-callable "^1.1.5" + is-number-object "^1.0.4" + is-regex "^1.0.5" + is-string "^1.0.5" + is-subset "^0.1.1" + lodash.escape "^4.0.1" + lodash.isequal "^4.5.0" + object-inspect "^1.7.0" + object-is "^1.0.2" + object.assign "^4.1.0" + object.entries "^1.1.1" + object.values "^1.1.1" + raf "^3.4.1" + rst-selector-parser "^2.2.3" + string.prototype.trim "^1.2.1" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +error@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894" + integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA== + dependencies: + string-template "~0.2.1" + +es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: + version "1.21.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" + integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== + dependencies: + array-buffer-byte-length "^1.0.0" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.0" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.0.2, escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +exec-buffer@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/exec-buffer/-/exec-buffer-3.2.0.tgz#b1686dbd904c7cf982e652c1f5a79b1e5573082b" + integrity sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA== + dependencies: + execa "^0.7.0" + p-finally "^1.0.0" + pify "^3.0.0" + rimraf "^2.5.4" + tempfile "^2.0.0" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw== + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +executable@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== + dependencies: + pify "^2.2.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA== + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + integrity sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA== + dependencies: + fill-range "^2.1.0" + +express@^4.17.1: + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext-list@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" + integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== + dependencies: + mime-db "^1.28.0" + +ext-name@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" + integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== + dependencies: + ext-list "^2.0.0" + sort-keys-length "^1.0.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-folder-size@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/fast-folder-size/-/fast-folder-size-1.6.1.tgz#1dc1674842854032cf07a387ba77c66546c547eb" + integrity sha512-F3tRpfkAzb7TT2JNKaJUglyuRjRa+jelQD94s9OSqkfEeytLmupCqQiD+H2KoIXGtp4pB5m4zNmv5m2Ktcr+LA== + dependencies: + unzipper "^0.10.11" + +fast-glob@^2.0.2: + version "2.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" + integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== + dependencies: + "@mrmlnc/readdir-enhanced" "^2.2.1" + "@nodelib/fs.stat" "^1.1.2" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.3" + micromatch "^3.1.10" + +fast-glob@^3.1.1: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-xml-parser@^4.1.3: + version "4.4.1" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" + integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== + dependencies: + strnum "^1.0.5" + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +faye-websocket@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ== + dependencies: + websocket-driver ">=0.5.1" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +feed@^4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" + integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== + dependencies: + xml-js "^1.6.11" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + integrity sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ== + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-type@5.2.0, file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ== + +file-type@^10.4.0, file-type@^10.7.0: + version "10.11.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.11.0.tgz#2961d09e4675b9fb9a3ee6b69e9cd23f43fd1890" + integrity sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw== + +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== + +file-type@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-4.4.0.tgz#1b600e5fca1fbdc6e80c0a70c71c8dba5f7906c5" + integrity sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ== + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== + +file-type@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-8.1.0.tgz#244f3b7ef641bbe0cca196c7276e4b332399f68c" + integrity sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ== + +filename-reserved-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" + integrity sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ== + +filenamify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-2.1.0.tgz#88faf495fb1b47abfd612300002a16228c677ee9" + integrity sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA== + dependencies: + filename-reserved-regex "^2.0.0" + strip-outer "^1.0.0" + trim-repeated "^1.0.0" + +filesize@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" + integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg== + +fill-range@^2.1.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" + integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^3.0.0" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-up@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA== + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-versions@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" + integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== + dependencies: + semver-regex "^2.0.0" + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +fork-ts-checker-webpack-plugin@4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5" + integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw== + dependencies: + "@babel/code-frame" "^7.5.5" + chalk "^2.4.1" + micromatch "^3.1.10" + minimatch "^3.0.4" + semver "^5.6.0" + tapable "^1.0.0" + worker-rpc "^0.1.0" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA== + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +from2@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.2, function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2, functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gaze@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== + dependencies: + globule "^1.0.0" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-proxy@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/get-proxy/-/get-proxy-2.1.0.tgz#349f2b4d91d44c4d4d4e9cba2ad90143fac5ef93" + integrity sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw== + dependencies: + npm-conf "^1.1.0" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw== + +get-stream@3.0.0, get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ== + +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +gifsicle@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/gifsicle/-/gifsicle-4.0.1.tgz#30e1e61e3ee4884ef702641b2e98a15c2127b2e2" + integrity sha512-A/kiCLfDdV+ERV/UB+2O41mifd+RxH8jlRG8DMxZO84Bma/Fw0htqZ+hY2iaalLRNyUu7tYZQslqUBJxBggxbg== + dependencies: + bin-build "^3.0.0" + bin-wrapper "^4.0.0" + execa "^1.0.0" + logalot "^2.0.0" + +github-slugger@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" + integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA== + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig== + +glob@^7.0.0, glob@^7.0.5, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~7.1.1: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +globby@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" + integrity sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w== + dependencies: + array-union "^1.0.1" + dir-glob "2.0.0" + fast-glob "^2.0.2" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +globule@^1.0.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.4.tgz#7c11c43056055a75a6e68294453c17f2796170fb" + integrity sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg== + dependencies: + glob "~7.1.1" + lodash "^4.17.21" + minimatch "~3.0.2" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" + integrity sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw== + dependencies: + decompress-response "^3.2.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-plain-obj "^1.1.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + p-cancelable "^0.3.0" + p-timeout "^1.1.1" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + url-parse-lax "^1.0.0" + url-to-options "^1.0.1" + +got@^8.3.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" + integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== + dependencies: + "@sindresorhus/is" "^0.7.0" + cacheable-request "^2.1.1" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + into-stream "^3.1.0" + is-retry-allowed "^1.1.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + mimic-response "^1.0.0" + p-cancelable "^0.4.0" + p-timeout "^2.0.1" + pify "^3.0.0" + safe-buffer "^5.1.1" + timed-out "^4.0.1" + url-parse-lax "^3.0.0" + url-to-options "^1.0.1" + +graceful-fs@^4.1.10, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +gray-matter@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e" + integrity sha512-vbmvP1Fe/fxuT2QuLVcqb2BfK7upGhhbLIt9/owWEvPYrZZEkelLcq2HqzxosV+PQ67dUFLaAeNpH7C4hhICAA== + dependencies: + ansi-red "^0.1.1" + coffee-script "^1.12.4" + extend-shallow "^2.0.1" + js-yaml "^3.8.1" + toml "^2.3.2" + +gulp-header@^1.7.1: + version "1.8.12" + resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" + integrity sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ== + dependencies: + concat-with-sourcemaps "*" + lodash.template "^4.4.0" + through2 "^2.0.0" + +gzip-size@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" + integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== + dependencies: + duplexer "^0.1.1" + pify "^4.0.1" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== + dependencies: + ansi-regex "^2.0.0" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbol-support-x@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" + integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-to-string-tag-x@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" + integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== + dependencies: + has-symbol-support-x "^1.4.1" + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q== + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw== + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ== + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ== + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +highlight.js@^9.16.2: + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A== + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA== + +html-element-map@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.3.1.tgz#44b2cbcfa7be7aa4ff59779e47e51012e1c73c08" + integrity sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg== + dependencies: + array.prototype.filter "^1.0.0" + call-bind "^1.0.2" + +htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + +http-cache-semantics@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +ignore@^5.1.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +imagemin-gifsicle@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/imagemin-gifsicle/-/imagemin-gifsicle-6.0.1.tgz#6abad4e95566d52e5a104aba1c24b4f3b48581b3" + integrity sha512-kuu47c6iKDQ6R9J10xCwL0lgs0+sMz3LRHqRcJ2CRBWdcNmo3T5hUaM8hSZfksptZXJLGKk8heSAvwtSdB1Fng== + dependencies: + exec-buffer "^3.0.0" + gifsicle "^4.0.0" + is-gif "^3.0.0" + +imagemin-jpegtran@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/imagemin-jpegtran/-/imagemin-jpegtran-6.0.0.tgz#c8d3bcfb6ec9c561c20a987142854be70d90b04f" + integrity sha512-Ih+NgThzqYfEWv9t58EItncaaXIHR0u9RuhKa8CtVBlMBvY0dCIxgQJQCfwImA4AV1PMfmUKlkyIHJjb7V4z1g== + dependencies: + exec-buffer "^3.0.0" + is-jpg "^2.0.0" + jpegtran-bin "^4.0.0" + +imagemin-optipng@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/imagemin-optipng/-/imagemin-optipng-6.0.0.tgz#a6bfc7b542fc08fc687e83dfb131249179a51a68" + integrity sha512-FoD2sMXvmoNm/zKPOWdhKpWdFdF9qiJmKC17MxZJPH42VMAp17/QENI/lIuP7LCUnLVAloO3AUoTSNzfhpyd8A== + dependencies: + exec-buffer "^3.0.0" + is-png "^1.0.0" + optipng-bin "^5.0.0" + +imagemin-svgo@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/imagemin-svgo/-/imagemin-svgo-7.1.0.tgz#528a42fd3d55eff5d4af8fd1113f25fb61ad6d9a" + integrity sha512-0JlIZNWP0Luasn1HT82uB9nU9aa+vUj6kpT+MjPW11LbprXC+iC4HDwn1r4Q2/91qj4iy9tRZNsFySMlEpLdpg== + dependencies: + is-svg "^4.2.1" + svgo "^1.3.2" + +imagemin@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/imagemin/-/imagemin-6.1.0.tgz#62508b465728fea36c03cdc07d915fe2d8cf9e13" + integrity sha512-8ryJBL1CN5uSHpiBMX0rJw79C9F9aJqMnjGnrd/1CafegpNuA81RBAAru/jQQEOWlOJJlpRnlcVFF6wq+Ist0A== + dependencies: + file-type "^10.7.0" + globby "^8.0.1" + make-dir "^1.0.0" + p-pipe "^1.1.0" + pify "^4.0.1" + replace-ext "^1.0.0" + +immer@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" + integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-lazy@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc" + integrity sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ== + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg== + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4, ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +into-stream@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" + integrity sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ== + dependencies: + from2 "^2.1.1" + p-is-promise "^1.1.0" + +ip-regex@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A== + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA== + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-core-module@^2.11.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg== + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== + +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + +is-gif@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-gif/-/is-gif-3.0.0.tgz#c4be60b26a301d695bb833b20d9b5d66c6cf83b1" + integrity sha512-IqJ/jlbw5WJSNfwQ/lHEDXF8rxhRgF6ythk2oiEvhpG29F704eX9NO6TvPfMiq9DrbwgcEDnETYNcZDPewQoVw== + dependencies: + file-type "^10.4.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw== + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-jpg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-jpg/-/is-jpg-2.0.0.tgz#2e1997fa6e9166eaac0242daae443403e4ef1d97" + integrity sha512-ODlO0ruzhkzD3sdynIainVP5eoOFNN85rxA1+cwwnPe4dKyX0r5+hxNO5XpCrxlHcmb9vkOit9mhRD2JVuimHg== + +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg== + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg== + dependencies: + kind-of "^3.0.2" + +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" + integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-png@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-png/-/is-png-1.1.0.tgz#d574b12bf275c0350455570b0e5b57ab062077ce" + integrity sha512-23Rmps8UEx3Bzqr0JqAtQo0tYP6sDfIfMt1rL9rzlla/zbteftI9LSJoqsIoGgL06sJboDGdVns4RTakAW/WTw== + +is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + +is-root@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^1.0.0, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw== + +is-svg@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.4.0.tgz#34db20a38146be5f2b3060154da33d11e6f74b7c" + integrity sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug== + dependencies: + fast-xml-parser "^4.1.3" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-url@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is2@^2.0.6: + version "2.0.9" + resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.9.tgz#ff63b441f90de343fa8fac2125ee170da8e8240d" + integrity sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g== + dependencies: + deep-is "^0.1.3" + ip-regex "^4.1.0" + is-url "^1.2.4" + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA== + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +isurl@^1.0.0-alpha5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" + integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== + dependencies: + has-to-string-tag-x "^1.2.0" + is-object "^1.0.1" + +jpegtran-bin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz#d00aed809fba7aa6f30817e59eee4ddf198f8f10" + integrity sha512-2cRl1ism+wJUoYAYFt6O/rLBfpXNWG2dUWbgcEkTt5WGMnqI46eEro8T4C5zGROxKRqyKpCBSdHPvt5UYCtxaQ== + dependencies: + bin-build "^3.0.0" + bin-wrapper "^4.0.0" + logalot "^2.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1, js-yaml@^3.8.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^2.1.2, json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +keyv@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" + integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== + dependencies: + json-buffer "3.0.0" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +lazy-cache@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" + integrity sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA== + dependencies: + set-getter "^0.1.0" + +list-item@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/list-item/-/list-item-1.1.1.tgz#0c65d00e287cb663ccb3cb3849a77e89ec268a56" + integrity sha512-S3D0WZ4J6hyM8o5SNKWaMYB1ALSacPZ2nHGEuCjmHZ+dc03gFeNZoNDcqfcnO4vDhTZmNrqrpYZCdXsRh22bzw== + dependencies: + expand-range "^1.8.1" + extend-shallow "^2.0.1" + is-number "^2.1.0" + repeat-string "^1.5.2" + +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== + +livereload-js@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" + integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw== + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A== + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +loader-utils@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== + +lodash.assignin@^4.0.9: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" + integrity sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg== + +lodash.bind@^4.1.4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" + integrity sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA== + +lodash.chunk@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.defaults@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + integrity sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw== + +lodash.filter@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" + integrity sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ== + +lodash.flatten@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ== + +lodash.foreach@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + integrity sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.map@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + integrity sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.4.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.padstart@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + integrity sha512-sW73O6S8+Tg66eY56DBk85aQzzUJDtpoXFBgELMd5P/SotAguo+1kYO6RuYgXxA4HJH3LFTFPASX6ET6bjfriw== + +lodash.pick@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== + +lodash.reduce@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" + integrity sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw== + +lodash.reject@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" + integrity sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ== + +lodash.some@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" + integrity sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash.template@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +logalot@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/logalot/-/logalot-2.1.0.tgz#5f8e8c90d304edf12530951a5554abb8c5e3f552" + integrity sha512-Ah4CgdSRfeCJagxQhcVNMi9BfGYyEKLa6d7OA6xSbld/Hg3Cf2QiOa1mDpmG7Ve8LOH6DN3mdttzjQAvWTyVkw== + dependencies: + figures "^1.3.5" + squeak "^1.0.0" + +longest@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + integrity sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ== + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lowercase-keys@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + integrity sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A== + +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lpad-align@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/lpad-align/-/lpad-align-1.1.2.tgz#21f600ac1c3095c3c6e497ee67271ee08481fe9e" + integrity sha512-MMIcFmmR9zlGZtBcFOows6c2COMekHCIFJz3ew/rRpKZ1wR4mXDPzvcVqLarux8M33X4TPSq2Jdw8WJj0q0KbQ== + dependencies: + get-stdin "^4.0.1" + indent-string "^2.1.0" + longest "^1.0.0" + meow "^3.3.0" + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^1.0.0, make-dir@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w== + dependencies: + object-visit "^1.0.0" + +markdown-link@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/markdown-link/-/markdown-link-0.1.1.tgz#32c5c65199a6457316322d1e4229d13407c8c7cf" + integrity sha512-TurLymbyLyo+kAUUAV9ggR9EPcDjP/ctlv9QAFiqUH7c+t6FlsbivPo9OKTU8xdOx9oNd2drW/Fi5RRElQbUqA== + +markdown-toc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/markdown-toc/-/markdown-toc-1.2.0.tgz#44a15606844490314afc0444483f9e7b1122c339" + integrity sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg== + dependencies: + concat-stream "^1.5.2" + diacritics-map "^0.1.0" + gray-matter "^2.1.0" + lazy-cache "^2.0.2" + list-item "^1.1.1" + markdown-link "^0.1.1" + minimist "^1.2.0" + mixin-deep "^1.1.3" + object.pick "^1.2.0" + remarkable "^1.7.1" + repeat-string "^1.6.1" + strip-color "^0.1.0" + +math-random@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" + integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA== + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge2@^1.2.3, merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +microevent.ts@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" + integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== + +micromatch@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0, mime-db@^1.28.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@~3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mixin-deep@^1.1.3, mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.6, mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +moo@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" + integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +nearley@^2.7.10: + version "2.20.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== + dependencies: + commander "^2.19.0" + moo "^0.5.0" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-releases@^1.1.61: + version "1.1.77" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.77.tgz#50b0cfede855dd374e7585bf228ff34e57c1c32e" + integrity sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ== + +node-releases@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" + integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +normalize-url@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + +npm-conf@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" + integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== + dependencies: + config-chain "^1.1.11" + pify "^3.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + +nth-check@^1.0.2, nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ== + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.12.3, object-inspect@^1.7.0, object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-is@^1.0.2, object-is@^1.1.2, object-is@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA== + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0, object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.1, object.entries@^1.1.2: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" + integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.fromentries@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" + integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.6" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz#5e5c384dd209fa4efffead39e3a0512770ccc312" + integrity sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ== + dependencies: + array.prototype.reduce "^1.0.5" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.21.2" + safe-array-concat "^1.0.0" + +object.pick@^1.2.0, object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== + dependencies: + isobject "^3.0.1" + +object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^7.0.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +optipng-bin@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/optipng-bin/-/optipng-bin-5.1.0.tgz#a7c7ab600a3ab5a177dae2f94c2d800aa386b5a9" + integrity sha512-9baoqZTNNmXQjq/PQTWEXbVV3AMO2sI/GaaqZJZ8SExfAzjijeAP7FEeT+TtyumSw7gr0PZtSUYB/Ke7iHQVKA== + dependencies: + bin-build "^3.0.0" + bin-wrapper "^4.0.0" + logalot "^2.0.0" + +os-filter-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/os-filter-obj/-/os-filter-obj-2.0.0.tgz#1c0b62d5f3a2442749a2d139e6dddee6e81d8d16" + integrity sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg== + dependencies: + arch "^2.1.0" + +p-cancelable@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" + integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw== + +p-cancelable@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== + +p-event@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-event/-/p-event-1.3.0.tgz#8e6b4f4f65c72bc5b6fe28b75eda874f96a4a085" + integrity sha512-hV1zbA7gwqPVFcapfeATaNjQ3J0NuzorHPyG8GPL9g/Y/TplWVBVoCKCXL6Ej2zscrCEv195QNWJXuBH6XZuzA== + dependencies: + p-timeout "^1.1.1" + +p-event@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/p-event/-/p-event-2.3.1.tgz#596279ef169ab2c3e0cae88c1cfbb08079993ef6" + integrity sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA== + dependencies: + p-timeout "^2.0.1" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-is-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + integrity sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg== + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" + integrity sha512-4k9LlvY6Bo/1FcIdV33wqZQES0Py+iKISU9Uc8p8AjWoZPnFKMpVIVD3s0EYn4jzLh1I+WeUZkJ0Yoa4Qfw3Kg== + dependencies: + p-reduce "^1.0.0" + +p-pipe@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" + integrity sha512-IA8SqjIGA8l9qOksXJvsvkeQ+VGb0TAzNCzvKvz9wt5wWLqfWbV6fXy43gpR2L4Te8sOq3S+Ql9biAaMKPdbtw== + +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ== + +p-timeout@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386" + integrity sha512-gb0ryzr+K2qFqFv6qi3khoeqMZF/+ajxQipEF6NteZVnvz9tzdsfAVj3lYtn1gAXvH5lfLwfxEII799gt/mRIA== + dependencies: + p-finally "^1.0.0" + +p-timeout@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" + integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== + dependencies: + p-finally "^1.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ== + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + +parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q== + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ== + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg== + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + +pirates@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-up@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +portfinder@^1.0.28: + version "1.0.32" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81" + integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg== + dependencies: + async "^2.6.4" + debug "^3.2.7" + mkdirp "^0.5.6" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== + +postcss-calc@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== + dependencies: + postcss "^7.0.27" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.2" + +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== + dependencies: + postcss "^7.0.0" + +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-selector-parser@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" + integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== + dependencies: + dot-prop "^5.2.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^6.0.2: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" + integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.23, postcss@^7.0.27, postcss@^7.0.32: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg== + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== + +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +prismjs@^1.22.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +prompts@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" + integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types-exact@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" + integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== + dependencies: + has "^1.0.3" + object.assign "^4.1.0" + reflect.ownkeys "^0.2.0" + +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +qs@^6.4.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + +randomatic@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" + integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425" + integrity sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg== + dependencies: + bytes "1" + string_decoder "0.10" + +react-dev-utils@^11.0.1: + version "11.0.4" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a" + integrity sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A== + dependencies: + "@babel/code-frame" "7.10.4" + address "1.1.2" + browserslist "4.14.2" + chalk "2.4.2" + cross-spawn "7.0.3" + detect-port-alt "1.1.6" + escape-string-regexp "2.0.0" + filesize "6.1.0" + find-up "4.1.0" + fork-ts-checker-webpack-plugin "4.1.6" + global-modules "2.0.0" + globby "11.0.1" + gzip-size "5.1.1" + immer "8.0.1" + is-root "2.1.0" + loader-utils "2.0.0" + open "^7.0.2" + pkg-up "3.1.0" + prompts "2.4.0" + react-error-overlay "^6.0.9" + recursive-readdir "2.2.2" + shell-quote "1.7.2" + strip-ansi "6.0.0" + text-table "0.2.0" + +react-dom@^16.8.4: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" + integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.19.1" + +react-error-overlay@^6.0.9: + version "6.0.11" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" + integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== + +react-is@^16.13.1, react-is@^16.8.6: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-test-renderer@^16.0.0-0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" + integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.19.1" + +react@^16.8.4: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A== + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ== + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.4.0.tgz#55ce132d60a988c460d75c631e9ccf6a7229b468" + integrity sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +recursive-readdir@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" + integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + dependencies: + minimatch "3.0.4" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g== + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +reflect.ownkeys@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + integrity sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg== + +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-transform@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" + integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp.prototype.flags@^1.4.3: + version "1.5.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" + integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + functions-have-names "^1.2.3" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +remarkable@^1.7.1: + version "1.7.4" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00" + integrity sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg== + dependencies: + argparse "^1.0.10" + autolinker "~0.28.0" + +remarkable@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-2.0.1.tgz#280ae6627384dfb13d98ee3995627ca550a12f31" + integrity sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA== + dependencies: + argparse "^1.0.10" + autolinker "^3.11.0" + +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.5.2, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A== + dependencies: + is-finite "^1.0.0" + +replace-ext@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== + +request@^2.53.0, request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== + +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== + dependencies: + lowercase-keys "^1.0.0" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w== + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg== + +rimraf@2, rimraf@^2.5.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rst-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" + integrity sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA== + dependencies: + lodash.flattendeep "^4.4.0" + nearley "^2.7.10" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" + integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-json-parse@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" + integrity sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg== + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@^1.2.4, sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +scheduler@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +seek-bzip@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" + integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== + dependencies: + commander "^2.8.1" + +semver-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" + integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== + +semver-truncate@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/semver-truncate/-/semver-truncate-1.1.2.tgz#57f41de69707a62709a7e0104ba2117109ea47e8" + integrity sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w== + dependencies: + semver "^5.3.0" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-getter@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.1.tgz#a3110e1b461d31a9cfc8c5c9ee2e9737ad447102" + integrity sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw== + dependencies: + to-object-path "^0.3.0" + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" + integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + +shelljs@^0.8.4: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +sitemap@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-3.2.2.tgz#3f77c358fa97b555c879e457098e39910095c62b" + integrity sha512-TModL/WU4m2q/mQcrDgNANn0P4LwprM9MMvG4hu5zP4c6IIKs2YLTu6nXXnNr8ODW/WFtxKggiJ1EGn2W0GNmg== + dependencies: + lodash.chunk "^4.2.0" + lodash.padstart "^4.6.1" + whatwg-url "^7.0.0" + xmlbuilder "^13.0.0" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sort-keys-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" + integrity sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw== + dependencies: + sort-keys "^1.0.0" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg== + dependencies: + is-plain-obj "^1.0.0" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg== + dependencies: + is-plain-obj "^1.0.0" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.16: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.13" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5" + integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +squeak@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/squeak/-/squeak-1.3.0.tgz#33045037b64388b567674b84322a6521073916c3" + integrity sha512-YQL1ulInM+ev8nXX7vfXsCsDh6IqXlrremc1hzi77776BtpWgYJUMto3UM05GSAaGzJgWekszjoKDrVNB5XG+A== + dependencies: + chalk "^1.0.0" + console-stream "^0.1.1" + lpad-align "^1.0.1" + +sshpk@^1.7.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g== + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== + +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== + +string.prototype.trim@^1.2.1, string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string_decoder@0.10: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g== + dependencies: + is-utf8 "^0.2.0" + +strip-color@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/strip-color/-/strip-color-0.1.0.tgz#106f65d3d3e6a2d9401cac0eb0ce8b8a702b4f7b" + integrity sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA== + +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== + dependencies: + is-natural-number "^4.0.1" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA== + dependencies: + get-stdin "^4.0.1" + +strip-outer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +stylehacks@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svgo@^1.0.0, svgo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tar-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== + dependencies: + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" + +tcp-port-used@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz#9652b7436eb1f4cfae111c79b558a25769f6faea" + integrity sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA== + dependencies: + debug "4.3.1" + is2 "^2.0.6" + +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ== + +tempfile@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-2.0.0.tgz#6b0446856a9b1114d1856ffcbe509cccb0977265" + integrity sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA== + dependencies: + temp-dir "^1.0.0" + uuid "^3.0.1" + +text-table@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +timed-out@^4.0.0, timed-out@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA== + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A== + +tiny-lr@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab" + integrity sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA== + dependencies: + body "^5.1.0" + debug "^3.1.0" + faye-websocket "~0.10.0" + livereload-js "^2.3.0" + object-assign "^4.1.0" + qs "^6.4.0" + +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg== + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toml@^2.3.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.6.tgz#25b0866483a9722474895559088b436fd11f861b" + integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ== + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + +tree-node-cli@^1.2.5: + version "1.6.0" + resolved "https://registry.yarnpkg.com/tree-node-cli/-/tree-node-cli-1.6.0.tgz#15b76fd7381be650746f5ea06bd70049a3448c08" + integrity sha512-M8um5Lbl76rWU5aC8oOeEhruiCM29lFCKnwpxrwMjpRicHXJx+bb9Cak11G3zYLrMb6Glsrhnn90rHIzDJrjvg== + dependencies: + commander "^5.0.0" + fast-folder-size "1.6.1" + pretty-bytes "^5.6.0" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw== + +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg== + dependencies: + escape-string-regexp "^1.0.2" + +truncate-html@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/truncate-html/-/truncate-html-1.0.4.tgz#268de7ba2650d697d748f1692a78197a88886e9d" + integrity sha512-FpDAlPzpJ3jlZiNEahRs584FS3jOSQafgj4cC9DmAYPct6uMZDLY625+eErRd43G35vGDrNq3i7b4aYUQ/Bxqw== + dependencies: + "@types/cheerio" "^0.22.8" + cheerio "0.22.0" + +tslib@^2.3.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" + integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +unbzip2-stream@^1.0.9: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA== + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ== + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +unzipper@^0.10.11: + version "0.10.14" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1" + integrity sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA== + dependencies: + prepend-http "^1.0.1" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== + dependencies: + prepend-http "^2.0.0" + +url-to-options@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" + integrity sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A== + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^3.0.1, uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vendors@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" + integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + +which@^1.2.9, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + integrity sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q== + +worker-rpc@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" + integrity sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg== + dependencies: + microevent.ts "~0.1.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + +xmlbuilder@^13.0.0: + version "13.0.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" + integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ== + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yamljs@^0.2.1: + version "0.2.10" + resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.2.10.tgz#481cc7c25ca73af59f591f0c96e3ce56c757a40f" + integrity sha512-sbkbOosewjeRmJ23Hjee1RgTxn+xa7mt4sew3tfD0SdH0LTcswnZC9dhSNq4PIz15roQMzb84DjECyQo5DWIww== + dependencies: + argparse "^1.0.7" + glob "^7.0.5" + +yargs@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-2.3.0.tgz#e900c87250ec5cd080db6009fe3dd63156f1d7fb" + integrity sha512-w48USdbTdaVMcE3CnXsEtSY9zYSN7dTyVnLBgrJF2quA5rLwobC9zixxfexereLGFaxjxtR3oWdydC0qoayakw== + dependencies: + wordwrap "0.0.2" + +yauzl@^2.4.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" diff --git a/website/core/Tutorial.js b/website/core/Tutorial.js deleted file mode 100644 index e8d409d3bb..0000000000 --- a/website/core/Tutorial.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -const React = require('react'); - -const fs = require('fs-extra'); -const path = require('path'); -const CWD = process.cwd(); - -const CompLibrary = require(`${CWD}/node_modules/docusaurus/lib/core/CompLibrary.js`); -const Container = CompLibrary.Container; - -const TutorialSidebar = require(`${CWD}/core/TutorialSidebar.js`); - -function renderDownloadIcon() { - return ( - - ); -} - -class Tutorial extends React.Component { - render() { - const {baseUrl, tutorialID} = this.props; - - const htmlFile = `${CWD}/_tutorials/${tutorialID}.html`; - const normalizedHtmlFile = path.normalize(htmlFile); - - return ( -
- - - - ); - } -} - -module.exports = Tutorial; diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js new file mode 100644 index 0000000000..94581a3810 --- /dev/null +++ b/website/docusaurus.config.js @@ -0,0 +1,174 @@ +import {themes as prismThemes} from 'prism-react-renderer'; +import remarkMath from 'remark-math'; +import rehypeKatex from 'rehype-katex'; + +module.exports={ + "title": "BoTorch", + "tagline": "Bayesian Optimization in PyTorch", + "url": "https://botorch.org", + "baseUrl": "/", + "organizationName": "pytorch", + "projectName": "botorch", + "scripts": [ + "/js/code_block_buttons.js", + "https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js", + ], + "markdown": { + format: "detect" + }, + "stylesheets": [ + "/css/code_block_buttons.css", + { + href: 'https://cdn.jsdelivr.net/npm/katex@0.13.24/dist/katex.min.css', + type: 'text/css', + integrity: + 'sha384-odtC+0UGzzFL/6PNoE8rX/SPcQDXBJ+uRepguP4QkPCm2LBxH3FA3y+fKSiJ+AmM', + crossorigin: 'anonymous', + }, + ], + "favicon": "img/botorch.ico", + "customFields": { + "users": [], + "wrapPagesHTML": true + }, + "onBrokenLinks": "log", + "onBrokenMarkdownLinks": "log", + "presets": [ + [ + "@docusaurus/preset-classic", + { + "docs": { + "showLastUpdateAuthor": true, + "showLastUpdateTime": true, + "editUrl": "https://github.com/pytorch/botorch/edit/main/docs/", + "path": "../docs", + "sidebarPath": "../website-old/sidebars.json", + remarkPlugins: [remarkMath], + rehypePlugins: [rehypeKatex], + }, + "blog": {}, + "theme": { + "customCss": "static/css/custom.css" + }, + // "gtag": { + // "trackingID": "G-CXN3PGE3CC" + // } + } + ] + ], + "plugins": [], + "themeConfig": { + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + }, + "navbar": { + "title": "BoTorch", + "logo": { + "src": "img/botorch.png" + }, + "items": [ + { + "to": "docs/introduction", + "label": "Docs", + "position": "left" + }, + { + "href": "/tutorials/", + "label": "Tutorials", + "position": "left" + }, + { + "href": "/api/", + "label": "API Reference", + "position": "left" + }, + { + "href": "/docs/papers", + "label": "Papers", + "position": "left" + }, + { + "href": "https://github.com/pytorch/botorch", + "label": "GitHub", + "position": "left" + } + ] + }, + "image": "img/botorch.png", + "footer": { + style: 'dark', + "logo": { + alt: "Botorch", + "src": "img/meta_opensource_logo_negative.svg", + }, + links: [ + { + title: 'Docs', + items: [ + { + label: 'Introduction', + to: 'docs/introduction', + }, + { + label: 'Getting Started', + to: 'docs/getting_started', + }, + { + label: 'Tutorials', + to: 'docs/tutorials/', + }, + { + label: 'API Reference', + to: 'api/', // TODO: add link to API reference + }, + { + label: 'Paper', + href: 'https://arxiv.org/abs/1910.06403', + }, + ], + }, + { + title: 'Social', + items: [ + { + label: 'GitHub', + href: 'https://github.com/pytorch/botorch', + }, + { + html: `