class_docstr: Finish-to-start evaluation order of thunks.
  
  This supports lazy evaluation of subtrees and partial computations of
  graphs when only some inputs have changed.
  
  At a pseudo-code level, the basic idea is as follows:
  
  .. code-block:: python
  
      def recursively_evaluate(var):
          if var is up to date:
              return
          if var.owner.inputs are up to date:
              update var
              return
          for input in var.owner.inputs:
              recursively_evaluate(var)
  
      for output in outputs:
          recursively_evaluate(output)
  
  
  The actual logic is more complex to support intermediate garbage
  collection, lazily-evaluated nodes, and better speed.
class_docstr_tok: ["Finish-to-start","evaluation","order","of","thunks",".","This","supports","lazy","evaluation","of","subtrees","and","partial","computations","of","graphs","when","only","some","inputs","have","changed",".","At","a","pseudo-code","level",",","the","basic","idea","is","as","follows",":","..","code-block",":",":","python","def","recursively_evaluate","(","var",")",":","if","var","is","up","to","date",":","return","if","var.owner.inputs","are","up","to","date",":","update","var","return","for","input","in","var.owner.inputs",":","recursively_evaluate","(","var",")","for","output","in","outputs",":","recursively_evaluate","(","output",")","The","actual","logic","is","more","complex","to","support","intermediate","garbage","collection",",","lazily-evaluated","nodes",",","and","better","speed","."]
code: def __call__(self, output_subset=None):
      storage_map = self.storage_map
      compute_map = self.compute_map
      thunks = self.thunks
      dependencies = self.dependencies
      self.node_executed_order = []
      self.node_cleared_order = []
  
      for cont in self.pre_call_clear:
          cont[0] = None
  
      for k in self.storage_map:
          compute_map[k][0] = k.owner is None
          if self.callback_input and compute_map[k][0]:
              self.callback_input(k, self.storage_map[k][0])
  
      # apply_stack contains nodes
      if output_subset is not None:
          # Add the outputs that are needed for the in-place updates of the
          # inputs in `self.update_vars`
          output_subset = list(output_subset)
          for inp, out in self.update_vars.items():
              out_idx = self.fgraph.outputs.index(out)
              if out_idx not in output_subset:
                  output_subset.append(out_idx)
  
          apply_stack = [
              self.outputs[i].owner
              for i in output_subset
              if self.outputs[i].owner
          ]
      else:
          apply_stack = list(self.base_apply_stack)
  
      last_apply_stack_len = -1
  
      # This record all function inputs/shared variables and constants
      for var, data in self.storage_map.items():
          if data[0] is None:
              continue
          if hasattr(var.type, "get_shape_info"):
              sh = var.type.get_shape_info(data[0])
          else:
              sh = "no shape"
          self.variable_shape[var] = sh
          st = getattr(data[0], "strides", "no strides")
          if (
              getattr(data[0], "flags", False)
              and data[0].flags.c_contiguous
          ):
              st = "c"
          elif (
              hasattr(data[0], "is_c_contiguous")
              and data[0].is_c_contiguous()
          ):
              st = "c"
          self.variable_strides[var] = st
          off = getattr(data[0], "offset", "")
          self.variable_offset[var] = off
  
      while apply_stack:
          # Make sure something happened last time round.  This is
          # just a safety check to make sure the op is written
          # correctly apply_stack should either decrease in length
          # by one (a thunk successfully applied), or increase in
          # length (added dependencies over and above the original).
          # NB: this doesn't catch cycles (would be too expensive/slow),
          #     just stalls.
          apply_stack_len = len(apply_stack)
          assert apply_stack_len != last_apply_stack_len
          last_apply_stack_len = apply_stack_len
  
          current_apply = apply_stack.pop()
          current_inputs = current_apply.inputs
          current_outputs = current_apply.outputs
          current_deps = (
              current_inputs + self.destroy_dependencies[current_apply]
          )
  
          computed_ins = all(compute_map[v][0] for v in current_deps)
          computed_outs = all(
              compute_map[v][0] for v in current_outputs
          )
  
          if not thunks[self.node_idx[current_apply]].lazy:
              #
              # stack loop: Normal Non-Lazy Case
              # ================================
              #
              # Check if all inputs are in place
              # If so compute thunk and remove it from the apply_stack
              # If not leave it in, and add to the apply_stack those
              # that will produce you those inputs
  
              if computed_ins and not computed_outs:
                  # -- Non-lazy case: have inputs, time to compute outputs
                  try:
                      _, dt = self.run_thunk_of_node(current_apply)
                      del _
                      if config.profile or config.print_global_stats:
                          current_idx = self.node_idx[current_apply]
                          self.call_counts[current_idx] += 1
                          self.call_times[current_idx] += dt
                          # Computing the memory footprint of the the op
                          # ?? What about inplace .. if the op is inplace
                          # you don't actually ask for more memory!
                          for idx, o in enumerate(
                              thunks[
                                  self.node_idx[current_apply]
                              ].outputs
                          ):
                              var = self.nodes[current_idx].outputs[idx]
                              if hasattr(var.type, "get_shape_info"):
                                  sh = var.type.get_shape_info(o[0])
                              else:
                                  sh = "no shape"
                              self.variable_shape[var] = sh
                              st = getattr(
                                  o[0], "strides", "no strides"
                              )
                              if (
                                  getattr(o[0], "flags", False)
                                  and o[0].flags.c_contiguous
                              ):
                                  st = "c"
                              elif (
                                  hasattr(o[0], "is_c_contiguous")
                                  and o[0].is_c_contiguous()
                              ):
                                  st = "c"
                              self.variable_strides[var] = st
                              off = getattr(o[0], "offset", "")
                              self.variable_offset[var] = off
                  except Exception:
                      raise_with_op(
                          self.fgraph,
                          current_apply,
                          self.thunks[self.node_idx[current_apply]],
                          storage_map=storage_map,
                      )
                  for o in current_apply.outputs:
                      compute_map[o][0] = 1
  
                  input_index = []
                  # A list store the index of inputs variables
  
                  if self.allow_gc:
                      for i in current_apply.inputs:
                          # Garbage Collection -> check if anybody else uses
                          # this input
                          if (
                              dependencies[i]
                              and i.owner
                              and i not in self.outputs
                          ):
                              if all(
                                  compute_map[v][0]
                                  for v in dependencies[i]
                              ):
                                  storage_map[i][0] = None
                                  input_index.append(
                                      current_apply.inputs.index(i)
                                  )
  
                                  # DO NOT set compute_map to 0
  
                                  # If values become False and the
                                  # current_apply is still in the
                                  # stack, this will cause it to be
                                  # recomputed! This can cause wrong value
                                  # with some combination of inplace op.
                                  compute_map[i][0] = 2
                  self.node_cleared_order.append(input_index)
  
              elif not computed_ins:
                  # -- Non-lazy case, need inputs
                  apply_stack.append(current_apply)
                  apply_stack.extend(
                      inp.owner for inp in current_deps if inp.owner
                  )
  
          elif not computed_outs:
              #
              # stack loop: Lazy Evaluation Case
              # ================================
              #
              # Lazy evaluation protocol is to run the thunk with the
              # current storage_map and compute_map accessed via closure,
              # and the thunk will return a list of variables from its input
              # list that it requires.
  
              try:
                  requires, dt = self.run_thunk_of_node(current_apply)
                  current_idx = self.node_idx[current_apply]
                  self.call_counts[current_idx] += 1
                  self.call_times[current_idx] += dt
  
              except Exception:
                  raise_with_op(
                      self.fgraph,
                      current_apply,
                      self.thunks[self.node_idx[current_apply]],
                      storage_map=storage_map,
                  )
  
              if requires:
                  for r in requires:
                      # We are not done with this op ..  so we added
                      # back and see to get the inputs we are
                      # missing
                      apply_stack.append(current_apply)
                      if current_apply.inputs[r].owner:
                          apply_stack.append(
                              current_apply.inputs[r].owner
                          )
              else:
                  if config.profile or config.print_global_stats:
                      for idx, o in enumerate(
                          thunks[self.node_idx[current_apply]].outputs
                      ):
                          var = self.nodes[
                              self.node_idx[current_apply]
                          ].outputs[idx]
  
                          if hasattr(var.type, "get_shape_info"):
                              sh = var.type.get_shape_info(o[0])
                          else:
                              sh = "no shape"
                          self.variable_shape[var] = sh
                          st = getattr(o[0], "strides", "no strides")
                          if (
                              getattr(o[0], "flags", False)
                              and o[0].flags.c_contiguous
                          ):
                              st = "c"
                          elif (
                              hasattr(o[0], "is_c_contiguous")
                              and o[0].is_c_contiguous()
                          ):
                              st = "c"
                          self.variable_strides[var] = st
                          off = getattr(o[0], "offset", "")
                          self.variable_offset[var] = off
  
                  input_index = []
  
                  if self.allow_gc:
                      for i in current_apply.inputs:
                          if (
                              dependencies[i]
                              and i.owner
                              and i not in self.outputs
                          ):
                              empty_storage_map = True
                              for x in dependencies[i]:
                                  if not compute_map[x][0]:
                                      empty_storage_map = False
                                      break
                              if empty_storage_map:
                                  storage_map[i][0] = None
                                  input_index.append(
                                      current_apply.inputs.index(i)
                                  )
                                  # See the not lazy gc code for explanations
                                  # of compute_map change
                                  compute_map[i][0] = 2
  
                  self.node_cleared_order.append(input_index)
  
      # Hacky coarse gc final pass
      # This is required until we have a proper gc algorithm for graphs with
      # lazy evaluation. See discussion on theano-dev June 19 2012.
      final_index = []
  
      if self.allow_gc:
          for v in storage_map:
              if v.owner and v not in self.outputs:
                  if compute_map[v][0] == 2:
                      continue
                  else:
                      storage_map[v][0] = None
                      final_index.append(v)
                      compute_map[v][0] = 2
  
      self.node_cleared_order.append(final_index)
  
      return self.perform_updates()
  
imports: import platform
  import sys
  import time
  import warnings
  from abc import ABC, abstractmethod
  from collections import defaultdict
  from collections.abc import Sequence
  from itertools import zip_longest
  from typing import TYPE_CHECKING, Any, Optional
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Apply, Constant, Variable
  from pytensor.link.basic import Container, LocalLinker
  from pytensor.link.c.exceptions import MissingGXX
  from pytensor.link.utils import gc_helper, get_destroy_dependencies, map_storage, raise_with_op
label_desc: Use image node_id 3 for calling the Stack obj's underlying member method code with example usage: obj.__call__(output_subset) and returns: self