Compare commits

..

No commits in common. "cfec9e8c7651d40eac8dd1ad87bc16cf72fce1cd" and "db222373694a1911923c94c201cd41e39cc4cc46" have entirely different histories.

9 changed files with 38 additions and 234 deletions

View file

@ -98,20 +98,18 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
"""
pivot = numpy.asarray(pivot, dtype=float)
self.translate(-pivot)
if self.repetition is not None:
self.repetition.rotate(rotation)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
self.translate(+pivot)
return self
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
"""
Extrinsic transformation: Flip the label across a line in the pattern's
coordinate system. This affects both the label's offset and its
repetition grid.
Flip the label across a line in the pattern's coordinate system.
This operation mirrors the label's offset relative to the pattern's origin.
Args:
axis: Axis to mirror across. 0: x-axis (flip y), 1: y-axis (flip x).
axis: Axis to mirror across. 0 mirrors across y=0. 1 mirrors across x=0.
x: Vertical line x=val to mirror across.
y: Horizontal line y=val to mirror across.

View file

@ -2,7 +2,7 @@
Object representing a one multi-layer lithographic layout.
A single level of hierarchical references is included.
"""
from typing import cast, Self, Any, TypeVar
from typing import cast, Self, Any, TypeVar, TYPE_CHECKING
from collections.abc import Sequence, Mapping, MutableMapping, Iterable, Callable
import copy
import logging
@ -25,6 +25,8 @@ from .error import PatternError, PortError
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
from .ports import Port, PortList
if TYPE_CHECKING:
from .traits import Flippable
logger = logging.getLogger(__name__)
@ -172,8 +174,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
return s
def __copy__(self) -> 'Pattern':
logger.warning('Making a shallow copy of a Pattern... old shapes/refs/labels are re-referenced! '
'Consider using .deepcopy() if this was not intended.')
logger.warning('Making a shallow copy of a Pattern... old shapes are re-referenced!')
new = Pattern(
annotations=copy.deepcopy(self.annotations),
ports=copy.deepcopy(self.ports),
@ -746,9 +747,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
"""
Extrinsic transformation: Rotate the Pattern around the a location in the
container's coordinate system. This affects all elements' offsets and
their repetition grids.
Rotate the Pattern around the a location.
Args:
pivot: (x, y) location to rotate around
@ -767,9 +766,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def rotate_element_centers(self, rotation: float) -> Self:
"""
Extrinsic transformation part: Rotate the offsets and repetition grids of all
shapes, labels, refs, and ports around (0, 0) in the container's
coordinate system.
Rotate the offsets of all shapes, labels, refs, and ports around (0, 0)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
@ -780,15 +777,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
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)
if isinstance(entry, Repeatable) and entry.repetition is not None:
entry.repetition.rotate(rotation)
return self
def rotate_elements(self, rotation: float) -> Self:
"""
Intrinsic transformation part: Rotate each shape, ref, label, and port around its
origin (offset) in the container's coordinate system. This does NOT
affect their repetition grids.
Rotate each shape, ref, and port around its origin (offset)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
@ -796,32 +789,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
if isinstance(entry, Rotatable):
entry.rotate(rotation)
return self
def mirror_element_centers(self, axis: int = 0) -> Self:
"""
Extrinsic transformation part: Mirror the offsets and repetition grids of all
shapes, labels, refs, and ports relative to the container's origin.
Args:
axis: Axis to mirror across (0: x-axis, 1: y-axis)
Returns:
self
"""
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
cast('Positionable', entry).offset[1 - axis] *= -1
if isinstance(entry, Repeatable) and entry.repetition is not None:
entry.repetition.mirror(axis)
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
cast('Rotatable', entry).rotate(rotation)
return self
def mirror_elements(self, axis: int = 0) -> Self:
"""
Intrinsic transformation part: Mirror each shape, ref, label, and port relative
to its offset. This does NOT affect their repetition grids.
Mirror each shape, ref, and port relative to its offset.
Args:
axis: Axis to mirror across
@ -831,16 +805,14 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
if isinstance(entry, Mirrorable):
entry.mirror(axis=axis)
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
cast('Mirrorable', entry).mirror(axis=axis)
self._log_bulk_update(f"mirror_elements({axis})")
return self
def mirror(self, axis: int = 0) -> Self:
"""
Extrinsic transformation: Mirror the Pattern across an axis through its origin.
This affects all elements' offsets and their internal orientations.
Mirror the Pattern across an axis through its origin.
Args:
axis: Axis to mirror across (0: x-axis, 1: y-axis).
@ -848,8 +820,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
self.mirror_elements(axis=axis)
self.mirror_element_centers(axis=axis)
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
cast('Flippable', entry).flip_across(axis=axis)
self._log_bulk_update(f"mirror({axis})")
return self
@ -861,7 +833,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
A deep copy of the current Pattern.
"""
return self.deepcopy()
return copy.deepcopy(self)
def deepcopy(self) -> Self:
"""
@ -1196,7 +1168,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
for target, refs in pat.refs.items():
if target is None:
continue
assert library is not None
target_pat = library[target]
for ref in refs:
# Ref order of operations: mirror, rotate, scale, translate, repeat
@ -1576,9 +1547,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self._log_port_removal(ki)
map_out[vi] = None
if isinstance(other, Pattern) and not (append or skip_geometry):
raise PatternError('Must provide an `Abstract` (not a `Pattern`) when creating a reference. '
'Use `append=True` if you intended to append the full geometry.')
if isinstance(other, Pattern):
assert append or skip_geometry, 'Got a name (not an abstract) but was asked to reference (not append)'
self.place(
other,

View file

@ -166,11 +166,9 @@ class Ref(
return pattern
def rotate(self, rotation: float) -> Self:
"""
Intrinsic transformation: Rotate the target pattern relative to this Ref's
origin. This does NOT affect the repetition grid.
"""
self.rotation += rotation
if self.repetition is not None:
self.repetition.rotate(rotation)
return self
def mirror(self, axis: int = 0) -> Self:

View file

@ -397,8 +397,6 @@ class Arbitrary(Repetition):
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
if self.displacements.size == 0:
return None
xy_min = numpy.min(self.displacements, axis=0)
xy_max = numpy.max(self.displacements, axis=0)
return numpy.array((xy_min, xy_max))

View file

@ -1,133 +0,0 @@
from typing import cast
import numpy as np
from numpy.testing import assert_allclose
from ..pattern import Pattern
from ..ref import Ref
from ..label import Label
from ..repetition import Grid
def test_ref_rotate_intrinsic() -> None:
# Intrinsic rotate() should NOT affect repetition
rep = Grid(a_vector=(10, 0), a_count=2)
ref = Ref(repetition=rep)
ref.rotate(np.pi/2)
assert_allclose(ref.rotation, np.pi/2, atol=1e-10)
# Grid vector should still be (10, 0)
assert ref.repetition is not None
assert_allclose(cast('Grid', ref.repetition).a_vector, [10, 0], atol=1e-10)
def test_ref_rotate_around_extrinsic() -> None:
# Extrinsic rotate_around() SHOULD affect repetition
rep = Grid(a_vector=(10, 0), a_count=2)
ref = Ref(repetition=rep)
ref.rotate_around((0, 0), np.pi/2)
assert_allclose(ref.rotation, np.pi/2, atol=1e-10)
# Grid vector should be rotated to (0, 10)
assert ref.repetition is not None
assert_allclose(cast('Grid', ref.repetition).a_vector, [0, 10], atol=1e-10)
def test_pattern_rotate_around_extrinsic() -> None:
# Pattern.rotate_around() SHOULD affect repetition of its elements
rep = Grid(a_vector=(10, 0), a_count=2)
ref = Ref(repetition=rep)
pat = Pattern()
pat.refs['cell'].append(ref)
pat.rotate_around((0, 0), np.pi/2)
# Check the ref inside the pattern
ref_in_pat = pat.refs['cell'][0]
assert_allclose(ref_in_pat.rotation, np.pi/2, atol=1e-10)
# Grid vector should be rotated to (0, 10)
assert ref_in_pat.repetition is not None
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [0, 10], atol=1e-10)
def test_label_rotate_around_extrinsic() -> None:
# Extrinsic rotate_around() SHOULD affect repetition of labels
rep = Grid(a_vector=(10, 0), a_count=2)
lbl = Label("test", repetition=rep, offset=(5, 0))
lbl.rotate_around((0, 0), np.pi/2)
# Label offset should be (0, 5)
assert_allclose(lbl.offset, [0, 5], atol=1e-10)
# Grid vector should be rotated to (0, 10)
assert lbl.repetition is not None
assert_allclose(cast('Grid', lbl.repetition).a_vector, [0, 10], atol=1e-10)
def test_pattern_rotate_elements_intrinsic() -> None:
# rotate_elements() should NOT affect repetition
rep = Grid(a_vector=(10, 0), a_count=2)
ref = Ref(repetition=rep)
pat = Pattern()
pat.refs['cell'].append(ref)
pat.rotate_elements(np.pi/2)
ref_in_pat = pat.refs['cell'][0]
assert_allclose(ref_in_pat.rotation, np.pi/2, atol=1e-10)
# Grid vector should still be (10, 0)
assert ref_in_pat.repetition is not None
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [10, 0], atol=1e-10)
def test_pattern_rotate_element_centers_extrinsic() -> None:
# rotate_element_centers() SHOULD affect repetition and offset
rep = Grid(a_vector=(10, 0), a_count=2)
ref = Ref(repetition=rep, offset=(5, 0))
pat = Pattern()
pat.refs['cell'].append(ref)
pat.rotate_element_centers(np.pi/2)
ref_in_pat = pat.refs['cell'][0]
# Offset should be (0, 5)
assert_allclose(ref_in_pat.offset, [0, 5], atol=1e-10)
# Grid vector should be rotated to (0, 10)
assert ref_in_pat.repetition is not None
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [0, 10], atol=1e-10)
# Ref rotation should NOT be changed
assert_allclose(ref_in_pat.rotation, 0, atol=1e-10)
def test_pattern_mirror_elements_intrinsic() -> None:
# mirror_elements() should NOT affect repetition or offset
rep = Grid(a_vector=(10, 5), a_count=2)
ref = Ref(repetition=rep, offset=(5, 2))
pat = Pattern()
pat.refs['cell'].append(ref)
pat.mirror_elements(axis=0) # Mirror across x (flip y)
ref_in_pat = pat.refs['cell'][0]
assert ref_in_pat.mirrored is True
# Repetition and offset should be unchanged
assert ref_in_pat.repetition is not None
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [10, 5], atol=1e-10)
assert_allclose(ref_in_pat.offset, [5, 2], atol=1e-10)
def test_pattern_mirror_element_centers_extrinsic() -> None:
# mirror_element_centers() SHOULD affect repetition and offset
rep = Grid(a_vector=(10, 5), a_count=2)
ref = Ref(repetition=rep, offset=(5, 2))
pat = Pattern()
pat.refs['cell'].append(ref)
pat.mirror_element_centers(axis=0) # Mirror across x (flip y)
ref_in_pat = pat.refs['cell'][0]
# Offset should be (5, -2)
assert_allclose(ref_in_pat.offset, [5, -2], atol=1e-10)
# Grid vector should be (10, -5)
assert ref_in_pat.repetition is not None
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [10, -5], atol=1e-10)
# Ref mirrored state should NOT be changed
assert ref_in_pat.mirrored is False

View file

@ -18,8 +18,7 @@ class Mirrorable(metaclass=ABCMeta):
@abstractmethod
def mirror(self, axis: int = 0) -> Self:
"""
Intrinsic transformation: Mirror the entity across an axis through its origin.
This does NOT affect the object's repetition grid.
Mirror the entity across an axis through its origin.
This operation is performed relative to the object's internal origin (ignoring
its offset). For objects like `Polygon` and `Path` where the offset is forced
@ -76,8 +75,7 @@ class Flippable(Positionable, metaclass=ABCMeta):
@abstractmethod
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
"""
Extrinsic transformation: Mirror the object across a line in the container's
coordinate system. This affects both the object's offset and its repetition grid.
Mirror the object across a line in the container's coordinate system.
Unlike `mirror()`, this operation is performed relative to the container's origin
(e.g. the `Pattern` origin, in the case of shapes) and takes the object's offset

View file

@ -25,8 +25,7 @@ class Rotatable(metaclass=ABCMeta):
@abstractmethod
def rotate(self, val: float) -> Self:
"""
Intrinsic transformation: Rotate the shape around its origin (0, 0), ignoring its offset.
This does NOT affect the object's repetition grid.
Rotate the shape around its origin (0, 0), ignoring its offset.
Args:
val: Angle to rotate by (counterclockwise, radians)
@ -64,10 +63,6 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
# Methods
#
def rotate(self, rotation: float) -> Self:
"""
Intrinsic transformation: Rotate the shape around its origin (0, 0), ignoring its offset.
This does NOT affect the object's repetition grid.
"""
self.rotation += rotation
return self
@ -87,7 +82,7 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
class Pivotable(Positionable, metaclass=ABCMeta):
"""
Trait class for entities which can be rotated around a point.
Trait class for entites which can be rotated around a point.
This requires that they are `Positionable` but not necessarily `Rotatable` themselves.
"""
__slots__ = ()
@ -95,11 +90,7 @@ class Pivotable(Positionable, metaclass=ABCMeta):
@abstractmethod
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
"""
Extrinsic transformation: Rotate the object around a point in the container's
coordinate system. This affects both the object's offset and its repetition grid.
For objects that are also `Rotatable`, this also performs an intrinsic
rotation of the object.
Rotate the object around a point.
Args:
pivot: Point (x, y) to rotate around
@ -119,12 +110,9 @@ class PivotableImpl(Pivotable, Rotatable, metaclass=ABCMeta):
__slots__ = ()
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
from .repeatable import Repeatable #noqa: PLC0415
pivot = numpy.asarray(pivot, dtype=float)
self.translate(-pivot)
self.rotate(rotation)
if isinstance(self, Repeatable) and self.repetition is not None:
self.repetition.rotate(rotation)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
self.translate(+pivot)
return self

View file

@ -69,25 +69,14 @@ def euler_bend(
num_points_arc = num_points - 2 * num_points_spiral
def gen_spiral(ll_max: float) -> NDArray[numpy.float64]:
if ll_max == 0:
return numpy.zeros((num_points_spiral, 2))
resolution = 100000
qq = numpy.linspace(0, ll_max, resolution)
dx = numpy.cos(qq * qq / 2)
dy = -numpy.sin(qq * qq / 2)
dq = ll_max / (resolution - 1)
ix = numpy.zeros(resolution)
iy = numpy.zeros(resolution)
ix[1:] = numpy.cumsum((dx[:-1] + dx[1:]) / 2) * dq
iy[1:] = numpy.cumsum((dy[:-1] + dy[1:]) / 2) * dq
ll_target = numpy.linspace(0, ll_max, num_points_spiral)
x_target = numpy.interp(ll_target, qq, ix)
y_target = numpy.interp(ll_target, qq, iy)
return numpy.stack((x_target, y_target), axis=1)
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]

View file

@ -236,9 +236,7 @@ def pack_patterns(
locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects)
pat = Pattern()
for ii, (pp, oo, loc) in enumerate(zip(patterns, offsets, locations, strict=True)):
if ii in reject_inds:
continue
for pp, oo, loc in zip(patterns, offsets, locations, strict=True):
pat.ref(pp, offset=oo + loc)
rejects = [patterns[ii] for ii in reject_inds]