code: def raise_with_op(
      fgraph: FunctionGraph,
      node: Apply,
      thunk=None,
      exc_info=None,
      storage_map=None,
  ) -> NoReturn:
      """
      Re-raise an exception while annotating the exception object with
      debug info.
  
      Parameters
      ----------
      fgraph
          The `FunctionGraph` that contains `node`.
      node : Apply node
          The Apply node object that resulted in the raised exception.
      thunk :
          An optional thunk.
      exc_info : tuple, optional
          A tuple containing the exception type, exception object and
          associated traceback, as would be returned by a call to
          `sys.exc_info` (which is done if ``None`` is passed).
      storage_map: dict, optional
          storage map of the pytensor function that resulted in the
          raised exception.
  
      Notes
      -----
      This re-raises the exception described by `exc_info` (or the last
      one raised, if `exc_info` is omitted) and annotates the exception
      object with several new members which may be helpful for debugging
      PyTensor graphs. They are:
  
       * ``__op_instance__``: The `Op` that is responsible for the exception
         being raised.
       * ``__thunk_trace__``: A traceback corresponding to the code that
         actually generated the exception, if it is available.
       * ``__applynode_index__``: The index of the `Apply` node corresponding
         to this `Op` in ``op.fgraph.toposort``.
  
      The exception is not annotated if it is of type `KeyboardInterrupt`.
  
      TODO: Make this work with linker defined schedule
      """
      verbosity = config.exception_verbosity
  
      if exc_info is None:
          exc_info = sys.exc_info()
      exc_type, exc_value, exc_trace = exc_info
      if exc_type == KeyboardInterrupt:
          # print a simple traceback from KeyboardInterrupt
          raise exc_value.with_traceback(exc_trace)
  
      trace = getattr(node.outputs[0].tag, "trace", ())
  
      exc_value.__thunk_trace__ = trace
      exc_value.__op_instance__ = node
      topo = fgraph.toposort()
      if node in topo:
          node_index = topo.index(node)
      else:
          node_index = None
      exc_value.__applynode_index__ = node_index
  
      hints = []
      detailed_err_msg = f"\nApply node that caused the error: {node}"
      if exc_value.__applynode_index__ is not None:
          detailed_err_msg += f"\nToposort index: {node_index}"
  
      types = [getattr(ipt, "type", "No type") for ipt in node.inputs]
      detailed_err_msg += f"\nInputs types: {types}\n"
  
      shapes: Union[list, str]
      strides: Union[list, str]
      scalar_values: Union[list, str]
  
      if thunk is not None:
          if hasattr(thunk, "inputs"):
              shapes = [
                  getattr(ipt[0], "shape", "No shapes")
                  for ipt in thunk.inputs
              ]
              strides = [
                  getattr(ipt[0], "strides", "No strides")
                  for ipt in thunk.inputs
              ]
              scalar_values = []
              for ipt in thunk.inputs:
                  if getattr(ipt[0], "size", -1) <= 5:
                      scalar_values.append(ipt[0])
                  else:
                      scalar_values.append("not shown")
          else:
              shapes = "The thunk doesn't have an `inputs` attributes."
              strides = (
                  "So we can't access the strides of the input values"
              )
              scalar_values = (
                  "and we can't print its scalar input values"
              )
          clients = [
              [c[0] for c in fgraph.clients[var]]
              for var in node.outputs
          ]
          detailed_err_msg += (
              f"Inputs shapes: {shapes}"
              + f"\nInputs strides: {strides}"
              + f"\nInputs values: {scalar_values}"
          )
          if verbosity == "high":
              detailed_err_msg += "\nInputs type_num: %s" % str(
                  [
                      getattr(getattr(i[0], "dtype", ""), "num", "")
                      for i in thunk.inputs
                  ]
              )
  
          detailed_err_msg += f"\nOutputs clients: {clients}\n"
      else:
          hints.append(
              "HINT: Use a linker other than the C linker to"
              " print the inputs' shapes and strides."
          )
  
      # Print node backtraces
      tr = getattr(node.outputs[0].tag, "trace", [])
      if isinstance(tr, list) and len(tr) > 0:
          detailed_err_msg += "\nBacktrace when the node is created "
          detailed_err_msg += "(use PyTensor flag traceback__limit=N to make it longer):\n"
  
          # Print separate message for each element in the list of batcktraces
          sio = io.StringIO()
          for subtr in tr:
              traceback.print_list(subtr, sio)
          detailed_err_msg += str(sio.getvalue())
      else:
          hints.append(
              "HINT: Re-running with most PyTensor optimizations disabled could"
              " provide a back-trace showing when this node was created. This can"
              " be done by setting the PyTensor flag"
              " 'optimizer=fast_compile'. If that does not work,"
              " PyTensor optimizations can be disabled with 'optimizer=None'."
          )
  
      if verbosity == "high":
          import pytensor.printing
  
          f = io.StringIO()
          pytensor.printing.debugprint(
              node, file=f, stop_on_name=True, print_type=True
          )
          detailed_err_msg += "\nDebug print of the apply node: \n"
          detailed_err_msg += f.getvalue()
  
      # Prints output_map
      if verbosity == "high" and storage_map is not None:
          detailed_err_msg += "\nStorage map footprint:\n"
          shared_input_list = [
              item
              for item in fgraph.inputs
              if isinstance(item, pytensor.compile.SharedVariable)
          ]
          nonshared_input_list = [
              item
              for item in fgraph.inputs
              if not isinstance(item, pytensor.compile.SharedVariable)
          ]
          storage_map_list: list = []
          total_size = 0
          total_size_inputs = 0
          for k in storage_map:
              storage_map_item: list = []
  
              # storage_map_item[0]: the variable
              storage_map_item.append(str(k))
  
              # storage_map_item[1]: the shape
              shapeinfo = None
              if hasattr(storage_map[k][0], "shape"):
                  shapeinfo = storage_map[k][0].shape
                  if len(shapeinfo) != 0:
                      storage_map_item.append(shapeinfo)
                  else:
                      storage_map_item.append(tuple())
              else:
                  storage_map_item.append(None)
  
              # storage_map_item[2]: itemsize
              # storage_map_item[3]: bytes
              if hasattr(storage_map[k][0], "dtype"):
                  dtype = storage_map[k][0].dtype
                  storage_map_item.append(np.dtype(dtype).itemsize)
                  if shapeinfo is None:
                      storage_map_item.append(-1)
                  else:
                      sz = np.dtype(dtype).itemsize * np.prod(shapeinfo)
                      storage_map_item.append(sz)
                      total_size += sz
                      if not k.owner:
                          total_size_inputs += sz
                      else:
                          # If it is a view, don't count it twice.
                          vmap = k.owner.op.view_map
                          if vmap:
                              out_idx = k.owner.outputs.index(k)
                              data = storage_map[k][0]
                              if out_idx in vmap:
                                  assert len(vmap[out_idx]) == 1
                                  input_data = storage_map[
                                      k.owner.inputs[vmap[out_idx][0]]
                                  ][0]
                                  if k.type.may_share_memory(
                                      data, input_data
                                  ):
                                      total_size -= sz
                          # If it is a destroyed input, the input
                          # shouldn't be in the storage_map anymore
                          # except if there is a special flag used. So
                          # we still must check it.
                          dmap = k.owner.op.destroy_map
                          if dmap:
                              out_idx = k.owner.outputs.index(k)
                              data = storage_map[k][0]
                              if out_idx in dmap:
                                  assert len(dmap[out_idx]) == 1
                                  input_data = storage_map[
                                      k.owner.inputs[dmap[out_idx][0]]
                                  ][0]
                                  if k.type.may_share_memory(
                                      data, input_data
                                  ):
                                      total_size -= sz
              else:
                  bytes = sys.getsizeof(storage_map[k][0])
                  storage_map_item.append(bytes)
                  storage_map_item.append(-1)
  
              # Flag of shared val
              # storage_map_item[4]
              if k in shared_input_list:
                  storage_map_item.append(True)
              elif k in nonshared_input_list:
                  storage_map_item.append(False)
              else:
                  storage_map_item.append(None)
              storage_map_list.append(storage_map_item)
  
          storage_map_list.sort(key=itemgetter(3), reverse=True)
          for item in storage_map_list:
              if item[3] == -1:
                  continue
              detailed_err_msg += " - " + item[0] + ", "
              if item[4] is True:
                  detailed_err_msg += "Shared Input, "
              elif item[4] is False:
                  detailed_err_msg += "Input, "
              if item[1] is not None:
                  detailed_err_msg += f"Shape: {item[1]}, "
              detailed_err_msg += f"ElemSize: {item[2]} Byte(s)"
              if item[3] is not None:
                  detailed_err_msg += (
                      f", TotalSize: {item[3]} Byte(s)\n"
                  )
              else:
                  detailed_err_msg += "\n"
          detailed_err_msg += (
              " TotalSize: {} Byte(s) {:.3f} GB\n".format(
                  total_size,
                  total_size / 1024 / 1024 / 1024,
              )
          )
          detailed_err_msg += (
              " TotalSize inputs: {} Byte(s) {:.3f} GB\n".format(
                  total_size_inputs,
                  total_size_inputs / 1024 / 1024 / 1024,
              )
          )
  
      else:
          hints.append(
              "HINT: Use the PyTensor flag `exception_verbosity=high`"
              " for a debug print-out and storage map footprint of this Apply node."
          )
  
      try:
          exc_value = exc_type(
              str(exc_value)
              + detailed_err_msg
              + "\n"
              + "\n".join(hints)
          )
      except TypeError:
          warnings.warn(
              f"{exc_type} error does not allow us to add an extra error message"
          )
          # Some exception need extra parameter in inputs. So forget the
          # extra long error message in that case.
      raise exc_value.with_traceback(exc_trace)
  
imports: import builtins
  import io
  import re
  import sys
  import traceback
  import warnings
  from collections import Counter, defaultdict
  from collections.abc import Iterable, Sequence
  from keyword import iskeyword
  from operator import itemgetter
  from tempfile import NamedTemporaryFile
  from textwrap import dedent, indent
  from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, TextIO, TypeVar, Union, cast
  import numpy
  from pytensor import config, utils
  from pytensor.graph.basic import Apply, Constant, Variable
  from pytensor.graph.fg import FunctionGraph
label_desc: Use image node_id 4 for calling a global function with example usage: raise_with_op(fgraph, node, thunk, exc_info, storage_map) without return types