# SPDX-License-Identifier: Apache-2.0
# Copyright 2019 Blue Cheetah Analog Design Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import abc
import numpy as np
from enum import IntEnum
from typing import Any, Optional, Mapping, cast, Union, Tuple, Sequence
from pybag.core import BBox, Transform, BBoxArray
from pybag.enum import MinLenMode, RoundMode, Direction, Orientation
from bag.util.immutable import Param, combine_hash
from bag.layout.template import TemplateDB
from bag.layout.tech import TechInfo
from bag.layout.routing.base import WDictType, SpDictType, WireArray, TrackID
from bag.layout.routing.grid import RoutingGrid, TrackSpec
from ..mos.data import MOSType
from ..enum import ExtendMode
from ..array.base import ArrayPlaceInfo, ArrayBase
from .tech import ResTech
[docs]class ResTermType(IntEnum):
[docs]class ResBasePlaceInfo(ArrayPlaceInfo):
def __init__(self, parent_grid: RoutingGrid, wire_specs: Mapping[int, Any],
tr_widths: WDictType, tr_spaces: SpDictType, top_layer: int, nx: int, ny: int, *,
conn_layer: Optional[int] = None, res_type: str = 'standard', mos_type: str = '',
threshold: str = '', tr_specs: Optional[Sequence[TrackSpec]] = None,
half_space: bool = True, ext_mode: ExtendMode = ExtendMode.AREA,
**kwargs: Any) -> None:
metal = (res_type == 'metal')
tech_cls: ResTech = parent_grid.tech_info.get_device_tech('res', metal=metal)
self._res_config = parent_grid.tech_info.config['res_metal' if metal else 'res']
if not mos_type:
mos_type = tech_cls.mos_type_default
if not threshold:
threshold = tech_cls.threshold_default
if 'res_type' in kwargs['unit_specs']['params']:
res_type = kwargs['unit_specs']['params']['res_type']
self._res_type = res_type
self._mos_type = MOSType[mos_type]
self._threshold = threshold
self._w_res = tech_cls.get_width(**kwargs)
self._l_res = tech_cls.get_length(**kwargs)
ArrayPlaceInfo.__init__(self, parent_grid, wire_specs, tr_widths, tr_spaces, top_layer,
nx, ny, tech_cls, conn_layer=conn_layer, tr_specs=tr_specs,
half_space=half_space, ext_mode=ext_mode, res_type=res_type,
mos_type=mos_type, threshold=threshold, **kwargs)
[docs] def compute_hash(self):
seed = super().compute_hash()
seed = combine_hash(seed, hash(self._res_type))
seed = combine_hash(seed, hash(self._mos_type))
seed = combine_hash(seed, hash(self._threshold))
seed = combine_hash(seed, self._w_res)
seed = combine_hash(seed, self._l_res)
return seed
[docs] def __hash__(self) -> int:
return self._hash
[docs] def __eq__(self, other: Any) -> bool:
# noinspection PyProtectedMember
return (ArrayPlaceInfo.__eq__(self, other) and
self._res_type == other._res_type and
self._mos_type == other._mos_type and
self._threshold == other._threshold)
@classmethod
[docs] def get_tech_cls(cls, tech_info: TechInfo, **kwargs: Any) -> ResTech:
res_type = kwargs.get('res_type', 'standard')
metal = (res_type == 'metal')
return tech_info.get_device_tech('res', metal=metal)
@property
[docs] def res_type(self) -> str:
return self._res_type
@property
[docs] def mos_type(self) -> MOSType:
return self._mos_type
@property
[docs] def threshold(self) -> str:
return self._threshold
@property
[docs] def res_config(self) -> Mapping[str, Any]:
return self._res_config
@property
[docs] def has_substrate_port(self) -> bool:
return self._res_config['has_substrate_port']
@property
[docs] def w_res(self) -> int:
return self._w_res
@property
[docs] def l_res(self) -> int:
return self._l_res
[docs]class ResArrayBase(ArrayBase, abc.ABC):
"""An abstract template that draws analog resistors array and connections.
This template assumes that the resistor array uses 4 routing layers, with
directions x/y/x/y. The lower 2 routing layers is used to connect between
adjacent resistors, and pin will be drawn on the upper 2 routing layers.
Like for MOSBase, conn_layer should return the top-most layer of the primitive,
i.e. the pin layer. We can then connect BBoxs or WireArrays to the pins using
the next layer.
Unlike BAG2, we assume the conn_layer is BELOW the bottom-most routing layer
described above (i.e. below the first horizontal resistor routing layer).
This is to allow for more control of the low level wire placement.
One effect of this is that resistor primitives can be either WireArrays or BBoxs,
so classes using ResArrayBase need to be coded for both.
Primitives must be design with connecting to the above horizontal metal in mind.
"""
def __init__(self, temp_db: TemplateDB, params: Param, **kwargs: Any) -> None:
ArrayBase.__init__(self, temp_db, params, **kwargs)
@property
[docs] def has_substrate_port(self) -> bool:
return cast(ResTech, self.tech_cls).has_substrate_port
@property
[docs] def sub_type(self) -> MOSType:
return cast(ResBasePlaceInfo, self.place_info).mos_type
[docs] def draw_base(self, obj: Union[ResBasePlaceInfo, Mapping[str, Any]], **kwargs) -> ResBasePlaceInfo:
if isinstance(obj, ResBasePlaceInfo):
pinfo = obj
else:
pinfo = ResBasePlaceInfo(self.grid, **obj)
super().draw_base(pinfo, **kwargs)
return pinfo
[docs] def get_res_bbox(self, row_idx: int, col_idx: int) -> BBox:
"""Returns the bounding box of the given resistor."""
# TODO: fix this
x0, y0 = 0, 0 # self._core_offset
xp, yp = self.place_info.width, self.place_info.height
x0 += xp * col_idx
y0 += yp * row_idx
return BBox(x0, y0, x0 + xp, y0 + yp)
[docs] def get_res_ports(self, row_idx: int, col_idx: int,
top_port_name: str = "PLUS", bot_port_name: str = 'MINUS'
) -> Union[Tuple[WireArray, WireArray], Tuple[BBox, BBox]]:
"""Returns the port of the given resistor.
Parameters
----------
row_idx : int
the resistor row index. 0 is the bottom row.
col_idx : int
the resistor column index. 0 is the left-most column.
top_port_name: str
name of the top port. Defaults to "PLUS"
bot_port_name: str
name of the bottom port. Defaults to "MINUS"
Returns
-------
bot_warr : WireArray
the bottom port as WireArray.
top_warr : WireArray
the top port as WireArray.
"""
return self.get_device_port(col_idx, row_idx, bot_port_name), \
self.get_device_port(col_idx, row_idx, top_port_name)
[docs] def connect_units(self, warrs: Mapping[int, Mapping[ResTermType, np.ndarray]], x0: int, x1: int, y0: int, y1: int
) -> Tuple[WireArray, WireArray]:
"""Connect all unit resistors to have parallel connection from x0 to x1 and series from y0 to y1.
Returns bottom and top vm_layer WireArrays."""
hm_layer = self.conn_layer + 1
vm_layer = hm_layer + 1
prev_vm: Optional[WireArray] = None
bot_vm: Optional[WireArray] = None
top_vm: Optional[WireArray] = None
for yidx in range(y0, y1):
# parallel connections
self.connect_wires(warrs[hm_layer][ResTermType.BOT][x0:x1, yidx].tolist())
self.connect_wires(warrs[hm_layer][ResTermType.TOP][x0:x1, yidx].tolist())
_bot_vm = self.connect_wires(warrs[vm_layer][ResTermType.BOT][x0:x1, yidx].tolist())[0]
_top_vm = self.connect_wires(warrs[vm_layer][ResTermType.TOP][x0:x1, yidx].tolist())[0]
# series connections
if yidx == y0:
bot_vm = _bot_vm
else:
self.connect_wires([_bot_vm, prev_vm])
if yidx == y1 - 1:
top_vm = _top_vm
else:
prev_vm = _top_vm
return bot_vm, top_vm
[docs] def snake_connect_units(self, warrs: Mapping[int, Mapping[ResTermType, np.ndarray]], x0: int, x1: int,
y0: int, y1: int) -> Tuple[WireArray, WireArray]:
"""Connect all unit resistors to have snaking series connection from x0 to x1 and from y0 to y1.
Returns first and last vm_layer WireArrays."""
hm_layer = self.conn_layer + 1
vm_layer = hm_layer + 1
# across all columns, connect row units in series
for yidx in range(y0, y1 - 1):
self.connect_wires(warrs[vm_layer][ResTermType.TOP][:, yidx].tolist() +
warrs[vm_layer][ResTermType.BOT][:, yidx + 1].tolist())
# snake connect the top and bottom rows
for xidx in range(x0, x1 - 1):
if (xidx - x0) & 1:
# connect on bottom row
self.connect_wires([warrs[hm_layer][ResTermType.BOT][xidx, y0],
warrs[hm_layer][ResTermType.BOT][xidx + 1, y0]])
else:
# connect on top row
self.connect_wires([warrs[hm_layer][ResTermType.TOP][xidx, y1 - 1],
warrs[hm_layer][ResTermType.TOP][xidx + 1, y1 - 1]])
vm0 = warrs[vm_layer][ResTermType.BOT][x0, y0] # bottom left
if (x1 - x0) & 1:
vm1 = warrs[vm_layer][ResTermType.TOP][x1 - 1, y1 - 1] # top right
else:
vm1 = warrs[vm_layer][ResTermType.BOT][x1 - 1, y0] # bottom right
return vm0, vm1
[docs] def connect_dummies(self, warrs: Mapping[int, Mapping[ResTermType, np.ndarray]],
bulk_warrs: Mapping[int, Union[WireArray, Sequence[WireArray]]]) -> None:
"""Connect all the dummy resistors for the case where dummies are on the periphery."""
nx = self.place_info.nx
ny = self.place_info.ny
nx_dum = self.params['nx_dum']
ny_dum = self.params['ny_dum']
hm_layer = self.conn_layer + 1
vm_layer = hm_layer + 1
for yidx in range(ny):
for xidx in range(nx):
# connect PLUS and MINUS of dummy units to supply connections
if xidx < nx_dum or xidx >= (nx - nx_dum) or yidx < ny_dum or yidx >= (ny - ny_dum):
self.connect_to_track_wires(warrs[vm_layer][ResTermType.BOT][xidx, yidx],
bulk_warrs[hm_layer][yidx])
self.connect_to_track_wires(warrs[vm_layer][ResTermType.TOP][xidx, yidx],
bulk_warrs[hm_layer][yidx + 1])
[docs] def connect_bulk_xm(self, warrs: Mapping[int, Sequence[Union[WireArray, Sequence[WireArray]]]]
) -> Tuple[WireArray, WireArray]:
"""Connect all bulk connections to supply on xm_layer"""
# connect bulk vm_layer wires to xm_layer
hm_layer = self.conn_layer + 1
vm_layer = hm_layer + 1
xm_layer = vm_layer + 1
top_layer = self.place_info.top_layer
assert top_layer >= xm_layer, f'top_layer={top_layer} must be greater than {xm_layer}.'
w_sup_xm = self.tr_manager.get_width(xm_layer, 'sup')
xm_tidx0 = self.grid.coord_to_track(xm_layer, 0, RoundMode.NEAREST)
xm_tid0 = TrackID(xm_layer, xm_tidx0, w_sup_xm)
bot_xm = self.connect_to_tracks(warrs[vm_layer][0], xm_tid0)
xm_tidx1 = self.grid.coord_to_track(xm_layer, self.place_info.ny * self.place_info.height, RoundMode.NEAREST)
xm_tid1 = TrackID(xm_layer, xm_tidx1, w_sup_xm)
top_xm = self.connect_to_tracks(warrs[vm_layer][1], xm_tid1)
# Add pins
sup_name = 'VDD' if cast(ResBasePlaceInfo, self.place_info).res_config['sub_type_default'] == 'ntap' else 'VSS'
self.add_pin(sup_name, [bot_xm, top_xm])
return bot_xm, top_xm
[docs] def connect_hm_vm(self, sig_type: str = 'sig') -> Tuple[Mapping[int, Mapping[ResTermType, np.ndarray]],
Mapping[int, Union[WireArray, Sequence[WireArray]]]]:
"""Connect all resistor ports on hm_layer and vm_layer. BULK ports are shorted.
Returns:
1. terms:
hm_layer:
ResTermType.BOT: numpy array of all top hm_layer wires
ResTermType.TOP: numpy array of all top vm_layer wires
vm_layer:
ResTermType.BOT: numpy array of all bottom hm_layer wires
ResTermType.TOP: numpy array of all bottom vm_layer wires
2. bulk_warrs:
hm_layer: WireArray with num >= 1 of all hm_layer bulk wires
vm_layer: [bottom vm_layer WireArray with num >= 1, top vm_layer WireArray with num >= 1]
"""
nx = self.place_info.nx
ny = self.place_info.ny
conn_layer = self.place_info.conn_layer
prim_lp = self.grid.tech_info.get_lay_purp_list(conn_layer)[0]
hm_layer = conn_layer + 1
vm_layer = hm_layer + 1
w_sup_hm = self.tr_manager.get_width(hm_layer, 'sup')
w_sup_vm = self.tr_manager.get_width(vm_layer, 'sup')
w_sig_hm = self.tr_manager.get_width(hm_layer, sig_type)
w_sig_vm = self.tr_manager.get_width(vm_layer, sig_type)
bulk_warrs = {}
# Use numpy 2D arrays to support index slicing in self.connect_units() and easy conversion to list for passing
# to self.connect_wires(), self.connect_to_tracks(), etc.
terms = {
hm_layer: {
ResTermType.BOT: np.empty((nx, ny), WireArray),
ResTermType.TOP: np.empty((nx, ny), WireArray),
},
vm_layer: {
ResTermType.BOT: np.empty((nx, ny), WireArray),
ResTermType.TOP: np.empty((nx, ny), WireArray),
},
}
# get hm_layer wires for supply connections
unit_h = self.place_info.height
hm_warr_list = []
for yidx in range(ny + 1):
hm_tidx = self.grid.coord_to_track(hm_layer, unit_h * yidx, RoundMode.NEAREST)
hm_warr_list.append(self.add_wires(hm_layer, hm_tidx, self.bound_box.xl, self.bound_box.xh, width=w_sup_hm))
# connect from conn_layer to hm_layer and vm_layer
for yidx in range(ny):
# get hm_layer wires for signal connections
if yidx & 1:
top = 'MINUS'
bot = 'PLUS'
else:
top = 'PLUS'
bot = 'MINUS'
bbox_bot = self.get_device_port(0, yidx, bot)
hm_idx0 = self.grid.coord_to_track(hm_layer, bbox_bot.yl, RoundMode.NEAREST)
hm_tid0 = TrackID(hm_layer, hm_idx0, w_sig_hm)
bbox_top = self.get_device_port(0, yidx, top)
hm_idx1 = self.grid.coord_to_track(hm_layer, bbox_top.yh, RoundMode.NEAREST)
hm_tid1 = TrackID(hm_layer, hm_idx1, w_sig_hm)
for xidx in range(nx):
# connect PLUS and MINUS of every resistor unit to hm_layer signal wires
hm_warr0 = self.connect_bbox_to_tracks(Direction.LOWER, prim_lp, self.get_device_port(xidx, yidx, bot),
hm_tid0, min_len_mode=MinLenMode.MIDDLE)
terms[hm_layer][ResTermType.BOT][xidx, yidx] = hm_warr0
hm_warr1 = self.connect_bbox_to_tracks(Direction.LOWER, prim_lp, self.get_device_port(xidx, yidx, top),
hm_tid1, min_len_mode=MinLenMode.MIDDLE)
terms[hm_layer][ResTermType.TOP][xidx, yidx] = hm_warr1
# connect to vm_layer signal wires
vm_idx = self.grid.coord_to_track(vm_layer, hm_warr0.middle, RoundMode.NEAREST)
vm_tid = TrackID(vm_layer, vm_idx, w_sig_vm)
vm_warr0 = self.connect_to_tracks(hm_warr0, vm_tid, min_len_mode=MinLenMode.MIDDLE)
terms[vm_layer][ResTermType.BOT][xidx, yidx] = vm_warr0
vm_warr1 = self.connect_to_tracks(hm_warr1, vm_tid, min_len_mode=MinLenMode.MIDDLE)
terms[vm_layer][ResTermType.TOP][xidx, yidx] = vm_warr1
# connect BULK of every resistor unit, if it exists
if self.has_substrate_port:
bulk_bbox = self.get_device_port(xidx, yidx, 'BULK')
if isinstance(bulk_bbox, BBoxArray) and bulk_bbox.ny > 1:
bulk_bbox0 = bulk_bbox.get_bbox(0)
bulk_bbox1 = bulk_bbox.get_bbox(bulk_bbox.ny - 1)
if yidx & 1:
# Unit cell is placed in MX orientation in odd rows,
# so top & bottom bulk connections are flipped
bulk_bbox0, bulk_bbox1 = bulk_bbox1, bulk_bbox0
else:
bulk_bbox0 = bulk_bbox1 = bulk_bbox
self.connect_bbox_to_track_wires(Direction.LOWER, prim_lp, bulk_bbox0, hm_warr_list[yidx])
self.connect_bbox_to_track_wires(Direction.LOWER, prim_lp, bulk_bbox1, hm_warr_list[yidx + 1])
# connect bottom and top hm_layer supply wires to vm_layer supply wires
vm_tidx0 = self.grid.coord_to_track(vm_layer, 0, RoundMode.NEAREST)
vm_tidx1 = self.grid.coord_to_track(vm_layer, self.place_info.width, RoundMode.NEAREST)
vm_tid = TrackID(vm_layer, vm_tidx0, w_sup_vm, nx + 1, vm_tidx1 - vm_tidx0)
bot_vm = self.connect_to_tracks(hm_warr_list[0], vm_tid, min_len_mode=MinLenMode.MIDDLE)
top_vm = self.connect_to_tracks(hm_warr_list[-1], vm_tid, min_len_mode=MinLenMode.MIDDLE)
self.connect_to_track_wires(hm_warr_list, [bot_vm[0], top_vm[0], bot_vm[-1], top_vm[-1]])
bulk_warrs[hm_layer] = self.connect_wires(hm_warr_list)[0]
bulk_warrs[vm_layer] = [bot_vm, top_vm]
return terms, bulk_warrs
[docs] def connect_port_hm(self, port: Union[WireArray, BBox,
Sequence[WireArray, BBox]], adjust: int = 0
) -> Union[WireArray, Sequence[WireArray]]:
"""Connects a single or list of resistor port from conn_layer to hm_layer
By default, hm wire will be (nearest) centered to the port. If
the port needs to be moved, use the adjust option.
Parameters:
----------------------
port: Union[WireArray, BBox, Sequence[WireArray, BBox]]
Port in question, on conn_layer
If a sequence of ports is passed in, function is called on each one
adjust: int
If given, this parameter is passed to get_next_track to get
the track id `adjust` tracks away.
Returns the WireArray or list of WireArrays on hm_layer
"""
if isinstance(port, Sequence):
return [self.connect_port_hm(p, adjust) for p in port]
conn_layer = self.place_info.conn_layer
prim_lp = self.grid.tech_info.get_lay_purp_list(conn_layer)[0]
hm_layer = conn_layer + 1
w_sig_hm = self.tr_manager.get_width(hm_layer, 'sig')
wire_bbox: BBox = port if isinstance(port, BBox) else port.bound_box
bbox_ym = wire_bbox.ym
near_tidx = self.grid.coord_to_track(hm_layer, bbox_ym, RoundMode.NEAREST)
if adjust:
near_tidx = self.tr_manager.get_next_track(hm_layer, near_tidx, 'sig', 'sig', up=adjust)
port_tid = TrackID(hm_layer, near_tidx, w_sig_hm)
if isinstance(port, BBox):
port_hm = self.connect_bbox_to_tracks(Direction.LOWER, prim_lp, wire_bbox, port_tid)
else:
port_hm = self.connect_to_tracks(port, port_tid)
return port_hm