docstr: 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.
docstr_tok: ["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","."]
code: def _compute_qehvi(
      self, samples: Tensor, X: Optional[Tensor] = 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)
  
imports: from __future__ import annotations
  from abc import ABC, abstractmethod
  from typing import Callable, List, Optional, Union
  import torch
  from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
  from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective, MCMultiOutputObjective
  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 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
label_desc: Use image node_id 2 for calling the qExpectedHypervolumeImprovement obj's underlying member method code with example usage: obj._compute_qehvi(samples, X) and returns: areas_per_segment