You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
snarled/snarled/tracker.py

154 lines
4.7 KiB
Python

from typing import List, Set, ClassVar, Optional, Dict
2 years ago
from collections import defaultdict
from dataclasses import dataclass
2 years ago
from .types import layer_t, contour_t
class NetName:
2 years ago
"""
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
2 years ago
count: ClassVar[defaultdict[Optional[str], int]] = defaultdict(int)
2 years ago
""" 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}'
2 years ago
class NetsInfo:
2 years ago
"""
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]]
2 years ago
"""
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]
2 years ago
"""
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
"""
2 years ago
def __init__(self) -> None:
self.nets = defaultdict(lambda: defaultdict(list))
self.net_aliases = {}
2 years ago
def resolve_name(self, net_name: NetName) -> NetName:
2 years ago
"""
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.
"""
2 years ago
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:
2 years ago
"""
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
"""
2 years ago
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))
2 years ago
#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]]:
2 years ago
"""
List groups of non-anonymous nets which were merged.
Returns:
A list of sets of shorted nets.
"""
2 years ago
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)
2 years ago
shorted_sets = [set([kk] + others)
for kk, others in shorts.items()]
return shorted_sets
def get_open_nets(self) -> defaultdict[str, List[NetName]]:
2 years ago
"""
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