from typing import List, Set, ClassVar, Optional, Dict from collections import defaultdict from dataclasses import dataclass 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 self.subname = self.count[name] NetName.count[name] += 1 def __lt__(self, other: 'NetName') -> bool: if self.name == other.name: return self.subname < other.subname elif self.name is None: return False elif other.name is None: return True else: return self.name < other.name def __repr__(self) -> str: if self.name is not None: name = self.name else: name = '(None)' if NetName.count[self.name] == 1: return name else: return f'{name}__{self.subname}' 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]] """ 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 = {} 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) # Always keep named nets if the other is anonymous keep_net, old_net = sorted((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_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: continue base_name = self.resolve_name(kk) assert(base_name.name is not None) shorts[base_name].append(kk) shorted_sets = [set([kk] + others) for kk, others in shorts.items()] 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: if kk.name is None: continue if kk.name in seen_names: if kk.name not in opens: opens[kk.name].append(seen_names[kk.name]) opens[kk.name].append(kk) else: seen_names[kk.name] = kk return opens