mirror of https://github.com/VLSIDA/OpenRAM.git
Routing multilayer, around blockages.
This commit is contained in:
parent
b947989970
commit
784bad2e99
|
|
@ -5,28 +5,26 @@ class cell:
|
||||||
A single cell that can be occupied in a given layer, blocked,
|
A single cell that can be occupied in a given layer, blocked,
|
||||||
visited, etc.
|
visited, etc.
|
||||||
"""
|
"""
|
||||||
scale=1
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.visited = 0
|
self.path = False
|
||||||
|
|
||||||
self.blocked = False
|
self.blocked = False
|
||||||
|
|
||||||
self.is_source = False
|
self.source = False
|
||||||
self.is_target = False
|
self.target = False
|
||||||
|
|
||||||
|
|
||||||
def get_color(self):
|
def get_color(self):
|
||||||
|
|
||||||
# Blues are horizontal
|
# Blues are horizontal
|
||||||
if self.blocked:
|
if self.blocked:
|
||||||
return ImageColor.getrgb("Blue")
|
return ImageColor.getrgb("Green")
|
||||||
# Reds are source/sink
|
# Reds are source/sink
|
||||||
if self.is_source or self.is_target:
|
if self.source or self.target:
|
||||||
return ImageColor.getrgb("Red")
|
return ImageColor.getrgb("Red")
|
||||||
|
|
||||||
if self.visited>0:
|
if self.path:
|
||||||
return [255-min(int(self.visited/cell.scale * 255),255)] * 3
|
return ImageColor.getrgb("Blue")
|
||||||
|
|
||||||
return [255,255,255]
|
return [255,255,255]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import debug
|
import debug
|
||||||
|
from vector3d import vector3d
|
||||||
|
|
||||||
from cell import cell
|
from cell import cell
|
||||||
|
try:
|
||||||
|
import Queue as Q # ver. < 3.0
|
||||||
|
except ImportError:
|
||||||
|
import queue as Q
|
||||||
|
|
||||||
|
|
||||||
class grid:
|
class grid:
|
||||||
"""A two layer routing map. Each cell can be blocked in the vertical
|
"""A two layer routing map. Each cell can be blocked in the vertical
|
||||||
|
|
@ -14,11 +20,19 @@ class grid:
|
||||||
""" Create a routing map of width x height cells and 2 in the z-axis. """
|
""" Create a routing map of width x height cells and 2 in the z-axis. """
|
||||||
self.width=width
|
self.width=width
|
||||||
self.height=height
|
self.height=height
|
||||||
|
self.source = []
|
||||||
|
self.target = []
|
||||||
|
self.blocked = []
|
||||||
self.map={}
|
self.map={}
|
||||||
for x in range(width):
|
for x in range(width):
|
||||||
for y in range(height):
|
for y in range(height):
|
||||||
for z in range(2):
|
for z in range(2):
|
||||||
self.map[x,y,z]=cell()
|
self.map[vector3d(x,y,z)]=cell()
|
||||||
|
|
||||||
|
# priority queue for the maze routing
|
||||||
|
self.q = Q.PriorityQueue()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def view(self,):
|
def view(self,):
|
||||||
"""
|
"""
|
||||||
|
|
@ -35,8 +49,8 @@ class grid:
|
||||||
cell.scale = 1.5 * (self.width+self.height)
|
cell.scale = 1.5 * (self.width+self.height)
|
||||||
for x in range(self.width):
|
for x in range(self.width):
|
||||||
for y in range(self.height):
|
for y in range(self.height):
|
||||||
h_map[x,y] = self.map[x,y,0].get_color()
|
h_map[x,y] = self.map[vector3d(x,y,0)].get_color()
|
||||||
v_map[x,y] = self.map[x,y,1].get_color()
|
v_map[x,y] = self.map[vector3d(x,y,1)].get_color()
|
||||||
|
|
||||||
v_img = Image.fromarray(v_map, 'RGB').rotate(90)
|
v_img = Image.fromarray(v_map, 'RGB').rotate(90)
|
||||||
mid_img = Image.fromarray(mid_map, 'RGB').rotate(90)
|
mid_img = Image.fromarray(mid_map, 'RGB').rotate(90)
|
||||||
|
|
@ -52,7 +66,8 @@ class grid:
|
||||||
def set_property(self,ll,ur,z,name,value=True):
|
def set_property(self,ll,ur,z,name,value=True):
|
||||||
for x in range(int(ll[0]),int(ur[0])):
|
for x in range(int(ll[0]),int(ur[0])):
|
||||||
for y in range(int(ll[1]),int(ur[1])):
|
for y in range(int(ll[1]),int(ur[1])):
|
||||||
setattr (self.map[x,y,z], name, True)
|
setattr (self.map[vector3d(x,y,z)], name, True)
|
||||||
|
getattr (self, name).append(vector3d(x,y,z))
|
||||||
|
|
||||||
def add_blockage(self,ll,ur,z):
|
def add_blockage(self,ll,ur,z):
|
||||||
debug.info(1,"Adding blockage ll={0} ur={1} z={2}".format(str(ll),str(ur),z))
|
debug.info(1,"Adding blockage ll={0} ur={1} z={2}".format(str(ll),str(ur),z))
|
||||||
|
|
@ -60,9 +75,114 @@ class grid:
|
||||||
|
|
||||||
def set_source(self,ll,ur,z):
|
def set_source(self,ll,ur,z):
|
||||||
debug.info(1,"Adding source ll={0} ur={1} z={2}".format(str(ll),str(ur),z))
|
debug.info(1,"Adding source ll={0} ur={1} z={2}".format(str(ll),str(ur),z))
|
||||||
self.set_property(ll,ur,z,"is_source")
|
self.set_property(ll,ur,z,"source")
|
||||||
|
|
||||||
|
|
||||||
def set_target(self,ll,ur,z):
|
def set_target(self,ll,ur,z):
|
||||||
debug.info(1,"Adding target ll={0} ur={1} z={2}".format(str(ll),str(ur),z))
|
debug.info(1,"Adding target ll={0} ur={1} z={2}".format(str(ll),str(ur),z))
|
||||||
self.set_property(ll,ur,z,"is_target")
|
self.set_property(ll,ur,z,"target")
|
||||||
|
|
||||||
|
def set_path(self,path):
|
||||||
|
"""
|
||||||
|
Mark the path in the routing grid for visualization
|
||||||
|
"""
|
||||||
|
for p in path:
|
||||||
|
self.map[p].path=True
|
||||||
|
|
||||||
|
def route(self):
|
||||||
|
"""
|
||||||
|
This does the A* maze routing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Make sure the queue is empty if we run another route
|
||||||
|
while not self.q.empty():
|
||||||
|
self.q.get()
|
||||||
|
|
||||||
|
# Put the source items into the queue
|
||||||
|
self.init_queue()
|
||||||
|
cheapest_path = None
|
||||||
|
cheapest_cost = None
|
||||||
|
|
||||||
|
# Keep expanding and adding to the priority queue until we are done
|
||||||
|
while not self.q.empty():
|
||||||
|
(cost,path) = self.q.get()
|
||||||
|
debug.info(2,"Expanding: cost=" + str(cost) + " " + str(path))
|
||||||
|
|
||||||
|
# expand the last element
|
||||||
|
neighbors = self.expand_dirs(path[-1])
|
||||||
|
debug.info(2,"Neighbors: " + str(neighbors))
|
||||||
|
|
||||||
|
for n in neighbors:
|
||||||
|
newpath = path + [n]
|
||||||
|
# check if we hit the target and are done
|
||||||
|
if self.is_target(n):
|
||||||
|
return newpath
|
||||||
|
else:
|
||||||
|
# path cost + predicted cost
|
||||||
|
cost = len(newpath) + self.cost_to_target(n)
|
||||||
|
self.q.put((cost,newpath))
|
||||||
|
|
||||||
|
debug.error("Unable to route path. Expand area?",-1)
|
||||||
|
|
||||||
|
def is_target(self,point):
|
||||||
|
"""
|
||||||
|
Point is in the target set, so we are done.
|
||||||
|
"""
|
||||||
|
return point in self.target
|
||||||
|
|
||||||
|
def expand_dirs(self,point):
|
||||||
|
"""
|
||||||
|
Expand each of the four cardinal directions plus up or down
|
||||||
|
but not expanding to blocked cells. Always follow horizontal/vertical
|
||||||
|
routing layer requirements. Extend in the future if not routable?
|
||||||
|
"""
|
||||||
|
neighbors = []
|
||||||
|
# check z layer for enforced direction routing
|
||||||
|
if point.z==0:
|
||||||
|
east = point + vector3d(1,0,0)
|
||||||
|
west= point + vector3d(-11,0,0)
|
||||||
|
if east.x<self.width and not self.map[east].blocked:
|
||||||
|
neighbors.append(east)
|
||||||
|
if west.x>=0 and not self.map[west].blocked:
|
||||||
|
neighbors.append(west)
|
||||||
|
up = point + vector3d(0,0,1)
|
||||||
|
if not self.map[up].blocked:
|
||||||
|
neighbors.append(up)
|
||||||
|
elif point.z==1:
|
||||||
|
north = point + vector3d(0,1,0)
|
||||||
|
south = point + vector3d(0,-1,0)
|
||||||
|
if north.y<self.height and not self.map[north].blocked:
|
||||||
|
neighbors.append(north)
|
||||||
|
if south.y>=0 and not self.map[south].blocked:
|
||||||
|
neighbors.append(south)
|
||||||
|
|
||||||
|
down = point + vector3d(0,0,-1)
|
||||||
|
if not self.map[down].blocked:
|
||||||
|
neighbors.append(down)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
debug.info(0,"Initializing queue.")
|
||||||
|
for s in self.source:
|
||||||
|
cost = self.cost_to_target(s)
|
||||||
|
debug.info(1,"Init: cost=" + str(cost) + " " + str([s]))
|
||||||
|
self.q.put((cost,[s]))
|
||||||
|
|
||||||
|
def cost_to_target(self,source):
|
||||||
|
"""
|
||||||
|
Find the cheapest HPWL distance to any target point
|
||||||
|
"""
|
||||||
|
cost = source.hpwl(self.target[0])
|
||||||
|
for t in self.target:
|
||||||
|
cost = min(source.hpwl(t),cost)
|
||||||
|
return cost
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ from vector import vector
|
||||||
import grid
|
import grid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class router:
|
class router:
|
||||||
"""A router class to read an obstruction map from a gds and plan a
|
"""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.
|
route on a given layer. This is limited to two layer routes.
|
||||||
|
|
@ -51,15 +53,10 @@ class router:
|
||||||
""" If we want to route something besides the top-level cell."""
|
""" If we want to route something besides the top-level cell."""
|
||||||
self.top_name = top_name
|
self.top_name = top_name
|
||||||
|
|
||||||
|
|
||||||
def set_layers(self, layers):
|
def set_layers(self, layers):
|
||||||
""" Allows us to change the layers that we are routing on. """
|
""" Allows us to change the layers that we are routing on. """
|
||||||
self.layers = layers
|
self.layers = layers
|
||||||
(horiz_layer, via_layer, vert_layer) = self.layers
|
(horiz_layer, via_layer, vert_layer) = self.layers
|
||||||
if (via_layer != None):
|
|
||||||
self.via_layer_name = via_layer
|
|
||||||
else:
|
|
||||||
self.via_layer_name = None
|
|
||||||
|
|
||||||
self.vert_layer_name = vert_layer
|
self.vert_layer_name = vert_layer
|
||||||
self.vert_layer_width = tech.drc["minwidth_{0}".format(vert_layer)]
|
self.vert_layer_width = tech.drc["minwidth_{0}".format(vert_layer)]
|
||||||
|
|
@ -112,6 +109,10 @@ class router:
|
||||||
self.write_obstacle(self.top_name)
|
self.write_obstacle(self.top_name)
|
||||||
|
|
||||||
|
|
||||||
|
def route(self):
|
||||||
|
path = self.rg.route()
|
||||||
|
debug.info(0,"Found path: " + str(path))
|
||||||
|
self.rg.set_path(path)
|
||||||
|
|
||||||
def add_route(self,start, end, layerstack):
|
def add_route(self,start, end, layerstack):
|
||||||
""" Add a wire route from the start to the end point"""
|
""" Add a wire route from the start to the end point"""
|
||||||
|
|
@ -136,7 +137,7 @@ class router:
|
||||||
return coordinate
|
return coordinate
|
||||||
|
|
||||||
def min_max_coord(self, coordTrans):
|
def min_max_coord(self, coordTrans):
|
||||||
"""Find the lowest and highest conner of a Rectangle"""
|
"""Find the lowest and highest corner of a Rectangle"""
|
||||||
coordinate = []
|
coordinate = []
|
||||||
minx = min(coordTrans[0][0], coordTrans[1][0], coordTrans[2][0], coordTrans[3][0])
|
minx = min(coordTrans[0][0], coordTrans[1][0], coordTrans[2][0], coordTrans[3][0])
|
||||||
maxx = max(coordTrans[0][0], coordTrans[1][0], coordTrans[2][0], coordTrans[3][0])
|
maxx = max(coordTrans[0][0], coordTrans[1][0], coordTrans[2][0], coordTrans[3][0])
|
||||||
|
|
@ -152,6 +153,7 @@ class router:
|
||||||
debug.info(0,"Set source: " + str(name) + " " + str(shape) + " z=" + str(zindex))
|
debug.info(0,"Set source: " + str(name) + " " + str(shape) + " z=" + str(zindex))
|
||||||
self.rg.set_source(shape[0],shape[1],zindex)
|
self.rg.set_source(shape[0],shape[1],zindex)
|
||||||
|
|
||||||
|
|
||||||
def set_target(self,name):
|
def set_target(self,name):
|
||||||
shape = self.find_pin(name)
|
shape = self.find_pin(name)
|
||||||
zindex = 0 if self.pin_layers[name]==self.horiz_layer_number else 1
|
zindex = 0 if self.pin_layers[name]==self.horiz_layer_number else 1
|
||||||
|
|
@ -171,6 +173,7 @@ class router:
|
||||||
|
|
||||||
shape_tracks=self.convert_to_tracks([ll_microns,ur_microns])
|
shape_tracks=self.convert_to_tracks([ll_microns,ur_microns])
|
||||||
|
|
||||||
|
# don't add a blockage if this shape was a pin shape
|
||||||
if shape_tracks not in self.pin_shapes.values():
|
if shape_tracks not in self.pin_shapes.values():
|
||||||
# inflate the ll and ur by 1 track in each direction
|
# inflate the ll and ur by 1 track in each direction
|
||||||
[ll,ur]=shape_tracks
|
[ll,ur]=shape_tracks
|
||||||
|
|
@ -183,7 +186,6 @@ class router:
|
||||||
debug.info(2,"Skip: "+str(shape_tracks))
|
debug.info(2,"Skip: "+str(shape_tracks))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# recurse given the mirror, angle, etc.
|
# recurse given the mirror, angle, etc.
|
||||||
for cur_sref in self.layout.structures[sref].srefs:
|
for cur_sref in self.layout.structures[sref].srefs:
|
||||||
sMirr = 1
|
sMirr = 1
|
||||||
|
|
@ -201,22 +203,19 @@ class router:
|
||||||
|
|
||||||
self.write_obstacle(cur_sref.sName, layer,sMirr, sAngle, sxyShift)
|
self.write_obstacle(cur_sref.sName, layer,sMirr, sAngle, sxyShift)
|
||||||
|
|
||||||
def inflate_obstacle(self,shape):
|
|
||||||
# TODO: inflate by the layer design rules
|
|
||||||
return shape
|
|
||||||
|
|
||||||
def convert_to_tracks(self,shape):
|
def convert_to_tracks(self,shape):
|
||||||
"""
|
"""
|
||||||
Convert a rectangular shape into track units.
|
Convert a rectangular shape into track units.
|
||||||
"""
|
"""
|
||||||
[ll,ur] = shape
|
[ll,ur] = shape
|
||||||
|
|
||||||
# fix offset
|
# offset lowest corner object to to (0,0)
|
||||||
ll = snap_to_grid(ll-self.offset)
|
ll = snap_to_grid(ll-self.offset)
|
||||||
ur = snap_to_grid(ur-self.offset)
|
ur = snap_to_grid(ur-self.offset)
|
||||||
|
|
||||||
# always round down, because we will add a track
|
# always round down, because we will add a track
|
||||||
# to inflate each object later
|
# to inflate each obstacle object later.
|
||||||
|
# whereas pins should be conservative
|
||||||
ll = ll.scale(self.track_factor).ceil()
|
ll = ll.scale(self.track_factor).ceil()
|
||||||
ur = ur.scale(self.track_factor).floor()
|
ur = ur.scale(self.track_factor).floor()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,10 @@ class no_blockages_test(unittest.TestCase):
|
||||||
r.set_layers(("metal1","via1","metal2"))
|
r.set_layers(("metal1","via1","metal2"))
|
||||||
|
|
||||||
r.create_routing_grid()
|
r.create_routing_grid()
|
||||||
|
|
||||||
r.set_source("A")
|
r.set_source("A")
|
||||||
|
|
||||||
r.set_target("B")
|
r.set_target("B")
|
||||||
|
|
||||||
r.find_blockages()
|
r.find_blockages()
|
||||||
|
r.route()
|
||||||
r.rg.view()
|
r.rg.view()
|
||||||
|
|
||||||
#drc_errors = calibre.run_drc(name, gds_name)
|
#drc_errors = calibre.run_drc(name, gds_name)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
import debug
|
||||||
|
import math
|
||||||
|
|
||||||
|
class vector3d():
|
||||||
|
"""
|
||||||
|
This is the vector3d class to represent a 3D coordinate.
|
||||||
|
It needs to override several operators to support
|
||||||
|
concise vector3d operations, output, and other more complex
|
||||||
|
data structures like lists.
|
||||||
|
"""
|
||||||
|
def __init__(self, x, y=None, z=None):
|
||||||
|
""" init function support two init method"""
|
||||||
|
# will take single input as a coordinate
|
||||||
|
if y==None:
|
||||||
|
self.x = x[0]
|
||||||
|
self.y = x[1]
|
||||||
|
self.z = x[2]
|
||||||
|
#will take two inputs as the values of a coordinate
|
||||||
|
else:
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.z = z
|
||||||
|
self.tpl=(x,y,z)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
""" override print function output """
|
||||||
|
return "vector3d:["+str(self.x)+", "+str(self.y)+", "+str(self.z)+"]"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
""" override print function output """
|
||||||
|
return "["+str(self.x)+", "+str(self.y)+", "+str(self.z)+"]"
|
||||||
|
|
||||||
|
def __setitem__(self, index, value):
|
||||||
|
"""
|
||||||
|
override setitem function
|
||||||
|
can set value by vector3d[index]=value
|
||||||
|
"""
|
||||||
|
if index==0:
|
||||||
|
self.x=value
|
||||||
|
elif index==1:
|
||||||
|
self.y=value
|
||||||
|
elif index==2:
|
||||||
|
self.z=value
|
||||||
|
else:
|
||||||
|
self.x=value[0]
|
||||||
|
self.y=value[1]
|
||||||
|
self.z=value[2]
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
"""
|
||||||
|
override getitem function
|
||||||
|
can get value by value=vector3d[index]
|
||||||
|
"""
|
||||||
|
if index==0:
|
||||||
|
return self.x
|
||||||
|
elif index==1:
|
||||||
|
return self.y
|
||||||
|
elif index==2:
|
||||||
|
return self.z
|
||||||
|
else:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
"""
|
||||||
|
Override + function (left add)
|
||||||
|
Can add by vector3d(x1,y1,z1)+vector(x2,y2,z2)
|
||||||
|
"""
|
||||||
|
return vector3d(self.x + other[0], self.y + other[1], self.z + other[2])
|
||||||
|
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
"""
|
||||||
|
Override + function (right add)
|
||||||
|
"""
|
||||||
|
if other == 0:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return self.__add__(other)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
"""
|
||||||
|
Override - function (left)
|
||||||
|
"""
|
||||||
|
return vector3d(self.x - other[0], self.y - other[1], self.z - other[2])
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""
|
||||||
|
Override - function (hash)
|
||||||
|
Note: This assumes that you DON'T CHANGE THE VECTOR or it will
|
||||||
|
break things.
|
||||||
|
"""
|
||||||
|
return hash(self.tpl)
|
||||||
|
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
"""
|
||||||
|
Override - function (right)
|
||||||
|
"""
|
||||||
|
return vector3d(other[0]- self.x, other[1] - self.y, other[2] - self.z)
|
||||||
|
|
||||||
|
def rotate(self):
|
||||||
|
""" pass a copy of rotated vector3d, without altering the vector3d! """
|
||||||
|
return vector3d(self.y,self.x,self.z)
|
||||||
|
|
||||||
|
def scale(self, x_factor, y_factor=None,z_factor=None):
|
||||||
|
""" pass a copy of scaled vector3d, without altering the vector3d! """
|
||||||
|
if y_factor==None:
|
||||||
|
z_factor=x_factor[2]
|
||||||
|
y_factor=x_factor[1]
|
||||||
|
x_factor=x_factor[0]
|
||||||
|
return vector3d(self.x*x_factor,self.y*y_factor,self.z*z_factor)
|
||||||
|
|
||||||
|
def rotate_scale(self, x_factor, y_factor=None, z_factor=None):
|
||||||
|
""" pass a copy of scaled vector3d, without altering the vector3d! """
|
||||||
|
if y_factor==None:
|
||||||
|
z_factor=x_factor[2]
|
||||||
|
y_factor=x_factor[1]
|
||||||
|
x_factor=x_factor[0]
|
||||||
|
return vector3d(self.y*x_factor,self.x*y_factor,self.z*z_factor)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""Override the default Equals behavior"""
|
||||||
|
if isinstance(other, self.__class__):
|
||||||
|
return self.__dict__ == other.__dict__
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
"""Override the default non-equality behavior"""
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def max(self, other):
|
||||||
|
""" Max of both values """
|
||||||
|
return vector3d(max(self.x,other.x),max(self.y,other.y),max(self.z,other.z))
|
||||||
|
|
||||||
|
def min(self, other):
|
||||||
|
""" Min of both values """
|
||||||
|
return vector3d(min(self.x,other.x),min(self.y,other.y),min(self.z,other.z))
|
||||||
|
|
||||||
|
def hpwl(self, other):
|
||||||
|
""" Return half perimeter wire length from point to another.
|
||||||
|
Either point can have positive or negative coordinates. """
|
||||||
|
hpwl = max(abs(self.x-other.x),abs(other.x-self.x))
|
||||||
|
hpwl += max(abs(self.y-other.y),abs(other.y-self.y))
|
||||||
|
hpwl += max(abs(self.z-other.z),abs(other.z-self.z))
|
||||||
|
return hpwl
|
||||||
Loading…
Reference in New Issue