Compare commits
103 Commits
Author | SHA1 | Date | |
---|---|---|---|
c071b33732 | |||
42a7df3055 | |||
5e0eef7c59 | |||
ad00ade097 | |||
385a37e0a2 | |||
35e28acb89 | |||
c1bfee1ddd | |||
560c165f2e | |||
284c7e4fd0 | |||
1eac3baf6a | |||
5a4b9609bd | |||
30cfa0da31 | |||
d11c910dfd | |||
9b2f8599e6 | |||
6c76e1f5cf | |||
6567394fbf | |||
858ef4a114 | |||
b27b1d93d8 | |||
c74573e7dd | |||
0e34242ba5 | |||
c3534beb3f | |||
cae6de69c1 | |||
f14528654b | |||
6631c5e558 | |||
cd60dcc765 | |||
fcb470a02c | |||
93471a221c | |||
4e862d6853 | |||
9917355bb0 | |||
94a1b3d793 | |||
7c7a7e916c | |||
73193473df | |||
febaaeff0b | |||
a54ee5a26c | |||
8d671ed709 | |||
a816a7db8e | |||
a8a42bba1d | |||
da7118f521 | |||
ef6c5df386 | |||
ad0adec8e8 | |||
8fd6896a71 | |||
1ae3ffb9a2 | |||
810a09f18b | |||
97688ffae1 | |||
445c5690e1 | |||
7e1f617274 | |||
b10803efe9 | |||
5f0a450ffa | |||
aa3636ebc6 | |||
48ffc9709e | |||
5cdafd580f | |||
2cf187fdb8 | |||
99e55f931c | |||
c48b427c77 | |||
62fc64c344 | |||
f304217d76 | |||
ae21a2132e | |||
e159c80b0c | |||
38e9d5c250 | |||
5614eea3b4 | |||
8035daee7e | |||
4c69e773fd | |||
39d9b88fa4 | |||
9d5b1ef5e6 | |||
3d50ff0070 | |||
01fe53dc79 | |||
d5adf57bc6 | |||
4c721feaec | |||
6ec94fb3c3 | |||
b1d78b9acb | |||
dca918e63f | |||
cda895a7d3 | |||
6db4bb96db | |||
94aa853a49 | |||
bb054b9eee | |||
5fb736eb74 | |||
4334d0d50b | |||
31863c9799 | |||
30982d742b | |||
447d4ba35b | |||
70a51ed8ef | |||
b33c632569 | |||
c115780bc7 | |||
66d9a4eff8 | |||
3a0c49174b | |||
8d122cbd2e | |||
383b5a0bef | |||
24c77fd3c3 | |||
33529f5ed3 | |||
2516f06e40 | |||
1f6d78386c | |||
41d670eef3 | |||
7f927c46b3 | |||
55e3066485 | |||
c7736a18c3 | |||
aefd79fb5d | |||
7353617878 | |||
f28c31fe29 | |||
8ef5e2e852 | |||
ed433861e3 | |||
e710fa44b5 | |||
9a7a5583ed | |||
b4d31903c1 |
@ -8,6 +8,7 @@ to output to multiple formats.
|
|||||||
|
|
||||||
- [Source repository](https://mpxd.net/code/jan/masque)
|
- [Source repository](https://mpxd.net/code/jan/masque)
|
||||||
- [PyPI](https://pypi.org/project/masque)
|
- [PyPI](https://pypi.org/project/masque)
|
||||||
|
- [Github mirror](https://github.com/anewusername/masque)
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@ -101,7 +102,7 @@ References are accomplished by listing the target's name, not its `Pattern` obje
|
|||||||
|
|
||||||
## Glossary
|
## Glossary
|
||||||
- `Library`: A collection of named cells. OASIS or GDS "library" or file.
|
- `Library`: A collection of named cells. OASIS or GDS "library" or file.
|
||||||
- "tree": Any Library which has only one topcell.
|
- `Tree`: Any `{name: pattern}` mapping which has only one topcell.
|
||||||
- `Pattern`: A collection of geometry, text labels, and reference to other patterns.
|
- `Pattern`: A collection of geometry, text labels, and reference to other patterns.
|
||||||
OASIS or GDS "Cell", DXF "Block".
|
OASIS or GDS "Cell", DXF "Block".
|
||||||
- `Ref`: A reference to another pattern. GDS "AREF/SREF", OASIS "Placement".
|
- `Ref`: A reference to another pattern. GDS "AREF/SREF", OASIS "Placement".
|
||||||
@ -142,6 +143,11 @@ my_pattern.ref(new_name, ...) # instantiate the cell
|
|||||||
|
|
||||||
# In practice, you may do lots of
|
# In practice, you may do lots of
|
||||||
my_pattern.ref(lib << make_tree(...), ...)
|
my_pattern.ref(lib << make_tree(...), ...)
|
||||||
|
|
||||||
|
# With a `Builder` and `place()`/`plug()` the `lib <<` portion can be implicit:
|
||||||
|
my_builder = Builder(library=lib, ...)
|
||||||
|
...
|
||||||
|
my_builder.place(make_tree(...))
|
||||||
```
|
```
|
||||||
|
|
||||||
We can also use this shorthand to quickly add and reference a single flat (as yet un-named) pattern:
|
We can also use this shorthand to quickly add and reference a single flat (as yet un-named) pattern:
|
||||||
@ -166,6 +172,7 @@ my_pattern.place(abstract, ...)
|
|||||||
|
|
||||||
# or
|
# or
|
||||||
my_pattern.place(library << make_tree(...), ...)
|
my_pattern.place(library << make_tree(...), ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Quickly add geometry, labels, or refs:
|
### Quickly add geometry, labels, or refs:
|
||||||
|
@ -99,6 +99,7 @@ def main():
|
|||||||
print('\nAdded aref_test')
|
print('\nAdded aref_test')
|
||||||
|
|
||||||
folder = Path('./layouts/')
|
folder = Path('./layouts/')
|
||||||
|
folder.mkdir(exist_ok=True)
|
||||||
print(f'...writing files to {folder}...')
|
print(f'...writing files to {folder}...')
|
||||||
|
|
||||||
gds1 = folder / 'rep.gds.gz'
|
gds1 = folder / 'rep.gds.gz'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Sequence, Mapping
|
from collections.abc import Sequence, Mapping
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from typing import Sequence, Callable, Any
|
from typing import Any
|
||||||
|
from collections.abc import Sequence, Callable
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Manual wire routing tutorial: Pather and BasicTool
|
Manual wire routing tutorial: Pather and BasicTool
|
||||||
"""
|
"""
|
||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers
|
from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers
|
||||||
from masque.builder.tools import BasicTool, PathTool
|
from masque.builder.tools import BasicTool, PathTool
|
||||||
@ -265,6 +265,12 @@ def main() -> None:
|
|||||||
# when using pather.retool().
|
# when using pather.retool().
|
||||||
pather.path_to('VCC', None, -50_000, out_ptype='m1wire')
|
pather.path_to('VCC', None, -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)
|
||||||
|
|
||||||
# Save the pather's pattern into our library
|
# Save the pather's pattern into our library
|
||||||
library['Pather_and_BasicTool'] = pather.pattern
|
library['Pather_and_BasicTool'] = pather.pattern
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Routines for creating normalized 2D lattices and common photonic crystal
|
Routines for creating normalized 2D lattices and common photonic crystal
|
||||||
cavity designs.
|
cavity designs.
|
||||||
"""
|
"""
|
||||||
from typing import Sequence
|
from collection.abc import Sequence
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
@ -233,8 +233,8 @@ def ln_shift_defect(
|
|||||||
|
|
||||||
# Shift holes
|
# Shift holes
|
||||||
# Expand shifts as necessary
|
# Expand shifts as necessary
|
||||||
tmp_a = numpy.array(shifts_a)
|
tmp_a = numpy.asarray(shifts_a)
|
||||||
tmp_r = numpy.array(shifts_r)
|
tmp_r = numpy.asarray(shifts_r)
|
||||||
n_shifted = max(tmp_a.size, tmp_r.size)
|
n_shifted = max(tmp_a.size, tmp_r.size)
|
||||||
|
|
||||||
shifts_a = numpy.ones(n_shifted)
|
shifts_a = numpy.ones(n_shifted)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Manual wire routing tutorial: RenderPather an PathTool
|
Manual wire routing tutorial: RenderPather an PathTool
|
||||||
"""
|
"""
|
||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
from masque import RenderPather, Library, Pattern, Port, layer_t, map_layers
|
from masque import RenderPather, Library, Pattern, Port, layer_t, map_layers
|
||||||
from masque.builder.tools import PathTool
|
from masque.builder.tools import PathTool
|
||||||
from masque.file.gdsii import writefile
|
from masque.file.gdsii import writefile
|
||||||
|
@ -28,25 +28,67 @@
|
|||||||
can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally.
|
can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .utils import layer_t, annotations_t, SupportsBool
|
from .utils import (
|
||||||
from .error import MasqueError, PatternError, LibraryError, BuildError
|
layer_t as layer_t,
|
||||||
from .shapes import Shape, Polygon, Path, Circle, Arc, Ellipse
|
annotations_t as annotations_t,
|
||||||
from .label import Label
|
SupportsBool as SupportsBool,
|
||||||
from .ref import Ref
|
)
|
||||||
from .pattern import Pattern, map_layers, map_targets, chain_elements
|
from .error import (
|
||||||
|
MasqueError as MasqueError,
|
||||||
|
PatternError as PatternError,
|
||||||
|
LibraryError as LibraryError,
|
||||||
|
BuildError as BuildError,
|
||||||
|
)
|
||||||
|
from .shapes import (
|
||||||
|
Shape as Shape,
|
||||||
|
Polygon as Polygon,
|
||||||
|
Path as Path,
|
||||||
|
Circle as Circle,
|
||||||
|
Arc as Arc,
|
||||||
|
Ellipse as Ellipse,
|
||||||
|
)
|
||||||
|
from .label import Label as Label
|
||||||
|
from .ref import Ref as Ref
|
||||||
|
from .pattern import (
|
||||||
|
Pattern as Pattern,
|
||||||
|
map_layers as map_layers,
|
||||||
|
map_targets as map_targets,
|
||||||
|
chain_elements as chain_elements,
|
||||||
|
)
|
||||||
|
|
||||||
from .library import (
|
from .library import (
|
||||||
ILibraryView, ILibrary,
|
ILibraryView as ILibraryView,
|
||||||
LibraryView, Library, LazyLibrary,
|
ILibrary as ILibrary,
|
||||||
AbstractView,
|
LibraryView as LibraryView,
|
||||||
|
Library as Library,
|
||||||
|
LazyLibrary as LazyLibrary,
|
||||||
|
AbstractView as AbstractView,
|
||||||
|
TreeView as TreeView,
|
||||||
|
Tree as Tree,
|
||||||
|
)
|
||||||
|
from .ports import (
|
||||||
|
Port as Port,
|
||||||
|
PortList as PortList,
|
||||||
|
)
|
||||||
|
from .abstract import Abstract as Abstract
|
||||||
|
from .builder import (
|
||||||
|
Builder as Builder,
|
||||||
|
Tool as Tool,
|
||||||
|
Pather as Pather,
|
||||||
|
RenderPather as RenderPather,
|
||||||
|
RenderStep as RenderStep,
|
||||||
|
BasicTool as BasicTool,
|
||||||
|
PathTool as PathTool,
|
||||||
|
)
|
||||||
|
from .utils import (
|
||||||
|
ports2data as ports2data,
|
||||||
|
oneshot as oneshot,
|
||||||
|
R90 as R90,
|
||||||
|
R180 as R180,
|
||||||
)
|
)
|
||||||
from .ports import Port, PortList
|
|
||||||
from .abstract import Abstract
|
|
||||||
from .builder import Builder, Tool, Pather, RenderPather, RenderStep, BasicTool, PathTool
|
|
||||||
from .utils import ports2data, oneshot
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
__version__ = '3.0'
|
__version__ = '3.4'
|
||||||
version = __version__ # legacy
|
version = __version__ # legacy
|
||||||
|
@ -97,7 +97,7 @@ class Abstract(PortList):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
pivot = numpy.array(pivot)
|
pivot = numpy.asarray(pivot, dtype=float)
|
||||||
self.translate_ports(-pivot)
|
self.translate_ports(-pivot)
|
||||||
self.rotate_ports(rotation)
|
self.rotate_ports(rotation)
|
||||||
self.rotate_port_offsets(rotation)
|
self.rotate_port_offsets(rotation)
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
from .builder import Builder
|
from .builder import Builder as Builder
|
||||||
from .pather import Pather
|
from .pather import Pather as Pather
|
||||||
from .renderpather import RenderPather
|
from .renderpather import RenderPather as RenderPather
|
||||||
from .utils import ell
|
from .utils import ell as ell
|
||||||
from .tools import Tool, RenderStep, BasicTool, PathTool
|
from .tools import (
|
||||||
|
Tool as Tool,
|
||||||
|
RenderStep as RenderStep,
|
||||||
|
BasicTool as BasicTool,
|
||||||
|
PathTool as PathTool,
|
||||||
|
)
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Simplified Pattern assembly (`Builder`)
|
Simplified Pattern assembly (`Builder`)
|
||||||
"""
|
"""
|
||||||
from typing import Self, Sequence, Mapping
|
from typing import Self
|
||||||
|
from collections.abc import Iterable, Sequence, Mapping
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..library import ILibrary
|
from ..library import ILibrary, TreeView
|
||||||
from ..error import BuildError
|
from ..error import BuildError
|
||||||
from ..ports import PortList, Port
|
from ..ports import PortList, Port
|
||||||
from ..abstract import Abstract
|
from ..abstract import Abstract
|
||||||
@ -136,7 +138,7 @@ class Builder(PortList):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def interface(
|
def interface(
|
||||||
cls,
|
cls: type['Builder'],
|
||||||
source: PortList | Mapping[str, Port] | str,
|
source: PortList | Mapping[str, Port] | str,
|
||||||
*,
|
*,
|
||||||
library: ILibrary | None = None,
|
library: ILibrary | None = None,
|
||||||
@ -188,9 +190,35 @@ class Builder(PortList):
|
|||||||
new = Builder(library=library, pattern=pat, name=name)
|
new = Builder(library=library, pattern=pat, name=name)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
@wraps(Pattern.label)
|
||||||
|
def label(self, *args, **kwargs) -> Self:
|
||||||
|
self.pattern.label(*args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@wraps(Pattern.ref)
|
||||||
|
def ref(self, *args, **kwargs) -> Self:
|
||||||
|
self.pattern.ref(*args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@wraps(Pattern.polygon)
|
||||||
|
def polygon(self, *args, **kwargs) -> Self:
|
||||||
|
self.pattern.polygon(*args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@wraps(Pattern.rect)
|
||||||
|
def rect(self, *args, **kwargs) -> Self:
|
||||||
|
self.pattern.rect(*args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
# Note: We're a superclass of `Pather`, where path() means something different...
|
||||||
|
#@wraps(Pattern.path)
|
||||||
|
#def path(self, *args, **kwargs) -> Self:
|
||||||
|
# self.pattern.path(*args, **kwargs)
|
||||||
|
# return self
|
||||||
|
|
||||||
def plug(
|
def plug(
|
||||||
self,
|
self,
|
||||||
other: Abstract | str | Pattern,
|
other: Abstract | str | Pattern | TreeView,
|
||||||
map_in: dict[str, str],
|
map_in: dict[str, str],
|
||||||
map_out: dict[str, str | None] | None = None,
|
map_out: dict[str, str | None] | None = None,
|
||||||
*,
|
*,
|
||||||
@ -198,14 +226,20 @@ class Builder(PortList):
|
|||||||
inherit_name: bool = True,
|
inherit_name: bool = True,
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
append: bool = False,
|
append: bool = False,
|
||||||
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Wrapper around `Pattern.plug` which allows a string for `other`.
|
Wrapper around `Pattern.plug` which allows a string for `other`.
|
||||||
|
|
||||||
The `Builder`'s library is used to dereference the string (or `Abstract`, if
|
The `Builder`'s library is used to dereference the string (or `Abstract`, if
|
||||||
one is passed with `append=True`).
|
one is passed with `append=True`). If a `TreeView` is passed, it is first
|
||||||
|
added into `self.library`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
other: An `Abstract`, string, or `Pattern` describing the device to be instatiated.
|
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
|
map_in: dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||||
port connections between the two devices.
|
port connections between the two devices.
|
||||||
map_out: dict of `{'old_name': 'new_name'}` mappings, specifying
|
map_out: dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||||
@ -227,6 +261,11 @@ class Builder(PortList):
|
|||||||
append: If `True`, `other` is appended instead of being referenced.
|
append: If `True`, `other` is appended instead of being referenced.
|
||||||
Note that this does not flatten `other`, so its refs will still
|
Note that this does not flatten `other`, so its refs will still
|
||||||
be refs (now inside `self`).
|
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:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -243,6 +282,10 @@ class Builder(PortList):
|
|||||||
logger.error('Skipping plug() since device is dead')
|
logger.error('Skipping plug() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
if not isinstance(other, str | Abstract | Pattern):
|
||||||
|
# We got a Tree; add it into self.library and grab an Abstract for it
|
||||||
|
other = self.library << other
|
||||||
|
|
||||||
if isinstance(other, str):
|
if isinstance(other, str):
|
||||||
other = self.library.abstract(other)
|
other = self.library.abstract(other)
|
||||||
if append and isinstance(other, Abstract):
|
if append and isinstance(other, Abstract):
|
||||||
@ -256,12 +299,13 @@ class Builder(PortList):
|
|||||||
inherit_name=inherit_name,
|
inherit_name=inherit_name,
|
||||||
set_rotation=set_rotation,
|
set_rotation=set_rotation,
|
||||||
append=append,
|
append=append,
|
||||||
|
ok_connections=ok_connections,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def place(
|
def place(
|
||||||
self,
|
self,
|
||||||
other: Abstract | str | Pattern,
|
other: Abstract | str | Pattern | TreeView,
|
||||||
*,
|
*,
|
||||||
offset: ArrayLike = (0, 0),
|
offset: ArrayLike = (0, 0),
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
@ -272,12 +316,17 @@ class Builder(PortList):
|
|||||||
append: bool = False,
|
append: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Wrapper around `Pattern.place` which allows a string for `other`.
|
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
|
The `Builder`'s library is used to dereference the string (or `Abstract`, if
|
||||||
one is passed with `append=True`).
|
one is passed with `append=True`). If a `TreeView` is passed, it is first
|
||||||
|
added into `self.library`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
other: An `Abstract`, string, or `Pattern` describing the device to be instatiated.
|
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).
|
offset: Offset at which to place the instance. Default (0, 0).
|
||||||
rotation: Rotation applied to the instance before placement. Default 0.
|
rotation: Rotation applied to the instance before placement. Default 0.
|
||||||
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
||||||
@ -306,6 +355,10 @@ class Builder(PortList):
|
|||||||
logger.error('Skipping place() since device is dead')
|
logger.error('Skipping place() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
if not isinstance(other, str | Abstract | Pattern):
|
||||||
|
# We got a Tree; add it into self.library and grab an Abstract for it
|
||||||
|
other = self.library << other
|
||||||
|
|
||||||
if isinstance(other, str):
|
if isinstance(other, str):
|
||||||
other = self.library.abstract(other)
|
other = self.library.abstract(other)
|
||||||
if append and isinstance(other, Abstract):
|
if append and isinstance(other, Abstract):
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Manual wire/waveguide routing (`Pather`)
|
Manual wire/waveguide routing (`Pather`)
|
||||||
"""
|
"""
|
||||||
from typing import Self, Sequence, MutableMapping, Mapping
|
from typing import Self
|
||||||
|
from collections.abc import Sequence, MutableMapping, Mapping, Iterator
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
from contextlib import contextmanager
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -15,7 +17,7 @@ from ..library import ILibrary, SINGLE_USE_PREFIX
|
|||||||
from ..error import PortError, BuildError
|
from ..error import PortError, BuildError
|
||||||
from ..ports import PortList, Port
|
from ..ports import PortList, Port
|
||||||
from ..abstract import Abstract
|
from ..abstract import Abstract
|
||||||
from ..utils import SupportsBool
|
from ..utils import SupportsBool, rotation_matrix_2d
|
||||||
from .tools import Tool
|
from .tools import Tool
|
||||||
from .utils import ell
|
from .utils import ell
|
||||||
from .builder import Builder
|
from .builder import Builder
|
||||||
@ -174,7 +176,7 @@ class Pather(Builder):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_builder(
|
def from_builder(
|
||||||
cls,
|
cls: type['Pather'],
|
||||||
builder: Builder,
|
builder: Builder,
|
||||||
*,
|
*,
|
||||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
||||||
@ -194,7 +196,7 @@ class Pather(Builder):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def interface(
|
def interface(
|
||||||
cls,
|
cls: type['Pather'],
|
||||||
source: PortList | Mapping[str, Port] | str,
|
source: PortList | Mapping[str, Port] | str,
|
||||||
*,
|
*,
|
||||||
library: ILibrary | None = None,
|
library: ILibrary | None = None,
|
||||||
@ -280,6 +282,37 @@ class Pather(Builder):
|
|||||||
self.tools[key] = tool
|
self.tools[key] = tool
|
||||||
return self
|
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(
|
def path(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
@ -287,6 +320,7 @@ class Pather(Builder):
|
|||||||
length: float,
|
length: float,
|
||||||
*,
|
*,
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
plug_into: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
@ -307,6 +341,8 @@ class Pather(Builder):
|
|||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||||
that you will need to change these. The first port is the input (to be
|
that you will need to change these. The first port is the input (to be
|
||||||
connected to `portspec`).
|
connected to `portspec`).
|
||||||
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
|
port on `self`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -323,7 +359,11 @@ class Pather(Builder):
|
|||||||
in_ptype = self.pattern[portspec].ptype
|
in_ptype = self.pattern[portspec].ptype
|
||||||
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||||
abstract = self.library << tree
|
abstract = self.library << tree
|
||||||
return self.plug(abstract, {portspec: tool_port_names[0]})
|
if plug_into is not None:
|
||||||
|
output = {plug_into: tool_port_names[1]}
|
||||||
|
else:
|
||||||
|
output = {}
|
||||||
|
return self.plug(abstract, {portspec: tool_port_names[0], **output})
|
||||||
|
|
||||||
def path_to(
|
def path_to(
|
||||||
self,
|
self,
|
||||||
@ -334,6 +374,7 @@ class Pather(Builder):
|
|||||||
x: float | None = None,
|
x: float | None = None,
|
||||||
y: float | None = None,
|
y: float | None = None,
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
plug_into: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
@ -362,6 +403,8 @@ class Pather(Builder):
|
|||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||||
that you will need to change these. The first port is the input (to be
|
that you will need to change these. The first port is the input (to be
|
||||||
connected to `portspec`).
|
connected to `portspec`).
|
||||||
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
|
port on `self`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -411,7 +454,136 @@ class Pather(Builder):
|
|||||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
||||||
length = numpy.abs(position - y0)
|
length = numpy.abs(position - y0)
|
||||||
|
|
||||||
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
return self.path(
|
||||||
|
portspec,
|
||||||
|
ccw,
|
||||||
|
length,
|
||||||
|
tool_port_names=tool_port_names,
|
||||||
|
plug_into=plug_into,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def path_into(
|
||||||
|
self,
|
||||||
|
portspec_src: str,
|
||||||
|
portspec_dst: str,
|
||||||
|
*,
|
||||||
|
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
plug_destination: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
Create a "wire"/"waveguide" and traveling between the ports `portspec_src` and
|
||||||
|
`portspec_dst`, and `plug` it into both (or just the source port).
|
||||||
|
|
||||||
|
Only unambiguous scenarios are allowed:
|
||||||
|
- Straight connector between facing ports
|
||||||
|
- Single 90 degree bend
|
||||||
|
- Jog between facing ports
|
||||||
|
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
||||||
|
|
||||||
|
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
||||||
|
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec_src: The name of the starting port into which the wire will be plugged.
|
||||||
|
portspec_dst: The name of the destination port.
|
||||||
|
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||||
|
that you will need to change these. The first port is the input (to be
|
||||||
|
connected to `portspec`).
|
||||||
|
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
||||||
|
to be generated at the destination end. If `None` (default), the destination
|
||||||
|
port's `ptype` will be used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortError if either port does not have a specified rotation.
|
||||||
|
BuildError if and invalid port config is encountered:
|
||||||
|
- Non-manhattan ports
|
||||||
|
- U-bend
|
||||||
|
- Destination too close to (or behind) source
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping path_into() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
port_src = self.pattern[portspec_src]
|
||||||
|
port_dst = self.pattern[portspec_dst]
|
||||||
|
|
||||||
|
if out_ptype is None:
|
||||||
|
out_ptype = port_dst.ptype
|
||||||
|
|
||||||
|
if port_src.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
|
||||||
|
if port_dst.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
||||||
|
|
||||||
|
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_into was asked to route from non-manhattan port')
|
||||||
|
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_into was asked to route to non-manhattan port')
|
||||||
|
|
||||||
|
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
|
||||||
|
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
|
||||||
|
xs, ys = port_src.offset
|
||||||
|
xd, yd = port_dst.offset
|
||||||
|
|
||||||
|
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
||||||
|
|
||||||
|
src_ne = port_src.rotation % (2 * pi) > (3 * pi / 4) # path from src will go north or east
|
||||||
|
|
||||||
|
def get_jog(ccw: SupportsBool, length: float) -> float:
|
||||||
|
tool = self.tools.get(portspec_src, self.tools[None])
|
||||||
|
in_ptype = 'unk' # Could use port_src.ptype, but we're assuming this is after one bend already...
|
||||||
|
tree2 = tool.path(ccw, length, in_ptype=in_ptype, port_names=('A', 'B'), out_ptype=out_ptype, **kwargs)
|
||||||
|
top2 = tree2.top_pattern()
|
||||||
|
jog = rotation_matrix_2d(top2['A'].rotation) @ (top2['B'].offset - top2['A'].offset)
|
||||||
|
return jog[1] * [-1, 1][int(bool(ccw))]
|
||||||
|
|
||||||
|
dst_extra_args = {'out_ptype': out_ptype}
|
||||||
|
if plug_destination:
|
||||||
|
dst_extra_args['plug_into'] = portspec_dst
|
||||||
|
|
||||||
|
src_args = {**kwargs, 'tool_port_names': tool_port_names}
|
||||||
|
dst_args = {**src_args, **dst_extra_args}
|
||||||
|
if src_is_horizontal and not dst_is_horizontal:
|
||||||
|
# single bend should suffice
|
||||||
|
self.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
elif dst_is_horizontal and not src_is_horizontal:
|
||||||
|
# single bend should suffice
|
||||||
|
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif numpy.isclose(angle, pi):
|
||||||
|
if src_is_horizontal and ys == yd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif not src_is_horizontal and xs == xd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
elif src_is_horizontal:
|
||||||
|
# figure out how much x our y-segment (2nd) takes up, then path based on that
|
||||||
|
y_len = numpy.abs(yd - ys)
|
||||||
|
ccw2 = src_ne != (yd > ys)
|
||||||
|
jog = get_jog(ccw2, y_len) * numpy.sign(xd - xs)
|
||||||
|
self.path_to(portspec_src, not ccw2, x=xd - jog, **src_args)
|
||||||
|
self.path_to(portspec_src, ccw2, y=yd, **dst_args)
|
||||||
|
else:
|
||||||
|
# figure out how much y our x-segment (2nd) takes up, then path based on that
|
||||||
|
x_len = numpy.abs(xd - xs)
|
||||||
|
ccw2 = src_ne != (xd < xs)
|
||||||
|
jog = get_jog(ccw2, x_len) * numpy.sign(yd - ys)
|
||||||
|
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
|
||||||
|
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
|
||||||
|
elif numpy.isclose(angle, 0):
|
||||||
|
raise BuildError('Don\'t know how to route a U-bend at this time!')
|
||||||
|
else:
|
||||||
|
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def mpath(
|
def mpath(
|
||||||
self,
|
self,
|
||||||
@ -508,14 +680,17 @@ class Pather(Builder):
|
|||||||
if 'bound_type' in kwargs:
|
if 'bound_type' in kwargs:
|
||||||
bound_types.add(kwargs['bound_type'])
|
bound_types.add(kwargs['bound_type'])
|
||||||
bound = kwargs['bound']
|
bound = kwargs['bound']
|
||||||
|
del kwargs['bound_type']
|
||||||
|
del kwargs['bound']
|
||||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
||||||
if bt in kwargs:
|
if bt in kwargs:
|
||||||
bound_types.add(bt)
|
bound_types.add(bt)
|
||||||
bound = kwargs[bt]
|
bound = kwargs[bt]
|
||||||
|
del kwargs[bt]
|
||||||
|
|
||||||
if not bound_types:
|
if not bound_types:
|
||||||
raise BuildError('No bound type specified for mpath')
|
raise BuildError('No bound type specified for mpath')
|
||||||
elif len(bound_types) > 1:
|
if len(bound_types) > 1:
|
||||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||||
bound_type = tuple(bound_types)[0]
|
bound_type = tuple(bound_types)[0]
|
||||||
|
|
||||||
@ -528,16 +703,16 @@ class Pather(Builder):
|
|||||||
if len(ports) == 1 and not force_container:
|
if len(ports) == 1 and not force_container:
|
||||||
# Not a bus, so having a container just adds noise to the layout
|
# Not a bus, so having a container just adds noise to the layout
|
||||||
port_name = tuple(portspec)[0]
|
port_name = tuple(portspec)[0]
|
||||||
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
|
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names, **kwargs)
|
||||||
else:
|
|
||||||
bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
|
bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
|
||||||
for port_name, length in extensions.items():
|
for port_name, length in extensions.items():
|
||||||
bld.path(port_name, ccw, length, tool_port_names=tool_port_names)
|
bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
||||||
name = self.library.get_name(base_name)
|
name = self.library.get_name(base_name)
|
||||||
self.library[name] = bld.pattern
|
self.library[name] = bld.pattern
|
||||||
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
|
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
||||||
|
|
||||||
# TODO def path_join() and def bus_join()?
|
# TODO def bus_join()?
|
||||||
|
|
||||||
def flatten(self) -> Self:
|
def flatten(self) -> Self:
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Pather with batched (multi-step) rendering
|
Pather with batched (multi-step) rendering
|
||||||
"""
|
"""
|
||||||
from typing import Self, Sequence, Mapping, MutableMapping
|
from typing import Self
|
||||||
|
from collections.abc import Sequence, Mapping, MutableMapping
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@ -127,7 +128,7 @@ class RenderPather(PortList):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def interface(
|
def interface(
|
||||||
cls,
|
cls: type['RenderPather'],
|
||||||
source: PortList | Mapping[str, Port] | str,
|
source: PortList | Mapping[str, Port] | str,
|
||||||
*,
|
*,
|
||||||
library: ILibrary | None = None,
|
library: ILibrary | None = None,
|
||||||
@ -247,7 +248,7 @@ class RenderPather(PortList):
|
|||||||
other_tgt = self.library[other.name]
|
other_tgt = self.library[other.name]
|
||||||
|
|
||||||
# get rid of plugged ports
|
# get rid of plugged ports
|
||||||
for kk in map_in.keys():
|
for kk in map_in:
|
||||||
if kk in self.paths:
|
if kk in self.paths:
|
||||||
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
|
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
|
||||||
|
|
||||||
@ -560,7 +561,7 @@ class RenderPather(PortList):
|
|||||||
|
|
||||||
if not bound_types:
|
if not bound_types:
|
||||||
raise BuildError('No bound type specified for mpath')
|
raise BuildError('No bound type specified for mpath')
|
||||||
elif len(bound_types) > 1:
|
if len(bound_types) > 1:
|
||||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||||
bound_type = tuple(bound_types)[0]
|
bound_type = tuple(bound_types)[0]
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ Tools are objects which dynamically generate simple single-use devices (e.g. wir
|
|||||||
|
|
||||||
# TODO document all tools
|
# TODO document all tools
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Literal, Callable, Any
|
from typing import Literal, Any
|
||||||
|
from collections.abc import Sequence, Callable
|
||||||
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@ -222,8 +223,8 @@ class Tool:
|
|||||||
self,
|
self,
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: Sequence[str] = ('A', 'B'),
|
port_names: Sequence[str] = ('A', 'B'), # noqa: ARG002 (unused)
|
||||||
**kwargs,
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
"""
|
"""
|
||||||
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
||||||
@ -289,12 +290,12 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
|||||||
|
|
||||||
gen_straight, sport_in, sport_out = self.straight
|
gen_straight, sport_in, sport_out = self.straight
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.add_port_pair(names=port_names)
|
pat.add_port_pair(names=port_names, ptype=in_ptype)
|
||||||
if data.in_transition:
|
if data.in_transition:
|
||||||
ipat, iport_theirs, _iport_ours = data.in_transition
|
ipat, iport_theirs, _iport_ours = data.in_transition
|
||||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
pat.plug(ipat, {port_names[1]: iport_theirs})
|
||||||
if not numpy.isclose(data.straight_length, 0):
|
if not numpy.isclose(data.straight_length, 0):
|
||||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length)}
|
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)}
|
||||||
pat.plug(straight, {port_names[1]: sport_in})
|
pat.plug(straight, {port_names[1]: sport_in})
|
||||||
if data.ccw is not None:
|
if data.ccw is not None:
|
||||||
bend, bport_in, bport_out = self.bend
|
bend, bport_in, bport_out = self.bend
|
||||||
@ -312,7 +313,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
|||||||
*,
|
*,
|
||||||
in_ptype: str | None = None,
|
in_ptype: str | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
**kwargs,
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> tuple[Port, LData]:
|
) -> tuple[Port, LData]:
|
||||||
# TODO check all the math for L-shaped bends
|
# TODO check all the math for L-shaped bends
|
||||||
if ccw is not None:
|
if ccw is not None:
|
||||||
@ -404,7 +405,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
|||||||
ipat, iport_theirs, _iport_ours = in_transition
|
ipat, iport_theirs, _iport_ours = in_transition
|
||||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
pat.plug(ipat, {port_names[1]: iport_theirs})
|
||||||
if not numpy.isclose(straight_length, 0):
|
if not numpy.isclose(straight_length, 0):
|
||||||
straight_pat = gen_straight(straight_length)
|
straight_pat = gen_straight(straight_length, **kwargs)
|
||||||
if append:
|
if append:
|
||||||
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
|
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
|
||||||
else:
|
else:
|
||||||
@ -454,7 +455,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||||||
in_ptype: str | None = None,
|
in_ptype: str | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
port_names: tuple[str, str] = ('A', 'B'),
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
**kwargs,
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> Library:
|
) -> Library:
|
||||||
out_port, dxy = self.planL(
|
out_port, dxy = self.planL(
|
||||||
ccw,
|
ccw,
|
||||||
@ -485,9 +486,9 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
*,
|
*,
|
||||||
in_ptype: str | None = None,
|
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
**kwargs,
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> tuple[Port, NDArray[numpy.float64]]:
|
) -> tuple[Port, NDArray[numpy.float64]]:
|
||||||
# TODO check all the math for L-shaped bends
|
# TODO check all the math for L-shaped bends
|
||||||
|
|
||||||
@ -521,7 +522,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: Sequence[str] = ('A', 'B'),
|
port_names: Sequence[str] = ('A', 'B'),
|
||||||
**kwargs,
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
path_vertices = [batch[0].start_port.offset]
|
path_vertices = [batch[0].start_port.offset]
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from typing import Mapping, Sequence, SupportsFloat, cast, TYPE_CHECKING
|
from typing import SupportsFloat, cast, TYPE_CHECKING
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -20,7 +21,7 @@ def ell(
|
|||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
) -> dict[str, float]:
|
) -> dict[str, numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Calculate extension for each port in order to build a 90-degree bend with the provided
|
Calculate extension for each port in order to build a 90-degree bend with the provided
|
||||||
channel spacing:
|
channel spacing:
|
||||||
@ -111,9 +112,9 @@ def ell(
|
|||||||
|
|
||||||
is_horizontal = numpy.isclose(rotations[0] % pi, 0)
|
is_horizontal = numpy.isclose(rotations[0] % pi, 0)
|
||||||
if bound_type in ('ymin', 'ymax') and is_horizontal:
|
if bound_type in ('ymin', 'ymax') and is_horizontal:
|
||||||
raise BuildError('Asked for {bound_type} position but ports are pointing along the x-axis!')
|
raise BuildError(f'Asked for {bound_type} position but ports are pointing along the x-axis!')
|
||||||
elif bound_type in ('xmin', 'xmax') and not is_horizontal:
|
if bound_type in ('xmin', 'xmax') and not is_horizontal:
|
||||||
raise BuildError('Asked for {bound_type} position but ports are pointing along the y-axis!')
|
raise BuildError(f'Asked for {bound_type} position but ports are pointing along the y-axis!')
|
||||||
|
|
||||||
direction = rotations[0] + pi # direction we want to travel in (+pi relative to port)
|
direction = rotations[0] + pi # direction we want to travel in (+pi relative to port)
|
||||||
rot_matrix = rotation_matrix_2d(-direction)
|
rot_matrix = rotation_matrix_2d(-direction)
|
||||||
@ -168,11 +169,11 @@ def ell(
|
|||||||
'emax', 'max_extension',
|
'emax', 'max_extension',
|
||||||
'min_past_furthest',):
|
'min_past_furthest',):
|
||||||
if numpy.size(bound) == 2:
|
if numpy.size(bound) == 2:
|
||||||
bound = cast(Sequence[float], bound)
|
bound = cast('Sequence[float]', bound)
|
||||||
rot_bound = (rot_matrix @ ((bound[0], 0),
|
rot_bound = (rot_matrix @ ((bound[0], 0),
|
||||||
(0, bound[1])))[0, :]
|
(0, bound[1])))[0, :]
|
||||||
else:
|
else:
|
||||||
bound = cast(float, bound)
|
bound = cast('float', bound)
|
||||||
rot_bound = numpy.array(bound)
|
rot_bound = numpy.array(bound)
|
||||||
|
|
||||||
if rot_bound < 0:
|
if rot_bound < 0:
|
||||||
@ -184,10 +185,10 @@ def ell(
|
|||||||
offsets += rot_bound.min() - offsets.max()
|
offsets += rot_bound.min() - offsets.max()
|
||||||
else:
|
else:
|
||||||
if numpy.size(bound) == 2:
|
if numpy.size(bound) == 2:
|
||||||
bound = cast(Sequence[float], bound)
|
bound = cast('Sequence[float]', bound)
|
||||||
rot_bound = (rot_matrix @ bound)[0]
|
rot_bound = (rot_matrix @ bound)[0]
|
||||||
else:
|
else:
|
||||||
bound = cast(float, bound)
|
bound = cast('float', bound)
|
||||||
neg = (direction + pi / 4) % (2 * pi) > pi
|
neg = (direction + pi / 4) % (2 * pi) > pi
|
||||||
rot_bound = -bound if neg else bound
|
rot_bound = -bound if neg else bound
|
||||||
|
|
||||||
@ -201,7 +202,7 @@ def ell(
|
|||||||
if extension < 0:
|
if extension < 0:
|
||||||
ext_floor = -numpy.floor(extension)
|
ext_floor = -numpy.floor(extension)
|
||||||
raise BuildError(f'Position is too close by at least {ext_floor}. Total extensions would be\n\t'
|
raise BuildError(f'Position is too close by at least {ext_floor}. Total extensions would be\n\t'
|
||||||
+ '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets)))
|
+ '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets, strict=True)))
|
||||||
|
|
||||||
result = dict(zip(ports.keys(), offsets))
|
result = dict(zip(ports.keys(), offsets, strict=True))
|
||||||
return result
|
return result
|
||||||
|
@ -6,7 +6,8 @@ Notes:
|
|||||||
* ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID
|
* ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID
|
||||||
to unique values, so byte-for-byte reproducibility is not achievable for now
|
to unique values, so byte-for-byte reproducibility is not achievable for now
|
||||||
"""
|
"""
|
||||||
from typing import Any, Callable, Mapping, cast, TextIO, IO
|
from typing import Any, cast, TextIO, IO
|
||||||
|
from collections.abc import Mapping, Callable
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
@ -15,6 +16,7 @@ import gzip
|
|||||||
import numpy
|
import numpy
|
||||||
import ezdxf
|
import ezdxf
|
||||||
from ezdxf.enums import TextEntityAlignment
|
from ezdxf.enums import TextEntityAlignment
|
||||||
|
from ezdxf.entities import LWPolyline, Polyline, Text, Insert
|
||||||
|
|
||||||
from .utils import is_gzipped, tmpfile
|
from .utils import is_gzipped, tmpfile
|
||||||
from .. import Pattern, Ref, PatternError, Label
|
from .. import Pattern, Ref, PatternError, Label
|
||||||
@ -38,7 +40,7 @@ def write(
|
|||||||
top_name: str,
|
top_name: str,
|
||||||
stream: TextIO,
|
stream: TextIO,
|
||||||
*,
|
*,
|
||||||
dxf_version='AC1024',
|
dxf_version: str = 'AC1024',
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
||||||
@ -130,7 +132,7 @@ def writefile(
|
|||||||
with tmpfile(path) as base_stream:
|
with tmpfile(path) as base_stream:
|
||||||
streams: tuple[Any, ...] = (base_stream,)
|
streams: tuple[Any, ...] = (base_stream,)
|
||||||
if path.suffix == '.gz':
|
if path.suffix == '.gz':
|
||||||
gz_stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
gz_stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||||
streams = (gz_stream,) + streams
|
streams = (gz_stream,) + streams
|
||||||
else:
|
else:
|
||||||
gz_stream = base_stream
|
gz_stream = base_stream
|
||||||
@ -204,26 +206,25 @@ def read(
|
|||||||
return mlib, library_info
|
return mlib, library_info
|
||||||
|
|
||||||
|
|
||||||
def _read_block(block) -> tuple[str, Pattern]:
|
def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> tuple[str, Pattern]:
|
||||||
name = block.name
|
name = block.name
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
for element in block:
|
for element in block:
|
||||||
eltype = element.dxftype()
|
if isinstance(element, LWPolyline | Polyline):
|
||||||
if eltype in ('POLYLINE', 'LWPOLYLINE'):
|
if isinstance(element, LWPolyline):
|
||||||
if eltype == 'LWPOLYLINE':
|
points = numpy.asarray(element.get_points())
|
||||||
points = numpy.array(tuple(element.lwpoints))
|
elif isinstance(element, Polyline):
|
||||||
else:
|
points = numpy.asarray([pp.xyz for pp in element.points()])
|
||||||
points = numpy.array(tuple(element.points()))
|
|
||||||
attr = element.dxfattribs()
|
attr = element.dxfattribs()
|
||||||
layer = attr.get('layer', DEFAULT_LAYER)
|
layer = attr.get('layer', DEFAULT_LAYER)
|
||||||
|
|
||||||
if points.shape[1] == 2:
|
if points.shape[1] == 2:
|
||||||
raise PatternError('Invalid or unimplemented polygon?')
|
raise PatternError('Invalid or unimplemented polygon?')
|
||||||
#shape = Polygon()
|
|
||||||
elif points.shape[1] > 2:
|
if points.shape[1] > 2:
|
||||||
if (points[0, 2] != points[:, 2]).any():
|
if (points[0, 2] != points[:, 2]).any():
|
||||||
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)')
|
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)')
|
||||||
elif points.shape[1] == 4 and (points[:, 3] != 0).any():
|
if points.shape[1] == 4 and (points[:, 3] != 0).any():
|
||||||
raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
|
raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
|
||||||
|
|
||||||
width = points[0, 2]
|
width = points[0, 2]
|
||||||
@ -238,9 +239,9 @@ def _read_block(block) -> tuple[str, Pattern]:
|
|||||||
|
|
||||||
pat.shapes[layer].append(shape)
|
pat.shapes[layer].append(shape)
|
||||||
|
|
||||||
elif eltype in ('TEXT',):
|
elif isinstance(element, Text):
|
||||||
args = dict(
|
args = dict(
|
||||||
offset=numpy.array(element.get_pos()[1])[:2],
|
offset=numpy.asarray(element.get_placement()[1])[:2],
|
||||||
layer=element.dxfattribs().get('layer', DEFAULT_LAYER),
|
layer=element.dxfattribs().get('layer', DEFAULT_LAYER),
|
||||||
)
|
)
|
||||||
string = element.dxfattribs().get('text', '')
|
string = element.dxfattribs().get('text', '')
|
||||||
@ -251,7 +252,7 @@ def _read_block(block) -> tuple[str, Pattern]:
|
|||||||
pat.label(string=string, **args)
|
pat.label(string=string, **args)
|
||||||
# else:
|
# else:
|
||||||
# pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????))
|
# pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????))
|
||||||
elif eltype in ('INSERT',):
|
elif isinstance(element, Insert):
|
||||||
attr = element.dxfattribs()
|
attr = element.dxfattribs()
|
||||||
xscale = attr.get('xscale', 1)
|
xscale = attr.get('xscale', 1)
|
||||||
yscale = attr.get('yscale', 1)
|
yscale = attr.get('yscale', 1)
|
||||||
@ -261,7 +262,7 @@ def _read_block(block) -> tuple[str, Pattern]:
|
|||||||
mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0))
|
mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0))
|
||||||
rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle
|
rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle
|
||||||
|
|
||||||
offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2]
|
offset = numpy.asarray(attr.get('insert', (0, 0, 0)))[:2]
|
||||||
|
|
||||||
args = dict(
|
args = dict(
|
||||||
target=attr.get('name', None),
|
target=attr.get('name', None),
|
||||||
@ -336,10 +337,10 @@ def _mrefs_to_drefs(
|
|||||||
def _shapes_to_elements(
|
def _shapes_to_elements(
|
||||||
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
||||||
shapes: dict[layer_t, list[Shape]],
|
shapes: dict[layer_t, list[Shape]],
|
||||||
polygonize_paths: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# Add `LWPolyline`s for each shape.
|
# Add `LWPolyline`s for each shape.
|
||||||
# Could set do paths with width setting, but need to consider endcaps.
|
# Could set do paths with width setting, but need to consider endcaps.
|
||||||
|
# TODO: can DXF do paths?
|
||||||
for layer, sseq in shapes.items():
|
for layer, sseq in shapes.items():
|
||||||
attribs = dict(layer=_mlayer2dxf(layer))
|
attribs = dict(layer=_mlayer2dxf(layer))
|
||||||
for shape in sseq:
|
for shape in sseq:
|
||||||
|
@ -19,7 +19,8 @@ Notes:
|
|||||||
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility.
|
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility.
|
||||||
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
|
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
|
||||||
"""
|
"""
|
||||||
from typing import Callable, Iterable, Mapping, IO, cast, Any
|
from typing import IO, cast, Any
|
||||||
|
from collections.abc import Iterable, Mapping, Callable
|
||||||
import io
|
import io
|
||||||
import mmap
|
import mmap
|
||||||
import logging
|
import logging
|
||||||
@ -144,7 +145,7 @@ def writefile(
|
|||||||
with tmpfile(path) as base_stream:
|
with tmpfile(path) as base_stream:
|
||||||
streams: tuple[Any, ...] = (base_stream,)
|
streams: tuple[Any, ...] = (base_stream,)
|
||||||
if path.suffix == '.gz':
|
if path.suffix == '.gz':
|
||||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
||||||
streams = (stream,) + streams
|
streams = (stream,) + streams
|
||||||
else:
|
else:
|
||||||
stream = base_stream
|
stream = base_stream
|
||||||
@ -356,7 +357,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
|
|||||||
if isinstance(rep, Grid):
|
if isinstance(rep, Grid):
|
||||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||||
b_count = rep.b_count if rep.b_count is not None else 1
|
b_count = rep.b_count if rep.b_count is not None else 1
|
||||||
xy = numpy.array(ref.offset) + numpy.array([
|
xy = numpy.asarray(ref.offset) + numpy.array([
|
||||||
[0.0, 0.0],
|
[0.0, 0.0],
|
||||||
rep.a_vector * rep.a_count,
|
rep.a_vector * rep.a_count,
|
||||||
b_vector * b_count,
|
b_vector * b_count,
|
||||||
@ -408,8 +409,8 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
|
|||||||
for key, vals in annotations.items():
|
for key, vals in annotations.items():
|
||||||
try:
|
try:
|
||||||
i = int(key)
|
i = int(key)
|
||||||
except ValueError:
|
except ValueError as err:
|
||||||
raise PatternError(f'Annotation key {key} is not convertable to an integer')
|
raise PatternError(f'Annotation key {key} is not convertable to an integer') from err
|
||||||
if not (0 < i < 126):
|
if not (0 < i < 126):
|
||||||
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
|
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
|
||||||
|
|
||||||
@ -596,19 +597,19 @@ def load_libraryfile(
|
|||||||
path = pathlib.Path(filename)
|
path = pathlib.Path(filename)
|
||||||
stream: IO[bytes]
|
stream: IO[bytes]
|
||||||
if is_gzipped(path):
|
if is_gzipped(path):
|
||||||
if mmap:
|
if use_mmap:
|
||||||
logger.info('Asked to mmap a gzipped file, reading into memory instead...')
|
logger.info('Asked to mmap a gzipped file, reading into memory instead...')
|
||||||
gz_stream = gzip.open(path, mode='rb')
|
gz_stream = gzip.open(path, mode='rb') # noqa: SIM115
|
||||||
stream = io.BytesIO(gz_stream.read()) # type: ignore
|
stream = io.BytesIO(gz_stream.read()) # type: ignore
|
||||||
else:
|
else:
|
||||||
gz_stream = gzip.open(path, mode='rb')
|
gz_stream = gzip.open(path, mode='rb') # noqa: SIM115
|
||||||
stream = io.BufferedReader(gz_stream) # type: ignore
|
stream = io.BufferedReader(gz_stream) # type: ignore
|
||||||
else:
|
else: # noqa: PLR5501
|
||||||
if mmap:
|
if use_mmap:
|
||||||
base_stream = open(path, mode='rb', buffering=0)
|
base_stream = path.open(mode='rb', buffering=0) # noqa: SIM115
|
||||||
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
|
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
|
||||||
else:
|
else:
|
||||||
stream = open(path, mode='rb')
|
stream = path.open(mode='rb') # noqa: SIM115
|
||||||
return load_library(stream, full_load=full_load, postprocess=postprocess)
|
return load_library(stream, full_load=full_load, postprocess=postprocess)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ Note that OASIS references follow the same convention as `masque`,
|
|||||||
Notes:
|
Notes:
|
||||||
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
|
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
|
||||||
"""
|
"""
|
||||||
from typing import Any, Callable, Iterable, IO, Mapping, cast, Sequence
|
from typing import Any, IO, cast
|
||||||
|
from collections.abc import Sequence, Iterable, Mapping, Callable
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
import gzip
|
import gzip
|
||||||
@ -189,7 +190,7 @@ def writefile(
|
|||||||
with tmpfile(path) as base_stream:
|
with tmpfile(path) as base_stream:
|
||||||
streams: tuple[Any, ...] = (base_stream,)
|
streams: tuple[Any, ...] = (base_stream,)
|
||||||
if path.suffix == '.gz':
|
if path.suffix == '.gz':
|
||||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||||
streams += (stream,)
|
streams += (stream,)
|
||||||
else:
|
else:
|
||||||
stream = base_stream
|
stream = base_stream
|
||||||
@ -297,7 +298,7 @@ def read(
|
|||||||
cap_start = path_cap_map[element.get_extension_start()[0]]
|
cap_start = path_cap_map[element.get_extension_start()[0]]
|
||||||
cap_end = path_cap_map[element.get_extension_end()[0]]
|
cap_end = path_cap_map[element.get_extension_end()[0]]
|
||||||
if cap_start != cap_end:
|
if cap_start != cap_end:
|
||||||
raise Exception('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types
|
raise PatternError('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types
|
||||||
cap = cap_start
|
cap = cap_start
|
||||||
|
|
||||||
path_args: dict[str, Any] = {}
|
path_args: dict[str, Any] = {}
|
||||||
@ -452,6 +453,8 @@ def read(
|
|||||||
|
|
||||||
for placement in cell.placements:
|
for placement in cell.placements:
|
||||||
target, ref = _placement_to_ref(placement, lib)
|
target, ref = _placement_to_ref(placement, lib)
|
||||||
|
if isinstance(target, int):
|
||||||
|
target = lib.cellnames[target].nstring.string
|
||||||
pat.refs[target].append(ref)
|
pat.refs[target].append(ref)
|
||||||
|
|
||||||
mlib[cell_name] = pat
|
mlib[cell_name] = pat
|
||||||
@ -548,7 +551,7 @@ def _shapes_to_elements(
|
|||||||
circle = fatrec.Circle(
|
circle = fatrec.Circle(
|
||||||
layer=layer,
|
layer=layer,
|
||||||
datatype=datatype,
|
datatype=datatype,
|
||||||
radius=cast(int, radius),
|
radius=cast('int', radius),
|
||||||
x=offset[0],
|
x=offset[0],
|
||||||
y=offset[1],
|
y=offset[1],
|
||||||
properties=properties,
|
properties=properties,
|
||||||
@ -565,8 +568,8 @@ def _shapes_to_elements(
|
|||||||
path = fatrec.Path(
|
path = fatrec.Path(
|
||||||
layer=layer,
|
layer=layer,
|
||||||
datatype=datatype,
|
datatype=datatype,
|
||||||
point_list=cast(Sequence[Sequence[int]], deltas),
|
point_list=cast('Sequence[Sequence[int]]', deltas),
|
||||||
half_width=cast(int, half_width),
|
half_width=cast('int', half_width),
|
||||||
x=xy[0],
|
x=xy[0],
|
||||||
y=xy[1],
|
y=xy[1],
|
||||||
extension_start=extension_start, # TODO implement multiple cap types?
|
extension_start=extension_start, # TODO implement multiple cap types?
|
||||||
@ -584,7 +587,7 @@ def _shapes_to_elements(
|
|||||||
datatype=datatype,
|
datatype=datatype,
|
||||||
x=xy[0],
|
x=xy[0],
|
||||||
y=xy[1],
|
y=xy[1],
|
||||||
point_list=cast(list[list[int]], points),
|
point_list=cast('list[list[int]]', points),
|
||||||
properties=properties,
|
properties=properties,
|
||||||
repetition=repetition,
|
repetition=repetition,
|
||||||
))
|
))
|
||||||
@ -648,10 +651,10 @@ def repetition_masq2fata(
|
|||||||
a_count = rint_cast(rep.a_count)
|
a_count = rint_cast(rep.a_count)
|
||||||
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
|
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
|
||||||
frep = fatamorgana.GridRepetition(
|
frep = fatamorgana.GridRepetition(
|
||||||
a_vector=cast(list[int], a_vector),
|
a_vector=cast('list[int]', a_vector),
|
||||||
b_vector=cast(list[int] | None, b_vector),
|
b_vector=cast('list[int] | None', b_vector),
|
||||||
a_count=cast(int, a_count),
|
a_count=cast('int', a_count),
|
||||||
b_count=cast(int | None, b_count),
|
b_count=cast('int | None', b_count),
|
||||||
)
|
)
|
||||||
offset = (0, 0)
|
offset = (0, 0)
|
||||||
elif isinstance(rep, Arbitrary):
|
elif isinstance(rep, Arbitrary):
|
||||||
@ -692,9 +695,9 @@ def properties_to_annotations(
|
|||||||
|
|
||||||
assert proprec.values is not None
|
assert proprec.values is not None
|
||||||
for value in proprec.values:
|
for value in proprec.values:
|
||||||
if isinstance(value, (float, int)):
|
if isinstance(value, float | int):
|
||||||
values.append(value)
|
values.append(value)
|
||||||
elif isinstance(value, (NString, AString)):
|
elif isinstance(value, NString | AString):
|
||||||
values.append(value.string)
|
values.append(value.string)
|
||||||
elif isinstance(value, PropStringReference):
|
elif isinstance(value, PropStringReference):
|
||||||
values.append(propstrings[value.ref].string) # dereference
|
values.append(propstrings[value.ref].string) # dereference
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
SVG file format readers and writers
|
SVG file format readers and writers
|
||||||
"""
|
"""
|
||||||
from typing import Mapping
|
from collections.abc import Mapping
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -50,7 +50,7 @@ def writefile(
|
|||||||
bounds = pattern.get_bounds(library=library)
|
bounds = pattern.get_bounds(library=library)
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||||
else:
|
else:
|
||||||
bounds_min, bounds_max = bounds
|
bounds_min, bounds_max = bounds
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ def writefile_inverted(
|
|||||||
bounds = pattern.get_bounds(library=library)
|
bounds = pattern.get_bounds(library=library)
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||||
else:
|
else:
|
||||||
bounds_min, bounds_max = bounds
|
bounds_min, bounds_max = bounds
|
||||||
|
|
||||||
@ -154,9 +154,9 @@ def poly2path(vertices: ArrayLike) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
SVG path-string.
|
SVG path-string.
|
||||||
"""
|
"""
|
||||||
verts = numpy.array(vertices, copy=False)
|
verts = numpy.asarray(vertices)
|
||||||
commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1])
|
commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1]) # noqa: UP032
|
||||||
for vertex in verts[1:]:
|
for vertex in verts[1:]:
|
||||||
commands += 'L{:g},{:g}'.format(vertex[0], vertex[1])
|
commands += 'L{:g},{:g}'.format(vertex[0], vertex[1]) # noqa: UP032
|
||||||
commands += ' Z '
|
commands += ' Z '
|
||||||
return commands
|
return commands
|
||||||
|
@ -1,21 +1,93 @@
|
|||||||
"""
|
"""
|
||||||
Helper functions for file reading and writing
|
Helper functions for file reading and writing
|
||||||
"""
|
"""
|
||||||
from typing import IO, Iterator
|
from typing import IO
|
||||||
|
from collections.abc import Iterator, Mapping
|
||||||
import re
|
import re
|
||||||
import pathlib
|
import pathlib
|
||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from pprint import pformat
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from .. import Pattern, PatternError
|
from .. import Pattern, PatternError, Library, LibraryError
|
||||||
from ..shapes import Polygon, Path
|
from ..shapes import Polygon, Path
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def preflight(
|
||||||
|
lib: Library,
|
||||||
|
sort: bool = True,
|
||||||
|
sort_elements: bool = False,
|
||||||
|
allow_dangling_refs: bool | None = None,
|
||||||
|
allow_named_layers: bool = True,
|
||||||
|
prune_empty_patterns: bool = False,
|
||||||
|
wrap_repeated_shapes: bool = False,
|
||||||
|
) -> Library:
|
||||||
|
"""
|
||||||
|
Run a standard set of useful operations and checks, usually done immediately prior
|
||||||
|
to writing to a file (or immediately after reading).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sort: Whether to sort the patterns based on their names, and optionaly sort the pattern contents.
|
||||||
|
Default True. Useful for reproducible builds.
|
||||||
|
sort_elements: Whether to sort the pattern contents. Requires sort=True to run.
|
||||||
|
allow_dangling_refs: If `None` (default), warns about any refs to patterns that are not
|
||||||
|
in the provided library. If `True`, no check is performed; if `False`, a `LibraryError`
|
||||||
|
is raised instead.
|
||||||
|
allow_named_layers: If `False`, raises a `PatternError` if any layer is referred to by
|
||||||
|
a string instead of a number (or tuple).
|
||||||
|
prune_empty_patterns: Runs `Library.prune_empty()`, recursively deleting any empty patterns.
|
||||||
|
wrap_repeated_shapes: Runs `Library.wrap_repeated_shapes()`, turning repeated shapes into
|
||||||
|
repeated refs containing non-repeated shapes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`lib` or an equivalent sorted library
|
||||||
|
"""
|
||||||
|
if sort:
|
||||||
|
lib = Library(dict(sorted(
|
||||||
|
(nn, pp.sort(sort_elements=sort_elements)) for nn, pp in lib.items()
|
||||||
|
)))
|
||||||
|
|
||||||
|
if not allow_dangling_refs:
|
||||||
|
refs = lib.referenced_patterns()
|
||||||
|
dangling = refs - set(lib.keys())
|
||||||
|
if dangling:
|
||||||
|
msg = 'Dangling refs found: ' + pformat(dangling)
|
||||||
|
if allow_dangling_refs is None:
|
||||||
|
logger.warning(msg)
|
||||||
|
else:
|
||||||
|
raise LibraryError(msg)
|
||||||
|
|
||||||
|
if not allow_named_layers:
|
||||||
|
named_layers: Mapping[str, set] = defaultdict(set)
|
||||||
|
for name, pat in lib.items():
|
||||||
|
for layer in chain(pat.shapes.keys(), pat.labels.keys()):
|
||||||
|
if isinstance(layer, str):
|
||||||
|
named_layers[name].add(layer)
|
||||||
|
named_layers = dict(named_layers)
|
||||||
|
if named_layers:
|
||||||
|
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
|
||||||
|
|
||||||
|
if prune_empty_patterns:
|
||||||
|
pruned = lib.prune_empty()
|
||||||
|
if pruned:
|
||||||
|
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
|
||||||
|
logger.debug('Pruned: ' + pformat(pruned))
|
||||||
|
else:
|
||||||
|
logger.debug('Preflight found no empty patterns')
|
||||||
|
|
||||||
|
if wrap_repeated_shapes:
|
||||||
|
lib.wrap_repeated_shapes()
|
||||||
|
|
||||||
|
return lib
|
||||||
|
|
||||||
|
|
||||||
def mangle_name(name: str) -> str:
|
def mangle_name(name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Sanitize a name.
|
Sanitize a name.
|
||||||
@ -45,7 +117,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
|
|||||||
for shapes in pat.shapes.values():
|
for shapes in pat.shapes.values():
|
||||||
remove_inds = []
|
remove_inds = []
|
||||||
for ii, shape in enumerate(shapes):
|
for ii, shape in enumerate(shapes):
|
||||||
if not isinstance(shape, (Polygon, Path)):
|
if not isinstance(shape, Polygon | Path):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
shape.clean_vertices()
|
shape.clean_vertices()
|
||||||
@ -57,7 +129,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
|
|||||||
|
|
||||||
|
|
||||||
def is_gzipped(path: pathlib.Path) -> bool:
|
def is_gzipped(path: pathlib.Path) -> bool:
|
||||||
with open(path, 'rb') as stream:
|
with path.open('rb') as stream:
|
||||||
magic_bytes = stream.read(2)
|
magic_bytes = stream.read(2)
|
||||||
return magic_bytes == b'\x1f\x8b'
|
return magic_bytes == b'\x1f\x8b'
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
from typing import Self
|
from typing import Self, Any
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from .repetition import Repetition
|
from .repetition import Repetition
|
||||||
from .utils import rotation_matrix_2d, annotations_t
|
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
|
||||||
from .traits import AnnotatableImpl
|
from .traits import AnnotatableImpl
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable):
|
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable):
|
||||||
"""
|
"""
|
||||||
A text annotation with a position (but no size; it is not drawn)
|
A text annotation with a position (but no size; it is not drawn)
|
||||||
@ -47,7 +49,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
|||||||
annotations: annotations_t | None = None,
|
annotations: annotations_t | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.string = string
|
self.string = string
|
||||||
self.offset = numpy.array(offset, dtype=float, copy=True)
|
self.offset = numpy.array(offset, dtype=float)
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations if annotations is not None else {}
|
||||||
|
|
||||||
@ -64,6 +66,23 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
|||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __lt__(self, other: 'Label') -> bool:
|
||||||
|
if self.string != other.string:
|
||||||
|
return self.string < other.string
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
self.string == other.string
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||||
"""
|
"""
|
||||||
Rotate the label around a point.
|
Rotate the label around a point.
|
||||||
@ -75,7 +94,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
pivot = numpy.array(pivot, dtype=float)
|
pivot = numpy.asarray(pivot, dtype=float)
|
||||||
self.translate(-pivot)
|
self.translate(-pivot)
|
||||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||||
self.translate(+pivot)
|
self.translate(+pivot)
|
||||||
|
@ -14,22 +14,21 @@ Classes include:
|
|||||||
- `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked
|
- `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked
|
||||||
library. Generated with `ILibraryView.abstract_view()`.
|
library. Generated with `ILibraryView.abstract_view()`.
|
||||||
"""
|
"""
|
||||||
from typing import Callable, Self, Type, TYPE_CHECKING, cast
|
from typing import Self, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal
|
||||||
from typing import Iterator, Mapping, MutableMapping, Sequence
|
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable
|
||||||
import logging
|
import logging
|
||||||
import base64
|
|
||||||
import struct
|
|
||||||
import re
|
import re
|
||||||
import copy
|
import copy
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from graphlib import TopologicalSorter
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from .error import LibraryError, PatternError
|
from .error import LibraryError, PatternError
|
||||||
from .utils import rotation_matrix_2d, layer_t
|
from .utils import layer_t, apply_transforms
|
||||||
from .shapes import Shape, Polygon
|
from .shapes import Shape, Polygon
|
||||||
from .label import Label
|
from .label import Label
|
||||||
from .abstract import Abstract
|
from .abstract import Abstract
|
||||||
@ -42,7 +41,24 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
visitor_function_t = Callable[..., 'Pattern']
|
class visitor_function_t(Protocol):
|
||||||
|
""" Signature for `Library.dfs()` visitor functions. """
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
pattern: 'Pattern',
|
||||||
|
hierarchy: tuple[str | None, ...],
|
||||||
|
memo: dict,
|
||||||
|
transform: NDArray[numpy.float64] | Literal[False],
|
||||||
|
) -> 'Pattern':
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
TreeView: TypeAlias = Mapping[str, 'Pattern']
|
||||||
|
""" A name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
||||||
|
|
||||||
|
Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
||||||
|
""" A mutable name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
||||||
|
|
||||||
|
|
||||||
SINGLE_USE_PREFIX = '_'
|
SINGLE_USE_PREFIX = '_'
|
||||||
"""
|
"""
|
||||||
@ -158,7 +174,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
tops = tuple(self.keys())
|
tops = tuple(self.keys())
|
||||||
|
|
||||||
if skip is None:
|
if skip is None:
|
||||||
skip = set([None])
|
skip = {None}
|
||||||
|
|
||||||
if isinstance(tops, str):
|
if isinstance(tops, str):
|
||||||
tops = (tops,)
|
tops = (tops,)
|
||||||
@ -195,7 +211,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
if isinstance(tops, str):
|
if isinstance(tops, str):
|
||||||
tops = (tops,)
|
tops = (tops,)
|
||||||
|
|
||||||
keep = cast(set[str], self.referenced_patterns(tops) - set((None,)))
|
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
||||||
keep |= set(tops)
|
keep |= set(tops)
|
||||||
|
|
||||||
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
||||||
@ -267,7 +283,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
if isinstance(tops, str):
|
if isinstance(tops, str):
|
||||||
tops = (tops,)
|
tops = (tops,)
|
||||||
|
|
||||||
flattened: dict[str, 'Pattern | None'] = {}
|
flattened: dict[str, Pattern | None] = {}
|
||||||
|
|
||||||
def flatten_single(name: str) -> None:
|
def flatten_single(name: str) -> None:
|
||||||
flattened[name] = None
|
flattened[name] = None
|
||||||
@ -298,7 +314,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
flatten_single(top)
|
flatten_single(top)
|
||||||
|
|
||||||
assert None not in flattened.values()
|
assert None not in flattened.values()
|
||||||
return cast(dict[str, 'Pattern'], flattened)
|
return cast('dict[str, Pattern]', flattened)
|
||||||
|
|
||||||
def get_name(
|
def get_name(
|
||||||
self,
|
self,
|
||||||
@ -331,12 +347,13 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
else:
|
else:
|
||||||
sanitized_name = name
|
sanitized_name = name
|
||||||
|
|
||||||
ii = 0
|
|
||||||
suffixed_name = sanitized_name
|
suffixed_name = sanitized_name
|
||||||
|
if sanitized_name in self:
|
||||||
|
ii = sum(1 for nn in self.keys() if nn.startswith(sanitized_name))
|
||||||
|
else:
|
||||||
|
ii = 0
|
||||||
while suffixed_name in self or suffixed_name == '':
|
while suffixed_name in self or suffixed_name == '':
|
||||||
suffix = base64.b64encode(struct.pack('>Q', ii), altchars=b'$?').decode('ASCII')
|
suffixed_name = sanitized_name + b64suffix(ii)
|
||||||
|
|
||||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
|
||||||
ii += 1
|
ii += 1
|
||||||
|
|
||||||
if len(suffixed_name) > max_length:
|
if len(suffixed_name) > max_length:
|
||||||
@ -370,6 +387,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
def top(self) -> str:
|
def top(self) -> str:
|
||||||
"""
|
"""
|
||||||
Return the name of the topcell, or raise an exception if there isn't a single topcell
|
Return the name of the topcell, or raise an exception if there isn't a single topcell
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
LibraryError if there is not exactly one topcell.
|
||||||
"""
|
"""
|
||||||
tops = self.tops()
|
tops = self.tops()
|
||||||
if len(tops) != 1:
|
if len(tops) != 1:
|
||||||
@ -379,6 +399,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
def top_pattern(self) -> 'Pattern':
|
def top_pattern(self) -> 'Pattern':
|
||||||
"""
|
"""
|
||||||
Shorthand for self[self.top()]
|
Shorthand for self[self.top()]
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
LibraryError if there is not exactly one topcell.
|
||||||
"""
|
"""
|
||||||
return self[self.top()]
|
return self[self.top()]
|
||||||
|
|
||||||
@ -438,7 +461,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
if transform is None or transform is True:
|
if transform is None or transform is True:
|
||||||
transform = numpy.zeros(4)
|
transform = numpy.zeros(4)
|
||||||
elif transform is not False:
|
elif transform is not False:
|
||||||
transform = numpy.array(transform)
|
transform = numpy.asarray(transform, dtype=float)
|
||||||
|
|
||||||
original_pattern = pattern
|
original_pattern = pattern
|
||||||
|
|
||||||
@ -452,16 +475,13 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
|
raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
|
||||||
|
|
||||||
for ref in pattern.refs[target]:
|
for ref in pattern.refs[target]:
|
||||||
|
ref_transforms: list[bool] | NDArray[numpy.float64]
|
||||||
if transform is not False:
|
if transform is not False:
|
||||||
sign = numpy.ones(2)
|
ref_transforms = apply_transforms(transform, ref.as_transforms())
|
||||||
if transform[3]:
|
|
||||||
sign[1] = -1
|
|
||||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
|
|
||||||
ref_transform = transform + (xy[0], xy[1], ref.rotation, ref.mirrored)
|
|
||||||
ref_transform[3] %= 2
|
|
||||||
else:
|
else:
|
||||||
ref_transform = False
|
ref_transforms = [False]
|
||||||
|
|
||||||
|
for ref_transform in ref_transforms:
|
||||||
self.dfs(
|
self.dfs(
|
||||||
pattern=self[target],
|
pattern=self[target],
|
||||||
visit_before=visit_before,
|
visit_before=visit_before,
|
||||||
@ -484,10 +504,147 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
raise LibraryError('visit_* functions returned a new `Pattern` object'
|
raise LibraryError('visit_* functions returned a new `Pattern` object'
|
||||||
' but no top-level name was provided in `hierarchy`')
|
' but no top-level name was provided in `hierarchy`')
|
||||||
|
|
||||||
cast(ILibrary, self)[name] = pattern
|
cast('ILibrary', self)[name] = pattern
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def child_graph(self) -> dict[str, set[str | None]]:
|
||||||
|
"""
|
||||||
|
Return a mapping from pattern name to a set of all child patterns
|
||||||
|
(patterns it references).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mapping from pattern name to a set of all pattern names it references.
|
||||||
|
"""
|
||||||
|
graph = {name: set(pat.refs.keys()) for name, pat in self.items()}
|
||||||
|
return graph
|
||||||
|
|
||||||
|
def parent_graph(self) -> dict[str, set[str]]:
|
||||||
|
"""
|
||||||
|
Return a mapping from pattern name to a set of all parent patterns
|
||||||
|
(patterns which reference it).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mapping from pattern name to a set of all patterns which reference it.
|
||||||
|
"""
|
||||||
|
igraph: dict[str, set[str]] = {name: set() for name in self}
|
||||||
|
for name, pat in self.items():
|
||||||
|
for child, reflist in pat.refs.items():
|
||||||
|
if reflist and child is not None:
|
||||||
|
igraph[child].add(name)
|
||||||
|
return igraph
|
||||||
|
|
||||||
|
def child_order(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Return a topologically sorted list of all contained pattern names.
|
||||||
|
Child (referenced) patterns will appear before their parents.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Topologically sorted list of pattern names.
|
||||||
|
"""
|
||||||
|
return cast('list[str]', list(TopologicalSorter(self.child_graph()).static_order()))
|
||||||
|
|
||||||
|
def find_refs_local(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
|
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||||
|
"""
|
||||||
|
Find the location and orientation of all refs pointing to `name`.
|
||||||
|
Refs with a `repetition` are resolved into multiple instances (locations).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name of the referenced pattern.
|
||||||
|
parent_graph: Mapping from pattern name to the set of patterns which
|
||||||
|
reference it. Default (`None`) calls `self.parent_graph()`.
|
||||||
|
The provided graph may be for a superset of `self` (i.e. it may
|
||||||
|
contain additional patterns which are not present in self; they
|
||||||
|
will be ignored).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mapping of {parent_name: transform_list}, where transform_list
|
||||||
|
is an Nx4 ndarray with rows
|
||||||
|
`(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||||
|
"""
|
||||||
|
instances = defaultdict(list)
|
||||||
|
if parent_graph is None:
|
||||||
|
parent_graph = self.parent_graph()
|
||||||
|
for parent in parent_graph[name]:
|
||||||
|
if parent not in self: # parent_graph may be a for a superset of self
|
||||||
|
continue
|
||||||
|
for ref in self[parent].refs[name]:
|
||||||
|
instances[parent].append(ref.as_transforms())
|
||||||
|
|
||||||
|
return instances
|
||||||
|
|
||||||
|
def find_refs_global(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
order: list[str] | None = None,
|
||||||
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
|
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
||||||
|
"""
|
||||||
|
Find the absolute (top-level) location and orientation of all refs (including
|
||||||
|
repetitions) pointing to `name`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name of the referenced pattern.
|
||||||
|
order: List of pattern names in which children are guaranteed
|
||||||
|
to appear before their parents (i.e. topologically sorted).
|
||||||
|
Default (`None`) calls `self.child_order()`.
|
||||||
|
parent_graph: Passed to `find_refs_local`.
|
||||||
|
Mapping from pattern name to the set of patterns which
|
||||||
|
reference it. Default (`None`) calls `self.parent_graph()`.
|
||||||
|
The provided graph may be for a superset of `self` (i.e. it may
|
||||||
|
contain additional patterns which are not present in self; they
|
||||||
|
will be ignored).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mapping of `{hierarchy: transform_list}`, where `hierarchy` is a tuple of the form
|
||||||
|
`(toplevel_pattern, lvl1_pattern, ..., name)` and `transform_list` is an Nx4 ndarray
|
||||||
|
with rows `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||||
|
"""
|
||||||
|
if name not in self:
|
||||||
|
return {}
|
||||||
|
if order is None:
|
||||||
|
order = self.child_order()
|
||||||
|
if parent_graph is None:
|
||||||
|
parent_graph = self.parent_graph()
|
||||||
|
|
||||||
|
self_keys = set(self.keys())
|
||||||
|
|
||||||
|
transforms: dict[str, list[tuple[
|
||||||
|
tuple[str, ...],
|
||||||
|
NDArray[numpy.float64]
|
||||||
|
]]]
|
||||||
|
transforms = defaultdict(list)
|
||||||
|
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph).items():
|
||||||
|
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
||||||
|
|
||||||
|
for next_name in order:
|
||||||
|
if next_name not in transforms:
|
||||||
|
continue
|
||||||
|
if not parent_graph[next_name] & self_keys:
|
||||||
|
continue
|
||||||
|
|
||||||
|
outers = self.find_refs_local(next_name, parent_graph=parent_graph)
|
||||||
|
inners = transforms.pop(next_name)
|
||||||
|
for parent, outer in outers.items():
|
||||||
|
for path, inner in inners:
|
||||||
|
combined = apply_transforms(numpy.concatenate(outer), inner)
|
||||||
|
transforms[parent].append((
|
||||||
|
(next_name,) + path,
|
||||||
|
combined,
|
||||||
|
))
|
||||||
|
result = {}
|
||||||
|
for parent, targets in transforms.items():
|
||||||
|
for path, instances in targets:
|
||||||
|
full_path = (parent,) + path
|
||||||
|
assert full_path not in result
|
||||||
|
result[full_path] = instances
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
@ -643,7 +800,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
duplicates = set(self.keys()) & set(other.keys())
|
duplicates = set(self.keys()) & set(other.keys())
|
||||||
|
|
||||||
if not duplicates:
|
if not duplicates:
|
||||||
for key in other.keys():
|
for key in other:
|
||||||
self._merge(key, other, key)
|
self._merge(key, other, key)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -670,11 +827,19 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
for old_name in temp:
|
for old_name in temp:
|
||||||
new_name = rename_map.get(old_name, old_name)
|
new_name = rename_map.get(old_name, old_name)
|
||||||
pat = self[new_name]
|
pat = self[new_name]
|
||||||
pat.refs = map_targets(pat.refs, lambda tt: cast(dict[str | None, str | None], rename_map).get(tt, tt))
|
pat.refs = map_targets(pat.refs, lambda tt: cast('dict[str | None, str | None]', rename_map).get(tt, tt))
|
||||||
|
|
||||||
return rename_map
|
return rename_map
|
||||||
|
|
||||||
def __lshift__(self, other: Mapping[str, 'Pattern']) -> str:
|
def __lshift__(self, other: TreeView) -> str:
|
||||||
|
"""
|
||||||
|
`add()` items from a tree (single-topcell name: pattern mapping) into this one,
|
||||||
|
and return the name of the tree's topcell (in this library; it may have changed
|
||||||
|
based on `add()`'s default `rename_theirs` argument).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
LibraryError if there is more than one topcell in `other`.
|
||||||
|
"""
|
||||||
if len(other) == 1:
|
if len(other) == 1:
|
||||||
name = next(iter(other))
|
name = next(iter(other))
|
||||||
else:
|
else:
|
||||||
@ -692,13 +857,20 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
return new_name
|
return new_name
|
||||||
|
|
||||||
def __le__(self, other: Mapping[str, 'Pattern']) -> Abstract:
|
def __le__(self, other: Mapping[str, 'Pattern']) -> Abstract:
|
||||||
|
"""
|
||||||
|
Perform the same operation as `__lshift__` / `<<`, but return an `Abstract` instead
|
||||||
|
of just the pattern's name.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
LibraryError if there is more than one topcell in `other`.
|
||||||
|
"""
|
||||||
new_name = self << other
|
new_name = self << other
|
||||||
return self.abstract(new_name)
|
return self.abstract(new_name)
|
||||||
|
|
||||||
def dedup(
|
def dedup(
|
||||||
self,
|
self,
|
||||||
norm_value: int = int(1e6),
|
norm_value: int = int(1e6),
|
||||||
exclude_types: tuple[Type] = (Polygon,),
|
exclude_types: tuple[type] = (Polygon,),
|
||||||
label2name: Callable[[tuple], str] | None = None,
|
label2name: Callable[[tuple], str] | None = None,
|
||||||
threshold: int = 2,
|
threshold: int = 2,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
@ -736,7 +908,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
exclude_types = ()
|
exclude_types = ()
|
||||||
|
|
||||||
if label2name is None:
|
if label2name is None:
|
||||||
def label2name(label):
|
def label2name(label: tuple) -> str: # noqa: ARG001
|
||||||
return self.get_name(SINGLE_USE_PREFIX + 'shape')
|
return self.get_name(SINGLE_USE_PREFIX + 'shape')
|
||||||
|
|
||||||
shape_counts: MutableMapping[tuple, int] = defaultdict(int)
|
shape_counts: MutableMapping[tuple, int] = defaultdict(int)
|
||||||
@ -772,8 +944,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
|
|
||||||
shape_table: dict[tuple, list] = defaultdict(list)
|
shape_table: dict[tuple, list] = defaultdict(list)
|
||||||
for layer, sseq in pat.shapes.items():
|
for layer, sseq in pat.shapes.items():
|
||||||
for i, shape in enumerate(sseq):
|
for ii, shape in enumerate(sseq):
|
||||||
if any(isinstance(shape, t) for t in exclude_types):
|
if any(isinstance(shape, tt) for tt in exclude_types):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
base_label, values, _func = shape.normalized_form(norm_value)
|
base_label, values, _func = shape.normalized_form(norm_value)
|
||||||
@ -782,16 +954,16 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
if label not in shape_pats:
|
if label not in shape_pats:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
shape_table[label].append((i, values))
|
shape_table[label].append((ii, values))
|
||||||
|
|
||||||
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
# 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
|
# 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.
|
# we should delete the `pat.shapes` entries for which we made `Ref`s.
|
||||||
shapes_to_remove = []
|
shapes_to_remove = []
|
||||||
for label in shape_table:
|
for label, shape_entries in shape_table.items():
|
||||||
layer = label[-1]
|
layer = label[-1]
|
||||||
target = label2name(label)
|
target = label2name(label)
|
||||||
for ii, values in shape_table[label]:
|
for ii, values in shape_entries:
|
||||||
offset, scale, rotation, mirror_x = values
|
offset, scale, rotation, mirror_x = values
|
||||||
pat.ref(target=target, offset=offset, scale=scale,
|
pat.ref(target=target, offset=offset, scale=scale,
|
||||||
rotation=rotation, mirrored=(mirror_x, False))
|
rotation=rotation, mirrored=(mirror_x, False))
|
||||||
@ -826,8 +998,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
from .pattern import Pattern
|
from .pattern import Pattern
|
||||||
|
|
||||||
if name_func is None:
|
if name_func is None:
|
||||||
def name_func(_pat, _shape):
|
def name_func(_pat: Pattern, _shape: Shape | Label) -> str:
|
||||||
return self.get_name(SINGLE_USE_PREFIX = 'rep')
|
return self.get_name(SINGLE_USE_PREFIX + 'rep')
|
||||||
|
|
||||||
for pat in tuple(self.values()):
|
for pat in tuple(self.values()):
|
||||||
for layer in pat.shapes:
|
for layer in pat.shapes:
|
||||||
@ -875,7 +1047,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
if isinstance(tops, str):
|
if isinstance(tops, str):
|
||||||
tops = (tops,)
|
tops = (tops,)
|
||||||
|
|
||||||
keep = cast(set[str], self.referenced_patterns(tops) - set((None,)))
|
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
||||||
keep |= set(tops)
|
keep |= set(tops)
|
||||||
|
|
||||||
new = type(self)()
|
new = type(self)()
|
||||||
@ -896,20 +1068,22 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
Returns:
|
Returns:
|
||||||
A set containing the names of all deleted patterns
|
A set containing the names of all deleted patterns
|
||||||
"""
|
"""
|
||||||
|
parent_graph = self.parent_graph()
|
||||||
|
empty = {name for name, pat in self.items() if pat.is_empty()}
|
||||||
trimmed = set()
|
trimmed = set()
|
||||||
while empty := set(name for name, pat in self.items() if pat.is_empty()):
|
while empty:
|
||||||
|
parents = set()
|
||||||
for name in empty:
|
for name in empty:
|
||||||
del self[name]
|
del self[name]
|
||||||
|
for parent in parent_graph[name]:
|
||||||
for pat in self.values():
|
del self[parent].refs[name]
|
||||||
for name in empty:
|
parents |= parent_graph[name]
|
||||||
# Second pass to skip looking at refs in empty patterns
|
|
||||||
if name in pat.refs:
|
|
||||||
del pat.refs[name]
|
|
||||||
|
|
||||||
trimmed |= empty
|
trimmed |= empty
|
||||||
if not repeat:
|
if not repeat:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
empty = {parent for parent in parents if self[parent].is_empty()}
|
||||||
return trimmed
|
return trimmed
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
@ -1001,10 +1175,7 @@ class Library(ILibrary):
|
|||||||
if key in self.mapping:
|
if key in self.mapping:
|
||||||
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!')
|
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!')
|
||||||
|
|
||||||
if callable(value):
|
value = value() if callable(value) else value
|
||||||
value = value()
|
|
||||||
else:
|
|
||||||
value = value
|
|
||||||
self.mapping[key] = value
|
self.mapping[key] = value
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
def __delitem__(self, key: str) -> None:
|
||||||
@ -1017,7 +1188,7 @@ class Library(ILibrary):
|
|||||||
return f'<Library ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>'
|
return f'<Library ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mktree(cls, name: str) -> tuple[Self, 'Pattern']:
|
def mktree(cls: type[Self], name: str) -> tuple[Self, 'Pattern']:
|
||||||
"""
|
"""
|
||||||
Create a new Library and immediately add a pattern
|
Create a new Library and immediately add a pattern
|
||||||
|
|
||||||
@ -1193,3 +1364,20 @@ class AbstractView(Mapping[str, Abstract]):
|
|||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self.library.__len__()
|
return self.library.__len__()
|
||||||
|
|
||||||
|
|
||||||
|
def b64suffix(ii: int) -> str:
|
||||||
|
"""
|
||||||
|
Turn an integer into a base64-equivalent suffix.
|
||||||
|
|
||||||
|
This could be done with base64.b64encode, but this way is faster for many small `ii`.
|
||||||
|
"""
|
||||||
|
def i2a(nn: int) -> str:
|
||||||
|
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$?'[nn]
|
||||||
|
|
||||||
|
parts = ['$', i2a(ii % 64)]
|
||||||
|
ii >>= 6
|
||||||
|
while ii:
|
||||||
|
parts.append(i2a(ii % 64))
|
||||||
|
ii >>= 6
|
||||||
|
return ''.join(parts)
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
Object representing a one multi-layer lithographic layout.
|
Object representing a one multi-layer lithographic layout.
|
||||||
A single level of hierarchical references is included.
|
A single level of hierarchical references is included.
|
||||||
"""
|
"""
|
||||||
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
|
from typing import cast, Self, Any, TypeVar
|
||||||
|
from collections.abc import Sequence, Mapping, MutableMapping, Iterable, Callable
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
import functools
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@ -17,7 +19,8 @@ from .ref import Ref
|
|||||||
from .abstract import Abstract
|
from .abstract import Abstract
|
||||||
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
|
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
|
||||||
from .label import Label
|
from .label import Label
|
||||||
from .utils import rotation_matrix_2d, annotations_t, layer_t
|
from .utils import rotation_matrix_2d, annotations_t, layer_t, annotations_eq, annotations_lt, layer2key
|
||||||
|
from .utils import ports_eq, ports_lt
|
||||||
from .error import PatternError, PortError
|
from .error import PatternError, PortError
|
||||||
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
|
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
|
||||||
from .ports import Port, PortList
|
from .ports import Port, PortList
|
||||||
@ -26,6 +29,7 @@ from .ports import Port, PortList
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
"""
|
"""
|
||||||
2D layout consisting of some set of shapes, labels, and references to other
|
2D layout consisting of some set of shapes, labels, and references to other
|
||||||
@ -87,7 +91,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
__slots__ = (
|
__slots__ = (
|
||||||
'shapes', 'labels', 'refs', '_ports',
|
'shapes', 'labels', 'refs', '_ports',
|
||||||
# inherited
|
# inherited
|
||||||
'_offset', '_annotations',
|
'_annotations',
|
||||||
)
|
)
|
||||||
|
|
||||||
shapes: defaultdict[layer_t, list[Shape]]
|
shapes: defaultdict[layer_t, list[Shape]]
|
||||||
@ -192,6 +196,146 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
# )
|
# )
|
||||||
# return new
|
# return new
|
||||||
|
|
||||||
|
def __lt__(self, other: 'Pattern') -> bool:
|
||||||
|
self_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||||
|
other_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||||
|
self_tgtkeys = tuple(sorted((target is None, target) for target in self_nonempty_targets))
|
||||||
|
other_tgtkeys = tuple(sorted((target is None, target) for target in other_nonempty_targets))
|
||||||
|
|
||||||
|
if self_tgtkeys != other_tgtkeys:
|
||||||
|
return self_tgtkeys < other_tgtkeys
|
||||||
|
|
||||||
|
for _, target in self_tgtkeys:
|
||||||
|
refs_ours = tuple(sorted(self.refs[target]))
|
||||||
|
refs_theirs = tuple(sorted(other.refs[target]))
|
||||||
|
if refs_ours != refs_theirs:
|
||||||
|
return refs_ours < refs_theirs
|
||||||
|
|
||||||
|
self_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||||
|
other_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||||
|
self_layerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_layers))
|
||||||
|
other_layerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_layers))
|
||||||
|
|
||||||
|
if self_layerkeys != other_layerkeys:
|
||||||
|
return self_layerkeys < other_layerkeys
|
||||||
|
|
||||||
|
for _, _, layer in self_layerkeys:
|
||||||
|
shapes_ours = tuple(sorted(self.shapes[layer]))
|
||||||
|
shapes_theirs = tuple(sorted(self.shapes[layer]))
|
||||||
|
if shapes_ours != shapes_theirs:
|
||||||
|
return shapes_ours < shapes_theirs
|
||||||
|
|
||||||
|
self_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||||
|
other_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||||
|
self_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_txtlayers))
|
||||||
|
other_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_txtlayers))
|
||||||
|
|
||||||
|
if self_txtlayerkeys != other_txtlayerkeys:
|
||||||
|
return self_txtlayerkeys < other_txtlayerkeys
|
||||||
|
|
||||||
|
for _, _, layer in self_layerkeys:
|
||||||
|
labels_ours = tuple(sorted(self.labels[layer]))
|
||||||
|
labels_theirs = tuple(sorted(self.labels[layer]))
|
||||||
|
if labels_ours != labels_theirs:
|
||||||
|
return labels_ours < labels_theirs
|
||||||
|
|
||||||
|
if not annotations_eq(self.annotations, other.annotations):
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
|
if not ports_eq(self.ports, other.ports):
|
||||||
|
return ports_lt(self.ports, other.ports)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||||
|
other_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||||
|
self_tgtkeys = tuple(sorted((target is None, target) for target in self_nonempty_targets))
|
||||||
|
other_tgtkeys = tuple(sorted((target is None, target) for target in other_nonempty_targets))
|
||||||
|
|
||||||
|
if self_tgtkeys != other_tgtkeys:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for _, target in self_tgtkeys:
|
||||||
|
refs_ours = tuple(sorted(self.refs[target]))
|
||||||
|
refs_theirs = tuple(sorted(other.refs[target]))
|
||||||
|
if refs_ours != refs_theirs:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||||
|
other_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||||
|
self_layerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_layers))
|
||||||
|
other_layerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_layers))
|
||||||
|
|
||||||
|
if self_layerkeys != other_layerkeys:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for _, _, layer in self_layerkeys:
|
||||||
|
shapes_ours = tuple(sorted(self.shapes[layer]))
|
||||||
|
shapes_theirs = tuple(sorted(self.shapes[layer]))
|
||||||
|
if shapes_ours != shapes_theirs:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||||
|
other_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||||
|
self_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_txtlayers))
|
||||||
|
other_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_txtlayers))
|
||||||
|
|
||||||
|
if self_txtlayerkeys != other_txtlayerkeys:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for _, _, layer in self_layerkeys:
|
||||||
|
labels_ours = tuple(sorted(self.labels[layer]))
|
||||||
|
labels_theirs = tuple(sorted(self.labels[layer]))
|
||||||
|
if labels_ours != labels_theirs:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not annotations_eq(self.annotations, other.annotations):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not ports_eq(self.ports, other.ports): # noqa: SIM103
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def sort(self, sort_elements: bool = True) -> Self:
|
||||||
|
"""
|
||||||
|
Sort the element dicts (shapes, labels, refs) and (optionally) their contained lists.
|
||||||
|
This is primarily useful for making builds more reproducible.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sort_elements: Whether to sort all the shapes/labels/refs within each layer/target.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if sort_elements:
|
||||||
|
def maybe_sort(xx): # noqa:ANN001,ANN202
|
||||||
|
return sorted(xx)
|
||||||
|
else:
|
||||||
|
def maybe_sort(xx): # noqa:ANN001,ANN202
|
||||||
|
return xx
|
||||||
|
|
||||||
|
self.refs = defaultdict(list, sorted(
|
||||||
|
(tgt, maybe_sort(rrs)) for tgt, rrs in self.refs.items()
|
||||||
|
))
|
||||||
|
self.labels = defaultdict(list, sorted(
|
||||||
|
((layer, maybe_sort(lls)) for layer, lls in self.labels.items()),
|
||||||
|
key=lambda tt: layer2key(tt[0]),
|
||||||
|
))
|
||||||
|
self.shapes = defaultdict(list, sorted(
|
||||||
|
((layer, maybe_sort(sss)) for layer, sss in self.shapes.items()),
|
||||||
|
key=lambda tt: layer2key(tt[0]),
|
||||||
|
))
|
||||||
|
|
||||||
|
self.ports = dict(sorted(self.ports.items()))
|
||||||
|
self.annotations = dict(sorted(self.annotations.items()))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def append(self, other_pattern: 'Pattern') -> Self:
|
def append(self, other_pattern: 'Pattern') -> Self:
|
||||||
"""
|
"""
|
||||||
Appends all shapes, labels and refs from other_pattern to self's shapes,
|
Appends all shapes, labels and refs from other_pattern to self's shapes,
|
||||||
@ -328,10 +472,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
|
|
||||||
self.polygonize()
|
self.polygonize()
|
||||||
for layer in self.shapes:
|
for layer in self.shapes:
|
||||||
self.shapes[layer] = list(chain.from_iterable((
|
self.shapes[layer] = list(chain.from_iterable(
|
||||||
ss.manhattanize(grid_x, grid_y)
|
ss.manhattanize(grid_x, grid_y)
|
||||||
for ss in self.shapes[layer]
|
for ss in self.shapes[layer]
|
||||||
)))
|
))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]:
|
def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]:
|
||||||
@ -347,7 +491,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
"""
|
"""
|
||||||
pat = self.deepcopy().polygonize().flatten(library=library)
|
pat = self.deepcopy().polygonize().flatten(library=library)
|
||||||
polys = [
|
polys = [
|
||||||
cast(Polygon, shape).vertices + cast(Polygon, shape).offset
|
cast('Polygon', shape).vertices + cast('Polygon', shape).offset
|
||||||
for shape in chain_elements(pat.shapes)
|
for shape in chain_elements(pat.shapes)
|
||||||
]
|
]
|
||||||
return polys
|
return polys
|
||||||
@ -389,7 +533,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
n_elems = sum(1 for _ in chain_elements(self.shapes, self.labels))
|
n_elems = sum(1 for _ in chain_elements(self.shapes, self.labels))
|
||||||
ebounds = numpy.full((n_elems, 2, 2), nan)
|
ebounds = numpy.full((n_elems, 2, 2), nan)
|
||||||
for ee, entry in enumerate(chain_elements(self.shapes, self.labels)):
|
for ee, entry in enumerate(chain_elements(self.shapes, self.labels)):
|
||||||
maybe_ebounds = cast(Bounded, entry).get_bounds()
|
maybe_ebounds = cast('Bounded', entry).get_bounds()
|
||||||
if maybe_ebounds is not None:
|
if maybe_ebounds is not None:
|
||||||
ebounds[ee] = maybe_ebounds
|
ebounds[ee] = maybe_ebounds
|
||||||
mask = ~numpy.isnan(ebounds[:, 0, 0])
|
mask = ~numpy.isnan(ebounds[:, 0, 0])
|
||||||
@ -436,6 +580,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T
|
corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T
|
||||||
bounds = numpy.vstack((numpy.min(corners, axis=0),
|
bounds = numpy.vstack((numpy.min(corners, axis=0),
|
||||||
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
|
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
|
||||||
|
if ref.repetition is not None:
|
||||||
|
bounds += ref.repetition.get_bounds()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
|
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
|
||||||
@ -449,7 +595,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
|
|
||||||
if (cbounds[1] < cbounds[0]).any():
|
if (cbounds[1] < cbounds[0]).any():
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
return cbounds
|
return cbounds
|
||||||
|
|
||||||
def get_bounds_nonempty(
|
def get_bounds_nonempty(
|
||||||
@ -471,7 +616,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
Returns:
|
Returns:
|
||||||
`[[x_min, y_min], [x_max, y_max]]`
|
`[[x_min, y_min], [x_max, y_max]]`
|
||||||
"""
|
"""
|
||||||
bounds = self.get_bounds(library)
|
bounds = self.get_bounds(library, recurse=recurse)
|
||||||
assert bounds is not None
|
assert bounds is not None
|
||||||
return bounds
|
return bounds
|
||||||
|
|
||||||
@ -486,7 +631,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
|
||||||
cast(Positionable, entry).translate(offset)
|
cast('Positionable', entry).translate(offset)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def scale_elements(self, c: float) -> Self:
|
def scale_elements(self, c: float) -> Self:
|
||||||
@ -500,33 +645,37 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain_elements(self.shapes, self.refs):
|
for entry in chain_elements(self.shapes, self.refs):
|
||||||
cast(Scalable, entry).scale_by(c)
|
cast('Scalable', entry).scale_by(c)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def scale_by(self, c: float) -> Self:
|
def scale_by(self, c: float, scale_refs: bool = True) -> Self:
|
||||||
"""
|
"""
|
||||||
Scale this Pattern by the given value
|
Scale this Pattern by the given value
|
||||||
(all shapes and refs and their offsets are scaled,
|
All shapes and (optionally) refs and their offsets are scaled,
|
||||||
as are all label and port offsets)
|
as are all label and port offsets.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
c: factor to scale by
|
c: factor to scale by
|
||||||
|
scale_refs: Whether to scale refs. Ref offsets are always scaled,
|
||||||
|
but it may be desirable to not scale the ref itself (e.g. if
|
||||||
|
the target cell was also scaled).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain_elements(self.shapes, self.refs):
|
for entry in chain_elements(self.shapes, self.refs):
|
||||||
cast(Positionable, entry).offset *= c
|
cast('Positionable', entry).offset *= c
|
||||||
cast(Scalable, entry).scale_by(c)
|
if scale_refs or not isinstance(entry, Ref):
|
||||||
|
cast('Scalable', entry).scale_by(c)
|
||||||
|
|
||||||
rep = cast(Repeatable, entry).repetition
|
rep = cast('Repeatable', entry).repetition
|
||||||
if rep:
|
if rep:
|
||||||
rep.scale_by(c)
|
rep.scale_by(c)
|
||||||
|
|
||||||
for label in chain_elements(self.labels):
|
for label in chain_elements(self.labels):
|
||||||
cast(Positionable, label).offset *= c
|
cast('Positionable', label).offset *= c
|
||||||
|
|
||||||
rep = cast(Repeatable, label).repetition
|
rep = cast('Repeatable', label).repetition
|
||||||
if rep:
|
if rep:
|
||||||
rep.scale_by(c)
|
rep.scale_by(c)
|
||||||
|
|
||||||
@ -545,7 +694,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
pivot = numpy.array(pivot)
|
pivot = numpy.asarray(pivot, dtype=float)
|
||||||
self.translate_elements(-pivot)
|
self.translate_elements(-pivot)
|
||||||
self.rotate_elements(rotation)
|
self.rotate_elements(rotation)
|
||||||
self.rotate_element_centers(rotation)
|
self.rotate_element_centers(rotation)
|
||||||
@ -563,8 +712,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||||
old_offset = cast(Positionable, entry).offset
|
old_offset = cast('Positionable', entry).offset
|
||||||
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
cast('Positionable', entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rotate_elements(self, rotation: float) -> Self:
|
def rotate_elements(self, rotation: float) -> Self:
|
||||||
@ -578,7 +727,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||||
cast(Rotatable, entry).rotate(rotation)
|
cast('Rotatable', entry).rotate(rotation)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror_element_centers(self, across_axis: int = 0) -> Self:
|
def mirror_element_centers(self, across_axis: int = 0) -> Self:
|
||||||
@ -593,7 +742,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||||
cast(Positionable, entry).offset[across_axis - 1] *= -1
|
cast('Positionable', entry).offset[across_axis - 1] *= -1
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror_elements(self, across_axis: int = 0) -> Self:
|
def mirror_elements(self, across_axis: int = 0) -> Self:
|
||||||
@ -609,7 +758,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||||
cast(Mirrorable, entry).mirror(across_axis)
|
cast('Mirrorable', entry).mirror(across_axis)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror(self, across_axis: int = 0) -> Self:
|
def mirror(self, across_axis: int = 0) -> Self:
|
||||||
@ -808,7 +957,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
flattened: dict[str | None, 'Pattern | None'] = {}
|
flattened: dict[str | None, Pattern | None] = {}
|
||||||
|
|
||||||
def flatten_single(name: str | None) -> None:
|
def flatten_single(name: str | None) -> None:
|
||||||
if name is None:
|
if name is None:
|
||||||
@ -870,15 +1019,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
try:
|
try:
|
||||||
from matplotlib import pyplot # type: ignore
|
from matplotlib import pyplot # type: ignore
|
||||||
import matplotlib.collections # type: ignore
|
import matplotlib.collections # type: ignore
|
||||||
except ImportError as err:
|
except ImportError:
|
||||||
logger.error('Pattern.visualize() depends on matplotlib!')
|
logger.exception('Pattern.visualize() depends on matplotlib!\n'
|
||||||
logger.error('Make sure to install masque with the [visualize] option to pull in the needed dependencies.')
|
+ 'Make sure to install masque with the [visualize] option to pull in the needed dependencies.')
|
||||||
raise err
|
raise
|
||||||
|
|
||||||
if self.has_refs() and library is None:
|
if self.has_refs() and library is None:
|
||||||
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
||||||
|
|
||||||
offset = numpy.array(offset, dtype=float)
|
offset = numpy.asarray(offset, dtype=float)
|
||||||
|
|
||||||
if not overdraw:
|
if not overdraw:
|
||||||
figure = pyplot.figure()
|
figure = pyplot.figure()
|
||||||
@ -1080,6 +1229,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
inherit_name: bool = True,
|
inherit_name: bool = True,
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
append: bool = False,
|
append: bool = False,
|
||||||
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Instantiate or append a pattern into the current pattern, connecting
|
Instantiate or append a pattern into the current pattern, connecting
|
||||||
@ -1087,7 +1237,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
ports specified by `map_out`.
|
ports specified by `map_out`.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
=========
|
======list, ===
|
||||||
- `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
- `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||||
instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B'
|
instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B'
|
||||||
of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports
|
of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||||
@ -1125,6 +1275,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
append: If `True`, `other` is appended instead of being referenced.
|
append: If `True`, `other` is appended instead of being referenced.
|
||||||
Note that this does not flatten `other`, so its refs will still
|
Note that this does not flatten `other`, so its refs will still
|
||||||
be refs (now inside `self`).
|
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:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -1155,6 +1310,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
map_in,
|
map_in,
|
||||||
mirrored=mirrored,
|
mirrored=mirrored,
|
||||||
set_rotation=set_rotation,
|
set_rotation=set_rotation,
|
||||||
|
ok_connections=ok_connections,
|
||||||
)
|
)
|
||||||
|
|
||||||
# get rid of plugged ports
|
# get rid of plugged ports
|
||||||
@ -1163,7 +1319,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
map_out[vi] = None
|
map_out[vi] = None
|
||||||
|
|
||||||
if isinstance(other, Pattern):
|
if isinstance(other, Pattern):
|
||||||
assert append
|
assert append, 'Got a name (not an abstract) but was asked to reference (not append)'
|
||||||
|
|
||||||
self.place(
|
self.place(
|
||||||
other,
|
other,
|
||||||
@ -1179,7 +1335,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def interface(
|
def interface(
|
||||||
cls,
|
cls: type['Pattern'],
|
||||||
source: PortList | Mapping[str, Port],
|
source: PortList | Mapping[str, Port],
|
||||||
*,
|
*,
|
||||||
in_prefix: str = 'in_',
|
in_prefix: str = 'in_',
|
||||||
|
151
masque/ports.py
151
masque/ports.py
@ -1,9 +1,12 @@
|
|||||||
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn
|
from typing import overload, Self, NoReturn, Any
|
||||||
|
from collections.abc import Iterable, KeysView, ValuesView, Mapping
|
||||||
import warnings
|
import warnings
|
||||||
import traceback
|
import traceback
|
||||||
import logging
|
import logging
|
||||||
|
import functools
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
@ -17,6 +20,7 @@ from .error import PortError
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||||
"""
|
"""
|
||||||
A point at which a `Device` can be snapped to another `Device`.
|
A point at which a `Device` can be snapped to another `Device`.
|
||||||
@ -68,7 +72,28 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
|||||||
raise PortError('Rotation must be a scalar')
|
raise PortError('Rotation must be a scalar')
|
||||||
self._rotation = val % (2 * pi)
|
self._rotation = val % (2 * pi)
|
||||||
|
|
||||||
def get_bounds(self):
|
@property
|
||||||
|
def x(self) -> float:
|
||||||
|
""" Alias for offset[0] """
|
||||||
|
return self.offset[0]
|
||||||
|
|
||||||
|
@x.setter
|
||||||
|
def x(self, val: float) -> None:
|
||||||
|
self.offset[0] = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self) -> float:
|
||||||
|
""" Alias for offset[1] """
|
||||||
|
return self.offset[1]
|
||||||
|
|
||||||
|
@y.setter
|
||||||
|
def y(self, val: float) -> None:
|
||||||
|
self.offset[1] = val
|
||||||
|
|
||||||
|
def copy(self) -> Self:
|
||||||
|
return self.deepcopy()
|
||||||
|
|
||||||
|
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||||
return numpy.vstack((self.offset, self.offset))
|
return numpy.vstack((self.offset, self.offset))
|
||||||
|
|
||||||
def set_ptype(self, ptype: str) -> Self:
|
def set_ptype(self, ptype: str) -> Self:
|
||||||
@ -99,6 +124,27 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
|||||||
rot = str(numpy.rad2deg(self.rotation))
|
rot = str(numpy.rad2deg(self.rotation))
|
||||||
return f'<{self.offset}, {rot}, [{self.ptype}]>'
|
return f'<{self.offset}, {rot}, [{self.ptype}]>'
|
||||||
|
|
||||||
|
def __lt__(self, other: 'Port') -> bool:
|
||||||
|
if self.ptype != other.ptype:
|
||||||
|
return self.ptype < other.ptype
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.rotation != other.rotation:
|
||||||
|
if self.rotation is None:
|
||||||
|
return True
|
||||||
|
if other.rotation is None:
|
||||||
|
return False
|
||||||
|
return self.rotation < other.rotation
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and self.ptype == other.ptype
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and self.rotation == other.rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PortList(metaclass=ABCMeta):
|
class PortList(metaclass=ABCMeta):
|
||||||
__slots__ = () # Allow subclasses to use __slots__
|
__slots__ = () # Allow subclasses to use __slots__
|
||||||
@ -135,7 +181,7 @@ class PortList(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
return self.ports[key]
|
return self.ports[key]
|
||||||
else:
|
else: # noqa: RET505
|
||||||
return {k: self.ports[k] for k in key}
|
return {k: self.ports[k] for k in key}
|
||||||
|
|
||||||
def __contains__(self, key: str) -> NoReturn:
|
def __contains__(self, key: str) -> NoReturn:
|
||||||
@ -193,7 +239,7 @@ class PortList(metaclass=ABCMeta):
|
|||||||
if duplicates:
|
if duplicates:
|
||||||
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
||||||
|
|
||||||
renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()}
|
renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()}
|
||||||
if None in renamed:
|
if None in renamed:
|
||||||
del renamed[None]
|
del renamed[None]
|
||||||
|
|
||||||
@ -228,6 +274,75 @@ class PortList(metaclass=ABCMeta):
|
|||||||
self.ports.update(new_ports)
|
self.ports.update(new_ports)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def plugged(
|
||||||
|
self,
|
||||||
|
connections: dict[str, str],
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
Verify that the ports specified by `connections` are coincident and have opposing
|
||||||
|
rotations, then remove the ports.
|
||||||
|
|
||||||
|
This is used when ports have been "manually" aligned as part of some other routing,
|
||||||
|
but for whatever reason were not eliminated via `plug()`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
connections: Pairs of ports which "plug" each other (same offset, opposing directions)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`PortError` if the ports are not properly aligned.
|
||||||
|
"""
|
||||||
|
a_names, b_names = list(zip(*connections.items(), strict=True))
|
||||||
|
a_ports = [self.ports[pp] for pp in a_names]
|
||||||
|
b_ports = [self.ports[pp] for pp in b_names]
|
||||||
|
|
||||||
|
a_types = [pp.ptype for pp in a_ports]
|
||||||
|
b_types = [pp.ptype for pp in b_ports]
|
||||||
|
type_conflicts = numpy.array([at != bt and 'unk' not in (at, bt)
|
||||||
|
for at, bt in zip(a_types, b_types, strict=True)])
|
||||||
|
|
||||||
|
if type_conflicts.any():
|
||||||
|
msg = 'Ports have conflicting types:\n'
|
||||||
|
for nn, (k, v) in enumerate(connections.items()):
|
||||||
|
if type_conflicts[nn]:
|
||||||
|
msg += f'{k} | {a_types[nn]}:{b_types[nn]} | {v}\n'
|
||||||
|
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||||
|
warnings.warn(msg, stacklevel=2)
|
||||||
|
|
||||||
|
a_offsets = numpy.array([pp.offset for pp in a_ports])
|
||||||
|
b_offsets = numpy.array([pp.offset for pp in b_ports])
|
||||||
|
a_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in a_ports])
|
||||||
|
b_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in b_ports])
|
||||||
|
a_has_rot = numpy.array([pp.rotation is not None for pp in a_ports], dtype=bool)
|
||||||
|
b_has_rot = numpy.array([pp.rotation is not None for pp in b_ports], dtype=bool)
|
||||||
|
has_rot = a_has_rot & b_has_rot
|
||||||
|
|
||||||
|
if has_rot.any():
|
||||||
|
rotations = numpy.mod(a_rotations - b_rotations - pi, 2 * pi)
|
||||||
|
rotations[~has_rot] = rotations[has_rot][0]
|
||||||
|
|
||||||
|
if not numpy.allclose(rotations, 0):
|
||||||
|
rot_deg = numpy.rad2deg(rotations)
|
||||||
|
msg = 'Port orientations do not match:\n'
|
||||||
|
for nn, (k, v) in enumerate(connections.items()):
|
||||||
|
if not numpy.isclose(rot_deg[nn], 0):
|
||||||
|
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||||
|
raise PortError(msg)
|
||||||
|
|
||||||
|
translations = a_offsets - b_offsets
|
||||||
|
if not numpy.allclose(translations, 0):
|
||||||
|
msg = 'Port translations do not match:\n'
|
||||||
|
for nn, (k, v) in enumerate(connections.items()):
|
||||||
|
if not numpy.allclose(translations[nn], 0):
|
||||||
|
msg += f'{k} | {translations[nn]} | {v}\n'
|
||||||
|
raise PortError(msg)
|
||||||
|
|
||||||
|
for pp in chain(a_names, b_names):
|
||||||
|
del self.ports[pp]
|
||||||
|
return self
|
||||||
|
|
||||||
def check_ports(
|
def check_ports(
|
||||||
self,
|
self,
|
||||||
other_names: Iterable[str],
|
other_names: Iterable[str],
|
||||||
@ -304,6 +419,7 @@ class PortList(metaclass=ABCMeta):
|
|||||||
*,
|
*,
|
||||||
mirrored: bool = False,
|
mirrored: bool = False,
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||||
"""
|
"""
|
||||||
Given a device `other` and a mapping `map_in` specifying port connections,
|
Given a device `other` and a mapping `map_in` specifying port connections,
|
||||||
@ -320,6 +436,11 @@ class PortList(metaclass=ABCMeta):
|
|||||||
port with `rotation=None`), `set_rotation` must be provided
|
port with `rotation=None`), `set_rotation` must be provided
|
||||||
to indicate how much `other` should be rotated. Otherwise,
|
to indicate how much `other` should be rotated. Otherwise,
|
||||||
`set_rotation` must remain `None`.
|
`set_rotation` must remain `None`.
|
||||||
|
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:
|
Returns:
|
||||||
- The (x, y) translation (performed last)
|
- The (x, y) translation (performed last)
|
||||||
@ -336,6 +457,7 @@ class PortList(metaclass=ABCMeta):
|
|||||||
map_in=map_in,
|
map_in=map_in,
|
||||||
mirrored=mirrored,
|
mirrored=mirrored,
|
||||||
set_rotation=set_rotation,
|
set_rotation=set_rotation,
|
||||||
|
ok_connections=ok_connections,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -346,13 +468,14 @@ class PortList(metaclass=ABCMeta):
|
|||||||
*,
|
*,
|
||||||
mirrored: bool = False,
|
mirrored: bool = False,
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||||
"""
|
"""
|
||||||
Given two sets of ports (s_ports and o_ports) and a mapping `map_in`
|
Given two sets of ports (s_ports and o_ports) and a mapping `map_in`
|
||||||
specifying port connections, find the transform which will correctly
|
specifying port connections, find the transform which will correctly
|
||||||
align the specified o_ports onto their respective s_ports.
|
align the specified o_ports onto their respective s_ports.
|
||||||
|
|
||||||
Args:t
|
Args:
|
||||||
s_ports: A list of stationary ports
|
s_ports: A list of stationary ports
|
||||||
o_ports: A list of ports which are to be moved/mirrored.
|
o_ports: A list of ports which are to be moved/mirrored.
|
||||||
map_in: dict of `{'s_port': 'o_port'}` mappings, specifying
|
map_in: dict of `{'s_port': 'o_port'}` mappings, specifying
|
||||||
@ -364,6 +487,11 @@ class PortList(metaclass=ABCMeta):
|
|||||||
port with `rotation=None`), `set_rotation` must be provided
|
port with `rotation=None`), `set_rotation` must be provided
|
||||||
to indicate how much `o_ports` should be rotated. Otherwise,
|
to indicate how much `o_ports` should be rotated. Otherwise,
|
||||||
`set_rotation` must remain `None`.
|
`set_rotation` must remain `None`.
|
||||||
|
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:
|
Returns:
|
||||||
- The (x, y) translation (performed last)
|
- The (x, y) translation (performed last)
|
||||||
@ -387,8 +515,9 @@ class PortList(metaclass=ABCMeta):
|
|||||||
o_offsets[:, 1] *= -1
|
o_offsets[:, 1] *= -1
|
||||||
o_rotations *= -1
|
o_rotations *= -1
|
||||||
|
|
||||||
type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
|
ok_pairs = {tuple(sorted(pair)) for pair in ok_connections if pair[0] != pair[1]}
|
||||||
for st, ot in zip(s_types, o_types)])
|
type_conflicts = numpy.array([(st != ot) and ('unk' not in (st, ot)) and (tuple(sorted((st, ot))) not in ok_pairs)
|
||||||
|
for st, ot in zip(s_types, o_types, strict=True)])
|
||||||
if type_conflicts.any():
|
if type_conflicts.any():
|
||||||
msg = 'Ports have conflicting types:\n'
|
msg = 'Ports have conflicting types:\n'
|
||||||
for nn, (k, v) in enumerate(map_in.items()):
|
for nn, (k, v) in enumerate(map_in.items()):
|
||||||
@ -408,8 +537,8 @@ class PortList(metaclass=ABCMeta):
|
|||||||
if not numpy.allclose(rotations[:1], rotations):
|
if not numpy.allclose(rotations[:1], rotations):
|
||||||
rot_deg = numpy.rad2deg(rotations)
|
rot_deg = numpy.rad2deg(rotations)
|
||||||
msg = 'Port orientations do not match:\n'
|
msg = 'Port orientations do not match:\n'
|
||||||
for nn, (k, v) in enumerate(map_in.items()):
|
for nn, (kk, vv) in enumerate(map_in.items()):
|
||||||
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
msg += f'{kk} | {rot_deg[nn]:g} | {vv}\n'
|
||||||
raise PortError(msg)
|
raise PortError(msg)
|
||||||
|
|
||||||
pivot = o_offsets[0].copy()
|
pivot = o_offsets[0].copy()
|
||||||
@ -417,8 +546,8 @@ class PortList(metaclass=ABCMeta):
|
|||||||
translations = s_offsets - o_offsets
|
translations = s_offsets - o_offsets
|
||||||
if not numpy.allclose(translations[:1], translations):
|
if not numpy.allclose(translations[:1], translations):
|
||||||
msg = 'Port translations do not match:\n'
|
msg = 'Port translations do not match:\n'
|
||||||
for nn, (k, v) in enumerate(map_in.items()):
|
for nn, (kk, vv) in enumerate(map_in.items()):
|
||||||
msg += f'{k} | {translations[nn]} | {v}\n'
|
msg += f'{kk} | {translations[nn]} | {vv}\n'
|
||||||
raise PortError(msg)
|
raise PortError(msg)
|
||||||
|
|
||||||
return translations[0], rotations[0], o_offsets[0]
|
return translations[0], rotations[0], o_offsets[0]
|
||||||
|
@ -2,14 +2,16 @@
|
|||||||
Ref provides basic support for nesting Pattern objects within each other.
|
Ref provides basic support for nesting Pattern objects within each other.
|
||||||
It carries offset, rotation, mirroring, and scaling data for each individual instance.
|
It carries offset, rotation, mirroring, and scaling data for each individual instance.
|
||||||
"""
|
"""
|
||||||
from typing import Mapping, TYPE_CHECKING, Self
|
from typing import TYPE_CHECKING, Self, Any
|
||||||
|
from collections.abc import Mapping
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
|
|
||||||
from .utils import annotations_t, rotation_matrix_2d
|
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key
|
||||||
from .repetition import Repetition
|
from .repetition import Repetition
|
||||||
from .traits import (
|
from .traits import (
|
||||||
PositionableImpl, RotatableImpl, ScalableImpl,
|
PositionableImpl, RotatableImpl, ScalableImpl,
|
||||||
@ -21,6 +23,7 @@ if TYPE_CHECKING:
|
|||||||
from . import Pattern
|
from . import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Ref(
|
class Ref(
|
||||||
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
||||||
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
||||||
@ -99,6 +102,29 @@ class Ref(
|
|||||||
#new.annotations = copy.deepcopy(self.annotations, memo)
|
#new.annotations = copy.deepcopy(self.annotations, memo)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __lt__(self, other: 'Ref') -> bool:
|
||||||
|
if (self.offset != other.offset).any():
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.mirrored != other.mirrored:
|
||||||
|
return self.mirrored < other.mirrored
|
||||||
|
if self.rotation != other.rotation:
|
||||||
|
return self.rotation < other.rotation
|
||||||
|
if self.scale != other.scale:
|
||||||
|
return self.scale < other.scale
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
numpy.array_equal(self.offset, other.offset)
|
||||||
|
and self.mirrored == other.mirrored
|
||||||
|
and self.rotation == other.rotation
|
||||||
|
and self.scale == other.scale
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
def as_pattern(
|
def as_pattern(
|
||||||
self,
|
self,
|
||||||
pattern: 'Pattern',
|
pattern: 'Pattern',
|
||||||
@ -157,6 +183,16 @@ class Ref(
|
|||||||
self.rotation += pi
|
self.rotation += pi
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def as_transforms(self) -> NDArray[numpy.float64]:
|
||||||
|
xys = self.offset[None, :]
|
||||||
|
if self.repetition is not None:
|
||||||
|
xys = xys + self.repetition.displacements
|
||||||
|
transforms = numpy.empty((xys.shape[0], 4))
|
||||||
|
transforms[:, :2] = xys
|
||||||
|
transforms[:, 2] = self.rotation
|
||||||
|
transforms[:, 3] = self.mirrored
|
||||||
|
return transforms
|
||||||
|
|
||||||
def get_bounds_single(
|
def get_bounds_single(
|
||||||
self,
|
self,
|
||||||
pattern: 'Pattern',
|
pattern: 'Pattern',
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
Repetitions provide support for efficiently representing multiple identical
|
Repetitions provide support for efficiently representing multiple identical
|
||||||
instances of an object .
|
instances of an object .
|
||||||
"""
|
"""
|
||||||
from typing import Any, Type, Self, TypeVar
|
from typing import Any, Self, TypeVar, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -17,6 +18,7 @@ from .utils import rotation_matrix_2d
|
|||||||
GG = TypeVar('GG', bound='Grid')
|
GG = TypeVar('GG', bound='Grid')
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta):
|
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
Interface common to all objects which specify repetitions
|
Interface common to all objects which specify repetitions
|
||||||
@ -31,6 +33,14 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=A
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __le__(self, other: 'Repetition') -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Grid(Repetition):
|
class Grid(Repetition):
|
||||||
"""
|
"""
|
||||||
@ -91,7 +101,6 @@ class Grid(Repetition):
|
|||||||
if b_vector is None:
|
if b_vector is None:
|
||||||
if b_count > 1:
|
if b_count > 1:
|
||||||
raise PatternError('Repetition has b_count > 1 but no b_vector')
|
raise PatternError('Repetition has b_count > 1 but no b_vector')
|
||||||
else:
|
|
||||||
b_vector = numpy.array([0.0, 0.0])
|
b_vector = numpy.array([0.0, 0.0])
|
||||||
|
|
||||||
if a_count < 1:
|
if a_count < 1:
|
||||||
@ -106,7 +115,7 @@ class Grid(Repetition):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aligned(
|
def aligned(
|
||||||
cls: Type[GG],
|
cls: type[GG],
|
||||||
x: float,
|
x: float,
|
||||||
y: float,
|
y: float,
|
||||||
x_count: int,
|
x_count: int,
|
||||||
@ -147,12 +156,11 @@ class Grid(Repetition):
|
|||||||
|
|
||||||
@a_vector.setter
|
@a_vector.setter
|
||||||
def a_vector(self, val: ArrayLike) -> None:
|
def a_vector(self, val: ArrayLike) -> None:
|
||||||
if not isinstance(val, numpy.ndarray):
|
|
||||||
val = numpy.array(val, dtype=float)
|
val = numpy.array(val, dtype=float)
|
||||||
|
|
||||||
if val.size != 2:
|
if val.size != 2:
|
||||||
raise PatternError('a_vector must be convertible to size-2 ndarray')
|
raise PatternError('a_vector must be convertible to size-2 ndarray')
|
||||||
self._a_vector = val.flatten().astype(float)
|
self._a_vector = val.flatten()
|
||||||
|
|
||||||
# b_vector property
|
# b_vector property
|
||||||
@property
|
@property
|
||||||
@ -161,8 +169,7 @@ class Grid(Repetition):
|
|||||||
|
|
||||||
@b_vector.setter
|
@b_vector.setter
|
||||||
def b_vector(self, val: ArrayLike) -> None:
|
def b_vector(self, val: ArrayLike) -> None:
|
||||||
if not isinstance(val, numpy.ndarray):
|
val = numpy.array(val, dtype=float)
|
||||||
val = numpy.array(val, dtype=float, copy=True)
|
|
||||||
|
|
||||||
if val.size != 2:
|
if val.size != 2:
|
||||||
raise PatternError('b_vector must be convertible to size-2 ndarray')
|
raise PatternError('b_vector must be convertible to size-2 ndarray')
|
||||||
@ -270,7 +277,7 @@ class Grid(Repetition):
|
|||||||
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>')
|
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>')
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
if not isinstance(other, type(self)):
|
if type(other) is not type(self):
|
||||||
return False
|
return False
|
||||||
if self.a_count != other.a_count or self.b_count != other.b_count:
|
if self.a_count != other.a_count or self.b_count != other.b_count:
|
||||||
return False
|
return False
|
||||||
@ -280,10 +287,28 @@ class Grid(Repetition):
|
|||||||
return True
|
return True
|
||||||
if self.b_vector is None or other.b_vector is None:
|
if self.b_vector is None or other.b_vector is None:
|
||||||
return False
|
return False
|
||||||
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)):
|
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): # noqa: SIM103
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def __le__(self, other: Repetition) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
other = cast('Grid', other)
|
||||||
|
if self.a_count != other.a_count:
|
||||||
|
return self.a_count < other.a_count
|
||||||
|
if self.b_count != other.b_count:
|
||||||
|
return self.b_count < other.b_count
|
||||||
|
if not numpy.array_equal(self.a_vector, other.a_vector):
|
||||||
|
return tuple(self.a_vector) < tuple(other.a_vector)
|
||||||
|
if self.b_vector is None:
|
||||||
|
return other.b_vector is not None
|
||||||
|
if other.b_vector is None:
|
||||||
|
return False
|
||||||
|
if not numpy.array_equal(self.b_vector, other.b_vector):
|
||||||
|
return tuple(self.a_vector) < tuple(other.a_vector)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Arbitrary(Repetition):
|
class Arbitrary(Repetition):
|
||||||
"""
|
"""
|
||||||
@ -307,9 +332,9 @@ class Arbitrary(Repetition):
|
|||||||
|
|
||||||
@displacements.setter
|
@displacements.setter
|
||||||
def displacements(self, val: ArrayLike) -> None:
|
def displacements(self, val: ArrayLike) -> None:
|
||||||
vala: NDArray[numpy.float64] = numpy.array(val, dtype=float)
|
vala = numpy.array(val, dtype=float)
|
||||||
vala = numpy.sort(vala.view([('', vala.dtype)] * vala.shape[1]), 0).view(vala.dtype) # sort rows
|
order = numpy.lexsort(vala.T[::-1]) # sortrows
|
||||||
self._displacements = vala
|
self._displacements = vala[order]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -325,10 +350,23 @@ class Arbitrary(Repetition):
|
|||||||
return (f'<Arbitrary {len(self.displacements)}pts >')
|
return (f'<Arbitrary {len(self.displacements)}pts >')
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
if not isinstance(other, type(self)):
|
if not type(other) is not type(self):
|
||||||
return False
|
return False
|
||||||
return numpy.array_equal(self.displacements, other.displacements)
|
return numpy.array_equal(self.displacements, other.displacements)
|
||||||
|
|
||||||
|
def __le__(self, other: Repetition) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
other = cast('Arbitrary', other)
|
||||||
|
if self.displacements.size != other.displacements.size:
|
||||||
|
return self.displacements.size < other.displacements.size
|
||||||
|
|
||||||
|
neq = (self.displacements != other.displacements)
|
||||||
|
if neq.any():
|
||||||
|
return self.displacements[neq][0] < other.displacements[neq][0]
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def rotate(self, rotation: float) -> Self:
|
def rotate(self, rotation: float) -> Self:
|
||||||
"""
|
"""
|
||||||
Rotate dispacements (around (0, 0))
|
Rotate dispacements (around (0, 0))
|
||||||
|
@ -3,11 +3,15 @@ Shapes for use with the Pattern class, as well as the Shape abstract class from
|
|||||||
which they are derived.
|
which they are derived.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
from .shape import (
|
||||||
|
Shape as Shape,
|
||||||
|
normalized_shape_tuple as normalized_shape_tuple,
|
||||||
|
DEFAULT_POLY_NUM_VERTICES as DEFAULT_POLY_NUM_VERTICES,
|
||||||
|
)
|
||||||
|
|
||||||
from .polygon import Polygon
|
from .polygon import Polygon as Polygon
|
||||||
from .circle import Circle
|
from .circle import Circle as Circle
|
||||||
from .ellipse import Ellipse
|
from .ellipse import Ellipse as Ellipse
|
||||||
from .arc import Arc
|
from .arc import Arc as Arc
|
||||||
from .text import Text
|
from .text import Text as Text
|
||||||
from .path import Path
|
from .path import Path as Path
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
@ -8,9 +9,10 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, annotations_t
|
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Arc(Shape):
|
class Arc(Shape):
|
||||||
"""
|
"""
|
||||||
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
|
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
|
||||||
@ -187,6 +189,38 @@ class Arc(Shape):
|
|||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and numpy.array_equal(self.radii, other.radii)
|
||||||
|
and numpy.array_equal(self.angles, other.angles)
|
||||||
|
and self.width == other.width
|
||||||
|
and self.rotation == other.rotation
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other: Shape) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
if repr(type(self)) != repr(type(other)):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
return id(type(self)) < id(type(other))
|
||||||
|
other = cast('Arc', other)
|
||||||
|
if self.width != other.width:
|
||||||
|
return self.width < other.width
|
||||||
|
if not numpy.array_equal(self.radii, other.radii):
|
||||||
|
return tuple(self.radii) < tuple(other.radii)
|
||||||
|
if not numpy.array_equal(self.angles, other.angles):
|
||||||
|
return tuple(self.angles) < tuple(other.angles)
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.rotation != other.rotation:
|
||||||
|
return self.rotation < other.rotation
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
self,
|
self,
|
||||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||||
@ -199,7 +233,7 @@ class Arc(Shape):
|
|||||||
r0, r1 = self.radii
|
r0, r1 = self.radii
|
||||||
|
|
||||||
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||||
a_ranges = self._angles_to_parameters()
|
a_ranges = cast('_array2x2_t', self._angles_to_parameters())
|
||||||
|
|
||||||
# Approximate perimeter via numerical integration
|
# Approximate perimeter via numerical integration
|
||||||
|
|
||||||
@ -210,43 +244,50 @@ class Arc(Shape):
|
|||||||
#t0 = ellipeinc(a0 - pi / 2, m)
|
#t0 = ellipeinc(a0 - pi / 2, m)
|
||||||
#perimeter2 = r0 * (t1 - t0)
|
#perimeter2 = r0 * (t1 - t0)
|
||||||
|
|
||||||
def get_arclens(n_pts: int, a0: float, a1: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
def get_arclens(n_pts: int, a0: float, a1: float, dr: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||||
""" Get `n_pts` arclengths """
|
""" Get `n_pts` arclengths """
|
||||||
t, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
tt, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
||||||
r0sin = r0 * numpy.sin(t)
|
r0sin = (r0 + dr) * numpy.sin(tt)
|
||||||
r1cos = r1 * numpy.cos(t)
|
r1cos = (r1 + dr) * numpy.cos(tt)
|
||||||
arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos)
|
arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos)
|
||||||
#arc_lengths = numpy.diff(t) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
#arc_lengths = numpy.diff(tt) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
||||||
arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2
|
arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2
|
||||||
return arc_lengths, t
|
return arc_lengths, tt
|
||||||
|
|
||||||
|
wh = self.width / 2.0
|
||||||
if num_vertices is not None:
|
if num_vertices is not None:
|
||||||
n_pts = numpy.ceil(max(self.radii) / min(self.radii) * num_vertices * 100).astype(int)
|
n_pts = numpy.ceil(max(self.radii + wh) / min(self.radii) * num_vertices * 100).astype(int)
|
||||||
perimeter_inner = get_arclens(n_pts, *a_ranges[0])[0].sum()
|
perimeter_inner = get_arclens(n_pts, *a_ranges[0], dr=-wh)[0].sum()
|
||||||
perimeter_outer = get_arclens(n_pts, *a_ranges[1])[0].sum()
|
perimeter_outer = get_arclens(n_pts, *a_ranges[1], dr= wh)[0].sum()
|
||||||
implied_arclen = (perimeter_outer + perimeter_inner + self.width * 2) / num_vertices
|
implied_arclen = (perimeter_outer + perimeter_inner + self.width * 2) / num_vertices
|
||||||
max_arclen = min(implied_arclen, max_arclen if max_arclen is not None else numpy.inf)
|
max_arclen = min(implied_arclen, max_arclen if max_arclen is not None else numpy.inf)
|
||||||
assert max_arclen is not None
|
assert max_arclen is not None
|
||||||
|
|
||||||
def get_thetas(inner: bool) -> NDArray[numpy.float64]:
|
def get_thetas(inner: bool) -> NDArray[numpy.float64]:
|
||||||
""" Figure out the parameter values at which we should place vertices to meet the arclength constraint"""
|
""" Figure out the parameter values at which we should place vertices to meet the arclength constraint"""
|
||||||
#dr = -self.width / 2.0 * (-1 if inner else 1)
|
dr = -wh if inner else wh
|
||||||
|
|
||||||
n_pts = numpy.ceil(2 * pi * max(self.radii) / max_arclen).astype(int)
|
n_pts = numpy.ceil(2 * pi * max(self.radii + dr) / max_arclen).astype(int)
|
||||||
arc_lengths, thetas = get_arclens(n_pts, *a_ranges[0 if inner else 1])
|
arc_lengths, thetas = get_arclens(n_pts, *a_ranges[0 if inner else 1], dr=dr)
|
||||||
|
|
||||||
keep = []
|
keep = [0]
|
||||||
removable = (numpy.cumsum(arc_lengths) <= max_arclen)
|
removable = (numpy.cumsum(arc_lengths) <= max_arclen)
|
||||||
start = 0
|
start = 1
|
||||||
while start < arc_lengths.size:
|
while start < arc_lengths.size:
|
||||||
next_to_keep = start + numpy.where(removable)[0][-1] # TODO: any chance we haven't sampled finely enough?
|
next_to_keep = start + numpy.where(removable)[0][-1] # TODO: any chance we haven't sampled finely enough?
|
||||||
keep.append(next_to_keep)
|
keep.append(next_to_keep)
|
||||||
removable = (numpy.cumsum(arc_lengths[next_to_keep + 1:]) <= max_arclen)
|
removable = (numpy.cumsum(arc_lengths[next_to_keep + 1:]) <= max_arclen)
|
||||||
start = next_to_keep + 1
|
start = next_to_keep + 1
|
||||||
return thetas[keep]
|
if keep[-1] != thetas.size - 1:
|
||||||
|
keep.append(thetas.size - 1)
|
||||||
|
|
||||||
wh = self.width / 2.0
|
thetas = thetas[keep]
|
||||||
if wh == r0 or wh == r1:
|
if inner:
|
||||||
|
thetas = thetas[::-1]
|
||||||
|
return thetas
|
||||||
|
|
||||||
|
thetas_inner: NDArray[numpy.float64]
|
||||||
|
if wh in (r0, r1):
|
||||||
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
|
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
|
||||||
else:
|
else:
|
||||||
thetas_inner = get_thetas(inner=True)
|
thetas_inner = get_thetas(inner=True)
|
||||||
@ -268,7 +309,7 @@ class Arc(Shape):
|
|||||||
return [poly]
|
return [poly]
|
||||||
|
|
||||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||||
'''
|
"""
|
||||||
Equation for rotated ellipse is
|
Equation for rotated ellipse is
|
||||||
`x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)`
|
`x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)`
|
||||||
`y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)`
|
`y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)`
|
||||||
@ -279,12 +320,12 @@ class Arc(Shape):
|
|||||||
where -+ is for x, y cases, so that's where the extrema are.
|
where -+ is for x, y cases, so that's where the extrema are.
|
||||||
|
|
||||||
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
|
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
|
||||||
'''
|
"""
|
||||||
a_ranges = self._angles_to_parameters()
|
a_ranges = cast('_array2x2_t', self._angles_to_parameters())
|
||||||
|
|
||||||
mins = []
|
mins = []
|
||||||
maxs = []
|
maxs = []
|
||||||
for a, sgn in zip(a_ranges, (-1, +1)):
|
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||||
wh = sgn * self.width / 2
|
wh = sgn * self.width / 2
|
||||||
rx = self.radius_x + wh
|
rx = self.radius_x + wh
|
||||||
ry = self.radius_y + wh
|
ry = self.radius_y + wh
|
||||||
@ -295,13 +336,13 @@ class Arc(Shape):
|
|||||||
maxs.append([0, 0])
|
maxs.append([0, 0])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
a0, a1 = a
|
a0, a1 = aa
|
||||||
a0_offset = a0 - (a0 % (2 * pi))
|
a0_offset = a0 - (a0 % (2 * pi))
|
||||||
|
|
||||||
sin_r = numpy.sin(self.rotation)
|
sin_r = numpy.sin(self.rotation)
|
||||||
cos_r = numpy.cos(self.rotation)
|
cos_r = numpy.cos(self.rotation)
|
||||||
sin_a = numpy.sin(a)
|
sin_a = numpy.sin(aa)
|
||||||
cos_a = numpy.cos(a)
|
cos_a = numpy.cos(aa)
|
||||||
|
|
||||||
# Cutoff angles
|
# Cutoff angles
|
||||||
xpt = (-self.rotation) % (2 * pi) + a0_offset
|
xpt = (-self.rotation) % (2 * pi) + a0_offset
|
||||||
@ -384,26 +425,26 @@ class Arc(Shape):
|
|||||||
))
|
))
|
||||||
|
|
||||||
def get_cap_edges(self) -> NDArray[numpy.float64]:
|
def get_cap_edges(self) -> NDArray[numpy.float64]:
|
||||||
'''
|
"""
|
||||||
Returns:
|
Returns:
|
||||||
```
|
```
|
||||||
[[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which
|
[[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which
|
||||||
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
|
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
|
||||||
```
|
```
|
||||||
'''
|
"""
|
||||||
a_ranges = self._angles_to_parameters()
|
a_ranges = cast('_array2x2_t', self._angles_to_parameters())
|
||||||
|
|
||||||
mins = []
|
mins = []
|
||||||
maxs = []
|
maxs = []
|
||||||
for a, sgn in zip(a_ranges, (-1, +1)):
|
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||||
wh = sgn * self.width / 2
|
wh = sgn * self.width / 2
|
||||||
rx = self.radius_x + wh
|
rx = self.radius_x + wh
|
||||||
ry = self.radius_y + wh
|
ry = self.radius_y + wh
|
||||||
|
|
||||||
sin_r = numpy.sin(self.rotation)
|
sin_r = numpy.sin(self.rotation)
|
||||||
cos_r = numpy.cos(self.rotation)
|
cos_r = numpy.cos(self.rotation)
|
||||||
sin_a = numpy.sin(a)
|
sin_a = numpy.sin(aa)
|
||||||
cos_a = numpy.cos(a)
|
cos_a = numpy.cos(aa)
|
||||||
|
|
||||||
# arc endpoints
|
# arc endpoints
|
||||||
xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a)
|
xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a)
|
||||||
@ -414,27 +455,30 @@ class Arc(Shape):
|
|||||||
return numpy.array([mins, maxs]) + self.offset
|
return numpy.array([mins, maxs]) + self.offset
|
||||||
|
|
||||||
def _angles_to_parameters(self) -> NDArray[numpy.float64]:
|
def _angles_to_parameters(self) -> NDArray[numpy.float64]:
|
||||||
'''
|
"""
|
||||||
|
Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
|
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
|
||||||
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
|
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
|
||||||
'''
|
"""
|
||||||
a = []
|
aa = []
|
||||||
for sgn in (-1, +1):
|
for sgn in (-1, +1):
|
||||||
wh = sgn * self.width / 2
|
wh = sgn * self.width / 2.0
|
||||||
rx = self.radius_x + wh
|
rx = self.radius_x + wh
|
||||||
ry = self.radius_y + wh
|
ry = self.radius_y + wh
|
||||||
|
|
||||||
# create paremeter 'a' for parametrized ellipse
|
a0, a1 = (numpy.arctan2(rx * numpy.sin(ai), ry * numpy.cos(ai)) for ai in self.angles)
|
||||||
a0, a1 = (numpy.arctan2(rx * numpy.sin(a), ry * numpy.cos(a)) for a in self.angles)
|
|
||||||
sign = numpy.sign(self.angles[1] - self.angles[0])
|
sign = numpy.sign(self.angles[1] - self.angles[0])
|
||||||
if sign != numpy.sign(a1 - a0):
|
if sign != numpy.sign(a1 - a0):
|
||||||
a1 += sign * 2 * pi
|
a1 += sign * 2 * pi
|
||||||
|
|
||||||
a.append((a0, a1))
|
aa.append((a0, a1))
|
||||||
return numpy.array(a)
|
return numpy.array(aa, dtype=float)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||||
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||||
|
|
||||||
|
_array2x2_t = tuple[tuple[float, float], tuple[float, float]]
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
from typing import Any, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
@ -7,9 +9,10 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, annotations_t
|
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Circle(Shape):
|
class Circle(Shape):
|
||||||
"""
|
"""
|
||||||
A circle, which has a position and radius.
|
A circle, which has a position and radius.
|
||||||
@ -67,6 +70,29 @@ class Circle(Shape):
|
|||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and self.radius == other.radius
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other: Shape) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
if repr(type(self)) != repr(type(other)):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
return id(type(self)) < id(type(other))
|
||||||
|
other = cast('Circle', other)
|
||||||
|
if not self.radius == other.radius:
|
||||||
|
return self.radius < other.radius
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
self,
|
self,
|
||||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||||
@ -93,10 +119,10 @@ class Circle(Shape):
|
|||||||
return numpy.vstack((self.offset - self.radius,
|
return numpy.vstack((self.offset - self.radius,
|
||||||
self.offset + self.radius))
|
self.offset + self.radius))
|
||||||
|
|
||||||
def rotate(self, theta: float) -> 'Circle':
|
def rotate(self, theta: float) -> 'Circle': # noqa: ARG002 (theta unused)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror(self, axis: int = 0) -> 'Circle':
|
def mirror(self, axis: int = 0) -> 'Circle': # noqa: ARG002 (axis unused)
|
||||||
self.offset *= -1
|
self.offset *= -1
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -104,7 +130,7 @@ class Circle(Shape):
|
|||||||
self.radius *= c
|
self.radius *= c
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def normalized_form(self, norm_value) -> normalized_shape_tuple:
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
rotation = 0.0
|
rotation = 0.0
|
||||||
magnitude = self.radius / norm_value
|
magnitude = self.radius / norm_value
|
||||||
return ((type(self),),
|
return ((type(self),),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from typing import Any, Self
|
from typing import Any, Self, cast
|
||||||
import copy
|
import copy
|
||||||
import math
|
import math
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
@ -9,9 +10,10 @@ from numpy.typing import ArrayLike, NDArray
|
|||||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_t
|
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Ellipse(Shape):
|
class Ellipse(Shape):
|
||||||
"""
|
"""
|
||||||
An ellipse, which has a position, two radii, and a rotation.
|
An ellipse, which has a position, two radii, and a rotation.
|
||||||
@ -117,6 +119,32 @@ class Ellipse(Shape):
|
|||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and numpy.array_equal(self.radii, other.radii)
|
||||||
|
and self.rotation == other.rotation
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other: Shape) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
if repr(type(self)) != repr(type(other)):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
return id(type(self)) < id(type(other))
|
||||||
|
other = cast('Ellipse', other)
|
||||||
|
if not numpy.array_equal(self.radii, other.radii):
|
||||||
|
return tuple(self.radii) < tuple(other.radii)
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.rotation != other.rotation:
|
||||||
|
return self.rotation < other.rotation
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
self,
|
self,
|
||||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from typing import Sequence, Any, cast
|
from typing import Any, cast
|
||||||
|
from collections.abc import Sequence
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -9,10 +11,11 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from . import Shape, normalized_shape_tuple, Polygon, Circle
|
from . import Shape, normalized_shape_tuple, Polygon, Circle
|
||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, rotation_matrix_2d
|
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class PathCap(Enum):
|
class PathCap(Enum):
|
||||||
Flush = 0 # Path ends at final vertices
|
Flush = 0 # Path ends at final vertices
|
||||||
Circle = 1 # Path extends past final vertices with a semicircle of radius width/2
|
Circle = 1 # Path extends past final vertices with a semicircle of radius width/2
|
||||||
@ -20,14 +23,17 @@ class PathCap(Enum):
|
|||||||
SquareCustom = 4 # Path extends past final vertices with a rectangle of length
|
SquareCustom = 4 # Path extends past final vertices with a rectangle of length
|
||||||
# # defined by path.cap_extensions
|
# # defined by path.cap_extensions
|
||||||
|
|
||||||
|
def __lt__(self, other: Any) -> bool:
|
||||||
|
return self.value == other.value
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Path(Shape):
|
class Path(Shape):
|
||||||
"""
|
"""
|
||||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
||||||
and an offset.
|
and an offset.
|
||||||
|
|
||||||
Note that the setter for `Path.vertices` may (but may not) create a copy of the
|
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
|
||||||
passed vertex coordinates. See `numpy.array(..., copy=False)` for details.
|
|
||||||
|
|
||||||
A normalized_form(...) is available, but can be quite slow with lots of vertices.
|
A normalized_form(...) is available, but can be quite slow with lots of vertices.
|
||||||
"""
|
"""
|
||||||
@ -98,11 +104,11 @@ class Path(Shape):
|
|||||||
custom_caps = (PathCap.SquareCustom,)
|
custom_caps = (PathCap.SquareCustom,)
|
||||||
if self.cap in custom_caps:
|
if self.cap in custom_caps:
|
||||||
if vals is None:
|
if vals is None:
|
||||||
raise Exception('Tried to set cap extensions to None on path with custom cap type')
|
raise PatternError('Tried to set cap extensions to None on path with custom cap type')
|
||||||
self._cap_extensions = numpy.array(vals, dtype=float)
|
self._cap_extensions = numpy.array(vals, dtype=float)
|
||||||
else:
|
else:
|
||||||
if vals is not None:
|
if vals is not None:
|
||||||
raise Exception('Tried to set custom cap extensions on path with non-custom cap type')
|
raise PatternError('Tried to set custom cap extensions on path with non-custom cap type')
|
||||||
self._cap_extensions = vals
|
self._cap_extensions = vals
|
||||||
|
|
||||||
# vertices property
|
# vertices property
|
||||||
@ -111,8 +117,7 @@ class Path(Shape):
|
|||||||
"""
|
"""
|
||||||
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
|
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
|
||||||
|
|
||||||
When setting, note that a copy of the provided vertices may or may not be made,
|
When setting, note that a copy of the provided vertices will be made.
|
||||||
following the rules from `numpy.array(.., copy=False)`.
|
|
||||||
"""
|
"""
|
||||||
return self._vertices
|
return self._vertices
|
||||||
|
|
||||||
@ -201,6 +206,40 @@ class Path(Shape):
|
|||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and numpy.array_equal(self.vertices, other.vertices)
|
||||||
|
and self.width == other.width
|
||||||
|
and self.cap == other.cap
|
||||||
|
and numpy.array_equal(self.cap_extensions, other.cap_extensions) # type: ignore
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other: Shape) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
if repr(type(self)) != repr(type(other)):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
return id(type(self)) < id(type(other))
|
||||||
|
other = cast('Path', other)
|
||||||
|
if self.width != other.width:
|
||||||
|
return self.width < other.width
|
||||||
|
if self.cap != other.cap:
|
||||||
|
return self.cap < other.cap
|
||||||
|
if not numpy.array_equal(self.cap_extensions, other.cap_extensions): # type: ignore
|
||||||
|
if other.cap_extensions is None:
|
||||||
|
return False
|
||||||
|
if self.cap_extensions is None:
|
||||||
|
return True
|
||||||
|
return tuple(self.cap_extensions) < tuple(other.cap_extensions)
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def travel(
|
def travel(
|
||||||
travel_pairs: Sequence[tuple[float, float]],
|
travel_pairs: Sequence[tuple[float, float]],
|
||||||
@ -232,7 +271,7 @@ class Path(Shape):
|
|||||||
# TODO: Path.travel() needs testing
|
# TODO: Path.travel() needs testing
|
||||||
direction = numpy.array([1, 0])
|
direction = numpy.array([1, 0])
|
||||||
|
|
||||||
verts = [numpy.zeros(2)]
|
verts: list[NDArray[numpy.float64]] = [numpy.zeros(2)]
|
||||||
for angle, distance in travel_pairs:
|
for angle, distance in travel_pairs:
|
||||||
direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
|
direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
|
||||||
verts.append(verts[-1] + direction * distance)
|
verts.append(verts[-1] + direction * distance)
|
||||||
@ -268,8 +307,8 @@ class Path(Shape):
|
|||||||
bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1]
|
bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1]
|
||||||
ds = v[1:-1] - v[:-2] - perp[1:] + perp[:-1]
|
ds = v[1:-1] - v[:-2] - perp[1:] + perp[:-1]
|
||||||
|
|
||||||
rp = numpy.linalg.solve(As, bs)[:, 0, None]
|
rp = numpy.linalg.solve(As, bs[:, :, None])[:, 0]
|
||||||
rn = numpy.linalg.solve(As, ds)[:, 0, None]
|
rn = numpy.linalg.solve(As, ds[:, :, None])[:, 0]
|
||||||
|
|
||||||
intersection_p = v[:-2] + rp * dv[:-1] + perp[:-1]
|
intersection_p = v[:-2] + rp * dv[:-1] + perp[:-1]
|
||||||
intersection_n = v[:-2] + rn * dv[:-1] - perp[:-1]
|
intersection_n = v[:-2] + rn * dv[:-1] - perp[:-1]
|
||||||
@ -366,7 +405,7 @@ class Path(Shape):
|
|||||||
x_min = rotated_vertices[:, 0].argmin()
|
x_min = rotated_vertices[:, 0].argmin()
|
||||||
if not is_scalar(x_min):
|
if not is_scalar(x_min):
|
||||||
y_min = rotated_vertices[x_min, 1].argmin()
|
y_min = rotated_vertices[x_min, 1].argmin()
|
||||||
x_min = cast(Sequence, x_min)[y_min]
|
x_min = cast('Sequence', x_min)[y_min]
|
||||||
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
||||||
|
|
||||||
width0 = self.width / norm_value
|
width0 = self.width / norm_value
|
||||||
@ -390,22 +429,22 @@ class Path(Shape):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def remove_duplicate_vertices(self) -> 'Path':
|
def remove_duplicate_vertices(self) -> 'Path':
|
||||||
'''
|
"""
|
||||||
Removes all consecutive duplicate (repeated) vertices.
|
Removes all consecutive duplicate (repeated) vertices.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
'''
|
"""
|
||||||
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False)
|
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def remove_colinear_vertices(self) -> 'Path':
|
def remove_colinear_vertices(self) -> 'Path':
|
||||||
'''
|
"""
|
||||||
Removes consecutive co-linear vertices.
|
Removes consecutive co-linear vertices.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
'''
|
"""
|
||||||
self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
|
self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Sequence, Any, cast
|
from typing import Any, cast, TYPE_CHECKING
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
@ -8,17 +9,21 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from . import Shape, normalized_shape_tuple
|
from . import Shape, normalized_shape_tuple
|
||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, rotation_matrix_2d
|
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Polygon(Shape):
|
class Polygon(Shape):
|
||||||
"""
|
"""
|
||||||
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
||||||
implicitly-closed boundary, and an offset.
|
implicitly-closed boundary, and an offset.
|
||||||
|
|
||||||
Note that the setter for `Polygon.vertices` may (but may not) create a copy of the
|
Note that the setter for `Polygon.vertices` creates a copy of the
|
||||||
passed vertex coordinates. See `numpy.array(..., copy=False)` for details.
|
passed vertex coordinates.
|
||||||
|
|
||||||
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
|
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
|
||||||
"""
|
"""
|
||||||
@ -37,8 +42,7 @@ class Polygon(Shape):
|
|||||||
"""
|
"""
|
||||||
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
||||||
|
|
||||||
When setting, note that a copy of the provided vertices may or may not be made,
|
When setting, note that a copy of the provided vertices will be made,
|
||||||
following the rules from `numpy.array(.., copy=False)`.
|
|
||||||
"""
|
"""
|
||||||
return self._vertices
|
return self._vertices
|
||||||
|
|
||||||
@ -103,6 +107,7 @@ class Polygon(Shape):
|
|||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations if annotations is not None else {}
|
||||||
|
if rotation:
|
||||||
self.rotate(rotation)
|
self.rotate(rotation)
|
||||||
|
|
||||||
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
|
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
|
||||||
@ -113,6 +118,35 @@ class Polygon(Shape):
|
|||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and numpy.array_equal(self.vertices, other.vertices)
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other: Shape) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
if repr(type(self)) != repr(type(other)):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
return id(type(self)) < id(type(other))
|
||||||
|
other = cast('Polygon', other)
|
||||||
|
if not numpy.array_equal(self.vertices, other.vertices):
|
||||||
|
min_len = min(self.vertices.shape[0], other.vertices.shape[0])
|
||||||
|
eq_mask = self.vertices[:min_len] != other.vertices[:min_len]
|
||||||
|
eq_lt = self.vertices[:min_len] < other.vertices[:min_len]
|
||||||
|
eq_lt_masked = eq_lt[eq_mask]
|
||||||
|
if eq_lt_masked.size > 0:
|
||||||
|
return eq_lt_masked.flat[0]
|
||||||
|
return self.vertices.shape[0] < other.vertices.shape[0]
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def square(
|
def square(
|
||||||
side_length: float,
|
side_length: float,
|
||||||
@ -221,7 +255,7 @@ class Polygon(Shape):
|
|||||||
lx = 2 * (xmax - xctr)
|
lx = 2 * (xmax - xctr)
|
||||||
else:
|
else:
|
||||||
raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
|
raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
|
||||||
else:
|
else: # noqa: PLR5501
|
||||||
if xctr is not None:
|
if xctr is not None:
|
||||||
pass
|
pass
|
||||||
elif xmax is None:
|
elif xmax is None:
|
||||||
@ -251,7 +285,7 @@ class Polygon(Shape):
|
|||||||
ly = 2 * (ymax - yctr)
|
ly = 2 * (ymax - yctr)
|
||||||
else:
|
else:
|
||||||
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
||||||
else:
|
else: # noqa: PLR5501
|
||||||
if yctr is not None:
|
if yctr is not None:
|
||||||
pass
|
pass
|
||||||
elif ymax is None:
|
elif ymax is None:
|
||||||
@ -299,10 +333,7 @@ class Polygon(Shape):
|
|||||||
Returns:
|
Returns:
|
||||||
A Polygon object containing the requested octagon
|
A Polygon object containing the requested octagon
|
||||||
"""
|
"""
|
||||||
if regular:
|
s = (1 + numpy.sqrt(2)) if regular else 2
|
||||||
s = 1 + numpy.sqrt(2)
|
|
||||||
else:
|
|
||||||
s = 2
|
|
||||||
|
|
||||||
norm_oct = numpy.array([
|
norm_oct = numpy.array([
|
||||||
[-1, -s],
|
[-1, -s],
|
||||||
@ -326,8 +357,8 @@ class Polygon(Shape):
|
|||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
self,
|
self,
|
||||||
num_vertices: int | None = None, # unused
|
num_vertices: int | None = None, # unused # noqa: ARG002
|
||||||
max_arclen: float | None = None, # unused
|
max_arclen: float | None = None, # unused # noqa: ARG002
|
||||||
) -> list['Polygon']:
|
) -> list['Polygon']:
|
||||||
return [copy.deepcopy(self)]
|
return [copy.deepcopy(self)]
|
||||||
|
|
||||||
@ -351,8 +382,9 @@ class Polygon(Shape):
|
|||||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
# Note: this function is going to be pretty slow for many-vertexed polygons, relative to
|
# Note: this function is going to be pretty slow for many-vertexed polygons, relative to
|
||||||
# other shapes
|
# other shapes
|
||||||
offset = self.vertices.mean(axis=0) + self.offset
|
meanv = self.vertices.mean(axis=0)
|
||||||
zeroed_vertices = self.vertices - offset
|
zeroed_vertices = self.vertices - meanv
|
||||||
|
offset = meanv + self.offset
|
||||||
|
|
||||||
scale = zeroed_vertices.std()
|
scale = zeroed_vertices.std()
|
||||||
normed_vertices = zeroed_vertices / scale
|
normed_vertices = zeroed_vertices / scale
|
||||||
@ -366,7 +398,7 @@ class Polygon(Shape):
|
|||||||
x_min = rotated_vertices[:, 0].argmin()
|
x_min = rotated_vertices[:, 0].argmin()
|
||||||
if not is_scalar(x_min):
|
if not is_scalar(x_min):
|
||||||
y_min = rotated_vertices[x_min, 1].argmin()
|
y_min = rotated_vertices[x_min, 1].argmin()
|
||||||
x_min = cast(Sequence, x_min)[y_min]
|
x_min = cast('Sequence', x_min)[y_min]
|
||||||
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
||||||
|
|
||||||
# TODO: normalize mirroring?
|
# TODO: normalize mirroring?
|
||||||
@ -386,22 +418,22 @@ class Polygon(Shape):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def remove_duplicate_vertices(self) -> 'Polygon':
|
def remove_duplicate_vertices(self) -> 'Polygon':
|
||||||
'''
|
"""
|
||||||
Removes all consecutive duplicate (repeated) vertices.
|
Removes all consecutive duplicate (repeated) vertices.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
'''
|
"""
|
||||||
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True)
|
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def remove_colinear_vertices(self) -> 'Polygon':
|
def remove_colinear_vertices(self) -> 'Polygon':
|
||||||
'''
|
"""
|
||||||
Removes consecutive co-linear vertices.
|
Removes consecutive co-linear vertices.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
'''
|
"""
|
||||||
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
|
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from typing import Callable, Self, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from collections.abc import Callable
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -32,16 +33,24 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
|||||||
"""
|
"""
|
||||||
__slots__ = () # Children should use AutoSlots or set slots themselves
|
__slots__ = () # Children should use AutoSlots or set slots themselves
|
||||||
|
|
||||||
def __copy__(self) -> Self:
|
#def __copy__(self) -> Self:
|
||||||
cls = self.__class__
|
# cls = self.__class__
|
||||||
new = cls.__new__(cls)
|
# new = cls.__new__(cls)
|
||||||
for name in self.__slots__: # type: str
|
# for name in self.__slots__: # type: str
|
||||||
object.__setattr__(new, name, getattr(self, name))
|
# object.__setattr__(new, name, getattr(self, name))
|
||||||
return new
|
# return new
|
||||||
|
|
||||||
#
|
#
|
||||||
# Methods (abstract)
|
# Methods (abstract)
|
||||||
#
|
#
|
||||||
|
@abstractmethod
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __lt__(self, other: 'Shape') -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
self,
|
self,
|
||||||
@ -126,7 +135,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
|||||||
|
|
||||||
vertex_lists = []
|
vertex_lists = []
|
||||||
p_verts = polygon.vertices + polygon.offset
|
p_verts = polygon.vertices + polygon.offset
|
||||||
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0)):
|
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True):
|
||||||
dv = v_next - v
|
dv = v_next - v
|
||||||
|
|
||||||
# Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape
|
# Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape
|
||||||
@ -156,7 +165,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
|||||||
|
|
||||||
m = dv[1] / dv[0]
|
m = dv[1] / dv[0]
|
||||||
|
|
||||||
def get_grid_inds(xes: ArrayLike) -> NDArray[numpy.float64]:
|
def get_grid_inds(xes: ArrayLike, m: float = m, v: NDArray = v) -> NDArray[numpy.float64]:
|
||||||
ys = m * (xes - v[0]) + v[1]
|
ys = m * (xes - v[0]) + v[1]
|
||||||
|
|
||||||
# (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid
|
# (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid
|
||||||
@ -257,11 +266,12 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
|||||||
mins, maxs = bounds
|
mins, maxs = bounds
|
||||||
keep_x = numpy.logical_and(grx > mins[0], grx < maxs[0])
|
keep_x = numpy.logical_and(grx > mins[0], grx < maxs[0])
|
||||||
keep_y = numpy.logical_and(gry > mins[1], gry < maxs[1])
|
keep_y = numpy.logical_and(gry > mins[1], gry < maxs[1])
|
||||||
for k in (keep_x, keep_y):
|
# Flood left & rightwards by 2 cells
|
||||||
for s in (1, 2):
|
for kk in (keep_x, keep_y):
|
||||||
k[s:] += k[:-s]
|
for ss in (1, 2):
|
||||||
k[:-s] += k[s:]
|
kk[ss:] += kk[:-ss]
|
||||||
k = k > 0
|
kk[:-ss] += kk[ss:]
|
||||||
|
kk[:] = kk > 0
|
||||||
|
|
||||||
gx = grx[keep_x]
|
gx = grx[keep_x]
|
||||||
gy = gry[keep_y]
|
gy = gry[keep_y]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Self
|
from typing import Self, Any, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi, nan
|
from numpy import pi, nan
|
||||||
@ -9,13 +10,14 @@ from . import Shape, Polygon, normalized_shape_tuple
|
|||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..traits import RotatableImpl
|
from ..traits import RotatableImpl
|
||||||
from ..utils import is_scalar, get_bit, annotations_t
|
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
# Loaded on use:
|
# Loaded on use:
|
||||||
# from freetype import Face
|
# from freetype import Face
|
||||||
# from matplotlib.path import Path
|
# from matplotlib.path import Path
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class Text(RotatableImpl, Shape):
|
class Text(RotatableImpl, Shape):
|
||||||
"""
|
"""
|
||||||
Text (to be printed e.g. as a set of polygons).
|
Text (to be printed e.g. as a set of polygons).
|
||||||
@ -96,10 +98,42 @@ class Text(RotatableImpl, Shape):
|
|||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and numpy.array_equal(self.offset, other.offset)
|
||||||
|
and self.string == other.string
|
||||||
|
and self.height == other.height
|
||||||
|
and self.font_path == other.font_path
|
||||||
|
and self.rotation == other.rotation
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other: Shape) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
if repr(type(self)) != repr(type(other)):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
return id(type(self)) < id(type(other))
|
||||||
|
other = cast('Text', other)
|
||||||
|
if not self.height == other.height:
|
||||||
|
return self.height < other.height
|
||||||
|
if not self.string == other.string:
|
||||||
|
return self.string < other.string
|
||||||
|
if not self.font_path == other.font_path:
|
||||||
|
return self.font_path < other.font_path
|
||||||
|
if not numpy.array_equal(self.offset, other.offset):
|
||||||
|
return tuple(self.offset) < tuple(other.offset)
|
||||||
|
if self.rotation != other.rotation:
|
||||||
|
return self.rotation < other.rotation
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
self,
|
self,
|
||||||
num_vertices: int | None = None, # unused
|
num_vertices: int | None = None, # unused # noqa: ARG002
|
||||||
max_arclen: float | None = None, # unused
|
max_arclen: float | None = None, # unused # noqa: ARG002
|
||||||
) -> list[Polygon]:
|
) -> list[Polygon]:
|
||||||
all_polygons = []
|
all_polygons = []
|
||||||
total_advance = 0.0
|
total_advance = 0.0
|
||||||
@ -157,6 +191,11 @@ class Text(RotatableImpl, Shape):
|
|||||||
|
|
||||||
return bounds
|
return bounds
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||||
|
mirrored = ' m{:d}' if self.mirrored else ''
|
||||||
|
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
||||||
|
|
||||||
|
|
||||||
def get_char_as_polygons(
|
def get_char_as_polygons(
|
||||||
font_path: str,
|
font_path: str,
|
||||||
@ -182,7 +221,7 @@ def get_char_as_polygons(
|
|||||||
'advance' distance (distance from the start of this glyph to the start of the next one)
|
'advance' distance (distance from the start of this glyph to the start of the next one)
|
||||||
"""
|
"""
|
||||||
if len(char) != 1:
|
if len(char) != 1:
|
||||||
raise Exception('get_char_as_polygons called with non-char')
|
raise PatternError('get_char_as_polygons called with non-char')
|
||||||
|
|
||||||
face = Face(font_path)
|
face = Face(font_path)
|
||||||
face.set_char_size(resolution)
|
face.set_char_size(resolution)
|
||||||
@ -191,7 +230,8 @@ def get_char_as_polygons(
|
|||||||
outline = slot.outline
|
outline = slot.outline
|
||||||
|
|
||||||
start = 0
|
start = 0
|
||||||
all_verts_list, all_codes = [], []
|
all_verts_list = []
|
||||||
|
all_codes = []
|
||||||
for end in outline.contours:
|
for end in outline.contours:
|
||||||
points = outline.points[start:end + 1]
|
points = outline.points[start:end + 1]
|
||||||
points.append(points[0])
|
points.append(points[0])
|
||||||
@ -244,8 +284,3 @@ def get_char_as_polygons(
|
|||||||
polygons = path.to_polygons()
|
polygons = path.to_polygons()
|
||||||
|
|
||||||
return polygons, advance
|
return polygons, advance
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
|
||||||
mirrored = ' m{:d}' if self.mirrored else ''
|
|
||||||
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
|
||||||
|
@ -3,11 +3,32 @@ Traits (mixins) and default implementations
|
|||||||
|
|
||||||
Traits and mixins should set `__slots__ = ()` to enable use of `__slots__` in subclasses.
|
Traits and mixins should set `__slots__ = ()` to enable use of `__slots__` in subclasses.
|
||||||
"""
|
"""
|
||||||
from .positionable import Positionable, PositionableImpl, Bounded
|
from .positionable import (
|
||||||
from .layerable import Layerable, LayerableImpl
|
Positionable as Positionable,
|
||||||
from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
|
PositionableImpl as PositionableImpl,
|
||||||
from .repeatable import Repeatable, RepeatableImpl
|
Bounded as Bounded,
|
||||||
from .scalable import Scalable, ScalableImpl
|
)
|
||||||
from .mirrorable import Mirrorable
|
from .layerable import (
|
||||||
from .copyable import Copyable
|
Layerable as Layerable,
|
||||||
from .annotatable import Annotatable, AnnotatableImpl
|
LayerableImpl as LayerableImpl,
|
||||||
|
)
|
||||||
|
from .rotatable import (
|
||||||
|
Rotatable as Rotatable,
|
||||||
|
RotatableImpl as RotatableImpl,
|
||||||
|
Pivotable as Pivotable,
|
||||||
|
PivotableImpl as PivotableImpl,
|
||||||
|
)
|
||||||
|
from .repeatable import (
|
||||||
|
Repeatable as Repeatable,
|
||||||
|
RepeatableImpl as RepeatableImpl,
|
||||||
|
)
|
||||||
|
from .scalable import (
|
||||||
|
Scalable as Scalable,
|
||||||
|
ScalableImpl as ScalableImpl,
|
||||||
|
)
|
||||||
|
from .mirrorable import Mirrorable as Mirrorable
|
||||||
|
from .copyable import Copyable as Copyable
|
||||||
|
from .annotatable import (
|
||||||
|
Annotatable as Annotatable,
|
||||||
|
AnnotatableImpl as AnnotatableImpl,
|
||||||
|
)
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from typing import Self
|
from typing import Self
|
||||||
from abc import ABCMeta
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
|
||||||
class Copyable(metaclass=ABCMeta):
|
class Copyable:
|
||||||
"""
|
"""
|
||||||
Trait class which adds .copy() and .deepcopy()
|
Trait class which adds .copy() and .deepcopy()
|
||||||
"""
|
"""
|
||||||
|
@ -63,7 +63,7 @@ class LayerableImpl(Layerable, metaclass=ABCMeta):
|
|||||||
return self._layer
|
return self._layer
|
||||||
|
|
||||||
@layer.setter
|
@layer.setter
|
||||||
def layer(self, val: layer_t):
|
def layer(self, val: layer_t) -> None:
|
||||||
self._layer = val
|
self._layer = val
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -44,7 +44,7 @@ class Mirrorable(metaclass=ABCMeta):
|
|||||||
# """
|
# """
|
||||||
# __slots__ = ()
|
# __slots__ = ()
|
||||||
#
|
#
|
||||||
# _mirrored: numpy.ndarray # ndarray[bool]
|
# _mirrored: NDArray[numpy.bool]
|
||||||
# """ Whether to mirror the instance across the x and/or y axes. """
|
# """ Whether to mirror the instance across the x and/or y axes. """
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
@ -52,15 +52,15 @@ class Mirrorable(metaclass=ABCMeta):
|
|||||||
# #
|
# #
|
||||||
# # Mirrored property
|
# # Mirrored property
|
||||||
# @property
|
# @property
|
||||||
# def mirrored(self) -> numpy.ndarray: # ndarray[bool]
|
# def mirrored(self) -> NDArray[numpy.bool]:
|
||||||
# """ Whether to mirror across the [x, y] axes, respectively """
|
# """ Whether to mirror across the [x, y] axes, respectively """
|
||||||
# return self._mirrored
|
# return self._mirrored
|
||||||
#
|
#
|
||||||
# @mirrored.setter
|
# @mirrored.setter
|
||||||
# def mirrored(self, val: Sequence[bool]):
|
# def mirrored(self, val: Sequence[bool]) -> None:
|
||||||
# if is_scalar(val):
|
# if is_scalar(val):
|
||||||
# raise MasqueError('Mirrored must be a 2-element list of booleans')
|
# raise MasqueError('Mirrored must be a 2-element list of booleans')
|
||||||
# self._mirrored = numpy.array(val, dtype=bool, copy=True)
|
# self._mirrored = numpy.array(val, dtype=bool)
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
# # Methods
|
# # Methods
|
||||||
|
@ -81,12 +81,11 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
|||||||
|
|
||||||
@offset.setter
|
@offset.setter
|
||||||
def offset(self, val: ArrayLike) -> None:
|
def offset(self, val: ArrayLike) -> None:
|
||||||
if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64:
|
|
||||||
val = numpy.array(val, dtype=float)
|
val = numpy.array(val, dtype=float)
|
||||||
|
|
||||||
if val.size != 2:
|
if val.size != 2:
|
||||||
raise MasqueError('Offset must be convertible to size-2 ndarray')
|
raise MasqueError('Offset must be convertible to size-2 ndarray')
|
||||||
self._offset = val.flatten() # type: ignore
|
self._offset = val.flatten()
|
||||||
|
|
||||||
#
|
#
|
||||||
# Methods
|
# Methods
|
||||||
|
@ -34,7 +34,7 @@ class Repeatable(metaclass=ABCMeta):
|
|||||||
|
|
||||||
# @repetition.setter
|
# @repetition.setter
|
||||||
# @abstractmethod
|
# @abstractmethod
|
||||||
# def repetition(self, repetition: 'Repetition | None'):
|
# def repetition(self, repetition: 'Repetition | None') -> None:
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -75,7 +75,7 @@ class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
|
|||||||
return self._repetition
|
return self._repetition
|
||||||
|
|
||||||
@repetition.setter
|
@repetition.setter
|
||||||
def repetition(self, repetition: 'Repetition | None'):
|
def repetition(self, repetition: 'Repetition | None') -> None:
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
if repetition is not None and not isinstance(repetition, Repetition):
|
if repetition is not None and not isinstance(repetition, Repetition):
|
||||||
raise MasqueError(f'{repetition} is not a valid Repetition object!')
|
raise MasqueError(f'{repetition} is not a valid Repetition object!')
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
from typing import Self, cast, Any
|
from typing import Self, cast, Any, TYPE_CHECKING
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike
|
||||||
|
|
||||||
from .positionable import Positionable
|
|
||||||
from ..error import MasqueError
|
from ..error import MasqueError
|
||||||
from ..utils import rotation_matrix_2d
|
from ..utils import rotation_matrix_2d
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .positionable import Positionable
|
||||||
|
|
||||||
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
|
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
|
|||||||
return self._rotation
|
return self._rotation
|
||||||
|
|
||||||
@rotation.setter
|
@rotation.setter
|
||||||
def rotation(self, val: float):
|
def rotation(self, val: float) -> None:
|
||||||
if not numpy.size(val) == 1:
|
if not numpy.size(val) == 1:
|
||||||
raise MasqueError('Rotation must be a scalar')
|
raise MasqueError('Rotation must be a scalar')
|
||||||
self._rotation = val % (2 * pi)
|
self._rotation = val % (2 * pi)
|
||||||
@ -112,10 +113,10 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
|
|||||||
""" `[x_offset, y_offset]` """
|
""" `[x_offset, y_offset]` """
|
||||||
|
|
||||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||||
pivot = numpy.array(pivot, dtype=float)
|
pivot = numpy.asarray(pivot, dtype=float)
|
||||||
cast(Positionable, self).translate(-pivot)
|
cast('Positionable', self).translate(-pivot)
|
||||||
cast(Rotatable, self).rotate(rotation)
|
cast('Rotatable', self).rotate(rotation)
|
||||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004
|
||||||
cast(Positionable, self).translate(+pivot)
|
cast('Positionable', self).translate(+pivot)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class ScalableImpl(Scalable, metaclass=ABCMeta):
|
|||||||
return self._scale
|
return self._scale
|
||||||
|
|
||||||
@scale.setter
|
@scale.setter
|
||||||
def scale(self, val: float):
|
def scale(self, val: float) -> None:
|
||||||
if not is_scalar(val):
|
if not is_scalar(val):
|
||||||
raise MasqueError('Scale must be a scalar')
|
raise MasqueError('Scale must be a scalar')
|
||||||
if not val > 0:
|
if not val > 0:
|
||||||
|
@ -1,18 +1,43 @@
|
|||||||
"""
|
"""
|
||||||
Various helper functions, type definitions, etc.
|
Various helper functions, type definitions, etc.
|
||||||
"""
|
"""
|
||||||
from .types import layer_t, annotations_t, SupportsBool
|
from .types import (
|
||||||
from .array import is_scalar
|
layer_t as layer_t,
|
||||||
from .autoslots import AutoSlots
|
annotations_t as annotations_t,
|
||||||
from .deferreddict import DeferredDict
|
SupportsBool as SupportsBool,
|
||||||
from .decorators import oneshot
|
|
||||||
|
|
||||||
from .bitwise import get_bit, set_bit
|
|
||||||
from .vertices import (
|
|
||||||
remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points
|
|
||||||
)
|
)
|
||||||
from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_around
|
from .array import is_scalar as is_scalar
|
||||||
|
from .autoslots import AutoSlots as AutoSlots
|
||||||
|
from .deferreddict import DeferredDict as DeferredDict
|
||||||
|
from .decorators import oneshot as oneshot
|
||||||
|
|
||||||
from . import ports2data
|
from .bitwise import (
|
||||||
|
get_bit as get_bit,
|
||||||
|
set_bit as set_bit,
|
||||||
|
)
|
||||||
|
from .vertices import (
|
||||||
|
remove_duplicate_vertices as remove_duplicate_vertices,
|
||||||
|
remove_colinear_vertices as remove_colinear_vertices,
|
||||||
|
poly_contains_points as poly_contains_points,
|
||||||
|
)
|
||||||
|
from .transform import (
|
||||||
|
rotation_matrix_2d as rotation_matrix_2d,
|
||||||
|
normalize_mirror as normalize_mirror,
|
||||||
|
rotate_offsets_around as rotate_offsets_around,
|
||||||
|
apply_transforms as apply_transforms,
|
||||||
|
R90 as R90,
|
||||||
|
R180 as R180,
|
||||||
|
)
|
||||||
|
from .comparisons import (
|
||||||
|
annotation2key as annotation2key,
|
||||||
|
annotations_lt as annotations_lt,
|
||||||
|
annotations_eq as annotations_eq,
|
||||||
|
layer2key as layer2key,
|
||||||
|
ports_lt as ports_lt,
|
||||||
|
ports_eq as ports_eq,
|
||||||
|
rep2key as rep2key,
|
||||||
|
)
|
||||||
|
|
||||||
from . import pack2d
|
from . import ports2data as ports2data
|
||||||
|
|
||||||
|
from . import pack2d as pack2d
|
||||||
|
@ -12,16 +12,16 @@ class AutoSlots(ABCMeta):
|
|||||||
classes, they can have empty `__slots__` and their attribute type annotations
|
classes, they can have empty `__slots__` and their attribute type annotations
|
||||||
can be used to generate a full `__slots__` for the concrete class.
|
can be used to generate a full `__slots__` for the concrete class.
|
||||||
"""
|
"""
|
||||||
def __new__(cls, name, bases, dctn):
|
def __new__(cls, name, bases, dctn): # noqa: ANN001,ANN204
|
||||||
parents = set()
|
parents = set()
|
||||||
for base in bases:
|
for base in bases:
|
||||||
parents |= set(base.mro())
|
parents |= set(base.mro())
|
||||||
|
|
||||||
slots = tuple(dctn.get('__slots__', tuple()))
|
slots = tuple(dctn.get('__slots__', ()))
|
||||||
for parent in parents:
|
for parent in parents:
|
||||||
if not hasattr(parent, '__annotations__'):
|
if not hasattr(parent, '__annotations__'):
|
||||||
continue
|
continue
|
||||||
slots += tuple(getattr(parent, '__annotations__').keys())
|
slots += tuple(parent.__annotations__.keys())
|
||||||
|
|
||||||
dctn['__slots__'] = slots
|
dctn['__slots__'] = slots
|
||||||
return super().__new__(cls, name, bases, dctn)
|
return super().__new__(cls, name, bases, dctn)
|
||||||
|
106
masque/utils/comparisons.py
Normal file
106
masque/utils/comparisons.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .types import annotations_t, layer_t
|
||||||
|
from ..ports import Port
|
||||||
|
from ..repetition import Repetition
|
||||||
|
|
||||||
|
|
||||||
|
def annotation2key(aaa: int | float | str) -> tuple[bool, Any]:
|
||||||
|
return (isinstance(aaa, str), aaa)
|
||||||
|
|
||||||
|
|
||||||
|
def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool:
|
||||||
|
if aa is None:
|
||||||
|
return bb is not None
|
||||||
|
elif bb is None: # noqa: RET505
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(aa) != len(bb):
|
||||||
|
return len(aa) < len(bb)
|
||||||
|
|
||||||
|
keys_a = tuple(sorted(aa.keys()))
|
||||||
|
keys_b = tuple(sorted(bb.keys()))
|
||||||
|
if keys_a != keys_b:
|
||||||
|
return keys_a < keys_b
|
||||||
|
|
||||||
|
for key in keys_a:
|
||||||
|
va = aa[key]
|
||||||
|
vb = bb[key]
|
||||||
|
if len(va) != len(vb):
|
||||||
|
return len(va) < len(vb)
|
||||||
|
|
||||||
|
for aaa, bbb in zip(va, vb, strict=True):
|
||||||
|
if aaa != bbb:
|
||||||
|
return annotation2key(aaa) < annotation2key(bbb)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool:
|
||||||
|
if aa is None:
|
||||||
|
return bb is None
|
||||||
|
elif bb is None: # noqa: RET505
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(aa) != len(bb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
keys_a = tuple(sorted(aa.keys()))
|
||||||
|
keys_b = tuple(sorted(bb.keys()))
|
||||||
|
if keys_a != keys_b:
|
||||||
|
return keys_a < keys_b
|
||||||
|
|
||||||
|
for key in keys_a:
|
||||||
|
va = aa[key]
|
||||||
|
vb = bb[key]
|
||||||
|
if len(va) != len(vb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for aaa, bbb in zip(va, vb, strict=True):
|
||||||
|
if aaa != bbb:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def layer2key(layer: layer_t) -> tuple[bool, bool, Any]:
|
||||||
|
is_int = isinstance(layer, int)
|
||||||
|
is_str = isinstance(layer, str)
|
||||||
|
layer_tup = (layer) if (is_str or is_int) else layer
|
||||||
|
tup = (
|
||||||
|
is_str,
|
||||||
|
not is_int,
|
||||||
|
layer_tup,
|
||||||
|
)
|
||||||
|
return tup
|
||||||
|
|
||||||
|
|
||||||
|
def rep2key(repetition: Repetition | None) -> tuple[bool, Repetition | None]:
|
||||||
|
return (repetition is None, repetition)
|
||||||
|
|
||||||
|
|
||||||
|
def ports_eq(aa: dict[str, Port], bb: dict[str, Port]) -> bool:
|
||||||
|
if len(aa) != len(bb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
keys = sorted(aa.keys())
|
||||||
|
if keys != sorted(bb.keys()):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return all(aa[kk] == bb[kk] for kk in keys)
|
||||||
|
|
||||||
|
|
||||||
|
def ports_lt(aa: dict[str, Port], bb: dict[str, Port]) -> bool:
|
||||||
|
if len(aa) != len(bb):
|
||||||
|
return len(aa) < len(bb)
|
||||||
|
|
||||||
|
aa_keys = tuple(sorted(aa.keys()))
|
||||||
|
bb_keys = tuple(sorted(bb.keys()))
|
||||||
|
if aa_keys != bb_keys:
|
||||||
|
return aa_keys < bb_keys
|
||||||
|
|
||||||
|
for key in aa_keys:
|
||||||
|
pa = aa[key]
|
||||||
|
pb = bb[key]
|
||||||
|
if pa != pb:
|
||||||
|
return pa < pb
|
||||||
|
return False
|
104
masque/utils/curves.py
Normal file
104
masque/utils/curves.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import numpy
|
||||||
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
try:
|
||||||
|
from numpy import trapezoid
|
||||||
|
except ImportError:
|
||||||
|
from numpy import trapz as trapezoid
|
||||||
|
|
||||||
|
|
||||||
|
def bezier(
|
||||||
|
nodes: ArrayLike,
|
||||||
|
tt: ArrayLike,
|
||||||
|
weights: ArrayLike | None = None,
|
||||||
|
) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
Sample a Bezier curve with the provided control points at the parametrized positions `tt`.
|
||||||
|
|
||||||
|
Using the calculation method from arXiv:1803.06843, Chudy and Woźny.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes: `[[x0, y0], ...]` control points for the Bezier curve
|
||||||
|
tt: Parametrized positions at which to sample the curve (1D array with values in the interval [0, 1])
|
||||||
|
weights: Control point weights; if provided, length should be the same as number of control points.
|
||||||
|
Default 1 for all control points.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`[[x0, y0], [x1, y1], ...]` corresponding to `[tt0, tt1, ...]`
|
||||||
|
"""
|
||||||
|
nodes = numpy.asarray(nodes)
|
||||||
|
tt = numpy.asarray(tt)
|
||||||
|
nn = nodes.shape[0]
|
||||||
|
weights = numpy.ones(nn) if weights is None else numpy.asarray(weights)
|
||||||
|
|
||||||
|
with numpy.errstate(divide='ignore'):
|
||||||
|
umul = (tt / (1 - tt)).clip(max=1)
|
||||||
|
udiv = ((1 - tt) / tt).clip(max=1)
|
||||||
|
|
||||||
|
hh = numpy.ones((tt.size,))
|
||||||
|
qq = nodes[None, 0, :] * hh[:, None]
|
||||||
|
for kk in range(1, nn):
|
||||||
|
hh *= umul * (nn - kk) * weights[kk]
|
||||||
|
hh /= kk * udiv * weights[kk - 1] + hh
|
||||||
|
qq *= 1.0 - hh[:, None]
|
||||||
|
qq += hh[:, None] * nodes[None, kk, :]
|
||||||
|
return qq
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def euler_bend(
|
||||||
|
switchover_angle: float,
|
||||||
|
num_points: int = 200,
|
||||||
|
) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
Generate a 90 degree Euler bend (AKA Clothoid bend or Cornu spiral).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
switchover_angle: After this angle, the bend will transition into a circular arc
|
||||||
|
(and transition back to an Euler spiral on the far side). If this is set to
|
||||||
|
`>= pi / 4`, no circular arc will be added.
|
||||||
|
num_points: Number of points in the curve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`[[x0, y0], ...]` for the curve
|
||||||
|
"""
|
||||||
|
ll_max = numpy.sqrt(2 * switchover_angle) # total length of (one) spiral portion
|
||||||
|
ll_tot = 2 * ll_max + (pi / 2 - 2 * switchover_angle)
|
||||||
|
num_points_spiral = numpy.floor(ll_max / ll_tot * num_points).astype(int)
|
||||||
|
num_points_arc = num_points - 2 * num_points_spiral
|
||||||
|
|
||||||
|
def gen_spiral(ll_max: float) -> NDArray[numpy.float64]:
|
||||||
|
xx = []
|
||||||
|
yy = []
|
||||||
|
for ll in numpy.linspace(0, ll_max, num_points_spiral):
|
||||||
|
qq = numpy.linspace(0, ll, 1000) # integrate to current arclength
|
||||||
|
xx.append(trapezoid( numpy.cos(qq * qq / 2), qq))
|
||||||
|
yy.append(trapezoid(-numpy.sin(qq * qq / 2), qq))
|
||||||
|
xy_part = numpy.stack((xx, yy), axis=1)
|
||||||
|
return xy_part
|
||||||
|
|
||||||
|
xy_spiral = gen_spiral(ll_max)
|
||||||
|
xy_parts = [xy_spiral]
|
||||||
|
|
||||||
|
if switchover_angle < pi / 4:
|
||||||
|
# Build a circular segment to join the two euler portions
|
||||||
|
rmin = 1.0 / ll_max
|
||||||
|
half_angle = pi / 4 - switchover_angle
|
||||||
|
qq = numpy.linspace(half_angle * 2, 0, num_points_arc + 1) + switchover_angle
|
||||||
|
xc = rmin * numpy.cos(qq)
|
||||||
|
yc = rmin * numpy.sin(qq) + xy_spiral[-1, 1]
|
||||||
|
xc += xy_spiral[-1, 0] - xc[0]
|
||||||
|
yc += xy_spiral[-1, 1] - yc[0]
|
||||||
|
xy_parts.append(numpy.stack((xc[1:], yc[1:]), axis=1))
|
||||||
|
|
||||||
|
endpoint_xy = xy_parts[-1][-1, :]
|
||||||
|
second_spiral = xy_spiral[::-1, ::-1] + endpoint_xy - xy_spiral[-1, ::-1]
|
||||||
|
|
||||||
|
xy_parts.append(second_spiral)
|
||||||
|
xy = numpy.concatenate(xy_parts)
|
||||||
|
|
||||||
|
# Remove any 2x-duplicate points
|
||||||
|
xy = xy[(numpy.roll(xy, 1, axis=0) != xy).any(axis=1)]
|
||||||
|
|
||||||
|
return xy
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from ..error import OneShotError
|
from ..error import OneShotError
|
||||||
@ -11,7 +11,7 @@ def oneshot(func: Callable) -> Callable:
|
|||||||
expired = False
|
expired = False
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs): # noqa: ANN202
|
||||||
nonlocal expired
|
nonlocal expired
|
||||||
if expired:
|
if expired:
|
||||||
raise OneShotError(func.__name__)
|
raise OneShotError(func.__name__)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from typing import Callable, TypeVar, Generic
|
from typing import TypeVar, Generic
|
||||||
|
from collections.abc import Callable
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
2D bin-packing
|
2D bin-packing
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Callable, Mapping
|
from collections.abc import Sequence, Mapping, Callable
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
@ -38,8 +38,8 @@ def maxrects_bssf(
|
|||||||
Raises:
|
Raises:
|
||||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||||
"""
|
"""
|
||||||
regions = numpy.array(containers, copy=False, dtype=float)
|
regions = numpy.asarray(containers, dtype=float)
|
||||||
rect_sizes = numpy.array(rects, copy=False, dtype=float)
|
rect_sizes = numpy.asarray(rects, dtype=float)
|
||||||
rect_locs = numpy.zeros_like(rect_sizes)
|
rect_locs = numpy.zeros_like(rect_sizes)
|
||||||
rejected_inds = set()
|
rejected_inds = set()
|
||||||
|
|
||||||
@ -62,14 +62,14 @@ def maxrects_bssf(
|
|||||||
|
|
||||||
''' Place the rect '''
|
''' Place the rect '''
|
||||||
# Best short-side fit (bssf) to pick a region
|
# Best short-side fit (bssf) to pick a region
|
||||||
bssf_scores = ((regions[:, 2:] - regions[:, :2]) - rect_size).min(axis=1).astype(float)
|
region_sizes = regions[:, 2:] - regions[:, :2]
|
||||||
|
bssf_scores = (region_sizes - rect_size).min(axis=1).astype(float)
|
||||||
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
||||||
rr = bssf_scores.argmin()
|
rr = bssf_scores.argmin()
|
||||||
if numpy.isinf(bssf_scores[rr]):
|
if numpy.isinf(bssf_scores[rr]):
|
||||||
if allow_rejects:
|
if allow_rejects:
|
||||||
rejected_inds.add(rect_ind)
|
rejected_inds.add(rect_ind)
|
||||||
continue
|
continue
|
||||||
else:
|
|
||||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||||
|
|
||||||
# Read out location
|
# Read out location
|
||||||
@ -139,8 +139,8 @@ def guillotine_bssf_sas(
|
|||||||
Raises:
|
Raises:
|
||||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||||
"""
|
"""
|
||||||
regions = numpy.array(containers, copy=False, dtype=float)
|
regions = numpy.asarray(containers, dtype=float)
|
||||||
rect_sizes = numpy.array(rects, copy=False, dtype=float)
|
rect_sizes = numpy.asarray(rects, dtype=float)
|
||||||
rect_locs = numpy.zeros_like(rect_sizes)
|
rect_locs = numpy.zeros_like(rect_sizes)
|
||||||
rejected_inds = set()
|
rejected_inds = set()
|
||||||
|
|
||||||
@ -152,21 +152,21 @@ def guillotine_bssf_sas(
|
|||||||
for rect_ind, rect_size in enumerate(rect_sizes):
|
for rect_ind, rect_size in enumerate(rect_sizes):
|
||||||
''' Place the rect '''
|
''' Place the rect '''
|
||||||
# Best short-side fit (bssf) to pick a region
|
# Best short-side fit (bssf) to pick a region
|
||||||
bssf_scores = ((regions[:, 2:] - regions[:, :2]) - rect_size).min(axis=1).astype(float)
|
region_sizes = regions[:, 2:] - regions[:, :2]
|
||||||
|
bssf_scores = (region_sizes - rect_size).min(axis=1).astype(float)
|
||||||
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
||||||
rr = bssf_scores.argmin()
|
rr = bssf_scores.argmin()
|
||||||
if numpy.isinf(bssf_scores[rr]):
|
if numpy.isinf(bssf_scores[rr]):
|
||||||
if allow_rejects:
|
if allow_rejects:
|
||||||
rejected_inds.add(rect_ind)
|
rejected_inds.add(rect_ind)
|
||||||
continue
|
continue
|
||||||
else:
|
|
||||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||||
|
|
||||||
# Read out location
|
# Read out location
|
||||||
loc = regions[rr, :2]
|
loc = regions[rr, :2]
|
||||||
rect_locs[rect_ind] = loc
|
rect_locs[rect_ind] = loc
|
||||||
|
|
||||||
region_size = regions[rr, 2:] - loc
|
region_size = region_sizes[rr]
|
||||||
split_horiz = region_size[0] < region_size[1]
|
split_horiz = region_size[0] < region_size[1]
|
||||||
|
|
||||||
new_region0 = regions[rr].copy()
|
new_region0 = regions[rr].copy()
|
||||||
@ -227,7 +227,7 @@ def pack_patterns(
|
|||||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
half_spacing = numpy.array(spacing, copy=False, dtype=float) / 2
|
half_spacing = numpy.asarray(spacing, dtype=float) / 2
|
||||||
|
|
||||||
bounds = [library[pp].get_bounds() for pp in patterns]
|
bounds = [library[pp].get_bounds() for pp in patterns]
|
||||||
sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds]
|
sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds]
|
||||||
@ -236,7 +236,7 @@ def pack_patterns(
|
|||||||
locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects)
|
locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects)
|
||||||
|
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
for pp, oo, loc in zip(patterns, offsets, locations):
|
for pp, oo, loc in zip(patterns, offsets, locations, strict=True):
|
||||||
pat.ref(pp, offset=oo + loc)
|
pat.ref(pp, offset=oo + loc)
|
||||||
|
|
||||||
rejects = [patterns[ii] for ii in reject_inds]
|
rejects = [patterns[ii] for ii in reject_inds]
|
||||||
|
@ -6,7 +6,7 @@ and retrieving it (`data_to_ports`).
|
|||||||
the port locations. This particular approach is just a sensible default; feel free to
|
the port locations. This particular approach is just a sensible default; feel free to
|
||||||
to write equivalent functions for your own format or alternate storage methods.
|
to write equivalent functions for your own format or alternate storage methods.
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Mapping
|
from collections.abc import Sequence, Mapping
|
||||||
import logging
|
import logging
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ def data_to_ports_flat(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated `pattern`. Port labels are not removed.
|
The updated `pattern`. Port labels are not removed.
|
||||||
"""
|
"""
|
||||||
labels = list(chain.from_iterable((pattern.labels[layer] for layer in layers)))
|
labels = list(chain.from_iterable(pattern.labels[layer] for layer in layers))
|
||||||
if not labels:
|
if not labels:
|
||||||
return pattern
|
return pattern
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
Geometric transforms
|
Geometric transforms
|
||||||
"""
|
"""
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray, ArrayLike
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
|
||||||
|
|
||||||
|
# Constants for shorthand rotations
|
||||||
|
R90 = pi / 2
|
||||||
|
R180 = pi
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
@ -57,8 +62,62 @@ def rotate_offsets_around(
|
|||||||
) -> NDArray[numpy.float64]:
|
) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Rotates offsets around a pivot point.
|
Rotates offsets around a pivot point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offsets: Nx2 array, rows are (x, y) offsets
|
||||||
|
pivot: (x, y) location to rotate around
|
||||||
|
angle: rotation angle in radians
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Nx2 ndarray of (x, y) position after the rotation is applied.
|
||||||
"""
|
"""
|
||||||
offsets -= pivot
|
offsets -= pivot
|
||||||
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
|
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
|
||||||
offsets += pivot
|
offsets += pivot
|
||||||
return offsets
|
return offsets
|
||||||
|
|
||||||
|
|
||||||
|
def apply_transforms(
|
||||||
|
outer: ArrayLike,
|
||||||
|
inner: ArrayLike,
|
||||||
|
tensor: bool = False,
|
||||||
|
) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
Apply a set of transforms (`outer`) to a second set (`inner`).
|
||||||
|
This is used to find the "absolute" transform for nested `Ref`s.
|
||||||
|
|
||||||
|
The two transforms should be of shape Ox4 and Ix4.
|
||||||
|
Rows should be of the form `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||||
|
The output will be of the form (O*I)x4 (if `tensor=False`) or OxIx4 (`tensor=True`).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
outer: Transforms for the container refs. Shape Ox4.
|
||||||
|
inner: Transforms for the contained refs. Shape Ix4.
|
||||||
|
tensor: If `True`, an OxIx4 array is returned, with `result[oo, ii, :]` corresponding
|
||||||
|
to the `oo`th `outer` transform applied to the `ii`th inner transform.
|
||||||
|
If `False` (default), this is concatenated into `(O*I)x4` to allow simple
|
||||||
|
chaining into additional `apply_transforms()` calls.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OxIx4 or (O*I)x4 array. Final dimension is
|
||||||
|
`(total_x, total_y, total_rotation_ccw_rad, net_mirrored_x)`.
|
||||||
|
"""
|
||||||
|
outer = numpy.atleast_2d(outer).astype(float, copy=False)
|
||||||
|
inner = numpy.atleast_2d(inner).astype(float, copy=False)
|
||||||
|
|
||||||
|
# If mirrored, flip y's
|
||||||
|
xy_mir = numpy.tile(inner[:, :2], (outer.shape[0], 1, 1)) # dims are outer, inner, xyrm
|
||||||
|
xy_mir[outer[:, 3].astype(bool), :, 1] *= -1
|
||||||
|
|
||||||
|
rot_mats = [rotation_matrix_2d(angle) for angle in outer[:, 2]]
|
||||||
|
xy = numpy.einsum('ort,oit->oir', rot_mats, xy_mir)
|
||||||
|
|
||||||
|
tot = numpy.empty((outer.shape[0], inner.shape[0], 4))
|
||||||
|
tot[:, :, :2] = outer[:, None, :2] + xy
|
||||||
|
tot[:, :, 2:] = outer[:, None, 2:] + inner[None, :, 2:] # sum rotations and mirrored
|
||||||
|
tot[:, :, 2] %= 2 * pi # clamp rot
|
||||||
|
tot[:, :, 3] %= 2 # clamp mirrored
|
||||||
|
|
||||||
|
if tensor:
|
||||||
|
return tot
|
||||||
|
return numpy.concatenate(tot)
|
||||||
|
@ -15,9 +15,9 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) ->
|
|||||||
(i.e. the last vertex will be removed if it is the same as the first)
|
(i.e. the last vertex will be removed if it is the same as the first)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`vertices` with no consecutive duplicates.
|
`vertices` with no consecutive duplicates. This may be a view into the original array.
|
||||||
"""
|
"""
|
||||||
vertices = numpy.array(vertices)
|
vertices = numpy.asarray(vertices)
|
||||||
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
|
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
|
||||||
if not closed_path:
|
if not closed_path:
|
||||||
duplicates[0] = False
|
duplicates[0] = False
|
||||||
@ -35,7 +35,7 @@ def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> N
|
|||||||
closed path. If `False`, the path is assumed to be open. Default `True`.
|
closed path. If `False`, the path is assumed to be open. Default `True`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`vertices` with colinear (superflous) vertices removed.
|
`vertices` with colinear (superflous) vertices removed. May be a view into the original array.
|
||||||
"""
|
"""
|
||||||
vertices = remove_duplicate_vertices(vertices)
|
vertices = remove_duplicate_vertices(vertices)
|
||||||
|
|
||||||
@ -73,8 +73,8 @@ def poly_contains_points(
|
|||||||
Returns:
|
Returns:
|
||||||
ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...]
|
ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...]
|
||||||
"""
|
"""
|
||||||
points = numpy.array(points, copy=False)
|
points = numpy.asarray(points, dtype=float)
|
||||||
vertices = numpy.array(vertices, copy=False)
|
vertices = numpy.asarray(vertices, dtype=float)
|
||||||
|
|
||||||
if points.size == 0:
|
if points.size == 0:
|
||||||
return numpy.zeros(0, dtype=numpy.int8)
|
return numpy.zeros(0, dtype=numpy.int8)
|
||||||
|
@ -39,11 +39,11 @@ classifiers = [
|
|||||||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||||
"Topic :: Scientific/Engineering :: Visualization",
|
"Topic :: Scientific/Engineering :: Visualization",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.11"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"numpy~=1.21",
|
"numpy>=1.26",
|
||||||
"klamath~=1.2",
|
"klamath~=1.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -57,3 +57,39 @@ svg = ["svgwrite"]
|
|||||||
visualize = ["matplotlib"]
|
visualize = ["matplotlib"]
|
||||||
text = ["matplotlib", "freetype-py"]
|
text = ["matplotlib", "freetype-py"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [
|
||||||
|
".git",
|
||||||
|
"dist",
|
||||||
|
]
|
||||||
|
line-length = 145
|
||||||
|
indent-width = 4
|
||||||
|
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
lint.select = [
|
||||||
|
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
|
||||||
|
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
|
||||||
|
"ARG", "PL", "R", "TRY",
|
||||||
|
"G010", "G101", "G201", "G202",
|
||||||
|
"Q002", "Q003", "Q004",
|
||||||
|
]
|
||||||
|
lint.ignore = [
|
||||||
|
#"ANN001", # No annotation
|
||||||
|
"ANN002", # *args
|
||||||
|
"ANN003", # **kwargs
|
||||||
|
"ANN401", # Any
|
||||||
|
"SIM108", # single-line if / else assignment
|
||||||
|
"RET504", # x=y+z; return x
|
||||||
|
"PIE790", # unnecessary pass
|
||||||
|
"ISC003", # non-implicit string concatenation
|
||||||
|
"C408", # dict(x=y) instead of {'x': y}
|
||||||
|
"PLR09", # Too many xxx
|
||||||
|
"PLR2004", # magic number
|
||||||
|
"PLC0414", # import x as x
|
||||||
|
"TRY003", # Long exception message
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-rsXx"
|
||||||
|
testpaths = ["masque"]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user