class_docstr: A `FunctionGraph` represents a subgraph bound by a set of input variables and
  a set of output variables, ie a subgraph that specifies an PyTensor function.
  The inputs list should contain all the inputs on which the outputs depend.
  `Variable`\s of type `Constant` are not counted as inputs.
  
  The `FunctionGraph` supports the replace operation which allows to replace
  a variable in the subgraph by another, e.g. replace ``(x + x).out`` by
  ``(2 * x).out``. This is the basis for optimization in PyTensor.
  
  This class is also responsible for verifying that a graph is valid
  (ie, all the dtypes and broadcast patterns are compatible with the
  way the `Variable`\s are used) and for tracking the `Variable`\s with
  a :attr:`FunctionGraph.clients` ``dict`` that specifies which `Apply` nodes
  use the `Variable`.  The :attr:`FunctionGraph.clients` field, combined with
  the :attr:`Variable.owner` and each :attr:`Apply.inputs`, allows the graph
  to be traversed in both directions.
  
  It can also be extended with new features using
  :meth:`FunctionGraph.attach_feature`.  See `Feature` for event types and
  documentation.  Extra features allow the `FunctionGraph` to verify new
  properties of a graph as it is optimized.
  
  The constructor creates a `FunctionGraph` which operates on the subgraph
  bound by the inputs and outputs sets.
  
  This class keeps lists for the inputs and outputs and modifies them
  in-place.
docstr: Remove an `Apply` node from the `FunctionGraph`.
  
  This will remove everything that depends on the outputs of `node`, as
  well as any "orphaned" variables and nodes created by `node`'s removal.
docstr_tok: ["Remove","an","`","Apply","`","node","from","the","`","FunctionGraph","`",".","This","will","remove","everything","that","depends","on","the","outputs","of","`","node","`",",","as","well","as","any","``","orphaned","''","variables","and","nodes","created","by","`","node","`","'s","removal","."]
code: def remove_node(self, node: Apply, reason: Optional[str] = None):
      """Remove an `Apply` node from the `FunctionGraph`.
  
      This will remove everything that depends on the outputs of `node`, as
      well as any "orphaned" variables and nodes created by `node`'s removal.
      """
  
      if node not in self.apply_nodes:
          return
  
      self.apply_nodes.remove(node)
  
      if not hasattr(node.tag, "removed_by"):
          node.tag.removed_by = []
  
      node.tag.removed_by.append(str(reason))
  
      # Remove the outputs of the node (i.e. everything "below" it)
      for out in node.outputs:
          self.variables.remove(out)
  
          out_clients = self.clients.get(out, ())
          while out_clients:
              out_client, out_idx = out_clients.pop()
  
              if out_client == "output":
                  self._remove_output(out_idx)
  
                  # TODO: We could short-circuit all of the graph walking and
                  # clear everything at once when all the outputs are gone.
                  # if not self.outputs:
                  #     self.clients = {inp: [] for inp in self.inputs}
                  #     self.variables = set()
                  #     while self.apply_nodes:
                  #         node = self.apply_nodes.pop()
                  #         if not hasattr(node.tag, "removed_by"):
                  #             node.tag.removed_by = []
                  #
                  #         node.tag.removed_by.append(str(reason))
                  #
                  #         self.execute_callbacks("on_prune", node, reason)
              else:
                  assert isinstance(out_client, Apply)
                  self.remove_node(out_client, reason=reason)
  
          if out in self.clients:
              del self.clients[out]
  
      # Remove all the arrows pointing to this `node`, and any orphaned
      # variables created by removing those arrows
      for inp_idx, inp in enumerate(node.inputs):
          inp_clients: list[ClientType] = self.clients.get(inp, [])
  
          arrow = (node, inp_idx)
  
          if arrow not in inp_clients:
              continue
  
          inp_clients.remove(arrow)
  
          if not inp_clients and inp not in self.outputs:
              if inp.owner:
                  # If this input has no clients (after removing this arrow),
                  # is not an input (i.e. it has a non-`None` owner) or an
                  # output to the `FunctionGraph`, then it's an orphan
  
                  # We need to check whether or not this orphaned input's
                  # node is still needed in the graph
                  inp_node = inp.owner
  
                  if not any(
                      out in self.variables
                      for out in inp_node.outputs
                      if out is not inp
                  ):
                      self.remove_node(inp_node, reason=reason)
              else:
                  # This is an unused input
                  self.variables.remove(inp)
  
      # The callbacks be triggered after everything has been removed so that
      # the `FunctionGraph` state subscribers see is valid.
      self.execute_callbacks("on_prune", node, reason)
  
imports: import time
  from collections import OrderedDict
  from collections.abc import Iterable, Sequence
  from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
  import pytensor
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Apply, AtomicVariable, Variable, applys_between, clone_get_equiv, graph_inputs, io_toposort, vars_between
  from pytensor.graph.basic import as_string
  from pytensor.graph.features import AlreadyThere, Feature, ReplaceValidate
  from pytensor.graph.utils import MetaObject, MissingInputError, TestValueError
  from pytensor.misc.ordered_set import OrderedSet
label_desc: Use image node_id 14 for calling the FunctionGraph obj's underlying member method code with example usage: obj.remove_node(node, reason) without return types