# 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
from typing import Any, Optional, Tuple, Union, Mapping, List, Sequence, Dict, Iterable
from pathlib import Path
from bisect import bisect_right
from pybag.enum import RoundMode, Orient2D
from bag.math import lcm
from bag.util.math import HalfInt
from bag.util.immutable import ImmutableList, ImmutableSortedDict, Param, combine_hash
from bag.io.file import read_yaml, write_yaml
from bag.layout.tech import TechInfo
from bag.layout.routing.base import TrackManager, WDictType, SpDictType, TrackID
from bag.layout.routing.grid import RoutingGrid
from ...enum import MOSWireType, Alignment
from ...wires import WireGraph, WireLookup, WireData
from ..data import MOSRowSpecs, RowPlaceInfo, ExtWidthInfo
from ..tech import MOSTech
from .compact import place_rows_compact
[docs]class MOSArrayPlaceInfo:
"""A class that stores layout information of a transistor array island.
This class stores information common to all tiles within the array.
Parameters
----------
parent_grid : RoutingGrid
the RoutingGrid object.
lch : int
the transistor channel length.
tr_widths : WDictType
dictionary from wire types to its width on each layer.
tr_spaces : SpDictType
dictionary from wire types to its spaces on each layer.
top_layer : Optional[int]
the top layer ID.
conn_layer : Optional[int]
the connection layer ID.
half_space : bool
True to allow half-track spaces.
arr_options : Optional[Mapping[str, Any]]
Process-specific options for the entire array.
"""
def __init__(self, parent_grid: RoutingGrid, lch: int, tr_widths: WDictType,
tr_spaces: SpDictType, *, top_layer: Optional[int] = None,
conn_layer: Optional[int] = None, half_space: bool = True,
arr_options: Optional[Mapping[str, Any]] = None) -> None:
arr_options = ImmutableSortedDict(arr_options)
self._tech_cls: MOSTech = parent_grid.tech_info.get_device_tech('mos', lch=lch,
arr_options=arr_options)
# update routing grid
if conn_layer is None:
conn_layer = self._tech_cls.conn_layer
if top_layer is None:
top_layer = conn_layer + 1
tr_specs = self._tech_cls.get_track_specs(conn_layer, top_layer)
grid: RoutingGrid = parent_grid.get_copy_with(tr_specs=tr_specs)
# set attributes
self._tr_manager = TrackManager(grid, tr_widths, tr_spaces, half_space=half_space)
self._lch = lch
self._conn_layer = conn_layer
self._top_layer = top_layer
self._half_space = half_space
self._arr_options: Param = arr_options
# compute hash
seed = self._lch
seed = combine_hash(seed, hash(self._tr_manager))
seed = combine_hash(seed, self._conn_layer)
seed = combine_hash(seed, self._top_layer)
seed = combine_hash(seed, self._half_space)
seed = combine_hash(seed, hash(self._arr_options))
self._hash = seed
[docs] def __hash__(self) -> int:
return self._hash
[docs] def __eq__(self, other: Any) -> bool:
return (isinstance(other, MOSArrayPlaceInfo) and
self._lch == other._lch and
self._conn_layer == other._conn_layer and
self._top_layer == other._top_layer and
self._half_space == other._half_space and
self._tr_manager == other._tr_manager and
self._arr_options == other._arr_options)
@classmethod
[docs] def get_conn_layer(cls, tech_info: TechInfo, lch: int,
arr_options: Optional[Mapping[str, Any]] = None) -> int:
if arr_options is None:
arr_options = {}
return tech_info.get_device_tech('mos', lch=lch, arr_options=arr_options).conn_layer
@classmethod
[docs] def make_array_info(cls, grid: RoutingGrid, val: Mapping[str, Any]) -> MOSArrayPlaceInfo:
return MOSArrayPlaceInfo(grid, val['lch'], val['tr_widths'], val['tr_spaces'],
top_layer=val.get('top_layer', None),
conn_layer=val.get('conn_layer', None),
half_space=val.get('half_space', True),
arr_options=val.get('arr_options', None))
@property
[docs] def grid(self) -> RoutingGrid:
return self._tr_manager.grid
@property
[docs] def tr_manager(self) -> TrackManager:
"""TrackManager: The TrackManager object."""
return self._tr_manager
@property
[docs] def tech_cls(self) -> MOSTech:
"""MOSTech: The primitive technology object."""
return self._tech_cls
@property
[docs] def lch(self) -> int:
"""int: The channel length."""
return self._lch
@property
[docs] def top_layer(self) -> int:
"""int: The top layer ID."""
return self._top_layer
@property
[docs] def conn_layer(self) -> int:
"""int: The transistor port layer ID."""
return self._conn_layer
@property
[docs] def sd_pitch(self) -> int:
return self._tech_cls.sd_pitch
@property
[docs] def half_space(self) -> int:
return self._half_space
@property
[docs] def arr_options(self) -> Param:
return self._arr_options
[docs] def get_tile_blk_h(self, half_blk: bool = True) -> int:
return lcm([self._tr_manager.grid.get_block_size(self._top_layer, half_blk_y=half_blk)[1],
self._tech_cls.blk_h_pitch])
[docs] def get_source_track(self, col_idx: int) -> HalfInt:
return HalfInt(self.grid.coord_to_htr(self.conn_layer, col_idx * self.sd_pitch,
RoundMode.NONE, False))
[docs] def get_source_track_col(self, track_index: HalfInt) -> int:
return self.coord_to_col(self.grid.track_to_coord(self.conn_layer, track_index))
[docs] def coord_to_col(self, coord: int, round_mode: RoundMode = RoundMode.NONE) -> int:
q, r = divmod(coord, self.sd_pitch)
if r != 0:
if round_mode is RoundMode.NONE:
raise ValueError(f'Coordinate {coord} is not on a column')
if round_mode < 0:
return q
if round_mode > 0:
return q + 1
if r < self.sd_pitch // 2:
return q
return q + 1
if round_mode is RoundMode.LESS:
return q - 1
if round_mode is RoundMode.GREATER:
return q + 1
return q
[docs] def col_to_coord(self, col: int) -> int:
return self.sd_pitch * col
[docs] def col_to_track(self, layer: int, col: int,
mode: RoundMode = RoundMode.NONE, even: bool = False) -> HalfInt:
coord = self.col_to_coord(col)
return self.grid.coord_to_track(layer, coord, mode=mode, even=even)
[docs] def track_to_col(self, layer: int, track_index: HalfInt, mode: RoundMode = RoundMode.NEAREST
) -> int:
coord = self.grid.track_to_coord(layer, track_index)
return self.coord_to_col(coord, round_mode=mode)
[docs] def get_column_span(self, vm_layer: int, num_tracks: HalfInt) -> int:
"""Returns the minimum number of columns to fit given number of tracks.
Parameters
----------
vm_layer : int
the vertical routing layer ID.
num_tracks : HalfInt
number of tracks.
Returns
-------
num_col : int
number of MOSBase columns.
"""
return self.coord_to_col(self.grid.track_to_coord(vm_layer, num_tracks),
round_mode=RoundMode.GREATER_EQ)
[docs] def get_block_ncol(self, vm_layer: int, half_blk: bool = False) -> int:
"""Returns the number of columns in a unit block for the given vertical routing layer.
this is the number of columns of the smallest unit block that you can tile and guarantee
that the wires on the given vertical layers will stay on grid.
Parameters
----------
vm_layer : int
a vertical routing layer ID.
half_blk : bool
True to allow half-block size. Defaults to False.
"""
grid = self.grid
top_layer = self.top_layer
if grid.get_direction(vm_layer) != Orient2D.y:
raise ValueError(f'layer {vm_layer} is not a vertical routing layer.')
if vm_layer > top_layer:
raise ValueError(f'this method only works on layers <= {top_layer}')
tr_pitch = self.grid.get_track_pitch(vm_layer)
if half_blk:
tr_pitch //= 2
sd_pitch = self.sd_pitch
return lcm([tr_pitch, sd_pitch]) // sd_pitch
[docs] def round_up_to_block_size(self, vm_layer: int, ncol: int,
even_diff: bool = False, half_blk: bool = False) -> int:
blk_ncol = self.get_block_ncol(vm_layer, half_blk=half_blk)
new_ncol = -(-ncol // blk_ncol) * blk_ncol
if even_diff and (new_ncol - ncol) & 1:
if blk_ncol & 1:
return new_ncol + blk_ncol
raise ValueError('Cannot round up to block size by appending even number of columns.')
return new_ncol
[docs]def make_pinfo_compact(arr_info: MOSArrayPlaceInfo, row_specs: Sequence[MOSRowSpecs],
bot_mirror: bool, top_mirror: bool, name: str = '', min_height: int = 0,
tile_options: Optional[Param] = None, priority: int = 0,
wire_graphs: Optional[Dict[int, WireGraph]] = None) -> MOSBasePlaceInfo:
if tile_options is None:
tile_options = ImmutableSortedDict()
if wire_graphs is None:
wire_graphs = {}
# set info object
tr_manager = arr_info.tr_manager
tmp = place_rows_compact(tr_manager, arr_info.tech_cls, row_specs, min_height,
arr_info.get_tile_blk_h(), bot_mirror, top_mirror, tile_options)
rp_list, rg_list = tmp
wire_lookup = {}
for lay, wg in wire_graphs.items():
wg.align_wires(lay, tr_manager, rp_list[0].yb, rp_list[-1].yt)
wire_lookup[lay] = wg.get_wire_lookup()
return MOSBasePlaceInfo(name, arr_info, rp_list, bot_mirror, top_mirror, tile_options,
rg_list=rg_list, priority=priority, wire_lookup=wire_lookup)
[docs]def make_pinfo_compact_specs(arr_info: MOSArrayPlaceInfo, name: str, specs: Mapping[str, Any]
) -> MOSBasePlaceInfo:
row_specs: List[Mapping[str, Any]] = specs['row_specs']
min_height: int = specs.get('min_height', 0)
options: Mapping[str, Any] = specs.get('options', {})
bot_mirror: bool = specs.get('bot_mirror', True)
top_mirror: bool = specs.get('top_mirror', True)
priority: int = specs.get('priority', 0)
wire_specs: Mapping[int, Any] = specs.get('wire_specs', {})
top_layer = arr_info.top_layer
tr_manager = arr_info.tr_manager
grid = tr_manager.grid
wire_graphs = {}
for layer, wd_specs in wire_specs.items():
if not grid.is_horizontal(layer):
raise ValueError('Can only specify horizontal routing layers in wire_specs.')
if layer > top_layer:
raise ValueError(f'Cannot specify wires on layer={layer} > top_layer={top_layer}')
wd = WireData.make_wire_data(wd_specs, Alignment.CENTER_COMPACT, '')
wg = WireGraph.make_wire_graph(layer, tr_manager, wd)
wg.place_compact(layer, tr_manager, lower=0)
min_height = max(min_height, wg.upper)
wire_graphs[layer] = wg
rs_list = [MOSRowSpecs.make_row_specs(v) for v in row_specs]
return make_pinfo_compact(arr_info, rs_list, bot_mirror, top_mirror, name=name,
min_height=min_height, tile_options=ImmutableSortedDict(options),
priority=priority, wire_graphs=wire_graphs)
[docs]class MOSBasePlaceInfo:
"""A class that stores layout information of a single tile in MOSBase.
Parameters
----------
name : str
name of this tile.
arr_info : MOSArrayPlaceInfo
the transistor array information object.
rp_list : ImmutableList[RowPlaceInfo]
list of placement information of each transistor row in this tile.
bot_mirror : bool
True to satisfy mirror placement constraint on the bottom edge.
top_mirror : bool
True to satisfy mirror placement constraint on the top edge.
options : Param
process-specific options for this tile.
rg_list : Optional[List[Tuple[WireGraph, WireGraph]]]
list of wire graph objects for each transistor row.
optional. Only use for debugging/visualization purposes.
"""
def __init__(self, name: str, arr_info: MOSArrayPlaceInfo, rp_list: ImmutableList[RowPlaceInfo],
bot_mirror: bool, top_mirror: bool, options: Param,
rg_list: Optional[List[Tuple[WireGraph, WireGraph]]] = None,
priority: int = 0, wire_lookup: Optional[Dict[int, WireLookup]] = None) -> None:
if not name:
name = 'unnamed'
self._name = name
self._arr_info = arr_info
self._pinfo_list = rp_list
self._options = options
self._bot_mirror = bot_mirror
self._top_mirror = top_mirror
self._row_graph_list = rg_list
self._priority = priority
self._wire_lookup = ImmutableSortedDict(wire_lookup)
# compute hash
seed = hash(self._arr_info)
seed = combine_hash(seed, hash(self._name))
seed = combine_hash(seed, hash(self._pinfo_list))
seed = combine_hash(seed, hash(self._options))
self._hash = seed
[docs] def __hash__(self) -> int:
return self._hash
[docs] def __eq__(self, other: Any) -> bool:
return (isinstance(other, MOSBasePlaceInfo) and
self._name == other._name and
self._arr_info == other._arr_info and
self._pinfo_list == other._pinfo_list and
self._options == other._options and
self._wire_lookup == other._wire_lookup)
@classmethod
[docs] def get_conn_layer(cls, tech_info: TechInfo, lch: int) -> int:
return MOSArrayPlaceInfo.get_conn_layer(tech_info, lch)
@classmethod
[docs] def make_place_info(cls, grid: RoutingGrid,
val: Union[MOSBasePlaceInfo,
Tuple[Union[MOSBasePlaceInfo, TilePattern], TileInfoTable],
Mapping[str, Any]],
name: str = ''
) -> Union[MOSBasePlaceInfo,
Tuple[Union[MOSBasePlaceInfo, TilePattern], TileInfoTable]]:
if isinstance(val, MOSBasePlaceInfo) or isinstance(val, tuple):
return val
root_dir: str = val.get('root_dir', '')
tile_specs: Mapping[str, Any] = val.get('tile_specs', {})
if root_dir:
info_table = TileInfoTable.load(grid, Path(root_dir))
return info_table.make_place_info(val), info_table
elif tile_specs:
info_table = TileInfoTable.make_tiles(grid, tile_specs)
return info_table.make_place_info(val), info_table
else:
# specs for MOSBasePlaceInfo
arr_info = MOSArrayPlaceInfo.make_array_info(grid, val)
pinfo = make_pinfo_compact_specs(arr_info, name, val)
return pinfo
@property
[docs] def name(self) -> str:
return self._name
@property
[docs] def num_rows(self) -> int:
return len(self._pinfo_list)
@property
[docs] def height(self) -> int:
return self._pinfo_list[-1].yt
@property
[docs] def true_height(self) -> int:
return self._pinfo_list[-1].yt_blk - self._pinfo_list[0].yb_blk
@property
[docs] def ext_h_bot(self) -> int:
tmp = self._pinfo_list[0]
return tmp.yb_blk - tmp.yb
@property
[docs] def ext_h_top(self) -> int:
tmp = self._pinfo_list[-1]
return tmp.yt - tmp.yt_blk
@property
[docs] def extend_priority(self) -> int:
return self._priority
@property
[docs] def wire_lookup(self) -> ImmutableSortedDict[int, WireLookup]:
return self._wire_lookup
@property
[docs] def tile_options(self) -> Param:
return self._options
@property
[docs] def arr_info(self) -> MOSArrayPlaceInfo:
return self._arr_info
@property
[docs] def grid(self) -> RoutingGrid:
return self._arr_info.grid
@property
[docs] def tr_manager(self) -> TrackManager:
return self._arr_info.tr_manager
@property
[docs] def lch(self) -> int:
return self._arr_info.lch
@property
[docs] def top_layer(self) -> int:
return self._arr_info.top_layer
@property
[docs] def conn_layer(self) -> int:
return self._arr_info.conn_layer
@property
[docs] def tech_cls(self) -> MOSTech:
return self._arr_info.tech_cls
@property
[docs] def sd_pitch(self) -> int:
return self._arr_info.sd_pitch
@property
[docs] def is_complementary(self) -> bool:
is_pwell = False
is_nwell = False
for pinfo in self._pinfo_list:
if pinfo.row_info.row_type.is_pwell:
is_pwell = True
else:
is_nwell = True
if is_pwell and is_nwell:
return True
return False
[docs] def get_mirror(self, top_edge: bool) -> bool:
"""Returns True if the specify edge satisfies mirror placement constraint."""
return (top_edge and self._top_mirror) or ((not top_edge) and self._bot_mirror)
[docs] def get_row_place_info(self, row_idx: int) -> RowPlaceInfo:
return self._pinfo_list[row_idx]
[docs] def get_hm_track_info(self, hm_layer: int, wire_name: str, wire_idx: int = 0
) -> Tuple[HalfInt, int]:
return self._wire_lookup[hm_layer].get_track_info(wire_name, wire_idx)
[docs] def get_abut_info(self, rhs: MOSBasePlaceInfo, top_edge: bool, rhs_top_edge: bool,
shared: Sequence[str], rhs_shared: Sequence[str]
) -> Tuple[int, ExtWidthInfo, int, int]:
"""Returns the margin needed to abut this tile with the given tile.
Parameters
----------
rhs : MOSBasePlaceInfo
the other tile.
top_edge : bool
True if rhs is abutting to top edge of this tile.
rhs_top_edge : bool
True if we're abutting to top edge of the other tile.
shared: Sequence[str]
list of edge wires shared with the other tile.
rhs_shared : Sequence[str]
list of edge wires from the other tile shared with this tile.
Returns
-------
margin : int
the margin in resolution units.
ext_w_info : ExtWidthInfo
the ExtWidthInfo object.
em1 : int
the existing extension margin (in resolution units).
em2 : int
the existing extension margin on the other tile (in resolution units).
"""
tech_cls = self.tech_cls
blk_h_pitch = tech_cls.blk_h_pitch
idx = int(top_edge) * (self.num_rows - 1)
rhs_idx = int(rhs_top_edge) * (rhs.num_rows - 1)
rpinfo = self.get_row_place_info(idx)
rhs_rpinfo = rhs.get_row_place_info(rhs_idx)
wmargin, einfo1, einfo2 = rpinfo.get_abut_info(rhs_rpinfo, top_edge, rhs_top_edge,
shared, rhs_shared,
self.tr_manager, self.conn_layer + 1)
emargin1 = rpinfo.get_ext_margin(top_edge)
emargin2 = rhs_rpinfo.get_ext_margin(rhs_top_edge)
emargin_tot = emargin1 + emargin2
ext_w_min = -(-(emargin_tot + wmargin) // blk_h_pitch)
ext_w_info = tech_cls.get_ext_width_info(einfo1, einfo2)
ext_w = ext_w_info.get_next_width(ext_w_min)
return blk_h_pitch * ext_w - emargin_tot, ext_w_info, emargin1, emargin2
[docs] def get_source_track(self, col_idx: int) -> HalfInt:
return self._arr_info.get_source_track(col_idx)
[docs] def get_source_track_col(self, track_index: HalfInt) -> int:
return self._arr_info.get_source_track_col(track_index)
[docs] def coord_to_col(self, coord: int, round_mode: RoundMode = RoundMode.NONE) -> int:
return self._arr_info.coord_to_col(coord, round_mode=round_mode)
[docs] def col_to_coord(self, col: int) -> int:
return self._arr_info.col_to_coord(col)
[docs] def get_column_span(self, vm_layer: int, num_tracks: HalfInt) -> int:
return self._arr_info.get_column_span(vm_layer, num_tracks)
[docs] def get_block_ncol(self, vm_layer: int) -> int:
return self._arr_info.get_block_ncol(vm_layer)
[docs] def show_wire_graph(self, row_idx: int, top_or_bot: str = 'bot') -> None:
if self._row_graph_list is None:
raise ValueError('WireGraph objects not stored.')
from networkx.drawing import layout, draw_networkx
import matplotlib.pyplot as plt
dir_index = 0 if top_or_bot == 'bot' else 1
graph = self._row_graph_list[row_idx][dir_index].graph
pos = layout.kamada_kawai_layout(graph)
draw_networkx(graph, pos)
plt.title(f'row: {row_idx}, {top_or_bot} side')
plt.show()
[docs] def get_extend(self, margin: int, top_edge: bool, ext_w_info: ExtWidthInfo, cur_em: int,
other_em: int, shared: Sequence[str], max_iter: int = 1000) -> MOSBasePlaceInfo:
blk_h_pitch = self.tech_cls.blk_h_pitch
tot_h_pitch = self.grid.get_block_size(self.top_layer, half_blk_y=True)[1]
em_tot = cur_em + other_em
for iter_idx in range(max_iter):
cur_ext_w = -(-(em_tot + margin) // blk_h_pitch)
next_ext_w = ext_w_info.get_next_width(cur_ext_w)
ext_dim = next_ext_w * blk_h_pitch - em_tot
q, r = divmod(ext_dim, tot_h_pitch)
if r == 0:
# we can extension safely and satisfy height quantization
return self._get_extend_helper(ext_dim, top_edge, shared)
else:
# need more margin
margin = (q + 1) * tot_h_pitch
raise ValueError('Fail to extend tile with height quantization constraint in '
f'{max_iter} iterations')
[docs] def _get_extend_helper(self, delta: int, top_edge: bool, shared: Sequence[str]
) -> MOSBasePlaceInfo:
tr_pitch = self.grid.get_track_pitch(self.conn_layer + 1)
new_info_list: List[RowPlaceInfo] = self._pinfo_list.to_list()
if top_edge:
new_info_list[-1] = new_info_list[-1].get_extend(tr_pitch, delta, True, shared)
else:
new_info_list[0] = new_info_list[0].get_extend(tr_pitch, delta, False, shared)
for idx in range(1, len(new_info_list)):
new_info_list[idx] = new_info_list[idx].get_move(tr_pitch, delta)
return MOSBasePlaceInfo(self._name, self._arr_info, ImmutableList(new_info_list),
self._bot_mirror, self._top_mirror, self._options,
rg_list=self._row_graph_list, priority=self._priority)
[docs]class TilePatternElement:
"""A single element inside a tile pattern.
A tile pattern element consists of a single tile or a tile pattern repeated multiple times,
with possible mirroring.
NOTE: the multiplier parameter is only used to compute num_rows, num_tiles, and height
properties. All other methods assumes that the unit element is repeated indefinitely.
This design choice allows us to use one class to represent both a given element that repeats
a finite amount, or a dynamically growing repeating tile pattern.
"""
def __init__(self, info: Union[MOSBasePlaceInfo, TilePattern],
mirror: bool = True, flip: bool = False, mult: int = 1) -> None:
self._info = info
self._mirror = mirror
self._flip = flip
self._mult = mult
# compute hash
seed = self._mult
seed = combine_hash(seed, hash(self._mirror))
seed = combine_hash(seed, hash(self._flip))
seed = combine_hash(seed, hash(self._info))
self._hash = seed
[docs] def __hash__(self) -> int:
return self._hash
[docs] def __eq__(self, other: Any) -> bool:
return (isinstance(other, TilePatternElement) and
self._info == other._info and
self._mirror == other._mirror and
self._flip == other._flip and
self._mult == other._mult)
[docs] def __bool__(self) -> bool:
return isinstance(self._info, MOSBasePlaceInfo) or bool(self._info)
@property
[docs] def name(self) -> str:
if isinstance(self._info, MOSBasePlaceInfo):
return self._info.name
return ''
@property
[docs] def arr_info(self) -> MOSArrayPlaceInfo:
return self._info.arr_info
@property
[docs] def num_rows(self) -> int:
return self._info.num_rows * self._mult
@property
[docs] def num_tiles(self) -> int:
return self.num_tiles_unit * self._mult
@property
[docs] def num_tiles_unit(self) -> int:
return 1 if isinstance(self._info, MOSBasePlaceInfo) else self._info.num_tiles
@property
[docs] def height(self) -> int:
return self._info.height * self._mult
@property
[docs] def mult(self) -> int:
return self._mult
@classmethod
[docs] def make_element(cls, table: Mapping[str, MOSBasePlaceInfo],
specs: Union[str, Mapping[str, Any]]) -> TilePatternElement:
if isinstance(specs, str):
# A MOSBasePlaceInfo element
return TilePatternElement(table[specs])
mirror: bool = specs.get('mirror', True)
flip: bool = specs.get('flip', False)
mult: int = specs.get('mult', 1)
name: str = specs.get('name', '')
if name:
# A MOSBasePlaceInfo element
return TilePatternElement(table[name], mirror=mirror, flip=flip, mult=mult)
else:
return TilePatternElement(TilePattern.make_pattern(table, specs['tiles']),
mirror=mirror, flip=flip, mult=mult)
[docs] def get_flip_unit(self, unit_idx: int) -> bool:
return ((unit_idx & 1) and self._mirror) ^ self._flip
[docs] def num_tiles_to_rows(self, num_tiles: int) -> int:
if isinstance(self._info, MOSBasePlaceInfo):
return num_tiles * self._info.num_rows
elif num_tiles == 0:
return 0
else:
pat_ntile = self._info.num_tiles
q, r = divmod(num_tiles, pat_ntile)
ans = q * self._info.num_rows
flip_pat = self.get_flip_unit(q)
if flip_pat:
r = pat_ntile - r
return ans + self._info.num_rows - self._info.num_tiles_to_rows(r)
else:
return ans + self._info.num_tiles_to_rows(r)
[docs] def get_tile_info(self, tile_idx: int) -> Tuple[MOSBasePlaceInfo, int, bool]:
"""Returns the tile information for the given tile index.
Parameters
----------
tile_idx : int
the tile index.
Returns
-------
pinfo : MOSBasePlaceInfo
the tile layout information object.
y0 : int
the bottom Y coordinate of the tile.
flip : bool
True if this tile is flipped.
"""
if isinstance(self._info, MOSBasePlaceInfo):
y0 = tile_idx * self._info.height
return self._info, y0, self.get_flip_unit(tile_idx)
else:
pat_ntile = self._info.num_tiles
pat_height = self._info.height
q, r = divmod(tile_idx, pat_ntile)
flip_pat = self.get_flip_unit(q)
if flip_pat:
r = pat_ntile - 1 - r
ans, y0, flip = self._info.get_tile_info(r)
if flip_pat:
y0 = pat_height - y0 - ans.height
y0 += q * pat_height
return ans, y0, flip ^ flip_pat
[docs] def get_tile_pinfo(self, tile_idx: int) -> MOSBasePlaceInfo:
return self.get_tile_info(tile_idx)[0]
[docs] def get_hm_track_info(self, hm_layer: int, wire_name: str, wire_idx: int = 0, *,
tile_idx: int = 0) -> Tuple[HalfInt, int]:
grid = self.arr_info.grid
pinfo, y0, flip_tile = self.get_tile_info(tile_idx)
tr_idx, tr_w = pinfo.get_hm_track_info(hm_layer, wire_name, wire_idx)
if flip_tile:
y0 += pinfo.height - grid.track_to_coord(hm_layer, tr_idx)
else:
y0 += grid.track_to_coord(hm_layer, tr_idx)
return grid.coord_to_track(hm_layer, y0), tr_w
[docs] def get_hm_track_id(self, hm_layer: int, wire_name: str, wire_idx: int = 0, *,
tile_idx: int = 0) -> TrackID:
tr_idx, tr_w = self.get_hm_track_info(hm_layer, wire_name, wire_idx=wire_idx,
tile_idx=tile_idx)
return TrackID(hm_layer, tr_idx, width=tr_w, grid=self.arr_info.grid)
[docs] def get_hm_track_index(self, hm_layer: int, wire_name: str, wire_idx: int = 0, *,
tile_idx: int = 0) -> HalfInt:
return self.get_hm_track_info(hm_layer, wire_name, wire_idx=wire_idx, tile_idx=tile_idx)[0]
[docs] def get_track_info(self, row_idx: int, wire_type: Union[MOSWireType, bool], wire_name: str,
wire_idx: int = 0, *, tile_idx: int = 0) -> Tuple[HalfInt, int]:
ainfo = self.arr_info
grid = ainfo.grid
hm_layer = ainfo.conn_layer + 1
wlookup, yoff, sign = self._get_wire_info(row_idx, wire_type, tile_idx)
tr_idx, tr_w = wlookup.get_track_info(wire_name, wire_idx)
y0 = yoff + sign * grid.track_to_coord(hm_layer, tr_idx)
return grid.coord_to_track(hm_layer, y0), tr_w
[docs] def get_track_index(self, row_idx: int, wire_type: Union[MOSWireType, bool], wire_name: str,
wire_idx: int = 0, *, tile_idx: int = 0) -> HalfInt:
return self.get_track_info(row_idx, wire_type, wire_name, wire_idx=wire_idx,
tile_idx=tile_idx)[0]
[docs] def get_track_id(self, row_idx: int, wire_type: Union[MOSWireType, bool], wire_name: str,
wire_idx: int = 0, *, tile_idx: int = 0) -> TrackID:
tr_idx, tr_w = self.get_track_info(row_idx, wire_type, wire_name, wire_idx=wire_idx,
tile_idx=tile_idx)
arr_info = self.arr_info
return TrackID(arr_info.conn_layer + 1, tr_idx, width=tr_w, grid=arr_info.grid)
[docs] def get_wire_range(self, row_idx: int, wire_type: Union[MOSWireType, bool], wire_name: str,
*, tile_idx: int = 0) -> Tuple[int, int]:
wlookup = self._get_wire_info(row_idx, wire_type, tile_idx)[0]
return wlookup.get_wire_range(wire_name)
[docs] def get_num_wires(self, row_idx: int, wire_type: Union[MOSWireType, bool], wire_name: str,
*, tile_idx: int = 0) -> int:
lower, upper = self.get_wire_range(row_idx, wire_type, wire_name, tile_idx=tile_idx)
return upper - lower
[docs] def get_flat_row_idx_and_flip(self, tile_idx: int, row_idx: int) -> Tuple[int, bool]:
ans = self.num_tiles_to_rows(tile_idx)
info, _, flip = self.get_tile_info(tile_idx)
if flip:
return ans + info.num_rows - row_idx - 1, flip
else:
return ans + row_idx, flip
[docs] def flat_row_to_tile_row(self, flat_row_idx: int) -> Tuple[int, int]:
if isinstance(self._info, MOSBasePlaceInfo):
num_rows = self._info.num_rows
tile_idx, row_idx = divmod(flat_row_idx, num_rows)
if self.get_flip_unit(tile_idx):
row_idx = num_rows - 1 - row_idx
return tile_idx, row_idx
else:
pat_nrow = self._info.num_rows
q, r = divmod(flat_row_idx, pat_nrow)
flip_pat = self.get_flip_unit(q)
if flip_pat:
r = pat_nrow - 1 - r
tile_idx, row_idx = self._info.flat_row_to_tile_row(r)
tile_idx += q * self._info.num_tiles
return tile_idx, row_idx
[docs] def get_sub_pattern_element(self, num_tiles: int, mult: int, mirror: bool, flip: bool,
start_idx: int = 0) -> TilePatternElement:
"""Returns a repeated sub-pattern (analogous to substrings).
"""
start_idx = start_idx % self.num_tiles_unit
if start_idx == 0:
return self._get_sub_pattern_element_helper(num_tiles, mult, mirror, flip)
# self._info must be TilePattern
num0 = self.num_tiles_unit - start_idx
if num_tiles <= num0:
return self._info.get_sub_pattern_element(num_tiles, mult, mirror, flip,
start_idx=start_idx)
num1 = num_tiles - num0
ele_list = [self._info.get_sub_pattern_element(num0, 1, False, False, start_idx=start_idx),
self._get_sub_pattern_element_helper(num1, 1, False, False)]
return TilePatternElement(TilePattern(ele_list), mirror=mirror, flip=flip, mult=mult)
[docs] def get_reverse(self, mult: int) -> TilePatternElement:
flip_last = self.get_flip_unit(mult - 1)
return TilePatternElement(self._info, mirror=self._mirror, flip=not flip_last, mult=mult)
[docs] def append_sub_pattern(self, ele_list: List[TilePatternElement], num_tiles: int,
flip_element: bool) -> None:
q, r = divmod(num_tiles, self.num_tiles_unit)
if flip_element:
flip_q = self.get_flip_unit(self._mult - 1)
flip_r = not self.get_flip_unit(self._mult - 1 - q)
else:
flip_q = self._flip
flip_r = self.get_flip_unit(q)
if q > 0:
ele_list.append(TilePatternElement(self._info, mirror=self._mirror,
flip=flip_q, mult=q))
if r > 0:
self._info.append_sub_pattern(ele_list, r, flip_r)
[docs] def _get_sub_pattern_element_helper(self, num_tiles: int, mult: int, mirror: bool, flip: bool
) -> TilePatternElement:
if num_tiles == 0:
raise ValueError('Must have positive number of tiles.')
mirror = mirror and (mult > 1)
q, r = divmod(num_tiles, self.num_tiles_unit)
q_mirror = self._mirror and (q > 1)
if r == 0:
# we have two nested repeated element, see if we can collapse
if q == 1:
return TilePatternElement(self._info, mirror=mirror,
flip=flip ^ self._flip, mult=mult)
elif mult == 1:
if flip:
return self.get_reverse(q)
else:
return TilePatternElement(self._info, mirror=q_mirror,
flip=self._flip, mult=q)
elif q_mirror == mirror:
if mirror and flip:
flip0 = self._flip ^ (q & 1)
else:
flip0 = self._flip ^ flip
return TilePatternElement(self._info, mirror=mirror, flip=flip0, mult=q * mult)
elif mirror or (q & 1):
# nested tile pattern
ele_inner = TilePatternElement(self._info, mirror=q_mirror, flip=self._flip,
mult=q)
return TilePatternElement(TilePattern([ele_inner]), mirror=mirror, flip=flip,
mult=mult)
else:
# here if q_mirror = True, mirror = False, q is even
return TilePatternElement(self._info, mirror=True, flip=self._flip, mult=q * mult)
else:
ele_list = []
if q > 0:
ele_list.append(TilePatternElement(self._info, mirror=q_mirror,
flip=self._flip, mult=q))
flip_last = self.get_flip_unit(q)
self._info.append_sub_pattern(ele_list, r, flip_last)
return TilePatternElement(TilePattern(ele_list), mirror=mirror, flip=flip, mult=mult)
[docs] def _get_wire_info(self, row_idx: int, wire_type: Union[MOSWireType, bool], tile_idx: int
) -> Tuple[WireLookup, int, int]:
if tile_idx < 0:
raise ValueError(f'tile_idx cannot be negative.')
pinfo, yb, flip_tile = self.get_tile_info(tile_idx)
if row_idx < 0:
row_idx += pinfo.num_rows
if row_idx < 0:
raise ValueError(f'Invalid row after wrapping negative: {row_idx}')
rpinfo = pinfo.get_row_place_info(row_idx)
if isinstance(wire_type, bool):
top = wire_type
wlookup = rpinfo.top_wires if top else rpinfo.bot_wires
else:
if rpinfo.row_info.double_gate:
flip_row = rpinfo.row_info.flip
if not (wire_type.is_gate or wire_type.is_gate2):
wlookup = rpinfo.mid_wires
else:
top = (wire_type.is_gate == flip_row)
wlookup = rpinfo.top_wires if top else rpinfo.bot_wires
else:
flip_row = rpinfo.row_info.flip
top = (wire_type.is_gate == flip_row)
wlookup = rpinfo.top_wires if top else rpinfo.bot_wires
if flip_tile:
return wlookup, yb + pinfo.height, -1
else:
return wlookup, yb, 1
[docs]class TilePattern:
"""A recursive list of tile layout information objects.
Used to represent repeating/fractal tile patterns in a transistor array.
"""
def __init__(self, data: List[TilePatternElement]) -> None:
self._pat_list = ImmutableList(data)
self._nrow_list: List[int] = [0]
self._ntile_list: List[int] = [0]
self._dy_list: List[int] = [0]
seed = cum_tile = cum_h = cum_row = 0
for obj in data:
seed = combine_hash(seed, hash(obj))
cum_row += obj.num_rows
cum_tile += obj.num_tiles
cum_h += obj.height
self._nrow_list.append(cum_row)
self._ntile_list.append(cum_tile)
self._dy_list.append(cum_h)
[docs] def __hash__(self) -> int:
return hash(self._pat_list)
[docs] def __eq__(self, other: Any) -> bool:
return (isinstance(other, TilePattern) and
self._pat_list == other._pat_list)
[docs] def __bool__(self) -> bool:
return bool(self._pat_list)
@property
[docs] def arr_info(self) -> MOSArrayPlaceInfo:
return self._pat_list[0].arr_info
@property
[docs] def num_rows(self) -> int:
return self._nrow_list[-1]
@property
[docs] def num_tiles(self) -> int:
return self._ntile_list[-1]
@property
[docs] def height(self) -> int:
return self._dy_list[-1]
@classmethod
[docs] def make_pattern(cls, table: Mapping[str, MOSBasePlaceInfo],
spec_list: Iterable[Mapping[str, Any]]
) -> TilePattern:
tile_list = [TilePatternElement.make_element(table, element) for element in spec_list]
return TilePattern(tile_list)
[docs] def get_element_idx(self, tile_idx: int) -> int:
return bisect_right(self._ntile_list, tile_idx) - 1
[docs] def num_tiles_to_rows(self, num_tiles: int) -> int:
if num_tiles == 0:
return 0
list_idx = self.get_element_idx(num_tiles - 1)
tile_offset = self._ntile_list[list_idx]
ans = self._nrow_list[list_idx]
obj = self._pat_list[list_idx]
return ans + obj.num_tiles_to_rows(num_tiles - tile_offset)
[docs] def get_tile_info(self, tile_idx: int) -> Tuple[MOSBasePlaceInfo, int, bool]:
"""Returns the tile information for the given tile index.
Parameters
----------
tile_idx : int
the tile index.
Returns
-------
pinfo : MOSBasePlaceInfo
the tile layout information object.
y0 : int
the bottom Y coordinate of the tile.
flip : bool
True if this tile is flipped.
"""
list_idx = self.get_element_idx(tile_idx)
tile_offset = self._ntile_list[list_idx]
obj = self._pat_list[list_idx]
pinfo, y0, flip_tile = obj.get_tile_info(tile_idx - tile_offset)
return pinfo, y0 + self._dy_list[list_idx], flip_tile
[docs] def get_tile_pinfo(self, tile_idx: int) -> MOSBasePlaceInfo:
return self.get_tile_info(tile_idx)[0]
[docs] def flat_row_to_tile_row(self, flat_row_idx: int) -> Tuple[int, int]:
list_idx = bisect_right(self._nrow_list, flat_row_idx) - 1
row_offset = self._nrow_list[list_idx]
obj = self._pat_list[list_idx]
return obj.flat_row_to_tile_row(flat_row_idx - row_offset)
[docs] def get_sub_pattern_element(self, num_tiles: int, mult: int, mirror: bool, flip: bool,
start_idx: int = 0) -> TilePatternElement:
list_idx = self.get_element_idx(start_idx)
tile_offset = self._ntile_list[list_idx]
obj = self._pat_list[list_idx]
start_idx -= tile_offset
obj_num_tiles_tot = obj.num_tiles
obj_num = obj_num_tiles_tot - start_idx
if obj_num >= num_tiles:
return obj.get_sub_pattern_element(num_tiles, mult, mirror, flip, start_idx=start_idx)
ele_list = [obj.get_sub_pattern_element(obj_num, 1, False, False, start_idx=start_idx)]
num_tiles -= obj_num
for cur_idx in range(list_idx + 1, len(self._pat_list)):
obj = self._pat_list[cur_idx]
cur_num_tot = obj.num_tiles
if cur_num_tot >= num_tiles:
ele_list.append(obj.get_sub_pattern_element(num_tiles, 1, False, False))
break
else:
ele_list.append(obj.get_sub_pattern_element(cur_num_tot, 1, False, False))
num_tiles -= cur_num_tot
return TilePatternElement(TilePattern(ele_list), mirror=mirror, flip=flip, mult=mult)
[docs] def append_sub_pattern(self, ele_list: List[TilePatternElement], num_tiles: int,
flip_pattern: bool) -> None:
if flip_pattern:
for obj in reversed(self._pat_list):
cur_num_tiles = obj.num_tiles
if num_tiles >= cur_num_tiles:
num_tiles -= cur_num_tiles
ele_list.append(obj.get_reverse(obj.mult))
else:
if num_tiles > 0:
obj.append_sub_pattern(ele_list, num_tiles, True)
break
else:
for obj in self._pat_list:
cur_num_tiles = obj.num_tiles
if num_tiles >= cur_num_tiles:
num_tiles -= cur_num_tiles
ele_list.append(obj)
else:
if num_tiles > 0:
obj.append_sub_pattern(ele_list, num_tiles, False)
break
[docs]class TileInfoTable:
"""A table storing various compatible tiles."""
[docs] _arr_info_fname = 'arr_info'
def __init__(self, ainfo: MOSArrayPlaceInfo, pinfo_dict: Mapping[str, MOSBasePlaceInfo]
) -> None:
self._arr_info = ainfo
self._pinfo_dict = ImmutableSortedDict(pinfo_dict)
self._hash = combine_hash(hash(self._arr_info), hash(self._pinfo_dict))
[docs] def __hash__(self) -> int:
return self._hash
[docs] def __eq__(self, rhs: TileInfoTable) -> bool:
return (isinstance(rhs, TileInfoTable) and self._arr_info == rhs._arr_info and
self._pinfo_dict == rhs._pinfo_dict)
[docs] def __getitem__(self, name: str) -> MOSBasePlaceInfo:
return self._pinfo_dict[name]
@property
[docs] def arr_info(self) -> MOSArrayPlaceInfo:
return self._arr_info
@classmethod
[docs] def load(cls, grid: RoutingGrid, root_dir: Path) -> TileInfoTable:
arr_specs = read_yaml(root_dir / f'{cls._arr_info_fname}.yaml')
ainfo = MOSArrayPlaceInfo.make_array_info(grid, arr_specs)
table_specs = read_yaml(root_dir / 'specs.yaml')
pinfo_specs: Mapping[str, Mapping[str, Any]] = table_specs['place_info']
pinfo_dict: Dict[str, MOSBasePlaceInfo] = {}
for name in pinfo_specs.keys():
if name == cls._arr_info_fname:
raise ValueError(f'Illegal MOSBasePlaceInfo name: {name}')
pinfo_yaml = read_yaml(root_dir / f'{name}.yaml')
rp_list_raw: List[Mapping[str, Any]] = pinfo_yaml['rp_list']
bot_mirror: bool = pinfo_yaml['bot_mirror']
top_mirror: bool = pinfo_yaml['top_mirror']
options: Mapping[str, Any] = pinfo_yaml['options']
rp_list = ImmutableList([RowPlaceInfo.from_dict(val) for val in rp_list_raw])
pinfo_dict[name] = MOSBasePlaceInfo(name, ainfo, rp_list, bot_mirror, top_mirror,
ImmutableSortedDict(options))
return TileInfoTable(ainfo, pinfo_dict)
@classmethod
[docs] def make_tiles_dir(cls, grid: RoutingGrid, root_dir: Union[str, Path]) -> TileInfoTable:
"""Create a new TileInfoTable from spec file."""
if isinstance(root_dir, str):
root_dir = Path(root_dir)
specs: Mapping[str, Any] = read_yaml(root_dir / 'specs.yaml')
return TileInfoTable.make_tiles(grid, specs)
@classmethod
[docs] def make_tiles(cls, grid: RoutingGrid, specs: Mapping[str, Any]) -> TileInfoTable:
"""Create a new TileInfoTable from specification dictionary.
Tile extension rule:
1. If a tile does not have mirror placement constraint on its edge, and the other one
does, then the tile with no mirror placement constraint gets extended.
2. Otherwise, we extend the tile that has lower priority.
Parameters
----------
grid : RoutingGrid
the routing grid object.
specs : Mapping[str, Any]
tile specifications dictionary.
Returns
-------
table : TileInfoTable
a table of all created tiles.
"""
arr_info: Mapping[str, Any] = specs['arr_info']
place_info: Mapping[str, Mapping[str, Any]] = specs['place_info']
abut_list: List[Tuple[Tuple[str, int], Tuple[str, int]]] = specs['abut_list']
ainfo = MOSArrayPlaceInfo.make_array_info(grid, arr_info)
pinfo_dict: Dict[str, MOSBasePlaceInfo] = {
name: make_pinfo_compact_specs(ainfo, name, specs)
for name, specs in place_info.items()
}
for val in abut_list:
if isinstance(val, Mapping):
(name1, edge_code1), (name2, edge_code2) = val['edges']
shared1 = val.get('shared1', [])
shared2 = val.get('shared2', [])
else:
(name1, edge_code1), (name2, edge_code2) = val
shared1 = shared2 = []
top_edge1 = bool(edge_code1)
top_edge2 = bool(edge_code2)
pinfo1 = pinfo_dict[name1]
pinfo2 = pinfo_dict[name2]
margin, ext_w_info, em1, em2 = pinfo1.get_abut_info(pinfo2, top_edge1, top_edge2,
shared1, shared2)
if margin > 0:
# decide which tile to extend
mirror1 = pinfo1.get_mirror(top_edge1)
mirror2 = pinfo2.get_mirror(top_edge2)
if mirror1 == mirror2:
# use priority to break ties
if pinfo1.extend_priority > pinfo2.extend_priority:
ext1 = True
elif pinfo2.extend_priority > pinfo1.extend_priority:
ext1 = False
else:
raise ValueError('Cannot decide whether to extend tile '
f'{name1} or {name2}, please set the priority property.')
elif mirror1:
ext1 = False
else:
ext1 = True
if ext1:
pinfo_dict[name1] = pinfo1.get_extend(margin, top_edge1, ext_w_info, em1, em2,
shared1)
else:
pinfo_dict[name2] = pinfo2.get_extend(margin, top_edge2, ext_w_info, em2, em1,
shared2)
# check names of tiles are valid
for name in pinfo_dict.keys():
if name == cls._arr_info_fname:
raise ValueError(f'Illegal MOSBasePlaceInfo name: {name}')
return TileInfoTable(ainfo, pinfo_dict)
[docs] def save(self, root_dir: Path) -> None:
arr_fname = root_dir / f'{self._arr_info_fname}.yaml'
_save_arr_info(self._arr_info, arr_fname)
for name, pinfo in self._pinfo_dict.items():
_save_place_info(pinfo, root_dir / f'{name}.yaml')
[docs] def make_place_info(self, val: Mapping[str, Any]) -> Union[MOSBasePlaceInfo, TilePattern]:
name: str = val.get('name', '')
if name:
# return MOSBasePlaceInfo
return self._pinfo_dict[name]
else:
# return TilePattern
return TilePattern.make_pattern(self._pinfo_dict, val['tiles'])
[docs] def make_tile_pattern(self, tiles: Iterable[Mapping[str, Any]]) -> TilePattern:
return TilePattern.make_pattern(self._pinfo_dict, tiles)
[docs]def _save_arr_info(ainfo: MOSArrayPlaceInfo, fname: Path) -> None:
tr_widths = ainfo.tr_manager.tr_widths
tr_spaces = ainfo.tr_manager.tr_spaces
w_dict = {k: v.to_dict() for k, v in tr_widths.items()}
s_dict = {k: {a: b.value if isinstance(b, HalfInt) else b for a, b in v.items()}
for k, v in tr_spaces.items()}
write_yaml(fname, dict(
lch=ainfo.lch,
tr_widths=w_dict,
tr_spaces=s_dict,
top_layer=ainfo.top_layer,
conn_layer=ainfo.conn_layer,
half_space=ainfo.half_space,
arr_options=ainfo.arr_options.to_yaml(),
))
[docs]def _save_place_info(pinfo: MOSBasePlaceInfo, fname: Path) -> None:
write_yaml(fname, dict(
rp_list=[pinfo.get_row_place_info(idx).to_dict() for idx in range(pinfo.num_rows)],
bot_mirror=pinfo.get_mirror(False),
top_mirror=pinfo.get_mirror(True),
options=pinfo.tile_options.to_dict(),
priority=pinfo.extend_priority,
))