code: def save_mem_new_scan(fgraph, node):
      r"""Graph optimizer that reduces scan memory consumption.
  
      This optimizations attempts to determine if a `Scan` node, during its execution,
      for any of its outputs, can get away with allocating a memory buffer that is
      large enough to contain some of the computed timesteps of that output but not
      all of them.
  
      By default, during the execution of a `Scan` node, memory buffers will be
      allocated to store the values computed for every output at every iteration.
      However, in some cases, there are outputs for which there is only really a
      need to store the most recent ``N`` values, not all of them.
  
      For instance, if a `Scan` node has a SITSOT output (last computed value is
      fed back as an input at the next iteration) and only the last timestep of
      that output is ever used in the outer function, the `ScanSaveMem` optimization
      could determine that there is no need to store all computed timesteps for
      that SITSOT output. Only the most recently computed timestep ever needs to
      be kept in memory.
  
      There are two ways in which the Scan buffer size is controlled:
      1. Each recurring output is saved in an input empty tensor x with the initial
      state written at x[:abs(min(taps))]. The remaining x[abs(min(taps)):]
      positions determine how many intermediate results should be stored.
      This rewrite shortens x[abs(min(taps)):] to the smallest possible size.
      2. Each non-recurrent output (nit-sot) is associated with a scalar integer
      input that determines how many steps should be saved in the perform method.
      This rewrite reduces this number to the smallest possible.
  
      The scan perform implementation takes the output sizes into consideration,
      saving the newest results over the oldest ones whenever the buffer is filled.
      """
      if not isinstance(node.op, Scan):
          return False
  
      if hasattr(fgraph, "shape_feature"):
          shape_of = fgraph.shape_feature.shape_of
      else:
          # Each access to shape_of is in a try..except block in order to
          # use a default version when the variable is not in the shape_of
          # dictionary.
          shape_of = {}
      # 1. Initialization of variables
      # Note 1) We do not actually care about outputs representing shared
      # variables (those have no intermediate values) so it is safer to
      # ignore them and not change them in any way. To simplify the
      # optimizations I construct the variable ``c_outs`` ( that counts
      # outputs up to those we care) and the list ``init_l`` which for any
      # output we care says the length of its initial state. Note that
      # defining ``init_l`` for mit_mot sequences is a bit trickier but
      # it is safe to set it to 0
      op = node.op
      op_info = op.info
      c_outs = (
          op_info.n_mit_mot
          + op_info.n_mit_sot
          + op_info.n_sit_sot
          + op_info.n_nit_sot
      )
  
      init_l = [0 for x in range(op_info.n_mit_mot)]
      init_l += [
          abs(min(v))
          for v in chain(
              op_info.mit_sot_in_slices, op_info.sit_sot_in_slices
          )
      ]
      init_l += [0 for x in range(op_info.n_nit_sot)]
      # 2. Check the clients of each output and see for how many steps
      # does scan need to run
  
      # This comparison checks if there is any uncounted output, which
      # can only be an output corresponding to a shared variable
  
      # 2.1 Initialize
      # global_nsteps is a dictionary having two fields ( 'real' deals
      # with int values, 'sym' with symbolic ones) or None
      # given that a scan op has k outputs o_1, .. o_k and each
      # output has n_j clients c_1^1, c_1^2, .. c_1^{n_1}, c_2^1, ..,
      # global_nsteps is None if any of the clients is different
      # from a subtensor or its real and sym field equal to
      # max(c_i_j.idx_list[0].stop), meaning store up to which maximal
      # index(step) for any output scan actually needs to compute
      # In other words n_steps should be equal to this maximal !
      # Note: if we have a shared variable that gets updated at every step
      # of the loop, reducing the number of steps will affect the
      # value of the shared variable after the loop so we cannot
      # change the number of steps in that case. To do this we set
      # global_nsteps to None which is seen as a flag that nothing needs
      # to be done.
      # Note: For simplicity while Scans also have global_nsteps set to None.
      #  All step optimizations require knowing the shape of the output, which
      #  cannot be determined from the inputs alone.
      assert len(node.outputs) >= c_outs
      if len(node.outputs) == c_outs and not op.info.as_while:
          global_nsteps = {"real": -1, "sym": []}
      else:
          global_nsteps = None
  
      # Keeps track of the original slices that each client represent
      slices = [None for o in node.outputs]
  
      # A list for each output indicating how many intermediate values
      # should be stored. If negative it means none of the intermediate
      # values (i.e. the output can be removed since it is not used
      # afterwards in the computations), if 0 it means that all
      # intermediate values are required, otherwise is up to that number
      # of intermediate values
      # Note that for mit_mot outputs and shared outputs we can not change
      # the number of intermediate steps stored without affecting the
      # result of the op
      store_steps = [0 for o in range(op_info.n_mit_mot)]
      store_steps += [
          -1 for o in node.outputs[op_info.n_mit_mot : c_outs]
      ]
      # Flag that says if an input has changed and we need to do something
      # or not
      flag_store = False
  
      # 2.2 Loop over the clients
      for i, out in enumerate(node.outputs[:c_outs]):
          # look at all its clients
          slices[i] = []
          for cl, _ in fgraph.clients[out]:
              # 2.1 outputs of the function
              # => output needs all its intermediate values
              if isinstance(cl, str):
                  # if the node is actually an output, then
                  # we need to store the entire thing
                  global_nsteps = None
                  slices[i] = None
                  break
              # 2.2 non-subtensor nodes
              # => output needs all its intermediate values
              elif not isinstance(cl.op, Subtensor):
                  global_nsteps = None
                  slices[i] = None
                  break
              # 2.3 subtensor nodes
              # => output might need to store just a subset of its values
              else:
                  # 2.3.1 extract idx list of subtensor
                  this_slice = get_idx_list(cl.inputs, cl.op.idx_list)
                  if this_slice is None:
                      # if unable to extract idx_list
                      # => outputs needs all its intermediate values
                      global_nsteps = None
                      slices[i] = None
                      break
  
                  # 2.3.2 extract the begin/end of the first dimension
                  if i >= op_info.n_mit_mot:
                      try:
                          length = shape_of[out][0]
                      except KeyError:
                          length = node.inputs[0] + init_l[i]
                  else:
                      try:
                          length = shape_of[out][0]
                      except KeyError:
                          length = out.shape[0]
                  cf_slice = get_canonical_form_slice(
                      this_slice[0], length
                  )
                  slices[i] += [(cf_slice, this_slice)]
  
                  if (
                      isinstance(this_slice[0], slice)
                      and this_slice[0].stop is None
                  ):
                      global_nsteps = None
                  if isinstance(cf_slice[0], slice):
                      stop = pt.extract_constant(cf_slice[0].stop)
                  else:
                      stop = pt.extract_constant(cf_slice[0]) + 1
                  if stop == maxsize or stop == pt.extract_constant(
                      length
                  ):
                      stop = None
                  else:
                      # there is a **gotcha** here ! Namely, scan returns an
                      # array that contains the initial state of the output
                      # as well. Which means that if y has an initial state of
                      # length 3, and you look for 5 steps you get an output
                      # y of length 8. If you only use y[:5], this does not
                      # mean that you only need to loop for 5 steps but
                      # actually only for 2 steps ( the first 3 are the
                      # initial state)
                      stop = stop - init_l[i]
  
                  # 2.3.3 we might get away with less number of steps
                  if stop is not None and global_nsteps is not None:
                      # yes if it is a tensor
                      if isinstance(stop, Variable):
                          global_nsteps["sym"] += [stop]
                      # not if it is maxsize
                      elif isinstance(stop, int) and stop == maxsize:
                          global_nsteps = None
                      # yes if it is a int k, 0 < k < maxsize
                      elif (
                          isinstance(stop, int)
                          and global_nsteps["real"] < stop
                      ):
                          global_nsteps["real"] = stop
                      # yes if it is a int k, 0 < k < maxsize
                      elif isinstance(stop, int) and stop > 0:
                          pass
                      # not otherwise
                      else:
                          global_nsteps = None
  
      # 2.3. Analyze global_nsteps to figure out for how many steps scan
      # needs to iterate
      if global_nsteps is None:
          nw_steps = node.inputs[0]
      else:
          # there are some symbolic tensors that limit the number of
          # steps
          if len(global_nsteps["sym"]) == 0:
              sym_steps = None
          else:
              sym_steps = global_nsteps["sym"][0]
              for c in global_nsteps["sym"][1:]:
                  sym_steps = maximum(sym_steps, c)
  
          if global_nsteps["real"] >= 0:
              real_steps = global_nsteps["real"]
          else:
              real_steps = None
          nw_steps = select_min(
              select_max(sym_steps, real_steps), node.inputs[0]
          )
  
          # FIXME: This is not correct. Scan with 0 steps seems to be supported
          # Make sure the ScanSaveMem optimization never makes the new
          # number of steps to be 0 (this could happen, for instance, if
          # the optimization detects that the outputs of the Scan go through
          # subtensor nodes that end up taking no elements) because Scan with
          # 0 iterations are not supported. Make sure the new number of steps
          # is at least 1.
          nw_steps = select_max(nw_steps, 1)
  
      # 2.4 Loop over the clients again now looking just to see how many
      # intermediate steps to store
      for i, out in enumerate(node.outputs[:c_outs]):
          # look at all its clients
          for cl, _ in fgraph.clients[out]:
              if isinstance(cl, str):
                  store_steps[i] = 0
                  break
              elif not isinstance(cl.op, Subtensor):
                  store_steps[i] = 0
                  break
              else:
                  this_slice = get_idx_list(cl.inputs, cl.op.idx_list)
                  if this_slice is None:
                      store_steps[i] = 0
                      break
  
                  if (
                      isinstance(this_slice[0], slice)
                      and this_slice[0].start is None
                  ):
                      store_steps[i] = 0
                      break
  
                  # Special case for recurrent outputs where only the last result
                  # is requested. This is needed for this rewrite to apply to
                  # do-while Scans at all. Otherwise, `get_canonical_form_slice` in
                  # the `else` branch would reintroduce a shape dependency on the
                  # original Scan which would lead this rewrite to abort in the end.
                  if (
                      i <= op.info.n_mit_mot
                      and isinstance(this_slice[0], ScalarConstant)
                      and this_slice[0].value == -1
                  ):
                      start = nw_steps - 1
                  else:
                      if i <= op.info.n_mit_mot:
                          try:
                              length = shape_of[out][0]
                          except KeyError:
                              length = out.shape[0]
                      else:
                          length = node.inputs[0] + init_l[i]
  
                      cf_slice = get_canonical_form_slice(
                          this_slice[0], length
                      )
  
                      if isinstance(cf_slice[0], slice):
                          start = pt.extract_constant(cf_slice[0].start)
                      else:
                          start = pt.extract_constant(cf_slice[0])
  
                  if start == 0 or store_steps[i] == 0:
                      store_steps[i] = 0
                  else:
                      # The "+ 1" is because of the memory pre-allocation
                      # mechanism used to in the Scan op to reduce overhead.
                      # To prevent aliasing between the inputs and outputs
                      # of recurrent states, it requires that the buffer be
                      # large enough to that, the new state and the oldest
                      # tap needed don't occupy the sample place in the
                      # circular buffer. For now, this only needs to be done
                      # for mitsots and sitsots (because mitmots are not
                      # currently supported by the mechanism) and only if
                      # the pre-allocation mechanism is activated.
                      prealloc_outs = config.scan__allow_output_prealloc
  
                      first_mitsot_idx = op_info.n_mit_mot
                      last_sitsot_idx = (
                          op_info.n_mit_mot
                          + op_info.n_mit_sot
                          + op_info.n_sit_sot
                          - 1
                      )
                      preallocable_output = (
                          first_mitsot_idx <= i <= last_sitsot_idx
                      )
  
                      if prealloc_outs and preallocable_output:
                          pval = select_max(
                              nw_steps - start + init_l[i],
                              init_l[i] + 1,
                          )
                      else:
                          pval = select_max(
                              nw_steps - start + init_l[i], init_l[i]
                          )
  
                      if store_steps[i] != -1:
                          pval = select_max(pval, store_steps[i])
  
                      store_steps[i] = pval
                      flag_store = True
  
      orphane_outs = [
          i
          for i, x in enumerate(store_steps)
          if isinstance(x, int) and (x < 0)
      ]
      flag_store = flag_store or (len(orphane_outs) > 0)
      # 3. is there anything to change ?
      if flag_store or global_nsteps is not None:
          # 3.1 initialize inputs for the new scan
          old_outputs = []
          nw_inputs = list(node.inputs)
          nw_inputs[0] = nw_steps
  
          # 3.2 check orphane outputs to see if we can eliminate any
          required, not_required = scan_can_remove_outs(
              node.op, orphane_outs
          )
          # 3.3. compose replace pairs for those nodes that need not
          # to store everything in memory ( or ar orphane and required
          # by the inner function .. )
          replaced_outs = []
          offset = 1 + op_info.n_seqs + op_info.n_mit_mot
          for idx, _val in enumerate(store_steps[op_info.n_mit_mot :]):
              i = idx + op_info.n_mit_mot
              if not (
                  isinstance(_val, int)
                  and _val <= 0
                  and i not in required
              ):
                  if idx + op_info.n_mit_mot in required:
                      val = 1
                  else:
                      val = _val
                  # If the memory for this output has been pre-allocated
                  # before going into the scan op (by an alloc node)
                  if idx < op_info.n_mit_sot + op_info.n_sit_sot:
                      # In case the input is still an alloc node, we
                      # actually have two options:
                      #   a) the input is a set_subtensor, in that case we
                      #      can replace the initial tensor by a slice,
                      #   b) it is not, and we simply take a slice of it.
                      # TODO: commit change below with Razvan
                      if (
                          nw_inputs[offset + idx].owner
                          and isinstance(
                              nw_inputs[offset + idx].owner.op,
                              IncSubtensor,
                          )
                          and nw_inputs[
                              offset + idx
                          ].owner.op.set_instead_of_inc
                          and isinstance(
                              nw_inputs[offset + idx].owner.op.idx_list[
                                  0
                              ],
                              slice,
                          )
                          # Don't try to create a smart Alloc, if set_subtensor is broadcasting the fill value
                          # As it happens in set_subtensor(empty(2)[:], 0)
                          and not (
                              nw_inputs[offset + idx].ndim
                              > nw_inputs[offset + idx]
                              .owner.inputs[1]
                              .ndim
                          )
                      ):
                          _nw_input = nw_inputs[
                              offset + idx
                          ].owner.inputs[1]
                          cval = pt.as_tensor_variable(val)
                          initl = pt.as_tensor_variable(init_l[i])
                          tmp_idx = pt.switch(
                              cval < initl, cval + initl, cval - initl
                          )
                          nw_input = expand_empty(_nw_input, tmp_idx)
                      else:
                          tmp = pt.as_tensor_variable(val)
                          initl = pt.as_tensor_variable(init_l[i])
                          tmp = maximum(tmp, initl)
                          nw_input = nw_inputs[offset + idx][:tmp]
  
                      nw_inputs[offset + idx] = nw_input
                      replaced_outs.append(op_info.n_mit_mot + idx)
                      odx = op_info.n_mit_mot + idx
                      old_outputs += [
                          (
                              odx,
                              [
                                  x[0].outputs[0]
                                  for x in fgraph.clients[
                                      node.outputs[odx]
                                  ]
                              ],
                          )
                      ]
                  # If there is no memory pre-allocated for this output
                  elif (
                      idx
                      < op_info.n_mit_sot
                      + op_info.n_sit_sot
                      + op_info.n_nit_sot
                  ):
                      pos = (
                          op_info.n_mit_mot
                          + idx
                          + op_info.n_seqs
                          + 1
                          + op_info.n_shared_outs
                      )
                      if nw_inputs[pos] == node.inputs[0]:
                          nw_inputs[pos] = val
                      odx = op_info.n_mit_mot + idx
                      replaced_outs.append(odx)
                      old_outputs += [
                          (
                              odx,
                              [
                                  x[0].outputs[0]
                                  for x in fgraph.clients[
                                      node.outputs[odx]
                                  ]
                              ],
                          )
                      ]
          # 3.4. Recompute inputs for everything else based on the new
          # number of steps
          if global_nsteps is not None:
              for idx, val in enumerate(
                  store_steps[op_info.n_mit_mot :]
              ):
                  if val == 0:
                      # val == 0 means that we want to keep all intermediate
                      # results for that state, including the initial values.
                      if idx < op_info.n_mit_sot + op_info.n_sit_sot:
                          in_idx = offset + idx
                          # Number of steps in the initial state
                          initl = init_l[op_info.n_mit_mot + idx]
  
                          # If the initial buffer has the form
                          # inc_subtensor(zeros(...)[...], _nw_input)
                          # we want to make the zeros tensor as small as
                          # possible (nw_steps + initl), and call
                          # inc_subtensor on that instead.
                          # Otherwise, simply take 0:(nw_steps+initl).
                          if (
                              nw_inputs[in_idx].owner
                              and isinstance(
                                  nw_inputs[in_idx].owner.op,
                                  IncSubtensor,
                              )
                              and isinstance(
                                  nw_inputs[in_idx].owner.op.idx_list[
                                      0
                                  ],
                                  slice,
                              )
                          ):
                              _nw_input = nw_inputs[
                                  in_idx
                              ].owner.inputs[1]
                              nw_input = expand_empty(
                                  _nw_input, nw_steps
                              )
                              nw_inputs[in_idx] = nw_input
                          else:
                              # FIXME: This is never used
                              nw_input = nw_inputs[in_idx][
                                  : (initl + nw_steps)
                              ]
  
                      elif (
                          idx
                          < op_info.n_mit_sot
                          + op_info.n_sit_sot
                          + op_info.n_nit_sot
                      ):
                          in_idx = offset + idx + op_info.n_shared_outs
                          if nw_inputs[in_idx] == node.inputs[0]:
                              nw_inputs[in_idx] = nw_steps
  
          # 3.5 Remove unwanted orphane outputs
          (inps, outs, info, node_ins, compress_map) = compress_outs(
              op, not_required, nw_inputs
          )
          inv_compress_map = {}
          for k, v in compress_map.items():
              inv_compress_map[v] = k
  
          # 3.6 Compose the new scan
          # TODO: currently we don't support scan with 0 step. So
          # don't create one.
          if pt.extract_constant(node_ins[0]) == 0:
              return False
  
          # Do not call make_node for test_value
          new_op = Scan(
              inps,
              outs,
              info,
              mode=op.mode,
              profile=op.profile,
              truncate_gradient=op.truncate_gradient,
              # TODO: This seems questionable
              name=op.name,
              allow_gc=op.allow_gc,
          )
          new_outs = new_op(*node_ins, return_list=True)
  
          old_new = []
          # 3.7 Get replace pairs for those outputs that do not change
          # the number of intermediate steps stored
          for idx, sl in enumerate(slices):
              if (
                  global_nsteps
                  and sl is not None
                  and store_steps[idx] == 0
              ):
                  for hdx, cl in enumerate(
                      fgraph.clients[node.outputs[idx]]
                  ):
                      cnf_slice, old_slices = sl[hdx]
                      # Sanitize the nw_slice by converting ints back into
                      # constants :) I only need to do this for the first
                      # slice since that is the only slice
  
                      if isinstance(cnf_slice[0], slice):
                          fslice = slice(
                              sanitize(cnf_slice[0].start),
                              sanitize(cnf_slice[0].stop),
                              sanitize(cnf_slice[0].step),
                          )
                      else:
                          fslice = sanitize(cnf_slice[0])
                      nw_slice = (fslice,) + tuple(old_slices[1:])
  
                      nw_pos = inv_compress_map[idx]
  
                      subtens = Subtensor(nw_slice)
                      # slice inputs
                      sl_ins = get_slice_elements(
                          nw_slice,
                          lambda entry: isinstance(entry, Variable),
                      )
                      new_o = subtens(new_outs[nw_pos], *sl_ins)
                      if new_o.ndim > 0:
                          new_o = new_o[:: cnf_slice[1]]
                      replaced_outs.append(idx)
                      old_new += [(cl[0].outputs[0], new_o)]
          # 3.8. Get replace pairs for those outputs that change
          # the number of stored intermediate steps
          for pos, old_outs in old_outputs:
              if len(old_outs) > 0:
                  nw_pos = compress_map[pos]
                  for k, old in enumerate(old_outs):
                      # Get the correct slice
                      cnf_slice, old_slices = slices[pos][k]
                      if isinstance(cnf_slice[0], slice):
                          start = (
                              cnf_slice[0].start
                              - nw_steps
                              - init_l[pos]
                              + store_steps[pos]
                          )
                          if (
                              cnf_slice[0].stop is not None
                              and cnf_slice[0].stop != maxsize
                          ):
                              stop = (
                                  cnf_slice[0].stop
                                  - nw_steps
                                  - init_l[pos]
                                  + store_steps[pos]
                              )
                          else:
                              stop = None
                          nw_slice = (
                              slice(
                                  sanitize(start),
                                  sanitize(stop),
                                  sanitize(cnf_slice[0].step),
                              ),
                          ) + tuple(old_slices[1:])
  
                      else:
                          # Special case when only last value is requested
                          if (
                              isinstance(old_slices[0], ScalarConstant)
                              and old_slices[0].value == -1
                          ):
                              position = old_slices[0]
                          else:
                              position = (
                                  cnf_slice[0]
                                  - nw_steps
                                  - init_l[pos]
                                  + store_steps[pos]
                              )
  
                          nw_slice = (sanitize(position),) + tuple(
                              old_slices[1:]
                          )
                      subtens = Subtensor(nw_slice)
                      sl_ins = get_slice_elements(
                          nw_slice,
                          lambda entry: isinstance(entry, Variable),
                      )
                      new_o = subtens(new_outs[nw_pos], *sl_ins)
                      if new_o.ndim > 0:
                          new_o = new_o[:: cnf_slice[1]]
                      old_new += [(old, new_o)]
  
          # 3.9. Get replace pairs for all other nodes
          if flag_store or global_nsteps is not None:
              for idx, o in enumerate(node.outputs):
                  if (
                      idx not in replaced_outs
                      and idx not in not_required
                  ):
                      nw_pos = compress_map[idx]
                      old_new += [(o, new_outs[nw_pos])]
              # Check if the new outputs depend on the old scan node
              old_scan_is_used = [
                  apply_depends_on(new.owner, node)
                  for old, new in old_new
              ]
              if any(old_scan_is_used):
                  return False
  
              replacements = dict(old_new)
  
              # remove = [old.owner for (old, new) in old_new]
              # As Fred suggested assert that also the old node is not in
              # the Graph as that will make things suboptimal
              # remove.append(node)
              replacements["remove"] = [node]
  
              return replacements
  
          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 13 for calling a global function with example usage: save_mem_new_scan(fgraph, node) and returns: False, False, False, replacements, False