code: def adjoint(
      self, sum_op, bin_op, root, targets=None, *, batch_vars=set()
  ):
      zero = to_funsor(ops.UNITS[sum_op])
      one = to_funsor(ops.UNITS[bin_op])
      adjoint_values = defaultdict(lambda: zero)
      adjoint_values[root] = one
  
      reached_root = False
      while self.tape:
          output, fn, inputs = self.tape.pop()
          if not reached_root:
              if output is root:
                  reached_root = True
              else:
                  continue
  
          # reverse the effects of alpha-renaming
          with reflect:
              lazy_output = self._eager_to_lazy[output]
              lazy_fn = type(lazy_output)
              lazy_inputs = lazy_output._ast_values
              # TODO abstract this into a helper function
              # FIXME make lazy_output linear instead of quadratic in the size of the tape
              lazy_other_subs = tuple(
                  (name, to_funsor(name.split("__BOUND")[0], domain))
                  for name, domain in lazy_output.inputs.items()
                  if "__BOUND" in name
              )
              lazy_inputs = _alpha_unmangle(
                  substitute(lazy_fn(*lazy_inputs), lazy_other_subs)
              )
              lazy_output = type(lazy_output)(
                  *_alpha_unmangle(
                      substitute(lazy_output, lazy_other_subs)
                  )
              )
  
              other_subs = tuple(
                  (name, to_funsor(name.split("__BOUND")[0], domain))
                  for name, domain in output.inputs.items()
                  if "__BOUND" in name
              )
              inputs = _alpha_unmangle(
                  substitute(fn(*inputs), other_subs)
              )
              output = type(output)(
                  *_alpha_unmangle(substitute(output, other_subs))
              )
  
              self._eager_to_lazy[output] = lazy_output
  
          in_adjs = adjoint_ops(
              fn, sum_op, bin_op, adjoint_values[output], *inputs
          )
          for v, adjv in in_adjs:
              # Marginalize out message variables that don't appear in recipients.
              agg_vars = (
                  adjv.input_vars
                  - v.input_vars
                  - root.input_vars
                  - batch_vars
              )
              assert "particle" not in {
                  var.name for var in agg_vars
              }  # DEBUG FIXME
              old_value = adjoint_values[v]
              adjoint_values[v] = sum_op(
                  old_value, adjv.reduce(sum_op, agg_vars)
              )
  
      result = defaultdict(lambda: zero)
      for key, value in adjoint_values.items():
          lazy_key = self._eager_to_lazy.get(key, key)
          result[lazy_key] = value
  
      if targets is None:
          return result
      return {target: result[target] for target in targets}
  
imports: from collections import defaultdict
  from collections.abc import Hashable
  from funsor.cnf import Contraction
  from funsor.interpretations import Interpretation, reflect
  from funsor.interpreter import stack_reinterpret
  from funsor.ops import AssociativeOp
  from funsor.registry import KeyedRegistry
  from funsor.terms import Approximate, Binary, Cat, Funsor, Reduce, Scatter, Slice, Subs, substitute, to_funsor
  from .None import instrument, interpreter, ops
label_desc: Use image node_id 4 for calling the AdjointTape obj's underlying member method code with example usage: obj.adjoint(sum_op, bin_op, root, targets) and returns: unknown, result