Merge changes from subprocess_fix

This commit is contained in:
Bugra Onal 2023-08-10 16:19:28 -07:00
commit 230454d567
33 changed files with 1392 additions and 3954 deletions

View File

@ -1 +1 @@
1.2.25
1.2.28

View File

@ -14,7 +14,6 @@ from openram.base import rom_verilog
from openram import OPTS, print_time
from openram.sram_factory import factory
from openram.tech import drc, layer, parameter
from openram.router import router_tech
class rom_bank(design,rom_verilog):
@ -111,21 +110,16 @@ class rom_bank(design,rom_verilog):
self.place_top_level_pins()
self.route_output_buffers()
rt = router_tech(self.supply_stack, 1)
init_bbox = self.get_bbox(side="ring",
margin=rt.track_width)
self.route_supplies(init_bbox)
# We need the initial bbox for the supply rings later
# because the perimeter pins will change the bbox
# FIXME: Somehow ROM layout behaves weird and doesn't add all the pin
# shapes before routing supplies
init_bbox = self.get_bbox()
if OPTS.route_supplies:
self.route_supplies(init_bbox)
# Route the pins to the perimeter
if OPTS.perimeter_pins:
# We now route the escape routes far enough out so that they will
# reach past the power ring or stripes on the sides
bbox = self.get_bbox(side="ring",
margin=11*rt.track_width)
self.route_escape_pins(bbox)
self.route_escape_pins(init_bbox)
def setup_layout_constants(self):
@ -450,24 +444,17 @@ class rom_bank(design,rom_verilog):
pin_num = msb - self.col_bits
self.add_io_pin(self.decode_inst, "A{}".format(pin_num), name)
def route_supplies(self, bbox=None):
def route_supplies(self, bbox):
for pin_name in ["vdd", "gnd"]:
for inst in self.insts:
self.copy_power_pins(inst, pin_name)
if not OPTS.route_supplies:
# Do not route the power supply (leave as must-connect pins)
return
elif OPTS.route_supplies == "grid":
from openram.router import supply_grid_router as router
else:
from openram.router import supply_tree_router as router
rtr=router(layers=self.supply_stack,
design=self,
bbox=bbox,
pin_type=OPTS.supply_pin_type)
from openram.router import supply_router as router
rtr = router(layers=self.supply_stack,
design=self,
bbox=bbox,
pin_type=OPTS.supply_pin_type)
rtr.route()
if OPTS.supply_pin_type in ["left", "right", "top", "bottom", "ring"]:
@ -507,7 +494,7 @@ class rom_bank(design,rom_verilog):
pins_to_route.append("clk")
pins_to_route.append("cs")
from openram.router import signal_escape_router as router
rtr=router(layers=self.m3_stack,
design=self,
bbox=bbox)
rtr.escape_route(pins_to_route)
rtr = router(layers=self.m3_stack,
bbox=bbox,
design=self)
rtr.route(pins_to_route)

View File

@ -14,7 +14,6 @@ from openram.base import channel_route
from openram.base import design
from openram.base import verilog
from openram.base import lef
from openram.router import router_tech
from openram.sram_factory import factory
from openram.tech import spice
from openram import OPTS, print_time
@ -252,18 +251,11 @@ class sram_1bank(design, verilog, lef):
for inst in self.insts:
self.copy_power_pins(inst, pin_name, self.ext_supply[pin_name])
if not OPTS.route_supplies:
# Do not route the power supply (leave as must-connect pins)
return
elif OPTS.route_supplies == "grid":
from openram.router import supply_grid_router as router
else:
from openram.router import supply_tree_router as router
rtr=router(layers=self.supply_stack,
design=self,
bbox=bbox,
pin_type=OPTS.supply_pin_type)
from openram.router import supply_router as router
rtr = router(layers=self.supply_stack,
design=self,
bbox=bbox,
pin_type=OPTS.supply_pin_type)
rtr.route()
if OPTS.supply_pin_type in ["left", "right", "top", "bottom", "ring"]:
@ -288,7 +280,7 @@ class sram_1bank(design, verilog, lef):
pin.width(),
pin.height())
elif OPTS.route_supplies and OPTS.supply_pin_type == "single":
elif OPTS.supply_pin_type == "single":
# Update these as we may have routed outside the region (perimeter pins)
lowest_coord = self.find_lowest_coords()
@ -326,7 +318,7 @@ class sram_1bank(design, verilog, lef):
# Grid is left with many top level pins
pass
def route_escape_pins(self, bbox):
def route_escape_pins(self, bbox=None):
"""
Add the top-level pins for a single bank SRAM with control.
"""
@ -370,10 +362,10 @@ class sram_1bank(design, verilog, lef):
pins_to_route.append("spare_wen{0}[{1}]".format(port, bit))
from openram.router import signal_escape_router as router
rtr=router(layers=self.m3_stack,
design=self,
bbox=bbox)
rtr.escape_route(pins_to_route)
rtr = router(layers=self.m3_stack,
bbox=bbox,
design=self)
rtr.route(pins_to_route)
def compute_bus_sizes(self):
""" Compute the independent bus widths shared between two and four bank SRAMs """
@ -1077,24 +1069,15 @@ class sram_1bank(design, verilog, lef):
# Some technologies have an isolation
self.add_dnwell(inflate=2.5)
init_bbox = self.get_bbox()
# Route the supplies together and/or to the ring/stripes.
# This is done with the original bbox since the escape routes need to
# be outside of the ring for OpenLane
rt = router_tech(self.supply_stack, 1)
init_bbox = self.get_bbox(side="ring",
margin=rt.track_width)
# We need the initial bbox for the supply rings later
# because the perimeter pins will change the bbox
# Route the pins to the perimeter
if OPTS.perimeter_pins:
# We now route the escape routes far enough out so that they will
# reach past the power ring or stripes on the sides
bbox = self.get_bbox(side="ring",
margin=11*rt.track_width)
self.route_escape_pins(bbox)
self.route_supplies(init_bbox)
self.route_escape_pins(init_bbox)
if OPTS.route_supplies:
self.route_supplies(init_bbox)
def route_dffs(self, add_routes=True):

View File

@ -115,7 +115,7 @@ class options(optparse.Values):
# When enabled, layout is not generated (and no DRC or LVS are performed)
netlist_only = False
# Whether we should do the final power routing
route_supplies = "tree"
route_supplies = True
supply_pin_type = "ring"
# This determines whether LVS and DRC is checked at all.
check_lvsdrc = False

View File

@ -3,8 +3,5 @@
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
from .router import *
from .signal_escape_router import *
from .signal_router import *
from .supply_grid_router import *
from .supply_tree_router import *
from .supply_router import *

View File

@ -1,75 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from enum import Enum
from openram import debug
from openram.base.vector3d import vector3d
class direction(Enum):
NORTH = 1
SOUTH = 2
EAST = 3
WEST = 4
UP = 5
DOWN = 6
NORTHEAST = 7
NORTHWEST = 8
SOUTHEAST = 9
SOUTHWEST = 10
def get_offset(direct):
"""
Returns the vector offset for a given direction.
"""
if direct==direction.NORTH:
offset = vector3d(0, 1, 0)
elif direct==direction.SOUTH:
offset = vector3d(0, -1 ,0)
elif direct==direction.EAST:
offset = vector3d(1, 0, 0)
elif direct==direction.WEST:
offset = vector3d(-1, 0, 0)
elif direct==direction.UP:
offset = vector3d(0, 0, 1)
elif direct==direction.DOWN:
offset = vector3d(0, 0, -1)
elif direct==direction.NORTHEAST:
offset = vector3d(1, 1, 0)
elif direct==direction.NORTHWEST:
offset = vector3d(-1, 1, 0)
elif direct==direction.SOUTHEAST:
offset = vector3d(1, -1, 0)
elif direct==direction.SOUTHWEST:
offset = vector3d(-1, -1, 0)
else:
debug.error("Invalid direction {}".format(direct))
return offset
def cardinal_directions(up_down_too=False):
temp_dirs = [direction.NORTH, direction.EAST, direction.SOUTH, direction.WEST]
if up_down_too:
temp_dirs.extend([direction.UP, direction.DOWN])
return temp_dirs
def cardinal_offsets(up_down_too=False):
return [direction.get_offset(d) for d in direction.cardinal_directions(up_down_too)]
def all_directions():
return [direction.NORTH, direction.EAST, direction.SOUTH, direction.WEST,
direction.NORTHEAST, direction.NORTHWEST, direction.SOUTHEAST, direction.SOUTHWEST]
def all_offsets():
return [direction.get_offset(d) for d in direction.all_directions()]
def all_neighbors(cell):
return [cell + x for x in direction.all_offsets()]
def cardinal_neighbors(cell):
return [cell + x for x in direction.cardinal_offsets()]

442
compiler/router/graph.py Normal file
View File

@ -0,0 +1,442 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
import heapq
from copy import deepcopy
from openram import debug
from openram.base.vector import vector
from openram.base.vector3d import vector3d
from openram.tech import drc
from .graph_node import graph_node
from .graph_probe import graph_probe
from .graph_utils import snap
class graph:
""" This is the graph created from the blockages. """
def __init__(self, router):
# This is the graph router that uses this graph
self.router = router
self.source_nodes = []
self.target_nodes = []
def is_routable(self, shape):
""" Return if a shape is routable in this graph. """
return shape.name == self.source.name
def inside_shape(self, point, shape):
""" Return if the point is inside the shape. """
# Check if they're on the same layer
if point.z != self.router.get_zindex(shape.lpp):
return False
# Check if the point is inside the shape
ll, ur = shape.rect
return shape.on_segment(ll, point, ur)
def get_safe_pin_values(self, pin):
""" Get the safe x and y values of the given pin. """
# Constant values
pin = pin.get_core()
offset = self.router.half_wire
spacing = self.router.track_space
size_limit = snap(offset * 4 + spacing)
x_values = []
y_values = []
# If one axis size of the pin is greater than the limit, we will take
# two points at both ends. Otherwise, we will only take the center of
# this pin.
if pin.width() > size_limit:
x_values.append(snap(pin.lx() + offset))
x_values.append(snap(pin.rx() - offset))
else:
x_values.append(snap(pin.cx()))
if pin.height() > size_limit:
y_values.append(snap(pin.by() + offset))
y_values.append(snap(pin.uy() - offset))
else:
y_values.append(snap(pin.cy()))
return x_values, y_values
def is_probe_blocked(self, p1, p2):
"""
Return if a probe sent from p1 to p2 encounters a blockage.
The probe must be sent vertically or horizontally.
This function assumes that p1 and p2 are on the same layer.
"""
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:
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
# Probe is blocked if the shape isn't routable
if not self.is_routable(blockage):
return True
blockage = blockage.get_core()
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:
return True
return False
def is_node_blocked(self, node, pin_safe=True):
""" Return if a node is blocked by a blockage. """
p = node.center
x = p.x
y = p.y
z = p.z
def closest(value, checklist):
""" Return the distance of the closest value in the checklist. """
diffs = [abs(value - other) for other in checklist]
return snap(min(diffs))
wide = self.router.track_wire
half_wide = self.router.half_wire
spacing = snap(self.router.track_space + half_wide + drc["grid"])
blocked = False
for blockage in self.graph_blockages:
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
# Blocked if not routable
if not self.is_routable(blockage):
blocked = True
continue
blockage = blockage.get_core()
ll, ur = blockage.rect
# Not overlapping
if ll.x > x or x > ur.x or ll.y > y or y > ur.y:
blocked = True
continue
# Check if the node is too close to one edge of the shape
lengths = [blockage.width(), blockage.height()]
centers = blockage.center()
ll, ur = blockage.rect
safe = [True, True]
for i in range(2):
if lengths[i] >= wide:
min_diff = closest(p[i], [ll[i], ur[i]])
if min_diff < half_wide:
safe[i] = False
elif centers[i] != p[i]:
safe[i] = False
if not all(safe):
blocked = True
continue
# Check if the node is in a safe region of the shape
xs, ys = self.get_safe_pin_values(blockage)
xdiff = closest(p.x, xs)
ydiff = closest(p.y, ys)
if xdiff == 0 and ydiff == 0:
if pin_safe and blockage in [self.source, self.target]:
return False
elif xdiff < spacing and ydiff < spacing:
blocked = True
return blocked
def is_via_blocked(self, nodes):
""" Return if a via on the given point is blocked by another via. """
# If the nodes are blocked by a blockage other than a via
for node in nodes:
if self.is_node_blocked(node, pin_safe=False):
return True
# 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:
ll, ur = via.rect
# Not overlapping
if ll.x > x or x > ur.x or ll.y > y or y > ur.y:
continue
center = via.center()
# If not in the center
if center.x != x or center.y != y:
return True
return False
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))
# Save source and target information
self.source = source
self.target = target
# 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))
# Find the blockages that are in the routing area
self.graph_blockages = []
self.find_graph_blockages(region)
# Find the vias that are in the routing area
self.graph_vias = []
self.find_graph_vias(region)
# Generate the cartesian values from shapes in the area
x_values, y_values = self.generate_cartesian_values()
# Adjust the routing region to include "edge" shapes
region.bbox(self.graph_blockages)
# Find and include edge shapes to prevent DRC errors
self.find_graph_blockages(region)
# 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)))
# Return the region to scale later if no path is found
return region.rect
def find_graph_blockages(self, region):
""" Find blockages that overlap the routing region. """
for blockage in self.router.blockages:
# Skip if already included
if blockage in self.graph_blockages:
continue
# Set the region's lpp to current blockage's lpp so that the
# overlaps method works
region.lpp = blockage.lpp
if region.overlaps(blockage):
self.graph_blockages.append(blockage)
# Make sure that the source or target fake pins are included as blockage
for shape in [self.source, self.target]:
for blockage in self.graph_blockages:
blockage = blockage.get_core()
if shape == blockage:
break
else:
self.graph_blockages.append(shape)
def find_graph_vias(self, region):
""" Find vias that overlap the routing region. """
for via in self.router.vias:
# Skip if already included
if via in self.graph_vias:
continue
# Set the regions's lpp to current via's lpp so that the
# overlaps method works
region.lpp = via.lpp
if region.overlaps(via):
self.graph_vias.append(via)
def generate_cartesian_values(self):
"""
Generate x and y values from all the corners of the shapes in the
routing region.
"""
x_values = set()
y_values = set()
# Add inner values for blockages of the routed type
for shape in self.graph_blockages:
if not self.is_routable(shape):
continue
# Get the safe pin values
xs, ys = self.get_safe_pin_values(shape)
x_values.update(xs)
y_values.update(ys)
# Add corners for blockages
offset = vector([drc["grid"]] * 2)
for blockage in self.graph_blockages:
ll, ur = blockage.rect
# Add minimum offset to the blockage corner nodes to prevent overlap
nll = snap(ll - offset)
nur = snap(ur + offset)
x_values.update([nll.x, nur.x])
y_values.update([nll.y, nur.y])
# Add center values for existing vias
for via in self.graph_vias:
p = via.center()
x_values.add(p.x)
y_values.add(p.y)
# Sort x and y values
x_values = list(x_values)
y_values = list(y_values)
x_values.sort()
y_values.sort()
return x_values, y_values
def generate_graph_nodes(self, x_values, y_values):
"""
Generate all graph nodes using the cartesian values and connect the
orthogonal neighbors.
"""
# Generate all nodes
self.nodes = []
for x in x_values:
for y in y_values:
for z in [0, 1]:
self.nodes.append(graph_node([x, y, z]))
# Mark nodes that will be removed
self.mark_blocked_nodes()
# Connect closest nodes that won't be removed
def search(index, condition, shift):
""" Search and connect neighbor nodes. """
base_nodes = self.nodes[index:index+2]
found = [base_nodes[0].remove,
base_nodes[1].remove]
while condition(index) and not all(found):
nodes = self.nodes[index - shift:index - shift + 2]
for k in range(2):
if not found[k] and not nodes[k].remove:
found[k] = True
if not self.is_probe_blocked(base_nodes[k].center, nodes[k].center):
base_nodes[k].add_neighbor(nodes[k])
index -= shift
y_len = len(y_values)
for i in range(0, len(self.nodes), 2):
search(i, lambda count: (count / 2) % y_len, 2) # Down
search(i, lambda count: (count / 2) >= y_len, y_len * 2) # Left
if not self.nodes[i].remove and \
not self.nodes[i + 1].remove and \
not self.is_via_blocked(self.nodes[i:i+2]):
self.nodes[i].add_neighbor(self.nodes[i + 1])
# Remove marked nodes
self.remove_blocked_nodes()
def mark_blocked_nodes(self):
""" Mark graph nodes to be removed that are blocked by a blockage. """
for i in range(len(self.nodes) - 1, -1, -1):
node = self.nodes[i]
if self.is_node_blocked(node):
node.remove = True
def remove_blocked_nodes(self):
""" Remove graph nodes that are marked to be removed. """
for i in range(len(self.nodes) - 1, -1, -1):
node = self.nodes[i]
if node.remove:
node.remove_all_neighbors()
self.nodes.remove(node)
def save_end_nodes(self):
""" Save graph nodes that are inside source and target pins. """
for node in self.nodes:
if self.inside_shape(node.center, self.source):
self.source_nodes.append(node)
elif self.inside_shape(node.center, self.target):
self.target_nodes.append(node)
def find_shortest_path(self):
"""
Find the shortest path from the source node to target node using the
A* algorithm.
"""
# Heuristic function to calculate the scores
def h(node):
""" Return the estimated distance to the closest target. """
min_dist = float("inf")
for t in self.target_nodes:
dist = t.center.distance(node.center) + abs(t.center.z - node.center.z)
if dist < min_dist:
min_dist = dist
return min_dist
# Initialize data structures to be used for A* search
queue = []
close_set = set()
came_from = {}
g_scores = {}
f_scores = {}
# Initialize score values for the source nodes
for node in self.source_nodes:
g_scores[node.id] = 0
f_scores[node.id] = h(node)
heapq.heappush(queue, (f_scores[node.id], node.id, node))
# Run the A* algorithm
while len(queue) > 0:
# Get the closest node from the queue
current = heapq.heappop(queue)[2]
# Skip this node if already discovered
if current in close_set:
continue
close_set.add(current)
# Check if we've reached the target
if current in self.target_nodes:
path = []
while current.id in came_from:
path.append(current)
current = came_from[current.id]
path.append(current)
return path
# Get the previous node to better calculate the next costs
prev_node = None
if current.id in came_from:
prev_node = came_from[current.id]
# Update neighbor scores
for node in current.neighbors:
tentative_score = current.get_edge_cost(node, prev_node) + g_scores[current.id]
if node.id not in g_scores or tentative_score < g_scores[node.id]:
came_from[node.id] = current
g_scores[node.id] = tentative_score
f_scores[node.id] = tentative_score + h(node)
heapq.heappush(queue, (f_scores[node.id], node.id, node))
# Return None if not connected
return None

View File

@ -0,0 +1,74 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
from openram.base.vector3d import vector3d
from openram.tech import drc
class graph_node:
""" This class represents a node on the graph. """
# This is used to assign unique ids to nodes
next_id = 0
def __init__(self, center):
self.id = graph_node.next_id
graph_node.next_id += 1
if isinstance(center, vector3d):
self.center = center
else:
self.center = vector3d(center)
self.neighbors = []
self.remove = False
def add_neighbor(self, other):
""" Connect two nodes. """
if other not in self.neighbors:
self.neighbors.append(other)
other.neighbors.append(self)
def remove_neighbor(self, other):
""" Disconnect two nodes. """
if other in self.neighbors:
self.neighbors.remove(other)
other.neighbors.remove(self)
def remove_all_neighbors(self):
""" Disconnect all current neighbors. """
for neighbor in self.neighbors:
self.neighbors.remove(neighbor)
neighbor.neighbors.remove(self)
def get_direction(self, b):
""" Return the direction of path from a to b. """
horiz = self.center.x == b.center.x
vert = self.center.y == b.center.y
return (horiz, vert)
def get_edge_cost(self, other, prev_node=None):
""" Get the cost of going from this node to the other node. """
if other in self.neighbors:
is_vertical = self.center.x == other.center.x
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
# 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)
return layer_dist + via_dist
return float("inf")

View File

@ -0,0 +1,16 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
class graph_probe:
"""
This class represents a probe sent from one point to another on Hanan graph.
This is used to mimic the pin_layout class to utilize its methods.
"""
def __init__(self, p1, p2, lpp):
self.rect = (p1.min(p2), p1.max(p2))
self.lpp = lpp

View File

@ -0,0 +1,112 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
from openram.base.pin_layout import pin_layout
from openram.base.vector import vector
from openram.tech import drc
from .graph_utils import snap
class graph_shape(pin_layout):
"""
This class inherits the pin_layout class to change some of its behavior for
the graph router.
"""
def __init__(self, name, rect, layer_name_pp, core=None):
pin_layout.__init__(self, name, rect, layer_name_pp)
# Snap the shape to the grid here
ll, ur = self.rect
self.rect = [snap(ll), snap(ur)]
# Core is the original shape from which this shape is inflated
self.core = core
def center(self):
""" Override the default `center` behavior. """
return snap(super().center())
def height(self):
""" Override the default `height` behavior. """
return snap(super().height())
def width(self):
""" Override the default `width` behavior. """
return snap(super().width())
def rename(self, new_name):
""" Change the name of `self` and `self.core`. """
self.name = new_name
self.get_core().name = new_name
def get_core(self):
"""
Return `self` if `self.core` is None. Otherwise, return `self.core`.
"""
if self.core is None:
return self
return self.core
def inflated_pin(self, spacing=None, multiple=0.5, extra_spacing=0):
""" Override the default inflated_pin behavior. """
ll, ur = self.inflate(spacing, multiple)
extra = vector([extra_spacing] * 2)
newll = ll - extra
newur = ur + extra
inflated_area = (newll, newur)
return graph_shape(self.name, inflated_area, self.layer, self)
def multiply(self, scale):
""" Multiply the width and height with the scale value. """
width = (self.width() * (scale - 1)) / 2
height = (self.height() * (scale - 1)) / 2
ll, ur = self.rect
newll = vector(ll.x - width, ll.y - height)
newur = vector(ur.x + width, ur.y + height)
self.rect = [snap(newll), snap(newur)]
def core_contained_by_any(self, shape_list):
"""
Return if the core of this shape is contained by any shape's core in the
given list.
"""
self_core = self.get_core()
for shape in shape_list:
shape_core = shape.get_core()
if shape_core.contains(self_core):
return True
return False
def aligns(self, other):
""" Return if the other shape aligns with this shape. """
# Shapes must overlap to be able to align
if not self.overlaps(other):
return False
ll, ur = self.rect
oll, our = other.rect
if ll.x == oll.x and ur.x == our.x:
return True
if ll.y == oll.y and ur.y == our.y:
return True
return False

View File

@ -0,0 +1,18 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
"""
Utility functions for graph router.
"""
from openram.base import vector
from openram.tech import drc
def snap(a):
""" Use custom `snap` since `vector.snap_to_grid` isn't working. """
if isinstance(a, vector):
return vector(snap(a.x), snap(a.y))
return round(a, len(str(drc["grid"]).split('.')[1]))

View File

@ -1,215 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from openram import debug
from openram.base.vector3d import vector3d
from .grid_cell import grid_cell
class grid:
"""
A two layer routing map. Each cell can be blocked in the vertical
or horizontal layer.
"""
# costs are relative to a unit grid
# non-preferred cost allows an off-direction jog of 1 grid
# rather than 2 vias + preferred direction (cost 5)
VIA_COST = 2
NONPREFERRED_COST = 4
PREFERRED_COST = 1
def __init__(self, ll, ur, track_width):
""" Initialize the map and define the costs. """
# list of the source/target grid coordinates
self.source = set()
self.target = set()
self.track_width = track_width
self.track_widths = [self.track_width, self.track_width, 1.0]
self.track_factor = [1 / self.track_width, 1 / self.track_width, 1.0]
# The bounds are in grids for this
# This is really lower left bottom layer and upper right top layer in 3D.
self.ll = vector3d(ll.x, ll.y, 0).scale(self.track_factor).round()
self.ur = vector3d(ur.x, ur.y, 0).scale(self.track_factor).round()
debug.info(1, "BBOX coords: ll=" + str(ll) + " ur=" + str(ur))
debug.info(1, "BBOX grids: ll=" + str(self.ll) + " ur=" + str(self.ur))
# let's leave the map sparse, cells are created on demand to reduce memory
self.map={}
def add_all_grids(self):
for x in range(self.ll.x, self.ur.x, 1):
for y in range(self.ll.y, self.ur.y, 1):
self.add_map(vector3d(x, y, 0))
self.add_map(vector3d(x, y, 1))
def set_blocked(self, n, value=True):
if not isinstance(n, vector3d):
for item in n:
self.set_blocked(item, value)
else:
self.add_map(n)
self.map[n].blocked=value
def is_blocked(self, n):
if not isinstance(n, vector3d):
for item in n:
if self.is_blocked(item):
return True
else:
return False
else:
self.add_map(n)
return self.map[n].blocked
def is_inside(self, n):
if not isinstance(n, vector3d):
for item in n:
if self.is_inside(item):
return True
else:
return False
else:
return n.x >= self.ll.x and n.x <= self.ur.x and n.y >= self.ll.y and n.y <= self.ur.y
def set_path(self, n, value=True):
if isinstance(n, (list, tuple, set, frozenset)):
for item in n:
self.set_path(item, value)
else:
self.add_map(n)
self.map[n].path=value
def clear_blockages(self):
for k in self.map:
self.map[k].blocked=False
def clear_source(self):
for k in self.map:
self.map[k].source=False
self.source = set()
def set_source(self, n):
if not isinstance(n, vector3d):
for item in n:
self.set_source(item)
else:
self.add_map(n)
self.map[n].source=True
self.map[n].blocked=False
self.source.add(n)
def clear_target(self):
for k in self.map:
self.map[k].target=False
self.target = set()
def set_target(self, n):
if not isinstance(n, vector3d):
for item in n:
self.set_target(item)
else:
self.add_map(n)
self.map[n].target=True
self.map[n].blocked=False
self.target.add(n)
def add_source(self, track_list):
debug.info(3, "Adding source list={0}".format(str(track_list)))
for n in track_list:
debug.info(4, "Adding source ={0}".format(str(n)))
self.set_source(n)
# self.set_blocked(n, False)
def add_target(self, track_list):
debug.info(3, "Adding target list={0}".format(str(track_list)))
for n in track_list:
debug.info(4, "Adding target ={0}".format(str(n)))
self.set_target(n)
# self.set_blocked(n, False)
def get_perimeter_list(self, side="left", layers=[0, 1], width=1, margin=0, offset=0):
"""
Side specifies which side.
Layer specifies horizontal (0) or vertical (1)
Width specifies how wide the perimeter "stripe" should be.
Works from the inside out from the bbox (ll, ur)
"""
if "ring" in side:
ring_width = width
else:
ring_width = 0
if "ring" in side:
ring_offset = offset
else:
ring_offset = 0
perimeter_list = []
# Add the left/right columns
if side=="all" or "left" in side:
for x in range(self.ll.x - offset, self.ll.x - width - offset, -1):
for y in range(self.ll.y - ring_offset - margin - ring_width + 1, self.ur.y + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
if side=="all" or "right" in side:
for x in range(self.ur.x + offset, self.ur.x + width + offset, 1):
for y in range(self.ll.y - ring_offset - margin - ring_width + 1, self.ur.y + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
if side=="all" or "bottom" in side:
for y in range(self.ll.y - offset, self.ll.y - width - offset, -1):
for x in range(self.ll.x - ring_offset - margin - ring_width + 1, self.ur.x + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
if side=="all" or "top" in side:
for y in range(self.ur.y + offset, self.ur.y + width + offset, 1):
for x in range(self.ll.x - ring_offset - margin - ring_width + 1, self.ur.x + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
# Add them all to the map
self.add_map(perimeter_list)
return perimeter_list
def add_perimeter_target(self, side="all", layers=[0, 1]):
debug.info(3, "Adding perimeter target")
perimeter_list = self.get_perimeter_list(side, layers)
self.set_target(perimeter_list)
def is_target(self, point):
"""
Point is in the target set, so we are done.
"""
return point in self.target
def add_map(self, n):
"""
Add a point to the map if it doesn't exist.
"""
if not isinstance(n, vector3d):
for item in n:
self.add_map(item)
else:
if n not in self.map:
self.map[n]=grid_cell()
def block_path(self, path):
"""
Mark the path in the routing grid as blocked.
Also unsets the path flag.
"""
path.set_path(False)
path.set_blocked(True)

View File

@ -1,52 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
class grid_cell:
"""
A single cell that can be occupied in a given layer, blocked,
visited, etc.
"""
def __init__(self):
self.path = False
self.blocked = False
self.source = False
self.target = False
# -1 means it isn't visited yet
self.min_cost = -1
def reset(self):
"""
Reset the dynamic info about routing.
"""
self.min_cost=-1
self.min_path=None
self.blocked=False
self.source=False
self.target=False
def get_cost(self):
# We can display the cost of the frontier
if self.min_cost > 0:
return self.min_cost
def get_type(self):
type_string = ""
if self.blocked:
type_string += "X"
if self.source:
type_string += "S"
if self.target:
type_string += "T"
if self.path:
type_string += "P"
return type_string

View File

@ -1,215 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from itertools import tee
from openram.base.vector3d import vector3d
from .grid import grid
from .direction import direction
class grid_path:
"""
A grid path is a list of lists of grid cells.
It can have a width that is more than one cell.
All of the sublists will be the same dimension.
Cells should be continguous.
It can have a name to define pin shapes as well.
"""
def __init__(self, items=[], name=""):
self.name = name
if items:
self.pathlist = [items]
else:
self.pathlist = []
def __str__(self):
p = str(self.pathlist)
if self.name != "":
return (str(self.name) + " : " + p)
return p
def __setitem__(self, index, value):
"""
override setitem function
can set value by pathinstance[index]=value
"""
self.pathlist[index]=value
def __getitem__(self, index):
"""
override getitem function
can get value by value=pathinstance[index]
"""
return self.pathlist[index]
def __contains__(self, key):
"""
Determine if cell exists in this path
"""
# FIXME: Could maintain a hash to make in O(1)
for sublist in self.pathlist:
for item in sublist:
if item == key:
return True
else:
return False
def __add__(self, items):
"""
Override add to do append
"""
return self.pathlist.extend(items)
def __len__(self):
return len(self.pathlist)
def trim_last(self):
"""
Drop the last item
"""
if len(self.pathlist)>0:
self.pathlist.pop()
def trim_first(self):
"""
Drop the first item
"""
if len(self.pathlist)>0:
self.pathlist.pop(0)
def append(self,item):
"""
Append the list of items to the cells
"""
self.pathlist.append(item)
def extend(self,item):
"""
Extend the list of items to the cells
"""
self.pathlist.extend(item)
def set_path(self,value=True):
for sublist in self.pathlist:
for p in sublist:
p.path=value
def set_blocked(self,value=True):
for sublist in self.pathlist:
for p in sublist:
p.blocked=value
def get_grids(self):
"""
Return a set of all the grids in this path.
"""
newset = set()
for sublist in self.pathlist:
newset.update(sublist)
return newset
def get_wire_grids(self, start_index, end_index):
"""
Return a set of all the wire grids in this path.
These are the indices in the wave path in a certain range.
"""
newset = set()
for sublist in self.pathlist:
newset.update(sublist[start_index:end_index])
return newset
def cost(self):
"""
The cost of the path is the length plus a penalty for the number
of vias. We assume that non-preferred direction is penalized.
This cost only works with 1 wide tracks.
"""
# Ignore the source pin layer change, FIXME?
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = tee(iterable)
next(b, None)
return zip(a, b)
plist = list(pairwise(self.pathlist))
cost = 0
for p0list,p1list in plist:
# This is because they are "waves" so pick the first item
p0=p0list[0]
p1=p1list[0]
if p0.z != p1.z: # via
cost += grid.VIA_COST
elif p0.x != p1.x and p0.z==1: # horizontal on vertical layer
cost += grid.NONPREFERRED_COST
elif p0.y != p1.y and p0.z==0: # vertical on horizontal layer
cost += grid.NONPREFERRED_COST
else:
cost += grid.PREFERRED_COST
return cost
def expand_dirs(self):
"""
Expand from the end in each of the four cardinal directions plus up
or down but not expanding to blocked cells. Expands in all
directions regardless of preferred directions.
If the width is more than one, it can only expand in one direction
(for now). This is assumed for the supply router for now.
"""
neighbors = []
for d in direction.cardinal_directions(True):
n = self.neighbor(d)
if n:
neighbors.append(n)
return neighbors
def neighbor(self, d):
offset = direction.get_offset(d)
newwave = [point + offset for point in self.pathlist[-1]]
if newwave in self.pathlist:
return None
elif newwave[0].z>1 or newwave[0].z<0:
return None
return newwave
def set_layer(self, zindex):
new_pathlist = [vector3d(item.x, item.y, zindex) for wave in self.pathlist for item in wave]
self.pathlist = new_pathlist
def overlap(self, other):
"""
Return the overlap waves ignoring different layers
"""
my_zindex = self.pathlist[0][0].z
other_flat_cells = [vector3d(item.x,item.y,my_zindex) for wave in other.pathlist for item in wave]
# This keeps the wave structure of the self layer
shared_waves = []
for wave in self.pathlist:
for item in wave:
# If any item in the wave is not contained, skip it
if not item in other_flat_cells:
break
else:
shared_waves.append(wave)
if len(shared_waves)>0:
ll = shared_waves[0][0]
ur = shared_waves[-1][-1]
return [ll,ur]
return None

View File

@ -1,167 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
"""
Some utility functions for sets of grid cells.
"""
import math
from openram.base.vector3d import vector3d
from .direction import direction
def increment_set(curset, direct):
"""
Return the cells incremented in given direction
"""
offset = direction.get_offset(direct)
newset = set()
for c in curset:
newc = c+offset
newset.add(newc)
return newset
def remove_border(curset, direct):
"""
Remove the cells on a given border.
"""
border = get_border(curset, direct)
curset.difference_update(border)
def get_upper_right(curset):
ur = None
for p in curset:
if ur == None or (p.x>=ur.x and p.y>=ur.y):
ur = p
return ur
def get_lower_left(curset):
ll = None
for p in curset:
if ll == None or (p.x<=ll.x and p.y<=ll.y):
ll = p
return ll
def get_border(curset, direct):
"""
Return the furthest cell(s) in a given direction.
"""
# find direction-most cell(s)
maxc = []
if direct==direction.NORTH:
for c in curset:
if len(maxc)==0 or c.y>maxc[0].y:
maxc = [c]
elif c.y==maxc[0].y:
maxc.append(c)
elif direct==direct.SOUTH:
for c in curset:
if len(maxc)==0 or c.y<maxc[0].y:
maxc = [c]
elif c.y==maxc[0].y:
maxc.append(c)
elif direct==direct.EAST:
for c in curset:
if len(maxc)==0 or c.x>maxc[0].x:
maxc = [c]
elif c.x==maxc[0].x:
maxc.append(c)
elif direct==direct.WEST:
for c in curset:
if len(maxc)==0 or c.x<maxc[0].x:
maxc = [c]
elif c.x==maxc[0].x:
maxc.append(c)
newset = set(maxc)
return newset
def expand_border(curset, direct):
"""
Expand the current set of sells in a given direction.
Only return the contiguous cells.
"""
border_set = get_border(curset, direct)
next_border_set = increment_set(border_set, direct)
return next_border_set
def expand_borders(curset):
"""
Return the expansions in planar directions.
"""
north_set=expand_border(curset,direction.NORTH)
south_set=expand_border(curset,direction.SOUTH)
east_set=expand_border(curset,direction.EAST)
west_set=expand_border(curset,direction.WEST)
return(north_set, east_set, south_set, west_set)
def inflate_cell(cell, distance):
"""
Expand the current cell in all directions and return the set.
"""
newset = set(cell)
if distance==0:
return(newset)
# recursively call this based on the distance
for offset in direction.all_offsets():
# FIXME: If distance is large this will be inefficient, but it is like 1 or 2
newset.update(inflate_cell(cell+offset,distance-1))
return newset
def inflate_set(curset, distance):
"""
Expand the set in all directions by the given number of grids.
"""
if distance<=0:
return curset
newset = curset.copy()
# Add all my neighbors
for c in curset:
newset.update(direction.all_neighbors(c))
# Recurse with less depth
return inflate_set(newset,distance-1)
def flatten_set(curset):
"""
Flatten until we have a set of vector3d objects.
"""
newset = set()
for c in curset:
if isinstance(c,vector3d):
newset.add(c)
else:
newset.update(flatten_set(c))
return newset
def distance_set(coord, curset):
"""
Return the distance from a coordinate to any item in the set
"""
min_dist = math.inf
for c in curset:
min_dist = min(coord.euclidean_distance(c), min_dist)
return min_dist

View File

@ -1,689 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from openram import debug
from openram.base.vector import vector
from openram.base.vector3d import vector3d
from openram.base.pin_layout import pin_layout
from .direction import direction
class pin_group:
"""
A class to represent a group of rectangular design pin.
It requires a router to define the track widths and blockages which
determine how pin shapes get mapped to tracks.
It is initially constructed with a single set of (touching) pins.
"""
def __init__(self, name, pin_set, router):
self.name = name
# Flag for when it is routed
self.routed = False
# Flag for when it is enclosed
self.enclosed = False
# This is a list because we can have a pin
# group of disconnected sets of pins
# and these are represented by separate lists
self.pins = set(pin_set)
# Remove any redundant pins (i.e. contained in other pins)
self.remove_redundant_pins()
self.router = router
# These are the corresponding pin grids for each pin group.
self.grids = set()
# These are the secondary grids that could
# or could not be part of the pin
self.secondary_grids = set()
# The set of blocked grids due to this pin
self.blockages = set()
# This is a set of pin_layout shapes to cover the grids
self.enclosures = set()
def __str__(self):
""" override print function output """
total_string = "(pg {} ".format(self.name)
pin_string = "\n pins={}".format(self.pins)
total_string += pin_string
grids_string = "\n grids={}".format(self.grids)
total_string += grids_string
grids_string = "\n secondary={}".format(self.secondary_grids)
total_string += grids_string
if self.enclosed:
enclosure_string = "\n enclose={}".format(self.enclosures)
total_string += enclosure_string
total_string += ")"
return total_string
def add_pin(self, pin):
self.pins.add(pin)
self.remove_redundant_pins()
def __repr__(self):
""" override repr function output """
return str(self)
def size(self):
return len(self.grids)
def set_routed(self, value=True):
self.routed = value
def is_routed(self):
return self.routed
def remove_redundant_pins(self):
"""
Remove redundant pin shapes
"""
new_pin_list = self.remove_redundant_shapes(list(self.pins))
self.pins = set(new_pin_list)
def remove_redundant_shapes(self, pin_list):
"""
Remove any pin layout that is contained within another.
Returns a new list without modifying pin_list.
"""
local_debug = False
if local_debug:
debug.info(0, "INITIAL: {}".format(pin_list))
add_indices = set(range(len(pin_list)))
# This is n^2, but the number is small
for index1, pin1 in enumerate(pin_list):
# If we remove this pin, it can't contain other pins
if index1 not in add_indices:
continue
for index2, pin2 in enumerate(pin_list):
# Can't contain yourself,
# but compare the indices and not the pins
# so you can remove duplicate copies.
if index1 == index2:
continue
# If we already removed it, can't remove it again...
if index2 not in add_indices:
continue
if pin1.contains(pin2):
if local_debug:
debug.info(0, "{0} contains {1}".format(pin1, pin2))
add_indices.remove(index2)
new_pin_list = [pin_list[x] for x in add_indices]
if local_debug:
debug.info(0, "FINAL : {}".format(new_pin_list))
return new_pin_list
def compute_enclosures(self):
"""
Find the minimum rectangle enclosures of the given tracks.
"""
# Enumerate every possible enclosure
pin_list = []
for seed in self.grids:
(ll, ur) = self.enclose_pin_grids(seed,
direction.NORTH,
direction.EAST)
enclosure = self.router.compute_pin_enclosure(ll, ur, ll.z)
pin_list.append(enclosure)
(ll, ur) = self.enclose_pin_grids(seed,
direction.EAST,
direction.NORTH)
enclosure = self.router.compute_pin_enclosure(ll, ur, ll.z)
pin_list.append(enclosure)
if len(pin_list) == 0:
debug.error("Did not find any enclosures for {}".format(self.name))
self.router.write_debug_gds("pin_enclosure_error.gds")
# Now simplify the enclosure list
new_pin_list = self.remove_redundant_shapes(pin_list)
# Now add the right name
for pin in new_pin_list:
pin.name = self.name
debug.check(len(new_pin_list) > 0,
"Did not find any enclosures.")
return new_pin_list
def compute_connector(self, pin, enclosure):
"""
Compute a shape to connect the pin to the enclosure shape.
This assumes the shape will be the dimension of the pin.
"""
if pin.xoverlaps(enclosure):
# Is it vertical overlap, extend pin shape to enclosure
plc = pin.lc()
prc = pin.rc()
elc = enclosure.lc()
# erc = enclosure.rc()
ymin = min(plc.y, elc.y)
ymax = max(plc.y, elc.y)
ll = vector(plc.x, ymin)
ur = vector(prc.x, ymax)
elif pin.yoverlaps(enclosure):
# Is it horizontal overlap, extend pin shape to enclosure
pbc = pin.bc()
puc = pin.uc()
ebc = enclosure.bc()
# euc = enclosure.uc()
xmin = min(pbc.x, ebc.x)
xmax = max(pbc.x, ebc.x)
ll = vector(xmin, pbc.y)
ur = vector(xmax, puc.y)
else:
# Neither, so we must do a corner-to corner
pc = pin.center()
ec = enclosure.center()
xmin = min(pc.x, ec.x)
xmax = max(pc.x, ec.x)
ymin = min(pc.y, ec.y)
ymax = max(pc.y, ec.y)
ll = vector(xmin, ymin)
ur = vector(xmax, ymax)
if ll.x == ur.x or ll.y == ur.y:
return None
p = pin_layout(pin.name, [ll, ur], pin.layer)
return p
def find_above_connector(self, pin, enclosures):
"""
Find the enclosure that is to above the pin
and make a connector to it's upper edge.
"""
# Create the list of shapes that contain the pin edge
edge_list = []
for shape in enclosures:
if shape.xcontains(pin):
edge_list.append(shape)
# Sort them by their bottom edge
edge_list.sort(key=lambda x: x.by(), reverse=True)
# Find the bottom edge that is next to the pin's top edge
above_item = None
for item in edge_list:
if item.by() >= pin.uy():
above_item = item
else:
break
# There was nothing
if not above_item:
return None
# If it already overlaps, no connector needed
if above_item.overlaps(pin):
return None
# Otherwise, make a connector to the item
p = self.compute_connector(pin, above_item)
return p
def find_below_connector(self, pin, enclosures):
"""
Find the enclosure that is below the pin
and make a connector to it's upper edge.
"""
# Create the list of shapes that contain the pin edge
edge_list = []
for shape in enclosures:
if shape.xcontains(pin):
edge_list.append(shape)
# Sort them by their upper edge
edge_list.sort(key=lambda x: x.uy())
# Find the upper edge that is next to the pin's bottom edge
bottom_item = None
for item in edge_list:
if item.uy() <= pin.by():
bottom_item = item
else:
break
# There was nothing to the left
if not bottom_item:
return None
# If it already overlaps, no connector needed
if bottom_item.overlaps(pin):
return None
# Otherwise, make a connector to the item
p = self.compute_connector(pin, bottom_item)
return p
def find_left_connector(self, pin, enclosures):
"""
Find the enclosure that is to the left of the pin
and make a connector to it's right edge.
"""
# Create the list of shapes that contain the pin edge
edge_list = []
for shape in enclosures:
if shape.ycontains(pin):
edge_list.append(shape)
# Sort them by their right edge
edge_list.sort(key=lambda x: x.rx())
# Find the right edge that is to the pin's left edge
left_item = None
for item in edge_list:
if item.rx() <= pin.lx():
left_item = item
else:
break
# There was nothing to the left
if not left_item:
return None
# If it already overlaps, no connector needed
if left_item.overlaps(pin):
return None
# Otherwise, make a connector to the item
p = self.compute_connector(pin, left_item)
return p
def find_right_connector(self, pin, enclosures):
"""
Find the enclosure that is to the right of the pin
and make a connector to it's left edge.
"""
# Create the list of shapes that contain the pin edge
edge_list = []
for shape in enclosures:
if shape.ycontains(pin):
edge_list.append(shape)
# Sort them by their right edge
edge_list.sort(key=lambda x: x.lx(), reverse=True)
# Find the left edge that is next to the pin's right edge
right_item = None
for item in edge_list:
if item.lx() >= pin.rx():
right_item = item
else:
break
# There was nothing to the right
if not right_item:
return None
# If it already overlaps, no connector needed
if right_item.overlaps(pin):
return None
# Otherwise, make a connector to the item
p = self.compute_connector(pin, right_item)
return p
def find_smallest_connector(self, pin_list, shape_list):
"""
Compute all of the connectors between the overlapping
pins and enclosure shape list.
Return the smallest.
"""
smallest = None
for pin in pin_list:
for enclosure in shape_list:
new_enclosure = self.compute_connector(pin, enclosure)
if not smallest or new_enclosure.area() < smallest.area():
smallest = new_enclosure
return smallest
def find_smallest_overlapping(self, pin_list, shape_list):
"""
Find the smallest area shape in shape_list that overlaps with any
pin in pin_list by a min width.
"""
smallest_shape = None
for pin in pin_list:
overlap_shape = self.find_smallest_overlapping_pin(pin, shape_list)
if overlap_shape:
# overlap_length = pin.overlap_length(overlap_shape)
if not smallest_shape or overlap_shape.area() < smallest_shape.area():
smallest_shape = overlap_shape
return smallest_shape
def find_smallest_overlapping_pin(self, pin, shape_list):
"""
Find the smallest area shape in shape_list that overlaps with any
pin in pin_list by a min width.
"""
smallest_shape = None
zindex = self.router.get_zindex(pin.lpp[0])
(min_width, min_space) = self.router.get_layer_width_space(zindex)
# Now compare it with every other shape to check how much they overlap
for other in shape_list:
overlap_length = pin.overlap_length(other)
if overlap_length > min_width:
if not smallest_shape or other.area() < smallest_shape.area():
smallest_shape = other
return smallest_shape
def overlap_any_shape(self, pin_list, shape_list):
"""
Does the given pin overlap any of the shapes in the pin list.
"""
for pin in pin_list:
for other in shape_list:
if pin.overlaps(other):
return True
return False
def max_pin_layout(self, pin_list):
"""
Return the max area pin_layout
"""
biggest = pin_list[0]
for pin in pin_list:
if pin.area() > biggest.area():
biggest = pin
return pin
def enclose_pin_grids(self, ll, dir1=direction.NORTH, dir2=direction.EAST):
"""
This encloses a single pin component with a rectangle
starting with the seed and expanding dir1 until blocked
and then dir2 until blocked.
dir1 and dir2 should be two orthogonal directions.
"""
offset1 = direction.get_offset(dir1)
offset2 = direction.get_offset(dir2)
# We may have started with an empty set
debug.check(len(self.grids) > 0, "Cannot seed an grid empty set.")
common_blockages = self.router.get_blocked_grids() & self.grids
# Start with the ll and make the widest row
row = [ll]
# Move in dir1 while we can
while True:
next_cell = row[-1] + offset1
# Can't move if not in the pin shape
if next_cell in self.grids and next_cell not in common_blockages:
row.append(next_cell)
else:
break
# Move in dir2 while we can
while True:
next_row = [x + offset2 for x in row]
for cell in next_row:
# Can't move if any cell is not in the pin shape
if cell not in self.grids or cell in common_blockages:
break
else:
row = next_row
# Skips the second break
continue
# Breaks from the nested break
break
# Add a shape from ll to ur
ur = row[-1]
return (ll, ur)
def enclose_pin(self):
"""
If there is one set of connected pin shapes,
this will find the smallest rectangle enclosure that
overlaps with any pin.
If there is not, it simply returns all the enclosures.
"""
self.enclosed = True
# Compute the enclosure pin_layout list of the set of tracks
self.enclosures = self.compute_enclosures()
# Find a connector to every pin and add it to the enclosures
for pin in self.pins:
# If it is contained, it won't need a connector
if pin.contained_by_any(self.enclosures):
continue
# Find a connector in the cardinal directions
# If there is overlap, but it isn't contained,
# these could all be None
# These could also be none if the pin is
# diagonal from the enclosure
left_connector = self.find_left_connector(pin, self.enclosures)
right_connector = self.find_right_connector(pin, self.enclosures)
above_connector = self.find_above_connector(pin, self.enclosures)
below_connector = self.find_below_connector(pin, self.enclosures)
connector_list = [left_connector,
right_connector,
above_connector,
below_connector]
filtered_list = list(filter(lambda x: x != None, connector_list))
if (len(filtered_list) > 0):
import copy
bbox_connector = copy.copy(pin)
bbox_connector.bbox(filtered_list)
self.enclosures.append(bbox_connector)
# Now, make sure each pin touches an enclosure.
# If not, add another (diagonal) connector.
# This could only happen when there was no enclosure
# in any cardinal direction from a pin
if not self.overlap_any_shape(self.pins, self.enclosures):
connector = self.find_smallest_connector(self.pins,
self.enclosures)
if not connector:
debug.error("Could not find a connector for {} with {}".format(self.pins,
self.enclosures))
self.router.write_debug_gds("no_connector.gds")
self.enclosures.append(connector)
# At this point, the pins are overlapping,
# but there might be more than one!
overlap_set = set()
for pin in self.pins:
overlap_set.update(self.transitive_overlap(pin, self.enclosures))
# Use the new enclosures and recompute the grids
# that correspond to them
if len(overlap_set) < len(self.enclosures):
self.enclosures = overlap_set
self.grids = set()
# Also update the grid locations with the new
# (possibly pruned) enclosures
for enclosure in self.enclosures:
(sufficient, insufficient) = self.router.convert_pin_to_tracks(self.name,
enclosure)
self.grids.update(sufficient)
debug.info(3, "Computed enclosure(s) {0}\n {1}\n {2}\n {3}".format(self.name,
self.pins,
self.grids,
self.enclosures))
def transitive_overlap(self, shape, shape_list):
"""
Given shape, find the elements in shape_list that overlap transitively.
I.e. if shape overlaps A and A overlaps B, return both A and B.
"""
augmented_shape_list = set(shape_list)
old_connected_set = set()
connected_set = set([shape])
# Repeat as long as we expand the set
while len(connected_set) > len(old_connected_set):
old_connected_set = connected_set
connected_set = set([shape])
for old_shape in old_connected_set:
for cur_shape in augmented_shape_list:
if old_shape.overlaps(cur_shape):
connected_set.add(cur_shape)
# Remove the original shape
connected_set.remove(shape)
# if len(connected_set)<len(shape_list):
# import pprint
# print("S: ",shape)
# pprint.pprint(shape_list)
# pprint.pprint(connected_set)
return connected_set
def add_enclosure(self, cell):
"""
Add the enclosure shape to the given cell.
"""
for enclosure in self.enclosures:
debug.info(4, "Adding enclosure {0} {1}".format(self.name,
enclosure))
cell.add_rect(layer=enclosure.layer,
offset=enclosure.ll(),
width=enclosure.width(),
height=enclosure.height())
def perimeter_grids(self):
"""
Return a list of the grids on the perimeter.
This assumes that we have a single contiguous shape.
"""
perimeter_set = set()
cardinal_offsets = direction.cardinal_offsets()
for g1 in self.grids:
neighbor_grids = [g1 + offset for offset in cardinal_offsets]
neighbor_count = sum([x in self.grids for x in neighbor_grids])
# If we aren't completely enclosed, we are on the perimeter
if neighbor_count < 4:
perimeter_set.add(g1)
return perimeter_set
def adjacent(self, other):
"""
Chck if the two pin groups have at least one adjacent pin grid.
"""
# We could optimize this to just check the boundaries
for g1 in self.perimeter_grids():
for g2 in other.perimeter_grids():
if g1.adjacent(g2):
return True
return False
def adjacent_grids(self, other, separation):
"""
Determine the sets of grids that are within a separation distance
of any grid in the other set.
"""
# We could optimize this to just check the boundaries
adj_grids = set()
for g1 in self.grids:
for g2 in other.grids:
if g1.distance(g2) <= separation:
adj_grids.add(g1)
return adj_grids
def convert_pin(self):
"""
Convert the list of pin shapes into sets of routing grids.
The secondary set of grids are "optional" pin shapes that
should be either blocked or part of the pin.
"""
# Set of tracks that overlap a pin
pin_set = set()
# Set of track adjacent to or paritally overlap a pin (not full DRC connection)
partial_set = set()
# for pin in self.pins:
# lx = pin.lx()
# ly = pin.by()
# if lx > 87.9 and lx < 87.99 and ly > 18.56 and ly < 18.6:
# breakpoint()
for pin in self.pins:
debug.info(4, " Converting {0}".format(pin))
# Determine which tracks the pin overlaps
(sufficient, insufficient) = self.router.convert_pin_to_tracks(self.name,
pin)
pin_set.update(sufficient)
partial_set.update(insufficient)
# Blockages will be a super-set of pins since
# it uses the inflated pin shape.
blockage_in_tracks = self.router.convert_blockage(pin)
# Must include the pins here too because these are computed in a different
# way than blockages.
blockages = sufficient | insufficient | blockage_in_tracks
self.blockages.update(blockages)
# If we have a blockage, we must remove the grids
# Remember, this excludes the pin blockages already
blocked_grids = self.router.get_blocked_grids()
pin_set.difference_update(blocked_grids)
partial_set.difference_update(blocked_grids)
# At least one of the groups must have some valid tracks
if (len(pin_set) == 0 and len(partial_set) == 0):
# debug.warning("Pin is very close to metal blockage.\nAttempting to expand blocked pin {}".format(self.pins))
for pin in self.pins:
debug.warning(" Expanding conversion {0}".format(pin))
# Determine which tracks the pin overlaps
(sufficient, insufficient) = self.router.convert_pin_to_tracks(self.name,
pin,
expansion=1)
# This time, don't remove blockages in the hopes that it might be ok.
# Could cause DRC problems!
pin_set.update(sufficient)
partial_set.update(insufficient)
# If it's still empty, we must bail.
if len(pin_set) == 0 and len(partial_set) == 0:
debug.error("Unable to find unblocked pin {} {}".format(self.name,
self.pins))
self.router.write_debug_gds("blocked_pin.gds")
# Consider the fully connected set first and if not the partial set
# if len(pin_set) > 0:
# self.grids = pin_set
# else:
# self.grids = partial_set
# Just using the full set simplifies the enclosures, otherwise
# we get some pin enclose DRC errors due to off grid pins
self.grids = pin_set | partial_set
if len(self.grids) < 0:
debug.error("Did not find any unblocked grids: {}".format(str(self.pins)))
self.router.write_debug_gds("blocked_pin.gds")
# Remember the secondary grids for removing adjacent pins
self.secondary_grids = partial_set
debug.info(4, " pins {}".format(self.grids))
debug.info(4, " secondary {}".format(self.secondary_grids))

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,11 @@ class router_tech:
self.vert_layer_minwidth = max(self.vert_layer_minwidth, max_via_size)
self.horiz_layer_minwidth = max(self.horiz_layer_minwidth, max_via_size)
# Update spacing for the new widths
max_width = max(self.vert_layer_minwidth, self.horiz_layer_minwidth)
self.vert_layer_spacing = self.get_layer_space(1, max_width)
self.horiz_layer_spacing = self.get_layer_space(0, max_width)
self.horiz_track_width = self.horiz_layer_minwidth + self.horiz_layer_spacing
self.vert_track_width = self.vert_layer_minwidth + self.vert_layer_spacing
@ -109,23 +114,34 @@ class router_tech:
else:
debug.error("Invalid zindex {}".format(zindex), -1)
def get_lpp(self, zindex):
if zindex == 1:
return self.vert_lpp
elif zindex == 0:
return self.horiz_lpp
else:
debug.error("Invalid zindex {}".format(zindex), -1)
def get_layer_width_space(self, zindex):
"""
These are the width and spacing of a supply layer given a supply rail
of the given number of min wire widths.
"""
if zindex==1:
layer_name = self.vert_layer_name
elif zindex==0:
layer_name = self.horiz_layer_name
else:
debug.error("Invalid zindex for track", -1)
min_wire_width = drc("minwidth_{0}".format(layer_name), 0, math.inf)
min_width = self.route_track_width * drc("minwidth_{0}".format(layer_name), self.route_track_width * min_wire_width, math.inf)
min_spacing = drc(str(layer_name)+"_to_"+str(layer_name), self.route_track_width * min_wire_width, math.inf)
min_width = self.get_layer_width(zindex)
min_spacing = self.get_layer_space(zindex, min_width)
return (min_width, min_spacing)
def get_layer_width(self, zindex):
""" Return the minimum width of a layer. """
layer_name = self.get_layer(zindex)
min_wire_width = drc("minwidth_{0}".format(layer_name), 0, math.inf)
min_width = self.route_track_width * drc("minwidth_{0}".format(layer_name), self.route_track_width * min_wire_width, math.inf)
return min_width
def get_layer_space(self, zindex, width=None):
""" Return the minimum spacing of a layer given wire width. """
if width is None:
width = self.get_layer_width(zindex)
layer_name = self.get_layer(zindex)
min_spacing = drc(str(layer_name)+"_to_"+str(layer_name), self.route_track_width * width, math.inf)
return min_spacing

View File

@ -1,104 +1,169 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
from datetime import datetime
from openram import debug
from openram import print_time
from openram.base.vector import vector
from openram import OPTS
from .graph import graph
from .graph_shape import graph_shape
from .router import router
from .signal_grid import signal_grid
class signal_escape_router(router):
"""
A router that routes signals to perimeter and makes pins.
This is the signal escape router that uses the Hanan grid graph method.
"""
def __init__(self, layers, design, bbox=None, margin=0):
def __init__(self, layers, design, bbox=None):
# `router` is the base router class
router.__init__(self, layers, design, bbox)
# New pins are the side supply pins
self.new_pins = {}
def route(self, pin_names):
""" Route the given pins to the perimeter. """
debug.info(1, "Running signal escape router...")
# Prepare gdsMill to find pins and blockages
self.prepare_gds_reader()
# Find pins to be routed
for name in pin_names:
self.find_pins(name)
# Find blockages and vias
self.find_blockages()
self.find_vias()
# Convert blockages and vias if they overlap a pin
self.convert_vias()
self.convert_blockages()
# Add fake pins on the perimeter to do the escape routing on
self.add_perimeter_fake_pins()
# Add vdd and gnd pins as blockages as well
# NOTE: This is done to make vdd and gnd pins DRC-safe
for pin in self.all_pins:
self.blockages.append(self.inflate_shape(pin))
# Route vdd and gnd
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
self.replace_layout_pins()
def add_perimeter_fake_pins(self):
"""
This will route on layers in design. It will get the blockages from
either the gds file name or the design itself (by saving to a gds file).
Add the fake pins on the perimeter to where the signals will be routed.
"""
router.__init__(self,
layers=layers,
design=design,
bbox=bbox,
margin=margin)
def perimeter_dist(self, pin_name):
"""
Return the shortest Manhattan distance to the bounding box perimeter.
"""
loc = self.cell.get_pin(pin_name).center()
x_dist = min(loc.x - self.ll.x, self.ur.x - loc.x)
y_dist = min(loc.y - self.ll.y, self.ur.y - loc.y)
ll, ur = self.bbox
wide = self.track_wire
return min(x_dist, y_dist)
for side in ["top", "bottom", "left", "right"]:
vertical = side in ["left", "right"]
def escape_route(self, pin_names):
"""
Takes a list of tuples (name, side) and routes them. After routing,
it removes the old pin and places a new one on the perimeter.
"""
self.create_routing_grid(signal_grid)
# Calculate the lower left coordinate
if side == "top":
offset = vector(ll.x, ur.y - wide)
elif side == "bottom":
offset = vector(ll.x, ll.y)
elif side == "left":
offset = vector(ll.x, ll.y)
elif side == "right":
offset = vector(ur.x - wide, ll.y)
start_time = datetime.now()
self.find_pins_and_blockages(pin_names)
print_time("Finding pins and blockages",datetime.now(), start_time, 3)
# Calculate width and height
shape = ur - ll
if vertical:
shape_width = wide
shape_height = shape.y
else:
shape_width = shape.x
shape_height = wide
# Order the routes by closest to the perimeter first
# This prevents some pins near the perimeter from being blocked by other pins
ordered_pin_names = sorted(pin_names, key=lambda x: self.perimeter_dist(x))
# Add this new pin
# They must lie on the non-preferred direction since the side supply
# pins will lie on the preferred direction
layer = self.get_layer(int(not vertical))
nll = vector(offset.x, offset.y)
nur = vector(offset.x + shape_width, offset.y + shape_height)
rect = [nll, nur]
pin = graph_shape(name="fake",
rect=rect,
layer_name_pp=layer)
self.fake_pins.append(pin)
# Route the supply pins to the supply rails
# Route vdd first since we want it to be shorter
start_time = datetime.now()
for pin_name in ordered_pin_names:
self.route_signal(pin_name)
# if pin_name == "dout0[1]":
# self.write_debug_gds("postroute.gds", True)
print_time("Maze routing pins",datetime.now(), start_time, 3)
def get_closest_perimeter_fake_pin(self, pin):
""" Return the closest fake pin for the given pin. """
#self.write_debug_gds("final_escape_router.gds",False)
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
return True
def route_signal(self, pin_name, side="all"):
def get_route_pairs(self, pin_names):
""" Return the pairs to be routed. """
for detour_scale in [5 * pow(2, x) for x in range(5)]:
debug.info(1, "Escape routing {0} with scale {1}".format(pin_name, detour_scale))
to_route = []
for name in pin_names:
pin = next(iter(self.pins[name]))
fake = self.get_closest_perimeter_fake_pin(pin)
to_route.append((pin, fake, pin.distance(fake)))
return sorted(to_route, key=lambda x: x[2])
# Clear everything in the routing grid.
self.rg.reinit()
# This is inefficient since it is non-incremental, but it was
# easier to debug.
self.prepare_blockages()
self.clear_blockages(pin_name)
def replace_layout_pins(self):
""" Replace the old layout pins with new ones around the perimeter. """
# Add the single component of the pin as the source
# which unmarks it as a blockage too
self.add_source(pin_name)
# Marks the grid cells all along the perimeter as a target
self.add_perimeter_target(side)
# if pin_name == "dout0[3]":
# self.write_debug_gds("pre_route.gds", False)
# breakpoint()
# Actually run the A* router
if self.run_router(detour_scale=detour_scale):
new_pin = self.get_perimeter_pin()
self.cell.replace_layout_pin(pin_name, new_pin)
return
# if pin_name == "dout0[3]":
# self.write_debug_gds("pre_route.gds", False)
# breakpoint()
self.write_debug_gds("debug_route.gds", True)
for name, pin in self.new_pins.items():
pin = graph_shape(pin.name, pin.boundary, pin.lpp)
# Find the intersection of this pin on the perimeter
for fake in self.fake_pins:
edge = pin.intersection(fake)
if edge:
break
self.design.replace_layout_pin(name, edge)

View File

@ -1,165 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from copy import deepcopy
from heapq import heappush,heappop
from openram import debug
from openram.base.vector3d import vector3d
from .grid import grid
from .grid_path import grid_path
class signal_grid(grid):
"""
Expand the two layer grid to include A* search functions for a source and target.
"""
def __init__(self, ll, ur, track_factor):
""" Create a routing map of width x height cells and 2 in the z-axis. """
grid.__init__(self, ll, ur, track_factor)
def reinit(self):
""" Reinitialize everything for a new route. """
# Reset all the cells in the map
for p in self.map.values():
p.reset()
self.clear_source()
self.clear_target()
def init_queue(self):
"""
Populate the queue with all the source pins with cost
to the target. Each item is a path of the grid cells.
We will use an A* search, so this cost must be pessimistic.
Cost so far will be the length of the path.
"""
# Counter is used to not require data comparison in Python 3.x
# Items will be returned in order they are added during cost ties
self.q = []
self.counter = 0
for s in self.source:
cost = self.cost_to_target(s)
debug.info(3, "Init: cost=" + str(cost) + " " + str([s]))
heappush(self.q, (cost, self.counter, grid_path([vector3d(s)])))
self.counter += 1
def route(self, detour_scale):
"""
This does the A* maze routing with preferred direction routing.
This only works for 1 track wide routes!
"""
# We set a cost bound of the HPWL for run-time. This can be
# over-ridden if the route fails due to pruning a feasible solution.
any_source_element = next(iter(self.source))
cost_bound = detour_scale * self.cost_to_target(any_source_element) * grid.PREFERRED_COST
# Check if something in the queue is already a source and a target!
for s in self.source:
if self.is_target(s):
return((grid_path([vector3d(s)]), 0))
# Put the source items into the queue
self.init_queue()
# Keep expanding and adding to the priority queue until we are done
while len(self.q)>0:
# should we keep the path in the queue as well or just the final node?
(cost, count, curpath) = heappop(self.q)
debug.info(3, "Queue size: size=" + str(len(self.q)) + " " + str(cost))
debug.info(4, "Expanding: cost=" + str(cost) + " " + str(curpath))
# expand the last element
neighbors = self.expand_dirs(curpath)
debug.info(4, "Neighbors: " + str(neighbors))
for n in neighbors:
# make a new copy of the path to not update the old ones
newpath = deepcopy(curpath)
# node is added to the map by the expand routine
newpath.append(n)
# check if we hit the target and are done
if self.is_target(n[0]): # This uses the [0] item because we are assuming 1-track wide
return (newpath, newpath.cost())
else:
# current path cost + predicted cost
current_cost = newpath.cost()
target_cost = self.cost_to_target(n[0])
predicted_cost = current_cost + target_cost
# only add the cost if it is less than our bound
if (predicted_cost < cost_bound):
if (self.map[n[0]].min_cost==-1 or predicted_cost<self.map[n[0]].min_cost):
self.map[n[0]].min_path = newpath
self.map[n[0]].min_cost = predicted_cost
debug.info(4, "Enqueuing: cost=" + str(current_cost) + "+" + str(target_cost) + " " + str(newpath))
# add the cost to get to this point if we haven't reached it yet
heappush(self.q, (predicted_cost, self.counter, newpath))
self.counter += 1
#else:
# print("Better previous cost.")
#else:
# print("Cost bounded")
return (None, None)
def expand_dirs(self, curpath):
"""
Expand each of the four cardinal directions plus up or down
but not expanding to blocked cells. Expands in all directions
regardless of preferred directions.
"""
# Expand all directions.
neighbors = curpath.expand_dirs()
# Filter the out of region ones
# Filter the blocked ones
valid_neighbors = [x for x in neighbors if self.is_inside(x) and not self.is_blocked(x)]
return valid_neighbors
def hpwl(self, src, dest):
"""
Return half perimeter wire length from point to another.
Either point can have positive or negative coordinates.
Include the via penalty if there is one.
"""
hpwl = abs(src.x - dest.x)
hpwl += abs(src.y - dest.y)
if src.x!=dest.x and src.y!=dest.y:
hpwl += grid.VIA_COST
return hpwl
def cost_to_target(self, source):
"""
Find the cheapest HPWL distance to any target point ignoring
blockages for A* search.
"""
any_target_element = next(iter(self.target))
cost = self.hpwl(source, any_target_element)
for t in self.target:
cost = min(self.hpwl(source, t), cost)
return cost
def get_inertia(self, p0, p1):
"""
Sets the direction based on the previous direction we came from.
"""
# direction (index) of movement
if p0.x==p1.x:
return 1
elif p0.y==p1.y:
return 0
else:
# z direction
return 2

View File

@ -1,69 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from openram import debug
from openram.router import router
class signal_router(router):
"""
A router class to read an obstruction map from a gds and plan a
route on a given layer. This is limited to two layer routes.
"""
def __init__(self, layers, design, bbox=None):
"""
This will route on layers in design. It will get the blockages from
either the gds file name or the design itself (by saving to a gds file).
"""
router.__init__(self, layers, design, bbox)
def route(self, src, dest, detour_scale=5):
"""
Route a single source-destination net and return
the simplified rectilinear path. Cost factor is how sub-optimal to explore for a feasible route.
This is used to speed up the routing when there is not much detouring needed.
"""
debug.info(1, "Running signal router from {0} to {1}...".format(src, dest))
self.pins[src] = []
self.pins[dest] = []
# Clear the pins if we have previously routed
if (hasattr(self, 'rg')):
self.clear_pins()
else:
# Creat a routing grid over the entire area
# FIXME: This could be created only over the routing region,
# but this is simplest for now.
self.create_routing_grid(signal_grid)
# Get the pin shapes
self.find_pins_and_blockages([src, dest])
# Block everything
self.prepare_blockages()
# Clear the pins we are routing
self.set_blockages(self.pin_components[src], False)
self.set_blockages(self.pin_components[dest], False)
# Now add the src/tgt if they are not blocked by other shapes
self.add_source(src)
self.add_target(dest)
if not self.run_router(detour_scale=detour_scale):
self.write_debug_gds(stop_program=False)
return False
#self.write_debug_gds(stop_program=False)
return True

View File

@ -1,79 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from .signal_grid import signal_grid
from .grid_path import grid_path
class supply_grid(signal_grid):
"""
This routes a supply grid. It is derived from a signal grid because it still
routes the pins to the supply rails using the same routines.
It has a few extra routines to support "waves" which are multiple track wide
directional routes (no bends).
"""
def __init__(self, ll, ur, track_width):
""" Create a routing map of width x height cells and 2 in the z-axis. """
signal_grid.__init__(self, ll, ur, track_width)
def reinit(self):
""" Reinitialize everything for a new route. """
self.source = set()
self.target = set()
# Reset all the cells in the map
for p in self.map.values():
p.reset()
def find_start_wave(self, wave, direct):
"""
Finds the first loc starting at loc and up that is open.
Returns None if it reaches max size first.
"""
# Don't expand outside the bounding box
if wave[0].x > self.ur.x:
return None
if wave[-1].y > self.ur.y:
return None
while wave and self.is_wave_blocked(wave):
wf = grid_path(wave)
wave = wf.neighbor(direct)
# Bail out if we couldn't increment futher
if wave[0].x > self.ur.x or wave[-1].y > self.ur.y:
return None
# Return a start if it isn't blocked
if not self.is_wave_blocked(wave):
return wave
return wave
def is_wave_blocked(self, wave):
"""
Checks if any of the locations are blocked
"""
for v in wave:
if self.is_blocked(v):
return True
else:
return False
def probe(self, wave, direct):
"""
Expand the wave until there is a blockage and return
the wave path.
"""
wave_path = grid_path()
while wave and not self.is_wave_blocked(wave):
if wave[0].x > self.ur.x or wave[-1].y > self.ur.y:
break
wave_path.append(wave)
wave = wave_path.neighbor(direct)
return wave_path

View File

@ -1,394 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from datetime import datetime
from openram import debug
from openram.base.vector3d import vector3d
from openram import print_time
from .router import router
from .direction import direction
from .supply_grid import supply_grid
from . import grid_utils
class supply_grid_router(router):
"""
A router class to read an obstruction map from a gds and
routes a grid to connect the supply on the two layers.
"""
def __init__(self, layers, design, bbox=None, pin_type=None):
"""
This will route on layers in design. It will get the blockages from
either the gds file name or the design itself (by saving to a gds file).
"""
start_time = datetime.now()
# Power rail width in minimum wire widths
self.route_track_width = 1
router.__init__(self, layers, design, bbox=bbox, margin=margin, route_track_width=self.route_track_width)
# The list of supply rails (grid sets) that may be routed
self.supply_rails = {}
# This is the same as above but as a sigle set for the all the rails
self.supply_rail_tracks = {}
print_time("Init supply router", datetime.now(), start_time, 3)
def route(self, vdd_name="vdd", gnd_name="gnd"):
"""
Add power supply rails and connect all pins to these rails.
"""
debug.info(1, "Running supply router on {0} and {1}...".format(vdd_name, gnd_name))
self.vdd_name = vdd_name
self.gnd_name = gnd_name
# Clear the pins if we have previously routed
if (hasattr(self, 'rg')):
self.clear_pins()
else:
# Creat a routing grid over the entire area
# FIXME: This could be created only over the routing region,
# but this is simplest for now.
self.create_routing_grid(supply_grid)
# Get the pin shapes
start_time = datetime.now()
self.find_pins_and_blockages([self.vdd_name, self.gnd_name])
print_time("Finding pins and blockages", datetime.now(), start_time, 3)
# Add the supply rails in a mesh network and connect H/V with vias
start_time = datetime.now()
# Block everything
self.prepare_blockages()
self.clear_blockages(self.gnd_name)
# Determine the rail locations
self.route_supply_rails(self.gnd_name, 0)
# Block everything
self.prepare_blockages()
self.clear_blockages(self.vdd_name)
# Determine the rail locations
self.route_supply_rails(self.vdd_name, 1)
print_time("Routing supply rails", datetime.now(), start_time, 3)
start_time = datetime.now()
self.route_simple_overlaps(vdd_name)
self.route_simple_overlaps(gnd_name)
print_time("Simple overlap routing", datetime.now(), start_time, 3)
# Route the supply pins to the supply rails
# Route vdd first since we want it to be shorter
start_time = datetime.now()
self.route_pins_to_rails(vdd_name)
self.route_pins_to_rails(gnd_name)
print_time("Maze routing supplies", datetime.now(), start_time, 3)
# self.write_debug_gds("final.gds", False)
# Did we route everything??
if not self.check_all_routed(vdd_name):
return False
if not self.check_all_routed(gnd_name):
return False
return True
def route_simple_overlaps(self, pin_name):
"""
This checks for simple cases where a pin component already overlaps a supply rail.
It will add an enclosure to ensure the overlap in wide DRC rule cases.
"""
debug.info(1, "Routing simple overlap pins for {0}".format(pin_name))
# These are the wire tracks
wire_tracks = self.supply_rail_tracks[pin_name]
routed_count=0
for pg in self.pin_groups[pin_name]:
if pg.is_routed():
continue
# First, check if we just overlap, if so, we are done.
overlap_grids = wire_tracks & pg.grids
if len(overlap_grids)>0:
routed_count += 1
pg.set_routed()
continue
# Else, if we overlap some of the space track, we can patch it with an enclosure
# pg.create_simple_overlap_enclosure(pg.grids)
# pg.add_enclosure(self.cell)
debug.info(1, "Routed {} simple overlap pins".format(routed_count))
def finalize_supply_rails(self, name):
"""
Determine which supply rails overlap and can accomodate a via.
Remove any supply rails that do not have a via since they are disconnected.
NOTE: It is still possible though unlikely that there are disconnected groups of rails.
"""
all_rails = self.supply_rails[name]
connections = set()
via_areas = []
for i1, r1 in enumerate(all_rails):
# Only consider r1 horizontal rails
e = next(iter(r1))
if e.z==1:
continue
# We need to move this rail to the other layer for the z indices to match
# during the intersection. This also makes a copy.
new_r1 = {vector3d(i.x, i.y, 1) for i in r1}
for i2, r2 in enumerate(all_rails):
# Never compare to yourself
if i1==i2:
continue
# Only consider r2 vertical rails
e = next(iter(r2))
if e.z==0:
continue
# Determine if we have sufficient overlap and, if so,
# remember:
# the indices to determine a rail is connected to another
# the overlap area for placement of a via
overlap = new_r1 & r2
if len(overlap) >= 1:
debug.info(3, "Via overlap {0} {1}".format(len(overlap),overlap))
connections.update([i1, i2])
via_areas.append(overlap)
# Go through and add the vias at the center of the intersection
for area in via_areas:
ll = grid_utils.get_lower_left(area)
ur = grid_utils.get_upper_right(area)
center = (ll + ur).scale(0.5, 0.5, 0)
self.add_via(center, 1)
# Determien which indices were not connected to anything above
missing_indices = set([x for x in range(len(self.supply_rails[name]))])
missing_indices.difference_update(connections)
# Go through and remove those disconnected indices
# (No via was added, so that doesn't need to be removed)
for rail_index in sorted(missing_indices, reverse=True):
ll = grid_utils.get_lower_left(all_rails[rail_index])
ur = grid_utils.get_upper_right(all_rails[rail_index])
debug.info(1, "Removing disconnected supply rail {0} .. {1}".format(ll, ur))
self.supply_rails[name].pop(rail_index)
# Make the supply rails into a big giant set of grids for easy blockages.
# Must be done after we determine which ones are connected.
self.create_supply_track_set(name)
def add_supply_rails(self, name):
"""
Add the shapes that represent the routed supply rails.
This is after the paths have been pruned and only include rails that are
connected with vias.
"""
for rail in self.supply_rails[name]:
ll = grid_utils.get_lower_left(rail)
ur = grid_utils.get_upper_right(rail)
z = ll.z
pin = self.compute_pin_enclosure(ll, ur, z, name)
debug.info(3, "Adding supply rail {0} {1}->{2} {3}".format(name, ll, ur, pin))
self.cell.add_layout_pin(text=name,
layer=pin.layer,
offset=pin.ll(),
width=pin.width(),
height=pin.height())
def compute_supply_rails(self, name, supply_number):
"""
Compute the unblocked locations for the horizontal and vertical supply rails.
Go in a raster order from bottom to the top (for horizontal) and left to right
(for vertical). Start with an initial start_offset in x and y direction.
"""
self.supply_rails[name]=[]
max_yoffset = self.rg.ur.y
max_xoffset = self.rg.ur.x
min_yoffset = self.rg.ll.y
min_xoffset = self.rg.ll.x
# Horizontal supply rails
start_offset = min_yoffset + supply_number
for offset in range(start_offset, max_yoffset, 2):
# Seed the function at the location with the given width
wave = [vector3d(min_xoffset, offset, 0)]
# While we can keep expanding east in this horizontal track
while wave and wave[0].x < max_xoffset:
added_rail = self.find_supply_rail(name, wave, direction.EAST)
if not added_rail:
# Just seed with the next one
wave = [x+vector3d(1, 0, 0) for x in wave]
else:
# Seed with the neighbor of the end of the last rail
wave = added_rail.neighbor(direction.EAST)
# Vertical supply rails
start_offset = min_xoffset + supply_number
for offset in range(start_offset, max_xoffset, 2):
# Seed the function at the location with the given width
wave = [vector3d(offset, min_yoffset, 1)]
# While we can keep expanding north in this vertical track
while wave and wave[0].y < max_yoffset:
added_rail = self.find_supply_rail(name, wave, direction.NORTH)
if not added_rail:
# Just seed with the next one
wave = [x + vector3d(0, 1, 0) for x in wave]
else:
# Seed with the neighbor of the end of the last rail
wave = added_rail.neighbor(direction.NORTH)
def find_supply_rail(self, name, seed_wave, direct):
"""
Find a start location, probe in the direction, and see if the rail is big enough
to contain a via, and, if so, add it.
"""
# Sweep to find an initial unblocked valid wave
start_wave = self.rg.find_start_wave(seed_wave, direct)
# This means there were no more unblocked grids in the row/col
if not start_wave:
return None
wave_path = self.probe_supply_rail(name, start_wave, direct)
self.approve_supply_rail(name, wave_path)
# Return the rail whether we approved it or not,
# as it will be used to find the next start location
return wave_path
def probe_supply_rail(self, name, start_wave, direct):
"""
This finds the first valid starting location and routes a supply rail
in the given direction.
It returns the space after the end of the rail to seed another call for multiple
supply rails in the same "track" when there is a blockage.
"""
# Expand the wave to the right
wave_path = self.rg.probe(start_wave, direct)
if not wave_path:
return None
# drop the first and last steps to leave escape routing room
# around the blockage that stopped the probe
# except, don't drop the first if it is the first in a row/column
if (direct==direction.NORTH and start_wave[0].y>0):
wave_path.trim_first()
elif (direct == direction.EAST and start_wave[0].x>0):
wave_path.trim_first()
wave_path.trim_last()
return wave_path
def approve_supply_rail(self, name, wave_path):
"""
Check if the supply rail is sufficient (big enough) and add it to the
data structure. Return whether it was added or not.
"""
# We must have at least 2 tracks to drop plus 2 tracks for a via
if len(wave_path) >= 4 * self.route_track_width:
grid_set = wave_path.get_grids()
self.supply_rails[name].append(grid_set)
return True
return False
def route_supply_rails(self, name, supply_number):
"""
Route the horizontal and vertical supply rails across the entire design.
Must be done with lower left at 0,0
"""
debug.info(1, "Routing supply rail {0}.".format(name))
# Compute the grid locations of the supply rails
self.compute_supply_rails(name, supply_number)
# Add the supply rail vias (and prune disconnected rails)
self.finalize_supply_rails(name)
# Add the rails themselves
self.add_supply_rails(name)
def create_supply_track_set(self, pin_name):
"""
Make a single set of all the tracks for the rail and wire itself.
"""
rail_set = set()
for rail in self.supply_rails[pin_name]:
rail_set.update(rail)
self.supply_rail_tracks[pin_name] = rail_set
def route_pins_to_rails(self, pin_name):
"""
This will route each of the remaining pin components to the supply rails.
After it is done, the cells are added to the pin blockage list.
"""
remaining_components = sum(not x.is_routed() for x in self.pin_groups[pin_name])
debug.info(1, "Maze routing {0} with {1} pin components to connect.".format(pin_name,
remaining_components))
for index, pg in enumerate(self.pin_groups[pin_name]):
if pg.is_routed():
continue
debug.info(3, "Routing component {0} {1}".format(pin_name, index))
# Clear everything in the routing grid.
self.rg.reinit()
# This is inefficient since it is non-incremental, but it was
# easier to debug.
self.prepare_blockages()
self.clear_blockages(self.vdd_name)
# Add the single component of the pin as the source
# which unmarks it as a blockage too
self.add_pin_component_source(pin_name, index)
# Add all of the rails as targets
# Don't add the other pins, but we could?
self.add_supply_rail_target(pin_name)
# Actually run the A* router
if not self.run_router(detour_scale=5):
self.write_debug_gds("debug_route.gds")
# if index==3 and pin_name=="vdd":
# self.write_debug_gds("route.gds",False)
def add_supply_rail_target(self, pin_name):
"""
Add the supply rails of given name as a routing target.
"""
debug.info(4, "Add supply rail target {}".format(pin_name))
# Add the wire itself as the target
self.rg.set_target(self.supply_rail_tracks[pin_name])
# But unblock all the rail tracks including the space
self.rg.set_blocked(self.supply_rail_tracks[pin_name], False)
def set_supply_rail_blocked(self, value=True):
"""
Add the supply rails of given name as a routing target.
"""
debug.info(4, "Blocking supply rail")
for rail_name in self.supply_rail_tracks:
self.rg.set_blocked(self.supply_rail_tracks[rail_name])

View File

@ -0,0 +1,269 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California, Santa Cruz
# All rights reserved.
#
from openram import debug
from openram.base.vector import vector
from openram import OPTS
from .graph import graph
from .graph_shape import graph_shape
from .router import router
class supply_router(router):
"""
This is the supply router that uses the Hanan grid graph method.
"""
def __init__(self, layers, design, bbox=None, pin_type=None):
# `router` is the base router class
router.__init__(self, layers, design, bbox)
# Side supply pin type
# (can be "top", "bottom", "right", "left", and "ring")
self.pin_type = pin_type
# New pins are the side supply pins
self.new_pins = {}
def route(self, vdd_name="vdd", gnd_name="gnd"):
""" Route the given pins in the given order. """
debug.info(1, "Running router for {} and {}...".format(vdd_name, gnd_name))
# Save pin names
self.vdd_name = vdd_name
self.gnd_name = gnd_name
# Prepare gdsMill to find pins and blockages
self.prepare_gds_reader()
# Find pins to be routed
self.find_pins(vdd_name)
self.find_pins(gnd_name)
# Find blockages and vias
self.find_blockages()
self.find_vias()
# Convert blockages and vias if they overlap a pin
self.convert_vias()
self.convert_blockages()
# Add side pins
if self.pin_type in ["top", "bottom", "right", "left"]:
self.add_side_pin(vdd_name)
self.add_side_pin(gnd_name)
elif self.pin_type == "ring":
self.add_ring_pin(vdd_name)
self.add_ring_pin(gnd_name)
else:
debug.warning("Side supply pins aren't created.")
# Add vdd and gnd pins as blockages as well
# NOTE: This is done to make vdd and gnd pins DRC-safe
for pin in self.all_pins:
self.blockages.append(self.inflate_shape(pin))
# Route vdd and gnd
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
def add_side_pin(self, pin_name, side, num_vias=3, num_fake_pins=4):
""" Add supply pin to one side of the layout. """
ll, ur = self.bbox
vertical = side in ["left", "right"]
inner = pin_name == self.gnd_name
# Calculate wires' wideness
wideness = self.track_wire * num_vias + self.track_space * (num_vias - 1)
# Calculate the offset for the inner ring
if inner:
margin = wideness * 2
else:
margin = 0
# Calculate the lower left coordinate
if side == "top":
offset = vector(ll.x + margin, ur.y - wideness - margin)
elif side == "bottom":
offset = vector(ll.x + margin, ll.y + margin)
elif side == "left":
offset = vector(ll.x + margin, ll.y + margin)
elif side == "right":
offset = vector(ur.x - wideness - margin, ll.y + margin)
# Calculate width and height
shape = ur - ll
if vertical:
shape_width = wideness
shape_height = shape.y
else:
shape_width = shape.x
shape_height = wideness
if inner:
if vertical:
shape_height -= margin * 2
else:
shape_width -= margin * 2
# Add this new pin
layer = self.get_layer(int(vertical))
pin = self.design.add_layout_pin(text=pin_name,
layer=layer,
offset=offset,
width=shape_width,
height=shape_height)
# Add fake pins on this new pin evenly
fake_pins = []
if vertical:
space = (shape_height - (2 * wideness) - num_fake_pins * self.track_wire) / (num_fake_pins + 1)
start_offset = vector(offset.x, offset.y + wideness)
else:
space = (shape_width - (2 * wideness) - num_fake_pins * self.track_wire) / (num_fake_pins + 1)
start_offset = vector(offset.x + wideness, offset.y)
for i in range(1, num_fake_pins + 1):
if vertical:
offset = vector(start_offset.x, start_offset.y + i * (space + self.track_wire))
ll = vector(offset.x, offset.y - self.track_wire)
ur = vector(offset.x + wideness, offset.y)
else:
offset = vector(start_offset.x + i * (space + self.track_wire), start_offset.y)
ll = vector(offset.x - self.track_wire, offset.y)
ur = vector(offset.x, offset.y + wideness)
rect = [ll, ur]
fake_pin = graph_shape(name=pin_name,
rect=rect,
layer_name_pp=layer)
fake_pins.append(fake_pin)
return pin, fake_pins
def add_ring_pin(self, pin_name, num_vias=3, num_fake_pins=4):
""" Add the supply ring to the layout. """
# Add side pins
new_pins = []
for side in ["top", "bottom", "right", "left"]:
new_shape, fake_pins = self.add_side_pin(pin_name, side, num_vias, num_fake_pins)
ll, ur = new_shape.rect
rect = [ll, ur]
layer = self.get_layer(side in ["left", "right"])
new_pin = graph_shape(name=pin_name,
rect=rect,
layer_name_pp=layer)
new_pins.append(new_pin)
self.pins[pin_name].update(fake_pins)
self.fake_pins.extend(fake_pins)
# Add vias to the corners
shift = self.track_wire + self.track_space
half_wide = self.track_wire / 2
for i in range(4):
ll, ur = new_pins[i].rect
if i % 2:
top_left = vector(ur.x - (num_vias - 1) * shift - half_wide, ll.y + (num_vias - 1) * shift + half_wide)
else:
top_left = vector(ll.x + half_wide, ur.y - half_wide)
for j in range(num_vias):
for k in range(num_vias):
offset = vector(top_left.x + j * shift, top_left.y - k * shift)
self.design.add_via_center(layers=self.layers,
offset=offset)
# Save side pins for routing
self.new_pins[pin_name] = new_pins
for pin in new_pins:
self.blockages.append(self.inflate_shape(pin))
def get_mst_pairs(self, pins):
"""
Return the pin pairs from the minimum spanning tree in a graph that
connects all pins together.
"""
pin_count = len(pins)
# Create an adjacency matrix that connects all pins
edges = [[0] * pin_count for i in range(pin_count)]
for i in range(pin_count):
for j in range(pin_count):
# Skip if they're the same pin
if i == j:
continue
# Skip if both pins are fake
if pins[i] in self.fake_pins and pins[j] in self.fake_pins:
continue
edges[i][j] = pins[i].distance(pins[j])
pin_connected = [False] * pin_count
pin_connected[0] = True
# Add the minimum cost edge in each iteration (Prim's)
mst_pairs = []
for i in range(pin_count - 1):
min_cost = float("inf")
s = 0
t = 0
# Iterate over already connected pins
for m in range(pin_count):
# Skip if not connected
if not pin_connected[m]:
continue
# Iterate over this pin's neighbors
for n in range(pin_count):
# Skip if already connected or isn't a neighbor
if pin_connected[n] or edges[m][n] == 0:
continue
# Choose this edge if it's better the the current one
if edges[m][n] < min_cost:
min_cost = edges[m][n]
s = m
t = n
pin_connected[t] = True
mst_pairs.append((pins[s], pins[t]))
return mst_pairs
def get_new_pins(self, name):
""" Return the new supply pins added by this router. """
return self.new_pins[name]

View File

@ -1,208 +0,0 @@
# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from datetime import datetime
from scipy.sparse import csr_matrix
from scipy.sparse.csgraph import minimum_spanning_tree
from openram import debug
from openram import print_time
from .router import router
from . import grid_utils
from .signal_grid import signal_grid
class supply_tree_router(router):
"""
A router class to read an obstruction map from a gds and
routes a grid to connect the supply on the two layers.
"""
def __init__(self, layers, design, bbox=None, pin_type=None):
"""
This will route on layers in design. It will get the blockages from
either the gds file name or the design itself (by saving to a gds file).
"""
# Power rail width in minimum wire widths
# This is set to match the signal router so that the grids are aligned
# for prettier routes.
self.route_track_width = 1
# The pin escape router already made the bounding box big enough,
# so we can use the regular bbox here.
if pin_type:
debug.check(pin_type in ["left", "right", "top", "bottom", "single", "ring"],
"Invalid pin type {}".format(pin_type))
self.pin_type = pin_type
router.__init__(self,
layers,
design,
bbox=bbox,
route_track_width=self.route_track_width)
def route(self, vdd_name="vdd", gnd_name="gnd"):
"""
Route the two nets in a single layer.
Setting pin stripe will make a power rail on the left side.
"""
debug.info(1, "Running supply router on {0} and {1}...".format(vdd_name, gnd_name))
self.vdd_name = vdd_name
self.gnd_name = gnd_name
# Clear the pins if we have previously routed
if (hasattr(self, 'rg')):
self.clear_pins()
else:
# Creat a routing grid over the entire area
# FIXME: This could be created only over the routing region,
# but this is simplest for now.
self.create_routing_grid(signal_grid)
start_time = datetime.now()
# Get the pin shapes
self.find_pins_and_blockages([self.vdd_name, self.gnd_name])
print_time("Finding pins and blockages", datetime.now(), start_time, 3)
# Add side pins if enabled
if self.pin_type in ["left", "right", "top", "bottom"]:
self.add_side_supply_pin(self.vdd_name, side=self.pin_type)
self.add_side_supply_pin(self.gnd_name, side=self.pin_type)
elif self.pin_type == "ring":
self.add_ring_supply_pin(self.vdd_name)
self.add_ring_supply_pin(self.gnd_name)
#self.write_debug_gds("initial_tree_router.gds",False)
#breakpoint()
# Route the supply pins to the supply rails
# Route vdd first since we want it to be shorter
start_time = datetime.now()
self.route_pins(vdd_name)
self.route_pins(gnd_name)
print_time("Maze routing supplies", datetime.now(), start_time, 3)
# Did we route everything??
if not self.check_all_routed(vdd_name):
return False
if not self.check_all_routed(gnd_name):
return False
return True
def route_pins(self, pin_name):
"""
This will route each of the remaining pin components to the other pins.
After it is done, the cells are added to the pin blockage list.
"""
remaining_components = sum(not x.is_routed() for x in self.pin_groups[pin_name])
debug.info(1, "Routing {0} with {1} pins.".format(pin_name,
remaining_components))
# Save pin center locations
if False:
debug.info(2, "Creating location file {0}_{1}.csv".format(self.cell.name, pin_name))
f = open("{0}_{1}.csv".format(self.cell.name, pin_name), "w")
pin_size = len(self.pin_groups[pin_name])
for index1, pg1 in enumerate(self.pin_groups[pin_name]):
location = list(pg1.grids)[0]
f.write("{0},{1},{2}\n".format(location.x, location.y, location.z))
f.close()
# Create full graph
debug.info(2, "Creating adjacency matrix")
pin_size = len(self.pin_groups[pin_name])
adj_matrix = [[0] * pin_size for i in range(pin_size)]
for index1, pg1 in enumerate(self.pin_groups[pin_name]):
for index2, pg2 in enumerate(self.pin_groups[pin_name]):
if index1>=index2:
continue
dist = int(grid_utils.distance_set(list(pg1.grids)[0], pg2.grids))
adj_matrix[index1][index2] = dist
# Find MST
debug.info(2, "Finding Minimum Spanning Tree")
X = csr_matrix(adj_matrix)
from scipy.sparse import save_npz
#print("Saving {}.npz".format(self.cell.name))
#save_npz("{}.npz".format(self.cell.name), X)
#exit(1)
Tcsr = minimum_spanning_tree(X)
mst = Tcsr.toarray().astype(int)
connections = []
for x in range(pin_size):
for y in range(pin_size):
if x >= y:
continue
if mst[x][y]>0:
connections.append((x, y))
# Route MST components
level=99
for index, (src, dest) in enumerate(connections):
if not (index % 25):
debug.info(1, "{0} supply segments routed, {1} remaining.".format(index, len(connections) - index))
self.route_signal(pin_name, src, dest)
if False and pin_name == "gnd":
debug.info(level, "\nSRC {}: ".format(src) + str(self.pin_groups[pin_name][src].grids) + str(self.pin_groups[pin_name][src].blockages))
debug.info(level, ("DST {}: ".format(dest) + str(self.pin_groups[pin_name][dest].grids) + str(self.pin_groups[pin_name][dest].blockages)))
self.write_debug_gds("post_{0}_{1}.gds".format(src, dest), False)
#self.write_debug_gds("final_tree_router_{}.gds".format(pin_name), False)
#return
def route_signal(self, pin_name, src_idx, dest_idx):
# First pass, try to route normally
# Second pass, clear prior pin blockages so that you can route over other metal
# of the same supply. Otherwise, this can create a lot of circular routes due to accidental overlaps.
for unblock_routes in [False, True]:
for detour_scale in [2 * pow(2, x) for x in range(5)]:
debug.info(2, "Routing {0} to {1} with scale {2}".format(src_idx, dest_idx, detour_scale))
# Clear everything in the routing grid.
self.rg.reinit()
# This is inefficient since it is non-incremental, but it was
# easier to debug.
self.prepare_blockages(src=(pin_name, src_idx), dest=(pin_name, dest_idx))
if unblock_routes:
msg = "Unblocking supply self blockages to improve access (may cause DRC errors):\n{0}\n{1})"
debug.warning(msg.format(pin_name,
self.pin_groups[pin_name][src_idx].pins))
self.set_blockages(self.path_blockages, False)
# Add the single component of the pin as the source
# which unmarks it as a blockage too
self.set_pin_component_source(pin_name, src_idx)
# Marks all pin components except index as target
# which unmarks it as a blockage too
self.set_pin_component_target(pin_name, dest_idx)
# Actually run the A* router
if self.run_router(detour_scale=detour_scale):
return
#if detour_scale > 2:
# self.write_debug_gds("route_{0}_{1}_d{2}.gds".format(src_idx, dest_idx, detour_scale), False)
self.write_debug_gds("debug_route.gds", True)
def add_io_pin(self, instance, pin_name, new_name=""):
"""
Add a signle input or output pin up to metal 3.
"""
pin = instance.get_pins(pin_name)
if new_name == "":
new_name = pin_name
# Just use the power pin function for now to save code
self.add_power_pin(name=new_name, loc=pin.center(), start_layer=pin.layer)

View File

@ -23,9 +23,6 @@ class sram_1bank_2mux_1rw_1r_spare_cols_test(openram_test):
openram.init_openram(config_file, is_unit_test=True)
from openram import sram_config
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
OPTS.num_rw_ports = 1
OPTS.num_r_ports = 1
OPTS.num_w_ports = 0

View File

@ -23,9 +23,6 @@ class sram_1bank_2mux_1w_1r_spare_cols_test(openram_test):
openram.init_openram(config_file, is_unit_test=True)
from openram import sram_config
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
OPTS.num_rw_ports = 0
OPTS.num_w_ports = 1
OPTS.num_r_ports = 1

View File

@ -31,9 +31,6 @@ class sram_1bank_2mux_global_test(openram_test):
num_spare_rows = 0
num_spare_cols = 0
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
c = sram_config(word_size=8,
num_words=32,
num_banks=1,

View File

@ -30,9 +30,6 @@ class sram_1bank_2mux_wmask_spare_cols_test(openram_test):
num_spare_rows = 0
num_spare_cols = 0
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
c = sram_config(word_size=8,
write_size=4,
num_words=64,

View File

@ -30,9 +30,6 @@ class sram_1bank_2mux_wmask_test(openram_test):
num_spare_rows = 0
num_spare_cols = 0
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
c = sram_config(word_size=8,
write_size=4,
num_words=64,

View File

@ -23,9 +23,6 @@ class sram_1bank_4mux_1rw_1r_test(openram_test):
openram.init_openram(config_file, is_unit_test=True)
from openram import sram_config
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
OPTS.num_rw_ports = 1
OPTS.num_r_ports = 1
OPTS.num_w_ports = 0

View File

@ -30,9 +30,6 @@ class sram_1bank_4mux_test(openram_test):
num_spare_rows = 0
num_spare_cols = 0
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
c = sram_config(word_size=4,
num_words=64,
num_banks=1,

View File

@ -23,9 +23,6 @@ class sram_1bank_8mux_1rw_1r_test(openram_test):
openram.init_openram(config_file, is_unit_test=True)
from openram import sram_config
if OPTS.tech_name == "freepdk45":
OPTS.route_supplies = False
OPTS.num_rw_ports = 1
OPTS.num_r_ports = 1
OPTS.num_w_ports = 0