code: def make_scipy_bounds(
      X: Tensor,
      lower_bounds: Optional[Union[float, Tensor]] = None,
      upper_bounds: Optional[Union[float, Tensor]] = None,
  ) -> Optional[Bounds]:
      r"""Creates a scipy Bounds object for optimziation
  
      Args:
          X: `... x d` tensor
          lower_bounds: Lower bounds on each column (last dimension) of `X`. If
              this is a single float, then all columns have the same bound.
          upper_bounds: Lower bounds on each column (last dimension) of `X`. If
              this is a single float, then all columns have the same bound.
  
      Returns:
          A scipy `Bounds` object if either lower_bounds or upper_bounds is not
          None, and None otherwise.
  
      Example:
          >>> X = torch.rand(5, 2)
          >>> scipy_bounds = make_scipy_bounds(X, 0.1, 0.8)
      """
      if lower_bounds is None and upper_bounds is None:
          return None
  
      def _expand(
          bounds: Union[float, Tensor], X: Tensor, lower: bool
      ) -> Tensor:
          if bounds is None:
              ebounds = torch.full_like(
                  X, float("-inf" if lower else "inf")
              )
          else:
              if not torch.is_tensor(bounds):
                  bounds = torch.tensor(bounds)
              ebounds = bounds.expand_as(X)
          return _arrayify(ebounds).flatten()
  
      lb = _expand(bounds=lower_bounds, X=X, lower=True)
      ub = _expand(bounds=upper_bounds, X=X, lower=False)
      return Bounds(lb=lb, ub=ub, keep_feasible=True)
  
imports: from __future__ import annotations
  from functools import partial
  from typing import Callable, Dict, List, Optional, Tuple, Union
  import numpy
  import torch
  from botorch.exceptions.errors import CandidateGenerationError, UnsupportedError
  from scipy.optimize import Bounds
  from torch import Tensor
label_desc: Use image node_id 1 for calling a global function with example usage: make_scipy_bounds(X, lower_bounds, upper_bounds) and returns: Bounds, None, _arrayify