class_docstr: Implementation of a Gaussian process model which out-integrates its hyperparameters by
  Markow-Chain-Monte-Carlo (MCMC). If you use this class make sure that you also use an integrated acquisition
  function to integrate over the GP's hyperparameter as proposed by Snoek et al.
  
  This code is based on the implementation of RoBO:
  
  Klein, A. and Falkner, S. and Mansur, N. and Hutter, F.
  RoBO: A Flexible and Robust Bayesian Optimization Framework in Python
  In: NIPS 2017 Bayesian Optimization Workshop
  
  Parameters
  ----------
  configspace : ConfigurationSpace
  kernel : Kernel
      Kernel which is used for the Gaussian process.
  n_mcmc_walkers : int, defaults to 20
      The number of hyperparameter samples. This also determines the number of walker for MCMC sampling as each
      walker will return one hyperparameter sample.
  chain_length : int, defaults to 50
      The length of the MCMC chain. We start `n_mcmc_walkers` walker for `chain_length` steps, and we use the last
      sample in the chain as a hyperparameter sample.
  burning_steps : int, defaults to 50
      The number of burning steps before the actual MCMC sampling starts.
  mcmc_sampler : str, defaults to "emcee"
      Choose a self-tuning MCMC sampler. Can be either ``emcee`` or ``nuts``.
  normalize_y : bool, defaults to True
      Zero mean unit variance normalization of the output values.
  instance_features : dict[str, list[int | float]] | None, defaults to None
      Features (list of int or floats) of the instances (str). The features are incorporated into the X data,
      on which the model is trained on.
  pca_components : float, defaults to 7
      Number of components to keep when using PCA to reduce dimensionality of instance features.
  seed : int
docstr: Performs MCMC sampling to sample hyperparameter configurations from the likelihood and
  trains for each sample a Gaussian process on X and y.
  
  Parameters
  ----------
  X : np.ndarray [#samples, #hyperparameters + #features]
      Input data points.
  Y : np.ndarray [#samples, #objectives]
      The corresponding target values.
  optimize_hyperparameters: boolean
      If set to true, we perform MCMC sampling. Otherwise, we just use the hyperparameter specified in the kernel.
docstr_tok: ["Performs","MCMC","sampling","to","sample","hyperparameter","configurations","from","the","likelihood","and","trains","for","each","sample","a","Gaussian","process","on","X","and","y.","Parameters","--","--","--","--","--","X",":","np.ndarray","[","#","samples",",","#","hyperparameters","+","#","features","]","Input","data","points",".","Y",":","np.ndarray","[","#","samples",",","#","objectives","]","The","corresponding","target","values",".","optimize_hyperparameters",":","boolean","If","set","to","true",",","we","perform","MCMC","sampling",".","Otherwise",",","we","just","use","the","hyperparameter","specified","in","the","kernel","."]
code: def _train(
      self: Self,
      X: np.ndarray,
      y: np.ndarray,
      optimize_hyperparameters: bool = True,
  ) -> Self:
      """Performs MCMC sampling to sample hyperparameter configurations from the likelihood and
      trains for each sample a Gaussian process on X and y.
  
      Parameters
      ----------
      X : np.ndarray [#samples, #hyperparameters + #features]
          Input data points.
      Y : np.ndarray [#samples, #objectives]
          The corresponding target values.
      optimize_hyperparameters: boolean
          If set to true, we perform MCMC sampling. Otherwise, we just use the hyperparameter specified in the kernel.
      """
      X = self._impute_inactive(X)
      if self._normalize_y:
          # A note on normalization for the Gaussian process with MCMC:
          # Scikit-learn uses a different "normalization" than we use in SMAC3. Scikit-learn normalizes the data to
          # have zero mean, while we normalize it to have zero mean unit variance. To make sure the scikit-learn GP
          # behaves the same when we use it directly or indirectly (through the gaussian_process.py file), we
          # normalize the data here. Then, after the individual GPs are fit, we inject the statistics into them, so
          # they unnormalize the data at prediction time.
          y = self._normalize(y)
  
      self._gp = self._get_gaussian_process()
  
      if optimize_hyperparameters:
          self._gp.fit(X, y)
          self._all_priors = self._get_all_priors(
              add_bound_priors=True,
              add_soft_bounds=True
              if self._mcmc_sampler == "nuts"
              else False,
          )
  
          if self._mcmc_sampler == "emcee":
              sampler = emcee.EnsembleSampler(
                  self._n_mcmc_walkers,
                  len(self._kernel.theta),
                  self._ll,
              )
              sampler.random_state = self._rng.get_state()
              # Do a burn-in in the first iteration
              if not self._burned:
                  # Initialize the walkers by sampling from the prior
                  dim_samples = []
  
                  prior: AbstractPrior | list[
                      AbstractPrior
                  ] | None = None
                  for dim, prior in enumerate(self._all_priors):
                      # Always sample from the first prior
                      if isinstance(prior, list):
                          if len(prior) == 0:
                              prior = None
                          else:
                              prior = prior[0]
                      prior = cast(Optional[AbstractPrior], prior)
                      if prior is None:
                          raise NotImplementedError()
                      else:
                          dim_samples.append(
                              prior.sample_from_prior(
                                  self._n_mcmc_walkers
                              ).flatten()
                          )
                  self.p0 = np.vstack(dim_samples).transpose()
  
                  # Run MCMC sampling
                  with warnings.catch_warnings():
                      warnings.filterwarnings(
                          "ignore",
                          r"invalid value encountered in double_scalars.*",
                      )
                      self.p0, _, _ = sampler.run_mcmc(
                          self.p0, self._burning_steps
                      )
  
                  self.burned = True
  
              # Start sampling & save the current position, it will be the start point in the next iteration
              with warnings.catch_warnings():
                  warnings.filterwarnings(
                      "ignore",
                      r"invalid value encountered in double_scalars.*",
                  )
                  self.p0, _, _ = sampler.run_mcmc(
                      self.p0, self._chain_length
                  )
  
              # Take the last samples from each walker
              self._samples = sampler.get_chain()[-1]
          elif self._mcmc_sampler == "nuts":
              # Originally published as:
              # http://www.stat.columbia.edu/~gelman/research/published/nuts.pdf
              # A good explanation of HMC:
              # https://theclevermachine.wordpress.com/2012/11/18/mcmc-hamiltonian-monte-carlo-a-k-a-hybrid-monte-carlo/
              # A good explanation of HMC and NUTS can be found in:
              # https://besjournals.onlinelibrary.wiley.com/doi/full/10.1111/2041-210X.12681
  
              # Do not require the installation of NUTS for SMAC
              # This requires NUTS from https://github.com/mfeurer/NUTS
              import nuts.nuts  # type: ignore
  
              # Perform initial fit to the data to obtain theta0
              if not self.burned:
                  theta0 = self._gp.kernel.theta
                  self._burned = True
              else:
                  theta0 = self.p0
              samples, _, _ = nuts.nuts.nuts6(
                  f=self._ll_w_grad,
                  Madapt=self._burning_steps,
                  M=self._chain_length,
                  theta0=theta0,
                  # Increasing this value results in longer running times
                  delta=0.5,
                  adapt_mass=False,
                  # Rather low max depth to keep the number of required gradient steps low
                  max_depth=10,
                  rng=self._rng,
              )
              indices = [
                  int(np.rint(ind))
                  for ind in np.linspace(
                      start=0, stop=len(samples) - 1, num=10
                  )
              ]
              self._samples = samples[indices]
  
              assert self._samples is not None
              self.p0 = self._samples.mean(axis=0)
          else:
              raise ValueError(self._mcmc_sampler)
  
          if self._average_samples:
              assert self._samples is not None
              self._samples = [self._samples.mean(axis=0)]  # type: ignore
  
      else:
          self._samples = self._gp.kernel.theta
          self._samples = [self._samples]  # type: ignore
  
      self._models = []
  
      assert self._samples is not None
      for sample in self._samples:
          if (sample < -50).any():
              sample[sample < -50] = -50
          if (sample > 50).any():
              sample[sample > 50] = 50
  
          # Instantiate a GP for each hyperparameter configuration
          kernel = deepcopy(self._kernel)
          kernel.theta = sample
          model = GaussianProcess(
              configspace=self._configspace,
              kernel=kernel,
              normalize_y=False,
              seed=self._rng.randint(low=0, high=10000),
          )
          try:
              model._train(X, y, optimize_hyperparameters=False)
              self._models.append(model)
          except np.linalg.LinAlgError:
              pass
  
      if len(self._models) == 0:
          kernel = deepcopy(self._kernel)
          kernel.theta = self.p0
          model = GaussianProcess(
              configspace=self._configspace,
              kernel=kernel,
              normalize_y=False,
              seed=self._rng.randint(low=0, high=10000),
          )
          model._train(X, y, optimize_hyperparameters=False)
          self._models.append(model)
  
      if self._normalize_y:
          # Inject the normalization statistics into the individual models. Setting normalize_y to True makes the
          # individual GPs unnormalize the data at predict time.
          for model in self._models:
              model._normalize_y = True
              model.mean_y_ = self.mean_y_
              model.std_y_ = self.std_y_
  
      self._is_trained = True
      return self
  
imports: from __future__ import annotations
  from typing import Any, Optional, TypeVar, cast
  import logging
  import warnings
  from copy import deepcopy
  import emcee
  import numpy
  from ConfigSpace import ConfigurationSpace
  from sklearn.gaussian_process import GaussianProcessRegressor
  from sklearn.gaussian_process.kernels import Kernel
  from smac.model.gaussian_process.abstract_gaussian_process import AbstractGaussianProcess
  from smac.model.gaussian_process.gaussian_process import GaussianProcess
  from smac.model.gaussian_process.priors.abstract_prior import AbstractPrior
label_desc: Use image node_id 4 for calling the MCMCGaussianProcess obj's underlying member method code with example usage: obj._train(X, y, optimize_hyperparameters) and returns: self