docs: 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