Compare commits
142 Commits
master
...
libcentric
Author | SHA1 | Date | |
---|---|---|---|
dc4f24a028 | |||
e5de33e1f1 | |||
f22e737e60 | |||
6ec4823244 | |||
fa7b94a4c0 | |||
9b88be0e92 | |||
4aad8ab786 | |||
de04d06b7a | |||
8b3f76c2e3 | |||
66f3ad04b7 | |||
ed77e389af | |||
372deaca09 | |||
8b92d1ee96 | |||
e7a1d1824a | |||
9c9d3c3928 | |||
c7505a12b0 | |||
abef8771db | |||
f1baf8b577 | |||
355af43fe4 | |||
e8b5c7dec8 | |||
438b81e62e | |||
41409cf4f7 | |||
0a14325af8 | |||
5b1abf5f72 | |||
463c41b62a | |||
62b82eb230 | |||
3e48cc7190 | |||
2364288ba7 | |||
ddcd38674f | |||
742058885f | |||
0917b02a31 | |||
c515ada2f8 | |||
68ac593270 | |||
e87b13c4eb | |||
f5d1fd2c29 | |||
28424be3db | |||
a710494dd8 | |||
c9402500e2 | |||
dfd745a76b | |||
23c64b4f63 | |||
7a4a96ff5f | |||
3191866ce0 | |||
8c42145e44 | |||
1d720b6577 | |||
38a7ba6434 | |||
2e8d06ad6e | |||
ea1a882c4e | |||
ed8f2c1864 | |||
492565c1a6 | |||
c6b8027b4d | |||
e8348dfa75 | |||
81b381e031 | |||
cca6b90830 | |||
d079b44883 | |||
1d649389a0 | |||
dc2c12c26f | |||
a1c4cdee1e | |||
44f823c736 | |||
9d466882a0 | |||
369bad9ae4 | |||
ee6d857cad | |||
5e2018a1a1 | |||
5446a8c40b | |||
a9188c5655 | |||
05f327387e | |||
482ca058bb | |||
c69081331c | |||
4a2c4c5220 | |||
83c710a85f | |||
81171e9b02 | |||
02da37a890 | |||
7da6f5126b | |||
b9848d149c | |||
454f167340 | |||
7191e5f62c | |||
3be4da3e7c | |||
6f6143da1a | |||
3105a669b4 | |||
171d61ccab | |||
95485ab4cd | |||
0be7f9d42a | |||
7fc0902fe7 | |||
3758df6938 | |||
3205091286 | |||
9351f5b5f8 | |||
658eca5eea | |||
783953bb73 | |||
afab6fd940 | |||
7adaea32ec | |||
d6b897131b | |||
6172abf77c | |||
6cbdd7930d | |||
a061c5a2d9 | |||
f80c21ed4d | |||
089e5192e3 | |||
6599dad48f | |||
c2ce9ed547 | |||
6eb4af3203 | |||
22735125d5 | |||
34a5369a55 | |||
7cec1e84c9 | |||
592b91044b | |||
060f6978cd | |||
1b04fb7ed0 | |||
88b64bf525 | |||
bd14ef37c7 | |||
95c41d4519 | |||
c0b9b7fe81 | |||
189f517dcf | |||
9f041e51f4 | |||
09d76203e8 | |||
2302d29433 | |||
09b7ecd80e | |||
aff0df33cc | |||
326c9b9727 | |||
8484628f2f | |||
6565b8baa3 | |||
df1acd7c87 | |||
743428d8d7 | |||
e482107366 | |||
a15131f22a | |||
81eadffa56 | |||
cb8897b8fe | |||
83b9af0cc3 | |||
dfdceefdcd | |||
ecb61c9174 | |||
1741cfb755 | |||
58894fa596 | |||
5bb67c817f | |||
fd045e16d4 | |||
073ccacee9 | |||
804b662780 | |||
14e9a7ccbe | |||
9bb7ddbb79 | |||
273d828d87 | |||
b7df6a3f73 | |||
b4eee6b84e | |||
42c3a2b1e1 | |||
fff20b3da9 | |||
4bae737630 | |||
9891ba9e47 | |||
df320e80cc |
234
README.md
234
README.md
@ -8,221 +8,38 @@ to output to multiple formats.
|
||||
|
||||
- [Source repository](https://mpxd.net/code/jan/masque)
|
||||
- [PyPI](https://pypi.org/project/masque)
|
||||
- [Github mirror](https://github.com/anewusername/masque)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Requirements:
|
||||
* python >= 3.11
|
||||
* python >= 3.8
|
||||
* numpy
|
||||
* klamath (used for GDSII i/o)
|
||||
|
||||
Optional requirements:
|
||||
* `ezdxf` (DXF i/o): ezdxf
|
||||
* `oasis` (OASIS i/o): fatamorgana
|
||||
* `svg` (SVG output): svgwrite
|
||||
* `visualization` (shape plotting): matplotlib
|
||||
* `text` (`Text` shape): matplotlib, freetype
|
||||
* klamath (optional, used for `gdsii` i/o)
|
||||
* matplotlib (optional, used for `visualization` functions and `text`)
|
||||
* ezdxf (optional, used for `dxf` i/o)
|
||||
* fatamorgana (optional, used for `oasis` i/o)
|
||||
* svgwrite (optional, used for `svg` output)
|
||||
* freetype (optional, used for `text`)
|
||||
|
||||
|
||||
Install with pip:
|
||||
```bash
|
||||
pip install 'masque[oasis,dxf,svg,visualization,text]'
|
||||
pip3 install 'masque[visualization,oasis,dxf,svg,text]'
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
A layout consists of a hierarchy of `Pattern`s stored in a single `Library`.
|
||||
Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, `Label`s, and `Port`s.
|
||||
|
||||
|
||||
`masque` departs from several "classic" GDSII paradigms:
|
||||
- A `Pattern` object does not store its own name. A name is only assigned when the pattern is placed
|
||||
into a `Library`, which is effectively a name->`Pattern` mapping.
|
||||
- Layer info for `Shape`ss and `Label`s is not stored in the individual shape and label objects.
|
||||
Instead, the layer is determined by the key for the container dict (e.g. `pattern.shapes[layer]`).
|
||||
* This simplifies many common tasks: filtering `Shape`s by layer, remapping layers, and checking if
|
||||
a layer is empty.
|
||||
* Technically, this allows reusing the same shape or label object across multiple layers. This isn't
|
||||
part of the standard workflow since a mixture of single-use and multi-use shapes could be confusing.
|
||||
* This is similar to the approach used in [KLayout](https://www.klayout.de)
|
||||
- `Ref` target names are also determined in the key of the container dict (e.g. `pattern.refs[target_name]`).
|
||||
* This similarly simplifies filtering `Ref`s by target name, updating to a new target, and checking
|
||||
if a given `Pattern` is referenced.
|
||||
- `Pattern` names are set by their containing `Library` and are not stored in the `Pattern` objects.
|
||||
* This guarantees that there are no duplicate pattern names within any given `Library`.
|
||||
* Likewise, enumerating all the names (and all the `Pattern`s) in a `Library` is straightforward.
|
||||
- Each `Ref`, `Shape`, or `Label` can be repeated multiple times by attaching a `repetition` object to it.
|
||||
* This is similar to how OASIS reptitions are handled, and provides extra flexibility over the GDSII
|
||||
approach of only allowing arrays through AREF (`Ref` + `repetition`).
|
||||
- `Label`s do not have an orientation or presentation
|
||||
* This is in line with how they are used in practice, and how they are represented in OASIS.
|
||||
- Non-polygonal `Shape`s are allowed. For example, elliptical arcs are a basic shape type.
|
||||
* This enables compatibility with OASIS (e.g. circles) and other formats.
|
||||
* `Shape`s provide a `.to_polygons()` method for GDSII compatibility.
|
||||
- Most coordinate values are stored as 64-bit floats internally.
|
||||
* 1 earth radii in nanometers (6e15) is still represented without approximation (53 bit mantissa -> 2^53 > 9e15)
|
||||
* Operations that would otherwise clip/round on are still represented approximately.
|
||||
* Memory usage is usually dominated by other Python overhead.
|
||||
- `Pattern` objects also contain `Port` information, which can be used to "snap" together
|
||||
multiple sub-components by matching up the requested port offsets and rotations.
|
||||
* Port rotations are defined as counter-clockwise angles from the +x axis.
|
||||
* Ports point into the interior of their associated device.
|
||||
* Port rotations may be `None` in the case of non-oriented ports.
|
||||
* Ports have a `ptype` string which is compared in order to catch mismatched connections at build time.
|
||||
* Ports can be exported into/imported from `Label`s stored directly in the layout,
|
||||
editable from standard tools (e.g. KLayout). A default format is provided.
|
||||
|
||||
In one important way, `masque` stays very orthodox:
|
||||
References are accomplished by listing the target's name, not its `Pattern` object.
|
||||
|
||||
- The main downside of this is that any operations that traverse the hierarchy require
|
||||
both the `Pattern` and the `Library` which is contains its reference targets.
|
||||
- This guarantees that names within a `Library` remain unique at all times.
|
||||
* Since this can be tedious in cases where you don't actually care about the name of a
|
||||
pattern, patterns whose names start with `SINGLE_USE_PREFIX` (default: an underscore)
|
||||
may be silently renamed in order to maintain uniqueness.
|
||||
See `masque.library.SINGLE_USE_PREFIX`, `masque.library._rename_patterns()`,
|
||||
and `ILibrary.add()` for more details.
|
||||
- Having all patterns accessible through the `Library` avoids having to perform a
|
||||
tree traversal for every operation which needs to touch all `Pattern` objects
|
||||
(e.g. deleting a layer everywhere or scaling all patterns).
|
||||
- Since `Pattern` doesn't know its own name, you can't create a reference by passing in
|
||||
a `Pattern` object -- you need to know its name.
|
||||
- You *can* reference a `Pattern` before it is created, so long as you have already decided
|
||||
on its name.
|
||||
- Functions like `Pattern.place()` and `Pattern.plug()` need to receive a pattern's name
|
||||
in order to create a reference, but they also need to access the pattern's ports.
|
||||
* One way to provide this data is through an `Abstract`, generated via
|
||||
`Library.abstract()` or through a `Library.abstract_view()`.
|
||||
* Another way is use `Builder.place()` or `Builder.plug()`, which automatically creates
|
||||
an `Abstract` from its internally-referenced `Library`.
|
||||
|
||||
|
||||
## Glossary
|
||||
- `Library`: A collection of named cells. OASIS or GDS "library" or file.
|
||||
- `Tree`: Any `{name: pattern}` mapping which has only one topcell.
|
||||
- `Pattern`: A collection of geometry, text labels, and reference to other patterns.
|
||||
OASIS or GDS "Cell", DXF "Block".
|
||||
- `Ref`: A reference to another pattern. GDS "AREF/SREF", OASIS "Placement".
|
||||
- `Shape`: Individual geometric entity. OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline".
|
||||
- `repetition`: Repetition operation. OASIS "repetition".
|
||||
GDS "AREF" is a `Ref` combined with a `Grid` repetition.
|
||||
- `Label`: Text label. Not rendered into geometry. OASIS, GDS, DXF "Text".
|
||||
- `annotation`: Additional metadata. OASIS or GDS "property".
|
||||
|
||||
|
||||
## Syntax, shorthand, and design patterns
|
||||
Most syntax and behavior should follow normal python conventions.
|
||||
There are a few exceptions, either meant to catch common mistakes or to provide a shorthand for common operations:
|
||||
|
||||
### `Library` objects don't allow overwriting already-existing patterns
|
||||
```python3
|
||||
library['mycell'] = pattern0
|
||||
library['mycell'] = pattern1 # Error! 'mycell' already exists and can't be overwritten
|
||||
del library['mycell'] # We can explicitly delete it
|
||||
library['mycell'] = pattern1 # And now it's ok to assign a new value
|
||||
library.delete('mycell') # This also deletes all refs pointing to 'mycell' by default
|
||||
Alternatively, install from git
|
||||
```bash
|
||||
pip3 install git+https://mpxd.net/code/jan/masque.git@release
|
||||
```
|
||||
|
||||
### Insert a newly-made hierarchical pattern (with children) into a layout
|
||||
```python3
|
||||
# Let's say we have a function which returns a new library containing one topcell (and possibly children)
|
||||
tree = make_tree(...)
|
||||
|
||||
# To reference this cell in our layout, we have to add all its children to our `library` first:
|
||||
top_name = tree.top() # get the name of the topcell
|
||||
name_mapping = library.add(tree) # add all patterns from `tree`, renaming elgible conflicting patterns
|
||||
new_name = name_mapping.get(top_name, top_name) # get the new name for the cell (in case it was auto-renamed)
|
||||
my_pattern.ref(new_name, ...) # instantiate the cell
|
||||
|
||||
# This can be accomplished as follows
|
||||
new_name = library << tree # Add `tree` into `library` and return the top cell's new name
|
||||
my_pattern.ref(new_name, ...) # instantiate the cell
|
||||
|
||||
# In practice, you may do lots of
|
||||
my_pattern.ref(lib << make_tree(...), ...)
|
||||
|
||||
# With a `Builder` and `place()`/`plug()` the `lib <<` portion can be implicit:
|
||||
my_builder = Builder(library=lib, ...)
|
||||
...
|
||||
my_builder.place(make_tree(...))
|
||||
```
|
||||
|
||||
We can also use this shorthand to quickly add and reference a single flat (as yet un-named) pattern:
|
||||
```python3
|
||||
anonymous_pattern = Pattern(...)
|
||||
my_pattern.ref(lib << {'_tentative_name': anonymous_pattern}, ...)
|
||||
```
|
||||
|
||||
### Place a hierarchical pattern into a layout, preserving its port info
|
||||
```python3
|
||||
# As above, we have a function that makes a new library containing one topcell (and possibly children)
|
||||
tree = make_tree(...)
|
||||
|
||||
# We need to go get its port info to `place()` it into our existing layout,
|
||||
new_name = library << tree # Add the tree to the library and return its name (see `<<` above)
|
||||
abstract = library.abstract(tree) # An `Abstract` stores a pattern's name and its ports (but no geometry)
|
||||
my_pattern.place(abstract, ...)
|
||||
|
||||
# With shorthand,
|
||||
abstract = library <= tree
|
||||
my_pattern.place(abstract, ...)
|
||||
|
||||
# or
|
||||
my_pattern.place(library << make_tree(...), ...)
|
||||
|
||||
|
||||
### Quickly add geometry, labels, or refs:
|
||||
The long form for adding elements can be overly verbose:
|
||||
```python3
|
||||
my_pattern.shapes[layer].append(Polygon(vertices, ...))
|
||||
my_pattern.labels[layer] += [Label('my text')]
|
||||
my_pattern.refs[target_name].append(Ref(offset=..., ...))
|
||||
```
|
||||
|
||||
There is shorthand for the most common elements:
|
||||
```python3
|
||||
my_pattern.polygon(layer=layer, vertices=vertices, ...)
|
||||
my_pattern.rect(layer=layer, xctr=..., xmin=..., ymax=..., ly=...) # rectangle; pick 4 of 6 constraints
|
||||
my_pattern.rect(layer=layer, ymin=..., ymax=..., xctr=..., lx=...)
|
||||
my_pattern.path(...)
|
||||
my_pattern.label(layer, 'my_text')
|
||||
my_pattern.ref(target_name, offset=..., ...)
|
||||
```
|
||||
|
||||
### Accessing ports
|
||||
```python3
|
||||
# Square brackets pull from the underlying `.ports` dict:
|
||||
assert pattern['input'] is pattern.ports['input']
|
||||
|
||||
# And you can use them to read multiple ports at once:
|
||||
assert pattern[('input', 'output')] == {
|
||||
'input': pattern.ports['input'],
|
||||
'output': pattern.ports['output'],
|
||||
}
|
||||
|
||||
# But you shouldn't use them for anything except reading
|
||||
pattern['input'] = Port(...) # Error!
|
||||
has_input = ('input' in pattern) # Error!
|
||||
```
|
||||
|
||||
### Building patterns
|
||||
```python3
|
||||
library = Library(...)
|
||||
my_pattern_name, my_pattern = library.mkpat(some_name_generator())
|
||||
...
|
||||
def _make_my_subpattern() -> str:
|
||||
# This function can draw from the outer scope (e.g. `library`) but will not pollute the outer scope
|
||||
# (e.g. the variable `subpattern` will not be accessible from outside the function; you must load it
|
||||
# from within `library`).
|
||||
subpattern_name, subpattern = library.mkpat(...)
|
||||
subpattern.rect(...)
|
||||
...
|
||||
return subpattern_name
|
||||
my_pattern.ref(_make_my_subpattern(), offset=..., ...)
|
||||
```
|
||||
## Translation
|
||||
- `Pattern`: OASIS or GDS "Cell", DXF "Block"
|
||||
- `Ref`: GDS "AREF/SREF", OASIS "Placement"
|
||||
- `Shape`: OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline"
|
||||
- `repetition`: OASIS "repetition". GDS "AREF" is a `Ref` combined with a `Grid` repetition.
|
||||
- `Label`: OASIS, GDS, DXF "Text".
|
||||
- `annotation`: OASIS or GDS "property"
|
||||
|
||||
|
||||
## TODO
|
||||
@ -230,8 +47,13 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...)
|
||||
* Better interface for polygon operations (e.g. with `pyclipper`)
|
||||
- de-embedding
|
||||
- boolean ops
|
||||
* Tests tests tests
|
||||
* check renderpather
|
||||
* pather and renderpather examples
|
||||
* context manager for retool
|
||||
* allow a specific mismatch when connecting ports
|
||||
* Deal with shape repetitions for dxf, svg
|
||||
* Maybe lib.create(bname) -> (name, pat)
|
||||
* Schematic:
|
||||
- Simple cell:
|
||||
+ Assumes no internal hierarchy, or only other simple hierarchy
|
||||
+ Return pattern, refer to it by a well-known name
|
||||
- Parametrized cell:
|
||||
+ Take in `lib`
|
||||
+ lib.create(), and return a string
|
||||
+ Can have pcell hierarchy inside
|
||||
|
@ -2,33 +2,29 @@
|
||||
|
||||
import numpy
|
||||
|
||||
from masque.file import gdsii
|
||||
from masque import Arc, Pattern
|
||||
import masque
|
||||
import masque.file.klamath
|
||||
from masque import shapes
|
||||
|
||||
|
||||
def main():
|
||||
pat = Pattern()
|
||||
layer = (0, 0)
|
||||
pat.shapes[layer].extend([
|
||||
Arc(
|
||||
pat = masque.Pattern(name='ellip_grating')
|
||||
for rmin in numpy.arange(10, 15, 0.5):
|
||||
pat.shapes.append(shapes.Arc(
|
||||
radii=(rmin, rmin),
|
||||
width=0.1,
|
||||
angles=(-numpy.pi/4, numpy.pi/4),
|
||||
)
|
||||
for rmin in numpy.arange(10, 15, 0.5)]
|
||||
)
|
||||
layer=(0, 0),
|
||||
))
|
||||
|
||||
pat.label(string='grating centerline', offset=(1, 0), layer=(1, 2))
|
||||
pat.labels.append(masque.Label(string='grating centerline', offset=(1, 0), layer=(1, 2)))
|
||||
|
||||
pat.scale_by(1000)
|
||||
pat.visualize()
|
||||
pat2 = pat.copy()
|
||||
pat2.name = 'grating2'
|
||||
|
||||
lib = {
|
||||
'ellip_grating': pat,
|
||||
'grating2': pat.copy(),
|
||||
}
|
||||
|
||||
gdsii.writefile(lib, 'out.gds.gz', meters_per_unit=1e-9, logical_units_per_unit=1e-3)
|
||||
masque.file.klamath.writefile((pat, pat2), 'out.gds.gz', 1e-9, 1e-3)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -33,9 +33,7 @@ pyplot.show(block=False)
|
||||
# Create the layout from the contours
|
||||
#
|
||||
pat = Pattern()
|
||||
pat.shapes[(0, 0)].extend([
|
||||
Polygon(vertices=vv) for vv in contours if len(vv) < 1_000
|
||||
])
|
||||
pat.shapes = [Polygon(vertices=vv) for vv in contours if len(vv) < 1_000]
|
||||
|
||||
lib = {}
|
||||
lib['my_mask_name'] = pat
|
||||
|
@ -16,10 +16,8 @@ def main():
|
||||
|
||||
cell_name = 'ellip_grating'
|
||||
pat = masque.Pattern()
|
||||
|
||||
layer = (0, 0)
|
||||
for rmin in numpy.arange(10, 15, 0.5):
|
||||
pat.shapes[layer].append(Arc(
|
||||
pat.shapes.append(Arc(
|
||||
radii=(rmin, rmin),
|
||||
width=0.1,
|
||||
angles=(0 * -pi/4, pi/4),
|
||||
@ -37,27 +35,27 @@ def main():
|
||||
print(f'\nAdded a copy of {cell_name} as {new_name}')
|
||||
|
||||
pat3 = Pattern()
|
||||
pat3.refs[cell_name] = [
|
||||
Ref(offset=(1e5, 3e5), annotations={'4': ['Hello I am the base Ref']}),
|
||||
Ref(offset=(2e5, 3e5), rotation=pi/3),
|
||||
Ref(offset=(3e5, 3e5), rotation=pi/2),
|
||||
Ref(offset=(4e5, 3e5), rotation=pi),
|
||||
Ref(offset=(5e5, 3e5), rotation=3*pi/2),
|
||||
Ref(mirrored=True, offset=(1e5, 4e5)),
|
||||
Ref(mirrored=True, offset=(2e5, 4e5), rotation=pi/3),
|
||||
Ref(mirrored=True, offset=(3e5, 4e5), rotation=pi/2),
|
||||
Ref(mirrored=True, offset=(4e5, 4e5), rotation=pi),
|
||||
Ref(mirrored=True, offset=(5e5, 4e5), rotation=3*pi/2),
|
||||
Ref(offset=(1e5, 5e5)).mirror_target(1),
|
||||
Ref(offset=(2e5, 5e5), rotation=pi/3).mirror_target(1),
|
||||
Ref(offset=(3e5, 5e5), rotation=pi/2).mirror_target(1),
|
||||
Ref(offset=(4e5, 5e5), rotation=pi).mirror_target(1),
|
||||
Ref(offset=(5e5, 5e5), rotation=3*pi/2).mirror_target(1),
|
||||
Ref(offset=(1e5, 6e5)).mirror2d_target(True, True),
|
||||
Ref(offset=(2e5, 6e5), rotation=pi/3).mirror2d_target(True, True),
|
||||
Ref(offset=(3e5, 6e5), rotation=pi/2).mirror2d_target(True, True),
|
||||
Ref(offset=(4e5, 6e5), rotation=pi).mirror2d_target(True, True),
|
||||
Ref(offset=(5e5, 6e5), rotation=3*pi/2).mirror2d_target(True, True),
|
||||
pat3.refs = [
|
||||
Ref(cell_name, offset=(1e5, 3e5), annotations={'4': ['Hello I am the base Ref']}),
|
||||
Ref(cell_name, offset=(2e5, 3e5), rotation=pi/3),
|
||||
Ref(cell_name, offset=(3e5, 3e5), rotation=pi/2),
|
||||
Ref(cell_name, offset=(4e5, 3e5), rotation=pi),
|
||||
Ref(cell_name, offset=(5e5, 3e5), rotation=3*pi/2),
|
||||
Ref(cell_name, mirrored=(True, False), offset=(1e5, 4e5)),
|
||||
Ref(cell_name, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3),
|
||||
Ref(cell_name, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2),
|
||||
Ref(cell_name, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi),
|
||||
Ref(cell_name, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2),
|
||||
Ref(cell_name, mirrored=(False, True), offset=(1e5, 5e5)),
|
||||
Ref(cell_name, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3),
|
||||
Ref(cell_name, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2),
|
||||
Ref(cell_name, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi),
|
||||
Ref(cell_name, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2),
|
||||
Ref(cell_name, mirrored=(True, True), offset=(1e5, 6e5)),
|
||||
Ref(cell_name, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3),
|
||||
Ref(cell_name, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2),
|
||||
Ref(cell_name, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi),
|
||||
Ref(cell_name, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2),
|
||||
]
|
||||
|
||||
lib['sref_test'] = pat3
|
||||
@ -72,34 +70,33 @@ def main():
|
||||
b_count=2,
|
||||
)
|
||||
pat4 = Pattern()
|
||||
pat4.refs[cell_name] = [
|
||||
Ref(repetition=rep, offset=(1e5, 3e5)),
|
||||
Ref(repetition=rep, offset=(2e5, 3e5), rotation=pi/3),
|
||||
Ref(repetition=rep, offset=(3e5, 3e5), rotation=pi/2),
|
||||
Ref(repetition=rep, offset=(4e5, 3e5), rotation=pi),
|
||||
Ref(repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2),
|
||||
Ref(repetition=rep, mirrored=True, offset=(1e5, 4e5)),
|
||||
Ref(repetition=rep, mirrored=True, offset=(2e5, 4e5), rotation=pi/3),
|
||||
Ref(repetition=rep, mirrored=True, offset=(3e5, 4e5), rotation=pi/2),
|
||||
Ref(repetition=rep, mirrored=True, offset=(4e5, 4e5), rotation=pi),
|
||||
Ref(repetition=rep, mirrored=True, offset=(5e5, 4e5), rotation=3*pi/2),
|
||||
Ref(repetition=rep, offset=(1e5, 5e5)).mirror_target(1),
|
||||
Ref(repetition=rep, offset=(2e5, 5e5), rotation=pi/3).mirror_target(1),
|
||||
Ref(repetition=rep, offset=(3e5, 5e5), rotation=pi/2).mirror_target(1),
|
||||
Ref(repetition=rep, offset=(4e5, 5e5), rotation=pi).mirror_target(1),
|
||||
Ref(repetition=rep, offset=(5e5, 5e5), rotation=3*pi/2).mirror_target(1),
|
||||
Ref(repetition=rep, offset=(1e5, 6e5)).mirror2d_target(True, True),
|
||||
Ref(repetition=rep, offset=(2e5, 6e5), rotation=pi/3).mirror2d_target(True, True),
|
||||
Ref(repetition=rep, offset=(3e5, 6e5), rotation=pi/2).mirror2d_target(True, True),
|
||||
Ref(repetition=rep, offset=(4e5, 6e5), rotation=pi).mirror2d_target(True, True),
|
||||
Ref(repetition=rep, offset=(5e5, 6e5), rotation=3*pi/2).mirror2d_target(True, True),
|
||||
pat4.refs = [
|
||||
Ref(cell_name, repetition=rep, offset=(1e5, 3e5)),
|
||||
Ref(cell_name, repetition=rep, offset=(2e5, 3e5), rotation=pi/3),
|
||||
Ref(cell_name, repetition=rep, offset=(3e5, 3e5), rotation=pi/2),
|
||||
Ref(cell_name, repetition=rep, offset=(4e5, 3e5), rotation=pi),
|
||||
Ref(cell_name, repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(1e5, 4e5)),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2),
|
||||
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(1e5, 5e5)),
|
||||
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3),
|
||||
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2),
|
||||
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi),
|
||||
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(1e5, 6e5)),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi),
|
||||
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2),
|
||||
]
|
||||
|
||||
lib['aref_test'] = pat4
|
||||
print('\nAdded aref_test')
|
||||
|
||||
folder = Path('./layouts/')
|
||||
folder.mkdir(exist_ok=True)
|
||||
print(f'...writing files to {folder}...')
|
||||
|
||||
gds1 = folder / 'rep.gds.gz'
|
||||
|
@ -1,39 +1 @@
|
||||
masque Tutorial
|
||||
===============
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
- [basic_shapes](basic_shapes.py):
|
||||
* Draw basic geometry
|
||||
* Export to GDS
|
||||
- [devices](devices.py)
|
||||
* Reference other patterns
|
||||
* Add ports to a pattern
|
||||
* Snap ports together to build a circuit
|
||||
* Check for dangling references
|
||||
- [library](library.py)
|
||||
* Create a `LazyLibrary`, which loads / generates patterns only when they are first used
|
||||
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
|
||||
* Design a pattern which is meant to plug into an existing pattern (via `.interface()`)
|
||||
- [pather](pather.py)
|
||||
* Use `Pather` to route individual wires and wire bundles
|
||||
* Use `BasicTool` to generate paths
|
||||
* Use `BasicTool` to automatically transition between path types
|
||||
- [renderpather](rendpather.py)
|
||||
* Use `RenderPather` and `PathTool` to build a layout similar to the one in [pather](pather.py),
|
||||
but using `Path` shapes instead of `Polygon`s.
|
||||
|
||||
|
||||
Additionaly, [pcgen](pcgen.py) is a utility module for generating photonic crystal lattices.
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
Run from inside the examples directory:
|
||||
```bash
|
||||
cd examples/tutorial
|
||||
python3 basic_shapes.py
|
||||
klayout -e basic_shapes.gds
|
||||
```
|
||||
TODO write tutorial readme
|
||||
|
@ -1,4 +1,4 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Sequence
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -32,10 +32,9 @@ def hole(
|
||||
Returns:
|
||||
Pattern containing a circle.
|
||||
"""
|
||||
pat = Pattern()
|
||||
pat.shapes[layer].append(
|
||||
Circle(radius=radius, offset=(0, 0))
|
||||
)
|
||||
pat = Pattern(shapes=[
|
||||
Circle(radius=radius, offset=(0, 0), layer=layer),
|
||||
])
|
||||
return pat
|
||||
|
||||
|
||||
@ -59,9 +58,8 @@ def triangle(
|
||||
(numpy.cos( - pi / 6), numpy.sin( - pi / 6)),
|
||||
]) * radius
|
||||
|
||||
pat = Pattern()
|
||||
pat.shapes[layer].extend([
|
||||
Polygon(offset=(0, 0), vertices=vertices),
|
||||
pat = Pattern(shapes=[
|
||||
Polygon(offset=(0, 0), layer=layer, vertices=vertices),
|
||||
])
|
||||
return pat
|
||||
|
||||
@ -86,18 +84,16 @@ def smile(
|
||||
pat = Pattern()
|
||||
|
||||
# Add all the shapes we want
|
||||
pat.shapes[layer] += [
|
||||
Circle(radius=radius, offset=(0, 0)), # Outer circle
|
||||
]
|
||||
|
||||
pat.shapes[secondary_layer] += [
|
||||
Circle(radius=radius / 10, offset=(radius / 3, radius / 3)),
|
||||
Circle(radius=radius / 10, offset=(-radius / 3, radius / 3)),
|
||||
pat.shapes += [
|
||||
Circle(radius=radius, offset=(0, 0), layer=layer), # Outer circle
|
||||
Circle(radius=radius / 10, offset=(radius / 3, radius / 3), layer=secondary_layer),
|
||||
Circle(radius=radius / 10, offset=(-radius / 3, radius / 3), layer=secondary_layer),
|
||||
Arc(
|
||||
radii=(radius * 2 / 3, radius * 2 / 3), # Underlying ellipse radii
|
||||
angles=(7 / 6 * pi, 11 / 6 * pi), # Angles limiting the arc
|
||||
width=radius / 10,
|
||||
offset=(0, 0),
|
||||
layer=secondary_layer,
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from collections.abc import Sequence, Mapping
|
||||
# TODO update tutorials
|
||||
from typing import Sequence, Mapping
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -30,7 +31,7 @@ def ports_to_data(pat: Pattern) -> Pattern:
|
||||
|
||||
def data_to_ports(lib: Mapping[str, Pattern], name: str, pat: Pattern) -> Pattern:
|
||||
"""
|
||||
Scan the Pattern to determine port locations. Same port format as `ports_to_data`
|
||||
Scans the Pattern to determine port locations. Same port format as `ports_to_data`
|
||||
"""
|
||||
return ports2data.data_to_ports(layers=[(3, 0)], library=lib, pattern=pat, name=name)
|
||||
|
||||
@ -81,18 +82,18 @@ def perturbed_l3(
|
||||
|
||||
# Build L3 cavity, using references to the provided hole pattern
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(scale=r, offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
pat.refs += [
|
||||
Ref(hole, scale=r, offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y, r in xyr]
|
||||
|
||||
# Add rectangular undercut aids
|
||||
min_xy, max_xy = pat.get_bounds_nonempty(hole_lib)
|
||||
trench_dx = max_xy[0] - min_xy[0]
|
||||
|
||||
pat.shapes[trench_layer] += [
|
||||
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width),
|
||||
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width),
|
||||
pat.shapes += [
|
||||
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer),
|
||||
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer),
|
||||
]
|
||||
|
||||
# Ports are at outer extents of the device (with y=0)
|
||||
@ -130,9 +131,9 @@ def waveguide(
|
||||
|
||||
# Build the pattern
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
pat.refs += [
|
||||
Ref(hole, offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Ports are at outer edges, with y=0
|
||||
@ -169,9 +170,9 @@ def bend(
|
||||
|
||||
# Build the pattern
|
||||
pat= Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
pat.refs += [
|
||||
Ref(hole, offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Figure out port locations.
|
||||
@ -208,9 +209,9 @@ def y_splitter(
|
||||
|
||||
# Build pattern
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
pat.refs += [
|
||||
Ref(hole, offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Determine port locations
|
||||
@ -245,41 +246,32 @@ def main(interactive: bool = True) -> None:
|
||||
devices['ysplit'] = y_splitter(lattice_constant=a, hole='hole', mirror_periods=5)
|
||||
devices['l3cav'] = perturbed_l3(lattice_constant=a, hole='smile', hole_lib=shape_lib, xy_size=(4, 10)) # uses smile :)
|
||||
|
||||
# Turn our dict of devices into a Library.
|
||||
# This provides some convenience functions in the future!
|
||||
# Turn our dict of devices into a Library -- useful for getting abstracts
|
||||
lib = Library(devices)
|
||||
abv = lib.abstract_view() # lets us use abv[cell] instead of lib.abstract(cell)
|
||||
|
||||
#
|
||||
# Build a circuit
|
||||
#
|
||||
# Create a `Builder`, and add the circuit to our library as "my_circuit".
|
||||
circ = Builder(library=lib, name='my_circuit')
|
||||
circ = Builder(library=lib)
|
||||
|
||||
# Start by placing a waveguide. Call its ports "in" and "signal".
|
||||
circ.place('wg10', offset=(0, 0), port_map={'left': 'in', 'right': 'signal'})
|
||||
circ.place(abv['wg10'], offset=(0, 0), port_map={'left': 'in', 'right': 'signal'})
|
||||
|
||||
# Extend the signal path by attaching the "left" port of a waveguide.
|
||||
# Since there is only one other port ("right") on the waveguide we
|
||||
# are attaching (wg10), it automatically inherits the name "signal".
|
||||
circ.plug('wg10', {'signal': 'left'})
|
||||
|
||||
# We could have done the following instead:
|
||||
# circ_pat = Pattern()
|
||||
# lib['my_circuit'] = circ_pat
|
||||
# circ_pat.place(lib.abstract('wg10'), ...)
|
||||
# circ_pat.plug(lib.abstract('wg10'), ...)
|
||||
# but `Builder` lets us omit some of the repetition of `lib.abstract(...)`, and uses similar
|
||||
# syntax to `Pather` and `RenderPather`, which add wire/waveguide routing functionality.
|
||||
circ.plug(abv['wg10'], {'signal': 'left'})
|
||||
|
||||
# Attach a y-splitter to the signal path.
|
||||
# Since the y-splitter has 3 ports total, we can't auto-inherit the
|
||||
# port name, so we have to specify what we want to name the unattached
|
||||
# ports. We can call them "signal1" and "signal2".
|
||||
circ.plug('ysplit', {'signal': 'in'}, {'top': 'signal1', 'bot': 'signal2'})
|
||||
circ.plug(abv['ysplit'], {'signal': 'in'}, {'top': 'signal1', 'bot': 'signal2'})
|
||||
|
||||
# Add a waveguide to both signal ports, inheriting their names.
|
||||
circ.plug('wg05', {'signal1': 'left'})
|
||||
circ.plug('wg05', {'signal2': 'left'})
|
||||
circ.plug(abv['wg05'], {'signal1': 'left'})
|
||||
circ.plug(abv['wg05'], {'signal2': 'left'})
|
||||
|
||||
# Add a bend to both ports.
|
||||
# Our bend's ports "left" and "right" refer to the original counterclockwise
|
||||
@ -288,22 +280,22 @@ def main(interactive: bool = True) -> None:
|
||||
# to "signal2" to bend counterclockwise.
|
||||
# We could also use `mirrored=(True, False)` to mirror one of the devices
|
||||
# and then use same device port on both paths.
|
||||
circ.plug('bend0', {'signal1': 'right'})
|
||||
circ.plug('bend0', {'signal2': 'left'})
|
||||
circ.plug(abv['bend0'], {'signal1': 'right'})
|
||||
circ.plug(abv['bend0'], {'signal2': 'left'})
|
||||
|
||||
# We add some waveguides and a cavity to "signal1".
|
||||
circ.plug('wg10', {'signal1': 'left'})
|
||||
circ.plug('l3cav', {'signal1': 'input'})
|
||||
circ.plug('wg10', {'signal1': 'left'})
|
||||
circ.plug(abv['wg10'], {'signal1': 'left'})
|
||||
circ.plug(abv['l3cav'], {'signal1': 'input'})
|
||||
circ.plug(abv['wg10'], {'signal1': 'left'})
|
||||
|
||||
# "signal2" just gets a single of equivalent length
|
||||
circ.plug('wg28', {'signal2': 'left'})
|
||||
circ.plug(abv['wg28'], {'signal2': 'left'})
|
||||
|
||||
# Now we bend both waveguides back towards each other
|
||||
circ.plug('bend0', {'signal1': 'right'})
|
||||
circ.plug('bend0', {'signal2': 'left'})
|
||||
circ.plug('wg05', {'signal1': 'left'})
|
||||
circ.plug('wg05', {'signal2': 'left'})
|
||||
circ.plug(abv['bend0'], {'signal1': 'right'})
|
||||
circ.plug(abv['bend0'], {'signal2': 'left'})
|
||||
circ.plug(abv['wg05'], {'signal1': 'left'})
|
||||
circ.plug(abv['wg05'], {'signal2': 'left'})
|
||||
|
||||
# To join the waveguides, we attach a second y-junction.
|
||||
# We plug "signal1" into the "bot" port, and "signal2" into the "top" port.
|
||||
@ -311,16 +303,19 @@ def main(interactive: bool = True) -> None:
|
||||
# This operation would raise an exception if the ports did not line up
|
||||
# correctly (i.e. they required different rotations or translations of the
|
||||
# y-junction device).
|
||||
circ.plug('ysplit', {'signal1': 'bot', 'signal2': 'top'}, {'in': 'signal_out'})
|
||||
circ.plug(abv['ysplit'], {'signal1': 'bot', 'signal2': 'top'}, {'in': 'signal_out'})
|
||||
|
||||
# Finally, add some more waveguide to "signal_out".
|
||||
circ.plug('wg10', {'signal_out': 'left'})
|
||||
circ.plug(abv['wg10'], {'signal_out': 'left'})
|
||||
|
||||
# We can also add text labels for our circuit's ports.
|
||||
# They will appear at the uppermost hierarchy level, while the individual
|
||||
# device ports will appear further down, in their respective cells.
|
||||
ports_to_data(circ.pattern)
|
||||
|
||||
# Add the pattern into our library
|
||||
lib['my_circuit'] = circ.pattern
|
||||
|
||||
# Check if we forgot to include any patterns... ooops!
|
||||
if dangling := lib.dangling_refs():
|
||||
print('Warning: The following patterns are referenced, but not present in the'
|
||||
|
@ -1,5 +1,4 @@
|
||||
from typing import Any
|
||||
from collections.abc import Sequence, Callable
|
||||
from typing import Sequence, Callable
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
@ -39,7 +38,7 @@ def main() -> None:
|
||||
#
|
||||
|
||||
lib['triangle'] = lambda: basic_shapes.triangle(devices.RADIUS)
|
||||
opts: dict[str, Any] = dict(
|
||||
opts = dict(
|
||||
lattice_constant = devices.LATTICE_CONSTANT,
|
||||
hole = 'triangle',
|
||||
)
|
||||
@ -61,23 +60,22 @@ def main() -> None:
|
||||
circ2 = Builder(library=lib, ports='tri_l3cav')
|
||||
|
||||
# First way to get abstracts is `lib.abstract(name)`
|
||||
# We can use this syntax directly with `Pattern.plug()` and `Pattern.place()` as well as through `Builder`.
|
||||
circ2.plug(lib.abstract('wg10'), {'input': 'right'})
|
||||
|
||||
# Second way to get abstracts is to use an AbstractView
|
||||
# This also works directly with `Pattern.plug()` / `Pattern.place()`.
|
||||
abstracts = lib.abstract_view()
|
||||
circ2.plug(abstracts['wg10'], {'output': 'left'})
|
||||
|
||||
# Third way to specify an abstract works by automatically getting
|
||||
# it from the library already within the Builder object.
|
||||
# This wouldn't work if we only had a `Pattern` (not a `Builder`).
|
||||
# it from the library already within the Builder object:
|
||||
# Just pass the pattern name!
|
||||
circ2.plug('tri_wg10', {'input': 'right'})
|
||||
circ2.plug('tri_wg10', {'output': 'left'})
|
||||
|
||||
# Add the circuit to the device library.
|
||||
lib['mixed_wg_cav'] = circ2.pattern
|
||||
# It has already been generated, so we can use `set_const` as a shorthand for
|
||||
# `lib['mixed_wg_cav'] = lambda: circ2.pattern`
|
||||
lib.set_const('mixed_wg_cav', circ2.pattern)
|
||||
|
||||
|
||||
#
|
||||
@ -89,7 +87,7 @@ def main() -> None:
|
||||
|
||||
# ... that lets us continue from where we left off.
|
||||
circ3.plug('tri_bend0', {'input': 'right'})
|
||||
circ3.plug('tri_bend0', {'input': 'left'}, mirrored=True) # mirror since no tri y-symmetry
|
||||
circ3.plug('tri_bend0', {'input': 'left'}, mirrored=(True, False)) # mirror since no tri y-symmetry
|
||||
circ3.plug('tri_bend0', {'input': 'right'})
|
||||
circ3.plug('bend0', {'output': 'left'})
|
||||
circ3.plug('bend0', {'output': 'left'})
|
||||
@ -98,7 +96,7 @@ def main() -> None:
|
||||
circ3.plug('tri_wg28', {'input': 'right'})
|
||||
circ3.plug('tri_wg10', {'input': 'right', 'output': 'left'})
|
||||
|
||||
lib['loop_segment'] = circ3.pattern
|
||||
lib.set_const('loop_segment', circ3.pattern)
|
||||
|
||||
#
|
||||
# Write all devices into a GDS file
|
||||
@ -130,6 +128,7 @@ if __name__ == '__main__':
|
||||
# name = port_map.get(name, name)
|
||||
# if name is None:
|
||||
# continue
|
||||
# self.pattern.label(string=name, offset=self.ports[name].offset, layer=label_layer)
|
||||
# self.pattern.labels += [
|
||||
# Label(string=name, offset=self.ports[name].offset, layer=layer)]
|
||||
# return self
|
||||
#
|
||||
|
@ -1,277 +0,0 @@
|
||||
"""
|
||||
Manual wire routing tutorial: Pather and BasicTool
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
from numpy import pi
|
||||
from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers
|
||||
from masque.builder.tools import BasicTool, PathTool
|
||||
from masque.file.gdsii import writefile
|
||||
|
||||
from basic_shapes import GDS_OPTS
|
||||
|
||||
#
|
||||
# Define some basic wire widths, in nanometers
|
||||
# M2 is the top metal; M1 is below it and connected with vias on V1
|
||||
#
|
||||
M1_WIDTH = 1000
|
||||
V1_WIDTH = 500
|
||||
M2_WIDTH = 4000
|
||||
|
||||
#
|
||||
# First, we can define some functions for generating our wire geometry
|
||||
#
|
||||
|
||||
def make_pad() -> Pattern:
|
||||
"""
|
||||
Create a pattern with a single rectangle of M2, with a single port on the bottom
|
||||
|
||||
Every pad will be an instance of the same pattern, so we will only call this function once.
|
||||
"""
|
||||
pat = Pattern()
|
||||
pat.rect(layer='M2', xctr=0, yctr=0, lx=3 * M2_WIDTH, ly=4 * M2_WIDTH)
|
||||
pat.ports['wire_port'] = Port((0, -2 * M2_WIDTH), rotation=pi / 2, ptype='m2wire')
|
||||
return pat
|
||||
|
||||
|
||||
def make_via(
|
||||
layer_top: layer_t,
|
||||
layer_via: layer_t,
|
||||
layer_bot: layer_t,
|
||||
width_top: float,
|
||||
width_via: float,
|
||||
width_bot: float,
|
||||
ptype_top: str,
|
||||
ptype_bot: str,
|
||||
) -> Pattern:
|
||||
"""
|
||||
Generate three concentric squares, on the provided layers
|
||||
(`layer_top`, `layer_via`, `layer_bot`) and with the provided widths
|
||||
(`width_top`, `width_via`, `width_bot`).
|
||||
|
||||
Two ports are added, with the provided ptypes (`ptype_top`, `ptype_bot`).
|
||||
They are placed at the left edge of the top layer and right edge of the
|
||||
bottom layer, respectively.
|
||||
|
||||
We only have one via type, so we will only call this function once.
|
||||
"""
|
||||
pat = Pattern()
|
||||
pat.rect(layer=layer_via, xctr=0, yctr=0, lx=width_via, ly=width_via)
|
||||
pat.rect(layer=layer_bot, xctr=0, yctr=0, lx=width_bot, ly=width_bot)
|
||||
pat.rect(layer=layer_top, xctr=0, yctr=0, lx=width_top, ly=width_top)
|
||||
pat.ports = {
|
||||
'top': Port(offset=(-width_top / 2, 0), rotation=0, ptype=ptype_top),
|
||||
'bottom': Port(offset=(width_bot / 2, 0), rotation=pi, ptype=ptype_bot),
|
||||
}
|
||||
return pat
|
||||
|
||||
|
||||
def make_bend(layer: layer_t, width: float, ptype: str) -> Pattern:
|
||||
"""
|
||||
Generate a triangular wire, with ports at the left (input) and bottom (output) edges.
|
||||
This is effectively a clockwise wire bend.
|
||||
|
||||
Every bend will be the same, so we only need to call this twice (once each for M1 and M2).
|
||||
We could call it additional times for different wire widths or bend types (e.g. squares).
|
||||
"""
|
||||
pat = Pattern()
|
||||
pat.polygon(layer=layer, vertices=[(0, -width / 2), (0, width / 2), (width, -width / 2)])
|
||||
pat.ports = {
|
||||
'input': Port(offset=(0, 0), rotation=0, ptype=ptype),
|
||||
'output': Port(offset=(width / 2, -width / 2), rotation=pi / 2, ptype=ptype),
|
||||
}
|
||||
return pat
|
||||
|
||||
|
||||
def make_straight_wire(layer: layer_t, width: float, ptype: str, length: float) -> Pattern:
|
||||
"""
|
||||
Generate a straight wire with ports along either end (x=0 and x=length).
|
||||
|
||||
Every waveguide will be single-use, so we'll need to create lots of (mostly unique)
|
||||
`Pattern`s, and this function will get called very often.
|
||||
"""
|
||||
pat = Pattern()
|
||||
pat.rect(layer=layer, xmin=0, xmax=length, yctr=0, ly=width)
|
||||
pat.ports = {
|
||||
'input': Port(offset=(0, 0), rotation=0, ptype=ptype),
|
||||
'output': Port(offset=(length, 0), rotation=pi, ptype=ptype),
|
||||
}
|
||||
return pat
|
||||
|
||||
|
||||
def map_layer(layer: layer_t) -> layer_t:
|
||||
"""
|
||||
Map from a strings to GDS layer numbers
|
||||
"""
|
||||
layer_mapping = {
|
||||
'M1': (10, 0),
|
||||
'M2': (20, 0),
|
||||
'V1': (30, 0),
|
||||
}
|
||||
return layer_mapping.get(layer, layer)
|
||||
|
||||
|
||||
#
|
||||
# Now we can start building up our library (collection of static cells) and pathing tools.
|
||||
#
|
||||
# If any of the operations below are confusing, you can cross-reference against the `RenderPather`
|
||||
# tutorial, which handles some things more explicitly (e.g. via placement) and simplifies others
|
||||
# (e.g. geometry definition).
|
||||
#
|
||||
def main() -> None:
|
||||
# Build some patterns (static cells) using the above functions and store them in a library
|
||||
library = Library()
|
||||
library['pad'] = make_pad()
|
||||
library['m1_bend'] = make_bend(layer='M1', ptype='m1wire', width=M1_WIDTH)
|
||||
library['m2_bend'] = make_bend(layer='M2', ptype='m2wire', width=M2_WIDTH)
|
||||
library['v1_via'] = make_via(
|
||||
layer_top='M2',
|
||||
layer_via='V1',
|
||||
layer_bot='M1',
|
||||
width_top=M2_WIDTH,
|
||||
width_via=V1_WIDTH,
|
||||
width_bot=M1_WIDTH,
|
||||
ptype_bot='m1wire',
|
||||
ptype_top='m2wire',
|
||||
)
|
||||
|
||||
#
|
||||
# Now, define two tools.
|
||||
# M1_tool will route on M1, using wires with M1_WIDTH
|
||||
# M2_tool will route on M2, using wires with M2_WIDTH
|
||||
# Both tools are able to automatically transition from the other wire type (with a via)
|
||||
#
|
||||
# Note that while we use BasicTool for this tutorial, you can define your own `Tool`
|
||||
# with arbitrary logic inside -- e.g. with single-use bends, complex transition rules,
|
||||
# transmission line geometry, or other features.
|
||||
#
|
||||
M1_tool = BasicTool(
|
||||
straight = (
|
||||
# First, we need a function which takes in a length and spits out an M1 wire
|
||||
lambda length: make_straight_wire(layer='M1', ptype='m1wire', width=M1_WIDTH, length=length),
|
||||
'input', # When we get a pattern from make_straight_wire, use the port named 'input' as the input
|
||||
'output', # and use the port named 'output' as the output
|
||||
),
|
||||
bend = (
|
||||
library.abstract('m1_bend'), # When we need a bend, we'll reference the pattern we generated earlier
|
||||
'input', # To orient it clockwise, use the port named 'input' as the input
|
||||
'output', # and 'output' as the output
|
||||
),
|
||||
transitions = { # We can automate transitions for different (normally incompatible) port types
|
||||
'm2wire': ( # For example, when we're attaching to a port with type 'm2wire'
|
||||
library.abstract('v1_via'), # we can place a V1 via
|
||||
'top', # using the port named 'top' as the input (i.e. the M2 side of the via)
|
||||
'bottom', # and using the port named 'bottom' as the output
|
||||
),
|
||||
},
|
||||
default_out_ptype = 'm1wire', # Unless otherwise requested, we'll default to trying to stay on M1
|
||||
)
|
||||
|
||||
M2_tool = BasicTool(
|
||||
straight = (
|
||||
# Again, we use make_straight_wire, but this time we set parameters for M2
|
||||
lambda length: make_straight_wire(layer='M2', ptype='m2wire', width=M2_WIDTH, length=length),
|
||||
'input',
|
||||
'output',
|
||||
),
|
||||
bend = (
|
||||
library.abstract('m2_bend'), # and we use an M2 bend
|
||||
'input',
|
||||
'output',
|
||||
),
|
||||
transitions = {
|
||||
'm1wire': (
|
||||
library.abstract('v1_via'), # We still use the same via,
|
||||
'bottom', # but the input port is now 'bottom'
|
||||
'top', # and the output port is now 'top'
|
||||
),
|
||||
},
|
||||
default_out_ptype = 'm2wire', # We default to trying to stay on M2
|
||||
)
|
||||
|
||||
#
|
||||
# Create a new pather which writes to `library` and uses `M2_tool` as its default tool.
|
||||
# Then, place some pads and start routing wires!
|
||||
#
|
||||
pather = Pather(library, tools=M2_tool)
|
||||
|
||||
# Place two pads, and define their ports as 'VCC' and 'GND'
|
||||
pather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'})
|
||||
pather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'})
|
||||
# Add some labels to make the pads easier to distinguish
|
||||
pather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3))
|
||||
pather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3))
|
||||
|
||||
# Path VCC forward (in this case south) and turn clockwise 90 degrees (ccw=False)
|
||||
# The total distance forward (including the bend's forward component) must be 6um
|
||||
pather.path('VCC', ccw=False, length=6_000)
|
||||
|
||||
# Now path VCC to x=0. This time, don't include any bend (ccw=None).
|
||||
# Note that if we tried y=0 here, we would get an error since the VCC port is facing in the x-direction.
|
||||
pather.path_to('VCC', ccw=None, x=0)
|
||||
|
||||
# Path GND forward by 5um, turning clockwise 90 degrees.
|
||||
# This time we use shorthand (bool(0) == False) and omit the parameter labels
|
||||
# Note that although ccw=0 is equivalent to ccw=False, ccw=None is not!
|
||||
pather.path('GND', 0, 5_000)
|
||||
|
||||
# This time, path GND until it matches the current x-coordinate of VCC. Don't place a bend.
|
||||
pather.path_to('GND', None, x=pather['VCC'].offset[0])
|
||||
|
||||
# Now, start using M1_tool for GND.
|
||||
# Since we have defined an M2-to-M1 transition for BasicPather, we don't need to place one ourselves.
|
||||
# If we wanted to place our via manually, we could add `pather.plug('m1_via', {'GND': 'top'})` here
|
||||
# and achieve the same result without having to define any transitions in M1_tool.
|
||||
# Note that even though we have changed the tool used for GND, the via doesn't get placed until
|
||||
# the next time we draw a path on GND (the pather.mpath() statement below).
|
||||
pather.retool(M1_tool, keys=['GND'])
|
||||
|
||||
# Bundle together GND and VCC, and path the bundle forward and counterclockwise.
|
||||
# Pick the distance so that the leading/outermost wire (in this case GND) ends up at x=-10_000.
|
||||
# Other wires in the bundle (in this case VCC) should be spaced at 5_000 pitch (so VCC ends up at x=-5_000)
|
||||
#
|
||||
# Since we recently retooled GND, its path starts with a via down to M1 (included in the distance
|
||||
# calculation), and its straight segment and bend will be drawn using M1 while VCC's are drawn with M2.
|
||||
pather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000)
|
||||
|
||||
# Now use M1_tool as the default tool for all ports/signals.
|
||||
# Since VCC does not have an explicitly assigned tool, it will now transition down to M1.
|
||||
pather.retool(M1_tool)
|
||||
|
||||
# Path the GND + VCC bundle forward and counterclockwise by 90 degrees.
|
||||
# The total extension (travel distance along the forward direction) for the longest segment (in
|
||||
# this case the segment being added to GND) should be exactly 50um.
|
||||
# After turning, the wire pitch should be reduced only 1.2um.
|
||||
pather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200)
|
||||
|
||||
# Make a U-turn with the bundle and expand back out to 4.5um wire pitch.
|
||||
# Here, emin specifies the travel distance for the shortest segment. For the first mpath() call
|
||||
# that applies to VCC, and for teh second call, that applies to GND; the relative lengths of the
|
||||
# segments depend on their starting positions and their ordering within the bundle.
|
||||
pather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200)
|
||||
pather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500)
|
||||
|
||||
# Now, set the default tool back to M2_tool. Note that GND remains on M1 since it has been
|
||||
# explicitly assigned a tool. We could `del pather.tools['GND']` to force it to use the default.
|
||||
pather.retool(M2_tool)
|
||||
|
||||
# Now path both ports to x=-28_000.
|
||||
# When ccw is not None, xmin constrains the trailing/innermost port to stop at the target x coordinate,
|
||||
# However, with ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is
|
||||
# equivalent.
|
||||
pather.mpath(['GND', 'VCC'], None, xmin=-28_000)
|
||||
|
||||
# Further extend VCC out to x=-50_000, and specify that we would like to get an output on M1.
|
||||
# This results in a via at the end of the wire (instead of having one at the start like we got
|
||||
# when using pather.retool().
|
||||
pather.path_to('VCC', None, -50_000, out_ptype='m1wire')
|
||||
|
||||
# Save the pather's pattern into our library
|
||||
library['Pather_and_BasicTool'] = pather.pattern
|
||||
|
||||
# Convert from text-based layers to numeric layers for GDS, and output the file
|
||||
library.map_layers(map_layer)
|
||||
writefile(library, 'pather.gds', **GDS_OPTS)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -2,7 +2,7 @@
|
||||
Routines for creating normalized 2D lattices and common photonic crystal
|
||||
cavity designs.
|
||||
"""
|
||||
from collection.abc import Sequence
|
||||
from typing import Sequence
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
@ -233,8 +233,8 @@ def ln_shift_defect(
|
||||
|
||||
# Shift holes
|
||||
# Expand shifts as necessary
|
||||
tmp_a = numpy.asarray(shifts_a)
|
||||
tmp_r = numpy.asarray(shifts_r)
|
||||
tmp_a = numpy.array(shifts_a)
|
||||
tmp_r = numpy.array(shifts_r)
|
||||
n_shifted = max(tmp_a.size, tmp_r.size)
|
||||
|
||||
shifts_a = numpy.ones(n_shifted)
|
||||
|
@ -1,96 +0,0 @@
|
||||
"""
|
||||
Manual wire routing tutorial: RenderPather an PathTool
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
from masque import RenderPather, Library, Pattern, Port, layer_t, map_layers
|
||||
from masque.builder.tools import PathTool
|
||||
from masque.file.gdsii import writefile
|
||||
|
||||
from basic_shapes import GDS_OPTS
|
||||
from pather import M1_WIDTH, V1_WIDTH, M2_WIDTH, map_layer, make_pad, make_via
|
||||
|
||||
|
||||
def main() -> None:
|
||||
#
|
||||
# To illustrate the advantages of using `RenderPather`, we use `PathTool` instead
|
||||
# of `BasicTool`. `PathTool` lacks some sophistication (e.g. no automatic transitions)
|
||||
# but when used with `RenderPather`, it can consolidate multiple routing steps into
|
||||
# a single `Path` shape.
|
||||
#
|
||||
# We'll try to nearly replicate the layout from the `Pather` tutorial; see `pather.py`
|
||||
# for more detailed descriptions of the individual pathing steps.
|
||||
#
|
||||
|
||||
# First, we make a library and generate some of the same patterns as in the pather tutorial
|
||||
library = Library()
|
||||
library['pad'] = make_pad()
|
||||
library['v1_via'] = make_via(
|
||||
layer_top='M2',
|
||||
layer_via='V1',
|
||||
layer_bot='M1',
|
||||
width_top=M2_WIDTH,
|
||||
width_via=V1_WIDTH,
|
||||
width_bot=M1_WIDTH,
|
||||
ptype_bot='m1wire',
|
||||
ptype_top='m2wire',
|
||||
)
|
||||
|
||||
# `PathTool` is more limited than `BasicTool`. It only generates one type of shape
|
||||
# (`Path`), so it only needs to know what layer to draw on, what width to draw with,
|
||||
# and what port type to present.
|
||||
M1_ptool = PathTool(layer='M1', width=M1_WIDTH, ptype='m1wire')
|
||||
M2_ptool = PathTool(layer='M2', width=M2_WIDTH, ptype='m2wire')
|
||||
rpather = RenderPather(tools=M2_ptool, library=library)
|
||||
|
||||
# As in the pather tutorial, we make soem pads and labels...
|
||||
rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'})
|
||||
rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'})
|
||||
rpather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3))
|
||||
rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3))
|
||||
|
||||
# ...and start routing the signals.
|
||||
rpather.path('VCC', ccw=False, length=6_000)
|
||||
rpather.path_to('VCC', ccw=None, x=0)
|
||||
rpather.path('GND', 0, 5_000)
|
||||
rpather.path_to('GND', None, x=rpather['VCC'].offset[0])
|
||||
|
||||
# `PathTool` doesn't know how to transition betwen metal layers, so we have to
|
||||
# `plug` the via into the GND wire ourselves.
|
||||
rpather.plug('v1_via', {'GND': 'top'})
|
||||
rpather.retool(M1_ptool, keys=['GND'])
|
||||
rpather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000)
|
||||
|
||||
# Same thing on the VCC wire when it goes down to M1.
|
||||
rpather.plug('v1_via', {'VCC': 'top'})
|
||||
rpather.retool(M1_ptool)
|
||||
rpather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200)
|
||||
rpather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200)
|
||||
rpather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500)
|
||||
|
||||
# And again when VCC goes back up to M2.
|
||||
rpather.plug('v1_via', {'VCC': 'bottom'})
|
||||
rpather.retool(M2_ptool)
|
||||
rpather.mpath(['GND', 'VCC'], None, xmin=-28_000)
|
||||
|
||||
# Finally, since PathTool has no conception of transitions, we can't
|
||||
# just ask it to transition to an 'm1wire' port at the end of the final VCC segment.
|
||||
# Instead, we have to calculate the via size ourselves, and adjust the final position
|
||||
# to account for it.
|
||||
via_size = abs(
|
||||
library['v1_via'].ports['top'].offset[0]
|
||||
- library['v1_via'].ports['bottom'].offset[0]
|
||||
)
|
||||
rpather.path_to('VCC', None, -50_000 + via_size)
|
||||
rpather.plug('v1_via', {'VCC': 'top'})
|
||||
|
||||
rpather.render()
|
||||
library['RenderPather_and_PathTool'] = rpather.pattern
|
||||
|
||||
|
||||
# Convert from text-based layers to numeric layers for GDS, and output the file
|
||||
library.map_layers(map_layer)
|
||||
writefile(library, 'render_pather.gds', **GDS_OPTS)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
masque 2D CAD library
|
||||
|
||||
masque is an attempt to make a relatively compact library for designing lithography
|
||||
masque is an attempt to make a relatively small library for designing lithography
|
||||
masks. The general idea is to implement something resembling the GDSII and OASIS file-formats,
|
||||
but with some additional vectorized element types (eg. ellipses, not just polygons), and the
|
||||
ability to interface with multiple file formats.
|
||||
@ -20,73 +20,32 @@
|
||||
NOTES ON INTERNALS
|
||||
==========================
|
||||
- Many of `masque`'s classes make use of `__slots__` to make them faster / smaller.
|
||||
Since `__slots__` doesn't play well with multiple inheritance, often they are left
|
||||
empty for superclasses and it is the subclass's responsibility to set them correctly.
|
||||
- File I/O submodules are not imported by `masque.file` to avoid creating hard dependencies
|
||||
on external file-format reader/writers
|
||||
- Try to accept the broadest-possible inputs: e.g., don't demand an `ILibraryView` if you
|
||||
can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally.
|
||||
Since `__slots__` doesn't play well with multiple inheritance, the `masque.utils.AutoSlots`
|
||||
metaclass is used to auto-generate slots based on superclass type annotations.
|
||||
- File I/O submodules are imported by `masque.file` to avoid creating hard dependencies on
|
||||
external file-format reader/writers
|
||||
|
||||
"""
|
||||
|
||||
from .utils import (
|
||||
layer_t as layer_t,
|
||||
annotations_t as annotations_t,
|
||||
SupportsBool as SupportsBool,
|
||||
)
|
||||
from .error import (
|
||||
MasqueError as MasqueError,
|
||||
PatternError as PatternError,
|
||||
LibraryError as LibraryError,
|
||||
BuildError as BuildError,
|
||||
)
|
||||
from .shapes import (
|
||||
Shape as Shape,
|
||||
Polygon as Polygon,
|
||||
Path as Path,
|
||||
Circle as Circle,
|
||||
Arc as Arc,
|
||||
Ellipse as Ellipse,
|
||||
)
|
||||
from .label import Label as Label
|
||||
from .ref import Ref as Ref
|
||||
from .pattern import (
|
||||
Pattern as Pattern,
|
||||
map_layers as map_layers,
|
||||
map_targets as map_targets,
|
||||
chain_elements as chain_elements,
|
||||
)
|
||||
from .utils import layer_t, annotations_t, SupportsBool
|
||||
from .error import MasqueError, PatternError, LibraryError, BuildError
|
||||
from .shapes import Shape, Polygon, Path, Circle, Arc, Ellipse
|
||||
from .label import Label
|
||||
from .ref import Ref
|
||||
from .pattern import Pattern
|
||||
|
||||
from .library import (
|
||||
ILibraryView as ILibraryView,
|
||||
ILibrary as ILibrary,
|
||||
LibraryView as LibraryView,
|
||||
Library as Library,
|
||||
LazyLibrary as LazyLibrary,
|
||||
AbstractView as AbstractView,
|
||||
TreeView as TreeView,
|
||||
Tree as Tree,
|
||||
)
|
||||
from .ports import (
|
||||
Port as Port,
|
||||
PortList as PortList,
|
||||
)
|
||||
from .abstract import Abstract as Abstract
|
||||
from .builder import (
|
||||
Builder as Builder,
|
||||
Tool as Tool,
|
||||
Pather as Pather,
|
||||
RenderPather as RenderPather,
|
||||
RenderStep as RenderStep,
|
||||
BasicTool as BasicTool,
|
||||
PathTool as PathTool,
|
||||
)
|
||||
from .utils import (
|
||||
ports2data as ports2data,
|
||||
oneshot as oneshot,
|
||||
ILibraryView, ILibrary,
|
||||
LibraryView, Library, LazyLibrary,
|
||||
AbstractView,
|
||||
)
|
||||
from .ports import Port, PortList
|
||||
from .abstract import Abstract
|
||||
from .builder import Builder, Tool, Pather, RenderPather, render_step_t
|
||||
from .utils import ports2data, oneshot
|
||||
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
__version__ = '3.2'
|
||||
__version__ = '2.7'
|
||||
version = __version__ # legacy
|
||||
|
@ -7,7 +7,7 @@ from numpy.typing import ArrayLike
|
||||
|
||||
from .ref import Ref
|
||||
from .ports import PortList, Port
|
||||
from .utils import rotation_matrix_2d
|
||||
from .utils import rotation_matrix_2d, normalize_mirror
|
||||
|
||||
#if TYPE_CHECKING:
|
||||
# from .builder import Builder, Tool
|
||||
@ -18,12 +18,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Abstract(PortList):
|
||||
"""
|
||||
An `Abstract` is a container for a name and associated ports.
|
||||
|
||||
When snapping a sub-component to an existing pattern, only the name (not contained
|
||||
in a `Pattern` object) and port info is needed, and not the geometry itself.
|
||||
"""
|
||||
__slots__ = ('name', '_ports')
|
||||
|
||||
name: str
|
||||
@ -48,6 +42,23 @@ class Abstract(PortList):
|
||||
self.name = name
|
||||
self.ports = copy.deepcopy(ports)
|
||||
|
||||
# def build(
|
||||
# self,
|
||||
# library: 'ILibrary',
|
||||
# tools: 'None | Tool | MutableMapping[str | None, Tool]' = None,
|
||||
# ) -> 'Builder':
|
||||
# """
|
||||
# Begin building a new device around an instance of the current device
|
||||
# (rather than modifying the current device).
|
||||
#
|
||||
# Returns:
|
||||
# The new `Builder` object.
|
||||
# """
|
||||
# pat = Pattern(ports=self.ports)
|
||||
# pat.ref(self.name)
|
||||
# new = Builder(library=library, pattern=pat, tools=tools) # TODO should Abstract have tools?
|
||||
# return new
|
||||
|
||||
# TODO do we want to store a Ref instead of just a name? then we can translate/rotate/mirror...
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@ -97,7 +108,7 @@ class Abstract(PortList):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
pivot = numpy.array(pivot)
|
||||
self.translate_ports(-pivot)
|
||||
self.rotate_ports(rotation)
|
||||
self.rotate_port_offsets(rotation)
|
||||
@ -132,7 +143,7 @@ class Abstract(PortList):
|
||||
port.rotate(rotation)
|
||||
return self
|
||||
|
||||
def mirror_port_offsets(self, across_axis: int = 0) -> Self:
|
||||
def mirror_port_offsets(self, across_axis: int) -> Self:
|
||||
"""
|
||||
Mirror the offsets of all shapes, labels, and refs across an axis
|
||||
|
||||
@ -147,7 +158,7 @@ class Abstract(PortList):
|
||||
port.offset[across_axis - 1] *= -1
|
||||
return self
|
||||
|
||||
def mirror_ports(self, across_axis: int = 0) -> Self:
|
||||
def mirror_ports(self, across_axis: int) -> Self:
|
||||
"""
|
||||
Mirror each port's rotation across an axis, relative to its
|
||||
offset
|
||||
@ -163,7 +174,7 @@ class Abstract(PortList):
|
||||
port.mirror(across_axis)
|
||||
return self
|
||||
|
||||
def mirror(self, across_axis: int = 0) -> Self:
|
||||
def mirror(self, across_axis: int) -> Self:
|
||||
"""
|
||||
Mirror the Pattern across an axis
|
||||
|
||||
@ -189,10 +200,11 @@ class Abstract(PortList):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if ref.mirrored:
|
||||
self.mirror()
|
||||
self.rotate_ports(ref.rotation)
|
||||
self.rotate_port_offsets(ref.rotation)
|
||||
mirrored_across_x, angle = normalize_mirror(ref.mirrored)
|
||||
if mirrored_across_x:
|
||||
self.mirror(across_axis=0)
|
||||
self.rotate_ports(angle + ref.rotation)
|
||||
self.rotate_port_offsets(angle + ref.rotation)
|
||||
self.translate_ports(ref.offset)
|
||||
return self
|
||||
|
||||
@ -209,9 +221,10 @@ class Abstract(PortList):
|
||||
|
||||
# TODO test undo_ref_transform
|
||||
"""
|
||||
mirrored_across_x, angle = normalize_mirror(ref.mirrored)
|
||||
self.translate_ports(-ref.offset)
|
||||
self.rotate_port_offsets(-ref.rotation)
|
||||
self.rotate_ports(-ref.rotation)
|
||||
if ref.mirrored:
|
||||
self.mirror(0)
|
||||
self.rotate_port_offsets(-angle - ref.rotation)
|
||||
self.rotate_ports(-angle - ref.rotation)
|
||||
if mirrored_across_x:
|
||||
self.mirror(across_axis=0)
|
||||
return self
|
||||
|
@ -1,10 +1,5 @@
|
||||
from .builder import Builder as Builder
|
||||
from .pather import Pather as Pather
|
||||
from .renderpather import RenderPather as RenderPather
|
||||
from .utils import ell as ell
|
||||
from .tools import (
|
||||
Tool as Tool,
|
||||
RenderStep as RenderStep,
|
||||
BasicTool as BasicTool,
|
||||
PathTool as PathTool,
|
||||
)
|
||||
from .builder import Builder
|
||||
from .pather import Pather
|
||||
from .renderpather import RenderPather
|
||||
from .utils import ell
|
||||
from .tools import Tool, render_step_t
|
||||
|
@ -1,17 +1,14 @@
|
||||
"""
|
||||
Simplified Pattern assembly (`Builder`)
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Mapping
|
||||
from typing import Self, Sequence, Mapping, Literal, overload, Final, cast
|
||||
import copy
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary, TreeView
|
||||
from ..error import BuildError
|
||||
from ..ref import Ref
|
||||
from ..library import ILibrary
|
||||
from ..error import PortError, BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
|
||||
@ -21,44 +18,39 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Builder(PortList):
|
||||
"""
|
||||
A `Builder` is a helper object used for snapping together multiple
|
||||
lower-level patterns at their `Port`s.
|
||||
TODO DOCUMENT Builder
|
||||
A `Device` is a combination of a `Pattern` with a set of named `Port`s
|
||||
which can be used to "snap" devices together to make complex layouts.
|
||||
|
||||
The `Builder` mostly just holds context, in the form of a `Library`,
|
||||
in addition to its underlying pattern. This simplifies some calls
|
||||
to `plug` and `place`, by making the library implicit.
|
||||
`Device`s can be as simple as one or two ports (e.g. an electrical pad
|
||||
or wire), but can also be used to build and represent a large routed
|
||||
layout (e.g. a logical block with multiple I/O connections or even a
|
||||
full chip).
|
||||
|
||||
`Builder` can also be `set_dead()`, at which point further calls to `plug()`
|
||||
and `place()` are ignored (intended for debugging).
|
||||
For convenience, ports can be read out using square brackets:
|
||||
- `device['A'] == Port((0, 0), 0)`
|
||||
- `device[['A', 'B']] == {'A': Port((0, 0), 0), 'B': Port((0, 0), pi)}`
|
||||
|
||||
|
||||
Examples: Creating a Builder
|
||||
Examples: Creating a Device
|
||||
===========================
|
||||
- `Builder(library, ports={'A': port_a, 'C': port_c}, name='mypat')` makes
|
||||
an empty pattern, adds the given ports, and places it into `library`
|
||||
under the name `'mypat'`.
|
||||
- `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing
|
||||
pattern and defines some ports.
|
||||
|
||||
- `Builder(library)` makes an empty pattern with no ports. The pattern
|
||||
is not added into `library` and must later be added with e.g.
|
||||
`library['mypat'] = builder.pattern`
|
||||
- `Device(ports=None)` makes a new empty pattern with
|
||||
default ports ('A' and 'B', in opposite directions, at (0, 0)).
|
||||
|
||||
- `Builder(library, pattern=pattern, name='mypat')` uses an existing
|
||||
pattern (including its ports) and sets `library['mypat'] = pattern`.
|
||||
- `my_device.build('my_layout')` makes a new pattern and instantiates
|
||||
`my_device` in it with offset (0, 0) as a base for further building.
|
||||
|
||||
- `Builder.interface(other_pat, port_map=['A', 'B'], library=library)`
|
||||
makes a new (empty) pattern, copies over ports 'A' and 'B' from
|
||||
`other_pat`, and creates additional ports 'in_A' and 'in_B' facing
|
||||
in the opposite directions. This can be used to build a device which
|
||||
can plug into `other_pat` (using the 'in_*' ports) but which does not
|
||||
itself include `other_pat` as a subcomponent.
|
||||
- `my_device.as_interface('my_component', port_map=['A', 'B'])` makes a new
|
||||
(empty) pattern, copies over ports 'A' and 'B' from `my_device`, and
|
||||
creates additional ports 'in_A' and 'in_B' facing in the opposite
|
||||
directions. This can be used to build a device which can plug into
|
||||
`my_device` (using the 'in_*' ports) but which does not itself include
|
||||
`my_device` as a subcomponent.
|
||||
|
||||
- `Builder.interface(other_builder, ...)` does the same thing as
|
||||
`Builder.interface(other_builder.pattern, ...)` but also uses
|
||||
`other_builder.library` as its library by default.
|
||||
|
||||
|
||||
Examples: Adding to a pattern
|
||||
=============================
|
||||
Examples: Adding to a Device
|
||||
============================
|
||||
- `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||
instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B'
|
||||
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||
@ -83,9 +75,10 @@ class Builder(PortList):
|
||||
pattern: Pattern
|
||||
""" Layout of this device """
|
||||
|
||||
library: ILibrary
|
||||
library: ILibrary | None
|
||||
"""
|
||||
Library from which patterns should be referenced
|
||||
Library from which existing patterns should be referenced, and to which
|
||||
new ones should be added
|
||||
"""
|
||||
|
||||
_dead: bool
|
||||
@ -101,22 +94,14 @@ class Builder(PortList):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: ILibrary,
|
||||
library: ILibrary | None = None,
|
||||
*,
|
||||
pattern: Pattern | None = None,
|
||||
ports: str | Mapping[str, Port] | None = None,
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
library: The library from which referenced patterns will be taken
|
||||
pattern: The pattern which will be modified by subsequent operations.
|
||||
If `None` (default), a new pattern is created.
|
||||
ports: Allows specifying the initial set of ports, if `pattern` does
|
||||
not already have any ports (or is not provided). May be a string,
|
||||
in which case it is interpreted as a name in `library`.
|
||||
Default `None` (no ports).
|
||||
name: If specified, `library[name]` is set to `self.pattern`.
|
||||
# TODO documentation for Builder() constructor
|
||||
"""
|
||||
self._dead = False
|
||||
self.library = library
|
||||
@ -129,16 +114,20 @@ class Builder(PortList):
|
||||
if self.pattern.ports:
|
||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
||||
if isinstance(ports, str):
|
||||
if library is None:
|
||||
raise BuildError('Ports given as a string, but `library` was `None`!')
|
||||
ports = library.abstract(ports).ports
|
||||
|
||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
||||
|
||||
if name is not None:
|
||||
if library is None:
|
||||
raise BuildError('Name was supplied, but no library was given!')
|
||||
library[name] = self.pattern
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls: type['Builder'],
|
||||
cls,
|
||||
source: PortList | Mapping[str, Port] | str,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
@ -148,15 +137,31 @@ class Builder(PortList):
|
||||
name: str | None = None,
|
||||
) -> 'Builder':
|
||||
"""
|
||||
Wrapper for `Pattern.interface()`, which returns a Builder instead.
|
||||
Begin building a new device based on all or some of the ports in the
|
||||
source device. Do not include the source device; instead use it
|
||||
to define ports (the "interface") for the new device.
|
||||
|
||||
The ports specified by `port_map` (default: all ports) are copied to
|
||||
new device, and additional (input) ports are created facing in the
|
||||
opposite directions. The specified `in_prefix` and `out_prefix` are
|
||||
prepended to the port names to differentiate them.
|
||||
|
||||
By default, the flipped ports are given an 'in_' prefix and unflipped
|
||||
ports keep their original names, enabling intuitive construction of
|
||||
a device that will "plug into" the current device; the 'in_*' ports
|
||||
are used for plugging the devices together while the original port
|
||||
names are used for building the new device.
|
||||
|
||||
Another use-case could be to build the new device using the 'in_'
|
||||
ports, creating a new device which could be used in place of the
|
||||
current device.
|
||||
|
||||
Args:
|
||||
source: A collection of ports (e.g. Pattern, Builder, or dict)
|
||||
from which to create the interface. May be a pattern name if
|
||||
`library` is provided.
|
||||
library: Library from which existing patterns should be referenced,
|
||||
and to which the new one should be added (if named). If not provided,
|
||||
`source.library` must exist and will be used.
|
||||
from which to create the interface.
|
||||
library: Library from which existing patterns should be referenced, TODO
|
||||
and to which new ones should be added. If not provided,
|
||||
the source's library will be used (if available).
|
||||
in_prefix: Prepended to port names for newly-created ports with
|
||||
reversed directions compared to the current device.
|
||||
out_prefix: Prepended to port names for ports which are directly
|
||||
@ -180,71 +185,112 @@ class Builder(PortList):
|
||||
if library is None:
|
||||
if hasattr(source, 'library') and isinstance(source.library, ILibrary):
|
||||
library = source.library
|
||||
else:
|
||||
raise BuildError('No library was given, and `source.library` does not have one either.')
|
||||
|
||||
if isinstance(source, str):
|
||||
source = library.abstract(source).ports
|
||||
if library is None:
|
||||
raise BuildError('Source given as a string, but `library` was `None`!')
|
||||
orig_ports = library.abstract(source).ports
|
||||
elif isinstance(source, PortList):
|
||||
orig_ports = source.ports
|
||||
elif isinstance(source, dict):
|
||||
orig_ports = source
|
||||
else:
|
||||
raise BuildError(f'Unable to get ports from {type(source)}: {source}')
|
||||
|
||||
pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
|
||||
new = Builder(library=library, pattern=pat, name=name)
|
||||
if port_map:
|
||||
if isinstance(port_map, dict):
|
||||
missing_inkeys = set(port_map.keys()) - set(orig_ports.keys())
|
||||
mapped_ports = {port_map[k]: v for k, v in orig_ports.items() if k in port_map}
|
||||
else:
|
||||
port_set = set(port_map)
|
||||
missing_inkeys = port_set - set(orig_ports.keys())
|
||||
mapped_ports = {k: v for k, v in orig_ports.items() if k in port_set}
|
||||
|
||||
if missing_inkeys:
|
||||
raise PortError(f'`port_map` keys not present in source: {missing_inkeys}')
|
||||
else:
|
||||
mapped_ports = orig_ports
|
||||
|
||||
ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi)
|
||||
for name, port in mapped_ports.items()}
|
||||
ports_out = {f'{out_prefix}{name}': port.deepcopy()
|
||||
for name, port in mapped_ports.items()}
|
||||
|
||||
duplicates = set(ports_out.keys()) & set(ports_in.keys())
|
||||
if duplicates:
|
||||
raise PortError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}')
|
||||
|
||||
new = Builder(library=library, ports={**ports_in, **ports_out}, name=name)
|
||||
return new
|
||||
|
||||
@wraps(Pattern.label)
|
||||
def label(self, *args, **kwargs) -> Self:
|
||||
self.pattern.label(*args, **kwargs)
|
||||
return self
|
||||
|
||||
@wraps(Pattern.ref)
|
||||
def ref(self, *args, **kwargs) -> Self:
|
||||
self.pattern.ref(*args, **kwargs)
|
||||
return self
|
||||
|
||||
@wraps(Pattern.polygon)
|
||||
def polygon(self, *args, **kwargs) -> Self:
|
||||
self.pattern.polygon(*args, **kwargs)
|
||||
return self
|
||||
|
||||
@wraps(Pattern.rect)
|
||||
def rect(self, *args, **kwargs) -> Self:
|
||||
self.pattern.rect(*args, **kwargs)
|
||||
return self
|
||||
|
||||
# Note: We're a superclass of `Pather`, where path() means something different...
|
||||
#@wraps(Pattern.path)
|
||||
#def path(self, *args, **kwargs) -> Self:
|
||||
# self.pattern.path(*args, **kwargs)
|
||||
# return self
|
||||
# @overload
|
||||
# def plug(
|
||||
# self,
|
||||
# other: Abstract | str,
|
||||
# map_in: dict[str, str],
|
||||
# map_out: dict[str, str | None] | None,
|
||||
# *,
|
||||
# mirrored: tuple[bool, bool],
|
||||
# inherit_name: bool,
|
||||
# set_rotation: bool | None,
|
||||
# append: bool,
|
||||
# ) -> Self:
|
||||
# pass
|
||||
#
|
||||
# @overload
|
||||
# def plug(
|
||||
# self,
|
||||
# other: Pattern,
|
||||
# map_in: dict[str, str],
|
||||
# map_out: dict[str, str | None] | None = None,
|
||||
# *,
|
||||
# mirrored: tuple[bool, bool] = (False, False),
|
||||
# inherit_name: bool = True,
|
||||
# set_rotation: bool | None = None,
|
||||
# append: bool = False,
|
||||
# ) -> Self:
|
||||
# pass
|
||||
|
||||
def plug(
|
||||
self,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
other: Abstract | str | Pattern,
|
||||
map_in: dict[str, str],
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
mirrored: tuple[bool, bool] = (False, False),
|
||||
inherit_name: bool = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
) -> Self:
|
||||
"""
|
||||
Wrapper around `Pattern.plug` which allows a string for `other`.
|
||||
Instantiate or append a pattern into the current device, connecting
|
||||
the ports specified by `map_in` and renaming the unconnected
|
||||
ports specified by `map_out`.
|
||||
|
||||
The `Builder`'s library is used to dereference the string (or `Abstract`, if
|
||||
one is passed with `append=True`). If a `TreeView` is passed, it is first
|
||||
added into `self.library`.
|
||||
Examples:
|
||||
=========
|
||||
- `my_device.plug(lib, 'subdevice', {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||
instantiates `lib['subdevice']` into `my_device`, plugging ports 'A' and 'B'
|
||||
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||
are removed and any unconnected ports from `subdevice` are added to
|
||||
`my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'.
|
||||
|
||||
- `my_device.plug(lib, 'wire', {'myport': 'A'})` places port 'A' of `lib['wire']`
|
||||
at 'myport' of `my_device`.
|
||||
If `'wire'` has only two ports (e.g. 'A' and 'B'), no `map_out` argument is
|
||||
provided, and the `inherit_name` argument is not explicitly set to `False`,
|
||||
the unconnected port of `wire` is automatically renamed to 'myport'. This
|
||||
allows easy extension of existing ports without changing their names or
|
||||
having to provide `map_out` each time `plug` is called.
|
||||
|
||||
Args:
|
||||
other: An `Abstract`, string, `Pattern`, or `TreeView` describing the
|
||||
device to be instatiated. If it is a `TreeView`, it is first
|
||||
added into `self.library`, after which the topcell is plugged;
|
||||
an equivalent statement is `self.plug(self.library << other, ...)`.
|
||||
other: An `Abstract` describing the device to be instatiated.
|
||||
map_in: dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||
port connections between the two devices.
|
||||
map_out: dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||
new names for ports in `other`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to
|
||||
connecting any ports.
|
||||
mirrored: Enables mirroring `other` across the x or y axes prior
|
||||
to connecting any ports.
|
||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
||||
and `map_out` is `None`, and `other` has only two ports total,
|
||||
then automatically renames the output port of `other` to the
|
||||
@ -257,9 +303,6 @@ class Builder(PortList):
|
||||
port with `rotation=None`), `set_rotation` must be provided
|
||||
to indicate how much `other` should be rotated. Otherwise,
|
||||
`set_rotation` must remain `None`.
|
||||
append: If `True`, `other` is appended instead of being referenced.
|
||||
Note that this does not flatten `other`, so its refs will still
|
||||
be refs (now inside `self`).
|
||||
|
||||
Returns:
|
||||
self
|
||||
@ -276,64 +319,114 @@ class Builder(PortList):
|
||||
logger.error('Skipping plug() since device is dead')
|
||||
return self
|
||||
|
||||
if not isinstance(other, str | Abstract | Pattern):
|
||||
# We got a Tree; add it into self.library and grab an Abstract for it
|
||||
other = self.library << other
|
||||
|
||||
if isinstance(other, str):
|
||||
if self.library is None:
|
||||
raise BuildError('No library available, but `other` was a string!')
|
||||
other = self.library.abstract(other)
|
||||
if append and isinstance(other, Abstract):
|
||||
other = self.library[other.name]
|
||||
|
||||
self.pattern.plug(
|
||||
other=other,
|
||||
map_in=map_in,
|
||||
map_out=map_out,
|
||||
# If asked to inherit a name, check that all conditions are met
|
||||
if (inherit_name
|
||||
and not map_out
|
||||
and len(map_in) == 1
|
||||
and len(other.ports) == 2):
|
||||
out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values())))
|
||||
map_out = {out_port_name: next(iter(map_in.keys()))}
|
||||
|
||||
if map_out is None:
|
||||
map_out = {}
|
||||
map_out = copy.deepcopy(map_out)
|
||||
|
||||
self.check_ports(other.ports.keys(), map_in, map_out)
|
||||
translation, rotation, pivot = self.find_transform(
|
||||
other,
|
||||
map_in,
|
||||
mirrored=mirrored,
|
||||
inherit_name=inherit_name,
|
||||
set_rotation=set_rotation,
|
||||
append=append,
|
||||
)
|
||||
|
||||
# get rid of plugged ports
|
||||
for ki, vi in map_in.items():
|
||||
del self.ports[ki]
|
||||
map_out[vi] = None
|
||||
|
||||
if isinstance(other, Pattern):
|
||||
assert append
|
||||
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
|
||||
mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append)
|
||||
else:
|
||||
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
|
||||
mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append)
|
||||
return self
|
||||
|
||||
@overload
|
||||
def place(
|
||||
self,
|
||||
other: Abstract | str,
|
||||
*,
|
||||
offset: ArrayLike,
|
||||
rotation: float,
|
||||
pivot: ArrayLike,
|
||||
mirrored: tuple[bool, bool],
|
||||
port_map: dict[str, str | None] | None,
|
||||
skip_port_check: bool,
|
||||
append: bool,
|
||||
) -> Self:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def place(
|
||||
self,
|
||||
other: Pattern,
|
||||
*,
|
||||
offset: ArrayLike,
|
||||
rotation: float,
|
||||
pivot: ArrayLike,
|
||||
mirrored: tuple[bool, bool],
|
||||
port_map: dict[str, str | None] | None,
|
||||
skip_port_check: bool,
|
||||
append: Literal[True],
|
||||
) -> Self:
|
||||
pass
|
||||
|
||||
def place(
|
||||
self,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
other: Abstract | str | Pattern,
|
||||
*,
|
||||
offset: ArrayLike = (0, 0),
|
||||
rotation: float = 0,
|
||||
pivot: ArrayLike = (0, 0),
|
||||
mirrored: bool = False,
|
||||
mirrored: tuple[bool, bool] = (False, False),
|
||||
port_map: dict[str, str | None] | None = None,
|
||||
skip_port_check: bool = False,
|
||||
append: bool = False,
|
||||
) -> Self:
|
||||
"""
|
||||
Wrapper around `Pattern.place` which allows a string or `TreeView` for `other`.
|
||||
Instantiate or append the device `other` into the current device, adding its
|
||||
ports to those of the current device (but not connecting any ports).
|
||||
|
||||
The `Builder`'s library is used to dereference the string (or `Abstract`, if
|
||||
one is passed with `append=True`). If a `TreeView` is passed, it is first
|
||||
added into `self.library`.
|
||||
Mirroring is applied before rotation; translation (`offset`) is applied last.
|
||||
|
||||
Examples:
|
||||
=========
|
||||
- `my_device.place(pad, offset=(10, 10), rotation=pi / 2, port_map={'A': 'gnd'})`
|
||||
instantiates `pad` at the specified (x, y) offset and with the specified
|
||||
rotation, adding its ports to those of `my_device`. Port 'A' of `pad` is
|
||||
renamed to 'gnd' so that further routing can use this signal or net name
|
||||
rather than the port name on the original `pad` device.
|
||||
|
||||
Args:
|
||||
other: An `Abstract`, string, `Pattern`, or `TreeView` describing the
|
||||
device to be instatiated. If it is a `TreeView`, it is first
|
||||
added into `self.library`, after which the topcell is plugged;
|
||||
an equivalent statement is `self.plug(self.library << other, ...)`.
|
||||
other: An `Abstract` describing the device to be instatiated.
|
||||
offset: Offset at which to place the instance. Default (0, 0).
|
||||
rotation: Rotation applied to the instance before placement. Default 0.
|
||||
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
||||
Rotation is applied prior to translation (`offset`).
|
||||
mirrored: Whether theinstance should be mirrored across the x axis.
|
||||
mirrored: Whether theinstance should be mirrored across the x and y axes.
|
||||
Mirroring is applied before translation and rotation.
|
||||
port_map: dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||
new names for ports in the instantiated device. New names can be
|
||||
`None`, which will delete those ports.
|
||||
skip_port_check: Can be used to skip the internal call to `check_ports`,
|
||||
in case it has already been performed elsewhere.
|
||||
append: If `True`, `other` is appended instead of being referenced.
|
||||
Note that this does not flatten `other`, so its refs will still
|
||||
be refs (now inside `self`).
|
||||
|
||||
Returns:
|
||||
self
|
||||
@ -348,25 +441,51 @@ class Builder(PortList):
|
||||
logger.error('Skipping place() since device is dead')
|
||||
return self
|
||||
|
||||
if not isinstance(other, str | Abstract | Pattern):
|
||||
# We got a Tree; add it into self.library and grab an Abstract for it
|
||||
other = self.library << other
|
||||
|
||||
if isinstance(other, str):
|
||||
if self.library is None:
|
||||
raise BuildError('No library available, but `other` was a string!')
|
||||
other = self.library.abstract(other)
|
||||
if append and isinstance(other, Abstract):
|
||||
other = self.library[other.name]
|
||||
|
||||
self.pattern.place(
|
||||
other=other,
|
||||
offset=offset,
|
||||
rotation=rotation,
|
||||
pivot=pivot,
|
||||
mirrored=mirrored,
|
||||
port_map=port_map,
|
||||
skip_port_check=skip_port_check,
|
||||
append=append,
|
||||
)
|
||||
if port_map is None:
|
||||
port_map = {}
|
||||
|
||||
if not skip_port_check:
|
||||
self.check_ports(other.ports.keys(), map_in=None, map_out=port_map)
|
||||
|
||||
ports = {}
|
||||
for name, port in other.ports.items():
|
||||
new_name = port_map.get(name, name)
|
||||
if new_name is None:
|
||||
continue
|
||||
ports[new_name] = port
|
||||
|
||||
for name, port in ports.items():
|
||||
p = port.deepcopy()
|
||||
p.mirror2d(mirrored)
|
||||
p.rotate_around(pivot, rotation)
|
||||
p.translate(offset)
|
||||
self.ports[name] = p
|
||||
|
||||
if append:
|
||||
if isinstance(other, Pattern):
|
||||
other_pat = other
|
||||
elif isinstance(other, Abstract):
|
||||
assert self.library is not None
|
||||
other_pat = self.library[other.name]
|
||||
else:
|
||||
other_pat = self.library[name]
|
||||
other_copy = other_pat.deepcopy()
|
||||
other_copy.ports.clear()
|
||||
other_copy.mirror2d(mirrored)
|
||||
other_copy.rotate_around(pivot, rotation)
|
||||
other_copy.translate_elements(offset)
|
||||
self.pattern.append(other_copy)
|
||||
else:
|
||||
assert not isinstance(other, Pattern)
|
||||
ref = Ref(other.name, mirrored=mirrored)
|
||||
ref.rotate_around(pivot, rotation)
|
||||
ref.translate(offset)
|
||||
self.pattern.refs.append(ref)
|
||||
return self
|
||||
|
||||
def translate(self, offset: ArrayLike) -> Self:
|
||||
@ -398,7 +517,7 @@ class Builder(PortList):
|
||||
port.rotate_around(pivot, angle)
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
def mirror(self, axis: int) -> Self:
|
||||
"""
|
||||
Mirror the pattern and all ports across the specified axis.
|
||||
|
||||
@ -409,6 +528,8 @@ class Builder(PortList):
|
||||
self
|
||||
"""
|
||||
self.pattern.mirror(axis)
|
||||
for p in self.ports.values():
|
||||
p.mirror(axis)
|
||||
return self
|
||||
|
||||
def set_dead(self) -> Self:
|
||||
@ -430,7 +551,7 @@ class Builder(PortList):
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Builder {self.pattern} L({len(self.library)})>'
|
||||
s = f'<Builder {self.pattern} >' # TODO maybe show lib and tools? in builder repr?
|
||||
return s
|
||||
|
||||
|
||||
|
@ -1,22 +1,17 @@
|
||||
"""
|
||||
Manual wire/waveguide routing (`Pather`)
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, MutableMapping, Mapping
|
||||
from typing import Self, Sequence, MutableMapping, Mapping
|
||||
import copy
|
||||
import logging
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary, SINGLE_USE_PREFIX
|
||||
from ..library import ILibrary
|
||||
from ..error import PortError, BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
from ..utils import SupportsBool, rotation_matrix_2d
|
||||
from ..utils import SupportsBool
|
||||
from .tools import Tool
|
||||
from .utils import ell
|
||||
from .builder import Builder
|
||||
@ -27,87 +22,57 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Pather(Builder):
|
||||
"""
|
||||
An extension of `Builder` which provides functionality for routing and attaching
|
||||
single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns.
|
||||
TODO DOCUMENT Builder
|
||||
A `Device` is a combination of a `Pattern` with a set of named `Port`s
|
||||
which can be used to "snap" devices together to make complex layouts.
|
||||
|
||||
`Pather` is mostly concerned with calculating how long each wire should be. It calls
|
||||
out to `Tool.path` functions provided by subclasses of `Tool` to build the actual patterns.
|
||||
`Tool`s are assigned on a per-port basis and stored in `.tools`; a key of `None` represents
|
||||
a "default" `Tool` used for all ports which do not have a port-specific `Tool` assigned.
|
||||
`Device`s can be as simple as one or two ports (e.g. an electrical pad
|
||||
or wire), but can also be used to build and represent a large routed
|
||||
layout (e.g. a logical block with multiple I/O connections or even a
|
||||
full chip).
|
||||
|
||||
For convenience, ports can be read out using square brackets:
|
||||
- `device['A'] == Port((0, 0), 0)`
|
||||
- `device[['A', 'B']] == {'A': Port((0, 0), 0), 'B': Port((0, 0), pi)}`
|
||||
|
||||
Examples: Creating a Pather
|
||||
Examples: Creating a Device
|
||||
===========================
|
||||
- `Pather(library, tools=my_tool)` makes an empty pattern with no ports. The pattern
|
||||
is not added into `library` and must later be added with e.g.
|
||||
`library['mypat'] = pather.pattern`.
|
||||
The default wire/waveguide generating tool for all ports is set to `my_tool`.
|
||||
- `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing
|
||||
pattern and defines some ports.
|
||||
|
||||
- `Pather(library, ports={'in': Port(...), 'out': ...}, name='mypat', tools=my_tool)`
|
||||
makes an empty pattern, adds the given ports, and places it into `library`
|
||||
under the name `'mypat'`. The default wire/waveguide generating tool
|
||||
for all ports is set to `my_tool`
|
||||
- `Device(ports=None)` makes a new empty pattern with
|
||||
default ports ('A' and 'B', in opposite directions, at (0, 0)).
|
||||
|
||||
- `Pather(..., tools={'in': top_metal_40um, 'out': bottom_metal_1um, None: my_tool})`
|
||||
assigns specific tools to individual ports, and `my_tool` as a default for ports
|
||||
which are not specified.
|
||||
- `my_device.build('my_layout')` makes a new pattern and instantiates
|
||||
`my_device` in it with offset (0, 0) as a base for further building.
|
||||
|
||||
- `Pather.interface(other_pat, port_map=['A', 'B'], library=library, tools=my_tool)`
|
||||
makes a new (empty) pattern, copies over ports 'A' and 'B' from
|
||||
`other_pat`, and creates additional ports 'in_A' and 'in_B' facing
|
||||
in the opposite directions. This can be used to build a device which
|
||||
can plug into `other_pat` (using the 'in_*' ports) but which does not
|
||||
itself include `other_pat` as a subcomponent.
|
||||
- `my_device.as_interface('my_component', port_map=['A', 'B'])` makes a new
|
||||
(empty) pattern, copies over ports 'A' and 'B' from `my_device`, and
|
||||
creates additional ports 'in_A' and 'in_B' facing in the opposite
|
||||
directions. This can be used to build a device which can plug into
|
||||
`my_device` (using the 'in_*' ports) but which does not itself include
|
||||
`my_device` as a subcomponent.
|
||||
|
||||
- `Pather.interface(other_pather, ...)` does the same thing as
|
||||
`Builder.interface(other_builder.pattern, ...)` but also uses
|
||||
`other_builder.library` as its library by default.
|
||||
Examples: Adding to a Device
|
||||
============================
|
||||
- `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||
instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B'
|
||||
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||
are removed and any unconnected ports from `subdevice` are added to
|
||||
`my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'.
|
||||
|
||||
|
||||
Examples: Adding to a pattern
|
||||
=============================
|
||||
- `pather.path('my_port', ccw=True, distance)` creates a "wire" for which the output
|
||||
port is `distance` units away along the axis of `'my_port'` and rotated 90 degrees
|
||||
counterclockwise (since `ccw=True`) relative to `'my_port'`. The wire is `plug`ged
|
||||
into the existing `'my_port'`, causing the port to move to the wire's output.
|
||||
|
||||
There is no formal guarantee about how far off-axis the output will be located;
|
||||
there may be a significant width to the bend that is used to accomplish the 90 degree
|
||||
turn. However, an error is raised if `distance` is too small to fit the bend.
|
||||
|
||||
- `pather.path('my_port', ccw=None, distance)` creates a straight wire with a length
|
||||
of `distance` and `plug`s it into `'my_port'`.
|
||||
|
||||
- `pather.path_to('my_port', ccw=False, position)` creates a wire which starts at
|
||||
`'my_port'` and has its output at the specified `position`, pointing 90 degrees
|
||||
clockwise relative to the input. Again, the off-axis position or distance to the
|
||||
output is not specified, so `position` takes the form of a single coordinate. To
|
||||
ease debugging, position may be specified as `x=position` or `y=position` and an
|
||||
error will be raised if the wrong coordinate is given.
|
||||
|
||||
- `pather.mpath(['A', 'B', 'C'], ..., spacing=spacing)` is a superset of `path`
|
||||
and `path_to` which can act on multiple ports simultaneously. Each port's wire is
|
||||
generated using its own `Tool` (or the default tool if left unspecified).
|
||||
The output ports are spaced out by `spacing` along the input ports' axis, unless
|
||||
`ccw=None` is specified (i.e. no bends) in which case they all end at the same
|
||||
destination coordinate.
|
||||
|
||||
- `pather.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
||||
of `pather.pattern`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
||||
- `my_device.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
||||
of `my_device`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
||||
argument is provided, and the `inherit_name` argument is not explicitly
|
||||
set to `False`, the unconnected port of `wire` is automatically renamed to
|
||||
'myport'. This allows easy extension of existing ports without changing
|
||||
their names or having to provide `map_out` each time `plug` is called.
|
||||
|
||||
- `pather.place(pad, offset=(10, 10), rotation=pi / 2, port_map={'A': 'gnd'})`
|
||||
- `my_device.place(pad, offset=(10, 10), rotation=pi / 2, port_map={'A': 'gnd'})`
|
||||
instantiates `pad` at the specified (x, y) offset and with the specified
|
||||
rotation, adding its ports to those of `pather.pattern`. Port 'A' of `pad` is
|
||||
rotation, adding its ports to those of `my_device`. Port 'A' of `pad` is
|
||||
renamed to 'gnd' so that further routing can use this signal or net name
|
||||
rather than the port name on the original `pad` device.
|
||||
|
||||
- `pather.retool(tool)` or `pather.retool(tool, ['in', 'out', None])` can change
|
||||
which tool is used for the given ports (or as the default tool). Useful
|
||||
when placing vias or using multiple waveguide types along a route.
|
||||
"""
|
||||
__slots__ = ('tools',)
|
||||
|
||||
@ -119,9 +84,8 @@ class Pather(Builder):
|
||||
|
||||
tools: dict[str | None, Tool]
|
||||
"""
|
||||
Tool objects are used to dynamically generate new single-use `Pattern`s
|
||||
(e.g wires or waveguides) to be plugged into this device. A key of `None`
|
||||
indicates the default `Tool`.
|
||||
Tool objects are used to dynamically generate new single-use Devices
|
||||
(e.g wires or waveguides) to be plugged into this device.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@ -134,19 +98,13 @@ class Pather(Builder):
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
library: The library from which referenced patterns will be taken,
|
||||
and where new patterns (e.g. generated by the `tools`) will be placed.
|
||||
pattern: The pattern which will be modified by subsequent operations.
|
||||
If `None` (default), a new pattern is created.
|
||||
ports: Allows specifying the initial set of ports, if `pattern` does
|
||||
not already have any ports (or is not provided). May be a string,
|
||||
in which case it is interpreted as a name in `library`.
|
||||
Default `None` (no ports).
|
||||
tools: A mapping of {port: tool} which specifies what `Tool` should be used
|
||||
to generate waveguide or wire segments when `path`/`path_to`/`mpath`
|
||||
are called. Relies on `Tool.path` implementations.
|
||||
name: If specified, `library[name]` is set to `self.pattern`.
|
||||
# TODO documentation for Builder() constructor
|
||||
|
||||
# TODO MOVE THE BELOW DOCS to PortList
|
||||
# If `ports` is `None`, two default ports ('A' and 'B') are created.
|
||||
# Both are placed at (0, 0) and have default `ptype`, but 'A' has rotation 0
|
||||
# (attached devices will be placed to the left) and 'B' has rotation
|
||||
# pi (attached devices will be placed to the right).
|
||||
"""
|
||||
self._dead = False
|
||||
self.library = library
|
||||
@ -163,9 +121,6 @@ class Pather(Builder):
|
||||
|
||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
||||
|
||||
if name is not None:
|
||||
library[name] = self.pattern
|
||||
|
||||
if tools is None:
|
||||
self.tools = {}
|
||||
elif isinstance(tools, Tool):
|
||||
@ -173,29 +128,40 @@ class Pather(Builder):
|
||||
else:
|
||||
self.tools = dict(tools)
|
||||
|
||||
if name is not None:
|
||||
library[name] = self.pattern
|
||||
|
||||
@classmethod
|
||||
def mk(
|
||||
cls,
|
||||
library: ILibrary,
|
||||
name: str,
|
||||
*,
|
||||
ports: str | Mapping[str, Port] | None = None,
|
||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
||||
) -> tuple[str, 'Pather']:
|
||||
""" Name-and-make combination """ # TODO document
|
||||
pather = Pather(library, name=name, ports=ports, tools=tools)
|
||||
return name, pather
|
||||
|
||||
@classmethod
|
||||
def from_builder(
|
||||
cls: type['Pather'],
|
||||
cls,
|
||||
builder: Builder,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
||||
) -> 'Pather':
|
||||
"""
|
||||
Construct a `Pather` by adding tools to a `Builder`.
|
||||
|
||||
Args:
|
||||
builder: Builder to turn into a Pather
|
||||
tools: Tools for the `Pather`
|
||||
|
||||
Returns:
|
||||
A new Pather object, using `builder.library` and `builder.pattern`.
|
||||
"""
|
||||
new = Pather(library=builder.library, tools=tools, pattern=builder.pattern)
|
||||
"""TODO from_builder docs"""
|
||||
library = library if library is not None else builder.library
|
||||
if library is None:
|
||||
raise BuildError('No library available for Pather!')
|
||||
new = Pather(library=library, tools=tools, pattern=builder.pattern)
|
||||
return new
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls: type['Pather'],
|
||||
cls,
|
||||
source: PortList | Mapping[str, Port] | str,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
@ -206,36 +172,7 @@ class Pather(Builder):
|
||||
name: str | None = None,
|
||||
) -> 'Pather':
|
||||
"""
|
||||
Wrapper for `Pattern.interface()`, which returns a Pather instead.
|
||||
|
||||
Args:
|
||||
source: A collection of ports (e.g. Pattern, Builder, or dict)
|
||||
from which to create the interface. May be a pattern name if
|
||||
`library` is provided.
|
||||
library: Library from which existing patterns should be referenced,
|
||||
and to which the new one should be added (if named). If not provided,
|
||||
`source.library` must exist and will be used.
|
||||
tools: `Tool`s which will be used by the pather for generating new wires
|
||||
or waveguides (via `path`/`path_to`/`mpath`).
|
||||
in_prefix: Prepended to port names for newly-created ports with
|
||||
reversed directions compared to the current device.
|
||||
out_prefix: Prepended to port names for ports which are directly
|
||||
copied from the current device.
|
||||
port_map: Specification for ports to copy into the new device:
|
||||
- If `None`, all ports are copied.
|
||||
- If a sequence, only the listed ports are copied
|
||||
- If a mapping, the listed ports (keys) are copied and
|
||||
renamed (to the values).
|
||||
|
||||
Returns:
|
||||
The new pather, with an empty pattern and 2x as many ports as
|
||||
listed in port_map.
|
||||
|
||||
Raises:
|
||||
`PortError` if `port_map` contains port names not present in the
|
||||
current device.
|
||||
`PortError` if applying the prefixes results in duplicate port
|
||||
names.
|
||||
TODO doc pather.interface
|
||||
"""
|
||||
if library is None:
|
||||
if hasattr(source, 'library') and isinstance(source.library, ILibrary):
|
||||
@ -246,15 +183,21 @@ class Pather(Builder):
|
||||
if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict):
|
||||
tools = source.tools
|
||||
|
||||
if isinstance(source, str):
|
||||
source = library.abstract(source).ports
|
||||
|
||||
pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
|
||||
new = Pather(library=library, pattern=pat, name=name, tools=tools)
|
||||
new = Pather.from_builder(
|
||||
Builder.interface(
|
||||
source=source,
|
||||
library=library,
|
||||
in_prefix=in_prefix,
|
||||
out_prefix=out_prefix,
|
||||
port_map=port_map,
|
||||
name=name,
|
||||
),
|
||||
tools=tools,
|
||||
)
|
||||
return new
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
s = f'<Pather {self.pattern} >' # TODO maybe show lib and tools? in builder repr?
|
||||
return s
|
||||
|
||||
def retool(
|
||||
@ -262,18 +205,6 @@ class Pather(Builder):
|
||||
tool: Tool,
|
||||
keys: str | Sequence[str | None] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Update the `Tool` which will be used when generating `Pattern`s for the ports
|
||||
given by `keys`.
|
||||
|
||||
Args:
|
||||
tool: The new `Tool` to use for the given ports.
|
||||
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
||||
used when there is no matching entry in `self.tools` for the port in question.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if keys is None or isinstance(keys, str):
|
||||
self.tools[keys] = tool
|
||||
else:
|
||||
@ -288,112 +219,36 @@ class Pather(Builder):
|
||||
length: float,
|
||||
*,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
plug_into: str | None = None,
|
||||
base_name: str = '_path',
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance.
|
||||
|
||||
The wire will travel `length` distance along the port's axis, an an unspecified
|
||||
(tool-dependent) distance in the perpendicular direction. The output port will
|
||||
be rotated (or not) based on the `ccw` parameter.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
ccw: If `None`, the output should be along the same axis as the input.
|
||||
Otherwise, cast to bool and turn counterclockwise if True
|
||||
and clockwise otherwise.
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||
that you will need to change these. The first port is the input (to be
|
||||
connected to `portspec`).
|
||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||
port on `self`.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
BuildError if `distance` is too small to fit the bend (if a bend is present).
|
||||
LibraryError if no valid name could be picked for the pattern.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path() since device is dead')
|
||||
return self
|
||||
|
||||
tool = self.tools.get(portspec, self.tools[None])
|
||||
in_ptype = self.pattern[portspec].ptype
|
||||
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||
abstract = self.library << tree
|
||||
if plug_into is not None:
|
||||
output = {plug_into: tool_port_names[1]}
|
||||
else:
|
||||
output = {}
|
||||
return self.plug(abstract, {portspec: tool_port_names[0], **output})
|
||||
pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||
name = self.library.get_name(base_name)
|
||||
self.library[name] = pat
|
||||
return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]})
|
||||
|
||||
def path_to(
|
||||
self,
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
position: float | None = None,
|
||||
position: float,
|
||||
*,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
plug_into: str | None = None,
|
||||
base_name: str = '_pathto',
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of ending exactly at a target position.
|
||||
|
||||
The wire will travel so that the output port will be placed at exactly the target
|
||||
position along the input port's axis. There can be an unspecified (tool-dependent)
|
||||
offset in the perpendicular direction. The output port will be rotated (or not)
|
||||
based on the `ccw` parameter.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
ccw: If `None`, the output should be along the same axis as the input.
|
||||
Otherwise, cast to bool and turn counterclockwise if True
|
||||
and clockwise otherwise.
|
||||
position: The final port position, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
Only one of `position`, `x`, and `y` may be specified.
|
||||
x: The final port position along the x axis.
|
||||
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
||||
BuildError will be raised.
|
||||
y: The final port position along the y axis.
|
||||
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
||||
BuildError will be raised.
|
||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||
that you will need to change these. The first port is the input (to be
|
||||
connected to `portspec`).
|
||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||
port on `self`.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
||||
is present).
|
||||
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
||||
BuildError if more than one of `x`, `y`, and `position` is specified.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path_to() since device is dead')
|
||||
return self
|
||||
|
||||
pos_count = sum(vv is not None for vv in (position, x, y))
|
||||
if pos_count > 1:
|
||||
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
||||
if pos_count < 1:
|
||||
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
||||
|
||||
port = self.pattern[portspec]
|
||||
x, y = port.offset
|
||||
if port.rotation is None:
|
||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||
|
||||
@ -402,156 +257,15 @@ class Pather(Builder):
|
||||
|
||||
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
||||
if is_horizontal:
|
||||
if y is not None:
|
||||
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
||||
if position is None:
|
||||
position = x
|
||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x):
|
||||
raise BuildError(f'path_to routing to behind source port: x={x:g} to {position:g}')
|
||||
length = numpy.abs(position - x)
|
||||
else:
|
||||
if x is not None:
|
||||
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
||||
if position is None:
|
||||
position = y
|
||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y):
|
||||
raise BuildError(f'path_to routing to behind source port: y={y:g} to {position:g}')
|
||||
length = numpy.abs(position - y)
|
||||
|
||||
x0, y0 = port.offset
|
||||
if is_horizontal:
|
||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
||||
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
||||
length = numpy.abs(position - x0)
|
||||
else:
|
||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
||||
length = numpy.abs(position - y0)
|
||||
|
||||
return self.path(
|
||||
portspec,
|
||||
ccw,
|
||||
length,
|
||||
tool_port_names=tool_port_names,
|
||||
plug_into=plug_into,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def path_into(
|
||||
self,
|
||||
portspec_src: str,
|
||||
portspec_dst: str,
|
||||
*,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
out_ptype: str | None = None,
|
||||
plug_destination: bool = True,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a "wire"/"waveguide" and traveling between the ports `portspec_src` and
|
||||
`portspec_dst`, and `plug` it into both (or just the source port).
|
||||
|
||||
Only unambiguous scenarios are allowed:
|
||||
- Straight connector between facing ports
|
||||
- Single 90 degree bend
|
||||
- Jog between facing ports
|
||||
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
||||
|
||||
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
||||
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
||||
|
||||
Args:
|
||||
portspec_src: The name of the starting port into which the wire will be plugged.
|
||||
portspec_dst: The name of the destination port.
|
||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||
that you will need to change these. The first port is the input (to be
|
||||
connected to `portspec`).
|
||||
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
||||
to be generated at the destination end. If `None` (default), the destination
|
||||
port's `ptype` will be used.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
PortError if either port does not have a specified rotation.
|
||||
BuildError if and invalid port config is encountered:
|
||||
- Non-manhattan ports
|
||||
- U-bend
|
||||
- Destination too close to (or behind) source
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path_into() since device is dead')
|
||||
return self
|
||||
|
||||
port_src = self.pattern[portspec_src]
|
||||
port_dst = self.pattern[portspec_dst]
|
||||
|
||||
if out_ptype is None:
|
||||
out_ptype = port_dst.ptype
|
||||
|
||||
if port_src.rotation is None:
|
||||
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
|
||||
if port_dst.rotation is None:
|
||||
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
||||
|
||||
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
||||
raise BuildError('path_into was asked to route from non-manhattan port')
|
||||
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
||||
raise BuildError('path_into was asked to route to non-manhattan port')
|
||||
|
||||
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
|
||||
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
|
||||
xs, ys = port_src.offset
|
||||
xd, yd = port_dst.offset
|
||||
|
||||
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
||||
|
||||
src_ne = port_src.rotation % (2 * pi) > (3 * pi / 4) # path from src will go north or east
|
||||
|
||||
def get_jog(ccw: SupportsBool, length: float) -> float:
|
||||
tool = self.tools.get(portspec_src, self.tools[None])
|
||||
in_ptype = 'unk' # Could use port_src.ptype, but we're assuming this is after one bend already...
|
||||
tree2 = tool.path(ccw, length, in_ptype=in_ptype, port_names=('A', 'B'), out_ptype=out_ptype, **kwargs)
|
||||
top2 = tree2.top_pattern()
|
||||
jog = rotation_matrix_2d(top2['A'].rotation) @ (top2['B'].offset - top2['A'].offset)
|
||||
return jog[1]
|
||||
|
||||
dst_extra_args = {'out_ptype': out_ptype}
|
||||
if plug_destination:
|
||||
dst_extra_args['plug_into'] = portspec_dst
|
||||
|
||||
src_args = {**kwargs, 'tool_port_names': tool_port_names}
|
||||
dst_args = {**src_args, **dst_extra_args}
|
||||
if src_is_horizontal and not dst_is_horizontal:
|
||||
# single bend should suffice
|
||||
self.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||
elif dst_is_horizontal and not src_is_horizontal:
|
||||
# single bend should suffice
|
||||
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||
elif numpy.isclose(angle, pi):
|
||||
if src_is_horizontal and ys == yd:
|
||||
# straight connector
|
||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||
elif not src_is_horizontal and xs == xd:
|
||||
# straight connector
|
||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||
elif src_is_horizontal:
|
||||
# figure out how much x our y-segment (2nd) takes up, then path based on that
|
||||
y_len = numpy.abs(yd - ys)
|
||||
ccw2 = src_ne != (yd > ys)
|
||||
jog = get_jog(ccw2, y_len) * numpy.sign(xd - xs)
|
||||
self.path_to(portspec_src, not ccw2, x=xd - jog, **src_args)
|
||||
self.path_to(portspec_src, ccw2, y=yd, **dst_args)
|
||||
else:
|
||||
# figure out how much y our x-segment (2nd) takes up, then path based on that
|
||||
x_len = numpy.abs(xd - xs)
|
||||
ccw2 = src_ne != (xd < xs)
|
||||
jog = get_jog(ccw2, x_len) * numpy.sign(yd - ys)
|
||||
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
|
||||
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
|
||||
elif numpy.isclose(angle, 0):
|
||||
raise BuildError('Don\'t know how to route a U-bend at this time!')
|
||||
else:
|
||||
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
||||
|
||||
return self
|
||||
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs)
|
||||
|
||||
def mpath(
|
||||
self,
|
||||
@ -562,84 +276,9 @@ class Pather(Builder):
|
||||
set_rotation: float | None = None,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
force_container: bool = False,
|
||||
base_name: str = SINGLE_USE_PREFIX + 'mpath',
|
||||
base_name: str = '_mpath',
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
||||
of "wires or "waveguides".
|
||||
|
||||
The wires will travel so that the output ports will be placed at well-defined
|
||||
locations along the axis of their input ports, but may have arbitrary (tool-
|
||||
dependent) offsets in the perpendicular direction.
|
||||
|
||||
If `ccw` is not `None`, the wire bundle will turn 90 degres in either the
|
||||
clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the
|
||||
bundle, the center-to-center wire spacings after the turn are set by `spacing`,
|
||||
which is required when `ccw` is not `None`. The final position of bundle as a
|
||||
whole can be set in a number of ways:
|
||||
|
||||
=A>---------------------------V turn direction: `ccw=False`
|
||||
=B>-------------V |
|
||||
=C>-----------------------V |
|
||||
=D=>----------------V |
|
||||
|
|
||||
|
||||
x---x---x---x `spacing` (can be scalar or array)
|
||||
|
||||
<--------------> `emin=`
|
||||
<------> `bound_type='min_past_furthest', bound=`
|
||||
<--------------------------------> `emax=`
|
||||
x `pmin=`
|
||||
x `pmax=`
|
||||
|
||||
- `emin=`, equivalent to `bound_type='min_extension', bound=`
|
||||
The total extension value for the furthest-out port (B in the diagram).
|
||||
- `emax=`, equivalent to `bound_type='max_extension', bound=`:
|
||||
The total extension value for the closest-in port (C in the diagram).
|
||||
- `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`:
|
||||
The coordinate of the innermost bend (D's bend).
|
||||
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||
- `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`:
|
||||
The coordinate of the outermost bend (A's bend).
|
||||
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||
- `bound_type='min_past_furthest', bound=`:
|
||||
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
||||
|
||||
If `ccw=None`, final output positions (along the input axis) of all wires will be
|
||||
identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is
|
||||
required. In this case, `emin=` and `emax=` are equivalent to each other, and
|
||||
`pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other.
|
||||
|
||||
|
||||
Args:
|
||||
portspec: The names of the ports which are to be routed.
|
||||
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
||||
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
||||
and clockwise otherwise.
|
||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
||||
Must be provided if (and only if) `ccw` is not `None`.
|
||||
set_rotation: If the provided ports have `rotation=None`, this can be used
|
||||
to set a rotation for them.
|
||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||
that you will need to change these. The first port is the input (to be
|
||||
connected to `portspec`).
|
||||
force_container: If `False` (default), and only a single port is provided, the
|
||||
generated wire for that port will be referenced directly, rather than being
|
||||
wrapped in an additonal `Pattern`.
|
||||
base_name: Name to use for the generated `Pattern`. This will be passed through
|
||||
`self.library.get_name()` to get a unique name for each new `Pattern`.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
BuildError if the implied length for any wire is too close to fit the bend
|
||||
(if a bend is requested).
|
||||
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
||||
match the axis of `portspec`.
|
||||
BuildError if an incorrect bound type or spacing is specified.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping mpath() since device is dead')
|
||||
return self
|
||||
@ -648,17 +287,14 @@ class Pather(Builder):
|
||||
if 'bound_type' in kwargs:
|
||||
bound_types.add(kwargs['bound_type'])
|
||||
bound = kwargs['bound']
|
||||
del kwargs['bound_type']
|
||||
del kwargs['bound']
|
||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'min_past_furthest'):
|
||||
if bt in kwargs:
|
||||
bound_types.add(bt)
|
||||
bound = kwargs[bt]
|
||||
del kwargs[bt]
|
||||
|
||||
if not bound_types:
|
||||
raise BuildError('No bound type specified for mpath')
|
||||
if len(bound_types) > 1:
|
||||
elif len(bound_types) > 1:
|
||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||
bound_type = tuple(bound_types)[0]
|
||||
|
||||
@ -671,16 +307,16 @@ class Pather(Builder):
|
||||
if len(ports) == 1 and not force_container:
|
||||
# Not a bus, so having a container just adds noise to the layout
|
||||
port_name = tuple(portspec)[0]
|
||||
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names, **kwargs)
|
||||
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
|
||||
else:
|
||||
bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
|
||||
for port_name, length in extensions.items():
|
||||
bld.path(port_name, ccw, length, tool_port_names=tool_port_names)
|
||||
name = self.library.get_name(base_name)
|
||||
self.library[name] = bld.pattern
|
||||
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
|
||||
|
||||
bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
|
||||
for port_name, length in extensions.items():
|
||||
bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
||||
name = self.library.get_name(base_name)
|
||||
self.library[name] = bld.pattern
|
||||
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
||||
|
||||
# TODO def bus_join()?
|
||||
# TODO def path_join() and def bus_join()?
|
||||
|
||||
def flatten(self) -> Self:
|
||||
"""
|
||||
|
@ -1,62 +1,42 @@
|
||||
"""
|
||||
Pather with batched (multi-step) rendering
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Mapping, MutableMapping
|
||||
from typing import Self, Sequence, Mapping, Final
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..ref import Ref
|
||||
from ..library import ILibrary
|
||||
from ..error import PortError, BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
from ..utils import rotation_matrix_2d
|
||||
from ..utils import SupportsBool
|
||||
from .tools import Tool, RenderStep
|
||||
from .tools import Tool, render_step_t
|
||||
from .utils import ell
|
||||
from .builder import Builder
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RenderPather(PortList):
|
||||
"""
|
||||
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath`
|
||||
functions to plan out wire paths without incrementally generating the layout. Instead,
|
||||
it waits until `render` is called, at which point it draws all the planned segments
|
||||
simultaneously. This allows it to e.g. draw each wire using a single `Path` or
|
||||
`Polygon` shape instead of multiple rectangles.
|
||||
|
||||
`RenderPather` calls out to `Tool.planL` and `Tool.render` to provide tool-specific
|
||||
dimensions and build the final geometry for each wire. `Tool.planL` provides the
|
||||
output port data (relative to the input) for each segment. The tool, input and output
|
||||
ports are placed into a `RenderStep`, and a sequence of `RenderStep`s is stored for
|
||||
each port. When `render` is called, it bundles `RenderStep`s into batches which use
|
||||
the same `Tool`, and passes each batch to the relevant tool's `Tool.render` to build
|
||||
the geometry.
|
||||
|
||||
See `Pather` for routing examples. After routing is complete, `render` must be called
|
||||
to generate the final geometry.
|
||||
"""
|
||||
__slots__ = ('pattern', 'library', 'paths', 'tools', '_dead', )
|
||||
|
||||
pattern: Pattern
|
||||
""" Layout of this device """
|
||||
|
||||
library: ILibrary
|
||||
library: ILibrary | None
|
||||
""" Library from which patterns should be referenced """
|
||||
|
||||
_dead: bool
|
||||
""" If True, plug()/place() are skipped (for debugging) """
|
||||
|
||||
paths: defaultdict[str, list[RenderStep]]
|
||||
""" Per-port list of operations, to be used by `render` """
|
||||
paths: defaultdict[str, list[render_step_t]]
|
||||
# op, start_port, dx, dy, o_ptype tool
|
||||
|
||||
tools: dict[str | None, Tool]
|
||||
"""
|
||||
@ -74,30 +54,17 @@ class RenderPather(PortList):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: ILibrary,
|
||||
library: ILibrary | None = None,
|
||||
*,
|
||||
pattern: Pattern | None = None,
|
||||
ports: str | Mapping[str, Port] | None = None,
|
||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
library: The library from which referenced patterns will be taken,
|
||||
and where new patterns (e.g. generated by the `tools`) will be placed.
|
||||
pattern: The pattern which will be modified by subsequent operations.
|
||||
If `None` (default), a new pattern is created.
|
||||
ports: Allows specifying the initial set of ports, if `pattern` does
|
||||
not already have any ports (or is not provided). May be a string,
|
||||
in which case it is interpreted as a name in `library`.
|
||||
Default `None` (no ports).
|
||||
tools: A mapping of {port: tool} which specifies what `Tool` should be used
|
||||
to generate waveguide or wire segments when `path`/`path_to`/`mpath`
|
||||
are called. Relies on `Tool.planL` and `Tool.render` implementations.
|
||||
name: If specified, `library[name]` is set to `self.pattern`.
|
||||
# TODO documentation for Builder() constructor
|
||||
|
||||
"""
|
||||
self._dead = False
|
||||
self.paths = defaultdict(list)
|
||||
self.library = library
|
||||
if pattern is not None:
|
||||
self.pattern = pattern
|
||||
@ -119,37 +86,46 @@ class RenderPather(PortList):
|
||||
raise BuildError('Name was supplied, but no library was given!')
|
||||
library[name] = self.pattern
|
||||
|
||||
if tools is None:
|
||||
self.tools = {}
|
||||
elif isinstance(tools, Tool):
|
||||
self.tools = {None: tools}
|
||||
else:
|
||||
self.tools = dict(tools)
|
||||
self.paths = defaultdict(list)
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls: type['RenderPather'],
|
||||
cls,
|
||||
source: PortList | Mapping[str, Port] | str,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
||||
in_prefix: str = 'in_',
|
||||
out_prefix: str = '',
|
||||
port_map: dict[str, str] | Sequence[str] | None = None,
|
||||
name: str | None = None,
|
||||
) -> 'RenderPather':
|
||||
"""
|
||||
Wrapper for `Pattern.interface()`, which returns a RenderPather instead.
|
||||
Begin building a new device based on all or some of the ports in the
|
||||
source device. Do not include the source device; instead use it
|
||||
to define ports (the "interface") for the new device.
|
||||
|
||||
The ports specified by `port_map` (default: all ports) are copied to
|
||||
new device, and additional (input) ports are created facing in the
|
||||
opposite directions. The specified `in_prefix` and `out_prefix` are
|
||||
prepended to the port names to differentiate them.
|
||||
|
||||
By default, the flipped ports are given an 'in_' prefix and unflipped
|
||||
ports keep their original names, enabling intuitive construction of
|
||||
a device that will "plug into" the current device; the 'in_*' ports
|
||||
are used for plugging the devices together while the original port
|
||||
names are used for building the new device.
|
||||
|
||||
Another use-case could be to build the new device using the 'in_'
|
||||
ports, creating a new device which could be used in place of the
|
||||
current device.
|
||||
|
||||
Args:
|
||||
source: A collection of ports (e.g. Pattern, Builder, or dict)
|
||||
from which to create the interface. May be a pattern name if
|
||||
`library` is provided.
|
||||
from which to create the interface.
|
||||
library: Used for buildin functions; if not passed and the source
|
||||
library: Library from which existing patterns should be referenced,
|
||||
and to which the new one should be added (if named). If not provided,
|
||||
`source.library` must exist and will be used.
|
||||
tools: `Tool`s which will be used by the pather for generating new wires
|
||||
or waveguides (via `path`/`path_to`/`mpath`).
|
||||
and to which new ones should be added. If not provided,
|
||||
the source's library will be used (if available).
|
||||
in_prefix: Prepended to port names for newly-created ports with
|
||||
reversed directions compared to the current device.
|
||||
out_prefix: Prepended to port names for ports which are directly
|
||||
@ -161,7 +137,7 @@ class RenderPather(PortList):
|
||||
renamed (to the values).
|
||||
|
||||
Returns:
|
||||
The new `RenderPather`, with an empty pattern and 2x as many ports as
|
||||
The new builder, with an empty pattern and 2x as many ports as
|
||||
listed in port_map.
|
||||
|
||||
Raises:
|
||||
@ -173,17 +149,42 @@ class RenderPather(PortList):
|
||||
if library is None:
|
||||
if hasattr(source, 'library') and isinstance(source.library, ILibrary):
|
||||
library = source.library
|
||||
else:
|
||||
raise BuildError('No library provided (and not present in `source.library`')
|
||||
|
||||
if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict):
|
||||
tools = source.tools
|
||||
|
||||
if isinstance(source, str):
|
||||
source = library.abstract(source).ports
|
||||
if library is None:
|
||||
raise BuildError('Source given as a string, but `library` was `None`!')
|
||||
orig_ports = library.abstract(source).ports
|
||||
elif isinstance(source, PortList):
|
||||
orig_ports = source.ports
|
||||
elif isinstance(source, dict):
|
||||
orig_ports = source
|
||||
else:
|
||||
raise BuildError(f'Unable to get ports from {type(source)}: {source}')
|
||||
|
||||
pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
|
||||
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
||||
if port_map:
|
||||
if isinstance(port_map, dict):
|
||||
missing_inkeys = set(port_map.keys()) - set(orig_ports.keys())
|
||||
mapped_ports = {port_map[k]: v for k, v in orig_ports.items() if k in port_map}
|
||||
else:
|
||||
port_set = set(port_map)
|
||||
missing_inkeys = port_set - set(orig_ports.keys())
|
||||
mapped_ports = {k: v for k, v in orig_ports.items() if k in port_set}
|
||||
|
||||
if missing_inkeys:
|
||||
raise PortError(f'`port_map` keys not present in source: {missing_inkeys}')
|
||||
else:
|
||||
mapped_ports = orig_ports
|
||||
|
||||
ports_in = {f'{in_prefix}{pname}': port.deepcopy().rotate(pi)
|
||||
for pname, port in mapped_ports.items()}
|
||||
ports_out = {f'{out_prefix}{pname}': port.deepcopy()
|
||||
for pname, port in mapped_ports.items()}
|
||||
|
||||
duplicates = set(ports_out.keys()) & set(ports_in.keys())
|
||||
if duplicates:
|
||||
raise PortError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}')
|
||||
|
||||
new = RenderPather(library=library, ports={**ports_in, **ports_out}, name=name)
|
||||
return new
|
||||
|
||||
def plug(
|
||||
@ -192,84 +193,48 @@ class RenderPather(PortList):
|
||||
map_in: dict[str, str],
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
mirrored: tuple[bool, bool] = (False, False),
|
||||
inherit_name: bool = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
) -> Self:
|
||||
"""
|
||||
Wrapper for `Pattern.plug` which adds a `RenderStep` with opcode 'P'
|
||||
for any affected ports. This separates any future `RenderStep`s on the
|
||||
same port into a new batch, since the plugged device interferes with drawing.
|
||||
|
||||
Args:
|
||||
other: An `Abstract`, string, or `Pattern` describing the device to be instatiated.
|
||||
map_in: dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||
port connections between the two devices.
|
||||
map_out: dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||
new names for ports in `other`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to
|
||||
connecting any ports.
|
||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
||||
and `map_out` is `None`, and `other` has only two ports total,
|
||||
then automatically renames the output port of `other` to the
|
||||
name of the port from `self` that appears in `map_in`. This
|
||||
makes it easy to extend a device with simple 2-port devices
|
||||
(e.g. wires) without providing `map_out` each time `plug` is
|
||||
called. See "Examples" above for more info. Default `True`.
|
||||
set_rotation: If the necessary rotation cannot be determined from
|
||||
the ports being connected (i.e. all pairs have at least one
|
||||
port with `rotation=None`), `set_rotation` must be provided
|
||||
to indicate how much `other` should be rotated. Otherwise,
|
||||
`set_rotation` must remain `None`.
|
||||
append: If `True`, `other` is appended instead of being referenced.
|
||||
Note that this does not flatten `other`, so its refs will still
|
||||
be refs (now inside `self`).
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
||||
exist in `self.ports` or `other_names`.
|
||||
`PortError` if there are any duplicate names after `map_in` and `map_out`
|
||||
are applied.
|
||||
`PortError` if the specified port mapping is not achieveable (the ports
|
||||
do not line up)
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping plug() since device is dead')
|
||||
return self
|
||||
|
||||
other_tgt: Pattern | Abstract
|
||||
if isinstance(other, str):
|
||||
other_tgt = self.library.abstract(other)
|
||||
if append and isinstance(other, Abstract):
|
||||
other_tgt = self.library[other.name]
|
||||
if self.library is None:
|
||||
raise BuildError('No library available, but `other` was a string!')
|
||||
other = self.library.abstract(other)
|
||||
|
||||
# get rid of plugged ports
|
||||
for kk in map_in:
|
||||
if kk in self.paths:
|
||||
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
|
||||
# If asked to inherit a name, check that all conditions are met
|
||||
if (inherit_name
|
||||
and not map_out
|
||||
and len(map_in) == 1
|
||||
and len(other.ports) == 2):
|
||||
out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values())))
|
||||
map_out = {out_port_name: next(iter(map_in.keys()))}
|
||||
|
||||
plugged = map_in.values()
|
||||
for name, port in other_tgt.ports.items():
|
||||
if name in plugged:
|
||||
continue
|
||||
new_name = map_out.get(name, name) if map_out is not None else name
|
||||
if new_name is not None and new_name in self.paths:
|
||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||
if map_out is None:
|
||||
map_out = {}
|
||||
map_out = copy.deepcopy(map_out)
|
||||
|
||||
self.pattern.plug(
|
||||
other=other_tgt,
|
||||
map_in=map_in,
|
||||
map_out=map_out,
|
||||
self.check_ports(other.ports.keys(), map_in, map_out)
|
||||
translation, rotation, pivot = self.find_transform(
|
||||
other,
|
||||
map_in,
|
||||
mirrored=mirrored,
|
||||
inherit_name=inherit_name,
|
||||
set_rotation=set_rotation,
|
||||
append=append,
|
||||
)
|
||||
|
||||
# get rid of plugged ports
|
||||
for ki, vi in map_in.items():
|
||||
del self.ports[ki]
|
||||
map_out[vi] = None
|
||||
if ki in self.paths:
|
||||
self.paths[ki].append(('P', None, 0.0, 0.0, 'unk', None))
|
||||
|
||||
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
|
||||
mirrored=mirrored, port_map=map_out, skip_port_check=True)
|
||||
return self
|
||||
|
||||
def place(
|
||||
@ -279,94 +244,45 @@ class RenderPather(PortList):
|
||||
offset: ArrayLike = (0, 0),
|
||||
rotation: float = 0,
|
||||
pivot: ArrayLike = (0, 0),
|
||||
mirrored: bool = False,
|
||||
mirrored: tuple[bool, bool] = (False, False),
|
||||
port_map: dict[str, str | None] | None = None,
|
||||
skip_port_check: bool = False,
|
||||
append: bool = False,
|
||||
) -> Self:
|
||||
"""
|
||||
Wrapper for `Pattern.place` which adds a `RenderStep` with opcode 'P'
|
||||
for any affected ports. This separates any future `RenderStep`s on the
|
||||
same port into a new batch, since the placed device interferes with drawing.
|
||||
|
||||
Note that mirroring is applied before rotation; translation (`offset`) is applied last.
|
||||
|
||||
Args:
|
||||
other: An `Abstract` or `Pattern` describing the device to be instatiated.
|
||||
offset: Offset at which to place the instance. Default (0, 0).
|
||||
rotation: Rotation applied to the instance before placement. Default 0.
|
||||
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
||||
Rotation is applied prior to translation (`offset`).
|
||||
mirrored: Whether theinstance should be mirrored across the x axis.
|
||||
Mirroring is applied before translation and rotation.
|
||||
port_map: dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||
new names for ports in the instantiated pattern. New names can be
|
||||
`None`, which will delete those ports.
|
||||
skip_port_check: Can be used to skip the internal call to `check_ports`,
|
||||
in case it has already been performed elsewhere.
|
||||
append: If `True`, `other` is appended instead of being referenced.
|
||||
Note that this does not flatten `other`, so its refs will still
|
||||
be refs (now inside `self`).
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
||||
exist in `self.ports` or `other.ports`.
|
||||
`PortError` if there are any duplicate names after `map_in` and `map_out`
|
||||
are applied.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping place() since device is dead')
|
||||
return self
|
||||
|
||||
other_tgt: Pattern | Abstract
|
||||
if isinstance(other, str):
|
||||
other_tgt = self.library.abstract(other)
|
||||
if append and isinstance(other, Abstract):
|
||||
other_tgt = self.library[other.name]
|
||||
if self.library is None:
|
||||
raise BuildError('No library available, but `other` was a string!')
|
||||
other = self.library.abstract(other)
|
||||
|
||||
for name, port in other_tgt.ports.items():
|
||||
new_name = port_map.get(name, name) if port_map is not None else name
|
||||
if new_name is not None and new_name in self.paths:
|
||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||
if port_map is None:
|
||||
port_map = {}
|
||||
|
||||
self.pattern.place(
|
||||
other=other_tgt,
|
||||
offset=offset,
|
||||
rotation=rotation,
|
||||
pivot=pivot,
|
||||
mirrored=mirrored,
|
||||
port_map=port_map,
|
||||
skip_port_check=skip_port_check,
|
||||
append=append,
|
||||
)
|
||||
if not skip_port_check:
|
||||
self.check_ports(other.ports.keys(), map_in=None, map_out=port_map)
|
||||
|
||||
return self
|
||||
ports = {}
|
||||
for name, port in other.ports.items():
|
||||
new_name = port_map.get(name, name)
|
||||
if new_name is None:
|
||||
continue
|
||||
ports[new_name] = port
|
||||
if new_name in self.paths:
|
||||
self.paths[new_name].append(('P', None, 0.0, 0.0, 'unk', None))
|
||||
|
||||
def retool(
|
||||
self,
|
||||
tool: Tool,
|
||||
keys: str | Sequence[str | None] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Update the `Tool` which will be used when generating `Pattern`s for the ports
|
||||
given by `keys`.
|
||||
for name, port in ports.items():
|
||||
p = port.deepcopy()
|
||||
p.mirror2d(mirrored)
|
||||
p.rotate_around(pivot, rotation)
|
||||
p.translate(offset)
|
||||
self.ports[name] = p
|
||||
|
||||
Args:
|
||||
tool: The new `Tool` to use for the given ports.
|
||||
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
||||
used when there is no matching entry in `self.tools` for the port in question.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if keys is None or isinstance(keys, str):
|
||||
self.tools[keys] = tool
|
||||
else:
|
||||
for key in keys:
|
||||
self.tools[key] = tool
|
||||
sp = Ref(other.name, mirrored=mirrored)
|
||||
sp.rotate_around(pivot, rotation)
|
||||
sp.translate(offset)
|
||||
self.pattern.refs.append(sp)
|
||||
return self
|
||||
|
||||
def path(
|
||||
@ -376,31 +292,6 @@ class RenderPather(PortList):
|
||||
length: float,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance.
|
||||
|
||||
The wire will travel `length` distance along the port's axis, an an unspecified
|
||||
(tool-dependent) distance in the perpendicular direction. The output port will
|
||||
be rotated (or not) based on the `ccw` parameter.
|
||||
|
||||
`RenderPather.render` must be called after all paths have been fully planned.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
ccw: If `None`, the output should be along the same axis as the input.
|
||||
Otherwise, cast to bool and turn counterclockwise if True
|
||||
and clockwise otherwise.
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
BuildError if `distance` is too small to fit the bend (if a bend is present).
|
||||
LibraryError if no valid name could be picked for the pattern.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path() since device is dead')
|
||||
return self
|
||||
@ -408,79 +299,53 @@ class RenderPather(PortList):
|
||||
port = self.pattern[portspec]
|
||||
in_ptype = port.ptype
|
||||
port_rot = port.rotation
|
||||
assert port_rot is not None # TODO allow manually setting rotation for RenderPather.path()?
|
||||
assert port_rot is not None # TODO allow manually setting rotation?
|
||||
|
||||
tool = self.tools.get(portspec, self.tools[None])
|
||||
# ask the tool for bend size (fill missing dx or dy), check feasibility, and get out_ptype
|
||||
out_port, data = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
||||
bend_radius, out_ptype = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
||||
|
||||
# Update port
|
||||
out_port.rotate_around((0, 0), pi + port_rot)
|
||||
out_port.translate(port.offset)
|
||||
if ccw is None:
|
||||
bend_run = 0.0
|
||||
elif bool(ccw):
|
||||
bend_run = bend_radius
|
||||
else:
|
||||
bend_run = -bend_radius
|
||||
|
||||
step = RenderStep('L', tool, port.copy(), out_port.copy(), data)
|
||||
dx, dy = rotation_matrix_2d(port_rot + pi) @ [length, bend_run]
|
||||
|
||||
step: Final = ('L', port.deepcopy(), dx, dy, out_ptype, tool)
|
||||
self.paths[portspec].append(step)
|
||||
|
||||
self.pattern.ports[portspec] = out_port.copy()
|
||||
# Update port
|
||||
port.offset += (dx, dy)
|
||||
if ccw is not None:
|
||||
port.rotate((-1 if ccw else 1) * pi / 2)
|
||||
port.ptype = out_ptype
|
||||
|
||||
return self
|
||||
|
||||
'''
|
||||
- record ('path', port, dx, dy, out_ptype, tool)
|
||||
- to render, ccw = {0: None, 1: True, -1: False}[numpy.sign(dx) * numpy.sign(dy) * (-1 if x_start else 1)
|
||||
- length is just dx or dy
|
||||
- in_ptype and out_ptype are taken directly
|
||||
- for sbend: dx and dy are maybe reordered (length and jog)
|
||||
'''
|
||||
|
||||
def path_to(
|
||||
self,
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
position: float | None = None,
|
||||
*,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
position: float,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
||||
of ending exactly at a target position.
|
||||
|
||||
The wire will travel so that the output port will be placed at exactly the target
|
||||
position along the input port's axis. There can be an unspecified (tool-dependent)
|
||||
offset in the perpendicular direction. The output port will be rotated (or not)
|
||||
based on the `ccw` parameter.
|
||||
|
||||
`RenderPather.render` must be called after all paths have been fully planned.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
ccw: If `None`, the output should be along the same axis as the input.
|
||||
Otherwise, cast to bool and turn counterclockwise if True
|
||||
and clockwise otherwise.
|
||||
position: The final port position, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
Only one of `position`, `x`, and `y` may be specified.
|
||||
x: The final port position along the x axis.
|
||||
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
||||
BuildError will be raised.
|
||||
y: The final port position along the y axis.
|
||||
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
||||
BuildError will be raised.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
||||
is present).
|
||||
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
||||
BuildError if more than one of `x`, `y`, and `position` is specified.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path_to() since device is dead')
|
||||
return self
|
||||
|
||||
pos_count = sum(vv is not None for vv in (position, x, y))
|
||||
if pos_count > 1:
|
||||
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
||||
if pos_count < 1:
|
||||
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
||||
|
||||
port = self.pattern[portspec]
|
||||
x, y = port.offset
|
||||
if port.rotation is None:
|
||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||
|
||||
@ -489,25 +354,13 @@ class RenderPather(PortList):
|
||||
|
||||
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
||||
if is_horizontal:
|
||||
if y is not None:
|
||||
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
||||
if position is None:
|
||||
position = x
|
||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x):
|
||||
raise BuildError(f'path_to routing to behind source port: x={x:g} to {position:g}')
|
||||
length = numpy.abs(position - x)
|
||||
else:
|
||||
if x is not None:
|
||||
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
||||
if position is None:
|
||||
position = y
|
||||
|
||||
x0, y0 = port.offset
|
||||
if is_horizontal:
|
||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
||||
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
||||
length = numpy.abs(position - x0)
|
||||
else:
|
||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
||||
length = numpy.abs(position - y0)
|
||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y):
|
||||
raise BuildError(f'path_to routing to behind source port: y={y:g} to {position:g}')
|
||||
length = numpy.abs(position - y)
|
||||
|
||||
return self.path(portspec, ccw, length, **kwargs)
|
||||
|
||||
@ -520,32 +373,6 @@ class RenderPather(PortList):
|
||||
set_rotation: float | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
||||
of "wires or "waveguides".
|
||||
|
||||
See `Pather.mpath` for details.
|
||||
|
||||
Args:
|
||||
portspec: The names of the ports which are to be routed.
|
||||
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
||||
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
||||
and clockwise otherwise.
|
||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
||||
Must be provided if (and only if) `ccw` is not `None`.
|
||||
set_rotation: If the provided ports have `rotation=None`, this can be used
|
||||
to set a rotation for them.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
BuildError if the implied length for any wire is too close to fit the bend
|
||||
(if a bend is requested).
|
||||
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
||||
match the axis of `portspec`.
|
||||
BuildError if an incorrect bound type or spacing is specified.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping mpath() since device is dead')
|
||||
return self
|
||||
@ -554,14 +381,14 @@ class RenderPather(PortList):
|
||||
if 'bound_type' in kwargs:
|
||||
bound_types.add(kwargs['bound_type'])
|
||||
bound = kwargs['bound']
|
||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'min_past_furthest'):
|
||||
if bt in kwargs:
|
||||
bound_types.add(bt)
|
||||
bound = kwargs[bt]
|
||||
|
||||
if not bound_types:
|
||||
raise BuildError('No bound type specified for mpath')
|
||||
if len(bound_types) > 1:
|
||||
elif len(bound_types) > 1:
|
||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||
bound_type = tuple(bound_types)[0]
|
||||
|
||||
@ -580,61 +407,48 @@ class RenderPather(PortList):
|
||||
self.path(port_name, ccw, length)
|
||||
return self
|
||||
|
||||
def render(
|
||||
self,
|
||||
append: bool = True,
|
||||
) -> Self:
|
||||
"""
|
||||
Generate the geometry which has been planned out with `path`/`path_to`/etc.
|
||||
def render(self, lib: ILibrary | None = None) -> Self:
|
||||
lib = lib if lib is not None else self.library
|
||||
assert lib is not None
|
||||
|
||||
Args:
|
||||
append: If `True`, the rendered geometry will be directly appended to
|
||||
`self.pattern`. Note that it will not be flattened, so if only one
|
||||
layer of hierarchy is eliminated.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
lib = self.library
|
||||
tool_port_names = ('A', 'B')
|
||||
pat = Pattern()
|
||||
|
||||
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
|
||||
assert batch[0].tool is not None
|
||||
name = lib << batch[0].tool.render(batch, port_names=tool_port_names)
|
||||
pat.ports[portspec] = batch[0].start_port.copy()
|
||||
if append:
|
||||
pat.plug(lib[name], {portspec: tool_port_names[0]}, append=append)
|
||||
del lib[name] # NOTE if the rendered pattern has refs, those are now in `pat` but not flattened
|
||||
else:
|
||||
pat.plug(lib.abstract(name), {portspec: tool_port_names[0]}, append=append)
|
||||
bb = Builder(lib)
|
||||
|
||||
for portspec, steps in self.paths.items():
|
||||
batch: list[RenderStep] = []
|
||||
batch: list[render_step_t] = []
|
||||
for step in steps:
|
||||
appendable_op = step.opcode in ('L', 'S', 'U')
|
||||
same_tool = batch and step.tool == batch[0].tool
|
||||
opcode, _start_port, _dx, _dy, _out_ptype, tool = step
|
||||
|
||||
appendable_op = opcode in ('L', 'S', 'U')
|
||||
same_tool = batch and tool == batch[-1]
|
||||
|
||||
# If we can't continue a batch, render it
|
||||
if batch and (not appendable_op or not same_tool):
|
||||
render_batch(portspec, batch, append)
|
||||
# If we can't continue a batch, render it
|
||||
assert tool is not None
|
||||
assert batch[0][1] is not None
|
||||
name = lib << tool.render(batch, portnames=tool_port_names)
|
||||
bb.ports[portspec] = batch[0][1]
|
||||
bb.plug(name, {portspec: tool_port_names[0]})
|
||||
batch = []
|
||||
|
||||
# batch is emptied already if we couldn't continue it
|
||||
# batch is emptied already if we couldn't
|
||||
if appendable_op:
|
||||
batch.append(step)
|
||||
|
||||
# Opcodes which break the batch go below this line
|
||||
if not appendable_op and portspec in pat.ports:
|
||||
del pat.ports[portspec]
|
||||
if not appendable_op:
|
||||
del bb.ports[portspec]
|
||||
|
||||
#If the last batch didn't end yet
|
||||
if batch:
|
||||
render_batch(portspec, batch, append)
|
||||
# A batch didn't end yet
|
||||
assert tool is not None
|
||||
assert batch[0][1] is not None
|
||||
name = lib << tool.render(batch, portnames=tool_port_names)
|
||||
bb.ports[portspec] = batch[0][1]
|
||||
bb.plug(name, {portspec: tool_port_names[0]})
|
||||
|
||||
self.paths.clear()
|
||||
pat.ports.clear()
|
||||
self.pattern.append(pat)
|
||||
bb.ports.clear()
|
||||
self.pattern.append(bb.pattern)
|
||||
|
||||
return self
|
||||
|
||||
@ -697,7 +511,7 @@ class RenderPather(PortList):
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
s = f'<RenderPather {self.pattern} >' # TODO maybe show lib and tools? in builder repr?
|
||||
return s
|
||||
|
||||
|
||||
|
@ -1,61 +1,26 @@
|
||||
"""
|
||||
Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides)
|
||||
|
||||
# TODO document all tools
|
||||
"""
|
||||
from typing import Literal, Any
|
||||
from collections.abc import Sequence, Callable
|
||||
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Sequence, Literal, Callable
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
from numpy import pi
|
||||
|
||||
from ..utils import SupportsBool, rotation_matrix_2d, layer_t
|
||||
from ..utils import SupportsBool, rotation_matrix_2d
|
||||
from ..ports import Port
|
||||
from ..pattern import Pattern
|
||||
from ..abstract import Abstract
|
||||
from ..library import ILibrary, Library, SINGLE_USE_PREFIX
|
||||
from ..library import ILibrary, Library
|
||||
from ..error import BuildError
|
||||
from .builder import Builder
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RenderStep:
|
||||
"""
|
||||
Representation of a single saved operation, used by `RenderPather` and passed
|
||||
to `Tool.render()` when `RenderPather.render()` is called.
|
||||
"""
|
||||
opcode: Literal['L', 'S', 'U', 'P']
|
||||
""" What operation is being performed.
|
||||
L: planL (straight, optionally with a single bend)
|
||||
S: planS (s-bend)
|
||||
U: planU (u-bend)
|
||||
P: plug
|
||||
"""
|
||||
|
||||
tool: 'Tool | None'
|
||||
""" The current tool. May be `None` if `opcode='P'` """
|
||||
|
||||
start_port: Port
|
||||
end_port: Port
|
||||
|
||||
data: Any
|
||||
""" Arbitrary tool-specific data"""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.opcode != 'P' and self.tool is None:
|
||||
raise BuildError('Got tool=None but the opcode is not "P"')
|
||||
|
||||
render_step_t = (
|
||||
tuple[Literal['L', 'S', 'U'], Port, float, float, str, 'Tool']
|
||||
| tuple[Literal['P'], None, float, float, str, None]
|
||||
)
|
||||
|
||||
class Tool:
|
||||
"""
|
||||
Interface for path (e.g. wire or waveguide) generation.
|
||||
|
||||
Note that subclasses may implement only a subset of the methods and leave others
|
||||
unimplemented (e.g. in cases where they don't make sense or the required components
|
||||
are impractical or unavailable).
|
||||
"""
|
||||
def path(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
@ -65,40 +30,7 @@ class Tool:
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> Library:
|
||||
"""
|
||||
Create a wire or waveguide that travels exactly `length` distance along the axis
|
||||
of its input port.
|
||||
|
||||
Used by `Pather`.
|
||||
|
||||
The output port must be exactly `length` away along the input port's axis, but
|
||||
may be placed an additional (unspecified) distance away along the perpendicular
|
||||
direction. The output port should be rotated (or not) based on the value of
|
||||
`ccw`.
|
||||
|
||||
The input and output ports should be compatible with `in_ptype` and
|
||||
`out_ptype`, respectively. They should also be named `port_names[0]` and
|
||||
`port_names[1]`, respectively.
|
||||
|
||||
Args:
|
||||
ccw: If `None`, the output should be along the same axis as the input.
|
||||
Otherwise, cast to bool and turn counterclockwise if True
|
||||
and clockwise otherwise.
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
||||
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
||||
port_names: The output pattern will have its input port named `port_names[0]` and
|
||||
its output named `port_names[1]`.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
Returns:
|
||||
A pattern tree containing the requested L-shaped (or straight) wire or waveguide
|
||||
|
||||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
) -> Pattern:
|
||||
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||
|
||||
def planL(
|
||||
@ -109,167 +41,38 @@ class Tool:
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[Port, Any]:
|
||||
"""
|
||||
Plan a wire or waveguide that travels exactly `length` distance along the axis
|
||||
of its input port.
|
||||
|
||||
Used by `RenderPather`.
|
||||
|
||||
The output port must be exactly `length` away along the input port's axis, but
|
||||
may be placed an additional (unspecified) distance away along the perpendicular
|
||||
direction. The output port should be rotated (or not) based on the value of
|
||||
`ccw`.
|
||||
|
||||
The input and output ports should be compatible with `in_ptype` and
|
||||
`out_ptype`, respectively.
|
||||
|
||||
Args:
|
||||
ccw: If `None`, the output should be along the same axis as the input.
|
||||
Otherwise, cast to bool and turn counterclockwise if True
|
||||
and clockwise otherwise.
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
||||
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
Returns:
|
||||
The calculated output `Port` for the wire.
|
||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||
|
||||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
) -> tuple[float, str]:
|
||||
raise NotImplementedError(f'planL() not implemented for {type(self)}')
|
||||
|
||||
def planS(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[Port, Any]:
|
||||
"""
|
||||
Plan a wire or waveguide that travels exactly `length` distance along the axis
|
||||
of its input port and `jog` distance along the perpendicular axis (i.e. an S-bend).
|
||||
|
||||
Used by `RenderPather`.
|
||||
|
||||
The output port must have an orientation rotated by pi from the input port.
|
||||
|
||||
The input and output ports should be compatible with `in_ptype` and
|
||||
`out_ptype`, respectively.
|
||||
|
||||
Args:
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
jog: The total offset from the input to output, along the perpendicular axis.
|
||||
A positive number implies a rightwards shift (i.e. clockwise bend followed
|
||||
by a counterclockwise bend)
|
||||
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
||||
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
Returns:
|
||||
The calculated output `Port` for the wire.
|
||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||
|
||||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
) -> str: # out_ptype only?
|
||||
raise NotImplementedError(f'planS() not implemented for {type(self)}')
|
||||
|
||||
def planU(
|
||||
self,
|
||||
jog: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[Port, Any]:
|
||||
"""
|
||||
# NOTE: TODO: U-bend is WIP; this interface may change in the future.
|
||||
|
||||
Plan a wire or waveguide that travels exactly `jog` distance along the axis
|
||||
perpendicular to its input port (i.e. a U-bend).
|
||||
|
||||
Used by `RenderPather`.
|
||||
|
||||
The output port must have an orientation identical to the input port.
|
||||
|
||||
The input and output ports should be compatible with `in_ptype` and
|
||||
`out_ptype`, respectively.
|
||||
|
||||
Args:
|
||||
jog: The total offset from the input to output, along the perpendicular axis.
|
||||
A positive number implies a rightwards shift (i.e. clockwise bend followed
|
||||
by a counterclockwise bend)
|
||||
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
||||
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
Returns:
|
||||
The calculated output `Port` for the wire.
|
||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||
|
||||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'planU() not implemented for {type(self)}')
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
batch: Sequence[render_step_t],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'), # noqa: ARG002 (unused)
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
"""
|
||||
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
||||
(a Library with a single topcell).
|
||||
|
||||
Args:
|
||||
batch: A sequence of `RenderStep` objects containing the ports and data
|
||||
provided by this tool's `planL`/`planS`/`planU` functions.
|
||||
port_names: The topcell's input and output ports should be named
|
||||
`port_names[0]` and `port_names[1]` respectively.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
"""
|
||||
assert not batch or batch[0].tool == self
|
||||
assert batch[0][-1] == self
|
||||
raise NotImplementedError(f'render() not implemented for {type(self)}')
|
||||
|
||||
|
||||
abstract_tuple_t = tuple[Abstract, str, str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BasicTool(Tool, metaclass=ABCMeta):
|
||||
"""
|
||||
A simple tool which relies on a single pre-rendered `bend` pattern, a function
|
||||
for generating straight paths, and a table of pre-rendered `transitions` for converting
|
||||
from non-native ptypes.
|
||||
"""
|
||||
straight: tuple[Callable[[float], Pattern], str, str]
|
||||
""" `create_straight(length: float), in_port_name, out_port_name` """
|
||||
|
||||
bend: abstract_tuple_t # Assumed to be clockwise
|
||||
""" `clockwise_bend_abstract, in_port_name, out_port_name` """
|
||||
|
||||
transitions: dict[str, abstract_tuple_t]
|
||||
""" `{ptype: (transition_abstract`, ptype_port_name, other_port_name), ...}` """
|
||||
|
||||
default_out_ptype: str
|
||||
""" Default value for out_ptype """
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LData:
|
||||
""" Data for planL """
|
||||
straight_length: float
|
||||
ccw: SupportsBool | None
|
||||
in_transition: abstract_tuple_t | None
|
||||
out_transition: abstract_tuple_t | None
|
||||
bend: tuple[Abstract, str, str] # Assumed to be clockwise
|
||||
transitions: dict[str, tuple[Abstract, str, str]]
|
||||
|
||||
def path(
|
||||
self,
|
||||
@ -280,63 +83,26 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> Library:
|
||||
_out_port, data = self.planL(
|
||||
ccw,
|
||||
length,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
)
|
||||
) -> Pattern:
|
||||
|
||||
gen_straight, sport_in, sport_out = self.straight
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
pat.add_port_pair(names=port_names)
|
||||
if data.in_transition:
|
||||
ipat, iport_theirs, _iport_ours = data.in_transition
|
||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
||||
if not numpy.isclose(data.straight_length, 0):
|
||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)}
|
||||
pat.plug(straight, {port_names[1]: sport_in})
|
||||
if data.ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
|
||||
if data.out_transition:
|
||||
opat, oport_theirs, oport_ours = data.out_transition
|
||||
pat.plug(opat, {port_names[1]: oport_ours})
|
||||
|
||||
return tree
|
||||
|
||||
def planL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> tuple[Port, LData]:
|
||||
# TODO check all the math for L-shaped bends
|
||||
straight_length = length
|
||||
bend_run = 0
|
||||
if ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
|
||||
angle_in = bend.ports[bport_in].rotation
|
||||
angle_out = bend.ports[bport_out].rotation
|
||||
assert angle_in is not None
|
||||
assert angle_out is not None
|
||||
|
||||
bend_dxy = rotation_matrix_2d(-angle_in) @ (
|
||||
bend.ports[bport_out].offset
|
||||
- bend.ports[bport_in].offset
|
||||
brot = bend.ports[bport_in].rotation
|
||||
assert brot is not None
|
||||
bend_dxy = numpy.abs(
|
||||
rotation_matrix_2d(-brot) @ (
|
||||
bend.ports[bport_out].offset
|
||||
- bend.ports[bport_in].offset
|
||||
)
|
||||
)
|
||||
|
||||
bend_angle = angle_out - angle_in
|
||||
|
||||
if bool(ccw):
|
||||
bend_dxy[1] *= -1
|
||||
bend_angle *= -1
|
||||
straight_length -= bend_dxy[0]
|
||||
bend_run += bend_dxy[1]
|
||||
else:
|
||||
bend_dxy = numpy.zeros(2)
|
||||
bend_angle = 0
|
||||
|
||||
in_transition = self.transitions.get('unk' if in_ptype is None else in_ptype, None)
|
||||
if in_transition is not None:
|
||||
@ -347,6 +113,9 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
ipat.ports[iport_ours].offset
|
||||
- ipat.ports[iport_theirs].offset
|
||||
)
|
||||
|
||||
straight_length -= itrans_dxy[0]
|
||||
bend_run += itrans_dxy[1]
|
||||
else:
|
||||
itrans_dxy = numpy.zeros(2)
|
||||
|
||||
@ -355,199 +124,35 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
opat, oport_theirs, oport_ours = out_transition
|
||||
orot = opat.ports[oport_ours].rotation
|
||||
assert orot is not None
|
||||
|
||||
otrans_dxy = rotation_matrix_2d(-orot + bend_angle) @ (
|
||||
otrans_dxy = rotation_matrix_2d(-orot) @ (
|
||||
opat.ports[oport_theirs].offset
|
||||
- opat.ports[oport_ours].offset
|
||||
)
|
||||
if ccw:
|
||||
otrans_dxy[0] *= -1
|
||||
|
||||
straight_length -= otrans_dxy[1]
|
||||
bend_run += otrans_dxy[0]
|
||||
else:
|
||||
otrans_dxy = numpy.zeros(2)
|
||||
|
||||
if out_transition is not None:
|
||||
out_ptype_actual = opat.ports[oport_theirs].ptype
|
||||
elif ccw is not None:
|
||||
out_ptype_actual = bend.ports[bport_out].ptype
|
||||
else:
|
||||
out_ptype_actual = self.default_out_ptype
|
||||
|
||||
straight_length = length - bend_dxy[0] - itrans_dxy[0] - otrans_dxy[0]
|
||||
bend_run = bend_dxy[1] + itrans_dxy[1] + otrans_dxy[1]
|
||||
|
||||
if straight_length < 0:
|
||||
raise BuildError(
|
||||
f'Asked to draw path with total length {length:,g}, shorter than required bends and transitions:\n'
|
||||
f'bend: {bend_dxy[0]:,g} in_trans: {itrans_dxy[0]:,g} out_trans: {otrans_dxy[0]:,g}'
|
||||
)
|
||||
|
||||
data = self.LData(straight_length, ccw, in_transition, out_transition)
|
||||
out_port = Port((length, bend_run), rotation=bend_angle, ptype=out_ptype_actual)
|
||||
return out_port, data
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
append: bool = True,
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
||||
|
||||
gen_straight, sport_in, _sport_out = self.straight
|
||||
for step in batch:
|
||||
straight_length, ccw, in_transition, out_transition = step.data
|
||||
assert step.tool == self
|
||||
|
||||
if step.opcode == 'L':
|
||||
if in_transition:
|
||||
ipat, iport_theirs, _iport_ours = in_transition
|
||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
||||
if not numpy.isclose(straight_length, 0):
|
||||
straight_pat = gen_straight(straight_length, **kwargs)
|
||||
if append:
|
||||
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
|
||||
else:
|
||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat}
|
||||
pat.plug(straight, {port_names[1]: sport_in}, append=True)
|
||||
if ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
|
||||
if out_transition:
|
||||
opat, oport_theirs, oport_ours = out_transition
|
||||
pat.plug(opat, {port_names[1]: oport_ours})
|
||||
return tree
|
||||
|
||||
|
||||
@dataclass
|
||||
class PathTool(Tool, metaclass=ABCMeta):
|
||||
"""
|
||||
A tool which draws `Path` geometry elements.
|
||||
|
||||
If `planL` / `render` are used, the `Path` elements can cover >2 vertices;
|
||||
with `path` only individual rectangles will be drawn.
|
||||
"""
|
||||
layer: layer_t
|
||||
""" Layer to draw on """
|
||||
|
||||
width: float
|
||||
""" `Path` width """
|
||||
|
||||
ptype: str = 'unk'
|
||||
""" ptype for any ports in patterns generated by this tool """
|
||||
|
||||
#@dataclass(frozen=True, slots=True)
|
||||
#class LData:
|
||||
# dxy: NDArray[numpy.float64]
|
||||
|
||||
#def __init__(self, layer: layer_t, width: float, ptype: str = 'unk') -> None:
|
||||
# Tool.__init__(self)
|
||||
# self.layer = layer
|
||||
# self.width = width
|
||||
# self.ptype: str
|
||||
|
||||
def path(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> Library:
|
||||
out_port, dxy = self.planL(
|
||||
ccw,
|
||||
length,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
|
||||
|
||||
if ccw is None:
|
||||
out_rot = pi
|
||||
elif bool(ccw):
|
||||
out_rot = -pi / 2
|
||||
else:
|
||||
out_rot = pi / 2
|
||||
|
||||
pat.ports = {
|
||||
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
|
||||
port_names[1]: Port(dxy, rotation=out_rot, ptype=self.ptype),
|
||||
}
|
||||
|
||||
return tree
|
||||
|
||||
def planL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||
out_ptype: str | None = None,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> tuple[Port, NDArray[numpy.float64]]:
|
||||
# TODO check all the math for L-shaped bends
|
||||
|
||||
if out_ptype and out_ptype != self.ptype:
|
||||
raise BuildError(f'Requested {out_ptype=} does not match path ptype {self.ptype}')
|
||||
raise BuildError(f'Asked to draw path with total length {length:g}, shorter than required bends and tapers:\n'
|
||||
f'bend: {bend_dxy[0]:g} in_taper: {abs(itrans_dxy[0])} out_taper: {otrans_dxy[1]}')
|
||||
|
||||
gen_straight, sport_in, sport_out = self.straight
|
||||
tree = Library()
|
||||
bb = Builder(library=tree, name='_path').add_port_pair(names=port_names)
|
||||
if in_transition:
|
||||
bb.plug(ipat, {port_names[1]: iport_theirs})
|
||||
if not numpy.isclose(straight_length, 0):
|
||||
straight = tree << {'_straight': gen_straight(straight_length)}
|
||||
bb.plug(straight, {port_names[1]: sport_in})
|
||||
if ccw is not None:
|
||||
bend_dxy = numpy.array([1, -1]) * self.width / 2
|
||||
bend_angle = pi / 2
|
||||
bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw)))
|
||||
if out_transition:
|
||||
bb.plug(opat, {port_names[1]: oport_ours})
|
||||
|
||||
if bool(ccw):
|
||||
bend_dxy[1] *= -1
|
||||
bend_angle *= -1
|
||||
else:
|
||||
bend_dxy = numpy.zeros(2)
|
||||
bend_angle = pi
|
||||
|
||||
straight_length = length - bend_dxy[0]
|
||||
bend_run = bend_dxy[1]
|
||||
return bb.pattern
|
||||
|
||||
if straight_length < 0:
|
||||
raise BuildError(
|
||||
f'Asked to draw path with total length {length:,g}, shorter than required bend: {bend_dxy[0]:,g}'
|
||||
)
|
||||
data = numpy.array((length, bend_run))
|
||||
out_port = Port(data, rotation=bend_angle, ptype=self.ptype)
|
||||
return out_port, data
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
|
||||
path_vertices = [batch[0].start_port.offset]
|
||||
for step in batch:
|
||||
assert step.tool == self
|
||||
|
||||
port_rot = step.start_port.rotation
|
||||
assert port_rot is not None
|
||||
|
||||
if step.opcode == 'L':
|
||||
length, bend_run = step.data
|
||||
dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0)
|
||||
#path_vertices.append(step.start_port.offset)
|
||||
path_vertices.append(step.start_port.offset + dxy)
|
||||
else:
|
||||
raise BuildError(f'Unrecognized opcode "{step.opcode}"')
|
||||
|
||||
if (path_vertices[-1] != batch[-1].end_port.offset).any():
|
||||
# If the path ends in a bend, we need to add the final vertex
|
||||
path_vertices.append(batch[-1].end_port.offset)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
||||
pat.ports = {
|
||||
port_names[0]: batch[0].start_port.copy().rotate(pi),
|
||||
port_names[1]: batch[-1].end_port.copy().rotate(pi),
|
||||
}
|
||||
return tree
|
||||
|
@ -1,5 +1,4 @@
|
||||
from typing import SupportsFloat, cast, TYPE_CHECKING
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Mapping, Sequence, SupportsFloat, cast, TYPE_CHECKING
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
@ -54,9 +53,9 @@ def ell(
|
||||
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
||||
- 'max_extension' or 'emax':
|
||||
The total extension value for the closest-in port (C in the diagram).
|
||||
- 'min_position', 'pmin', 'xmin', 'ymin':
|
||||
- 'min_position' or 'pmin':
|
||||
The coordinate of the innermost bend (D's bend).
|
||||
- 'max_position', 'pmax', 'xmax', 'ymax':
|
||||
- 'max_position' or 'pmax':
|
||||
The coordinate of the outermost bend (A's bend).
|
||||
|
||||
`bound` can also be a vector. If specifying an extension (e.g. 'min_extension',
|
||||
@ -110,12 +109,6 @@ def ell(
|
||||
raise BuildError('set_rotation must be specified if no ports have rotations!')
|
||||
rotations = numpy.full_like(has_rotation, set_rotation, dtype=float)
|
||||
|
||||
is_horizontal = numpy.isclose(rotations[0] % pi, 0)
|
||||
if bound_type in ('ymin', 'ymax') and is_horizontal:
|
||||
raise BuildError(f'Asked for {bound_type} position but ports are pointing along the x-axis!')
|
||||
if bound_type in ('xmin', 'xmax') and not is_horizontal:
|
||||
raise BuildError(f'Asked for {bound_type} position but ports are pointing along the y-axis!')
|
||||
|
||||
direction = rotations[0] + pi # direction we want to travel in (+pi relative to port)
|
||||
rot_matrix = rotation_matrix_2d(-direction)
|
||||
|
||||
@ -123,8 +116,6 @@ def ell(
|
||||
orig_offsets = numpy.array([p.offset for p in ports.values()])
|
||||
rot_offsets = (rot_matrix @ orig_offsets.T).T
|
||||
|
||||
# ordering_base = rot_offsets.T * [[1], [-1 if ccw else 1]] # could work, but this is actually a more complex routing problem
|
||||
# y_order = numpy.lexsort(ordering_base) # (need to make sure we don't collide with the next input port @ same y)
|
||||
y_order = ((-1 if ccw else 1) * rot_offsets[:, 1]).argsort(kind='stable')
|
||||
y_ind = numpy.empty_like(y_order, dtype=int)
|
||||
y_ind[y_order] = numpy.arange(y_ind.shape[0])
|
||||
@ -193,16 +184,16 @@ def ell(
|
||||
rot_bound = -bound if neg else bound
|
||||
|
||||
min_possible = x_start + offsets
|
||||
if bound_type in ('pmax', 'max_position', 'xmax', 'ymax'):
|
||||
if bound_type in ('pmax', 'max_position'):
|
||||
extension = rot_bound - min_possible.max()
|
||||
elif bound_type in ('pmin', 'min_position', 'xmin', 'ymin'):
|
||||
elif bound_type in ('pmin', 'min_position'):
|
||||
extension = rot_bound - min_possible.min()
|
||||
|
||||
offsets += extension
|
||||
if extension < 0:
|
||||
ext_floor = -numpy.floor(extension)
|
||||
raise BuildError(f'Position is too close by at least {ext_floor}. Total extensions would be\n\t'
|
||||
+ '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets, strict=True)))
|
||||
+ '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets)))
|
||||
|
||||
result = dict(zip(ports.keys(), offsets, strict=True))
|
||||
result = dict(zip(ports.keys(), offsets))
|
||||
return result
|
||||
|
@ -6,8 +6,7 @@ Notes:
|
||||
* ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID
|
||||
to unique values, so byte-for-byte reproducibility is not achievable for now
|
||||
"""
|
||||
from typing import Any, cast, TextIO, IO
|
||||
from collections.abc import Mapping, Callable
|
||||
from typing import Any, Callable, Mapping, cast, TextIO, IO
|
||||
import io
|
||||
import logging
|
||||
import pathlib
|
||||
@ -16,14 +15,13 @@ import gzip
|
||||
import numpy
|
||||
import ezdxf
|
||||
from ezdxf.enums import TextEntityAlignment
|
||||
from ezdxf.entities import LWPolyline, Polyline, Text, Insert
|
||||
|
||||
from .utils import is_gzipped, tmpfile
|
||||
from .. import Pattern, Ref, PatternError, Label
|
||||
from ..library import ILibraryView, LibraryView, Library
|
||||
from ..shapes import Shape, Polygon, Path
|
||||
from ..repetition import Grid
|
||||
from ..utils import rotation_matrix_2d, layer_t, normalize_mirror
|
||||
from ..utils import rotation_matrix_2d, layer_t
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -40,7 +38,7 @@ def write(
|
||||
top_name: str,
|
||||
stream: TextIO,
|
||||
*,
|
||||
dxf_version: str = 'AC1024',
|
||||
dxf_version='AC1024',
|
||||
) -> None:
|
||||
"""
|
||||
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
||||
@ -206,25 +204,26 @@ def read(
|
||||
return mlib, library_info
|
||||
|
||||
|
||||
def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> tuple[str, Pattern]:
|
||||
def _read_block(block) -> tuple[str, Pattern]:
|
||||
name = block.name
|
||||
pat = Pattern()
|
||||
for element in block:
|
||||
if isinstance(element, LWPolyline | Polyline):
|
||||
if isinstance(element, LWPolyline):
|
||||
points = numpy.asarray(element.get_points())
|
||||
elif isinstance(element, Polyline):
|
||||
points = numpy.asarray(element.points())[:, :2]
|
||||
eltype = element.dxftype()
|
||||
if eltype in ('POLYLINE', 'LWPOLYLINE'):
|
||||
if eltype == 'LWPOLYLINE':
|
||||
points = numpy.array(tuple(element.lwpoints))
|
||||
else:
|
||||
points = numpy.array(tuple(element.points()))
|
||||
attr = element.dxfattribs()
|
||||
layer = attr.get('layer', DEFAULT_LAYER)
|
||||
|
||||
if points.shape[1] == 2:
|
||||
raise PatternError('Invalid or unimplemented polygon?')
|
||||
|
||||
if points.shape[1] > 2:
|
||||
#shape = Polygon(layer=layer)
|
||||
elif points.shape[1] > 2:
|
||||
if (points[0, 2] != points[:, 2]).any():
|
||||
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)')
|
||||
if points.shape[1] == 4 and (points[:, 3] != 0).any():
|
||||
elif points.shape[1] == 4 and (points[:, 3] != 0).any():
|
||||
raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
|
||||
|
||||
width = points[0, 2]
|
||||
@ -233,15 +232,15 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
|
||||
|
||||
shape: Path | Polygon
|
||||
if width == 0 and len(points) > 2 and numpy.array_equal(points[0], points[-1]):
|
||||
shape = Polygon(vertices=points[:-1, :2])
|
||||
shape = Polygon(layer=layer, vertices=points[:-1, :2])
|
||||
else:
|
||||
shape = Path(width=width, vertices=points[:, :2])
|
||||
shape = Path(layer=layer, width=width, vertices=points[:, :2])
|
||||
|
||||
pat.shapes[layer].append(shape)
|
||||
pat.shapes.append(shape)
|
||||
|
||||
elif isinstance(element, Text):
|
||||
elif eltype in ('TEXT',):
|
||||
args = dict(
|
||||
offset=numpy.asarray(element.get_placement()[1])[:2],
|
||||
offset=numpy.array(element.get_pos()[1])[:2],
|
||||
layer=element.dxfattribs().get('layer', DEFAULT_LAYER),
|
||||
)
|
||||
string = element.dxfattribs().get('text', '')
|
||||
@ -249,20 +248,20 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
|
||||
# if height != 0:
|
||||
# logger.warning('Interpreting DXF TEXT as a label despite nonzero height. '
|
||||
# 'This could be changed in the future by setting a font path in the masque DXF code.')
|
||||
pat.label(string=string, **args)
|
||||
pat.labels.append(Label(string=string, **args))
|
||||
# else:
|
||||
# pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????))
|
||||
elif isinstance(element, Insert):
|
||||
# pat.shapes.append(Text(string=string, height=height, font_path=????))
|
||||
elif eltype in ('INSERT',):
|
||||
attr = element.dxfattribs()
|
||||
xscale = attr.get('xscale', 1)
|
||||
yscale = attr.get('yscale', 1)
|
||||
if abs(xscale) != abs(yscale):
|
||||
logger.warning('Masque does not support per-axis scaling; using x-scaling only!')
|
||||
scale = abs(xscale)
|
||||
mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0))
|
||||
rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle
|
||||
mirrored = (yscale < 0, xscale < 0)
|
||||
rotation = numpy.deg2rad(attr.get('rotation', 0))
|
||||
|
||||
offset = numpy.asarray(attr.get('insert', (0, 0, 0)))[:2]
|
||||
offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2]
|
||||
|
||||
args = dict(
|
||||
target=attr.get('name', None),
|
||||
@ -287,13 +286,17 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
|
||||
|
||||
def _mrefs_to_drefs(
|
||||
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
||||
refs: dict[str | None, list[Ref]],
|
||||
refs: list[Ref],
|
||||
) -> None:
|
||||
def mk_blockref(encoded_name: str, ref: Ref) -> None:
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
encoded_name = ref.target
|
||||
|
||||
rotation = numpy.rad2deg(ref.rotation) % 360
|
||||
attribs = dict(
|
||||
xscale=ref.scale,
|
||||
yscale=ref.scale * (-1 if ref.mirrored else 1),
|
||||
xscale=ref.scale * (-1 if ref.mirrored[1] else 1),
|
||||
yscale=ref.scale * (-1 if ref.mirrored[0] else 1),
|
||||
rotation=rotation,
|
||||
)
|
||||
|
||||
@ -327,47 +330,36 @@ def _mrefs_to_drefs(
|
||||
for dd in rep.displacements:
|
||||
block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs)
|
||||
|
||||
for target, rseq in refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
for ref in rseq:
|
||||
mk_blockref(target, ref)
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
||||
shapes: dict[layer_t, list[Shape]],
|
||||
shapes: list[Shape],
|
||||
polygonize_paths: bool = False,
|
||||
) -> None:
|
||||
# Add `LWPolyline`s for each shape.
|
||||
# Could set do paths with width setting, but need to consider endcaps.
|
||||
# TODO: can DXF do paths?
|
||||
for layer, sseq in shapes.items():
|
||||
attribs = dict(layer=_mlayer2dxf(layer))
|
||||
for shape in sseq:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError(
|
||||
'Shape repetitions are not supported by DXF.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.'
|
||||
)
|
||||
for shape in shapes:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError(
|
||||
'Shape repetitions are not supported by DXF.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.'
|
||||
)
|
||||
|
||||
for polygon in shape.to_polygons():
|
||||
xy_open = polygon.vertices + polygon.offset
|
||||
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
||||
attribs = dict(layer=_mlayer2dxf(shape.layer))
|
||||
for polygon in shape.to_polygons():
|
||||
xy_open = polygon.vertices + polygon.offset
|
||||
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
||||
|
||||
|
||||
def _labels_to_texts(
|
||||
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
||||
labels: dict[layer_t, list[Label]],
|
||||
labels: list[Label],
|
||||
) -> None:
|
||||
for layer, lseq in labels.items():
|
||||
attribs = dict(layer=_mlayer2dxf(layer))
|
||||
for label in lseq:
|
||||
xy = label.offset
|
||||
block.add_text(
|
||||
label.string,
|
||||
dxfattribs=attribs
|
||||
).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||
for label in labels:
|
||||
attribs = dict(layer=_mlayer2dxf(label.layer))
|
||||
xy = label.offset
|
||||
block.add_text(label.string, dxfattribs=attribs).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||
|
||||
|
||||
def _mlayer2dxf(layer: layer_t) -> str:
|
||||
|
@ -19,8 +19,7 @@ Notes:
|
||||
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility.
|
||||
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
|
||||
"""
|
||||
from typing import IO, cast, Any
|
||||
from collections.abc import Iterable, Mapping, Callable
|
||||
from typing import Callable, Iterable, Mapping, IO, cast, Any
|
||||
import io
|
||||
import mmap
|
||||
import logging
|
||||
@ -38,7 +37,7 @@ from .utils import is_gzipped, tmpfile
|
||||
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
|
||||
from ..shapes import Polygon, Path
|
||||
from ..repetition import Grid
|
||||
from ..utils import layer_t, annotations_t
|
||||
from ..utils import layer_t, normalize_mirror, annotations_t
|
||||
from ..library import LazyLibrary, Library, ILibrary, ILibraryView
|
||||
|
||||
|
||||
@ -145,7 +144,7 @@ def writefile(
|
||||
with tmpfile(path) as base_stream:
|
||||
streams: tuple[Any, ...] = (base_stream,)
|
||||
if path.suffix == '.gz':
|
||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||
streams = (stream,) + streams
|
||||
else:
|
||||
stream = base_stream
|
||||
@ -254,21 +253,21 @@ def read_elements(
|
||||
elements = klamath.library.read_elements(stream)
|
||||
for element in elements:
|
||||
if isinstance(element, klamath.elements.Boundary):
|
||||
layer, poly = _boundary_to_polygon(element, raw_mode)
|
||||
pat.shapes[layer].append(poly)
|
||||
poly = _boundary_to_polygon(element, raw_mode)
|
||||
pat.shapes.append(poly)
|
||||
elif isinstance(element, klamath.elements.Path):
|
||||
layer, path = _gpath_to_mpath(element, raw_mode)
|
||||
pat.shapes[layer].append(path)
|
||||
path = _gpath_to_mpath(element, raw_mode)
|
||||
pat.shapes.append(path)
|
||||
elif isinstance(element, klamath.elements.Text):
|
||||
pat.label(
|
||||
layer=element.layer,
|
||||
label = Label(
|
||||
offset=element.xy.astype(float),
|
||||
layer=element.layer,
|
||||
string=element.string.decode('ASCII'),
|
||||
annotations=_properties_to_annotations(element.properties),
|
||||
)
|
||||
pat.labels.append(label)
|
||||
elif isinstance(element, klamath.elements.Reference):
|
||||
target, ref = _gref_to_mref(element)
|
||||
pat.refs[target].append(ref)
|
||||
pat.refs.append(_gref_to_mref(element))
|
||||
return pat
|
||||
|
||||
|
||||
@ -288,7 +287,7 @@ def _mlayer2gds(mlayer: layer_t) -> tuple[int, int]:
|
||||
return layer, data_type
|
||||
|
||||
|
||||
def _gref_to_mref(ref: klamath.library.Reference) -> tuple[str, Ref]:
|
||||
def _gref_to_mref(ref: klamath.library.Reference) -> Ref:
|
||||
"""
|
||||
Helper function to create a Ref from an SREF or AREF. Sets ref.target to struct_name.
|
||||
"""
|
||||
@ -302,19 +301,19 @@ def _gref_to_mref(ref: klamath.library.Reference) -> tuple[str, Ref]:
|
||||
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
||||
a_count=a_count, b_count=b_count)
|
||||
|
||||
target = ref.struct_name.decode('ASCII')
|
||||
mref = Ref(
|
||||
target=ref.struct_name.decode('ASCII'),
|
||||
offset=offset,
|
||||
rotation=numpy.deg2rad(ref.angle_deg),
|
||||
scale=ref.mag,
|
||||
mirrored=ref.invert_y,
|
||||
mirrored=(ref.invert_y, False),
|
||||
annotations=_properties_to_annotations(ref.properties),
|
||||
repetition=repetition,
|
||||
)
|
||||
return target, mref
|
||||
return mref
|
||||
|
||||
|
||||
def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> tuple[layer_t, Path]:
|
||||
def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
||||
if gpath.path_type in path_cap_map:
|
||||
cap = path_cap_map[gpath.path_type]
|
||||
else:
|
||||
@ -322,6 +321,7 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> tuple[layer_
|
||||
|
||||
mpath = Path(
|
||||
vertices=gpath.xy.astype(float),
|
||||
layer=gpath.layer,
|
||||
width=gpath.width,
|
||||
cap=cap,
|
||||
offset=numpy.zeros(2),
|
||||
@ -330,72 +330,74 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> tuple[layer_
|
||||
)
|
||||
if cap == Path.Cap.SquareCustom:
|
||||
mpath.cap_extensions = gpath.extension
|
||||
return gpath.layer, mpath
|
||||
return mpath
|
||||
|
||||
|
||||
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> tuple[layer_t, Polygon]:
|
||||
return boundary.layer, Polygon(
|
||||
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon:
|
||||
return Polygon(
|
||||
vertices=boundary.xy[:-1].astype(float),
|
||||
layer=boundary.layer,
|
||||
offset=numpy.zeros(2),
|
||||
annotations=_properties_to_annotations(boundary.properties),
|
||||
raw=raw_mode,
|
||||
)
|
||||
|
||||
|
||||
def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.Reference]:
|
||||
def _mrefs_to_grefs(refs: list[Ref]) -> list[klamath.library.Reference]:
|
||||
grefs = []
|
||||
for target, rseq in refs.items():
|
||||
if target is None:
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
encoded_name = target.encode('ASCII')
|
||||
for ref in rseq:
|
||||
# Note: GDS also mirrors first and rotates second
|
||||
rep = ref.repetition
|
||||
angle_deg = numpy.rad2deg(ref.rotation) % 360
|
||||
properties = _annotations_to_properties(ref.annotations, 512)
|
||||
encoded_name = ref.target.encode('ASCII')
|
||||
|
||||
if isinstance(rep, Grid):
|
||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
b_count = rep.b_count if rep.b_count is not None else 1
|
||||
xy = numpy.asarray(ref.offset) + numpy.array([
|
||||
[0.0, 0.0],
|
||||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
])
|
||||
aref = klamath.library.Reference(
|
||||
# Note: GDS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
rep = ref.repetition
|
||||
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
properties = _annotations_to_properties(ref.annotations, 512)
|
||||
|
||||
if isinstance(rep, Grid):
|
||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
b_count = rep.b_count if rep.b_count is not None else 1
|
||||
xy = numpy.array(ref.offset) + numpy.array([
|
||||
[0.0, 0.0],
|
||||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
])
|
||||
aref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast(xy),
|
||||
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
grefs.append(aref)
|
||||
elif rep is None:
|
||||
sref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([ref.offset]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
grefs.append(sref)
|
||||
else:
|
||||
new_srefs = [
|
||||
klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast(xy),
|
||||
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
||||
angle_deg=angle_deg,
|
||||
invert_y=ref.mirrored,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
grefs.append(aref)
|
||||
elif rep is None:
|
||||
sref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([ref.offset]),
|
||||
xy=rint_cast([ref.offset + dd]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=ref.mirrored,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
grefs.append(sref)
|
||||
else:
|
||||
new_srefs = [
|
||||
klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([ref.offset + dd]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=ref.mirrored,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
for dd in rep.displacements]
|
||||
grefs += new_srefs
|
||||
for dd in rep.displacements]
|
||||
grefs += new_srefs
|
||||
return grefs
|
||||
|
||||
|
||||
@ -409,8 +411,8 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
|
||||
for key, vals in annotations.items():
|
||||
try:
|
||||
i = int(key)
|
||||
except ValueError as err:
|
||||
raise PatternError(f'Annotation key {key} is not convertable to an integer') from err
|
||||
except ValueError:
|
||||
raise PatternError(f'Annotation key {key} is not convertable to an integer')
|
||||
if not (0 < i < 126):
|
||||
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
|
||||
|
||||
@ -426,41 +428,51 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
shapes: dict[layer_t, list[Shape]],
|
||||
shapes: list[Shape],
|
||||
polygonize_paths: bool = False,
|
||||
) -> list[klamath.elements.Element]:
|
||||
elements: list[klamath.elements.Element] = []
|
||||
# Add a Boundary element for each shape, and Path elements if necessary
|
||||
for mlayer, sseq in shapes.items():
|
||||
layer, data_type = _mlayer2gds(mlayer)
|
||||
for shape in sseq:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError('Shape repetitions are not supported by GDS.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.')
|
||||
for shape in shapes:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError('Shape repetitions are not supported by GDS.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.')
|
||||
|
||||
properties = _annotations_to_properties(shape.annotations, 128)
|
||||
if isinstance(shape, Path) and not polygonize_paths:
|
||||
xy = rint_cast(shape.vertices + shape.offset)
|
||||
width = rint_cast(shape.width)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
layer, data_type = _mlayer2gds(shape.layer)
|
||||
properties = _annotations_to_properties(shape.annotations, 128)
|
||||
if isinstance(shape, Path) and not polygonize_paths:
|
||||
xy = rint_cast(shape.vertices + shape.offset)
|
||||
width = rint_cast(shape.width)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
|
||||
extension: tuple[int, int]
|
||||
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None:
|
||||
extension = tuple(shape.cap_extensions) # type: ignore
|
||||
else:
|
||||
extension = (0, 0)
|
||||
extension: tuple[int, int]
|
||||
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None:
|
||||
extension = tuple(shape.cap_extensions) # type: ignore
|
||||
else:
|
||||
extension = (0, 0)
|
||||
|
||||
path = klamath.elements.Path(
|
||||
layer=(layer, data_type),
|
||||
xy=xy,
|
||||
path_type=path_type,
|
||||
width=int(width),
|
||||
extension=extension,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(path)
|
||||
elif isinstance(shape, Polygon):
|
||||
polygon = shape
|
||||
path = klamath.elements.Path(
|
||||
layer=(layer, data_type),
|
||||
xy=xy,
|
||||
path_type=path_type,
|
||||
width=int(width),
|
||||
extension=extension,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(path)
|
||||
elif isinstance(shape, Polygon):
|
||||
polygon = shape
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
boundary = klamath.elements.Boundary(
|
||||
layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
@ -470,40 +482,28 @@ def _shapes_to_elements(
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
boundary = klamath.elements.Boundary(
|
||||
layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
return elements
|
||||
|
||||
|
||||
def _labels_to_texts(labels: dict[layer_t, list[Label]]) -> list[klamath.elements.Text]:
|
||||
def _labels_to_texts(labels: list[Label]) -> list[klamath.elements.Text]:
|
||||
texts = []
|
||||
for mlayer, lseq in labels.items():
|
||||
layer, text_type = _mlayer2gds(mlayer)
|
||||
for label in lseq:
|
||||
properties = _annotations_to_properties(label.annotations, 128)
|
||||
xy = rint_cast([label.offset])
|
||||
text = klamath.elements.Text(
|
||||
layer=(layer, text_type),
|
||||
xy=xy,
|
||||
string=label.string.encode('ASCII'),
|
||||
properties=properties,
|
||||
presentation=0, # font number & alignment -- unused by us
|
||||
angle_deg=0, # rotation -- unused by us
|
||||
invert_y=False, # inversion -- unused by us
|
||||
width=0, # stroke width -- unused by us
|
||||
path_type=0, # text path endcaps, unused
|
||||
mag=1, # size -- unused by us
|
||||
)
|
||||
texts.append(text)
|
||||
for label in labels:
|
||||
properties = _annotations_to_properties(label.annotations, 128)
|
||||
layer, text_type = _mlayer2gds(label.layer)
|
||||
xy = rint_cast([label.offset])
|
||||
text = klamath.elements.Text(
|
||||
layer=(layer, text_type),
|
||||
xy=xy,
|
||||
string=label.string.encode('ASCII'),
|
||||
properties=properties,
|
||||
presentation=0, # TODO maybe set some of these?
|
||||
angle_deg=0,
|
||||
invert_y=False,
|
||||
width=0,
|
||||
path_type=0,
|
||||
mag=1,
|
||||
)
|
||||
texts.append(text)
|
||||
return texts
|
||||
|
||||
|
||||
@ -597,19 +597,19 @@ def load_libraryfile(
|
||||
path = pathlib.Path(filename)
|
||||
stream: IO[bytes]
|
||||
if is_gzipped(path):
|
||||
if use_mmap:
|
||||
if mmap:
|
||||
logger.info('Asked to mmap a gzipped file, reading into memory instead...')
|
||||
gz_stream = gzip.open(path, mode='rb') # noqa: SIM115
|
||||
gz_stream = gzip.open(path, mode='rb')
|
||||
stream = io.BytesIO(gz_stream.read()) # type: ignore
|
||||
else:
|
||||
gz_stream = gzip.open(path, mode='rb') # noqa: SIM115
|
||||
gz_stream = gzip.open(path, mode='rb')
|
||||
stream = io.BufferedReader(gz_stream) # type: ignore
|
||||
else: # noqa: PLR5501
|
||||
if use_mmap:
|
||||
base_stream = path.open(mode='rb', buffering=0) # noqa: SIM115
|
||||
else:
|
||||
if mmap:
|
||||
base_stream = open(path, mode='rb', buffering=0)
|
||||
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
|
||||
else:
|
||||
stream = path.open(mode='rb') # noqa: SIM115
|
||||
stream = open(path, mode='rb')
|
||||
return load_library(stream, full_load=full_load, postprocess=postprocess)
|
||||
|
||||
|
||||
|
@ -14,8 +14,7 @@ Note that OASIS references follow the same convention as `masque`,
|
||||
Notes:
|
||||
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
|
||||
"""
|
||||
from typing import Any, IO, cast
|
||||
from collections.abc import Sequence, Iterable, Mapping, Callable
|
||||
from typing import Any, Callable, Iterable, IO, Mapping, cast, Sequence
|
||||
import logging
|
||||
import pathlib
|
||||
import gzip
|
||||
@ -31,9 +30,9 @@ from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringR
|
||||
from .utils import is_gzipped, tmpfile
|
||||
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
|
||||
from ..library import Library, ILibrary
|
||||
from ..shapes import Path, Circle
|
||||
from ..shapes import Polygon, Path, Circle
|
||||
from ..repetition import Grid, Arbitrary, Repetition
|
||||
from ..utils import layer_t, annotations_t
|
||||
from ..utils import layer_t, normalize_mirror, annotations_t
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -285,20 +284,23 @@ def read(
|
||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list()[:-1])), axis=0)
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.polygon(
|
||||
poly = Polygon(
|
||||
vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
annotations=annotations,
|
||||
repetition=repetition,
|
||||
)
|
||||
|
||||
pat.shapes.append(poly)
|
||||
|
||||
elif isinstance(element, fatrec.Path):
|
||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0)
|
||||
|
||||
cap_start = path_cap_map[element.get_extension_start()[0]]
|
||||
cap_end = path_cap_map[element.get_extension_end()[0]]
|
||||
if cap_start != cap_end:
|
||||
raise PatternError('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types
|
||||
raise Exception('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types
|
||||
cap = cap_start
|
||||
|
||||
path_args: dict[str, Any] = {}
|
||||
@ -309,7 +311,7 @@ def read(
|
||||
))
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.path(
|
||||
path = Path(
|
||||
vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
@ -320,17 +322,20 @@ def read(
|
||||
**path_args,
|
||||
)
|
||||
|
||||
pat.shapes.append(path)
|
||||
|
||||
elif isinstance(element, fatrec.Rectangle):
|
||||
width = element.get_width()
|
||||
height = element.get_height()
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.polygon(
|
||||
rect = Polygon(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
|
||||
annotations=annotations,
|
||||
)
|
||||
pat.shapes.append(rect)
|
||||
|
||||
elif isinstance(element, fatrec.Trapezoid):
|
||||
vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (element.get_width(), element.get_height())
|
||||
@ -358,13 +363,14 @@ def read(
|
||||
vertices[2, 0] -= b
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.polygon(
|
||||
trapz = Polygon(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
vertices=vertices,
|
||||
annotations=annotations,
|
||||
)
|
||||
pat.shapes.append(trapz)
|
||||
|
||||
elif isinstance(element, fatrec.CTrapezoid):
|
||||
cttype = element.get_ctrapezoid_type()
|
||||
@ -413,24 +419,25 @@ def read(
|
||||
vertices[0, 1] += width
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.polygon(
|
||||
ctrapz = Polygon(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
vertices=vertices,
|
||||
annotations=annotations,
|
||||
)
|
||||
pat.shapes.append(ctrapz)
|
||||
|
||||
elif isinstance(element, fatrec.Circle):
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
layer = element.get_layer_tuple()
|
||||
circle = Circle(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
annotations=annotations,
|
||||
radius=float(element.get_radius()),
|
||||
)
|
||||
pat.shapes[layer].append(circle)
|
||||
pat.shapes.append(circle)
|
||||
|
||||
elif isinstance(element, fatrec.Text):
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
@ -439,23 +446,21 @@ def read(
|
||||
string = lib.textstrings[str_or_ref].string
|
||||
else:
|
||||
string = str_or_ref.string
|
||||
pat.label(
|
||||
label = Label(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
annotations=annotations,
|
||||
string=string,
|
||||
)
|
||||
pat.labels.append(label)
|
||||
|
||||
else:
|
||||
logger.warning(f'Skipping record {element} (unimplemented)')
|
||||
continue
|
||||
|
||||
for placement in cell.placements:
|
||||
target, ref = _placement_to_ref(placement, lib)
|
||||
if isinstance(target, int):
|
||||
target = lib.cellnames[target].nstring.string
|
||||
pat.refs[target].append(ref)
|
||||
pat.refs.append(_placement_to_ref(placement, lib))
|
||||
|
||||
mlib[cell_name] = pat
|
||||
|
||||
@ -479,9 +484,9 @@ def _mlayer2oas(mlayer: layer_t) -> tuple[int, int]:
|
||||
return layer, data_type
|
||||
|
||||
|
||||
def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> tuple[int | str, Ref]:
|
||||
def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> Ref:
|
||||
"""
|
||||
Helper function to create a Ref from a placment. Also returns the placement name (or id).
|
||||
Helper function to create a Ref from a placment. Sets ref.target to the placement name.
|
||||
"""
|
||||
assert not isinstance(placement.repetition, fatamorgana.ReuseRepetition)
|
||||
xy = numpy.array((placement.x, placement.y))
|
||||
@ -496,124 +501,124 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout)
|
||||
else:
|
||||
rotation = numpy.deg2rad(float(placement.angle))
|
||||
ref = Ref(
|
||||
target=name,
|
||||
offset=xy,
|
||||
mirrored=placement.flip,
|
||||
mirrored=(placement.flip, False),
|
||||
rotation=rotation,
|
||||
scale=float(mag),
|
||||
repetition=repetition_fata2masq(placement.repetition),
|
||||
annotations=annotations,
|
||||
)
|
||||
return name, ref
|
||||
return ref
|
||||
|
||||
|
||||
def _refs_to_placements(
|
||||
refs: dict[str | None, list[Ref]],
|
||||
refs: list[Ref],
|
||||
) -> list[fatrec.Placement]:
|
||||
placements = []
|
||||
for target, rseq in refs.items():
|
||||
if target is None:
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
for ref in rseq:
|
||||
# Note: OASIS also mirrors first and rotates second
|
||||
frep, rep_offset = repetition_masq2fata(ref.repetition)
|
||||
|
||||
offset = rint_cast(ref.offset + rep_offset)
|
||||
angle = numpy.rad2deg(ref.rotation) % 360
|
||||
placement = fatrec.Placement(
|
||||
name=target,
|
||||
flip=ref.mirrored,
|
||||
angle=angle,
|
||||
magnification=ref.scale,
|
||||
properties=annotations_to_properties(ref.annotations),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
repetition=frep,
|
||||
)
|
||||
# Note: OASIS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
frep, rep_offset = repetition_masq2fata(ref.repetition)
|
||||
|
||||
placements.append(placement)
|
||||
offset = rint_cast(ref.offset + rep_offset)
|
||||
angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
placement = fatrec.Placement(
|
||||
name=ref.target,
|
||||
flip=mirror_across_x,
|
||||
angle=angle,
|
||||
magnification=ref.scale,
|
||||
properties=annotations_to_properties(ref.annotations),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
repetition=frep,
|
||||
)
|
||||
|
||||
placements.append(placement)
|
||||
return placements
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
shapes: dict[layer_t, list[Shape]],
|
||||
shapes: list[Shape],
|
||||
layer2oas: Callable[[layer_t], tuple[int, int]],
|
||||
) -> list[fatrec.Polygon | fatrec.Path | fatrec.Circle]:
|
||||
# Add a Polygon record for each shape, and Path elements if necessary
|
||||
elements: list[fatrec.Polygon | fatrec.Path | fatrec.Circle] = []
|
||||
for mlayer, sseq in shapes.items():
|
||||
layer, datatype = layer2oas(mlayer)
|
||||
for shape in sseq:
|
||||
repetition, rep_offset = repetition_masq2fata(shape.repetition)
|
||||
properties = annotations_to_properties(shape.annotations)
|
||||
if isinstance(shape, Circle):
|
||||
offset = rint_cast(shape.offset + rep_offset)
|
||||
radius = rint_cast(shape.radius)
|
||||
circle = fatrec.Circle(
|
||||
for shape in shapes:
|
||||
layer, datatype = layer2oas(shape.layer)
|
||||
repetition, rep_offset = repetition_masq2fata(shape.repetition)
|
||||
properties = annotations_to_properties(shape.annotations)
|
||||
if isinstance(shape, Circle):
|
||||
offset = rint_cast(shape.offset + rep_offset)
|
||||
radius = rint_cast(shape.radius)
|
||||
circle = fatrec.Circle(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
radius=cast(int, radius),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(circle)
|
||||
elif isinstance(shape, Path):
|
||||
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
|
||||
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
|
||||
half_width = rint_cast(shape.width / 2)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
|
||||
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
|
||||
path = fatrec.Path(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=cast(Sequence[Sequence[int]], deltas),
|
||||
half_width=cast(int, half_width),
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
extension_end=extension_end,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(path)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
||||
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
||||
elements.append(fatrec.Polygon(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
radius=cast(int, radius),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(circle)
|
||||
elif isinstance(shape, Path):
|
||||
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
|
||||
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
|
||||
half_width = rint_cast(shape.width / 2)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
|
||||
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
|
||||
path = fatrec.Path(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=cast(Sequence[Sequence[int]], deltas),
|
||||
half_width=cast(int, half_width),
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
extension_end=extension_end,
|
||||
point_list=cast(list[list[int]], points),
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(path)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
||||
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
||||
elements.append(fatrec.Polygon(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
point_list=cast(list[list[int]], points),
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
))
|
||||
return elements
|
||||
|
||||
|
||||
def _labels_to_texts(
|
||||
labels: dict[layer_t, list[Label]],
|
||||
labels: list[Label],
|
||||
layer2oas: Callable[[layer_t], tuple[int, int]],
|
||||
) -> list[fatrec.Text]:
|
||||
texts = []
|
||||
for mlayer, lseq in labels.items():
|
||||
layer, datatype = layer2oas(mlayer)
|
||||
for label in lseq:
|
||||
repetition, rep_offset = repetition_masq2fata(label.repetition)
|
||||
xy = rint_cast(label.offset + rep_offset)
|
||||
properties = annotations_to_properties(label.annotations)
|
||||
texts.append(fatrec.Text(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
string=label.string,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
for label in labels:
|
||||
layer, datatype = layer2oas(label.layer)
|
||||
repetition, rep_offset = repetition_masq2fata(label.repetition)
|
||||
xy = rint_cast(label.offset + rep_offset)
|
||||
properties = annotations_to_properties(label.annotations)
|
||||
texts.append(fatrec.Text(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
string=label.string,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
return texts
|
||||
|
||||
|
||||
@ -695,9 +700,9 @@ def properties_to_annotations(
|
||||
|
||||
assert proprec.values is not None
|
||||
for value in proprec.values:
|
||||
if isinstance(value, float | int):
|
||||
if isinstance(value, (float, int)):
|
||||
values.append(value)
|
||||
elif isinstance(value, NString | AString):
|
||||
elif isinstance(value, (NString, AString)):
|
||||
values.append(value.string)
|
||||
elif isinstance(value, PropStringReference):
|
||||
values.append(propstrings[value.ref].string) # dereference
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
SVG file format readers and writers
|
||||
"""
|
||||
from collections.abc import Mapping
|
||||
from typing import Mapping
|
||||
import warnings
|
||||
|
||||
import numpy
|
||||
@ -50,7 +50,7 @@ def writefile(
|
||||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
||||
else:
|
||||
bounds_min, bounds_max = bounds
|
||||
|
||||
@ -65,24 +65,22 @@ def writefile(
|
||||
for name, pat in library.items():
|
||||
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red')
|
||||
|
||||
for layer, shapes in pat.shapes.items():
|
||||
for shape in shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec = poly2path(polygon.vertices + polygon.offset)
|
||||
for shape in pat.shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec = poly2path(polygon.vertices + polygon.offset)
|
||||
|
||||
path = svg.path(d=path_spec)
|
||||
if custom_attributes:
|
||||
path['pattern_layer'] = layer
|
||||
path = svg.path(d=path_spec)
|
||||
if custom_attributes:
|
||||
path['pattern_layer'] = polygon.layer
|
||||
|
||||
svg_group.add(path)
|
||||
svg_group.add(path)
|
||||
|
||||
for target, refs in pat.refs.items():
|
||||
if target is None:
|
||||
for ref in pat.refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
for ref in refs:
|
||||
transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})'
|
||||
use = svg.use(href='#' + mangle_name(target), transform=transform)
|
||||
svg_group.add(use)
|
||||
transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})'
|
||||
use = svg.use(href='#' + mangle_name(ref.target), transform=transform)
|
||||
svg_group.add(use)
|
||||
|
||||
svg.defs.add(svg_group)
|
||||
svg.add(svg.use(href='#' + mangle_name(top)))
|
||||
@ -117,7 +115,7 @@ def writefile_inverted(
|
||||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
||||
else:
|
||||
bounds_min, bounds_max = bounds
|
||||
|
||||
@ -135,10 +133,9 @@ def writefile_inverted(
|
||||
path_spec = poly2path(slab_edge)
|
||||
|
||||
# Draw polygons with reversed vertex order
|
||||
for _layer, shapes in pattern.shapes.items():
|
||||
for shape in shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec += poly2path(polygon.vertices[::-1] + polygon.offset)
|
||||
for shape in pattern.shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec += poly2path(polygon.vertices[::-1] + polygon.offset)
|
||||
|
||||
svg.add(svg.path(d=path_spec, fill='blue', stroke='red'))
|
||||
svg.save()
|
||||
@ -154,9 +151,9 @@ def poly2path(vertices: ArrayLike) -> str:
|
||||
Returns:
|
||||
SVG path-string.
|
||||
"""
|
||||
verts = numpy.asarray(vertices)
|
||||
commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1]) # noqa: UP032
|
||||
verts = numpy.array(vertices, copy=False)
|
||||
commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1])
|
||||
for vertex in verts[1:]:
|
||||
commands += 'L{:g},{:g}'.format(vertex[0], vertex[1]) # noqa: UP032
|
||||
commands += 'L{:g},{:g}'.format(vertex[0], vertex[1])
|
||||
commands += ' Z '
|
||||
return commands
|
||||
|
@ -1,93 +1,21 @@
|
||||
"""
|
||||
Helper functions for file reading and writing
|
||||
"""
|
||||
from typing import IO
|
||||
from collections.abc import Iterator, Mapping
|
||||
from typing import IO, Iterator
|
||||
import re
|
||||
import pathlib
|
||||
import logging
|
||||
import tempfile
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from pprint import pformat
|
||||
from itertools import chain
|
||||
|
||||
from .. import Pattern, PatternError, Library, LibraryError
|
||||
from .. import Pattern, PatternError
|
||||
from ..shapes import Polygon, Path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def preflight(
|
||||
lib: Library,
|
||||
sort: bool = True,
|
||||
sort_elements: bool = False,
|
||||
allow_dangling_refs: bool | None = None,
|
||||
allow_named_layers: bool = True,
|
||||
prune_empty_patterns: bool = False,
|
||||
wrap_repeated_shapes: bool = False,
|
||||
) -> Library:
|
||||
"""
|
||||
Run a standard set of useful operations and checks, usually done immediately prior
|
||||
to writing to a file (or immediately after reading).
|
||||
|
||||
Args:
|
||||
sort: Whether to sort the patterns based on their names, and optionaly sort the pattern contents.
|
||||
Default True. Useful for reproducible builds.
|
||||
sort_elements: Whether to sort the pattern contents. Requires sort=True to run.
|
||||
allow_dangling_refs: If `None` (default), warns about any refs to patterns that are not
|
||||
in the provided library. If `True`, no check is performed; if `False`, a `LibraryError`
|
||||
is raised instead.
|
||||
allow_named_layers: If `False`, raises a `PatternError` if any layer is referred to by
|
||||
a string instead of a number (or tuple).
|
||||
prune_empty_patterns: Runs `Library.prune_empty()`, recursively deleting any empty patterns.
|
||||
wrap_repeated_shapes: Runs `Library.wrap_repeated_shapes()`, turning repeated shapes into
|
||||
repeated refs containing non-repeated shapes.
|
||||
|
||||
Returns:
|
||||
`lib` or an equivalent sorted library
|
||||
"""
|
||||
if sort:
|
||||
lib = Library(dict(sorted(
|
||||
(nn, pp.sort(sort_elements=sort_elements)) for nn, pp in lib.items()
|
||||
)))
|
||||
|
||||
if not allow_dangling_refs:
|
||||
refs = lib.referenced_patterns()
|
||||
dangling = refs - set(lib.keys())
|
||||
if dangling:
|
||||
msg = 'Dangling refs found: ' + pformat(dangling)
|
||||
if allow_dangling_refs is None:
|
||||
logger.warning(msg)
|
||||
else:
|
||||
raise LibraryError(msg)
|
||||
|
||||
if not allow_named_layers:
|
||||
named_layers: Mapping[str, set] = defaultdict(set)
|
||||
for name, pat in lib.items():
|
||||
for layer in chain(pat.shapes.keys(), pat.labels.keys()):
|
||||
if isinstance(layer, str):
|
||||
named_layers[name].add(layer)
|
||||
named_layers = dict(named_layers)
|
||||
if named_layers:
|
||||
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
|
||||
|
||||
if prune_empty_patterns:
|
||||
pruned = lib.prune_empty()
|
||||
if pruned:
|
||||
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
|
||||
logger.debug('Pruned: ' + pformat(pruned))
|
||||
else:
|
||||
logger.debug('Preflight found no empty patterns')
|
||||
|
||||
if wrap_repeated_shapes:
|
||||
lib.wrap_repeated_shapes()
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
def mangle_name(name: str) -> str:
|
||||
"""
|
||||
Sanitize a name.
|
||||
@ -114,22 +42,21 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
|
||||
Returns:
|
||||
pat
|
||||
"""
|
||||
for shapes in pat.shapes.values():
|
||||
remove_inds = []
|
||||
for ii, shape in enumerate(shapes):
|
||||
if not isinstance(shape, Polygon | Path):
|
||||
continue
|
||||
try:
|
||||
shape.clean_vertices()
|
||||
except PatternError:
|
||||
remove_inds.append(ii)
|
||||
for ii in sorted(remove_inds, reverse=True):
|
||||
del shapes[ii]
|
||||
remove_inds = []
|
||||
for ii, shape in enumerate(pat.shapes):
|
||||
if not isinstance(shape, (Polygon, Path)):
|
||||
continue
|
||||
try:
|
||||
shape.clean_vertices()
|
||||
except PatternError:
|
||||
remove_inds.append(ii)
|
||||
for ii in sorted(remove_inds, reverse=True):
|
||||
del pat.shapes[ii]
|
||||
return pat
|
||||
|
||||
|
||||
def is_gzipped(path: pathlib.Path) -> bool:
|
||||
with path.open('rb') as stream:
|
||||
with open(path, 'rb') as stream:
|
||||
magic_bytes = stream.read(2)
|
||||
return magic_bytes == b'\x1f\x8b'
|
||||
|
||||
|
@ -1,26 +1,21 @@
|
||||
from typing import Self, Any
|
||||
from typing import Self
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from .repetition import Repetition
|
||||
from .utils import rotation_matrix_2d, annotations_t, annotations_eq, annotations_lt, rep2key
|
||||
from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded
|
||||
from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t
|
||||
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, RepeatableImpl
|
||||
from .traits import AnnotatableImpl
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable):
|
||||
class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
Pivotable, Copyable, metaclass=AutoSlots):
|
||||
"""
|
||||
A text annotation with a position (but no size; it is not drawn)
|
||||
A text annotation with a position and layer (but no size; it is not drawn)
|
||||
"""
|
||||
__slots__ = (
|
||||
'_string',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
__slots__ = ( '_string', )
|
||||
|
||||
_string: str
|
||||
""" Label string """
|
||||
@ -45,11 +40,13 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
||||
string: str,
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
) -> None:
|
||||
self.string = string
|
||||
self.offset = numpy.array(offset, dtype=float)
|
||||
self.offset = numpy.array(offset, dtype=float, copy=True)
|
||||
self.layer = layer
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
@ -57,6 +54,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
||||
return type(self)(
|
||||
string=self.string,
|
||||
offset=self.offset.copy(),
|
||||
layer=self.layer,
|
||||
repetition=self.repetition,
|
||||
)
|
||||
|
||||
@ -66,23 +64,6 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
||||
new._offset = self._offset.copy()
|
||||
return new
|
||||
|
||||
def __lt__(self, other: 'Label') -> bool:
|
||||
if self.string != other.string:
|
||||
return self.string < other.string
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
self.string == other.string
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||
"""
|
||||
Rotate the label around a point.
|
||||
@ -94,13 +75,13 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
pivot = numpy.array(pivot, dtype=float)
|
||||
self.translate(-pivot)
|
||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||
self.translate(+pivot)
|
||||
return self
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the bounds of the label.
|
||||
|
||||
|
@ -1,38 +1,29 @@
|
||||
"""
|
||||
Library classes for managing unique name->pattern mappings and deferred loading or execution.
|
||||
Library classes for managing unique name->pattern mappings and
|
||||
deferred loading or creation.
|
||||
|
||||
Classes include:
|
||||
- `ILibraryView`: Defines a general interface for read-only name->pattern mappings.
|
||||
- `LibraryView`: An implementation of `ILibraryView` backed by an arbitrary `Mapping`.
|
||||
Can be used to wrap any arbitrary `Mapping` to give it all the functionality in `ILibraryView`
|
||||
- `ILibrary`: Defines a general interface for mutable name->pattern mappings.
|
||||
- `Library`: An implementation of `ILibrary` backed by an arbitrary `MutableMapping`.
|
||||
Can be used to wrap any arbitrary `MutableMapping` to give it all the functionality in `ILibrary`.
|
||||
By default, uses a `dict` as the underylingmapping.
|
||||
- `LazyLibrary`: An implementation of `ILibrary` which enables on-demand loading or generation
|
||||
of patterns.
|
||||
- `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked
|
||||
library. Generated with `ILibraryView.abstract_view()`.
|
||||
# TODO documentn all library classes
|
||||
# TODO toplevel documentation of library, classes, and abstracts
|
||||
"""
|
||||
from typing import Self, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal
|
||||
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable
|
||||
from typing import Callable, Self, Type, TYPE_CHECKING, cast
|
||||
from typing import Iterator, Mapping, MutableMapping, Sequence
|
||||
import logging
|
||||
import base64
|
||||
import struct
|
||||
import re
|
||||
import copy
|
||||
from pprint import pformat
|
||||
from collections import defaultdict
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from graphlib import TopologicalSorter
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from .error import LibraryError, PatternError
|
||||
from .utils import layer_t, apply_transforms
|
||||
from .utils import rotation_matrix_2d, normalize_mirror
|
||||
from .shapes import Shape, Polygon
|
||||
from .label import Label
|
||||
from .abstract import Abstract
|
||||
from .pattern import map_layers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .pattern import Pattern
|
||||
@ -41,64 +32,19 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class visitor_function_t(Protocol):
|
||||
""" Signature for `Library.dfs()` visitor functions. """
|
||||
def __call__(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
hierarchy: tuple[str | None, ...],
|
||||
memo: dict,
|
||||
transform: NDArray[numpy.float64] | Literal[False],
|
||||
) -> 'Pattern':
|
||||
...
|
||||
|
||||
|
||||
TreeView: TypeAlias = Mapping[str, 'Pattern']
|
||||
""" A name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
||||
|
||||
Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
||||
""" A mutable name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
||||
|
||||
|
||||
SINGLE_USE_PREFIX = '_'
|
||||
"""
|
||||
Names starting with this prefix are assumed to refer to single-use patterns,
|
||||
which may be renamed automatically by `ILibrary.add()` (via
|
||||
`rename_theirs=_rename_patterns()` )
|
||||
"""
|
||||
# TODO what are the consequences of making '_' special? maybe we can make this decision everywhere?
|
||||
visitor_function_t = Callable[..., 'Pattern']
|
||||
|
||||
|
||||
def _rename_patterns(lib: 'ILibraryView', name: str) -> str:
|
||||
"""
|
||||
The default `rename_theirs` function for `ILibrary.add`.
|
||||
|
||||
Treats names starting with `SINGLE_USE_PREFIX` (default: one underscore) as
|
||||
"one-offs" for which name conflicts should be automatically resolved.
|
||||
Conflicts are resolved by calling `lib.get_name(SINGLE_USE_PREFIX + stem)`
|
||||
where `stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0]`.
|
||||
Names lacking the prefix are directly returned (not renamed).
|
||||
|
||||
Args:
|
||||
lib: The library into which `name` is to be added (but is presumed to conflict)
|
||||
name: The original name, to be modified
|
||||
|
||||
Returns:
|
||||
The new name, not guaranteed to be conflict-free!
|
||||
"""
|
||||
if not name.startswith(SINGLE_USE_PREFIX):
|
||||
# TODO document rename function
|
||||
if not name.startswith('_'): # TODO what are the consequences of making '_' special? maybe we can make this decision everywhere?
|
||||
return name
|
||||
|
||||
stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0]
|
||||
return lib.get_name(SINGLE_USE_PREFIX + stem)
|
||||
stem = name.split('$')[0]
|
||||
return lib.get_name(stem)
|
||||
|
||||
|
||||
class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
"""
|
||||
Interface for a read-only library.
|
||||
|
||||
A library is a mapping from unique names (str) to collections of geometry (`Pattern`).
|
||||
"""
|
||||
# inherited abstract functions
|
||||
#def __getitem__(self, key: str) -> 'Pattern':
|
||||
#def __iter__(self) -> Iterator[str]:
|
||||
@ -106,14 +52,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
|
||||
#__contains__, keys, items, values, get, __eq__, __ne__ supplied by Mapping
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<ILibraryView with keys\n' + pformat(list(self.keys())) + '>'
|
||||
|
||||
def abstract_view(self) -> 'AbstractView':
|
||||
"""
|
||||
Returns:
|
||||
An AbstractView into this library
|
||||
"""
|
||||
return AbstractView(self)
|
||||
|
||||
def abstract(self, name: str) -> Abstract:
|
||||
@ -128,6 +67,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
"""
|
||||
return Abstract(name=name, ports=self[name].ports)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<ILibraryView with keys\n' + pformat(list(self.keys())) + '>'
|
||||
|
||||
def dangling_refs(
|
||||
self,
|
||||
tops: str | Sequence[str] | None = None,
|
||||
@ -174,7 +116,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
tops = tuple(self.keys())
|
||||
|
||||
if skip is None:
|
||||
skip = {None}
|
||||
skip = set([None])
|
||||
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
@ -211,7 +153,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - {None})
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - set((None,)))
|
||||
keep |= set(tops)
|
||||
|
||||
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
||||
@ -263,19 +205,14 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
def flatten(
|
||||
self,
|
||||
tops: str | Sequence[str],
|
||||
flatten_ports: bool = False,
|
||||
flatten_ports: bool = False, # TODO document
|
||||
) -> dict[str, 'Pattern']:
|
||||
"""
|
||||
Returns copies of all `tops` patterns with all refs
|
||||
removed and replaced with equivalent shapes.
|
||||
Also returns flattened copies of all referenced patterns.
|
||||
The originals in the calling `Library` are not modified.
|
||||
For an in-place variant, see `Pattern.flatten`.
|
||||
Removes all refs and adds equivalent shapes.
|
||||
Also flattens all referenced patterns.
|
||||
|
||||
Args:
|
||||
tops: The pattern(s) to flattern.
|
||||
flatten_ports: If `True`, keep ports from any referenced
|
||||
patterns; otherwise discard them.
|
||||
|
||||
Returns:
|
||||
{name: flat_pattern} mapping for all flattened patterns.
|
||||
@ -283,15 +220,17 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
flattened: dict[str, Pattern | None] = {}
|
||||
flattened: dict[str, 'Pattern | None'] = {}
|
||||
|
||||
def flatten_single(name: str) -> None:
|
||||
def flatten_single(name) -> None:
|
||||
flattened[name] = None
|
||||
pat = self[name].deepcopy()
|
||||
|
||||
for target in pat.refs:
|
||||
for ref in pat.refs:
|
||||
target = ref.target
|
||||
if target is None:
|
||||
continue
|
||||
|
||||
if target not in flattened:
|
||||
flatten_single(target)
|
||||
|
||||
@ -301,11 +240,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if target_pat.is_empty(): # avoid some extra allocations
|
||||
continue
|
||||
|
||||
for ref in pat.refs[target]:
|
||||
p = ref.as_pattern(pattern=target_pat)
|
||||
if not flatten_ports:
|
||||
p.ports.clear()
|
||||
pat.append(p)
|
||||
p = ref.as_pattern(pattern=flattened[target])
|
||||
if not flatten_ports:
|
||||
p.ports.clear()
|
||||
pat.append(p)
|
||||
|
||||
pat.refs.clear()
|
||||
flattened[name] = pat
|
||||
@ -318,7 +256,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
|
||||
def get_name(
|
||||
self,
|
||||
name: str = SINGLE_USE_PREFIX * 2,
|
||||
name: str = '__',
|
||||
sanitize: bool = True,
|
||||
max_length: int = 32,
|
||||
quiet: bool | None = None,
|
||||
@ -329,17 +267,17 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
This function may be overridden in a subclass or monkey-patched to fit the caller's requirements.
|
||||
|
||||
Args:
|
||||
name: Preferred name for the pattern. Default is `SINGLE_USE_PREFIX * 2`.
|
||||
name: Preferred name for the pattern. Default '__'.
|
||||
sanitize: Allows only alphanumeric charaters and _?$. Replaces invalid characters with underscores.
|
||||
max_length: Names longer than this will be truncated.
|
||||
quiet: If `True`, suppress log messages. Default `None` suppresses messages only if
|
||||
the name starts with `SINGLE_USE_PREFIX`.
|
||||
the name starts with an underscore.
|
||||
|
||||
Returns:
|
||||
Name, unique within this library.
|
||||
"""
|
||||
if quiet is None:
|
||||
quiet = name.startswith(SINGLE_USE_PREFIX)
|
||||
quiet = name.startswith('_')
|
||||
|
||||
if sanitize:
|
||||
# Remove invalid characters
|
||||
@ -347,13 +285,12 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
else:
|
||||
sanitized_name = name
|
||||
|
||||
ii = 0
|
||||
suffixed_name = sanitized_name
|
||||
if sanitized_name in self:
|
||||
ii = sum(1 for nn in self.keys() if nn.startswith(sanitized_name))
|
||||
else:
|
||||
ii = 0
|
||||
while suffixed_name in self or suffixed_name == '':
|
||||
suffixed_name = sanitized_name + b64suffix(ii)
|
||||
suffix = base64.b64encode(struct.pack('>Q', ii), b'$?').decode('ASCII')
|
||||
|
||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||
ii += 1
|
||||
|
||||
if len(suffixed_name) > max_length:
|
||||
@ -379,7 +316,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
names = set(self.keys())
|
||||
not_toplevel: set[str | None] = set()
|
||||
for name in names:
|
||||
not_toplevel |= set(self[name].refs.keys())
|
||||
not_toplevel |= set(sp.target for sp in self[name].refs)
|
||||
|
||||
toplevel = list(names - not_toplevel)
|
||||
return toplevel
|
||||
@ -387,9 +324,6 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
def top(self) -> str:
|
||||
"""
|
||||
Return the name of the topcell, or raise an exception if there isn't a single topcell
|
||||
|
||||
Raises:
|
||||
LibraryError if there is not exactly one topcell.
|
||||
"""
|
||||
tops = self.tops()
|
||||
if len(tops) != 1:
|
||||
@ -399,9 +333,6 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
def top_pattern(self) -> 'Pattern':
|
||||
"""
|
||||
Shorthand for self[self.top()]
|
||||
|
||||
Raises:
|
||||
LibraryError if there is not exactly one topcell.
|
||||
"""
|
||||
return self[self.top()]
|
||||
|
||||
@ -421,10 +352,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
At each pattern in the tree, the following sequence is called:
|
||||
```
|
||||
current_pattern = visit_before(current_pattern, **vist_args)
|
||||
for target in current_pattern.refs:
|
||||
for ref in pattern.refs[target]:
|
||||
self.dfs(target, visit_before, visit_after,
|
||||
hierarchy + (sp.target,), updated_transform, memo)
|
||||
for sp in current_pattern.refs]
|
||||
self.dfs(sp.target, visit_before, visit_after,
|
||||
hierarchy + (sp.target,), updated_transform, memo)
|
||||
current_pattern = visit_after(current_pattern, **visit_args)
|
||||
```
|
||||
where `visit_args` are
|
||||
@ -461,46 +391,49 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if transform is None or transform is True:
|
||||
transform = numpy.zeros(4)
|
||||
elif transform is not False:
|
||||
transform = numpy.asarray(transform, dtype=float)
|
||||
transform = numpy.array(transform)
|
||||
|
||||
original_pattern = pattern
|
||||
|
||||
if visit_before is not None:
|
||||
pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
|
||||
|
||||
for target in pattern.refs:
|
||||
if target is None:
|
||||
for ref in pattern.refs:
|
||||
if transform is not False:
|
||||
sign = numpy.ones(2)
|
||||
if transform[3]:
|
||||
sign[1] = -1
|
||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
|
||||
mirror_x, angle = normalize_mirror(ref.mirrored)
|
||||
angle += ref.rotation
|
||||
ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
||||
ref_transform[3] %= 2
|
||||
else:
|
||||
ref_transform = False
|
||||
|
||||
if ref.target is None:
|
||||
continue
|
||||
if target in hierarchy:
|
||||
raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
|
||||
if ref.target in hierarchy:
|
||||
raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"')
|
||||
|
||||
for ref in pattern.refs[target]:
|
||||
ref_transforms: list[bool] | NDArray[numpy.float64]
|
||||
if transform is not False:
|
||||
ref_transforms = apply_transforms(transform, ref.as_transforms())
|
||||
else:
|
||||
ref_transforms = [False]
|
||||
|
||||
for ref_transform in ref_transforms:
|
||||
self.dfs(
|
||||
pattern=self[target],
|
||||
visit_before=visit_before,
|
||||
visit_after=visit_after,
|
||||
hierarchy=hierarchy + (target,),
|
||||
transform=ref_transform,
|
||||
memo=memo,
|
||||
)
|
||||
self.dfs(
|
||||
pattern=self[ref.target],
|
||||
visit_before=visit_before,
|
||||
visit_after=visit_after,
|
||||
hierarchy=hierarchy + (ref.target,),
|
||||
transform=ref_transform,
|
||||
memo=memo,
|
||||
)
|
||||
|
||||
if visit_after is not None:
|
||||
pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
|
||||
|
||||
if pattern is not original_pattern:
|
||||
name = hierarchy[-1]
|
||||
name = hierarchy[-1] # TODO what is name=None?
|
||||
if not isinstance(self, ILibrary):
|
||||
raise LibraryError('visit_* functions returned a new `Pattern` object'
|
||||
' but the library is immutable')
|
||||
if name is None:
|
||||
# The top pattern is not the original pattern, but we don't know what to call it!
|
||||
raise LibraryError('visit_* functions returned a new `Pattern` object'
|
||||
' but no top-level name was provided in `hierarchy`')
|
||||
|
||||
@ -508,150 +441,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
|
||||
return self
|
||||
|
||||
def child_graph(self) -> dict[str, set[str | None]]:
|
||||
"""
|
||||
Return a mapping from pattern name to a set of all child patterns
|
||||
(patterns it references).
|
||||
|
||||
Returns:
|
||||
Mapping from pattern name to a set of all pattern names it references.
|
||||
"""
|
||||
graph = {name: set(pat.refs.keys()) for name, pat in self.items()}
|
||||
return graph
|
||||
|
||||
def parent_graph(self) -> dict[str, set[str]]:
|
||||
"""
|
||||
Return a mapping from pattern name to a set of all parent patterns
|
||||
(patterns which reference it).
|
||||
|
||||
Returns:
|
||||
Mapping from pattern name to a set of all patterns which reference it.
|
||||
"""
|
||||
igraph: dict[str, set[str]] = {name: set() for name in self}
|
||||
for name, pat in self.items():
|
||||
for child, reflist in pat.refs.items():
|
||||
if reflist and child is not None:
|
||||
igraph[child].add(name)
|
||||
return igraph
|
||||
|
||||
def child_order(self) -> list[str]:
|
||||
"""
|
||||
Return a topologically sorted list of all contained pattern names.
|
||||
Child (referenced) patterns will appear before their parents.
|
||||
|
||||
Return:
|
||||
Topologically sorted list of pattern names.
|
||||
"""
|
||||
return list(TopologicalSorter(self.child_graph()).static_order())
|
||||
|
||||
def find_refs_local(
|
||||
self,
|
||||
name: str,
|
||||
parent_graph: dict[str, set[str]] | None = None,
|
||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||
"""
|
||||
Find the location and orientation of all refs pointing to `name`.
|
||||
Refs with a `repetition` are resolved into multiple instances (locations).
|
||||
|
||||
Args:
|
||||
name: Name of the referenced pattern.
|
||||
parent_graph: Mapping from pattern name to the set of patterns which
|
||||
reference it. Default (`None`) calls `self.parent_graph()`.
|
||||
The provided graph may be for a superset of `self` (i.e. it may
|
||||
contain additional patterns which are not present in self; they
|
||||
will be ignored).
|
||||
|
||||
Returns:
|
||||
Mapping of {parent_name: transform_list}, where transform_list
|
||||
is an Nx4 ndarray with rows
|
||||
`(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||
"""
|
||||
instances = defaultdict(list)
|
||||
if parent_graph is None:
|
||||
parent_graph = self.parent_graph()
|
||||
for parent in parent_graph[name]:
|
||||
if parent not in self: # parent_graph may be a for a superset of self
|
||||
continue
|
||||
for ref in self[parent].refs[name]:
|
||||
instances[parent].append(ref.as_transforms())
|
||||
|
||||
return instances
|
||||
|
||||
def find_refs_global(
|
||||
self,
|
||||
name: str,
|
||||
order: list[str] | None = None,
|
||||
parent_graph: dict[str, set[str]] | None = None,
|
||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
||||
"""
|
||||
Find the absolute (top-level) location and orientation of all refs (including
|
||||
repetitions) pointing to `name`.
|
||||
|
||||
Args:
|
||||
name: Name of the referenced pattern.
|
||||
order: List of pattern names in which children are guaranteed
|
||||
to appear before their parents (i.e. topologically sorted).
|
||||
Default (`None`) calls `self.child_order()`.
|
||||
parent_graph: Passed to `find_refs_local`.
|
||||
Mapping from pattern name to the set of patterns which
|
||||
reference it. Default (`None`) calls `self.parent_graph()`.
|
||||
The provided graph may be for a superset of `self` (i.e. it may
|
||||
contain additional patterns which are not present in self; they
|
||||
will be ignored).
|
||||
|
||||
Returns:
|
||||
Mapping of `{hierarchy: transform_list}`, where `hierarchy` is a tuple of the form
|
||||
`(toplevel_pattern, lvl1_pattern, ..., name)` and `transform_list` is an Nx4 ndarray
|
||||
with rows `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||
"""
|
||||
if name not in self:
|
||||
return {}
|
||||
if order is None:
|
||||
order = self.child_order()
|
||||
if parent_graph is None:
|
||||
parent_graph = self.parent_graph()
|
||||
|
||||
self_keys = set(self.keys())
|
||||
|
||||
transforms: dict[str, list[tuple[
|
||||
tuple[str, ...],
|
||||
NDArray[numpy.float64]
|
||||
]]]
|
||||
transforms = defaultdict(list)
|
||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph).items():
|
||||
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
||||
|
||||
for next_name in order:
|
||||
if next_name not in transforms:
|
||||
continue
|
||||
if not parent_graph[next_name] & self_keys:
|
||||
continue
|
||||
|
||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph)
|
||||
inners = transforms.pop(next_name)
|
||||
for parent, outer in outers.items():
|
||||
for path, inner in inners:
|
||||
combined = apply_transforms(numpy.concatenate(outer), inner)
|
||||
transforms[parent].append((
|
||||
(next_name,) + path,
|
||||
combined,
|
||||
))
|
||||
result = {}
|
||||
for parent, targets in transforms.items():
|
||||
for path, instances in targets:
|
||||
full_path = (parent,) + path
|
||||
assert full_path not in result
|
||||
result[full_path] = instances
|
||||
return result
|
||||
|
||||
|
||||
|
||||
class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
"""
|
||||
Interface for a writeable library.
|
||||
|
||||
A library is a mapping from unique names (str) to collections of geometry (`Pattern`).
|
||||
"""
|
||||
# inherited abstract functions
|
||||
#def __getitem__(self, key: str) -> 'Pattern':
|
||||
#def __iter__(self) -> Iterator[str]:
|
||||
@ -687,8 +478,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
Args:
|
||||
old_name: Current name for the pattern
|
||||
new_name: New name for the pattern
|
||||
move_references: If `True`, any refs in this library pointing to `old_name`
|
||||
will be updated to point to `new_name`.
|
||||
#TODO move_Reference
|
||||
|
||||
Returns:
|
||||
self
|
||||
@ -718,31 +508,9 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
self
|
||||
"""
|
||||
for pattern in self.values():
|
||||
if old_target in pattern.refs:
|
||||
pattern.refs[new_target].extend(pattern.refs[old_target])
|
||||
del pattern.refs[old_target]
|
||||
return self
|
||||
|
||||
def map_layers(
|
||||
self,
|
||||
map_layer: Callable[[layer_t], layer_t],
|
||||
) -> Self:
|
||||
"""
|
||||
Move all the elements in all patterns from one layer onto a different layer.
|
||||
Can also handle multiple such mappings simultaneously.
|
||||
|
||||
Args:
|
||||
map_layer: Callable which may be called with each layer present in `elements`,
|
||||
and should return the new layer to which it will be mapped.
|
||||
A simple example which maps `old_layer` to `new_layer` and leaves all others
|
||||
as-is would look like `lambda layer: {old_layer: new_layer}.get(layer, layer)`
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for pattern in self.values():
|
||||
pattern.shapes = map_layers(pattern.shapes, map_layer)
|
||||
pattern.labels = map_layers(pattern.labels, map_layer)
|
||||
for ref in pattern.refs:
|
||||
if ref.target == old_target:
|
||||
ref.target = new_target
|
||||
return self
|
||||
|
||||
def mkpat(self, name: str) -> tuple[str, 'Pattern']:
|
||||
@ -765,52 +533,30 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
self,
|
||||
other: Mapping[str, 'Pattern'],
|
||||
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
|
||||
mutate_other: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Add items from another library into this one.
|
||||
Add keys from another library into this one.
|
||||
|
||||
If any name in `other` is already present in `self`, `rename_theirs(self, name)` is called
|
||||
to pick a new name for the newly-added pattern. If the new name still conflicts with a name
|
||||
in `self` a `LibraryError` is raised. All references to the original name (within `other)`
|
||||
are updated to the new name.
|
||||
If `mutate_other=False` (default), all changes are made to a deepcopy of `other`.
|
||||
|
||||
By default, `rename_theirs` makes no changes to the name (causing a `LibraryError`) unless the
|
||||
name starts with `SINGLE_USE_PREFIX`. Prefixed names are truncated to before their first
|
||||
non-prefix '$' and then passed to `self.get_name()` to create a new unique name.
|
||||
# TODO explain reference renaming and return
|
||||
|
||||
Args:
|
||||
other: The library to insert keys from.
|
||||
other: The library to insert keys from
|
||||
rename_theirs: Called as rename_theirs(self, name) for each duplicate name
|
||||
encountered in `other`. Should return the new name for the pattern in
|
||||
`other`. See above for default behavior.
|
||||
mutate_other: If `True`, modify the original library and its contained patterns
|
||||
(e.g. when renaming patterns and updating refs). Otherwise, operate on a deepcopy
|
||||
(default).
|
||||
|
||||
`other`.
|
||||
Default is effectively
|
||||
`name.split('$')[0] if name.startswith('_') else name`
|
||||
Returns:
|
||||
A mapping of `{old_name: new_name}` for all `old_name`s in `other`. Unchanged
|
||||
names map to themselves.
|
||||
|
||||
Raises:
|
||||
`LibraryError` if a duplicate name is encountered even after applying `rename_theirs()`.
|
||||
self
|
||||
"""
|
||||
from .pattern import map_targets
|
||||
duplicates = set(self.keys()) & set(other.keys())
|
||||
|
||||
if not duplicates:
|
||||
for key in other:
|
||||
for key in other.keys():
|
||||
self._merge(key, other, key)
|
||||
return {}
|
||||
|
||||
if mutate_other:
|
||||
if isinstance(other, Library):
|
||||
temp = other
|
||||
else:
|
||||
temp = Library(dict(other))
|
||||
else:
|
||||
temp = Library(copy.deepcopy(dict(other)))
|
||||
temp = Library(copy.deepcopy(dict(other))) # TODO maybe add a `mutate` arg? Might want to keep the same patterns
|
||||
rename_map = {}
|
||||
for old_name in temp:
|
||||
if old_name in self:
|
||||
@ -826,20 +572,12 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
# Update references in the newly-added cells
|
||||
for old_name in temp:
|
||||
new_name = rename_map.get(old_name, old_name)
|
||||
pat = self[new_name]
|
||||
pat.refs = map_targets(pat.refs, lambda tt: cast(dict[str | None, str | None], rename_map).get(tt, tt))
|
||||
for ref in self[new_name].refs:
|
||||
ref.target = rename_map.get(cast(str, ref.target), ref.target)
|
||||
|
||||
return rename_map
|
||||
|
||||
def __lshift__(self, other: TreeView) -> str:
|
||||
"""
|
||||
`add()` items from a tree (single-topcell name: pattern mapping) into this one,
|
||||
and return the name of the tree's topcell (in this library; it may have changed
|
||||
based on `add()`'s default `rename_theirs` argument).
|
||||
|
||||
Raises:
|
||||
LibraryError if there is more than one topcell in `other`.
|
||||
"""
|
||||
def __lshift__(self, other: Mapping[str, 'Pattern']) -> str:
|
||||
if len(other) == 1:
|
||||
name = next(iter(other))
|
||||
else:
|
||||
@ -853,24 +591,12 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
name = tops[0]
|
||||
|
||||
rename_map = self.add(other)
|
||||
new_name = rename_map.get(name, name)
|
||||
return new_name
|
||||
|
||||
def __le__(self, other: Mapping[str, 'Pattern']) -> Abstract:
|
||||
"""
|
||||
Perform the same operation as `__lshift__` / `<<`, but return an `Abstract` instead
|
||||
of just the pattern's name.
|
||||
|
||||
Raises:
|
||||
LibraryError if there is more than one topcell in `other`.
|
||||
"""
|
||||
new_name = self << other
|
||||
return self.abstract(new_name)
|
||||
return rename_map.get(name, name)
|
||||
|
||||
def dedup(
|
||||
self,
|
||||
norm_value: int = int(1e6),
|
||||
exclude_types: tuple[type] = (Polygon,),
|
||||
exclude_types: tuple[Type] = (Polygon,),
|
||||
label2name: Callable[[tuple], str] | None = None,
|
||||
threshold: int = 2,
|
||||
) -> Self:
|
||||
@ -891,8 +617,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
exclude_types: Shape types passed in this argument are always left untouched, for
|
||||
speed or convenience. Default: `(shapes.Polygon,)`
|
||||
label2name: Given a label tuple as returned by `shape.normalized_form(...)`, pick
|
||||
a name for the generated pattern.
|
||||
Default `self.get_name(SINGLE_USE_PREIX + 'shape')`.
|
||||
a name for the generated pattern. Default `self.get_name('_shape')`.
|
||||
threshold: Only replace shapes with refs if there will be at least this many
|
||||
instances.
|
||||
|
||||
@ -908,8 +633,9 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
exclude_types = ()
|
||||
|
||||
if label2name is None:
|
||||
def label2name(label: tuple) -> str: # noqa: ARG001
|
||||
return self.get_name(SINGLE_USE_PREFIX + 'shape')
|
||||
def label2name(label):
|
||||
return self.get_name('_shape')
|
||||
#label2name = lambda label: self.get_name('_shape')
|
||||
|
||||
shape_counts: MutableMapping[tuple, int] = defaultdict(int)
|
||||
shape_funcs = {}
|
||||
@ -918,13 +644,11 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
# Using the label tuple from `.normalized_form()` as a key, check how many of each shape
|
||||
# are present and store the shape function for each one
|
||||
for pat in tuple(self.values()):
|
||||
for layer, sseq in pat.shapes.items():
|
||||
for shape in sseq:
|
||||
if not any(isinstance(shape, t) for t in exclude_types):
|
||||
base_label, _values, func = shape.normalized_form(norm_value)
|
||||
label = (*base_label, layer)
|
||||
shape_funcs[label] = func
|
||||
shape_counts[label] += 1
|
||||
for i, shape in enumerate(pat.shapes):
|
||||
if not any(isinstance(shape, t) for t in exclude_types):
|
||||
label, _values, func = shape.normalized_form(norm_value)
|
||||
shape_funcs[label] = func
|
||||
shape_counts[label] += 1
|
||||
|
||||
shape_pats = {}
|
||||
for label, count in shape_counts.items():
|
||||
@ -932,8 +656,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
continue
|
||||
|
||||
shape_func = shape_funcs[label]
|
||||
shape_pat = Pattern()
|
||||
shape_pat.shapes[label[-1]] += [shape_func()]
|
||||
shape_pat = Pattern(shapes=[shape_func()])
|
||||
shape_pats[label] = shape_pat
|
||||
|
||||
# ## Second pass ##
|
||||
@ -942,36 +665,33 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
# are to be replaced.
|
||||
# The `values` are `(offset, scale, rotation)`.
|
||||
|
||||
shape_table: dict[tuple, list] = defaultdict(list)
|
||||
for layer, sseq in pat.shapes.items():
|
||||
for i, shape in enumerate(sseq):
|
||||
if any(isinstance(shape, t) for t in exclude_types):
|
||||
continue
|
||||
shape_table: MutableMapping[tuple, list] = defaultdict(list)
|
||||
for i, shape in enumerate(pat.shapes):
|
||||
if any(isinstance(shape, t) for t in exclude_types):
|
||||
continue
|
||||
|
||||
base_label, values, _func = shape.normalized_form(norm_value)
|
||||
label = (*base_label, layer)
|
||||
label, values, _func = shape.normalized_form(norm_value)
|
||||
|
||||
if label not in shape_pats:
|
||||
continue
|
||||
if label not in shape_pats:
|
||||
continue
|
||||
|
||||
shape_table[label].append((i, values))
|
||||
shape_table[label].append((i, values))
|
||||
|
||||
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
||||
# and add `pat.refs` entries for each occurrence in pat. Also, note down that
|
||||
# we should delete the `pat.shapes` entries for which we made `Ref`s.
|
||||
shapes_to_remove = []
|
||||
for label in shape_table:
|
||||
layer = label[-1]
|
||||
target = label2name(label)
|
||||
for ii, values in shape_table[label]:
|
||||
for i, values in shape_table[label]:
|
||||
offset, scale, rotation, mirror_x = values
|
||||
pat.ref(target=target, offset=offset, scale=scale,
|
||||
rotation=rotation, mirrored=(mirror_x, False))
|
||||
shapes_to_remove.append(ii)
|
||||
shapes_to_remove.append(i)
|
||||
|
||||
# Remove any shapes for which we have created refs.
|
||||
for ii in sorted(shapes_to_remove, reverse=True):
|
||||
del pat.shapes[layer][ii]
|
||||
# Remove any shapes for which we have created refs.
|
||||
for i in sorted(shapes_to_remove, reverse=True):
|
||||
del pat.shapes[i]
|
||||
|
||||
for ll, pp in shape_pats.items():
|
||||
self[label2name(ll)] = pp
|
||||
@ -989,8 +709,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
|
||||
Args:
|
||||
name_func: Function f(this_pattern, shape) which generates a name for the
|
||||
wrapping pattern.
|
||||
Default is `self.get_name(SINGLE_USE_PREFIX + 'rep')`.
|
||||
wrapping pattern. Default is `self.get_name('_rep')`.
|
||||
|
||||
Returns:
|
||||
self
|
||||
@ -998,34 +717,33 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
from .pattern import Pattern
|
||||
|
||||
if name_func is None:
|
||||
def name_func(_pat: Pattern, _shape: Shape | Label) -> str:
|
||||
return self.get_name(SINGLE_USE_PREFIX + 'rep')
|
||||
def name_func(_pat, _shape):
|
||||
return self.get_name('_rep')
|
||||
#name_func = lambda _pat, _shape: self.get_name('_rep')
|
||||
|
||||
for pat in tuple(self.values()):
|
||||
for layer in pat.shapes:
|
||||
new_shapes = []
|
||||
for shape in pat.shapes[layer]:
|
||||
if shape.repetition is None:
|
||||
new_shapes.append(shape)
|
||||
continue
|
||||
new_shapes = []
|
||||
for shape in pat.shapes:
|
||||
if shape.repetition is None:
|
||||
new_shapes.append(shape)
|
||||
continue
|
||||
|
||||
name = name_func(pat, shape)
|
||||
self[name] = Pattern(shapes={layer: [shape]})
|
||||
pat.ref(name, repetition=shape.repetition)
|
||||
shape.repetition = None
|
||||
pat.shapes[layer] = new_shapes
|
||||
name = name_func(pat, shape)
|
||||
self[name] = Pattern(shapes=[shape])
|
||||
pat.ref(name, repetition=shape.repetition)
|
||||
shape.repetition = None
|
||||
pat.shapes = new_shapes
|
||||
|
||||
for layer in pat.labels:
|
||||
new_labels = []
|
||||
for label in pat.labels[layer]:
|
||||
if label.repetition is None:
|
||||
new_labels.append(label)
|
||||
continue
|
||||
name = name_func(pat, label)
|
||||
self[name] = Pattern(labels={layer: [label]})
|
||||
pat.ref(name, repetition=label.repetition)
|
||||
label.repetition = None
|
||||
pat.labels[layer] = new_labels
|
||||
new_labels = []
|
||||
for label in pat.labels:
|
||||
if label.repetition is None:
|
||||
new_labels.append(label)
|
||||
continue
|
||||
name = name_func(pat, label)
|
||||
self[name] = Pattern(labels=[label])
|
||||
pat.ref(name, repetition=label.repetition)
|
||||
label.repetition = None
|
||||
pat.labels = new_labels
|
||||
|
||||
return self
|
||||
|
||||
@ -1047,7 +765,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - {None})
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - set((None,)))
|
||||
keep |= set(tops)
|
||||
|
||||
new = type(self)()
|
||||
@ -1059,25 +777,13 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
self,
|
||||
repeat: bool = True,
|
||||
) -> set[str]:
|
||||
"""
|
||||
Delete any empty patterns (i.e. where `Pattern.is_empty` returns `True`).
|
||||
|
||||
Args:
|
||||
repeat: Also recursively delete any patterns which only contain(ed) empty patterns.
|
||||
|
||||
Returns:
|
||||
A set containing the names of all deleted patterns
|
||||
"""
|
||||
# TODO doc prune_empty
|
||||
trimmed = set()
|
||||
while empty := {name for name, pat in self.items() if pat.is_empty()}:
|
||||
while empty := set(name for name, pat in self.items() if pat.is_empty()):
|
||||
for name in empty:
|
||||
del self[name]
|
||||
|
||||
for pat in self.values():
|
||||
for name in empty:
|
||||
# Second pass to skip looking at refs in empty patterns
|
||||
if name in pat.refs:
|
||||
del pat.refs[name]
|
||||
pat.refs = [ref for ref in pat.refs if ref.target not in empty]
|
||||
|
||||
trimmed |= empty
|
||||
if not repeat:
|
||||
@ -1089,28 +795,15 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
key: str,
|
||||
delete_refs: bool = True,
|
||||
) -> Self:
|
||||
"""
|
||||
Delete a pattern and (optionally) all refs pointing to that pattern.
|
||||
|
||||
Args:
|
||||
key: Name of the pattern to be deleted.
|
||||
delete_refs: If `True` (default), also delete all refs pointing to the pattern.
|
||||
"""
|
||||
# TODO doc delete()
|
||||
del self[key]
|
||||
if delete_refs:
|
||||
for pat in self.values():
|
||||
if key in pat.refs:
|
||||
del pat.refs[key]
|
||||
pat.refs = [ref for ref in pat.refs if ref.target != key]
|
||||
return self
|
||||
|
||||
|
||||
class LibraryView(ILibraryView):
|
||||
"""
|
||||
Default implementation for a read-only library.
|
||||
|
||||
A library is a mapping from unique names (str) to collections of geometry (`Pattern`).
|
||||
This library is backed by an arbitrary python object which implements the `Mapping` interface.
|
||||
"""
|
||||
mapping: Mapping[str, 'Pattern']
|
||||
|
||||
def __init__(
|
||||
@ -1136,12 +829,6 @@ class LibraryView(ILibraryView):
|
||||
|
||||
|
||||
class Library(ILibrary):
|
||||
"""
|
||||
Default implementation for a writeable library.
|
||||
|
||||
A library is a mapping from unique names (str) to collections of geometry (`Pattern`).
|
||||
This library is backed by an arbitrary python object which implements the `MutableMapping` interface.
|
||||
"""
|
||||
mapping: MutableMapping[str, 'Pattern']
|
||||
|
||||
def __init__(
|
||||
@ -1173,7 +860,10 @@ class Library(ILibrary):
|
||||
if key in self.mapping:
|
||||
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!')
|
||||
|
||||
value = value() if callable(value) else value
|
||||
if callable(value):
|
||||
value = value()
|
||||
else:
|
||||
value = value
|
||||
self.mapping[key] = value
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
@ -1186,15 +876,9 @@ class Library(ILibrary):
|
||||
return f'<Library ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>'
|
||||
|
||||
@classmethod
|
||||
def mktree(cls: type[Self], name: str) -> tuple[Self, 'Pattern']:
|
||||
def mktree(cls, name: str) -> tuple[Self, 'Pattern']:
|
||||
"""
|
||||
Create a new Library and immediately add a pattern
|
||||
|
||||
Args:
|
||||
name: The name for the new pattern (usually the name of the topcell).
|
||||
|
||||
Returns:
|
||||
The newly created `Library` and the newly created `Pattern`
|
||||
"""
|
||||
from .pattern import Pattern
|
||||
tree = cls()
|
||||
@ -1251,7 +935,7 @@ class LazyLibrary(ILibrary):
|
||||
raise LibraryError(
|
||||
f'Detected multiple simultaneous lookups of "{key}".\n'
|
||||
'This may be caused by an invalid (cyclical) reference, or buggy code.\n'
|
||||
'If you are lazy-loading a file, try a non-lazy load and check for reference cycles.' # TODO give advice on finding cycles
|
||||
'If you are lazy-loading a file, try a non-lazy load and check for refernce cycles.' # TODO give advice on finding cycles
|
||||
)
|
||||
|
||||
self._lookups_in_progress.add(key)
|
||||
@ -1323,9 +1007,9 @@ class LazyLibrary(ILibrary):
|
||||
"""
|
||||
self.precache()
|
||||
for pattern in self.cache.values():
|
||||
if old_target in pattern.refs:
|
||||
pattern.refs[new_target].extend(pattern.refs[old_target])
|
||||
del pattern.refs[old_target]
|
||||
for ref in pattern.refs:
|
||||
if ref.target == old_target:
|
||||
ref.target = new_target
|
||||
return self
|
||||
|
||||
def precache(self) -> Self:
|
||||
@ -1344,11 +1028,6 @@ class LazyLibrary(ILibrary):
|
||||
|
||||
|
||||
class AbstractView(Mapping[str, Abstract]):
|
||||
"""
|
||||
A read-only mapping from names to `Abstract` objects.
|
||||
|
||||
This is usually just used as a shorthand for repeated calls to `library.abstract()`.
|
||||
"""
|
||||
library: ILibraryView
|
||||
|
||||
def __init__(self, library: ILibraryView) -> None:
|
||||
@ -1362,20 +1041,3 @@ class AbstractView(Mapping[str, Abstract]):
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.library.__len__()
|
||||
|
||||
|
||||
def b64suffix(ii: int) -> str:
|
||||
"""
|
||||
Turn an integer into a base64-equivalent suffix.
|
||||
|
||||
This could be done with base64.b64encode, but this way is faster for many small `ii`.
|
||||
"""
|
||||
def i2a(nn: int) -> str:
|
||||
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$?'[nn]
|
||||
|
||||
parts = ['$', i2a(ii % 64)]
|
||||
ii >>= 6
|
||||
while ii:
|
||||
parts.append(i2a(ii % 64))
|
||||
ii >>= 6
|
||||
return ''.join(parts)
|
||||
|
1060
masque/pattern.py
1060
masque/pattern.py
File diff suppressed because it is too large
Load Diff
174
masque/ports.py
174
masque/ports.py
@ -1,12 +1,9 @@
|
||||
from typing import overload, Self, NoReturn, Any
|
||||
from collections.abc import Iterable, KeysView, ValuesView, Mapping
|
||||
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping
|
||||
import warnings
|
||||
import traceback
|
||||
import logging
|
||||
import functools
|
||||
from collections import Counter
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from itertools import chain
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -20,7 +17,6 @@ from .error import PortError
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
"""
|
||||
A point at which a `Device` can be snapped to another `Device`.
|
||||
@ -72,28 +68,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
raise PortError('Rotation must be a scalar')
|
||||
self._rotation = val % (2 * pi)
|
||||
|
||||
@property
|
||||
def x(self) -> float:
|
||||
""" Alias for offset[0] """
|
||||
return self.offset[0]
|
||||
|
||||
@x.setter
|
||||
def x(self, val: float) -> None:
|
||||
self.offset[0] = val
|
||||
|
||||
@property
|
||||
def y(self) -> float:
|
||||
""" Alias for offset[1] """
|
||||
return self.offset[1]
|
||||
|
||||
@y.setter
|
||||
def y(self, val: float) -> None:
|
||||
self.offset[1] = val
|
||||
|
||||
def copy(self) -> Self:
|
||||
return self.deepcopy()
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds(self):
|
||||
return numpy.vstack((self.offset, self.offset))
|
||||
|
||||
def set_ptype(self, ptype: str) -> Self:
|
||||
@ -101,7 +76,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
self.ptype = ptype
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
def mirror(self, axis: int) -> Self:
|
||||
self.offset[1 - axis] *= -1
|
||||
if self.rotation is not None:
|
||||
self.rotation *= -1
|
||||
@ -124,27 +99,6 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
rot = str(numpy.rad2deg(self.rotation))
|
||||
return f'<{self.offset}, {rot}, [{self.ptype}]>'
|
||||
|
||||
def __lt__(self, other: 'Port') -> bool:
|
||||
if self.ptype != other.ptype:
|
||||
return self.ptype < other.ptype
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
if self.rotation is None:
|
||||
return True
|
||||
if other.rotation is None:
|
||||
return False
|
||||
return self.rotation < other.rotation
|
||||
return False
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and self.ptype == other.ptype
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.rotation == other.rotation
|
||||
)
|
||||
|
||||
|
||||
class PortList(metaclass=ABCMeta):
|
||||
__slots__ = () # Allow subclasses to use __slots__
|
||||
@ -181,40 +135,13 @@ class PortList(metaclass=ABCMeta):
|
||||
"""
|
||||
if isinstance(key, str):
|
||||
return self.ports[key]
|
||||
else: # noqa: RET505
|
||||
else:
|
||||
return {k: self.ports[k] for k in key}
|
||||
|
||||
def __contains__(self, key: str) -> NoReturn:
|
||||
raise NotImplementedError('PortsList.__contains__ is left unimplemented. Use `key in container.ports` instead.')
|
||||
|
||||
# NOTE: Didn't add keys(), items(), values(), __contains__(), etc.
|
||||
# because it's weird on stuff like Pattern that contains other lists
|
||||
# and because you can just grab .ports and use that instead
|
||||
|
||||
def mkport(
|
||||
self,
|
||||
name: str,
|
||||
value: Port,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a port, raising a `PortError` if a port with the same name already exists.
|
||||
|
||||
Args:
|
||||
name: Name for the port. A port with this name should not already exist.
|
||||
value: The `Port` object to which `name` will refer.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
`PortError` if the name already exists.
|
||||
"""
|
||||
if name in self.ports:
|
||||
raise PortError(f'Port {name} already exists.')
|
||||
assert name not in self.ports
|
||||
self.ports[name] = value
|
||||
return self
|
||||
|
||||
def rename_ports(
|
||||
self,
|
||||
mapping: dict[str, str | None],
|
||||
@ -239,7 +166,7 @@ class PortList(metaclass=ABCMeta):
|
||||
if duplicates:
|
||||
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
||||
|
||||
renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()}
|
||||
renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()}
|
||||
if None in renamed:
|
||||
del renamed[None]
|
||||
|
||||
@ -274,75 +201,6 @@ class PortList(metaclass=ABCMeta):
|
||||
self.ports.update(new_ports)
|
||||
return self
|
||||
|
||||
def plugged(
|
||||
self,
|
||||
connections: dict[str, str],
|
||||
) -> Self:
|
||||
"""
|
||||
Verify that the ports specified by `connections` are coincident and have opposing
|
||||
rotations, then remove the ports.
|
||||
|
||||
This is used when ports have been "manually" aligned as part of some other routing,
|
||||
but for whatever reason were not eliminated via `plug()`.
|
||||
|
||||
Args:
|
||||
connections: Pairs of ports which "plug" each other (same offset, opposing directions)
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
`PortError` if the ports are not properly aligned.
|
||||
"""
|
||||
a_names, b_names = list(zip(*connections.items(), strict=True))
|
||||
a_ports = [self.ports[pp] for pp in a_names]
|
||||
b_ports = [self.ports[pp] for pp in b_names]
|
||||
|
||||
a_types = [pp.ptype for pp in a_ports]
|
||||
b_types = [pp.ptype for pp in b_ports]
|
||||
type_conflicts = numpy.array([at != bt and 'unk' not in (at, bt)
|
||||
for at, bt in zip(a_types, b_types, strict=True)])
|
||||
|
||||
if type_conflicts.any():
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if type_conflicts[nn]:
|
||||
msg += f'{k} | {a_types[nn]}:{b_types[nn]} | {v}\n'
|
||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
|
||||
a_offsets = numpy.array([pp.offset for pp in a_ports])
|
||||
b_offsets = numpy.array([pp.offset for pp in b_ports])
|
||||
a_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in a_ports])
|
||||
b_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in b_ports])
|
||||
a_has_rot = numpy.array([pp.rotation is not None for pp in a_ports], dtype=bool)
|
||||
b_has_rot = numpy.array([pp.rotation is not None for pp in b_ports], dtype=bool)
|
||||
has_rot = a_has_rot & b_has_rot
|
||||
|
||||
if has_rot.any():
|
||||
rotations = numpy.mod(a_rotations - b_rotations - pi, 2 * pi)
|
||||
rotations[~has_rot] = rotations[has_rot][0]
|
||||
|
||||
if not numpy.allclose(rotations, 0):
|
||||
rot_deg = numpy.rad2deg(rotations)
|
||||
msg = 'Port orientations do not match:\n'
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if not numpy.isclose(rot_deg[nn], 0):
|
||||
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
translations = a_offsets - b_offsets
|
||||
if not numpy.allclose(translations, 0):
|
||||
msg = 'Port translations do not match:\n'
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if not numpy.allclose(translations[nn], 0):
|
||||
msg += f'{k} | {translations[nn]} | {v}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
for pp in chain(a_names, b_names):
|
||||
del self.ports[pp]
|
||||
return self
|
||||
|
||||
def check_ports(
|
||||
self,
|
||||
other_names: Iterable[str],
|
||||
@ -417,7 +275,7 @@ class PortList(metaclass=ABCMeta):
|
||||
other: 'PortList',
|
||||
map_in: dict[str, str],
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
mirrored: tuple[bool, bool] = (False, False),
|
||||
set_rotation: bool | None = None,
|
||||
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||
"""
|
||||
@ -428,7 +286,7 @@ class PortList(metaclass=ABCMeta):
|
||||
other: a device
|
||||
map_in: dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||
port connections between the two devices.
|
||||
mirrored: Mirrors `other` across the x axis prior to
|
||||
mirrored: Mirrors `other` across the x or y axes prior to
|
||||
connecting any ports.
|
||||
set_rotation: If the necessary rotation cannot be determined from
|
||||
the ports being connected (i.e. all pairs have at least one
|
||||
@ -445,7 +303,7 @@ class PortList(metaclass=ABCMeta):
|
||||
"""
|
||||
s_ports = self[map_in.keys()]
|
||||
o_ports = other[map_in.values()]
|
||||
return self.find_port_transform(
|
||||
return self.find_ptransform(
|
||||
s_ports=s_ports,
|
||||
o_ports=o_ports,
|
||||
map_in=map_in,
|
||||
@ -454,12 +312,12 @@ class PortList(metaclass=ABCMeta):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_port_transform(
|
||||
def find_ptransform( # TODO needs better name
|
||||
s_ports: Mapping[str, Port],
|
||||
o_ports: Mapping[str, Port],
|
||||
map_in: dict[str, str],
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
mirrored: tuple[bool, bool] = (False, False),
|
||||
set_rotation: bool | None = None,
|
||||
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||
"""
|
||||
@ -472,7 +330,7 @@ class PortList(metaclass=ABCMeta):
|
||||
o_ports: A list of ports which are to be moved/mirrored.
|
||||
map_in: dict of `{'s_port': 'o_port'}` mappings, specifying
|
||||
port connections.
|
||||
mirrored: Mirrors `o_ports` across the x axis prior to
|
||||
mirrored: Mirrors `o_ports` across the x or y axes prior to
|
||||
connecting any ports.
|
||||
set_rotation: If the necessary rotation cannot be determined from
|
||||
the ports being connected (i.e. all pairs have at least one
|
||||
@ -498,12 +356,16 @@ class PortList(metaclass=ABCMeta):
|
||||
o_has_rot = numpy.array([p.rotation is not None for p in o_ports.values()], dtype=bool)
|
||||
has_rot = s_has_rot & o_has_rot
|
||||
|
||||
if mirrored:
|
||||
if mirrored[0]:
|
||||
o_offsets[:, 1] *= -1
|
||||
o_rotations *= -1
|
||||
if mirrored[1]:
|
||||
o_offsets[:, 0] *= -1
|
||||
o_rotations *= -1
|
||||
o_rotations += pi
|
||||
|
||||
type_conflicts = numpy.array([st != ot and 'unk' not in (st, ot)
|
||||
for st, ot in zip(s_types, o_types, strict=True)])
|
||||
type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
|
||||
for st, ot in zip(s_types, o_types)])
|
||||
if type_conflicts.any():
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
for nn, (k, v) in enumerate(map_in.items()):
|
||||
|
177
masque/ref.py
177
masque/ref.py
@ -1,17 +1,18 @@
|
||||
"""
|
||||
Ref provides basic support for nesting Pattern objects within each other.
|
||||
It carries offset, rotation, mirroring, and scaling data for each individual instance.
|
||||
Ref provides basic support for nesting Pattern objects within each other, by adding
|
||||
offset, rotation, scaling, and other such properties to the reference.
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Self, Any
|
||||
from collections.abc import Mapping
|
||||
#TODO more top-level documentation
|
||||
|
||||
from typing import Sequence, Mapping, TYPE_CHECKING, Any, Self
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key
|
||||
from .error import PatternError
|
||||
from .utils import is_scalar, annotations_t
|
||||
from .repetition import Repetition
|
||||
from .traits import (
|
||||
PositionableImpl, RotatableImpl, ScalableImpl,
|
||||
@ -23,73 +24,63 @@ if TYPE_CHECKING:
|
||||
from . import Pattern
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Ref(
|
||||
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
||||
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
||||
):
|
||||
"""
|
||||
`Ref` provides basic support for nesting Pattern objects within each other.
|
||||
|
||||
It containts the transformation (mirror, rotation, scale, offset, repetition)
|
||||
and annotations for a single instantiation of a `Pattern`.
|
||||
|
||||
Note that the target (i.e. which pattern a `Ref` instantiates) is not stored within the
|
||||
`Ref` itself, but is specified by the containing `Pattern`.
|
||||
|
||||
Order of operations is (mirror, rotate, scale, translate, repeat).
|
||||
`Ref` provides basic support for nesting Pattern objects within each other, by adding
|
||||
offset, rotation, scaling, and associated methods.
|
||||
"""
|
||||
__slots__ = (
|
||||
'_mirrored',
|
||||
'_target', '_mirrored',
|
||||
# inherited
|
||||
'_offset', '_rotation', 'scale', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_mirrored: bool
|
||||
""" Whether to mirror the instance across the x axis (new_y = -old_y)ubefore rotating. """
|
||||
_target: str | None
|
||||
""" The name of the `Pattern` being instanced """
|
||||
|
||||
# Mirrored property
|
||||
@property
|
||||
def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool
|
||||
return self._mirrored
|
||||
|
||||
@mirrored.setter
|
||||
def mirrored(self, val: bool) -> None:
|
||||
self._mirrored = bool(val)
|
||||
_mirrored: NDArray[numpy.bool_]
|
||||
""" Whether to mirror the instance across the x and/or y axes. """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target: str | None,
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
mirrored: bool = False,
|
||||
mirrored: Sequence[bool] | None = None,
|
||||
scale: float = 1.0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Note: Order is (mirror, rotate, scale, translate, repeat)
|
||||
|
||||
Args:
|
||||
target: Name of the Pattern to reference.
|
||||
offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc.
|
||||
rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
|
||||
mirrored: Whether to mirror the referenced pattern across its x axis before rotating.
|
||||
mirrored: Whether to mirror the referenced pattern across its x and y axes.
|
||||
scale: Scaling factor applied to the pattern's geometry.
|
||||
repetition: `Repetition` object, default `None`
|
||||
"""
|
||||
self.target = target
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.scale = scale
|
||||
if mirrored is None:
|
||||
mirrored = (False, False)
|
||||
self.mirrored = mirrored
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
def __copy__(self) -> 'Ref':
|
||||
new = Ref(
|
||||
target=self.target,
|
||||
offset=self.offset.copy(),
|
||||
rotation=self.rotation,
|
||||
scale=self.scale,
|
||||
mirrored=self.mirrored,
|
||||
mirrored=self.mirrored.copy(),
|
||||
repetition=copy.deepcopy(self.repetition),
|
||||
annotations=copy.deepcopy(self.annotations),
|
||||
)
|
||||
@ -98,51 +89,61 @@ class Ref(
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Ref':
|
||||
memo = {} if memo is None else memo
|
||||
new = copy.copy(self)
|
||||
#new.repetition = copy.deepcopy(self.repetition, memo)
|
||||
#new.annotations = copy.deepcopy(self.annotations, memo)
|
||||
new.repetition = copy.deepcopy(self.repetition, memo)
|
||||
new.annotations = copy.deepcopy(self.annotations, memo)
|
||||
return new
|
||||
|
||||
def __lt__(self, other: 'Ref') -> bool:
|
||||
if (self.offset != other.offset).any():
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.mirrored != other.mirrored:
|
||||
return self.mirrored < other.mirrored
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.scale != other.scale:
|
||||
return self.scale < other.scale
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
# target property
|
||||
@property
|
||||
def target(self) -> str | None:
|
||||
return self._target
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
numpy.array_equal(self.offset, other.offset)
|
||||
and self.mirrored == other.mirrored
|
||||
and self.rotation == other.rotation
|
||||
and self.scale == other.scale
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
@target.setter
|
||||
def target(self, val: str | None) -> None:
|
||||
if val is not None and not isinstance(val, str):
|
||||
raise PatternError(f'Provided target {val} is not a str or None!')
|
||||
self._target = val
|
||||
|
||||
# Mirrored property
|
||||
@property
|
||||
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]:
|
||||
return self._mirrored
|
||||
|
||||
@mirrored.setter
|
||||
def mirrored(self, val: ArrayLike) -> None:
|
||||
if is_scalar(val):
|
||||
raise PatternError('Mirrored must be a 2-element list of booleans')
|
||||
self._mirrored = numpy.array(val, dtype=bool, copy=True)
|
||||
|
||||
def as_pattern(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
*,
|
||||
pattern: 'Pattern | None' = None,
|
||||
library: Mapping[str, 'Pattern'] | None = None,
|
||||
) -> 'Pattern':
|
||||
"""
|
||||
Args:
|
||||
pattern: Pattern object to transform
|
||||
library: A str->Pattern mapping, used instead of `pattern`. Must contain
|
||||
`self.target`.
|
||||
|
||||
Returns:
|
||||
A copy of the referenced Pattern which has been scaled, rotated, etc.
|
||||
according to this `Ref`'s properties.
|
||||
"""
|
||||
if pattern is None:
|
||||
if library is None:
|
||||
raise PatternError('as_pattern() must be given a pattern or library.')
|
||||
|
||||
assert self.target is not None
|
||||
pattern = library[self.target]
|
||||
|
||||
pattern = pattern.deepcopy()
|
||||
|
||||
if self.scale != 1:
|
||||
pattern.scale_by(self.scale)
|
||||
if self.mirrored:
|
||||
pattern.mirror()
|
||||
if numpy.any(self.mirrored):
|
||||
pattern.mirror2d(self.mirrored)
|
||||
if self.rotation % (2 * pi) != 0:
|
||||
pattern.rotate_around((0.0, 0.0), self.rotation)
|
||||
if numpy.any(self.offset):
|
||||
@ -165,38 +166,17 @@ class Ref(
|
||||
self.repetition.rotate(rotation)
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
self.mirror_target(axis)
|
||||
def mirror(self, axis: int) -> Self:
|
||||
self.mirrored[axis] = not self.mirrored[axis]
|
||||
self.rotation *= -1
|
||||
if self.repetition is not None:
|
||||
self.repetition.mirror(axis)
|
||||
return self
|
||||
|
||||
def mirror_target(self, axis: int = 0) -> Self:
|
||||
self.mirrored = not self.mirrored
|
||||
self.rotation += axis * pi
|
||||
return self
|
||||
|
||||
def mirror2d_target(self, across_x: bool = False, across_y: bool = False) -> Self:
|
||||
self.mirrored = bool((self.mirrored + across_x + across_y) % 2)
|
||||
if across_y:
|
||||
self.rotation += pi
|
||||
return self
|
||||
|
||||
def as_transforms(self) -> NDArray[numpy.float64]:
|
||||
xys = self.offset[None, :]
|
||||
if self.repetition is not None:
|
||||
xys = xys + self.repetition.displacements
|
||||
transforms = numpy.empty((xys.shape[0], 4))
|
||||
transforms[:, :2] = xys
|
||||
transforms[:, 2] = self.rotation
|
||||
transforms[:, 3] = self.mirrored
|
||||
return transforms
|
||||
|
||||
def get_bounds_single(
|
||||
def get_bounds(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
*,
|
||||
pattern: 'Pattern | None' = None,
|
||||
library: Mapping[str, 'Pattern'] | None = None,
|
||||
) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
@ -210,27 +190,20 @@ class Ref(
|
||||
Returns:
|
||||
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
||||
"""
|
||||
if pattern.is_empty():
|
||||
if pattern is None and library is None:
|
||||
raise PatternError('as_pattern() must be given a pattern or library.')
|
||||
if pattern is None and self.target is None:
|
||||
return None
|
||||
if library is not None and self.target not in library:
|
||||
raise PatternError(f'get_bounds() called on dangling reference to "{self.target}"')
|
||||
if pattern is not None and pattern.is_empty():
|
||||
# no need to run as_pattern()
|
||||
return None
|
||||
|
||||
# if rotation is manhattan, can take pattern's bounds and transform them
|
||||
if numpy.isclose(self.rotation % (pi / 2), 0):
|
||||
unrot_bounds = pattern.get_bounds(library)
|
||||
if unrot_bounds is None:
|
||||
return None
|
||||
|
||||
if self.mirrored:
|
||||
unrot_bounds[:, 1] *= -1
|
||||
|
||||
corners = (rotation_matrix_2d(self.rotation) @ unrot_bounds.T).T
|
||||
bounds = numpy.vstack((numpy.min(corners, axis=0),
|
||||
numpy.max(corners, axis=0))) * self.scale + [self.offset]
|
||||
return bounds
|
||||
return self.as_pattern(pattern=pattern).get_bounds(library)
|
||||
return self.as_pattern(pattern=pattern, library=library).get_bounds(library)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
name = f'"{self.target}"' if self.target is not None else None
|
||||
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||
scale = f' d{self.scale:g}' if self.scale != 1 else ''
|
||||
mirrored = ' m' if self.mirrored else ''
|
||||
return f'<Ref {self.offset}{rotation}{scale}{mirrored}>'
|
||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
||||
return f'<Ref {name} at {self.offset}{rotation}{scale}{mirrored}>'
|
||||
|
@ -2,24 +2,20 @@
|
||||
Repetitions provide support for efficiently representing multiple identical
|
||||
instances of an object .
|
||||
"""
|
||||
from typing import Any, Self, TypeVar, cast
|
||||
|
||||
from typing import Any, Type
|
||||
import copy
|
||||
import functools
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from .traits import Copyable, Scalable, Rotatable, Mirrorable, Bounded
|
||||
from .traits import Copyable, Scalable, Rotatable, Mirrorable
|
||||
from .error import PatternError
|
||||
from .utils import rotation_matrix_2d
|
||||
|
||||
|
||||
GG = TypeVar('GG', bound='Grid')
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta):
|
||||
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
|
||||
"""
|
||||
Interface common to all objects which specify repetitions
|
||||
"""
|
||||
@ -33,14 +29,6 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=A
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __le__(self, other: 'Repetition') -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class Grid(Repetition):
|
||||
"""
|
||||
@ -101,7 +89,8 @@ class Grid(Repetition):
|
||||
if b_vector is None:
|
||||
if b_count > 1:
|
||||
raise PatternError('Repetition has b_count > 1 but no b_vector')
|
||||
b_vector = numpy.array([0.0, 0.0])
|
||||
else:
|
||||
b_vector = numpy.array([0.0, 0.0])
|
||||
|
||||
if a_count < 1:
|
||||
raise PatternError(f'Repetition has too-small a_count: {a_count}')
|
||||
@ -115,12 +104,12 @@ class Grid(Repetition):
|
||||
|
||||
@classmethod
|
||||
def aligned(
|
||||
cls: type[GG],
|
||||
cls: Type,
|
||||
x: float,
|
||||
y: float,
|
||||
x_count: int,
|
||||
y_count: int,
|
||||
) -> GG:
|
||||
) -> 'Grid':
|
||||
"""
|
||||
Simple constructor for an axis-aligned 2D grid
|
||||
|
||||
@ -144,7 +133,7 @@ class Grid(Repetition):
|
||||
)
|
||||
return new
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Grid':
|
||||
memo = {} if memo is None else memo
|
||||
new = copy.copy(self)
|
||||
return new
|
||||
@ -156,11 +145,12 @@ class Grid(Repetition):
|
||||
|
||||
@a_vector.setter
|
||||
def a_vector(self, val: ArrayLike) -> None:
|
||||
val = numpy.array(val, dtype=float)
|
||||
if not isinstance(val, numpy.ndarray):
|
||||
val = numpy.array(val, dtype=float)
|
||||
|
||||
if val.size != 2:
|
||||
raise PatternError('a_vector must be convertible to size-2 ndarray')
|
||||
self._a_vector = val.flatten()
|
||||
self._a_vector = val.flatten().astype(float)
|
||||
|
||||
# b_vector property
|
||||
@property
|
||||
@ -169,7 +159,8 @@ class Grid(Repetition):
|
||||
|
||||
@b_vector.setter
|
||||
def b_vector(self, val: ArrayLike) -> None:
|
||||
val = numpy.array(val, dtype=float)
|
||||
if not isinstance(val, numpy.ndarray):
|
||||
val = numpy.array(val, dtype=float, copy=True)
|
||||
|
||||
if val.size != 2:
|
||||
raise PatternError('b_vector must be convertible to size-2 ndarray')
|
||||
@ -206,7 +197,7 @@ class Grid(Repetition):
|
||||
return (aa.flatten()[:, None] * self.a_vector[None, :]
|
||||
+ bb.flatten()[:, None] * self.b_vector[None, :]) # noqa
|
||||
|
||||
def rotate(self, rotation: float) -> Self:
|
||||
def rotate(self, rotation: float) -> 'Grid':
|
||||
"""
|
||||
Rotate lattice vectors (around (0, 0))
|
||||
|
||||
@ -221,7 +212,7 @@ class Grid(Repetition):
|
||||
self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector)
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
def mirror(self, axis: int) -> 'Grid':
|
||||
"""
|
||||
Mirror the Grid across an axis.
|
||||
|
||||
@ -245,19 +236,15 @@ class Grid(Repetition):
|
||||
Returns:
|
||||
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
||||
"""
|
||||
a_extent = self.a_vector * (self.a_count - 1)
|
||||
if self.b_count is None:
|
||||
b_extent = numpy.zeros(2)
|
||||
else:
|
||||
assert self.b_vector is not None
|
||||
b_extent = self.b_vector * (self.b_count - 1)
|
||||
a_extent = self.a_vector * self.a_count
|
||||
b_extent = self.b_vector * self.b_count if (self.b_vector is not None) else 0 # type: NDArray[numpy.float64] | float
|
||||
|
||||
corners = numpy.stack(((0, 0), a_extent, b_extent, a_extent + b_extent))
|
||||
xy_min = numpy.min(corners, axis=0)
|
||||
xy_max = numpy.max(corners, axis=0)
|
||||
return numpy.array((xy_min, xy_max))
|
||||
|
||||
def scale_by(self, c: float) -> Self:
|
||||
def scale_by(self, c: float) -> 'Grid':
|
||||
"""
|
||||
Scale the Grid by a factor
|
||||
|
||||
@ -277,7 +264,7 @@ class Grid(Repetition):
|
||||
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>')
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if type(other) is not type(self):
|
||||
if not isinstance(other, type(self)):
|
||||
return False
|
||||
if self.a_count != other.a_count or self.b_count != other.b_count:
|
||||
return False
|
||||
@ -287,28 +274,10 @@ class Grid(Repetition):
|
||||
return True
|
||||
if self.b_vector is None or other.b_vector is None:
|
||||
return False
|
||||
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): # noqa: SIM103
|
||||
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __le__(self, other: Repetition) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
other = cast(Grid, other)
|
||||
if self.a_count != other.a_count:
|
||||
return self.a_count < other.a_count
|
||||
if self.b_count != other.b_count:
|
||||
return self.b_count < other.b_count
|
||||
if not numpy.array_equal(self.a_vector, other.a_vector):
|
||||
return tuple(self.a_vector) < tuple(other.a_vector)
|
||||
if self.b_vector is None:
|
||||
return other.b_vector is not None
|
||||
if other.b_vector is None:
|
||||
return False
|
||||
if not numpy.array_equal(self.b_vector, other.b_vector):
|
||||
return tuple(self.a_vector) < tuple(other.a_vector)
|
||||
return False
|
||||
|
||||
|
||||
class Arbitrary(Repetition):
|
||||
"""
|
||||
@ -327,14 +296,14 @@ class Arbitrary(Repetition):
|
||||
"""
|
||||
|
||||
@property
|
||||
def displacements(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def displacements(self) -> Any: # TODO: mypy#3004 NDArray[numpy.float64]:
|
||||
return self._displacements
|
||||
|
||||
@displacements.setter
|
||||
def displacements(self, val: ArrayLike) -> None:
|
||||
vala = numpy.array(val, dtype=float)
|
||||
order = numpy.lexsort(vala.T[::-1]) # sortrows
|
||||
self._displacements = vala[order]
|
||||
vala: NDArray[numpy.float64] = numpy.array(val, dtype=float)
|
||||
vala = numpy.sort(vala.view([('', vala.dtype)] * vala.shape[1]), 0).view(vala.dtype) # sort rows
|
||||
self._displacements = vala
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -350,24 +319,11 @@ class Arbitrary(Repetition):
|
||||
return (f'<Arbitrary {len(self.displacements)}pts >')
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not type(other) is not type(self):
|
||||
if not isinstance(other, type(self)):
|
||||
return False
|
||||
return numpy.array_equal(self.displacements, other.displacements)
|
||||
|
||||
def __le__(self, other: Repetition) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
other = cast(Arbitrary, other)
|
||||
if self.displacements.size != other.displacements.size:
|
||||
return self.displacements.size < other.displacements.size
|
||||
|
||||
neq = (self.displacements != other.displacements)
|
||||
if neq.any():
|
||||
return self.displacements[neq][0] < other.displacements[neq][0]
|
||||
|
||||
return False
|
||||
|
||||
def rotate(self, rotation: float) -> Self:
|
||||
def rotate(self, rotation: float) -> 'Arbitrary':
|
||||
"""
|
||||
Rotate dispacements (around (0, 0))
|
||||
|
||||
@ -380,7 +336,7 @@ class Arbitrary(Repetition):
|
||||
self.displacements = numpy.dot(rotation_matrix_2d(rotation), self.displacements.T).T
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
def mirror(self, axis: int) -> 'Arbitrary':
|
||||
"""
|
||||
Mirror the displacements across an axis.
|
||||
|
||||
@ -406,7 +362,7 @@ class Arbitrary(Repetition):
|
||||
xy_max = numpy.max(self.displacements, axis=0)
|
||||
return numpy.array((xy_min, xy_max))
|
||||
|
||||
def scale_by(self, c: float) -> Self:
|
||||
def scale_by(self, c: float) -> 'Arbitrary':
|
||||
"""
|
||||
Scale the displacements by a factor
|
||||
|
||||
|
@ -3,15 +3,11 @@ Shapes for use with the Pattern class, as well as the Shape abstract class from
|
||||
which they are derived.
|
||||
"""
|
||||
|
||||
from .shape import (
|
||||
Shape as Shape,
|
||||
normalized_shape_tuple as normalized_shape_tuple,
|
||||
DEFAULT_POLY_NUM_VERTICES as DEFAULT_POLY_NUM_VERTICES,
|
||||
)
|
||||
from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
|
||||
from .polygon import Polygon as Polygon
|
||||
from .circle import Circle as Circle
|
||||
from .ellipse import Ellipse as Ellipse
|
||||
from .arc import Arc as Arc
|
||||
from .text import Text as Text
|
||||
from .path import Path as Path
|
||||
from .polygon import Polygon
|
||||
from .circle import Circle
|
||||
from .ellipse import Ellipse
|
||||
from .arc import Arc
|
||||
from .text import Text
|
||||
from .path import Path
|
||||
|
@ -1,6 +1,6 @@
|
||||
from typing import Any, cast
|
||||
from typing import Sequence, Any
|
||||
import copy
|
||||
import functools
|
||||
import math
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -9,10 +9,9 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import is_scalar, layer_t, annotations_t
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Arc(Shape):
|
||||
"""
|
||||
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
|
||||
@ -25,7 +24,7 @@ class Arc(Shape):
|
||||
__slots__ = (
|
||||
'_radii', '_angles', '_width', '_rotation',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radii: NDArray[numpy.float64]
|
||||
@ -42,7 +41,7 @@ class Arc(Shape):
|
||||
|
||||
# radius properties
|
||||
@property
|
||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the radii `[rx, ry]`
|
||||
"""
|
||||
@ -79,7 +78,7 @@ class Arc(Shape):
|
||||
|
||||
# arc start/stop angle properties
|
||||
@property
|
||||
def angles(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def angles(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the start and stop angles `[a_start, a_stop]`.
|
||||
Angles are measured from x-axis after rotation
|
||||
@ -156,6 +155,8 @@ class Arc(Shape):
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -171,6 +172,7 @@ class Arc(Shape):
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.radii = radii
|
||||
self.angles = angles
|
||||
@ -179,6 +181,8 @@ class Arc(Shape):
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
|
||||
memo = {} if memo is None else memo
|
||||
@ -189,38 +193,6 @@ class Arc(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.radii, other.radii)
|
||||
and numpy.array_equal(self.angles, other.angles)
|
||||
and self.width == other.width
|
||||
and self.rotation == other.rotation
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Arc, other)
|
||||
if self.width != other.width:
|
||||
return self.width < other.width
|
||||
if not numpy.array_equal(self.radii, other.radii):
|
||||
return tuple(self.radii) < tuple(other.radii)
|
||||
if not numpy.array_equal(self.angles, other.angles):
|
||||
return tuple(self.angles) < tuple(other.angles)
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||
@ -235,62 +207,27 @@ class Arc(Shape):
|
||||
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||
a_ranges = self._angles_to_parameters()
|
||||
|
||||
# Approximate perimeter via numerical integration
|
||||
# Approximate perimeter
|
||||
# Ramanujan, S., "Modular Equations and Approximations to ,"
|
||||
# Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372
|
||||
a0, a1 = a_ranges[1] # use outer arc
|
||||
h = ((r1 - r0) / (r1 + r0)) ** 2
|
||||
ellipse_perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h)))
|
||||
perimeter = abs(a0 - a1) / (2 * pi) * ellipse_perimeter # TODO: make this more accurate
|
||||
|
||||
#perimeter1 = numpy.trapz(numpy.sqrt(r0sin * r0sin + r1cos * r1cos), dx=dt)
|
||||
#from scipy.special import ellipeinc
|
||||
#m = 1 - (r1 / r0) ** 2
|
||||
#t1 = ellipeinc(a1 - pi / 2, m)
|
||||
#t0 = ellipeinc(a0 - pi / 2, m)
|
||||
#perimeter2 = r0 * (t1 - t0)
|
||||
|
||||
def get_arclens(n_pts: int, a0: float, a1: float, dr: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||
""" Get `n_pts` arclengths """
|
||||
t, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
||||
r0sin = (r0 + dr) * numpy.sin(t)
|
||||
r1cos = (r1 + dr) * numpy.cos(t)
|
||||
arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos)
|
||||
#arc_lengths = numpy.diff(t) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
||||
arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2
|
||||
return arc_lengths, t
|
||||
n = []
|
||||
if num_vertices is not None:
|
||||
n += [num_vertices]
|
||||
if max_arclen is not None:
|
||||
n += [perimeter / max_arclen]
|
||||
num_vertices = int(round(max(n)))
|
||||
|
||||
wh = self.width / 2.0
|
||||
if num_vertices is not None:
|
||||
n_pts = numpy.ceil(max(self.radii + wh) / min(self.radii) * num_vertices * 100).astype(int)
|
||||
perimeter_inner = get_arclens(n_pts, *a_ranges[0], dr=-wh)[0].sum()
|
||||
perimeter_outer = get_arclens(n_pts, *a_ranges[1], dr= wh)[0].sum()
|
||||
implied_arclen = (perimeter_outer + perimeter_inner + self.width * 2) / num_vertices
|
||||
max_arclen = min(implied_arclen, max_arclen if max_arclen is not None else numpy.inf)
|
||||
assert max_arclen is not None
|
||||
|
||||
def get_thetas(inner: bool) -> NDArray[numpy.float64]:
|
||||
""" Figure out the parameter values at which we should place vertices to meet the arclength constraint"""
|
||||
dr = -wh if inner else wh
|
||||
|
||||
n_pts = numpy.ceil(2 * pi * max(self.radii + dr) / max_arclen).astype(int)
|
||||
arc_lengths, thetas = get_arclens(n_pts, *a_ranges[0 if inner else 1], dr=dr)
|
||||
|
||||
keep = [0]
|
||||
removable = (numpy.cumsum(arc_lengths) <= max_arclen)
|
||||
start = 1
|
||||
while start < arc_lengths.size:
|
||||
next_to_keep = start + numpy.where(removable)[0][-1] # TODO: any chance we haven't sampled finely enough?
|
||||
keep.append(next_to_keep)
|
||||
removable = (numpy.cumsum(arc_lengths[next_to_keep + 1:]) <= max_arclen)
|
||||
start = next_to_keep + 1
|
||||
if keep[-1] != thetas.size - 1:
|
||||
keep.append(thetas.size - 1)
|
||||
|
||||
thetas = thetas[keep]
|
||||
if inner:
|
||||
thetas = thetas[::-1]
|
||||
return thetas
|
||||
|
||||
if wh in (r0, r1):
|
||||
if wh == r0 or wh == r1:
|
||||
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
|
||||
else:
|
||||
thetas_inner = get_thetas(inner=True)
|
||||
thetas_outer = get_thetas(inner=False)
|
||||
thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_vertices, endpoint=True)
|
||||
thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], num_vertices, endpoint=True)
|
||||
|
||||
sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner))
|
||||
sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer))
|
||||
@ -304,11 +241,11 @@ class Arc(Shape):
|
||||
ys = numpy.hstack((ys1, ys2))
|
||||
xys = numpy.vstack((xs, ys)).T
|
||||
|
||||
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
|
||||
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
|
||||
return [poly]
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
'''
|
||||
Equation for rotated ellipse is
|
||||
`x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)`
|
||||
`y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)`
|
||||
@ -319,12 +256,12 @@ class Arc(Shape):
|
||||
where -+ is for x, y cases, so that's where the extrema are.
|
||||
|
||||
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
|
||||
"""
|
||||
'''
|
||||
a_ranges = self._angles_to_parameters()
|
||||
|
||||
mins = []
|
||||
maxs = []
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
for a, sgn in zip(a_ranges, (-1, +1)):
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
@ -381,7 +318,7 @@ class Arc(Shape):
|
||||
self.rotation += theta
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> 'Arc':
|
||||
def mirror(self, axis: int) -> 'Arc':
|
||||
self.offset[axis - 1] *= -1
|
||||
self.rotation *= -1
|
||||
self.rotation += axis * pi
|
||||
@ -415,27 +352,28 @@ class Arc(Shape):
|
||||
rotation %= 2 * pi
|
||||
width = self.width
|
||||
|
||||
return ((type(self), radii, angles, width / norm_value),
|
||||
return ((type(self), radii, angles, width / norm_value, self.layer),
|
||||
(self.offset, scale / norm_value, rotation, False),
|
||||
lambda: Arc(
|
||||
radii=radii * norm_value,
|
||||
angles=angles,
|
||||
width=width * norm_value,
|
||||
layer=self.layer,
|
||||
))
|
||||
|
||||
def get_cap_edges(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
'''
|
||||
Returns:
|
||||
```
|
||||
[[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which
|
||||
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
|
||||
```
|
||||
"""
|
||||
'''
|
||||
a_ranges = self._angles_to_parameters()
|
||||
|
||||
mins = []
|
||||
maxs = []
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
for a, sgn in zip(a_ranges, (-1, +1)):
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
@ -454,28 +392,27 @@ class Arc(Shape):
|
||||
return numpy.array([mins, maxs]) + self.offset
|
||||
|
||||
def _angles_to_parameters(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||
|
||||
'''
|
||||
Returns:
|
||||
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
|
||||
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
|
||||
"""
|
||||
'''
|
||||
a = []
|
||||
for sgn in (-1, +1):
|
||||
wh = sgn * self.width / 2.0
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
|
||||
# create paremeter 'a' for parametrized ellipse
|
||||
a0, a1 = (numpy.arctan2(rx * numpy.sin(a), ry * numpy.cos(a)) for a in self.angles)
|
||||
sign = numpy.sign(self.angles[1] - self.angles[0])
|
||||
if sign != numpy.sign(a1 - a0):
|
||||
a1 += sign * 2 * pi
|
||||
|
||||
a.append((a0, a1))
|
||||
return numpy.array(a, dtype=float)
|
||||
return numpy.array(a)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||
|
@ -1,6 +1,4 @@
|
||||
from typing import Any, cast
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -9,10 +7,9 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import is_scalar, layer_t, annotations_t
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Circle(Shape):
|
||||
"""
|
||||
A circle, which has a position and radius.
|
||||
@ -20,7 +17,7 @@ class Circle(Shape):
|
||||
__slots__ = (
|
||||
'_radius',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radius: float
|
||||
@ -47,6 +44,7 @@ class Circle(Shape):
|
||||
radius: float,
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -57,11 +55,13 @@ class Circle(Shape):
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.radius = radius
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
|
||||
memo = {} if memo is None else memo
|
||||
@ -70,29 +70,6 @@ class Circle(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.radius == other.radius
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Circle, other)
|
||||
if not self.radius == other.radius:
|
||||
return self.radius < other.radius
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||
@ -113,16 +90,16 @@ class Circle(Shape):
|
||||
ys = numpy.sin(thetas) * self.radius
|
||||
xys = numpy.vstack((xs, ys)).T
|
||||
|
||||
return [Polygon(xys, offset=self.offset)]
|
||||
return [Polygon(xys, offset=self.offset, layer=self.layer)]
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
return numpy.vstack((self.offset - self.radius,
|
||||
self.offset + self.radius))
|
||||
|
||||
def rotate(self, theta: float) -> 'Circle': # noqa: ARG002 (theta unused)
|
||||
def rotate(self, theta: float) -> 'Circle':
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> 'Circle': # noqa: ARG002 (axis unused)
|
||||
def mirror(self, axis: int) -> 'Circle':
|
||||
self.offset *= -1
|
||||
return self
|
||||
|
||||
@ -130,12 +107,12 @@ class Circle(Shape):
|
||||
self.radius *= c
|
||||
return self
|
||||
|
||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||
def normalized_form(self, norm_value) -> normalized_shape_tuple:
|
||||
rotation = 0.0
|
||||
magnitude = self.radius / norm_value
|
||||
return ((type(self),),
|
||||
return ((type(self), self.layer),
|
||||
(self.offset, magnitude, rotation, False),
|
||||
lambda: Circle(radius=norm_value))
|
||||
lambda: Circle(radius=norm_value, layer=self.layer))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Circle o{self.offset} r{self.radius:g}>'
|
||||
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}>'
|
||||
|
@ -1,7 +1,6 @@
|
||||
from typing import Any, Self, cast
|
||||
from typing import Sequence, Any
|
||||
import copy
|
||||
import math
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -10,10 +9,9 @@ from numpy.typing import ArrayLike, NDArray
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t, annotations_t
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Ellipse(Shape):
|
||||
"""
|
||||
An ellipse, which has a position, two radii, and a rotation.
|
||||
@ -22,7 +20,7 @@ class Ellipse(Shape):
|
||||
__slots__ = (
|
||||
'_radii', '_rotation',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radii: NDArray[numpy.float64]
|
||||
@ -33,7 +31,7 @@ class Ellipse(Shape):
|
||||
|
||||
# radius properties
|
||||
@property
|
||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the radii `[rx, ry]`
|
||||
"""
|
||||
@ -92,6 +90,8 @@ class Ellipse(Shape):
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -104,14 +104,17 @@ class Ellipse(Shape):
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.radii = radii
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse':
|
||||
memo = {} if memo is None else memo
|
||||
new = copy.copy(self)
|
||||
new._offset = self._offset.copy()
|
||||
@ -119,32 +122,6 @@ class Ellipse(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.radii, other.radii)
|
||||
and self.rotation == other.rotation
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Ellipse, other)
|
||||
if not numpy.array_equal(self.radii, other.radii):
|
||||
return tuple(self.radii) < tuple(other.radii)
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||
@ -175,25 +152,25 @@ class Ellipse(Shape):
|
||||
ys = r1 * sin_th
|
||||
xys = numpy.vstack((xs, ys)).T
|
||||
|
||||
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
|
||||
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
|
||||
return [poly]
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii)
|
||||
return numpy.vstack((self.offset - rot_radii[0],
|
||||
self.offset + rot_radii[1]))
|
||||
|
||||
def rotate(self, theta: float) -> Self:
|
||||
def rotate(self, theta: float) -> 'Ellipse':
|
||||
self.rotation += theta
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
def mirror(self, axis: int) -> 'Ellipse':
|
||||
self.offset[axis - 1] *= -1
|
||||
self.rotation *= -1
|
||||
self.rotation += axis * pi
|
||||
return self
|
||||
|
||||
def scale_by(self, c: float) -> Self:
|
||||
def scale_by(self, c: float) -> 'Ellipse':
|
||||
self.radii *= c
|
||||
return self
|
||||
|
||||
@ -206,10 +183,10 @@ class Ellipse(Shape):
|
||||
radii = self.radii[::-1] / self.radius_y
|
||||
scale = self.radius_y
|
||||
angle = (self.rotation + pi / 2) % pi
|
||||
return ((type(self), radii),
|
||||
return ((type(self), radii, self.layer),
|
||||
(self.offset, scale / norm_value, angle, False),
|
||||
lambda: Ellipse(radii=radii * norm_value))
|
||||
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
return f'<Ellipse o{self.offset} r{self.radii}{rotation}>'
|
||||
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}>'
|
||||
|
@ -1,7 +1,5 @@
|
||||
from typing import Any, cast
|
||||
from collections.abc import Sequence
|
||||
from typing import Sequence, Any, cast
|
||||
import copy
|
||||
import functools
|
||||
from enum import Enum
|
||||
|
||||
import numpy
|
||||
@ -11,11 +9,10 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, normalized_shape_tuple, Polygon, Circle
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class PathCap(Enum):
|
||||
Flush = 0 # Path ends at final vertices
|
||||
Circle = 1 # Path extends past final vertices with a semicircle of radius width/2
|
||||
@ -23,24 +20,18 @@ class PathCap(Enum):
|
||||
SquareCustom = 4 # Path extends past final vertices with a rectangle of length
|
||||
# # defined by path.cap_extensions
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
return self.value == other.value
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Path(Shape):
|
||||
"""
|
||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
||||
and an offset.
|
||||
|
||||
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
|
||||
|
||||
A normalized_form(...) is available, but can be quite slow with lots of vertices.
|
||||
"""
|
||||
__slots__ = (
|
||||
'_vertices', '_width', '_cap', '_cap_extensions',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
_vertices: NDArray[numpy.float64]
|
||||
_width: float
|
||||
@ -70,14 +61,12 @@ class Path(Shape):
|
||||
def cap(self) -> PathCap:
|
||||
"""
|
||||
Path end-cap
|
||||
|
||||
Note that `cap_extensions` will be reset to default values if
|
||||
`cap` is changed away from `PathCap.SquareCustom`.
|
||||
"""
|
||||
return self._cap
|
||||
|
||||
@cap.setter
|
||||
def cap(self, val: PathCap) -> None:
|
||||
# TODO: Document that setting cap can change cap_extensions
|
||||
self._cap = PathCap(val)
|
||||
if self.cap != PathCap.SquareCustom:
|
||||
self.cap_extensions = None
|
||||
@ -87,13 +76,10 @@ class Path(Shape):
|
||||
|
||||
# cap_extensions property
|
||||
@property
|
||||
def cap_extensions(self) -> Any | None: # mypy#3004 NDArray[numpy.float64]]:
|
||||
def cap_extensions(self) -> Any | None: # TODO mypy#3004 NDArray[numpy.float64]]:
|
||||
"""
|
||||
Path end-cap extension
|
||||
|
||||
Note that `cap_extensions` will be reset to default values if
|
||||
`cap` is changed away from `PathCap.SquareCustom`.
|
||||
|
||||
Returns:
|
||||
2-element ndarray or `None`
|
||||
"""
|
||||
@ -104,26 +90,24 @@ class Path(Shape):
|
||||
custom_caps = (PathCap.SquareCustom,)
|
||||
if self.cap in custom_caps:
|
||||
if vals is None:
|
||||
raise PatternError('Tried to set cap extensions to None on path with custom cap type')
|
||||
raise Exception('Tried to set cap extensions to None on path with custom cap type')
|
||||
self._cap_extensions = numpy.array(vals, dtype=float)
|
||||
else:
|
||||
if vals is not None:
|
||||
raise PatternError('Tried to set custom cap extensions on path with non-custom cap type')
|
||||
raise Exception('Tried to set custom cap extensions on path with non-custom cap type')
|
||||
self._cap_extensions = vals
|
||||
|
||||
# vertices property
|
||||
@property
|
||||
def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]:
|
||||
def vertices(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]]:
|
||||
"""
|
||||
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
|
||||
|
||||
When setting, note that a copy of the provided vertices will be made.
|
||||
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
||||
"""
|
||||
return self._vertices
|
||||
|
||||
@vertices.setter
|
||||
def vertices(self, val: ArrayLike) -> None:
|
||||
val = numpy.array(val, dtype=float)
|
||||
val = numpy.array(val, dtype=float) # TODO document that these might not be copied
|
||||
if len(val.shape) < 2 or val.shape[1] != 2:
|
||||
raise PatternError('Vertices must be an Nx2 array')
|
||||
if val.shape[0] < 2:
|
||||
@ -169,6 +153,8 @@ class Path(Shape):
|
||||
cap_extensions: ArrayLike | None = None,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -183,6 +169,7 @@ class Path(Shape):
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
self._width = width
|
||||
self._cap = cap
|
||||
self._cap_extensions = cap_extensions
|
||||
@ -191,10 +178,12 @@ class Path(Shape):
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
self.width = width
|
||||
self.cap = cap
|
||||
self.cap_extensions = cap_extensions
|
||||
self.rotate(rotation)
|
||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Path':
|
||||
memo = {} if memo is None else memo
|
||||
@ -206,40 +195,6 @@ class Path(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.vertices, other.vertices)
|
||||
and self.width == other.width
|
||||
and self.cap == other.cap
|
||||
and numpy.array_equal(self.cap_extensions, other.cap_extensions) # type: ignore
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Path, other)
|
||||
if self.width != other.width:
|
||||
return self.width < other.width
|
||||
if self.cap != other.cap:
|
||||
return self.cap < other.cap
|
||||
if not numpy.array_equal(self.cap_extensions, other.cap_extensions): # type: ignore
|
||||
if other.cap_extensions is None:
|
||||
return False
|
||||
if self.cap_extensions is None:
|
||||
return True
|
||||
return tuple(self.cap_extensions) < tuple(other.cap_extensions)
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
@staticmethod
|
||||
def travel(
|
||||
travel_pairs: Sequence[tuple[float, float]],
|
||||
@ -248,6 +203,8 @@ class Path(Shape):
|
||||
cap_extensions: tuple[float, float] | None = None,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
) -> 'Path':
|
||||
"""
|
||||
Build a path by specifying the turn angles and travel distances
|
||||
@ -264,11 +221,15 @@ class Path(Shape):
|
||||
Default `(0, 0)` or `None`, depending on cap type
|
||||
offset: Offset, default `(0, 0)`
|
||||
rotation: Rotation counterclockwise, in radians. Default `0`
|
||||
mirrored: Whether to mirror across the x or y axes. For example,
|
||||
`mirrored=(True, False)` results in a reflection across the x-axis,
|
||||
multiplying the path's y-coordinates by -1. Default `(False, False)`
|
||||
layer: Layer, default `0`
|
||||
|
||||
Returns:
|
||||
The resulting Path object
|
||||
"""
|
||||
# TODO: Path.travel() needs testing
|
||||
# TODO: needs testing
|
||||
direction = numpy.array([1, 0])
|
||||
|
||||
verts = [numpy.zeros(2)]
|
||||
@ -277,7 +238,8 @@ class Path(Shape):
|
||||
verts.append(verts[-1] + direction * distance)
|
||||
|
||||
return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions,
|
||||
offset=offset, rotation=rotation)
|
||||
offset=offset, rotation=rotation, mirrored=mirrored,
|
||||
layer=layer)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
@ -292,7 +254,7 @@ class Path(Shape):
|
||||
|
||||
if self.width == 0:
|
||||
verts = numpy.vstack((v, v[::-1]))
|
||||
return [Polygon(offset=self.offset, vertices=verts)]
|
||||
return [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
|
||||
|
||||
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
|
||||
|
||||
@ -343,33 +305,31 @@ class Path(Shape):
|
||||
o1.append(v[-1] - perp[-1])
|
||||
verts = numpy.vstack((o0, o1[::-1]))
|
||||
|
||||
polys = [Polygon(offset=self.offset, vertices=verts)]
|
||||
polys = [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
|
||||
|
||||
if self.cap == PathCap.Circle:
|
||||
#for vert in v: # not sure if every vertex, or just ends?
|
||||
for vert in [v[0], v[-1]]:
|
||||
circ = Circle(offset=vert, radius=self.width / 2)
|
||||
circ = Circle(offset=vert, radius=self.width / 2, layer=self.layer)
|
||||
polys += circ.to_polygons(num_vertices=num_vertices, max_arclen=max_arclen)
|
||||
|
||||
return polys
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
if self.cap == PathCap.Circle:
|
||||
bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
|
||||
numpy.max(self.vertices, axis=0) + self.width / 2))
|
||||
elif self.cap in (
|
||||
PathCap.Flush,
|
||||
PathCap.Square,
|
||||
PathCap.SquareCustom,
|
||||
):
|
||||
elif self.cap in (PathCap.Flush,
|
||||
PathCap.Square,
|
||||
PathCap.SquareCustom):
|
||||
bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
|
||||
polys = self.to_polygons()
|
||||
for poly in polys:
|
||||
poly_bounds = poly.get_bounds_single_nonempty()
|
||||
poly_bounds = poly.get_bounds_nonempty()
|
||||
bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :])
|
||||
bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :])
|
||||
else:
|
||||
raise PatternError(f'get_bounds_single() not implemented for endcaps: {self.cap}')
|
||||
raise PatternError(f'get_bounds() not implemented for endcaps: {self.cap}')
|
||||
|
||||
return bounds
|
||||
|
||||
@ -378,7 +338,7 @@ class Path(Shape):
|
||||
self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> 'Path':
|
||||
def mirror(self, axis: int) -> 'Path':
|
||||
self.vertices[:, axis - 1] *= -1
|
||||
return self
|
||||
|
||||
@ -410,12 +370,13 @@ class Path(Shape):
|
||||
|
||||
width0 = self.width / norm_value
|
||||
|
||||
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap),
|
||||
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer),
|
||||
(offset, scale / norm_value, rotation, False),
|
||||
lambda: Path(
|
||||
reordered_vertices * norm_value,
|
||||
width=self.width * norm_value,
|
||||
cap=self.cap,
|
||||
layer=self.layer,
|
||||
))
|
||||
|
||||
def clean_vertices(self) -> 'Path':
|
||||
@ -429,22 +390,22 @@ class Path(Shape):
|
||||
return self
|
||||
|
||||
def remove_duplicate_vertices(self) -> 'Path':
|
||||
"""
|
||||
'''
|
||||
Removes all consecutive duplicate (repeated) vertices.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
'''
|
||||
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False)
|
||||
return self
|
||||
|
||||
def remove_colinear_vertices(self) -> 'Path':
|
||||
"""
|
||||
'''
|
||||
Removes consecutive co-linear vertices.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
'''
|
||||
self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
|
||||
return self
|
||||
|
||||
@ -461,4 +422,4 @@ class Path(Shape):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
centroid = self.offset + self.vertices.mean(axis=0)
|
||||
return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
|
||||
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
|
||||
|
@ -1,7 +1,5 @@
|
||||
from typing import Any, cast
|
||||
from collections.abc import Sequence
|
||||
from typing import Sequence, Any, cast
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -10,25 +8,21 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, normalized_shape_tuple
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Polygon(Shape):
|
||||
"""
|
||||
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
||||
implicitly-closed boundary, and an offset.
|
||||
|
||||
Note that the setter for `Polygon.vertices` creates a copy of the
|
||||
passed vertex coordinates.
|
||||
|
||||
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
|
||||
"""
|
||||
__slots__ = (
|
||||
'_vertices',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_vertices: NDArray[numpy.float64]
|
||||
@ -36,17 +30,15 @@ class Polygon(Shape):
|
||||
|
||||
# vertices property
|
||||
@property
|
||||
def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def vertices(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
||||
|
||||
When setting, note that a copy of the provided vertices will be made,
|
||||
"""
|
||||
return self._vertices
|
||||
|
||||
@vertices.setter
|
||||
def vertices(self, val: ArrayLike) -> None:
|
||||
val = numpy.array(val, dtype=float)
|
||||
val = numpy.array(val, dtype=float) # TODO document that these might not be copied
|
||||
if len(val.shape) < 2 or val.shape[1] != 2:
|
||||
raise PatternError('Vertices must be an Nx2 array')
|
||||
if val.shape[0] < 3:
|
||||
@ -89,6 +81,8 @@ class Polygon(Shape):
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -100,12 +94,15 @@ class Polygon(Shape):
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.vertices = vertices
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
self.rotate(rotation)
|
||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
|
||||
memo = {} if memo is None else memo
|
||||
@ -115,41 +112,13 @@ class Polygon(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.vertices, other.vertices)
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Polygon, other)
|
||||
if not numpy.array_equal(self.vertices, other.vertices):
|
||||
min_len = min(self.vertices.shape[0], other.vertices.shape[0])
|
||||
eq_mask = self.vertices[:min_len] != other.vertices[:min_len]
|
||||
eq_lt = self.vertices[:min_len] < other.vertices[:min_len]
|
||||
eq_lt_masked = eq_lt[eq_mask]
|
||||
if eq_lt_masked.size > 0:
|
||||
return eq_lt_masked.flat[0]
|
||||
return self.vertices.shape[0] < other.vertices.shape[0]
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
@staticmethod
|
||||
def square(
|
||||
side_length: float,
|
||||
*,
|
||||
rotation: float = 0.0,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -159,6 +128,7 @@ class Polygon(Shape):
|
||||
side_length: Length of one side
|
||||
rotation: Rotation counterclockwise, in radians
|
||||
offset: Offset, default `(0, 0)`
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
@ -169,7 +139,7 @@ class Polygon(Shape):
|
||||
[+1, +1],
|
||||
[+1, -1]], dtype=float)
|
||||
vertices = 0.5 * side_length * norm_square
|
||||
poly = Polygon(vertices, offset=offset, repetition=repetition)
|
||||
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
|
||||
poly.rotate(rotation)
|
||||
return poly
|
||||
|
||||
@ -180,6 +150,7 @@ class Polygon(Shape):
|
||||
*,
|
||||
rotation: float = 0,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -190,6 +161,7 @@ class Polygon(Shape):
|
||||
ly: Length along y (before rotation)
|
||||
rotation: Rotation counterclockwise, in radians
|
||||
offset: Offset, default `(0, 0)`
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
@ -199,7 +171,7 @@ class Polygon(Shape):
|
||||
[-lx, +ly],
|
||||
[+lx, +ly],
|
||||
[+lx, -ly]], dtype=float)
|
||||
poly = Polygon(vertices, offset=offset, repetition=repetition)
|
||||
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
|
||||
poly.rotate(rotation)
|
||||
return poly
|
||||
|
||||
@ -214,6 +186,7 @@ class Polygon(Shape):
|
||||
yctr: float | None = None,
|
||||
ymax: float | None = None,
|
||||
ly: float | None = None,
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -231,6 +204,7 @@ class Polygon(Shape):
|
||||
yctr: Center y coordinate
|
||||
ymax: Maximum y coordinate
|
||||
ly: Length along y direction
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
@ -252,7 +226,7 @@ class Polygon(Shape):
|
||||
lx = 2 * (xmax - xctr)
|
||||
else:
|
||||
raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
|
||||
else: # noqa: PLR5501
|
||||
else:
|
||||
if xctr is not None:
|
||||
pass
|
||||
elif xmax is None:
|
||||
@ -282,7 +256,7 @@ class Polygon(Shape):
|
||||
ly = 2 * (ymax - yctr)
|
||||
else:
|
||||
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
||||
else: # noqa: PLR5501
|
||||
else:
|
||||
if yctr is not None:
|
||||
pass
|
||||
elif ymax is None:
|
||||
@ -296,7 +270,7 @@ class Polygon(Shape):
|
||||
else:
|
||||
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
||||
|
||||
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), repetition=repetition)
|
||||
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), layer=layer, repetition=repetition)
|
||||
return poly
|
||||
|
||||
@staticmethod
|
||||
@ -307,6 +281,7 @@ class Polygon(Shape):
|
||||
regular: bool = True,
|
||||
center: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -325,12 +300,16 @@ class Polygon(Shape):
|
||||
rotation: Rotation counterclockwise, in radians.
|
||||
`0` results in four axis-aligned sides (the long sides of the
|
||||
irregular octagon).
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
A Polygon object containing the requested octagon
|
||||
"""
|
||||
s = (1 + numpy.sqrt(2)) if regular else 2
|
||||
if regular:
|
||||
s = 1 + numpy.sqrt(2)
|
||||
else:
|
||||
s = 2
|
||||
|
||||
norm_oct = numpy.array([
|
||||
[-1, -s],
|
||||
@ -348,18 +327,18 @@ class Polygon(Shape):
|
||||
side_length = 2 * inner_radius / s
|
||||
|
||||
vertices = 0.5 * side_length * norm_oct
|
||||
poly = Polygon(vertices, offset=center, repetition=repetition)
|
||||
poly = Polygon(vertices, offset=center, layer=layer, repetition=repetition)
|
||||
poly.rotate(rotation)
|
||||
return poly
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = None, # unused # noqa: ARG002
|
||||
max_arclen: float | None = None, # unused # noqa: ARG002
|
||||
num_vertices: int | None = None, # unused
|
||||
max_arclen: float | None = None, # unused
|
||||
) -> list['Polygon']:
|
||||
return [copy.deepcopy(self)]
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0),
|
||||
self.offset + numpy.max(self.vertices, axis=0)))
|
||||
|
||||
@ -368,7 +347,7 @@ class Polygon(Shape):
|
||||
self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> 'Polygon':
|
||||
def mirror(self, axis: int) -> 'Polygon':
|
||||
self.vertices[:, axis - 1] *= -1
|
||||
return self
|
||||
|
||||
@ -379,9 +358,8 @@ class Polygon(Shape):
|
||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||
# Note: this function is going to be pretty slow for many-vertexed polygons, relative to
|
||||
# other shapes
|
||||
meanv = self.vertices.mean(axis=0)
|
||||
zeroed_vertices = self.vertices - meanv
|
||||
offset = meanv + self.offset
|
||||
offset = self.vertices.mean(axis=0) + self.offset
|
||||
zeroed_vertices = self.vertices - offset
|
||||
|
||||
scale = zeroed_vertices.std()
|
||||
normed_vertices = zeroed_vertices / scale
|
||||
@ -400,9 +378,9 @@ class Polygon(Shape):
|
||||
|
||||
# TODO: normalize mirroring?
|
||||
|
||||
return ((type(self), reordered_vertices.data.tobytes()),
|
||||
return ((type(self), reordered_vertices.data.tobytes(), self.layer),
|
||||
(offset, scale / norm_value, rotation, False),
|
||||
lambda: Polygon(reordered_vertices * norm_value))
|
||||
lambda: Polygon(reordered_vertices * norm_value, layer=self.layer))
|
||||
|
||||
def clean_vertices(self) -> 'Polygon':
|
||||
"""
|
||||
@ -415,25 +393,25 @@ class Polygon(Shape):
|
||||
return self
|
||||
|
||||
def remove_duplicate_vertices(self) -> 'Polygon':
|
||||
"""
|
||||
'''
|
||||
Removes all consecutive duplicate (repeated) vertices.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
'''
|
||||
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True)
|
||||
return self
|
||||
|
||||
def remove_colinear_vertices(self) -> 'Polygon':
|
||||
"""
|
||||
'''
|
||||
Removes consecutive co-linear vertices.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
'''
|
||||
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
centroid = self.offset + self.vertices.mean(axis=0)
|
||||
return f'<Polygon centroid {centroid} v{len(self.vertices)}>'
|
||||
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}>'
|
||||
|
@ -1,5 +1,4 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from collections.abc import Callable
|
||||
from typing import Callable, Self, TYPE_CHECKING
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
@ -7,7 +6,8 @@ from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from ..traits import (
|
||||
Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
PositionableImpl, LayerableImpl,
|
||||
PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -26,31 +26,23 @@ normalized_shape_tuple = tuple[
|
||||
DEFAULT_POLY_NUM_VERTICES = 24
|
||||
|
||||
|
||||
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
||||
"""
|
||||
Class specifying functions common to all shapes.
|
||||
"""
|
||||
__slots__ = () # Children should use AutoSlots or set slots themselves
|
||||
__slots__ = () # Children should use AutoSlots
|
||||
|
||||
#def __copy__(self) -> Self:
|
||||
# cls = self.__class__
|
||||
# new = cls.__new__(cls)
|
||||
# for name in self.__slots__: # type: str
|
||||
# object.__setattr__(new, name, getattr(self, name))
|
||||
# return new
|
||||
def __copy__(self) -> Self:
|
||||
cls = self.__class__
|
||||
new = cls.__new__(cls)
|
||||
for name in self.__slots__: # type: str
|
||||
object.__setattr__(new, name, getattr(self, name))
|
||||
return new
|
||||
|
||||
#
|
||||
# Methods (abstract)
|
||||
#
|
||||
@abstractmethod
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __lt__(self, other: 'Shape') -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_polygons(
|
||||
self,
|
||||
@ -127,7 +119,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
|
||||
polygon_contours = []
|
||||
for polygon in self.to_polygons():
|
||||
bounds = polygon.get_bounds_single()
|
||||
bounds = polygon.get_bounds()
|
||||
if bounds is None:
|
||||
continue
|
||||
|
||||
@ -135,7 +127,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
|
||||
vertex_lists = []
|
||||
p_verts = polygon.vertices + polygon.offset
|
||||
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True):
|
||||
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0)):
|
||||
dv = v_next - v
|
||||
|
||||
# Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape
|
||||
@ -165,7 +157,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
|
||||
m = dv[1] / dv[0]
|
||||
|
||||
def get_grid_inds(xes: ArrayLike, m: float = m, v: NDArray = v) -> NDArray[numpy.float64]:
|
||||
def get_grid_inds(xes: ArrayLike) -> NDArray[numpy.float64]:
|
||||
ys = m * (xes - v[0]) + v[1]
|
||||
|
||||
# (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid
|
||||
@ -202,7 +194,10 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
vertex_lists.append(vlist)
|
||||
polygon_contours.append(numpy.vstack(vertex_lists))
|
||||
|
||||
manhattan_polygons = [Polygon(vertices=contour) for contour in polygon_contours]
|
||||
manhattan_polygons = [
|
||||
Polygon(vertices=contour, layer=self.layer)
|
||||
for contour in polygon_contours
|
||||
]
|
||||
|
||||
return manhattan_polygons
|
||||
|
||||
@ -259,19 +254,18 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
polygon_contours = []
|
||||
for polygon in self.to_polygons():
|
||||
# Get rid of unused gridlines (anything not within 2 lines of the polygon bounds)
|
||||
bounds = polygon.get_bounds_single()
|
||||
bounds = polygon.get_bounds()
|
||||
if bounds is None:
|
||||
continue
|
||||
|
||||
mins, maxs = bounds
|
||||
keep_x = numpy.logical_and(grx > mins[0], grx < maxs[0])
|
||||
keep_y = numpy.logical_and(gry > mins[1], gry < maxs[1])
|
||||
# Flood left & rightwards by 2 cells
|
||||
for kk in (keep_x, keep_y):
|
||||
for ss in (1, 2):
|
||||
kk[ss:] += kk[:-ss]
|
||||
kk[:-ss] += kk[ss:]
|
||||
kk[:] = kk > 0
|
||||
for k in (keep_x, keep_y):
|
||||
for s in (1, 2):
|
||||
k[s:] += k[:-s]
|
||||
k[:-s] += k[s:]
|
||||
k = k > 0
|
||||
|
||||
gx = grx[keep_x]
|
||||
gy = gry[keep_y]
|
||||
@ -298,6 +292,9 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
vertices = numpy.hstack((grx[snapped_contour[:, None, 0] + offset_i[0]],
|
||||
gry[snapped_contour[:, None, 1] + offset_i[1]]))
|
||||
|
||||
manhattan_polygons.append(Polygon(vertices=vertices))
|
||||
manhattan_polygons.append(Polygon(
|
||||
vertices=vertices,
|
||||
layer=self.layer,
|
||||
))
|
||||
|
||||
return manhattan_polygons
|
||||
|
@ -1,23 +1,22 @@
|
||||
from typing import Self, Any, cast
|
||||
from typing import Sequence, Any
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi, nan
|
||||
from numpy import pi, inf
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from . import Shape, Polygon, normalized_shape_tuple
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..traits import RotatableImpl
|
||||
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import is_scalar, get_bit, normalize_mirror, layer_t
|
||||
from ..utils import annotations_t
|
||||
|
||||
# Loaded on use:
|
||||
# from freetype import Face
|
||||
# from matplotlib.path import Path
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Text(RotatableImpl, Shape):
|
||||
"""
|
||||
Text (to be printed e.g. as a set of polygons).
|
||||
@ -26,12 +25,12 @@ class Text(RotatableImpl, Shape):
|
||||
__slots__ = (
|
||||
'_string', '_height', '_mirrored', 'font_path',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations', '_rotation',
|
||||
'_offset', '_layer', '_repetition', '_annotations', '_rotation',
|
||||
)
|
||||
|
||||
_string: str
|
||||
_height: float
|
||||
_mirrored: bool
|
||||
_mirrored: NDArray[numpy.bool_]
|
||||
font_path: str
|
||||
|
||||
# vertices property
|
||||
@ -54,13 +53,16 @@ class Text(RotatableImpl, Shape):
|
||||
raise PatternError('Height must be a scalar')
|
||||
self._height = val
|
||||
|
||||
# Mirrored property
|
||||
@property
|
||||
def mirrored(self) -> bool: # mypy#3004, should be bool
|
||||
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]:
|
||||
return self._mirrored
|
||||
|
||||
@mirrored.setter
|
||||
def mirrored(self, val: bool) -> None:
|
||||
self._mirrored = bool(val)
|
||||
def mirrored(self, val: Sequence[bool]) -> None:
|
||||
if is_scalar(val):
|
||||
raise PatternError('Mirrored must be a 2-element list of booleans')
|
||||
self._mirrored = numpy.array(val, dtype=bool, copy=True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -70,70 +72,46 @@ class Text(RotatableImpl, Shape):
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
mirrored: ArrayLike = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
assert isinstance(offset, numpy.ndarray)
|
||||
assert isinstance(mirrored, numpy.ndarray)
|
||||
self._offset = offset
|
||||
self._layer = layer
|
||||
self._string = string
|
||||
self._height = height
|
||||
self._rotation = rotation
|
||||
self._mirrored = mirrored
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
else:
|
||||
self.offset = offset
|
||||
self.layer = layer
|
||||
self.string = string
|
||||
self.height = height
|
||||
self.rotation = rotation
|
||||
self.mirrored = mirrored
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.font_path = font_path
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Text':
|
||||
memo = {} if memo is None else memo
|
||||
new = copy.copy(self)
|
||||
new._offset = self._offset.copy()
|
||||
new._mirrored = copy.deepcopy(self._mirrored, memo)
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.string == other.string
|
||||
and self.height == other.height
|
||||
and self.font_path == other.font_path
|
||||
and self.rotation == other.rotation
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Text, other)
|
||||
if not self.height == other.height:
|
||||
return self.height < other.height
|
||||
if not self.string == other.string:
|
||||
return self.string < other.string
|
||||
if not self.font_path == other.font_path:
|
||||
return self.font_path < other.font_path
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = None, # unused # noqa: ARG002
|
||||
max_arclen: float | None = None, # unused # noqa: ARG002
|
||||
num_vertices: int | None = None, # unused
|
||||
max_arclen: float | None = None, # unused
|
||||
) -> list[Polygon]:
|
||||
all_polygons = []
|
||||
total_advance = 0.0
|
||||
@ -142,9 +120,8 @@ class Text(RotatableImpl, Shape):
|
||||
|
||||
# Move these polygons to the right of the previous letter
|
||||
for xys in raw_polys:
|
||||
poly = Polygon(xys)
|
||||
if self.mirrored:
|
||||
poly.mirror()
|
||||
poly = Polygon(xys, layer=self.layer)
|
||||
poly.mirror2d(self.mirrored)
|
||||
poly.scale_by(self.height)
|
||||
poly.offset = self.offset + [total_advance, 0]
|
||||
poly.rotate_around(self.offset, self.rotation)
|
||||
@ -155,47 +132,41 @@ class Text(RotatableImpl, Shape):
|
||||
|
||||
return all_polygons
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
self.mirrored = not self.mirrored
|
||||
if axis == 1:
|
||||
self.rotation += pi
|
||||
def mirror(self, axis: int) -> 'Text':
|
||||
self.mirrored[axis] = not self.mirrored[axis]
|
||||
return self
|
||||
|
||||
def scale_by(self, c: float) -> Self:
|
||||
def scale_by(self, c: float) -> 'Text':
|
||||
self.height *= c
|
||||
return self
|
||||
|
||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||
rotation = self.rotation % (2 * pi)
|
||||
return ((type(self), self.string, self.font_path),
|
||||
(self.offset, self.height / norm_value, rotation, bool(self.mirrored)),
|
||||
mirror_x, rotation = normalize_mirror(self.mirrored)
|
||||
rotation += self.rotation
|
||||
rotation %= 2 * pi
|
||||
return ((type(self), self.string, self.font_path, self.layer),
|
||||
(self.offset, self.height / norm_value, rotation, mirror_x),
|
||||
lambda: Text(
|
||||
string=self.string,
|
||||
height=self.height * norm_value,
|
||||
font_path=self.font_path,
|
||||
rotation=rotation,
|
||||
).mirror2d(across_x=self.mirrored),
|
||||
)
|
||||
mirrored=(mirror_x, False),
|
||||
layer=self.layer,
|
||||
))
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
# rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
|
||||
# just convert to polygons instead
|
||||
bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
|
||||
polys = self.to_polygons()
|
||||
pbounds = numpy.full((len(polys), 2, 2), nan)
|
||||
for pp, poly in enumerate(polys):
|
||||
pbounds[pp] = poly.get_bounds_nonempty()
|
||||
bounds = numpy.vstack((
|
||||
numpy.min(pbounds[: 0, :], axis=0),
|
||||
numpy.max(pbounds[: 1, :], axis=0),
|
||||
))
|
||||
for poly in polys:
|
||||
poly_bounds = poly.get_bounds()
|
||||
bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :])
|
||||
bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :])
|
||||
|
||||
return bounds
|
||||
|
||||
def __repr__(self) -> str:
|
||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
mirrored = ' m{:d}' if self.mirrored else ''
|
||||
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
||||
|
||||
|
||||
def get_char_as_polygons(
|
||||
font_path: str,
|
||||
@ -221,7 +192,7 @@ def get_char_as_polygons(
|
||||
'advance' distance (distance from the start of this glyph to the start of the next one)
|
||||
"""
|
||||
if len(char) != 1:
|
||||
raise PatternError('get_char_as_polygons called with non-char')
|
||||
raise Exception('get_char_as_polygons called with non-char')
|
||||
|
||||
face = Face(font_path)
|
||||
face.set_char_size(resolution)
|
||||
@ -230,8 +201,7 @@ def get_char_as_polygons(
|
||||
outline = slot.outline
|
||||
|
||||
start = 0
|
||||
all_verts_list = []
|
||||
all_codes = []
|
||||
all_verts_list, all_codes = [], []
|
||||
for end in outline.contours:
|
||||
points = outline.points[start:end + 1]
|
||||
points.append(points[0])
|
||||
@ -284,3 +254,8 @@ def get_char_as_polygons(
|
||||
polygons = path.to_polygons()
|
||||
|
||||
return polygons, advance
|
||||
|
||||
def __repr__(self) -> str:
|
||||
rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
||||
return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
||||
|
@ -1,34 +1,11 @@
|
||||
"""
|
||||
Traits (mixins) and default implementations
|
||||
|
||||
Traits and mixins should set `__slots__ = ()` to enable use of `__slots__` in subclasses.
|
||||
"""
|
||||
from .positionable import (
|
||||
Positionable as Positionable,
|
||||
PositionableImpl as PositionableImpl,
|
||||
Bounded as Bounded,
|
||||
)
|
||||
from .layerable import (
|
||||
Layerable as Layerable,
|
||||
LayerableImpl as LayerableImpl,
|
||||
)
|
||||
from .rotatable import (
|
||||
Rotatable as Rotatable,
|
||||
RotatableImpl as RotatableImpl,
|
||||
Pivotable as Pivotable,
|
||||
PivotableImpl as PivotableImpl,
|
||||
)
|
||||
from .repeatable import (
|
||||
Repeatable as Repeatable,
|
||||
RepeatableImpl as RepeatableImpl,
|
||||
)
|
||||
from .scalable import (
|
||||
Scalable as Scalable,
|
||||
ScalableImpl as ScalableImpl,
|
||||
)
|
||||
from .mirrorable import Mirrorable as Mirrorable
|
||||
from .copyable import Copyable as Copyable
|
||||
from .annotatable import (
|
||||
Annotatable as Annotatable,
|
||||
AnnotatableImpl as AnnotatableImpl,
|
||||
)
|
||||
from .positionable import Positionable, PositionableImpl
|
||||
from .layerable import Layerable, LayerableImpl
|
||||
from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
|
||||
from .repeatable import Repeatable, RepeatableImpl
|
||||
from .scalable import Scalable, ScalableImpl
|
||||
from .mirrorable import Mirrorable
|
||||
from .copyable import Copyable
|
||||
from .annotatable import Annotatable, AnnotatableImpl
|
||||
|
@ -1,8 +1,9 @@
|
||||
from typing import Self
|
||||
from abc import ABCMeta
|
||||
import copy
|
||||
|
||||
|
||||
class Copyable:
|
||||
class Copyable(metaclass=ABCMeta):
|
||||
"""
|
||||
Trait class which adds .copy() and .deepcopy()
|
||||
"""
|
||||
|
@ -63,7 +63,7 @@ class LayerableImpl(Layerable, metaclass=ABCMeta):
|
||||
return self._layer
|
||||
|
||||
@layer.setter
|
||||
def layer(self, val: layer_t) -> None:
|
||||
def layer(self, val: layer_t):
|
||||
self._layer = val
|
||||
|
||||
#
|
||||
|
@ -9,7 +9,7 @@ class Mirrorable(metaclass=ABCMeta):
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
def mirror(self, axis: int) -> Self:
|
||||
"""
|
||||
Mirror the entity across an axis.
|
||||
|
||||
@ -21,7 +21,7 @@ class Mirrorable(metaclass=ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
def mirror2d(self, across_x: bool = False, across_y: bool = False) -> Self:
|
||||
def mirror2d(self, axes: tuple[bool, bool]) -> Self:
|
||||
"""
|
||||
Optionally mirror the entity across both axes
|
||||
|
||||
@ -31,9 +31,9 @@ class Mirrorable(metaclass=ABCMeta):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if across_x:
|
||||
if axes[0]:
|
||||
self.mirror(0)
|
||||
if across_y:
|
||||
if axes[1]:
|
||||
self.mirror(1)
|
||||
return self
|
||||
|
||||
@ -44,7 +44,7 @@ class Mirrorable(metaclass=ABCMeta):
|
||||
# """
|
||||
# __slots__ = ()
|
||||
#
|
||||
# _mirrored: NDArray[numpy.bool]
|
||||
# _mirrored: numpy.ndarray # ndarray[bool]
|
||||
# """ Whether to mirror the instance across the x and/or y axes. """
|
||||
#
|
||||
# #
|
||||
@ -52,15 +52,15 @@ class Mirrorable(metaclass=ABCMeta):
|
||||
# #
|
||||
# # Mirrored property
|
||||
# @property
|
||||
# def mirrored(self) -> NDArray[numpy.bool]:
|
||||
# def mirrored(self) -> numpy.ndarray: # ndarray[bool]
|
||||
# """ Whether to mirror across the [x, y] axes, respectively """
|
||||
# return self._mirrored
|
||||
#
|
||||
# @mirrored.setter
|
||||
# def mirrored(self, val: Sequence[bool]) -> None:
|
||||
# def mirrored(self, val: Sequence[bool]):
|
||||
# if is_scalar(val):
|
||||
# raise MasqueError('Mirrored must be a 2-element list of booleans')
|
||||
# self._mirrored = numpy.array(val, dtype=bool)
|
||||
# self._mirrored = numpy.array(val, dtype=bool, copy=True)
|
||||
#
|
||||
# #
|
||||
# # Methods
|
||||
|
@ -1,3 +1,5 @@
|
||||
# TODO top-level comment about how traits should set __slots__ = (), and how to use AutoSlots
|
||||
|
||||
from typing import Self, Any
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
@ -58,6 +60,25 @@ class Positionable(metaclass=ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_bounds(self) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Returns `None` for an empty entity.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_bounds_nonempty(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds()
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
|
||||
class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
"""
|
||||
@ -73,7 +94,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
#
|
||||
# offset property
|
||||
@property
|
||||
def offset(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def offset(self) -> Any: # TODO mypy#3003 NDArray[numpy.float64]:
|
||||
"""
|
||||
[x, y] offset
|
||||
"""
|
||||
@ -81,11 +102,12 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
|
||||
@offset.setter
|
||||
def offset(self, val: ArrayLike) -> None:
|
||||
val = numpy.array(val, dtype=float)
|
||||
if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64:
|
||||
val = numpy.array(val, dtype=float)
|
||||
|
||||
if val.size != 2:
|
||||
raise MasqueError('Offset must be convertible to size-2 ndarray')
|
||||
self._offset = val.flatten()
|
||||
self._offset = val.flatten() # type: ignore
|
||||
|
||||
#
|
||||
# Methods
|
||||
@ -97,26 +119,3 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
def translate(self, offset: ArrayLike) -> Self:
|
||||
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
|
||||
return self
|
||||
|
||||
|
||||
class Bounded(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def get_bounds(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Returns `None` for an empty entity.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_bounds_nonempty(self, *args, **kwargs) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds(*args, **kwargs)
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
|
||||
|
@ -1,11 +1,7 @@
|
||||
from typing import Self, TYPE_CHECKING
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from ..error import MasqueError
|
||||
from .positionable import Bounded
|
||||
|
||||
|
||||
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
|
||||
@ -34,7 +30,7 @@ class Repeatable(metaclass=ABCMeta):
|
||||
|
||||
# @repetition.setter
|
||||
# @abstractmethod
|
||||
# def repetition(self, repetition: 'Repetition | None') -> None:
|
||||
# def repetition(self, repetition: 'Repetition | None'):
|
||||
# pass
|
||||
|
||||
#
|
||||
@ -54,19 +50,15 @@ class Repeatable(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
|
||||
class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
|
||||
class RepeatableImpl(Repeatable, metaclass=ABCMeta):
|
||||
"""
|
||||
Simple implementation of `Repeatable` and extension of `Bounded` to include repetition bounds.
|
||||
Simple implementation of `Repeatable`
|
||||
"""
|
||||
__slots__ = _empty_slots
|
||||
|
||||
_repetition: 'Repetition | None'
|
||||
""" Repetition object, or None (single instance only) """
|
||||
|
||||
@abstractmethod
|
||||
def get_bounds_single(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
|
||||
pass
|
||||
|
||||
#
|
||||
# Non-abstract properties
|
||||
#
|
||||
@ -75,7 +67,7 @@ class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
|
||||
return self._repetition
|
||||
|
||||
@repetition.setter
|
||||
def repetition(self, repetition: 'Repetition | None') -> None:
|
||||
def repetition(self, repetition: 'Repetition | None'):
|
||||
from ..repetition import Repetition
|
||||
if repetition is not None and not isinstance(repetition, Repetition):
|
||||
raise MasqueError(f'{repetition} is not a valid Repetition object!')
|
||||
@ -87,24 +79,3 @@ class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
|
||||
def set_repetition(self, repetition: 'Repetition | None') -> Self:
|
||||
self.repetition = repetition
|
||||
return self
|
||||
|
||||
def get_bounds_single_nonempty(self, *args, **kwargs) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds_single(*args, **kwargs)
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
def get_bounds(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
|
||||
bounds = self.get_bounds_single(*args, **kwargs)
|
||||
|
||||
if bounds is not None and self.repetition is not None:
|
||||
rep_bounds = self.repetition.get_bounds()
|
||||
if rep_bounds is None:
|
||||
return None
|
||||
bounds += rep_bounds
|
||||
return bounds
|
||||
|
@ -54,7 +54,7 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float) -> None:
|
||||
def rotation(self, val: float):
|
||||
if not numpy.size(val) == 1:
|
||||
raise MasqueError('Rotation must be a scalar')
|
||||
self._rotation = val % (2 * pi)
|
||||
@ -112,10 +112,10 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
|
||||
""" `[x_offset, y_offset]` """
|
||||
|
||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
pivot = numpy.array(pivot, dtype=float)
|
||||
cast(Positionable, self).translate(-pivot)
|
||||
cast(Rotatable, self).rotate(rotation)
|
||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004
|
||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # TODO: mypy#3004
|
||||
cast(Positionable, self).translate(+pivot)
|
||||
return self
|
||||
|
||||
|
@ -48,7 +48,7 @@ class ScalableImpl(Scalable, metaclass=ABCMeta):
|
||||
return self._scale
|
||||
|
||||
@scale.setter
|
||||
def scale(self, val: float) -> None:
|
||||
def scale(self, val: float):
|
||||
if not is_scalar(val):
|
||||
raise MasqueError('Scale must be a scalar')
|
||||
if not val > 0:
|
||||
|
@ -1,41 +1,18 @@
|
||||
"""
|
||||
Various helper functions, type definitions, etc.
|
||||
"""
|
||||
from .types import (
|
||||
layer_t as layer_t,
|
||||
annotations_t as annotations_t,
|
||||
SupportsBool as SupportsBool,
|
||||
)
|
||||
from .array import is_scalar as is_scalar
|
||||
from .autoslots import AutoSlots as AutoSlots
|
||||
from .deferreddict import DeferredDict as DeferredDict
|
||||
from .decorators import oneshot as oneshot
|
||||
from .types import layer_t, annotations_t, SupportsBool
|
||||
from .array import is_scalar
|
||||
from .autoslots import AutoSlots
|
||||
from .deferreddict import DeferredDict
|
||||
from .decorators import oneshot
|
||||
|
||||
from .bitwise import (
|
||||
get_bit as get_bit,
|
||||
set_bit as set_bit,
|
||||
)
|
||||
from .bitwise import get_bit, set_bit
|
||||
from .vertices import (
|
||||
remove_duplicate_vertices as remove_duplicate_vertices,
|
||||
remove_colinear_vertices as remove_colinear_vertices,
|
||||
poly_contains_points as poly_contains_points,
|
||||
)
|
||||
from .transform import (
|
||||
rotation_matrix_2d as rotation_matrix_2d,
|
||||
normalize_mirror as normalize_mirror,
|
||||
rotate_offsets_around as rotate_offsets_around,
|
||||
apply_transforms as apply_transforms,
|
||||
)
|
||||
from .comparisons import (
|
||||
annotation2key as annotation2key,
|
||||
annotations_lt as annotations_lt,
|
||||
annotations_eq as annotations_eq,
|
||||
layer2key as layer2key,
|
||||
ports_lt as ports_lt,
|
||||
ports_eq as ports_eq,
|
||||
rep2key as rep2key,
|
||||
remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points
|
||||
)
|
||||
from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_around
|
||||
|
||||
from . import ports2data as ports2data
|
||||
from . import ports2data
|
||||
|
||||
from . import pack2d as pack2d
|
||||
#from . import pack2d
|
||||
|
@ -12,16 +12,16 @@ class AutoSlots(ABCMeta):
|
||||
classes, they can have empty `__slots__` and their attribute type annotations
|
||||
can be used to generate a full `__slots__` for the concrete class.
|
||||
"""
|
||||
def __new__(cls, name, bases, dctn): # noqa: ANN001,ANN204
|
||||
def __new__(cls, name, bases, dctn):
|
||||
parents = set()
|
||||
for base in bases:
|
||||
parents |= set(base.mro())
|
||||
|
||||
slots = tuple(dctn.get('__slots__', ()))
|
||||
slots = tuple(dctn.get('__slots__', tuple()))
|
||||
for parent in parents:
|
||||
if not hasattr(parent, '__annotations__'):
|
||||
continue
|
||||
slots += tuple(parent.__annotations__.keys())
|
||||
slots += tuple(getattr(parent, '__annotations__').keys())
|
||||
|
||||
dctn['__slots__'] = slots
|
||||
return super().__new__(cls, name, bases, dctn)
|
||||
|
@ -1,106 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from .types import annotations_t, layer_t
|
||||
from ..ports import Port
|
||||
from ..repetition import Repetition
|
||||
|
||||
|
||||
def annotation2key(aaa: int | float | str) -> tuple[bool, Any]:
|
||||
return (isinstance(aaa, str), aaa)
|
||||
|
||||
|
||||
def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool:
|
||||
if aa is None:
|
||||
return bb is not None
|
||||
elif bb is None: # noqa: RET505
|
||||
return False
|
||||
|
||||
if len(aa) != len(bb):
|
||||
return len(aa) < len(bb)
|
||||
|
||||
keys_a = tuple(sorted(aa.keys()))
|
||||
keys_b = tuple(sorted(bb.keys()))
|
||||
if keys_a != keys_b:
|
||||
return keys_a < keys_b
|
||||
|
||||
for key in keys_a:
|
||||
va = aa[key]
|
||||
vb = bb[key]
|
||||
if len(va) != len(vb):
|
||||
return len(va) < len(vb)
|
||||
|
||||
for aaa, bbb in zip(va, vb, strict=True):
|
||||
if aaa != bbb:
|
||||
return annotation2key(aaa) < annotation2key(bbb)
|
||||
return False
|
||||
|
||||
|
||||
def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool:
|
||||
if aa is None:
|
||||
return bb is None
|
||||
elif bb is None: # noqa: RET505
|
||||
return False
|
||||
|
||||
if len(aa) != len(bb):
|
||||
return False
|
||||
|
||||
keys_a = tuple(sorted(aa.keys()))
|
||||
keys_b = tuple(sorted(bb.keys()))
|
||||
if keys_a != keys_b:
|
||||
return keys_a < keys_b
|
||||
|
||||
for key in keys_a:
|
||||
va = aa[key]
|
||||
vb = bb[key]
|
||||
if len(va) != len(vb):
|
||||
return False
|
||||
|
||||
for aaa, bbb in zip(va, vb, strict=True):
|
||||
if aaa != bbb:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def layer2key(layer: layer_t) -> tuple[bool, bool, Any]:
|
||||
is_int = isinstance(layer, int)
|
||||
is_str = isinstance(layer, str)
|
||||
layer_tup = (layer) if (is_str or is_int) else layer
|
||||
tup = (
|
||||
is_str,
|
||||
not is_int,
|
||||
layer_tup,
|
||||
)
|
||||
return tup
|
||||
|
||||
|
||||
def rep2key(repetition: Repetition | None) -> tuple[bool, Repetition | None]:
|
||||
return (repetition is None, repetition)
|
||||
|
||||
|
||||
def ports_eq(aa: dict[str, Port], bb: dict[str, Port]) -> bool:
|
||||
if len(aa) != len(bb):
|
||||
return False
|
||||
|
||||
keys = sorted(aa.keys())
|
||||
if keys != sorted(bb.keys()):
|
||||
return False
|
||||
|
||||
return all(aa[kk] == bb[kk] for kk in keys)
|
||||
|
||||
|
||||
def ports_lt(aa: dict[str, Port], bb: dict[str, Port]) -> bool:
|
||||
if len(aa) != len(bb):
|
||||
return len(aa) < len(bb)
|
||||
|
||||
aa_keys = tuple(sorted(aa.keys()))
|
||||
bb_keys = tuple(sorted(bb.keys()))
|
||||
if aa_keys != bb_keys:
|
||||
return aa_keys < bb_keys
|
||||
|
||||
for key in aa_keys:
|
||||
pa = aa[key]
|
||||
pb = bb[key]
|
||||
if pa != pb:
|
||||
return pa < pb
|
||||
return False
|
@ -1,4 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Callable
|
||||
from functools import wraps
|
||||
|
||||
from ..error import OneShotError
|
||||
@ -11,7 +11,7 @@ def oneshot(func: Callable) -> Callable:
|
||||
expired = False
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs): # noqa: ANN202
|
||||
def wrapper(*args, **kwargs):
|
||||
nonlocal expired
|
||||
if expired:
|
||||
raise OneShotError(func.__name__)
|
||||
|
@ -1,5 +1,4 @@
|
||||
from typing import TypeVar, Generic
|
||||
from collections.abc import Callable
|
||||
from typing import Callable, TypeVar, Generic
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""
|
||||
2D bin-packing
|
||||
"""
|
||||
from collections.abc import Sequence, Mapping, Callable
|
||||
from typing import Sequence, Callable, Mapping
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from ..error import MasqueError
|
||||
from ..pattern import Pattern
|
||||
from ..ref import Ref
|
||||
|
||||
|
||||
def maxrects_bssf(
|
||||
@ -17,34 +18,16 @@ def maxrects_bssf(
|
||||
allow_rejects: bool = True,
|
||||
) -> tuple[NDArray[numpy.float64], set[int]]:
|
||||
"""
|
||||
Pack rectangles `rects` into regions `containers` using the "maximal rectangles best short side fit"
|
||||
algorithm (maxrects_bssf) from "A thousand ways to pack the bin", Jukka Jylanki, 2010.
|
||||
|
||||
This algorithm gives the best results, but is asymptotically slower than `guillotine_bssf_sas`.
|
||||
|
||||
Args:
|
||||
rects: Nx2 array of rectangle sizes `[[x_size0, y_size0], ...]`.
|
||||
containers: Mx4 array of regions into which `rects` will be placed, specified using their
|
||||
corner coordinates ` [[x_min0, y_min0, x_max0, y_max0], ...]`.
|
||||
presort: If `True` (default), largest-shortest-side rectangles will be placed
|
||||
first. Otherwise, they will be placed in the order provided.
|
||||
allow_rejects: If `False`, `MasqueError` will be raised if any rectangle cannot be placed.
|
||||
|
||||
Returns:
|
||||
`[[x_min0, y_min0], ...]` placement locations for `rects`, with the same ordering.
|
||||
The second argument is a set of indicies of `rects` entries which were rejected; their
|
||||
corresponding placement locations should be ignored.
|
||||
|
||||
Raises:
|
||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||
sizes should be Nx2
|
||||
regions should be Mx4 (xmin, ymin, xmax, ymax)
|
||||
"""
|
||||
regions = numpy.asarray(containers, dtype=float)
|
||||
rect_sizes = numpy.asarray(rects, dtype=float)
|
||||
regions = numpy.array(containers, copy=False, dtype=float)
|
||||
rect_sizes = numpy.array(rects, copy=False, dtype=float)
|
||||
rect_locs = numpy.zeros_like(rect_sizes)
|
||||
rejected_inds = set()
|
||||
|
||||
if presort:
|
||||
rotated_sizes = numpy.sort(rect_sizes, axis=1) # shortest side first
|
||||
rotated_sizes = numpy.sort(rect_sizes, axis=0) # shortest side first
|
||||
rect_order = numpy.lexsort(rotated_sizes.T)[::-1] # Descending shortest side
|
||||
rect_sizes = rect_sizes[rect_order]
|
||||
|
||||
@ -62,15 +45,15 @@ def maxrects_bssf(
|
||||
|
||||
''' Place the rect '''
|
||||
# Best short-side fit (bssf) to pick a region
|
||||
region_sizes = regions[:, 2:] - regions[:, :2]
|
||||
bssf_scores = (region_sizes - rect_size).min(axis=1).astype(float)
|
||||
bssf_scores = ((regions[:, 2:] - regions[:, :2]) - rect_size).min(axis=1).astype(float)
|
||||
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
||||
rr = bssf_scores.argmin()
|
||||
if numpy.isinf(bssf_scores[rr]):
|
||||
if allow_rejects:
|
||||
rejected_inds.add(rect_ind)
|
||||
continue
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
else:
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
|
||||
# Read out location
|
||||
loc = regions[rr, :2]
|
||||
@ -98,146 +81,87 @@ def maxrects_bssf(
|
||||
r_top[:, 1] = loc[1] + rect_size[1]
|
||||
|
||||
regions = numpy.vstack((regions[~intersects], r_lft, r_bot, r_rgt, r_top))
|
||||
|
||||
if presort:
|
||||
unsort_order = rect_order.argsort()
|
||||
rect_locs = rect_locs[unsort_order]
|
||||
rejected_inds = set(unsort_order[list(rejected_inds)])
|
||||
|
||||
return rect_locs, rejected_inds
|
||||
|
||||
|
||||
def guillotine_bssf_sas(
|
||||
rects: ArrayLike,
|
||||
containers: ArrayLike,
|
||||
presort: bool = True,
|
||||
allow_rejects: bool = True,
|
||||
) -> tuple[NDArray[numpy.float64], set[int]]:
|
||||
def guillotine_bssf_sas(rect_sizes: numpy.ndarray,
|
||||
regions: numpy.ndarray,
|
||||
presort: bool = True,
|
||||
allow_rejects: bool = True,
|
||||
) -> tuple[numpy.ndarray, set[int]]:
|
||||
"""
|
||||
Pack rectangles `rects` into regions `containers` using the "guillotine best short side fit with
|
||||
shorter axis split rule" algorithm (guillotine-BSSF-SAS) from "A thousand ways to pack the bin",
|
||||
Jukka Jylanki, 2010.
|
||||
|
||||
This algorithm gives the worse results than `maxrects_bssf`, but is asymptotically faster.
|
||||
|
||||
# TODO consider adding rectangle-merge?
|
||||
# TODO guillotine could use some additional testing
|
||||
|
||||
Args:
|
||||
rects: Nx2 array of rectangle sizes `[[x_size0, y_size0], ...]`.
|
||||
containers: Mx4 array of regions into which `rects` will be placed, specified using their
|
||||
corner coordinates ` [[x_min0, y_min0, x_max0, y_max0], ...]`.
|
||||
presort: If `True` (default), largest-shortest-side rectangles will be placed
|
||||
first. Otherwise, they will be placed in the order provided.
|
||||
allow_rejects: If `False`, `MasqueError` will be raised if any rectangle cannot be placed.
|
||||
|
||||
Returns:
|
||||
`[[x_min0, y_min0], ...]` placement locations for `rects`, with the same ordering.
|
||||
The second argument is a set of indicies of `rects` entries which were rejected; their
|
||||
corresponding placement locations should be ignored.
|
||||
|
||||
Raises:
|
||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||
sizes should be Nx2
|
||||
regions should be Mx4 (xmin, ymin, xmax, ymax)
|
||||
#TODO: test me!
|
||||
# TODO add rectangle-merge?
|
||||
"""
|
||||
regions = numpy.asarray(containers, dtype=float)
|
||||
rect_sizes = numpy.asarray(rects, dtype=float)
|
||||
rect_sizes = numpy.array(rect_sizes)
|
||||
rect_locs = numpy.zeros_like(rect_sizes)
|
||||
rejected_inds = set()
|
||||
|
||||
if presort:
|
||||
rotated_sizes = numpy.sort(rect_sizes, axis=1) # shortest side first
|
||||
rotated_sizes = numpy.sort(rect_sizes, axis=0) # shortest side first
|
||||
rect_order = numpy.lexsort(rotated_sizes.T)[::-1] # Descending shortest side
|
||||
rect_sizes = rect_sizes[rect_order]
|
||||
|
||||
for rect_ind, rect_size in enumerate(rect_sizes):
|
||||
''' Place the rect '''
|
||||
# Best short-side fit (bssf) to pick a region
|
||||
region_sizes = regions[:, 2:] - regions[:, :2]
|
||||
bssf_scores = (region_sizes - rect_size).min(axis=1).astype(float)
|
||||
bssf_scores = ((regions[:, 2:] - regions[:, :2]) - rect_size).min(axis=1).astype(float)
|
||||
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
||||
rr = bssf_scores.argmin()
|
||||
if numpy.isinf(bssf_scores[rr]):
|
||||
if allow_rejects:
|
||||
rejected_inds.add(rect_ind)
|
||||
continue
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
else:
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
|
||||
# Read out location
|
||||
loc = regions[rr, :2]
|
||||
rect_locs[rect_ind] = loc
|
||||
|
||||
region_size = region_sizes[rr]
|
||||
region_size = regions[rr, 2:] - loc
|
||||
split_horiz = region_size[0] < region_size[1]
|
||||
|
||||
new_region0 = regions[rr].copy()
|
||||
new_region1 = new_region0.copy()
|
||||
split_vertex = loc + rect_size
|
||||
split_vert = loc + rect_size
|
||||
if split_horiz:
|
||||
new_region0[2] = split_vertex[0]
|
||||
new_region0[1] = split_vertex[1]
|
||||
new_region1[0] = split_vertex[0]
|
||||
new_region0[2] = split_vert[0]
|
||||
new_region0[1] = split_vert[1]
|
||||
new_region1[0] = split_vert[0]
|
||||
else:
|
||||
new_region0[3] = split_vertex[1]
|
||||
new_region0[0] = split_vertex[0]
|
||||
new_region1[1] = split_vertex[1]
|
||||
new_region0[3] = split_vert[1]
|
||||
new_region0[0] = split_vert[0]
|
||||
new_region1[1] = split_vert[1]
|
||||
|
||||
regions = numpy.vstack((regions[:rr], regions[rr + 1:],
|
||||
new_region0, new_region1))
|
||||
|
||||
if presort:
|
||||
unsort_order = rect_order.argsort()
|
||||
rect_locs = rect_locs[unsort_order]
|
||||
rejected_inds = set(unsort_order[list(rejected_inds)])
|
||||
|
||||
return rect_locs, rejected_inds
|
||||
|
||||
|
||||
def pack_patterns(
|
||||
library: Mapping[str, Pattern],
|
||||
patterns: Sequence[str],
|
||||
containers: ArrayLike,
|
||||
regions: numpy.ndarray,
|
||||
spacing: tuple[float, float],
|
||||
presort: bool = True,
|
||||
allow_rejects: bool = True,
|
||||
packer: Callable = maxrects_bssf,
|
||||
) -> tuple[Pattern, list[str]]:
|
||||
"""
|
||||
Pick placement locations for `patterns` inside the regions specified by `containers`.
|
||||
No rotations are performed.
|
||||
|
||||
Args:
|
||||
library: Library from which `Pattern` objects will be drawn.
|
||||
patterns: Sequence of pattern names which are to be placed.
|
||||
containers: Mx4 array of regions into which `patterns` will be placed, specified using their
|
||||
corner coordinates ` [[x_min0, y_min0, x_max0, y_max0], ...]`.
|
||||
spacing: (x, y) spacing between adjacent patterns. Patterns are effectively expanded outwards
|
||||
by `spacing / 2` prior to placement, so this also affects pattern position relative to
|
||||
container edges.
|
||||
presort: If `True` (default), largest-shortest-side rectangles will be placed
|
||||
first. Otherwise, they will be placed in the order provided.
|
||||
allow_rejects: If `False`, `MasqueError` will be raised if any rectangle cannot be placed.
|
||||
packer: Bin-packing method; see the other functions in this module (namely `maxrects_bssf`
|
||||
and `guillotine_bssf_sas`).
|
||||
|
||||
Returns:
|
||||
A `Pattern` containing one `Ref` for each entry in `patterns`.
|
||||
A list of "rejected" pattern names, for which a valid placement location could not be found.
|
||||
|
||||
Raises:
|
||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||
"""
|
||||
|
||||
half_spacing = numpy.asarray(spacing, dtype=float) / 2
|
||||
half_spacing = numpy.array(spacing) / 2
|
||||
|
||||
bounds = [library[pp].get_bounds() for pp in patterns]
|
||||
sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds]
|
||||
offsets = [half_spacing - bb[0] if bb is not None else (0, 0) for bb in bounds]
|
||||
|
||||
locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects)
|
||||
locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects)
|
||||
|
||||
pat = Pattern()
|
||||
for pp, oo, loc in zip(patterns, offsets, locations, strict=True):
|
||||
pat.ref(pp, offset=oo + loc)
|
||||
pat.refs = [Ref(pp, offset=oo + loc)
|
||||
for pp, oo, loc in zip(patterns, offsets, locations)]
|
||||
|
||||
rejects = [patterns[ii] for ii in reject_inds]
|
||||
return pat, rejects
|
||||
|
@ -6,13 +6,13 @@ and retrieving it (`data_to_ports`).
|
||||
the port locations. This particular approach is just a sensible default; feel free to
|
||||
to write equivalent functions for your own format or alternate storage methods.
|
||||
"""
|
||||
from collections.abc import Sequence, Mapping
|
||||
from typing import Sequence, Mapping
|
||||
import logging
|
||||
from itertools import chain
|
||||
|
||||
import numpy
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..label import Label
|
||||
from ..utils import layer_t
|
||||
from ..ports import Port
|
||||
from ..error import PatternError
|
||||
@ -44,7 +44,9 @@ def ports_to_data(pattern: Pattern, layer: layer_t) -> Pattern:
|
||||
angle_deg = numpy.inf
|
||||
else:
|
||||
angle_deg = numpy.rad2deg(port.rotation)
|
||||
pattern.label(layer=layer, string=f'{name}:{port.ptype} {angle_deg:g}', offset=port.offset)
|
||||
pattern.labels += [
|
||||
Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset)
|
||||
]
|
||||
return pattern
|
||||
|
||||
|
||||
@ -60,8 +62,8 @@ def data_to_ports(
|
||||
# TODO missing ok?
|
||||
) -> Pattern:
|
||||
"""
|
||||
# TODO fixup documentation in ports2data
|
||||
# TODO move to utils.file?
|
||||
# TODO fixup documentation in port_utils
|
||||
# TODO move port_utils to utils.file?
|
||||
Examine `pattern` for labels specifying port info, and use that info
|
||||
to fill out its `ports` attribute.
|
||||
|
||||
@ -95,7 +97,7 @@ def data_to_ports(
|
||||
|
||||
# Load ports for all subpatterns, and use any we find
|
||||
found_ports = False
|
||||
for target in pattern.refs:
|
||||
for target in set(rr.target for rr in pattern.refs):
|
||||
if target is None:
|
||||
continue
|
||||
pp = data_to_ports(
|
||||
@ -111,20 +113,17 @@ def data_to_ports(
|
||||
if not found_ports:
|
||||
return pattern
|
||||
|
||||
for target, refs in pattern.refs.items():
|
||||
if target is None:
|
||||
for ref in pattern.refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
if not refs:
|
||||
aa = library.abstract(ref.target)
|
||||
if not aa.ports:
|
||||
continue
|
||||
|
||||
for ref in refs:
|
||||
aa = library.abstract(target)
|
||||
if not aa.ports:
|
||||
break
|
||||
aa.apply_ref_transform(ref)
|
||||
|
||||
aa.apply_ref_transform(ref)
|
||||
pattern.check_ports(other_names=aa.ports.keys())
|
||||
pattern.ports.update(aa.ports)
|
||||
pattern.check_ports(other_names=aa.ports.keys())
|
||||
pattern.ports.update(aa.ports)
|
||||
return pattern
|
||||
|
||||
|
||||
@ -150,13 +149,13 @@ def data_to_ports_flat(
|
||||
Returns:
|
||||
The updated `pattern`. Port labels are not removed.
|
||||
"""
|
||||
labels = list(chain.from_iterable(pattern.labels[layer] for layer in layers))
|
||||
labels = [ll for ll in pattern.labels if ll.layer in layers]
|
||||
if not labels:
|
||||
return pattern
|
||||
|
||||
pstr = cell_name if cell_name is not None else repr(pattern)
|
||||
if pattern.ports:
|
||||
raise PatternError(f'Pattern "{pstr}" has pre-existing ports!')
|
||||
raise PatternError('Pattern "{pstr}" has pre-existing ports!')
|
||||
|
||||
local_ports = {}
|
||||
for label in labels:
|
||||
|
@ -1,15 +1,12 @@
|
||||
"""
|
||||
Geometric transforms
|
||||
"""
|
||||
from collections.abc import Sequence
|
||||
from functools import lru_cache
|
||||
from typing import Sequence
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
from numpy import pi
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
@lru_cache
|
||||
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
2D rotation matrix for rotating counterclockwise around the origin.
|
||||
@ -20,15 +17,8 @@ def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||
Returns:
|
||||
rotation matrix
|
||||
"""
|
||||
arr = numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
|
||||
[numpy.sin(theta), +numpy.cos(theta)]])
|
||||
|
||||
# If this was a manhattan rotation, round to remove some inacuraccies in sin & cos
|
||||
if numpy.isclose(theta % (pi / 2), 0):
|
||||
arr = numpy.round(arr)
|
||||
|
||||
arr.flags.writeable = False
|
||||
return arr
|
||||
return numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
|
||||
[numpy.sin(theta), +numpy.cos(theta)]])
|
||||
|
||||
|
||||
def normalize_mirror(mirrored: Sequence[bool]) -> tuple[bool, float]:
|
||||
@ -57,62 +47,8 @@ def rotate_offsets_around(
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Rotates offsets around a pivot point.
|
||||
|
||||
Args:
|
||||
offsets: Nx2 array, rows are (x, y) offsets
|
||||
pivot: (x, y) location to rotate around
|
||||
angle: rotation angle in radians
|
||||
|
||||
Returns:
|
||||
Nx2 ndarray of (x, y) position after the rotation is applied.
|
||||
"""
|
||||
offsets -= pivot
|
||||
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
|
||||
offsets += pivot
|
||||
return offsets
|
||||
|
||||
|
||||
def apply_transforms(
|
||||
outer: ArrayLike,
|
||||
inner: ArrayLike,
|
||||
tensor: bool = False,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Apply a set of transforms (`outer`) to a second set (`inner`).
|
||||
This is used to find the "absolute" transform for nested `Ref`s.
|
||||
|
||||
The two transforms should be of shape Ox4 and Ix4.
|
||||
Rows should be of the form `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||
The output will be of the form (O*I)x4 (if `tensor=False`) or OxIx4 (`tensor=True`).
|
||||
|
||||
Args:
|
||||
outer: Transforms for the container refs. Shape Ox4.
|
||||
inner: Transforms for the contained refs. Shape Ix4.
|
||||
tensor: If `True`, an OxIx4 array is returned, with `result[oo, ii, :]` corresponding
|
||||
to the `oo`th `outer` transform applied to the `ii`th inner transform.
|
||||
If `False` (default), this is concatenated into `(O*I)x4` to allow simple
|
||||
chaining into additional `apply_transforms()` calls.
|
||||
|
||||
Returns:
|
||||
OxIx4 or (O*I)x4 array. Final dimension is
|
||||
`(total_x, total_y, total_rotation_ccw_rad, net_mirrored_x)`.
|
||||
"""
|
||||
outer = numpy.atleast_2d(outer).astype(float, copy=False)
|
||||
inner = numpy.atleast_2d(inner).astype(float, copy=False)
|
||||
|
||||
# If mirrored, flip y's
|
||||
xy_mir = numpy.tile(inner[:, :2], (outer.shape[0], 1, 1)) # dims are outer, inner, xyrm
|
||||
xy_mir[outer[:, 3].astype(bool), :, 1] *= -1
|
||||
|
||||
rot_mats = [rotation_matrix_2d(angle) for angle in outer[:, 2]]
|
||||
xy = numpy.einsum('ort,oit->oir', rot_mats, xy_mir)
|
||||
|
||||
tot = numpy.empty((outer.shape[0], inner.shape[0], 4))
|
||||
tot[:, :, :2] = outer[:, None, :2] + xy
|
||||
tot[:, :, 2:] = outer[:, None, 2:] + inner[None, :, 2:] # sum rotations and mirrored
|
||||
tot[:, :, 2] %= 2 * pi # clamp rot
|
||||
tot[:, :, 3] %= 2 # clamp mirrored
|
||||
|
||||
if tensor:
|
||||
return tot
|
||||
return numpy.concatenate(tot)
|
||||
|
@ -15,9 +15,9 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) ->
|
||||
(i.e. the last vertex will be removed if it is the same as the first)
|
||||
|
||||
Returns:
|
||||
`vertices` with no consecutive duplicates. This may be a view into the original array.
|
||||
`vertices` with no consecutive duplicates.
|
||||
"""
|
||||
vertices = numpy.asarray(vertices)
|
||||
vertices = numpy.array(vertices)
|
||||
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
|
||||
if not closed_path:
|
||||
duplicates[0] = False
|
||||
@ -35,7 +35,7 @@ def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> N
|
||||
closed path. If `False`, the path is assumed to be open. Default `True`.
|
||||
|
||||
Returns:
|
||||
`vertices` with colinear (superflous) vertices removed. May be a view into the original array.
|
||||
`vertices` with colinear (superflous) vertices removed.
|
||||
"""
|
||||
vertices = remove_duplicate_vertices(vertices)
|
||||
|
||||
@ -73,8 +73,8 @@ def poly_contains_points(
|
||||
Returns:
|
||||
ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...]
|
||||
"""
|
||||
points = numpy.asarray(points, dtype=float)
|
||||
vertices = numpy.asarray(vertices, dtype=float)
|
||||
points = numpy.array(points, copy=False)
|
||||
vertices = numpy.array(vertices, copy=False)
|
||||
|
||||
if points.size == 0:
|
||||
return numpy.zeros(0, dtype=numpy.int8)
|
||||
|
@ -39,11 +39,11 @@ classifiers = [
|
||||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||
"Topic :: Scientific/Engineering :: Visualization",
|
||||
]
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.8"
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"numpy>=1.26",
|
||||
"klamath~=1.4",
|
||||
"numpy~=1.21",
|
||||
"klamath~=1.2",
|
||||
]
|
||||
|
||||
|
||||
@ -57,36 +57,3 @@ svg = ["svgwrite"]
|
||||
visualize = ["matplotlib"]
|
||||
text = ["matplotlib", "freetype-py"]
|
||||
|
||||
|
||||
[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
|
||||
]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user