diff --git a/masque/label.py b/masque/label.py index b662035..8b67c65 100644 --- a/masque/label.py +++ b/masque/label.py @@ -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. diff --git a/masque/pattern.py b/masque/pattern.py index 46b564a..e515614 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -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, diff --git a/masque/ref.py b/masque/ref.py index f3241e4..6423394 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -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: diff --git a/masque/repetition.py b/masque/repetition.py index a8de94c..e1507b8 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -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)) diff --git a/masque/test/test_rotation_consistency.py b/masque/test/test_rotation_consistency.py deleted file mode 100644 index f574f52..0000000 --- a/masque/test/test_rotation_consistency.py +++ /dev/null @@ -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 diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index 2a3a9fb..ac00147 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -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 diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index 436d0a2..2517e2e 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -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 diff --git a/masque/utils/curves.py b/masque/utils/curves.py index 2348678..8b3fcc4 100644 --- a/masque/utils/curves.py +++ b/masque/utils/curves.py @@ -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] diff --git a/masque/utils/pack2d.py b/masque/utils/pack2d.py index a99b01e..ce6b006 100644 --- a/masque/utils/pack2d.py +++ b/masque/utils/pack2d.py @@ -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]