code: def execute(
      tapes: Sequence[QuantumTape],
      device: device_type,
      gradient_fn: Optional[Union[Callable, str]] = None,
      interface="auto",
      transform_program=None,
      config=None,
      grad_on_execution="best",
      gradient_kwargs=None,
      cache: Union[bool, dict, Cache] = True,
      cachesize=10000,
      max_diff=1,
      override_shots: int = False,
      expand_fn="device",  # type: ignore
      max_expansion=10,
      device_batch_transform=True,
      device_vjp=False,
  ) -> ResultBatch:
      """New function to execute a batch of tapes on a device in an autodifferentiable-compatible manner. More cases will be added,
      during the project. The current version is supporting forward execution for Numpy and does not support shot vectors.
  
      Args:
          tapes (Sequence[.QuantumTape]): batch of tapes to execute
          device (pennylane.Device): Device to use to execute the batch of tapes.
              If the device does not provide a ``batch_execute`` method,
              by default the tapes will be executed in serial.
          gradient_fn (None or callable): The gradient transform function to use
              for backward passes. If "device", the device will be queried directly
              for the gradient (if supported).
          interface (str): The interface that will be used for classical autodifferentiation.
              This affects the types of parameters that can exist on the input tapes.
              Available options include ``autograd``, ``torch``, ``tf``, ``jax`` and ``auto``.
          transform_program(.TransformProgram): A transform program to be applied to the initial tape.
          config (qml.devices.ExecutionConfig): A datastructure describing the parameters needed to fully describe the execution.
          grad_on_execution (bool, str): Whether the gradients should be computed on the execution or not. Only applies
              if the device is queried for the gradient; gradient transform
              functions available in ``qml.gradients`` are only supported on the backward
              pass. The 'best' option chooses automatically between the two options and is default.
          gradient_kwargs (dict): dictionary of keyword arguments to pass when
              determining the gradients of tapes
          cache (bool, dict, Cache): Whether to cache evaluations. This can result in
              a significant reduction in quantum evaluations during gradient computations.
          cachesize (int): the size of the cache
          max_diff (int): If ``gradient_fn`` is a gradient transform, this option specifies
              the maximum number of derivatives to support. Increasing this value allows
              for higher order derivatives to be extracted, at the cost of additional
              (classical) computational overhead during the backwards pass.
          override_shots (int): The number of shots to use for the execution. If ``False``, then the
              number of shots on the device is used.
          expand_fn (str, function): Tape expansion function to be called prior to device execution.
              Must have signature of the form ``expand_fn(tape, max_expansion)``, and return a
              single :class:`~.QuantumTape`. If not provided, by default :meth:`Device.expand_fn`
              is called.
          max_expansion (int): The number of times the internal circuit should be expanded when
              executed on a device. Expansion occurs when an operation or measurement is not
              supported, and results in a gate decomposition. If any operations in the decomposition
              remain unsupported by the device, another expansion occurs.
          device_batch_transform (bool): Whether to apply any batch transforms defined by the device
              (within :meth:`Device.batch_transform`) to each tape to be executed. The default behaviour
              of the device batch transform is to expand out Hamiltonian measurements into
              constituent terms if not supported on the device.
          device_vjp=False (Optional[bool]): whether or not to use the device provided jacobian
              product if it is available.
  
      Returns:
          list[tensor_like[float]]: A nested list of tape results. Each element in
          the returned list corresponds in order to the provided tapes.
  
      **Example**
  
      Consider the following cost function:
  
      .. code-block:: python
  
          dev = qml.device("lightning.qubit", wires=2)
  
          def cost_fn(params, x):
              ops1 = [qml.RX(params[0], wires=0), qml.RY(params[1], wires=0)]
              measurements1 = [qml.expval(qml.PauliZ(0))]
              tape1 = qml.tape.QuantumTape(ops1, measurements1)
  
              ops2 = [
                  qml.RX(params[2], wires=0),
                  qml.RY(x[0], wires=1),
                  qml.CNOT(wires=(0,1))
              ]
              measurements2 = [qml.probs(wires=0)]
              tape2 = qml.tape.QuantumTape(ops2, measurements2)
  
              tapes = [tape1, tape2]
  
              # execute both tapes in a batch on the given device
              res = qml.execute(tapes, dev, gradient_fn=qml.gradients.param_shift, max_diff=2)
  
              return res[0] + res[1][0] - res[1][1]
  
      In this cost function, two **independent** quantum tapes are being
      constructed; one returning an expectation value, the other probabilities.
      We then batch execute the two tapes, and reduce the results to obtain
      a scalar.
  
      Let's execute this cost function while tracking the gradient:
  
      >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True)
      >>> x = np.array([0.5], requires_grad=True)
      >>> cost_fn(params, x)
      1.93050682
  
      Since the ``execute`` function is differentiable, we can
      also compute the gradient:
  
      >>> qml.grad(cost_fn)(params, x)
      (array([-0.0978434 , -0.19767681, -0.29552021]), array([5.37764278e-17]))
  
      Finally, we can also compute any nth-order derivative. Let's compute the Jacobian
      of the gradient (that is, the Hessian):
  
      >>> x.requires_grad = False
      >>> qml.jacobian(qml.grad(cost_fn))(params, x)
      array([[-0.97517033,  0.01983384,  0.        ],
             [ 0.01983384, -0.97517033,  0.        ],
             [ 0.        ,  0.        , -0.95533649]])
      """
      if logger.isEnabledFor(logging.DEBUG):
          logger.debug(
              """Entry with args=(tapes=%s, device=%s, gradient_fn=%s, interface=%s, grad_on_execution=%s, gradient_kwargs=%s, cache=%s, cachesize=%s, max_diff=%s, override_shots=%s, expand_fn=%s, max_expansion=%s, device_batch_transform=%s) called by=%s""",
              tapes,
              repr(device),
              gradient_fn
              if not (
                  logger.isEnabledFor(qml.logging.TRACE)
                  and inspect.isfunction(gradient_fn)
              )
              else "\n" + inspect.getsource(gradient_fn) + "\n",
              interface,
              grad_on_execution,
              gradient_kwargs,
              cache,
              cachesize,
              max_diff,
              override_shots,
              expand_fn
              if not (
                  logger.isEnabledFor(qml.logging.TRACE)
                  and inspect.isfunction(expand_fn)
              )
              else "\n" + inspect.getsource(expand_fn) + "\n",
              max_expansion,
              device_batch_transform,
              "::L".join(
                  str(i)
                  for i in inspect.getouterframes(
                      inspect.currentframe(), 2
                  )[1][1:3]
              ),
          )
  
      ### Specifying and preprocessing variables ####
  
      if interface == "auto":
          params = []
          for tape in tapes:
              params.extend(tape.get_parameters(trainable_only=False))
          interface = qml.math.get_interface(*params)
      if interface == "jax":
          try:  # pragma: no-cover
              from .interfaces.jax import get_jax_interface_name
          except ImportError as e:  # pragma: no-cover
              raise qml.QuantumFunctionError(  # pragma: no-cover
                  "jax not found. Please install the latest "  # pragma: no-cover
                  "version of jax to enable the 'jax' interface."  # pragma: no-cover
              ) from e  # pragma: no-cover
  
          interface = get_jax_interface_name(tapes)
          # Only need to calculate derivatives with jax when we know it will be executed later.
          if interface in {"jax", "jax-jit"}:
              grad_on_execution = (
                  grad_on_execution
                  if isinstance(gradient_fn, Callable)
                  else False
              )
  
      if (
          device_vjp
          and isinstance(device, qml.Device)
          and "lightning" not in getattr(device, "short_name", "")
      ):
          raise qml.QuantumFunctionError(
              "device provided jacobian products are not compatible with the old device interface."
          )
  
      gradient_kwargs = gradient_kwargs or {}
      config = config or _get_execution_config(
          gradient_fn, grad_on_execution, interface, device, device_vjp
      )
  
      if transform_program is None:
          if isinstance(device, qml.devices.Device):
              transform_program = device.preprocess(config)[0]
          else:
              transform_program = qml.transforms.core.TransformProgram()
  
      if isinstance(cache, bool) and cache:
          # cache=True: create a LRUCache object
          cache = LRUCache(maxsize=cachesize)
          setattr(cache, "_persistent_cache", False)
  
      expand_fn = _preprocess_expand_fn(
          expand_fn, device, max_expansion
      )
  
      # changing this set of conditions causes a bunch of tests to break.
      no_interface_boundary_required = (
          interface is None or gradient_fn in {None, "backprop"}
      )
      device_supports_interface_data = (
          no_interface_boundary_required
          and (
              interface is None
              or gradient_fn == "backprop"
              or getattr(device, "short_name", "") == "default.mixed"
              or "passthru_interface"
              in getattr(device, "capabilities", lambda: {})()
          )
      )
  
      inner_execute = _make_inner_execute(
          device,
          override_shots,
          cache,
          expand_fn,
          config,
          numpy_only=not device_supports_interface_data,
      )
  
      # moved to its own explicit step so it will be easier to remove
      def inner_execute_with_empty_jac(tapes, **_):
          return (inner_execute(tapes), [])
  
      if interface in jpc_interfaces:
          execute_fn = inner_execute
      else:
          execute_fn = inner_execute_with_empty_jac
      #### Executing the configured setup #####
  
      if isinstance(device, qml.devices.Device):
          if not device_batch_transform:
              warnings.warn(
                  "device batch transforms cannot be turned off with the new device interface.",
                  UserWarning,
              )
          tapes, post_processing = transform_program(tapes)
      else:
          # TODO: Remove once old device are removed
          tapes, program_post_processing = transform_program(tapes)
          tapes, program_pre_processing, config = _batch_transform(
              tapes,
              device,
              config,
              override_shots,
              device_batch_transform,
          )
  
          def post_processing(results):
              return program_post_processing(
                  program_pre_processing(results)
              )
  
      if transform_program.is_informative:
          return post_processing(tapes)
  
      # Exiting early if we do not need to deal with an interface boundary
      if no_interface_boundary_required:
          results = inner_execute(tapes)
          return post_processing(results)
  
      _grad_on_execution = False
  
      if (
          device_vjp
          and "lightning" in getattr(device, "short_name", "")
          and interface in jpc_interfaces
      ):
          if (
              INTERFACE_MAP[interface] == "jax"
              and "use_device_state" in gradient_kwargs
          ):
              gradient_kwargs["use_device_state"] = False
          tapes = [expand_fn(t) for t in tapes]
          tapes = _adjoint_jacobian_expansion(
              tapes, grad_on_execution, interface, max_expansion
          )
          jpc = LightningVJPs(device, gradient_kwargs=gradient_kwargs)
  
      elif (
          config.use_device_jacobian_product
          and interface in jpc_interfaces
      ):
          jpc = DeviceJacobianProducts(device, config)
  
      elif config.use_device_gradient:
          jpc = DeviceDerivatives(device, config)
  
          # must be new device if this is specified as true
          _grad_on_execution = config.grad_on_execution
  
          if interface in jpc_interfaces:
              execute_fn = (
                  jpc.execute_and_cache_jacobian
                  if config.grad_on_execution
                  else inner_execute
              )
  
          elif config.grad_on_execution:
  
              def execute_fn(internal_tapes):
                  """A partial function that wraps the execute_and_compute_derivatives method of the device.
  
                  Closure Variables:
                      device: The device to execute on
                      config: the ExecutionConfig that specifies how to perform the simulations.
                  """
                  numpy_tapes = tuple(
                      qml.transforms.convert_to_numpy_parameters(t)
                      for t in internal_tapes
                  )
                  return device.execute_and_compute_derivatives(
                      numpy_tapes, config
                  )
  
              gradient_fn = None
  
          else:
  
              def execute_fn(
                  internal_tapes,
              ) -> Tuple[ResultBatch, Tuple]:
                  """A wrapper around device.execute that adds an empty tuple instead of derivatives.
  
                  Closure Variables:
                      device: the device to execute on
                      config: the ExecutionConfig that specifies how to perform the simulations.
                  """
                  numpy_tapes = tuple(
                      qml.transforms.convert_to_numpy_parameters(t)
                      for t in internal_tapes
                  )
                  return (device.execute(numpy_tapes, config), tuple())
  
              def gradient_fn(internal_tapes):
                  """A partial function that wraps compute_derivatives method of the device.
  
                  Closure Variables:
                      device: the device to execute on
                      config: the ExecutionConfig that specifies how to take the derivative.
                  """
                  numpy_tapes = tuple(
                      qml.transforms.convert_to_numpy_parameters(t)
                      for t in internal_tapes
                  )
                  return device.compute_derivatives(numpy_tapes, config)
  
      elif gradient_fn == "device":
          # gradient function is a device method
  
          # Expand all tapes as per the device's expand function here.
          # We must do this now, prior to the interface, to ensure that
          # decompositions with parameter processing is tracked by the
          # autodiff frameworks.
          tapes = [expand_fn(t) for t in tapes]
  
          jpc = DeviceDerivatives(
              device, config, gradient_kwargs=gradient_kwargs
          )
  
          if gradient_kwargs.get("method", "") == "adjoint_jacobian":
              tapes = _adjoint_jacobian_expansion(
                  tapes, grad_on_execution, interface, max_expansion
              )
  
          _grad_on_execution = grad_on_execution
  
          if interface in jpc_interfaces:
              execute_fn = (
                  jpc.execute_and_cache_jacobian
                  if grad_on_execution
                  else inner_execute
              )
  
          elif grad_on_execution is True or grad_on_execution == "best":
              # replace the forward execution function to return
              # both results and gradients
              def device_execute_and_gradients(
                  internal_tapes, **gradient_kwargs
              ):
                  numpy_tapes = tuple(
                      qml.transforms.convert_to_numpy_parameters(t)
                      for t in internal_tapes
                  )
                  return set_shots(device, override_shots)(
                      device.execute_and_gradients
                  )(numpy_tapes, **gradient_kwargs)
  
              execute_fn = device_execute_and_gradients
              gradient_fn = None
  
          else:
              # need to override to have no cache
              inner_execute = _make_inner_execute(
                  device, override_shots, cache=None
              )
  
              def inner_execute_with_empty_jac(tapes, **_):
                  return (inner_execute(tapes), [])
  
              execute_fn = inner_execute_with_empty_jac
  
              # replace the backward gradient computation
              gradient_fn_with_shots = set_shots(
                  device, override_shots
              )(device.gradients)
              cached_gradient_fn = qml.workflow.cache_execute(
                  gradient_fn_with_shots,
                  cache,
                  pass_kwargs=True,
                  return_tuple=False,
              )
  
              def device_gradient_fn(inner_tapes, **gradient_kwargs):
                  numpy_tapes = tuple(
                      qml.transforms.convert_to_numpy_parameters(t)
                      for t in inner_tapes
                  )
                  return cached_gradient_fn(
                      numpy_tapes, **gradient_kwargs
                  )
  
              gradient_fn = device_gradient_fn
  
      elif grad_on_execution is True:
          # In "forward" mode, gradients are automatically handled
          # within execute_and_gradients, so providing a gradient_fn
          # in this case would have ambiguous behaviour.
          raise ValueError(
              "Gradient transforms cannot be used with grad_on_execution=True"
          )
      elif interface in jpc_interfaces:
          # See autograd.py submodule docstring for explanation for ``cache_full_jacobian``
          cache_full_jacobian = (interface == "autograd") and not cache
  
          # we can have higher order derivatives when the `inner_execute` used to take
          # transform gradients is itself differentiable
          # To make the inner execute itself differentiable, we make it an interface boundary with
          # its own jacobian product class
          # this mechanism unpacks the currently existing recursion
          jpc = TransformJacobianProducts(
              execute_fn,
              gradient_fn,
              gradient_kwargs,
              cache_full_jacobian,
          )
          for _ in range(1, max_diff):
              ml_boundary_execute = _get_ml_boundary_execute(
                  interface, _grad_on_execution
              )
              execute_fn = partial(
                  ml_boundary_execute,
                  execute_fn=execute_fn,
                  jpc=jpc,
                  device=device,
              )
              jpc = TransformJacobianProducts(
                  execute_fn, gradient_fn, gradient_kwargs
              )
  
              if interface == "jax-jit":
                  # no need to use pure callbacks around execute_fn or the jpc when taking
                  # higher order derivatives
                  interface = "jax"
  
      # trainable parameters can only be set on the first pass for jax
      # not higher order passes for higher order derivatives
      if interface in {"jax", "jax-python", "jax-jit"}:
          for tape in tapes:
              params = tape.get_parameters(trainable_only=False)
              tape.trainable_params = qml.math.get_trainable_indices(
                  params
              )
  
      ml_boundary_execute = _get_ml_boundary_execute(
          interface,
          _grad_on_execution,
          config.use_device_jacobian_product,
      )
  
      if interface in jpc_interfaces:
          results = ml_boundary_execute(
              tapes, execute_fn, jpc, device=device
          )
      else:
          results = ml_boundary_execute(
              tapes,
              device,
              execute_fn,
              gradient_fn,
              gradient_kwargs,
              _n=1,
              max_diff=max_diff,
          )
  
      return post_processing(results)
  
file_path: pennylane/pennylane/workflow/execution.py
imports: import inspect
  import warnings
  from functools import wraps, partial
  from typing import Callable, Sequence, Optional, Union, Tuple
  import logging
  from cachetools import LRUCache, Cache
  import pennylane
  from pennylane.tape import QuantumTape
  from pennylane.typing import ResultBatch
  from .set_shots import set_shots
  from .jacobian_products import TransformJacobianProducts, DeviceDerivatives, DeviceJacobianProducts, LightningVJPs
label_desc: Use image node_id 7 for calling a global function with example usage: execute(tapes, device, gradient_fn, interface, transform_program, config, grad_on_execution, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform, device_vjp) and returns: post_processing, inner_execute,  list, post_processing, post_processing, program_post_processing, device, device,  tuple, device, set_shots, inner_execute,  list, cached_gradient_fn