docs: def add(self, name, interface, inputs=None, outputs=None,
              requirements=None, wall_time=None, annotations=None, **kwargs):
          """
          Adds a processing Node to the pipeline
  
          Parameters
          ----------
          name : str
              Name for the node
          interface : nipype.Interface
              The interface to use for the node
          inputs : dict[str, (str, FileFormat) | (Node, str)]
              Connections from inputs of the pipeline and outputs of other nodes
              to inputs of node. The keys of the dictionary are the field names
              and the values are 2-tuple containing either the name of the data
              spec and the data format it is expected in for pipeline inputs or
              the sending Node and the the name of an output of the sending Node.
              Note that pipeline inputs can be specified outside this method
              using the 'connect_input' method and connections between nodes with
              the the 'connect' method.
          outputs : dict[str, (str, FileFormat)]
              Connections to outputs of the pipeline from fields of the
              interface. The keys of the dictionary are the names of the data
              specs that will be written to and the values are the interface
              field name and the data format it is produced in. Note that output
              connections can also be specified using the 'connect_output'
              method.
          requirements : list(Requirement)
              List of required packages need for the node to run (default: [])
          wall_time : float
              Time required to execute the node in minutes (default: 1)
          mem_gb : int
              Required memory for the node in GB
          n_procs : int
              Preferred number of threads to run the node on (default: 1)
          annotations : dict[str, *]
              Additional annotations to add to the node, which may be used by
              the Processor node to optimise execution (e.g. 'gpu': True)
          iterfield : str
              Name of field to be passed an iterable to iterator over.
              If present, a MapNode will be created instead of a regular node
          joinsource : str
              Name of iterator field to join. Typically one of the implicit
              iterators (i.e. Study.SUBJECT_ID or Study.VISIT_ID)
              to join over the subjects and/or visits
          joinfield : str
              Name of field to pass the joined list when creating a JoinNode
  
          Returns
          -------
          node : Node
              The Node object that has been added to the pipeline
          """
          if annotations is None:
              annotations = {}
          if requirements is None:
              requirements = []
          if wall_time is None:
              wall_time = self.study.processor.default_wall_time
          if 'mem_gb' not in kwargs or kwargs['mem_gb'] is None:
              kwargs['mem_gb'] = self.study.processor.default_mem_gb
          if 'iterfield' in kwargs:
              if 'joinfield' in kwargs or 'joinsource' in kwargs:
                  raise ArcanaDesignError(
                      "Cannot provide both joinsource and iterfield to when "
                      "attempting to add '{}' node to {}"
                      .foramt(name, self._error_msg_loc))
              node_cls = self.study.environment.node_types['map']
          elif 'joinsource' in kwargs or 'joinfield' in kwargs:
              if not ('joinfield' in kwargs and 'joinsource' in kwargs):
                  raise ArcanaDesignError(
                      "Both joinsource and joinfield kwargs are required to "
                      "create a JoinNode (see {})".format(name,
                                                          self._error_msg_loc))
              joinsource = kwargs['joinsource']
              if joinsource in self.study.ITERFIELDS:
                  self._iterator_joins.add(joinsource)
              node_cls = self.study.environment.node_types['join']
              # Prepend name of pipeline of joinsource to match name of nodes
              kwargs['joinsource'] = '{}_{}'.format(self.name, joinsource)
          else:
              node_cls = self.study.environment.node_types['base']
          # Create node
          node = node_cls(self.study.environment,
                          interface,
                          name="{}_{}".format(self._name, name),
                          requirements=requirements,
                          wall_time=wall_time,
                          annotations=annotations,
                          **kwargs)
          # Ensure node is added to workflow
          self._workflow.add_nodes([node])
          # Connect inputs, outputs and internal connections
          if inputs is not None:
              assert isinstance(inputs, dict)
              for node_input, connect_from in inputs.items():
                  if isinstance(connect_from[0], basestring):
                      input_spec, input_format = connect_from
                      self.connect_input(input_spec, node,
                                         node_input, input_format)
                  else:
                      conn_node, conn_field = connect_from
                      self.connect(conn_node, conn_field, node, node_input)
          if outputs is not None:
              assert isinstance(outputs, dict)
              for output_spec, (node_output, output_format) in outputs.items():
                  self.connect_output(output_spec, node, node_output,
                                      output_format)
          return node