class_docstr: Abstract base class for models based on GPyTorch models.
  
  The easiest way to use this is to subclass a model from a GPyTorch model
  class (e.g. an `ExactGP`) and this `GPyTorchModel`. See e.g. `SingleTaskGP`.
  
  :meta private:
class_docstr_tok: ["Abstract","base","class","for","models","based","on","GPyTorch","models",".","The","easiest","way","to","use","this","is","to","subclass","a","model","from","a","GPyTorch","model","class","(","e.g",".","an","`","ExactGP","`",")","and","this","`","GPyTorchModel","`",".","See","e.g",".","`","SingleTaskGP","`",".",":","meta","private",":"]
docstr: Condition the model on new observations.
  
  Args:
      X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of
          the feature space, `n'` is the number of points per batch, and
          `batch_shape` is the batch shape (must be compatible with the
          batch shape of the model).
      Y: A `batch_shape' x n x m`-dim Tensor, where `m` is the number of
          model outputs, `n'` is the number of points per batch, and
          `batch_shape'` is the batch shape of the observations.
          `batch_shape'` must be broadcastable to `batch_shape` using
          standard broadcasting semantics. If `Y` has fewer batch dimensions
          than `X`, its is assumed that the missing batch dimensions are
          the same for all `Y`.
  
  Returns:
      A `Model` object of the same type, representing the original model
      conditioned on the new observations `(X, Y)` (and possibly noise
      observations passed in via kwargs).
  
  Example:
      >>> train_X = torch.rand(20, 2)
      >>> train_Y = torch.sin(train_X[:, 0]) + torch.cos(train_X[:, 1])
      >>> model = SingleTaskGP(train_X, train_Y)
      >>> new_X = torch.rand(5, 2)
      >>> new_Y = torch.sin(new_X[:, 0]) + torch.cos(new_X[:, 1])
      >>> model = model.condition_on_observations(X=new_X, Y=new_Y)
docstr_tok: ["Condition","the","model","on","new","observations",".","Args",":","X",":","A","`","batch_shape","x","n","'","x","d","`","-dim","Tensor",",","where","`","d","`","is","the","dimension","of","the","feature","space",",","`","n","'","`","is","the","number","of","points","per","batch",",","and","`","batch_shape","`","is","the","batch","shape","(","must","be","compatible","with","the","batch","shape","of","the","model",")",".","Y",":","A","`","batch_shape","'","x","n","x","m","`","-dim","Tensor",",","where","`","m","`","is","the","number","of","model","outputs",",","`","n","'","`","is","the","number","of","points","per","batch",",","and","`","batch_shape","'","`","is","the","batch","shape","of","the","observations",".","`","batch_shape","'","`","must","be","broadcastable","to","`","batch_shape","`","using","standard","broadcasting","semantics",".","If","`","Y","`","has","fewer","batch","dimensions","than","`","X","`",",","its","is","assumed","that","the","missing","batch","dimensions","are","the","same","for","all","`","Y","`",".","Returns",":","A","`","Model","`","object","of","the","same","type",",","representing","the","original","model","conditioned","on","the","new","observations","`","(","X",",","Y",")","`","(","and","possibly","noise","observations","passed","in","via","kwargs",")",".","Example",":",">",">",">","train_X","=","torch.rand","(","20",",","2",")",">",">",">","train_Y","=","torch.sin","(","train_X","[",":",",","0","]",")","+","torch.cos","(","train_X","[",":",",","1","]",")",">",">",">","model","=","SingleTaskGP","(","train_X",",","train_Y",")",">",">",">","new_X","=","torch.rand","(","5",",","2",")",">",">",">","new_Y","=","torch.sin","(","new_X","[",":",",","0","]",")","+","torch.cos","(","new_X","[",":",",","1","]",")",">",">",">","model","=","model.condition_on_observations","(","X=new_X",",","Y=new_Y",")"]
code: def condition_on_observations(
      self, X: Tensor, Y: Tensor, **kwargs: Any
  ) -> Model:
      r"""Condition the model on new observations.
  
      Args:
          X: A `batch_shape x n' x d`-dim Tensor, where `d` is the dimension of
              the feature space, `n'` is the number of points per batch, and
              `batch_shape` is the batch shape (must be compatible with the
              batch shape of the model).
          Y: A `batch_shape' x n x m`-dim Tensor, where `m` is the number of
              model outputs, `n'` is the number of points per batch, and
              `batch_shape'` is the batch shape of the observations.
              `batch_shape'` must be broadcastable to `batch_shape` using
              standard broadcasting semantics. If `Y` has fewer batch dimensions
              than `X`, its is assumed that the missing batch dimensions are
              the same for all `Y`.
  
      Returns:
          A `Model` object of the same type, representing the original model
          conditioned on the new observations `(X, Y)` (and possibly noise
          observations passed in via kwargs).
  
      Example:
          >>> train_X = torch.rand(20, 2)
          >>> train_Y = torch.sin(train_X[:, 0]) + torch.cos(train_X[:, 1])
          >>> model = SingleTaskGP(train_X, train_Y)
          >>> new_X = torch.rand(5, 2)
          >>> new_Y = torch.sin(new_X[:, 0]) + torch.cos(new_X[:, 1])
          >>> model = model.condition_on_observations(X=new_X, Y=new_Y)
      """
      Yvar = kwargs.pop("noise", None)
  
      if hasattr(self, "outcome_transform"):
          # pass the transformed data to get_fantasy_model below
          # (unless we've already trasnformed if BatchedMultiOutputGPyTorchModel)
          if not isinstance(self, BatchedMultiOutputGPyTorchModel):
              # `noise` is assumed to already be outcome-transformed.
              Y, _ = self.outcome_transform(Y, Yvar)
      # validate using strict=False, since we cannot tell if Y has an explicit
      # output dimension
      self._validate_tensor_args(X=X, Y=Y, Yvar=Yvar, strict=False)
      if Y.size(-1) == 1:
          Y = Y.squeeze(-1)
          if Yvar is not None:
              kwargs.update({"noise": Yvar.squeeze(-1)})
      # get_fantasy_model will properly copy any existing outcome transforms
      # (since it deepcopies the original model)
  
      return self.get_fantasy_model(inputs=X, targets=Y, **kwargs)
  
imports: from __future__ import annotations
  import itertools
  import warnings
  from abc import ABC
  from copy import deepcopy
  from typing import Any, List, Optional, Tuple, TYPE_CHECKING, Union
  import torch
  from botorch.acquisition.objective import PosteriorTransform
  from botorch.exceptions.errors import BotorchTensorDimensionError, InputDataError
  from botorch.exceptions.warnings import _get_single_precision_warning, BotorchTensorDimensionWarning
  from botorch.models.model import Model, ModelList
  from botorch.models.utils import _make_X_full, add_output_dim, gpt_posterior_settings, mod_batch_shape, multioutput_to_batch_mode_transform
  from botorch.posteriors.fully_bayesian import GaussianMixturePosterior
  from botorch.posteriors.gpytorch import GPyTorchPosterior
  from botorch.utils.multitask import separate_mtmvn
  from botorch.utils.transforms import is_ensemble
  from gpytorch.distributions import MultitaskMultivariateNormal, MultivariateNormal
  from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood
  from linear_operator.operators import BlockDiagLinearOperator, CatLinearOperator
  from torch import Tensor
label_desc: Use image node_id 5 for calling the GPyTorchModel obj's underlying member method code with example usage: obj.condition_on_observations(X, Y) and returns: self