mirror of https://github.com/VLSIDA/OpenRAM.git
Updates to supply routing.
Rename astar_grid to signal_grid to parallel supply routing. Wave expansion for supply rails. Pin addition for supply rails.
This commit is contained in:
parent
59956f1446
commit
cd987479b8
|
|
@ -15,12 +15,12 @@ class vector():
|
|||
""" 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.x = float(x[0])
|
||||
self.y = float(x[1])
|
||||
#will take two inputs as the values of a coordinate
|
||||
else:
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.x = float(x)
|
||||
self.y = float(y)
|
||||
|
||||
def __str__(self):
|
||||
""" override print function output """
|
||||
|
|
@ -36,12 +36,12 @@ class vector():
|
|||
can set value by vector[index]=value
|
||||
"""
|
||||
if index==0:
|
||||
self.x=value
|
||||
self.x=float(value)
|
||||
elif index==1:
|
||||
self.y=value
|
||||
self.y=float(value)
|
||||
else:
|
||||
self.x=value[0]
|
||||
self.y=value[1]
|
||||
self.x=float(value[0])
|
||||
self.y=float(value[1])
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
|
|
@ -84,6 +84,14 @@ class vector():
|
|||
"""
|
||||
return vector(other[0]- self.x, other[1] - self.y)
|
||||
|
||||
def __hash__(self):
|
||||
"""
|
||||
Override - function (hash)
|
||||
Note: This assumes that you DON'T CHANGE THE VECTOR or it will
|
||||
break things.
|
||||
"""
|
||||
return hash((self.x,self.y))
|
||||
|
||||
def snap_to_grid(self):
|
||||
self.x = self.snap_offset_to_grid(self.x)
|
||||
self.y = self.snap_offset_to_grid(self.y)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class grid:
|
|||
or horizontal layer.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, ll, ur, track_width):
|
||||
""" Initialize the map and define the costs. """
|
||||
|
||||
# costs are relative to a unit grid
|
||||
|
|
@ -22,17 +22,43 @@ class grid:
|
|||
self.NONPREFERRED_COST = 4
|
||||
self.PREFERRED_COST = 1
|
||||
|
||||
# list of the source/target grid coordinates
|
||||
self.source = []
|
||||
self.target = []
|
||||
|
||||
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,1).scale(self.track_factor).round()
|
||||
|
||||
# let's leave the map sparse, cells are created on demand to reduce memory
|
||||
self.map={}
|
||||
|
||||
def set_blocked(self,n):
|
||||
self.add_map(n)
|
||||
self.map[n].blocked=True
|
||||
def set_blocked(self,n,value=True):
|
||||
if isinstance(n,list):
|
||||
for item in n:
|
||||
self.set_blocked(item,value)
|
||||
else:
|
||||
self.add_map(n)
|
||||
self.map[n].blocked=value
|
||||
|
||||
def is_blocked(self,n):
|
||||
self.add_map(n)
|
||||
return self.map[n].blocked
|
||||
|
||||
def set_path(self,n,value=True):
|
||||
if isinstance(n,list):
|
||||
for item in n:
|
||||
self.set_path(item,value)
|
||||
else:
|
||||
self.add_map(n)
|
||||
self.map[n].path=value
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
|
@ -48,12 +74,54 @@ class grid:
|
|||
for n in block_list:
|
||||
self.set_blocked(n)
|
||||
|
||||
def add_map(self,p):
|
||||
def set_source(self,n):
|
||||
if isinstance(n,list):
|
||||
for item in n:
|
||||
self.set_source(item)
|
||||
else:
|
||||
self.add_map(n)
|
||||
self.map[n].source=True
|
||||
self.source.append(n)
|
||||
|
||||
def set_target(self,n):
|
||||
if isinstance(n,list):
|
||||
for item in n:
|
||||
self.set_target(item)
|
||||
else:
|
||||
self.add_map(n)
|
||||
self.map[n].target=True
|
||||
self.target.append(n)
|
||||
|
||||
|
||||
def add_source(self,track_list):
|
||||
debug.info(2,"Adding source list={0}".format(str(track_list)))
|
||||
for n in track_list:
|
||||
debug.info(3,"Adding source ={0}".format(str(n)))
|
||||
self.set_source(n)
|
||||
|
||||
|
||||
def add_target(self,track_list):
|
||||
debug.info(2,"Adding target list={0}".format(str(track_list)))
|
||||
for n in track_list:
|
||||
debug.info(3,"Adding target ={0}".format(str(n)))
|
||||
self.set_target(n)
|
||||
|
||||
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 p not in self.map.keys():
|
||||
self.map[p]=cell()
|
||||
if isinstance(n,list):
|
||||
for item in n:
|
||||
self.add_map(item)
|
||||
else:
|
||||
if n not in self.map.keys():
|
||||
self.map[n]=cell()
|
||||
|
||||
def add_path(self,path):
|
||||
"""
|
||||
|
|
@ -61,7 +129,7 @@ class grid:
|
|||
"""
|
||||
self.path=path
|
||||
for p in path:
|
||||
self.map[p].path=True
|
||||
self.set_path(p)
|
||||
|
||||
def block_path(self,path):
|
||||
"""
|
||||
|
|
@ -69,8 +137,8 @@ class grid:
|
|||
Also unsets the path flag.
|
||||
"""
|
||||
for p in path:
|
||||
self.map[p].path=False
|
||||
self.map[p].blocked=True
|
||||
self.set_path(p,False)
|
||||
self.set_blocked(p)
|
||||
|
||||
def cost(self,path):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -35,16 +35,31 @@ class router:
|
|||
self.reader.loadFromFile(gds_name)
|
||||
self.top_name = self.layout.rootStructureName
|
||||
|
||||
# A map of pin names to pin structures
|
||||
self.pins = {}
|
||||
|
||||
# A list of pin blockages (represented by the pin structures too)
|
||||
self.blockages=[]
|
||||
|
||||
# all the paths we've routed so far (to supplement the blockages)
|
||||
self.paths = []
|
||||
self.wave_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])
|
||||
# These must be un-indexed to get rid of the matrix type
|
||||
self.ll = vector(self.boundary[0][0], self.boundary[0][1])
|
||||
self.ur = vector(self.boundary[1][0], self.boundary[1][1])
|
||||
|
||||
|
||||
def clear_pins(self):
|
||||
"""
|
||||
Convert the routed path to blockages.
|
||||
Keep the other blockages unchanged.
|
||||
"""
|
||||
self.pins = {}
|
||||
self.rg.reinit()
|
||||
|
||||
def set_top(self,top_name):
|
||||
""" If we want to route something besides the top-level cell."""
|
||||
self.top_name = top_name
|
||||
|
|
@ -55,6 +70,20 @@ class router:
|
|||
else:
|
||||
return 1
|
||||
|
||||
def get_layer(self, zindex):
|
||||
if zindex==1:
|
||||
return self.vert_layer_name
|
||||
elif zindex==0:
|
||||
return self.horiz_layer_name
|
||||
else:
|
||||
debug.error(-1,"Invalid zindex {}".format(zindex))
|
||||
|
||||
def is_wave(self,path):
|
||||
"""
|
||||
Determines if this is a wave (True) or a normal route (False)
|
||||
"""
|
||||
return isinstance(path[0],list)
|
||||
|
||||
def set_layers(self, layers):
|
||||
"""Allows us to change the layers that we are routing on. First layer
|
||||
is always horizontal, middle is via, and last is always
|
||||
|
|
@ -254,9 +283,16 @@ class router:
|
|||
Convert a path set of tracks to center line path.
|
||||
"""
|
||||
pt = vector3d(p)
|
||||
pt=pt.scale(self.track_widths[0],self.track_widths[1],1)
|
||||
pt = pt.scale(self.track_widths[0],self.track_widths[1],1)
|
||||
return pt
|
||||
|
||||
def convert_wave_to_units(self,wave):
|
||||
"""
|
||||
Convert a wave to a set of center points
|
||||
"""
|
||||
return [self.convert_point_to_units(i) for i in wave]
|
||||
|
||||
|
||||
def convert_blockage_to_tracks(self,shape):
|
||||
"""
|
||||
Convert a rectangular blockage shape into track units.
|
||||
|
|
@ -305,8 +341,8 @@ class router:
|
|||
track_list = []
|
||||
block_list = []
|
||||
|
||||
for x in range(ll[0],ur[0]):
|
||||
for y in range(ll[1],ur[1]):
|
||||
for x in range(int(ll[0]),int(ur[0])):
|
||||
for y in range(int(ll[1]),int(ur[1])):
|
||||
debug.info(1,"Converting [ {0} , {1} ]".format(x,y))
|
||||
|
||||
# however, if there is not enough overlap, then if there is any overlap at all,
|
||||
|
|
@ -481,9 +517,9 @@ class router:
|
|||
height=ur.y-ll.y)
|
||||
|
||||
|
||||
def add_route(self,path):
|
||||
def prepare_path(self,path):
|
||||
"""
|
||||
Add the current wire route to the given design instance.
|
||||
Prepare a path or wave for routing
|
||||
"""
|
||||
debug.info(3,"Set path: " + str(path))
|
||||
|
||||
|
|
@ -497,18 +533,46 @@ class router:
|
|||
if False or path==None:
|
||||
self.write_debug_gds()
|
||||
|
||||
|
||||
# First, simplify the path for
|
||||
#debug.info(1,str(self.path))
|
||||
contracted_path = self.contract_path(path)
|
||||
debug.info(1,str(contracted_path))
|
||||
|
||||
return contracted_path
|
||||
|
||||
|
||||
def add_route(self,path):
|
||||
"""
|
||||
Add the current wire route to the given design instance.
|
||||
"""
|
||||
|
||||
path=self.prepare_path(path)
|
||||
|
||||
# convert the path back to absolute units from tracks
|
||||
abs_path = map(self.convert_point_to_units,contracted_path)
|
||||
abs_path = list(map(self.convert_point_to_units,path))
|
||||
debug.info(1,str(abs_path))
|
||||
self.cell.add_route(self.layers,abs_path)
|
||||
|
||||
|
||||
def add_wave(self, name, path):
|
||||
"""
|
||||
Add the current wave to the given design instance.
|
||||
"""
|
||||
path=self.prepare_path(path)
|
||||
|
||||
# convert the path back to absolute units from tracks
|
||||
abs_path = [self.convert_wave_to_units(i) for i in path]
|
||||
debug.info(1,str(abs_path))
|
||||
if self.is_wave(path):
|
||||
ur = abs_path[-1][-1]
|
||||
ll = abs_path[0][0]
|
||||
self.cell.add_layout_pin(name,
|
||||
layer=self.get_layer(ll.z),
|
||||
offset=vector(ll.x,ll.y),
|
||||
width=ur.x-ll.x,
|
||||
height=ur.y-ll.y)
|
||||
|
||||
|
||||
def get_inertia(self,p0,p1):
|
||||
"""
|
||||
Sets the direction based on the previous direction we came from.
|
||||
|
|
@ -524,8 +588,13 @@ class router:
|
|||
|
||||
def contract_path(self,path):
|
||||
"""
|
||||
Remove intermediate points in a rectilinear path.
|
||||
Remove intermediate points in a rectilinear path or a wave.
|
||||
"""
|
||||
# Waves are always linear, so just return the first and last.
|
||||
if self.is_wave(path):
|
||||
return [path[0],path[-1]]
|
||||
|
||||
# Make a list only of points that change inertia of the path
|
||||
newpath = [path[0]]
|
||||
for i in range(1,len(path)-1):
|
||||
prev_inertia=self.get_inertia(path[i-1],path[i])
|
||||
|
|
|
|||
|
|
@ -4,52 +4,18 @@ from vector3d import vector3d
|
|||
import grid
|
||||
from heapq import heappush,heappop
|
||||
|
||||
class astar_grid(grid.grid):
|
||||
class signal_grid(grid.grid):
|
||||
"""
|
||||
Expand the two layer grid to include A* search functions for a source and target.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, ll, ur, track_factor):
|
||||
""" Create a routing map of width x height cells and 2 in the z-axis. """
|
||||
grid.grid.__init__(self)
|
||||
grid.grid.__init__(self, ll, ur, track_factor)
|
||||
|
||||
# list of the source/target grid coordinates
|
||||
self.source = []
|
||||
self.target = []
|
||||
|
||||
# priority queue for the maze routing
|
||||
self.q = []
|
||||
|
||||
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 add_source(self,track_list):
|
||||
debug.info(2,"Adding source list={0}".format(str(track_list)))
|
||||
for n in track_list:
|
||||
debug.info(3,"Adding source ={0}".format(str(n)))
|
||||
self.set_source(n)
|
||||
|
||||
|
||||
def add_target(self,track_list):
|
||||
debug.info(2,"Adding target list={0}".format(str(track_list)))
|
||||
for n in track_list:
|
||||
debug.info(3,"Adding target ={0}".format(str(n)))
|
||||
self.set_target(n)
|
||||
|
||||
def is_target(self,point):
|
||||
"""
|
||||
Point is in the target set, so we are done.
|
||||
"""
|
||||
return point in self.target
|
||||
|
||||
def reinit(self):
|
||||
""" Reinitialize everything for a new route. """
|
||||
|
||||
|
|
@ -10,21 +10,18 @@ from globals import OPTS
|
|||
from router import router
|
||||
|
||||
class signal_router(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.
|
||||
"""
|
||||
|
||||
def __init__(self, gds_name=None, module=None):
|
||||
"""Use the gds file for the blockages with the top module topName and
|
||||
"""
|
||||
Use the gds file for the blockages with the top module topName and
|
||||
layers for the layers to route on
|
||||
"""
|
||||
router.__init__(self, gds_name, module)
|
||||
|
||||
self.pins = {}
|
||||
|
||||
# all the paths we've routed so far (to supplement the blockages)
|
||||
self.paths = []
|
||||
|
||||
|
||||
def create_routing_grid(self):
|
||||
"""
|
||||
|
|
@ -35,8 +32,8 @@ class signal_router(router):
|
|||
size = self.ur - self.ll
|
||||
debug.info(1,"Size: {0} x {1}".format(size.x,size.y))
|
||||
|
||||
import astar_grid
|
||||
self.rg = astar_grid.astar_grid()
|
||||
import signal_grid
|
||||
self.rg = signal_grid.signal_grid(self.ll, self.ur, self.track_width)
|
||||
|
||||
|
||||
def route(self, cell, layers, src, dest, detour_scale=5):
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ class supply_grid(grid.grid):
|
|||
or horizontal layer.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, ll, ur, track_width):
|
||||
""" Create a routing map of width x height cells and 2 in the z-axis. """
|
||||
grid.grid.__init__(self)
|
||||
grid.grid.__init__(self, ll, ur, track_width)
|
||||
|
||||
# list of the vdd/gnd rail cells
|
||||
self.vdd_rails = []
|
||||
self.gnd_rails = []
|
||||
# Current rail
|
||||
self.rail = []
|
||||
|
||||
def reinit(self):
|
||||
""" Reinitialize everything for a new route. """
|
||||
|
|
@ -25,3 +24,63 @@ class supply_grid(grid.grid):
|
|||
p.reset()
|
||||
|
||||
|
||||
def start_wave(self, loc, width):
|
||||
"""
|
||||
Finds the first loc starting at loc and to the right that is open.
|
||||
Returns false if it reaches max size first.
|
||||
"""
|
||||
wave = [loc+vector3d(0,i,0) for i in range(width)]
|
||||
self.width = width
|
||||
|
||||
# Don't expand outside the bounding box
|
||||
if wave[0].y > self.ur.y:
|
||||
return None
|
||||
|
||||
# Increment while the wave is blocked
|
||||
while self.is_wave_blocked(wave):
|
||||
# Or until we cannot increment further
|
||||
if not self.increment_wave(wave):
|
||||
return None
|
||||
|
||||
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 increment_wave(self, wave):
|
||||
"""
|
||||
Increment the head by moving one step right. Return
|
||||
new wave if successful.
|
||||
"""
|
||||
new_wave = [v+vector3d(1,0,0) for v in wave]
|
||||
|
||||
# Don't expand outside the bounding box
|
||||
if new_wave[0].x>self.ur.x:
|
||||
return None
|
||||
|
||||
if not self.is_wave_blocked(new_wave):
|
||||
return new_wave
|
||||
return None
|
||||
|
||||
def probe_wave(self, wave):
|
||||
"""
|
||||
Expand the wave until there is a blockage and return
|
||||
the wave path.
|
||||
"""
|
||||
wave_path = []
|
||||
while wave and not self.is_wave_blocked(wave):
|
||||
wave_path.append(wave)
|
||||
wave = self.increment_wave(wave)
|
||||
|
||||
return wave_path
|
||||
|
||||
|
|
|
|||
|
|
@ -17,23 +17,21 @@ class supply_router(router):
|
|||
"""
|
||||
|
||||
def __init__(self, gds_name=None, module=None):
|
||||
"""Use the gds file for the blockages with the top module topName and
|
||||
"""
|
||||
Use the gds file for the blockages with the top module topName and
|
||||
layers for the layers to route on
|
||||
"""
|
||||
router.__init__(self, gds_name, module)
|
||||
|
||||
self.pins = {}
|
||||
|
||||
|
||||
def clear_pins(self):
|
||||
def create_routing_grid(self):
|
||||
"""
|
||||
Create a sprase routing grid with A* expansion functions.
|
||||
"""
|
||||
Convert the routed path to blockages.
|
||||
Keep the other blockages unchanged.
|
||||
"""
|
||||
self.pins = {}
|
||||
self.rg.reinit()
|
||||
|
||||
size = self.ur - self.ll
|
||||
debug.info(1,"Size: {0} x {1}".format(size.x,size.y))
|
||||
|
||||
import supply_grid
|
||||
self.rg = supply_grid.supply_grid(self.ll, self.ur, self.track_width)
|
||||
|
||||
def route(self, cell, layers, vdd_name="vdd", gnd_name="gnd"):
|
||||
"""
|
||||
|
|
@ -64,7 +62,7 @@ class supply_router(router):
|
|||
# Now add the blockages (all shapes except the pins)
|
||||
self.add_blockages()
|
||||
|
||||
#self.route_supply_rails()
|
||||
self.route_supply_rails()
|
||||
|
||||
#self.route_supply_pins()
|
||||
|
||||
|
|
@ -91,13 +89,26 @@ class supply_router(router):
|
|||
Add supply rails for vdd and gnd alternating in both layers.
|
||||
Connect cross-over points with vias.
|
||||
"""
|
||||
# vdd will be the even grids
|
||||
# vdd will be the odd grids
|
||||
vdd_rails = self.route_supply_rail(name="vdd",offset=0,width=2)
|
||||
|
||||
# gnd will be the odd grids
|
||||
|
||||
# gnd will be the even grids (0 + width)
|
||||
gnd_rails = self.route_supply_rail(name="gnd",offset=0,width=2)
|
||||
|
||||
pass
|
||||
|
||||
def route_supply_rail(self, name, offset=0, width=1):
|
||||
"""
|
||||
Add supply rails alternating layers.
|
||||
"""
|
||||
wave = self.rg.start_wave(loc=vector3d(0,offset,0), width=width)
|
||||
wave_path = self.rg.probe_wave(wave)
|
||||
self.add_wave(name, wave_path)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def route_supply_pins(self, pin):
|
||||
"""
|
||||
This will route all the supply pins to supply rails one at a time.
|
||||
|
|
@ -145,17 +156,6 @@ class supply_router(router):
|
|||
self.cell.add_route(self.layers,abs_path)
|
||||
|
||||
|
||||
def create_routing_grid(self):
|
||||
"""
|
||||
Create a sprase routing grid with A* expansion functions.
|
||||
"""
|
||||
# We will add a halo around the boundary
|
||||
# of this many tracks
|
||||
size = self.ur - self.ll
|
||||
debug.info(1,"Size: {0} x {1}".format(size.x,size.y))
|
||||
|
||||
import supply_grid
|
||||
self.rg = supply_grid.supply_grid()
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ class vector3d():
|
|||
self.x = x[0]
|
||||
self.y = x[1]
|
||||
self.z = x[2]
|
||||
#will take two inputs as the values of a coordinate
|
||||
#will take 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)+"]"
|
||||
return "["+str(self.x)+", "+str(self.y)+", "+str(self.z)+"]"
|
||||
|
||||
def __repr__(self):
|
||||
""" override print function output """
|
||||
|
|
@ -89,7 +89,7 @@ class vector3d():
|
|||
Note: This assumes that you DON'T CHANGE THE VECTOR or it will
|
||||
break things.
|
||||
"""
|
||||
return hash(self.tpl)
|
||||
return hash((self.x,self.y,self.z))
|
||||
|
||||
|
||||
def __rsub__(self, other):
|
||||
|
|
@ -118,6 +118,24 @@ class vector3d():
|
|||
x_factor=x_factor[0]
|
||||
return vector3d(self.y*x_factor,self.x*y_factor,self.z*z_factor)
|
||||
|
||||
def floor(self):
|
||||
"""
|
||||
Override floor function
|
||||
"""
|
||||
return vector3d(int(math.floor(self.x)),int(math.floor(self.y)), self.z)
|
||||
|
||||
def ceil(self):
|
||||
"""
|
||||
Override ceil function
|
||||
"""
|
||||
return vector3d(int(math.ceil(self.x)),int(math.ceil(self.y)), self.z)
|
||||
|
||||
def round(self):
|
||||
"""
|
||||
Override round function
|
||||
"""
|
||||
return vector3d(int(round(self.x)),int(round(self.y)), self.z)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Override the default Equals behavior"""
|
||||
if isinstance(other, self.__class__):
|
||||
|
|
|
|||
Loading…
Reference in New Issue