docs: def add_hyperedge(self, tail, head, attr_dict=None, **attr):
          """Adds a hyperedge to the hypergraph, along with any related
          attributes of the hyperedge.
          This method will automatically add any node from the tail and
          head that was not in the hypergraph.
          A hyperedge without a "weight" attribute specified will be
          assigned the default value of 1.
  
          :param tail: iterable container of references to nodes in the
                      tail of the hyperedge to be added.
          :param head: iterable container of references to nodes in the
                      head of the hyperedge to be added.
          :param attr_dict: dictionary of attributes shared by all
                      the hyperedges.
          :param attr: keyword arguments of attributes of the hyperedge;
                      attr's values will override attr_dict's values
                      if both are provided.
          :returns: str -- the ID of the hyperedge that was added.
          :raises: ValueError -- tail and head arguments cannot both be empty.
  
          Examples:
          ::
  
              >>> H = DirectedHypergraph()
              >>> x = H.add_hyperedge(["A", "B"], ["C", "D"])
              >>> y = H.add_hyperedge(("A", "C"), ("B"), 'weight'=2)
              >>> z = H.add_hyperedge(set(["D"]),
                                      set(["A", "C"]),
                                      {color: "red"})
  
          """
          attr_dict = self._combine_attribute_arguments(attr_dict, attr)
  
          # Don't allow both empty tail and head containers (invalid hyperedge)
          if not tail and not head:
              raise ValueError("tail and head arguments \
                               cannot both be empty.")
  
          # Use frozensets for tail and head sets to allow for hashable keys
          frozen_tail = frozenset(tail)
          frozen_head = frozenset(head)
  
          # Initialize a successor dictionary for the tail and head, respectively
          if frozen_tail not in self._successors:
              self._successors[frozen_tail] = {}
          if frozen_head not in self._predecessors:
              self._predecessors[frozen_head] = {}
  
          is_new_hyperedge = not self.has_hyperedge(frozen_tail, frozen_head)
          if is_new_hyperedge:
              # Add tail and head nodes to graph (if not already present)
              self.add_nodes(frozen_head)
              self.add_nodes(frozen_tail)
  
              # Create new hyperedge name to use as reference for that hyperedge
              hyperedge_id = self._assign_next_hyperedge_id()
  
              # Add hyperedge to the forward-star and to the backward-star
              # for each node in the tail and head sets, respectively
              for node in frozen_tail:
                  self._forward_star[node].add(hyperedge_id)
              for node in frozen_head:
                  self._backward_star[node].add(hyperedge_id)
  
              # Add the hyperedge as the successors and predecessors
              # of the tail set and head set, respectively
              self._successors[frozen_tail][frozen_head] = hyperedge_id
              self._predecessors[frozen_head][frozen_tail] = hyperedge_id
  
              # Assign some special attributes to this hyperedge. We assign
              # a default weight of 1 to the hyperedge. We also store the
              # original tail and head sets in order to return them exactly
              # as the user passed them into add_hyperedge.
              self._hyperedge_attributes[hyperedge_id] = \
                  {"tail": tail, "__frozen_tail": frozen_tail,
                   "head": head, "__frozen_head": frozen_head,
                   "weight": 1}
          else:
              # If its not a new hyperedge, just get its ID to update attributes
              hyperedge_id = self._successors[frozen_tail][frozen_head]
  
          # Set attributes and return hyperedge ID
          self._hyperedge_attributes[hyperedge_id].update(attr_dict)
          return hyperedge_id