diff --git a/VERSION b/VERSION index 550c9e96..47f5bfd9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.29 +1.2.33 diff --git a/compiler/router/bbox.py b/compiler/router/bbox.py new file mode 100644 index 00000000..56f53be0 --- /dev/null +++ b/compiler/router/bbox.py @@ -0,0 +1,63 @@ +# See LICENSE for licensing information. +# +# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz +# All rights reserved. +# +from openram.base.vector import vector +from .graph_utils import snap + + +class bbox: + """ + This class represents a bounding box object that is used in `bbox_node` + class. We are using bbox objects to group shapes in the router graphs. + """ + + def __init__(self, shape=None): + + self.shape = shape + self.rect = None + if self.shape: + self.rect = self.shape.rect + + + def area(self): + """ Return the area of this bbox. """ + + ll, ur = self.rect + width = ur.x - ll.x + height = ur.y - ll.y + return snap(width * height) + + + def merge(self, other): + """ Return the bbox created by merging two bbox objects. """ + + ll, ur = self.rect + oll, our = other.rect + min_x = min(ll.x, oll.x) + max_x = max(ur.x, our.x) + min_y = min(ll.y, oll.y) + max_y = max(ur.y, our.y) + rect = [vector(min_x, min_y), vector(max_x, max_y)] + merged = bbox() + merged.rect = rect + return merged + + + def overlap(self, other): + """ Return the bbox created by overlapping two bbox objects. """ + + ll, ur = self.rect + oll, our = other.rect + min_x = max(ll.x, oll.x) + max_x = min(ur.x, our.x) + min_y = max(ll.y, oll.y) + max_y = min(ur.y, our.y) + if max_x >= min_x and max_y >= min_y: + rect = [vector(min_x, min_y), vector(max_x, max_y)] + else: + return None + overlapped = bbox() + overlapped.rect = rect + return overlapped diff --git a/compiler/router/bbox_node.py b/compiler/router/bbox_node.py new file mode 100644 index 00000000..e5c1aadd --- /dev/null +++ b/compiler/router/bbox_node.py @@ -0,0 +1,117 @@ +# See LICENSE for licensing information. +# +# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz +# All rights reserved. +# +from .bbox import bbox + + +class bbox_node: + """ + This class represents a node in the bbox tree structure. Bbox trees are + binary trees we use to partition the shapes in the routing region so that + we can detect overlaps faster in a binary search-like manner. + """ + + def __init__(self, bbox, left=None, right=None): + + self.bbox = bbox + self.is_leaf = not left and not right + self.left = left + self.right = right + + + def iterate_point(self, point, check_done=False): + """ Iterate over shapes in the tree that overlap the given point. """ + + px, py = point.x, point.y + # Return this shape if it's a leaf + if self.is_leaf: + ll, ur = self.bbox.rect + if check_done or (ll.x <= px and px <= ur.x and ll.y <= py and py <= ur.y): + yield self.bbox.shape + else: + # Check the left child + if self.left: + ll, ur = self.left.bbox.rect + if ll.x <= px and px <= ur.x and ll.y <= py and py <= ur.y: + yield from self.left.iterate_point(point, True) + # Check the right child + if self.right: + ll, ur = self.right.bbox.rect + if ll.x <= px and px <= ur.x and ll.y <= py and py <= ur.y: + yield from self.right.iterate_point(point, True) + + + def iterate_shape(self, shape, check_done=False): + """ Iterate over shapes in the tree that overlap the given shape. """ + + sll, sur = shape.rect + # Return this shape if it's a leaf + if self.is_leaf: + ll, ur = self.bbox.rect + if check_done or (ll.x <= sur.x and sll.x <= ur.x and ll.y <= sur.y and sll.y <= ur.y): + yield self.bbox.shape + else: + # Check the left child + if self.left: + ll, ur = self.left.bbox.rect + if ll.x <= sur.x and sll.x <= ur.x and ll.y <= sur.y and sll.y <= ur.y: + yield from self.left.iterate_shape(shape, True) + # Check the right child + if self.right: + ll, ur = self.right.bbox.rect + if ll.x <= sur.x and sll.x <= ur.x and ll.y <= sur.y and sll.y <= ur.y: + yield from self.right.iterate_shape(shape, True) + + + def get_costs(self, bbox): + """ Return the costs of bbox nodes after merging the given bbox. """ + + # Find the new areas for all possible cases + self_merge = bbox.merge(self.bbox) + left_merge = bbox.merge(self.left.bbox) + right_merge = bbox.merge(self.right.bbox) + + # Add the change in areas as cost + self_cost = self_merge.area() + left_cost = self_merge.area() - self.bbox.area() + left_cost += left_merge.area() - self.left.bbox.area() + right_cost = self_merge.area() - self.bbox.area() + right_cost += right_merge.area() - self.right.bbox.area() + + # Add the overlaps in areas as cost + self_overlap = self.bbox.overlap(bbox) + left_overlap = left_merge.overlap(self.right.bbox) + right_overlap = right_merge.overlap(self.left.bbox) + if self_overlap: + self_cost += self_overlap.area() + if left_overlap: + left_cost += left_overlap.area() + if right_overlap: + right_cost += right_overlap.area() + + return self_cost, left_cost, right_cost + + + def insert(self, bbox): + """ Insert a bbox to the bbox tree. """ + + if self.is_leaf: + # Put the current bbox to the left child + self.left = bbox_node(self.bbox) + # Put the new bbox to the right child + self.right = bbox_node(bbox) + else: + # Calculate the costs of adding the new bbox + self_cost, left_cost, right_cost = self.get_costs(bbox) + if self_cost < left_cost and self_cost < right_cost: # Add here + self.left = bbox_node(self.bbox, left=self.left, right=self.right) + self.right = bbox_node(bbox) + elif left_cost < right_cost: # Add to the left + self.left.insert(bbox) + else: # Add to the right + self.right.insert(bbox) + # Update the current bbox + self.bbox = self.left.bbox.merge(self.right.bbox) + self.is_leaf = False diff --git a/compiler/router/graph.py b/compiler/router/graph.py index 132681c9..37bec676 100644 --- a/compiler/router/graph.py +++ b/compiler/router/graph.py @@ -9,6 +9,8 @@ from openram import debug from openram.base.vector import vector from openram.base.vector3d import vector3d from openram.tech import drc +from .bbox import bbox +from .bbox_node import bbox_node from .graph_node import graph_node from .graph_probe import graph_probe from .graph_utils import snap @@ -80,11 +82,8 @@ class graph: probe_shape = graph_probe(p1, p2, self.router.get_lpp(p1.z)) pll, pur = probe_shape.rect # Check if any blockage blocks this probe - for blockage in self.graph_blockages: + for blockage in self.blockage_bbox_tree.iterate_shape(probe_shape): bll, bur = blockage.rect - # Not overlapping - if bll.x > pur.x or pll.x > bur.x or bll.y > pur.y or pll.y > bur.y: - continue # Not on the same layer if not blockage.same_lpp(blockage.lpp, probe_shape.lpp): continue @@ -116,11 +115,8 @@ class graph: half_wide = self.router.half_wire spacing = snap(self.router.track_space + half_wide + drc["grid"]) blocked = False - for blockage in self.graph_blockages: + for blockage in self.blockage_bbox_tree.iterate_point(p): ll, ur = blockage.rect - # Not overlapping - if ll.x > x or x > ur.x or ll.y > y or y > ur.y: - continue # Not on the same layer if self.router.get_zindex(blockage.lpp) != z: continue @@ -168,11 +164,16 @@ class graph: for node in nodes: if self.is_node_blocked(node, pin_safe=False): return True + + # Skip if no via is present + if len(self.graph_vias) == 0: + return False + # If the nodes are blocked by a via x = node.center.x y = node.center.y z = node.center.z - for via in self.graph_vias: + for via in self.via_bbox_tree.iterate_point(node.center): ll, ur = via.rect # Not overlapping if ll.x > x or x > ur.x or ll.y > y or y > ur.y: @@ -186,7 +187,7 @@ class graph: def create_graph(self, source, target, scale=1): """ Create the graph to run routing on later. """ - debug.info(2, "Creating the graph for source '{}' and target'{}'.".format(source, target)) + debug.info(3, "Creating the graph for source '{}' and target'{}'.".format(source, target)) # Save source and target information self.source = source @@ -195,9 +196,8 @@ class graph: # Find the region to be routed and only include objects inside that region region = deepcopy(source) region.bbox([target]) - region.multiply(scale) - region = region.inflated_pin(spacing=self.router.track_space) - debug.info(3, "Routing region is {}".format(region.rect)) + region = region.inflated_pin(spacing=self.router.track_width + self.router.track_space) + debug.info(4, "Routing region is {}".format(region.rect)) # Find the blockages that are in the routing area self.graph_blockages = [] @@ -213,13 +213,15 @@ class graph: region.bbox(self.graph_blockages) # Find and include edge shapes to prevent DRC errors self.find_graph_blockages(region) + # Build the bbox tree + self.build_bbox_trees() # Generate the graph nodes from cartesian values self.generate_graph_nodes(x_values, y_values) # Save the graph nodes that lie in source and target shapes self.save_end_nodes() - debug.info(3, "Number of blockages detected in the routing region: {}".format(len(self.graph_blockages))) - debug.info(3, "Number of vias detected in the routing region: {}".format(len(self.graph_vias))) - debug.info(3, "Number of nodes in the routing graph: {}".format(len(self.nodes))) + debug.info(4, "Number of blockages detected in the routing region: {}".format(len(self.graph_blockages))) + debug.info(4, "Number of vias detected in the routing region: {}".format(len(self.graph_vias))) + debug.info(4, "Number of nodes in the routing graph: {}".format(len(self.nodes))) # Return the region to scale later if no path is found return region.rect @@ -261,6 +263,21 @@ class graph: self.graph_vias.append(via) + def build_bbox_trees(self): + """ Build bbox trees for blockages and vias in the routing region. """ + + # Bbox tree for blockages + self.blockage_bbox_tree = bbox_node(bbox(self.graph_blockages[0])) + for i in range(1, len(self.graph_blockages)): + self.blockage_bbox_tree.insert(bbox(self.graph_blockages[i])) + # Bbox tree for vias + if len(self.graph_vias) == 0: + return + self.via_bbox_tree = bbox_node(bbox(self.graph_vias[0])) + for i in range(1, len(self.graph_vias)): + self.via_bbox_tree.insert(bbox(self.graph_vias[i])) + + def generate_cartesian_values(self): """ Generate x and y values from all the corners of the shapes in the diff --git a/compiler/router/graph_node.py b/compiler/router/graph_node.py index d849673b..9ea30a80 100644 --- a/compiler/router/graph_node.py +++ b/compiler/router/graph_node.py @@ -65,10 +65,10 @@ class graph_node: layer_dist = self.center.distance(other.center) # Double the cost if the edge is in non-preferred direction if is_vertical != bool(self.center.z): - layer_dist *= 2 + layer_dist *= 4 # Add a constant wire cost to prevent dog-legs if prev_node and self.get_direction(prev_node) != self.get_direction(other): layer_dist += drc["grid"] - via_dist = abs(self.center.z - other.center.z) + via_dist = abs(self.center.z - other.center.z) * 2 return layer_dist + via_dist return float("inf") diff --git a/compiler/router/router.py b/compiler/router/router.py index fe445ae1..f1d30941 100644 --- a/compiler/router/router.py +++ b/compiler/router/router.py @@ -87,7 +87,7 @@ class router(router_tech): def find_pins(self, pin_name): """ Find the pins with the given name. """ - debug.info(2, "Finding all pins for {}".format(pin_name)) + debug.info(4, "Finding all pins for {}".format(pin_name)) shape_list = self.layout.getAllPinShapes(str(pin_name)) pin_set = set() @@ -111,7 +111,7 @@ class router(router_tech): def find_blockages(self, name="blockage"): """ Find all blockages in the routing layers. """ - debug.info(2, "Finding blockages...") + debug.info(4, "Finding blockages...") for lpp in [self.vert_lpp, self.horiz_lpp]: shapes = self.layout.getAllShapes(lpp) @@ -134,7 +134,7 @@ class router(router_tech): def find_vias(self): """ Find all vias in the routing layers. """ - debug.info(2, "Finding vias...") + debug.info(4, "Finding vias...") # Prepare lpp values here from openram.tech import layer diff --git a/compiler/router/signal_escape_router.py b/compiler/router/signal_escape_router.py index 5e4ae66e..f4d5c3b1 100644 --- a/compiler/router/signal_escape_router.py +++ b/compiler/router/signal_escape_router.py @@ -5,6 +5,7 @@ # from openram import debug from openram.base.vector import vector +from openram.base.vector3d import vector3d from openram import OPTS from .graph import graph from .graph_shape import graph_shape @@ -53,45 +54,89 @@ class signal_escape_router(router): self.blockages.append(self.inflate_shape(pin)) # Route vdd and gnd + routed_count = 0 + routed_max = len(pin_names) for source, target, _ in self.get_route_pairs(pin_names): # Change fake pin's name so the graph will treat it as routable target.name = source.name - # This is the routing region scale - scale = 1 - while True: - # Create the graph - g = graph(self) - region = g.create_graph(source, target, scale) - # Find the shortest path from source to target - path = g.find_shortest_path() - # If there is no path found, exponentially try again with a - # larger routing region - if path is None: - rll, rur = region - bll, bur = self.bbox - # Stop scaling the region and throw an error - if rll.x < bll.x and rll.y < bll.y and \ - rur.x > bur.x and rur.y > bur.y: - self.write_debug_gds(gds_name="{}error.gds".format(OPTS.openram_temp), g=g, source=source, target=target) - debug.error("Couldn't route from {} to {}.".format(source, target), -1) - # Exponentially scale the region - scale *= 2 - debug.info(0, "Retry routing in larger routing region with scale {}".format(scale)) - continue - # Create the path shapes on layout - new_shapes = self.add_path(path) - self.new_pins[source.name] = new_shapes[0] - # Find the recently added shapes - self.prepare_gds_reader() - self.find_blockages(name) - self.find_vias() - break + # Create the graph + g = graph(self) + g.create_graph(source, target) + # Find the shortest path from source to target + path = g.find_shortest_path() + # If no path is found, throw an error + if path is None: + self.write_debug_gds(gds_name="{}error.gds".format(OPTS.openram_temp), g=g, source=source, target=target) + debug.error("Couldn't route from {} to {}.".format(source, target), -1) + # Create the path shapes on layout + new_wires, new_vias = self.add_path(path) + self.new_pins[source.name] = new_wires[-1] + # Find the recently added shapes + self.find_blockages(name, new_wires) + self.find_vias(new_vias) + routed_count += 1 + debug.info(2, "Routed {} of {} signal pins".format(routed_count, routed_max)) self.replace_layout_pins() + def get_closest_edge(self, point): + """ Return a point's the closest edge and the edge's axis direction. """ + + ll, ur = self.bbox + + # Snap the pin to the perimeter and break the iteration + ll_diff_x = abs(point.x - ll.x) + ll_diff_y = abs(point.y - ll.y) + ur_diff_x = abs(point.x - ur.x) + ur_diff_y = abs(point.y - ur.y) + min_diff = min(ll_diff_x, ll_diff_y, ur_diff_x, ur_diff_y) + + if min_diff == ll_diff_x: + return "left", True + if min_diff == ll_diff_y: + return "bottom", False + if min_diff == ur_diff_x: + return "right", True + return "top", False + + + def prepare_path(self, path): + """ + Override the `prepare_path` method from the `router` class to prevent + overflows from the SRAM layout area. + """ + + ll, ur = self.bbox + nodes = super().prepare_path(path) + new_nodes = [] + for i in range(len(nodes)): + node = nodes[i] + c = node.center + # Haven't overflown yet + if ll.x < c.x and c.x < ur.x and ll.y < c.y and c.y < ur.y: + new_nodes.append(node) + continue + # Snap the pin to the perimeter and break the iteration + edge, _ = self.get_closest_edge(c) + if edge == "left": + fake_center = vector3d(ll.x + self.half_wire, c.y, c.z) + if edge == "bottom": + fake_center = vector3d(c.x, ll.y + self.half_wire, c.z) + if edge == "right": + fake_center = vector3d(ur.x - self.half_wire, c.y, c.z) + if edge == "top": + fake_center = vector3d(c.x, ur.y - self.half_wire, c.z) + node.center = fake_center + new_nodes.append(node) + break + return new_nodes + + def add_perimeter_fake_pins(self): """ Add the fake pins on the perimeter to where the signals will be routed. + These perimeter fake pins are only used to replace layout pins at the + end of routing. """ ll, ur = self.bbox @@ -132,17 +177,36 @@ class signal_escape_router(router): self.fake_pins.append(pin) - def get_closest_perimeter_fake_pin(self, pin): - """ Return the closest fake pin for the given pin. """ + def create_fake_pin(self, pin): + """ Create a fake pin on the perimeter orthogonal to the given pin. """ - min_dist = float("inf") - close_fake = None - for fake in self.fake_pins: - dist = pin.distance(fake) - if dist < min_dist: - min_dist = dist - close_fake = fake - return close_fake + ll, ur = self.bbox + c = pin.center() + + # Find the closest edge + edge, vertical = self.get_closest_edge(c) + + # Keep the fake pin out of the SRAM layout are so that they won't be + # blocked by previous signals if they're on the same orthogonal line + if edge == "left": + fake_center = vector(ll.x - self.track_wire * 2, c.y) + if edge == "bottom": + fake_center = vector(c.x, ll.y - self.track_wire * 2) + if edge == "right": + fake_center = vector(ur.x + self.track_wire * 2, c.y) + if edge == "top": + fake_center = vector(c.x, ur.y + self.track_wire * 2) + + # Create the fake pin shape + layer = self.get_layer(int(not vertical)) + half_wire_vector = vector([self.half_wire] * 2) + nll = fake_center - half_wire_vector + nur = fake_center + half_wire_vector + rect = [nll, nur] + pin = graph_shape(name="fake", + rect=rect, + layer_name_pp=layer) + return pin def get_route_pairs(self, pin_names): @@ -151,7 +215,7 @@ class signal_escape_router(router): to_route = [] for name in pin_names: pin = next(iter(self.pins[name])) - fake = self.get_closest_perimeter_fake_pin(pin) + fake = self.create_fake_pin(pin) to_route.append((pin, fake, pin.distance(fake))) return sorted(to_route, key=lambda x: x[2]) diff --git a/compiler/router/supply_router.py b/compiler/router/supply_router.py index f6acd24c..a7614b22 100644 --- a/compiler/router/supply_router.py +++ b/compiler/router/supply_router.py @@ -67,39 +67,29 @@ class supply_router(router): self.blockages.append(self.inflate_shape(pin)) # Route vdd and gnd + routed_count = 0 + routed_max = len(self.pins[vdd_name]) + len(self.pins[gnd_name]) for pin_name in [vdd_name, gnd_name]: pins = self.pins[pin_name] # Route closest pins according to the minimum spanning tree for source, target in self.get_mst_pairs(list(pins)): - # This is the routing region scale - scale = 1 - while True: - # Create the graph - g = graph(self) - region = g.create_graph(source, target, scale) - # Find the shortest path from source to target - path = g.find_shortest_path() - # If there is no path found, exponentially try again with a - # larger routing region - if path is None: - rll, rur = region - bll, bur = self.bbox - # Stop scaling the region and throw an error - if rll.x < bll.x and rll.y < bll.y and \ - rur.x > bur.x and rur.y > bur.y: - self.write_debug_gds(gds_name="{}error.gds".format(OPTS.openram_temp), g=g, source=source, target=target) - debug.error("Couldn't route from {} to {}.".format(source, target), -1) - # Exponentially scale the region - scale *= 2 - debug.info(0, "Retry routing in larger routing region with scale {}".format(scale)) - continue - # Create the path shapes on layout - self.add_path(path) - # Find the recently added shapes - self.prepare_gds_reader() - self.find_blockages(pin_name) - self.find_vias() - break + # Create the graph + g = graph(self) + g.create_graph(source, target) + # Find the shortest path from source to target + path = g.find_shortest_path() + # If no path is found, throw an error + if path is None: + self.write_debug_gds(gds_name="{}error.gds".format(OPTS.openram_temp), g=g, source=source, target=target) + debug.error("Couldn't route from {} to {}.".format(source, target), -1) + # Create the path shapes on layout + new_wires, new_vias = self.add_path(path) + # Find the recently added shapes + self.find_blockages(pin_name, new_wires) + self.find_vias(new_vias) + # Report routed count + routed_count += 1 + debug.info(2, "Routed {} of {} supply pins".format(routed_count, routed_max)) def add_side_pin(self, pin_name, side, num_vias=3, num_fake_pins=4):