Using notation where 0 is top level

        Intuitively speaking, when the level of a nontree edge is increased, it
        is because we have discovered that its end points are close enough in F
        to fit in a smaller tree on a higher level.

def remove_edge(self, u, v):
        """
        Using notation where 0 is top level

        Intuitively speaking, when the level of a nontree edge is increased, it
        is because we have discovered that its end points are close enough in F
        to fit in a smaller tree on a higher level.
        """
        # Remove (u, v) from represented graph
        print('Dynamically removing uv=(%r, %r)' % (u, v))
        self.graph.remove_edge(u, v)
        e = (u, v)
        # Remove edge e = (u, v) from all graphs.
        if not self.forests[0].has_edge(u, v):
            # If (u, v) is a non-tree edge, simply delete it.
            # Nothing else to do.
            return
        # If (u, v) is a tree edge we delete it and search for a replacement.
        # Delete from all higher levels
        for i in reversed(range(0, self.level[e] + 1)):
            self.forests[i].remove_edge(u, v)

        # Determine if another edge that connects u and v exists.
        # (This must be an edge r, level[r] <= level[e])
        # (Find max possible level[r] <= level[e])
        for i in reversed(range(0, self.level[e] + 1)):
            # Tu != Tw b/c (u, v) was just deleted from all forests
            Tu = self.forests[i].subtree(u)
            print('Tu = %r' % (list(Tu.nodes()),))
            Tv = self.forests[i].subtree(v)
            print('Tv = %r' % (list(Tv.nodes()),))
            # Relabel so len(Tu) <= len(Tv)
            # This ensures len(Tu) < 2 ** (floor(log(n)) - i)
            if len(Tu) > len(Tv):
                Tu, Tv = Tv, Tu
                # Note len(Tu) <= 2 * (len(Tu) + len(Tv) + 1)
            # We can afford to push all of Tu's edges to the next level and
            # still preserve invariant 1.
            seen_ = set([])
            for x in Tu.nodes():
                # Visit all edges INCIDENT (in real graph) to nodes in Tu.
                # This lets us find non-tree edges to make a tree edge
                seen_.add(x)
                for y in self.graph.neighbors(x):
                    if y in seen_:
                        continue
                    # print('Check replacement edge xy=(%r, %r)' % (x, y))
                    if y in Tv:
                        print('* Found replacement xy=(%r, %r)' % (x, y))
                        # edge (x, y) is a replacement edge.
                        # add (x, y) to prev forests F[0:i+1]
                        # This is the only place edges are added to forets of
                        # higher levels.
                        if len(self.forests) == i + 1:
                            self.forests.append(DummyEulerTourForest(self.graph.nodes()))
                        for j in range(0, i + 2):
                            print('* Add replacment to F[j=%r]' % (j,))
                            # Need euler tree augmentation for outgoing level edges
                            self.forests[j].add_edge(x, y)
                        return
                    else:
                        print('* Charging xy=(%r, %r)' % (x, y))
                        # charge --- add (x, y) to next level
                        # this pays for our search in an amortized sense
                        # (ie, the next search at this level wont consider this)
                        if len(self.forests) == i + 1:
                            self.forests.append(DummyEulerTourForest(self.graph.nodes()))
                        if self.forests[i].has_edge(x, y):
                            self.forests[i + 1].add_edge(x, y)
                        #     # assert False, 'we got it, should add it?'
                        self.level[(x, y)] = i + 1