class_docstr: This is parameterized so that it works for `Elemwise` `Op`\s.
class_docstr_tok: ["This","is","parameterized","so","that","it","works","for","`","Elemwise","`","`","Op","`","\\s","."]
docstr: Attempts to replace all `Elemwise`\s by versions of them that operate
  inplace. It operates greedily: for each `Elemwise` that is encountered,
  for each output, it tries each input to see if it can operate inplace
  on that input. If so, it makes the change and goes to the next output
  or `Elemwise`.
  
  Examples
  --------
  
      x + y + z -> x += y += z
      (x + y) * (x * y) -> (x += y) *= (x * y) or (x + y) *= (x *= y)
code: def apply(self, fgraph):
      r"""
  
      Attempts to replace all `Elemwise`\s by versions of them that operate
      inplace. It operates greedily: for each `Elemwise` that is encountered,
      for each output, it tries each input to see if it can operate inplace
      on that input. If so, it makes the change and goes to the next output
      or `Elemwise`.
  
      Examples
      --------
  
          x + y + z -> x += y += z
          (x + y) * (x * y) -> (x += y) *= (x * y) or (x + y) *= (x *= y)
  
      """
      # We should not validate too often as this takes too much time to
      # execute!
      # It is the _dfs_toposort() fct in pytensor/graph/destroyhandler.py
      # that takes so much time.
      # Should we try to use another lib that does toposort?
      #   igraph: http://igraph.sourceforge.net/
      #   networkx: https://networkx.lanl.gov/
      # Should we try to use cython?
      #   Compiling only that fct is not enough, should we try to add the
      #   deque class too?
      #   And init the deque and other list to an upper bound number of
      #   elements?
      # Maybe PyTensor should do online toposort as in
      #   http://code.google.com/p/acyclic
      #
      # The next longest rewriter is the canonizer phase.
      # Then I think it is the [io_?]toposort (need to validate) so check if
      # the solution is also applicable there.
  
      # We execute `validate` after this number of change.
      prof = {
          "opt": self,
          "node_before": len(fgraph.apply_nodes),
          "nb_call_replace": 0,
          "nb_call_validate": 0,
          "nb_inconsistent": 0,
          "ndim": defaultdict(int),
      }
  
      check_each_change = (
          config.tensor__insert_inplace_optimizer_validate_nb
      )
      if check_each_change == -1:
          if len(fgraph.apply_nodes) > 500:
              check_each_change = 10
          else:
              check_each_change = 1
  
      nb_change_no_validate = 0
      chk = fgraph.checkpoint()
  
      if fgraph.update_mapping:
          update_outs = [
              fgraph.outputs[i] for i in fgraph.update_mapping
          ]
      else:
          update_outs = []
  
      protected_inputs = [
          f.protected
          for f in fgraph._features
          if isinstance(f, pytensor.compile.function.types.Supervisor)
      ]
      protected_inputs = sum(protected_inputs, [])  # flatten the list
      protected_inputs.extend(fgraph.outputs)
      for node in list(io_toposort(fgraph.inputs, fgraph.outputs)):
          op = node.op
          if not isinstance(op, self.op):
              continue
          # If big graph and the outputs are scalar, do not make it
          # inplace.
          if (
              check_each_change != 1
              and
              # If multiple outputs, they must all have the same size,
              # so only check the first.
              getattr(node.outputs[0].type, "ndim", -1) == 0
          ):
              continue
  
          if op.inplace_pattern:
              # Maybe this isn't needed anymore, but I don't want to
              # rish regression now. This case only happen if the
              # original node add already some inplace patter and we
              # still try to add more pattern.
  
              baseline = op.inplace_pattern
              candidate_outputs = [
                  i
                  for i in self.candidate_input_idxs(node)
                  if i not in baseline
              ]
              # node inputs that are Constant, already destroyed,
              # or fgraph protected inputs and fgraph outputs can't be used as
              # inplace target.
              # Remove here as faster.
              candidate_inputs = [
                  i
                  for i in range(len(node.inputs))
                  if i not in baseline.values()
                  and not isinstance(node.inputs[i], Constant)
                  and
                  # the next line should not be costly most of the time.
                  not fgraph.has_destroyers([node.inputs[i]])
                  and node.inputs[i] not in protected_inputs
              ]
          else:
              baseline = []
              candidate_outputs = self.candidate_input_idxs(node)
              # node inputs that are Constant, already destroyed,
              # fgraph protected inputs and fgraph outputs can't be used as inplace
              # target.
              # Remove here as faster.
              candidate_inputs = [
                  i
                  for i in range(len(node.inputs))
                  if not isinstance(node.inputs[i], Constant)
                  and not fgraph.has_destroyers([node.inputs[i]])
                  and node.inputs[i] not in protected_inputs
              ]
  
          verbose = False
  
          raised_warning = not verbose
  
          for candidate_output in candidate_outputs:
              # If the output of the node can be established as an update
              # output of the fgraph, visit the candidate_inputs in an order
              # that will improve the chances of making the node operate
              # inplace on the input it's meant to update
              candidate_out_var = node.outputs[candidate_output]
              sorted_candidate_inputs = candidate_inputs
  
              if candidate_out_var in update_outs:
                  # The candidate output is an update. Sort the
                  # variables in candidate_inputs in the following order:
                  # - Vars corresponding to the actual updated input
                  #   (best case scenario is for the node that procudes
                  #   an update to operate inplace on the variable to
                  #   update)
                  # - Vars computed inplace on the updates input (second
                  #   best scenario if for the node to work inplace on
                  #   a variable obtained by a chain of inplace on the
                  #   variable to update. In some cases, this will be
                  #   equivalent to operating inplace on the variable to
                  #   update)
                  # - Remaining variables
                  updated_inputs = []
                  for i, f_out in enumerate(fgraph.outputs):
                      if (
                          f_out is candidate_out_var
                          and i in fgraph.update_mapping
                      ):
                          updated_inp_idx = fgraph.update_mapping[i]
                          updated_inputs.append(
                              fgraph.inputs[updated_inp_idx]
                          )
  
                  updated_vars = []
                  vars_from_inplace = []
                  other_vars = []
                  for inp_idx in candidate_inputs:
                      inp = node.inputs[inp_idx]
                      if inp in updated_inputs:
                          # the candidate input is the actual updated input
                          updated_vars.append(inp_idx)
                      elif (
                          hasattr(fgraph, "destroy_handler")
                          and inp.owner
                          and any(
                              fgraph.destroy_handler.root_destroyer.get(
                                  up_inp, None
                              )
                              is inp.owner
                              for up_inp in updated_inputs
                          )
                      ):
                          # the candidate input is a variable computed
                          # inplace on the updated input via a sequence of
                          # one or more inplace operations
                          vars_from_inplace.append(inp_idx)
                      else:
                          other_vars.append(inp_idx)
  
                  sorted_candidate_inputs = (
                      updated_vars + vars_from_inplace + other_vars
                  )
  
              for candidate_input in sorted_candidate_inputs:
                  # remove inputs that don't have the same dtype as the output
                  if (
                      node.inputs[candidate_input].type
                      != node.outputs[candidate_output].type
                  ):
                      continue
  
                  inplace_pattern = dict(baseline)
                  inplace_pattern[candidate_output] = candidate_input
                  try:
                      if hasattr(op.scalar_op, "make_new_inplace"):
                          new_scal = op.scalar_op.make_new_inplace(
                              ps.transfer_type(
                                  *[
                                      inplace_pattern.get(i, o.dtype)
                                      for i, o in enumerate(
                                          node.outputs
                                      )
                                  ]
                              )
                          )
                      else:
                          new_scal = op.scalar_op.__class__(
                              ps.transfer_type(
                                  *[
                                      inplace_pattern.get(i, None)
                                      for i in range(len(node.outputs))
                                  ]
                              )
                          )
                      new_outputs = self.op(new_scal, inplace_pattern)(
                          *node.inputs, return_list=True
                      )
                      new_node = new_outputs[0].owner
  
                      for r, new_r in zip(node.outputs, new_outputs):
                          prof["nb_call_replace"] += 1
                          fgraph.replace(
                              r,
                              new_r,
                              reason="inplace_elemwise_optimizer",
                          )
                      nb_change_no_validate += 1
                      prof["ndim"][candidate_out_var.ndim] += 1
                      if nb_change_no_validate >= check_each_change:
                          prof["nb_call_validate"] += 1
                          fgraph.validate()
                          chk = fgraph.checkpoint()
                          nb_change_no_validate = 0
                  except (ValueError, InconsistencyError) as e:
                      prof["nb_inconsistent"] += 1
                      if check_each_change != 1 and not raised_warning:
                          print(
                              (
                                  "Some inplace rewriting was not "
                                  "performed due to an unexpected error:"
                              ),
                              file=sys.stderr,
                          )
                          print(e, file=sys.stderr)
                          raised_warning = True
                      fgraph.revert(chk)
                      continue
                  candidate_inputs.remove(candidate_input)
                  node = new_node
                  baseline = inplace_pattern
                  break
  
      if nb_change_no_validate > 0:
          try:
              fgraph.validate()
          except Exception:
              if not raised_warning:
                  print(
                      (
                          "Some inplace rewriting was not "
                          "performed due to an unexpected error"
                      ),
                      file=sys.stderr,
                  )
              fgraph.revert(chk)
      return prof
  
imports: import sys
  from collections import defaultdict, deque
  from collections.abc import Generator
  from functools import cache
  from typing import TypeVar
  from warnings import warn
  import pytensor
  import pytensor.scalar.basic
  from pytensor import clone_replace, compile
  from pytensor.compile.mode import get_target_language
  from pytensor.configdefaults import config
  from pytensor.graph import FunctionGraph
  from pytensor.graph.basic import Apply, Constant, Variable, ancestors, io_toposort
  from pytensor.graph.features import ReplaceValidate
  from pytensor.graph.fg import ApplyOrOutput
  from pytensor.graph.rewriting.basic import EquilibriumGraphRewriter, GraphRewriter, copy_stack_trace, in2out, node_rewriter
  from pytensor.graph.rewriting.db import SequenceDB
  from pytensor.graph.utils import InconsistencyError, MethodNotDefined
  from pytensor.scalar.loop import ScalarLoop
  from pytensor.scalar.math import Grad2F1Loop, _grad_2f1_loop
  from pytensor.tensor.basic import MakeVector, alloc, cast, get_underlying_scalar_constant_value
  from pytensor.tensor.elemwise import CAReduce, DimShuffle, Elemwise
  from pytensor.tensor.exceptions import NotScalarConstantError
  from pytensor.tensor.math import exp
  from pytensor.tensor.rewriting.basic import alloc_like, register_canonicalize, register_specialize
  from pytensor.tensor.shape import shape_padleft
  from pytensor.tensor.variable import TensorConstant, get_unique_constant_value
label_desc: Use image node_id 5 for calling the InplaceElemwiseOptimizer obj's underlying member method code with example usage: obj.apply(fgraph) and returns: prof