class_docstr: A Model is a network (more precisely, a directed acyclic graph) of Steps,
  and it is defined from the input/output specification of the pipeline.
  Models have fit and predict routines that, together with graph-based engine,
  allow the automatic (feed-forward) computation of each of the pipeline steps
  when fed with data.
  
  Parameters
  ----------
  inputs
      Inputs to the model.
  
  outputs
      Outputs of the model.
  
  targets
      Targets of the model.
  
  name
      Name of the model (optional). If no name is passed, a name will be
      automatically generated.
docstr: Backtrack from the desired outputs until the given inputs and targets
  to get the required steps. That is, find the ancestors of the nodes that
  provide the desired outputs. Raise an error if there is an ancestor whose
  input/target is not in the given inputs/targets. We assume a DAG (guaranteed
  by the success of topological_sort) and unique given_inputs, given_targets
  and required_outputs (guaranteed by the callers of this function).
  
  inputs and targets are handled separately to allow ignoring the targets
  when getting the required steps at predict time.
  
  Unused inputs might be allowed. This is the case in predict.
  Unused targets might be allowed. This is the case in fit.
docstr_tok: ["Backtrack","from","the","desired","outputs","until","the","given","inputs","and","targets","to","get","the","required","steps",".","That","is",",","find","the","ancestors","of","the","nodes","that","provide","the","desired","outputs",".","Raise","an","error","if","there","is","an","ancestor","whose","input\/target","is","not","in","the","given","inputs\/targets",".","We","assume","a","DAG","(","guaranteed","by","the","success","of","topological_sort",")","and","unique","given_inputs",",","given_targets","and","required_outputs","(","guaranteed","by","the","callers","of","this","function",")",".","inputs","and","targets","are","handled","separately","to","allow","ignoring","the","targets","when","getting","the","required","steps","at","predict","time",".","Unused","inputs","might","be","allowed",".","This","is","the","case","in","predict",".","Unused","targets","might","be","allowed",".","This","is","the","case","in","fit","."]
code: def _get_required_nodes(
      self,
      given_inputs: Iterable[DataPlaceholder],
      given_targets: Iterable[DataPlaceholder],
      desired_outputs: Iterable[DataPlaceholder],
      *,
      allow_unused_inputs=False,
      allow_unused_targets=False,
      follow_targets=True,
      ignore_trainable_false=True,
  ) -> List[Node]:
      """Backtrack from the desired outputs until the given inputs and targets
      to get the required steps. That is, find the ancestors of the nodes that
      provide the desired outputs. Raise an error if there is an ancestor whose
      input/target is not in the given inputs/targets. We assume a DAG (guaranteed
      by the success of topological_sort) and unique given_inputs, given_targets
      and required_outputs (guaranteed by the callers of this function).
  
      inputs and targets are handled separately to allow ignoring the targets
      when getting the required steps at predict time.
  
      Unused inputs might be allowed. This is the case in predict.
      Unused targets might be allowed. This is the case in fit.
      """
      trainable_flags = tuple(node.trainable for node in self._graph)
  
      # We use as keys all the information that affects the
      # computation of the required steps
      cache_key = (
          tuple(sorted(given_inputs)),
          tuple(sorted(given_targets)),
          tuple(sorted(desired_outputs)),
          allow_unused_inputs,
          allow_unused_targets,
          follow_targets,
          ignore_trainable_false,
          trainable_flags,
      )
  
      if cache_key in self._nodes_cache:
          return self._nodes_cache[cache_key]
  
      given_inputs = set(given_inputs)
      given_targets = set(given_targets)
      desired_outputs = set(desired_outputs)
      given_inputs_found = set()  # type: Set[DataPlaceholder]
      given_targets_found = set()  # type: Set[DataPlaceholder]
      required_nodes = set()  # type: Set[Node]
  
      # Depth-first search
      # backtracking stops if any of the following happen:
      #   - found a given input or target
      #   - found a known required step
      #   - hit an InputStep
      def backtrack(output):
          nodes_required_by_output = set()
  
          if output in given_inputs:
              given_inputs_found.add(output)
              return nodes_required_by_output
  
          if output in given_targets:
              given_targets_found.add(output)
              return nodes_required_by_output
  
          parent_node = output.node
          if parent_node in required_nodes:
              return nodes_required_by_output
  
          nodes_required_by_output = {parent_node}
          for input in parent_node.inputs:
              nodes_required_by_output |= backtrack(input)
  
          if follow_targets and (
              parent_node.trainable or ignore_trainable_false
          ):
              for target in parent_node.targets:
                  nodes_required_by_output |= backtrack(target)
  
          return nodes_required_by_output
  
      for output in desired_outputs:
          required_nodes |= backtrack(output)
  
      # Check for missing inputs/targets
      # InputSteps that were reached by backtracking are missing inputs/targets.
      # We *do not* compare given_inputs_found/given_targets_found with
      # self._internal_inputs/self._internal_targets because we allow giving
      # intermediate inputs directly.
      missing_inputs_or_targets = set()  # type: Set[DataPlaceholder]
      for node in required_nodes:
          if isinstance(node.step, InputStep):
              missing_inputs_or_targets |= set(node.outputs)
  
      if missing_inputs_or_targets:
          raise ValueError(
              "The following inputs or targets are required but were not given:\n"
              "{}".format(
                  ",".join(
                      [
                          input.name
                          for input in missing_inputs_or_targets
                      ]
                  )
              )
          )
  
      # Check for any unused inputs/targets
      unused_inputs = given_inputs - given_inputs_found
      if unused_inputs and not allow_unused_inputs:
          raise ValueError(
              "The following inputs were given but are not required:\n{}".format(
                  ",".join([input.name for input in unused_inputs])
              )
          )
  
      unused_targets = given_targets - given_targets_found
      if unused_targets and not allow_unused_targets:
          raise ValueError(
              "The following targets were given but are not required:\n{}".format(
                  ",".join([target.name for target in unused_targets])
              )
          )
  
      required_nodes_sorted = [
          node
          for node in self._all_nodes_sorted
          if node in required_nodes
      ]
      self._nodes_cache[cache_key] = required_nodes_sorted
  
      return required_nodes_sorted
  
imports: from collections import defaultdict
  from typing import Union, List, Dict, Set, Iterable, Optional
  from baikal._core.data_placeholder import is_data_placeholder_list, DataPlaceholder
  from baikal._core.digraph import DiGraph
  from baikal._core.step import Step, InputStep, Node
  from baikal._core.typing import ArrayLike
  from baikal._core.utils import listify, safezip2, SimpleCache, unlistify
label_desc: Use image node_id 3 for calling the Model obj's underlying member method code with example usage: obj._get_required_nodes(given_inputs, given_targets, desired_outputs) and returns: required_nodes_sorted, self, nodes_required_by_output, nodes_required_by_output, nodes_required_by_output, nodes_required_by_output