Compare commits
16 Commits
performanc
...
master
Author | SHA1 | Date | |
---|---|---|---|
ceaebeb4c7 | |||
bb9552ce1f | |||
22f8015b82 | |||
7c718401d4 | |||
a3978b5f5e | |||
012a242379 | |||
2d174f7a1d | |||
1935c220dc | |||
9cef719ba9 | |||
87ee1578a5 | |||
afe3a3f129 | |||
cfc7eb1890 | |||
3638e17777 | |||
693d04ccc1 | |||
0ffe18c9f1 | |||
9017984b4b |
29
.flake8
Normal file
29
.flake8
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[flake8]
|
||||||
|
ignore =
|
||||||
|
# E501 line too long
|
||||||
|
E501,
|
||||||
|
# W391 newlines at EOF
|
||||||
|
W391,
|
||||||
|
# E241 multiple spaces after comma
|
||||||
|
E241,
|
||||||
|
# E302 expected 2 newlines
|
||||||
|
E302,
|
||||||
|
# W503 line break before binary operator (to be deprecated)
|
||||||
|
W503,
|
||||||
|
# E265 block comment should start with '# '
|
||||||
|
E265,
|
||||||
|
# E123 closing bracket does not match indentation of opening bracket's line
|
||||||
|
E123,
|
||||||
|
# E124 closing bracket does not match visual indentation
|
||||||
|
E124,
|
||||||
|
# E221 multiple spaces before operator
|
||||||
|
E221,
|
||||||
|
# E201 whitespace after '['
|
||||||
|
E201,
|
||||||
|
# E741 ambiguous variable name 'I'
|
||||||
|
E741,
|
||||||
|
|
||||||
|
|
||||||
|
per-file-ignores =
|
||||||
|
# F401 import without use
|
||||||
|
*/__init__.py: F401,
|
128
README.md
128
README.md
@ -10,107 +10,105 @@ has deprived the man of both a schematic and a better connectivity tool.
|
|||||||
|
|
||||||
- [Source repository](https://mpxd.net/code/jan/snarled)
|
- [Source repository](https://mpxd.net/code/jan/snarled)
|
||||||
- [PyPI](https://pypi.org/project/snarled)
|
- [PyPI](https://pypi.org/project/snarled)
|
||||||
|
- [Github mirror](https://github.com/anewusername/snarled)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
* python >= 3.9 (written and tested with 3.10)
|
* python >= 3.10 (written and tested with 3.11)
|
||||||
* numpy
|
* numpy
|
||||||
* pyclipper
|
* klayout (python package only)
|
||||||
|
|
||||||
|
|
||||||
Install with pip:
|
Install with pip:
|
||||||
```bash
|
```bash
|
||||||
pip3 install snarled
|
pip install snarled
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, install from git
|
Alternatively, install from git
|
||||||
```bash
|
```bash
|
||||||
pip3 install git+https://mpxd.net/code/jan/snarled.git@release
|
pip install git+https://mpxd.net/code/jan/snarled.git@release
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
See `examples/check.py`. Note that the example uses `masque` to load data.
|
See `examples/check.py` (python interface) or `examples/run.sh` (command-line interface).
|
||||||
|
|
||||||
|
Command line:
|
||||||
|
```bash
|
||||||
|
snarled connectivity.oas connectivity.txt -m layermap.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Python interface:
|
||||||
```python3
|
```python3
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from masque.file import gdsii, oasis
|
import logging
|
||||||
import snarled
|
|
||||||
import snarled.interfaces.masque
|
|
||||||
|
|
||||||
# Layer definitions
|
import snarled
|
||||||
connectivity = {
|
from snarled.types import layer_t
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
logging.getLogger('snarled').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
connectivity = [
|
||||||
((1, 0), (1, 2), (2, 0)), # M1 to M2 (via V12)
|
((1, 0), (1, 2), (2, 0)), # M1 to M2 (via V12)
|
||||||
((1, 0), (1, 3), (3, 0)), # M1 to M3 (via V13)
|
((1, 0), (1, 3), (3, 0)), # M1 to M3 (via V13)
|
||||||
((2, 0), (2, 3), (3, 0)), # M2 to M3 (via V23)
|
((2, 0), (2, 3), (3, 0)), # M2 to M3 (via V23)
|
||||||
|
]
|
||||||
|
|
||||||
|
labels_map: dict[layer_t, layer_t] = {
|
||||||
|
(1, 0): (1, 0),
|
||||||
|
(2, 0): (2, 0),
|
||||||
|
(3, 0): (3, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filename = 'connectivity.oas'
|
||||||
|
|
||||||
cells, props = oasis.readfile('connectivity.oas')
|
nets = snarled.trace_layout(filename, connectivity, topcell='top', labels_map=labels_map)
|
||||||
topcell = cells['top']
|
result = snarled.TraceAnalysis(nets)
|
||||||
|
|
||||||
polys, labels = snarled.interfaces.masque.read_cell(topcell, connectivity)
|
print('\n')
|
||||||
nets_info = snarled.trace_connectivity(polys, labels, connectivity)
|
print(result)
|
||||||
|
|
||||||
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:
|
this prints the following:
|
||||||
|
|
||||||
```
|
```
|
||||||
Nets ['SignalD', 'SignalI'] are shorted on layer (1, 0) in poly:
|
INFO:snarled.trace:Adding layer (3, 0)
|
||||||
[[13000.0, -3000.0],
|
INFO:snarled.trace:Adding layer (2, 3)
|
||||||
[16000.0, -3000.0],
|
INFO:snarled.trace:Adding layer (1, 3)
|
||||||
[16000.0, -1000.0],
|
INFO:snarled.trace:Adding layer (1, 2)
|
||||||
[13000.0, -1000.0],
|
INFO:snarled.trace:Adding layer (1, 0)
|
||||||
[13000.0, 2000.0],
|
INFO:snarled.trace:Adding layer (2, 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:
|
|
||||||
[SignalA, SignalC__0, SignalE, SignalG, SignalK__0, SignalK__2, SignalL]
|
|
||||||
|
|
||||||
Shorted net sets:
|
Trace analysis
|
||||||
(SignalC__0,SignalC__1,SignalD,SignalI)
|
=============
|
||||||
(SignalK__0,SignalK__1)
|
Nets
|
||||||
(SignalG,SignalH)
|
(groups of electrically connected labels)
|
||||||
(SignalA,SignalB)
|
SignalA,SignalB
|
||||||
(SignalE,SignalF)
|
SignalC,SignalD,SignalI
|
||||||
|
SignalE,SignalF
|
||||||
|
SignalG,SignalH
|
||||||
|
SignalK
|
||||||
|
SignalK
|
||||||
|
SignalL
|
||||||
|
|
||||||
Open nets:
|
Opens
|
||||||
{'SignalK': [SignalK__0, SignalK__2]}
|
(2+ nets containing the same name)
|
||||||
|
SignalK : 2 nets
|
||||||
|
|
||||||
|
Shorts
|
||||||
|
(2+ unique names for the same net)
|
||||||
|
SignalA,SignalB
|
||||||
|
SignalC,SignalD,SignalI
|
||||||
|
SignalE,SignalF
|
||||||
|
SignalG,SignalH
|
||||||
|
=============
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code organization
|
## Code organization
|
||||||
|
|
||||||
- The main functionality is in `trace_connectivity`.
|
- The primary functionality is in `trace`; specifically `trace.trace_layout()`.
|
||||||
- Useful classes, namely `NetsInfo` and `NetName`, are in `snarled.tracker`.
|
- `main` provides a command-line interface, supported by the functions in `utils`.
|
||||||
- `snarled.interfaces` contains helper code for interfacing with other packages.
|
|
||||||
|
|
||||||
## Caveats
|
|
||||||
|
|
||||||
This package is slow, dumb, and the code is ugly. There's only a basic test case.
|
|
||||||
|
|
||||||
If you know what you're doing, you could probably do a much better job of it.
|
|
||||||
|
|
||||||
...but you *have* heard of it :)
|
|
||||||
|
|
||||||
|
@ -1,40 +1,33 @@
|
|||||||
"""
|
"""
|
||||||
Example code for checking connectivity in a layout by using
|
Example code for checking connectivity in a layout by using `snarled`
|
||||||
`snarled` and `masque`.
|
|
||||||
"""
|
"""
|
||||||
from pprint import pformat
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from masque.file import gdsii, oasis
|
|
||||||
|
|
||||||
import snarled
|
import snarled
|
||||||
import snarled.interfaces.masque
|
from snarled.types import layer_t
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
logging.getLogger('snarled').setLevel(logging.INFO)
|
logging.getLogger('snarled').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# How are the conductors connected to each other?
|
||||||
connectivity = [
|
connectivity = [
|
||||||
((1, 0), (1, 2), (2, 0)), # M1 to M2 (via V12)
|
((1, 0), (1, 2), (2, 0)), # M1 to M2 (via V12)
|
||||||
((1, 0), (1, 3), (3, 0)), # M1 to M3 (via V13)
|
((1, 0), (1, 3), (3, 0)), # M1 to M3 (via V13)
|
||||||
((2, 0), (2, 3), (3, 0)), # M2 to M3 (via V23)
|
((2, 0), (2, 3), (3, 0)), # M2 to M3 (via V23)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# What labels should be loaded, and which geometry layers should they apply to?
|
||||||
|
labels_map: dict[layer_t, layer_t] = {
|
||||||
|
(1, 0): (1, 0),
|
||||||
|
(2, 0): (2, 0),
|
||||||
|
(3, 0): (3, 0),
|
||||||
|
}
|
||||||
|
|
||||||
#cells, props = gdsii.readfile('connectivity.gds')
|
filename = 'connectivity.oas'
|
||||||
cells, props = oasis.readfile('connectivity.oas')
|
|
||||||
topcell = cells['top']
|
|
||||||
|
|
||||||
get_layer = snarled.interfaces.masque.prepare_cell(topcell)
|
nets = snarled.trace_layout(filename, connectivity, topcell='top', labels_map=labels_map)
|
||||||
nets_info = snarled.trace_connectivity(get_layer, connectivity)
|
result = snarled.TraceAnalysis(nets)
|
||||||
|
|
||||||
print('\nFinal nets:')
|
print('\n')
|
||||||
print([kk for kk in sorted(nets_info.nets.keys()) if isinstance(kk.name, str)])
|
print(result)
|
||||||
|
|
||||||
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())))
|
|
||||||
|
Binary file not shown.
3
examples/connectivity.txt
Normal file
3
examples/connectivity.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
M1, V12, M2
|
||||||
|
M1, V13, M3
|
||||||
|
M2, V23, M3
|
6
examples/layermap.txt
Normal file
6
examples/layermap.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
1/0:M1
|
||||||
|
2/0:M2
|
||||||
|
3/0:M3
|
||||||
|
1/2:V12
|
||||||
|
1/3:V13
|
||||||
|
2/3:V23
|
5
examples/run.sh
Executable file
5
examples/run.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
cd $(dirname -- "$0") # cd to this script's parent directory
|
||||||
|
|
||||||
|
snarled connectivity.oas connectivity.txt -m layermap.txt
|
93
pyproject.toml
Normal file
93
pyproject.toml
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "snarled"
|
||||||
|
description = "CAD layout electrical connectivity checker"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { file = "LICENSE.md" }
|
||||||
|
authors = [
|
||||||
|
{ name="Jan Petykiewicz", email="jan@mpxd.net" },
|
||||||
|
]
|
||||||
|
homepage = "https://mpxd.net/code/jan/snarled"
|
||||||
|
repository = "https://mpxd.net/code/jan/snarled"
|
||||||
|
keywords = [
|
||||||
|
"layout",
|
||||||
|
"design",
|
||||||
|
"CAD",
|
||||||
|
"EDA",
|
||||||
|
"electronics",
|
||||||
|
"photonics",
|
||||||
|
"IC",
|
||||||
|
"mask",
|
||||||
|
"pattern",
|
||||||
|
"drawing",
|
||||||
|
"lvs",
|
||||||
|
"connectivity",
|
||||||
|
"short",
|
||||||
|
"unintentional",
|
||||||
|
"label",
|
||||||
|
"schematic",
|
||||||
|
"verification",
|
||||||
|
"checking",
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: Information Technology",
|
||||||
|
"Intended Audience :: Manufacturing",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
|
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
"klayout~=0.29",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "snarled/__init__.py"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
snarled = "snarled.main:main"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [
|
||||||
|
".git",
|
||||||
|
"dist",
|
||||||
|
]
|
||||||
|
line-length = 145
|
||||||
|
indent-width = 4
|
||||||
|
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
lint.select = [
|
||||||
|
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
|
||||||
|
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
|
||||||
|
"ARG", "PL", "R", "TRY",
|
||||||
|
"G010", "G101", "G201", "G202",
|
||||||
|
"Q002", "Q003", "Q004",
|
||||||
|
]
|
||||||
|
lint.ignore = [
|
||||||
|
#"ANN001", # No annotation
|
||||||
|
"ANN002", # *args
|
||||||
|
"ANN003", # **kwargs
|
||||||
|
"ANN401", # Any
|
||||||
|
"ANN101", # self: Self
|
||||||
|
"SIM108", # single-line if / else assignment
|
||||||
|
"RET504", # x=y+z; return x
|
||||||
|
"PIE790", # unnecessary pass
|
||||||
|
"ISC003", # non-implicit string concatenation
|
||||||
|
"C408", # dict(x=y) instead of {'x': y}
|
||||||
|
"PLR09", # Too many xxx
|
||||||
|
"PLR2004", # magic number
|
||||||
|
"PLC0414", # import x as x
|
||||||
|
"TRY003", # Long exception message
|
||||||
|
"PTH123", # open()
|
||||||
|
"UP015", # open(..., 'rt')
|
||||||
|
"PLW2901", # overwriting loop var
|
||||||
|
]
|
||||||
|
|
63
setup.py
63
setup.py
@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
|
|
||||||
with open('README.md', 'rt') as f:
|
|
||||||
long_description = f.read()
|
|
||||||
|
|
||||||
with open('snarled/VERSION.py', 'rt') as f:
|
|
||||||
version = f.readlines()[2].strip()
|
|
||||||
|
|
||||||
setup(name='snarled',
|
|
||||||
version=version,
|
|
||||||
description='CAD layout electrical connectivity checker',
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type='text/markdown',
|
|
||||||
author='Jan Petykiewicz',
|
|
||||||
author_email='jan@mpxd.net',
|
|
||||||
url='https://mpxd.net/code/jan/snarled',
|
|
||||||
packages=find_packages(),
|
|
||||||
package_data={
|
|
||||||
'snarled': ['py.typed',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
install_requires=[
|
|
||||||
'numpy',
|
|
||||||
'pyclipper',
|
|
||||||
],
|
|
||||||
extras_require={
|
|
||||||
'masque': ['masque'],
|
|
||||||
'oasis': ['fatamorgana>=0.7'],
|
|
||||||
'gdsii': ['klamath>=1.0'],
|
|
||||||
},
|
|
||||||
classifiers=[
|
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Development Status :: 4 - Beta',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'Intended Audience :: Information Technology',
|
|
||||||
'Intended Audience :: Manufacturing',
|
|
||||||
'Intended Audience :: Science/Research',
|
|
||||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
|
||||||
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
|
|
||||||
],
|
|
||||||
keywords=[
|
|
||||||
'layout',
|
|
||||||
'design',
|
|
||||||
'CAD',
|
|
||||||
'EDA',
|
|
||||||
'electronics',
|
|
||||||
'photonics',
|
|
||||||
'IC',
|
|
||||||
'mask',
|
|
||||||
'pattern',
|
|
||||||
'drawing',
|
|
||||||
'lvs',
|
|
||||||
'connectivity',
|
|
||||||
'short',
|
|
||||||
'unintentional',
|
|
||||||
'label',
|
|
||||||
'schematic',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
|||||||
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
|
|
||||||
__version__ = '''
|
|
||||||
0.6
|
|
||||||
'''.strip()
|
|
@ -7,17 +7,15 @@ Layout connectivity checker.
|
|||||||
`snarled` is a python package for checking electrical connectivity in multi-layer layouts.
|
`snarled` 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
|
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.
|
has deprived the man of a schematic and a better connectivity tool.
|
||||||
|
|
||||||
The main functionality is in `trace_connectivity`.
|
The main functionality is in `trace`.
|
||||||
Useful classes, namely `NetsInfo` and `NetName`, are in `snarled.tracker`.
|
`__main__.py` details the command-line interface.
|
||||||
`snarled.interfaces` contains helper code for interfacing with other packages.
|
|
||||||
"""
|
"""
|
||||||
from .main import trace_connectivity, trace_connectivity_preloaded
|
from .trace import (
|
||||||
from .tracker import NetsInfo, NetName
|
trace_layout as trace_layout,
|
||||||
from . import interfaces
|
TraceAnalysis as TraceAnalysis,
|
||||||
|
)
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
__version__ = '1.0'
|
||||||
from .VERSION import __version__
|
|
||||||
|
3
snarled/__main__.py
Normal file
3
snarled/__main__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
from .main import main
|
||||||
|
main()
|
@ -1,64 +0,0 @@
|
|||||||
"""
|
|
||||||
Wrappers to simplify some pyclipper functions
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Optional, List
|
|
||||||
|
|
||||||
from numpy.typing import ArrayLike
|
|
||||||
from pyclipper import (
|
|
||||||
Pyclipper, PT_CLIP, PT_SUBJECT, CT_UNION, CT_INTERSECTION, PFT_NONZERO, PFT_EVENODD,
|
|
||||||
PyPolyNode, CT_DIFFERENCE,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .types import contour_t
|
|
||||||
|
|
||||||
|
|
||||||
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],
|
|
||||||
) -> 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=True)
|
|
||||||
return pc.Execute(CT_INTERSECTION, PFT_EVENODD, PFT_EVENODD)
|
|
||||||
|
|
||||||
|
|
||||||
def difference_evenodd(
|
|
||||||
subject_shapes: Sequence[ArrayLike],
|
|
||||||
clip_shapes: Sequence[ArrayLike],
|
|
||||||
) -> List[contour_t]:
|
|
||||||
if not subject_shapes:
|
|
||||||
return []
|
|
||||||
if not clip_shapes:
|
|
||||||
return subject_shapes
|
|
||||||
pc = Pyclipper()
|
|
||||||
pc.AddPaths(subject_shapes, PT_SUBJECT, closed=True)
|
|
||||||
pc.AddPaths(clip_shapes, PT_CLIP, closed=True)
|
|
||||||
return pc.Execute(CT_DIFFERENCE, PFT_EVENODD, PFT_EVENODD)
|
|
||||||
|
|
||||||
|
|
||||||
def hier2oriented(polys: Sequence[PyPolyNode]) -> List[ArrayLike]:
|
|
||||||
contours = []
|
|
||||||
for poly in polys:
|
|
||||||
contours.append(poly.Contour)
|
|
||||||
contours += [hole.Contour for hole in poly.Childs]
|
|
||||||
|
|
||||||
return contours
|
|
@ -1,127 +0,0 @@
|
|||||||
"""
|
|
||||||
Functionality for extracting geometry and label info from `masque` patterns.
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Dict, List, Any, Tuple, Optional, Mapping, Callable
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
from numpy.typing import NDArray
|
|
||||||
from masque import Pattern
|
|
||||||
from masque.file import oasis, gdsii
|
|
||||||
from masque.shapes import Polygon
|
|
||||||
|
|
||||||
from ..types import layer_t
|
|
||||||
from ..utils import connectivity2layers
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_cell(
|
|
||||||
cell: Pattern,
|
|
||||||
label_mapping: Optional[Mapping[layer_t, layer_t]] = None,
|
|
||||||
) -> Callable[[layer_t], Tuple[
|
|
||||||
List[NDArray[numpy.float64]],
|
|
||||||
List[Tuple[float, float, str]]
|
|
||||||
]]:
|
|
||||||
"""
|
|
||||||
Generate a function for extracting `polys` and `labels` from a `masque.Pattern`.
|
|
||||||
The returned function can be passed to `snarled.trace_connectivity`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cell: A `masque` `Pattern` object. Usually your topcell.
|
|
||||||
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.
|
|
||||||
Default `None` reads labels from the same layer as the geometry.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
`get_layer` function, to be passed to `snarled.trace_connectivity`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_layer(
|
|
||||||
layer: layer_t,
|
|
||||||
) -> Tuple[
|
|
||||||
List[NDArray[numpy.float64]],
|
|
||||||
List[Tuple[float, float, str]]
|
|
||||||
]:
|
|
||||||
|
|
||||||
if label_mapping is None:
|
|
||||||
label_layers = {layer: layer}
|
|
||||||
else:
|
|
||||||
label_layers = {label_layer for label_layer, metal_layer in label_mapping.items()
|
|
||||||
if metal_layer == layer}
|
|
||||||
|
|
||||||
subset = cell.deepcopy().subset( # TODO add single-op subset-and-copy, to avoid copying unwanted stuff
|
|
||||||
shapes_func=lambda ss: ss.layer == layer,
|
|
||||||
labels_func=lambda ll: ll.layer in label_layers,
|
|
||||||
subpatterns_func=lambda ss: True,
|
|
||||||
recursive=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
polygonized = subset.polygonize() # Polygonize Path shapes
|
|
||||||
flat = polygonized.flatten()
|
|
||||||
|
|
||||||
# load polygons
|
|
||||||
polys = []
|
|
||||||
for ss in flat.shapes:
|
|
||||||
assert(isinstance(ss, Polygon))
|
|
||||||
|
|
||||||
if ss.repetition is None:
|
|
||||||
displacements = [(0, 0)]
|
|
||||||
else:
|
|
||||||
displacements = ss.repetition.displacements
|
|
||||||
|
|
||||||
for displacement in displacements:
|
|
||||||
polys.append(
|
|
||||||
ss.vertices + ss.offset + displacement
|
|
||||||
)
|
|
||||||
|
|
||||||
# load metal labels
|
|
||||||
labels = []
|
|
||||||
for ll in flat.labels:
|
|
||||||
if ll.repetition is None:
|
|
||||||
displacements = [(0, 0)]
|
|
||||||
else:
|
|
||||||
displacements = ll.repetition.displacements
|
|
||||||
|
|
||||||
for displacement in displacements:
|
|
||||||
offset = ll.offset + displacement
|
|
||||||
labels.append((*offset, ll.string))
|
|
||||||
|
|
||||||
return polys, labels
|
|
||||||
return get_layer
|
|
||||||
|
|
||||||
|
|
||||||
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 `snarled.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 `snarled.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 `snarled.trace_connectivity`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
metal_layers, via_layers = connectivity2layers(connectivity)
|
|
||||||
poly_layers = metal_layers | via_layers
|
|
||||||
|
|
||||||
get_layer = prepare_cell(cell, label_mapping)
|
|
||||||
|
|
||||||
polys = defaultdict(list)
|
|
||||||
labels = defaultdict(list)
|
|
||||||
for layer in poly_layers:
|
|
||||||
polys[layer], labels[layer] = get_layer(layer)
|
|
||||||
|
|
||||||
return polys, labels
|
|
399
snarled/main.py
399
snarled/main.py
@ -1,369 +1,80 @@
|
|||||||
"""
|
from typing import Any
|
||||||
Main connectivity-checking functionality for `snarled`
|
import argparse
|
||||||
"""
|
|
||||||
from typing import Tuple, List, Dict, Set, Optional, Union, Sequence, Mapping, Callable
|
|
||||||
from collections import defaultdict
|
|
||||||
from pprint import pformat
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import numpy
|
from . import utils
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from .trace import trace_layout, TraceAnalysis
|
||||||
from pyclipper import scale_to_clipper, scale_from_clipper, PyPolyNode
|
|
||||||
|
|
||||||
from .types import connectivity_t, layer_t, contour_t
|
|
||||||
from .poly import poly_contains_points, intersects
|
|
||||||
from .clipper import union_nonzero, union_evenodd, intersection_evenodd, difference_evenodd, hier2oriented
|
|
||||||
from .tracker import NetsInfo, NetName
|
|
||||||
from .utils import connectivity2layers
|
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def trace_connectivity_preloaded(
|
def main() -> int:
|
||||||
polys: Mapping[layer_t, Sequence[ArrayLike]],
|
parser = argparse.ArgumentParser(
|
||||||
labels: Mapping[layer_t, Sequence[Tuple[float, float, str]]],
|
prog='snarled',
|
||||||
connectivity: Sequence[Tuple[layer_t, Optional[layer_t], layer_t]],
|
description='layout connectivity checker',
|
||||||
clipper_scale_factor: int = int(2 ** 24),
|
)
|
||||||
) -> NetsInfo:
|
|
||||||
"""
|
|
||||||
Analyze the electrical connectivity of the provided layout.
|
|
||||||
|
|
||||||
The resulting `NetsInfo` will contain only disjoint `nets`, and its `net_aliases` can be used to
|
parser.add_argument('file_path')
|
||||||
understand which nets are shorted (and therefore known by more than one name).
|
parser.add_argument('connectivity_path')
|
||||||
|
parser.add_argument('-m', '--layermap')
|
||||||
|
parser.add_argument('-t', '--top')
|
||||||
|
parser.add_argument('-p', '--labels-remap')
|
||||||
|
|
||||||
Args:
|
parser.add_argument('-l', '--lfile-path')
|
||||||
polys: A full description of all conducting paths in the layout. Consists of lists of polygons
|
parser.add_argument('-r', '--lremap')
|
||||||
(Nx2 arrays of vertices), indexed by layer. The structure looks roughly like
|
parser.add_argument('-n', '--llayermap')
|
||||||
`{layer0: [poly0, poly1, ..., [(x0, y0), (x1, y1), ...]], ...}`
|
parser.add_argument('-s', '--ltop')
|
||||||
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:
|
parser.add_argument('-o', '--output')
|
||||||
`NetsInfo` object describing the various nets and their connectivities.
|
parser.add_argument('-u', '--raw-label-names', action='store_true')
|
||||||
"""
|
|
||||||
def get_layer(layer: layer_t) -> Tuple[Sequence[ArrayLike], Sequence[Tuple[float, float, str]]]:
|
|
||||||
return polys[layer], labels[layer]
|
|
||||||
|
|
||||||
return trace_connectivity(get_layer, connectivity, clipper_scale_factor)
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
filepath = args.file_path
|
||||||
|
connectivity = utils.read_connectivity(args.connectivity_path)
|
||||||
|
|
||||||
def trace_connectivity(
|
kwargs: dict[str, Any] = {}
|
||||||
get_layer: Callable[[layer_t], Tuple[Sequence[ArrayLike], Sequence[Tuple[float, float, str]]]],
|
|
||||||
connectivity: Sequence[Tuple[layer_t, Optional[layer_t], layer_t]],
|
|
||||||
clipper_scale_factor: int = int(2 ** 24),
|
|
||||||
) -> NetsInfo:
|
|
||||||
"""
|
|
||||||
Analyze the electrical connectivity of a layout.
|
|
||||||
|
|
||||||
The resulting `NetsInfo` will contain only disjoint `nets`, and its `net_aliases` can be used to
|
if args.layermap:
|
||||||
understand which nets are shorted (and therefore known by more than one name).
|
kwargs['layer_map'] = utils.read_layermap(args.layermap)
|
||||||
|
|
||||||
This function attempts to reduce memory usage by lazy-loading layout data (layer-by-layer) and
|
if args.top:
|
||||||
pruning away layers for which all interactions have already been computed.
|
kwargs['topcell'] = args.top
|
||||||
TODO: In the future, this will be extended to cover partial loading of spatial extents in
|
|
||||||
addition to layers.
|
|
||||||
|
|
||||||
Args:
|
if args.labels_remap:
|
||||||
get_layer: When called, `polys, labels = get_layer(layer)` should return the geometry and labels
|
kwargs['labels_remap'] = utils.read_remap(args.labels_remap)
|
||||||
on that layer. Returns
|
|
||||||
|
|
||||||
polys, A list of polygons (Nx2 arrays of vertices) on the layer. The structure looks like
|
if args.lfile_path:
|
||||||
`[poly0, poly1, ..., [(x0, y0), (x1, y1), ...]]`
|
assert args.lremap
|
||||||
|
kwargs['lfile_path'] = args.lfile_path
|
||||||
|
kwargs['lfile_map'] = utils.read_remap(args.lremap)
|
||||||
|
|
||||||
labels, A list of "named points" which are used to assign names to the nets they touch.
|
if args.llayermap:
|
||||||
A list of (x, y, name) tuples targetting this layer.
|
kwargs['lfile_layermap'] = utils.read_layermap(args.llayermap)
|
||||||
`[(x0, y0, name0), (x1, y1, name1), ...]`
|
|
||||||
|
|
||||||
connectivity: A sequence of 3-tuples specifying the electrical connectivity between layers.
|
if args.ltop:
|
||||||
|
kwargs['lfile_topcell'] = args.ltop
|
||||||
|
|
||||||
Each 3-tuple looks like `(top_layer, via_layer, bottom_layer)` and indicates that
|
if args.output:
|
||||||
`top_layer` and `bottom_layer` are electrically connected at any location where
|
kwargs['output_path'] = args.output
|
||||||
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`
|
if not args.raw_label_names:
|
||||||
and `bottom_layer` is automatically considered a short (with no third shape necessary).
|
from .utils import strip_underscored_label as parse_label
|
||||||
|
|
||||||
NOTE that the order in which connectivity is specified (i.e. top-level ordering of the
|
|
||||||
tuples) directly sets the order in which the layers are loaded and merged, and thus
|
|
||||||
has a significant impact on memory usage by determining when layers can be pruned away.
|
|
||||||
Try to group entries by the layers they affect!
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
loaded_layers = set()
|
|
||||||
nets_info = NetsInfo()
|
|
||||||
|
|
||||||
for ii, (top_layer, via_layer, bot_layer) in enumerate(connectivity):
|
|
||||||
logger.info(f'{ii}, {top_layer}, {via_layer}, {bot_layer}')
|
|
||||||
for metal_layer in (top_layer, bot_layer):
|
|
||||||
if metal_layer in loaded_layers:
|
|
||||||
continue
|
|
||||||
# Load and run initial union on each layer
|
|
||||||
raw_polys, labels = get_layer(metal_layer)
|
|
||||||
polys = union_input_polys(scale_to_clipper(raw_polys, clipper_scale_factor))
|
|
||||||
|
|
||||||
# Check each polygon for labels, and assign it to a net (possibly anonymous).
|
|
||||||
nets_on_layer, merge_groups = label_polys(polys, labels, clipper_scale_factor)
|
|
||||||
for name, net_polys in nets_on_layer.items():
|
|
||||||
nets_info.nets[name][metal_layer] += hier2oriented(net_polys)
|
|
||||||
|
|
||||||
# Merge any nets that were shorted by having their labels on the same polygon
|
|
||||||
for group in merge_groups:
|
|
||||||
net_names = set(nn.name for nn in group)
|
|
||||||
if len(net_names) > 1:
|
|
||||||
logger.warning(f'Nets {net_names} are shorted on layer {metal_layer}')
|
|
||||||
first_net, *defunct_nets = group
|
|
||||||
for defunct_net in defunct_nets:
|
|
||||||
nets_info.merge(first_net, defunct_net)
|
|
||||||
|
|
||||||
loaded_layers.add(metal_layer)
|
|
||||||
|
|
||||||
# Load and union vias
|
|
||||||
via_raw_polys, _labels = get_layer(via_layer)
|
|
||||||
via_union = union_input_polys(scale_to_clipper(via_raw_polys, clipper_scale_factor))
|
|
||||||
via_polylists = scale_from_clipper(hier2oriented(via_union), clipper_scale_factor)
|
|
||||||
via_polys = [numpy.array(vv) for vv in via_polylists]
|
|
||||||
|
|
||||||
# Figure out which nets are shorted by vias, then merge them
|
|
||||||
merge_pairs = find_merge_pairs(nets_info.nets, top_layer, bot_layer, via_polys, clipper_scale_factor)
|
|
||||||
for net_a, net_b in merge_pairs:
|
|
||||||
nets_info.merge(net_a, net_b)
|
|
||||||
|
|
||||||
|
|
||||||
remaining_layers = set()
|
|
||||||
for layer_a, _, layer_b in connectivity[ii + 1:]:
|
|
||||||
remaining_layers.add(layer_a)
|
|
||||||
remaining_layers.add(layer_b)
|
|
||||||
|
|
||||||
finished_layers = loaded_layers - remaining_layers
|
|
||||||
for layer in finished_layers:
|
|
||||||
nets_info.prune(layer)
|
|
||||||
loaded_layers.remove(layer)
|
|
||||||
|
|
||||||
return nets_info
|
|
||||||
|
|
||||||
|
|
||||||
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 []
|
|
||||||
|
|
||||||
# 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: # type: ignore
|
|
||||||
unvisited_nodes.append(hole)
|
|
||||||
|
|
||||||
return outer_nodes
|
|
||||||
|
|
||||||
def label_polys(
|
|
||||||
polys: Sequence[PyPolyNode],
|
|
||||||
labels: Sequence[Tuple[float, float, str]],
|
|
||||||
clipper_scale_factor: int,
|
|
||||||
) -> Tuple[
|
|
||||||
defaultdict[NetName, List[PyPolyNode]],
|
|
||||||
List[List[NetName]]
|
|
||||||
]:
|
|
||||||
merge_groups = []
|
|
||||||
point_xys = []
|
|
||||||
point_names = []
|
|
||||||
nets = defaultdict(list)
|
|
||||||
|
|
||||||
for x, y, point_name in labels:
|
|
||||||
point_xys.append((x, y))
|
|
||||||
point_names.append(point_name)
|
|
||||||
|
|
||||||
for poly in polys:
|
|
||||||
found_nets = label_poly(poly, point_xys, point_names, clipper_scale_factor)
|
|
||||||
|
|
||||||
if found_nets:
|
|
||||||
name = NetName(found_nets[0])
|
|
||||||
else:
|
else:
|
||||||
name = NetName() # Anonymous net
|
def parse_label(string: str) -> str:
|
||||||
|
return string
|
||||||
|
|
||||||
nets[name].append(poly)
|
nets = trace_layout(
|
||||||
|
filepath=filepath,
|
||||||
|
connectivity=connectivity,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
if len(found_nets) > 1:
|
parsed_nets = [{parse_label(ll) for ll in net} for net in nets]
|
||||||
# Found a short
|
result = TraceAnalysis(parsed_nets)
|
||||||
poly = pformat(scale_from_clipper(poly.Contour, clipper_scale_factor))
|
print('\n')
|
||||||
merge_groups.append([name] + [NetName(nn) for nn in found_nets[1:]])
|
print(result)
|
||||||
return nets, merge_groups
|
|
||||||
|
|
||||||
|
return 0
|
||||||
def label_poly(
|
|
||||||
poly: PyPolyNode,
|
|
||||||
point_xys: ArrayLike,
|
|
||||||
point_names: Sequence[str],
|
|
||||||
clipper_scale_factor: int,
|
|
||||||
) -> 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).
|
|
||||||
"""
|
|
||||||
if not point_names:
|
|
||||||
return []
|
|
||||||
|
|
||||||
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 inside.any():
|
|
||||||
return inside_nets
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def find_merge_pairs(
|
|
||||||
nets: Mapping[NetName, Mapping[layer_t, Sequence[contour_t]]],
|
|
||||||
top_layer: layer_t,
|
|
||||||
bot_layer: layer_t,
|
|
||||||
via_polys: Optional[Sequence[contour_t]],
|
|
||||||
clipper_scale_factor: int,
|
|
||||||
) -> Set[Tuple[NetName, NetName]]:
|
|
||||||
"""
|
|
||||||
Given a collection of (possibly anonymous) nets, figure out which pairs of
|
|
||||||
nets are shorted through a via (and thus should be merged).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nets: A collection of all nets (seqences of polygons in mappings indexed by `NetName`
|
|
||||||
and layer). See `NetsInfo.nets`.
|
|
||||||
top_layer: Layer name of first layer
|
|
||||||
bot_layer: Layer name of second layer
|
|
||||||
via_polys: Sequence of via contours. `None` denotes to vias necessary (overlap is sufficent).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A set containing pairs of `NetName`s for each pair of nets which are shorted.
|
|
||||||
"""
|
|
||||||
merge_pairs = set()
|
|
||||||
if via_polys is not None and not via_polys:
|
|
||||||
logger.warning(f'No vias between layers {top_layer}, {bot_layer}')
|
|
||||||
return merge_pairs
|
|
||||||
|
|
||||||
tested_pairs = set()
|
|
||||||
with ThreadPoolExecutor() as executor:
|
|
||||||
for top_name in nets.keys():
|
|
||||||
top_polys = nets[top_name][top_layer]
|
|
||||||
if not top_polys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for bot_name in nets.keys():
|
|
||||||
if bot_name == top_name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
name_pair: Tuple[NetName, NetName] = tuple(sorted((top_name, bot_name))) #type: ignore
|
|
||||||
if name_pair in tested_pairs:
|
|
||||||
continue
|
|
||||||
tested_pairs.add(name_pair)
|
|
||||||
|
|
||||||
bot_polys = nets[bot_name][bot_layer]
|
|
||||||
if not bot_polys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
executor.submit(check_overlap, top_polys, via_polys, bot_polys, clipper_scale_factor,
|
|
||||||
lambda np=name_pair: merge_pairs.add(np))
|
|
||||||
|
|
||||||
return merge_pairs
|
|
||||||
|
|
||||||
|
|
||||||
def check_overlap(
|
|
||||||
top_polys: Sequence[contour_t],
|
|
||||||
via_polys: Optional[Sequence[NDArray[numpy.float64]]],
|
|
||||||
bot_polys: Sequence[contour_t],
|
|
||||||
clipper_scale_factor: int,
|
|
||||||
action: Callable[[], None],
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Check for interaction between top and bottom polys, mediated by via polys if present.
|
|
||||||
"""
|
|
||||||
if via_polys is not None:
|
|
||||||
top_bot = intersection_evenodd(top_polys, bot_polys)
|
|
||||||
descaled = scale_from_clipper(top_bot, clipper_scale_factor)
|
|
||||||
overlap = check_any_intersection(descaled, via_polys)
|
|
||||||
# overlap = intersection_evenodd(top_bot, via_polys)
|
|
||||||
# via_polys = difference_evenodd(via_polys, overlap) # reduce set of via polys for future nets
|
|
||||||
else:
|
|
||||||
# overlap = intersection_evenodd(top_polys, bot_polys) # TODO verify there aren't any suspicious corner cases for this
|
|
||||||
overlap = check_any_intersection(
|
|
||||||
scale_from_clipper(top_polys, clipper_scale_factor),
|
|
||||||
scale_from_clipper(bot_polys, clipper_scale_factor))
|
|
||||||
|
|
||||||
if overlap:
|
|
||||||
action()
|
|
||||||
|
|
||||||
|
|
||||||
def check_any_intersection(polys_a, polys_b) -> bool:
|
|
||||||
for poly_a in polys_a:
|
|
||||||
for poly_b in polys_b:
|
|
||||||
if intersects(poly_a, poly_b):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
157
snarled/poly.py
157
snarled/poly.py
@ -1,157 +0,0 @@
|
|||||||
"""
|
|
||||||
Utilities for working with polygons
|
|
||||||
"""
|
|
||||||
import numpy
|
|
||||||
from numpy.typing import NDArray, ArrayLike
|
|
||||||
|
|
||||||
|
|
||||||
def poly_contains_points(
|
|
||||||
vertices: ArrayLike,
|
|
||||||
points: ArrayLike,
|
|
||||||
include_boundary: bool = True,
|
|
||||||
) -> NDArray[numpy.int_]:
|
|
||||||
"""
|
|
||||||
Tests whether the provided points are inside the implicitly closed polygon
|
|
||||||
described by the provided list of vertices.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
vertices: Nx2 Arraylike of form [[x0, y0], [x1, y1], ...], describing an implicitly-
|
|
||||||
closed polygon. Note that this should include any offsets.
|
|
||||||
points: Nx2 ArrayLike of form [[x0, y0], [x1, y1], ...] containing the points to test.
|
|
||||||
include_boundary: True if points on the boundary should be count as inside the shape.
|
|
||||||
Default True.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...]
|
|
||||||
"""
|
|
||||||
points = numpy.array(points, copy=False)
|
|
||||||
vertices = numpy.array(vertices, copy=False)
|
|
||||||
|
|
||||||
if points.size == 0:
|
|
||||||
return numpy.zeros(0)
|
|
||||||
|
|
||||||
min_bounds = numpy.min(vertices, axis=0)[None, :]
|
|
||||||
max_bounds = numpy.max(vertices, axis=0)[None, :]
|
|
||||||
|
|
||||||
trivially_outside = ((points < min_bounds).any(axis=1)
|
|
||||||
| (points > max_bounds).any(axis=1))
|
|
||||||
|
|
||||||
nontrivial = ~trivially_outside
|
|
||||||
if trivially_outside.all():
|
|
||||||
inside = numpy.zeros_like(trivially_outside, dtype=bool)
|
|
||||||
return inside
|
|
||||||
|
|
||||||
ntpts = points[None, nontrivial, :] # nontrivial points, along axis 1 of ndarray
|
|
||||||
verts = vertices[:, None, :] # vertices, along axis 0
|
|
||||||
xydiff = ntpts - verts # Expands into (n_vertices, n_ntpts, 2)
|
|
||||||
|
|
||||||
y0_le = xydiff[:, :, 1] >= 0 # y_point >= y_vertex (axes 0, 1 for all points & vertices)
|
|
||||||
y1_le = numpy.roll(y0_le, -1, axis=0) # same thing for next vertex
|
|
||||||
|
|
||||||
upward = y0_le & ~y1_le # edge passes point y coord going upwards
|
|
||||||
downward = ~y0_le & y1_le # edge passes point y coord going downwards
|
|
||||||
|
|
||||||
dv = numpy.roll(verts, -1, axis=0) - verts
|
|
||||||
is_left = (dv[..., 0] * xydiff[..., 1] # >0 if left of dv, <0 if right, 0 if on the line
|
|
||||||
- dv[..., 1] * xydiff[..., 0])
|
|
||||||
|
|
||||||
winding_number = ((upward & (is_left > 0)).sum(axis=0)
|
|
||||||
- (downward & (is_left < 0)).sum(axis=0))
|
|
||||||
|
|
||||||
nontrivial_inside = winding_number != 0 # filter nontrivial points based on winding number
|
|
||||||
if include_boundary:
|
|
||||||
nontrivial_inside[(is_left == 0).any(axis=0)] = True # check if point lies on any edge
|
|
||||||
|
|
||||||
inside = nontrivial.copy()
|
|
||||||
inside[nontrivial] = nontrivial_inside
|
|
||||||
return inside
|
|
||||||
|
|
||||||
|
|
||||||
def intersects(poly_a: ArrayLike, poly_b: ArrayLike) -> bool:
|
|
||||||
"""
|
|
||||||
Check if two polygons overlap and/or touch.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
poly_a: List of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
|
|
||||||
poly_b: List of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
`True` if the polygons overlap and/or touch.
|
|
||||||
"""
|
|
||||||
poly_a = numpy.array(poly_a, copy=False)
|
|
||||||
poly_b = numpy.array(poly_b, copy=False)
|
|
||||||
|
|
||||||
# Check bounding boxes
|
|
||||||
min_a = poly_a.min(axis=0)
|
|
||||||
min_b = poly_b.min(axis=0)
|
|
||||||
max_a = poly_a.max(axis=0)
|
|
||||||
max_b = poly_b.max(axis=0)
|
|
||||||
|
|
||||||
if (min_a > max_b).any() or (min_b > max_a).any():
|
|
||||||
return False
|
|
||||||
|
|
||||||
#TODO: Check against sorted coords?
|
|
||||||
|
|
||||||
#Check if edges intersect
|
|
||||||
if poly_edges_intersect(poly_a, poly_b):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check if either polygon contains the other
|
|
||||||
if poly_contains_points(poly_b, poly_a).any():
|
|
||||||
return True
|
|
||||||
|
|
||||||
if poly_contains_points(poly_a, poly_b).any():
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def poly_edges_intersect(
|
|
||||||
poly_a: NDArray[numpy.float64],
|
|
||||||
poly_b: NDArray[numpy.float64],
|
|
||||||
) -> NDArray[numpy.int_]:
|
|
||||||
"""
|
|
||||||
Check if the edges of two polygons intersect.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
poly_a: NDArray of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
|
|
||||||
poly_b: NDArray of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
`True` if the polygons' edges intersect.
|
|
||||||
"""
|
|
||||||
a_next = numpy.roll(poly_a, -1, axis=0)
|
|
||||||
b_next = numpy.roll(poly_b, -1, axis=0)
|
|
||||||
|
|
||||||
# Lists of initial/final coordinates for polygon segments
|
|
||||||
xi1 = poly_a[:, 0, None]
|
|
||||||
yi1 = poly_a[:, 1, None]
|
|
||||||
xf1 = a_next[:, 0, None]
|
|
||||||
yf1 = a_next[:, 1, None]
|
|
||||||
|
|
||||||
xi2 = poly_b[None, :, 0]
|
|
||||||
yi2 = poly_b[None, :, 1]
|
|
||||||
xf2 = b_next[None, :, 0]
|
|
||||||
yf2 = b_next[None, :, 1]
|
|
||||||
|
|
||||||
# Perform calculation
|
|
||||||
dxi = xi1 - xi2
|
|
||||||
dyi = yi1 - yi2
|
|
||||||
dx1 = xf1 - xi1
|
|
||||||
dx2 = xf2 - xi2
|
|
||||||
dy1 = yf1 - yi1
|
|
||||||
dy2 = yf2 - yi2
|
|
||||||
|
|
||||||
numerator_a = dx2 * dyi - dy2 * dxi
|
|
||||||
numerator_b = dx1 * dyi - dy1 * dxi
|
|
||||||
denominator = dy2 * dx1 - dx2 * dy1
|
|
||||||
|
|
||||||
# Avoid warnings since we may multiply eg. NaN*False
|
|
||||||
with numpy.errstate(invalid='ignore', divide='ignore'):
|
|
||||||
u_a = numerator_a / denominator
|
|
||||||
u_b = numerator_b / denominator
|
|
||||||
|
|
||||||
# Find the adjacency matrix
|
|
||||||
adjacency = numpy.logical_and.reduce((u_a >= 0, u_a <= 1, u_b >= 0, u_b <= 1))
|
|
||||||
|
|
||||||
return adjacency.any()
|
|
315
snarled/trace.py
Normal file
315
snarled/trace.py
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
from collections.abc import Sequence, Iterable
|
||||||
|
import logging
|
||||||
|
from collections import Counter
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from klayout import db
|
||||||
|
from .types import lnum_t, layer_t
|
||||||
|
from .utils import SnarledError
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TraceAnalysis:
|
||||||
|
"""
|
||||||
|
Short/Open analysis for a list of nets
|
||||||
|
"""
|
||||||
|
|
||||||
|
nets: list[set[str]]
|
||||||
|
""" List of nets (connected sets of labels) """
|
||||||
|
|
||||||
|
opens: dict[str, int]
|
||||||
|
""" Labels which appear on 2+ disconnected nets, and the number of nets they touch """
|
||||||
|
|
||||||
|
shorts: list[set[str]]
|
||||||
|
""" Nets containing more than one unique label """
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
nets: Sequence[Iterable[str]],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
nets: Sequence of nets. Each net is a sequence of labels
|
||||||
|
which were found to be electrically connected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
setnets = [set(net) for net in nets]
|
||||||
|
|
||||||
|
# Shorts contain more than one label
|
||||||
|
shorts = [net for net in setnets if len(net) > 1]
|
||||||
|
|
||||||
|
# Check number of times each label appears
|
||||||
|
net_occurences = Counter(chain.from_iterable(setnets))
|
||||||
|
|
||||||
|
# Opens are where the same label appears on more than one net
|
||||||
|
opens = {
|
||||||
|
nn: count
|
||||||
|
for nn, count in net_occurences.items()
|
||||||
|
if count > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nets = setnets
|
||||||
|
self.shorts = shorts
|
||||||
|
self.opens = opens
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
def format_net(net: Iterable[str]) -> str:
|
||||||
|
names = [f"'{name}'" if any(cc in name for cc in ' \t\n') else name for name in sorted(net)]
|
||||||
|
return ','.join(names)
|
||||||
|
|
||||||
|
def sort_nets(nets: Sequence[Iterable[str]]) -> list[Iterable[str]]:
|
||||||
|
return sorted(nets, key=lambda net: ','.join(sorted(net)))
|
||||||
|
|
||||||
|
ss = 'Trace analysis'
|
||||||
|
ss += '\n============='
|
||||||
|
|
||||||
|
ss += '\nNets'
|
||||||
|
ss += '\n(groups of electrically connected labels)\n'
|
||||||
|
for net in sort_nets(self.nets):
|
||||||
|
ss += '\t' + format_net(net) + '\n'
|
||||||
|
if not self.nets:
|
||||||
|
ss += '\t<NO NETS FOUND>'
|
||||||
|
|
||||||
|
ss += '\nOpens'
|
||||||
|
ss += '\n(2+ nets containing the same name)\n'
|
||||||
|
for label, count in sorted(self.opens.items()):
|
||||||
|
ss += f'\t{label} : {count} nets\n'
|
||||||
|
if not self.opens:
|
||||||
|
ss += '\t<No opens found>'
|
||||||
|
|
||||||
|
ss += '\nShorts'
|
||||||
|
ss += '\n(2+ unique names for the same net)\n'
|
||||||
|
for net in sort_nets(self.shorts):
|
||||||
|
ss += '\t' + format_net(net) + '\n'
|
||||||
|
if not self.shorts:
|
||||||
|
ss += '\t<No shorts found>'
|
||||||
|
|
||||||
|
ss += '=============\n'
|
||||||
|
return ss
|
||||||
|
|
||||||
|
|
||||||
|
def trace_layout(
|
||||||
|
filepath: str,
|
||||||
|
connectivity: Sequence[tuple[layer_t, layer_t | None, layer_t]],
|
||||||
|
layer_map: dict[str, lnum_t] | None = None,
|
||||||
|
topcell: str | None = None,
|
||||||
|
*,
|
||||||
|
labels_map: dict[layer_t, layer_t] | None = None,
|
||||||
|
lfile_path: str | None = None,
|
||||||
|
lfile_map: dict[layer_t, layer_t] | None = None,
|
||||||
|
lfile_layer_map: dict[str, lnum_t] | None = None,
|
||||||
|
lfile_topcell: str | None = None,
|
||||||
|
output_path: str | None = None,
|
||||||
|
) -> list[set[str]]:
|
||||||
|
"""
|
||||||
|
Trace a layout to identify labeled nets.
|
||||||
|
|
||||||
|
To label a net, place a text label anywhere touching the net.
|
||||||
|
Labels may be mapped from a different layer, or even a different
|
||||||
|
layout file altogether.
|
||||||
|
Note: Labels must not contain commas (,)!!
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to the primary layout, containing the conductor geometry
|
||||||
|
(and optionally also the labels)
|
||||||
|
connectivity: List of (conductor1, via12, conductor2) tuples,
|
||||||
|
which indicate that the specified layers are electrically connected
|
||||||
|
(conductor1 to via12 and via12 to conductor2). The middle (via) layer
|
||||||
|
may be `None`, in which case the outer layers are directly connected
|
||||||
|
at any overlap (conductor1 to conductor2).
|
||||||
|
layer_map: {layer_name: (layer_num, dtype_num)} translation table.
|
||||||
|
Should contain any strings present in `connectivity` and `labels_map`.
|
||||||
|
Default is an empty dict.
|
||||||
|
topcell: Cell name of the topcell. If `None`, it is automatically chosen.
|
||||||
|
labels_map: {label_layer: metal_layer} mapping, which allows labels to
|
||||||
|
reside on a different layer from their corresponding metals.
|
||||||
|
Only labels on the provided label layers are used, so
|
||||||
|
{metal_layer: metal_layer} entries must be explicitly specified if
|
||||||
|
they are desired.
|
||||||
|
If `None`, labels on each layer in `connectivity` are used alongside
|
||||||
|
that same layer's geometry ({layer: layer} for all participating
|
||||||
|
geometry layers)
|
||||||
|
Default `None`.
|
||||||
|
lfile_path: Path to a separate file from which labels should be merged.
|
||||||
|
lfile_map: {lfile_layer: primary_layer} mapping, used when merging the
|
||||||
|
labels into the primary layout.
|
||||||
|
lfile_layer_map: {layer_name: (layer_num, dtype_num)} mapping for the
|
||||||
|
secondary (label) file. Should contain all string keys in
|
||||||
|
`lfile_map`.
|
||||||
|
`None` reuses `layer_map` (default).
|
||||||
|
lfile_topcell: Cell name for the topcell in the secondary (label) file.
|
||||||
|
`None` automatically chooses the topcell (default).
|
||||||
|
output_path: If provided, outputs the final net geometry to a layout
|
||||||
|
at the given path. Default `None`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of labeled nets, where each entry is a set of label strings which
|
||||||
|
were found on the given net.
|
||||||
|
"""
|
||||||
|
if layer_map is None:
|
||||||
|
layer_map = {}
|
||||||
|
|
||||||
|
if labels_map is None:
|
||||||
|
labels_map = {
|
||||||
|
layer: layer
|
||||||
|
for layer in chain(*connectivity)
|
||||||
|
if layer is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
layout = db.Layout()
|
||||||
|
layout.read(filepath)
|
||||||
|
|
||||||
|
topcell_obj = _get_topcell(layout, topcell)
|
||||||
|
|
||||||
|
# Merge labels from a separate layout if asked
|
||||||
|
if lfile_path:
|
||||||
|
if not lfile_map:
|
||||||
|
raise SnarledError('Asked to load labels from a separate file, but no '
|
||||||
|
+ 'label layers were specified in lfile_map')
|
||||||
|
|
||||||
|
if lfile_layer_map is None:
|
||||||
|
lfile_layer_map = layer_map
|
||||||
|
|
||||||
|
lnum_map = {}
|
||||||
|
for ltext, lshape in lfile_map.items():
|
||||||
|
if isinstance(ltext, str):
|
||||||
|
ltext = lfile_layer_map[ltext]
|
||||||
|
if isinstance(lshape, str):
|
||||||
|
lshape = layer_map[lshape]
|
||||||
|
lnum_map[ltext] = lshape
|
||||||
|
|
||||||
|
_merge_labels_from(lfile_path, layout, topcell_obj, lnum_map, lfile_topcell)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Build a netlist from the layout
|
||||||
|
#
|
||||||
|
l2n = db.LayoutToNetlist(db.RecursiveShapeIterator(layout, topcell_obj, []))
|
||||||
|
#l2n.include_floating_subcircuits = True
|
||||||
|
|
||||||
|
# Create l2n polygon layers
|
||||||
|
layer2polys = {}
|
||||||
|
for layer in set(chain(*connectivity)):
|
||||||
|
if layer is None:
|
||||||
|
continue
|
||||||
|
if isinstance(layer, str):
|
||||||
|
layer = layer_map[layer]
|
||||||
|
klayer = layout.layer(*layer)
|
||||||
|
layer2polys[layer] = l2n.make_polygon_layer(klayer)
|
||||||
|
|
||||||
|
# Create l2n text layers
|
||||||
|
layer2texts = {}
|
||||||
|
for layer in labels_map:
|
||||||
|
if isinstance(layer, str):
|
||||||
|
layer = layer_map[layer]
|
||||||
|
klayer = layout.layer(*layer)
|
||||||
|
texts = l2n.make_text_layer(klayer)
|
||||||
|
texts.flatten()
|
||||||
|
layer2texts[layer] = texts
|
||||||
|
|
||||||
|
# Connect each layer to itself
|
||||||
|
for name, polys in layer2polys.items():
|
||||||
|
logger.info(f'Adding layer {name}')
|
||||||
|
l2n.connect(polys)
|
||||||
|
|
||||||
|
# Connect layers, optionally with vias
|
||||||
|
for top, via, bot in connectivity:
|
||||||
|
if isinstance(top, str):
|
||||||
|
top = layer_map[top]
|
||||||
|
if isinstance(via, str):
|
||||||
|
via = layer_map[via]
|
||||||
|
if isinstance(bot, str):
|
||||||
|
bot = layer_map[bot]
|
||||||
|
|
||||||
|
if via is None:
|
||||||
|
l2n.connect(layer2polys[top], layer2polys[bot])
|
||||||
|
else:
|
||||||
|
l2n.connect(layer2polys[top], layer2polys[via])
|
||||||
|
l2n.connect(layer2polys[bot], layer2polys[via])
|
||||||
|
|
||||||
|
# Label nets
|
||||||
|
for label_layer, metal_layer in labels_map.items():
|
||||||
|
if isinstance(label_layer, str):
|
||||||
|
label_layer = layer_map[label_layer]
|
||||||
|
if isinstance(metal_layer, str):
|
||||||
|
metal_layer = layer_map[metal_layer]
|
||||||
|
|
||||||
|
l2n.connect(layer2polys[metal_layer], layer2texts[label_layer])
|
||||||
|
|
||||||
|
# Get netlist
|
||||||
|
l2n.extract_netlist()
|
||||||
|
nl = l2n.netlist()
|
||||||
|
nl.make_top_level_pins()
|
||||||
|
|
||||||
|
if output_path:
|
||||||
|
_write_net_layout(l2n, output_path, layer2polys)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Return merged nets
|
||||||
|
#
|
||||||
|
top_circuits = [cc for cc, _ in zip(nl.each_circuit_top_down(), range(nl.top_circuit_count()), strict=False)]
|
||||||
|
|
||||||
|
# Nets with more than one label get their labels joined with a comma
|
||||||
|
nets = [
|
||||||
|
set(nn.name.split(','))
|
||||||
|
for cc in top_circuits
|
||||||
|
for nn in cc.each_net()
|
||||||
|
if nn.name
|
||||||
|
]
|
||||||
|
return nets
|
||||||
|
|
||||||
|
|
||||||
|
def _get_topcell(
|
||||||
|
layout: db.Layout,
|
||||||
|
name: str | None = None,
|
||||||
|
) -> db.Cell:
|
||||||
|
"""
|
||||||
|
Get the topcell by name or hierarchy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: Layout to get the cell from
|
||||||
|
name: If given, use the name to find the topcell; otherwise use hierarchy.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cell object
|
||||||
|
"""
|
||||||
|
if name is None:
|
||||||
|
return layout.top_cell()
|
||||||
|
ind = layout.cell_by_name(name)
|
||||||
|
return layout.cell(ind)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_net_layout(
|
||||||
|
l2n: db.LayoutToNetlist,
|
||||||
|
filepath: str,
|
||||||
|
layer2polys: dict[lnum_t, db.Region],
|
||||||
|
) -> None:
|
||||||
|
layout = db.Layout()
|
||||||
|
top = layout.create_cell('top')
|
||||||
|
lmap = {layout.layer(*layer): polys for layer, polys in layer2polys.items()}
|
||||||
|
l2n.build_all_nets(l2n.cell_mapping_into(layout, top), layout, lmap, 'net_', 'prop_', l2n.BNH_Flatten, 'circuit_')
|
||||||
|
layout.write(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_labels_from(
|
||||||
|
filepath: str,
|
||||||
|
into_layout: db.Layout,
|
||||||
|
into_cell: db.Cell,
|
||||||
|
lnum_map: dict[lnum_t, lnum_t],
|
||||||
|
topcell: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
layout = db.Layout()
|
||||||
|
layout.read(filepath)
|
||||||
|
|
||||||
|
topcell_obj = _get_topcell(layout, topcell)
|
||||||
|
|
||||||
|
for labels_layer, conductor_layer in lnum_map.items():
|
||||||
|
layer_ind_src = layout.layer(*labels_layer)
|
||||||
|
layer_ind_dst = into_layout.layer(*conductor_layer)
|
||||||
|
|
||||||
|
shapes_dst = topcell_obj.shapes(layer_ind_dst)
|
||||||
|
shapes_src = into_cell.shapes(layer_ind_src)
|
||||||
|
for shape in shapes_src.each():
|
||||||
|
new_shape = shapes_dst.insert(shape)
|
||||||
|
shapes_dst.replace_prop_id(new_shape, 0) # clear shape properties
|
@ -1,165 +0,0 @@
|
|||||||
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)
|
|
||||||
if net_a is net_b:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 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 prune(self, layer: layer_t) -> None:
|
|
||||||
"""
|
|
||||||
Delete all geometry for the given layer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
layer: The layer to "forget"
|
|
||||||
"""
|
|
||||||
for net in self.nets.values():
|
|
||||||
if layer in net:
|
|
||||||
del 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:
|
|
||||||
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
|
|
@ -1,5 +1,3 @@
|
|||||||
from typing import Union, Tuple, List, Sequence, Optional, Hashable
|
|
||||||
|
|
||||||
layer_t = Hashable
|
lnum_t = tuple[int, int]
|
||||||
contour_t = List[Tuple[int, int]]
|
layer_t = lnum_t | str
|
||||||
connectivity_t = Sequence[Tuple[layer_t, Optional[layer_t], layer_t]]
|
|
||||||
|
210
snarled/utils.py
210
snarled/utils.py
@ -1,28 +1,198 @@
|
|||||||
|
import logging
|
||||||
|
from .types import layer_t
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SnarledError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def strip_underscored_label(string: str) -> str:
|
||||||
"""
|
"""
|
||||||
Some utility code that gets reused
|
If the label ends in an underscore followed by an integer, strip
|
||||||
|
that suffix. Otherwise, just return the label.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
string: The label string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The label string, with the suffix removed (if one was found)
|
||||||
"""
|
"""
|
||||||
from typing import Set, Tuple
|
try:
|
||||||
|
parts = string.split('_')
|
||||||
from .types import connectivity_t, layer_t
|
int(parts[-1]) # must succeed to continue
|
||||||
|
return '_'.join(parts[:-1])
|
||||||
|
except Exception:
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
def connectivity2layers(
|
def read_layermap(path: str) -> dict[str, tuple[int, int]]:
|
||||||
connectivity: connectivity_t,
|
|
||||||
) -> Tuple[Set[layer_t], Set[layer_t]]:
|
|
||||||
"""
|
"""
|
||||||
Extract the set of all metal layers and the set of all via layers
|
Read a klayout-compatible layermap file.
|
||||||
from the connectivity description.
|
|
||||||
|
Only the simplest format is supported:
|
||||||
|
layer/dtype:layer_name
|
||||||
|
|
||||||
|
Empty lines are ignored.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: filepath for the input file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict of {name: (layer, dtype)}
|
||||||
"""
|
"""
|
||||||
metal_layers = set()
|
with open(path, 'rt') as ff:
|
||||||
via_layers = set()
|
lines = ff.readlines()
|
||||||
for top, via, bot in connectivity:
|
|
||||||
metal_layers.add(top)
|
|
||||||
metal_layers.add(bot)
|
|
||||||
if via is not None:
|
|
||||||
via_layers.add(via)
|
|
||||||
|
|
||||||
both = metal_layers.intersection(via_layers)
|
layer_map = {}
|
||||||
if both:
|
for nn, line in enumerate(lines):
|
||||||
raise Exception(f'The following layers are both vias and metals!? {both}')
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
return metal_layers, via_layers
|
for cc in '*-()':
|
||||||
|
if cc in line:
|
||||||
|
raise SnarledError(f'Failed to read layermap on line {nn} due to special character "{cc}"')
|
||||||
|
|
||||||
|
for cc in ':/':
|
||||||
|
if cc not in line:
|
||||||
|
raise SnarledError(f'Failed to read layermap on line {nn}; missing "{cc}"')
|
||||||
|
|
||||||
|
try:
|
||||||
|
layer_part, name = line.split(':')
|
||||||
|
layer_nums = str2lnum(layer_part)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f'Layer map read failed on line {nn}')
|
||||||
|
raise
|
||||||
|
|
||||||
|
layer_map[name.strip()] = layer_nums
|
||||||
|
|
||||||
|
return layer_map
|
||||||
|
|
||||||
|
|
||||||
|
def read_connectivity(path: str) -> list[tuple[layer_t, layer_t | None, layer_t]]:
|
||||||
|
"""
|
||||||
|
Read a connectivity spec file, which takes the form
|
||||||
|
|
||||||
|
conductor0, via01, conductor1
|
||||||
|
conductor1, via12, conductor2
|
||||||
|
conductor0, via02, conductor2
|
||||||
|
...
|
||||||
|
conductorX, conductorY
|
||||||
|
|
||||||
|
where each comma-separated entry is a layer name or numerical layer/dtype
|
||||||
|
deisgnation (e.g. 123/45). Empty lines are ignored. Lines with only 2 entries
|
||||||
|
are directly connected without needing a separate via layer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: filepath for the input file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of layer spec tuples (A, viaAB, B); the middle entry will be None
|
||||||
|
if no via is given.
|
||||||
|
"""
|
||||||
|
with open(path, 'rt') as ff:
|
||||||
|
lines = ff.readlines()
|
||||||
|
|
||||||
|
connections: list[tuple[layer_t, layer_t | None, layer_t]] = []
|
||||||
|
for nn, line in enumerate(lines):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = line.split(',')
|
||||||
|
|
||||||
|
if len(parts) not in (2, 3):
|
||||||
|
raise SnarledError(f'Too many commas in connectivity spec on line {nn}')
|
||||||
|
|
||||||
|
layers = []
|
||||||
|
for part in parts:
|
||||||
|
layer: layer_t
|
||||||
|
if '/' in part:
|
||||||
|
try:
|
||||||
|
layer = str2lnum(part)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f'Connectivity spec read failed on line {nn}')
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
layer = part.strip()
|
||||||
|
if not layer:
|
||||||
|
raise SnarledError(f'Empty layer in connectivity spec on line {nn}')
|
||||||
|
layers.append(layer)
|
||||||
|
|
||||||
|
if len(layers) == 2:
|
||||||
|
connections.append((layers[0], None, layers[1]))
|
||||||
|
else:
|
||||||
|
connections.append((layers[0], layers[1], layers[2]))
|
||||||
|
|
||||||
|
return connections
|
||||||
|
|
||||||
|
|
||||||
|
def read_remap(path: str) -> dict[layer_t, layer_t]:
|
||||||
|
"""
|
||||||
|
Read a layer remap spec file, which takes the form
|
||||||
|
|
||||||
|
old_layer1 : new_layer1
|
||||||
|
old_layer2 : new_layer2
|
||||||
|
...
|
||||||
|
|
||||||
|
where each layer entry is a layer name or numerical layer/dtype
|
||||||
|
designation (e.g. 123/45).
|
||||||
|
Empty lines are ignored.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: filepath for the input file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping from left (old) layers to right (new) layers
|
||||||
|
"""
|
||||||
|
with open(path, 'rt') as ff:
|
||||||
|
lines = ff.readlines()
|
||||||
|
|
||||||
|
remap = {}
|
||||||
|
for nn, line in enumerate(lines):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = line.split(':')
|
||||||
|
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise SnarledError(f'Too many commas in layer remap spec on line {nn}')
|
||||||
|
|
||||||
|
layers = []
|
||||||
|
for part in parts:
|
||||||
|
layer: layer_t
|
||||||
|
if '/' in part:
|
||||||
|
try:
|
||||||
|
layer = str2lnum(part)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f'Layer remap spec read failed on line {nn}')
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
layer = part.strip()
|
||||||
|
if not layer:
|
||||||
|
raise SnarledError(f'Empty layer in layer remap spec on line {nn}')
|
||||||
|
layers.append(layer)
|
||||||
|
|
||||||
|
remap[layers[0]] = layers[1]
|
||||||
|
|
||||||
|
return remap
|
||||||
|
|
||||||
|
|
||||||
|
def str2lnum(string: str) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Parse a '123/45'-style layer/dtype spec string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
string: String specifying the layer/dtype
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(layer, dtype)
|
||||||
|
"""
|
||||||
|
layer_str, dtype_str = string.split('/')
|
||||||
|
layer = int(layer_str)
|
||||||
|
dtype = int(dtype_str)
|
||||||
|
return (layer, dtype)
|
||||||
|
Loading…
Reference in New Issue
Block a user