code: def scan_merge_inouts(fgraph, node):
      """
      This optimization attempts to merge a `Scan` `Op`'s identical outer inputs as well
      as merge its identical outer outputs (outputs that perform the same
      computation on the same inputs). This can reduce the amount of computation as
      well as result in a simpler graph for both the inner function and the outer
      function.
      """
      if not isinstance(node.op, Scan):
          return False
  
      # Do a first pass to merge identical external inputs.
      # Equivalent inputs will be stored in inp_equiv, then a new
      # scan node created without duplicates.
      a = ScanArgs(
          node.inputs,
          node.outputs,
          node.op.inner_inputs,
          node.op.inner_outputs,
          node.op.info,
      )
  
      inp_equiv = {}
  
      if has_duplicates(a.outer_in_seqs):
          new_outer_seqs = []
          new_inner_seqs = []
          for out_seq, in_seq in zip(a.outer_in_seqs, a.inner_in_seqs):
              if out_seq in new_outer_seqs:
                  i = new_outer_seqs.index(out_seq)
                  inp_equiv[in_seq] = new_inner_seqs[i]
              else:
                  new_outer_seqs.append(out_seq)
                  new_inner_seqs.append(in_seq)
          a.outer_in_seqs = new_outer_seqs
          a.inner_in_seqs = new_inner_seqs
  
      if has_duplicates(a.outer_in_non_seqs):
          new_outer_nseqs = []
          new_inner_nseqs = []
          for out_nseq, in_nseq in zip(
              a.outer_in_non_seqs, a.inner_in_non_seqs
          ):
              if out_nseq in new_outer_nseqs:
                  i = new_outer_nseqs.index(out_nseq)
                  inp_equiv[in_nseq] = new_inner_nseqs[i]
              else:
                  new_outer_nseqs.append(out_nseq)
                  new_inner_nseqs.append(in_nseq)
          a.outer_in_non_seqs = new_outer_nseqs
          a.inner_in_non_seqs = new_inner_nseqs
  
      if len(inp_equiv) > 0:
          # do the replacement now. The rest will be left to ScanSaveMem
          inner_inputs = a.inner_inputs
          outer_inputs = a.outer_inputs
          info = a.info
          a_inner_outs = a.inner_outputs
          inner_outputs = clone_replace(a_inner_outs, replace=inp_equiv)
  
          new_op = Scan(
              inner_inputs,
              inner_outputs,
              info,
              mode=node.op.mode,
              profile=node.op.profile,
              truncate_gradient=node.op.truncate_gradient,
              # TODO: This seems questionable
              name=node.op.name,
              allow_gc=node.op.allow_gc,
          )
          outputs = new_op(*outer_inputs)
  
          if not isinstance(outputs, (list, tuple)):
              outputs = [outputs]
  
          na = ScanArgs(
              outer_inputs,
              outputs,
              new_op.inner_inputs,
              new_op.inner_outputs,
              new_op.info,
          )
          remove = [node]
      else:
          na = a
          remove = []
  
      # Now that the identical external inputs have been merged, we do a new
      # loop in order to merge external outputs that compute the same things
      # from the same inputs.
      left = []
      right = []
  
      if has_duplicates(na.outer_in_shared):
          _left, _right = make_equiv(
              na.outer_in_shared, na.inner_in_shared
          )
          left += _left
          right += _right
      if has_duplicates(na.outer_in_sit_sot):
          _left, _right = make_equiv(
              na.outer_in_sit_sot, na.inner_in_sit_sot
          )
          left += _left
          right += _right
      if has_duplicates(na.outer_in_mit_mot):
          seen = {}
          for omm, imm, _sl in zip(
              na.outer_in_mit_mot,
              na.inner_in_mit_mot,
              na.mit_mot_in_slices,
          ):
              sl = tuple(_sl)
              if (omm, sl) in seen:
                  simm = seen[(omm, sl)]
                  left += imm
                  right += simm
              else:
                  seen[(omm, sl)] = imm
  
      if has_duplicates(na.outer_in_mit_sot):
          seen = {}
          for oms, ims, _sl in zip(
              na.outer_in_mit_sot,
              na.inner_in_mit_sot,
              na.mit_sot_in_slices,
          ):
              sl = tuple(_sl)
              if (oms, sl) in seen:
                  sims = seen[(oms, sl)]
                  left += ims
                  right += sims
              else:
                  seen[(oms, sl)] = ims
  
      def map_out(outer_i, inner_o, outer_o, seen):
          # Return the outer input corresponding to an
          # (outer input, inner output) pair. If we see that pair for the first
          # time, return the provided outer output. If an equivalent pair had
          # already been seen, return that one instead.
          # Note that we need to check that the outer input match as well,
          # because they could have different sizes, and the corresponding
          # outer outputs cannot be merged in that case.
          for s_outer_i, s_inner_o, s_outer_o in seen:
              if (
                  equal_computations(
                      [inner_o], [s_inner_o], left, right
                  )
                  and outer_i == s_outer_i
              ):
                  return s_outer_o
          seen.append((outer_i, inner_o, outer_o))
          return outer_o
  
      seen = []
  
      assert len(na.outer_in_nit_sot) == len(na.inner_out_nit_sot)
      assert len(na.inner_out_nit_sot) == len(na.outer_out_nit_sot)
      na.outer_out_nit_sot = [
          map_out(outer_i, inner_o, outer_o, seen)
          for outer_i, inner_o, outer_o in zip(
              na.outer_in_nit_sot,
              na.inner_out_nit_sot,
              na.outer_out_nit_sot,
          )
      ]
  
      seen = []
      assert len(na.outer_in_sit_sot) == len(na.inner_out_sit_sot)
      assert len(na.inner_out_sit_sot) == len(na.outer_out_sit_sot)
      na.outer_out_sit_sot = [
          map_out(outer_i, inner_o, outer_o, seen)
          for outer_i, inner_o, outer_o in zip(
              na.outer_in_sit_sot,
              na.inner_out_sit_sot,
              na.outer_out_sit_sot,
          )
      ]
  
      seen = []
      assert len(na.outer_in_mit_sot) == len(na.inner_out_mit_sot)
      assert len(na.inner_out_mit_sot) == len(na.outer_out_mit_sot)
      na.outer_out_mit_sot = [
          map_out(outer_i, inner_o, outer_o, seen)
          for outer_i, inner_o, outer_o in zip(
              na.outer_in_mit_sot,
              na.inner_out_mit_sot,
              na.outer_out_mit_sot,
          )
      ]
  
      seen = []
      new_outer_out_mit_mot = []
      assert len(na.outer_in_mit_mot) == len(na.inner_out_mit_mot)
      assert len(na.inner_out_mit_mot) == len(na.outer_out_mit_mot)
      assert len(na.outer_out_mit_mot) == len(na.mit_mot_out_slices)
      for outer_imm, inner_omm, outer_omm, osl in zip(
          na.outer_in_mit_mot,
          na.inner_out_mit_mot,
          na.outer_out_mit_mot,
          na.mit_mot_out_slices,
      ):
          for s_outer_imm, s_inner_omm, s_outer_omm, sosl in seen:
              if (
                  osl == sosl
                  and equal_computations(
                      inner_omm, s_inner_omm, left, right
                  )
                  and outer_imm == s_outer_imm
              ):
                  new_outer_out_mit_mot.append(s_outer_omm)
                  break
          else:
              seen.append((outer_imm, inner_omm, outer_omm, osl))
              new_outer_out_mit_mot.append(outer_omm)
      na.outer_out_mit_mot = new_outer_out_mit_mot
      if remove:
          return dict(
              [("remove", remove)]
              + list(zip(node.outputs, na.outer_outputs))
          )
      return na.outer_outputs
  
imports: import copy
  import dataclasses
  from itertools import chain
  from sys import maxsize
  from typing import Optional, cast
  import numpy
  import pytensor
  from pytensor import scalar
  from pytensor import tensor
  from pytensor.compile import optdb
  from pytensor.compile.function.types import deep_copy_op
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Apply, Constant, NominalVariable, Variable, ancestors, apply_depends_on, equal_computations, graph_inputs, io_toposort
  from pytensor.graph.destroyhandler import DestroyHandler
  from pytensor.graph.features import ReplaceValidate
  from pytensor.graph.fg import FunctionGraph
  from pytensor.graph.op import compute_test_value
  from pytensor.graph.replace import clone_replace
  from pytensor.graph.rewriting.basic import GraphRewriter, copy_stack_trace, in2out, node_rewriter
  from pytensor.graph.rewriting.db import EquilibriumDB, SequenceDB
  from pytensor.graph.rewriting.utils import get_clients_at_depth
  from pytensor.graph.type import HasShape
  from pytensor.graph.utils import InconsistencyError
  from pytensor.raise_op import Assert
  from pytensor.scalar import ScalarConstant
  from pytensor.scan.op import Scan, ScanInfo
  from pytensor.scan.utils import ScanArgs, compress_outs, expand_empty, reconstruct_graph, safe_new, scan_can_remove_outs
  from pytensor.tensor.basic import Alloc, AllocEmpty, get_underlying_scalar_constant_value
  from pytensor.tensor.elemwise import DimShuffle, Elemwise
  from pytensor.tensor.exceptions import NotScalarConstantError
  from pytensor.tensor.math import Dot, dot, maximum, minimum
  from pytensor.tensor.rewriting.basic import constant_folding, local_useless_switch
  from pytensor.tensor.rewriting.elemwise import local_upcast_elemwise_constant_inputs
  from pytensor.tensor.rewriting.math import local_abs_merge, local_mul_switch_sink
  from pytensor.tensor.shape import shape
  from pytensor.tensor.subtensor import IncSubtensor, Subtensor, get_canonical_form_slice, get_idx_list, get_slice_elements, set_subtensor
  from pytensor.tensor.variable import TensorConstant, get_unique_constant_value
label_desc: Use image node_id 16 for calling a global function with example usage: scan_merge_inouts(fgraph, node) and returns: na, False, outer_o, dict, s_outer_o