Compare commits
206 commits
master
...
pather_rew
| Author | SHA1 | Date | |
|---|---|---|---|
| bdc4dfdd06 | |||
| 6cf9600193 | |||
| e6f5136357 | |||
| 778b3d9be7 | |||
| 02f0833fb3 | |||
| 47f150f579 | |||
| 84106dc355 | |||
| 429e687666 | |||
| c501a8ff99 | |||
| fd2698c503 | |||
| 8100d8095a | |||
| 2c5243237e | |||
| cf0a245143 | |||
| bbe3586ba9 | |||
| e071bd89b0 | |||
| 06ed2ce54a | |||
| 0f2b4d713b | |||
| 524503031c | |||
| ce7bf5ce70 | |||
| f0a4b08a31 | |||
| b3a1489258 | |||
| d366db5a62 | |||
| 20f37ea0f7 | |||
| 6fd73b9d46 | |||
| 32744512e0 | |||
| 75a9114709 | |||
| df578d7764 | |||
| 786716fc62 | |||
| a82365ec8c | |||
| 28be89f047 | |||
| afc49f945d | |||
| ce46cc18dc | |||
| 7c50f95fde | |||
| ae314cce93 | |||
| 09a95a6608 | |||
| fbe138d443 | |||
| 4b416745da | |||
| 0830dce50c | |||
| ac87179da2 | |||
| f0eea0382b | |||
| 0c9b435e94 | |||
| f461222852 | |||
| 9767ee4e62 | |||
| 395ad4df9d | |||
| 35b42c397b | |||
| 6a7b3b2259 | |||
| 8d50f497f1 | |||
| 2176d56b4c | |||
| f1e25debec | |||
| 4b07bb9e25 | |||
| 2952e6ef8f | |||
| c303a0c114 | |||
| f34b9b2f5c | |||
| 89cdd23f00 | |||
| 620b001af5 | |||
| 46a3559391 | |||
| 08421d6a54 | |||
| 462a05a665 | |||
| 2b29e46b93 | |||
| 2e0b64bdab | |||
| 20c845a881 | |||
| 707a16fe64 | |||
| 932565d531 | |||
| e7f847d4c7 | |||
| 3beadd2bf0 | |||
| 1bcf5901d6 | |||
| 56e401196a | |||
| 83ec64158a | |||
| aa7007881f | |||
| d03fafcaf6 | |||
| d3be6aeba3 | |||
| ffbe15c465 | |||
| b44c962e07 | |||
| 20bd0640e1 | |||
| 4ae8115139 | |||
| c2ef3e4217 | |||
| c32168dc64 | |||
| b843ffb4d3 | |||
| 9adfcac437 | |||
| 26cc0290b9 | |||
| 548b51df47 | |||
| 06f8611a90 | |||
| 9ede16df5d | |||
| add82e955d | |||
| cfec9e8c76 | |||
| 2275bf415a | |||
| fa3dfa1e74 | |||
| 75dc391540 | |||
| feb5d87cf4 | |||
| 5f91bd9c6c | |||
| db22237369 | |||
| a6ea5c08e6 | |||
| 3792248cd1 | |||
| e8083cc24c | |||
| d307589995 | |||
| ea93a7ef37 | |||
| 495babf837 | |||
| 5d20a061fd | |||
| 25b8fe8448 | |||
| f154303bef | |||
| 5596e2b1af | |||
| 6c42049b23 | |||
| da20922224 | |||
| b8ee4bb05d | |||
| 169f66cc85 | |||
| a38c5bb085 | |||
| 0ad89d6d95 | |||
| 6c96968341 | |||
| b7143e3287 | |||
| 0cce5e0586 | |||
| 36cb86a15d | |||
| 5e0936e15f | |||
| a467a0baca | |||
| 564ff10db3 | |||
| e261585894 | |||
| f42114bf43 | |||
| 5eb460ecb7 | |||
| fb822829ec | |||
| 838c742651 | |||
| 9a76ce5b66 | |||
| 2019fc0d74 | |||
| e3f8d28529 | |||
| 9296011d4b | |||
| 92d0140093 | |||
| c4dc9f9573 | |||
| 0b8e11e8bf | |||
| 5989e45906 | |||
| 7eec2b7acf | |||
| 2a6458b1ac | |||
| 9ee3c7ff89 | |||
| 3bedab2301 | |||
| 4eb1d8d486 | |||
| 3ceeba23b8 | |||
| 963103b859 | |||
| e5a6aab940 | |||
| 042941c838 | |||
| 0f63acbad0 | |||
| a0d7d0ed26 | |||
| d32a5ee762 | |||
| 19dafad157 | |||
| 5cb608734d | |||
| d0b48e6bfc | |||
| ef5c8c715e | |||
| 049864ddc7 | |||
| 3bf7efc404 | |||
| 74fa377450 | |||
| c3581243c8 | |||
| 338c123fb1 | |||
| a89f07c441 | |||
| bb7f4906af | |||
| 2513c7f8fd | |||
| ad4e9af59d | |||
| 46555dbd4d | |||
| 26e6a44559 | |||
| 32681edb47 | |||
| 84f37195ad | |||
| 0189756df4 | |||
| 1070815730 | |||
| 8a45c6d8d6 | |||
| 9d6fb985d8 | |||
| 69ac25078c | |||
| babbe78daa | |||
| 16875e9cd6 | |||
| 4332cf14c0 | |||
| ff8ca92963 | |||
| ed021e3d81 | |||
| 07a25ec290 | |||
| 504f89796c | |||
| 0f49924aa6 | |||
| ebfe1b559c | |||
| 7ad59d6b89 | |||
| 5d040061f4 | |||
| f42e720c68 | |||
| cf822c7dcf | |||
| 59e996e680 | |||
| abf236a046 | |||
| d40bdb1cb2 | |||
| 5e08579498 | |||
| c18e5b8d3e | |||
| 48f7569c1f | |||
| 8a56679884 | |||
| 1cce6c1f70 | |||
| d9adb4e1b9 | |||
| 1de76bff47 | |||
| 9bb0d5190d | |||
| ad49276345 | |||
| fe70d0574b | |||
| 36fed84249 | |||
| 278f0783da | |||
| 72f462d077 | |||
| 66d6fae2bd | |||
| 2b7ad00204 | |||
| 2d63e72802 | |||
| 51ced2fe83 | |||
| 19fac463e4 | |||
| 44986bac67 | |||
| accad3db9f | |||
| 05098c0c13 | |||
| f64b080b15 | |||
| 54f3b273bc | |||
| add0600bac | |||
| 737d41d592 | |||
| 395244ee83 | |||
| 43ccd8de2f | |||
| dfa0259997 | |||
| 37418d2137 |
93 changed files with 10223 additions and 3072 deletions
299
MIGRATION.md
Normal file
299
MIGRATION.md
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
# Migration Guide
|
||||
|
||||
This guide covers changes between the git tag `release` and the current tree.
|
||||
At `release`, `masque.__version__` was `3.3`; the current tree reports `3.4`.
|
||||
|
||||
Most downstream changes are in `masque/builder/*`, but there are a few other
|
||||
API changes that may require code updates.
|
||||
|
||||
## Routing API: renamed and consolidated
|
||||
|
||||
The routing helpers were consolidated into a single implementation in
|
||||
`masque/builder/pather.py`.
|
||||
|
||||
The biggest migration point is that the old routing verbs were renamed:
|
||||
|
||||
| Old API | New API |
|
||||
| --- | --- |
|
||||
| `Pather.path(...)` | `Pather.trace(...)` |
|
||||
| `Pather.path_to(...)` | `Pather.trace_to(...)` |
|
||||
| `Pather.mpath(...)` | `Pather.trace(...)` / `Pather.trace_to(...)` with multiple ports |
|
||||
| `Pather.pathS(...)` | `Pather.jog(...)` |
|
||||
| `Pather.pathU(...)` | `Pather.uturn(...)` |
|
||||
| `Pather.path_into(...)` | `Pather.trace_into(...)` |
|
||||
| `Pather.path_from(src, dst)` | `Pather.at(src).trace_into(dst)` |
|
||||
| `RenderPather.path(...)` | `Pather(..., auto_render=False).trace(...)` |
|
||||
| `RenderPather.path_to(...)` | `Pather(..., auto_render=False).trace_to(...)` |
|
||||
| `RenderPather.mpath(...)` | `Pather(..., auto_render=False).trace(...)` / `Pather(..., auto_render=False).trace_to(...)` |
|
||||
| `RenderPather.pathS(...)` | `Pather(..., auto_render=False).jog(...)` |
|
||||
| `RenderPather.pathU(...)` | `Pather(..., auto_render=False).uturn(...)` |
|
||||
| `RenderPather.path_into(...)` | `Pather(..., auto_render=False).trace_into(...)` |
|
||||
| `RenderPather.path_from(src, dst)` | `Pather(..., auto_render=False).at(src).trace_into(dst)` |
|
||||
|
||||
There are also new convenience wrappers:
|
||||
|
||||
- `straight(...)` for `trace_to(..., ccw=None, ...)`
|
||||
- `ccw(...)` for `trace_to(..., ccw=True, ...)`
|
||||
- `cw(...)` for `trace_to(..., ccw=False, ...)`
|
||||
- `jog(...)` for S-bends
|
||||
- `uturn(...)` for U-bends
|
||||
|
||||
Important: `Pather.path()` is no longer the routing API. It now forwards to
|
||||
`Pattern.path()` and creates a geometric `Path` element. Any old routing code
|
||||
that still calls `pather.path(...)` must be renamed.
|
||||
|
||||
### Common rewrites
|
||||
|
||||
```python
|
||||
# old
|
||||
pather.path('VCC', False, 6_000)
|
||||
pather.path_to('VCC', None, x=0)
|
||||
pather.mpath(['GND', 'VCC'], True, xmax=-10_000, spacing=5_000)
|
||||
pather.pathS('VCC', offset=-2_000, length=8_000)
|
||||
pather.pathU('VCC', offset=4_000, length=5_000)
|
||||
pather.path_into('src', 'dst')
|
||||
pather.path_from('src', 'dst')
|
||||
|
||||
# new
|
||||
pather.cw('VCC', 6_000)
|
||||
pather.straight('VCC', x=0)
|
||||
pather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000)
|
||||
pather.jog('VCC', offset=-2_000, length=8_000)
|
||||
pather.uturn('VCC', offset=4_000, length=5_000)
|
||||
pather.trace_into('src', 'dst')
|
||||
pather.at('src').trace_into('dst')
|
||||
```
|
||||
|
||||
If you prefer the more explicit spelling, `trace(...)` and `trace_to(...)`
|
||||
remain the underlying primitives:
|
||||
|
||||
```python
|
||||
pather.trace('VCC', False, 6_000)
|
||||
pather.trace_to('VCC', None, x=0)
|
||||
```
|
||||
|
||||
## `PortPather` and `.at(...)`
|
||||
|
||||
Routing can now be written in a fluent style via `.at(...)`, which returns a
|
||||
`PortPather`.
|
||||
|
||||
```python
|
||||
(rpather.at('VCC')
|
||||
.trace(False, length=6_000)
|
||||
.trace_to(None, x=0)
|
||||
)
|
||||
```
|
||||
|
||||
This is additive, not required for migration. Existing code can stay with the
|
||||
non-fluent `Pather` methods after renaming the verbs above.
|
||||
|
||||
Old `PortPather` helper names were also cleaned up:
|
||||
|
||||
| Old API | New API |
|
||||
| --- | --- |
|
||||
| `save_copy(...)` | `mark(...)` |
|
||||
| `rename_to(...)` | `rename(...)` |
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
# old
|
||||
pp.save_copy('branch')
|
||||
pp.rename_to('feed')
|
||||
|
||||
# new
|
||||
pp.mark('branch')
|
||||
pp.rename('feed')
|
||||
```
|
||||
|
||||
## Imports and module layout
|
||||
|
||||
`Pather` now provides the remaining builder/routing surface in
|
||||
`masque/builder/pather.py`. The old module files
|
||||
`masque/builder/builder.py` and `masque/builder/renderpather.py` were removed.
|
||||
|
||||
Update imports like this:
|
||||
|
||||
```python
|
||||
# old
|
||||
from masque.builder.builder import Builder
|
||||
from masque.builder.renderpather import RenderPather
|
||||
|
||||
# new
|
||||
from masque.builder import Pather
|
||||
|
||||
builder = Pather(...)
|
||||
deferred = Pather(..., auto_render=False)
|
||||
```
|
||||
|
||||
Top-level imports from `masque` also continue to work.
|
||||
|
||||
`Pather` now defaults to `auto_render=True`, so plain construction replaces the
|
||||
old `Builder` behavior. Use `Pather(..., auto_render=False)` where you
|
||||
previously used `RenderPather`.
|
||||
|
||||
## `BasicTool` was replaced
|
||||
|
||||
`BasicTool` is no longer exported. Use:
|
||||
|
||||
- `SimpleTool` for the simple "one straight generator + one bend cell" case
|
||||
- `AutoTool` if you need transitions, multiple candidate straights/bends, or
|
||||
S-bends/U-bends
|
||||
|
||||
### Old `BasicTool`
|
||||
|
||||
```python
|
||||
from masque.builder.tools import BasicTool
|
||||
|
||||
tool = BasicTool(
|
||||
straight=(make_straight, 'input', 'output'),
|
||||
bend=(lib.abstract('bend'), 'input', 'output'),
|
||||
transitions={
|
||||
'm2wire': (lib.abstract('via'), 'top', 'bottom'),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### New `AutoTool`
|
||||
|
||||
```python
|
||||
from masque.builder.tools import AutoTool
|
||||
|
||||
tool = AutoTool(
|
||||
straights=[
|
||||
AutoTool.Straight(
|
||||
ptype='m1wire',
|
||||
fn=make_straight,
|
||||
in_port_name='input',
|
||||
out_port_name='output',
|
||||
),
|
||||
],
|
||||
bends=[
|
||||
AutoTool.Bend(
|
||||
abstract=lib.abstract('bend'),
|
||||
in_port_name='input',
|
||||
out_port_name='output',
|
||||
clockwise=True,
|
||||
),
|
||||
],
|
||||
sbends=[],
|
||||
transitions={
|
||||
('m2wire', 'm1wire'): AutoTool.Transition(
|
||||
lib.abstract('via'),
|
||||
'top',
|
||||
'bottom',
|
||||
),
|
||||
},
|
||||
default_out_ptype='m1wire',
|
||||
)
|
||||
```
|
||||
|
||||
The key differences are:
|
||||
|
||||
- `BasicTool` -> `SimpleTool` or `AutoTool`
|
||||
- `straight=(fn, in_name, out_name)` -> `straights=[AutoTool.Straight(...)]`
|
||||
- `bend=(abstract, in_name, out_name)` -> `bends=[AutoTool.Bend(...)]`
|
||||
- transition keys are now `(external_ptype, internal_ptype)` tuples
|
||||
- transitions use `AutoTool.Transition(...)` instead of raw tuples
|
||||
|
||||
If your old `BasicTool` usage did not rely on transitions or multiple routing
|
||||
options, `SimpleTool` is the closest replacement.
|
||||
|
||||
## Custom `Tool` subclasses
|
||||
|
||||
If you maintain your own `Tool` subclass, the interface changed:
|
||||
|
||||
- `Tool.path(...)` became `Tool.traceL(...)`
|
||||
- `Tool.traceS(...)` and `Tool.traceU(...)` were added for native S/U routes
|
||||
- `planL()` / `planS()` / `planU()` remain the planning hooks used by deferred rendering
|
||||
|
||||
In practice, a minimal old implementation like:
|
||||
|
||||
```python
|
||||
class MyTool(Tool):
|
||||
def path(self, ccw, length, **kwargs):
|
||||
...
|
||||
```
|
||||
|
||||
should now become:
|
||||
|
||||
```python
|
||||
class MyTool(Tool):
|
||||
def traceL(self, ccw, length, **kwargs):
|
||||
...
|
||||
```
|
||||
|
||||
If you do not implement `traceS()` or `traceU()`, the unified pather will
|
||||
either fall back to the planning hooks or synthesize those routes from simpler
|
||||
steps where possible.
|
||||
|
||||
## Transform semantics changed
|
||||
|
||||
The other major user-visible change is that `mirror()` and `rotate()` are now
|
||||
treated more consistently as intrinsic transforms on low-level objects.
|
||||
|
||||
The practical migration rule is:
|
||||
|
||||
- use `mirror()` / `rotate()` when you want to change the object relative to its
|
||||
own origin
|
||||
- use `flip_across(...)`, `rotate_around(...)`, or container-level transforms
|
||||
when you want to move the object in its parent coordinate system
|
||||
|
||||
### Example: `Port`
|
||||
|
||||
Old behavior:
|
||||
|
||||
```python
|
||||
port.mirror(0) # changed both offset and orientation
|
||||
```
|
||||
|
||||
New behavior:
|
||||
|
||||
```python
|
||||
port.mirror(0) # changes orientation only
|
||||
port.flip_across(axis=0) # old "mirror in the parent pattern" behavior
|
||||
```
|
||||
|
||||
### What to audit
|
||||
|
||||
Check code that calls:
|
||||
|
||||
- `Port.mirror(...)`
|
||||
- `Ref.rotate(...)`
|
||||
- `Ref.mirror(...)`
|
||||
- `Label.rotate_around(...)` / `Label.mirror(...)`
|
||||
|
||||
If that code expected offsets or repetition grids to move automatically, it
|
||||
needs updating. For whole-pattern transforms, prefer calling `Pattern.mirror()`
|
||||
or `Pattern.rotate_around(...)` at the container level.
|
||||
|
||||
## Other user-facing changes
|
||||
|
||||
### DXF environments
|
||||
|
||||
If you install the DXF extra, the supported `ezdxf` baseline moved from
|
||||
`~=1.0.2` to `~=1.4`. Any pinned environments should be updated accordingly.
|
||||
|
||||
### New exports
|
||||
|
||||
These are additive, but available now from `masque` and `masque.builder`:
|
||||
|
||||
- `PortPather`
|
||||
- `SimpleTool`
|
||||
- `AutoTool`
|
||||
- `boolean`
|
||||
|
||||
## Minimal migration checklist
|
||||
|
||||
If your code uses the routing stack, do these first:
|
||||
|
||||
1. Replace `path`/`path_to`/`mpath`/`path_into` calls with
|
||||
`trace`/`trace_to`/multi-port `trace`/`trace_into`.
|
||||
2. Replace `BasicTool` with `SimpleTool` or `AutoTool`.
|
||||
3. Fix imports that still reference `masque.builder.builder` or
|
||||
`masque.builder.renderpather`.
|
||||
4. Audit any low-level `mirror()` usage, especially on `Port` and `Ref`.
|
||||
|
||||
If your code only uses `Pattern`, `Library`, `place()`, and `plug()` without the
|
||||
routing helpers, you may not need any changes beyond the transform audit and any
|
||||
stale imports.
|
||||
12
README.md
12
README.md
|
|
@ -145,7 +145,7 @@ References are accomplished by listing the target's name, not its `Pattern` obje
|
|||
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
|
||||
* Another way is use `Pather.place()` or `Pather.plug()`, which automatically creates
|
||||
an `Abstract` from its internally-referenced `Library`.
|
||||
|
||||
|
||||
|
|
@ -193,8 +193,8 @@ 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, ...)
|
||||
# With a `Pather` and `place()`/`plug()` the `lib <<` portion can be implicit:
|
||||
my_builder = Pather(library=lib, ...)
|
||||
...
|
||||
my_builder.place(make_tree(...))
|
||||
```
|
||||
|
|
@ -277,12 +277,6 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...)
|
|||
|
||||
## TODO
|
||||
|
||||
* Rework naming/args for path-related (Builder, PortPather, path/pathL/pathS/pathU, path_to, mpath)
|
||||
* PolyCollection & arrow-based read/write
|
||||
* pather and renderpather examples, including .at() (PortPather)
|
||||
* Bus-to-bus connections?
|
||||
* Tests tests tests
|
||||
* Better interface for polygon operations (e.g. with `pyclipper`)
|
||||
- de-embedding
|
||||
- boolean ops
|
||||
* tuple / string layer auto-translation
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from masque.file import gdsii
|
|||
from masque import Arc, Pattern
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
pat = Pattern()
|
||||
layer = (0, 0)
|
||||
pat.shapes[layer].extend([
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import numpy
|
||||
from pyclipper import (
|
||||
Pyclipper, PT_CLIP, PT_SUBJECT, CT_UNION, CT_INTERSECTION, PFT_NONZERO,
|
||||
scale_to_clipper, scale_from_clipper,
|
||||
Pyclipper, PT_SUBJECT, CT_UNION, PFT_NONZERO,
|
||||
)
|
||||
p = Pyclipper()
|
||||
p.AddPaths([
|
||||
|
|
@ -12,8 +10,8 @@ p.AddPaths([
|
|||
], PT_SUBJECT, closed=True)
|
||||
#p.Execute2?
|
||||
#p.Execute?
|
||||
p.Execute(PT_UNION, PT_NONZERO, PT_NONZERO)
|
||||
p.Execute(CT_UNION, PT_NONZERO, PT_NONZERO)
|
||||
p.Execute(CT_UNION, PFT_NONZERO, PFT_NONZERO)
|
||||
p.Execute(CT_UNION, PFT_NONZERO, PFT_NONZERO)
|
||||
p.Execute(CT_UNION, PFT_NONZERO, PFT_NONZERO)
|
||||
|
||||
p = Pyclipper()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from masque.file import gdsii, dxf, oasis
|
|||
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
lib = Library()
|
||||
|
||||
cell_name = 'ellip_grating'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
masque Tutorial
|
||||
===============
|
||||
|
||||
These examples are meant to be read roughly in order.
|
||||
|
||||
- Start with `basic_shapes.py` for the core `Pattern` / GDS concepts.
|
||||
- Then read `devices.py` and `library.py` for hierarchical composition and libraries.
|
||||
- Read the `pather*` tutorials separately when you want routing helpers.
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
|
|
@ -8,24 +14,30 @@ Contents
|
|||
* Draw basic geometry
|
||||
* Export to GDS
|
||||
- [devices](devices.py)
|
||||
* Build hierarchical photonic-crystal example devices
|
||||
* Reference other patterns
|
||||
* Add ports to a pattern
|
||||
* Snap ports together to build a circuit
|
||||
* Use `Pather` to snap ports together into a circuit
|
||||
* Check for dangling references
|
||||
- [library](library.py)
|
||||
* Continue from `devices.py` using a lazy library
|
||||
* 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),
|
||||
* Use `AutoTool` to generate paths
|
||||
* Use `AutoTool` to automatically transition between path types
|
||||
- [renderpather](renderpather.py)
|
||||
* Use `Pather(auto_render=False)` and `PathTool` to build a layout similar to the one in [pather](pather.py),
|
||||
but using `Path` shapes instead of `Polygon`s.
|
||||
- [port_pather](port_pather.py)
|
||||
* Use `PortPather` and the `.at()` syntax for more concise routing
|
||||
* Advanced port manipulation and connections
|
||||
|
||||
|
||||
Additionaly, [pcgen](pcgen.py) is a utility module for generating photonic crystal lattices.
|
||||
Additionally, [pcgen](pcgen.py) is a utility module used by `devices.py` for generating
|
||||
photonic-crystal lattices; it is support code rather than a step-by-step tutorial.
|
||||
|
||||
|
||||
Running
|
||||
|
|
@ -37,3 +49,6 @@ cd examples/tutorial
|
|||
python3 basic_shapes.py
|
||||
klayout -e basic_shapes.gds
|
||||
```
|
||||
|
||||
Some tutorials depend on outputs from earlier ones. In particular, `library.py`
|
||||
expects `circuit.gds`, which is generated by `devices.py`.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
from collections.abc import Sequence
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from masque import (
|
||||
layer_t, Pattern, Label, Port,
|
||||
Circle, Arc, Polygon,
|
||||
)
|
||||
from masque import layer_t, Pattern, Circle, Arc, Ref
|
||||
from masque.repetition import Grid
|
||||
import masque.file.gdsii
|
||||
|
||||
|
||||
|
|
@ -39,6 +36,45 @@ def hole(
|
|||
return pat
|
||||
|
||||
|
||||
def hole_array(
|
||||
radius: float,
|
||||
num_x: int = 5,
|
||||
num_y: int = 3,
|
||||
pitch: float = 2000,
|
||||
layer: layer_t = (1, 0),
|
||||
) -> Pattern:
|
||||
"""
|
||||
Generate an array of circular holes using `Repetition`.
|
||||
|
||||
Args:
|
||||
radius: Circle radius.
|
||||
num_x, num_y: Number of holes in x and y.
|
||||
pitch: Center-to-center spacing.
|
||||
layer: Layer to draw the holes on.
|
||||
|
||||
Returns:
|
||||
Pattern containing a grid of holes.
|
||||
"""
|
||||
# First, make a pattern for a single hole
|
||||
hpat = hole(radius, layer)
|
||||
|
||||
# Now, create a pattern that references it multiple times using a Grid
|
||||
pat = Pattern()
|
||||
pat.refs['hole'] = [
|
||||
Ref(
|
||||
offset=(0, 0),
|
||||
repetition=Grid(a_vector=(pitch, 0), a_count=num_x,
|
||||
b_vector=(0, pitch), b_count=num_y)
|
||||
)]
|
||||
|
||||
# We can also add transformed references (rotation, mirroring, etc.)
|
||||
pat.refs['hole'].append(
|
||||
Ref(offset=(0, -pitch), rotation=pi / 4, mirrored=True)
|
||||
)
|
||||
|
||||
return pat, hpat
|
||||
|
||||
|
||||
def triangle(
|
||||
radius: float,
|
||||
layer: layer_t = (1, 0),
|
||||
|
|
@ -60,9 +96,7 @@ def triangle(
|
|||
]) * radius
|
||||
|
||||
pat = Pattern()
|
||||
pat.shapes[layer].extend([
|
||||
Polygon(offset=(0, 0), vertices=vertices),
|
||||
])
|
||||
pat.polygon(layer, vertices=vertices)
|
||||
return pat
|
||||
|
||||
|
||||
|
|
@ -111,9 +145,13 @@ def main() -> None:
|
|||
lib['smile'] = smile(1000)
|
||||
lib['triangle'] = triangle(1000)
|
||||
|
||||
# Use a Grid to make many holes efficiently
|
||||
lib['grid'], lib['hole'] = hole_array(1000)
|
||||
|
||||
masque.file.gdsii.writefile(lib, 'basic_shapes.gds', **GDS_OPTS)
|
||||
|
||||
lib['triangle'].visualize()
|
||||
lib['grid'].visualize(lib)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
"""
|
||||
Tutorial: building hierarchical devices with `Pattern`, `Port`, and `Pather`.
|
||||
|
||||
This file uses photonic-crystal components as the concrete example, so some of
|
||||
the geometry-generation code is domain-specific. The tutorial value is in the
|
||||
Masque patterns around it: creating reusable cells, annotating ports, composing
|
||||
hierarchy with references, and snapping ports together to build a larger circuit.
|
||||
"""
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from masque import (
|
||||
layer_t, Pattern, Ref, Label, Builder, Port, Polygon,
|
||||
Library, ILibraryView,
|
||||
layer_t, Pattern, Ref, Pather, Port, Polygon,
|
||||
Library,
|
||||
)
|
||||
from masque.utils import ports2data
|
||||
from masque.file.gdsii import writefile, check_valid_names
|
||||
|
|
@ -64,9 +72,9 @@ def perturbed_l3(
|
|||
Provided sequence should have same length as `shifts_a`.
|
||||
xy_size: `(x, y)` number of mirror periods in each direction; total size is
|
||||
`2 * n + 1` holes in each direction. Default (10, 10).
|
||||
perturbed_radius: radius of holes perturbed to form an upwards-driected beam
|
||||
perturbed_radius: radius of holes perturbed to form an upwards-directed beam
|
||||
(multiplicative factor). Default 1.1.
|
||||
trench width: Width of the undercut trenches. Default 1200.
|
||||
trench_width: Width of the undercut trenches. Default 1200.
|
||||
|
||||
Returns:
|
||||
`Pattern` object representing the L3 design.
|
||||
|
|
@ -79,14 +87,15 @@ def perturbed_l3(
|
|||
shifts_a=shifts_a,
|
||||
shifts_r=shifts_r)
|
||||
|
||||
# Build L3 cavity, using references to the provided hole pattern
|
||||
# Build the cavity by instancing the supplied `hole` pattern many times.
|
||||
# Using references keeps the pattern compact even though it contains many holes.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(scale=r, offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y, r in xyr]
|
||||
|
||||
# Add rectangular undercut aids
|
||||
# Add rectangular undercut aids based on the referenced hole extents.
|
||||
min_xy, max_xy = pat.get_bounds_nonempty(hole_lib)
|
||||
trench_dx = max_xy[0] - min_xy[0]
|
||||
|
||||
|
|
@ -95,7 +104,7 @@ def perturbed_l3(
|
|||
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width),
|
||||
]
|
||||
|
||||
# Ports are at outer extents of the device (with y=0)
|
||||
# Define the interface in Masque terms: two ports at the left/right extents.
|
||||
extent = lattice_constant * xy_size[0]
|
||||
pat.ports = dict(
|
||||
input=Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -125,17 +134,17 @@ def waveguide(
|
|||
Returns:
|
||||
`Pattern` object representing the waveguide.
|
||||
"""
|
||||
# Generate hole locations
|
||||
# Generate the normalized lattice locations for the line defect.
|
||||
xy = pcgen.waveguide(length=length, num_mirror=mirror_periods)
|
||||
|
||||
# Build the pattern
|
||||
# Build the pattern by placing repeated references to the same hole cell.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Ports are at outer edges, with y=0
|
||||
# Publish the device interface as two ports at the outer edges.
|
||||
extent = lattice_constant * length / 2
|
||||
pat.ports = dict(
|
||||
left=Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -164,17 +173,17 @@ def bend(
|
|||
`Pattern` object representing the waveguide bend.
|
||||
Ports are named 'left' (input) and 'right' (output).
|
||||
"""
|
||||
# Generate hole locations
|
||||
# Generate the normalized lattice locations for the bend.
|
||||
xy = pcgen.wgbend(num_mirror=mirror_periods)
|
||||
|
||||
# Build the pattern
|
||||
pat= Pattern()
|
||||
# Build the pattern by instancing the shared hole cell.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Figure out port locations.
|
||||
# Publish the bend interface as two ports.
|
||||
extent = lattice_constant * mirror_periods
|
||||
pat.ports = dict(
|
||||
left=Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -203,17 +212,17 @@ def y_splitter(
|
|||
`Pattern` object representing the y-splitter.
|
||||
Ports are named 'in', 'top', and 'bottom'.
|
||||
"""
|
||||
# Generate hole locations
|
||||
# Generate the normalized lattice locations for the splitter.
|
||||
xy = pcgen.y_splitter(num_mirror=mirror_periods)
|
||||
|
||||
# Build pattern
|
||||
# Build the pattern by instancing the shared hole cell.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Determine port locations
|
||||
# Publish the splitter interface as one input and two outputs.
|
||||
extent = lattice_constant * mirror_periods
|
||||
pat.ports = {
|
||||
'in': Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -227,13 +236,13 @@ def y_splitter(
|
|||
|
||||
|
||||
def main(interactive: bool = True) -> None:
|
||||
# Generate some basic hole patterns
|
||||
# First make a couple of reusable primitive cells.
|
||||
shape_lib = {
|
||||
'smile': basic_shapes.smile(RADIUS),
|
||||
'hole': basic_shapes.hole(RADIUS),
|
||||
}
|
||||
|
||||
# Build some devices
|
||||
# Then build a small library of higher-level devices from those primitives.
|
||||
a = LATTICE_CONSTANT
|
||||
|
||||
devices = {}
|
||||
|
|
@ -245,22 +254,23 @@ 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 the device mapping into a `Library`.
|
||||
# That gives us convenience helpers for hierarchy inspection and abstract views.
|
||||
lib = Library(devices)
|
||||
|
||||
#
|
||||
# Build a circuit
|
||||
#
|
||||
# Create a `Builder`, and add the circuit to our library as "my_circuit".
|
||||
circ = Builder(library=lib, name='my_circuit')
|
||||
# Create a `Pather`, and register the resulting top cell as "my_circuit".
|
||||
circ = Pather(library=lib, name='my_circuit')
|
||||
|
||||
# Start by placing a waveguide. Call its ports "in" and "signal".
|
||||
# Start by placing a waveguide and renaming its ports to match the circuit-level
|
||||
# names we want to use while assembling the design.
|
||||
circ.place('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".
|
||||
# Extend the signal path by attaching another waveguide.
|
||||
# Because `wg10` only has one unattached port left after the plug, Masque can
|
||||
# infer that it should keep the name `signal`.
|
||||
circ.plug('wg10', {'signal': 'left'})
|
||||
|
||||
# We could have done the following instead:
|
||||
|
|
@ -268,8 +278,8 @@ def main(interactive: bool = True) -> None:
|
|||
# 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.
|
||||
# but `Pather` removes some repeated `lib.abstract(...)` boilerplate and keeps
|
||||
# the assembly code focused on port-level intent.
|
||||
|
||||
# Attach a y-splitter to the signal path.
|
||||
# Since the y-splitter has 3 ports total, we can't auto-inherit the
|
||||
|
|
@ -281,13 +291,10 @@ def main(interactive: bool = True) -> None:
|
|||
circ.plug('wg05', {'signal1': 'left'})
|
||||
circ.plug('wg05', {'signal2': 'left'})
|
||||
|
||||
# Add a bend to both ports.
|
||||
# Our bend's ports "left" and "right" refer to the original counterclockwise
|
||||
# orientation. We want the bends to turn in opposite directions, so we attach
|
||||
# the "right" port to "signal1" to bend clockwise, and the "left" port
|
||||
# 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.
|
||||
# Add a bend to both branches.
|
||||
# Our bend primitive is defined with a specific orientation, so choosing which
|
||||
# port to plug determines whether the path turns clockwise or counterclockwise.
|
||||
# We could also mirror one instance instead of using opposite ports.
|
||||
circ.plug('bend0', {'signal1': 'right'})
|
||||
circ.plug('bend0', {'signal2': 'left'})
|
||||
|
||||
|
|
@ -296,29 +303,26 @@ def main(interactive: bool = True) -> None:
|
|||
circ.plug('l3cav', {'signal1': 'input'})
|
||||
circ.plug('wg10', {'signal1': 'left'})
|
||||
|
||||
# "signal2" just gets a single of equivalent length
|
||||
# `signal2` gets a single waveguide of equivalent overall length.
|
||||
circ.plug('wg28', {'signal2': 'left'})
|
||||
|
||||
# Now we bend both waveguides back towards each other
|
||||
# Now bend both branches back towards each other.
|
||||
circ.plug('bend0', {'signal1': 'right'})
|
||||
circ.plug('bend0', {'signal2': 'left'})
|
||||
circ.plug('wg05', {'signal1': 'left'})
|
||||
circ.plug('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.
|
||||
# The remaining port gets named "signal_out".
|
||||
# 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).
|
||||
# To join the branches, attach a second y-junction.
|
||||
# This succeeds only if both chosen ports agree on the same translation and
|
||||
# rotation for the inserted device; otherwise Masque raises an exception.
|
||||
circ.plug('ysplit', {'signal1': 'bot', 'signal2': 'top'}, {'in': 'signal_out'})
|
||||
|
||||
# Finally, add some more waveguide to "signal_out".
|
||||
circ.plug('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.
|
||||
# Bake the top-level port metadata into labels so it survives GDS export.
|
||||
# These labels appear on the circuit cell; individual child devices keep their
|
||||
# own port labels in their own cells.
|
||||
ports_to_data(circ.pattern)
|
||||
|
||||
# Check if we forgot to include any patterns... ooops!
|
||||
|
|
@ -330,12 +334,12 @@ def main(interactive: bool = True) -> None:
|
|||
lib.add(shape_lib)
|
||||
assert not lib.dangling_refs()
|
||||
|
||||
# We can visualize the design. Usually it's easier to just view the GDS.
|
||||
# We can visualize the design directly, though opening the written GDS is often easier.
|
||||
if interactive:
|
||||
print('Visualizing... this step may be slow')
|
||||
circ.pattern.visualize(lib)
|
||||
|
||||
#Write out to GDS, only keeping patterns referenced by our circuit (including itself)
|
||||
# Write out only the subtree reachable from our top cell.
|
||||
subtree = lib.subtree('my_circuit') # don't include wg90, which we don't use
|
||||
check_valid_names(subtree.keys())
|
||||
writefile(subtree, 'circuit.gds', **GDS_OPTS)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
"""
|
||||
Tutorial: using `LazyLibrary` and `Pather.interface()`.
|
||||
|
||||
This example assumes you have already read `devices.py` and generated the
|
||||
`circuit.gds` file it writes. The goal here is not the photonic-crystal geometry
|
||||
itself, but rather how Masque lets you mix lazily loaded GDS content with
|
||||
python-generated devices inside one library.
|
||||
"""
|
||||
from typing import Any
|
||||
from collections.abc import Sequence, Callable
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from masque import Pattern, Builder, LazyLibrary
|
||||
from masque import Pather, LazyLibrary
|
||||
from masque.file.gdsii import writefile, load_libraryfile
|
||||
|
||||
import pcgen
|
||||
import basic_shapes
|
||||
import devices
|
||||
from devices import ports_to_data, data_to_ports
|
||||
from devices import data_to_ports
|
||||
from basic_shapes import GDS_OPTS
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Define a `LazyLibrary`, which provides lazy evaluation for generating
|
||||
# patterns and lazy-loading of GDS contents.
|
||||
# A `LazyLibrary` delays work until a pattern is actually needed.
|
||||
# That applies both to GDS cells we load from disk and to python callables
|
||||
# that generate patterns on demand.
|
||||
lib = LazyLibrary()
|
||||
|
||||
#
|
||||
|
|
@ -27,9 +32,9 @@ def main() -> None:
|
|||
# Scan circuit.gds and prepare to lazy-load its contents
|
||||
gds_lib, _properties = load_libraryfile('circuit.gds', postprocess=data_to_ports)
|
||||
|
||||
# Add it into the device library by providing a way to read port info
|
||||
# This maintains the lazy evaluation from above, so no patterns
|
||||
# are actually read yet.
|
||||
# Add those cells into our lazy library.
|
||||
# Nothing is read yet; we are only registering how to fetch and postprocess
|
||||
# each pattern when it is first requested.
|
||||
lib.add(gds_lib)
|
||||
|
||||
print('Patterns loaded from GDS into library:\n' + pformat(list(lib.keys())))
|
||||
|
|
@ -44,8 +49,8 @@ def main() -> None:
|
|||
hole = 'triangle',
|
||||
)
|
||||
|
||||
# Triangle-based variants. These are defined here, but they won't run until they're
|
||||
# retrieved from the library.
|
||||
# Triangle-based variants. These lambdas are only recipes for building the
|
||||
# patterns; they do not execute until someone asks for the cell.
|
||||
lib['tri_wg10'] = lambda: devices.waveguide(length=10, mirror_periods=5, **opts)
|
||||
lib['tri_wg05'] = lambda: devices.waveguide(length=5, mirror_periods=5, **opts)
|
||||
lib['tri_wg28'] = lambda: devices.waveguide(length=28, mirror_periods=5, **opts)
|
||||
|
|
@ -57,22 +62,22 @@ def main() -> None:
|
|||
# Build a mixed waveguide with an L3 cavity in the middle
|
||||
#
|
||||
|
||||
# Immediately start building from an instance of the L3 cavity
|
||||
circ2 = Builder(library=lib, ports='tri_l3cav')
|
||||
# Start a new design by copying the ports from an existing library cell.
|
||||
# This gives `circ2` the same external interface as `tri_l3cav`.
|
||||
circ2 = Pather(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`.
|
||||
# First way to specify what we are plugging in: request an explicit abstract.
|
||||
# This works with `Pattern` methods directly as well as with `Pather`.
|
||||
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()`.
|
||||
# Second way: use an `AbstractView`, which behaves like a mapping of names
|
||||
# to abstracts.
|
||||
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`).
|
||||
# Just pass the pattern name!
|
||||
# Third way: let `Pather` resolve a pattern name through its own library.
|
||||
# This shorthand is convenient, but it is specific to helpers that already
|
||||
# carry a library reference.
|
||||
circ2.plug('tri_wg10', {'input': 'right'})
|
||||
circ2.plug('tri_wg10', {'output': 'left'})
|
||||
|
||||
|
|
@ -81,13 +86,15 @@ def main() -> None:
|
|||
|
||||
|
||||
#
|
||||
# Build a device that could plug into our mixed_wg_cav and joins the two ports
|
||||
# Build a second device that is explicitly designed to mate with `circ2`.
|
||||
#
|
||||
|
||||
# We'll be designing against an existing device's interface...
|
||||
circ3 = Builder.interface(source=circ2)
|
||||
# `Pather.interface()` makes a new pattern whose ports mirror an existing
|
||||
# design's external interface. That is useful when you want to design an
|
||||
# adapter, continuation, or mating structure.
|
||||
circ3 = Pather.interface(source=circ2)
|
||||
|
||||
# ... that lets us continue from where we left off.
|
||||
# Continue routing outward from those inherited ports.
|
||||
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': 'right'})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
"""
|
||||
Manual wire routing tutorial: Pather and BasicTool
|
||||
Manual wire routing tutorial: Pather and AutoTool
|
||||
"""
|
||||
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 import Pather, Library, Pattern, Port, layer_t
|
||||
from masque.builder.tools import AutoTool, Tool
|
||||
from masque.file.gdsii import writefile
|
||||
|
||||
from basic_shapes import GDS_OPTS
|
||||
|
|
@ -107,31 +106,29 @@ def map_layer(layer: layer_t) -> layer_t:
|
|||
'M2': (20, 0),
|
||||
'V1': (30, 0),
|
||||
}
|
||||
return layer_mapping.get(layer, layer)
|
||||
if isinstance(layer, str):
|
||||
return layer_mapping.get(layer, layer)
|
||||
return 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:
|
||||
def prepare_tools() -> tuple[Library, Tool, Tool]:
|
||||
"""
|
||||
Create some basic library elements and tools for drawing M1 and M2
|
||||
"""
|
||||
# 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',
|
||||
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',
|
||||
)
|
||||
|
||||
#
|
||||
|
|
@ -140,53 +137,79 @@ def main() -> None:
|
|||
# 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`
|
||||
# Note that while we use AutoTool 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
|
||||
),
|
||||
M1_tool = AutoTool(
|
||||
# First, we need a function which takes in a length and spits out an M1 wire
|
||||
straights = [
|
||||
AutoTool.Straight(
|
||||
ptype = 'm1wire',
|
||||
fn = lambda length: make_straight_wire(layer='M1', ptype='m1wire', width=M1_WIDTH, length=length),
|
||||
in_port_name = 'input', # When we get a pattern from make_straight_wire, use the port named 'input' as the input
|
||||
out_port_name = 'output', # and use the port named 'output' as the output
|
||||
),
|
||||
],
|
||||
bends = [
|
||||
AutoTool.Bend(
|
||||
abstract = library.abstract('m1_bend'), # When we need a bend, we'll reference the pattern we generated earlier
|
||||
in_port_name = 'input',
|
||||
out_port_name = 'output',
|
||||
clockwise = True,
|
||||
),
|
||||
],
|
||||
transitions = { # We can automate transitions for different (normally incompatible) port types
|
||||
'm2wire': ( # For example, when we're attaching to a port with type 'm2wire'
|
||||
('m2wire', 'm1wire'): AutoTool.Transition( # 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
|
||||
),
|
||||
},
|
||||
sbends = [],
|
||||
default_out_ptype = 'm1wire', # Unless otherwise requested, we'll default to trying to stay on M1
|
||||
)
|
||||
|
||||
M2_tool = BasicTool(
|
||||
straight = (
|
||||
M2_tool = AutoTool(
|
||||
straights = [
|
||||
# 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',
|
||||
),
|
||||
AutoTool.Straight(
|
||||
ptype = 'm2wire',
|
||||
fn = lambda length: make_straight_wire(layer='M2', ptype='m2wire', width=M2_WIDTH, length=length),
|
||||
in_port_name = 'input',
|
||||
out_port_name = 'output',
|
||||
),
|
||||
],
|
||||
bends = [
|
||||
# and we use an M2 bend
|
||||
AutoTool.Bend(
|
||||
abstract = library.abstract('m2_bend'),
|
||||
in_port_name = 'input',
|
||||
out_port_name = 'output',
|
||||
),
|
||||
],
|
||||
transitions = {
|
||||
'm1wire': (
|
||||
('m1wire', 'm2wire'): AutoTool.Transition(
|
||||
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'
|
||||
),
|
||||
},
|
||||
sbends = [],
|
||||
default_out_ptype = 'm2wire', # We default to trying to stay on M2
|
||||
)
|
||||
return library, M1_tool, M2_tool
|
||||
|
||||
|
||||
#
|
||||
# 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 deferred
|
||||
# `Pather` tutorial, which handles some things more explicitly (e.g. via placement) and simplifies
|
||||
# others (e.g. geometry definition).
|
||||
#
|
||||
def main() -> None:
|
||||
library, M1_tool, M2_tool = prepare_tools()
|
||||
|
||||
#
|
||||
# Create a new pather which writes to `library` and uses `M2_tool` as its default tool.
|
||||
|
|
@ -203,27 +226,25 @@ def main() -> None:
|
|||
|
||||
# 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)
|
||||
pather.cw('VCC', 6_000)
|
||||
|
||||
# Now path VCC to x=0. This time, don't include any bend (ccw=None).
|
||||
# Now path VCC to x=0. This time, don't include any bend.
|
||||
# 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)
|
||||
pather.straight('VCC', 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)
|
||||
pather.cw('GND', 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])
|
||||
pather.straight('GND', 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.
|
||||
# Since we have defined an M2-to-M1 transition for Pather, 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'])
|
||||
# the next time we route GND (the `pather.ccw()` call 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.
|
||||
|
|
@ -231,7 +252,7 @@ def main() -> None:
|
|||
#
|
||||
# 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)
|
||||
pather.ccw(['GND', 'VCC'], 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.
|
||||
|
|
@ -241,38 +262,37 @@ def main() -> None:
|
|||
# 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)
|
||||
pather.ccw(['GND', 'VCC'], 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
|
||||
# Here, emin specifies the travel distance for the shortest segment. For the first call
|
||||
# that applies to VCC, and for the 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)
|
||||
pather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200)
|
||||
pather.cw(['GND', 'VCC'], 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.
|
||||
# explicitly assigned a tool.
|
||||
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
|
||||
# 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)
|
||||
pather.straight(['GND', 'VCC'], 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')
|
||||
pather.straight('VCC', x=-50_000, out_ptype='m1wire')
|
||||
|
||||
# Now extend GND out to x=-50_000, using M2 for a portion of the path.
|
||||
# We can use `pather.toolctx()` to temporarily retool, instead of calling `retool()` twice.
|
||||
with pather.toolctx(M2_tool, keys=['GND']):
|
||||
pather.path_to('GND', None, -40_000)
|
||||
pather.path_to('GND', None, -50_000)
|
||||
with pather.toolctx(M2_tool, keys='GND'):
|
||||
pather.straight('GND', x=-40_000)
|
||||
pather.straight('GND', x=-50_000)
|
||||
|
||||
# Save the pather's pattern into our library
|
||||
library['Pather_and_BasicTool'] = pather.pattern
|
||||
library['Pather_and_AutoTool'] = pather.pattern
|
||||
|
||||
# Convert from text-based layers to numeric layers for GDS, and output the file
|
||||
library.map_layers(map_layer)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Routines for creating normalized 2D lattices and common photonic crystal
|
||||
cavity designs.
|
||||
"""
|
||||
from collection.abc import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
|
@ -50,7 +50,7 @@ def triangular_lattice(
|
|||
elif origin == 'corner':
|
||||
pass
|
||||
else:
|
||||
raise Exception(f'Invalid value for `origin`: {origin}')
|
||||
raise ValueError(f'Invalid value for `origin`: {origin}')
|
||||
|
||||
return xy[xy[:, 0].argsort(), :]
|
||||
|
||||
|
|
@ -197,12 +197,12 @@ def ln_defect(
|
|||
`[[x0, y0], [x1, y1], ...]` for all the holes
|
||||
"""
|
||||
if defect_length % 2 != 1:
|
||||
raise Exception('defect_length must be odd!')
|
||||
p = triangular_lattice([2 * d + 1 for d in mirror_dims])
|
||||
raise ValueError('defect_length must be odd!')
|
||||
pp = triangular_lattice([2 * dd + 1 for dd in mirror_dims])
|
||||
half_length = numpy.floor(defect_length / 2)
|
||||
hole_nums = numpy.arange(-half_length, half_length + 1)
|
||||
holes_to_keep = numpy.in1d(p[:, 0], hole_nums, invert=True)
|
||||
return p[numpy.logical_or(holes_to_keep, p[:, 1] != 0), ]
|
||||
holes_to_keep = numpy.isin(pp[:, 0], hole_nums, invert=True)
|
||||
return pp[numpy.logical_or(holes_to_keep, pp[:, 1] != 0), :]
|
||||
|
||||
|
||||
def ln_shift_defect(
|
||||
|
|
@ -248,7 +248,7 @@ def ln_shift_defect(
|
|||
for sign in (-1, 1):
|
||||
x_val = sign * (x_removed + ind + 1)
|
||||
which = numpy.logical_and(xyr[:, 0] == x_val, xyr[:, 1] == 0)
|
||||
xyr[which, ] = (x_val + numpy.sign(x_val) * shifts_a[ind], 0, shifts_r[ind])
|
||||
xyr[which, :] = (x_val + numpy.sign(x_val) * shifts_a[ind], 0, shifts_r[ind])
|
||||
|
||||
return xyr
|
||||
|
||||
|
|
@ -309,7 +309,7 @@ def l3_shift_perturbed_defect(
|
|||
|
||||
# which holes should be perturbed? (xs[[3, 7]], ys[1]) and (xs[[2, 6]], ys[2])
|
||||
perturbed_holes = ((xs[a], ys[b]) for a, b in ((3, 1), (7, 1), (2, 2), (6, 2)))
|
||||
for row in xyr:
|
||||
if numpy.fabs(row) in perturbed_holes:
|
||||
row[2] = perturbed_radius
|
||||
for xy in perturbed_holes:
|
||||
which = (numpy.fabs(xyr[:, :2]) == xy).all(axis=1)
|
||||
xyr[which, 2] = perturbed_radius
|
||||
return xyr
|
||||
|
|
|
|||
169
examples/tutorial/port_pather.py
Normal file
169
examples/tutorial/port_pather.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"""
|
||||
PortPather tutorial: Using .at() syntax
|
||||
"""
|
||||
from masque import Pather, Pattern, Port, R90
|
||||
from masque.file.gdsii import writefile
|
||||
|
||||
from basic_shapes import GDS_OPTS
|
||||
from pather import map_layer, prepare_tools
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Reuse the same patterns (pads, bends, vias) and tools as in pather.py
|
||||
library, M1_tool, M2_tool = prepare_tools()
|
||||
|
||||
# Create a deferred Pather and place some initial pads (same as Pather tutorial)
|
||||
rpather = Pather(library, tools=M2_tool, auto_render=False)
|
||||
|
||||
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))
|
||||
|
||||
#
|
||||
# Routing with .at() chaining
|
||||
#
|
||||
# The .at(port_name) method returns a PortPather object which wraps the Pather
|
||||
# and remembers the selected port(s). This allows method chaining.
|
||||
|
||||
# Route VCC: 6um South, then West to x=0.
|
||||
# (Note: since the port points North into the pad, trace() moves South by default)
|
||||
(rpather.at('VCC')
|
||||
.trace(False, length=6_000) # Move South, turn West (Clockwise)
|
||||
.trace_to(None, x=0) # Continue West to x=0
|
||||
)
|
||||
|
||||
# Route GND: 5um South, then West to match VCC's x-coordinate.
|
||||
rpather.at('GND').trace(False, length=5_000).trace_to(None, x=rpather['VCC'].x)
|
||||
|
||||
|
||||
#
|
||||
# Tool management and manual plugging
|
||||
#
|
||||
# We can use .retool() to change the tool for specific ports.
|
||||
# We can also use .plug() directly on a PortPather.
|
||||
|
||||
# Manually add a via to GND and switch to M1_tool for subsequent segments
|
||||
(rpather.at('GND')
|
||||
.plug('v1_via', 'top')
|
||||
.retool(M1_tool) # this only retools the 'GND' port
|
||||
)
|
||||
|
||||
# We can also pass multiple ports to .at(), and then route them together.
|
||||
# Here we bundle them, turn South, and retool both to M1 (VCC gets an auto-via).
|
||||
(rpather.at(['GND', 'VCC'])
|
||||
.trace(True, xmax=-10_000, spacing=5_000) # Move West to -10k, turn South
|
||||
.retool(M1_tool) # Retools both GND and VCC
|
||||
.trace(True, emax=50_000, spacing=1_200) # Turn East, moves 50um extension
|
||||
.trace(False, emin=1_000, spacing=1_200) # U-turn back South
|
||||
.trace(False, emin=2_000, spacing=4_500) # U-turn back West
|
||||
)
|
||||
|
||||
# Retool VCC back to M2 and move both to x=-28k
|
||||
rpather.at('VCC').retool(M2_tool)
|
||||
rpather.at(['GND', 'VCC']).trace(None, xmin=-28_000)
|
||||
|
||||
# Final segments to -50k
|
||||
rpather.at('VCC').trace_to(None, x=-50_000, out_ptype='m1wire')
|
||||
with rpather.at('GND').toolctx(M2_tool):
|
||||
rpather.at('GND').trace_to(None, x=-40_000)
|
||||
rpather.at('GND').trace_to(None, x=-50_000)
|
||||
|
||||
|
||||
#
|
||||
# Branching with mark and fork
|
||||
#
|
||||
# .mark(new_name) creates a port copy and keeps the original selected.
|
||||
# .fork(new_name) creates a port copy and selects the new one.
|
||||
|
||||
# Create a tap on GND
|
||||
(rpather.at('GND')
|
||||
.trace(None, length=5_000) # Move GND further West
|
||||
.mark('GND_TAP') # Mark this location for a later branch
|
||||
.jog(offset=-10_000, length=10_000) # Continue GND with an S-bend
|
||||
)
|
||||
|
||||
# Branch VCC and follow the new branch
|
||||
(rpather.at('VCC')
|
||||
.trace(None, length=5_000)
|
||||
.fork('VCC_BRANCH') # We are now manipulating 'VCC_BRANCH'
|
||||
.trace(True, length=5_000) # VCC_BRANCH turns South
|
||||
)
|
||||
# The original 'VCC' port remains at x=-55k, y=VCC.y
|
||||
|
||||
|
||||
#
|
||||
# Port set management: add, drop, rename, delete
|
||||
#
|
||||
|
||||
# Route the GND_TAP we saved earlier.
|
||||
(rpather.at('GND_TAP')
|
||||
.retool(M1_tool)
|
||||
.trace(True, length=10_000) # Turn South
|
||||
.rename('GND_FEED') # Give it a more descriptive name
|
||||
.retool(M1_tool) # Re-apply tool to the new name
|
||||
)
|
||||
|
||||
# We can manage the active set of ports in a PortPather
|
||||
pp = rpather.at(['VCC_BRANCH', 'GND_FEED'])
|
||||
pp.select('GND') # Now tracking 3 ports
|
||||
pp.deselect('VCC_BRANCH') # Now tracking 2 ports: GND_FEED, GND
|
||||
pp.trace(None, each=5_000) # Move both 5um forward (length > transition size)
|
||||
|
||||
# We can also delete ports from the pather entirely
|
||||
rpather.at('VCC').delete() # VCC is gone (we have VCC_BRANCH instead)
|
||||
|
||||
|
||||
#
|
||||
# Advanced Connections: trace_into
|
||||
#
|
||||
# trace_into routes FROM the selected port TO a target port.
|
||||
|
||||
# Create a destination component
|
||||
dest_ports = {
|
||||
'in_A': Port((0, 0), rotation=R90, ptype='m2wire'),
|
||||
'in_B': Port((5_000, 0), rotation=R90, ptype='m2wire')
|
||||
}
|
||||
library['dest'] = Pattern(ports=dest_ports)
|
||||
# Place dest so that its ports are to the West and South of our current wires.
|
||||
# Rotating by pi/2 makes the ports face West (pointing East).
|
||||
rpather.place('dest', offset=(-100_000, -100_000), rotation=R90, port_map={'in_A': 'DEST_A', 'in_B': 'DEST_B'})
|
||||
|
||||
# Connect GND_FEED to DEST_A
|
||||
# Since GND_FEED is moving South and DEST_A faces West, a single bend will suffice.
|
||||
rpather.at('GND_FEED').trace_into('DEST_A')
|
||||
|
||||
# Connect VCC_BRANCH to DEST_B
|
||||
rpather.at('VCC_BRANCH').trace_into('DEST_B')
|
||||
|
||||
|
||||
#
|
||||
# Direct Port Transformations and Metadata
|
||||
#
|
||||
(rpather.at('GND')
|
||||
.set_ptype('m1wire') # Change metadata
|
||||
.translate((1000, 0)) # Shift the port 1um East
|
||||
.rotate(R90 / 2) # Rotate it 45 degrees
|
||||
.set_rotation(R90) # Force it to face West
|
||||
)
|
||||
|
||||
# Demonstrate .plugged() to acknowledge a manual connection
|
||||
# (Normally used when you place components so their ports perfectly overlap)
|
||||
rpather.add_port_pair(offset=(0, 0), names=('TMP1', 'TMP2'))
|
||||
rpather.at('TMP1').plugged('TMP2') # Removes both ports
|
||||
|
||||
|
||||
#
|
||||
# Rendering and Saving
|
||||
#
|
||||
# Since we deferred auto-rendering, we must call .render() to generate the geometry.
|
||||
rpather.render()
|
||||
|
||||
library['PortPather_Tutorial'] = rpather.pattern
|
||||
library.map_layers(map_layer)
|
||||
writefile(library, 'port_pather.gds', **GDS_OPTS)
|
||||
print("Tutorial complete. Output written to port_pather.gds")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
"""
|
||||
Manual wire routing tutorial: RenderPather an PathTool
|
||||
Manual wire routing tutorial: deferred Pather and PathTool
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
from masque import RenderPather, Library, Pattern, Port, layer_t, map_layers
|
||||
from masque import Pather, Library
|
||||
from masque.builder.tools import PathTool
|
||||
from masque.file.gdsii import writefile
|
||||
|
||||
|
|
@ -12,9 +11,9 @@ 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
|
||||
# To illustrate deferred routing with `Pather`, we use `PathTool` instead
|
||||
# of `AutoTool`. `PathTool` lacks some sophistication (e.g. no automatic transitions)
|
||||
# but when used with `Pather(auto_render=False)`, 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`
|
||||
|
|
@ -25,66 +24,68 @@ def main() -> None:
|
|||
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',
|
||||
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
|
||||
# `PathTool` is more limited than `AutoTool`. 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)
|
||||
rpather = Pather(tools=M2_ptool, library=library, auto_render=False)
|
||||
|
||||
# As in the pather tutorial, we make soem pads and labels...
|
||||
# As in the pather tutorial, we make some 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])
|
||||
rpather.cw('VCC', 6_000)
|
||||
rpather.straight('VCC', x=0)
|
||||
rpather.cw('GND', 5_000)
|
||||
rpather.straight('GND', x=rpather.pattern['VCC'].x)
|
||||
|
||||
# `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)
|
||||
rpather.retool(M1_ptool, keys='GND')
|
||||
rpather.ccw(['GND', 'VCC'], 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)
|
||||
rpather.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200)
|
||||
rpather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200)
|
||||
rpather.cw(['GND', 'VCC'], 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)
|
||||
rpather.straight(['GND', 'VCC'], 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)
|
||||
v1pat = library['v1_via']
|
||||
via_size = abs(v1pat.ports['top'].x - v1pat.ports['bottom'].x)
|
||||
|
||||
# alternatively, via_size = v1pat.ports['top'].measure_travel(v1pat.ports['bottom'])[0][0]
|
||||
# would take into account the port orientations if we didn't already know they're along x
|
||||
rpather.straight('VCC', x=-50_000 + via_size)
|
||||
rpather.plug('v1_via', {'VCC': 'top'})
|
||||
|
||||
# Render the path we defined
|
||||
rpather.render()
|
||||
library['RenderPather_and_PathTool'] = rpather.pattern
|
||||
library['Deferred_Pather_and_PathTool'] = rpather.pattern
|
||||
|
||||
|
||||
# Convert from text-based layers to numeric layers for GDS, and output the file
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ from .pattern import (
|
|||
map_targets as map_targets,
|
||||
chain_elements as chain_elements,
|
||||
)
|
||||
from .utils.boolean import boolean as boolean
|
||||
|
||||
from .library import (
|
||||
ILibraryView as ILibraryView,
|
||||
|
|
@ -72,10 +73,8 @@ from .ports import (
|
|||
)
|
||||
from .abstract import Abstract as Abstract
|
||||
from .builder import (
|
||||
Builder as Builder,
|
||||
Tool as Tool,
|
||||
Pather as Pather,
|
||||
RenderPather as RenderPather,
|
||||
RenderStep as RenderStep,
|
||||
SimpleTool as SimpleTool,
|
||||
AutoTool as AutoTool,
|
||||
|
|
|
|||
|
|
@ -8,16 +8,13 @@ from numpy.typing import ArrayLike
|
|||
from .ref import Ref
|
||||
from .ports import PortList, Port
|
||||
from .utils import rotation_matrix_2d
|
||||
|
||||
#if TYPE_CHECKING:
|
||||
# from .builder import Builder, Tool
|
||||
# from .library import ILibrary
|
||||
from .traits import Mirrorable
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Abstract(PortList):
|
||||
class Abstract(PortList, Mirrorable):
|
||||
"""
|
||||
An `Abstract` is a container for a name and associated ports.
|
||||
|
||||
|
|
@ -131,50 +128,18 @@ class Abstract(PortList):
|
|||
port.rotate(rotation)
|
||||
return self
|
||||
|
||||
def mirror_port_offsets(self, across_axis: int = 0) -> Self:
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
"""
|
||||
Mirror the offsets of all shapes, labels, and refs across an axis
|
||||
Mirror the Abstract across an axis through its origin.
|
||||
|
||||
Args:
|
||||
across_axis: Axis to mirror across
|
||||
(0: mirror across x axis, 1: mirror across y axis)
|
||||
axis: Axis to mirror across (0: x-axis, 1: y-axis).
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for port in self.ports.values():
|
||||
port.offset[across_axis - 1] *= -1
|
||||
return self
|
||||
|
||||
def mirror_ports(self, across_axis: int = 0) -> Self:
|
||||
"""
|
||||
Mirror each port's rotation across an axis, relative to its
|
||||
offset
|
||||
|
||||
Args:
|
||||
across_axis: Axis to mirror across
|
||||
(0: mirror across x axis, 1: mirror across y axis)
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for port in self.ports.values():
|
||||
port.mirror(across_axis)
|
||||
return self
|
||||
|
||||
def mirror(self, across_axis: int = 0) -> Self:
|
||||
"""
|
||||
Mirror the Pattern across an axis
|
||||
|
||||
Args:
|
||||
axis: Axis to mirror across
|
||||
(0: mirror across x axis, 1: mirror across y axis)
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.mirror_ports(across_axis)
|
||||
self.mirror_port_offsets(across_axis)
|
||||
port.flip_across(axis=axis)
|
||||
return self
|
||||
|
||||
def apply_ref_transform(self, ref: Ref) -> Self:
|
||||
|
|
@ -192,6 +157,8 @@ class Abstract(PortList):
|
|||
self.mirror()
|
||||
self.rotate_ports(ref.rotation)
|
||||
self.rotate_port_offsets(ref.rotation)
|
||||
if ref.scale != 1:
|
||||
self.scale_by(ref.scale)
|
||||
self.translate_ports(ref.offset)
|
||||
return self
|
||||
|
||||
|
|
@ -209,6 +176,8 @@ class Abstract(PortList):
|
|||
# TODO test undo_ref_transform
|
||||
"""
|
||||
self.translate_ports(-ref.offset)
|
||||
if ref.scale != 1:
|
||||
self.scale_by(1 / ref.scale)
|
||||
self.rotate_port_offsets(-ref.rotation)
|
||||
self.rotate_ports(-ref.rotation)
|
||||
if ref.mirrored:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from .builder import Builder as Builder
|
||||
from .pather import Pather as Pather
|
||||
from .renderpather import RenderPather as RenderPather
|
||||
from .pather_mixin import PortPather as PortPather
|
||||
from .pather import (
|
||||
Pather as Pather,
|
||||
PortPather as PortPather,
|
||||
)
|
||||
from .utils import ell as ell
|
||||
from .tools import (
|
||||
Tool as Tool,
|
||||
|
|
@ -9,4 +9,5 @@ from .tools import (
|
|||
SimpleTool as SimpleTool,
|
||||
AutoTool as AutoTool,
|
||||
PathTool as PathTool,
|
||||
)
|
||||
)
|
||||
from .logging import logged_op as logged_op
|
||||
|
|
|
|||
|
|
@ -1,448 +0,0 @@
|
|||
"""
|
||||
Simplified Pattern assembly (`Builder`)
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Iterable, Sequence, Mapping
|
||||
import copy
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary, TreeView
|
||||
from ..error import BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
`Builder` can also be `set_dead()`, at which point further calls to `plug()`
|
||||
and `place()` are ignored (intended for debugging).
|
||||
|
||||
|
||||
Examples: Creating a Builder
|
||||
===========================
|
||||
- `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'`.
|
||||
|
||||
- `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`
|
||||
|
||||
- `Builder(library, pattern=pattern, name='mypat')` uses an existing
|
||||
pattern (including its ports) and sets `library['mypat'] = pattern`.
|
||||
|
||||
- `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.
|
||||
|
||||
- `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
|
||||
=============================
|
||||
- `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'.
|
||||
|
||||
- `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 `thru` 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.
|
||||
|
||||
- `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.
|
||||
"""
|
||||
__slots__ = ('pattern', 'library', '_dead')
|
||||
|
||||
pattern: Pattern
|
||||
""" Layout of this device """
|
||||
|
||||
library: ILibrary
|
||||
"""
|
||||
Library from which patterns should be referenced
|
||||
"""
|
||||
|
||||
_dead: bool
|
||||
""" If True, plug()/place() are skipped (for debugging)"""
|
||||
|
||||
@property
|
||||
def ports(self) -> dict[str, Port]:
|
||||
return self.pattern.ports
|
||||
|
||||
@ports.setter
|
||||
def ports(self, value: dict[str, Port]) -> None:
|
||||
self.pattern.ports = value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: ILibrary,
|
||||
*,
|
||||
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`.
|
||||
"""
|
||||
self._dead = False
|
||||
self.library = library
|
||||
if pattern is not None:
|
||||
self.pattern = pattern
|
||||
else:
|
||||
self.pattern = Pattern()
|
||||
|
||||
if ports is not None:
|
||||
if self.pattern.ports:
|
||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
||||
if isinstance(ports, str):
|
||||
ports = library.abstract(ports).ports
|
||||
|
||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
||||
|
||||
if name is not None:
|
||||
library[name] = self.pattern
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls: type['Builder'],
|
||||
source: PortList | Mapping[str, Port] | str,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
in_prefix: str = 'in_',
|
||||
out_prefix: str = '',
|
||||
port_map: dict[str, str] | Sequence[str] | None = None,
|
||||
name: str | None = None,
|
||||
) -> 'Builder':
|
||||
"""
|
||||
Wrapper for `Pattern.interface()`, which returns a Builder 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.
|
||||
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 builder, 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.
|
||||
"""
|
||||
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
|
||||
|
||||
pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
|
||||
new = Builder(library=library, pattern=pat, 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,
|
||||
# so we shouldn't wrap Pattern.path()
|
||||
#@wraps(Pattern.path)
|
||||
#def path(self, *args, **kwargs) -> Self:
|
||||
# self.pattern.path(*args, **kwargs)
|
||||
# return self
|
||||
|
||||
def plug(
|
||||
self,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
map_in: dict[str, str],
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
thru: bool | str = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> Self:
|
||||
"""
|
||||
Wrapper around `Pattern.plug` which allows a string for `other`.
|
||||
|
||||
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`.
|
||||
|
||||
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, ...)`.
|
||||
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.
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern 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`).
|
||||
ok_connections: Set of "allowed" ptype combinations. Identical
|
||||
ptypes are always allowed to connect, as is `'unk'` with
|
||||
any other ptypte. Non-allowed ptype connections will emit a
|
||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||
`(b, a)`.
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
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,
|
||||
mirrored = mirrored,
|
||||
thru = thru,
|
||||
set_rotation = set_rotation,
|
||||
append = append,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
return self
|
||||
|
||||
def place(
|
||||
self,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
*,
|
||||
offset: ArrayLike = (0, 0),
|
||||
rotation: float = 0,
|
||||
pivot: ArrayLike = (0, 0),
|
||||
mirrored: bool = 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`.
|
||||
|
||||
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`.
|
||||
|
||||
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, ...)`.
|
||||
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 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
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
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,
|
||||
)
|
||||
return self
|
||||
|
||||
def translate(self, offset: ArrayLike) -> Self:
|
||||
"""
|
||||
Translate the pattern and all ports.
|
||||
|
||||
Args:
|
||||
offset: (x, y) distance to translate by
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.translate_elements(offset)
|
||||
return self
|
||||
|
||||
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
||||
"""
|
||||
Rotate the pattern and all ports.
|
||||
|
||||
Args:
|
||||
angle: angle (radians, counterclockwise) to rotate by
|
||||
pivot: location to rotate around
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.rotate_around(pivot, angle)
|
||||
for port in self.ports.values():
|
||||
port.rotate_around(pivot, angle)
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
"""
|
||||
Mirror the pattern and all ports across the specified axis.
|
||||
|
||||
Args:
|
||||
axis: Axis to mirror across (x=0, y=1)
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.mirror(axis)
|
||||
return self
|
||||
|
||||
def set_dead(self) -> Self:
|
||||
"""
|
||||
Disallows further changes through `plug()` or `place()`.
|
||||
This is meant for debugging:
|
||||
```
|
||||
dev.plug(a, ...)
|
||||
dev.set_dead() # added for debug purposes
|
||||
dev.plug(b, ...) # usually raises an error, but now skipped
|
||||
dev.plug(c, ...) # also skipped
|
||||
dev.pattern.visualize() # shows the device as of the set_dead() call
|
||||
```
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self._dead = True
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Builder {self.pattern} L({len(self.library)})>'
|
||||
return s
|
||||
|
||||
|
||||
120
masque/builder/logging.py
Normal file
120
masque/builder/logging.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""
|
||||
Logging and operation decorators for Pather
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from collections.abc import Iterator, Sequence, Callable
|
||||
import logging
|
||||
from functools import wraps
|
||||
import inspect
|
||||
import numpy
|
||||
from contextlib import contextmanager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .pather import Pather
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_log_args(**kwargs) -> str:
|
||||
arg_strs = []
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, str | int | float | bool | None):
|
||||
arg_strs.append(f"{k}={v}")
|
||||
elif isinstance(v, numpy.ndarray):
|
||||
arg_strs.append(f"{k}={v.tolist()}")
|
||||
elif isinstance(v, list | tuple) and len(v) <= 10:
|
||||
arg_strs.append(f"{k}={v}")
|
||||
else:
|
||||
arg_strs.append(f"{k}=...")
|
||||
return ", ".join(arg_strs)
|
||||
|
||||
|
||||
class PatherLogger:
|
||||
"""
|
||||
Encapsulates state for Pather diagnostic logging.
|
||||
"""
|
||||
debug: bool
|
||||
indent: int
|
||||
depth: int
|
||||
|
||||
def __init__(self, debug: bool = False) -> None:
|
||||
self.debug = debug
|
||||
self.indent = 0
|
||||
self.depth = 0
|
||||
|
||||
def _log(self, module_name: str, msg: str) -> None:
|
||||
if self.debug and self.depth <= 1:
|
||||
log_obj = logging.getLogger(module_name)
|
||||
log_obj.info(' ' * self.indent + msg)
|
||||
|
||||
@contextmanager
|
||||
def log_operation(
|
||||
self,
|
||||
pather: 'Pather',
|
||||
op: str,
|
||||
portspec: str | Sequence[str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[None]:
|
||||
if not self.debug or self.depth > 0:
|
||||
self.depth += 1
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.depth -= 1
|
||||
return
|
||||
|
||||
target = f"({portspec})" if portspec else ""
|
||||
module_name = pather.__class__.__module__
|
||||
self._log(module_name, f"Operation: {op}{target} {_format_log_args(**kwargs)}")
|
||||
|
||||
before_ports = {name: port.copy() for name, port in pather.ports.items()}
|
||||
self.depth += 1
|
||||
self.indent += 1
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
after_ports = pather.ports
|
||||
for name in sorted(after_ports.keys()):
|
||||
if name not in before_ports or after_ports[name] != before_ports[name]:
|
||||
self._log(module_name, f"Port {name}: {pather.ports[name].describe()}")
|
||||
for name in sorted(before_ports.keys()):
|
||||
if name not in after_ports:
|
||||
self._log(module_name, f"Port {name}: removed")
|
||||
|
||||
self.indent -= 1
|
||||
self.depth -= 1
|
||||
|
||||
|
||||
def logged_op(
|
||||
portspec_getter: Callable[[dict[str, Any]], str | Sequence[str] | None] | None = None,
|
||||
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||
"""
|
||||
Decorator to wrap Pather methods with logging.
|
||||
"""
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
sig = inspect.signature(func)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self: 'Pather', *args: Any, **kwargs: Any) -> Any:
|
||||
logger_obj = getattr(self, '_logger', None)
|
||||
if logger_obj is None or not logger_obj.debug:
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
bound = sig.bind(self, *args, **kwargs)
|
||||
bound.apply_defaults()
|
||||
all_args = bound.arguments
|
||||
# remove 'self' from logged args
|
||||
logged_args = {k: v for k, v in all_args.items() if k != 'self'}
|
||||
|
||||
ps = portspec_getter(all_args) if portspec_getter else None
|
||||
|
||||
# Remove portspec from logged_args if it's there to avoid duplicate arg to log_operation
|
||||
logged_args.pop('portspec', None)
|
||||
|
||||
with logger_obj.log_operation(self, func.__name__, ps, **logged_args):
|
||||
if getattr(self, '_dead', False) and func.__name__ in ('plug', 'place'):
|
||||
logger.warning(f"Skipping geometry for {func.__name__}() since device is dead")
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,677 +0,0 @@
|
|||
from typing import Self, overload
|
||||
from collections.abc import Sequence, Iterator, Iterable
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from abc import abstractmethod, ABCMeta
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary, TreeView
|
||||
from ..error import PortError, BuildError
|
||||
from ..utils import SupportsBool
|
||||
from ..abstract import Abstract
|
||||
from .tools import Tool
|
||||
from .utils import ell
|
||||
from ..ports import PortList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PatherMixin(PortList, metaclass=ABCMeta):
|
||||
pattern: Pattern
|
||||
""" Layout of this device """
|
||||
|
||||
library: ILibrary
|
||||
""" Library from which patterns should be referenced """
|
||||
|
||||
_dead: bool
|
||||
""" If True, plug()/place() are skipped (for debugging) """
|
||||
|
||||
tools: dict[str | None, Tool]
|
||||
"""
|
||||
Tool objects are used to dynamically generate new single-use Devices
|
||||
(e.g wires or waveguides) to be plugged into this device.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def path(
|
||||
self,
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pathS(
|
||||
self,
|
||||
portspec: str,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def plug(
|
||||
self,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
map_in: dict[str, str],
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
thru: bool | str = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> Self:
|
||||
pass
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
return self
|
||||
|
||||
@contextmanager
|
||||
def toolctx(
|
||||
self,
|
||||
tool: Tool,
|
||||
keys: str | Sequence[str | None] | None = None,
|
||||
) -> Iterator[Self]:
|
||||
"""
|
||||
Context manager for temporarily `retool`-ing and reverting the `retool`
|
||||
upon exiting the context.
|
||||
|
||||
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):
|
||||
keys = [keys]
|
||||
saved_tools = {kk: self.tools.get(kk, None) for kk in keys} # If not in self.tools, save `None`
|
||||
try:
|
||||
yield self.retool(tool=tool, keys=keys)
|
||||
finally:
|
||||
for kk, tt in saved_tools.items():
|
||||
if tt is None:
|
||||
# delete if present
|
||||
self.tools.pop(kk, None)
|
||||
else:
|
||||
self.tools[kk] = tt
|
||||
|
||||
def path_to(
|
||||
self,
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
position: float | None = None,
|
||||
*,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Build 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.
|
||||
|
||||
If using `RenderPather`, `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.
|
||||
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]
|
||||
if port.rotation is None:
|
||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||
|
||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
return self.path(
|
||||
portspec,
|
||||
ccw,
|
||||
length,
|
||||
plug_into = plug_into,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def path_into(
|
||||
self,
|
||||
portspec_src: str,
|
||||
portspec_dst: str,
|
||||
*,
|
||||
out_ptype: str | None = None,
|
||||
plug_destination: bool = True,
|
||||
thru: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a "wire"/"waveguide" 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).
|
||||
|
||||
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||
|
||||
Args:
|
||||
portspec_src: The name of the starting port into which the wire will be plugged.
|
||||
portspec_dst: The name of the destination port.
|
||||
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.
|
||||
thru: If not `None`, the port by this name will be rename to `portspec_src`.
|
||||
This can be used when routing a signal through a pre-placed 2-port device.
|
||||
|
||||
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)
|
||||
|
||||
dst_extra_args = {'out_ptype': out_ptype}
|
||||
if plug_destination:
|
||||
dst_extra_args['plug_into'] = portspec_dst
|
||||
|
||||
src_args = {**kwargs}
|
||||
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)
|
||||
else:
|
||||
# S-bend, delegate to implementations
|
||||
(travel, jog), _ = port_src.measure_travel(port_dst)
|
||||
self.pathS(portspec_src, -travel, -jog, **dst_args)
|
||||
elif numpy.isclose(angle, 0):
|
||||
raise BuildError('Don\'t know how to route a U-bend yet (TODO)!')
|
||||
else:
|
||||
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
||||
|
||||
if thru is not None:
|
||||
self.rename_ports({thru: portspec_src})
|
||||
|
||||
return self
|
||||
|
||||
def mpath(
|
||||
self,
|
||||
portspec: str | Sequence[str],
|
||||
ccw: SupportsBool | None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
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".
|
||||
|
||||
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.
|
||||
|
||||
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||
|
||||
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
|
||||
|
||||
bound_types = set()
|
||||
if 'bound_type' in kwargs:
|
||||
bound_types.add(kwargs.pop('bound_type'))
|
||||
bound = kwargs.pop('bound')
|
||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
||||
if bt in kwargs:
|
||||
bound_types.add(bt)
|
||||
bound = kwargs.pop(bt)
|
||||
|
||||
if not bound_types:
|
||||
raise BuildError('No bound type specified for mpath')
|
||||
if len(bound_types) > 1:
|
||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||
bound_type = tuple(bound_types)[0]
|
||||
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
ports = self.pattern[tuple(portspec)]
|
||||
|
||||
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
||||
|
||||
#if container:
|
||||
# assert not getattr(self, 'render'), 'Containers not implemented for RenderPather'
|
||||
# bld = self.interface(source=ports, library=self.library, tools=self.tools)
|
||||
# for port_name, length in extensions.items():
|
||||
# bld.path(port_name, ccw, length, **kwargs)
|
||||
# self.library[container] = bld.pattern
|
||||
# self.plug(Abstract(container, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
||||
#else:
|
||||
for port_name, length in extensions.items():
|
||||
self.path(port_name, ccw, length, **kwargs)
|
||||
return self
|
||||
|
||||
# TODO def bus_join()?
|
||||
|
||||
def flatten(self) -> Self:
|
||||
"""
|
||||
Flatten the contained pattern, using the contained library to resolve references.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.flatten(self.library)
|
||||
return self
|
||||
|
||||
def at(self, portspec: str | Iterable[str]) -> 'PortPather':
|
||||
return PortPather(portspec, self)
|
||||
|
||||
|
||||
class PortPather:
|
||||
"""
|
||||
Port state manager
|
||||
|
||||
This class provides a convenient way to perform multiple pathing operations on a
|
||||
set of ports without needing to repeatedly pass their names.
|
||||
"""
|
||||
ports: list[str]
|
||||
pather: PatherMixin
|
||||
|
||||
def __init__(self, ports: str | Iterable[str], pather: PatherMixin) -> None:
|
||||
self.ports = [ports] if isinstance(ports, str) else list(ports)
|
||||
self.pather = pather
|
||||
|
||||
#
|
||||
# Delegate to pather
|
||||
#
|
||||
def retool(self, tool: Tool) -> Self:
|
||||
self.pather.retool(tool, keys=self.ports)
|
||||
return self
|
||||
|
||||
@contextmanager
|
||||
def toolctx(self, tool: Tool) -> Iterator[Self]:
|
||||
with self.pather.toolctx(tool, keys=self.ports):
|
||||
yield self
|
||||
|
||||
def path(self, *args, **kwargs) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
logger.warning('Use path_each() when pathing multiple ports independently')
|
||||
for port in self.ports:
|
||||
self.pather.path(port, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def path_each(self, *args, **kwargs) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather.path(port, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def pathS(self, *args, **kwargs) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
logger.warning('Use pathS_each() when pathing multiple ports independently')
|
||||
for port in self.ports:
|
||||
self.pather.pathS(port, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def pathS_each(self, *args, **kwargs) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather.pathS(port, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def path_to(self, *args, **kwargs) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
logger.warning('Use path_each_to() when pathing multiple ports independently')
|
||||
for port in self.ports:
|
||||
self.pather.path_to(port, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def path_each_to(self, *args, **kwargs) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather.path_to(port, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def mpath(self, *args, **kwargs) -> Self:
|
||||
self.pather.mpath(self.ports, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def path_into(self, *args, **kwargs) -> Self:
|
||||
""" Path_into, using the current port as the source """
|
||||
if len(self.ports) > 1:
|
||||
raise BuildError(f'Unable use implicit path_into() with {len(self.ports)} (>1) ports.')
|
||||
self.pather.path_into(self.ports[0], *args, **kwargs)
|
||||
return self
|
||||
|
||||
def path_from(self, *args, **kwargs) -> Self:
|
||||
""" Path_into, using the current port as the destination """
|
||||
if len(self.ports) > 1:
|
||||
raise BuildError(f'Unable use implicit path_from() with {len(self.ports)} (>1) ports.')
|
||||
thru = kwargs.pop('thru', None)
|
||||
self.pather.path_into(args[0], self.ports[0], *args[1:], **kwargs)
|
||||
if thru is not None:
|
||||
self.rename_from(thru)
|
||||
return self
|
||||
|
||||
def plug(
|
||||
self,
|
||||
other: Abstract | str,
|
||||
other_port: str,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
raise BuildError(f'Unable use implicit plug() with {len(self.ports)} ports.'
|
||||
'Use the pather or pattern directly to plug multiple ports.')
|
||||
self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def plugged(self, other_port: str) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.')
|
||||
self.pather.plugged({self.ports[0]: other_port})
|
||||
return self
|
||||
|
||||
#
|
||||
# Delegate to port
|
||||
#
|
||||
def set_ptype(self, ptype: str) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather[port].set_ptype(ptype)
|
||||
return self
|
||||
|
||||
def translate(self, *args, **kwargs) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather[port].translate(*args, **kwargs)
|
||||
return self
|
||||
|
||||
def mirror(self, *args, **kwargs) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather[port].mirror(*args, **kwargs)
|
||||
return self
|
||||
|
||||
def rotate(self, rotation: float) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather[port].rotate(rotation)
|
||||
return self
|
||||
|
||||
def set_rotation(self, rotation: float | None) -> Self:
|
||||
for port in self.ports:
|
||||
self.pather[port].set_rotation(rotation)
|
||||
return self
|
||||
|
||||
def rename_to(self, new_name: str) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
BuildError('Use rename_ports() for >1 port')
|
||||
self.pather.rename_ports({self.ports[0]: new_name})
|
||||
self.ports[0] = new_name
|
||||
return self
|
||||
|
||||
def rename_from(self, old_name: str) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
BuildError('Use rename_ports() for >1 port')
|
||||
self.pather.rename_ports({old_name: self.ports[0]})
|
||||
return self
|
||||
|
||||
def rename_ports(self, name_map: dict[str, str | None]) -> Self:
|
||||
self.pather.rename_ports(name_map)
|
||||
self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None]
|
||||
return self
|
||||
|
||||
def add_ports(self, ports: Iterable[str]) -> Self:
|
||||
ports = list(ports)
|
||||
conflicts = set(ports) & set(self.ports)
|
||||
if conflicts:
|
||||
raise BuildError(f'ports {conflicts} already selected')
|
||||
self.ports += ports
|
||||
return self
|
||||
|
||||
def add_port(self, port: str, index: int | None = None) -> Self:
|
||||
if port in self.ports:
|
||||
raise BuildError(f'{port=} already selected')
|
||||
if index is not None:
|
||||
self.ports.insert(index, port)
|
||||
else:
|
||||
self.ports.append(port)
|
||||
return self
|
||||
|
||||
def drop_port(self, port: str) -> Self:
|
||||
if port not in self.ports:
|
||||
raise BuildError(f'{port=} already not selected')
|
||||
self.ports = [pp for pp in self.ports if pp != port]
|
||||
return self
|
||||
|
||||
def into_copy(self, new_name: str, src: str | None = None) -> Self:
|
||||
""" Copy a port and replace it with the copy """
|
||||
if not self.ports:
|
||||
raise BuildError('Have no ports to copy')
|
||||
if len(self.ports) == 1:
|
||||
src = self.ports[0]
|
||||
elif src is None:
|
||||
raise BuildError('Must specify src when >1 port is available')
|
||||
if src not in self.ports:
|
||||
raise BuildError(f'{src=} not available')
|
||||
self.pather.ports[new_name] = self.pather[src].copy()
|
||||
self.ports = [(new_name if pp == src else pp) for pp in self.ports]
|
||||
return self
|
||||
|
||||
def save_copy(self, new_name: str, src: str | None = None) -> Self:
|
||||
""" Copy a port and but keep using the original """
|
||||
if not self.ports:
|
||||
raise BuildError('Have no ports to copy')
|
||||
if len(self.ports) == 1:
|
||||
src = self.ports[0]
|
||||
elif src is None:
|
||||
raise BuildError('Must specify src when >1 port is available')
|
||||
if src not in self.ports:
|
||||
raise BuildError(f'{src=} not available')
|
||||
self.pather.ports[new_name] = self.pather[src].copy()
|
||||
return self
|
||||
|
||||
@overload
|
||||
def delete(self, name: None) -> None: ...
|
||||
|
||||
@overload
|
||||
def delete(self, name: str) -> Self: ...
|
||||
|
||||
def delete(self, name: str | None = None) -> Self | None:
|
||||
if name is None:
|
||||
for pp in self.ports:
|
||||
del self.pather.ports[pp]
|
||||
return None
|
||||
del self.pather.ports[name]
|
||||
self.ports = [pp for pp in self.ports if pp != name]
|
||||
return self
|
||||
|
||||
|
|
@ -1,646 +0,0 @@
|
|||
"""
|
||||
Pather with batched (multi-step) rendering
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Mapping, MutableMapping, Iterable
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
from pprint import pformat
|
||||
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary, TreeView
|
||||
from ..error import BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
from ..utils import SupportsBool
|
||||
from .tools import Tool, RenderStep
|
||||
from .pather_mixin import PatherMixin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RenderPather(PatherMixin):
|
||||
"""
|
||||
`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 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` """
|
||||
|
||||
tools: dict[str | None, Tool]
|
||||
"""
|
||||
Tool objects are used to dynamically generate new single-use Devices
|
||||
(e.g wires or waveguides) to be plugged into this device.
|
||||
"""
|
||||
|
||||
@property
|
||||
def ports(self) -> dict[str, Port]:
|
||||
return self.pattern.ports
|
||||
|
||||
@ports.setter
|
||||
def ports(self, value: dict[str, Port]) -> None:
|
||||
self.pattern.ports = value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: ILibrary,
|
||||
*,
|
||||
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`.
|
||||
"""
|
||||
self._dead = False
|
||||
self.paths = defaultdict(list)
|
||||
self.library = library
|
||||
if pattern is not None:
|
||||
self.pattern = pattern
|
||||
else:
|
||||
self.pattern = Pattern()
|
||||
|
||||
if ports is not None:
|
||||
if self.pattern.ports:
|
||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
||||
if isinstance(ports, str):
|
||||
ports = library.abstract(ports).ports
|
||||
|
||||
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):
|
||||
self.tools = {None: tools}
|
||||
else:
|
||||
self.tools = dict(tools)
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls: type['RenderPather'],
|
||||
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.
|
||||
|
||||
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 `RenderPather`, 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.
|
||||
"""
|
||||
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
|
||||
|
||||
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)
|
||||
return new
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<RenderPather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
return s
|
||||
|
||||
def plug(
|
||||
self,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
map_in: dict[str, str],
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
thru: bool | str = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> 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.
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern 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`).
|
||||
ok_connections: Set of "allowed" ptype combinations. Identical
|
||||
ptypes are always allowed to connect, as is `'unk'` with
|
||||
any other ptypte. Non-allowed ptype connections will emit a
|
||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||
`(b, a)`.
|
||||
|
||||
|
||||
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]
|
||||
|
||||
# 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))
|
||||
|
||||
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))
|
||||
|
||||
self.pattern.plug(
|
||||
other = other_tgt,
|
||||
map_in = map_in,
|
||||
map_out = map_out,
|
||||
mirrored = mirrored,
|
||||
thru = thru,
|
||||
set_rotation = set_rotation,
|
||||
append = append,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def place(
|
||||
self,
|
||||
other: Abstract | str,
|
||||
*,
|
||||
offset: ArrayLike = (0, 0),
|
||||
rotation: float = 0,
|
||||
pivot: ArrayLike = (0, 0),
|
||||
mirrored: bool = 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]
|
||||
|
||||
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))
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def plugged(
|
||||
self,
|
||||
connections: dict[str, str],
|
||||
) -> Self:
|
||||
for aa, bb in connections.items():
|
||||
porta = self.ports[aa]
|
||||
portb = self.ports[bb]
|
||||
self.paths[aa].append(RenderStep('P', None, porta.copy(), porta.copy(), None))
|
||||
self.paths[bb].append(RenderStep('P', None, portb.copy(), portb.copy(), None))
|
||||
PortList.plugged(self, connections)
|
||||
return self
|
||||
|
||||
def path(
|
||||
self,
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
**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.)
|
||||
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
|
||||
|
||||
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()?
|
||||
|
||||
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)
|
||||
|
||||
# Update port
|
||||
out_port.rotate_around((0, 0), pi + port_rot)
|
||||
out_port.translate(port.offset)
|
||||
|
||||
step = RenderStep('L', tool, port.copy(), out_port.copy(), data)
|
||||
self.paths[portspec].append(step)
|
||||
|
||||
self.pattern.ports[portspec] = out_port.copy()
|
||||
|
||||
if plug_into is not None:
|
||||
self.plugged({portspec: plug_into})
|
||||
|
||||
return self
|
||||
|
||||
def pathS(
|
||||
self,
|
||||
portspec: str,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||
left of direction of travel).
|
||||
|
||||
The output port will have the same orientation as the source port (`portspec`).
|
||||
|
||||
`RenderPather.render` must be called after all paths have been fully planned.
|
||||
|
||||
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
|
||||
raises a NotImplementedError.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||
Positive values are to the left of the direction of travel.
|
||||
length: The total manhattan distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
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 s-bend (for nonzero jog).
|
||||
LibraryError if no valid name could be picked for the pattern.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping pathS() since device is dead')
|
||||
return self
|
||||
|
||||
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()?
|
||||
|
||||
tool = self.tools.get(portspec, self.tools[None])
|
||||
|
||||
# check feasibility, get output port and data
|
||||
try:
|
||||
out_port, data = tool.planS(length, jog, in_ptype=in_ptype, **kwargs)
|
||||
except NotImplementedError:
|
||||
# Fall back to drawing two L-bends
|
||||
ccw0 = jog > 0
|
||||
kwargs_no_out = (kwargs | {'out_ptype': None})
|
||||
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail with asymmetric ptypes
|
||||
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
||||
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
||||
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
|
||||
|
||||
kwargs_plug = kwargs | {'plug_into': plug_into}
|
||||
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||
return self
|
||||
|
||||
out_port.rotate_around((0, 0), pi + port_rot)
|
||||
out_port.translate(port.offset)
|
||||
step = RenderStep('S', tool, port.copy(), out_port.copy(), data)
|
||||
self.paths[portspec].append(step)
|
||||
self.pattern.ports[portspec] = out_port.copy()
|
||||
|
||||
if plug_into is not None:
|
||||
self.plugged({portspec: plug_into})
|
||||
return self
|
||||
|
||||
|
||||
def render(
|
||||
self,
|
||||
append: bool = True,
|
||||
) -> Self:
|
||||
"""
|
||||
Generate the geometry which has been planned out with `path`/`path_to`/etc.
|
||||
|
||||
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)
|
||||
|
||||
for portspec, steps in self.paths.items():
|
||||
batch: list[RenderStep] = []
|
||||
for step in steps:
|
||||
appendable_op = step.opcode in ('L', 'S', 'U')
|
||||
same_tool = batch and step.tool == batch[0].tool
|
||||
|
||||
# If we can't continue a batch, render it
|
||||
if batch and (not appendable_op or not same_tool):
|
||||
render_batch(portspec, batch, append)
|
||||
batch = []
|
||||
|
||||
# batch is emptied already if we couldn't continue it
|
||||
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 the last batch didn't end yet
|
||||
if batch:
|
||||
render_batch(portspec, batch, append)
|
||||
|
||||
self.paths.clear()
|
||||
pat.ports.clear()
|
||||
self.pattern.append(pat)
|
||||
|
||||
return self
|
||||
|
||||
def translate(self, offset: ArrayLike) -> Self:
|
||||
"""
|
||||
Translate the pattern and all ports.
|
||||
|
||||
Args:
|
||||
offset: (x, y) distance to translate by
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.translate_elements(offset)
|
||||
return self
|
||||
|
||||
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
||||
"""
|
||||
Rotate the pattern and all ports.
|
||||
|
||||
Args:
|
||||
angle: angle (radians, counterclockwise) to rotate by
|
||||
pivot: location to rotate around
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.rotate_around(pivot, angle)
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int) -> Self:
|
||||
"""
|
||||
Mirror the pattern and all ports across the specified axis.
|
||||
|
||||
Args:
|
||||
axis: Axis to mirror across (x=0, y=1)
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.mirror(axis)
|
||||
return self
|
||||
|
||||
def set_dead(self) -> Self:
|
||||
"""
|
||||
Disallows further changes through `plug()` or `place()`.
|
||||
This is meant for debugging:
|
||||
```
|
||||
dev.plug(a, ...)
|
||||
dev.set_dead() # added for debug purposes
|
||||
dev.plug(b, ...) # usually raises an error, but now skipped
|
||||
dev.plug(c, ...) # also skipped
|
||||
dev.pattern.visualize() # shows the device as of the set_dead() call
|
||||
```
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self._dead = True
|
||||
return self
|
||||
|
||||
@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
|
||||
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
"""
|
||||
Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides)
|
||||
|
||||
# TODO document all tools
|
||||
Concrete tools may implement native planning/rendering for `L`, `S`, or `U` routes.
|
||||
Any unimplemented planning method falls back to the corresponding `trace*()` method,
|
||||
and `Pather` may further synthesize some routes from simpler primitives when needed.
|
||||
"""
|
||||
from typing import Literal, Any, Self
|
||||
from collections.abc import Sequence, Callable
|
||||
from typing import Literal, Any, Self, cast
|
||||
from collections.abc import Sequence, Callable, Iterator
|
||||
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
|
@ -23,8 +25,8 @@ from ..error import BuildError
|
|||
@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.
|
||||
Representation of a single saved operation, used by deferred `Pather`
|
||||
instances and passed to `Tool.render()` when `Pather.render()` is called.
|
||||
"""
|
||||
opcode: Literal['L', 'S', 'U', 'P']
|
||||
""" What operation is being performed.
|
||||
|
|
@ -47,16 +49,72 @@ class RenderStep:
|
|||
if self.opcode != 'P' and self.tool is None:
|
||||
raise BuildError('Got tool=None but the opcode is not "P"')
|
||||
|
||||
def is_continuous_with(self, other: 'RenderStep') -> bool:
|
||||
"""
|
||||
Check if another RenderStep can be appended to this one.
|
||||
"""
|
||||
# Check continuity with tolerance
|
||||
offsets_match = bool(numpy.allclose(other.start_port.offset, self.end_port.offset))
|
||||
rotations_match = (other.start_port.rotation is None and self.end_port.rotation is None) or (
|
||||
other.start_port.rotation is not None and self.end_port.rotation is not None and
|
||||
bool(numpy.isclose(other.start_port.rotation, self.end_port.rotation))
|
||||
)
|
||||
return offsets_match and rotations_match
|
||||
|
||||
def transformed(self, translation: NDArray[numpy.float64], rotation: float, pivot: NDArray[numpy.float64]) -> 'RenderStep':
|
||||
"""
|
||||
Return a new RenderStep with transformed start and end ports.
|
||||
"""
|
||||
new_start = self.start_port.copy()
|
||||
new_end = self.end_port.copy()
|
||||
|
||||
for pp in (new_start, new_end):
|
||||
pp.rotate_around(pivot, rotation)
|
||||
pp.translate(translation)
|
||||
|
||||
return RenderStep(
|
||||
opcode = self.opcode,
|
||||
tool = self.tool,
|
||||
start_port = new_start,
|
||||
end_port = new_end,
|
||||
data = self.data,
|
||||
)
|
||||
|
||||
def mirrored(self, axis: int) -> 'RenderStep':
|
||||
"""
|
||||
Return a new RenderStep with mirrored start and end ports.
|
||||
"""
|
||||
new_start = self.start_port.copy()
|
||||
new_end = self.end_port.copy()
|
||||
|
||||
new_start.flip_across(axis=axis)
|
||||
new_end.flip_across(axis=axis)
|
||||
|
||||
return RenderStep(
|
||||
opcode = self.opcode,
|
||||
tool = self.tool,
|
||||
start_port = new_start,
|
||||
end_port = new_end,
|
||||
data = self.data,
|
||||
)
|
||||
|
||||
|
||||
def measure_tool_plan(tree: ILibrary, port_names: tuple[str, str]) -> tuple[Port, Any]:
|
||||
"""
|
||||
Extracts a Port and returns the tree (as data) for tool planning fallbacks.
|
||||
"""
|
||||
pat = tree.top_pattern()
|
||||
in_p = pat[port_names[0]]
|
||||
out_p = pat[port_names[1]]
|
||||
(travel, jog), rot = in_p.measure_travel(out_p)
|
||||
return Port((travel, jog), rotation=rot, ptype=out_p.ptype), tree
|
||||
|
||||
|
||||
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(
|
||||
def traceL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
|
|
@ -70,7 +128,7 @@ class Tool:
|
|||
Create a wire or waveguide that travels exactly `length` distance along the axis
|
||||
of its input port.
|
||||
|
||||
Used by `Pather` and `RenderPather`.
|
||||
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
|
||||
|
|
@ -99,9 +157,9 @@ class Tool:
|
|||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||
raise NotImplementedError(f'traceL() not implemented for {type(self)}')
|
||||
|
||||
def pathS(
|
||||
def traceS(
|
||||
self,
|
||||
length: float,
|
||||
jog: float,
|
||||
|
|
@ -116,7 +174,7 @@ class Tool:
|
|||
of its input port, and `jog` distance on the perpendicular axis.
|
||||
`jog` is positive when moving left of the direction of travel (from input to ouput port).
|
||||
|
||||
Used by `Pather` and `RenderPather`.
|
||||
Used by `Pather`.
|
||||
|
||||
The output port should be rotated to face the input port (i.e. plugging the device
|
||||
into a port will move that port but keep its orientation).
|
||||
|
|
@ -141,7 +199,7 @@ class Tool:
|
|||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||
raise NotImplementedError(f'traceS() not implemented for {type(self)}')
|
||||
|
||||
def planL(
|
||||
self,
|
||||
|
|
@ -156,7 +214,7 @@ class Tool:
|
|||
Plan a wire or waveguide that travels exactly `length` distance along the axis
|
||||
of its input port.
|
||||
|
||||
Used by `RenderPather`.
|
||||
Used by `Pather` when `auto_render=False`.
|
||||
|
||||
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
|
||||
|
|
@ -183,7 +241,17 @@ class Tool:
|
|||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'planL() not implemented for {type(self)}')
|
||||
# Fallback implementation using traceL
|
||||
port_names = kwargs.pop('port_names', ('A', 'B'))
|
||||
tree = self.traceL(
|
||||
ccw,
|
||||
length,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
port_names=port_names,
|
||||
**kwargs,
|
||||
)
|
||||
return measure_tool_plan(tree, port_names)
|
||||
|
||||
def planS(
|
||||
self,
|
||||
|
|
@ -198,7 +266,7 @@ class Tool:
|
|||
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`.
|
||||
Used by `Pather` when `auto_render=False`.
|
||||
|
||||
The output port must have an orientation rotated by pi from the input port.
|
||||
|
||||
|
|
@ -208,8 +276,8 @@ class Tool:
|
|||
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)
|
||||
A positive number implies a leftward shift (i.e. counterclockwise bend followed
|
||||
by a clockwise 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.
|
||||
|
|
@ -221,7 +289,58 @@ class Tool:
|
|||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'planS() not implemented for {type(self)}')
|
||||
# Fallback implementation using traceS
|
||||
port_names = kwargs.pop('port_names', ('A', 'B'))
|
||||
tree = self.traceS(
|
||||
length,
|
||||
jog,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
port_names=port_names,
|
||||
**kwargs,
|
||||
)
|
||||
return measure_tool_plan(tree, port_names)
|
||||
|
||||
def traceU(
|
||||
self,
|
||||
jog: float,
|
||||
*,
|
||||
length: float = 0,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> Library:
|
||||
"""
|
||||
Create a wire or waveguide that travels exactly `jog` distance along the axis
|
||||
perpendicular to its input port (i.e. a U-bend).
|
||||
|
||||
Used by `Pather`. Tools may leave this unimplemented if they
|
||||
do not support a native U-bend primitive.
|
||||
|
||||
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. They should also be named `port_names[0]` and
|
||||
`port_names[1]`, respectively.
|
||||
|
||||
Args:
|
||||
jog: The total offset from the input to output, along the perpendicular axis.
|
||||
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
||||
followed by a clockwise 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.
|
||||
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 U-shaped wire or waveguide
|
||||
|
||||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'traceU() not implemented for {type(self)}')
|
||||
|
||||
def planU(
|
||||
self,
|
||||
|
|
@ -232,12 +351,12 @@ class Tool:
|
|||
**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`.
|
||||
Used by `Pather` when `auto_render=False`. This is an optional native-planning hook: tools may
|
||||
implement it when they can represent a U-turn directly, otherwise they may rely
|
||||
on `traceU()` or let `Pather` synthesize the route from simpler primitives.
|
||||
|
||||
The output port must have an orientation identical to the input port.
|
||||
|
||||
|
|
@ -246,11 +365,12 @@ class Tool:
|
|||
|
||||
Args:
|
||||
jog: The total offset from the input to output, along the perpendicular axis.
|
||||
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
||||
A positive number implies a leftwards shift (i.e. counterclockwise_bend
|
||||
followed by a clockwise 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.
|
||||
kwargs: Custom tool-specific parameters. `length` may be supplied here to
|
||||
request a U-turn whose final port is displaced along both axes.
|
||||
|
||||
Returns:
|
||||
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
||||
|
|
@ -259,14 +379,26 @@ class Tool:
|
|||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'planU() not implemented for {type(self)}')
|
||||
# Fallback implementation using traceU
|
||||
kwargs = dict(kwargs)
|
||||
length = kwargs.pop('length', 0)
|
||||
port_names = kwargs.pop('port_names', ('A', 'B'))
|
||||
tree = self.traceU(
|
||||
jog,
|
||||
length=length,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
port_names=port_names,
|
||||
**kwargs,
|
||||
)
|
||||
return measure_tool_plan(tree, port_names)
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'), # noqa: ARG002 (unused)
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
"""
|
||||
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
||||
|
|
@ -280,7 +412,50 @@ class Tool:
|
|||
kwargs: Custom tool-specific parameters.
|
||||
"""
|
||||
assert not batch or batch[0].tool == self
|
||||
raise NotImplementedError(f'render() not implemented for {type(self)}')
|
||||
# Fallback: render each step individually
|
||||
lib, pat = Library.mktree(SINGLE_USE_PREFIX + 'batch')
|
||||
pat.add_port_pair(names=port_names, ptype=batch[0].start_port.ptype if batch else 'unk')
|
||||
|
||||
for step in batch:
|
||||
if step.opcode == 'L':
|
||||
if isinstance(step.data, ILibrary):
|
||||
seg_tree = step.data
|
||||
else:
|
||||
# extract parameters from kwargs or data
|
||||
seg_tree = self.traceL(
|
||||
ccw=step.data.get('ccw') if isinstance(step.data, dict) else None,
|
||||
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
||||
port_names=port_names,
|
||||
**kwargs,
|
||||
)
|
||||
elif step.opcode == 'S':
|
||||
if isinstance(step.data, ILibrary):
|
||||
seg_tree = step.data
|
||||
else:
|
||||
seg_tree = self.traceS(
|
||||
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
||||
jog=float(step.data.get('jog', 0)) if isinstance(step.data, dict) else 0.0,
|
||||
port_names=port_names,
|
||||
**kwargs,
|
||||
)
|
||||
elif step.opcode == 'U':
|
||||
if isinstance(step.data, ILibrary):
|
||||
seg_tree = step.data
|
||||
else:
|
||||
seg_tree = self.traceU(
|
||||
jog=float(step.data.get('jog', 0)) if isinstance(step.data, dict) else 0.0,
|
||||
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
||||
port_names=port_names,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
continue
|
||||
|
||||
seg_name = lib << seg_tree
|
||||
pat.plug(lib[seg_name], {port_names[1]: port_names[0]}, append=True)
|
||||
del lib[seg_name]
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
abstract_tuple_t = tuple[Abstract, str, str]
|
||||
|
|
@ -390,7 +565,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
|
|||
pat.plug(bend, {port_names[1]: inport}, mirrored=mirrored)
|
||||
return tree
|
||||
|
||||
def path(
|
||||
def traceL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
|
|
@ -407,7 +582,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
|
|||
out_ptype = out_ptype,
|
||||
)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||
self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||
return tree
|
||||
|
|
@ -420,7 +595,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
|
|||
**kwargs,
|
||||
) -> ILibrary:
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
||||
|
||||
for step in batch:
|
||||
|
|
@ -497,6 +672,19 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
def reversed(self) -> Self:
|
||||
return type(self)(self.abstract, self.our_port_name, self.their_port_name)
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LPlan:
|
||||
""" Template for an L-path configuration """
|
||||
straight: 'AutoTool.Straight'
|
||||
bend: 'AutoTool.Bend | None'
|
||||
in_trans: 'AutoTool.Transition | None'
|
||||
b_trans: 'AutoTool.Transition | None'
|
||||
out_trans: 'AutoTool.Transition | None'
|
||||
overhead_x: float
|
||||
overhead_y: float
|
||||
bend_angle: float
|
||||
out_ptype: str
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LData:
|
||||
""" Data for planL """
|
||||
|
|
@ -509,6 +697,65 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
b_transition: 'AutoTool.Transition | None'
|
||||
out_transition: 'AutoTool.Transition | None'
|
||||
|
||||
def _iter_l_plans(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
in_ptype: str | None,
|
||||
out_ptype: str | None,
|
||||
) -> Iterator[LPlan]:
|
||||
"""
|
||||
Iterate over all possible combinations of straights and bends that
|
||||
could form an L-path.
|
||||
"""
|
||||
bends = cast('list[AutoTool.Bend | None]', self.bends)
|
||||
if ccw is None and not bends:
|
||||
bends = [None]
|
||||
|
||||
for straight in self.straights:
|
||||
for bend in bends:
|
||||
bend_dxy, bend_angle = self._bend2dxy(bend, ccw)
|
||||
|
||||
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, straight.ptype)
|
||||
in_transition = self.transitions.get(in_ptype_pair, None)
|
||||
itrans_dxy = self._itransition2dxy(in_transition)
|
||||
|
||||
out_ptype_pair = (
|
||||
'unk' if out_ptype is None else out_ptype,
|
||||
straight.ptype if ccw is None else cast('AutoTool.Bend', bend).out_port.ptype
|
||||
)
|
||||
out_transition = self.transitions.get(out_ptype_pair, None)
|
||||
otrans_dxy = self._otransition2dxy(out_transition, bend_angle)
|
||||
|
||||
b_transition = None
|
||||
if ccw is not None:
|
||||
assert bend is not None
|
||||
if bend.in_port.ptype != straight.ptype:
|
||||
b_transition = self.transitions.get((bend.in_port.ptype, straight.ptype), None)
|
||||
btrans_dxy = self._itransition2dxy(b_transition)
|
||||
|
||||
overhead_x = bend_dxy[0] + itrans_dxy[0] + btrans_dxy[0] + otrans_dxy[0]
|
||||
overhead_y = bend_dxy[1] + itrans_dxy[1] + btrans_dxy[1] + otrans_dxy[1]
|
||||
|
||||
if out_transition is not None:
|
||||
out_ptype_actual = out_transition.their_port.ptype
|
||||
elif ccw is not None:
|
||||
assert bend is not None
|
||||
out_ptype_actual = bend.out_port.ptype
|
||||
else:
|
||||
out_ptype_actual = straight.ptype
|
||||
|
||||
yield self.LPlan(
|
||||
straight = straight,
|
||||
bend = bend,
|
||||
in_trans = in_transition,
|
||||
b_trans = b_transition,
|
||||
out_trans = out_transition,
|
||||
overhead_x = overhead_x,
|
||||
overhead_y = overhead_y,
|
||||
bend_angle = bend_angle,
|
||||
out_ptype = out_ptype_actual,
|
||||
)
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SData:
|
||||
""" Data for planS """
|
||||
|
|
@ -521,6 +768,80 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
b_transition: 'AutoTool.Transition | None'
|
||||
out_transition: 'AutoTool.Transition | None'
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class UData:
|
||||
""" Data for planU or planS (double-L) """
|
||||
ldata0: 'AutoTool.LData'
|
||||
ldata1: 'AutoTool.LData'
|
||||
straight2: 'AutoTool.Straight'
|
||||
l2_length: float
|
||||
mid_transition: 'AutoTool.Transition | None'
|
||||
|
||||
def _solve_double_l(
|
||||
self,
|
||||
length: float,
|
||||
jog: float,
|
||||
ccw1: SupportsBool,
|
||||
ccw2: SupportsBool,
|
||||
in_ptype: str | None,
|
||||
out_ptype: str | None,
|
||||
**kwargs,
|
||||
) -> tuple[Port, UData]:
|
||||
"""
|
||||
Solve for a path consisting of two L-bends connected by a straight segment.
|
||||
Used for both U-turns (ccw1 == ccw2) and S-bends (ccw1 != ccw2).
|
||||
"""
|
||||
for plan1 in self._iter_l_plans(ccw1, in_ptype, None):
|
||||
for plan2 in self._iter_l_plans(ccw2, plan1.out_ptype, out_ptype):
|
||||
# Solving for:
|
||||
# X = L1_total +/- R2_actual = length
|
||||
# Y = R1_actual + L2_straight + overhead_mid + overhead_b2 + L3_total = jog
|
||||
|
||||
# Sign for overhead_y2 depends on whether it's a U-turn or S-bend
|
||||
is_u = bool(ccw1) == bool(ccw2)
|
||||
# U-turn: X = L1_total - R2 = length => L1_total = length + R2
|
||||
# S-bend: X = L1_total + R2 = length => L1_total = length - R2
|
||||
l1_total = length + (abs(plan2.overhead_y) if is_u else -abs(plan2.overhead_y))
|
||||
l1_straight = l1_total - plan1.overhead_x
|
||||
|
||||
|
||||
if plan1.straight.length_range[0] <= l1_straight < plan1.straight.length_range[1]:
|
||||
for straight_mid in self.straights:
|
||||
# overhead_mid accounts for the transition from bend1 to straight_mid
|
||||
mid_ptype_pair = (plan1.out_ptype, straight_mid.ptype)
|
||||
mid_trans = self.transitions.get(mid_ptype_pair, None)
|
||||
mid_trans_dxy = self._itransition2dxy(mid_trans)
|
||||
|
||||
# b_trans2 accounts for the transition from straight_mid to bend2
|
||||
b2_trans = None
|
||||
if plan2.bend is not None and plan2.bend.in_port.ptype != straight_mid.ptype:
|
||||
b2_trans = self.transitions.get((plan2.bend.in_port.ptype, straight_mid.ptype), None)
|
||||
b2_trans_dxy = self._itransition2dxy(b2_trans)
|
||||
|
||||
l2_straight = abs(jog) - abs(plan1.overhead_y) - plan2.overhead_x - mid_trans_dxy[0] - b2_trans_dxy[0]
|
||||
|
||||
if straight_mid.length_range[0] <= l2_straight < straight_mid.length_range[1]:
|
||||
# Found a solution!
|
||||
# For plan2, we assume l3_straight = 0.
|
||||
# We need to verify if l3=0 is valid for plan2.straight.
|
||||
l3_straight = 0
|
||||
if plan2.straight.length_range[0] <= l3_straight < plan2.straight.length_range[1]:
|
||||
ldata0 = self.LData(
|
||||
l1_straight, plan1.straight, kwargs, ccw1, plan1.bend,
|
||||
plan1.in_trans, plan1.b_trans, plan1.out_trans,
|
||||
)
|
||||
ldata1 = self.LData(
|
||||
l3_straight, plan2.straight, kwargs, ccw2, plan2.bend,
|
||||
b2_trans, None, plan2.out_trans,
|
||||
)
|
||||
|
||||
data = self.UData(ldata0, ldata1, straight_mid, l2_straight, mid_trans)
|
||||
# out_port is at (length, jog) rot pi (for S-bend) or 0 (for U-turn) relative to input
|
||||
out_rot = 0 if is_u else pi
|
||||
out_port = Port((length, jog), rotation=out_rot, ptype=plan2.out_ptype)
|
||||
return out_port, data
|
||||
raise BuildError(f"Failed to find a valid double-L configuration for {length=}, {jog=}")
|
||||
|
||||
straights: list[Straight]
|
||||
""" List of straight-generators to choose from, in order of priority """
|
||||
|
||||
|
|
@ -543,9 +864,10 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
return self
|
||||
|
||||
@staticmethod
|
||||
def _bend2dxy(bend: Bend, ccw: SupportsBool | None) -> tuple[NDArray[numpy.float64], float]:
|
||||
def _bend2dxy(bend: Bend | None, ccw: SupportsBool | None) -> tuple[NDArray[numpy.float64], float]:
|
||||
if ccw is None:
|
||||
return numpy.zeros(2), pi
|
||||
assert bend is not None
|
||||
bend_dxy, bend_angle = bend.in_port.measure_travel(bend.out_port)
|
||||
assert bend_angle is not None
|
||||
if bool(ccw):
|
||||
|
|
@ -589,54 +911,23 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
**kwargs,
|
||||
) -> tuple[Port, LData]:
|
||||
|
||||
success = False
|
||||
for straight in self.straights:
|
||||
for bend in self.bends:
|
||||
bend_dxy, bend_angle = self._bend2dxy(bend, ccw)
|
||||
|
||||
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, straight.ptype)
|
||||
in_transition = self.transitions.get(in_ptype_pair, None)
|
||||
itrans_dxy = self._itransition2dxy(in_transition)
|
||||
|
||||
out_ptype_pair = (
|
||||
'unk' if out_ptype is None else out_ptype,
|
||||
straight.ptype if ccw is None else bend.out_port.ptype
|
||||
for plan in self._iter_l_plans(ccw, in_ptype, out_ptype):
|
||||
straight_length = length - plan.overhead_x
|
||||
if plan.straight.length_range[0] <= straight_length < plan.straight.length_range[1]:
|
||||
data = self.LData(
|
||||
straight_length = straight_length,
|
||||
straight = plan.straight,
|
||||
straight_kwargs = kwargs,
|
||||
ccw = ccw,
|
||||
bend = plan.bend,
|
||||
in_transition = plan.in_trans,
|
||||
b_transition = plan.b_trans,
|
||||
out_transition = plan.out_trans,
|
||||
)
|
||||
out_transition = self.transitions.get(out_ptype_pair, None)
|
||||
otrans_dxy = self._otransition2dxy(out_transition, bend_angle)
|
||||
out_port = Port((length, plan.overhead_y), rotation=plan.bend_angle, ptype=plan.out_ptype)
|
||||
return out_port, data
|
||||
|
||||
b_transition = None
|
||||
if ccw is not None and bend.in_port.ptype != straight.ptype:
|
||||
b_transition = self.transitions.get((bend.in_port.ptype, straight.ptype), None)
|
||||
btrans_dxy = self._itransition2dxy(b_transition)
|
||||
|
||||
straight_length = length - bend_dxy[0] - itrans_dxy[0] - btrans_dxy[0] - otrans_dxy[0]
|
||||
bend_run = bend_dxy[1] + itrans_dxy[1] + btrans_dxy[1] + otrans_dxy[1]
|
||||
success = straight.length_range[0] <= straight_length < straight.length_range[1]
|
||||
if success:
|
||||
break
|
||||
if success:
|
||||
break
|
||||
else:
|
||||
# Failed to break
|
||||
raise BuildError(
|
||||
f'Asked to draw L-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}\n'
|
||||
f'out_trans: {otrans_dxy[0]:,g} bend_trans: {btrans_dxy[0]:,g}'
|
||||
)
|
||||
|
||||
if out_transition is not None:
|
||||
out_ptype_actual = out_transition.their_port.ptype
|
||||
elif ccw is not None:
|
||||
out_ptype_actual = bend.out_port.ptype
|
||||
elif not numpy.isclose(straight_length, 0):
|
||||
out_ptype_actual = straight.ptype
|
||||
else:
|
||||
out_ptype_actual = self.default_out_ptype
|
||||
|
||||
data = self.LData(straight_length, straight, kwargs, ccw, bend, in_transition, b_transition, out_transition)
|
||||
out_port = Port((length, bend_run), rotation=bend_angle, ptype=out_ptype_actual)
|
||||
return out_port, data
|
||||
raise BuildError(f'Failed to find a valid L-path configuration for {length=:,g}, {ccw=}, {in_ptype=}, {out_ptype=}')
|
||||
|
||||
def _renderL(
|
||||
self,
|
||||
|
|
@ -673,7 +964,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
|
||||
return tree
|
||||
|
||||
def path(
|
||||
def traceL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
|
|
@ -690,7 +981,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
out_ptype = out_ptype,
|
||||
)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||
self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||
return tree
|
||||
|
|
@ -746,7 +1037,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1]
|
||||
if sbend.jog_range[0] <= jog_remaining < sbend.jog_range[1]:
|
||||
sbend_dxy = self._sbend2dxy(sbend, jog_remaining)
|
||||
success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[1] + otrans_dxy[1])
|
||||
success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[0] + otrans_dxy[0])
|
||||
if success:
|
||||
b_transition = None
|
||||
straight_length = 0
|
||||
|
|
@ -755,26 +1046,8 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
break
|
||||
|
||||
if not success:
|
||||
try:
|
||||
ccw0 = jog > 0
|
||||
p_test0, ldata_test0 = self.planL(length / 2, ccw0, in_ptype=in_ptype)
|
||||
p_test1, ldata_test1 = self.planL(jog - p_test0.y, not ccw0, in_ptype=p_test0.ptype, out_ptype=out_ptype)
|
||||
|
||||
dx = p_test1.x - length / 2
|
||||
p0, ldata0 = self.planL(length - dx, ccw0, in_ptype=in_ptype)
|
||||
p1, ldata1 = self.planL(jog - p0.y, not ccw0, in_ptype=p0.ptype, out_ptype=out_ptype)
|
||||
success = True
|
||||
except BuildError as err:
|
||||
l2_err: BuildError | None = err
|
||||
else:
|
||||
l2_err = None
|
||||
raise NotImplementedError('TODO need to handle ldata below')
|
||||
|
||||
if not success:
|
||||
# Failed to break
|
||||
raise BuildError(
|
||||
f'Failed to find a valid s-bend configuration for {length=:,g}, {jog=:,g}, {in_ptype=}, {out_ptype=}'
|
||||
) from l2_err
|
||||
ccw0 = jog > 0
|
||||
return self._solve_double_l(length, jog, ccw0, not ccw0, in_ptype, out_ptype, **kwargs)
|
||||
|
||||
if out_transition is not None:
|
||||
out_ptype_actual = out_transition.their_port.ptype
|
||||
|
|
@ -829,7 +1102,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
|
||||
return tree
|
||||
|
||||
def pathS(
|
||||
def traceS(
|
||||
self,
|
||||
length: float,
|
||||
jog: float,
|
||||
|
|
@ -845,9 +1118,74 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
in_ptype = in_ptype,
|
||||
out_ptype = out_ptype,
|
||||
)
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'pathS')
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
|
||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
if isinstance(data, self.UData):
|
||||
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
else:
|
||||
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
return tree
|
||||
|
||||
def planU(
|
||||
self,
|
||||
jog: float,
|
||||
*,
|
||||
length: float = 0,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[Port, UData]:
|
||||
ccw = jog > 0
|
||||
return self._solve_double_l(length, jog, ccw, ccw, in_ptype, out_ptype, **kwargs)
|
||||
|
||||
def _renderU(
|
||||
self,
|
||||
data: UData,
|
||||
tree: ILibrary,
|
||||
port_names: tuple[str, str],
|
||||
gen_kwargs: dict[str, Any],
|
||||
) -> ILibrary:
|
||||
pat = tree.top_pattern()
|
||||
# 1. First L-bend
|
||||
self._renderL(data.ldata0, tree, port_names, gen_kwargs)
|
||||
# 2. Connecting straight
|
||||
if data.mid_transition:
|
||||
pat.plug(data.mid_transition.abstract, {port_names[1]: data.mid_transition.their_port_name})
|
||||
if not numpy.isclose(data.l2_length, 0):
|
||||
s2_pat_or_tree = data.straight2.fn(data.l2_length, **(gen_kwargs | data.ldata0.straight_kwargs))
|
||||
pmap = {port_names[1]: data.straight2.in_port_name}
|
||||
if isinstance(s2_pat_or_tree, Pattern):
|
||||
pat.plug(s2_pat_or_tree, pmap, append=True)
|
||||
else:
|
||||
s2_tree = s2_pat_or_tree
|
||||
top = s2_tree.top()
|
||||
s2_tree.flatten(top, dangling_ok=True)
|
||||
pat.plug(s2_tree[top], pmap, append=True)
|
||||
# 3. Second L-bend
|
||||
self._renderL(data.ldata1, tree, port_names, gen_kwargs)
|
||||
return tree
|
||||
|
||||
def traceU(
|
||||
self,
|
||||
jog: float,
|
||||
*,
|
||||
length: float = 0,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> Library:
|
||||
_out_port, data = self.planU(
|
||||
jog,
|
||||
length = length,
|
||||
in_ptype = in_ptype,
|
||||
out_ptype = out_ptype,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceU')
|
||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
return tree
|
||||
|
||||
def render(
|
||||
|
|
@ -858,7 +1196,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
**kwargs,
|
||||
) -> ILibrary:
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
||||
|
||||
for step in batch:
|
||||
|
|
@ -866,7 +1204,12 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
if step.opcode == 'L':
|
||||
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||
elif step.opcode == 'S':
|
||||
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
if isinstance(step.data, self.UData):
|
||||
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
else:
|
||||
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
elif step.opcode == 'U':
|
||||
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||
return tree
|
||||
|
||||
|
||||
|
|
@ -897,7 +1240,40 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
# self.width = width
|
||||
# self.ptype: str
|
||||
|
||||
def path(
|
||||
def _check_out_ptype(self, out_ptype: str | None) -> None:
|
||||
if out_ptype and out_ptype != self.ptype:
|
||||
raise BuildError(f'Requested {out_ptype=} does not match path ptype {self.ptype}')
|
||||
|
||||
def _bend_radius(self) -> float:
|
||||
return self.width / 2
|
||||
|
||||
def _plan_l_vertices(self, length: float, bend_run: float) -> NDArray[numpy.float64]:
|
||||
vertices = [(0.0, 0.0), (length, 0.0)]
|
||||
if not numpy.isclose(bend_run, 0):
|
||||
vertices.append((length, bend_run))
|
||||
return numpy.array(vertices, dtype=float)
|
||||
|
||||
def _plan_s_vertices(self, length: float, jog: float) -> NDArray[numpy.float64]:
|
||||
if numpy.isclose(jog, 0):
|
||||
return numpy.array([(0.0, 0.0), (length, 0.0)], dtype=float)
|
||||
|
||||
if length < self.width:
|
||||
raise BuildError(
|
||||
f'Asked to draw S-path with total length {length:,g}, shorter than required bend: {self.width:,g}'
|
||||
)
|
||||
|
||||
# Match AutoTool's straight-then-s-bend placement so the jog happens
|
||||
# width/2 before the end while still allowing smaller lateral offsets.
|
||||
jog_x = length - self._bend_radius()
|
||||
vertices = [
|
||||
(0.0, 0.0),
|
||||
(jog_x, 0.0),
|
||||
(jog_x, jog),
|
||||
(length, jog),
|
||||
]
|
||||
return numpy.array(vertices, dtype=float)
|
||||
|
||||
def traceL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
|
|
@ -907,15 +1283,15 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> Library:
|
||||
out_port, dxy = self.planL(
|
||||
out_port, data = 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)])
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
pat.path(layer=self.layer, width=self.width, vertices=self._plan_l_vertices(length, float(out_port.y)))
|
||||
|
||||
if ccw is None:
|
||||
out_rot = pi
|
||||
|
|
@ -926,7 +1302,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
|
||||
pat.ports = {
|
||||
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
|
||||
port_names[1]: Port(dxy, rotation=out_rot, ptype=self.ptype),
|
||||
port_names[1]: Port(out_port.offset, rotation=out_rot, ptype=self.ptype),
|
||||
}
|
||||
|
||||
return tree
|
||||
|
|
@ -942,11 +1318,10 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
) -> 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}')
|
||||
self._check_out_ptype(out_ptype)
|
||||
|
||||
if ccw is not None:
|
||||
bend_dxy = numpy.array([1, -1]) * self.width / 2
|
||||
bend_dxy = numpy.array([1, -1]) * self._bend_radius()
|
||||
bend_angle = pi / 2
|
||||
|
||||
if bool(ccw):
|
||||
|
|
@ -967,6 +1342,46 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
out_port = Port(data, rotation=bend_angle, ptype=self.ptype)
|
||||
return out_port, data
|
||||
|
||||
def traceS(
|
||||
self,
|
||||
length: float,
|
||||
jog: 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, _data = self.planS(
|
||||
length,
|
||||
jog,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
|
||||
pat.path(layer=self.layer, width=self.width, vertices=self._plan_s_vertices(length, jog))
|
||||
pat.ports = {
|
||||
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
|
||||
port_names[1]: out_port,
|
||||
}
|
||||
return tree
|
||||
|
||||
def planS(
|
||||
self,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||
out_ptype: str | None = None,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> tuple[Port, NDArray[numpy.float64]]:
|
||||
self._check_out_ptype(out_ptype)
|
||||
self._plan_s_vertices(length, jog)
|
||||
data = numpy.array((length, jog))
|
||||
out_port = Port((length, jog), rotation=pi, ptype=self.ptype)
|
||||
return out_port, data
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
|
|
@ -975,29 +1390,43 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
|
||||
path_vertices = [batch[0].start_port.offset]
|
||||
for step in batch:
|
||||
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
||||
# This allows the path to be rendered with its original orientation, simplified by
|
||||
# translation to the origin. Pather.render will handle the final placement
|
||||
# (including rotation alignment) via `pat.plug`.
|
||||
first_port = batch[0].start_port
|
||||
translation = -first_port.offset
|
||||
rotation = 0
|
||||
pivot = first_port.offset
|
||||
|
||||
# Localize the batch for rendering
|
||||
local_batch = [step.transformed(translation, rotation, pivot) for step in batch]
|
||||
|
||||
path_vertices = [local_batch[0].start_port.offset]
|
||||
for step in local_batch:
|
||||
assert step.tool == self
|
||||
|
||||
port_rot = step.start_port.rotation
|
||||
# Masque convention: Port rotation points INTO the device.
|
||||
# So the direction of travel for the path is AWAY from the port, i.e., port_rot + pi.
|
||||
assert port_rot is not None
|
||||
|
||||
transform = rotation_matrix_2d(port_rot + pi)
|
||||
delta = step.end_port.offset - step.start_port.offset
|
||||
local_end = rotation_matrix_2d(-(port_rot + pi)) @ delta
|
||||
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)
|
||||
local_vertices = self._plan_l_vertices(float(local_end[0]), float(local_end[1]))
|
||||
elif step.opcode == 'S':
|
||||
local_vertices = self._plan_s_vertices(float(local_end[0]), float(local_end[1]))
|
||||
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)
|
||||
for vertex in local_vertices[1:]:
|
||||
path_vertices.append(step.start_port.offset + transform @ vertex)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
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),
|
||||
port_names[0]: local_batch[0].start_port.copy().rotate(pi),
|
||||
port_names[1]: local_batch[-1].end_port.copy().rotate(pi),
|
||||
}
|
||||
return tree
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def ell(
|
|||
ccw: Turn direction. `True` means counterclockwise, `False` means clockwise,
|
||||
and `None` means no bend. If `None`, spacing must remain `None` or `0` (default),
|
||||
Otherwise, spacing must be set to a non-`None` value.
|
||||
bound_method: Method used for determining the travel distance; see diagram above.
|
||||
bound_type: Method used for determining the travel distance; see diagram above.
|
||||
Valid values are:
|
||||
- 'min_extension' or 'emin':
|
||||
The total extension value for the furthest-out port (B in the diagram).
|
||||
|
|
@ -64,7 +64,7 @@ def ell(
|
|||
the x- and y- axes. If specifying a position, it is projected onto
|
||||
the extension direction.
|
||||
|
||||
bound_value: Value associated with `bound_type`, see above.
|
||||
bound: Value associated with `bound_type`, see above.
|
||||
spacing: Distance between adjacent channels. Can be scalar, resulting in evenly
|
||||
spaced channels, or a vector with length one less than `ports`, allowing
|
||||
non-uniform spacing.
|
||||
|
|
@ -84,7 +84,7 @@ def ell(
|
|||
raise BuildError('Empty port list passed to `ell()`')
|
||||
|
||||
if ccw is None:
|
||||
if spacing is not None and not numpy.isclose(spacing, 0):
|
||||
if spacing is not None and not numpy.allclose(spacing, 0):
|
||||
raise BuildError('Spacing must be 0 or None when ccw=None')
|
||||
spacing = 0
|
||||
elif spacing is None:
|
||||
|
|
@ -106,7 +106,7 @@ def ell(
|
|||
raise BuildError('Asked to find aggregation for ports that face in different directions:\n'
|
||||
+ pformat(port_rotations))
|
||||
else:
|
||||
if set_rotation is not None:
|
||||
if set_rotation is None:
|
||||
raise BuildError('set_rotation must be specified if no ports have rotations!')
|
||||
rotations = numpy.full_like(has_rotation, set_rotation, dtype=float)
|
||||
|
||||
|
|
@ -132,8 +132,17 @@ def ell(
|
|||
if spacing is None:
|
||||
ch_offsets = numpy.zeros_like(y_order)
|
||||
else:
|
||||
spacing_arr = numpy.asarray(spacing, dtype=float).reshape(-1)
|
||||
steps = numpy.zeros_like(y_order)
|
||||
steps[1:] = spacing
|
||||
if spacing_arr.size == 1:
|
||||
steps[1:] = spacing_arr[0]
|
||||
elif spacing_arr.size == len(ports) - 1:
|
||||
steps[1:] = spacing_arr
|
||||
else:
|
||||
raise BuildError(
|
||||
f'spacing must be scalar or have length {len(ports) - 1} for {len(ports)} ports; '
|
||||
f'got length {spacing_arr.size}'
|
||||
)
|
||||
ch_offsets = numpy.cumsum(steps)[y_ind]
|
||||
|
||||
x_start = rot_offsets[:, 0]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import gzip
|
|||
import numpy
|
||||
import ezdxf
|
||||
from ezdxf.enums import TextEntityAlignment
|
||||
from ezdxf.entities import LWPolyline, Polyline, Text, Insert
|
||||
from ezdxf.entities import LWPolyline, Polyline, Text, Insert, Solid, Trace
|
||||
|
||||
from .utils import is_gzipped, tmpfile
|
||||
from .. import Pattern, Ref, PatternError, Label
|
||||
|
|
@ -55,8 +55,7 @@ def write(
|
|||
tuple: (1, 2) -> '1.2'
|
||||
str: '1.2' -> '1.2' (no change)
|
||||
|
||||
DXF does not support shape repetition (only block repeptition). Please call
|
||||
library.wrap_repeated_shapes() before writing to file.
|
||||
Shape repetitions are expanded into individual DXF entities.
|
||||
|
||||
Other functions you may want to call:
|
||||
- `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names
|
||||
|
|
@ -193,8 +192,37 @@ def read(
|
|||
|
||||
top_name, top_pat = _read_block(msp)
|
||||
mlib = Library({top_name: top_pat})
|
||||
|
||||
blocks_by_name = {
|
||||
bb.name: bb
|
||||
for bb in lib.blocks
|
||||
if not bb.is_any_layout
|
||||
}
|
||||
|
||||
referenced: set[str] = set()
|
||||
pending = [msp]
|
||||
seen_blocks: set[str] = set()
|
||||
while pending:
|
||||
block = pending.pop()
|
||||
block_name = getattr(block, 'name', None)
|
||||
if block_name is not None and block_name in seen_blocks:
|
||||
continue
|
||||
if block_name is not None:
|
||||
seen_blocks.add(block_name)
|
||||
for element in block:
|
||||
if not isinstance(element, Insert):
|
||||
continue
|
||||
target = element.dxfattribs().get('name')
|
||||
if target is None or target in referenced:
|
||||
continue
|
||||
referenced.add(target)
|
||||
if target in blocks_by_name:
|
||||
pending.append(blocks_by_name[target])
|
||||
|
||||
for bb in lib.blocks:
|
||||
if bb.name == '*Model_Space':
|
||||
if bb.is_any_layout:
|
||||
continue
|
||||
if bb.name.startswith('_') and bb.name not in referenced:
|
||||
continue
|
||||
name, pat = _read_block(bb)
|
||||
mlib[name] = pat
|
||||
|
|
@ -213,32 +241,60 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
|
|||
if isinstance(element, LWPolyline | Polyline):
|
||||
if isinstance(element, LWPolyline):
|
||||
points = numpy.asarray(element.get_points())
|
||||
elif isinstance(element, Polyline):
|
||||
is_closed = element.closed
|
||||
else:
|
||||
points = numpy.asarray([pp.xyz for pp in element.points()])
|
||||
is_closed = element.is_closed
|
||||
attr = element.dxfattribs()
|
||||
layer = attr.get('layer', DEFAULT_LAYER)
|
||||
|
||||
if points.shape[1] == 2:
|
||||
raise PatternError('Invalid or unimplemented polygon?')
|
||||
width = 0
|
||||
if isinstance(element, LWPolyline):
|
||||
# ezdxf 1.4+ get_points() returns (x, y, start_width, end_width, bulge)
|
||||
if points.shape[1] >= 5:
|
||||
if (points[:, 4] != 0).any():
|
||||
raise PatternError('LWPolyline has bulge (not yet representable in masque!)')
|
||||
if (points[:, 2] != points[:, 3]).any() or (points[:, 2] != points[0, 2]).any():
|
||||
raise PatternError('LWPolyline has non-constant width (not yet representable in masque!)')
|
||||
width = points[0, 2]
|
||||
elif points.shape[1] == 3:
|
||||
# width used to be in column 2
|
||||
width = points[0, 2]
|
||||
|
||||
if 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():
|
||||
raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
|
||||
if width == 0:
|
||||
width = attr.get('const_width', 0)
|
||||
|
||||
width = points[0, 2]
|
||||
if width == 0:
|
||||
width = attr.get('const_width', 0)
|
||||
verts = points[:, :2]
|
||||
if is_closed and (len(verts) < 2 or not numpy.allclose(verts[0], verts[-1])):
|
||||
verts = numpy.vstack((verts, verts[0]))
|
||||
|
||||
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: Path | Polygon
|
||||
if width == 0 and is_closed:
|
||||
# Use Polygon if it has at least 3 unique vertices
|
||||
shape_verts = verts[:-1] if len(verts) > 1 else verts
|
||||
if len(shape_verts) >= 3:
|
||||
shape = Polygon(vertices=shape_verts)
|
||||
else:
|
||||
shape = Path(width=width, vertices=points[:, :2])
|
||||
shape = Path(width=width, vertices=verts)
|
||||
else:
|
||||
shape = Path(width=width, vertices=verts)
|
||||
|
||||
pat.shapes[layer].append(shape)
|
||||
|
||||
elif isinstance(element, Solid | Trace):
|
||||
attr = element.dxfattribs()
|
||||
layer = attr.get('layer', DEFAULT_LAYER)
|
||||
points = numpy.array([element.get_dxf_attrib(f'vtx{i}') for i in range(4)
|
||||
if element.has_dxf_attrib(f'vtx{i}')])
|
||||
if len(points) >= 3:
|
||||
# If vtx2 == vtx3, it's a triangle. ezdxf handles this.
|
||||
if len(points) == 4 and numpy.allclose(points[2], points[3]):
|
||||
verts = points[:3, :2]
|
||||
# DXF Solid/Trace uses 0-1-3-2 vertex order for quadrilaterals!
|
||||
elif len(points) == 4:
|
||||
verts = points[[0, 1, 3, 2], :2]
|
||||
else:
|
||||
verts = points[:, :2]
|
||||
pat.shapes[layer].append(Polygon(vertices=verts))
|
||||
elif isinstance(element, Text):
|
||||
args = dict(
|
||||
offset=numpy.asarray(element.get_placement()[1])[:2],
|
||||
|
|
@ -273,12 +329,57 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
|
|||
)
|
||||
|
||||
if 'column_count' in attr:
|
||||
args['repetition'] = Grid(
|
||||
a_vector=(attr['column_spacing'], 0),
|
||||
b_vector=(0, attr['row_spacing']),
|
||||
a_count=attr['column_count'],
|
||||
b_count=attr['row_count'],
|
||||
col_spacing = attr['column_spacing']
|
||||
row_spacing = attr['row_spacing']
|
||||
col_count = attr['column_count']
|
||||
row_count = attr['row_count']
|
||||
local_x = numpy.array((col_spacing, 0.0))
|
||||
local_y = numpy.array((0.0, row_spacing))
|
||||
inv_rot = rotation_matrix_2d(-rotation)
|
||||
|
||||
candidates = (
|
||||
(inv_rot @ local_x, inv_rot @ local_y, col_count, row_count),
|
||||
(inv_rot @ local_y, inv_rot @ local_x, row_count, col_count),
|
||||
)
|
||||
repetition = None
|
||||
for a_vector, b_vector, a_count, b_count in candidates:
|
||||
rotated_a = rotation_matrix_2d(rotation) @ a_vector
|
||||
rotated_b = rotation_matrix_2d(rotation) @ b_vector
|
||||
if (numpy.isclose(rotated_a[1], 0, atol=1e-8)
|
||||
and numpy.isclose(rotated_b[0], 0, atol=1e-8)
|
||||
and numpy.isclose(rotated_a[0], col_spacing, atol=1e-8)
|
||||
and numpy.isclose(rotated_b[1], row_spacing, atol=1e-8)
|
||||
and a_count == col_count
|
||||
and b_count == row_count):
|
||||
repetition = Grid(
|
||||
a_vector=a_vector,
|
||||
b_vector=b_vector,
|
||||
a_count=a_count,
|
||||
b_count=b_count,
|
||||
)
|
||||
break
|
||||
if (numpy.isclose(rotated_a[0], 0, atol=1e-8)
|
||||
and numpy.isclose(rotated_b[1], 0, atol=1e-8)
|
||||
and numpy.isclose(rotated_b[0], col_spacing, atol=1e-8)
|
||||
and numpy.isclose(rotated_a[1], row_spacing, atol=1e-8)
|
||||
and b_count == col_count
|
||||
and a_count == row_count):
|
||||
repetition = Grid(
|
||||
a_vector=a_vector,
|
||||
b_vector=b_vector,
|
||||
a_count=a_count,
|
||||
b_count=b_count,
|
||||
)
|
||||
break
|
||||
|
||||
if repetition is None:
|
||||
repetition = Grid(
|
||||
a_vector=inv_rot @ local_x,
|
||||
b_vector=inv_rot @ local_y,
|
||||
a_count=col_count,
|
||||
b_count=row_count,
|
||||
)
|
||||
args['repetition'] = repetition
|
||||
pat.ref(**args)
|
||||
else:
|
||||
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
|
||||
|
|
@ -303,15 +404,23 @@ def _mrefs_to_drefs(
|
|||
elif isinstance(rep, Grid):
|
||||
a = rep.a_vector
|
||||
b = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
rotated_a = rotation_matrix_2d(-ref.rotation) @ a
|
||||
rotated_b = rotation_matrix_2d(-ref.rotation) @ b
|
||||
if rotated_a[1] == 0 and rotated_b[0] == 0:
|
||||
# In masque, the grid basis vectors are NOT rotated by the reference's rotation.
|
||||
# In DXF, the grid basis vectors are [column_spacing, 0] and [0, row_spacing],
|
||||
# which ARE then rotated by the block reference's rotation.
|
||||
# Therefore, we can only use a DXF array if ref.rotation is 0 (or a multiple of 90)
|
||||
# AND the grid is already manhattan.
|
||||
|
||||
# Rotate basis vectors by the reference rotation to see where they end up in the DXF frame
|
||||
rotated_a = rotation_matrix_2d(ref.rotation) @ a
|
||||
rotated_b = rotation_matrix_2d(ref.rotation) @ b
|
||||
|
||||
if numpy.isclose(rotated_a[1], 0, atol=1e-8) and numpy.isclose(rotated_b[0], 0, atol=1e-8):
|
||||
attribs['column_count'] = rep.a_count
|
||||
attribs['row_count'] = rep.b_count
|
||||
attribs['column_spacing'] = rotated_a[0]
|
||||
attribs['row_spacing'] = rotated_b[1]
|
||||
block.add_blockref(encoded_name, ref.offset, dxfattribs=attribs)
|
||||
elif rotated_a[0] == 0 and rotated_b[1] == 0:
|
||||
elif numpy.isclose(rotated_a[0], 0, atol=1e-8) and numpy.isclose(rotated_b[1], 0, atol=1e-8):
|
||||
attribs['column_count'] = rep.b_count
|
||||
attribs['row_count'] = rep.a_count
|
||||
attribs['column_spacing'] = rotated_b[0]
|
||||
|
|
@ -344,16 +453,23 @@ def _shapes_to_elements(
|
|||
for layer, sseq in shapes.items():
|
||||
attribs = dict(layer=_mlayer2dxf(layer))
|
||||
for shape in sseq:
|
||||
displacements = [numpy.zeros(2)]
|
||||
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.'
|
||||
)
|
||||
displacements = shape.repetition.displacements
|
||||
|
||||
for polygon in shape.to_polygons():
|
||||
xy_open = polygon.vertices
|
||||
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
||||
for dd in displacements:
|
||||
if isinstance(shape, Path):
|
||||
# preserve path.
|
||||
# Note: DXF paths don't support endcaps well, so this is still a bit limited.
|
||||
xy = shape.vertices + dd
|
||||
attribs_path = {**attribs}
|
||||
if shape.width > 0:
|
||||
attribs_path['const_width'] = shape.width
|
||||
block.add_lwpolyline(xy, dxfattribs=attribs_path)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy_open = polygon.vertices + dd
|
||||
block.add_lwpolyline(xy_open, close=True, dxfattribs=attribs)
|
||||
|
||||
|
||||
def _labels_to_texts(
|
||||
|
|
@ -363,11 +479,17 @@ def _labels_to_texts(
|
|||
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)
|
||||
if label.repetition is None:
|
||||
block.add_text(
|
||||
label.string,
|
||||
dxfattribs=attribs
|
||||
).set_placement(label.offset, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||
else:
|
||||
for dd in label.repetition.displacements:
|
||||
block.add_text(
|
||||
label.string,
|
||||
dxfattribs=attribs
|
||||
).set_placement(label.offset + dd, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||
|
||||
|
||||
def _mlayer2dxf(layer: layer_t) -> str:
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def write(
|
|||
datatype is chosen to be `shape.layer[1]` if available,
|
||||
otherwise `0`
|
||||
|
||||
GDS does not support shape repetition (only cell repeptition). Please call
|
||||
GDS does not support shape repetition (only cell repetition). Please call
|
||||
`library.wrap_repeated_shapes()` before writing to file.
|
||||
|
||||
Other functions you may want to call:
|
||||
|
|
@ -453,7 +453,7 @@ def _shapes_to_elements(
|
|||
|
||||
extension: tuple[int, int]
|
||||
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None:
|
||||
extension = tuple(shape.cap_extensions) # type: ignore
|
||||
extension = tuple(rint_cast(shape.cap_extensions))
|
||||
else:
|
||||
extension = (0, 0)
|
||||
|
||||
|
|
@ -617,7 +617,12 @@ def load_libraryfile(
|
|||
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
|
||||
else:
|
||||
stream = path.open(mode='rb') # noqa: SIM115
|
||||
return load_library(stream, full_load=full_load, postprocess=postprocess)
|
||||
|
||||
try:
|
||||
return load_library(stream, full_load=full_load, postprocess=postprocess)
|
||||
finally:
|
||||
if full_load:
|
||||
stream.close()
|
||||
|
||||
|
||||
def check_valid_names(
|
||||
|
|
@ -632,6 +637,7 @@ def check_valid_names(
|
|||
max_length: Max allowed length
|
||||
|
||||
"""
|
||||
names = tuple(names)
|
||||
allowed_chars = set(string.ascii_letters + string.digits + '_?$')
|
||||
|
||||
bad_chars = [
|
||||
|
|
@ -648,7 +654,7 @@ def check_valid_names(
|
|||
logger.error('Names contain invalid characters:\n' + pformat(bad_chars))
|
||||
|
||||
if bad_lengths:
|
||||
logger.error(f'Names too long (>{max_length}:\n' + pformat(bad_chars))
|
||||
logger.error(f'Names too long (>{max_length}):\n' + pformat(bad_lengths))
|
||||
|
||||
if bad_chars or bad_lengths:
|
||||
raise LibraryError('Library contains invalid names, see log above')
|
||||
|
|
|
|||
|
|
@ -120,10 +120,10 @@ def build(
|
|||
layer, data_type = _mlayer2oas(layer_num)
|
||||
lib.layers += [
|
||||
fatrec.LayerName(
|
||||
nstring=name,
|
||||
layer_interval=(layer, layer),
|
||||
type_interval=(data_type, data_type),
|
||||
is_textlayer=tt,
|
||||
nstring = name,
|
||||
layer_interval = (layer, layer),
|
||||
type_interval = (data_type, data_type),
|
||||
is_textlayer = tt,
|
||||
)
|
||||
for tt in (True, False)]
|
||||
|
||||
|
|
@ -182,8 +182,8 @@ def writefile(
|
|||
Args:
|
||||
library: A {name: Pattern} mapping of patterns to write.
|
||||
filename: Filename to save to.
|
||||
*args: passed to `oasis.write`
|
||||
**kwargs: passed to `oasis.write`
|
||||
*args: passed to `oasis.build()`
|
||||
**kwargs: passed to `oasis.build()`
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
|
||||
|
|
@ -213,9 +213,9 @@ def readfile(
|
|||
Will automatically decompress gzipped files.
|
||||
|
||||
Args:
|
||||
filename: Filename to save to.
|
||||
*args: passed to `oasis.read`
|
||||
**kwargs: passed to `oasis.read`
|
||||
filename: Filename to load from.
|
||||
*args: passed to `oasis.read()`
|
||||
**kwargs: passed to `oasis.read()`
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
if is_gzipped(path):
|
||||
|
|
@ -286,11 +286,11 @@ def read(
|
|||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.polygon(
|
||||
vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
annotations=annotations,
|
||||
repetition=repetition,
|
||||
vertices = vertices,
|
||||
layer = element.get_layer_tuple(),
|
||||
offset = element.get_xy(),
|
||||
annotations = annotations,
|
||||
repetition = repetition,
|
||||
)
|
||||
elif isinstance(element, fatrec.Path):
|
||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0)
|
||||
|
|
@ -310,13 +310,13 @@ def read(
|
|||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.path(
|
||||
vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
annotations=annotations,
|
||||
width=element.get_half_width() * 2,
|
||||
cap=cap,
|
||||
vertices = vertices,
|
||||
layer = element.get_layer_tuple(),
|
||||
offset = element.get_xy(),
|
||||
repetition = repetition,
|
||||
annotations = annotations,
|
||||
width = element.get_half_width() * 2,
|
||||
cap = cap,
|
||||
**path_args,
|
||||
)
|
||||
|
||||
|
|
@ -325,11 +325,11 @@ def read(
|
|||
height = element.get_height()
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
pat.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,
|
||||
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,
|
||||
)
|
||||
|
||||
elif isinstance(element, fatrec.Trapezoid):
|
||||
|
|
@ -440,11 +440,11 @@ def read(
|
|||
else:
|
||||
string = str_or_ref.string
|
||||
pat.label(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
annotations=annotations,
|
||||
string=string,
|
||||
layer = element.get_layer_tuple(),
|
||||
offset = element.get_xy(),
|
||||
repetition = repetition,
|
||||
annotations = annotations,
|
||||
string = string,
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
@ -549,33 +549,35 @@ def _shapes_to_elements(
|
|||
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,
|
||||
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
|
||||
path_type = next((k for k, v in path_cap_map.items() if v == shape.cap), None) # reverse lookup
|
||||
if path_type is None:
|
||||
raise PatternError(f'OASIS writer does not support path cap {shape.cap}')
|
||||
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,
|
||||
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:
|
||||
|
|
@ -583,13 +585,13 @@ def _shapes_to_elements(
|
|||
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,
|
||||
layer = layer,
|
||||
datatype = datatype,
|
||||
x = xy[0],
|
||||
y = xy[1],
|
||||
point_list = cast('list[list[int]]', points),
|
||||
properties = properties,
|
||||
repetition = repetition,
|
||||
))
|
||||
return elements
|
||||
|
||||
|
|
@ -606,13 +608,13 @@ def _labels_to_texts(
|
|||
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,
|
||||
layer = layer,
|
||||
datatype = datatype,
|
||||
x = xy[0],
|
||||
y = xy[1],
|
||||
string = label.string,
|
||||
properties = properties,
|
||||
repetition = repetition,
|
||||
))
|
||||
return texts
|
||||
|
||||
|
|
@ -622,10 +624,12 @@ def repetition_fata2masq(
|
|||
) -> Repetition | None:
|
||||
mrep: Repetition | None
|
||||
if isinstance(rep, fatamorgana.GridRepetition):
|
||||
mrep = Grid(a_vector=rep.a_vector,
|
||||
b_vector=rep.b_vector,
|
||||
a_count=rep.a_count,
|
||||
b_count=rep.b_count)
|
||||
mrep = Grid(
|
||||
a_vector = rep.a_vector,
|
||||
b_vector = rep.b_vector,
|
||||
a_count = rep.a_count,
|
||||
b_count = rep.b_count,
|
||||
)
|
||||
elif isinstance(rep, fatamorgana.ArbitraryRepetition):
|
||||
displacements = numpy.cumsum(numpy.column_stack((
|
||||
rep.x_displacements,
|
||||
|
|
@ -647,14 +651,19 @@ def repetition_masq2fata(
|
|||
frep: fatamorgana.GridRepetition | fatamorgana.ArbitraryRepetition | None
|
||||
if isinstance(rep, Grid):
|
||||
a_vector = rint_cast(rep.a_vector)
|
||||
b_vector = rint_cast(rep.b_vector) if rep.b_vector is not None else None
|
||||
a_count = rint_cast(rep.a_count)
|
||||
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
|
||||
a_count = int(rep.a_count)
|
||||
if rep.b_count > 1:
|
||||
b_vector = rint_cast(rep.b_vector)
|
||||
b_count = int(rep.b_count)
|
||||
else:
|
||||
b_vector = None
|
||||
b_count = None
|
||||
|
||||
frep = fatamorgana.GridRepetition(
|
||||
a_vector=cast('list[int]', a_vector),
|
||||
b_vector=cast('list[int] | None', b_vector),
|
||||
a_count=cast('int', a_count),
|
||||
b_count=cast('int | None', b_count),
|
||||
a_vector = a_vector,
|
||||
b_vector = b_vector,
|
||||
a_count = a_count,
|
||||
b_count = b_count,
|
||||
)
|
||||
offset = (0, 0)
|
||||
elif isinstance(rep, Arbitrary):
|
||||
|
|
@ -707,13 +716,9 @@ def properties_to_annotations(
|
|||
string = repr(value)
|
||||
logger.warning(f'Converting property value for key ({key}) to string ({string})')
|
||||
values.append(string)
|
||||
annotations[key] = values
|
||||
annotations.setdefault(key, []).extend(values)
|
||||
return annotations
|
||||
|
||||
properties = [fatrec.Property(key, vals, is_standard=False)
|
||||
for key, vals in annotations.items()]
|
||||
return properties
|
||||
|
||||
|
||||
def check_valid_names(
|
||||
names: Iterable[str],
|
||||
|
|
|
|||
|
|
@ -10,16 +10,47 @@ import svgwrite # type: ignore
|
|||
|
||||
from .utils import mangle_name
|
||||
from .. import Pattern
|
||||
from ..utils import rotation_matrix_2d
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ref_to_svg_transform(ref) -> str:
|
||||
linear = rotation_matrix_2d(ref.rotation) * ref.scale
|
||||
if ref.mirrored:
|
||||
linear = linear @ numpy.diag((1.0, -1.0))
|
||||
|
||||
a = linear[0, 0]
|
||||
b = linear[1, 0]
|
||||
c = linear[0, 1]
|
||||
d = linear[1, 1]
|
||||
e = ref.offset[0]
|
||||
f = ref.offset[1]
|
||||
return f'matrix({a:g} {b:g} {c:g} {d:g} {e:g} {f:g})'
|
||||
|
||||
|
||||
def _make_svg_ids(names: Mapping[str, Pattern]) -> dict[str, str]:
|
||||
svg_ids: dict[str, str] = {}
|
||||
seen_ids: set[str] = set()
|
||||
for name in names:
|
||||
base_id = mangle_name(name)
|
||||
svg_id = base_id
|
||||
suffix = 1
|
||||
while svg_id in seen_ids:
|
||||
suffix += 1
|
||||
svg_id = f'{base_id}_{suffix}'
|
||||
seen_ids.add(svg_id)
|
||||
svg_ids[name] = svg_id
|
||||
return svg_ids
|
||||
|
||||
|
||||
def writefile(
|
||||
library: Mapping[str, Pattern],
|
||||
top: str,
|
||||
filename: str,
|
||||
custom_attributes: bool = False,
|
||||
annotate_ports: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Write a Pattern to an SVG file, by first calling .polygonize() on it
|
||||
|
|
@ -44,6 +75,8 @@ def writefile(
|
|||
filename: Filename to write to.
|
||||
custom_attributes: Whether to write non-standard `pattern_layer` attribute to the
|
||||
SVG elements.
|
||||
annotate_ports: If True, draw an arrow for each port (similar to
|
||||
`Pattern.visualize(..., ports=True)`).
|
||||
"""
|
||||
pattern = library[top]
|
||||
|
||||
|
|
@ -63,10 +96,11 @@ def writefile(
|
|||
# Create file
|
||||
svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string,
|
||||
debug=(not custom_attributes))
|
||||
svg_ids = _make_svg_ids(library)
|
||||
|
||||
# Now create a group for each pattern and add in any Boundary and Use elements
|
||||
for name, pat in library.items():
|
||||
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red')
|
||||
svg_group = svg.g(id=svg_ids[name], fill='blue', stroke='red')
|
||||
|
||||
for layer, shapes in pat.shapes.items():
|
||||
for shape in shapes:
|
||||
|
|
@ -79,16 +113,37 @@ def writefile(
|
|||
|
||||
svg_group.add(path)
|
||||
|
||||
if annotate_ports:
|
||||
# Draw arrows for the ports, pointing into the device (per port definition)
|
||||
for port_name, port in pat.ports.items():
|
||||
if port.rotation is not None:
|
||||
p1 = port.offset
|
||||
angle = port.rotation
|
||||
size = 1.0 # arrow size
|
||||
p2 = p1 + size * numpy.array([numpy.cos(angle), numpy.sin(angle)])
|
||||
|
||||
# head
|
||||
head_angle = 0.5
|
||||
h1 = p1 + 0.7 * size * numpy.array([numpy.cos(angle + head_angle), numpy.sin(angle + head_angle)])
|
||||
h2 = p1 + 0.7 * size * numpy.array([numpy.cos(angle - head_angle), numpy.sin(angle - head_angle)])
|
||||
|
||||
line = svg.line(start=p1, end=p2, stroke='green', stroke_width=0.2)
|
||||
head = svg.polyline(points=[h1, p1, h2], fill='none', stroke='green', stroke_width=0.2)
|
||||
|
||||
svg_group.add(line)
|
||||
svg_group.add(head)
|
||||
svg_group.add(svg.text(port_name, insert=p2, font_size=0.5, fill='green'))
|
||||
|
||||
for target, refs in pat.refs.items():
|
||||
if 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)
|
||||
transform = _ref_to_svg_transform(ref)
|
||||
use = svg.use(href='#' + svg_ids[target], transform=transform)
|
||||
svg_group.add(use)
|
||||
|
||||
svg.defs.add(svg_group)
|
||||
svg.add(svg.use(href='#' + mangle_name(top)))
|
||||
svg.add(svg.use(href='#' + svg_ids[top]))
|
||||
svg.save()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ def preflight(
|
|||
Run a standard set of useful operations and checks, usually done immediately prior
|
||||
to writing to a file (or immediately after reading).
|
||||
|
||||
Note that this helper is not copy-isolating. When `sort=True`, it constructs a new
|
||||
`Library` wrapper around the same `Pattern` objects after sorting them in place, so
|
||||
later mutating preflight steps such as `prune_empty_patterns` and
|
||||
`wrap_repeated_shapes` may still mutate caller-owned patterns. Callers that need
|
||||
isolation should deep-copy the library before calling `preflight()`.
|
||||
|
||||
Args:
|
||||
sort: Whether to sort the patterns based on their names, and optionaly sort the pattern contents.
|
||||
Default True. Useful for reproducible builds.
|
||||
|
|
@ -75,7 +81,8 @@ def preflight(
|
|||
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
|
||||
|
||||
if prune_empty_patterns:
|
||||
pruned = lib.prune_empty()
|
||||
prune_dangling = 'error' if allow_dangling_refs is False else 'ignore'
|
||||
pruned = lib.prune_empty(dangling=prune_dangling)
|
||||
if pruned:
|
||||
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
|
||||
logger.debug('Pruned: ' + pformat(pruned))
|
||||
|
|
@ -144,7 +151,11 @@ def tmpfile(path: str | pathlib.Path) -> Iterator[IO[bytes]]:
|
|||
path = pathlib.Path(path)
|
||||
suffixes = ''.join(path.suffixes)
|
||||
with tempfile.NamedTemporaryFile(suffix=suffixes, delete=False) as tmp_stream:
|
||||
yield tmp_stream
|
||||
try:
|
||||
yield tmp_stream
|
||||
except Exception:
|
||||
pathlib.Path(tmp_stream.name).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
try:
|
||||
shutil.move(tmp_stream.name, path)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ 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 .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded, Flippable
|
||||
from .traits import AnnotatableImpl
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable):
|
||||
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable, Flippable):
|
||||
"""
|
||||
A text annotation with a position (but no size; it is not drawn)
|
||||
"""
|
||||
|
|
@ -58,12 +58,15 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
|||
string=self.string,
|
||||
offset=self.offset.copy(),
|
||||
repetition=self.repetition,
|
||||
annotations=copy.copy(self.annotations),
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
memo = {} if memo is None else memo
|
||||
new = copy.copy(self)
|
||||
new._offset = self._offset.copy()
|
||||
new._repetition = copy.deepcopy(self._repetition, memo)
|
||||
new._annotations = copy.deepcopy(self._annotations, memo)
|
||||
return new
|
||||
|
||||
def __lt__(self, other: 'Label') -> bool:
|
||||
|
|
@ -76,6 +79,8 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
|||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
return (
|
||||
self.string == other.string
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
|
|
@ -96,10 +101,34 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
|||
"""
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
self.translate(-pivot)
|
||||
if self.repetition is not None:
|
||||
self.repetition.rotate(rotation)
|
||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||
self.translate(+pivot)
|
||||
return self
|
||||
|
||||
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
|
||||
"""
|
||||
Extrinsic transformation: Flip the label across a line in the pattern's
|
||||
coordinate system. This affects both the label's offset and its
|
||||
repetition grid.
|
||||
|
||||
Args:
|
||||
axis: Axis to mirror across. 0: x-axis (flip y), 1: y-axis (flip x).
|
||||
x: Vertical line x=val to mirror across.
|
||||
y: Horizontal line y=val to mirror across.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
axis, pivot = self._check_flip_args(axis=axis, x=x, y=y)
|
||||
self.translate(-pivot)
|
||||
if self.repetition is not None:
|
||||
self.repetition.mirror(axis)
|
||||
self.offset[1 - axis] *= -1
|
||||
self.translate(+pivot)
|
||||
return self
|
||||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the bounds of the label.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import copy
|
|||
from pprint import pformat
|
||||
from collections import defaultdict
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from graphlib import TopologicalSorter
|
||||
from graphlib import TopologicalSorter, CycleError
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
|
@ -59,6 +59,9 @@ TreeView: TypeAlias = Mapping[str, 'Pattern']
|
|||
Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
||||
""" A mutable name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
||||
|
||||
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
||||
""" How helpers should handle refs whose targets are not present in the library. """
|
||||
|
||||
|
||||
SINGLE_USE_PREFIX = '_'
|
||||
"""
|
||||
|
|
@ -177,6 +180,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
tops = set(tops)
|
||||
skip |= tops # don't re-visit tops
|
||||
|
||||
# Get referenced patterns for all tops
|
||||
targets = set()
|
||||
|
|
@ -186,9 +191,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
# Perform recursive lookups, but only once for each name
|
||||
for target in targets - skip:
|
||||
assert target is not None
|
||||
skip.add(target)
|
||||
if target in self:
|
||||
targets |= self.referenced_patterns(target, skip=skip)
|
||||
skip.add(target)
|
||||
|
||||
return targets
|
||||
|
||||
|
|
@ -291,8 +296,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
def flatten_single(name: str) -> None:
|
||||
flattened[name] = None
|
||||
pat = self[name].deepcopy()
|
||||
refs_by_target = tuple((target, tuple(refs)) for target, refs in pat.refs.items())
|
||||
|
||||
for target in pat.refs:
|
||||
for target, refs in refs_by_target:
|
||||
if target is None:
|
||||
continue
|
||||
if dangling_ok and target not in self:
|
||||
|
|
@ -303,10 +309,16 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
target_pat = flattened[target]
|
||||
if target_pat is None:
|
||||
raise PatternError(f'Circular reference in {name} to {target}')
|
||||
if target_pat.is_empty(): # avoid some extra allocations
|
||||
ports_only = flatten_ports and bool(target_pat.ports)
|
||||
if target_pat.is_empty() and not ports_only: # avoid some extra allocations
|
||||
continue
|
||||
|
||||
for ref in pat.refs[target]:
|
||||
for ref in refs:
|
||||
if flatten_ports and ref.repetition is not None and target_pat.ports:
|
||||
raise PatternError(
|
||||
f'Cannot flatten ports from repeated ref to {target!r}; '
|
||||
'flatten with flatten_ports=False or expand/rename the ports manually first.'
|
||||
)
|
||||
p = ref.as_pattern(pattern=target_pat)
|
||||
if not flatten_ports:
|
||||
p.ports.clear()
|
||||
|
|
@ -412,6 +424,21 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
"""
|
||||
return self[self.top()]
|
||||
|
||||
@staticmethod
|
||||
def _dangling_refs_error(dangling: set[str], context: str) -> LibraryError:
|
||||
dangling_list = sorted(dangling)
|
||||
return LibraryError(f'Dangling refs found while {context}: ' + pformat(dangling_list))
|
||||
|
||||
def _raw_child_graph(self) -> tuple[dict[str, set[str]], set[str]]:
|
||||
existing = set(self.keys())
|
||||
graph: dict[str, set[str]] = {}
|
||||
dangling: set[str] = set()
|
||||
for name, pat in self.items():
|
||||
children = {child for child, refs in pat.refs.items() if child is not None and refs}
|
||||
graph[name] = children
|
||||
dangling |= children - existing
|
||||
return graph, dangling
|
||||
|
||||
def dfs(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
|
|
@ -466,9 +493,11 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
memo = {}
|
||||
|
||||
if transform is None or transform is True:
|
||||
transform = numpy.zeros(4)
|
||||
transform = numpy.array([0, 0, 0, 0, 1], dtype=float)
|
||||
elif transform is not False:
|
||||
transform = numpy.asarray(transform, dtype=float)
|
||||
if transform.size == 4:
|
||||
transform = numpy.append(transform, 1.0)
|
||||
|
||||
original_pattern = pattern
|
||||
|
||||
|
|
@ -511,50 +540,99 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
raise LibraryError('visit_* functions returned a new `Pattern` object'
|
||||
' but no top-level name was provided in `hierarchy`')
|
||||
|
||||
del cast('ILibrary', self)[name]
|
||||
cast('ILibrary', self)[name] = pattern
|
||||
|
||||
return self
|
||||
|
||||
def child_graph(self) -> dict[str, set[str | None]]:
|
||||
def child_graph(
|
||||
self,
|
||||
dangling: dangling_mode_t = 'error',
|
||||
) -> dict[str, set[str]]:
|
||||
"""
|
||||
Return a mapping from pattern name to a set of all child patterns
|
||||
(patterns it references).
|
||||
|
||||
Only non-empty ref lists with non-`None` targets are treated as graph edges.
|
||||
|
||||
Args:
|
||||
dangling: How refs to missing targets are handled. `'error'` raises,
|
||||
`'ignore'` drops those edges, and `'include'` exposes them as
|
||||
synthetic leaf nodes.
|
||||
|
||||
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()}
|
||||
graph, dangling_refs = self._raw_child_graph()
|
||||
if dangling == 'error':
|
||||
if dangling_refs:
|
||||
raise self._dangling_refs_error(dangling_refs, 'building child graph')
|
||||
return graph
|
||||
if dangling == 'ignore':
|
||||
existing = set(graph)
|
||||
return {name: {child for child in children if child in existing} for name, children in graph.items()}
|
||||
|
||||
for target in dangling_refs:
|
||||
graph.setdefault(target, set())
|
||||
return graph
|
||||
|
||||
def parent_graph(self) -> dict[str, set[str]]:
|
||||
def parent_graph(
|
||||
self,
|
||||
dangling: dangling_mode_t = 'error',
|
||||
) -> dict[str, set[str]]:
|
||||
"""
|
||||
Return a mapping from pattern name to a set of all parent patterns
|
||||
(patterns which reference it).
|
||||
|
||||
Args:
|
||||
dangling: How refs to missing targets are handled. `'error'` raises,
|
||||
`'ignore'` drops those targets, and `'include'` adds them as
|
||||
synthetic keys whose values are their existing parents.
|
||||
|
||||
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)
|
||||
child_graph, dangling_refs = self._raw_child_graph()
|
||||
if dangling == 'error' and dangling_refs:
|
||||
raise self._dangling_refs_error(dangling_refs, 'building parent graph')
|
||||
|
||||
existing = set(child_graph)
|
||||
igraph: dict[str, set[str]] = {name: set() for name in existing}
|
||||
for parent, children in child_graph.items():
|
||||
for child in children:
|
||||
if child in existing:
|
||||
igraph[child].add(parent)
|
||||
elif dangling == 'include':
|
||||
igraph.setdefault(child, set()).add(parent)
|
||||
return igraph
|
||||
|
||||
def child_order(self) -> list[str]:
|
||||
def child_order(
|
||||
self,
|
||||
dangling: dangling_mode_t = 'error',
|
||||
) -> list[str]:
|
||||
"""
|
||||
Return a topologically sorted list of all contained pattern names.
|
||||
Return a topologically sorted list of graph node names.
|
||||
Child (referenced) patterns will appear before their parents.
|
||||
|
||||
Args:
|
||||
dangling: Passed to `child_graph()`.
|
||||
|
||||
Return:
|
||||
Topologically sorted list of pattern names.
|
||||
"""
|
||||
return cast('list[str]', list(TopologicalSorter(self.child_graph()).static_order()))
|
||||
try:
|
||||
return cast('list[str]', list(TopologicalSorter(self.child_graph(dangling=dangling)).static_order()))
|
||||
except CycleError as exc:
|
||||
cycle = exc.args[1] if len(exc.args) > 1 else None
|
||||
if cycle is None:
|
||||
raise LibraryError('Cycle found while building child order') from exc
|
||||
raise LibraryError(f'Cycle found while building child order: {cycle}') from exc
|
||||
|
||||
def find_refs_local(
|
||||
self,
|
||||
name: str,
|
||||
parent_graph: dict[str, set[str]] | None = None,
|
||||
dangling: dangling_mode_t = 'error',
|
||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||
"""
|
||||
Find the location and orientation of all refs pointing to `name`.
|
||||
|
|
@ -567,6 +645,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
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).
|
||||
dangling: How refs to missing targets are handled if `parent_graph`
|
||||
is not provided. `'include'` also allows querying missing names.
|
||||
|
||||
Returns:
|
||||
Mapping of {parent_name: transform_list}, where transform_list
|
||||
|
|
@ -575,8 +655,18 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
"""
|
||||
instances = defaultdict(list)
|
||||
if parent_graph is None:
|
||||
parent_graph = self.parent_graph()
|
||||
for parent in parent_graph[name]:
|
||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
||||
|
||||
if name not in self:
|
||||
if name not in parent_graph:
|
||||
return instances
|
||||
if dangling == 'error':
|
||||
raise self._dangling_refs_error({name}, f'finding local refs for {name!r}')
|
||||
if dangling == 'ignore':
|
||||
return instances
|
||||
|
||||
for parent in parent_graph.get(name, set()):
|
||||
if parent not in self: # parent_graph may be a for a superset of self
|
||||
continue
|
||||
for ref in self[parent].refs[name]:
|
||||
|
|
@ -589,6 +679,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
name: str,
|
||||
order: list[str] | None = None,
|
||||
parent_graph: dict[str, set[str]] | None = None,
|
||||
dangling: dangling_mode_t = 'error',
|
||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
||||
"""
|
||||
Find the absolute (top-level) location and orientation of all refs (including
|
||||
|
|
@ -605,18 +696,28 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
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).
|
||||
dangling: How refs to missing targets are handled if `order` or
|
||||
`parent_graph` are not provided. `'include'` also allows
|
||||
querying missing names.
|
||||
|
||||
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 {}
|
||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
||||
if order is None:
|
||||
order = self.child_order()
|
||||
order = self.child_order(dangling=graph_mode)
|
||||
if parent_graph is None:
|
||||
parent_graph = self.parent_graph()
|
||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
||||
|
||||
if name not in self:
|
||||
if name not in parent_graph:
|
||||
return {}
|
||||
if dangling == 'error':
|
||||
raise self._dangling_refs_error({name}, f'finding global refs for {name!r}')
|
||||
if dangling == 'ignore':
|
||||
return {}
|
||||
|
||||
self_keys = set(self.keys())
|
||||
|
||||
|
|
@ -625,16 +726,16 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
NDArray[numpy.float64]
|
||||
]]]
|
||||
transforms = defaultdict(list)
|
||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph).items():
|
||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph, dangling=dangling).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:
|
||||
if not parent_graph.get(next_name, set()) & self_keys:
|
||||
continue
|
||||
|
||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph)
|
||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph, dangling=dangling)
|
||||
inners = transforms.pop(next_name)
|
||||
for parent, outer in outers.items():
|
||||
for path, inner in inners:
|
||||
|
|
@ -682,6 +783,33 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
||||
pass
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
other: 'Abstract | str | Pattern | TreeView',
|
||||
append: bool = False,
|
||||
) -> 'Abstract | Pattern':
|
||||
"""
|
||||
Resolve another device (name, Abstract, Pattern, or TreeView) into an Abstract or Pattern.
|
||||
If it is a TreeView, it is first added into this library.
|
||||
|
||||
Args:
|
||||
other: The device to resolve.
|
||||
append: If True and `other` is an `Abstract`, returns the full `Pattern` from the library.
|
||||
|
||||
Returns:
|
||||
An `Abstract` or `Pattern` object.
|
||||
"""
|
||||
from .pattern import Pattern #noqa: PLC0415
|
||||
if not isinstance(other, (str, Abstract, Pattern)):
|
||||
# We got a TreeView; add it into self and grab its topcell as an Abstract
|
||||
other = self << other
|
||||
|
||||
if isinstance(other, str):
|
||||
other = self.abstract(other)
|
||||
if append and isinstance(other, Abstract):
|
||||
other = self[other.name]
|
||||
return other
|
||||
|
||||
def rename(
|
||||
self,
|
||||
old_name: str,
|
||||
|
|
@ -700,6 +828,11 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
Returns:
|
||||
self
|
||||
"""
|
||||
if old_name not in self:
|
||||
raise LibraryError(f'"{old_name}" does not exist in the library.')
|
||||
if old_name == new_name:
|
||||
return self
|
||||
|
||||
self[new_name] = self[old_name]
|
||||
del self[old_name]
|
||||
if move_references:
|
||||
|
|
@ -724,6 +857,9 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
Returns:
|
||||
self
|
||||
"""
|
||||
if old_target == new_target:
|
||||
return self
|
||||
|
||||
for pattern in self.values():
|
||||
if old_target in pattern.refs:
|
||||
pattern.refs[new_target].extend(pattern.refs[old_target])
|
||||
|
|
@ -763,7 +899,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
Returns:
|
||||
(name, pattern) tuple
|
||||
"""
|
||||
from .pattern import Pattern
|
||||
from .pattern import Pattern #noqa: PLC0415
|
||||
pat = Pattern()
|
||||
self[name] = pat
|
||||
return name, pat
|
||||
|
|
@ -797,18 +933,23 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
(default).
|
||||
|
||||
Returns:
|
||||
A mapping of `{old_name: new_name}` for all `old_name`s in `other`. Unchanged
|
||||
names map to themselves.
|
||||
A mapping of `{old_name: new_name}` for all names in `other` which were
|
||||
renamed while being added. Unchanged names are omitted.
|
||||
|
||||
Raises:
|
||||
`LibraryError` if a duplicate name is encountered even after applying `rename_theirs()`.
|
||||
"""
|
||||
from .pattern import map_targets
|
||||
from .pattern import map_targets #noqa: PLC0415
|
||||
duplicates = set(self.keys()) & set(other.keys())
|
||||
|
||||
if not duplicates:
|
||||
for key in other:
|
||||
self._merge(key, other, key)
|
||||
if mutate_other:
|
||||
temp = other
|
||||
else:
|
||||
temp = Library(copy.deepcopy(dict(other)))
|
||||
|
||||
for key in temp:
|
||||
self._merge(key, temp, key)
|
||||
return {}
|
||||
|
||||
if mutate_other:
|
||||
|
|
@ -909,7 +1050,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
# This currently simplifies globally (same shape in different patterns is
|
||||
# merged into the same ref target).
|
||||
|
||||
from .pattern import Pattern
|
||||
from .pattern import Pattern #noqa: PLC0415
|
||||
|
||||
if exclude_types is None:
|
||||
exclude_types = ()
|
||||
|
|
@ -918,6 +1059,18 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
def label2name(label: tuple) -> str: # noqa: ARG001
|
||||
return self.get_name(SINGLE_USE_PREFIX + 'shape')
|
||||
|
||||
used_names = set(self.keys())
|
||||
|
||||
def reserve_target_name(label: tuple) -> str:
|
||||
base_name = label2name(label)
|
||||
name = base_name
|
||||
ii = sum(1 for nn in used_names if nn.startswith(base_name)) if base_name in used_names else 0
|
||||
while name in used_names or name == '':
|
||||
name = base_name + b64suffix(ii)
|
||||
ii += 1
|
||||
used_names.add(name)
|
||||
return name
|
||||
|
||||
shape_counts: MutableMapping[tuple, int] = defaultdict(int)
|
||||
shape_funcs = {}
|
||||
|
||||
|
|
@ -934,6 +1087,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
shape_counts[label] += 1
|
||||
|
||||
shape_pats = {}
|
||||
target_names = {}
|
||||
for label, count in shape_counts.items():
|
||||
if count < threshold:
|
||||
continue
|
||||
|
|
@ -942,6 +1096,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
shape_pat = Pattern()
|
||||
shape_pat.shapes[label[-1]] += [shape_func()]
|
||||
shape_pats[label] = shape_pat
|
||||
target_names[label] = reserve_target_name(label)
|
||||
|
||||
# ## Second pass ##
|
||||
for pat in tuple(self.values()):
|
||||
|
|
@ -966,14 +1121,14 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
# 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, shape_entries in shape_table.items():
|
||||
layer = label[-1]
|
||||
target = label2name(label)
|
||||
target = target_names[label]
|
||||
shapes_to_remove = []
|
||||
for ii, values in shape_entries:
|
||||
offset, scale, rotation, mirror_x = values
|
||||
pat.ref(target=target, offset=offset, scale=scale,
|
||||
rotation=rotation, mirrored=(mirror_x, False))
|
||||
rotation=rotation, mirrored=mirror_x)
|
||||
shapes_to_remove.append(ii)
|
||||
|
||||
# Remove any shapes for which we have created refs.
|
||||
|
|
@ -981,7 +1136,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
del pat.shapes[layer][ii]
|
||||
|
||||
for ll, pp in shape_pats.items():
|
||||
self[label2name(ll)] = pp
|
||||
self[target_names[ll]] = pp
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -1002,7 +1157,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
Returns:
|
||||
self
|
||||
"""
|
||||
from .pattern import Pattern
|
||||
from .pattern import Pattern #noqa: PLC0415
|
||||
|
||||
if name_func is None:
|
||||
def name_func(_pat: Pattern, _shape: Shape | Label) -> str:
|
||||
|
|
@ -1036,6 +1191,25 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
|
||||
return self
|
||||
|
||||
def resolve_repeated_refs(self, name: str | None = None) -> Self:
|
||||
"""
|
||||
Expand all repeated references into multiple individual references.
|
||||
Alters the library in-place.
|
||||
|
||||
Args:
|
||||
name: If specified, only resolve repeated refs in this pattern.
|
||||
Otherwise, resolve in all patterns.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if name is not None:
|
||||
self[name].resolve_repeated_refs()
|
||||
else:
|
||||
for pat in self.values():
|
||||
pat.resolve_repeated_refs()
|
||||
return self
|
||||
|
||||
def subtree(
|
||||
self,
|
||||
tops: str | Sequence[str],
|
||||
|
|
@ -1065,17 +1239,19 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
def prune_empty(
|
||||
self,
|
||||
repeat: bool = True,
|
||||
dangling: dangling_mode_t = 'error',
|
||||
) -> set[str]:
|
||||
"""
|
||||
Delete any empty patterns (i.e. where `Pattern.is_empty` returns `True`).
|
||||