OpenRAM/compiler/base/geometry.py

576 lines
21 KiB
Python
Raw Normal View History

# See LICENSE for licensing information.
#
2023-01-29 07:56:27 +01:00
# Copyright (c) 2016-2023 Regents of the University of California and The Board
2019-06-14 17:43:41 +02:00
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
2016-11-08 18:57:35 +01:00
"""
2019-10-03 01:26:02 +02:00
This provides a set of useful generic types for the gdsMill interface.
2016-11-08 18:57:35 +01:00
"""
import math
import copy
import numpy as np
2022-11-27 22:01:20 +01:00
from openram import debug
from openram import tech
from openram import OPTS
from .utils import round_to_grid
2022-11-27 22:01:20 +01:00
from .vector import vector
2016-11-08 18:57:35 +01:00
2019-10-03 01:26:02 +02:00
2016-11-08 18:57:35 +01:00
class geometry:
"""
A specific path, shape, or text geometry. Base class for shared
items.
"""
def __init__(self, lpp=None):
2016-11-08 18:57:35 +01:00
""" By default, everything has no size. """
self.width = 0
self.height = 0
if lpp:
self.lpp = lpp
self.layerNumber = lpp[0]
self.layerPurpose = lpp[1]
2016-11-08 18:57:35 +01:00
def __str__(self):
""" override print function output """
2019-10-03 01:26:02 +02:00
debug.error("__str__ must be overridden by all geometry types.", 1)
2016-11-08 18:57:35 +01:00
def __repr__(self):
""" override print function output """
2019-10-03 01:26:02 +02:00
debug.error("__repr__ must be overridden by all geometry types.", 1)
2016-11-08 18:57:35 +01:00
# def translate_coords(self, coords, mirr, angle, xyShift):
# """Calculate coordinates after flip, rotate, and shift"""
# coordinate = []
# for item in coords:
# x = (item[0]*math.cos(angle)-item[1]*mirr*math.sin(angle)+xyShift[0])
# y = (item[0]*math.sin(angle)+item[1]*mirr*math.cos(angle)+xyShift[1])
# coordinate += [(x, y)]
# return coordinate
def transform_coords(self, coords, offset, mirr, angle):
"""Calculate coordinates after flip, rotate, and shift"""
coordinate = []
for item in coords:
2019-10-03 01:26:02 +02:00
x = item[0] * math.cos(angle) - item[1] * mirr * math.sin(angle) + offset[0]
y = item[0] * math.sin(angle) + item[1] * mirr * math.cos(angle) + offset[1]
coordinate += [[x, y]]
return coordinate
def normalize(self):
""" Re-find the LL and UR points after a transform """
2019-10-03 01:26:02 +02:00
(first, second) = self.boundary
ll = vector(min(first[0], second[0]),
min(first[1], second[1])).snap_to_grid()
ur = vector(max(first[0], second[0]),
max(first[1], second[1])).snap_to_grid()
self.boundary = [ll, ur]
def update_boundary(self):
""" Update the boundary with a new placement. """
2019-10-03 01:26:02 +02:00
self.compute_boundary(self.offset, self.mirror, self.rotate)
2019-10-03 01:26:02 +02:00
def compute_boundary(self, offset=vector(0, 0), mirror="", rotate=0):
2020-05-12 01:22:17 +02:00
"""
Transform with offset, mirror and rotation to get the absolute pin location.
We must then re-find the ll and ur. The master is the cell instance.
"""
if OPTS.netlist_only:
2020-05-12 01:22:17 +02:00
self.boundary = [vector(0, 0), vector(0, 0)]
return
2019-10-03 01:26:02 +02:00
(ll, ur) = [vector(0, 0), vector(self.width, self.height)]
2020-05-12 01:22:17 +02:00
# Mirroring is performed before rotation
2019-10-03 01:26:02 +02:00
if mirror == "MX":
ll = ll.scale(1, -1)
ur = ur.scale(1, -1)
elif mirror == "MY":
ll = ll.scale(-1, 1)
ur = ur.scale(-1, 1)
elif mirror == "XY":
ll = ll.scale(-1, -1)
ur = ur.scale(-1, -1)
elif mirror == "" or mirror == "R0":
pass
else:
debug.error("Invalid mirroring: {}".format(mirror), -1)
if rotate == 0:
pass
elif rotate == 90:
2019-10-03 01:26:02 +02:00
ll = ll.rotate_scale(-1, 1)
ur = ur.rotate_scale(-1, 1)
elif rotate == 180:
ll = ll.scale(-1, -1)
ur = ur.scale(-1, -1)
elif rotate == 270:
ll = ll.rotate_scale(1, -1)
ur = ur.rotate_scale(1, -1)
else:
debug.error("Invalid rotation: {}".format(rotate), -1)
2019-10-03 01:26:02 +02:00
self.boundary = [offset + ll, offset + ur]
self.normalize()
def ll(self):
""" Return the lower left corner """
return self.boundary[0]
def ur(self):
""" Return the upper right corner """
return self.boundary[1]
def lr(self):
""" Return the lower right corner """
return vector(self.boundary[1].x, self.boundary[0].y)
def ul(self):
""" Return the upper left corner """
return vector(self.boundary[0].x, self.boundary[1].y)
def uy(self):
""" Return the upper edge """
return self.boundary[1].y
def by(self):
""" Return the bottom edge """
return self.boundary[0].y
def lx(self):
""" Return the left edge """
return self.boundary[0].x
def rx(self):
""" Return the right edge """
return self.boundary[1].x
def cx(self):
""" Return the center x """
2019-10-03 01:26:02 +02:00
return 0.5 * (self.boundary[0].x + self.boundary[1].x)
def cy(self):
""" Return the center y """
2019-10-03 01:26:02 +02:00
return 0.5 * (self.boundary[0].y + self.boundary[1].y)
2020-01-28 01:28:55 +01:00
def center(self):
""" Return the center coordinate """
return vector(self.cx(), self.cy())
2020-11-03 15:29:17 +01:00
class instance(geometry):
"""
An instance of a module with a specified location, rotation,
spice pins, and spice nets
"""
2019-10-03 01:26:02 +02:00
def __init__(self, name, mod, offset=[0, 0], mirror="R0", rotate=0):
"""Initializes an instance to represent a module"""
2020-08-06 20:33:26 +02:00
super().__init__()
2019-10-03 01:26:02 +02:00
debug.check(mirror not in ["R90", "R180", "R270"],
"Please use rotation and not mirroring during instantiation.")
self.name = name
self.mod = mod
self.gds = mod.gds
self.rotate = rotate
self.offset = vector(offset).snap_to_grid()
self.mirror = mirror
# track if the instance's spice pin connections have been made
self.connected = False
# deepcopy because this instance needs to
# change attributes in these spice objects
self.spice_pins = copy.deepcopy(self.mod.pins)
self.spice_nets = copy.deepcopy(self.mod.nets)
for pin in self.spice_pins.values():
pin.set_inst(self)
for net in self.spice_nets.values():
net.set_inst(self)
if OPTS.netlist_only:
self.width = 0
self.height = 0
else:
2019-10-03 01:26:02 +02:00
if mirror in ["R90", "R270"] or rotate in [90, 270]:
self.width = round_to_grid(mod.height)
self.height = round_to_grid(mod.width)
else:
self.width = round_to_grid(mod.width)
self.height = round_to_grid(mod.height)
2019-10-03 01:26:02 +02:00
self.compute_boundary(offset, mirror, rotate)
debug.info(4, "creating instance: " + self.name)
def get_blockages(self, lpp, top=False):
""" Retrieve blockages of all modules in this instance.
Apply the transform of the instance placement to give absolute blockages."""
angle = math.radians(float(self.rotate))
mirr = 1
2019-10-03 01:26:02 +02:00
if self.mirror == "R90":
angle += math.radians(90.0)
2019-10-03 01:26:02 +02:00
elif self.mirror == "R180":
angle += math.radians(180.0)
2019-10-03 01:26:02 +02:00
elif self.mirror == "R270":
angle += math.radians(270.0)
2019-10-03 01:26:02 +02:00
elif self.mirror == "MX":
mirr = -1
2019-10-03 01:26:02 +02:00
elif self.mirror == "MY":
mirr = -1
angle += math.radians(180.0)
2019-10-03 01:26:02 +02:00
elif self.mirror == "XY":
mirr = 1
angle += math.radians(180.0)
new_blockages = []
if self.mod.is_library_cell:
# Writes library cell blockages as shapes instead of a large metal blockage
blockages = []
blockages = self.mod.gds.getBlockages(lpp)
for b in blockages:
2020-05-12 01:22:17 +02:00
new_blockages.append(self.transform_coords(b, self.offset, mirr, angle))
else:
blockages = self.mod.get_blockages(lpp)
for b in blockages:
2020-05-12 01:22:17 +02:00
new_blockages.append(self.transform_coords(b, self.offset, mirr, angle))
return new_blockages
def gds_write_file(self, new_layout):
"""Recursively writes all the sub-modules in this instance"""
debug.info(4, "writing instance: " + self.name)
# make sure to write out my module/structure
# (it will only be written the first time though)
self.mod.gds_write_file(self.gds)
# now write an instance of my module/structure
new_layout.addInstance(self.gds,
self.mod.cell_name,
offsetInMicrons=self.offset,
mirror=self.mirror,
rotate=self.rotate)
def place(self, offset, mirror="R0", rotate=0):
""" This updates the placement of an instance. """
# Update the placement of an already added instance
self.offset = vector(offset).snap_to_grid()
self.mirror = mirror
self.rotate = rotate
self.update_boundary()
debug.info(3, "placing instance {}".format(self))
2020-05-12 01:22:17 +02:00
def get_pin(self, name, index=-1):
""" Return an absolute pin that is offset and transformed based on
this instance location. Index will return one of several pins."""
2019-10-03 01:26:02 +02:00
if index == -1:
pin = copy.deepcopy(self.mod.get_pin(name))
2020-05-12 01:22:17 +02:00
pin.transform(self.offset, self.mirror, self.rotate)
return pin
else:
pins = copy.deepcopy(self.mod.get_pin(name))
2020-05-12 01:22:17 +02:00
pins.transform(self.offset, self.mirror, self.rotate)
return pin[index]
def get_num_pins(self, name):
""" Return the number of pins of a given name """
return len(self.mod.get_pins(name))
2020-05-12 01:22:17 +02:00
def get_pins(self, name):
""" Return an absolute pin that is offset and transformed based on
this instance location. """
pin = copy.deepcopy(self.mod.get_pins(name))
new_pins = []
for p in pin:
2020-05-12 01:22:17 +02:00
p.transform(self.offset, self.mirror, self.rotate)
new_pins.append(p)
return new_pins
2020-11-03 15:29:17 +01:00
def connect_spice_pins(self, nets_list):
"""
add the connection between instance pins and module nets
to both of their respective objects
nets_list must be the same length as self.spice_pins
"""
2023-07-19 01:14:38 +02:00
if len(nets_list) == 0 and len(self.spice_pins) == 0:
# this is the only valid case to skip the following debug check
# because this with no pins are often connected arbitrarily
self.connected = True
return
debug.check(not self.connected,
"instance {} has already been connected".format(self.name))
debug.check(len(self.spice_pins) == len(nets_list),
"must provide list of nets the same length as pin list\
when connecting an instance")
2023-07-19 01:14:38 +02:00
for pin in self.spice_pins.values():
net = nets_list.pop(0)
pin.set_inst_net(net)
net.connect_pin(pin)
self.connected = True
def get_connections(self):
conns = []
2023-07-19 01:14:38 +02:00
for pin in self.spice_pins.values():
conns.append(pin.inst_net.name)
return conns
2020-01-20 13:16:30 +01:00
def calculate_transform(self, node):
2020-11-03 15:29:17 +01:00
#set up the rotation matrix
2020-01-20 13:16:30 +01:00
angle = math.radians(float(node.rotate))
mRotate = np.array([[math.cos(angle), -math.sin(angle), 0.0],
[math.sin(angle), math.cos(angle), 0.0],
[0.0, 0.0, 1.0]])
2020-01-20 13:16:30 +01:00
#set up translation matrix
translateX = float(node.offset[0])
translateY = float(node.offset[1])
mTranslate = np.array([[1.0, 0.0, translateX],
[0.0, 1.0, translateY],
[0.0, 0.0, 1.0]])
2020-11-03 15:29:17 +01:00
2020-01-20 13:16:30 +01:00
#set up the scale matrix (handles mirror X)
scaleX = 1.0
if(node.mirror == 'MX'):
scaleY = -1.0
else:
scaleY = 1.0
mScale = np.array([[scaleX, 0.0, 0.0],
[0.0, scaleY, 0.0],
[0.0, 0.0, 1.0]])
2020-11-03 15:29:17 +01:00
2020-01-20 13:16:30 +01:00
return (mRotate, mScale, mTranslate)
def apply_transform(self, mtransforms, uVector, vVector, origin):
origin = np.dot(mtransforms[0], origin) # rotate
uVector = np.dot(mtransforms[0], uVector) # rotate
vVector = np.dot(mtransforms[0], vVector) # rotate
origin = np.dot(mtransforms[1], origin) # scale
uVector = np.dot(mtransforms[1], uVector) # scale
vVector = np.dot(mtransforms[1], vVector) # scale
2020-01-20 13:16:30 +01:00
origin = np.dot(mtransforms[2], origin)
return(uVector, vVector, origin)
def apply_path_transform(self, path):
uVector = np.array([[1.0], [0.0], [0.0]])
vVector = np.array([[0.0], [1.0], [0.0]])
origin = np.array([[0.0], [0.0], [1.0]])
2020-01-20 13:16:30 +01:00
while(path):
instance = path.pop(-1)
mtransforms = self.calculate_transform(instance)
(uVector, vVector, origin) = self.apply_transform(mtransforms, uVector, vVector, origin)
2020-11-03 15:29:17 +01:00
2020-01-20 13:16:30 +01:00
return (uVector, vVector, origin)
2020-01-20 13:16:30 +01:00
def reverse_transformation_bitcell(self, cell_name):
2020-01-28 01:28:55 +01:00
path = [] # path currently follwed in bitcell search
cell_paths = [] # saved paths to bitcells
origin_offsets = [] # cell to bank offset
Q_offsets = [] # Q to cell offet
Q_bar_offsets = [] # Q_bar to cell offset
bl_offsets = [] # bl to cell offset
br_offsets = [] # br to cell offset
bl_meta = [] # bl offset metadata (row,col,name)
br_meta = [] # br offset metadata (row,col,name)
def walk_subtree(node):
path.append(node)
if node.mod.name == cell_name:
cell_paths.append(copy.copy(path))
2020-01-28 01:28:55 +01:00
# get the row and col names from the path
row = int(path[-1].name.split('_')[-2][1:])
col = int(path[-1].name.split('_')[-1][1:])
cell_bl_meta = []
cell_br_meta = []
normalized_storage_nets = node.mod.get_normalized_storage_nets_offset()
2020-01-28 01:28:55 +01:00
(normalized_bl_offsets, normalized_br_offsets, bl_names, br_names) = node.mod.get_normalized_bitline_offset()
2020-11-03 15:29:17 +01:00
2020-01-28 01:28:55 +01:00
for offset in range(len(normalized_bl_offsets)):
for port in range(len(bl_names)):
cell_bl_meta.append([bl_names[offset], row, col, port])
for offset in range(len(normalized_br_offsets)):
for port in range(len(br_names)):
cell_br_meta.append([br_names[offset], row, col, port])
2022-07-22 18:52:38 +02:00
if normalized_storage_nets == []:
debug.error("normalized storage nets should not be empty! Check if the GDS labels Q and Q_bar are correctly set on M1 of the cell",1)
Q_x = normalized_storage_nets[0][0]
Q_y = normalized_storage_nets[0][1]
Q_bar_x = normalized_storage_nets[1][0]
Q_bar_y = normalized_storage_nets[1][1]
2020-01-20 13:16:30 +01:00
if node.mirror == 'MX':
Q_y = -1 * Q_y
Q_bar_y = -1 * Q_bar_y
for pair in range(len(normalized_bl_offsets)):
2020-11-03 15:29:17 +01:00
normalized_bl_offsets[pair] = (normalized_bl_offsets[pair][0],
-1 * normalized_bl_offsets[pair][1])
for pair in range(len(normalized_br_offsets)):
2020-11-03 15:29:17 +01:00
normalized_br_offsets[pair] = (normalized_br_offsets[pair][0],
-1 * normalized_br_offsets[pair][1])
2020-11-03 15:29:17 +01:00
Q_offsets.append([Q_x, Q_y])
Q_bar_offsets.append([Q_bar_x, Q_bar_y])
2020-11-03 15:29:17 +01:00
bl_offsets.append(normalized_bl_offsets)
br_offsets.append(normalized_br_offsets)
2020-01-28 01:28:55 +01:00
bl_meta.append(cell_bl_meta)
br_meta.append(cell_br_meta)
elif node.mod.insts is not []:
for instance in node.mod.insts:
walk_subtree(instance)
path.pop(-1)
walk_subtree(self)
for path in cell_paths:
2020-01-20 13:16:30 +01:00
vector_spaces = self.apply_path_transform(path)
origin = vector_spaces[2]
2020-01-20 13:16:30 +01:00
origin_offsets.append([origin[0], origin[1]])
2020-01-28 01:28:55 +01:00
return(origin_offsets, Q_offsets, Q_bar_offsets, bl_offsets, br_offsets, bl_meta, br_meta)
2016-11-08 18:57:35 +01:00
def __str__(self):
""" override print function output """
return "( inst: " + self.name + " @" + str(self.offset) + " mod=" + self.mod.cell_name + " " + self.mirror + " R=" + str(self.rotate) + ")"
2016-11-08 18:57:35 +01:00
def __repr__(self):
""" override print function output """
return "( inst: " + self.name + " @" + str(self.offset) + " mod=" + self.mod.cell_name + " " + self.mirror + " R=" + str(self.rotate) + ")"
2016-11-08 18:57:35 +01:00
2020-11-03 15:29:17 +01:00
2016-11-08 18:57:35 +01:00
class path(geometry):
"""Represents a Path"""
def __init__(self, lpp, coordinates, path_width):
2016-11-08 18:57:35 +01:00
"""Initializes a path for the specified layer"""
super().__init__(lpp)
2016-11-08 18:57:35 +01:00
self.name = "path"
self.coordinates = map(lambda x: [x[0], x[1]], coordinates)
self.coordinates = vector(self.coordinates).snap_to_grid()
2016-11-08 18:57:35 +01:00
self.path_width = path_width
# FIXME figure out the width/height. This type of path is not
# supported right now. It might not work in gdsMill.
assert(0)
def gds_write_file(self, new_layout):
2016-11-08 18:57:35 +01:00
"""Writes the path to GDS"""
debug.info(4, "writing path (" + str(self.layerNumber) + "): " + self.coordinates)
new_layout.addPath(layerNumber=self.layerNumber,
purposeNumber=self.layerPurpose,
coordinates=self.coordinates,
width=self.path_width)
2016-11-08 18:57:35 +01:00
def get_blockages(self, layer):
""" Fail since we don't support paths yet. """
assert(0)
2016-11-08 18:57:35 +01:00
def __str__(self):
""" override print function output """
2020-04-22 00:20:30 +02:00
return "path: layer=" + self.layerNumber + " purpose=" + str(self.layerPurpose) + " w=" + self.width
2016-11-08 18:57:35 +01:00
def __repr__(self):
""" override print function output """
2020-04-22 00:20:30 +02:00
return "( path: layer=" + self.layerNumber + " purpose=" + str(self.layerPurpose) + " w=" + self.width + " coords=" + str(self.coordinates) + " )"
2016-11-08 18:57:35 +01:00
class label(geometry):
"""Represents a text label"""
2020-12-14 23:18:00 +01:00
def __init__(self, text, lpp, offset, zoom=None):
2016-11-08 18:57:35 +01:00
"""Initializes a text label for specified layer"""
super().__init__(lpp)
2016-11-08 18:57:35 +01:00
self.name = "label"
self.text = text
self.offset = vector(offset).snap_to_grid()
2020-12-14 23:18:00 +01:00
if not zoom:
try:
self.zoom = tech.GDS["zoom"]
except:
self.zoom = None
else:
self.zoom = zoom
2016-11-08 18:57:35 +01:00
self.size = 0
2020-05-12 01:22:17 +02:00
debug.info(4, "creating label " + self.text + " " + str(self.layerNumber) + " " + str(self.offset))
def gds_write_file(self, new_layout):
2016-11-08 18:57:35 +01:00
"""Writes the text label to GDS"""
debug.info(4, "writing label (" + str(self.layerNumber) + "): " + self.text)
new_layout.addText(text=self.text,
layerNumber=self.layerNumber,
2020-02-21 03:35:54 +01:00
purposeNumber=self.layerPurpose,
offsetInMicrons=self.offset,
magnification=self.zoom,
rotate=None)
2016-11-08 18:57:35 +01:00
def get_blockages(self, layer):
""" Returns an empty list since text cannot be blockages. """
return []
2016-11-08 18:57:35 +01:00
def __str__(self):
""" override print function output """
2020-05-12 01:22:17 +02:00
return "label: " + self.text + " layer=" + str(self.layerNumber) + " purpose=" + str(self.layerPurpose)
2016-11-08 18:57:35 +01:00
def __repr__(self):
""" override print function output """
2020-04-22 00:20:30 +02:00
return "( label: " + self.text + " @" + str(self.offset) + " layer=" + str(self.layerNumber) + " purpose=" + str(self.layerPurpose) + " )"
2016-11-08 18:57:35 +01:00
class rectangle(geometry):
"""Represents a rectangular shape"""
def __init__(self, lpp, offset, width, height):
2016-11-08 18:57:35 +01:00
"""Initializes a rectangular shape for specified layer"""
super().__init__(lpp)
2016-11-08 18:57:35 +01:00
self.name = "rect"
self.offset = vector(offset).snap_to_grid()
self.size = vector(width, height).snap_to_grid()
self.width = round_to_grid(self.size.x)
self.height = round_to_grid(self.size.y)
2019-10-03 01:26:02 +02:00
self.compute_boundary(offset, "", 0)
2016-11-08 18:57:35 +01:00
2019-10-03 01:26:02 +02:00
debug.info(4, "creating rectangle (" + str(self.layerNumber) + "): "
+ str(self.width) + "x" + str(self.height) + " @ " + str(self.offset))
def get_blockages(self, layer):
""" Returns a list of one rectangle if it is on this layer"""
if self.layerNumber == layer:
2019-10-03 01:26:02 +02:00
return [[self.offset,
vector(self.offset.x + self.width,
self.offset.y + self.height)]]
else:
return []
def gds_write_file(self, new_layout):
2016-11-08 18:57:35 +01:00
"""Writes the rectangular shape to GDS"""
2019-10-03 01:26:02 +02:00
debug.info(4, "writing rectangle (" + str(self.layerNumber) + "):"
+ str(self.width) + "x" + str(self.height) + " @ " + str(self.offset))
new_layout.addBox(layerNumber=self.layerNumber,
purposeNumber=self.layerPurpose,
offsetInMicrons=self.offset,
width=self.width,
height=self.height,
center=False)
2016-11-08 18:57:35 +01:00
def __str__(self):
""" override print function output """
return self.__repr__()
2016-11-08 18:57:35 +01:00
def __repr__(self):
""" override print function output """
2020-04-22 00:20:30 +02:00
return "( rect: @" + str(self.offset) + " WxH=" + str(self.width) + "x" + str(self.height) + " layer=" + str(self.layerNumber) + " purpose=" + str(self.layerPurpose) + " )"