From 0ffe18c9f14f19cb820410c711ee4cdfd4b483d4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 9 Jun 2023 00:38:19 -0700 Subject: [PATCH] lots more work on klayout approach --- examples/check.py | 12 +- examples/connectivity.oas | Bin 1078 -> 1100 bytes examples/run.sh | 2 +- pyproject.toml | 3 + snarled/__init__.py | 2 +- snarled/__main__.py | 80 +---------- snarled/main.py | 79 +++++++++++ snarled/trace.py | 270 ++++++++++++++++++++++++++------------ snarled/utils.py | 39 ++++-- 9 files changed, 310 insertions(+), 177 deletions(-) mode change 100644 => 100755 examples/run.sh create mode 100644 snarled/main.py diff --git a/examples/check.py b/examples/check.py index 5d9f5a5..08dddfc 100644 --- a/examples/check.py +++ b/examples/check.py @@ -6,6 +6,7 @@ from pprint import pformat import logging import snarled +from snarled.types import layer_t logging.basicConfig() @@ -13,12 +14,12 @@ logging.getLogger('snarled').setLevel(logging.INFO) connectivity = [ - ((1, 0), (1, 2), (2, 0)), #M1 to M2 (via V12) - ((1, 0), (1, 3), (3, 0)), #M1 to M3 (via V13) - ((2, 0), (2, 3), (3, 0)), #M2 to M3 (via V23) + ((1, 0), (1, 2), (2, 0)), # M1 to M2 (via V12) + ((1, 0), (1, 3), (3, 0)), # M1 to M3 (via V13) + ((2, 0), (2, 3), (3, 0)), # M2 to M3 (via V23) ] -labels_map = { +labels_map: dict[layer_t, layer_t] = { (1, 0): (1, 0), (2, 0): (2, 0), (3, 0): (3, 0), @@ -26,6 +27,7 @@ labels_map = { filename = 'connectivity.oas' -result = snarled.trace_layout(filename, connectivity, topcell='top', labels_map=labels_map) +nets = snarled.trace_layout(filename, connectivity, topcell='top', labels_map=labels_map) +result = snarled.TraceAnalysis(nets) print('Result:\n', pformat(result)) diff --git a/examples/connectivity.oas b/examples/connectivity.oas index 865bf318727c6f1e0a01a036daac774cf85d9f93..dd7da8bf44ef3d31d4f2028a9eb81b0cb3bf4821 100644 GIT binary patch delta 551 zcmYMwUq};i902h9{eE}ecDK6O?+ZQ#i5eC65EMQYR#>h$T4DszLzGA$R%Gy9E^g8W zBeG5>Lu1H-1SN`sidi7bU=Hlz=Fy%$6! zMY2j)MUKALfp?svT>DCw4{>ZtV{R8mDV8V8ZdF{zdOJr}))M6hva*OEbQS? zVIsKYSB0wyLLow{a!oI3=7%HD`P=x@=bkxiYnGAStqL_g2od&o-=s3vJ$)O{)+Spm zW1$7G*k4v{ZL5z*6gGDy+3tRczLFa3k$-b1U_pGy67uW;z_jGhnYI-baEVpefQTxZa2nw%){$ROcu*3)=C@Lb5u#4bbZk&xa zI7HUb$ZRTS*iwFJfx223<5S&fV{Uhv(w??0x68j0@*H z%HZu=cdiYeaB<#6zl+l@&bT<~;;f4S7pKO{1!I?lqfDCW$Ody6{wv8aYWCKY1>DqC zDSjk?PQKGB!FULx`2ky7pM!-@OqEJZ*^g9HtWe_#U8*V59c2y4?6tM}bGE5t zUsB11U-^(KYBp%j!p=Gsx|6MSyH(5vZOt@hp{xnR$E1ls)2B)|QVaxne^lAxK0%5z z#kP9{wN9=v2KE3q|LlQ9Oi zW`%Nl6ERh)>j508DStwLhp?}%m$i?^ag6ZN&5CMk%^@UPo9wbei=9aS4ogCH5kW z5pbZwXHx9vU8Dy80?Sv@BS_!n2|0f`;Xi=11Y5AKuMQ$rxdV^J*xO;GZ5ZR3u0~jl Us=&Wq{Cy14Vctw% str: - try: - parts = string.split('_') - _part_id = int(parts[-1]) # must succeed to return here - return '_'.join(parts[:-1]) - except Exception: - return string - - kwargs['parse_label'] = parse_label - -result = trace_layout( - filepath=filepath, - connectivity=connectivity, - **kwargs, - ) - -print('Nets: ', pformat(result.nets)) -print('Opens: ', pformat(result.opens)) -print('Shorts: ', pformat(result.shorts)) +from .main import main +main() diff --git a/snarled/main.py b/snarled/main.py new file mode 100644 index 0000000..1c30d70 --- /dev/null +++ b/snarled/main.py @@ -0,0 +1,79 @@ +from typing import Any +import argparse +import logging + +from . import utils +from .trace import trace_layout, TraceAnalysis + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def main() -> int: + parser = argparse.ArgumentParser( + prog='snarled', + description='layout connectivity checker', + ) + + parser.add_argument('file_path') + parser.add_argument('connectivity_path') + parser.add_argument('-m', '--layermap') + parser.add_argument('-t', '--top') + parser.add_argument('-p', '--labels-remap') + + parser.add_argument('-l', '--lfile-path') + parser.add_argument('-r', '--lremap') + parser.add_argument('-n', '--llayermap') + parser.add_argument('-s', '--ltop') + + parser.add_argument('-o', '--output') + parser.add_argument('-u', '--raw-label-names', action='store_true') + + args = parser.parse_args() + + filepath = args.file_path + connectivity = utils.read_connectivity(args.connectivity_path) + + kwargs: dict[str, Any] = {} + + if args.layermap: + kwargs['layer_map'] = utils.read_layermap(args.layermap) + + if args.top: + kwargs['topcell'] = args.top + + if args.labels_remap: + kwargs['labels_remap'] = utils.read_remap(args.labels_remap) + + if args.lfile_path: + assert args.lremap + kwargs['lfile_path'] = args.lfile_path + kwargs['lfile_map'] = utils.read_remap(args.lremap) + + if args.llayermap: + kwargs['lfile_layermap'] = utils.read_layermap(args.llayermap) + + if args.ltop: + kwargs['lfile_topcell'] = args.ltop + + if args.output: + kwargs['output_path'] = args.output + + if not args.raw_label_names: + from .utils import strip_underscored_label as parse_label + else: + def parse_label(string: str) -> str: + return string + + nets = trace_layout( + filepath=filepath, + connectivity=connectivity, + **kwargs, + ) + + parsed_nets = [{parse_label(ll) for ll in net} for net in nets] + result = TraceAnalysis(parsed_nets) + print(result) + + return 0 diff --git a/snarled/trace.py b/snarled/trace.py index 00156e7..ecd9017 100644 --- a/snarled/trace.py +++ b/snarled/trace.py @@ -1,7 +1,6 @@ -from typing import Sequence, Callable +from typing import Sequence, Iterable import logging from collections import Counter -from dataclasses import dataclass from itertools import chain from klayout import db @@ -11,83 +10,151 @@ from .types import lnum_t, layer_t logger = logging.getLogger(__name__) -def get_topcell( - layout: db.Layout, - name: str | None = None, - ) -> db.Cell: - if name is None: - return layout.top_cell() - else: - ind = layout.cell_by_name(name) - return layout.cell(ind) +class TraceAnalysis: + """ + Short/Open analysis for a list of nets + """ - -def write_net_layout( - l2n: db.LayoutToNetlist, - filepath: str, - layers: Sequence[lnum_t], - ) -> None: - layout = db.Layout() - top = layout.create_cell('top') - lmap = {layout.layer(*layer) for layer in layers} - l2n.build_all_nets(l2n.cell_mapping_into(ly, top), ly, lmap, 'net_', 'prop_', l2n.BNH_Flatten, 'circuit_') - layout.write(filepath) - - -def merge_labels_from( - filepath: str, - into_layout: db.Layout, - lnum_map: dict[lnum_t, lnum_t], - topcell: str | None = None, - ) -> None: - layout = db.Layout() - lm = layout.read(filepath) - - topcell_obj = get_topcell(layout, topcell) - - for labels_layer, conductor_layer in lnum_map: - layer_ind_src = layout.layer(*labels_layer) - layer_ind_dst = into_layout.layer(*conductor_layer) - - shapes_dst = topcell_obj.shapes(layer_ind_dst) - shapes_src = topcell_obj.shapes(layer_ind_src) - for shape in shapes_dst.each(): - new_shape = shapes_dst.insert(shape) - shapes_dst.replace_prop_id(new_shape, 0) # clear shape properties - - -@dataclass -class TraceResult: - shorts: list[str] - opens: list[str] nets: list[set[str]] + """ List of nets (connected sets of labels) """ + + opens: dict[str, int] + """ Labels which appear on 2+ disconnected nets, and the number of nets they touch """ + + shorts: list[set[str]] + """ Nets containing more than one unique label """ + + def __init__( + self, + nets: Sequence[Iterable[str]], + ) -> None: + """ + Args: + nets: Sequence of nets. Each net is a sequence of labels + which were found to be electrically connected. + """ + + setnets = [set(net) for net in nets] + + # Shorts contain more than one label + shorts = [net for net in setnets if len(net) > 1] + + # Check number of times each label appears + net_occurences = Counter(chain.from_iterable(setnets)) + + # Opens are where the same label appears on more than one net + opens = { + nn: count + for nn, count in net_occurences.items() + if count > 1 + } + + self.nets = setnets + self.shorts = shorts + self.opens = opens + + def __repr__(self) -> str: + def format_net(net: Iterable[str]) -> str: + names = [f"'{name}'" if any(cc in name for cc in ' \t\n') else name for name in sorted(net)] + return ','.join(names) + + def sort_nets(nets: Sequence[Iterable[str]]) -> list[Iterable[str]]: + return sorted(nets, key=lambda net: ','.join(sorted(net))) + + ss = 'Trace analysis' + ss += '\n=============' + + ss += '\nNets' + ss += '\n(groups of electrically connected labels)\n' + for net in sort_nets(self.nets): + ss += '\t' + format_net(net) + '\n' + + ss += '\nOpens' + ss += '\n(2+ nets containing the same name)\n' + for label, count in sorted(self.opens.items()): + ss += f'\t{label} : {count} nets\n' + + ss += '\nShorts' + ss += '\n(2+ unique names for the same net)\n' + for net in sort_nets(self.shorts): + ss += '\t' + format_net(net) + '\n' + + ss += '=============\n' + return ss def trace_layout( filepath: str, - connectivity: list[layer_t, layer_t | None, layer_t], + connectivity: Sequence[tuple[layer_t, layer_t | None, layer_t]], layer_map: dict[str, lnum_t] | None = None, topcell: str | None = None, *, - labels_map: dict[layer_t, layer_t] = {}, + labels_map: dict[layer_t, layer_t] | None = None, lfile_path: str | None = None, lfile_map: dict[layer_t, layer_t] | None = None, lfile_layer_map: dict[str, lnum_t] | None = None, lfile_topcell: str | None = None, output_path: str | None = None, - parse_label: Callable[[str], str] | None = None, - ) -> TraceResult: + ) -> list[set[str]]: + """ + Trace a layout to identify labeled nets. + + To label a net, place a text label anywhere touching the net. + Labels may be mapped from a different layer, or even a different + layout file altogether. + Note: Labels must not contain commas (,)!! + + Args: + filepath: Path to the primary layout, containing the conductor geometry + (and optionally also the labels) + connectivity: List of (conductor1, via12, conductor2) tuples, + which indicate that the specified layers are electrically connected + (conductor1 to via12 and via12 to conductor2). The middle (via) layer + may be `None`, in which case the outer layers are directly connected + at any overlap (conductor1 to conductor2). + layer_map: {layer_name: (layer_num, dtype_num)} translation table. + Should contain any strings present in `connectivity` and `labels_map`. + Default is an empty dict. + topcell: Cell name of the topcell. If `None`, it is automatically chosen. + labels_map: {label_layer: metal_layer} mapping, which allows labels to + reside on a different layer from their corresponding metals. + Only labels on the provided label layers are used, so + {metal_layer: metal_layer} entries must be explicitly specified if + they are desired. + If `None`, labels on each layer in `connectivity` are used alongside + that same layer's geometry ({layer: layer} for all participating + geometry layers) + Default `None`. + lfile_path: Path to a separate file from which labels should be merged. + lfile_map: {lfile_layer: primary_layer} mapping, used when merging the + labels into the primary layout. + lfile_layer_map: {layer_name: (layer_num, dtype_num)} mapping for the + secondary (label) file. Should contain all string keys in + `lfile_map`. + `None` reuses `layer_map` (default). + lfile_topcell: Cell name for the topcell in the secondary (label) file. + `None` automatically chooses the topcell (default). + output_path: If provided, outputs the final net geometry to a layout + at the given path. Default `None`. + + Returns: + List of labeled nets, where each entry is a set of label strings which + were found on the given net. + """ if layer_map is None: layer_map = {} - if parse_label is None: - def parse_label(label: str) -> str: - return label + if labels_map is None: + labels_map = { + layer: layer + for layer in chain(*connectivity) + if layer is not None + } layout = db.Layout() - lm = layout.read(filepath) + layout.read(filepath) - topcell_obj = get_topcell(layout, topcell) + topcell_obj = _get_topcell(layout, topcell) # Merge labels from a separate layout if asked if lfile_path: @@ -106,7 +173,7 @@ def trace_layout( lshape = layer_map[lshape] lnum_map[ltext] = lshape - merge_labels_from(lfile_path, layout, lnum_map, lfile_topcell) + _merge_labels_from(lfile_path, layout, lnum_map, lfile_topcell) # # Build a netlist from the layout @@ -117,6 +184,8 @@ def trace_layout( # Create l2n polygon layers layer2polys = {} for layer in set(chain(*connectivity)): + if layer is None: + continue if isinstance(layer, str): layer = layer_map[layer] klayer = layout.layer(*layer) @@ -143,7 +212,7 @@ def trace_layout( top = layer_map[top] if isinstance(via, str): via = layer_map[via] - if isinstance(top, str): + if isinstance(bot, str): bot = layer_map[bot] if via is None: @@ -162,43 +231,78 @@ def trace_layout( l2n.connect(layer2polys[metal_layer], layer2texts[label_layer]) # Get netlist - nle = l2n.extract_netlist() + l2n.extract_netlist() nl = l2n.netlist() nl.make_top_level_pins() if output_path: - write_net_layout(l2n, output_path, layer2polys.keys()) + _write_net_layout(l2n, output_path, layer2polys) # - # Analyze traced nets + # Return merged nets # top_circuits = [cc for cc, _ in zip(nl.each_circuit_top_down(), range(nl.top_circuit_count()))] # Nets with more than one label get their labels joined with a comma - nets = [ - {parse_label(ll) for ll in nn.name.split(',')} + nets = [ + set(nn.name.split(',')) for cc in top_circuits for nn in cc.each_net() if nn.name ] - nets2 = [ - nn.name - for cc in top_circuits - for nn in cc.each_net() - ] - print(nets2) + return nets - # Shorts contain more than one label - shorts = [net for net in nets if len(net) > 1] - # Check number of times each label appears - net_occurences = Counter(chain.from_iterable(nets)) +def _get_topcell( + layout: db.Layout, + name: str | None = None, + ) -> db.Cell: + """ + Get the topcell by name or hierarchy. - # If the same label appears on more than one net, warn about an open - opens = [ - (nn, count) - for nn, count in net_occurences.items() - if count > 1 - ] + Args: + layout: Layout to get the cell from + name: If given, use the name to find the topcell; otherwise use hierarchy. - return TraceResult(shorts=shorts, opens=opens, nets=nets) + Returns: + Cell object + """ + if name is None: + return layout.top_cell() + else: + ind = layout.cell_by_name(name) + return layout.cell(ind) + + +def _write_net_layout( + l2n: db.LayoutToNetlist, + filepath: str, + layer2polys: dict[lnum_t, db.Region], + ) -> None: + layout = db.Layout() + top = layout.create_cell('top') + lmap = {layout.layer(*layer): polys for layer, polys in layer2polys.items()} + l2n.build_all_nets(l2n.cell_mapping_into(layout, top), layout, lmap, 'net_', 'prop_', l2n.BNH_Flatten, 'circuit_') + layout.write(filepath) + + +def _merge_labels_from( + filepath: str, + into_layout: db.Layout, + lnum_map: dict[lnum_t, lnum_t], + topcell: str | None = None, + ) -> None: + layout = db.Layout() + layout.read(filepath) + + topcell_obj = _get_topcell(layout, topcell) + + for labels_layer, conductor_layer in lnum_map: + layer_ind_src = layout.layer(*labels_layer) + layer_ind_dst = into_layout.layer(*conductor_layer) + + shapes_dst = topcell_obj.shapes(layer_ind_dst) + shapes_src = topcell_obj.shapes(layer_ind_src) + for shape in shapes_src.each(): + new_shape = shapes_dst.insert(shape) + shapes_dst.replace_prop_id(new_shape, 0) # clear shape properties diff --git a/snarled/utils.py b/snarled/utils.py index 5671021..3274755 100644 --- a/snarled/utils.py +++ b/snarled/utils.py @@ -5,6 +5,25 @@ from .types import layer_t logger = logging.getLogger(__name__) +def strip_underscored_label(string: str) -> str: + """ + If the label ends in an underscore followed by an integer, strip + that suffix. Otherwise, just return the label. + + Args: + string: The label string + + Returns: + The label string, with the suffix removed (if one was found) + """ + try: + parts = string.split('_') + int(parts[-1]) # must succeed to continue + return '_'.join(parts[:-1]) + except Exception: + return string + + def read_layermap(path: str) -> dict[str, tuple[int, int]]: """ Read a klayout-compatible layermap file. @@ -44,7 +63,7 @@ def read_layermap(path: str) -> dict[str, tuple[int, int]]: logger.error(f'Layer map read failed on line {nn}') raise err - layer_map[name.strip()] = (layer, dtype) + layer_map[name.strip()] = layer_nums return layer_map @@ -73,7 +92,7 @@ def read_connectivity(path: str) -> list[tuple[layer_t, layer_t | None, layer_t] with open(path, 'rt') as ff: lines = ff.readlines() - connections = [] + connections: list[tuple[layer_t, layer_t | None, layer_t]] = [] for nn, line in enumerate(lines): line = line.strip() if not line: @@ -85,23 +104,24 @@ def read_connectivity(path: str) -> list[tuple[layer_t, layer_t | None, layer_t] raise Exception(f'Too many commas in connectivity spec on line {nn}') layers = [] - for part in enumerate(parts): + for part in parts: + layer: layer_t if '/' in part: try: - layer = str2lnum(layer_part) + layer = str2lnum(part) except Exception as err: logger.error(f'Connectivity spec read failed on line {nn}') raise err else: layer = part.strip() if not layer: - raise Exception(f'Empty layer in connectivity spec on line {nn}') + raise Exception(f'Empty layer in connectivity spec on line {nn}') layers.append(layer) if len(layers) == 2: connections.append((layers[0], None, layers[1])) else: - connections.append(tuple(layers)) + connections.append((layers[0], layers[1], layers[2])) return connections @@ -139,17 +159,18 @@ def read_remap(path: str) -> dict[layer_t, layer_t]: raise Exception(f'Too many commas in layer remap spec on line {nn}') layers = [] - for part in enumerate(parts): + for part in parts: + layer: layer_t if '/' in part: try: - layer = str2lnum(layer_part) + layer = str2lnum(part) except Exception as err: logger.error(f'Layer remap spec read failed on line {nn}') raise err else: layer = part.strip() if not layer: - raise Exception(f'Empty layer in layer remap spec on line {nn}') + raise Exception(f'Empty layer in layer remap spec on line {nn}') layers.append(layer) remap[layers[0]] = layers[1]