code: def _make_f_and_grad_nonlinear_inequality_constraints(
      f_np_wrapper: Callable, nlc: Callable
  ) -> Tuple[Callable[[Tensor], Tensor], Callable[[Tensor], Tensor]]:
      """
      Create callables for objective + grad for the nonlinear inequality constraints.
      The Scipy interface requires specifying separate callables and we use caching to
      avoid evaluating the same input twice. This caching only works if
      the returned functions are evaluated on the same input in immediate
      sequence (i.e., calling `f_obj(X_1)`, `f_grad(X_1)` will result in a
      single forward pass, while `f_obj(X_1)`, `f_grad(X_2)`, `f_obj(X_1)`
      will result in three forward passes).
      """
  
      def f_obj_and_grad(x):
          obj, grad = f_np_wrapper(x, f=nlc)
          return obj, grad
  
      cache = {"X": None, "obj": None, "grad": None}
  
      def f_obj(X):
          X_c = cache["X"]
          if X_c is None or not np.array_equal(X_c, X):
              cache["X"] = X.copy()
              cache["obj"], cache["grad"] = f_obj_and_grad(X)
          return cache["obj"]
  
      def f_grad(X):
          X_c = cache["X"]
          if X_c is None or not np.array_equal(X_c, X):
              cache["X"] = X.copy()
              cache["obj"], cache["grad"] = f_obj_and_grad(X)
          return cache["grad"]
  
      return f_obj, f_grad
  
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 12 for calling a global function with example usage: _make_f_and_grad_nonlinear_inequality_constraints(f_np_wrapper, nlc) and returns: f_obj,  f_grad, obj,  grad, cache, cache