snapshot 2022-03-30 23:17:32.485991

lethe/LATEST
jan 2 years ago
parent dc8f33da20
commit d90f162469

@ -3,4 +3,87 @@ snarl
Layout connectivity checker.
TODO: Documentation and examples!
`snarl` is a python package for checking electrical connectivity in multi-layer layouts.
It is intended to be "poor-man's LVS" (layout-versus-schematic), for when poverty
has deprived the man of both a schematic and a better connectivity tool.
#Organization
The main functionality is in `trace_connectivity`.
Useful classes, namely `NetsInfo` and `NetName`, are in `snarl.tracker`.
`snarl.interfaces` contains helper code for interfacing with other packages.
#Example
See `examples/check.py`.
```python3
from pprint import pformat
from masque.file import gdsii, oasis
import snarl
import snarl.interfaces.masque
# Layer definitions
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)
}
cells, props = oasis.readfile('connectivity.oas')
topcell = cells['top']
polys, labels = snarl.interfaces.masque.read_cell(topcell, connectivity)
nets_info = snarl.trace_connectivity(polys, labels, connectivity)
print('\nFinal nets:')
print([kk for kk in nets_info.nets if isinstance(kk.name, str)])
print('\nShorted net sets:')
for short in nets_info.get_shorted_nets():
print('(' + ','.join([repr(nn) for nn in sorted(list(short))]) + ')')
print('\nOpen nets:')
print(pformat(dict(nets_info.get_open_nets())))
```
this prints the following:
```
Nets ['SignalD', 'SignalI'] are shorted on layer (1, 0) in poly:
[[13000.0, -3000.0],
[16000.0, -3000.0],
[16000.0, -1000.0],
[13000.0, -1000.0],
[13000.0, 2000.0],
[12000.0, 2000.0],
[12000.0, -1000.0],
[11000.0, -1000.0],
[11000.0, -3000.0],
[12000.0, -3000.0],
[12000.0, -8000.0],
[13000.0, -8000.0]]
Nets ['SignalK', 'SignalK'] are shorted on layer (1, 0) in poly:
[[18500.0, -8500.0], [28200.0, -8500.0], [28200.0, 1000.0], [18500.0, 1000.0]]
Nets ['SignalC', 'SignalC'] are shorted on layer (1, 0) in poly:
[[10200.0, 0.0], [-1100.0, 0.0], [-1100.0, -1000.0], [10200.0, -1000.0]]
Nets ['SignalG', 'SignalH'] are shorted on layer (1, 0) in poly:
[[10100.0, -2000.0], [5100.0, -2000.0], [5100.0, -3000.0], [10100.0, -3000.0]]
Final nets:
[SignalD, SignalA, SignalF, SignalK__0, SignalC__0, SignalB, SignalG, SignalK__2, SignalL, SignalE]
Shorted net sets:
(SignalD,SignalI)
(SignalK__0,SignalK__1)
(SignalC__0,SignalC__1)
(SignalG,SignalH)
Open nets:
{'SignalK': [SignalK__0, SignalK__2]}
```

Binary file not shown.

@ -1,3 +1,7 @@
"""
Example code for checking connectivity in a layout by using
`snarl` and `masque`.
"""
from pprint import pformat
from masque.file import gdsii, oasis
@ -17,7 +21,7 @@ connectivity = {
cells, props = oasis.readfile('connectivity.oas')
topcell = cells['top']
polys, labels = snarl.interfaces.masque.read_topcell(topcell, connectivity)
polys, labels = snarl.interfaces.masque.read_cell(topcell, connectivity)
nets_info = snarl.trace_connectivity(polys, labels, connectivity)
print('\nFinal nets:')

@ -1,7 +1,20 @@
"""
TODO: ALL DOCSTRINGS
snarl
=====
Layout connectivity checker.
`snarl` is a python package for checking electrical connectivity in multi-layer layouts.
It is intended to be "poor-man's LVS" (layout-versus-schematic), for when poverty
has deprived the man of both a schematic and a better connectivity tool.
The main functionality is in `trace_connectivity`.
Useful classes, namely `NetsInfo` and `NetName`, are in `snarl.tracker`.
`snarl.interfaces` contains helper code for interfacing with other packages.
"""
from .main import trace_connectivity
from .tracker import NetsInfo, NetName
from . import interfaces

@ -1,3 +1,6 @@
"""
Wrappers to simplify some pyclipper functions
"""
from typing import Sequence, Optional, List
from numpy.typing import ArrayLike

@ -1,3 +1,6 @@
"""
Functionality for extracting geometry and label info from `masque` patterns.
"""
from typing import Sequence, Dict, List, Any, Tuple, Optional, Mapping
from collections import defaultdict
@ -11,13 +14,29 @@ from ..types import layer_t
from ..utils import connectivity2layers
def read_topcell(
topcell: Pattern,
def read_cell(
cell: Pattern,
connectivity: Sequence[Tuple[layer_t, Optional[layer_t], layer_t]],
label_mapping: Optional[Mapping[layer_t, layer_t]] = None,
) -> Tuple[
defaultdict[layer_t, List[NDArray[numpy.float64]]],
defaultdict[layer_t, List[Tuple[float, float, str]]]]:
"""
Extract `polys` and `labels` from a `masque.Pattern`.
This function extracts the data needed by `snarl.trace_connectivity`.
Args:
cell: A `masque` `Pattern` object. Usually your topcell.
connectivity: A sequence of 3-tuples specifying the layer connectivity.
Same as what is provided to `snarl.trace_connectivity`.
label_mapping: A mapping of `{label_layer: metal_layer}`. This allows labels
to refer to nets on metal layers without the labels themselves being on
that layer.
Returns:
`polys` and `labels` data structures, to be passed to `snarl.trace_connectivity`.
"""
metal_layers, via_layers = connectivity2layers(connectivity)
poly_layers = metal_layers | via_layers
@ -26,19 +45,19 @@ def read_topcell(
label_mapping = {layer: layer for layer in metal_layers}
label_layers = {label_layer for label_layer in label_mapping.keys()}
topcell = topcell.deepcopy().subset(
cell = cell.deepcopy().subset(
shapes_func=lambda ss: ss.layer in poly_layers,
labels_func=lambda ll: ll.layer in label_layers,
subpatterns_func=lambda ss: True,
)
topcell = topcell.flatten()
cell = cell.flatten()
polys = load_polys(topcell, list(poly_layers))
polys = load_polys(cell, list(poly_layers))
metal_labels = defaultdict(list)
for label_layer, metal_layer in label_mapping.items():
labels = []
for ll in topcell.labels:
for ll in cell.labels:
if ll.layer != label_layer:
continue
@ -57,11 +76,22 @@ def read_topcell(
def load_polys(
topcell: Pattern,
cell: Pattern,
layers: Sequence[layer_t],
) -> defaultdict[layer_t, List[NDArray[numpy.float64]]]:
"""
Given a *flat* `masque.Pattern`, extract the polygon info into the format used by `snarl`.
Args:
cell: The `Pattern` object to extract from.
layers: The layers to extract.
Returns:
`{layer0: [poly0, [(x0, y0), (x1, y1), ...], poly2, ...]}`
`polys` structure usable by `snarl.trace_connectivity`.
"""
polys = defaultdict(list)
for ss in topcell.shapes:
for ss in cell.shapes:
if ss.layer not in layers:
continue

@ -1,3 +1,6 @@
"""
Main connectivity-checking functionality for `snarl`
"""
from typing import Tuple, List, Dict, Set, Optional, Union, Sequence, Mapping
from collections import defaultdict
from pprint import pformat
@ -21,19 +24,50 @@ def trace_connectivity(
polys: Mapping[layer_t, Sequence[ArrayLike]],
labels: Mapping[layer_t, Sequence[Tuple[float, float, str]]],
connectivity: Sequence[Tuple[layer_t, Optional[layer_t], layer_t]],
label_mapping: Optional[Mapping[layer_t, layer_t]] = None,
clipper_scale_factor: int = int(2 ** 24),
) -> NetsInfo:
"""
Analyze the electrical connectivity of the layout.
This is the primary purpose of `snarl`.
The resulting `NetsInfo` will contain only disjoint `nets`, and its `net_aliases` can be used to
understand which nets are shorted (and therefore known by more than one name).
Args:
polys: A full description of all conducting paths in the layout. Consists of lists of polygons
(Nx2 arrays of vertices), indexed by layer. The structure looks roughly like
`{layer0: [poly0, poly1, ..., [(x0, y0), (x1, y1), ...]], ...}`
labels: A list of "named points" which are used to assign names to the nets they touch.
A collection of lists of (x, y, name) tuples, indexed *by the layer they target*.
`{layer0: [(x0, y0, name0), (x1, y1, name1), ...], ...}`
connectivity: A sequence of 3-tuples specifying the electrical connectivity between layers.
Each 3-tuple looks like `(top_layer, via_layer, bottom_layer)` and indicates that
`top_layer` and `bottom_layer` are electrically connected at any location where
shapes are present on all three (top, via, and bottom) layers.
`via_layer` may be `None`, in which case any overlap between shapes on `top_layer`
and `bottom_layer` is automatically considered a short (with no third shape necessary).
clipper_scale_factor: `pyclipper` uses 64-bit integer math, while we accept either floats or ints.
The coordinates from `polys` are scaled by this factor to put them roughly in the middle of
the range `pyclipper` wants; you may need to adjust this if you are already using coordinates
with large integer values.
Returns:
`NetsInfo` object describing the various nets and their connectivities.
"""
#
# Figure out which layers are metals vs vias, and run initial union on each layer
#
metal_layers, via_layers = connectivity2layers(connectivity)
if label_mapping is None:
label_mapping = {layer: layer for layer in metal_layers}
metal_polys = {layer: union_input_polys(scale_to_clipper(polys[layer], clipper_scale_factor))
for layer in metal_layers}
via_polys = {layer: union_input_polys(scale_to_clipper(polys[layer], clipper_scale_factor))
for layer in via_layers}
#
# Check each polygon for labels, and assign it to a net (possibly anonymous).
#
nets_info = NetsInfo()
merge_groups: List[List[NetName]] = []
@ -47,27 +81,29 @@ def trace_connectivity(
for poly in metal_polys[layer]:
found_nets = label_poly(poly, point_xys, point_names, clipper_scale_factor)
name: Optional[str]
if found_nets:
name = NetName(found_nets[0])
else:
name = NetName() # Anonymous net
nets_info.get(name, layer).append(poly)
nets_info.nets[name][layer].append(poly)
if len(found_nets) > 1:
# Found a short
logger.warning(f'Nets {found_nets} are shorted on layer {layer} in poly:\n {pformat(poly)}')
poly = pformat(scale_from_clipper(poly.Contour, clipper_scale_factor))
logger.warning(f'Nets {found_nets} are shorted on layer {layer} in poly:\n {poly}')
merge_groups.append([name] + [NetName(nn) for nn in found_nets[1:]])
#
# Merge any nets that were shorted by having their labels on the same polygon
#
for group in merge_groups:
first_net, *defunct_nets = group
for defunct_net in defunct_nets:
nets_info.merge(first_net, defunct_net)
#
# Take EVENODD union within each net
# & stay in EVENODD-friendly representation
# Convert to non-hierarchical polygon representation
#
for net in nets_info.nets.values():
for layer in net:
@ -77,7 +113,9 @@ def trace_connectivity(
for layer in via_polys:
via_polys[layer] = hier2oriented(via_polys[layer])
#
# Figure out which nets are shorted by vias, then merge them
#
merge_pairs = find_merge_pairs(connectivity, nets_info.nets, via_polys)
for net_a, net_b in merge_pairs:
nets_info.merge(net_a, net_b)
@ -85,12 +123,33 @@ def trace_connectivity(
return nets_info
def union_input_polys(polys: List[ArrayLike]) -> List[PyPolyNode]:
def union_input_polys(polys: Sequence[ArrayLike]) -> List[PyPolyNode]:
"""
Perform a union operation on the provided sequence of polygons, and return
a list of `PyPolyNode`s corresponding to all of the outer (i.e. non-hole)
contours.
Note that while islands are "outer" contours and returned in the list, they
also are still available through the `.Childs` property of the "hole" they
appear in. Meanwhile, "hole" contours are only accessible through the `.Childs`
property of their parent "outer" contour, and are not returned in the list.
Args:
polys: A sequence of polygons, `[[(x0, y0), (x1, y1), ...], poly1, poly2, ...]`
Polygons may be implicitly closed.
Returns:
List of PyPolyNodes, representing all "outer" contours (including islands) in
the union of `polys`.
"""
for poly in polys:
if (numpy.abs(poly) % 1).any():
logger.warning('Warning: union_polys got non-integer coordinates; all values will be truncated.')
break
#TODO: check if we need to reverse the order of points in some polygons
# via sum((x2-x1)(y2+y1)) (-ve means ccw)
poly_tree = union_nonzero(polys)
if poly_tree is None:
return []
@ -114,7 +173,27 @@ def label_poly(
point_names: Sequence[str],
clipper_scale_factor: int = int(2 ** 24),
) -> List[str]:
"""
Given a `PyPolyNode` (a polygon, possibly with holes) and a sequence of named points,
return the list of point names contained inside the polygon.
Args:
poly: A polygon, possibly with holes. "Islands" inside the holes (and deeper-nested
structures) are not considered (i.e. only one non-hole contour is considered).
point_xys: A sequence of point coordinates (Nx2, `[(x0, y0), (x1, y1), ...]`).
point_names: A sequence of point names (same length N as point_xys)
clipper_scale_factor: The PyPolyNode structure is from `pyclipper` and likely has
a scale factor applied in order to use integer arithmetic. Due to precision
limitations in `poly_contains_points`, it's prefereable to undo this scaling
rather than asking for similarly-scaled `point_xys` coordinates.
NOTE: This could be fixed by using `numpy.longdouble` in
`poly_contains_points`, but the exact length of long-doubles is platform-
dependent and so probably best avoided.
Result:
All the `point_names` which correspond to points inside the polygon (but not in
its holes).
"""
poly_contour = scale_from_clipper(poly.Contour, clipper_scale_factor)
inside = poly_contains_points(poly_contour, point_xys)
for hole in poly.Childs:
@ -134,9 +213,24 @@ def find_merge_pairs(
nets: Mapping[NetName, Mapping[layer_t, Sequence[contour_t]]],
via_polys: Mapping[layer_t, Sequence[contour_t]],
) -> Set[Tuple[NetName, NetName]]:
#
# Merge nets based on via connectivity
#
"""
Given a collection of (possibly anonymous) nets, figure out which pairs of
nets are shorted through a via (and thus should be merged).
Args:
connectivity: A sequence of 3-tuples specifying the electrical connectivity between layers.
Each 3-tuple looks like `(top_layer, via_layer, bottom_layer)` and indicates that
`top_layer` and `bottom_layer` are electrically connected at any location where
shapes are present on all three (top, via, and bottom) layers.
`via_layer` may be `None`, in which case any overlap between shapes on `top_layer`
and `bottom_layer` is automatically considered a short (with no third shape necessary).
nets: A collection of all nets (seqences of polygons in mappings indexed by `NetName`
and layer). See `NetsInfo.nets`.
via_polys: A collection of all vias (in a mapping indexed by layer).
Returns:
A set containing pairs of `NetName`s for each pair of nets which are shorted.
"""
merge_pairs = set()
for top_layer, via_layer, bot_layer in connectivity:
if via_layer is not None:
@ -152,7 +246,7 @@ def find_merge_pairs(
for bot_name in nets.keys():
if bot_name == top_name:
continue
name_pair = tuple(sorted((top_name, bot_name)))
name_pair: Tuple[NetName, NetName] = tuple(sorted((top_name, bot_name))) #type: ignore
if name_pair in merge_pairs:
continue
@ -164,7 +258,7 @@ def find_merge_pairs(
via_top = intersection_evenodd(top_polys, vias)
overlap = intersection_evenodd(via_top, bot_polys)
else:
overlap = intersection_evenodd(top_polys, bot_polys) # TODO verify there aren't any suspicious corner cases for this
overlap = intersection_evenodd(top_polys, bot_polys) # TODO verify there aren't any suspicious corner cases for this
if not overlap:
continue

@ -1,3 +1,6 @@
"""
Utilities for working with polygons
"""
import numpy
from numpy.typing import NDArray, ArrayLike

@ -1,4 +1,4 @@
from typing import List, Set, ClassVar, Optional
from typing import List, Set, ClassVar, Optional, Dict
from collections import defaultdict
from dataclasses import dataclass
@ -6,9 +6,17 @@ from .types import layer_t, contour_t
class NetName:
"""
Basically just a uniquely-sortable `Optional[str]`.
A `name` of `None` indicates that the net is anonymous.
The `subname` is used to track multiple same-named nets, to allow testing for opens.
"""
name: Optional[str]
subname: int
count: ClassVar[defaultdict[Optional[str], int]] = defaultdict(int)
""" Counter for how many classes have been instantiated with each name """
def __init__(self, name: Optional[str] = None) -> None:
self.name = name
@ -38,19 +46,57 @@ class NetName:
class NetsInfo:
"""
Container for describing all nets and keeping track of the "canonical" name for each
net. Nets which are known to be shorted together should be `merge`d together,
combining their geometry under the "canonical" name and adding the other name as an alias.
"""
nets: defaultdict[NetName, defaultdict[layer_t, List]]
net_aliases: defaultdict[NetName, NetName]
"""
Contains all polygons for all nets, in the format
`{net_name: {layer: [poly0, poly1, ...]}}`
Polygons are usually stored in pyclipper-friendly coordinates, but may be either `PyPolyNode`s
or simple lists of coordinates (oriented boundaries).
"""
net_aliases: Dict[NetName, NetName]
"""
A mapping from alias to underlying name.
Note that the underlying name may itself be an alias.
`resolve_name` can be used to simplify lookup
"""
def __init__(self) -> None:
self.nets = defaultdict(lambda: defaultdict(list))
self.net_aliases = defaultdict(list)
self.net_aliases = {}
def resolve_name(self, net_name: NetName) -> NetName:
"""
Find the canonical name (as used in `self.nets`) for any NetName.
Args:
net_name: The name of the net to look up. May be an alias.
Returns:
The canonical name for the net.
"""
while net_name in self.net_aliases:
net_name = self.net_aliases[net_name]
return net_name
def merge(self, net_a: NetName, net_b: NetName) -> None:
"""
Combine two nets into one.
Usually used when it is discovered that two nets are shorted.
The name that is preserved is based on the sort order of `NetName`s,
which favors non-anonymous, lexicograpically small names.
Args:
net_a: A net to merge
net_b: The other net to merge
"""
net_a = self.resolve_name(net_a)
net_b = self.resolve_name(net_b)
@ -64,10 +110,14 @@ class NetsInfo:
self.nets[keep_net][layer] += self.nets[old_net][layer]
del self.nets[old_net]
def get(self, net: NetName, layer: layer_t) -> List[contour_t]:
return self.nets[self.resolve_name(net)][layer]
def get_shorted_nets(self) -> List[Set[NetName]]:
"""
List groups of non-anonymous nets which were merged.
Returns:
A list of sets of shorted nets.
"""
shorts = defaultdict(list)
for kk in self.net_aliases:
if kk.name is None:
@ -82,6 +132,12 @@ class NetsInfo:
return shorted_sets
def get_open_nets(self) -> defaultdict[str, List[NetName]]:
"""
List groups of same-named nets which were *not* merged.
Returns:
A list of sets of same-named, non-shorted nets.
"""
opens = defaultdict(list)
seen_names = {}
for kk in self.nets:

@ -1,5 +1,5 @@
from typing import Union, Tuple, List, Sequence, Optional
from typing import Union, Tuple, List, Sequence, Optional, Hashable
layer_t = Tuple[int, int]
layer_t = Hashable
contour_t = List[Tuple[int, int]]
connectivity_t = Sequence[Tuple[layer_t, Optional[layer_t], layer_t]]

@ -1,3 +1,6 @@
"""
Some utility code that gets reused
"""
from typing import Set, Tuple
from .types import connectivity_t, layer_t
@ -6,6 +9,10 @@ from .types import connectivity_t, layer_t
def connectivity2layers(
connectivity: connectivity_t,
) -> Tuple[Set[layer_t], Set[layer_t]]:
"""
Extract the set of all metal layers and the set of all via layers
from the connectivity description.
"""
metal_layers = set()
via_layers = set()
for top, via, bot in connectivity:
@ -14,6 +21,8 @@ def connectivity2layers(
if via is not None:
via_layers.add(via)
# TODO verify no overlap between metal and via layer specifications
both = metal_layers.intersection(via_layers)
if both:
raise Exception(f'The following layers are both vias and metals!? {both}')
return metal_layers, via_layers

Loading…
Cancel
Save