From 9a077ea2dfd5efb103571fc4b80a72f8f8d7d785 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 12 Apr 2023 13:56:50 -0700 Subject: [PATCH] move to dicty layers and targets --- masque/builder/builder.py | 6 +- masque/builder/renderpather.py | 8 +- masque/builder/tools.py | 5 +- masque/file/dxf.py | 65 ++++--- masque/file/gdsii.py | 236 ++++++++++++------------- masque/file/oasis.py | 203 +++++++++++---------- masque/file/svg.py | 33 ++-- masque/file/utils.py | 21 +-- masque/label.py | 13 +- masque/library.py | 183 ++++++++++--------- masque/pattern.py | 312 ++++++++++++++++++++++----------- masque/ref.py | 66 +++---- masque/shapes/arc.py | 14 +- masque/shapes/circle.py | 15 +- masque/shapes/ellipse.py | 17 +- masque/shapes/path.py | 23 +-- masque/shapes/polygon.py | 29 +-- masque/shapes/shape.py | 17 +- masque/shapes/text.py | 16 +- masque/traits/__init__.py | 2 +- masque/traits/positionable.py | 2 + masque/utils/pack2d.py | 5 +- masque/utils/ports2data.py | 29 +-- 23 files changed, 688 insertions(+), 632 deletions(-) diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 21d6400..54ee4d5 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -1,4 +1,4 @@ -from typing import Self, Sequence, Mapping, Literal, overload, Final, cast +from typing import Self, Sequence, Mapping, Literal, overload import copy import logging @@ -482,10 +482,10 @@ class Builder(PortList): self.pattern.append(other_copy) else: assert not isinstance(other, Pattern) - ref = Ref(other.name, mirrored=mirrored) + ref = Ref(mirrored=mirrored) ref.rotate_around(pivot, rotation) ref.translate(offset) - self.pattern.refs.append(ref) + self.pattern.refs[other.name].append(ref) return self def translate(self, offset: ArrayLike) -> Self: diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 12a059b..ab0b43b 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -279,10 +279,10 @@ class RenderPather(PortList): p.translate(offset) self.ports[name] = p - sp = Ref(other.name, mirrored=mirrored) - sp.rotate_around(pivot, rotation) - sp.translate(offset) - self.pattern.refs.append(sp) + ref = Ref(mirrored=mirrored) + ref.rotate_around(pivot, rotation) + ref.translate(offset) + self.pattern.refs[other.name].append(ref) return self def path( diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 545ff73..f04bc5a 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -1,7 +1,7 @@ """ Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides) """ -from typing import TYPE_CHECKING, Sequence, Literal, Callable +from typing import Sequence, Literal, Callable from abc import ABCMeta, abstractmethod import numpy @@ -20,6 +20,7 @@ render_step_t = ( | tuple[Literal['P'], None, float, float, str, None] ) + class Tool: def path( self, @@ -153,6 +154,4 @@ class BasicTool(Tool, metaclass=ABCMeta): if out_transition: bb.plug(opat, {port_names[1]: oport_ours}) - return bb.pattern - diff --git a/masque/file/dxf.py b/masque/file/dxf.py index a2e9b60..8875672 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -219,7 +219,7 @@ def _read_block(block) -> tuple[str, Pattern]: if points.shape[1] == 2: raise PatternError('Invalid or unimplemented polygon?') - #shape = Polygon(layer=layer) + #shape = Polygon() elif points.shape[1] > 2: if (points[0, 2] != points[:, 2]).any(): raise PatternError('PolyLine has non-constant width (not yet representable in masque!)') @@ -232,11 +232,11 @@ def _read_block(block) -> tuple[str, Pattern]: shape: Path | Polygon if width == 0 and len(points) > 2 and numpy.array_equal(points[0], points[-1]): - shape = Polygon(layer=layer, vertices=points[:-1, :2]) + shape = Polygon(vertices=points[:-1, :2]) else: - shape = Path(layer=layer, width=width, vertices=points[:, :2]) + shape = Path(width=width, vertices=points[:, :2]) - pat.shapes.append(shape) + pat.shapes[layer].append(shape) elif eltype in ('TEXT',): args = dict( @@ -248,9 +248,9 @@ def _read_block(block) -> tuple[str, Pattern]: # if height != 0: # logger.warning('Interpreting DXF TEXT as a label despite nonzero height. ' # 'This could be changed in the future by setting a font path in the masque DXF code.') - pat.labels.append(Label(string=string, **args)) + pat.label(string=string, **args) # else: -# pat.shapes.append(Text(string=string, height=height, font_path=????)) +# pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????)) elif eltype in ('INSERT',): attr = element.dxfattribs() xscale = attr.get('xscale', 1) @@ -286,13 +286,9 @@ def _read_block(block) -> tuple[str, Pattern]: def _mrefs_to_drefs( block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, - refs: list[Ref], + refs: dict[str | None, list[Ref]], ) -> None: - for ref in refs: - if ref.target is None: - continue - encoded_name = ref.target - + 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), @@ -330,36 +326,47 @@ def _mrefs_to_drefs( for dd in rep.displacements: block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs) + for target, rseq in refs.items(): + if target is None: + continue + for ref in rseq: + mk_blockref(target, ref) + def _shapes_to_elements( block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, - shapes: list[Shape], + shapes: dict[layer_t, list[Shape]], polygonize_paths: bool = False, ) -> None: # Add `LWPolyline`s for each shape. # Could set do paths with width setting, but need to consider endcaps. - for shape in shapes: - if shape.repetition is not None: - raise PatternError( - 'Shape repetitions are not supported by DXF.' - ' Please call library.wrap_repeated_shapes() before writing to file.' - ) + for layer, sseq in shapes.items(): + attribs = dict(layer=_mlayer2dxf(layer)) + for shape in sseq: + if shape.repetition is not None: + raise PatternError( + 'Shape repetitions are not supported by DXF.' + ' Please call library.wrap_repeated_shapes() before writing to file.' + ) - attribs = dict(layer=_mlayer2dxf(shape.layer)) - for polygon in shape.to_polygons(): - xy_open = polygon.vertices + polygon.offset - xy_closed = numpy.vstack((xy_open, xy_open[0, :])) - block.add_lwpolyline(xy_closed, dxfattribs=attribs) + for polygon in shape.to_polygons(): + xy_open = polygon.vertices + polygon.offset + xy_closed = numpy.vstack((xy_open, xy_open[0, :])) + block.add_lwpolyline(xy_closed, dxfattribs=attribs) def _labels_to_texts( block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, - labels: list[Label], + labels: dict[layer_t, list[Label]], ) -> None: - for label in labels: - attribs = dict(layer=_mlayer2dxf(label.layer)) - xy = label.offset - block.add_text(label.string, dxfattribs=attribs).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT) + for layer, lseq in labels.items(): + attribs = dict(layer=_mlayer2dxf(layer)) + for label in lseq: + xy = label.offset + block.add_text( + label.string, + dxfattribs=attribs + ).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT) def _mlayer2dxf(layer: layer_t) -> str: diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 968a58a..039cc55 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -253,21 +253,21 @@ def read_elements( elements = klamath.library.read_elements(stream) for element in elements: if isinstance(element, klamath.elements.Boundary): - poly = _boundary_to_polygon(element, raw_mode) - pat.shapes.append(poly) + layer, poly = _boundary_to_polygon(element, raw_mode) + pat.shapes[layer].append(poly) elif isinstance(element, klamath.elements.Path): - path = _gpath_to_mpath(element, raw_mode) - pat.shapes.append(path) + layer, path = _gpath_to_mpath(element, raw_mode) + pat.shapes[layer].append(path) elif isinstance(element, klamath.elements.Text): - label = Label( - offset=element.xy.astype(float), + pat.label( layer=element.layer, + offset=element.xy.astype(float), string=element.string.decode('ASCII'), annotations=_properties_to_annotations(element.properties), ) - pat.labels.append(label) elif isinstance(element, klamath.elements.Reference): - pat.refs.append(_gref_to_mref(element)) + target, ref = _gref_to_mref(element) + pat.refs[target].append(ref) return pat @@ -287,7 +287,7 @@ def _mlayer2gds(mlayer: layer_t) -> tuple[int, int]: return layer, data_type -def _gref_to_mref(ref: klamath.library.Reference) -> Ref: +def _gref_to_mref(ref: klamath.library.Reference) -> tuple[str, Ref]: """ Helper function to create a Ref from an SREF or AREF. Sets ref.target to struct_name. """ @@ -301,8 +301,8 @@ def _gref_to_mref(ref: klamath.library.Reference) -> Ref: repetition = Grid(a_vector=a_vector, b_vector=b_vector, a_count=a_count, b_count=b_count) + target = ref.struct_name.decode('ASCII') mref = Ref( - target=ref.struct_name.decode('ASCII'), offset=offset, rotation=numpy.deg2rad(ref.angle_deg), scale=ref.mag, @@ -310,10 +310,10 @@ def _gref_to_mref(ref: klamath.library.Reference) -> Ref: annotations=_properties_to_annotations(ref.properties), repetition=repetition, ) - return mref + return target, mref -def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path: +def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> tuple[layer_t, Path]: if gpath.path_type in path_cap_map: cap = path_cap_map[gpath.path_type] else: @@ -321,7 +321,6 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path: mpath = Path( vertices=gpath.xy.astype(float), - layer=gpath.layer, width=gpath.width, cap=cap, offset=numpy.zeros(2), @@ -330,74 +329,73 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path: ) if cap == Path.Cap.SquareCustom: mpath.cap_extensions = gpath.extension - return mpath + return gpath.layer, mpath -def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon: - return Polygon( +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), - layer=boundary.layer, offset=numpy.zeros(2), annotations=_properties_to_annotations(boundary.properties), raw=raw_mode, ) -def _mrefs_to_grefs(refs: list[Ref]) -> list[klamath.library.Reference]: +def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.Reference]: grefs = [] - for ref in refs: - if ref.target is None: + for target, rseq in refs.items(): + if target is None: continue - encoded_name = ref.target.encode('ASCII') + 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) + rep = ref.repetition + angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360 + properties = _annotations_to_properties(ref.annotations, 512) - # Note: GDS mirrors first and rotates second - mirror_across_x, extra_angle = normalize_mirror(ref.mirrored) - rep = ref.repetition - angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360 - properties = _annotations_to_properties(ref.annotations, 512) - - if isinstance(rep, Grid): - b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) - b_count = rep.b_count if rep.b_count is not None else 1 - xy = numpy.array(ref.offset) + numpy.array([ - [0.0, 0.0], - rep.a_vector * rep.a_count, - b_vector * b_count, - ]) - aref = klamath.library.Reference( - struct_name=encoded_name, - xy=rint_cast(xy), - colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)), - angle_deg=angle_deg, - invert_y=mirror_across_x, - mag=ref.scale, - properties=properties, - ) - grefs.append(aref) - elif rep is None: - sref = klamath.library.Reference( - struct_name=encoded_name, - xy=rint_cast([ref.offset]), - colrow=None, - angle_deg=angle_deg, - invert_y=mirror_across_x, - mag=ref.scale, - properties=properties, - ) - grefs.append(sref) - else: - new_srefs = [ - klamath.library.Reference( + if isinstance(rep, Grid): + b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) + b_count = rep.b_count if rep.b_count is not None else 1 + xy = numpy.array(ref.offset) + numpy.array([ + [0.0, 0.0], + rep.a_vector * rep.a_count, + b_vector * b_count, + ]) + aref = klamath.library.Reference( struct_name=encoded_name, - xy=rint_cast([ref.offset + dd]), + xy=rint_cast(xy), + colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)), + angle_deg=angle_deg, + invert_y=mirror_across_x, + mag=ref.scale, + properties=properties, + ) + grefs.append(aref) + elif rep is None: + sref = klamath.library.Reference( + struct_name=encoded_name, + xy=rint_cast([ref.offset]), colrow=None, angle_deg=angle_deg, invert_y=mirror_across_x, mag=ref.scale, properties=properties, ) - for dd in rep.displacements] - grefs += new_srefs + grefs.append(sref) + else: + new_srefs = [ + klamath.library.Reference( + struct_name=encoded_name, + xy=rint_cast([ref.offset + dd]), + colrow=None, + angle_deg=angle_deg, + invert_y=mirror_across_x, + mag=ref.scale, + properties=properties, + ) + for dd in rep.displacements] + grefs += new_srefs return grefs @@ -428,51 +426,41 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) - def _shapes_to_elements( - shapes: list[Shape], + shapes: dict[layer_t, list[Shape]], polygonize_paths: bool = False, ) -> list[klamath.elements.Element]: elements: list[klamath.elements.Element] = [] # Add a Boundary element for each shape, and Path elements if necessary - for shape in shapes: - if shape.repetition is not None: - raise PatternError('Shape repetitions are not supported by GDS.' - ' Please call library.wrap_repeated_shapes() before writing to file.') + for mlayer, sseq in shapes.items(): + layer, data_type = _mlayer2gds(mlayer) + for shape in sseq: + if shape.repetition is not None: + raise PatternError('Shape repetitions are not supported by GDS.' + ' Please call library.wrap_repeated_shapes() before writing to file.') - layer, data_type = _mlayer2gds(shape.layer) - properties = _annotations_to_properties(shape.annotations, 128) - if isinstance(shape, Path) and not polygonize_paths: - xy = rint_cast(shape.vertices + shape.offset) - width = rint_cast(shape.width) - path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup + properties = _annotations_to_properties(shape.annotations, 128) + if isinstance(shape, Path) and not polygonize_paths: + xy = rint_cast(shape.vertices + shape.offset) + width = rint_cast(shape.width) + path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup - extension: tuple[int, int] - if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None: - extension = tuple(shape.cap_extensions) # type: ignore - else: - extension = (0, 0) + extension: tuple[int, int] + if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None: + extension = tuple(shape.cap_extensions) # type: ignore + else: + extension = (0, 0) - path = klamath.elements.Path( - layer=(layer, data_type), - xy=xy, - path_type=path_type, - width=int(width), - extension=extension, - properties=properties, - ) - elements.append(path) - elif isinstance(shape, Polygon): - polygon = shape - xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32) - numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe') - xy_closed[-1] = xy_closed[0] - boundary = klamath.elements.Boundary( - layer=(layer, data_type), - xy=xy_closed, - properties=properties, - ) - elements.append(boundary) - else: - for polygon in shape.to_polygons(): + path = klamath.elements.Path( + layer=(layer, data_type), + xy=xy, + path_type=path_type, + width=int(width), + extension=extension, + properties=properties, + ) + elements.append(path) + elif isinstance(shape, Polygon): + polygon = shape xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32) numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe') xy_closed[-1] = xy_closed[0] @@ -482,28 +470,40 @@ def _shapes_to_elements( properties=properties, ) elements.append(boundary) + else: + for polygon in shape.to_polygons(): + xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32) + numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe') + xy_closed[-1] = xy_closed[0] + boundary = klamath.elements.Boundary( + layer=(layer, data_type), + xy=xy_closed, + properties=properties, + ) + elements.append(boundary) return elements -def _labels_to_texts(labels: list[Label]) -> list[klamath.elements.Text]: +def _labels_to_texts(labels: dict[layer_t, list[Label]]) -> list[klamath.elements.Text]: texts = [] - for label in labels: - properties = _annotations_to_properties(label.annotations, 128) - layer, text_type = _mlayer2gds(label.layer) - xy = rint_cast([label.offset]) - text = klamath.elements.Text( - layer=(layer, text_type), - xy=xy, - string=label.string.encode('ASCII'), - properties=properties, - presentation=0, # TODO maybe set some of these? - angle_deg=0, - invert_y=False, - width=0, - path_type=0, - mag=1, - ) - texts.append(text) + for mlayer, lseq in labels.items(): + layer, text_type = _mlayer2gds(mlayer) + for label in lseq: + properties = _annotations_to_properties(label.annotations, 128) + xy = rint_cast([label.offset]) + text = klamath.elements.Text( + layer=(layer, text_type), + xy=xy, + string=label.string.encode('ASCII'), + properties=properties, + presentation=0, # TODO maybe set some of these? + angle_deg=0, + invert_y=False, + width=0, + path_type=0, + mag=1, + ) + texts.append(text) return texts diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 8de9c1d..d05c0a9 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -30,7 +30,7 @@ from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringR from .utils import is_gzipped, tmpfile from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from ..library import Library, ILibrary -from ..shapes import Polygon, Path, Circle +from ..shapes import Path, Circle from ..repetition import Grid, Arbitrary, Repetition from ..utils import layer_t, normalize_mirror, annotations_t @@ -284,16 +284,13 @@ def read( vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list()[:-1])), axis=0) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) - poly = Polygon( + pat.polygon( vertices=vertices, layer=element.get_layer_tuple(), offset=element.get_xy(), annotations=annotations, repetition=repetition, ) - - pat.shapes.append(poly) - elif isinstance(element, fatrec.Path): vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0) @@ -311,7 +308,7 @@ def read( )) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) - path = Path( + pat.path( vertices=vertices, layer=element.get_layer_tuple(), offset=element.get_xy(), @@ -322,20 +319,17 @@ def read( **path_args, ) - pat.shapes.append(path) - elif isinstance(element, fatrec.Rectangle): width = element.get_width() height = element.get_height() annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) - rect = Polygon( + pat.polygon( layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), annotations=annotations, ) - pat.shapes.append(rect) elif isinstance(element, fatrec.Trapezoid): vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (element.get_width(), element.get_height()) @@ -363,14 +357,13 @@ def read( vertices[2, 0] -= b annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) - trapz = Polygon( + pat.polygon( layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, vertices=vertices, annotations=annotations, ) - pat.shapes.append(trapz) elif isinstance(element, fatrec.CTrapezoid): cttype = element.get_ctrapezoid_type() @@ -419,25 +412,24 @@ def read( vertices[0, 1] += width annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) - ctrapz = Polygon( + pat.polygon( layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, vertices=vertices, annotations=annotations, ) - pat.shapes.append(ctrapz) elif isinstance(element, fatrec.Circle): annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) + layer = element.get_layer_tuple() circle = Circle( - layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, annotations=annotations, radius=float(element.get_radius()), ) - pat.shapes.append(circle) + pat.shapes[layer].append(circle) elif isinstance(element, fatrec.Text): annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) @@ -446,21 +438,21 @@ def read( string = lib.textstrings[str_or_ref].string else: string = str_or_ref.string - label = Label( + pat.label( layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, annotations=annotations, string=string, ) - pat.labels.append(label) else: logger.warning(f'Skipping record {element} (unimplemented)') continue for placement in cell.placements: - pat.refs.append(_placement_to_ref(placement, lib)) + target, ref = _placement_to_ref(placement, lib) + pat.refs[target].append(ref) mlib[cell_name] = pat @@ -484,9 +476,9 @@ def _mlayer2oas(mlayer: layer_t) -> tuple[int, int]: return layer, data_type -def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> Ref: +def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> tuple[int | str, Ref]: """ - Helper function to create a Ref from a placment. Sets ref.target to the placement name. + Helper function to create a Ref from a placment. Also returns the placement name (or id). """ assert not isinstance(placement.repetition, fatamorgana.ReuseRepetition) xy = numpy.array((placement.x, placement.y)) @@ -501,7 +493,6 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) else: rotation = numpy.deg2rad(float(placement.angle)) ref = Ref( - target=name, offset=xy, mirrored=(placement.flip, False), rotation=rotation, @@ -509,116 +500,118 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) repetition=repetition_fata2masq(placement.repetition), annotations=annotations, ) - return ref + return name, ref def _refs_to_placements( - refs: list[Ref], + refs: dict[str | None, list[Ref]], ) -> list[fatrec.Placement]: placements = [] - for ref in refs: - if ref.target is None: + for target, rseq in refs.items(): + 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) + frep, rep_offset = repetition_masq2fata(ref.repetition) - # Note: OASIS mirrors first and rotates second - mirror_across_x, extra_angle = normalize_mirror(ref.mirrored) - frep, rep_offset = repetition_masq2fata(ref.repetition) + offset = rint_cast(ref.offset + rep_offset) + angle = numpy.rad2deg(ref.rotation + extra_angle) % 360 + placement = fatrec.Placement( + name=target, + flip=mirror_across_x, + angle=angle, + magnification=ref.scale, + properties=annotations_to_properties(ref.annotations), + x=offset[0], + y=offset[1], + repetition=frep, + ) - offset = rint_cast(ref.offset + rep_offset) - angle = numpy.rad2deg(ref.rotation + extra_angle) % 360 - placement = fatrec.Placement( - name=ref.target, - flip=mirror_across_x, - angle=angle, - magnification=ref.scale, - properties=annotations_to_properties(ref.annotations), - x=offset[0], - y=offset[1], - repetition=frep, - ) - - placements.append(placement) + placements.append(placement) return placements def _shapes_to_elements( - shapes: list[Shape], + shapes: dict[layer_t, list[Shape]], layer2oas: Callable[[layer_t], tuple[int, int]], ) -> list[fatrec.Polygon | fatrec.Path | fatrec.Circle]: # Add a Polygon record for each shape, and Path elements if necessary elements: list[fatrec.Polygon | fatrec.Path | fatrec.Circle] = [] - for shape in shapes: - layer, datatype = layer2oas(shape.layer) - repetition, rep_offset = repetition_masq2fata(shape.repetition) - properties = annotations_to_properties(shape.annotations) - if isinstance(shape, Circle): - offset = rint_cast(shape.offset + rep_offset) - radius = rint_cast(shape.radius) - circle = fatrec.Circle( - layer=layer, - datatype=datatype, - radius=cast(int, radius), - x=offset[0], - y=offset[1], - properties=properties, - repetition=repetition, - ) - elements.append(circle) - elif isinstance(shape, Path): - xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset) - deltas = rint_cast(numpy.diff(shape.vertices, axis=0)) - half_width = rint_cast(shape.width / 2) - path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup - extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None) - extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None) - path = fatrec.Path( - layer=layer, - datatype=datatype, - point_list=cast(Sequence[Sequence[int]], deltas), - half_width=cast(int, half_width), - x=xy[0], - y=xy[1], - extension_start=extension_start, # TODO implement multiple cap types? - extension_end=extension_end, - properties=properties, - repetition=repetition, - ) - elements.append(path) - else: - for polygon in shape.to_polygons(): - xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset) - points = rint_cast(numpy.diff(polygon.vertices, axis=0)) - elements.append(fatrec.Polygon( + for mlayer, sseq in shapes.items(): + layer, datatype = layer2oas(mlayer) + for shape in sseq: + repetition, rep_offset = repetition_masq2fata(shape.repetition) + properties = annotations_to_properties(shape.annotations) + if isinstance(shape, Circle): + offset = rint_cast(shape.offset + rep_offset) + radius = rint_cast(shape.radius) + circle = fatrec.Circle( layer=layer, datatype=datatype, - x=xy[0], - y=xy[1], - point_list=cast(list[list[int]], points), + radius=cast(int, radius), + x=offset[0], + y=offset[1], properties=properties, repetition=repetition, - )) + ) + elements.append(circle) + elif isinstance(shape, Path): + xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset) + deltas = rint_cast(numpy.diff(shape.vertices, axis=0)) + half_width = rint_cast(shape.width / 2) + path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup + extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None) + extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None) + path = fatrec.Path( + layer=layer, + datatype=datatype, + point_list=cast(Sequence[Sequence[int]], deltas), + half_width=cast(int, half_width), + x=xy[0], + y=xy[1], + extension_start=extension_start, # TODO implement multiple cap types? + extension_end=extension_end, + properties=properties, + repetition=repetition, + ) + elements.append(path) + else: + for polygon in shape.to_polygons(): + xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset) + points = rint_cast(numpy.diff(polygon.vertices, axis=0)) + elements.append(fatrec.Polygon( + layer=layer, + datatype=datatype, + x=xy[0], + y=xy[1], + point_list=cast(list[list[int]], points), + properties=properties, + repetition=repetition, + )) return elements def _labels_to_texts( - labels: list[Label], + labels: dict[layer_t, list[Label]], layer2oas: Callable[[layer_t], tuple[int, int]], ) -> list[fatrec.Text]: texts = [] - for label in labels: - layer, datatype = layer2oas(label.layer) - repetition, rep_offset = repetition_masq2fata(label.repetition) - xy = rint_cast(label.offset + rep_offset) - properties = annotations_to_properties(label.annotations) - texts.append(fatrec.Text( - layer=layer, - datatype=datatype, - x=xy[0], - y=xy[1], - string=label.string, - properties=properties, - repetition=repetition, - )) + for mlayer, lseq in labels.items(): + layer, datatype = layer2oas(mlayer) + for label in lseq: + repetition, rep_offset = repetition_masq2fata(label.repetition) + xy = rint_cast(label.offset + rep_offset) + properties = annotations_to_properties(label.annotations) + texts.append(fatrec.Text( + layer=layer, + datatype=datatype, + x=xy[0], + y=xy[1], + string=label.string, + properties=properties, + repetition=repetition, + )) return texts diff --git a/masque/file/svg.py b/masque/file/svg.py index 1296980..baf2252 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -65,22 +65,24 @@ def writefile( for name, pat in library.items(): svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red') - for shape in pat.shapes: - for polygon in shape.to_polygons(): - path_spec = poly2path(polygon.vertices + polygon.offset) + for layer, shapes in pat.shapes.items(): + for shape in shapes: + for polygon in shape.to_polygons(): + path_spec = poly2path(polygon.vertices + polygon.offset) - path = svg.path(d=path_spec) - if custom_attributes: - path['pattern_layer'] = polygon.layer + path = svg.path(d=path_spec) + if custom_attributes: + path['pattern_layer'] = layer - svg_group.add(path) + svg_group.add(path) - for ref in pat.refs: - if ref.target is None: + for target, refs in pat.refs.items(): + if target is None: continue - transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})' - use = svg.use(href='#' + mangle_name(ref.target), transform=transform) - svg_group.add(use) + for ref in refs: + transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})' + use = svg.use(href='#' + mangle_name(target), transform=transform) + svg_group.add(use) svg.defs.add(svg_group) svg.add(svg.use(href='#' + mangle_name(top))) @@ -133,9 +135,10 @@ def writefile_inverted( path_spec = poly2path(slab_edge) # Draw polygons with reversed vertex order - for shape in pattern.shapes: - for polygon in shape.to_polygons(): - path_spec += poly2path(polygon.vertices[::-1] + polygon.offset) + for _layer, shapes in pattern.shapes.items(): + for shape in shapes: + for polygon in shape.to_polygons(): + path_spec += poly2path(polygon.vertices[::-1] + polygon.offset) svg.add(svg.path(d=path_spec, fill='blue', stroke='red')) svg.save() diff --git a/masque/file/utils.py b/masque/file/utils.py index 9d25682..9edc0f9 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -42,16 +42,17 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern: Returns: pat """ - remove_inds = [] - for ii, shape in enumerate(pat.shapes): - if not isinstance(shape, (Polygon, Path)): - continue - try: - shape.clean_vertices() - except PatternError: - remove_inds.append(ii) - for ii in sorted(remove_inds, reverse=True): - del pat.shapes[ii] + for shapes in pat.shapes.values(): + remove_inds = [] + for ii, shape in enumerate(shapes): + if not isinstance(shape, (Polygon, Path)): + continue + try: + shape.clean_vertices() + except PatternError: + remove_inds.append(ii) + for ii in sorted(remove_inds, reverse=True): + del shapes[ii] return pat diff --git a/masque/label.py b/masque/label.py index 40b6aba..a6d459c 100644 --- a/masque/label.py +++ b/masque/label.py @@ -5,15 +5,15 @@ import numpy from numpy.typing import ArrayLike, NDArray from .repetition import Repetition -from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t -from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, RepeatableImpl +from .utils import rotation_matrix_2d, AutoSlots, annotations_t +from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded from .traits import AnnotatableImpl -class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl, - Pivotable, Copyable, metaclass=AutoSlots): +class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, + Bounded, Pivotable, Copyable, metaclass=AutoSlots): """ - A text annotation with a position and layer (but no size; it is not drawn) + A text annotation with a position (but no size; it is not drawn) """ __slots__ = ( '_string', ) @@ -40,13 +40,11 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl, string: str, *, offset: ArrayLike = (0.0, 0.0), - layer: layer_t = 0, repetition: Repetition | None = None, annotations: annotations_t | None = None, ) -> None: self.string = string self.offset = numpy.array(offset, dtype=float, copy=True) - self.layer = layer self.repetition = repetition self.annotations = annotations if annotations is not None else {} @@ -54,7 +52,6 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl, return type(self)( string=self.string, offset=self.offset.copy(), - layer=self.layer, repetition=self.repetition, ) diff --git a/masque/library.py b/masque/library.py index 0699ea6..f51d9f9 100644 --- a/masque/library.py +++ b/masque/library.py @@ -226,11 +226,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): flattened[name] = None pat = self[name].deepcopy() - for ref in pat.refs: - target = ref.target + for target in pat.refs: if target is None: continue - if target not in flattened: flatten_single(target) @@ -240,10 +238,11 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if target_pat.is_empty(): # avoid some extra allocations continue - p = ref.as_pattern(pattern=flattened[target]) - if not flatten_ports: - p.ports.clear() - pat.append(p) + for ref in pat.refs[target]: + p = ref.as_pattern(pattern=target_pat) + if not flatten_ports: + p.ports.clear() + pat.append(p) pat.refs.clear() flattened[name] = pat @@ -316,7 +315,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): names = set(self.keys()) not_toplevel: set[str | None] = set() for name in names: - not_toplevel |= set(sp.target for sp in self[name].refs) + not_toplevel |= set(self[name].refs.keys()) toplevel = list(names - not_toplevel) return toplevel @@ -352,9 +351,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): At each pattern in the tree, the following sequence is called: ``` current_pattern = visit_before(current_pattern, **vist_args) - for sp in current_pattern.refs] - self.dfs(sp.target, visit_before, visit_after, - hierarchy + (sp.target,), updated_transform, memo) + for target in current_pattern.refs: + for ref in pattern.refs[target]: + self.dfs(target, visit_before, visit_after, + hierarchy + (sp.target,), updated_transform, memo) current_pattern = visit_after(current_pattern, **visit_args) ``` where `visit_args` are @@ -398,32 +398,33 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if visit_before is not None: pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform) - for ref in pattern.refs: - if transform is not False: - sign = numpy.ones(2) - 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[3] %= 2 - else: - ref_transform = False - - if ref.target is None: + for target in pattern.refs: + if target is None: continue - if ref.target in hierarchy: - raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"') + if target in hierarchy: + raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"') - self.dfs( - pattern=self[ref.target], - visit_before=visit_before, - visit_after=visit_after, - hierarchy=hierarchy + (ref.target,), - transform=ref_transform, - memo=memo, - ) + for ref in pattern.refs[target]: + if transform is not False: + sign = numpy.ones(2) + 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[3] %= 2 + else: + ref_transform = False + + self.dfs( + pattern=self[target], + visit_before=visit_before, + visit_after=visit_after, + hierarchy=hierarchy + (target,), + transform=ref_transform, + memo=memo, + ) if visit_after is not None: pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform) @@ -508,9 +509,9 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): self """ for pattern in self.values(): - for ref in pattern.refs: - if ref.target == old_target: - ref.target = new_target + if old_target in pattern.refs: + pattern.refs[new_target].extend(pattern.refs[old_target]) + del pattern.refs[old_target] return self def mkpat(self, name: str) -> tuple[str, 'Pattern']: @@ -549,6 +550,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): Returns: self """ + from .pattern import map_targets duplicates = set(self.keys()) & set(other.keys()) if not duplicates: @@ -572,8 +574,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): # Update references in the newly-added cells for old_name in temp: new_name = rename_map.get(old_name, old_name) - for ref in self[new_name].refs: - ref.target = rename_map.get(cast(str, ref.target), ref.target) + pat = self[new_name] + pat.refs = map_targets(pat.refs, rename_map) return rename_map @@ -644,11 +646,13 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): # Using the label tuple from `.normalized_form()` as a key, check how many of each shape # are present and store the shape function for each one for pat in tuple(self.values()): - for i, shape in enumerate(pat.shapes): - if not any(isinstance(shape, t) for t in exclude_types): - label, _values, func = shape.normalized_form(norm_value) - shape_funcs[label] = func - shape_counts[label] += 1 + for layer, sseq in pat.shapes.items(): + for shape in sseq: + if not any(isinstance(shape, t) for t in exclude_types): + base_label, _values, func = shape.normalized_form(norm_value) + label = (*base_label, layer) + shape_funcs[label] = func + shape_counts[label] += 1 shape_pats = {} for label, count in shape_counts.items(): @@ -656,7 +660,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): continue shape_func = shape_funcs[label] - shape_pat = Pattern(shapes=[shape_func()]) + shape_pat = Pattern() + shape_pat.shapes[label[-1]] += [shape_func()] shape_pats[label] = shape_pat # ## Second pass ## @@ -665,33 +670,36 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): # are to be replaced. # The `values` are `(offset, scale, rotation)`. - shape_table: MutableMapping[tuple, list] = defaultdict(list) - for i, shape in enumerate(pat.shapes): - if any(isinstance(shape, t) for t in exclude_types): - continue + shape_table: dict[tuple, list] = defaultdict(list) + for layer, sseq in pat.shapes.items(): + for i, shape in enumerate(sseq): + if any(isinstance(shape, t) for t in exclude_types): + continue - label, values, _func = shape.normalized_form(norm_value) + base_label, values, _func = shape.normalized_form(norm_value) + label = (*base_label, layer) - if label not in shape_pats: - continue + if label not in shape_pats: + continue - shape_table[label].append((i, values)) + shape_table[label].append((i, values)) # For repeated shapes, create a `Pattern` holding a normalized shape object, # and add `pat.refs` entries for each occurrence in pat. Also, note down that # we should delete the `pat.shapes` entries for which we made `Ref`s. shapes_to_remove = [] for label in shape_table: + layer = label[-1] target = label2name(label) - for i, values in shape_table[label]: + for ii, values in shape_table[label]: offset, scale, rotation, mirror_x = values pat.ref(target=target, offset=offset, scale=scale, rotation=rotation, mirrored=(mirror_x, False)) - shapes_to_remove.append(i) + shapes_to_remove.append(ii) - # Remove any shapes for which we have created refs. - for i in sorted(shapes_to_remove, reverse=True): - del pat.shapes[i] + # Remove any shapes for which we have created refs. + for ii in sorted(shapes_to_remove, reverse=True): + del pat.shapes[layer][ii] for ll, pp in shape_pats.items(): self[label2name(ll)] = pp @@ -722,28 +730,30 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): #name_func = lambda _pat, _shape: self.get_name('_rep') for pat in tuple(self.values()): - new_shapes = [] - for shape in pat.shapes: - if shape.repetition is None: - new_shapes.append(shape) - continue + for layer in pat.shapes: + new_shapes = [] + for shape in pat.shapes[layer]: + if shape.repetition is None: + new_shapes.append(shape) + continue - name = name_func(pat, shape) - self[name] = Pattern(shapes=[shape]) - pat.ref(name, repetition=shape.repetition) - shape.repetition = None - pat.shapes = new_shapes + name = name_func(pat, shape) + self[name] = Pattern(shapes={layer: [shape]}) + pat.ref(name, repetition=shape.repetition) + shape.repetition = None + pat.shapes[layer] = new_shapes - new_labels = [] - for label in pat.labels: - if label.repetition is None: - new_labels.append(label) - continue - name = name_func(pat, label) - self[name] = Pattern(labels=[label]) - pat.ref(name, repetition=label.repetition) - label.repetition = None - pat.labels = new_labels + for layer in pat.labels: + new_labels = [] + for label in pat.labels[layer]: + if label.repetition is None: + new_labels.append(label) + continue + name = name_func(pat, label) + self[name] = Pattern(labels={layer: [label]}) + pat.ref(name, repetition=label.repetition) + label.repetition = None + pat.labels[layer] = new_labels return self @@ -782,8 +792,12 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): while empty := set(name for name, pat in self.items() if pat.is_empty()): for name in empty: del self[name] + for pat in self.values(): - pat.refs = [ref for ref in pat.refs if ref.target not in empty] + for name in empty: + # Second pass to skip looking at refs in empty patterns + if name in pat.refs: + del pat.refs[name] trimmed |= empty if not repeat: @@ -799,7 +813,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): del self[key] if delete_refs: for pat in self.values(): - pat.refs = [ref for ref in pat.refs if ref.target != key] + if key in pat.refs: + del pat.refs[key] return self @@ -1007,9 +1022,9 @@ class LazyLibrary(ILibrary): """ self.precache() for pattern in self.cache.values(): - for ref in pattern.refs: - if ref.target == old_target: - ref.target = new_target + if old_target in pattern.refs: + pattern.refs[new_target].extend(pattern.refs[old_target]) + del pattern.refs[old_target] return self def precache(self) -> Self: diff --git a/masque/pattern.py b/masque/pattern.py index ef27216..e8770fc 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -1,10 +1,11 @@ """ Base object representing a lithography mask. """ - -from typing import Callable, Sequence, cast, Mapping, Self, Any +from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar import copy +import logging from itertools import chain +from collections import defaultdict import numpy from numpy import inf @@ -12,14 +13,17 @@ from numpy.typing import NDArray, ArrayLike # .visualize imports matplotlib and matplotlib.collections from .ref import Ref -from .shapes import Shape, Polygon, DEFAULT_POLY_NUM_VERTICES +from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES from .label import Label -from .utils import rotation_matrix_2d, annotations_t +from .utils import rotation_matrix_2d, annotations_t, layer_t from .error import PatternError -from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable +from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded from .ports import Port, PortList +logger = logging.getLogger(__name__) + + class Pattern(PortList, AnnotatableImpl, Mirrorable): """ 2D layout consisting of some set of shapes, labels, and references to other Pattern objects @@ -31,15 +35,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): '_offset', '_annotations', ) - shapes: list[Shape] - """ List of all shapes in this Pattern. + shapes: defaultdict[layer_t, list[Shape]] + """ Stores of all shapes in this Pattern, indexed by layer. Elements in this list are assumed to inherit from Shape or provide equivalent functions. """ - labels: list[Label] + labels: defaultdict[layer_t, list[Label]] """ List of all labels in this Pattern. """ - refs: list[Ref] + refs: defaultdict[str | None, list[Ref]] """ List of all references to other patterns (`Ref`s) in this `Pattern`. Multiple objects in this list may reference the same Pattern object (i.e. multiple instances of the same object). @@ -59,9 +63,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): def __init__( self, *, - shapes: Sequence[Shape] = (), - labels: Sequence[Label] = (), - refs: Sequence[Ref] = (), + shapes: Mapping[layer_t, Sequence[Shape]] | None = None, + labels: Mapping[layer_t, Sequence[Label]] | None = None, + refs: Mapping[str | None, Sequence[Ref]] | None = None, annotations: annotations_t | None = None, ports: Mapping[str, 'Port'] | None = None ) -> None: @@ -76,20 +80,18 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): annotations: Initial annotations for the pattern ports: Any ports in the pattern """ - if isinstance(shapes, list): - self.shapes = shapes - else: - self.shapes = list(shapes) - - if isinstance(labels, list): - self.labels = labels - else: - self.labels = list(labels) - - if isinstance(refs, list): - self.refs = refs - else: - self.refs = list(refs) + self.shapes = defaultdict(list) + self.labels = defaultdict(list) + self.refs = defaultdict(list) + if shapes: + for layer, sseq in shapes.items(): + self.shapes[layer].extend(sseq) + if labels: + for layer, lseq in labels.items(): + self.labels[layer].extend(lseq) + if refs: + for target, rseq in refs.items(): + self.refs[target].extend(rseq) if ports is not None: self.ports = dict(copy.deepcopy(ports)) @@ -99,32 +101,42 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): self.annotations = annotations if annotations is not None else {} def __repr__(self) -> str: - s = f' 'Pattern': - return Pattern( - shapes=copy.deepcopy(self.shapes), - labels=copy.deepcopy(self.labels), - refs=[copy.copy(sp) for sp in self.refs], + 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), ) + for target, rseq in self.refs.items(): + new.refs[target].extend(rseq) + for layer, sseq in self.shapes.items(): + new.shapes[layer].extend(sseq) + for layer, lseq in self.labels.items(): + new.labels[layer].extend(lseq) - def __deepcopy__(self, memo: dict | None = None) -> 'Pattern': - memo = {} if memo is None else memo - new = Pattern( - shapes=copy.deepcopy(self.shapes, memo), - labels=copy.deepcopy(self.labels, memo), - refs=copy.deepcopy(self.refs, memo), - annotations=copy.deepcopy(self.annotations, memo), - ports=copy.deepcopy(self.ports), - ) return new +# def __deepcopy__(self, memo: dict | None = None) -> 'Pattern': +# memo = {} if memo is None else memo +# new = Pattern( +# shapes=copy.deepcopy(self.shapes, memo), +# labels=copy.deepcopy(self.labels, memo), +# refs=copy.deepcopy(self.refs, memo), +# annotations=copy.deepcopy(self.annotations, memo), +# ports=copy.deepcopy(self.ports), +# ) +# return new + def append(self, other_pattern: 'Pattern') -> Self: """ Appends all shapes, labels and refs from other_pattern to self's shapes, @@ -136,9 +148,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - self.refs += other_pattern.refs - self.shapes += other_pattern.shapes - self.labels += other_pattern.labels + for target, rseq in other_pattern.refs.items(): + self.refs[target].extend(rseq) + for layer, sseq in other_pattern.shapes.items(): + self.shapes[layer].extend(sseq) + for layer, lseq in other_pattern.labels.items(): + self.labels[layer].extend(lseq) annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys()) if annotation_conflicts: @@ -154,9 +169,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): def subset( self, - shapes: Callable[[Shape], bool] | None = None, - labels: Callable[[Label], bool] | None = None, - refs: Callable[[Ref], bool] | None = None, + shapes: Callable[[layer_t, Shape], bool] | None = None, + labels: Callable[[layer_t, Label], bool] | None = None, + refs: Callable[[str | None, Ref], bool] | None = None, annotations: Callable[[str, list[int | float | str]], bool] | None = None, ports: Callable[[str, Port], bool] | None = None, default_keep: bool = False @@ -167,9 +182,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Self is _not_ altered, but shapes, labels, and refs are _not_ copied, just referenced. Args: - shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset. - labels: Given a label, returns a boolean denoting whether the label is a member of the subset. - refs: Given a ref, returns a boolean denoting if it is a member of the subset. + shapes: Given a layer and shape, returns a boolean denoting whether the shape is a + member of the subset. + labels: Given a layer and label, returns a boolean denoting whether the label is a + member of the subset. + refs: Given a target and ref, returns a boolean denoting if it is a member of the subset. annotations: Given an annotation, returns a boolean denoting if it is a member of the subset. ports: Given a port, returns a boolean denoting if it is a member of the subset. default_keep: If `True`, keeps all elements of a given type if no function is supplied. @@ -182,17 +199,20 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): pat = Pattern() if shapes is not None: - pat.shapes = [s for s in self.shapes if shapes(s)] + for layer in self.shapes: + pat.shapes[layer] = [ss for ss in self.shapes[layer] if shapes(layer, ss)] elif default_keep: pat.shapes = copy.copy(self.shapes) if labels is not None: - pat.labels = [s for s in self.labels if labels(s)] + for layer in self.labels: + pat.labels[layer] = [ll for ll in self.labels[layer] if labels(layer, ll)] elif default_keep: pat.labels = copy.copy(self.labels) if refs is not None: - pat.refs = [s for s in self.refs if refs(s)] + for target in self.refs: + pat.refs[target] = [rr for rr in self.refs[target] if refs(target, rr)] elif default_keep: pat.refs = copy.copy(self.refs) @@ -227,10 +247,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - old_shapes = self.shapes - self.shapes = list(chain.from_iterable(( - shape.to_polygons(num_vertices, max_arclen) - for shape in old_shapes))) + for layer in self.shapes: + self.shapes[layer] = list(chain.from_iterable( + ss.to_polygons(num_vertices, max_arclen) + for ss in self.shapes[layer] + )) return self def manhattanize( @@ -251,9 +272,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): """ self.polygonize() - old_shapes = self.shapes - self.shapes = list(chain.from_iterable( - (shape.manhattanize(grid_x, grid_y) for shape in old_shapes))) + for layer in self.shapes: + self.shapes[layer] = list(chain.from_iterable(( + ss.manhattanize(grid_x, grid_y) + for ss in self.shapes[layer] + ))) return self def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]: @@ -268,7 +291,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): is of the form `[[x0, y0], [x1, y1],...]`. """ pat = self.deepcopy().polygonize().flatten(library=library) - return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now + polys = [ + cast(Polygon, shape).vertices + cast(Polygon, shape).offset + for shape in chain_elements(pat.shapes) + ] + return polys def referenced_patterns(self) -> set[str | None]: """ @@ -277,7 +304,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: A set of all pattern names referenced by this pattern. """ - return set(sp.target for sp in self.refs) + return set(self.refs.keys()) def get_bounds( self, @@ -301,23 +328,29 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): min_bounds = numpy.array((+inf, +inf)) max_bounds = numpy.array((-inf, -inf)) - for entry in chain(self.shapes, self.labels): - bounds = entry.get_bounds() + for entry in chain_elements(self.shapes, self.labels): + bounds = cast(Bounded, entry).get_bounds() if bounds is None: continue min_bounds = numpy.minimum(min_bounds, bounds[0, :]) max_bounds = numpy.maximum(max_bounds, bounds[1, :]) - if self.refs and (library is None): - raise PatternError('Must provide a library to get_bounds() to resolve refs') + if recurse and self.has_refs(): + if library is None: + raise PatternError('Must provide a library to get_bounds() to resolve refs') - if recurse: - for entry in self.refs: - bounds = entry.get_bounds(library=library) - if bounds is None: + for target, refs in self.refs.items(): + if target is None: continue - min_bounds = numpy.minimum(min_bounds, bounds[0, :]) - max_bounds = numpy.maximum(max_bounds, bounds[1, :]) + if not refs: + continue + target_pat = library[target] + for ref in refs: + bounds = ref.get_bounds(target_pat, library=library) + if bounds is None: + continue + min_bounds = numpy.minimum(min_bounds, bounds[0, :]) + max_bounds = numpy.maximum(max_bounds, bounds[1, :]) if (max_bounds < min_bounds).any(): return None @@ -352,7 +385,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()): + for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()): cast(Positionable, entry).translate(offset) return self @@ -366,7 +399,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - for entry in chain(self.shapes, self.refs): + for entry in chain_elements(self.shapes, self.refs): cast(Scalable, entry).scale_by(c) return self @@ -382,7 +415,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - for entry in chain(self.shapes, self.refs): + for entry in chain_elements(self.shapes, self.refs): cast(Positionable, entry).offset *= c cast(Scalable, entry).scale_by(c) @@ -390,7 +423,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): if rep: rep.scale_by(c) - for label in self.labels: + for label in chain_elements(self.labels): cast(Positionable, label).offset *= c rep = cast(Repeatable, label).repetition @@ -429,7 +462,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()): + 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) return self @@ -444,7 +477,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - for entry in chain(self.shapes, self.refs, self.ports.values()): + for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()): cast(Rotatable, entry).rotate(rotation) return self @@ -459,7 +492,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()): + for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()): cast(Positionable, entry).offset[across_axis - 1] *= -1 return self @@ -475,7 +508,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - for entry in chain(self.shapes, self.refs, self.ports.values()): + for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()): cast(Mirrorable, entry).mirror(across_axis) return self @@ -521,68 +554,95 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: True if the pattern is contains no shapes, labels, or refs. """ - return (len(self.refs) == 0 - and len(self.shapes) == 0 - and len(self.labels) == 0) + return not (self.has_refs() or self.has_shapes() or self.has_labels()) - def ref(self, *args: Any, **kwargs: Any) -> Self: + def has_refs(self) -> bool: + return any(True for _ in chain.from_iterable(self.refs.values())) + + def has_shapes(self) -> bool: + return any(True for _ in chain.from_iterable(self.shapes.values())) + + def has_labels(self) -> bool: + return any(True for _ in chain.from_iterable(self.labels.values())) + + def ref(self, target: str | None, *args: Any, **kwargs: Any) -> Self: """ Convenience function which constructs a `Ref` object and adds it to this pattern. Args: + target: Target for the ref *args: Passed to `Ref()` **kwargs: Passed to `Ref()` Returns: self """ - self.refs.append(Ref(*args, **kwargs)) + self.refs[target].append(Ref(*args, **kwargs)) return self - def polygon(self, *args: Any, **kwargs: Any) -> Self: + def polygon(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self: """ Convenience function which constructs a `Polygon` object and adds it to this pattern. Args: + layer: Layer for the polygon *args: Passed to `Polygon()` **kwargs: Passed to `Polygon()` Returns: self """ - self.shapes.append(Polygon(*args, **kwargs)) + self.shapes[layer].append(Polygon(*args, **kwargs)) return self - def rect(self, *args: Any, **kwargs: Any) -> Self: + def rect(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self: """ Convenience function which calls `Polygon.rect` to construct a rectangle and adds it to this pattern. Args: + layer: Layer for the rectangle *args: Passed to `Polygon.rect()` **kwargs: Passed to `Polygon.rect()` Returns: self """ - self.shapes.append(Polygon.rect(*args, **kwargs)) + self.shapes[layer].append(Polygon.rect(*args, **kwargs)) return self - def label(self, *args: Any, **kwargs: Any) -> Self: + def path(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self: + """ + Convenience function which constructs a `Path` object and adds it + to this pattern. + + Args: + layer: Layer for the path + *args: Passed to `Path()` + **kwargs: Passed to `Path()` + + Returns: + self + """ + self.shapes[layer].append(Path(*args, **kwargs)) + return self + + def label(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self: """ Convenience function which constructs a `Label` object and adds it to this pattern. Args: + layer: Layer for the label *args: Passed to `Label()` **kwargs: Passed to `Label()` Returns: self """ - self.labels.append(Label(*args, **kwargs)) + self.labels[layer].append(Label(*args, **kwargs)) return self def flatten( @@ -610,24 +670,26 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): pat = library[name].deepcopy() flattened[name] = None - for ref in pat.refs: - target = ref.target + for target, refs in pat.refs.items(): if target is None: continue + if not refs: + continue if target not in flattened: flatten_single(target) - target_pat = flattened[target] + if target_pat is None: raise PatternError(f'Circular reference in {name} to {target}') if target_pat.is_empty(): # avoid some extra allocations continue - p = ref.as_pattern(pattern=flattened[target]) - if not flatten_ports: - p.ports.clear() - pat.append(p) + for ref in refs: + p = ref.as_pattern(pattern=target_pat) + if not flatten_ports: + p.ports.clear() + pat.append(p) pat.refs.clear() flattened[name] = pat @@ -661,7 +723,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): from matplotlib import pyplot # type: ignore import matplotlib.collections # type: ignore - if self.refs and library is None: + if self.has_refs() and library is None: raise PatternError('Must provide a library when visualizing a pattern with refs') offset = numpy.array(offset, dtype=float) @@ -675,7 +737,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): axes = figure.gca() polygons = [] - for shape in self.shapes: + for shape in chain.from_iterable(self.shapes.values()): polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()] mpl_poly_collection = matplotlib.collections.PolyCollection( @@ -686,16 +748,52 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): axes.add_collection(mpl_poly_collection) pyplot.axis('equal') - for ref in self.refs: - ref.as_pattern(library=library).visualize( - library=library, - offset=offset, - overdraw=True, - line_color=line_color, - fill_color=fill_color, - ) + for target, refs in self.refs.items(): + if target is None: + continue + if not refs: + continue + assert library is not None + target_pat = library[target] + for ref in refs: + ref.as_pattern(target_pat).visualize( + library=library, + offset=offset, + overdraw=True, + line_color=line_color, + fill_color=fill_color, + ) if not overdraw: pyplot.xlabel('x') pyplot.ylabel('y') pyplot.show() + + +TT = TypeVar('TT') + + +def chain_elements(*args: Mapping[Any, Iterable[TT]]) -> Iterable[TT]: + return chain(*(chain.from_iterable(aa.values()) for aa in args)) + + +def map_layers( + elements: Mapping[layer_t, Sequence[TT]], + layer_map: Mapping[layer_t, layer_t], + ) -> defaultdict[layer_t, list[TT]]: + new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list) + for old_layer, seq in elements.items(): + new_layer = layer_map.get(old_layer, old_layer) + new_elements[new_layer].extend(seq) + return new_elements + + +def map_targets( + refs: Mapping[str | None, Sequence[Ref]], + target_map: Mapping[str | None, str | None] | Mapping[str, str | None], + ) -> defaultdict[str | None, list[Ref]]: + new_refs: defaultdict[str | None, list[Ref]] = defaultdict(list) + for old_target, seq in refs.items(): + new_target = target_map.get(old_target, old_target) # type: ignore # OK to .get() wrong type + new_refs[new_target].extend(seq) + return new_refs diff --git a/masque/ref.py b/masque/ref.py index d3bdc94..83b1e8b 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -33,20 +33,16 @@ class Ref( offset, rotation, scaling, and associated methods. """ __slots__ = ( - '_target', '_mirrored', + '_mirrored', # inherited '_offset', '_rotation', 'scale', '_repetition', '_annotations', ) - _target: str | None - """ The name of the `Pattern` being instanced """ - _mirrored: NDArray[numpy.bool_] """ Whether to mirror the instance across the x and/or y axes. """ def __init__( self, - target: str | None, *, offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, @@ -57,14 +53,12 @@ class Ref( ) -> None: """ Args: - target: Name of the Pattern to reference. 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. scale: Scaling factor applied to the pattern's geometry. repetition: `Repetition` object, default `None` """ - self.target = target self.offset = offset self.rotation = rotation self.scale = scale @@ -76,7 +70,6 @@ class Ref( def __copy__(self) -> 'Ref': new = Ref( - target=self.target, offset=self.offset.copy(), rotation=self.rotation, scale=self.scale, @@ -93,17 +86,6 @@ class Ref( new.annotations = copy.deepcopy(self.annotations, memo) return new - # target property - @property - def target(self) -> str | None: - return self._target - - @target.setter - def target(self, val: str | None) -> None: - if val is not None and not isinstance(val, str): - raise PatternError(f'Provided target {val} is not a str or None!') - self._target = val - # Mirrored property @property def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]: @@ -117,27 +99,16 @@ class Ref( def as_pattern( self, - *, - pattern: 'Pattern | None' = None, - library: Mapping[str, 'Pattern'] | None = None, + pattern: 'Pattern', ) -> 'Pattern': """ Args: pattern: Pattern object to transform - library: A str->Pattern mapping, used instead of `pattern`. Must contain - `self.target`. Returns: A copy of the referenced Pattern which has been scaled, rotated, etc. according to this `Ref`'s properties. """ - if pattern is None: - if library is None: - raise PatternError('as_pattern() must be given a pattern or library.') - - assert self.target is not None - pattern = library[self.target] - pattern = pattern.deepcopy() if self.scale != 1: @@ -175,8 +146,8 @@ class Ref( def get_bounds( self, + pattern: 'Pattern', *, - pattern: 'Pattern | None' = None, library: Mapping[str, 'Pattern'] | None = None, ) -> NDArray[numpy.float64] | None: """ @@ -190,20 +161,29 @@ class Ref( Returns: `[[x_min, y_min], [x_max, y_max]]` or `None` """ - if pattern is None and library is None: - raise PatternError('as_pattern() must be given a pattern or library.') - if pattern is None and self.target is None: - return None - if library is not None and self.target not in library: - raise PatternError(f'get_bounds() called on dangling reference to "{self.target}"') - if pattern is not None and pattern.is_empty(): + if pattern.is_empty(): # no need to run as_pattern() return None - return self.as_pattern(pattern=pattern, library=library).get_bounds(library) + return self.as_pattern(pattern=pattern).get_bounds(library) # TODO can just take pattern's bounds and then transform those! + + def get_bounds_nonempty( + self, + pattern: 'Pattern', + *, + library: Mapping[str, 'Pattern'] | None = None, + ) -> NDArray[numpy.float64]: + """ + Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity. + Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None). + + This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()` + """ + bounds = self.get_bounds(pattern, library=library) + assert bounds is not None + return bounds def __repr__(self) -> str: - name = f'"{self.target}"' if self.target is not None else None - rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else '' + 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 '' - return f'' + return f'' diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index ac60630..ae1fc9a 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -9,7 +9,7 @@ from numpy.typing import NDArray, ArrayLike from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, layer_t, annotations_t +from ..utils import is_scalar, annotations_t class Arc(Shape): @@ -24,7 +24,7 @@ class Arc(Shape): __slots__ = ( '_radii', '_angles', '_width', '_rotation', # Inherited - '_offset', '_layer', '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _radii: NDArray[numpy.float64] @@ -156,7 +156,6 @@ class Arc(Shape): offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), - layer: layer_t = 0, repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -172,7 +171,6 @@ class Arc(Shape): self._rotation = rotation self._repetition = repetition self._annotations = annotations if annotations is not None else {} - self._layer = layer else: self.radii = radii self.angles = angles @@ -181,7 +179,6 @@ class Arc(Shape): self.rotation = rotation self.repetition = repetition self.annotations = annotations if annotations is not None else {} - self.layer = layer [self.mirror(a) for a, do in enumerate(mirrored) if do] def __deepcopy__(self, memo: dict | None = None) -> 'Arc': @@ -241,7 +238,7 @@ class Arc(Shape): ys = numpy.hstack((ys1, ys2)) xys = numpy.vstack((xs, ys)).T - poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation) + poly = Polygon(xys, offset=self.offset, rotation=self.rotation) return [poly] def get_bounds(self) -> NDArray[numpy.float64]: @@ -352,13 +349,12 @@ class Arc(Shape): rotation %= 2 * pi width = self.width - return ((type(self), radii, angles, width / norm_value, self.layer), + return ((type(self), radii, angles, width / norm_value), (self.offset, scale / norm_value, rotation, False), lambda: Arc( radii=radii * norm_value, angles=angles, width=width * norm_value, - layer=self.layer, )) def get_cap_edges(self) -> NDArray[numpy.float64]: @@ -415,4 +411,4 @@ class Arc(Shape): def __repr__(self) -> str: angles = f' a°{numpy.rad2deg(self.angles)}' rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' - return f'' + return f'' diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 2390ece..5f3abf1 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, layer_t, annotations_t +from ..utils import is_scalar, annotations_t class Circle(Shape): @@ -17,7 +17,7 @@ class Circle(Shape): __slots__ = ( '_radius', # Inherited - '_offset', '_layer', '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _radius: float @@ -44,7 +44,6 @@ class Circle(Shape): radius: float, *, offset: ArrayLike = (0.0, 0.0), - layer: layer_t = 0, repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -55,13 +54,11 @@ class Circle(Shape): self._offset = offset self._repetition = repetition self._annotations = annotations if annotations is not None else {} - self._layer = layer else: self.radius = radius self.offset = offset self.repetition = repetition self.annotations = annotations if annotations is not None else {} - self.layer = layer def __deepcopy__(self, memo: dict | None = None) -> 'Circle': memo = {} if memo is None else memo @@ -90,7 +87,7 @@ class Circle(Shape): ys = numpy.sin(thetas) * self.radius xys = numpy.vstack((xs, ys)).T - return [Polygon(xys, offset=self.offset, layer=self.layer)] + return [Polygon(xys, offset=self.offset)] def get_bounds(self) -> NDArray[numpy.float64]: return numpy.vstack((self.offset - self.radius, @@ -110,9 +107,9 @@ class Circle(Shape): def normalized_form(self, norm_value) -> normalized_shape_tuple: rotation = 0.0 magnitude = self.radius / norm_value - return ((type(self), self.layer), + return ((type(self),), (self.offset, magnitude, rotation, False), - lambda: Circle(radius=norm_value, layer=self.layer)) + lambda: Circle(radius=norm_value)) def __repr__(self) -> str: - return f'' + return f'' diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index f96f86a..da4db39 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -9,7 +9,7 @@ from numpy.typing import ArrayLike, NDArray from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, layer_t, annotations_t +from ..utils import is_scalar, rotation_matrix_2d, annotations_t class Ellipse(Shape): @@ -20,7 +20,7 @@ class Ellipse(Shape): __slots__ = ( '_radii', '_rotation', # Inherited - '_offset', '_layer', '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _radii: NDArray[numpy.float64] @@ -91,7 +91,6 @@ class Ellipse(Shape): offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), - layer: layer_t = 0, repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -104,14 +103,12 @@ class Ellipse(Shape): self._rotation = rotation self._repetition = repetition self._annotations = annotations if annotations is not None else {} - self._layer = layer else: self.radii = radii self.offset = offset self.rotation = rotation self.repetition = repetition self.annotations = annotations if annotations is not None else {} - self.layer = layer [self.mirror(a) for a, do in enumerate(mirrored) if do] def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse': @@ -152,7 +149,7 @@ class Ellipse(Shape): ys = r1 * sin_th xys = numpy.vstack((xs, ys)).T - poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation) + poly = Polygon(xys, offset=self.offset, rotation=self.rotation) return [poly] def get_bounds(self) -> NDArray[numpy.float64]: @@ -183,10 +180,10 @@ class Ellipse(Shape): radii = self.radii[::-1] / self.radius_y scale = self.radius_y angle = (self.rotation + pi / 2) % pi - return ((type(self), radii, self.layer), + return ((type(self), radii), (self.offset, scale / norm_value, angle, False), - lambda: Ellipse(radii=radii * norm_value, layer=self.layer)) + lambda: Ellipse(radii=radii * norm_value)) def __repr__(self) -> str: - rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else '' - return f'' + rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' + return f'' diff --git a/masque/shapes/path.py b/masque/shapes/path.py index eba3211..141b6a7 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -9,7 +9,7 @@ from numpy.typing import NDArray, ArrayLike from . import Shape, normalized_shape_tuple, Polygon, Circle from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, layer_t +from ..utils import is_scalar, rotation_matrix_2d from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t @@ -31,7 +31,7 @@ class Path(Shape): __slots__ = ( '_vertices', '_width', '_cap', '_cap_extensions', # Inherited - '_offset', '_layer', '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _vertices: NDArray[numpy.float64] _width: float @@ -154,7 +154,6 @@ class Path(Shape): offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), - layer: layer_t = 0, repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -169,7 +168,6 @@ class Path(Shape): self._offset = offset self._repetition = repetition self._annotations = annotations if annotations is not None else {} - self._layer = layer self._width = width self._cap = cap self._cap_extensions = cap_extensions @@ -178,7 +176,6 @@ class Path(Shape): self.offset = offset self.repetition = repetition self.annotations = annotations if annotations is not None else {} - self.layer = layer self.width = width self.cap = cap self.cap_extensions = cap_extensions @@ -204,7 +201,6 @@ class Path(Shape): offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), - layer: layer_t = 0, ) -> 'Path': """ Build a path by specifying the turn angles and travel distances @@ -224,7 +220,6 @@ class Path(Shape): 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)` - layer: Layer, default `0` Returns: The resulting Path object @@ -238,8 +233,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, - layer=layer) + offset=offset, rotation=rotation, mirrored=mirrored) def to_polygons( self, @@ -254,7 +248,7 @@ class Path(Shape): if self.width == 0: verts = numpy.vstack((v, v[::-1])) - return [Polygon(offset=self.offset, vertices=verts, layer=self.layer)] + return [Polygon(offset=self.offset, vertices=verts)] perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 @@ -305,12 +299,12 @@ class Path(Shape): o1.append(v[-1] - perp[-1]) verts = numpy.vstack((o0, o1[::-1])) - polys = [Polygon(offset=self.offset, vertices=verts, layer=self.layer)] + polys = [Polygon(offset=self.offset, vertices=verts)] if self.cap == PathCap.Circle: #for vert in v: # not sure if every vertex, or just ends? for vert in [v[0], v[-1]]: - circ = Circle(offset=vert, radius=self.width / 2, layer=self.layer) + circ = Circle(offset=vert, radius=self.width / 2) polys += circ.to_polygons(num_vertices=num_vertices, max_arclen=max_arclen) return polys @@ -370,13 +364,12 @@ class Path(Shape): width0 = self.width / norm_value - return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), + return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap), (offset, scale / norm_value, rotation, False), lambda: Path( reordered_vertices * norm_value, width=self.width * norm_value, cap=self.cap, - layer=self.layer, )) def clean_vertices(self) -> 'Path': @@ -422,4 +415,4 @@ class Path(Shape): def __repr__(self) -> str: centroid = self.offset + self.vertices.mean(axis=0) - return f'' + return f'' diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index c8f8326..fb90590 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -8,7 +8,7 @@ from numpy.typing import NDArray, ArrayLike from . import Shape, normalized_shape_tuple from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, layer_t +from ..utils import is_scalar, rotation_matrix_2d from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t @@ -22,7 +22,7 @@ class Polygon(Shape): __slots__ = ( '_vertices', # Inherited - '_offset', '_layer', '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _vertices: NDArray[numpy.float64] @@ -82,7 +82,6 @@ class Polygon(Shape): offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, mirrored: Sequence[bool] = (False, False), - layer: layer_t = 0, repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -94,13 +93,11 @@ class Polygon(Shape): self._offset = offset self._repetition = repetition self._annotations = annotations if annotations is not None else {} - self._layer = layer else: self.vertices = vertices self.offset = offset self.repetition = repetition self.annotations = annotations if annotations is not None else {} - self.layer = layer self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] @@ -118,7 +115,6 @@ class Polygon(Shape): *, rotation: float = 0.0, offset: ArrayLike = (0.0, 0.0), - layer: layer_t = 0, repetition: Repetition | None = None, ) -> 'Polygon': """ @@ -128,7 +124,6 @@ class Polygon(Shape): side_length: Length of one side rotation: Rotation counterclockwise, in radians offset: Offset, default `(0, 0)` - layer: Layer, default `0` repetition: `Repetition` object, default `None` Returns: @@ -139,7 +134,7 @@ class Polygon(Shape): [+1, +1], [+1, -1]], dtype=float) vertices = 0.5 * side_length * norm_square - poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition) + poly = Polygon(vertices, offset=offset, repetition=repetition) poly.rotate(rotation) return poly @@ -150,7 +145,6 @@ class Polygon(Shape): *, rotation: float = 0, offset: ArrayLike = (0.0, 0.0), - layer: layer_t = 0, repetition: Repetition | None = None, ) -> 'Polygon': """ @@ -161,7 +155,6 @@ class Polygon(Shape): ly: Length along y (before rotation) rotation: Rotation counterclockwise, in radians offset: Offset, default `(0, 0)` - layer: Layer, default `0` repetition: `Repetition` object, default `None` Returns: @@ -171,7 +164,7 @@ class Polygon(Shape): [-lx, +ly], [+lx, +ly], [+lx, -ly]], dtype=float) - poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition) + poly = Polygon(vertices, offset=offset, repetition=repetition) poly.rotate(rotation) return poly @@ -186,7 +179,6 @@ class Polygon(Shape): yctr: float | None = None, ymax: float | None = None, ly: float | None = None, - layer: layer_t = 0, repetition: Repetition | None = None, ) -> 'Polygon': """ @@ -204,7 +196,6 @@ class Polygon(Shape): yctr: Center y coordinate ymax: Maximum y coordinate ly: Length along y direction - layer: Layer, default `0` repetition: `Repetition` object, default `None` Returns: @@ -270,7 +261,7 @@ class Polygon(Shape): else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') - poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), layer=layer, repetition=repetition) + poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), repetition=repetition) return poly @staticmethod @@ -281,7 +272,6 @@ class Polygon(Shape): regular: bool = True, center: ArrayLike = (0.0, 0.0), rotation: float = 0.0, - layer: layer_t = 0, repetition: Repetition | None = None, ) -> 'Polygon': """ @@ -300,7 +290,6 @@ class Polygon(Shape): rotation: Rotation counterclockwise, in radians. `0` results in four axis-aligned sides (the long sides of the irregular octagon). - layer: Layer, default `0` repetition: `Repetition` object, default `None` Returns: @@ -327,7 +316,7 @@ class Polygon(Shape): side_length = 2 * inner_radius / s vertices = 0.5 * side_length * norm_oct - poly = Polygon(vertices, offset=center, layer=layer, repetition=repetition) + poly = Polygon(vertices, offset=center, repetition=repetition) poly.rotate(rotation) return poly @@ -378,9 +367,9 @@ class Polygon(Shape): # TODO: normalize mirroring? - return ((type(self), reordered_vertices.data.tobytes(), self.layer), + return ((type(self), reordered_vertices.data.tobytes()), (offset, scale / norm_value, rotation, False), - lambda: Polygon(reordered_vertices * norm_value, layer=self.layer)) + lambda: Polygon(reordered_vertices * norm_value)) def clean_vertices(self) -> 'Polygon': """ @@ -414,4 +403,4 @@ class Polygon(Shape): def __repr__(self) -> str: centroid = self.offset + self.vertices.mean(axis=0) - return f'' + return f'' diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 0699caf..c5237bb 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -5,9 +5,8 @@ import numpy from numpy.typing import NDArray, ArrayLike from ..traits import ( - Rotatable, Mirrorable, Copyable, Scalable, - PositionableImpl, LayerableImpl, - PivotableImpl, RepeatableImpl, AnnotatableImpl, + Rotatable, Mirrorable, Copyable, Scalable, Bounded, + PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl, ) if TYPE_CHECKING: @@ -26,7 +25,7 @@ normalized_shape_tuple = tuple[ DEFAULT_POLY_NUM_VERTICES = 24 -class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Scalable, +class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, Bounded, PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta): """ Class specifying functions common to all shapes. @@ -194,10 +193,7 @@ class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Sc vertex_lists.append(vlist) polygon_contours.append(numpy.vstack(vertex_lists)) - manhattan_polygons = [ - Polygon(vertices=contour, layer=self.layer) - for contour in polygon_contours - ] + manhattan_polygons = [Polygon(vertices=contour) for contour in polygon_contours] return manhattan_polygons @@ -292,9 +288,6 @@ class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Sc vertices = numpy.hstack((grx[snapped_contour[:, None, 0] + offset_i[0]], gry[snapped_contour[:, None, 1] + offset_i[1]])) - manhattan_polygons.append(Polygon( - vertices=vertices, - layer=self.layer, - )) + manhattan_polygons.append(Polygon(vertices=vertices)) return manhattan_polygons diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 940ea15..028cee6 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -9,7 +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, layer_t +from ..utils import is_scalar, get_bit, normalize_mirror from ..utils import annotations_t # Loaded on use: @@ -25,7 +25,7 @@ class Text(RotatableImpl, Shape): __slots__ = ( '_string', '_height', '_mirrored', 'font_path', # Inherited - '_offset', '_layer', '_repetition', '_annotations', '_rotation', + '_offset', '_repetition', '_annotations', '_rotation', ) _string: str @@ -73,7 +73,6 @@ class Text(RotatableImpl, Shape): offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, mirrored: ArrayLike = (False, False), - layer: layer_t = 0, repetition: Repetition | None = None, annotations: annotations_t | None = None, raw: bool = False, @@ -82,7 +81,6 @@ class Text(RotatableImpl, Shape): assert isinstance(offset, numpy.ndarray) assert isinstance(mirrored, numpy.ndarray) self._offset = offset - self._layer = layer self._string = string self._height = height self._rotation = rotation @@ -91,7 +89,6 @@ class Text(RotatableImpl, Shape): self._annotations = annotations if annotations is not None else {} else: self.offset = offset - self.layer = layer self.string = string self.height = height self.rotation = rotation @@ -120,7 +117,7 @@ class Text(RotatableImpl, Shape): # Move these polygons to the right of the previous letter for xys in raw_polys: - poly = Polygon(xys, layer=self.layer) + poly = Polygon(xys) poly.mirror2d(self.mirrored) poly.scale_by(self.height) poly.offset = self.offset + [total_advance, 0] @@ -144,7 +141,7 @@ class Text(RotatableImpl, Shape): mirror_x, rotation = normalize_mirror(self.mirrored) rotation += self.rotation rotation %= 2 * pi - return ((type(self), self.string, self.font_path, self.layer), + return ((type(self), self.string, self.font_path), (self.offset, self.height / norm_value, rotation, mirror_x), lambda: Text( string=self.string, @@ -152,7 +149,6 @@ class Text(RotatableImpl, Shape): font_path=self.font_path, rotation=rotation, mirrored=(mirror_x, False), - layer=self.layer, )) def get_bounds(self) -> NDArray[numpy.float64]: @@ -256,6 +252,6 @@ def get_char_as_polygons( return polygons, advance def __repr__(self) -> str: - rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else '' + 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 '' - return f'' + return f'' diff --git a/masque/traits/__init__.py b/masque/traits/__init__.py index 996b313..6b1541f 100644 --- a/masque/traits/__init__.py +++ b/masque/traits/__init__.py @@ -1,7 +1,7 @@ """ Traits (mixins) and default implementations """ -from .positionable import Positionable, PositionableImpl +from .positionable import Positionable, PositionableImpl, Bounded from .layerable import Layerable, LayerableImpl from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl from .repeatable import Repeatable, RepeatableImpl diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index 9067cbb..07b2e67 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -60,6 +60,8 @@ class Positionable(metaclass=ABCMeta): """ pass + +class Bounded(metaclass=ABCMeta): @abstractmethod def get_bounds(self) -> NDArray[numpy.float64] | None: """ diff --git a/masque/utils/pack2d.py b/masque/utils/pack2d.py index 873f91c..52c0949 100644 --- a/masque/utils/pack2d.py +++ b/masque/utils/pack2d.py @@ -8,7 +8,6 @@ from numpy.typing import NDArray, ArrayLike from ..error import MasqueError from ..pattern import Pattern -from ..ref import Ref def maxrects_bssf( @@ -160,8 +159,8 @@ def pack_patterns( locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects) pat = Pattern() - pat.refs = [Ref(pp, offset=oo + loc) - for pp, oo, loc in zip(patterns, offsets, locations)] + for pp, oo, loc in zip(patterns, offsets, locations): + pat.ref(pp, offset=oo + loc) rejects = [patterns[ii] for ii in reject_inds] return pat, rejects diff --git a/masque/utils/ports2data.py b/masque/utils/ports2data.py index 7963de7..1ef03da 100644 --- a/masque/utils/ports2data.py +++ b/masque/utils/ports2data.py @@ -8,11 +8,11 @@ to write equivalent functions for your own format or alternate storage methods. """ from typing import Sequence, Mapping import logging +from itertools import chain import numpy from ..pattern import Pattern -from ..label import Label from ..utils import layer_t from ..ports import Port from ..error import PatternError @@ -44,9 +44,7 @@ def ports_to_data(pattern: Pattern, layer: layer_t) -> Pattern: angle_deg = numpy.inf else: angle_deg = numpy.rad2deg(port.rotation) - pattern.labels += [ - Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset) - ] + pattern.label(layer=layer, string=f'{name}:{port.ptype} {angle_deg:g}', offset=port.offset) return pattern @@ -97,7 +95,7 @@ def data_to_ports( # Load ports for all subpatterns, and use any we find found_ports = False - for target in set(rr.target for rr in pattern.refs): + for target in pattern.refs: if target is None: continue pp = data_to_ports( @@ -113,17 +111,20 @@ def data_to_ports( if not found_ports: return pattern - for ref in pattern.refs: - if ref.target is None: + for target, refs in pattern.refs.items(): + if target is None: continue - aa = library.abstract(ref.target) - if not aa.ports: + if not refs: continue - aa.apply_ref_transform(ref) + for ref in refs: + aa = library.abstract(target) + if not aa.ports: + break - pattern.check_ports(other_names=aa.ports.keys()) - pattern.ports.update(aa.ports) + aa.apply_ref_transform(ref) + pattern.check_ports(other_names=aa.ports.keys()) + pattern.ports.update(aa.ports) return pattern @@ -149,13 +150,13 @@ def data_to_ports_flat( Returns: The updated `pattern`. Port labels are not removed. """ - labels = [ll for ll in pattern.labels if ll.layer in layers] + labels = list(chain.from_iterable((pattern.labels[layer] for layer in layers))) if not labels: return pattern pstr = cell_name if cell_name is not None else repr(pattern) if pattern.ports: - raise PatternError('Pattern "{pstr}" has pre-existing ports!') + raise PatternError(f'Pattern "{pstr}" has pre-existing ports!') local_ports = {} for label in labels: