class_docstr: 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)
docstr: 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`.
docstr_tok: ["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","`","."]
code: 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
  
imports: from __future__ import annotations
  from abc import ABC, abstractmethod
  from copy import deepcopy
  from math import log
  from typing import Any, Callable, Optional
  import numpy
  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
label_desc: Use image node_id 1 for calling the qLowerBoundMaxValueEntropy obj's underlying member method code with example usage: obj._compute_information_gain(X, mean_M, variance_M, covar_mM) and returns: unknown, acq