*Inplace* Nearest-neighbour interchange (NNI) operation.

        An edge in the tree has two or more subtrees at each end (ends are
        designated 'head' and 'tail'). The NNI operation exchanges one of the
        head subtrees for one of the tail subtrees, as follows:

            A      C                        C      A    | Subtree A is exchanged
             \    /        +NNI(A,C)         \    /     | with subtree C.
              --->        ==========>         --->      |
             /    \                          /    \     |
            B      D                        B      D

def nni(
            self,
            edge,
            head_subtree,
            tail_subtree,
    ):
        """ *Inplace* Nearest-neighbour interchange (NNI) operation.

        An edge in the tree has two or more subtrees at each end (ends are
        designated 'head' and 'tail'). The NNI operation exchanges one of the
        head subtrees for one of the tail subtrees, as follows:

            A      C                        C      A    | Subtree A is exchanged
             \    /        +NNI(A,C)         \    /     | with subtree C.
              --->        ==========>         --->      |
             /    \                          /    \     |
            B      D                        B      D


        """

        # This implementation works on unrooted Trees. If the input Tree is
        # rooted, the ReversibleDeroot decorator will temporarily unroot the
        # tree while the NNI is carried out

        original_seed = self.tree._tree.seed_node
        head = edge.head_node
        tail = edge.tail_node
        self.tree._tree.reseed_at(tail)
        try:
            assert head_subtree.parent_node == head
            assert tail_subtree.parent_node == tail
        except:
            print(head, tail, head_subtree, tail_subtree)
            raise
        head.remove_child(head_subtree)
        tail.remove_child(tail_subtree)
        head.add_child(tail_subtree)
        tail.add_child(head_subtree)
        self.tree._tree.reseed_at(original_seed)
        self.tree._tree.encode_bipartitions()
        self.tree._dirty = True