Source code for ACG.Rectangle

from ACG.VirtualObj import VirtualObj
from ACG.XY import XY
from bag.layout.util import BBox
from typing import Tuple, Union, Optional
from ACG import tech as tech_info
coord_type = Union[Tuple[float, float], XY]


[docs]class Rectangle(VirtualObj): """ Creates a better rectangle object with stretch and align capabilities """ """ Constructor Methods """ def __init__(self, xy, layer, virtual=False): """ xy: [[x0, y0], [x1, y1]] xy coordinates for ll and ur locations layer: str layer name virtual: Bool If True, do not draw the rectangle """ VirtualObj.__init__(self) # Init internal properties self._ll = None self._ur = None self._res = .001 self._lpp: Optional[Tuple[str, str]] = None # Init local variables self.xy = xy # property setter creates ll and ur coordinates self.layer = layer self.virtual: bool = virtual self.loc = { 'll': 0, 'ur': 0, 'ul': 0, 'lr': 0, 'l': 0, 'r': 0, 't': 0, 'b': 0, 'cl': 0, 'cr': 0, 'ct': 0, 'cb': 0, 'c': 0 } self.edges = ['l', 'r', 'b', 't'] self.v_edges = ['t', 'b'] self.h_edges = ['l', 'r'] # Init rect locations self.update_dict() # Describes all the required keys to define a Rect2 object with a dict dict_compatability = ('handle0', 'handle1', 'xy0', 'xy1', 'layer')
[docs] @classmethod def from_dict(cls, params: dict) -> 'Rectangle': """ Enable the creation of a rectangle from a dictionary of parameters """ # Check that all parameters required to create keys exist if not all(keys in params.keys() for keys in cls.dict_compatability): raise ValueError('Provided dict does not contain all parameters required to specify Rect2 obj') else: handles = [params['handle0'], params['handle1']] coordinates = [params['xy0'], params['xy1']] # Calculate ll and ur coordinates based on provided handle locations if ('ll' in handles) and ('ur' in handles): ll_loc = coordinates[handles.index('ll')] ur_loc = coordinates[handles.index('ur')] xy = [ll_loc, ur_loc] elif ('ul' in handles) and ('lr' in handles): ul_loc = coordinates[handles.index('ul')] lr_loc = coordinates[handles.index('lr')] ll_loc = [ul_loc[0], lr_loc[1]] ur_loc = [lr_loc[0], ul_loc[1]] xy = [ll_loc, ur_loc] else: raise ValueError('Provided handles do not adequately constrain rectangle dimensions') if 'virtual' in params.keys(): virtual = params['virtual'] else: virtual = False # construct class based on cleaned up xy coordinates rect = cls(xy, params['layer'], virtual) return rect
""" Properties """ @property def layer(self): return self._lpp[0] # return self._lpp @layer.setter def layer(self, value): if isinstance(value, tuple) and len(value) == 2: self._lpp = value elif isinstance(value, list) and len(value) == 2: self._lpp = tuple(value) elif isinstance(value, str): self._lpp = (value, 'drawing') else: raise ValueError(f"{value} cannot be used as a layer or layer purpose pair") @property def lpp(self) -> Tuple[str, str]: return self._lpp @lpp.setter def lpp(self, value): if len(value == 2): self._lpp = tuple(value) else: raise ValueError(f"{value} cannot be used as a layer purpose pair") @property def ll(self) -> XY: return self._ll @ll.setter def ll(self, xy): self._ll = XY(xy) @property def ur(self) -> XY: return self._ur @ur.setter def ur(self, xy): self._ur = XY(xy) @property def xy(self): return [self.ll, self.ur] @xy.setter def xy(self, coordinates): self.ll = coordinates[0] self.ur = coordinates[1] @property def width(self) -> float: return self.get_dim('x') @property def height(self) -> float: return self.get_dim('y') @property def center(self) -> XY: return self.loc['c'] """ Magic Methods """ def __repr__(self): return 'Rectangle(xy={!s}, layer={}, virtual={})'.format(self.xy, self.layer, self.virtual) def __str__(self): return '\tloc: {} \n\tlayer: {} \n\tvirtual: {}'.format(self.xy, self.layer, self.virtual) """ Utility Methods """
[docs] def export_locations(self): return self.loc
[docs] def update_dict(self): """ Updates the location dictionary based on the current ll and ur coordinates """ self.loc = { 'll': self.ll, 'ur': self.ur, 'ul': XY([self.ll.x, self.ur.y]), 'lr': XY([self.ur.x, self.ll.y]), 'l': self.ll.x, 'r': self.ur.x, 't': self.ur.y, 'b': self.ll.y, 'cl': XY([self.ll.x, .5 * (self.ur.y + self.ll.y)]), 'cr': XY([self.ur.x, .5 * (self.ur.y + self.ll.y)]), 'ct': XY([.5 * (self.ll.x + self.ur.x), self.ur.y]), 'cb': XY([.5 * (self.ll.x + self.ur.x), self.ll.y]), 'c': XY([.5 * (self.ll.x + self.ur.x), .5 * (self.ur.y + self.ll.y)]) }
[docs] def set_dim(self, dim: str, size: float) -> 'Rectangle': """ Sets either the width or height of the rect to desired value. Maintains center location of rect """ if dim == 'x': self.ll.x = self.loc['cb'].x - (.5 * size) self.ur.x = self.loc['ct'].x + (.5 * size) elif dim == 'y': self.ll.y = self.loc['cl'].y - (.5 * size) self.ur.y = self.loc['cr'].y + (.5 * size) elif dim == 'xy': self.ll.x = self.loc['cb'].x - (.5 * size) self.ur.x = self.loc['ct'].x + (.5 * size) self.ll.y = self.loc['cl'].y - (.5 * size) self.ur.y = self.loc['cr'].y + (.5 * size) else: raise ValueError('target_dim must be either x or y') # Update rectangle locations self.update_dict() return self
[docs] def get_dim(self, dim): """ Returns measurement of the dimension of the rectangle """ if dim == 'x': return abs(self.loc['l'] - self.loc['r']) elif dim == 'y': return abs(self.loc['t'] - self.loc['b']) else: raise ValueError('Provided dimension is not valid')
[docs] def scale(self, size, dim=None) -> 'Rectangle': """ Additvely resizes the rectangle by the provided size """ if dim == 'x': self.set_dim('x', self.get_dim('x') + size) elif dim == 'y': self.set_dim('y', self.get_dim('y') + size) else: self.set_dim('x', self.get_dim('x') + size) self.set_dim('y', self.get_dim('y') + size) return self
[docs] def align(self, target_handle: str, ref_rect: 'Rectangle' = None, ref_handle: str = None, track=None, align_opt: Tuple[bool, bool] = (True, True), offset: coord_type = (0, 0) ) -> 'Rectangle': """ Moves the rectangle to co-locate the target and ref handles """ if track is not None: # If a track is provided, calculate difference in dimension of track if track.x != 0: diffx = self.loc[target_handle].x - (track.x - offset[0]) else: diffx = 0 if track.y != 0: diffy = self.loc[target_handle].y - (track.y - offset[1]) else: diffy = 0 elif (ref_rect is None) and (ref_handle is None): # If no reference rectangle/handle is given if target_handle in self.loc: diffx = self.loc[target_handle].x - offset[0] diffy = self.loc[target_handle].y - offset[1] elif (target_handle in self.loc) and (ref_handle in ref_rect.loc): # If a reference rectangle and handles are given if (target_handle in self.edges) and (ref_handle in self.edges): # If we're provided with edges instead of vertices if (target_handle in self.v_edges) and (ref_handle in self.v_edges): # If both handles are vertical edges diffx = 0 diffy = self.loc[target_handle] - (ref_rect.loc[ref_handle] + offset[1]) elif (target_handle in self.h_edges) and (ref_handle in self.h_edges): # If both handles are horizontal edges diffx = self.loc[target_handle] - (ref_rect.loc[ref_handle] + offset[0]) diffy = 0 else: raise ValueError('{} and {} must both be edge handles to support edge alignment'. format(target_handle, ref_handle)) else: # Compute difference in location diffx = self.loc[target_handle].x - (ref_rect.loc[ref_handle].x + offset[0]) diffy = self.loc[target_handle].y - (ref_rect.loc[ref_handle].y + offset[1]) else: raise ValueError('Arguments do not specify a valid align operation') # Shift ll and ur locations to co-locate handles, unless align_opt is false if align_opt[0] or (align_opt is None): self.ll.x -= diffx self.ur.x -= diffx if align_opt[1] or (align_opt is None): self.ll.y -= diffy self.ur.y -= diffy # Update rectangle locations self.update_dict() return self
[docs] def stretch(self, target_handle: str, ref_rect=None, ref_handle: str = None, track=None, stretch_opt=(True, True), # type: Tuple[bool, bool] offset=(0, 0) # type: Tuple[float, float] ) -> 'Rectangle': """ Stretches rectangle to co-locate the target and ref handles. If ref handles are not provided, stretch by given offset """ if track is not None: # If a track is provided, calculate difference only in dimension of track if track.x != 0: diff_width = self.loc[target_handle].x - (track.x - offset[0]) else: diff_width = 0 if track.y != 0: diff_height = self.loc[target_handle].y - (track.y - offset[1]) else: diff_height = 0 elif (ref_rect is None) and (ref_handle is None): # If no reference rect/handle is provided, stretch the target rect by provided offset if target_handle in self.loc: # Check that the handle is valid diff_width = self.loc[target_handle].x - offset[0] diff_height = self.loc[target_handle].y - offset[1] # First align the two points self.align(target_handle, align_opt=stretch_opt, offset=offset) else: raise ValueError('target handle is invalid') elif (target_handle in self.loc) and (ref_handle in ref_rect.loc): # If a reference rect and handles are given if (target_handle in self.edges) and (ref_handle in self.edges): # If we're provided with edges instead of vertices if (target_handle in self.v_edges) and (ref_handle in self.v_edges): # If both handles are vertical edges diff_width = 0 diff_height = self.loc[target_handle] - (ref_rect.loc[ref_handle] + offset[1]) elif (target_handle in self.h_edges) and (ref_handle in self.h_edges): # If both handles are horizontal edges diff_width = self.loc[target_handle] - (ref_rect.loc[ref_handle] + offset[0]) diff_height = 0 else: raise ValueError('{} and {} must both be edge handles of same dimension to support edge alignment'. format(target_handle, ref_handle)) self.align(target_handle, ref_rect, ref_handle, align_opt=stretch_opt, offset=offset) else: diff_width = self.loc[target_handle].x - (ref_rect.loc[ref_handle].x + offset[0]) diff_height = self.loc[target_handle].y - (ref_rect.loc[ref_handle].y + offset[1]) # First align the two points self.align(target_handle, ref_rect, ref_handle, align_opt=stretch_opt, offset=offset) else: raise ValueError('Arguments do not specify a valid stretch operation') # Shift ll and ur locations to co-locate handles, unless stretch_opt is false # Stretch width of rectangle if target_handle == 'll' or target_handle == 'ul' or target_handle == 'cl' or target_handle == 'l': if stretch_opt[0]: self.ur.x += diff_width elif target_handle == 'lr' or target_handle == 'ur' or target_handle == 'cr' or target_handle == 'r': if stretch_opt[0]: self.ll.x += diff_width elif target_handle == 'c' or target_handle == 'ct' or target_handle == 'cb': if stretch_opt[0]: self.ll.x += .5 * diff_width self.ur.x += .5 * diff_width # Stretch height of rectangle if target_handle == 'll' or target_handle == 'lr' or target_handle == 'cb' or target_handle == 'b': if stretch_opt[1]: self.ur.y += diff_height elif target_handle == 'ul' or target_handle == 'ur' or target_handle == 'ct' or target_handle == 't': if stretch_opt[1]: self.ll.y += diff_height elif target_handle == 'c' or target_handle == 'cl' or target_handle == 'cr': if stretch_opt[1]: self.ll.y += .5 * diff_height self.ur.y += .5 * diff_height # Update rectangle locations self.update_dict() return self
[docs] def shift_origin(self, origin=(0, 0), orient='R0', virtual=True) -> 'Rectangle': """ Takes xy coordinates and rotation, returns a virtual Rect2 that is re-referenced to the new origin Assumes that the origin of the rectangle is (0, 0) """ # Apply the transformation to the xy coordinates that describe the rectangle new_xy = [0, 0] new_xy[0] = self.ll.shift_origin(origin, orient) new_xy[1] = self.ur.shift_origin(origin, orient) # Depending on the transformation, ll and ur may describe different rect corner if orient is 'MX': handle0 = 'ul' handle1 = 'lr' elif orient is 'MY': handle0 = 'lr' handle1 = 'ul' elif orient is 'R180': handle0 = 'ur' handle1 = 'll' else: handle0 = 'll' handle1 = 'ur' # Return the new shifted rectangle created from dictionary rect_dict = { 'handle0': handle0, 'handle1': handle1, 'xy0': new_xy[0], 'xy1': new_xy[1], 'virtual': virtual, 'layer': self.lpp } return Rectangle.from_dict(rect_dict)
[docs] def to_bbox(self) -> BBox: return BBox(self.ll.x, self.ll.y, self.ur.x, self.ur.y, self._res)
[docs] def copy(self, virtual=False, layer=None) -> 'Rectangle': if layer is None: layer = self.lpp return Rectangle(self.xy, layer, virtual=virtual)
[docs] def get_overlap(self, rect: 'Rectangle', virtual: bool = True ) -> 'Rectangle': """ Returns a rectangle corresponding to the overlapped region between two rectangles """ # Determine bounds of the intersection in x dimension x_min = max(self.ll.x, rect.ll.x) x_max = min(self.ur.x, rect.ur.x) # Determine bounds of the intersection in y dimension y_min = max(self.ll.y, rect.ll.y) y_max = min(self.ur.y, rect.ur.y) # Throw exception if the rectangles don't overlap if x_min > x_max: print(self) print(rect) print('x_min: {}'.format(x_min)) print('x_max: {}'.format(x_max)) raise ValueError('Given rectangles do not overlap in x direction') if y_min > y_max: print(self) print(rect) print('y_min: {}'.format(y_min)) print('y_max: {}'.format(y_max)) raise ValueError('Given rectangles do not overlap in y direction') return Rectangle([[x_min, y_min], [x_max, y_max]], layer=self.lpp, virtual=virtual)
[docs] def get_enclosure(self, rect: 'Rectangle', virtual: bool = True ) -> 'Rectangle': """ Returns a rectangle that encloses all provided rectangles """ ll = [min(self.ll.x, rect.ll.x), min(self.ll.y, rect.ll.y)] ur = [max(self.ur.x, rect.ur.x), max(self.ur.y, rect.ur.y)] return Rectangle(xy=[ll, ur], layer=self.get_highest_layer(rect), virtual=virtual)
[docs] def get_highest_layer(self, rect: Optional['Rectangle'] = None, layer: Optional[str] = None ) -> Tuple[str, str]: """ Returns the highest layer used by provided rectangles """ layerstack = tech_info.tech_info['metal_tech']['layerstack'] # TODO: Access layerstack from bag tech if rect: layer = rect.layer lpp = rect.lpp else: layer = layer lpp = (layer, 'drawing') # Check for non-routing layers and return the highest routing layer # TODO: Clean up this logic to deal with non-routing layers if (self.layer not in layerstack) and (layer not in layerstack): # print(f'both {self.layer} and {layer} are not valid routing layers, and cannot be ordered') return self.lpp elif self.layer not in layerstack: return lpp elif layer not in layerstack: return self.lpp i1 = layerstack.index(self.layer) i2 = layerstack.index(layer) if i2 > i1: return lpp else: return self.lpp
[docs] def get_midpoint(self, handle: str, coord: coord_type ) -> coord_type: """ Gets the midpoint between a location on this rectangle and another coordinate """ my_loc = self.loc[handle] their_loc = XY(coord) mid_x = (my_loc.x + their_loc.x) / 2 mid_y = (my_loc.y + their_loc.y) / 2 return XY([mid_x, mid_y])
[docs] @staticmethod def overlap(A, B): """ Returns whether or not two rectangles overlap in both dimensions """ x_min = max(A.ll.x, B.ll.x) x_max = min(A.ur.x, B.ur.x) y_min = max(A.ll.y, B.ll.y) y_max = min(A.ur.y, B.ur.y) return x_min < x_max and y_min < y_max