docs: def routing_tree_to_tables(routes, net_keys):
      """Convert a set of
      :py:class:`~rig.place_and_route.routing_tree.RoutingTree` s into a per-chip
      set of routing tables.
  
      .. warning::
          A :py:exc:`rig.routing_table.MultisourceRouteError` will
          be raised if entries with identical keys and masks but with differing
          routes are generated. This is not a perfect test, entries which would
          otherwise collide are not spotted.
  
      .. warning::
          The routing trees provided are assumed to be correct and continuous
          (not missing any hops). If this is not the case, the output is
          undefined.
  
      .. note::
          If a routing tree has a terminating vertex whose route is set to None,
          that vertex is ignored.
  
      Parameters
      ----------
      routes : {net: :py:class:`~rig.place_and_route.routing_tree.RoutingTree`, \
                ...}
          The complete set of RoutingTrees representing all routes in the system.
          (Note: this is the same data structure produced by routers in the
          :py:mod:`~rig.place_and_route` module.)
      net_keys : {net: (key, mask), ...}
          The key and mask associated with each net.
  
      Returns
      -------
      {(x, y): [:py:class:`~rig.routing_table.RoutingTableEntry`, ...]
      """
      # Pairs of inbound and outbound routes.
      InOutPair = namedtuple("InOutPair", "ins, outs")
  
      # {(x, y): {(key, mask): _InOutPair}}
      route_sets = defaultdict(OrderedDict)
  
      for net, routing_tree in iteritems(routes):
          key, mask = net_keys[net]
  
          # The direction is the Links entry which describes the direction in
          # which we last moved to reach the node (or None for the root).
          for direction, (x, y), out_directions in routing_tree.traverse():
              # Determine the in_direction
              in_direction = direction
              if in_direction is not None:
                  in_direction = direction.opposite
  
              # Add a routing entry
              if (key, mask) in route_sets[(x, y)]:
                  # If there is an existing route set raise an error if the out
                  # directions are not equivalent.
                  if route_sets[(x, y)][(key, mask)].outs != out_directions:
                      raise MultisourceRouteError(key, mask, (x, y))
  
                  # Otherwise, add the input directions as this represents a
                  # merge of the routes.
                  route_sets[(x, y)][(key, mask)].ins.add(in_direction)
              else:
                  # Otherwise create a new route set
                  route_sets[(x, y)][(key, mask)] = \
                      InOutPair({in_direction}, set(out_directions))
  
      # Construct the routing tables from the route sets
      routing_tables = defaultdict(list)
      for (x, y), routes in iteritems(route_sets):
          for (key, mask), route in iteritems(routes):
              # Add the route
              routing_tables[(x, y)].append(
                  RoutingTableEntry(route.outs, key, mask, route.ins)
              )
  
      return routing_tables