from typing import Tuple, List, Dict, Set, Optional, Union, Sequence from collections import defaultdict from pprint import pformat import logging import numpy from numpy.typing import NDArray, ArrayLike import pyclipper from pyclipper import ( Pyclipper, PT_CLIP, PT_SUBJECT, CT_UNION, CT_INTERSECTION, PFT_NONZERO, PFT_EVENODD, scale_to_clipper, scale_from_clipper, PyPolyNode, ) from masque.file import oasis, gdsii from masque import Pattern from masque.shapes import Polygon from .poly import poly_contains_points logger = logging.getLogger(__name__) layer_t = Tuple[int, int] contour_t = List[Tuple[int, int]] net_name_t = Union[str, object] CLIPPER_SCALE_FACTOR = 2**24 def union_nonzero(shapes: Sequence[ArrayLike]) -> Optional[PyPolyNode]: if not shapes: return None pc = Pyclipper() pc.AddPaths(shapes, PT_CLIP, closed=True) result = pc.Execute2(CT_UNION, PFT_NONZERO, PFT_NONZERO) return result def union_evenodd(shapes: Sequence[ArrayLike]) -> List[contour_t]: if not shapes: return [] pc = Pyclipper() pc.AddPaths(shapes, PT_CLIP, closed=True) return pc.Execute(CT_UNION, PFT_EVENODD, PFT_EVENODD) def intersection_evenodd( subject_shapes: Sequence[ArrayLike], clip_shapes: Sequence[ArrayLike], clip_closed: bool = True, ) -> List[contour_t]: if not subject_shapes or not clip_shapes: return [] pc = Pyclipper() pc.AddPaths(subject_shapes, PT_SUBJECT, closed=True) pc.AddPaths(clip_shapes, PT_CLIP, closed=clip_closed) return pc.Execute(CT_INTERSECTION, PFT_EVENODD, PFT_EVENODD) class NetsInfo: nets: defaultdict[str, defaultdict[layer_t, List]] net_aliases: defaultdict[str, List] def __init__(self) -> None: self.nets = defaultdict(lambda: defaultdict(list)) self.net_aliases = defaultdict(list) def resolve_name(self, net_name: net_name_t) -> net_name_t: while net_name in self.net_aliases: net_name = self.net_aliases[net_name] return net_name def merge(self, net_a: net_name_t, net_b: net_name_t) -> None: net_a = self.resolve_name(net_a) net_b = self.resolve_name(net_b) # Always keep named nets if the other is anonymous if not isinstance(net_a, str) and isinstance(net_b, str): keep_net, old_net = net_b, net_a else: keep_net, old_net = net_a, net_b #logger.info(f'merging {old_net} into {keep_net}') self.net_aliases[old_net] = keep_net if old_net in self.nets: for layer in self.nets[old_net]: self.nets[keep_net][layer] += self.nets[old_net][layer] del self.nets[old_net] def get(self, net: net_name_t, layer: layer_t) -> List[contour_t]: return self.nets[self.resolve_name(net)][layer] def get_shorted_nets(self) -> List[Set[str]]: shorts = defaultdict(list) for kk in self.net_aliases: if isinstance(kk, str): shorts[self.resolve_name(kk)].append(kk) shorted_sets = [set([kk] + others) for kk, others in shorts.items()] return shorted_sets def load_polys(layers: Sequence[Tuple[int, int]]) -> Dict[layer_t, List[NDArray[numpy.float64]]]: polys = defaultdict(list) for ss in topcell.shapes: if ss.layer not in layers: continue if ss.repetition is None: displacements = [(0, 0)] else: displacements = ss.repetition.displacements for displacement in displacements: polys[ss.layer].append( ss.vertices + ss.offset + displacement ) return dict(polys) def union_polys(polys: list[ArrayLike]) -> List[PyPolyNode]: scaled_polys = scale_to_clipper(polys, CLIPPER_SCALE_FACTOR) for poly in scaled_polys: if (numpy.abs(poly) % 1).any(): logger.warning('Warning: union_polys got non-integer coordinates; all values will be truncated.') break poly_tree = union_nonzero(scaled_polys) # Partially flatten the tree, reclassifying all the "outer" (non-hole) nodes as new root nodes unvisited_nodes = [poly_tree] outer_nodes = [] while unvisited_nodes: node = unvisited_nodes.pop() # node will be the tree parent node (a container), or a hole for poly in node.Childs: outer_nodes.append(poly) for hole in poly.Childs: unvisited_nodes.append(hole) return outer_nodes cells, props = oasis.readfile('connectivity.oas') #cells, props = gdsii.readfile('connectivity.gds') topcell = cells['top'] layer_info = { ((1, 0), (1, 2), (2, 0)), #M1 to M2 ((1, 0), (1, 3), (3, 0)), #M1 to M3 ((2, 0), (2, 3), (3, 0)), #M2 to M3 } metal_layers = set() via_layers = set() for top, via, bot in layer_info: metal_layers.add(top) metal_layers.add(bot) via_layers.add(via) topcell = topcell.subset( shapes_func=lambda ss: ss.layer in metal_layers | via_layers, labels_func=lambda ll: ll.layer in metal_layers, subpatterns_func=lambda ss: True, ) topcell = topcell.flatten() base_metal_polys = load_polys(metal_layers) metal_polys = {layer: union_polys(base_metal_polys[layer]) for layer in metal_layers} base_via_polys = load_polys(via_layers) via_polys = {layer: union_polys(base_via_polys[layer]) for layer in via_layers} ## write out polys to gds #pat = Pattern('metal_polys') #for layer in metal_polys: # for poly in metal_polys[layer]: # pat.shapes.append(Polygon(layer=layer, vertices=scale_from_clipper(poly.Contour, CLIPPER_SCALE_FACTOR))) # for hole in poly.Childs: # pat.shapes.append(Polygon(layer=(layer[0], layer[1] + 10), vertices=scale_from_clipper(hole.Contour, CLIPPER_SCALE_FACTOR))) #for layer in via_polys: # for poly in via_polys[layer]: # pat.shapes.append(Polygon(layer=layer, vertices=scale_from_clipper(poly.Contour, CLIPPER_SCALE_FACTOR))) # for hole in poly.Childs: # pat.shapes.append(Polygon(layer=(layer[0], layer[1] + 10), vertices=scale_from_clipper(hole.Contour, CLIPPER_SCALE_FACTOR))) #gdsii.writefile(pat, '_polys.gds', 1e-9, 1e-6) net_info = NetsInfo() def label_nets( net_info: NetsInfo, polys: Sequence[PyPolyNode], point_xys: ArrayLike, point_names: Sequence[str], ): for poly in polys: poly_contour = scale_from_clipper(poly.Contour, CLIPPER_SCALE_FACTOR) inside = poly_contains_points(poly_contour, point_xys) for hole in poly.Childs: hole_contour = scale_from_clipper(hole.Contour, CLIPPER_SCALE_FACTOR) inside &= ~poly_contains_points(hole_contour, point_xys) inside_nets = sorted([net_name for net_name, ii in zip(point_names, inside) if ii]) if not inside.any(): # No labels in this net, so it's anonymous name = object() net_info.nets[name][layer].append(poly) continue net_info.get(inside_nets[0], layer).append(poly) if inside.sum() == 1: # No short on this layer, continue continue logger.warning(f'Nets {inside_nets} are shorted on layer {layer} in poly:\n {pformat(poly)}') first_net, *defunct_nets = inside_nets for defunct_net in defunct_nets: net_info.merge(first_net, defunct_net) contours = [] def tree2oriented(polys: Sequence[PyPolyNode]) -> List[ArrayLike]: contours = [] for poly in polys: contours.append(poly.Contour) contours += [hole.Contour for hole in poly.Childs] return union_evenodd(contours) for layer in metal_layers: labels = sorted([ll for ll in topcell.labels if ll.layer == layer], key=lambda ll: ll.string) point_xys = [ll.offset for ll in labels] point_names = [ll.string for ll in labels] label_nets(net_info, metal_polys[layer], point_xys, point_names) # # Take EVENODD union within each net # & stay in EVENODD-friendly representation # for net in net_info.nets.values(): for layer in net: net[layer] = tree2oriented(net[layer]) for layer in via_polys: via_polys[layer] = tree2oriented(via_polys[layer]) ## write out nets to gds #pat = Pattern('nets') #for name, net in net_info.nets.items(): # sub = Pattern(str(name)) # for layer in net: # print('aaaaaa', layer) # for poly in net[layer]: # sub.shapes.append(Polygon(layer=layer, vertices=scale_from_clipper(poly, CLIPPER_SCALE_FACTOR))) # pat.addsp(sub) #gdsii.writefile(pat, '_nets.gds', 1e-9, 1e-6) # # Merge nets based on via connectivity # merge_pairs = set() for top_layer, via_layer, bot_layer in layer_info: vias = via_polys[via_layer] if not vias: continue #TODO deal with polygons that have holes (loops?) for top_name in net_info.nets.keys(): top_polys = net_info.nets[top_name][top_layer] if not top_polys: continue for bot_name in net_info.nets.keys(): if bot_name == top_name: continue name_pair = tuple(sorted((top_name, bot_name), key=lambda s: id(s))) if name_pair in merge_pairs: continue bot_polys = net_info.nets[bot_name][bot_layer] if not bot_polys: continue via_top = intersection_evenodd(top_polys, vias) overlap = intersection_evenodd(via_top, bot_polys) if not overlap: continue if isinstance(bot_name, str) and isinstance(top_name, str): logger.warning(f'Nets {top_name} and {bot_name} are shorted with via layer {via_layer} at:\n {pformat(scale_from_clipper(overlap[0], CLIPPER_SCALE_FACTOR))}') merge_pairs.add(name_pair) for net_a, net_b in merge_pairs: net_info.merge(net_a, net_b) print('merged pairs') print(pformat(merge_pairs)) print('\nFinal nets:') print([kk for kk in net_info.nets if isinstance(kk, str)]) print('\nNet sets:') for short in net_info.get_shorted_nets(): print('(' + ','.join(sorted(list(short))) + ')')