class_docstr: Convenience wrapper for sampling a function from a GP prior.
  
  This wrapper implicitly defines the GP sample as a self-updating function by keeping
  track of the evaluated points and respective base samples used during the
  evaluation.
  
  This does not yet support multi-output models.
class_docstr_tok: ["Convenience","wrapper","for","sampling","a","function","from","a","GP","prior",".","This","wrapper","implicitly","defines","the","GP","sample","as","a","self-updating","function","by","keeping","track","of","the","evaluated","points","and","respective","base","samples","used","during","the","evaluation",".","This","does","not","yet","support","multi-output","models","."]
docstr: Evaluate the GP sample function at a set of points X.
  
  Args:
      X: A `batch_shape x n x d`-dim tensor of points
  
  Returns:
      The value of the GP sample at the `n` points.
docstr_tok: ["Evaluate","the","GP","sample","function","at","a","set","of","points","X.","Args",":","X",":","A","`","batch_shape","x","n","x","d","`","-dim","tensor","of","points","Returns",":","The","value","of","the","GP","sample","at","the","`","n","`","points","."]
code: def forward(self, X: Tensor) -> Tensor:
      r"""Evaluate the GP sample function at a set of points X.
  
      Args:
          X: A `batch_shape x n x d`-dim tensor of points
  
      Returns:
          The value of the GP sample at the `n` points.
      """
      if self.Xs is None:
          X_eval = X  # first time, no previous evaluation points
      else:
          X_eval = torch.cat([self.Xs, X], dim=-2)
      posterior = self._model.posterior(X=X_eval)
      base_sample_shape = posterior.base_sample_shape
      if self._num_outputs == 1:
          # Needed to comply with base sample shape assumptions made here.
          base_sample_shape = base_sample_shape + (1,)
      # re-use old samples
      bs_shape = (
          base_sample_shape[:-2]
          + X.shape[-2:-1]
          + base_sample_shape[-1:]
      )
      with manual_seed(seed=int(self._seed)):
          new_base_samples = torch.randn(
              bs_shape, device=X.device, dtype=X.dtype
          )
      seed = self._seed + 1
      if self.Xs is None:
          base_samples = new_base_samples
      else:
          base_samples = torch.cat(
              [self._base_samples, new_base_samples], dim=-2
          )
      # TODO: Deduplicate repeated evaluations / deal with numerical degeneracies
      # that could lead to non-deterministic evaluations. We could use SVD- or
      # eigendecomposition-based sampling, but we probably don't want to use this
      # by default for performance reasonse.
      Ys = posterior.rsample_from_base_samples(
          torch.Size(),
          base_samples=base_samples.squeeze(-1)
          if self._num_outputs == 1
          else base_samples,
      )
      self.register_buffer("_Xs", X_eval)
      self.register_buffer("_Ys", Ys)
      self.register_buffer("_seed", seed)
      self.register_buffer("_base_samples", base_samples)
      return self.Ys[..., -(X.size(-2)) :, :]
  
imports: from __future__ import annotations
  from copy import deepcopy
  from math import pi
  from typing import List, Optional
  import torch
  from botorch.models.converter import batched_to_model_list
  from botorch.models.deterministic import GenericDeterministicModel
  from botorch.models.model import Model, ModelList
  from botorch.models.model_list_gp_regression import ModelListGP
  from botorch.models.multitask import MultiTaskGP
  from botorch.utils.sampling import manual_seed
  from botorch.utils.transforms import is_ensemble
  from gpytorch.kernels import Kernel, MaternKernel, RBFKernel, ScaleKernel
  from linear_operator.utils.cholesky import psd_safe_cholesky
  from torch import Tensor
  from torch.distributions import MultivariateNormal
  from torch.nn import Module
label_desc: Use image node_id 4 for calling the GPDraw obj's underlying member method code with example usage: obj.forward(X) and returns: self