class_docstr: Probit GP for preference learning with Laplace approximation
  
  A probit-likelihood GP that learns via pairwise comparison data, using a
  Laplace approximation of the posterior of the estimated utility values. By
  default it uses a scaled RBF kernel.
  
  Implementation is based on [Chu2005preference]_.
  Also see [Brochu2010tutorial]_ for additional reference.
  
  Note that in [Chu2005preference]_ the likelihood of a pairwise comparison
  is :math:`\left(\frac{f(x_1) - f(x_2)}{\sqrt{2}\sigma}\right)`, i.e. a scale is
  used in the denominator. To maintain consistency with usage of kernels
  elsewhere in BoTorch, we instead do not include :math:`\sigma` in the code
  (implicitly setting it to 1) and use ScaleKernel to scale the function.
  
  In the example below, the user/decision maker has stated that they prefer
  the first item over the second item and the third item over the second item,
  generating comparisons [0, 1] and [2, 1].
  Example:
      >>> from botorch.models import PairwiseGP
      >>> import torch
      >>> datapoints = torch.Tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
      >>> comparisons = torch.Tensor([[0, 1], [2, 1]])
      >>> model = PairwiseGP(datapoints, comparisons)
class_docstr_tok: ["Probit","GP","for","preference","learning","with","Laplace","approximation","A","probit-likelihood","GP","that","learns","via","pairwise","comparison","data",",","using","a","Laplace","approximation","of","the","posterior","of","the","estimated","utility","values",".","By","default","it","uses","a","scaled","RBF","kernel",".","Implementation","is","based","on","[","Chu2005preference","]","_",".","Also","see","[","Brochu2010tutorial","]","_","for","additional","reference",".","Note","that","in","[","Chu2005preference","]","_","the","likelihood","of","a","pairwise","comparison","is",":","math",":","`","\\left","(","\\frac","{","f","(","x_1",")","-","f","(","x_2",")","}","{","\\sqrt","{","2","}","\\sigma","}","\\right",")","`",",","i.e",".","a","scale","is","used","in","the","denominator",".","To","maintain","consistency","with","usage","of","kernels","elsewhere","in","BoTorch",",","we","instead","do","not","include",":","math",":","`","\\sigma","`","in","the","code","(","implicitly","setting","it","to","1",")","and","use","ScaleKernel","to","scale","the","function",".","In","the","example","below",",","the","user\/decision","maker","has","stated","that","they","prefer","the","first","item","over","the","second","item","and","the","third","item","over","the","second","item",",","generating","comparisons","[","0",",","1","]","and","[","2",",","1","]",".","Example",":",">",">",">","from","botorch.models","import","PairwiseGP",">",">",">","import","torch",">",">",">","datapoints","=","torch.Tensor","(","[","[","1",",","2",",","3","]",",","[","4",",","5",",","6","]",",","[","7",",","8",",","9","]","]",")",">",">",">","comparisons","=","torch.Tensor","(","[","[","0",",","1","]",",","[","2",",","1","]","]",")",">",">",">","model","=","PairwiseGP","(","datapoints",",","comparisons",")"]
docstr: Calculate a posterior or prior prediction.
  
  During training mode, forward implemented solely for gradient-based
  hyperparam opt. Essentially what it does is to re-calculate the utility
  f using its analytical form at f_map so that we are able to obtain
  gradients of the hyperparameters.
  
  Args:
      datapoints: A `batch_shape x n x d` Tensor,
          should be the same as self.datapoints during training
  
  Returns:
      A MultivariateNormal object, being one of the followings:
          1. Posterior centered at MAP points for training data (training mode)
          2. Prior predictions (prior mode)
          3. Predictive posterior (eval mode)
docstr_tok: ["Calculate","a","posterior","or","prior","prediction",".","During","training","mode",",","forward","implemented","solely","for","gradient-based","hyperparam","opt",".","Essentially","what","it","does","is","to","re-calculate","the","utility","f","using","its","analytical","form","at","f_map","so","that","we","are","able","to","obtain","gradients","of","the","hyperparameters",".","Args",":","datapoints",":","A","`","batch_shape","x","n","x","d","`","Tensor",",","should","be","the","same","as","self.datapoints","during","training","Returns",":","A","MultivariateNormal","object",",","being","one","of","the","followings",":","1",".","Posterior","centered","at","MAP","points","for","training","data","(","training","mode",")","2",".","Prior","predictions","(","prior","mode",")","3",".","Predictive","posterior","(","eval","mode",")"]
code: def forward(self, datapoints: Tensor) -> MultivariateNormal:
      r"""Calculate a posterior or prior prediction.
  
      During training mode, forward implemented solely for gradient-based
      hyperparam opt. Essentially what it does is to re-calculate the utility
      f using its analytical form at f_map so that we are able to obtain
      gradients of the hyperparameters.
  
      Args:
          datapoints: A `batch_shape x n x d` Tensor,
              should be the same as self.datapoints during training
  
      Returns:
          A MultivariateNormal object, being one of the followings:
              1. Posterior centered at MAP points for training data (training mode)
              2. Prior predictions (prior mode)
              3. Predictive posterior (eval mode)
      """
      # Training mode: optimizing
      if self.training:
          if self._has_no_data():
              raise RuntimeError(
                  "datapoints and comparisons cannot be None in training mode. "
                  "Call .eval() for prior predictions, "
                  "or call .set_train_data() to add training data."
              )
  
          if datapoints is not self.unconsolidated_datapoints:
              raise RuntimeError("Must train on training data")
  
          # We pass in the untransformed datapoints into set_train_data
          # as we will be setting self.datapoints as the untransformed datapoints
          # self.transform_inputs will be called inside before calling _update()
          self.set_train_data(
              datapoints=datapoints,
              comparisons=self.unconsolidated_comparisons,
              update_model=True,
          )
  
          transformed_dp = self.transform_inputs(self.datapoints)
  
          hl = self.likelihood_hess
          covar = self.covar
  
          # Apply matrix inversion lemma on eq. in page 27 of [Brochu2010tutorial]_
          # (A + B)^-1 = A^-1 - A^-1 @ (I + BA^-1)^-1 @ BA^-1
          # where A = covar_inv, B = hl
          hl_cov = hl @ covar
          eye = torch.eye(
              hl_cov.size(-1),
              dtype=self.datapoints.dtype,
              device=self.datapoints.device,
          ).expand(hl_cov.shape)
          hl_cov_I = hl_cov + eye  # add I to hl_cov
          output_covar = covar - covar @ torch.linalg.solve(
              hl_cov_I, hl_cov
          )
          output_mean = self.utility
  
      # Prior mode
      elif settings.prior_mode.on() or self._has_no_data():
          transformed_new_dp = self.transform_inputs(datapoints)
          # if we don't have any data yet, use prior GP to make predictions
          output_mean, output_covar = self._prior_predict(
              transformed_new_dp
          )
  
      # Posterior mode
      else:
          transformed_dp = self.transform_inputs(self.datapoints)
          transformed_new_dp = self.transform_inputs(datapoints).to(
              transformed_dp
          )
  
          # self.utility might be None if exception was raised and _update
          # was failed to be called during hyperparameter optimization
          # procedures (e.g., fit_gpytorch_mll_scipy)
          if self.utility is None:
              self._update(transformed_dp)
  
          if self.pred_cov_fac_need_update:
              self._update_utility_derived_values()
  
          X, X_new = self._transform_batch_shape(
              transformed_dp, transformed_new_dp
          )
          covar_chol, _ = self._transform_batch_shape(
              self.covar_chol, X_new
          )
          hl, _ = self._transform_batch_shape(
              self.likelihood_hess, X_new
          )
          hlcov_eye, _ = self._transform_batch_shape(
              self.hlcov_eye, X_new
          )
  
          # otherwise compute predictive mean and covariance
          covar_xnew_x = self._calc_covar(X_new, X)
          covar_x_xnew = covar_xnew_x.transpose(-1, -2)
          covar_xnew = self._calc_covar(X_new, X_new)
          p = self.utility - self._prior_mean(X)
  
          covar_inv_p = torch.cholesky_solve(
              p.unsqueeze(-1), covar_chol
          )
          pred_mean = (covar_xnew_x @ covar_inv_p).squeeze(-1)
          pred_mean = pred_mean + self._prior_mean(X_new)
  
          # Using the terminology from [Brochu2010tutorial]_ page 27:
          # hl = C; hlcov_eye = CK + I; k = covar_x_xnew
          #
          # To compute the predictive covariance, one term we need is
          # k^T (K + C^{-1})^{-1} k.
          # Rather than performing two matrix inversions, we can compute this
          # in a more efficient and numerically stable way by using
          # fac = hlcov_eye^-1 @ hl @ covar_x_xnew
          #     = (CK + I)^-1 @ C @ k
          #     = (K + C^-1)^{-1}
          # This is the substitution method.
          fac = torch.linalg.solve(hlcov_eye, hl @ covar_x_xnew)
          pred_covar = covar_xnew - (covar_xnew_x @ fac)
  
          output_mean, output_covar = pred_mean, pred_covar
  
      scale = (
          self.covar_module.outputscale.unsqueeze(-1)
          .unsqueeze(-1)
          .detach()
      )
      post = MultivariateNormal(
          mean=output_mean,
          # output_covar is sometimes non-PSD
          # perform a cholesky decomposition to check and amend
          covariance_matrix=RootLinearOperator(
              _scaled_psd_safe_cholesky(
                  matrix=output_covar,
                  scale=scale,
                  jitter=self._jitter,
              )
          ),
      )
      return post
  
imports: from __future__ import annotations
  import warnings
  from copy import deepcopy
  from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
  import numpy
  import torch
  from botorch.acquisition.objective import PosteriorTransform
  from botorch.exceptions import UnsupportedError
  from botorch.exceptions.warnings import _get_single_precision_warning, InputDataWarning
  from botorch.models.likelihoods.pairwise import PairwiseLikelihood, PairwiseProbitLikelihood
  from botorch.models.model import FantasizeMixin, Model
  from botorch.models.transforms.input import InputTransform
  from botorch.models.utils.assorted import consolidate_duplicates
  from botorch.posteriors.gpytorch import GPyTorchPosterior
  from botorch.posteriors.posterior import Posterior
  from gpytorch import settings
  from gpytorch.constraints import GreaterThan, Interval
  from gpytorch.distributions.multivariate_normal import MultivariateNormal
  from gpytorch.kernels.rbf_kernel import RBFKernel
  from gpytorch.kernels.scale_kernel import ScaleKernel
  from gpytorch.means.constant_mean import ConstantMean
  from gpytorch.mlls import MarginalLogLikelihood
  from gpytorch.models.gp import GP
  from gpytorch.priors.smoothed_box_prior import SmoothedBoxPrior
  from gpytorch.priors.torch_priors import GammaPrior
  from linear_operator.operators import LinearOperator, RootLinearOperator
  from linear_operator.utils.cholesky import psd_safe_cholesky
  from linear_operator.utils.errors import NotPSDError
  from scipy import optimize
  from torch import float32, float64, Tensor
  from torch.nn.modules.module import _IncompatibleKeys
label_desc: Use image node_id 23 for calling the PairwiseGP obj's underlying member method code with example usage: obj.forward(datapoints) and returns: post