diff --git a/examples/test_rep.py b/examples/test_rep.py index e4dbe28..dbc6d72 100644 --- a/examples/test_rep.py +++ b/examples/test_rep.py @@ -17,7 +17,8 @@ def main(): cell_name = 'ellip_grating' pat = masque.Pattern() for rmin in numpy.arange(10, 15, 0.5): - pat.shapes.append(Arc( + layer = (0, 0) + pat.shapes[layer].append(Arc( radii=(rmin, rmin), width=0.1, angles=(0 * -pi/4, pi/4), @@ -35,27 +36,27 @@ def main(): print(f'\nAdded a copy of {cell_name} as {new_name}') pat3 = Pattern() - pat3.refs = [ - Ref(cell_name, offset=(1e5, 3e5), annotations={'4': ['Hello I am the base Ref']}), - Ref(cell_name, offset=(2e5, 3e5), rotation=pi/3), - Ref(cell_name, offset=(3e5, 3e5), rotation=pi/2), - Ref(cell_name, offset=(4e5, 3e5), rotation=pi), - Ref(cell_name, offset=(5e5, 3e5), rotation=3*pi/2), - Ref(cell_name, mirrored=(True, False), offset=(1e5, 4e5)), - Ref(cell_name, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3), - Ref(cell_name, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2), - Ref(cell_name, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi), - Ref(cell_name, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2), - Ref(cell_name, mirrored=(False, True), offset=(1e5, 5e5)), - Ref(cell_name, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3), - Ref(cell_name, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2), - Ref(cell_name, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi), - Ref(cell_name, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2), - Ref(cell_name, mirrored=(True, True), offset=(1e5, 6e5)), - Ref(cell_name, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3), - Ref(cell_name, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2), - Ref(cell_name, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi), - Ref(cell_name, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2), + pat3.refs[cell_name] = [ + Ref(offset=(1e5, 3e5), annotations={'4': ['Hello I am the base Ref']}), + Ref(offset=(2e5, 3e5), rotation=pi/3), + Ref(offset=(3e5, 3e5), rotation=pi/2), + Ref(offset=(4e5, 3e5), rotation=pi), + Ref(offset=(5e5, 3e5), rotation=3*pi/2), + Ref(mirrored=True, offset=(1e5, 4e5)), + Ref(mirrored=True, offset=(2e5, 4e5), rotation=pi/3), + Ref(mirrored=True, offset=(3e5, 4e5), rotation=pi/2), + Ref(mirrored=True, offset=(4e5, 4e5), rotation=pi), + Ref(mirrored=True, offset=(5e5, 4e5), rotation=3*pi/2), + Ref(offset=(1e5, 5e5)).mirror_target(1), + Ref(offset=(2e5, 5e5), rotation=pi/3).mirror_target(1), + Ref(offset=(3e5, 5e5), rotation=pi/2).mirror_target(1), + Ref(offset=(4e5, 5e5), rotation=pi).mirror_target(1), + Ref(offset=(5e5, 5e5), rotation=3*pi/2).mirror_target(1), + Ref(offset=(1e5, 6e5)).mirror2d_target(True, True), + Ref(offset=(2e5, 6e5), rotation=pi/3).mirror2d_target(True, True), + Ref(offset=(3e5, 6e5), rotation=pi/2).mirror2d_target(True, True), + Ref(offset=(4e5, 6e5), rotation=pi).mirror2d_target(True, True), + Ref(offset=(5e5, 6e5), rotation=3*pi/2).mirror2d_target(True, True), ] lib['sref_test'] = pat3 @@ -70,27 +71,27 @@ def main(): b_count=2, ) pat4 = Pattern() - pat4.refs = [ - Ref(cell_name, repetition=rep, offset=(1e5, 3e5)), - Ref(cell_name, repetition=rep, offset=(2e5, 3e5), rotation=pi/3), - Ref(cell_name, repetition=rep, offset=(3e5, 3e5), rotation=pi/2), - Ref(cell_name, repetition=rep, offset=(4e5, 3e5), rotation=pi), - Ref(cell_name, repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2), - Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(1e5, 4e5)), - Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3), - Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2), - Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi), - Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2), - Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(1e5, 5e5)), - Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3), - Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2), - Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi), - Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2), - Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(1e5, 6e5)), - Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3), - Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2), - Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi), - Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2), + pat4.refs[cell_name] = [ + Ref(repetition=rep, offset=(1e5, 3e5)), + Ref(repetition=rep, offset=(2e5, 3e5), rotation=pi/3), + Ref(repetition=rep, offset=(3e5, 3e5), rotation=pi/2), + Ref(repetition=rep, offset=(4e5, 3e5), rotation=pi), + Ref(repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2), + Ref(repetition=rep, mirrored=True, offset=(1e5, 4e5)), + Ref(repetition=rep, mirrored=True, offset=(2e5, 4e5), rotation=pi/3), + Ref(repetition=rep, mirrored=True, offset=(3e5, 4e5), rotation=pi/2), + Ref(repetition=rep, mirrored=True, offset=(4e5, 4e5), rotation=pi), + Ref(repetition=rep, mirrored=True, offset=(5e5, 4e5), rotation=3*pi/2), + Ref(repetition=rep, offset=(1e5, 5e5)).mirror_target(1), + Ref(repetition=rep, offset=(2e5, 5e5), rotation=pi/3).mirror_target(1), + Ref(repetition=rep, offset=(3e5, 5e5), rotation=pi/2).mirror_target(1), + Ref(repetition=rep, offset=(4e5, 5e5), rotation=pi).mirror_target(1), + Ref(repetition=rep, offset=(5e5, 5e5), rotation=3*pi/2).mirror_target(1), + Ref(repetition=rep, offset=(1e5, 6e5)).mirror2d_target(True, True), + Ref(repetition=rep, offset=(2e5, 6e5), rotation=pi/3).mirror2d_target(True, True), + Ref(repetition=rep, offset=(3e5, 6e5), rotation=pi/2).mirror2d_target(True, True), + Ref(repetition=rep, offset=(4e5, 6e5), rotation=pi).mirror2d_target(True, True), + Ref(repetition=rep, offset=(5e5, 6e5), rotation=3*pi/2).mirror2d_target(True, True), ] lib['aref_test'] = pat4 diff --git a/masque/abstract.py b/masque/abstract.py index 0f06144..bdc2952 100644 --- a/masque/abstract.py +++ b/masque/abstract.py @@ -7,7 +7,7 @@ from numpy.typing import ArrayLike from .ref import Ref from .ports import PortList, Port -from .utils import rotation_matrix_2d, normalize_mirror +from .utils import rotation_matrix_2d #if TYPE_CHECKING: # from .builder import Builder, Tool @@ -143,7 +143,7 @@ class Abstract(PortList): port.rotate(rotation) return self - def mirror_port_offsets(self, across_axis: int) -> Self: + def mirror_port_offsets(self, across_axis: int = 0) -> Self: """ Mirror the offsets of all shapes, labels, and refs across an axis @@ -158,7 +158,7 @@ class Abstract(PortList): port.offset[across_axis - 1] *= -1 return self - def mirror_ports(self, across_axis: int) -> Self: + def mirror_ports(self, across_axis: int = 0) -> Self: """ Mirror each port's rotation across an axis, relative to its offset @@ -174,7 +174,7 @@ class Abstract(PortList): port.mirror(across_axis) return self - def mirror(self, across_axis: int) -> Self: + def mirror(self, across_axis: int = 0) -> Self: """ Mirror the Pattern across an axis @@ -200,11 +200,10 @@ class Abstract(PortList): Returns: self """ - mirrored_across_x, angle = normalize_mirror(ref.mirrored) - if mirrored_across_x: - self.mirror(across_axis=0) - self.rotate_ports(angle + ref.rotation) - self.rotate_port_offsets(angle + ref.rotation) + if ref.mirrored: + self.mirror() + self.rotate_ports(ref.rotation) + self.rotate_port_offsets(ref.rotation) self.translate_ports(ref.offset) return self @@ -221,10 +220,9 @@ class Abstract(PortList): # TODO test undo_ref_transform """ - mirrored_across_x, angle = normalize_mirror(ref.mirrored) self.translate_ports(-ref.offset) - self.rotate_port_offsets(-angle - ref.rotation) - self.rotate_ports(-angle - ref.rotation) - if mirrored_across_x: - self.mirror(across_axis=0) + self.rotate_port_offsets(-ref.rotation) + self.rotate_ports(-ref.rotation) + if ref.mirrored: + self.mirror(0) return self diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 0bae1c2..273361b 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -230,7 +230,7 @@ class Builder(PortList): # map_in: dict[str, str], # map_out: dict[str, str | None] | None, # *, -# mirrored: tuple[bool, bool], +# mirrored: bool = False, # inherit_name: bool, # set_rotation: bool | None, # append: bool, @@ -244,7 +244,7 @@ class Builder(PortList): # map_in: dict[str, str], # map_out: dict[str, str | None] | None = None, # *, -# mirrored: tuple[bool, bool] = (False, False), +# mirrored: bool = False, # inherit_name: bool = True, # set_rotation: bool | None = None, # append: bool = False, @@ -257,7 +257,7 @@ class Builder(PortList): map_in: dict[str, str], map_out: dict[str, str | None] | None = None, *, - mirrored: tuple[bool, bool] = (False, False), + mirrored: bool = False, inherit_name: bool = True, set_rotation: bool | None = None, append: bool = False, @@ -351,11 +351,9 @@ class Builder(PortList): if isinstance(other, Pattern): assert append - self.place(other, offset=translation, rotation=rotation, pivot=pivot, - mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append) - else: - self.place(other, offset=translation, rotation=rotation, pivot=pivot, - mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append) + + self.place(other, offset=translation, rotation=rotation, pivot=pivot, + mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append) return self # @overload @@ -366,7 +364,7 @@ class Builder(PortList): # offset: ArrayLike, # rotation: float, # pivot: ArrayLike, -# mirrored: tuple[bool, bool], +# mirrored: bool = False, # port_map: dict[str, str | None] | None, # skip_port_check: bool, # append: bool, @@ -381,7 +379,7 @@ class Builder(PortList): # offset: ArrayLike, # rotation: float, # pivot: ArrayLike, -# mirrored: tuple[bool, bool], +# mirrored: bool = False, # port_map: dict[str, str | None] | None, # skip_port_check: bool, # append: Literal[True], @@ -395,7 +393,7 @@ class Builder(PortList): offset: ArrayLike = (0, 0), rotation: float = 0, pivot: ArrayLike = (0, 0), - mirrored: tuple[bool, bool] = (False, False), + mirrored: bool = False, port_map: dict[str, str | None] | None = None, skip_port_check: bool = False, append: bool = False, @@ -461,7 +459,8 @@ class Builder(PortList): for name, port in ports.items(): p = port.deepcopy() - p.mirror2d(mirrored) + if mirrored: + p.mirror() p.rotate_around(pivot, rotation) p.translate(offset) self.ports[name] = p @@ -476,7 +475,8 @@ class Builder(PortList): other_pat = self.library[name] other_copy = other_pat.deepcopy() other_copy.ports.clear() - other_copy.mirror2d(mirrored) + if mirrored: + other_copy.mirror() other_copy.rotate_around(pivot, rotation) other_copy.translate_elements(offset) self.pattern.append(other_copy) @@ -517,7 +517,7 @@ class Builder(PortList): port.rotate_around(pivot, angle) return self - def mirror(self, axis: int) -> Self: + def mirror(self, axis: int = 0) -> Self: """ Mirror the pattern and all ports across the specified axis. @@ -528,8 +528,6 @@ class Builder(PortList): self """ self.pattern.mirror(axis) - for p in self.ports.values(): - p.mirror(axis) return self def set_dead(self) -> Self: diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 9194653..852bf2e 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -13,7 +13,6 @@ from ..library import ILibrary from ..error import PortError, BuildError from ..ports import PortList, Port from ..abstract import Abstract -from ..utils import rotation_matrix_2d from ..utils import SupportsBool from .tools import Tool, RenderStep from .utils import ell @@ -94,7 +93,6 @@ class RenderPather(PortList): else: self.tools = dict(tools) - @classmethod def interface( cls, @@ -200,7 +198,7 @@ class RenderPather(PortList): map_in: dict[str, str], map_out: dict[str, str | None] | None = None, *, - mirrored: tuple[bool, bool] = (False, False), + mirrored: bool = False, inherit_name: bool = True, set_rotation: bool | None = None, ) -> Self: @@ -250,7 +248,7 @@ class RenderPather(PortList): offset: ArrayLike = (0, 0), rotation: float = 0, pivot: ArrayLike = (0, 0), - mirrored: tuple[bool, bool] = (False, False), + mirrored: bool = False, port_map: dict[str, str | None] | None = None, skip_port_check: bool = False, ) -> Self: @@ -280,7 +278,8 @@ class RenderPather(PortList): for name, port in ports.items(): p = port.deepcopy() - p.mirror2d(mirrored) + if mirrored: + p.mirror() p.rotate_around(pivot, rotation) p.translate(offset) self.ports[name] = p diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 2aadc6f..b411295 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -125,7 +125,7 @@ class BasicTool(Tool, metaclass=ABCMeta): bb.plug(straight, {port_names[1]: sport_in}) if data.ccw is not None: bend, bport_in, bport_out = self.bend - bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw))) + bb.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw)) if data.out_transition: opat, oport_theirs, oport_ours = data.out_transition bb.plug(opat, {port_names[1]: oport_ours}) @@ -239,7 +239,7 @@ class BasicTool(Tool, metaclass=ABCMeta): bb.plug(straight, {port_names[1]: sport_in}, append=True) if ccw is not None: bend, bport_in, bport_out = self.bend - bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw))) + bb.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw)) if out_transition: opat, oport_theirs, oport_ours = out_transition bb.plug(opat, {port_names[1]: oport_ours}) @@ -256,7 +256,6 @@ class PathTool(Tool, metaclass=ABCMeta): #class LData: # dxy: NDArray[numpy.float64] - #def __init__(self, layer: layer_t, width: float, ptype: str = 'unk') -> None: # Tool.__init__(self) # self.layer = layer diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 8875672..31582b7 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -21,7 +21,7 @@ from .. import Pattern, Ref, PatternError, Label from ..library import ILibraryView, LibraryView, Library from ..shapes import Shape, Polygon, Path from ..repetition import Grid -from ..utils import rotation_matrix_2d, layer_t +from ..utils import rotation_matrix_2d, layer_t, normalize_mirror logger = logging.getLogger(__name__) @@ -258,8 +258,8 @@ def _read_block(block) -> tuple[str, Pattern]: if abs(xscale) != abs(yscale): logger.warning('Masque does not support per-axis scaling; using x-scaling only!') scale = abs(xscale) - mirrored = (yscale < 0, xscale < 0) - rotation = numpy.deg2rad(attr.get('rotation', 0)) + mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0)) + rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2] @@ -291,8 +291,8 @@ def _mrefs_to_drefs( def mk_blockref(encoded_name: str, ref: Ref) -> None: rotation = numpy.rad2deg(ref.rotation) % 360 attribs = dict( - xscale=ref.scale * (-1 if ref.mirrored[1] else 1), - yscale=ref.scale * (-1 if ref.mirrored[0] else 1), + xscale=ref.scale, + yscale=ref.scale * (-1 if ref.mirrored else 1), rotation=rotation, ) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 039cc55..e4175fa 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -37,7 +37,7 @@ from .utils import is_gzipped, tmpfile from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid -from ..utils import layer_t, normalize_mirror, annotations_t +from ..utils import layer_t, annotations_t from ..library import LazyLibrary, Library, ILibrary, ILibraryView @@ -306,7 +306,7 @@ def _gref_to_mref(ref: klamath.library.Reference) -> tuple[str, Ref]: offset=offset, rotation=numpy.deg2rad(ref.angle_deg), scale=ref.mag, - mirrored=(ref.invert_y, False), + mirrored=ref.invert_y, annotations=_properties_to_annotations(ref.properties), repetition=repetition, ) @@ -348,10 +348,9 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R continue encoded_name = target.encode('ASCII') for ref in rseq: - # Note: GDS mirrors first and rotates second - mirror_across_x, extra_angle = normalize_mirror(ref.mirrored) + # Note: GDS also mirrors first and rotates second rep = ref.repetition - angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360 + angle_deg = numpy.rad2deg(ref.rotation) % 360 properties = _annotations_to_properties(ref.annotations, 512) if isinstance(rep, Grid): @@ -367,7 +366,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R xy=rint_cast(xy), colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)), angle_deg=angle_deg, - invert_y=mirror_across_x, + invert_y=ref.mirrored, mag=ref.scale, properties=properties, ) @@ -378,7 +377,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R xy=rint_cast([ref.offset]), colrow=None, angle_deg=angle_deg, - invert_y=mirror_across_x, + invert_y=ref.mirrored, mag=ref.scale, properties=properties, ) @@ -390,7 +389,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R xy=rint_cast([ref.offset + dd]), colrow=None, angle_deg=angle_deg, - invert_y=mirror_across_x, + invert_y=ref.mirrored, mag=ref.scale, properties=properties, ) diff --git a/masque/file/oasis.py b/masque/file/oasis.py index d05c0a9..befa325 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -32,7 +32,7 @@ from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from ..library import Library, ILibrary from ..shapes import Path, Circle from ..repetition import Grid, Arbitrary, Repetition -from ..utils import layer_t, normalize_mirror, annotations_t +from ..utils import layer_t, annotations_t logger = logging.getLogger(__name__) @@ -494,7 +494,7 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) rotation = numpy.deg2rad(float(placement.angle)) ref = Ref( offset=xy, - mirrored=(placement.flip, False), + mirrored=placement.flip, rotation=rotation, scale=float(mag), repetition=repetition_fata2masq(placement.repetition), @@ -511,15 +511,14 @@ def _refs_to_placements( if target is None: continue for ref in rseq: - # Note: OASIS mirrors first and rotates second - mirror_across_x, extra_angle = normalize_mirror(ref.mirrored) + # Note: OASIS also mirrors first and rotates second frep, rep_offset = repetition_masq2fata(ref.repetition) offset = rint_cast(ref.offset + rep_offset) - angle = numpy.rad2deg(ref.rotation + extra_angle) % 360 + angle = numpy.rad2deg(ref.rotation) % 360 placement = fatrec.Placement( name=target, - flip=mirror_across_x, + flip=ref.mirrored, angle=angle, magnification=ref.scale, properties=annotations_to_properties(ref.annotations), diff --git a/masque/library.py b/masque/library.py index 2f107ed..6957031 100644 --- a/masque/library.py +++ b/masque/library.py @@ -20,7 +20,7 @@ import numpy from numpy.typing import ArrayLike from .error import LibraryError, PatternError -from .utils import rotation_matrix_2d, normalize_mirror +from .utils import rotation_matrix_2d from .shapes import Shape, Polygon from .label import Label from .abstract import Abstract @@ -410,9 +410,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if transform[3]: sign[1] = -1 xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign) - mirror_x, angle = normalize_mirror(ref.mirrored) - angle += ref.rotation - ref_transform = transform + (xy[0], xy[1], angle, mirror_x) + ref_transform = transform + (xy[0], xy[1], ref.rotation, ref.mirrored) ref_transform[3] %= 2 else: ref_transform = False diff --git a/masque/pattern.py b/masque/pattern.py index 2fc66c7..4be3e3f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -15,7 +15,7 @@ from numpy.typing import NDArray, ArrayLike from .ref import Ref from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES from .label import Label -from .utils import rotation_matrix_2d, annotations_t, layer_t, normalize_mirror +from .utils import rotation_matrix_2d, annotations_t, layer_t from .error import PatternError from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded from .ports import Port, PortList @@ -370,12 +370,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): bounds = None else: ubounds = unrot_bounds.copy() - mirr_x, rot2 = normalize_mirror(ref.mirrored) - if mirr_x: + if ref.mirrored: ubounds[:, 1] *= -1 - # note: rounding fixes up sin/cos inaccuracy, probably unnecessary - corners = (numpy.round(rotation_matrix_2d(ref.rotation + rot2)) @ ubounds.T).T + 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] @@ -518,7 +516,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): cast(Rotatable, entry).rotate(rotation) return self - def mirror_element_centers(self, across_axis: int) -> Self: + def mirror_element_centers(self, across_axis: int = 0) -> Self: """ Mirror the offsets of all shapes, labels, and refs across an axis @@ -533,7 +531,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): cast(Positionable, entry).offset[across_axis - 1] *= -1 return self - def mirror_elements(self, across_axis: int) -> Self: + def mirror_elements(self, across_axis: int = 0) -> Self: """ Mirror each shape, ref, and pattern across an axis, relative to its offset @@ -549,7 +547,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): cast(Mirrorable, entry).mirror(across_axis) return self - def mirror(self, across_axis: int) -> Self: + def mirror(self, across_axis: int = 0) -> Self: """ Mirror the Pattern across an axis diff --git a/masque/ports.py b/masque/ports.py index 688ed7a..add336c 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -76,7 +76,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable): self.ptype = ptype return self - def mirror(self, axis: int) -> Self: + def mirror(self, axis: int = 0) -> Self: self.offset[1 - axis] *= -1 if self.rotation is not None: self.rotation *= -1 @@ -275,7 +275,7 @@ class PortList(metaclass=ABCMeta): other: 'PortList', map_in: dict[str, str], *, - mirrored: tuple[bool, bool] = (False, False), + mirrored: bool = False, set_rotation: bool | None = None, ) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]: """ @@ -286,7 +286,7 @@ class PortList(metaclass=ABCMeta): other: a device map_in: dict of `{'self_port': 'other_port'}` mappings, specifying port connections between the two devices. - mirrored: Mirrors `other` across the x or y axes prior to + mirrored: Mirrors `other` across the x axis prior to connecting any ports. set_rotation: If the necessary rotation cannot be determined from the ports being connected (i.e. all pairs have at least one @@ -317,7 +317,7 @@ class PortList(metaclass=ABCMeta): o_ports: Mapping[str, Port], map_in: dict[str, str], *, - mirrored: tuple[bool, bool] = (False, False), + mirrored: bool = False, set_rotation: bool | None = None, ) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]: """ @@ -330,7 +330,7 @@ class PortList(metaclass=ABCMeta): o_ports: A list of ports which are to be moved/mirrored. map_in: dict of `{'s_port': 'o_port'}` mappings, specifying port connections. - mirrored: Mirrors `o_ports` across the x or y axes prior to + mirrored: Mirrors `o_ports` across the x axis prior to connecting any ports. set_rotation: If the necessary rotation cannot be determined from the ports being connected (i.e. all pairs have at least one @@ -356,13 +356,9 @@ class PortList(metaclass=ABCMeta): o_has_rot = numpy.array([p.rotation is not None for p in o_ports.values()], dtype=bool) has_rot = s_has_rot & o_has_rot - if mirrored[0]: + if mirrored: o_offsets[:, 1] *= -1 o_rotations *= -1 - if mirrored[1]: - o_offsets[:, 0] *= -1 - o_rotations *= -1 - o_rotations += pi type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk' for st, ot in zip(s_types, o_types)]) diff --git a/masque/ref.py b/masque/ref.py index 68a3e77..a3239a3 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -4,15 +4,14 @@ """ #TODO more top-level documentation -from typing import Sequence, Mapping, TYPE_CHECKING, Any, Self +from typing import Mapping, TYPE_CHECKING, Self import copy import numpy from numpy import pi from numpy.typing import NDArray, ArrayLike -from .error import PatternError -from .utils import is_scalar, annotations_t +from .utils import annotations_t from .repetition import Repetition from .traits import ( PositionableImpl, RotatableImpl, ScalableImpl, @@ -31,6 +30,8 @@ class Ref( """ `Ref` provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. + + Note: Order is (mirror, rotate, scale, translate, repeat) """ __slots__ = ( '_mirrored', @@ -38,32 +39,41 @@ class Ref( '_offset', '_rotation', 'scale', '_repetition', '_annotations', ) - _mirrored: NDArray[numpy.bool_] - """ Whether to mirror the instance across the x and/or y axes. """ + _mirrored: bool + """ Whether to mirror the instance across the x axis (new_y = -old_y)ubefore rotating. """ + + # Mirrored property + @property + def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool + return self._mirrored + + @mirrored.setter + def mirrored(self, val: bool) -> None: + self._mirrored = bool(val) def __init__( self, *, offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, - mirrored: Sequence[bool] | None = None, + mirrored: bool = False, scale: float = 1.0, repetition: Repetition | None = None, annotations: annotations_t | None = None, ) -> None: """ + Note: Order is (mirror, rotate, scale, translate, repeat) + Args: offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc. rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0). - mirrored: Whether to mirror the referenced pattern across its x and y axes. + mirrored: Whether to mirror the referenced pattern across its x axis before rotating. scale: Scaling factor applied to the pattern's geometry. repetition: `Repetition` object, default `None` """ self.offset = offset self.rotation = rotation self.scale = scale - if mirrored is None: - mirrored = (False, False) self.mirrored = mirrored self.repetition = repetition self.annotations = annotations if annotations is not None else {} @@ -73,7 +83,7 @@ class Ref( offset=self.offset.copy(), rotation=self.rotation, scale=self.scale, - mirrored=self.mirrored.copy(), + mirrored=self.mirrored, repetition=copy.deepcopy(self.repetition), annotations=copy.deepcopy(self.annotations), ) @@ -86,17 +96,6 @@ class Ref( new.annotations = copy.deepcopy(self.annotations, memo) return new - # Mirrored property - @property - def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]: - return self._mirrored - - @mirrored.setter - def mirrored(self, val: ArrayLike) -> None: - if is_scalar(val): - raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = numpy.array(val, dtype=bool, copy=True) - def as_pattern( self, pattern: 'Pattern', @@ -113,8 +112,8 @@ class Ref( if self.scale != 1: pattern.scale_by(self.scale) - if numpy.any(self.mirrored): - pattern.mirror2d(self.mirrored) + if self.mirrored: + pattern.mirror() if self.rotation % (2 * pi) != 0: pattern.rotate_around((0.0, 0.0), self.rotation) if numpy.any(self.offset): @@ -137,13 +136,24 @@ class Ref( self.repetition.rotate(rotation) return self - def mirror(self, axis: int) -> Self: - self.mirrored[axis] = not self.mirrored[axis] + def mirror(self, axis: int = 0) -> Self: + self.mirror_target(axis) self.rotation *= -1 if self.repetition is not None: self.repetition.mirror(axis) return self + def mirror_target(self, axis: int = 0) -> Self: + self.mirrored = not self.mirrored + self.rotation += axis * pi + return self + + def mirror2d_target(self, across_x: bool = False, across_y: bool = False) -> Self: + self.mirrored = bool((self.mirrored + across_x + across_y) % 2) + if across_y: + self.rotation += pi + return self + def get_bounds_single( self, pattern: 'Pattern', @@ -169,5 +179,5 @@ class Ref( def __repr__(self) -> str: rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' scale = f' d{self.scale:g}' if self.scale != 1 else '' - mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' + mirrored = ' m' if self.mirrored else '' return f'' diff --git a/masque/repetition.py b/masque/repetition.py index d77e47f..0cb9e69 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -2,8 +2,7 @@ Repetitions provide support for efficiently representing multiple identical instances of an object . """ - -from typing import Any, Type +from typing import Any, Type, Self, TypeVar import copy from abc import ABCMeta, abstractmethod @@ -15,6 +14,9 @@ from .error import PatternError from .utils import rotation_matrix_2d +GG = TypeVar('GG', bound='Grid') + + class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta): """ Interface common to all objects which specify repetitions @@ -104,12 +106,12 @@ class Grid(Repetition): @classmethod def aligned( - cls: Type, + cls: Type[GG], x: float, y: float, x_count: int, y_count: int, - ) -> 'Grid': + ) -> GG: """ Simple constructor for an axis-aligned 2D grid @@ -133,7 +135,7 @@ class Grid(Repetition): ) return new - def __deepcopy__(self, memo: dict | None = None) -> 'Grid': + def __deepcopy__(self, memo: dict | None = None) -> Self: memo = {} if memo is None else memo new = copy.copy(self) return new @@ -197,7 +199,7 @@ class Grid(Repetition): return (aa.flatten()[:, None] * self.a_vector[None, :] + bb.flatten()[:, None] * self.b_vector[None, :]) # noqa - def rotate(self, rotation: float) -> 'Grid': + def rotate(self, rotation: float) -> Self: """ Rotate lattice vectors (around (0, 0)) @@ -212,7 +214,7 @@ class Grid(Repetition): self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector) return self - def mirror(self, axis: int) -> 'Grid': + def mirror(self, axis: int = 0) -> Self: """ Mirror the Grid across an axis. @@ -248,7 +250,7 @@ class Grid(Repetition): xy_max = numpy.max(corners, axis=0) return numpy.array((xy_min, xy_max)) - def scale_by(self, c: float) -> 'Grid': + def scale_by(self, c: float) -> Self: """ Scale the Grid by a factor @@ -327,7 +329,7 @@ class Arbitrary(Repetition): return False return numpy.array_equal(self.displacements, other.displacements) - def rotate(self, rotation: float) -> 'Arbitrary': + def rotate(self, rotation: float) -> Self: """ Rotate dispacements (around (0, 0)) @@ -340,7 +342,7 @@ class Arbitrary(Repetition): self.displacements = numpy.dot(rotation_matrix_2d(rotation), self.displacements.T).T return self - def mirror(self, axis: int) -> 'Arbitrary': + def mirror(self, axis: int = 0) -> Self: """ Mirror the displacements across an axis. @@ -366,7 +368,7 @@ class Arbitrary(Repetition): xy_max = numpy.max(self.displacements, axis=0) return numpy.array((xy_min, xy_max)) - def scale_by(self, c: float) -> 'Arbitrary': + def scale_by(self, c: float) -> Self: """ Scale the displacements by a factor diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 56318a5..17ed3fe 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,4 +1,4 @@ -from typing import Sequence, Any +from typing import Any import copy import math @@ -155,7 +155,6 @@ class Arc(Shape): *, offset: ArrayLike = (0.0, 0.0), rotation: float = 0, - mirrored: Sequence[bool] = (False, False), repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -179,7 +178,6 @@ class Arc(Shape): self.rotation = rotation self.repetition = repetition self.annotations = annotations if annotations is not None else {} - [self.mirror(a) for a, do in enumerate(mirrored) if do] def __deepcopy__(self, memo: dict | None = None) -> 'Arc': memo = {} if memo is None else memo @@ -315,7 +313,7 @@ class Arc(Shape): self.rotation += theta return self - def mirror(self, axis: int) -> 'Arc': + def mirror(self, axis: int = 0) -> 'Arc': self.offset[axis - 1] *= -1 self.rotation *= -1 self.rotation += axis * pi diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 8176773..705c2d4 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -96,7 +96,7 @@ class Circle(Shape): def rotate(self, theta: float) -> 'Circle': return self - def mirror(self, axis: int) -> 'Circle': + def mirror(self, axis: int = 0) -> 'Circle': self.offset *= -1 return self diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 858e5d8..9a61478 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,4 +1,4 @@ -from typing import Sequence, Any +from typing import Any, Self import copy import math @@ -90,7 +90,6 @@ class Ellipse(Shape): *, offset: ArrayLike = (0.0, 0.0), rotation: float = 0, - mirrored: Sequence[bool] = (False, False), repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -109,9 +108,8 @@ class Ellipse(Shape): self.rotation = rotation self.repetition = repetition self.annotations = annotations if annotations is not None else {} - [self.mirror(a) for a, do in enumerate(mirrored) if do] - def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse': + def __deepcopy__(self, memo: dict | None = None) -> Self: memo = {} if memo is None else memo new = copy.copy(self) new._offset = self._offset.copy() @@ -157,17 +155,17 @@ class Ellipse(Shape): return numpy.vstack((self.offset - rot_radii[0], self.offset + rot_radii[1])) - def rotate(self, theta: float) -> 'Ellipse': + def rotate(self, theta: float) -> Self: self.rotation += theta return self - def mirror(self, axis: int) -> 'Ellipse': + def mirror(self, axis: int = 0) -> Self: self.offset[axis - 1] *= -1 self.rotation *= -1 self.rotation += axis * pi return self - def scale_by(self, c: float) -> 'Ellipse': + def scale_by(self, c: float) -> Self: self.radii *= c return self diff --git a/masque/shapes/path.py b/masque/shapes/path.py index a81318f..515be49 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -153,7 +153,6 @@ class Path(Shape): cap_extensions: ArrayLike | None = None, offset: ArrayLike = (0.0, 0.0), rotation: float = 0, - mirrored: Sequence[bool] = (False, False), repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -180,7 +179,6 @@ class Path(Shape): self.cap = cap self.cap_extensions = cap_extensions self.rotate(rotation) - [self.mirror(a) for a, do in enumerate(mirrored) if do] def __deepcopy__(self, memo: dict | None = None) -> 'Path': memo = {} if memo is None else memo @@ -200,7 +198,6 @@ class Path(Shape): cap_extensions: tuple[float, float] | None = None, offset: ArrayLike = (0.0, 0.0), rotation: float = 0, - mirrored: Sequence[bool] = (False, False), ) -> 'Path': """ Build a path by specifying the turn angles and travel distances @@ -217,9 +214,6 @@ class Path(Shape): Default `(0, 0)` or `None`, depending on cap type offset: Offset, default `(0, 0)` rotation: Rotation counterclockwise, in radians. Default `0` - mirrored: Whether to mirror across the x or y axes. For example, - `mirrored=(True, False)` results in a reflection across the x-axis, - multiplying the path's y-coordinates by -1. Default `(False, False)` Returns: The resulting Path object @@ -233,7 +227,7 @@ class Path(Shape): verts.append(verts[-1] + direction * distance) return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions, - offset=offset, rotation=rotation, mirrored=mirrored) + offset=offset, rotation=rotation) def to_polygons( self, @@ -334,7 +328,7 @@ class Path(Shape): self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T return self - def mirror(self, axis: int) -> 'Path': + def mirror(self, axis: int = 0) -> 'Path': self.vertices[:, axis - 1] *= -1 return self diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index fead3c7..0c562ae 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -81,7 +81,6 @@ class Polygon(Shape): *, offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, - mirrored: Sequence[bool] = (False, False), repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -99,7 +98,6 @@ class Polygon(Shape): self.repetition = repetition self.annotations = annotations if annotations is not None else {} self.rotate(rotation) - [self.mirror(a) for a, do in enumerate(mirrored) if do] def __deepcopy__(self, memo: dict | None = None) -> 'Polygon': memo = {} if memo is None else memo @@ -336,7 +334,7 @@ class Polygon(Shape): self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T return self - def mirror(self, axis: int) -> 'Polygon': + def mirror(self, axis: int = 0) -> 'Polygon': self.vertices[:, axis - 1] *= -1 return self diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 2192d3c..d4922f4 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,4 +1,4 @@ -from typing import Sequence, Any +from typing import Self import copy import numpy @@ -9,8 +9,7 @@ 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, normalize_mirror -from ..utils import annotations_t +from ..utils import is_scalar, get_bit, annotations_t # Loaded on use: # from freetype import Face @@ -30,7 +29,7 @@ class Text(RotatableImpl, Shape): _string: str _height: float - _mirrored: NDArray[numpy.bool_] + _mirrored: bool font_path: str # vertices property @@ -53,16 +52,13 @@ class Text(RotatableImpl, Shape): raise PatternError('Height must be a scalar') self._height = val - # Mirrored property @property - def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]: + def mirrored(self) -> bool: # mypy#3004, should be bool return self._mirrored @mirrored.setter - def mirrored(self, val: Sequence[bool]) -> None: - if is_scalar(val): - raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = numpy.array(val, dtype=bool, copy=True) + def mirrored(self, val: bool) -> None: + self._mirrored = bool(val) def __init__( self, @@ -72,19 +68,16 @@ class Text(RotatableImpl, Shape): *, offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, - mirrored: ArrayLike = (False, False), repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, ) -> None: if raw: assert isinstance(offset, numpy.ndarray) - assert isinstance(mirrored, numpy.ndarray) self._offset = offset self._string = string self._height = height self._rotation = rotation - self._mirrored = mirrored self._repetition = repetition self._annotations = annotations if annotations is not None else {} else: @@ -92,16 +85,14 @@ class Text(RotatableImpl, Shape): self.string = string self.height = height self.rotation = rotation - self.mirrored = mirrored self.repetition = repetition self.annotations = annotations if annotations is not None else {} self.font_path = font_path - def __deepcopy__(self, memo: dict | None = None) -> 'Text': + def __deepcopy__(self, memo: dict | None = None) -> Self: memo = {} if memo is None else memo new = copy.copy(self) new._offset = self._offset.copy() - new._mirrored = copy.deepcopy(self._mirrored, memo) new._annotations = copy.deepcopy(self._annotations) return new @@ -118,7 +109,8 @@ class Text(RotatableImpl, Shape): # Move these polygons to the right of the previous letter for xys in raw_polys: poly = Polygon(xys) - poly.mirror2d(self.mirrored) + if self.mirrored: + poly.mirror() poly.scale_by(self.height) poly.offset = self.offset + [total_advance, 0] poly.rotate_around(self.offset, self.rotation) @@ -129,27 +121,27 @@ class Text(RotatableImpl, Shape): return all_polygons - def mirror(self, axis: int) -> 'Text': - self.mirrored[axis] = not self.mirrored[axis] + def mirror(self, axis: int = 0) -> Self: + self.mirrored = not self.mirrored + if axis == 1: + self.rotation += pi return self - def scale_by(self, c: float) -> 'Text': + def scale_by(self, c: float) -> Self: self.height *= c return self def normalized_form(self, norm_value: float) -> normalized_shape_tuple: - mirror_x, rotation = normalize_mirror(self.mirrored) - rotation += self.rotation - rotation %= 2 * pi + rotation = self.rotation % (2 * pi) return ((type(self), self.string, self.font_path), - (self.offset, self.height / norm_value, rotation, mirror_x), + (self.offset, self.height / norm_value, rotation, bool(self.mirrored)), lambda: Text( string=self.string, height=self.height * norm_value, font_path=self.font_path, rotation=rotation, - mirrored=(mirror_x, False), - )) + ).mirror2d(across_x=self.mirrored), + ) def get_bounds_single(self) -> NDArray[numpy.float64]: # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so @@ -258,5 +250,5 @@ def get_char_as_polygons( def __repr__(self) -> str: rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' - mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' + mirrored = ' m{:d}' if self.mirrored else '' return f'' diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index 2d02f09..c547780 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -9,7 +9,7 @@ class Mirrorable(metaclass=ABCMeta): __slots__ = () @abstractmethod - def mirror(self, axis: int) -> Self: + def mirror(self, axis: int = 0) -> Self: """ Mirror the entity across an axis. @@ -21,7 +21,7 @@ class Mirrorable(metaclass=ABCMeta): """ pass - def mirror2d(self, axes: tuple[bool, bool]) -> Self: + def mirror2d(self, across_x: bool = False, across_y: bool = False) -> Self: """ Optionally mirror the entity across both axes @@ -31,9 +31,9 @@ class Mirrorable(metaclass=ABCMeta): Returns: self """ - if axes[0]: + if across_x: self.mirror(0) - if axes[1]: + if across_y: self.mirror(1) return self