code: def push_out_non_seq_scan(fgraph, node):
      r"""Push out the variables inside the `Scan` that depend only on non-sequences.
  
      This optimizations pushes, out of `Scan`'s inner function and into the outer
      function, computation that depends only on non-sequence inputs. Such
      computation ends up being done every iteration on the same values so moving
      it to the outer function to be executed only once, before the `Scan` `Op`,
      reduces the amount of computation that needs to be performed.
      """
      if not isinstance(node.op, Scan):
          return False
  
      node_inputs, node_outputs = (
          node.op.inner_inputs,
          node.op.inner_outputs,
      )
  
      local_fgraph_topo = io_toposort(node_inputs, node_outputs)
      local_fgraph_outs_set = set(node_outputs)
      local_fgraph_outs_map = {v: k for k, v in enumerate(node_outputs)}
  
      to_remove_set = set()
      to_replace_set = set()
      to_replace_map = {}
  
      def add_to_replace(y):
          to_replace_set.add(y)
          to_replace_map[y] = add_to_replace.n
          add_to_replace.n += 1
  
      add_to_replace.n = 0
  
      # The variables that will replace the variables pushed-out of the
      # inner-graph
      replace_with_in = []
      # The variables that have been pushed-out of the graph
      replace_with_out = []
  
      op = node.op
      # Construct the list of non_sequences to simplify a few things
      inner_non_seqs = op.inner_non_seqs(node_inputs)
      inner_non_seqs_set = set(inner_non_seqs)
      inner_non_seqs_map = {v: k for k, v in enumerate(inner_non_seqs)}
  
      outer_non_seqs = op.outer_non_seqs(node.inputs)
  
      inner_seqs = op.inner_seqs(node_inputs)
      outer_seqs = op.outer_seqs(node.inputs)
  
      assert len(inner_non_seqs) == len(outer_non_seqs)
      assert len(inner_seqs) == len(outer_seqs)
  
      for nd in local_fgraph_topo:
          if (  # we haven't already looked at this node
              nd not in to_remove_set
              and all(
                  (
                      (x in inner_non_seqs_set)
                      or (x.owner in to_remove_set)
                      or isinstance(x, Constant)
                  )
                  for x in nd.inputs
              )
              # We can (supposedly) do this because the assumption is that a
              # `ViewOp` or `DeepCopyOp` will be just at the end of the
              # function and not somewhere in the middle
              and not isinstance(nd.op, pytensor.compile.ViewOp)
              and not isinstance(nd.op, pytensor.compile.DeepCopyOp)
          ):
              # We have a candidate node to remove from the inner-graph
  
              # Step 1. Reconstruct the node using the relevant outer-inputs.
              #
              # More specifically, the node's current inputs are either
              # a) inner-graph input place-holders for non-sequences,
              # b) the outputs of other nodes being pushed out of the inner-graph,
              # c) or constants.
              to_remove_set.add(nd)
              new_inputs = []
              for old_input in nd.inputs:
                  if old_input in inner_non_seqs_set:
                      # This is case a), so we want to use the corresponding
                      # outer-graph input as the input to our new pushed-out node
                      _idx = inner_non_seqs_map[old_input]
                      new_input = outer_non_seqs[_idx]
                  elif old_input in to_replace_set:
                      # This is case b), so we want to use the new pushed-out node
                      # as the input to this new pushed-out node
                      new_input = replace_with_out[
                          to_replace_map[old_input]
                      ]
                  else:
                      assert isinstance(old_input, Constant)
                      new_input = old_input
  
                  new_input = old_input.type.filter_variable(new_input)
                  new_inputs.append(new_input)
  
              pushed_out_node = nd.op.make_node(*new_inputs)
  
              if config.compute_test_value != "off":
                  compute_test_value(pushed_out_node)
  
              # Step 2. Create variables to replace the old outputs of the node
              # that we're pushing out of the inner-graph
              for idx, y in enumerate(nd.outputs):
                  y_place_holder = y.clone()
                  # y_place_holder = safe_new(y, "_replace")
                  add_to_replace(y)
                  replace_with_in.append(y_place_holder)
                  assert isinstance(
                      y, type(pushed_out_node.outputs[idx])
                  )
                  replace_with_out.append(pushed_out_node.outputs[idx])
  
      # We need to check all candidate replacements and choose those that
      # make sense for us
      # Step 1. which elements of `to_replace` are used by remaining
      # components of the inner function
      clean_to_replace = []
      clean_replace_with_in = []
      clean_replace_with_out = []
      existent_nodes = [
          nd for nd in local_fgraph_topo if nd not in to_remove_set
      ]
      existent_nodes_set = set(existent_nodes)
  
      to_keep_set = set()
      for nd in existent_nodes:
          to_keep_set.update(nd.inputs)
  
      for out, idx in to_replace_map.items():
          if (  # If types are different, conversion Op will be inserted,
              # and it may trigger an infinite loop.
              out.type.is_super(replace_with_in[idx].type)
              and out in to_keep_set
              and out.owner not in existent_nodes_set
          ):
              clean_to_replace.append(out)
              clean_replace_with_in.append(replace_with_in[idx])
              clean_replace_with_out.append(replace_with_out[idx])
  
      if len(clean_to_replace) > 0:
          # We can finally put an end to all this madness
          givens = {}
          nw_outer = []
          nw_inner = []
          for to_repl, repl_in, repl_out in zip(
              clean_to_replace,
              clean_replace_with_in,
              clean_replace_with_out,
          ):
              if isinstance(repl_out, Constant):
                  repl_in = repl_out
              else:
                  nw_inner.append(repl_in)
                  nw_outer.append(repl_out)
              givens[to_repl] = repl_in
  
          op_outs = clone_replace(node_outputs, replace=givens)
          op_ins = node_inputs + nw_inner
  
          new_info = dataclasses.replace(
              op.info, n_non_seqs=op.info.n_non_seqs + len(nw_outer)
          )
  
          # Reconstruct node
          nwScan = Scan(
              op_ins,
              op_outs,
              new_info,
              mode=op.mode,
              profile=op.profile,
              truncate_gradient=op.truncate_gradient,
              # TODO: This seems questionable
              name=op.name,
              allow_gc=op.allow_gc,
          )
  
          # Do not call make_node for test_value
          nw_node = nwScan(*(node.inputs + nw_outer), return_list=True)[
              0
          ].owner
  
          replacements = dict(zip(node.outputs, nw_node.outputs))
          replacements["remove"] = [node]
          return replacements
      elif not to_keep_set:
          # Nothing in the inner graph should be kept
          replace_with = {}
          for out, idx in to_replace_map.items():
              if out in local_fgraph_outs_set:
                  x = node.outputs[local_fgraph_outs_map[out]]
                  y = replace_with_out[idx]
                  y_shape = list(y.shape)
                  replace_with[x] = pt.alloc(
                      y, node.inputs[0], *y_shape
                  )
  
          # We need to add one extra dimension to the outputs
          # because the scan op expects for a tensor3, to which an
          # subtensor is applied that takes only the last element
          if replace_with:
              if len(node.outputs) == len(replace_with):
                  # Every output of the node has a replacement, the Scan
                  # node can be removed from the graph
                  replace_with["remove"] = [node]
                  return replace_with
              else:
                  # The node has some outputs for which no replacement has
                  # been established. This can occur for outputs that are
                  # not produced by apply nodes (since the optimizations
                  # only visits apply nodes) such as constants or inputs
                  # passed directly as outputs. The replacements can be
                  # performed but the Scan node can't be removed at this
                  # point.
                  return replace_with
  
      else:
          return False
  
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 2 for calling a global function with example usage: push_out_non_seq_scan(fgraph, node) and returns: False, replacements, False, replace_with, replace_with