class_docstr: 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.
class_docstr_tok: ["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","."]
docstr: 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`.
docstr_tok: ["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","`","."]
code: def _compute_posterior_statistics(
      self, X: Tensor
  ) -> dict[str, Union[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
  
imports: from __future__ import annotations
  from math import pi
  from typing import Any, Callable, Optional, Tuple, Union
  import torch
  from botorch.acquisition.max_value_entropy_search import qMaxValueEntropy
  from botorch.acquisition.multi_objective.joint_entropy_search import LowerBoundMultiObjectiveEntropySearch
  from botorch.acquisition.multi_objective.monte_carlo import MultiObjectiveMCAcquisitionFunction
  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
label_desc: Use image node_id 2 for calling the qLowerBoundMultiObjectiveMaxValueEntropySearch obj's underlying member method code with example usage: obj._compute_posterior_statistics(X) and returns: posterior_statistics