From 11bb10554571a7ae9637a57df78a31a26371cce7 Mon Sep 17 00:00:00 2001 From: mguthaus Date: Mon, 5 Jun 2017 14:42:56 -0700 Subject: [PATCH] Mark inaccessible off-grid pins as blocked. Improve on-grid pin analysis, but not quite good enough yet. --- compiler/router/cell.py | 4 + compiler/router/grid.py | 61 ++--- compiler/router/router.py | 232 ++++++++++++------ compiler/router/tests/01_no_blockages_test.py | 17 +- compiler/router/tests/02_blockages_test.py | 17 +- .../router/tests/03_same_layer_pins_test.py | 17 +- .../router/tests/04_diff_layer_pins_test.py | 17 +- .../04_diff_layer_pins_test_freepdk45.gds | Bin 4096 -> 4096 bytes compiler/router/tests/05_two_nets_test.py | 23 +- .../tests/05_two_nets_test_freepdk45.gds | Bin 4096 -> 4096 bytes compiler/router/tests/06_pin_location_test.py | 17 +- compiler/router/tests/07_big_test.py | 39 ++- .../router/tests/07_big_test_scn3me_subm.gds | Bin 387072 -> 42886 bytes 13 files changed, 262 insertions(+), 182 deletions(-) diff --git a/compiler/router/cell.py b/compiler/router/cell.py index 0cbb200a..dfdfa0fe 100644 --- a/compiler/router/cell.py +++ b/compiler/router/cell.py @@ -22,6 +22,10 @@ class cell: self.visited=False self.min_cost=-1 self.min_path=None + self.blocked=False + self.source=False + self.target=False + def get_type(self): if self.blocked: diff --git a/compiler/router/grid.py b/compiler/router/grid.py index 0199c5b6..1bf538fa 100644 --- a/compiler/router/grid.py +++ b/compiler/router/grid.py @@ -36,15 +36,24 @@ class grid: # priority queue for the maze routing self.q = Q.PriorityQueue() + + def set_blocked(self,n): + self.add_map(n) + self.map[n].blocked=True + + def set_source(self,n): + self.add_map(n) + self.map[n].source=True + self.source.append(n) + def set_target(self,n): + self.add_map(n) + self.map[n].target=True + self.target.append(n) def reinit(self): """ Reinitialize everything for a new route. """ - self.convert_path_to_blockages() - - self.convert_pins_to_blockages() - self.reset_cells() # clear source and target pins @@ -54,33 +63,34 @@ class grid: # clear the queue while (not self.q.empty()): self.q.get(False) + - - def add_blockage(self,ll,ur,z): + def add_blockage_shape(self,ll,ur,z): debug.info(3,"Adding blockage ll={0} ur={1} z={2}".format(str(ll),str(ur),z)) for x in range(int(ll[0]),int(ur[0])+1): for y in range(int(ll[1]),int(ur[1])+1): n = vector3d(x,y,z) self.add_map(n) - self.map[n].blocked=True - + self.set_blocked(n) + + def add_blockage(self,block_list): + debug.info(3,"Adding blockage list={0}".format(str(block_list))) + for n in block_list: + self.set_blocked(n) + def add_source(self,track_list): debug.info(3,"Adding source list={0}".format(str(track_list))) for n in track_list: self.add_map(n) - self.map[n].source=True - # Can't have a blocked target otherwise it's infeasible - self.map[n].blocked=False - self.source.append(n) + if not self.map[n].blocked: + self.set_source(n) def add_target(self,track_list): debug.info(3,"Adding target list={0}".format(str(track_list))) for n in track_list: self.add_map(n) - self.map[n].target=True - # Can't have a blocked target otherwise it's infeasible - self.map[n].blocked=False - self.target.append(n) + if not self.map[n].blocked: + self.set_target(n) def reset_cells(self): """ @@ -89,25 +99,6 @@ class grid: for p in self.map.values(): p.reset() - def convert_pins_to_blockages(self): - """ - Convert all the pins to blockages and reset the pin sets. - """ - for p in self.map.values(): - if (p.source or p.target): - p.blocked=True - p.source=False - p.target=False - - def convert_path_to_blockages(self): - """ - Convert the routed path to blockages and reset the path. - """ - for p in self.map.values(): - if (p.path): - p.path=False - p.blocked=True - def add_path(self,path): """ diff --git a/compiler/router/router.py b/compiler/router/router.py index a68b51fc..bd312076 100644 --- a/compiler/router/router.py +++ b/compiler/router/router.py @@ -28,13 +28,16 @@ class router: self.source_pin_zindex = None self.target_pin_shapes = [] self.target_pin_zindex = None + # the list of all blockage shapes + self.blockages = [] + # all thepaths we've routed so far (to supplement the blockages) + self.paths = [] # The boundary will determine the limits to the size of the routing grid self.boundary = self.layout.measureBoundary(self.top_name) self.ll = vector(self.boundary[0]) self.ur = vector(self.boundary[1]) - def set_top(self,top_name): """ If we want to route something besides the top-level cell.""" self.top_name = top_name @@ -113,7 +116,7 @@ class router: if they are not actually a blockage. """ for layer in self.layers: - self.write_obstacle(self.top_name) + self.get_blockages(self.top_name) def clear_pins(self): @@ -123,19 +126,24 @@ class router: Convert the routed path to blockages. Keep the other blockages unchanged. """ + self.source_pin = None self.source_pin_shapes = [] self.source_pin_zindex = None + self.target_pin = None self.target_pin_shapes = [] self.target_pin_zindex = None + # DO NOT clear the blockages as these don't change self.rg.reinit() - def route(self, layers, src, dest, cost_bound_scale=1): + def route(self, cell, layers, src, dest, cost_bound_scale=1): """ 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. """ + self.cell = cell + # Clear the pins if we have previously routed if (hasattr(self,'rg')): self.clear_pins() @@ -146,97 +154,137 @@ class router: # FIXME: This could be created only over the routing region, # but this is simplest for now. self.create_routing_grid() - # This will write all shapes as blockages, but setting pins will - # clear the blockage attribute + # This will get all shapes as blockages self.find_blockages() - self.add_source(src) + # Get the pin shapes + self.get_source(src) + self.get_target(dest) + + # Now add the blockages (all shapes except the src/tgt pins) + self.add_blockages() + # Add blockages from previous paths + self.add_path_blockages() - self.add_target(dest) + # Now add the src/tgt if they are not blocked by other shapes + self.add_source() + self.add_target() + # returns the path in tracks - (self.path,cost) = self.rg.route(cost_bound_scale) - if self.path!=None: + (path,cost) = self.rg.route(cost_bound_scale) + if path: debug.info(1,"Found path: cost={0} ".format(cost)) - debug.info(2,str(self.path)) - self.add_path(self.path) + debug.info(2,str(path)) + self.add_route(path) return True + else: + self.write_debug_gds() return False - def add_router_info(self,cell): + def write_debug_gds(self,): + """ + Write out a GDS file with the routing grid and search information annotated on it. + """ + self.add_router_info() + debug.error("Writing debug_route.gds from {0} to {1}".format(self.source_pin,self.target_pin)) + self.cell.gds_write("debug_route.gds") + + def add_router_info(self): """ Write the routing grid and router cost, blockage, pins on the boundary layer for debugging purposes. This can only be called once or the labels will overlap. """ + debug.info(0,"Adding router info for {0} to {1}".format(self.source_pin,self.target_pin)) grid_keys=self.rg.map.keys() partial_track=vector(0,self.track_width/6.0) for g in grid_keys: shape = self.convert_full_track_to_shape(g) - cell.add_rect(layer="boundary", - offset=shape[0], - width=shape[1].x-shape[0].x, - height=shape[1].y-shape[0].y) - + self.cell.add_rect(layer="boundary", + offset=shape[0], + width=shape[1].x-shape[0].x, + height=shape[1].y-shape[0].y) + # These rae the on grid pins + rect = self.convert_track_to_pin(g) + self.cell.add_rect(layer="boundary", + offset=rect[0], + width=rect[1].x-rect[0].x, + height=rect[1].y-rect[0].y) + t=self.rg.map[g].get_type() - if t == None: continue # midpoint offset off=vector((shape[1].x+shape[0].x)/2, (shape[1].y+shape[0].y)/2) if g[2]==1: # Upper layer is upper right label - off+=partial_track + type_off=off+partial_track else: # Lower layer is lower left label - off-=partial_track - cell.add_label(text=str(t), - layer="text", - offset=off) + type_off=off-partial_track + if t!=None: + self.cell.add_label(text=str(t), + layer="text", + offset=type_off) + self.cell.add_label(text="{0},{1}".format(g[0],g[1]), + layer="text", + offset=shape[0]) - - def add_route(self,cell): + def add_route(self,path): """ Add the current wire route to the given design instance. """ + debug.info(3,"Set path: " + str(path)) - # For debugging... - #self.add_router_info(cell) + # Keep track of path for future blockages + self.paths.append(path) + # This is marked for debug + self.rg.add_path(path) + + # For debugging... if the path failed to route. + if False or path==None: + self.write_debug_gds() + + if 'Xout_4_1' in [self.source_pin, self.target_pin]: + self.write_debug_gds() + + # First, simplify the path for #debug.info(1,str(self.path)) - contracted_path = self.contract_path(self.path) + contracted_path = self.contract_path(path) debug.info(1,str(contracted_path)) # Make sure there's a pin enclosure on the source and dest add_src_via = contracted_path[0].z!=self.source_pin_zindex - self.add_grid_pin(cell,contracted_path[0],add_src_via) + self.add_grid_pin(contracted_path[0],add_src_via) add_tgt_via = contracted_path[-1].z!=self.target_pin_zindex - self.add_grid_pin(cell,contracted_path[-1],add_tgt_via) + self.add_grid_pin(contracted_path[-1],add_tgt_via) # convert the path back to absolute units from tracks abs_path = map(self.convert_point_to_units,contracted_path) debug.info(1,str(abs_path)) - cell.add_route(self.layers,abs_path) + self.cell.add_route(self.layers,abs_path) - def add_grid_pin(self,cell,point,add_via=False): + def add_grid_pin(self,point,add_via=False): """ Create a rectangle at the grid 3D point that is 1/2 DRC smaller than the routing grid on all sides. """ pin = self.convert_track_to_pin(point) - cell.add_rect(layer=self.layers[2*point.z], - offset=pin[0], - width=pin[1].x-pin[0].x, - height=pin[1].y-pin[0].y) + self.cell.add_rect(layer=self.layers[2*point.z], + offset=pin[0], + width=pin[1].x-pin[0].x, + height=pin[1].y-pin[0].y) if add_via: # offset this by 1/2 the via size c=contact(self.layers, (1, 1)) via_offset = vector(-0.5*c.width,-0.5*c.height) - cell.add_via(self.layers,vector(point[0],point[1])+via_offset) + self.cell.add_via(self.layers,vector(point[0],point[1])+via_offset) def create_steiner_routes(self,pins): @@ -316,56 +364,87 @@ class router: newpath.append(path[-1]) return newpath - def add_path(self,path): - """ - Mark the path in the routing grid. - """ - debug.info(3,"Set path: " + str(path)) - self.rg.add_path(path) - def add_source(self,pin): + def add_path_blockages(self): + """ + Go through all of the past paths and add them as blockages. + This is so we don't have to write/reload the GDS. + """ + for path in self.paths: + for grid in path: + self.rg.set_blocked(grid) + + + def get_source(self,pin): + """ + Gets the source pin shapes only. Doesn't add to grid. + """ + self.source_pin = pin + (self.source_pin_layer,self.source_pin_shapes) = self.find_pin(pin) + zindex = 0 if self.source_pin_layer==self.horiz_layer_number else 1 + self.source_pin_zindex = zindex + + def add_source(self): """ Mark the grids that are in the pin rectangle ranges to have the source property. pin can be a location or a label. """ - (pin_layer,self.source_pin_shapes) = self.find_pin(pin) - - zindex = 0 if pin_layer==self.horiz_layer_number else 1 - self.source_pin_zindex = zindex found_pin = False for shape in self.source_pin_shapes: - pin_in_tracks=self.convert_pin_to_tracks(shape,zindex,pin) + (pin_in_tracks,blockage_in_tracks)=self.convert_pin_to_tracks(shape,self.source_pin_zindex,self.source_pin) if (len(pin_in_tracks)>0): found_pin=True - debug.info(1,"Set source: " + str(pin) + " " + str(pin_in_tracks) + " z=" + str(zindex)) + debug.info(1,"Set source: " + str(self.source_pin) + " " + str(pin_in_tracks) + " z=" + str(self.source_pin_zindex)) self.rg.add_source(pin_in_tracks) - + self.rg.add_blockage(blockage_in_tracks) + + if not found_pin: + self.write_debug_gds() debug.check(found_pin,"Unable to find source pin on grid.") - def add_target(self,pin): + def get_target(self,pin): + """ + Gets the target pin shapes only. Doesn't add to grid. + """ + self.target_pin = pin + (self.target_pin_layer,self.target_pin_shapes) = self.find_pin(pin) + zindex = 0 if self.target_pin_layer==self.horiz_layer_number else 1 + self.target_pin_zindex = zindex + + def add_target(self): """ Mark the grids that are in the pin rectangle ranges to have the target property. pin can be a location or a label. """ - (pin_layer,self.target_pin_shapes) = self.find_pin(pin) - - zindex = 0 if pin_layer==self.horiz_layer_number else 1 - self.target_pin_zindex = zindex - found_pin=False for shape in self.target_pin_shapes: - pin_in_tracks=self.convert_pin_to_tracks(shape,zindex,pin) + (pin_in_tracks,blockage_in_tracks)=self.convert_pin_to_tracks(shape,self.target_pin_zindex,self.target_pin) if (len(pin_in_tracks)>0): found_pin=True - debug.info(1,"Set target: " + str(pin) + " " + str(pin_in_tracks) + " z=" + str(zindex)) + debug.info(1,"Set target: " + str(self.target_pin) + " " + str(pin_in_tracks) + " z=" + str(self.target_pin_zindex)) self.rg.add_target(pin_in_tracks) + self.rg.add_blockage(blockage_in_tracks) - debug.check(found_pin,"Unable to find source pin on grid.") + if not found_pin: + self.write_debug_gds() + debug.check(found_pin,"Unable to find target pin on grid.") + def add_blockages(self): + """ Add the blockages except the pin shapes """ + for blockage in self.blockages: + (shape,zlayer) = blockage + # Skip source pin shapes + if zlayer==self.source_pin_zindex and shape in self.source_pin_shapes: + continue + # Skip target pin shapes + if zlayer==self.target_pin_zindex and shape in self.target_pin_shapes: + continue + [ll,ur]=self.convert_blockage_to_tracks(shape) + self.rg.add_blockage_shape(ll,ur,zlayer) - def write_obstacle(self, sref, mirr = 1, angle = math.radians(float(0)), xyShift = (0, 0)): + def get_blockages(self, sref, mirr = 1, angle = math.radians(float(0)), xyShift = (0, 0)): """ - Recursive write boundaries as blockages to the routing grid. + Recursive find boundaries as blockages to the routing grid. Recurses for each Structure in GDS. """ for boundary in self.layout.structures[sref].boundaries: @@ -376,8 +455,7 @@ class router: # only consider the two layers that we are routing on if boundary.drawingLayer in [self.vert_layer_number,self.horiz_layer_number]: zlayer = 0 if boundary.drawingLayer==self.horiz_layer_number else 1 - [ll,ur]=self.convert_blockage_to_tracks(shape) - self.rg.add_blockage(ll,ur,zlayer) + self.blockages.append((shape,zlayer)) # recurse given the mirror, angle, etc. @@ -395,7 +473,7 @@ class router: newY = (x)*math.sin(angle) + mirr*(y)*math.cos(angle) + xyShift[1] sxyShift = (newX, newY) - self.write_obstacle(cur_sref.sName, sMirr, sAngle, sxyShift) + self.get_blockages(cur_sref.sName, sMirr, sAngle, sxyShift) def convert_point_to_units(self,p): """ @@ -426,6 +504,7 @@ class router: """ Convert a rectangular pin shape into a list of track locations,layers. If no on-grid pins are found, it searches for the nearest off-grid pin(s). + If a pin has insufficent overlap, it returns the blockage list to avoid it. """ [ll,ur] = shape ll = snap_to_grid(ll) @@ -444,24 +523,33 @@ class router: width = self.vert_layer_width track_list = [] - # include +- 1 track for neighors - for x in range(int(ll[0])-1,int(ur[0])+1): - for y in range(int(ll[1])-1,int(ur[1])+1): + block_list = [] + # include +- 1 so when a shape is less than one grid + for x in range(ll[0]-1,ur[0]+1): + for y in range(ll[1]-1,ur[1]+1): #debug.info(1,"Converting [ {0} , {1} ]".format(x,y)) # get the rectangular pin at a track location + # if dimension of overlap is greater than min width in any dimension, + # it will be an on-grid pin rect = self.convert_track_to_pin(vector3d(x,y,zindex)) - #debug.info(1,"Rect {0}".format(rect)) - # find the rectangular overlap shape (if any) - # if dimension of overlap is greater than min width in any dimension, add it max_overlap=max(self.compute_overlap(shape,rect)) + + # however, if there is not enough overlap, then if there is any overlap at all, + # we need to block it to prevent routes coming in on that grid + full_rect = self.convert_full_track_to_shape(vector3d(x,y,zindex)) + full_overlap=max(self.compute_overlap(shape,full_rect)) + + #debug.info(1,"Check overlap: {0} {1} max={2}".format(shape,rect,max_overlap)) if max_overlap >= width: track_list.append(vector3d(x,y,zindex)) + elif full_overlap>0: + block_list.append(vector3d(x,y,zindex)) else: debug.info(1,"No overlap: {0} {1} max={2}".format(shape,rect,max_overlap)) #debug.warning("Off-grid pin for {0}.".format(str(pin))) #debug.info(1,"Converted [ {0} , {1} ]".format(ll,ur)) - return track_list + return (track_list,block_list) def compute_overlap(self,r1,r2): """ Calculate the rectangular overlap of two rectangles. """ diff --git a/compiler/router/tests/01_no_blockages_test.py b/compiler/router/tests/01_no_blockages_test.py index 978a5262..99e5b601 100644 --- a/compiler/router/tests/01_no_blockages_test.py +++ b/compiler/router/tests/01_no_blockages_test.py @@ -36,29 +36,26 @@ class no_blockages_test(unittest.TestCase): design.hierarchy_layout.layout.__init__(self, name) design.hierarchy_spice.spice.__init__(self, name) - class routing(design.design): + class routing(design.design,unittest.TestCase): """ A generic GDS design that we can route on. """ - def __init__(self, name, gdsname): + def __init__(self, name): design.design.__init__(self, name) debug.info(2, "Create {0} object".format(name)) - cell = gdscell(gdsname) - self.add_inst(name=gdsname, + cell = gdscell(name) + self.add_inst(name=name, mod=cell, offset=[0,0]) self.connect_inst([]) - self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),gdsname) + self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),name) r=router.router(self.gdsname) layer_stack =("metal1","via1","metal2") - if r.route(layer_stack,src="A",dest="B"): - r.add_route(self) - else: - self.assertTrue(False) + self.assertTrue(r.route(self,layer_stack,src="A",dest="B")) - r = routing("test1", "01_no_blockages_test_{0}".format(OPTS.tech_name)) + r = routing("01_no_blockages_test_{0}".format(OPTS.tech_name)) self.local_check(r) # fails if there are any DRC errors on any cells diff --git a/compiler/router/tests/02_blockages_test.py b/compiler/router/tests/02_blockages_test.py index 36f4dc3f..2afefa4a 100644 --- a/compiler/router/tests/02_blockages_test.py +++ b/compiler/router/tests/02_blockages_test.py @@ -36,29 +36,26 @@ class blockages_test(unittest.TestCase): design.hierarchy_layout.layout.__init__(self, name) design.hierarchy_spice.spice.__init__(self, name) - class routing(design.design): + class routing(design.design,unittest.TestCase): """ A generic GDS design that we can route on. """ - def __init__(self, name, gdsname): + def __init__(self, name): design.design.__init__(self, name) debug.info(2, "Create {0} object".format(name)) - cell = gdscell(gdsname) - self.add_inst(name=gdsname, + cell = gdscell(name) + self.add_inst(name=name, mod=cell, offset=[0,0]) self.connect_inst([]) - self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),gdsname) + self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),name) r=router.router(self.gdsname) layer_stack =("metal1","via1","metal2") - if r.route(layer_stack,src="A",dest="B"): - r.add_route(self) - else: - self.assertTrue(False) + self.assertTrue(r.route(self,layer_stack,src="A",dest="B")) - r = routing("test1", "02_blockages_test_{0}".format(OPTS.tech_name)) + r = routing("02_blockages_test_{0}".format(OPTS.tech_name)) self.local_check(r) # fails if there are any DRC errors on any cells diff --git a/compiler/router/tests/03_same_layer_pins_test.py b/compiler/router/tests/03_same_layer_pins_test.py index 2bf51262..dfd849c9 100644 --- a/compiler/router/tests/03_same_layer_pins_test.py +++ b/compiler/router/tests/03_same_layer_pins_test.py @@ -35,29 +35,26 @@ class same_layer_pins_test(unittest.TestCase): design.hierarchy_layout.layout.__init__(self, name) design.hierarchy_spice.spice.__init__(self, name) - class routing(design.design): + class routing(design.design,unittest.TestCase): """ A generic GDS design that we can route on. """ - def __init__(self, name, gdsname): + def __init__(self, name): design.design.__init__(self, name) debug.info(2, "Create {0} object".format(name)) - cell = gdscell(gdsname) - self.add_inst(name=gdsname, + cell = gdscell(name) + self.add_inst(name=name, mod=cell, offset=[0,0]) self.connect_inst([]) - self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),gdsname) + self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),name) r=router.router(self.gdsname) layer_stack =("metal1","via1","metal2") - if r.route(layer_stack,src="A",dest="B"): - r.add_route(self) - else: - self.assertTrue(False) + self.assertTrue(r.route(self,layer_stack,src="A",dest="B")) - r = routing("test1", "03_same_layer_pins_test_{0}".format(OPTS.tech_name)) + r = routing("03_same_layer_pins_test_{0}".format(OPTS.tech_name)) self.local_check(r) diff --git a/compiler/router/tests/04_diff_layer_pins_test.py b/compiler/router/tests/04_diff_layer_pins_test.py index 6fc8da15..e0a54875 100644 --- a/compiler/router/tests/04_diff_layer_pins_test.py +++ b/compiler/router/tests/04_diff_layer_pins_test.py @@ -37,29 +37,26 @@ class diff_layer_pins_test(unittest.TestCase): design.hierarchy_layout.layout.__init__(self, name) design.hierarchy_spice.spice.__init__(self, name) - class routing(design.design): + class routing(design.design,unittest.TestCase): """ A generic GDS design that we can route on. """ - def __init__(self, name, gdsname): + def __init__(self, name): design.design.__init__(self, name) debug.info(2, "Create {0} object".format(name)) - cell = gdscell(gdsname) - self.add_inst(name=gdsname, + cell = gdscell(name) + self.add_inst(name=name, mod=cell, offset=[0,0]) self.connect_inst([]) - self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),gdsname) + self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),name) r=router.router(self.gdsname) layer_stack =("metal1","via1","metal2") - if r.route(layer_stack,src="A",dest="B"): - r.add_route(self) - else: - self.assertTrue(False) + self.assertTrue(r.route(self,layer_stack,src="A",dest="B")) - r = routing("test1", "04_diff_layer_pins_test_{0}".format(OPTS.tech_name)) + r = routing("04_diff_layer_pins_test_{0}".format(OPTS.tech_name)) self.local_check(r) # fails if there are any DRC errors on any cells diff --git a/compiler/router/tests/04_diff_layer_pins_test_freepdk45.gds b/compiler/router/tests/04_diff_layer_pins_test_freepdk45.gds index 57928eacca32cffd84127658c9d7c13a36d48514..9ddaabded13d5fd98c4e2ef6400981395dcdfcb0 100644 GIT binary patch delta 343 zcmZorXi%6S>B`2y${@<1z@X2-!^D=7nwD6aQ^LR?!pv%?aAfw<$HCt$m14v%_^x@! zAj8VUAi==Rz|X(~#HI{_3^2V06XRYA!#NCGY;5J3MXB*7lciW3CKicI{=m$l%DRt% zf%O>!1KSxO-hrTD;y{{3aALmbUu;Ve&dg zr^yA3#*^=}@T&dv`2YW3+W-F``hV*G|G%UVG)(+o+Qc;ylV7npOkTz2G&zUOZn74T ze#2@vc@?X}~GTm delta 349 zcmZorXi%6S>B_<&$RNbP&tS#C!^D=7nwD6aQ^LR?!pv%?aAfw<$HCt$m14v%_^$cE zAj8VUAi=;5RLcXzra*mgy%XbJ3d1=JTx@LRnMJAbCKC%pChM@6PORjf{D#?S@;YX_ z$vMmplXZaj4U@yea67R#s{U(0NFc$7)Hay`50IP8744|Np-S3~Z8YjuTf)PJY9tH#v$$Wb!&Thsk+tc9V60^cz;7RY07>>NHu8 z)o$_|7NApD>@@!W|G5aLISoSpSNZ?{SIPhXFd8QQFKu!bhutI&y~$}DW|MV*_!GOu iB`2y${@<1${@$U!^D=7nwD6aQ^LR?!pv%?aAfw<$HCt$m14v%_^#Q+ zAj8VUAi==Rz|X(~#HI{_3~;>@<6d&YIgQnKF6rctoW9JfnnPJ|NmDrFfgt} U(Ep&~|K3iX#%QqFiFF?r0DlB3Pyhe` delta 143 zcmZorXi%6S>B`C=&mh8}$Y9RE!^D=7nwD6aQ^LR?!pv%?aAfw<$HCt$m14v%_^#Q* zAj8VUAi==Rz|X(~#HI{_3~;>@<6d&YIgY22F8^L U`X5yMU(4iaj0T&XSod)O09}44IRF3v diff --git a/compiler/router/tests/06_pin_location_test.py b/compiler/router/tests/06_pin_location_test.py index c44516b7..10cb854b 100644 --- a/compiler/router/tests/06_pin_location_test.py +++ b/compiler/router/tests/06_pin_location_test.py @@ -36,35 +36,32 @@ class pin_location_test(unittest.TestCase): design.hierarchy_layout.layout.__init__(self, name) design.hierarchy_spice.spice.__init__(self, name) - class routing(design.design): + class routing(design.design,unittest.TestCase): """ A generic GDS design that we can route on. """ - def __init__(self, name, gdsname): + def __init__(self, name): design.design.__init__(self, name) debug.info(2, "Create {0} object".format(name)) - cell = gdscell(gdsname) - self.add_inst(name=gdsname, + cell = gdscell(name) + self.add_inst(name=name, mod=cell, offset=[0,0]) self.connect_inst([]) - self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),gdsname) + self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),name) r=router.router(self.gdsname) layer_stack =("metal1","via1","metal2") # these are user coordinates and layers src_pin = [[0.52, 4.099],11] tgt_pin = [[3.533, 1.087],11] #r.route(layer_stack,src="A",dest="B") - if r.route(layer_stack,src=src_pin,dest=tgt_pin): - r.add_route(self) - else: - debug.error("Unable to route") + self.assertTrue(r.route(self,layer_stack,src=src_pin,dest=tgt_pin)) # This only works for freepdk45 since the coordinates are hard coded if OPTS.tech_name == "freepdk45": - r = routing("test1", "06_pin_location_test_{0}".format(OPTS.tech_name)) + r = routing("06_pin_location_test_{0}".format(OPTS.tech_name)) self.local_check(r) else: debug.warning("This test does not support technology {0}".format(OPTS.tech_name)) diff --git a/compiler/router/tests/07_big_test.py b/compiler/router/tests/07_big_test.py index 46f5ffa9..4e66c07e 100644 --- a/compiler/router/tests/07_big_test.py +++ b/compiler/router/tests/07_big_test.py @@ -36,31 +36,50 @@ class big_test(unittest.TestCase): design.hierarchy_layout.layout.__init__(self, name) design.hierarchy_spice.spice.__init__(self, name) - class routing(design.design): + class routing(design.design,unittest.TestCase): """ A generic GDS design that we can route on. """ - def __init__(self, name, gdsname): + def __init__(self, name): design.design.__init__(self, name) debug.info(2, "Create {0} object".format(name)) - cell = gdscell(gdsname) - self.add_inst(name=gdsname, + cell = gdscell(name) + self.add_inst(name=name, mod=cell, offset=[0,0]) self.connect_inst([]) - self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),gdsname) + self.gdsname = "{0}/{1}.gds".format(os.path.dirname(os.path.realpath(__file__)),name) r=router.router(self.gdsname) layer_stack =("metal3","via2","metal2") - # first pin doesn't overlap a rectangle - #r.route(layer_stack,src="a_2_7",dest="B") - r.route(layer_stack,src="A",dest="B") - r.add_route(self) + connections=[('out_0_2', 'a_0_0'), + ('out_0_3', 'b_0_0'), + ('out_0_0', 'a_0_1'), + ('out_1_2', 'a_1_0'), + ('out_1_3', 'b_1_0'), + ('out_1_0', 'a_1_1'), + ('out_2_1', 'a_2_0'), + ('out_2_2', 'b_2_0'), + ('out_3_1', 'a_3_0'), + ('out_3_2', 'b_3_0'), + ('out_4_6', 'a_4_0'), + ('out_4_7', 'b_4_0'), + ('out_4_8', 'a_4_2'), + ('out_4_9', 'b_4_2'), + ('out_4_10', 'a_4_4'), + ('out_4_11', 'b_4_4'), + ('out_4_0', 'a_4_1'), + ('out_4_2', 'b_4_1'), + ('out_4_4', 'a_4_5'), + ('out_4_1', 'a_4_3'), + ('out_4_5', 'b_4_3')] + for (src,tgt) in connections: + self.assertTrue(r.route(self,layer_stack,src=src,dest=tgt)) # This test only runs on scn3me_subm tech if OPTS.tech_name=="scn3me_subm": - r = routing("test1", "07_big_test_{0}".format(OPTS.tech_name)) + r = routing("07_big_test_{0}".format(OPTS.tech_name)) self.local_check(r) else: debug.warning("This test does not support technology {0}".format(OPTS.tech_name)) diff --git a/compiler/router/tests/07_big_test_scn3me_subm.gds b/compiler/router/tests/07_big_test_scn3me_subm.gds index 3c8e12a84b6833c7500a70e738636f0e4f025dc1..e4d82803f98fbd147209ce28cfa1b6fd3154ef0a 100644 GIT binary patch literal 42886 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 G6YhW6?*tV9 literal 387072 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