class_docstr: A Pipenet object connects transformers, detectors and aggregators.
  
  Parameters
  ----------
  steps: dicts
      Components of the pipenet. Each key-value item represents a step (
      transformer, detector, or aggregator), where key is the unique name of
      the step and the value is a dict with the following key-value pairs:
  
          - input (str or list of str): Input to the model, which must be
            either 'original' (i.e. the input time series), or the name of
            a upstream component.
          - subset (str, list of str, or list of lists of str, optional): If
            a model does not use all series from an input component, use this
            field to specify which series should be included. If not given or
            "all", all series from the input component will be used.
          - model (object): A detector, transformer, or aggregator object.
  
  Attributes
  ----------
  steps_graph_: OrderedDict
      Order of steps to be executed. Keys are step names, values are 2-tuple
      (i, j) where i is the index of execution round and j is the the index
      within a round.
  
  final_step_: str
      Name of the final step to be executed. It is the single step in the
      last round of execution in attribute `steps_graph_`.
  
  Examples
  --------
  The following example show how to use a Pipenet to build a level shift
  detector with some basic transformers, detectors, and aggregator.
  
  >>> from adtk.detector import QuantileAD, ThresholdAD
  >>> from adtk.transformer import DoubleRollingAggregate
  >>> from adtk.aggregator import AndAggregator
  >>> from adtk.pipe import Pipenet
  >>> steps = {
          "diff_abs": {
              "input": "original",
              "model": DoubleRollingAggregate(
                  agg="median",
                  window=20,
                  center=True,
                  diff="l1",
              ),
          },
          "quantile_ad": {
              "input": "diff_abs",
              "model": QuantileAD(high=0.99, low=0),
          },
          "diff": {
              "input": "original",
              "model": DoubleRollingAggregate(
                  agg="median",
                  window=20,
                  center=True,
                  diff="diff",
              ),
          },
          "sign_check": {
              "input": "diff",
              "model": ThresholdAD(high=0.0, low=-float("inf")),
          },
          "and": {
              "model": AndAggregator(),
              "input": ["quantile_ad", "sign_check"],
          },
      }
  >>> myPipenet = Pipenet(steps)
docstr: Check the following issues and raise error if found
  - steps is not a list of dict
  - some step does not have required keys
  - some step has invalid keys other than required and optional keys
  - some step's `name` is not str
  - some steps have duplicated name
  - some step uses reserved name
  - some step's `input` is not str or list of str
  - some step's `input` is not a step name or "original"
  - some step's `subset` (optional) is not a str ("all"),
      or a list of (str ("all") or list of str)
  - some step's `subset` does not have the same length as `input`
  - aggregator has a `subset` field
  - some step's `model` is not a valid ADTK model object
  - upstream of a transformer or a detector must be a transformer
  - upstream of aggregator must be a detector or an aggregator
  
  Create a directed graph of model execution flowchart, when following
  issues will be checked:
  - some step cannot be reached
  - more than one outlet steps found
docstr_tok: ["Check","the","following","issues","and","raise","error","if","found","-","steps","is","not","a","list","of","dict","-","some","step","does","not","have","required","keys","-","some","step","has","invalid","keys","other","than","required","and","optional","keys","-","some","step","'s","`","name","`","is","not","str","-","some","steps","have","duplicated","name","-","some","step","uses","reserved","name","-","some","step","'s","`","input","`","is","not","str","or","list","of","str","-","some","step","'s","`","input","`","is","not","a","step","name","or","``","original","''","-","some","step","'s","`","subset","`","(","optional",")","is","not","a","str","(","``","all","''",")",",","or","a","list","of","(","str","(","``","all","''",")","or","list","of","str",")","-","some","step","'s","`","subset","`","does","not","have","the","same","length","as","`","input","`","-","aggregator","has","a","`","subset","`","field","-","some","step","'s","`","model","`","is","not","a","valid","ADTK","model","object","-","upstream","of","a","transformer","or","a","detector","must","be","a","transformer","-","upstream","of","aggregator","must","be","a","detector","or","an","aggregator","Create","a","directed","graph","of","model","execution","flowchart",",","when","following","issues","will","be","checked",":","-","some","step","can","not","be","reached","-","more","than","one","outlet","steps","found"]
code: def _validate(self) -> None:
      """
      Check the following issues and raise error if found
      - steps is not a list of dict
      - some step does not have required keys
      - some step has invalid keys other than required and optional keys
      - some step's `name` is not str
      - some steps have duplicated name
      - some step uses reserved name
      - some step's `input` is not str or list of str
      - some step's `input` is not a step name or "original"
      - some step's `subset` (optional) is not a str ("all"),
          or a list of (str ("all") or list of str)
      - some step's `subset` does not have the same length as `input`
      - aggregator has a `subset` field
      - some step's `model` is not a valid ADTK model object
      - upstream of a transformer or a detector must be a transformer
      - upstream of aggregator must be a detector or an aggregator
  
      Create a directed graph of model execution flowchart, when following
      issues will be checked:
      - some step cannot be reached
      - more than one outlet steps found
  
      """
  
      # check if step is a dict
      if not isinstance(self.steps, dict):
          raise TypeError("`steps` must be a dict objects.")
      if not all(
          [isinstance(value, dict) for value in self.steps.values()]
      ):
          raise TypeError(
              "Values of dict `steps` must be dict objects."
          )
  
      # check if each step has valid keys
      if not all(
          [
              {"model", "input"}
              <= step.keys()
              <= {"model", "input", "subset"}
              for step in self.steps.values()
          ]
      ):
          raise KeyError(
              "Each step must be a dict object with keys `model`, `input`, "
              "and `subset` (optional)."
          )
  
      # check if each step has valid name
      if not all(
          [
              isinstance(step_name, str)
              for step_name in self.steps.keys()
          ]
      ):
          raise TypeError("Model name must be a string.")
      if any(
          [step_name == "original" for step_name in self.steps.keys()]
      ):
          raise ValueError(
              "'original' is a reserved name for original time series input "
              "and you may not use it as a model name."
          )
  
      # check if each step has valid input
      def islistofstr(li: Any) -> bool:
          if not isinstance(li, list):
              return False
          if not all([isinstance(x, str) for x in li]):
              return False
          return True
  
      if not all(
          [
              isinstance(step["input"], str)
              or islistofstr(step["input"])
              for step in self.steps.values()
          ]
      ):
          raise TypeError(
              "Field `input` must be a string or a list of strings."
          )
      name_set = set(self.steps.keys()).union({"original"})
      for step in self.steps.values():
          if (
              isinstance(step["input"], str)
              and (step["input"] not in name_set)
          ) or (
              isinstance(step["input"], list)
              and (not set(step["input"]) <= name_set)
          ):
              raise ValueError(
                  "Field `input` must be 'original' or name of a model."
              )
  
      # check if only one step is not used as input (so it is the output)
      for step in self.steps.values():
          if isinstance(step["input"], str):
              name_set = name_set - {step["input"]}
          else:
              name_set = name_set - set(step["input"])
      if len(name_set) > 1:
          raise ValueError(
              "Pipenet output is ambiguous: "
              "found more than one steps with no downstream step: {}.".format(
                  name_set
              )
          )
  
      # check if each step has a valid subset, or has no subset (then we
      # will add default one to it)
      for step_name, step in self.steps.items():
          if isinstance(step["input"], str):
              if "subset" not in step.keys():
                  pass
              elif not (
                  (
                      isinstance(step["subset"], str)
                      or islistofstr(step["subset"])
                  )
              ):
                  raise TypeError(
                      "Field `subset` at step '{}' is invalid.".format(
                          step_name
                      )
                  )
              elif isinstance(step["subset"], str) and (
                  step["subset"] != "all"
              ):
                  raise ValueError(
                      "A subset corresponding to an input source must be "
                      "'all' or a list of strings (even if there is only "
                      "one element)."
                  )
          else:
              if "subset" not in step.keys():
                  pass
              elif isinstance(step["subset"], str):
                  if step["subset"] == "all":
                      pass
                  else:
                      raise ValueError(
                          "Field `subset` at step '{}' is invalid.".format(
                              step_name
                          )
                      )
              elif isinstance(step["subset"], list):
                  if len(step["input"]) != len(step["subset"]):
                      raise ValueError(
                          "Fields `input` and `subset` are inconsistent at "
                          "step '{}'.".format(step_name)
                      )
                  for subset in step["subset"]:
                      if not (
                          (
                              isinstance(subset, str)
                              or islistofstr(subset)
                          )
                      ):
                          raise TypeError(
                              "Field `subset` at step '{}' is invalid.".format(
                                  step_name
                              )
                          )
                      if isinstance(subset, str) and (subset != "all"):
                          raise ValueError(
                              "A subset corresponding to an input source "
                              "must be 'all' or a list of strings (even if "
                              "there is only one element)."
                          )
              else:
                  raise TypeError(
                      "Field `subset` at step '{}' is invalid.".format(
                          step_name
                      )
                  )
  
      # check if each step has valid model
      if not all(
          [
              isinstance(step["model"], _Model)
              for step in self.steps.values()
          ]
      ):
          raise ValueError(
              "Model must be a detector, transformer, or aggregator object."
          )
  
      # check:
      # 1. upstream of transformer and detector must be transformer, or input
      # 2. upstream of aggregator must be detector or aggregator
      for step_name, step in self.steps.items():
          if isinstance(step["model"], (_Detector, _Transformer)):
              if isinstance(step["input"], str):
                  if step["input"] == "original":
                      pass
                  elif not isinstance(
                      self.steps[step["input"]]["model"], _Transformer
                  ):
                      raise TypeError(
                          "Model in step '{}' cannot accept output from "
                          "step '{}'.".format(step_name, step["input"])
                      )
              else:
                  for input in step["input"]:
                      if input == "original":
                          pass
                      elif not isinstance(
                          self.steps[input]["model"], _Transformer
                      ):
                          raise TypeError(
                              "Model in step '{}' cannot accept output from "
                              "step '{}'.".format(step_name, input)
                          )
          elif isinstance(step["model"], _Aggregator):
              if isinstance(step["input"], str):
                  if (step["input"] == "original") or (
                      not isinstance(
                          self.steps[step["input"]]["model"],
                          (_Detector, _Aggregator),
                      )
                  ):
                      raise TypeError(
                          "Model in step '{}' cannot accept output from "
                          "step '{}'.".format(step_name, step["input"])
                      )
              else:
                  for input in step["input"]:
                      if (input == "original") or (
                          not isinstance(
                              self.steps[input]["model"],
                              (_Detector, _Aggregator),
                          )
                      ):
                          raise TypeError(
                              "Model in step '{}' cannot accept output from "
                              "step '{}'.".format(step_name, input)
                          )
  
      # sort out graph
      done_step = OrderedDict({"original": (0, 0)})
      counter = 0
      while True:
          counter += 1
          sub_counter = 0
          done_step_name_up_to_last_round = set(done_step.keys())
          for step_name, step in self.steps.items():
              if step_name in done_step_name_up_to_last_round:
                  continue
              if isinstance(step["input"], str):
                  if step["input"] in done_step_name_up_to_last_round:
                      done_step.update(
                          {step_name: (counter, sub_counter)}
                      )
                      sub_counter += 1
              else:
                  if (
                      set(step["input"])
                      <= done_step_name_up_to_last_round
                  ):
                      done_step.update(
                          {step_name: (counter, sub_counter)}
                      )
                      sub_counter += 1
          if len(done_step) == len(self.steps) + 1:
              break
          if sub_counter == 0:
              raise ValueError(
                  "The following step(s) cannot be reached: {}.".format(
                      set(self.steps.keys()) - set(done_step.keys())
                  )
              )
      self.steps_graph_ = done_step.copy()
      self.final_step_ = list(self.steps_graph_.keys())[-1]
  
imports: from collections import OrderedDict
  from typing import Any, Callable, Dict, List, Optional, Tuple, Union
  import matplotlib.pyplot
  import pandas
  from matplotlib.collections import PatchCollection
  from matplotlib.lines import Line2D
  from matplotlib.patches import Circle
  from tabulate import tabulate
  from .._aggregator_base import _Aggregator
  from .._base import _Model, _TrainableModel
  from .._detector_base import _NonTrainableUnivariateDetector, _TrainableMultivariateDetector, _TrainableUnivariateDetector
  from .._transformer_base import _NonTrainableMultivariateTransformer, _NonTrainableUnivariateTransformer, _TrainableMultivariateTransformer, _TrainableUnivariateTransformer
  from ..metrics import f1_score, iou, precision, recall
label_desc: Use image node_id 2 for calling the Pipenet obj's underlying member method code with example usage: obj._validate() and returns: True, False, False