class_docstr: The base class for any Gaussian process latent function to be used in conjunction
  with exact inference.
  
  :param torch.Tensor train_inputs: (size n x d) The training features :math:`\mathbf X`.
  :param torch.Tensor train_targets: (size n) The training targets :math:`\mathbf y`.
  :param ~gpytorch.likelihoods.GaussianLikelihood likelihood: The Gaussian likelihood that defines
      the observational distribution. Since we're using exact inference, the likelihood must be Gaussian.
  
  The :meth:`forward` function should describe how to compute the prior latent distribution
  on a given input. Typically, this will involve a mean and kernel function.
  The result must be a :obj:`~gpytorch.distributions.MultivariateNormal`.
  
  Calling this model will return the posterior of the latent Gaussian process when conditioned
  on the training data. The output will be a :obj:`~gpytorch.distributions.MultivariateNormal`.
  
  Example:
      >>> class MyGP(gpytorch.models.ExactGP):
      >>>     def __init__(self, train_x, train_y, likelihood):
      >>>         super().__init__(train_x, train_y, likelihood)
      >>>         self.mean_module = gpytorch.means.ZeroMean()
      >>>         self.covar_module = gpytorch.kernels.ScaleKernel(gpytorch.kernels.RBFKernel())
      >>>
      >>>     def forward(self, x):
      >>>         mean = self.mean_module(x)
      >>>         covar = self.covar_module(x)
      >>>         return gpytorch.distributions.MultivariateNormal(mean, covar)
      >>>
      >>> # train_x = ...; train_y = ...
      >>> likelihood = gpytorch.likelihoods.GaussianLikelihood()
      >>> model = MyGP(train_x, train_y, likelihood)
      >>>
      >>> # test_x = ...;
      >>> model(test_x)  # Returns the GP latent function at test_x
      >>> likelihood(model(test_x))  # Returns the (approximate) predictive posterior distribution at test_x
class_docstr_tok: ["The","base","class","for","any","Gaussian","process","latent","function","to","be","used","in","conjunction","with","exact","inference",".",":","param","torch.Tensor","train_inputs",":","(","size","n","x","d",")","The","training","features",":","math",":","`","\\mathbf","X","`",".",":","param","torch.Tensor","train_targets",":","(","size","n",")","The","training","targets",":","math",":","`","\\mathbf","y","`",".",":","param","~gpytorch.likelihoods.GaussianLikelihood","likelihood",":","The","Gaussian","likelihood","that","defines","the","observational","distribution",".","Since","we","'re","using","exact","inference",",","the","likelihood","must","be","Gaussian",".","The",":","meth",":","`","forward","`","function","should","describe","how","to","compute","the","prior","latent","distribution","on","a","given","input",".","Typically",",","this","will","involve","a","mean","and","kernel","function",".","The","result","must","be","a",":","obj",":","`","~gpytorch.distributions.MultivariateNormal","`",".","Calling","this","model","will","return","the","posterior","of","the","latent","Gaussian","process","when","conditioned","on","the","training","data",".","The","output","will","be","a",":","obj",":","`","~gpytorch.distributions.MultivariateNormal","`",".","Example",":",">",">",">","class","MyGP","(","gpytorch.models.ExactGP",")",":",">",">",">","def","__init__","(","self",",","train_x",",","train_y",",","likelihood",")",":",">",">",">","super","(",")",".__init__","(","train_x",",","train_y",",","likelihood",")",">",">",">","self.mean_module","=","gpytorch.means.ZeroMean","(",")",">",">",">","self.covar_module","=","gpytorch.kernels.ScaleKernel","(","gpytorch.kernels.RBFKernel","(",")",")",">",">",">",">",">",">","def","forward","(","self",",","x",")",":",">",">",">","mean","=","self.mean_module","(","x",")",">",">",">","covar","=","self.covar_module","(","x",")",">",">",">","return","gpytorch.distributions.MultivariateNormal","(","mean",",","covar",")",">",">",">",">",">",">","#","train_x","=","...",";","train_y","=","...",">",">",">","likelihood","=","gpytorch.likelihoods.GaussianLikelihood","(",")",">",">",">","model","=","MyGP","(","train_x",",","train_y",",","likelihood",")",">",">",">",">",">",">","#","test_x","=","...",";",">",">",">","model","(","test_x",")","#","Returns","the","GP","latent","function","at","test_x",">",">",">","likelihood","(","model","(","test_x",")",")","#","Returns","the","(","approximate",")","predictive","posterior","distribution","at","test_x"]
docstr: Returns a new GP model that incorporates the specified inputs and targets as new training data.
  
  Using this method is more efficient than updating with `set_train_data` when the number of inputs is relatively
  small, because any computed test-time caches will be updated in linear time rather than computed from scratch.
  
  .. note::
      If `targets` is a batch (e.g. `b x m`), then the GP returned from this method will be a batch mode GP.
      If `inputs` is of the same (or lesser) dimension as `targets`, then it is assumed that the fantasy points
      are the same for each target batch.
  
  :param torch.Tensor inputs: (`b1 x ... x bk x m x d` or `f x b1 x ... x bk x m x d`) Locations of fantasy
      observations.
  :param torch.Tensor targets: (`b1 x ... x bk x m` or `f x b1 x ... x bk x m`) Labels of fantasy observations.
  :return: An `ExactGP` model with `n + m` training examples, where the `m` fantasy examples have been added
      and all test-time caches have been updated.
  :rtype: ~gpytorch.models.ExactGP
docstr_tok: ["Returns","a","new","GP","model","that","incorporates","the","specified","inputs","and","targets","as","new","training","data",".","Using","this","method","is","more","efficient","than","updating","with","`","set_train_data","`","when","the","number","of","inputs","is","relatively","small",",","because","any","computed","test-time","caches","will","be","updated","in","linear","time","rather","than","computed","from","scratch",".","..","note",":",":","If","`","targets","`","is","a","batch","(","e.g",".","`","b","x","m","`",")",",","then","the","GP","returned","from","this","method","will","be","a","batch","mode","GP",".","If","`","inputs","`","is","of","the","same","(","or","lesser",")","dimension","as","`","targets","`",",","then","it","is","assumed","that","the","fantasy","points","are","the","same","for","each","target","batch",".",":","param","torch.Tensor","inputs",":","(","`","b1","x","...","x","bk","x","m","x","d","`","or","`","f","x","b1","x","...","x","bk","x","m","x","d","`",")","Locations","of","fantasy","observations",".",":","param","torch.Tensor","targets",":","(","`","b1","x","...","x","bk","x","m","`","or","`","f","x","b1","x","...","x","bk","x","m","`",")","Labels","of","fantasy","observations",".",":","return",":","An","`","ExactGP","`","model","with","`","n","+","m","`","training","examples",",","where","the","`","m","`","fantasy","examples","have","been","added","and","all","test-time","caches","have","been","updated",".",":","rtype",":","~gpytorch.models.ExactGP"]
code: def get_fantasy_model(self, inputs, targets, **kwargs):
      """
      Returns a new GP model that incorporates the specified inputs and targets as new training data.
  
      Using this method is more efficient than updating with `set_train_data` when the number of inputs is relatively
      small, because any computed test-time caches will be updated in linear time rather than computed from scratch.
  
      .. note::
          If `targets` is a batch (e.g. `b x m`), then the GP returned from this method will be a batch mode GP.
          If `inputs` is of the same (or lesser) dimension as `targets`, then it is assumed that the fantasy points
          are the same for each target batch.
  
      :param torch.Tensor inputs: (`b1 x ... x bk x m x d` or `f x b1 x ... x bk x m x d`) Locations of fantasy
          observations.
      :param torch.Tensor targets: (`b1 x ... x bk x m` or `f x b1 x ... x bk x m`) Labels of fantasy observations.
      :return: An `ExactGP` model with `n + m` training examples, where the `m` fantasy examples have been added
          and all test-time caches have been updated.
      :rtype: ~gpytorch.models.ExactGP
      """
      if self.prediction_strategy is None:
          raise RuntimeError(
              "Fantasy observations can only be added after making predictions with a model so that "
              "all test independent caches exist. Call the model on some data first!"
          )
  
      model_batch_shape = self.train_inputs[0].shape[:-2]
  
      if not isinstance(inputs, list):
          inputs = [inputs]
  
      inputs = [
          i.unsqueeze(-1) if i.ndimension() == 1 else i for i in inputs
      ]
  
      if not isinstance(
          self.prediction_strategy.train_prior_dist,
          MultitaskMultivariateNormal,
      ):
          data_dim_start = -1
      else:
          data_dim_start = -2
  
      target_batch_shape = targets.shape[:data_dim_start]
      input_batch_shape = inputs[0].shape[:-2]
      tbdim, ibdim = len(target_batch_shape), len(input_batch_shape)
  
      if not (tbdim == ibdim + 1 or tbdim == ibdim):
          raise RuntimeError(
              f"Unsupported batch shapes: The target batch shape ({target_batch_shape}) must have either the "
              f"same dimension as or one more dimension than the input batch shape ({input_batch_shape})"
          )
  
      # Check whether we can properly broadcast batch dimensions
      try:
          torch.broadcast_shapes(model_batch_shape, target_batch_shape)
      except RuntimeError:
          raise RuntimeError(
              f"Model batch shape ({model_batch_shape}) and target batch shape "
              f"({target_batch_shape}) are not broadcastable."
          )
  
      if len(model_batch_shape) > len(input_batch_shape):
          input_batch_shape = model_batch_shape
      if len(model_batch_shape) > len(target_batch_shape):
          target_batch_shape = model_batch_shape
  
      # If input has no fantasy batch dimension but target does, we can save memory and computation by not
      # computing the covariance for each element of the batch. Therefore we don't expand the inputs to the
      # size of the fantasy model here - this is done below, after the evaluation and fast fantasy update
      train_inputs = [
          tin.expand(input_batch_shape + tin.shape[-2:])
          for tin in self.train_inputs
      ]
      train_targets = self.train_targets.expand(
          target_batch_shape + self.train_targets.shape[data_dim_start:]
      )
  
      full_inputs = [
          torch.cat(
              [
                  train_input,
                  input.expand(input_batch_shape + input.shape[-2:]),
              ],
              dim=-2,
          )
          for train_input, input in length_safe_zip(
              train_inputs, inputs
          )
      ]
      full_targets = torch.cat(
          [
              train_targets,
              targets.expand(
                  target_batch_shape + targets.shape[data_dim_start:]
              ),
          ],
          dim=data_dim_start,
      )
  
      try:
          fantasy_kwargs = {"noise": kwargs.pop("noise")}
      except KeyError:
          fantasy_kwargs = {}
  
      full_output = super(ExactGP, self).__call__(
          *full_inputs, **kwargs
      )
  
      # Copy model without copying training data or prediction strategy (since we'll overwrite those)
      old_pred_strat = self.prediction_strategy
      old_train_inputs = self.train_inputs
      old_train_targets = self.train_targets
      old_likelihood = self.likelihood
      self.prediction_strategy = None
      self.train_inputs = None
      self.train_targets = None
      self.likelihood = None
      new_model = deepcopy(self)
      self.prediction_strategy = old_pred_strat
      self.train_inputs = old_train_inputs
      self.train_targets = old_train_targets
      self.likelihood = old_likelihood
  
      new_model.likelihood = old_likelihood.get_fantasy_likelihood(
          **fantasy_kwargs
      )
      new_model.prediction_strategy = (
          old_pred_strat.get_fantasy_strategy(
              inputs,
              targets,
              full_inputs,
              full_targets,
              full_output,
              **fantasy_kwargs,
          )
      )
  
      # if the fantasies are at the same points, we need to expand the inputs for the new model
      if tbdim == ibdim + 1:
          new_model.train_inputs = [
              fi.expand(target_batch_shape + fi.shape[-2:])
              for fi in full_inputs
          ]
      else:
          new_model.train_inputs = full_inputs
      new_model.train_targets = full_targets
  
      return new_model
  
imports: import warnings
  from copy import deepcopy
  import torch
  from ..None import settings
  from ..distributions import MultitaskMultivariateNormal, MultivariateNormal
  from ..likelihoods import _GaussianLikelihoodBase
  from ..utils.generic import length_safe_zip
  from ..utils.warnings import GPInputWarning
  from .exact_prediction_strategies import prediction_strategy
  from .gp import GP
label_desc: Use image node_id 8 for calling the ExactGP obj's underlying member method code with example usage: obj.get_fantasy_model(inputs, targets) and returns: new_model