class_docstr: MC-based batch Noisy Expected Improvement.
  
  This function does not assume a `best_f` is known (which would require
  noiseless observations). Instead, it uses samples from the joint posterior
  over the `q` test points and previously observed points. The improvement
  over previously observed points is computed for each sample and averaged.
  
  `qNEI(X) = E(max(max Y - max Y_baseline, 0))`, where
  `(Y, Y_baseline) ~ f((X, X_baseline)), X = (x_1,...,x_q)`
  
  Example:
      >>> model = SingleTaskGP(train_X, train_Y)
      >>> sampler = SobolQMCNormalSampler(1024)
      >>> qNEI = qNoisyExpectedImprovement(model, train_X, sampler)
      >>> qnei = qNEI(test_X)
class_docstr_tok: ["MC-based","batch","Noisy","Expected","Improvement",".","This","function","does","not","assume","a","`","best_f","`","is","known","(","which","would","require","noiseless","observations",")",".","Instead",",","it","uses","samples","from","the","joint","posterior","over","the","`","q","`","test","points","and","previously","observed","points",".","The","improvement","over","previously","observed","points","is","computed","for","each","sample","and","averaged",".","`","qNEI","(","X",")","=","E","(","max","(","max","Y","-","max","Y_baseline",",","0",")",")","`",",","where","`","(","Y",",","Y_baseline",")","~","f","(","(","X",",","X_baseline",")",")",",","X","=","(","x_1",",","...",",","x_q",")","`","Example",":",">",">",">","model","=","SingleTaskGP","(","train_X",",","train_Y",")",">",">",">","sampler","=","SobolQMCNormalSampler","(","1024",")",">",">",">","qNEI","=","qNoisyExpectedImprovement","(","model",",","train_X",",","sampler",")",">",">",">","qnei","=","qNEI","(","test_X",")"]
docstr: Compute samples at new points, using the cached root decomposition.
  
  Args:
      X: A `batch_shape x q x d`-dim tensor of inputs.
  
  Returns:
      A two-tuple `(samples, obj)`, where `samples` is a tensor of posterior
      samples with shape `sample_shape x batch_shape x q x m`, and `obj` is a
      tensor of MC objective values with shape `sample_shape x batch_shape x q`.
docstr_tok: ["Compute","samples","at","new","points",",","using","the","cached","root","decomposition",".","Args",":","X",":","A","`","batch_shape","x","q","x","d","`","-dim","tensor","of","inputs",".","Returns",":","A","two-tuple","`","(","samples",",","obj",")","`",",","where","`","samples","`","is","a","tensor","of","posterior","samples","with","shape","`","sample_shape","x","batch_shape","x","q","x","m","`",",","and","`","obj","`","is","a","tensor","of","MC","objective","values","with","shape","`","sample_shape","x","batch_shape","x","q","`","."]
code: def _get_samples_and_objectives(
      self, X: Tensor
  ) -> Tuple[Tensor, Tensor]:
      r"""Compute samples at new points, using the cached root decomposition.
  
      Args:
          X: A `batch_shape x q x d`-dim tensor of inputs.
  
      Returns:
          A two-tuple `(samples, obj)`, where `samples` is a tensor of posterior
          samples with shape `sample_shape x batch_shape x q x m`, and `obj` is a
          tensor of MC objective values with shape `sample_shape x batch_shape x q`.
      """
      q = X.shape[-2]
      X_full = torch.cat(
          [match_batch_shape(self.X_baseline, X), X], dim=-2
      )
      # TODO: Implement more efficient way to compute posterior over both training and
      # test points in GPyTorch (https://github.com/cornellius-gp/gpytorch/issues/567)
      posterior = self.model.posterior(
          X_full, posterior_transform=self.posterior_transform
      )
      if not self._cache_root:
          samples_full = super().get_posterior_samples(posterior)
          samples = samples_full[..., -q:, :]
          obj_full = self.objective(samples_full, X=X_full)
          # assigning baseline buffers so `best_f` can be computed in _sample_forward
          self.baseline_obj, obj = (
              obj_full[..., :-q],
              obj_full[..., -q:],
          )
          self.baseline_samples = samples_full[..., :-q, :]
      else:
          # handle one-to-many input transforms
          n_plus_q = X_full.shape[-2]
          n_w = posterior._extended_shape()[-2] // n_plus_q
          q_in = q * n_w
          self._set_sampler(q_in=q_in, posterior=posterior)
          samples = self._get_f_X_samples(
              posterior=posterior, q_in=q_in
          )
          obj = self.objective(samples, X=X_full[..., -q:, :])
  
      return samples, obj
  
imports: from __future__ import annotations
  import math
  from abc import ABC, abstractmethod
  from copy import deepcopy
  from functools import partial
  from typing import Callable, List, Optional, Protocol, Tuple, Union
  import torch
  from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin
  from botorch.acquisition.cached_cholesky import CachedCholeskyMCSamplerMixin
  from botorch.acquisition.objective import ConstrainedMCObjective, IdentityMCObjective, MCAcquisitionObjective, PosteriorTransform
  from botorch.acquisition.utils import compute_best_feasible_objective, prune_inferior_points, repeat_to_match_aug_dim
  from botorch.exceptions.errors import UnsupportedError
  from botorch.models.model import Model
  from botorch.sampling.base import MCSampler
  from botorch.utils.objective import compute_smoothed_feasibility_indicator
  from botorch.utils.transforms import concatenate_pending_points, match_batch_shape, t_batch_mode_transform
  from torch import Tensor
label_desc: Use image node_id 4 for calling the qNoisyExpectedImprovement obj's underlying member method code with example usage: obj._get_samples_and_objectives(X) and returns: samples,  obj