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