code: def qei_candidates_func(
      train_x: "torch.Tensor",
      train_obj: "torch.Tensor",
      train_con: Optional["torch.Tensor"],
      bounds: "torch.Tensor",
      pending_x: Optional["torch.Tensor"],
  ) -> "torch.Tensor":
      """Quasi MC-based batch Expected Improvement (qEI).
  
      Args:
          train_x:
              Previous parameter configurations. A ``torch.Tensor`` of shape
              ``(n_trials, n_params)``. ``n_trials`` is the number of already observed trials
              and ``n_params`` is the number of parameters. ``n_params`` may be larger than the
              actual number of parameters if categorical parameters are included in the search
              space, since these parameters are one-hot encoded.
              Values are not normalized.
          train_obj:
              Previously observed objectives. A ``torch.Tensor`` of shape
              ``(n_trials, n_objectives)``. ``n_trials`` is identical to that of ``train_x``.
              ``n_objectives`` is the number of objectives. Observations are not normalized.
          train_con:
              Objective constraints. A ``torch.Tensor`` of shape ``(n_trials, n_constraints)``.
              ``n_trials`` is identical to that of ``train_x``. ``n_constraints`` is the number of
              constraints. A constraint is violated if strictly larger than 0. If no constraints are
              involved in the optimization, this argument will be :obj:`None`.
          bounds:
              Search space bounds. A ``torch.Tensor`` of shape ``(2, n_params)``. ``n_params`` is
              identical to that of ``train_x``. The first and the second rows correspond to the
              lower and upper bounds for each parameter respectively.
          pending_x:
              Pending parameter configurations. A ``torch.Tensor`` of shape
              ``(n_pending, n_params)``. ``n_pending`` is the number of the trials which are already
              suggested all their parameters but have not completed their evaluation, and
              ``n_params`` is identical to that of ``train_x``.
      Returns:
          Next set of candidates. Usually the return value of BoTorch's ``optimize_acqf``.
  
      """
  
      if train_obj.size(-1) != 1:
          raise ValueError(
              "Objective may only contain single values with qEI."
          )
      if train_con is not None:
          train_y = torch.cat([train_obj, train_con], dim=-1)
  
          is_feas = (train_con <= 0).all(dim=-1)
          train_obj_feas = train_obj[is_feas]
  
          if train_obj_feas.numel() == 0:
              # TODO(hvy): Do not use 0 as the best observation.
              _logger.warning(
                  "No objective values are feasible. Using 0 as the best objective in qEI."
              )
              best_f = torch.zeros(())
          else:
              best_f = train_obj_feas.max()
  
          n_constraints = train_con.size(1)
          objective = ConstrainedMCObjective(
              objective=lambda Z: Z[..., 0],
              constraints=[
                  (lambda Z, i=i: Z[..., -n_constraints + i])
                  for i in range(n_constraints)
              ],
          )
      else:
          train_y = train_obj
  
          best_f = train_obj.max()
  
          objective = None  # Using the default identity objective.
  
      train_x = normalize(train_x, bounds=bounds)
      if pending_x is not None:
          pending_x = normalize(pending_x, bounds=bounds)
  
      model = SingleTaskGP(
          train_x,
          train_y,
          outcome_transform=Standardize(m=train_y.size(-1)),
      )
      mll = ExactMarginalLogLikelihood(model.likelihood, model)
      fit_gpytorch_mll(mll)
  
      acqf = qExpectedImprovement(
          model=model,
          best_f=best_f,
          sampler=_get_sobol_qmc_normal_sampler(256),
          objective=objective,
          X_pending=pending_x,
      )
  
      standard_bounds = torch.zeros_like(bounds)
      standard_bounds[1] = 1
  
      candidates, _ = optimize_acqf(
          acq_function=acqf,
          bounds=standard_bounds,
          q=1,
          num_restarts=10,
          raw_samples=512,
          options={"batch_limit": 5, "maxiter": 200},
          sequential=True,
      )
  
      candidates = unnormalize(candidates.detach(), bounds=bounds)
  
      return candidates
  
imports: from typing import Any
  from typing import Callable
  from typing import Dict
  from typing import Optional
  from typing import Sequence
  from typing import Union
  import warnings
  import numpy
  from packaging import version
  from optuna import logging
  from optuna._experimental import experimental_class
  from optuna._experimental import experimental_func
  from optuna._imports import try_import
  from optuna._transform import _SearchSpaceTransform
  from optuna.distributions import BaseDistribution
  from optuna.samplers import BaseSampler
  from optuna.samplers import RandomSampler
  from optuna.samplers._base import _CONSTRAINTS_KEY
  from optuna.samplers._base import _process_constraints_after_trial
  from optuna.search_space import IntersectionSearchSpace
  from optuna.study import Study
  from optuna.study import StudyDirection
  from optuna.trial import FrozenTrial
  from optuna.trial import TrialState
label_desc: Use image node_id 2 for calling a global function with example usage: qei_candidates_func(train_x, train_obj, train_con, bounds, pending_x) and returns: candidates