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
from typing import Union
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: Union[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
)