move to dicty layers and targets
This commit is contained in:
		
							parent
							
								
									6b240de268
								
							
						
					
					
						commit
						9a077ea2df
					
				@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,22 +326,29 @@ 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:
 | 
			
		||||
    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, :]))
 | 
			
		||||
@ -354,12 +357,16 @@ def _shapes_to_elements(
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
    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)
 | 
			
		||||
            block.add_text(
 | 
			
		||||
                label.string,
 | 
			
		||||
                dxfattribs=attribs
 | 
			
		||||
                ).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _mlayer2dxf(layer: layer_t) -> str:
 | 
			
		||||
 | 
			
		||||
@ -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,26 +329,25 @@ 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
 | 
			
		||||
@ -428,17 +426,18 @@ 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:
 | 
			
		||||
    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)
 | 
			
		||||
@ -485,11 +484,12 @@ def _shapes_to_elements(
 | 
			
		||||
    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:
 | 
			
		||||
    for mlayer, lseq in labels.items():
 | 
			
		||||
        layer, text_type = _mlayer2gds(mlayer)
 | 
			
		||||
        for label in lseq:
 | 
			
		||||
            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),
 | 
			
		||||
 | 
			
		||||
@ -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,17 +500,17 @@ 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)
 | 
			
		||||
@ -527,7 +518,7 @@ def _refs_to_placements(
 | 
			
		||||
            offset = rint_cast(ref.offset + rep_offset)
 | 
			
		||||
            angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
 | 
			
		||||
            placement = fatrec.Placement(
 | 
			
		||||
            name=ref.target,
 | 
			
		||||
                name=target,
 | 
			
		||||
                flip=mirror_across_x,
 | 
			
		||||
                angle=angle,
 | 
			
		||||
                magnification=ref.scale,
 | 
			
		||||
@ -542,13 +533,14 @@ def _refs_to_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)
 | 
			
		||||
    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):
 | 
			
		||||
@ -601,12 +593,13 @@ def _shapes_to_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)
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
@ -65,21 +65,23 @@ 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 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['pattern_layer'] = layer
 | 
			
		||||
 | 
			
		||||
                    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
 | 
			
		||||
            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(ref.target), transform=transform)
 | 
			
		||||
                use = svg.use(href='#' + mangle_name(target), transform=transform)
 | 
			
		||||
                svg_group.add(use)
 | 
			
		||||
 | 
			
		||||
        svg.defs.add(svg_group)
 | 
			
		||||
@ -133,7 +135,8 @@ def writefile_inverted(
 | 
			
		||||
    path_spec = poly2path(slab_edge)
 | 
			
		||||
 | 
			
		||||
    # Draw polygons with reversed vertex order
 | 
			
		||||
    for shape in pattern.shapes:
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -42,8 +42,9 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
 | 
			
		||||
    Returns:
 | 
			
		||||
        pat
 | 
			
		||||
    """
 | 
			
		||||
    for shapes in pat.shapes.values():
 | 
			
		||||
        remove_inds = []
 | 
			
		||||
    for ii, shape in enumerate(pat.shapes):
 | 
			
		||||
        for ii, shape in enumerate(shapes):
 | 
			
		||||
            if not isinstance(shape, (Polygon, Path)):
 | 
			
		||||
                continue
 | 
			
		||||
            try:
 | 
			
		||||
@ -51,7 +52,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
 | 
			
		||||
            except PatternError:
 | 
			
		||||
                remove_inds.append(ii)
 | 
			
		||||
        for ii in sorted(remove_inds, reverse=True):
 | 
			
		||||
        del pat.shapes[ii]
 | 
			
		||||
            del shapes[ii]
 | 
			
		||||
    return pat
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,7 +238,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
 | 
			
		||||
                if target_pat.is_empty():        # avoid some extra allocations
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                p = ref.as_pattern(pattern=flattened[target])
 | 
			
		||||
                for ref in pat.refs[target]:
 | 
			
		||||
                    p = ref.as_pattern(pattern=target_pat)
 | 
			
		||||
                    if not flatten_ports:
 | 
			
		||||
                        p.ports.clear()
 | 
			
		||||
                    pat.append(p)
 | 
			
		||||
@ -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,8 +351,9 @@ 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,
 | 
			
		||||
            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)
 | 
			
		||||
            ```
 | 
			
		||||
@ -398,7 +398,13 @@ 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:
 | 
			
		||||
        for target in pattern.refs:
 | 
			
		||||
            if target is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if target in hierarchy:
 | 
			
		||||
                raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
 | 
			
		||||
 | 
			
		||||
            for ref in pattern.refs[target]:
 | 
			
		||||
                if transform is not False:
 | 
			
		||||
                    sign = numpy.ones(2)
 | 
			
		||||
                    if transform[3]:
 | 
			
		||||
@ -411,16 +417,11 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
 | 
			
		||||
                else:
 | 
			
		||||
                    ref_transform = False
 | 
			
		||||
 | 
			
		||||
            if ref.target is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if ref.target in hierarchy:
 | 
			
		||||
                raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"')
 | 
			
		||||
 | 
			
		||||
                self.dfs(
 | 
			
		||||
                pattern=self[ref.target],
 | 
			
		||||
                    pattern=self[target],
 | 
			
		||||
                    visit_before=visit_before,
 | 
			
		||||
                    visit_after=visit_after,
 | 
			
		||||
                hierarchy=hierarchy + (ref.target,),
 | 
			
		||||
                    hierarchy=hierarchy + (target,),
 | 
			
		||||
                    transform=ref_transform,
 | 
			
		||||
                    memo=memo,
 | 
			
		||||
                    )
 | 
			
		||||
@ -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,9 +646,11 @@ 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):
 | 
			
		||||
            for layer, sseq in pat.shapes.items():
 | 
			
		||||
                for shape in sseq:
 | 
			
		||||
                    if not any(isinstance(shape, t) for t in exclude_types):
 | 
			
		||||
                    label, _values, func = shape.normalized_form(norm_value)
 | 
			
		||||
                        base_label, _values, func = shape.normalized_form(norm_value)
 | 
			
		||||
                        label = (*base_label, layer)
 | 
			
		||||
                        shape_funcs[label] = func
 | 
			
		||||
                        shape_counts[label] += 1
 | 
			
		||||
 | 
			
		||||
@ -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,12 +670,14 @@ 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):
 | 
			
		||||
            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
 | 
			
		||||
@ -682,16 +689,17 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
 | 
			
		||||
            # 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]
 | 
			
		||||
                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()):
 | 
			
		||||
            for layer in pat.shapes:
 | 
			
		||||
                new_shapes = []
 | 
			
		||||
            for shape in pat.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])
 | 
			
		||||
                    self[name] = Pattern(shapes={layer: [shape]})
 | 
			
		||||
                    pat.ref(name, repetition=shape.repetition)
 | 
			
		||||
                    shape.repetition = None
 | 
			
		||||
            pat.shapes = new_shapes
 | 
			
		||||
                pat.shapes[layer] = new_shapes
 | 
			
		||||
 | 
			
		||||
            for layer in pat.labels:
 | 
			
		||||
                new_labels = []
 | 
			
		||||
            for label in pat.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=[label])
 | 
			
		||||
                    self[name] = Pattern(labels={layer: [label]})
 | 
			
		||||
                    pat.ref(name, repetition=label.repetition)
 | 
			
		||||
                    label.repetition = None
 | 
			
		||||
            pat.labels = new_labels
 | 
			
		||||
                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:
 | 
			
		||||
 | 
			
		||||
@ -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: s{len(self.shapes)} r{len(self.refs)} l{len(self.labels)} ['
 | 
			
		||||
        nshapes = sum(len(seq) for seq in self.shapes.values())
 | 
			
		||||
        nrefs = sum(len(seq) for seq in self.refs.values())
 | 
			
		||||
        nlabels = sum(len(seq) for seq in self.labels.values())
 | 
			
		||||
 | 
			
		||||
        s = f'<Pattern: s{nshapes} r{nrefs} l{nlabels} ['
 | 
			
		||||
        for name, port in self.ports.items():
 | 
			
		||||
            s += f'\n\t{name}: {port}'
 | 
			
		||||
        s += ']>'
 | 
			
		||||
        return s
 | 
			
		||||
 | 
			
		||||
    def __copy__(self) -> '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,19 +328,25 @@ 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):
 | 
			
		||||
        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)
 | 
			
		||||
            for target, refs in self.refs.items():
 | 
			
		||||
                if target is None:
 | 
			
		||||
                    continue
 | 
			
		||||
                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, :])
 | 
			
		||||
@ -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,21 +670,23 @@ 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])
 | 
			
		||||
                for ref in refs:
 | 
			
		||||
                    p = ref.as_pattern(pattern=target_pat)
 | 
			
		||||
                    if not flatten_ports:
 | 
			
		||||
                        p.ports.clear()
 | 
			
		||||
                    pat.append(p)
 | 
			
		||||
@ -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,8 +748,15 @@ 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(
 | 
			
		||||
        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,
 | 
			
		||||
@ -699,3 +768,32 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
@ -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'<Ref {name} at {self.offset}{rotation}{scale}{mirrored}>'
 | 
			
		||||
        return f'<Ref {self.offset}{rotation}{scale}{mirrored}>'
 | 
			
		||||
 | 
			
		||||
@ -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'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
 | 
			
		||||
        return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
 | 
			
		||||
 | 
			
		||||
@ -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'<Circle l{self.layer} o{self.offset} r{self.radius:g}>'
 | 
			
		||||
        return f'<Circle o{self.offset} r{self.radius:g}>'
 | 
			
		||||
 | 
			
		||||
@ -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'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}>'
 | 
			
		||||
        rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
 | 
			
		||||
        return f'<Ellipse o{self.offset} r{self.radii}{rotation}>'
 | 
			
		||||
 | 
			
		||||
@ -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'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
 | 
			
		||||
        return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
 | 
			
		||||
 | 
			
		||||
@ -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'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}>'
 | 
			
		||||
        return f'<Polygon centroid {centroid} v{len(self.vertices)}>'
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}>'
 | 
			
		||||
        return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -60,6 +60,8 @@ class Positionable(metaclass=ABCMeta):
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Bounded(metaclass=ABCMeta):
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_bounds(self) -> NDArray[numpy.float64] | None:
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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,15 +111,18 @@ 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 refs:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        for ref in refs:
 | 
			
		||||
            aa = library.abstract(target)
 | 
			
		||||
            if not aa.ports:
 | 
			
		||||
            continue
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            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:
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user