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. 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 pprint import pformat
from masque.file import gdsii, oasis from masque.file import gdsii, oasis
@ -17,7 +21,7 @@ connectivity = {
cells, props = oasis.readfile('connectivity.oas') cells, props = oasis.readfile('connectivity.oas')
topcell = cells['top'] 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) nets_info = snarl.trace_connectivity(polys, labels, connectivity)
print('\nFinal nets:') 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 .main import trace_connectivity
from .tracker import NetsInfo, NetName
from . import interfaces from . import interfaces

@ -1,3 +1,6 @@
"""
Wrappers to simplify some pyclipper functions
"""
from typing import Sequence, Optional, List from typing import Sequence, Optional, List
from numpy.typing import ArrayLike 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 typing import Sequence, Dict, List, Any, Tuple, Optional, Mapping
from collections import defaultdict from collections import defaultdict
@ -11,13 +14,29 @@ from ..types import layer_t
from ..utils import connectivity2layers from ..utils import connectivity2layers
def read_topcell( def read_cell(
topcell: Pattern, cell: Pattern,
connectivity: Sequence[Tuple[layer_t, Optional[layer_t], layer_t]], connectivity: Sequence[Tuple[layer_t, Optional[layer_t], layer_t]],
label_mapping: Optional[Mapping[layer_t, layer_t]] = None, label_mapping: Optional[Mapping[layer_t, layer_t]] = None,
) -> Tuple[ ) -> Tuple[
defaultdict[layer_t, List[NDArray[numpy.float64]]], defaultdict[layer_t, List[NDArray[numpy.float64]]],
defaultdict[layer_t, List[Tuple[float, float, str]]]]: 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) metal_layers, via_layers = connectivity2layers(connectivity)
poly_layers = metal_layers | via_layers poly_layers = metal_layers | via_layers
@ -26,19 +45,19 @@ def read_topcell(
label_mapping = {layer: layer for layer in metal_layers} label_mapping = {layer: layer for layer in metal_layers}
label_layers = {label_layer for label_layer in label_mapping.keys()} 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, shapes_func=lambda ss: ss.layer in poly_layers,
labels_func=lambda ll: ll.layer in label_layers, labels_func=lambda ll: ll.layer in label_layers,
subpatterns_func=lambda ss: True, 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) metal_labels = defaultdict(list)
for label_layer, metal_layer in label_mapping.items(): for label_layer, metal_layer in label_mapping.items():
labels = [] labels = []
for ll in topcell.labels: for ll in cell.labels:
if ll.layer != label_layer: if ll.layer != label_layer:
continue continue
@ -57,11 +76,22 @@ def read_topcell(
def load_polys( def load_polys(
topcell: Pattern, cell: Pattern,
layers: Sequence[layer_t], layers: Sequence[layer_t],
) -> defaultdict[layer_t, List[NDArray[numpy.float64]]]: ) -> 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) polys = defaultdict(list)
for ss in topcell.shapes: for ss in cell.shapes:
if ss.layer not in layers: if ss.layer not in layers:
continue continue

@ -1,3 +1,6 @@
"""
Main connectivity-checking functionality for `snarl`
"""
from typing import Tuple, List, Dict, Set, Optional, Union, Sequence, Mapping from typing import Tuple, List, Dict, Set, Optional, Union, Sequence, Mapping
from collections import defaultdict from collections import defaultdict
from pprint import pformat from pprint import pformat
@ -21,19 +24,50 @@ def trace_connectivity(
polys: Mapping[layer_t, Sequence[ArrayLike]], polys: Mapping[layer_t, Sequence[ArrayLike]],
labels: Mapping[layer_t, Sequence[Tuple[float, float, str]]], labels: Mapping[layer_t, Sequence[Tuple[float, float, str]]],
connectivity: Sequence[Tuple[layer_t, Optional[layer_t], layer_t]], 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), clipper_scale_factor: int = int(2 ** 24),
) -> NetsInfo: ) -> 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) 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)) metal_polys = {layer: union_input_polys(scale_to_clipper(polys[layer], clipper_scale_factor))
for layer in metal_layers} for layer in metal_layers}
via_polys = {layer: union_input_polys(scale_to_clipper(polys[layer], clipper_scale_factor)) via_polys = {layer: union_input_polys(scale_to_clipper(polys[layer], clipper_scale_factor))
for layer in via_layers} for layer in via_layers}
#
# Check each polygon for labels, and assign it to a net (possibly anonymous).
#
nets_info = NetsInfo() nets_info = NetsInfo()
merge_groups: List[List[NetName]] = [] merge_groups: List[List[NetName]] = []
@ -47,27 +81,29 @@ def trace_connectivity(
for poly in metal_polys[layer]: for poly in metal_polys[layer]:
found_nets = label_poly(poly, point_xys, point_names, clipper_scale_factor) found_nets = label_poly(poly, point_xys, point_names, clipper_scale_factor)
name: Optional[str]
if found_nets: if found_nets:
name = NetName(found_nets[0]) name = NetName(found_nets[0])
else: else:
name = NetName() # Anonymous net name = NetName() # Anonymous net
nets_info.get(name, layer).append(poly) nets_info.nets[name][layer].append(poly)
if len(found_nets) > 1: if len(found_nets) > 1:
# Found a short # 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_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: for group in merge_groups:
first_net, *defunct_nets = group first_net, *defunct_nets = group
for defunct_net in defunct_nets: for defunct_net in defunct_nets:
nets_info.merge(first_net, defunct_net) nets_info.merge(first_net, defunct_net)
# #
# Take EVENODD union within each net # Convert to non-hierarchical polygon representation
# & stay in EVENODD-friendly representation
# #
for net in nets_info.nets.values(): for net in nets_info.nets.values():
for layer in net: for layer in net:
@ -77,7 +113,9 @@ def trace_connectivity(
for layer in via_polys: for layer in via_polys:
via_polys[layer] = hier2oriented(via_polys[layer]) 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) merge_pairs = find_merge_pairs(connectivity, nets_info.nets, via_polys)
for net_a, net_b in merge_pairs: for net_a, net_b in merge_pairs:
nets_info.merge(net_a, net_b) nets_info.merge(net_a, net_b)
@ -85,12 +123,33 @@ def trace_connectivity(
return nets_info 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: for poly in polys:
if (numpy.abs(poly) % 1).any(): if (numpy.abs(poly) % 1).any():
logger.warning('Warning: union_polys got non-integer coordinates; all values will be truncated.') logger.warning('Warning: union_polys got non-integer coordinates; all values will be truncated.')
break 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) poly_tree = union_nonzero(polys)
if poly_tree is None: if poly_tree is None:
return [] return []
@ -114,7 +173,27 @@ def label_poly(
point_names: Sequence[str], point_names: Sequence[str],
clipper_scale_factor: int = int(2 ** 24), clipper_scale_factor: int = int(2 ** 24),
) -> List[str]: ) -> 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) poly_contour = scale_from_clipper(poly.Contour, clipper_scale_factor)
inside = poly_contains_points(poly_contour, point_xys) inside = poly_contains_points(poly_contour, point_xys)
for hole in poly.Childs: for hole in poly.Childs:
@ -134,9 +213,24 @@ def find_merge_pairs(
nets: Mapping[NetName, Mapping[layer_t, Sequence[contour_t]]], nets: Mapping[NetName, Mapping[layer_t, Sequence[contour_t]]],
via_polys: Mapping[layer_t, Sequence[contour_t]], via_polys: Mapping[layer_t, Sequence[contour_t]],
) -> Set[Tuple[NetName, NetName]]: ) -> 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() merge_pairs = set()
for top_layer, via_layer, bot_layer in connectivity: for top_layer, via_layer, bot_layer in connectivity:
if via_layer is not None: if via_layer is not None:
@ -152,7 +246,7 @@ def find_merge_pairs(
for bot_name in nets.keys(): for bot_name in nets.keys():
if bot_name == top_name: if bot_name == top_name:
continue 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: if name_pair in merge_pairs:
continue continue
@ -164,7 +258,7 @@ def find_merge_pairs(
via_top = intersection_evenodd(top_polys, vias) via_top = intersection_evenodd(top_polys, vias)
overlap = intersection_evenodd(via_top, bot_polys) overlap = intersection_evenodd(via_top, bot_polys)
else: 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: if not overlap:
continue continue

@ -1,3 +1,6 @@
"""
Utilities for working with polygons
"""
import numpy import numpy
from numpy.typing import NDArray, ArrayLike 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 collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
@ -6,9 +6,17 @@ from .types import layer_t, contour_t
class NetName: 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] name: Optional[str]
subname: int subname: int
count: ClassVar[defaultdict[Optional[str], int]] = defaultdict(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: def __init__(self, name: Optional[str] = None) -> None:
self.name = name self.name = name
@ -38,19 +46,57 @@ class NetName:
class NetsInfo: 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]] 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: def __init__(self) -> None:
self.nets = defaultdict(lambda: defaultdict(list)) self.nets = defaultdict(lambda: defaultdict(list))
self.net_aliases = defaultdict(list) self.net_aliases = {}
def resolve_name(self, net_name: NetName) -> NetName: 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: while net_name in self.net_aliases:
net_name = self.net_aliases[net_name] net_name = self.net_aliases[net_name]
return net_name return net_name
def merge(self, net_a: NetName, net_b: NetName) -> None: 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_a = self.resolve_name(net_a)
net_b = self.resolve_name(net_b) net_b = self.resolve_name(net_b)
@ -64,10 +110,14 @@ class NetsInfo:
self.nets[keep_net][layer] += self.nets[old_net][layer] self.nets[keep_net][layer] += self.nets[old_net][layer]
del self.nets[old_net] 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]]: 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) shorts = defaultdict(list)
for kk in self.net_aliases: for kk in self.net_aliases:
if kk.name is None: if kk.name is None:
@ -82,6 +132,12 @@ class NetsInfo:
return shorted_sets return shorted_sets
def get_open_nets(self) -> defaultdict[str, List[NetName]]: 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) opens = defaultdict(list)
seen_names = {} seen_names = {}
for kk in self.nets: 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]] contour_t = List[Tuple[int, int]]
connectivity_t = Sequence[Tuple[layer_t, Optional[layer_t], layer_t]] 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 typing import Set, Tuple
from .types import connectivity_t, layer_t from .types import connectivity_t, layer_t
@ -6,6 +9,10 @@ from .types import connectivity_t, layer_t
def connectivity2layers( def connectivity2layers(
connectivity: connectivity_t, connectivity: connectivity_t,
) -> Tuple[Set[layer_t], Set[layer_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() metal_layers = set()
via_layers = set() via_layers = set()
for top, via, bot in connectivity: for top, via, bot in connectivity:
@ -14,6 +21,8 @@ def connectivity2layers(
if via is not None: if via is not None:
via_layers.add(via) 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 return metal_layers, via_layers

Loading…
Cancel
Save