code: def gen_candidates_torch(
      initial_conditions: Tensor,
      acquisition_function: AcquisitionFunction,
      lower_bounds: Optional[Union[float, Tensor]] = None,
      upper_bounds: Optional[Union[float, Tensor]] = None,
      optimizer: Type[Optimizer] = torch.optim.Adam,
      options: Optional[Dict[str, Union[float, str]]] = None,
      callback: Optional[
          Callable[[int, Tensor, Tensor], NoReturn]
      ] = None,
      fixed_features: Optional[Dict[int, Optional[float]]] = None,
      timeout_sec: Optional[float] = None,
  ) -> Tuple[Tensor, Tensor]:
      r"""Generate a set of candidates using a `torch.optim` optimizer.
  
      Optimizes an acquisition function starting from a set of initial candidates
      using an optimizer from `torch.optim`.
  
      Args:
          initial_conditions: Starting points for optimization.
          acquisition_function: Acquisition function to be used.
          lower_bounds: Minimum values for each column of initial_conditions.
          upper_bounds: Maximum values for each column of initial_conditions.
          optimizer (Optimizer): The pytorch optimizer to use to perform
              candidate search.
          options: Options used to control the optimization. Includes
              maxiter: Maximum number of iterations
          callback: A callback function accepting the current iteration, loss,
              and gradients as arguments. This function is executed after computing
              the loss and gradients, but before calling the optimizer.
          fixed_features: This is a dictionary of feature indices to values, where
              all generated candidates will have features fixed to these values.
              If the dictionary value is None, then that feature will just be
              fixed to the clamped value and not optimized. Assumes values to be
              compatible with lower_bounds and upper_bounds!
          timeout_sec: Timeout (in seconds) for optimization. If provided,
              `gen_candidates_torch` will stop after this many seconds and return
              the best solution found so far.
  
      Returns:
          2-element tuple containing
  
          - The set of generated candidates.
          - The acquisition value for each t-batch.
  
      Example:
          >>> qEI = qExpectedImprovement(model, best_f=0.2)
          >>> bounds = torch.tensor([[0., 0.], [1., 2.]])
          >>> Xinit = gen_batch_initial_conditions(
          >>>     qEI, bounds, q=3, num_restarts=25, raw_samples=500
          >>> )
          >>> batch_candidates, batch_acq_values = gen_candidates_torch(
                  initial_conditions=Xinit,
                  acquisition_function=qEI,
                  lower_bounds=bounds[0],
                  upper_bounds=bounds[1],
              )
      """
      start_time = time.monotonic()
      options = options or {}
  
      # if there are fixed features we may optimize over a domain of lower dimension
      if fixed_features:
          subproblem = _remove_fixed_features_from_optimization(
              fixed_features=fixed_features,
              acquisition_function=acquisition_function,
              initial_conditions=initial_conditions,
              lower_bounds=lower_bounds,
              upper_bounds=upper_bounds,
              inequality_constraints=None,
              equality_constraints=None,
              nonlinear_inequality_constraints=None,
          )
  
          # call the routine with no fixed_features
          elapsed = time.monotonic() - start_time
          clamped_candidates, batch_acquisition = gen_candidates_torch(
              initial_conditions=subproblem.initial_conditions,
              acquisition_function=subproblem.acquisition_function,
              lower_bounds=subproblem.lower_bounds,
              upper_bounds=subproblem.upper_bounds,
              optimizer=optimizer,
              options=options,
              callback=callback,
              fixed_features=None,
              timeout_sec=timeout_sec - elapsed
              if timeout_sec
              else None,
          )
          clamped_candidates = (
              subproblem.acquisition_function._construct_X_full(
                  clamped_candidates
              )
          )
          return clamped_candidates, batch_acquisition
      _clamp = partial(
          columnwise_clamp, lower=lower_bounds, upper=upper_bounds
      )
      clamped_candidates = _clamp(initial_conditions).requires_grad_(
          True
      )
      _optimizer = optimizer(
          params=[clamped_candidates], lr=options.get("lr", 0.025)
      )
  
      i = 0
      stop = False
      stopping_criterion = ExpMAStoppingCriterion(**options)
      while not stop:
          i += 1
          with torch.no_grad():
              X = _clamp(clamped_candidates).requires_grad_(True)
  
          loss = -acquisition_function(X).sum()
          grad = torch.autograd.grad(loss, X)[0]
          if callback:
              callback(i, loss, grad)
  
          def assign_grad():
              _optimizer.zero_grad()
              clamped_candidates.grad = grad
              return loss
  
          _optimizer.step(assign_grad)
          stop = stopping_criterion.evaluate(fvals=loss.detach())
          if timeout_sec is not None:
              runtime = time.monotonic() - start_time
              if runtime > timeout_sec:
                  stop = True
                  logger.info(
                      f"Optimization timed out after {runtime} seconds."
                  )
  
      clamped_candidates = _clamp(clamped_candidates)
      with torch.no_grad():
          batch_acquisition = acquisition_function(clamped_candidates)
  
      return clamped_candidates, batch_acquisition
  
imports: from __future__ import annotations
  import time
  import warnings
  from functools import partial
  from typing import Any, Callable, Dict, List, NoReturn, Optional, Tuple, Type, Union
  import numpy
  import torch
  from botorch.acquisition import AcquisitionFunction
  from botorch.exceptions.warnings import OptimizationWarning
  from botorch.generation.utils import _convert_nonlinear_inequality_constraints, _remove_fixed_features_from_optimization
  from botorch.logging import _get_logger
  from botorch.optim.parameter_constraints import _arrayify, make_scipy_bounds, make_scipy_linear_constraints, make_scipy_nonlinear_inequality_constraints, nonlinear_constraint_is_feasible
  from botorch.optim.stopping import ExpMAStoppingCriterion
  from botorch.optim.utils import columnwise_clamp, fix_features
  from botorch.optim.utils.timeout import minimize_with_timeout
  from scipy.optimize import OptimizeResult
  from torch import Tensor
  from torch.optim import Optimizer
label_desc: Use image node_id 2 for calling a global function with example usage: gen_candidates_torch(initial_conditions, acquisition_function, lower_bounds, upper_bounds, optimizer, options, callback, fixed_features, timeout_sec) and returns: clamped_candidates,  batch_acquisition, clamped_candidates,  batch_acquisition, loss