def _update_pareto_Y(self) -> bool: r"""Update the non-dominated front. Returns: A boolean indicating whether the Pareto frontier has changed. """ # is_non_dominated assumes maximization if self._neg_Y.shape[-2] == 0: pareto_Y = self._neg_Y else: # 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 self.sort: # 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])] if not hasattr(self, "_neg_pareto_Y") or not torch.equal( pareto_Y, self._neg_pareto_Y ): self.register_buffer("_neg_pareto_Y", pareto_Y) return True return False
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 num_outcomes`-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 compute_hypervolume(self) -> Tensor: r"""Compute the hypervolume for the given reference point. This method computes the hypervolume of the non-dominated space and computes the difference between the hypervolume between the ideal point and hypervolume of the non-dominated space. Returns: `(batch_shape)`-dim tensor containing the dominated hypervolume. """ 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, ) 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
def test_expand_ref_point(self): ref_point = torch.tensor([1.0, 2.0], device=self.device) for dtype in (torch.float, torch.double): ref_point = ref_point.to(dtype=dtype) # test non-batch self.assertTrue( torch.equal( _expand_ref_point(ref_point, batch_shape=torch.Size([])), ref_point, )) self.assertTrue( torch.equal( _expand_ref_point(ref_point, batch_shape=torch.Size([3])), ref_point.unsqueeze(0).expand(3, -1), )) # test ref point with wrong shape batch_shape with self.assertRaises(BotorchTensorDimensionError): _expand_ref_point(ref_point.unsqueeze(0), batch_shape=torch.Size([])) with self.assertRaises(BotorchTensorDimensionError): _expand_ref_point( ref_point.unsqueeze(0).expand(3, -1), torch.Size([2]))