From 37d462525cea9272de2e8e3bfe602c49611e9358 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 2 Apr 2026 19:55:30 -0700 Subject: [PATCH] [shapes] move to per-shape purpose-built _from_raw constructors --- masque/file/gdsii.py | 44 ++++++++----- masque/file/gdsii_arrow.py | 27 ++++++-- masque/shapes/arc.py | 49 ++++++++------ masque/shapes/circle.py | 32 +++++---- masque/shapes/ellipse.py | 38 ++++++----- masque/shapes/path.py | 47 ++++++++------ masque/shapes/poly_collection.py | 18 ++---- masque/shapes/polygon.py | 13 +--- masque/shapes/rect_collection.py | 13 +--- masque/shapes/text.py | 49 ++++++++------ masque/test/test_raw_constructors.py | 97 ++++++++++++++++++++++++++++ 11 files changed, 288 insertions(+), 139 deletions(-) create mode 100644 masque/test/test_raw_constructors.py diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 323dabd..1d8c3d1 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -323,26 +323,40 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> tuple[layer_ else: raise PatternError(f'Unrecognized path type: {gpath.path_type}') - mpath = Path( - vertices=gpath.xy.astype(float), - width=gpath.width, - cap=cap, - offset=numpy.zeros(2), - annotations=_properties_to_annotations(gpath.properties), - raw=raw_mode, - ) + vertices = gpath.xy.astype(float) + annotations = _properties_to_annotations(gpath.properties) + cap_extensions = None if cap == Path.Cap.SquareCustom: - mpath.cap_extensions = gpath.extension + cap_extensions = numpy.asarray(gpath.extension, dtype=float) + + if raw_mode: + mpath = Path._from_raw( + vertices=vertices, + width=gpath.width, + cap=cap, + cap_extensions=cap_extensions, + annotations=annotations, + ) + else: + mpath = Path( + vertices=vertices, + width=gpath.width, + cap=cap, + cap_extensions=cap_extensions, + offset=numpy.zeros(2), + annotations=annotations, + ) return gpath.layer, mpath def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> tuple[layer_t, Polygon]: - return boundary.layer, Polygon( - vertices=boundary.xy[:-1].astype(float), - offset=numpy.zeros(2), - annotations=_properties_to_annotations(boundary.properties), - raw=raw_mode, - ) + vertices = boundary.xy[:-1].astype(float) + annotations = _properties_to_annotations(boundary.properties) + if raw_mode: + poly = Polygon._from_raw(vertices=vertices, annotations=annotations) + else: + poly = Polygon(vertices=vertices, offset=numpy.zeros(2), annotations=annotations) + return boundary.layer, poly def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.Reference]: diff --git a/masque/file/gdsii_arrow.py b/masque/file/gdsii_arrow.py index c5e7681..58493c1 100644 --- a/masque/file/gdsii_arrow.py +++ b/masque/file/gdsii_arrow.py @@ -680,8 +680,23 @@ def _gpaths_to_mpaths( cap_extensions = None annotations = _read_annotations(prop_offs, prop_key, prop_val, ee) - path = Path(vertices=vertices, offset=ZERO_OFFSET, annotations=annotations, raw=raw_mode, - width=width, cap=cap,cap_extensions=cap_extensions) + if raw_mode: + path = Path._from_raw( + vertices=vertices, + width=width, + cap=cap, + cap_extensions=cap_extensions, + annotations=annotations, + ) + else: + path = Path( + vertices=vertices, + width=width, + cap=cap, + cap_extensions=cap_extensions, + offset=ZERO_OFFSET, + annotations=annotations, + ) pat.shapes[layer].append(path) @@ -718,13 +733,13 @@ def _boundary_batches_to_polygons( if raw_mode: poly = Polygon._from_raw(vertices=vertices, annotations=None) else: - poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=None, raw=False) + poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=None) pat.shapes[layer].append(poly) else: if raw_mode: polys = PolyCollection._from_raw(vertex_lists=vertices, vertex_offsets=vertex_offsets, annotations=None) else: - polys = PolyCollection(vertex_lists=vertices, vertex_offsets=vertex_offsets, offset=ZERO_OFFSET, annotations=None, raw=False) + polys = PolyCollection(vertex_lists=vertices, vertex_offsets=vertex_offsets, offset=ZERO_OFFSET, annotations=None) pat.shapes[layer].append(polys) @@ -755,7 +770,7 @@ def _rect_batches_to_rectcollections( if raw_mode: rect_collection = RectCollection._from_raw(rects=rects, annotations=None) else: - rect_collection = RectCollection(rects=rects, offset=ZERO_OFFSET, annotations=None, raw=False) + rect_collection = RectCollection(rects=rects, offset=ZERO_OFFSET, annotations=None) pat.shapes[layer].append(rect_collection) @@ -790,7 +805,7 @@ def _boundary_props_to_polygons( if raw_mode: poly = Polygon._from_raw(vertices=vertices, annotations=annotations) else: - poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=annotations, raw=False) + poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=annotations) pat.shapes[layer].append(poly) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 00c5714..9d5f65d 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -159,27 +159,36 @@ class Arc(PositionableImpl, Shape): rotation: float = 0, repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: - if raw: - assert isinstance(radii, numpy.ndarray) - assert isinstance(angles, numpy.ndarray) - assert isinstance(offset, numpy.ndarray) - self._radii = radii - self._angles = angles - self._width = width - self._offset = offset - self._rotation = rotation - self._repetition = repetition - self._annotations = annotations - else: - self.radii = radii - self.angles = angles - self.width = width - self.offset = offset - self.rotation = rotation - self.repetition = repetition - self.annotations = annotations + self.radii = radii + self.angles = angles + self.width = width + self.offset = offset + self.rotation = rotation + self.repetition = repetition + self.annotations = annotations + + @classmethod + def _from_raw( + cls, + *, + radii: NDArray[numpy.float64], + angles: NDArray[numpy.float64], + width: float, + offset: NDArray[numpy.float64], + rotation: float, + annotations: annotations_t = None, + repetition: Repetition | None = None, + ) -> 'Arc': + new = cls.__new__(cls) + new._radii = radii + new._angles = angles + new._width = width + new._offset = offset + new._rotation = rotation % (2 * pi) + new._repetition = repetition + new._annotations = annotations + return new def __deepcopy__(self, memo: dict | None = None) -> 'Arc': memo = {} if memo is None else memo diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 19f3f2b..d7591db 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -50,19 +50,27 @@ class Circle(PositionableImpl, Shape): offset: ArrayLike = (0.0, 0.0), repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: - if raw: - assert isinstance(offset, numpy.ndarray) - self._radius = radius - self._offset = offset - self._repetition = repetition - self._annotations = annotations - else: - self.radius = radius - self.offset = offset - self.repetition = repetition - self.annotations = annotations + self.radius = radius + self.offset = offset + self.repetition = repetition + self.annotations = annotations + + @classmethod + def _from_raw( + cls, + *, + radius: float, + offset: NDArray[numpy.float64], + annotations: annotations_t = None, + repetition: Repetition | None = None, + ) -> 'Circle': + new = cls.__new__(cls) + new._radius = radius + new._offset = offset + new._repetition = repetition + new._annotations = annotations + return new def __deepcopy__(self, memo: dict | None = None) -> 'Circle': memo = {} if memo is None else memo diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 38799da..52a3297 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -95,22 +95,30 @@ class Ellipse(PositionableImpl, Shape): rotation: float = 0, repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: - if raw: - assert isinstance(radii, numpy.ndarray) - assert isinstance(offset, numpy.ndarray) - self._radii = radii - self._offset = offset - self._rotation = rotation - self._repetition = repetition - self._annotations = annotations - else: - self.radii = radii - self.offset = offset - self.rotation = rotation - self.repetition = repetition - self.annotations = annotations + self.radii = radii + self.offset = offset + self.rotation = rotation + self.repetition = repetition + self.annotations = annotations + + @classmethod + def _from_raw( + cls, + *, + radii: NDArray[numpy.float64], + offset: NDArray[numpy.float64], + rotation: float, + annotations: annotations_t = None, + repetition: Repetition | None = None, + ) -> Self: + new = cls.__new__(cls) + new._radii = radii + new._offset = offset + new._rotation = rotation % pi + new._repetition = repetition + new._annotations = annotations + return new def __deepcopy__(self, memo: dict | None = None) -> Self: memo = {} if memo is None else memo diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 7038309..a1e04af 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -201,34 +201,43 @@ class Path(Shape): rotation: float = 0, repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: self._cap_extensions = None # Since .cap setter might access it - if raw: - assert isinstance(vertices, numpy.ndarray) - assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None - self._vertices = vertices - self._repetition = repetition - self._annotations = annotations - self._width = width - self._cap = cap - self._cap_extensions = cap_extensions + self.vertices = vertices + self.repetition = repetition + self.annotations = annotations + self._cap = cap + if cap == PathCap.SquareCustom and cap_extensions is None: + self._cap_extensions = numpy.zeros(2) else: - self.vertices = vertices - self.repetition = repetition - self.annotations = annotations - self._cap = cap - if cap == PathCap.SquareCustom and cap_extensions is None: - self._cap_extensions = numpy.zeros(2) - else: - self.cap_extensions = cap_extensions - self.width = width + self.cap_extensions = cap_extensions + self.width = width if rotation: self.rotate(rotation) if numpy.any(offset): self.translate(offset) + @classmethod + def _from_raw( + cls, + *, + vertices: NDArray[numpy.float64], + width: float, + cap: PathCap, + cap_extensions: NDArray[numpy.float64] | None = None, + annotations: annotations_t = None, + repetition: Repetition | None = None, + ) -> Self: + new = cls.__new__(cls) + new._vertices = vertices + new._width = width + new._cap = cap + new._cap_extensions = cap_extensions + new._repetition = repetition + new._annotations = annotations + return new + def __deepcopy__(self, memo: dict | None = None) -> 'Path': memo = {} if memo is None else memo new = copy.copy(self) diff --git a/masque/shapes/poly_collection.py b/masque/shapes/poly_collection.py index f90c29a..f1c840a 100644 --- a/masque/shapes/poly_collection.py +++ b/masque/shapes/poly_collection.py @@ -100,21 +100,11 @@ class PolyCollection(Shape): rotation: float = 0.0, repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: - if raw: - assert isinstance(vertex_lists, numpy.ndarray) - assert isinstance(vertex_offsets, numpy.ndarray) - assert numpy.issubdtype(vertex_offsets.dtype, numpy.integer) - self._vertex_lists = vertex_lists - self._vertex_offsets = vertex_offsets - self._repetition = repetition - self._annotations = annotations - else: - self._vertex_lists = numpy.asarray(vertex_lists, dtype=float) - self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp) - self.repetition = repetition - self.annotations = annotations + self._vertex_lists = numpy.asarray(vertex_lists, dtype=float) + self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp) + self.repetition = repetition + self.annotations = annotations if rotation: self.rotate(rotation) if numpy.any(offset): diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 42c2f78..06e5c2b 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -115,17 +115,10 @@ class Polygon(Shape): rotation: float = 0.0, repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: - if raw: - assert isinstance(vertices, numpy.ndarray) - self._vertices = vertices - self._repetition = repetition - self._annotations = annotations - else: - self.vertices = vertices - self.repetition = repetition - self.annotations = annotations + self.vertices = vertices + self.repetition = repetition + self.annotations = annotations if rotation: self.rotate(rotation) if numpy.any(offset): diff --git a/masque/shapes/rect_collection.py b/masque/shapes/rect_collection.py index 96becfd..eaf028f 100644 --- a/masque/shapes/rect_collection.py +++ b/masque/shapes/rect_collection.py @@ -86,17 +86,10 @@ class RectCollection(Shape): rotation: float = 0.0, repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: - if raw: - assert isinstance(rects, numpy.ndarray) - self._rects = rects - self._repetition = repetition - self._annotations = annotations - else: - self.rects = rects - self.repetition = repetition - self.annotations = annotations + self.rects = rects + self.repetition = repetition + self.annotations = annotations if rotation: self.rotate(rotation) if numpy.any(offset): diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 5047dc4..c078879 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -73,27 +73,40 @@ class Text(PositionableImpl, RotatableImpl, Shape): mirrored: bool = False, repetition: Repetition | None = None, annotations: annotations_t = None, - raw: bool = False, ) -> None: - if raw: - assert isinstance(offset, numpy.ndarray) - self._offset = offset - self._string = string - self._height = height - self._rotation = rotation - self._mirrored = mirrored - self._repetition = repetition - self._annotations = annotations - else: - self.offset = offset - self.string = string - self.height = height - self.rotation = rotation - self.mirrored = mirrored - self.repetition = repetition - self.annotations = annotations + self.offset = offset + self.string = string + self.height = height + self.rotation = rotation + self.mirrored = mirrored + self.repetition = repetition + self.annotations = annotations self.font_path = font_path + @classmethod + def _from_raw( + cls, + *, + string: str, + height: float, + font_path: str, + offset: NDArray[numpy.float64], + rotation: float, + mirrored: bool, + annotations: annotations_t = None, + repetition: Repetition | None = None, + ) -> Self: + new = cls.__new__(cls) + new._offset = offset + new._string = string + new._height = height + new._rotation = rotation % (2 * pi) + new._mirrored = mirrored + new._repetition = repetition + new._annotations = annotations + new.font_path = font_path + return new + def __deepcopy__(self, memo: dict | None = None) -> Self: memo = {} if memo is None else memo new = copy.copy(self) diff --git a/masque/test/test_raw_constructors.py b/masque/test/test_raw_constructors.py new file mode 100644 index 0000000..2f86ba0 --- /dev/null +++ b/masque/test/test_raw_constructors.py @@ -0,0 +1,97 @@ +import numpy +from numpy import pi +from numpy.testing import assert_allclose + +from ..shapes import Arc, Circle, Ellipse, Path, Text + + +def test_circle_raw_constructor_matches_public() -> None: + raw = Circle._from_raw( + radius=5.0, + offset=numpy.array([1.0, 2.0]), + annotations={'1': ['circle']}, + ) + public = Circle( + radius=5.0, + offset=(1.0, 2.0), + annotations={'1': ['circle']}, + ) + assert raw == public + + +def test_ellipse_raw_constructor_matches_public() -> None: + raw = Ellipse._from_raw( + radii=numpy.array([3.0, 5.0]), + offset=numpy.array([1.0, 2.0]), + rotation=5 * pi / 2, + annotations={'2': ['ellipse']}, + ) + public = Ellipse( + radii=(3.0, 5.0), + offset=(1.0, 2.0), + rotation=5 * pi / 2, + annotations={'2': ['ellipse']}, + ) + assert raw == public + + +def test_arc_raw_constructor_matches_public() -> None: + raw = Arc._from_raw( + radii=numpy.array([10.0, 6.0]), + angles=numpy.array([0.0, pi / 2]), + width=2.0, + offset=numpy.array([1.0, 2.0]), + rotation=5 * pi / 2, + annotations={'3': ['arc']}, + ) + public = Arc( + radii=(10.0, 6.0), + angles=(0.0, pi / 2), + width=2.0, + offset=(1.0, 2.0), + rotation=5 * pi / 2, + annotations={'3': ['arc']}, + ) + assert raw == public + + +def test_path_raw_constructor_matches_public() -> None: + raw = Path._from_raw( + vertices=numpy.array([[0.0, 0.0], [10.0, 0.0], [10.0, 5.0]]), + width=2.0, + cap=Path.Cap.SquareCustom, + cap_extensions=numpy.array([1.0, 3.0]), + annotations={'4': ['path']}, + ) + public = Path( + vertices=((0.0, 0.0), (10.0, 0.0), (10.0, 5.0)), + width=2.0, + cap=Path.Cap.SquareCustom, + cap_extensions=(1.0, 3.0), + annotations={'4': ['path']}, + ) + assert raw == public + assert raw.cap_extensions is not None + assert_allclose(raw.cap_extensions, [1.0, 3.0]) + + +def test_text_raw_constructor_matches_public() -> None: + raw = Text._from_raw( + string='RAW', + height=12.0, + font_path='font.otf', + offset=numpy.array([1.0, 2.0]), + rotation=5 * pi / 2, + mirrored=True, + annotations={'5': ['text']}, + ) + public = Text( + string='RAW', + height=12.0, + font_path='font.otf', + offset=(1.0, 2.0), + rotation=5 * pi / 2, + mirrored=True, + annotations={'5': ['text']}, + ) + assert raw == public