code: def trace_model_execution(
      model: Model, reapply_on_copied_layers: bool = False
  ) -> tuple[
      list[Layer],
      list[tuple[Layer, list[Tensor], list[Tensor]]],
      list[Tensor],
  ]:
      """
      Trace and linearize excecution of a model and it's possible containers.
      Return a triple with all layers, a list with a linearized execution
      with (layer, input_tensors, output_tensors), and, possible regenerated,
      outputs of the exectution.
  
      :param model: A kera model.
      :param reapply_on_copied_layers: If the execution needs to be linearized,
        reapply with copied layers. Might be slow. Prevents changes of the
        original layer's node lists.
      """
  
      # Get all layers in model.
      layers: list[Layer] = get_model_layers(model)
  
      # Check if some layers are containers.
      # Ignoring the outermost container, i.e. the passed model.
      contains_container: bool = any(
          l is not model and ichecks.is_module(l) for l in layers
      )
  
      outputs: list[Tensor]
  
      # If so rebuild the graph, otherwise recycle computations,
      # and create executed node list. (Keep track of paths?)
      if contains_container is True:
          # When containers/models are used as layers, then layers
          # inside the container/model do not keep track of nodes.
          # This makes it impossible to iterate of the nodes list and
          # recover the input output tensors. (see else clause)
          #
          # To recover the computational graph we need to re-apply it.
          # This implies that the tensors-object we use for the forward
          # pass are different to the passed model. This it not the case
          # for the else clause.
          #
          # Note that reapplying the model does only change the inbound
          # and outbound nodes of the model itself. We copy the model
          # so the passed model should not be affected from the
          # reapplication.
  
          # For each layer in the model (in forward order),
          # this list tracks the tensors going in and out of it
          executed_nodes: list[
              tuple[Layer, list[Tensor], list[Tensor]]
          ] = []
  
          # Monkeypatch the call function in all the used layer classes.
          monkey_patches: list[tuple[Layer, Callable]] = [
              (layer, layer.call) for layer in layers
          ]
          try:
  
              def patch(self, method: Callable):
                  if hasattr(method, "__patched__") is True:
                      raise Exception(
                          "Should not happen as we patch objects, not classes."
                      )
  
                  def patched_fn(*args, **kwargs):
                      input_tensors = args[0]
                      output_tensors = method(*args, **kwargs)
                      executed_nodes.append(
                          (self, input_tensors, output_tensors)
                      )
                      return output_tensors
  
                  patched_fn.__patched__ = True  # type: ignore
                  return patched_fn
  
              # Apply the patches.
              for layer in layers:
                  layer.call = patch(layer, layer.call)
  
              # Trigger reapplication of model.
              model_copy: Model = kmodels.Model(
                  inputs=model.inputs, outputs=model.outputs
              )
              outputs = ibackend.to_list(model_copy(model.inputs))
          finally:
              # Revert the monkey patches
              for layer, old_method in monkey_patches:
                  layer.call = old_method
  
          # Now we have the problem that all the tensors
          # do not have a keras_history attribute as they are not part
          # of any node. Apply the flat model to get it.
  
          tensor_mapping: dict[Tensor, Tensor] = {
              tmp: tmp for tmp in model.inputs
          }
          layer_mapping: dict[Layer, Layer]
          new_executed_nodes: list[
              tuple[Layer, list[Tensor], list[Tensor]]
          ] = []
  
          if reapply_on_copied_layers is True:
              layer_mapping = {
                  layer: copy_layer(layer) for layer in layers
              }
          else:
              layer_mapping = {layer: layer for layer in layers}
  
          for layer, Xs, Ys in executed_nodes:
              layer = layer_mapping[layer]
              Xs, Ys = ibackend.to_list(Xs), ibackend.to_list(Ys)
  
              if isinstance(layer, klayers.InputLayer):
                  # Special case. Do nothing.
                  new_Xs, new_Ys = Xs, Ys
              else:
                  new_Xs = [tensor_mapping[x] for x in Xs]
                  new_Ys = ibackend.to_list(
                      ibackend.apply(layer, new_Xs)
                  )
  
              # Update values of Ys in tensor_mapping with new_Ys
              tensor_mapping.update(dict(zip(Ys, new_Ys)))
              new_executed_nodes.append((layer, new_Xs, new_Ys))
  
          layers = [layer_mapping[layer] for layer in layers]
          outputs = [tensor_mapping[x] for x in outputs]
          executed_nodes = new_executed_nodes
      else:
          # Easy and safe way.
          # For each layer in the model (in reverse order),
          # this list tracks the tensors going in and out of it
          reverse_executed_nodes: list[
              tuple[Layer, list[Tensor], list[Tensor]]
          ]
          reverse_executed_nodes = [
              (
                  node.outbound_layer,
                  ibackend.to_list(node.input_tensors),
                  ibackend.to_list(node.output_tensors),
              )
              for depth in sorted(model._nodes_by_depth.keys())
              for node in model._nodes_by_depth[depth]
          ]
          outputs = model.outputs
  
          executed_nodes = list(reversed(reverse_executed_nodes))
  
      # `executed_nodes` potentially contains nodes that are not part
      # of the final execution graph.
      # E.g. if a layer was also applied outside of the model. Then its
      # node list contains nodes that do not contribute to the model's output.
      # Those nodes are filtered here.
      used_as_input = [id(out) for out in outputs]
      tmp = []
      for l, Xs, Ys in reversed(executed_nodes):
          if all(id(Y) in used_as_input for Y in Ys):
              used_as_input += [id(X) for X in Xs]
              tmp.append((l, Xs, Ys))
      executed_nodes = list(reversed(tmp))
  
      return layers, executed_nodes, outputs
  
imports: from __future__ import annotations
  import inspect
  import warnings
  from abc import ABCMeta, abstractmethod
  from typing import Any, Callable
  import numpy
  import tensorflow.keras.backend
  import tensorflow.keras.layers
  import tensorflow.keras.models
  import innvestigate.backend
  import innvestigate.backend.checks
  import innvestigate.layers
  from innvestigate.backend.types import Layer, LayerCheck, Model, NodeDict, OptionalList, ReverseTensorDict, Tensor
label_desc: Use image node_id 15 for calling a global function with example usage: trace_model_execution(model, reapply_on_copied_layers) and returns: layers,  executed_nodes,  outputs, patched_fn, output_tensors