lots more work on klayout approach

This commit is contained in:
Jan Petykiewicz 2023-06-09 00:38:19 -07:00
parent 9017984b4b
commit 0ffe18c9f1
9 changed files with 310 additions and 177 deletions

View File

@ -6,6 +6,7 @@ from pprint import pformat
import logging import logging
import snarled import snarled
from snarled.types import layer_t
logging.basicConfig() logging.basicConfig()
@ -13,12 +14,12 @@ logging.getLogger('snarled').setLevel(logging.INFO)
connectivity = [ connectivity = [
((1, 0), (1, 2), (2, 0)), #M1 to M2 (via V12) ((1, 0), (1, 2), (2, 0)), # M1 to M2 (via V12)
((1, 0), (1, 3), (3, 0)), #M1 to M3 (via V13) ((1, 0), (1, 3), (3, 0)), # M1 to M3 (via V13)
((2, 0), (2, 3), (3, 0)), #M2 to M3 (via V23) ((2, 0), (2, 3), (3, 0)), # M2 to M3 (via V23)
] ]
labels_map = { labels_map: dict[layer_t, layer_t] = {
(1, 0): (1, 0), (1, 0): (1, 0),
(2, 0): (2, 0), (2, 0): (2, 0),
(3, 0): (3, 0), (3, 0): (3, 0),
@ -26,6 +27,7 @@ labels_map = {
filename = 'connectivity.oas' 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)) print('Result:\n', pformat(result))

Binary file not shown.

2
examples/run.sh Normal file → Executable file
View File

@ -2,4 +2,4 @@
cd $(dirname -- "$0") # cd to this script's parent directory cd $(dirname -- "$0") # cd to this script's parent directory
snarled connectivity.oas connectivity.txt -l layermap.txt snarled connectivity.oas connectivity.txt -m layermap.txt

View File

@ -51,3 +51,6 @@ dependencies = [
[tool.hatch.version] [tool.hatch.version]
path = "snarled/__init__.py" path = "snarled/__init__.py"
[project.scripts]
snarled = "snarled.main:main"

View File

@ -12,7 +12,7 @@ has deprived the man of a schematic and a better connectivity tool.
The main functionality is in `trace`. The main functionality is in `trace`.
`__main__.py` details the command-line interface. `__main__.py` details the command-line interface.
""" """
from .trace import TraceResult, trace_layout from .trace import trace_layout, TraceAnalysis
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
__version__ = '1.0' __version__ = '1.0'

View File

@ -1,79 +1,3 @@
from typing import Any
import argparse
import logging
from pprint import pformat
from .main import main
logging.basicConfig(level=logging.INFO) main()
logger = logging.getLogger(__name__)
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:
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:
def parse_label(string: str) -> 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))

79
snarled/main.py Normal file
View File

@ -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

View File

@ -1,7 +1,6 @@
from typing import Sequence, Callable from typing import Sequence, Iterable
import logging import logging
from collections import Counter from collections import Counter
from dataclasses import dataclass
from itertools import chain from itertools import chain
from klayout import db from klayout import db
@ -11,83 +10,151 @@ from .types import lnum_t, layer_t
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_topcell( class TraceAnalysis:
layout: db.Layout, """
name: str | None = None, Short/Open analysis for a list of nets
) -> db.Cell: """
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,
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]] 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( def trace_layout(
filepath: str, 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, layer_map: dict[str, lnum_t] | None = None,
topcell: str | 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_path: str | None = None,
lfile_map: dict[layer_t, layer_t] | None = None, lfile_map: dict[layer_t, layer_t] | None = None,
lfile_layer_map: dict[str, lnum_t] | None = None, lfile_layer_map: dict[str, lnum_t] | None = None,
lfile_topcell: str | None = None, lfile_topcell: str | None = None,
output_path: str | None = None, output_path: str | None = None,
parse_label: Callable[[str], str] | None = None, ) -> list[set[str]]:
) -> TraceResult: """
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: if layer_map is None:
layer_map = {} layer_map = {}
if parse_label is None: if labels_map is None:
def parse_label(label: str) -> str: labels_map = {
return label layer: layer
for layer in chain(*connectivity)
if layer is not None
}
layout = db.Layout() 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 # Merge labels from a separate layout if asked
if lfile_path: if lfile_path:
@ -106,7 +173,7 @@ def trace_layout(
lshape = layer_map[lshape] lshape = layer_map[lshape]
lnum_map[ltext] = 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 # Build a netlist from the layout
@ -117,6 +184,8 @@ def trace_layout(
# Create l2n polygon layers # Create l2n polygon layers
layer2polys = {} layer2polys = {}
for layer in set(chain(*connectivity)): for layer in set(chain(*connectivity)):
if layer is None:
continue
if isinstance(layer, str): if isinstance(layer, str):
layer = layer_map[layer] layer = layer_map[layer]
klayer = layout.layer(*layer) klayer = layout.layer(*layer)
@ -143,7 +212,7 @@ def trace_layout(
top = layer_map[top] top = layer_map[top]
if isinstance(via, str): if isinstance(via, str):
via = layer_map[via] via = layer_map[via]
if isinstance(top, str): if isinstance(bot, str):
bot = layer_map[bot] bot = layer_map[bot]
if via is None: if via is None:
@ -162,43 +231,78 @@ def trace_layout(
l2n.connect(layer2polys[metal_layer], layer2texts[label_layer]) l2n.connect(layer2polys[metal_layer], layer2texts[label_layer])
# Get netlist # Get netlist
nle = l2n.extract_netlist() l2n.extract_netlist()
nl = l2n.netlist() nl = l2n.netlist()
nl.make_top_level_pins() nl.make_top_level_pins()
if output_path: 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()))] 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 with more than one label get their labels joined with a comma
nets = [ nets = [
{parse_label(ll) for ll in nn.name.split(',')} set(nn.name.split(','))
for cc in top_circuits for cc in top_circuits
for nn in cc.each_net() for nn in cc.each_net()
if nn.name if nn.name
] ]
nets2 = [ return nets
nn.name
for cc in top_circuits
for nn in cc.each_net()
]
print(nets2)
# Shorts contain more than one label
shorts = [net for net in nets if len(net) > 1]
# Check number of times each label appears def _get_topcell(
net_occurences = Counter(chain.from_iterable(nets)) 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 Args:
opens = [ layout: Layout to get the cell from
(nn, count) name: If given, use the name to find the topcell; otherwise use hierarchy.
for nn, count in net_occurences.items()
if count > 1
]
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

View File

@ -5,6 +5,25 @@ from .types import layer_t
logger = logging.getLogger(__name__) 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]]: def read_layermap(path: str) -> dict[str, tuple[int, int]]:
""" """
Read a klayout-compatible layermap file. 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}') logger.error(f'Layer map read failed on line {nn}')
raise err raise err
layer_map[name.strip()] = (layer, dtype) layer_map[name.strip()] = layer_nums
return layer_map 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: with open(path, 'rt') as ff:
lines = ff.readlines() lines = ff.readlines()
connections = [] connections: list[tuple[layer_t, layer_t | None, layer_t]] = []
for nn, line in enumerate(lines): for nn, line in enumerate(lines):
line = line.strip() line = line.strip()
if not line: 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}') raise Exception(f'Too many commas in connectivity spec on line {nn}')
layers = [] layers = []
for part in enumerate(parts): for part in parts:
layer: layer_t
if '/' in part: if '/' in part:
try: try:
layer = str2lnum(layer_part) layer = str2lnum(part)
except Exception as err: except Exception as err:
logger.error(f'Connectivity spec read failed on line {nn}') logger.error(f'Connectivity spec read failed on line {nn}')
raise err raise err
else: else:
layer = part.strip() layer = part.strip()
if not layer: 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) layers.append(layer)
if len(layers) == 2: if len(layers) == 2:
connections.append((layers[0], None, layers[1])) connections.append((layers[0], None, layers[1]))
else: else:
connections.append(tuple(layers)) connections.append((layers[0], layers[1], layers[2]))
return connections 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}') raise Exception(f'Too many commas in layer remap spec on line {nn}')
layers = [] layers = []
for part in enumerate(parts): for part in parts:
layer: layer_t
if '/' in part: if '/' in part:
try: try:
layer = str2lnum(layer_part) layer = str2lnum(part)
except Exception as err: except Exception as err:
logger.error(f'Layer remap spec read failed on line {nn}') logger.error(f'Layer remap spec read failed on line {nn}')
raise err raise err
else: else:
layer = part.strip() layer = part.strip()
if not layer: 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) layers.append(layer)
remap[layers[0]] = layers[1] remap[layers[0]] = layers[1]