move to dicty layers and targets

master
jan 1 year ago
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 copy
import logging import logging
@ -482,10 +482,10 @@ class Builder(PortList):
self.pattern.append(other_copy) self.pattern.append(other_copy)
else: else:
assert not isinstance(other, Pattern) assert not isinstance(other, Pattern)
ref = Ref(other.name, mirrored=mirrored) ref = Ref(mirrored=mirrored)
ref.rotate_around(pivot, rotation) ref.rotate_around(pivot, rotation)
ref.translate(offset) ref.translate(offset)
self.pattern.refs.append(ref) self.pattern.refs[other.name].append(ref)
return self return self
def translate(self, offset: ArrayLike) -> Self: def translate(self, offset: ArrayLike) -> Self:

@ -279,10 +279,10 @@ class RenderPather(PortList):
p.translate(offset) p.translate(offset)
self.ports[name] = p self.ports[name] = p
sp = Ref(other.name, mirrored=mirrored) ref = Ref(mirrored=mirrored)
sp.rotate_around(pivot, rotation) ref.rotate_around(pivot, rotation)
sp.translate(offset) ref.translate(offset)
self.pattern.refs.append(sp) self.pattern.refs[other.name].append(ref)
return self return self
def path( def path(

@ -1,7 +1,7 @@
""" """
Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides) 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 from abc import ABCMeta, abstractmethod
import numpy import numpy
@ -20,6 +20,7 @@ render_step_t = (
| tuple[Literal['P'], None, float, float, str, None] | tuple[Literal['P'], None, float, float, str, None]
) )
class Tool: class Tool:
def path( def path(
self, self,
@ -153,6 +154,4 @@ class BasicTool(Tool, metaclass=ABCMeta):
if out_transition: if out_transition:
bb.plug(opat, {port_names[1]: oport_ours}) bb.plug(opat, {port_names[1]: oport_ours})
return bb.pattern return bb.pattern

@ -219,7 +219,7 @@ def _read_block(block) -> tuple[str, Pattern]:
if points.shape[1] == 2: if points.shape[1] == 2:
raise PatternError('Invalid or unimplemented polygon?') raise PatternError('Invalid or unimplemented polygon?')
#shape = Polygon(layer=layer) #shape = Polygon()
elif points.shape[1] > 2: elif points.shape[1] > 2:
if (points[0, 2] != points[:, 2]).any(): if (points[0, 2] != points[:, 2]).any():
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)') 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 shape: Path | Polygon
if width == 0 and len(points) > 2 and numpy.array_equal(points[0], points[-1]): 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: 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',): elif eltype in ('TEXT',):
args = dict( args = dict(
@ -248,9 +248,9 @@ def _read_block(block) -> tuple[str, Pattern]:
# if height != 0: # if height != 0:
# logger.warning('Interpreting DXF TEXT as a label despite nonzero height. ' # 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.') # '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: # 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',): elif eltype in ('INSERT',):
attr = element.dxfattribs() attr = element.dxfattribs()
xscale = attr.get('xscale', 1) xscale = attr.get('xscale', 1)
@ -286,13 +286,9 @@ def _read_block(block) -> tuple[str, Pattern]:
def _mrefs_to_drefs( def _mrefs_to_drefs(
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
refs: list[Ref], refs: dict[str | None, list[Ref]],
) -> None: ) -> None:
for ref in refs: def mk_blockref(encoded_name: str, ref: Ref) -> None:
if ref.target is None:
continue
encoded_name = ref.target
rotation = numpy.rad2deg(ref.rotation) % 360 rotation = numpy.rad2deg(ref.rotation) % 360
attribs = dict( attribs = dict(
xscale=ref.scale * (-1 if ref.mirrored[1] else 1), xscale=ref.scale * (-1 if ref.mirrored[1] else 1),
@ -330,36 +326,47 @@ def _mrefs_to_drefs(
for dd in rep.displacements: for dd in rep.displacements:
block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs) 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( def _shapes_to_elements(
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
shapes: list[Shape], shapes: dict[layer_t, list[Shape]],
polygonize_paths: bool = False, polygonize_paths: bool = False,
) -> None: ) -> None:
# Add `LWPolyline`s for each shape. # Add `LWPolyline`s for each shape.
# Could set do paths with width setting, but need to consider endcaps. # Could set do paths with width setting, but need to consider endcaps.
for shape in shapes: for layer, sseq in shapes.items():
if shape.repetition is not None: attribs = dict(layer=_mlayer2dxf(layer))
raise PatternError( for shape in sseq:
'Shape repetitions are not supported by DXF.' if shape.repetition is not None:
' Please call library.wrap_repeated_shapes() before writing to file.' 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():
for polygon in shape.to_polygons(): xy_open = polygon.vertices + polygon.offset
xy_open = polygon.vertices + polygon.offset xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
xy_closed = numpy.vstack((xy_open, xy_open[0, :])) block.add_lwpolyline(xy_closed, dxfattribs=attribs)
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
def _labels_to_texts( def _labels_to_texts(
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
labels: list[Label], labels: dict[layer_t, list[Label]],
) -> None: ) -> None:
for label in labels: for layer, lseq in labels.items():
attribs = dict(layer=_mlayer2dxf(label.layer)) attribs = dict(layer=_mlayer2dxf(layer))
xy = label.offset for label in lseq:
block.add_text(label.string, dxfattribs=attribs).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT) xy = label.offset
block.add_text(
label.string,
dxfattribs=attribs
).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT)
def _mlayer2dxf(layer: layer_t) -> str: def _mlayer2dxf(layer: layer_t) -> str:

@ -253,21 +253,21 @@ def read_elements(
elements = klamath.library.read_elements(stream) elements = klamath.library.read_elements(stream)
for element in elements: for element in elements:
if isinstance(element, klamath.elements.Boundary): if isinstance(element, klamath.elements.Boundary):
poly = _boundary_to_polygon(element, raw_mode) layer, poly = _boundary_to_polygon(element, raw_mode)
pat.shapes.append(poly) pat.shapes[layer].append(poly)
elif isinstance(element, klamath.elements.Path): elif isinstance(element, klamath.elements.Path):
path = _gpath_to_mpath(element, raw_mode) layer, path = _gpath_to_mpath(element, raw_mode)
pat.shapes.append(path) pat.shapes[layer].append(path)
elif isinstance(element, klamath.elements.Text): elif isinstance(element, klamath.elements.Text):
label = Label( pat.label(
offset=element.xy.astype(float),
layer=element.layer, layer=element.layer,
offset=element.xy.astype(float),
string=element.string.decode('ASCII'), string=element.string.decode('ASCII'),
annotations=_properties_to_annotations(element.properties), annotations=_properties_to_annotations(element.properties),
) )
pat.labels.append(label)
elif isinstance(element, klamath.elements.Reference): 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 return pat
@ -287,7 +287,7 @@ def _mlayer2gds(mlayer: layer_t) -> tuple[int, int]:
return layer, data_type 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. 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, repetition = Grid(a_vector=a_vector, b_vector=b_vector,
a_count=a_count, b_count=b_count) a_count=a_count, b_count=b_count)
target = ref.struct_name.decode('ASCII')
mref = Ref( mref = Ref(
target=ref.struct_name.decode('ASCII'),
offset=offset, offset=offset,
rotation=numpy.deg2rad(ref.angle_deg), rotation=numpy.deg2rad(ref.angle_deg),
scale=ref.mag, scale=ref.mag,
@ -310,10 +310,10 @@ def _gref_to_mref(ref: klamath.library.Reference) -> Ref:
annotations=_properties_to_annotations(ref.properties), annotations=_properties_to_annotations(ref.properties),
repetition=repetition, 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: if gpath.path_type in path_cap_map:
cap = path_cap_map[gpath.path_type] cap = path_cap_map[gpath.path_type]
else: else:
@ -321,7 +321,6 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
mpath = Path( mpath = Path(
vertices=gpath.xy.astype(float), vertices=gpath.xy.astype(float),
layer=gpath.layer,
width=gpath.width, width=gpath.width,
cap=cap, cap=cap,
offset=numpy.zeros(2), offset=numpy.zeros(2),
@ -330,74 +329,73 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
) )
if cap == Path.Cap.SquareCustom: if cap == Path.Cap.SquareCustom:
mpath.cap_extensions = gpath.extension mpath.cap_extensions = gpath.extension
return mpath return gpath.layer, mpath
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon: def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> tuple[layer_t, Polygon]:
return Polygon( return boundary.layer, Polygon(
vertices=boundary.xy[:-1].astype(float), vertices=boundary.xy[:-1].astype(float),
layer=boundary.layer,
offset=numpy.zeros(2), offset=numpy.zeros(2),
annotations=_properties_to_annotations(boundary.properties), annotations=_properties_to_annotations(boundary.properties),
raw=raw_mode, 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 = [] grefs = []
for ref in refs: for target, rseq in refs.items():
if ref.target is None: if target is None:
continue continue
encoded_name = ref.target.encode('ASCII') encoded_name = target.encode('ASCII')
for ref in rseq:
# Note: GDS mirrors first and rotates second
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
rep = ref.repetition
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360
properties = _annotations_to_properties(ref.annotations, 512)
# Note: GDS mirrors first and rotates second if isinstance(rep, Grid):
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored) b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
rep = ref.repetition b_count = rep.b_count if rep.b_count is not None else 1
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360 xy = numpy.array(ref.offset) + numpy.array([
properties = _annotations_to_properties(ref.annotations, 512) [0.0, 0.0],
rep.a_vector * rep.a_count,
if isinstance(rep, Grid): b_vector * b_count,
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) ])
b_count = rep.b_count if rep.b_count is not None else 1 aref = klamath.library.Reference(
xy = numpy.array(ref.offset) + numpy.array([
[0.0, 0.0],
rep.a_vector * rep.a_count,
b_vector * b_count,
])
aref = klamath.library.Reference(
struct_name=encoded_name,
xy=rint_cast(xy),
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
angle_deg=angle_deg,
invert_y=mirror_across_x,
mag=ref.scale,
properties=properties,
)
grefs.append(aref)
elif rep is None:
sref = klamath.library.Reference(
struct_name=encoded_name,
xy=rint_cast([ref.offset]),
colrow=None,
angle_deg=angle_deg,
invert_y=mirror_across_x,
mag=ref.scale,
properties=properties,
)
grefs.append(sref)
else:
new_srefs = [
klamath.library.Reference(
struct_name=encoded_name, struct_name=encoded_name,
xy=rint_cast([ref.offset + dd]), xy=rint_cast(xy),
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
angle_deg=angle_deg,
invert_y=mirror_across_x,
mag=ref.scale,
properties=properties,
)
grefs.append(aref)
elif rep is None:
sref = klamath.library.Reference(
struct_name=encoded_name,
xy=rint_cast([ref.offset]),
colrow=None, colrow=None,
angle_deg=angle_deg, angle_deg=angle_deg,
invert_y=mirror_across_x, invert_y=mirror_across_x,
mag=ref.scale, mag=ref.scale,
properties=properties, properties=properties,
) )
for dd in rep.displacements] grefs.append(sref)
grefs += new_srefs else:
new_srefs = [
klamath.library.Reference(
struct_name=encoded_name,
xy=rint_cast([ref.offset + dd]),
colrow=None,
angle_deg=angle_deg,
invert_y=mirror_across_x,
mag=ref.scale,
properties=properties,
)
for dd in rep.displacements]
grefs += new_srefs
return grefs return grefs
@ -428,51 +426,41 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
def _shapes_to_elements( def _shapes_to_elements(
shapes: list[Shape], shapes: dict[layer_t, list[Shape]],
polygonize_paths: bool = False, polygonize_paths: bool = False,
) -> list[klamath.elements.Element]: ) -> list[klamath.elements.Element]:
elements: list[klamath.elements.Element] = [] elements: list[klamath.elements.Element] = []
# Add a Boundary element for each shape, and Path elements if necessary # Add a Boundary element for each shape, and Path elements if necessary
for shape in shapes: for mlayer, sseq in shapes.items():
if shape.repetition is not None: layer, data_type = _mlayer2gds(mlayer)
raise PatternError('Shape repetitions are not supported by GDS.' for shape in sseq:
' Please call library.wrap_repeated_shapes() before writing to file.') 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)
properties = _annotations_to_properties(shape.annotations, 128) if isinstance(shape, Path) and not polygonize_paths:
if isinstance(shape, Path) and not polygonize_paths: xy = rint_cast(shape.vertices + shape.offset)
xy = rint_cast(shape.vertices + shape.offset) width = rint_cast(shape.width)
width = rint_cast(shape.width) path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
extension: tuple[int, int] extension: tuple[int, int]
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None: if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None:
extension = tuple(shape.cap_extensions) # type: ignore extension = tuple(shape.cap_extensions) # type: ignore
else: else:
extension = (0, 0) extension = (0, 0)
path = klamath.elements.Path( path = klamath.elements.Path(
layer=(layer, data_type), layer=(layer, data_type),
xy=xy, xy=xy,
path_type=path_type, path_type=path_type,
width=int(width), width=int(width),
extension=extension, extension=extension,
properties=properties, properties=properties,
) )
elements.append(path) elements.append(path)
elif isinstance(shape, Polygon): elif isinstance(shape, Polygon):
polygon = shape polygon = shape
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
xy_closed[-1] = xy_closed[0]
boundary = klamath.elements.Boundary(
layer=(layer, data_type),
xy=xy_closed,
properties=properties,
)
elements.append(boundary)
else:
for polygon in shape.to_polygons():
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32) xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe') numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
xy_closed[-1] = xy_closed[0] xy_closed[-1] = xy_closed[0]
@ -482,28 +470,40 @@ def _shapes_to_elements(
properties=properties, properties=properties,
) )
elements.append(boundary) elements.append(boundary)
else:
for polygon in shape.to_polygons():
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
xy_closed[-1] = xy_closed[0]
boundary = klamath.elements.Boundary(
layer=(layer, data_type),
xy=xy_closed,
properties=properties,
)
elements.append(boundary)
return elements 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 = [] texts = []
for label in labels: for mlayer, lseq in labels.items():
properties = _annotations_to_properties(label.annotations, 128) layer, text_type = _mlayer2gds(mlayer)
layer, text_type = _mlayer2gds(label.layer) for label in lseq:
xy = rint_cast([label.offset]) properties = _annotations_to_properties(label.annotations, 128)
text = klamath.elements.Text( xy = rint_cast([label.offset])
layer=(layer, text_type), text = klamath.elements.Text(
xy=xy, layer=(layer, text_type),
string=label.string.encode('ASCII'), xy=xy,
properties=properties, string=label.string.encode('ASCII'),
presentation=0, # TODO maybe set some of these? properties=properties,
angle_deg=0, presentation=0, # TODO maybe set some of these?
invert_y=False, angle_deg=0,
width=0, invert_y=False,
path_type=0, width=0,
mag=1, path_type=0,
) mag=1,
texts.append(text) )
texts.append(text)
return texts return texts

@ -30,7 +30,7 @@ from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringR
from .utils import is_gzipped, tmpfile from .utils import is_gzipped, tmpfile
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
from ..library import Library, ILibrary from ..library import Library, ILibrary
from ..shapes import Polygon, Path, Circle from ..shapes import Path, Circle
from ..repetition import Grid, Arbitrary, Repetition from ..repetition import Grid, Arbitrary, Repetition
from ..utils import layer_t, normalize_mirror, annotations_t 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) vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list()[:-1])), axis=0)
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
poly = Polygon( pat.polygon(
vertices=vertices, vertices=vertices,
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
annotations=annotations, annotations=annotations,
repetition=repetition, repetition=repetition,
) )
pat.shapes.append(poly)
elif isinstance(element, fatrec.Path): elif isinstance(element, fatrec.Path):
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0) 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) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
path = Path( pat.path(
vertices=vertices, vertices=vertices,
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
@ -322,20 +319,17 @@ def read(
**path_args, **path_args,
) )
pat.shapes.append(path)
elif isinstance(element, fatrec.Rectangle): elif isinstance(element, fatrec.Rectangle):
width = element.get_width() width = element.get_width()
height = element.get_height() height = element.get_height()
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
rect = Polygon( pat.polygon(
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
annotations=annotations, annotations=annotations,
) )
pat.shapes.append(rect)
elif isinstance(element, fatrec.Trapezoid): elif isinstance(element, fatrec.Trapezoid):
vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (element.get_width(), element.get_height()) 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 vertices[2, 0] -= b
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
trapz = Polygon( pat.polygon(
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
vertices=vertices, vertices=vertices,
annotations=annotations, annotations=annotations,
) )
pat.shapes.append(trapz)
elif isinstance(element, fatrec.CTrapezoid): elif isinstance(element, fatrec.CTrapezoid):
cttype = element.get_ctrapezoid_type() cttype = element.get_ctrapezoid_type()
@ -419,25 +412,24 @@ def read(
vertices[0, 1] += width vertices[0, 1] += width
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
ctrapz = Polygon( pat.polygon(
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
vertices=vertices, vertices=vertices,
annotations=annotations, annotations=annotations,
) )
pat.shapes.append(ctrapz)
elif isinstance(element, fatrec.Circle): elif isinstance(element, fatrec.Circle):
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
layer = element.get_layer_tuple()
circle = Circle( circle = Circle(
layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
annotations=annotations, annotations=annotations,
radius=float(element.get_radius()), radius=float(element.get_radius()),
) )
pat.shapes.append(circle) pat.shapes[layer].append(circle)
elif isinstance(element, fatrec.Text): elif isinstance(element, fatrec.Text):
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
@ -446,21 +438,21 @@ def read(
string = lib.textstrings[str_or_ref].string string = lib.textstrings[str_or_ref].string
else: else:
string = str_or_ref.string string = str_or_ref.string
label = Label( pat.label(
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
annotations=annotations, annotations=annotations,
string=string, string=string,
) )
pat.labels.append(label)
else: else:
logger.warning(f'Skipping record {element} (unimplemented)') logger.warning(f'Skipping record {element} (unimplemented)')
continue continue
for placement in cell.placements: 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 mlib[cell_name] = pat
@ -484,9 +476,9 @@ def _mlayer2oas(mlayer: layer_t) -> tuple[int, int]:
return layer, data_type 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) assert not isinstance(placement.repetition, fatamorgana.ReuseRepetition)
xy = numpy.array((placement.x, placement.y)) xy = numpy.array((placement.x, placement.y))
@ -501,7 +493,6 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout)
else: else:
rotation = numpy.deg2rad(float(placement.angle)) rotation = numpy.deg2rad(float(placement.angle))
ref = Ref( ref = Ref(
target=name,
offset=xy, offset=xy,
mirrored=(placement.flip, False), mirrored=(placement.flip, False),
rotation=rotation, rotation=rotation,
@ -509,116 +500,118 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout)
repetition=repetition_fata2masq(placement.repetition), repetition=repetition_fata2masq(placement.repetition),
annotations=annotations, annotations=annotations,
) )
return ref return name, ref
def _refs_to_placements( def _refs_to_placements(
refs: list[Ref], refs: dict[str | None, list[Ref]],
) -> list[fatrec.Placement]: ) -> list[fatrec.Placement]:
placements = [] placements = []
for ref in refs: for target, rseq in refs.items():
if ref.target is None: if target is None:
continue continue
for ref in rseq:
# Note: OASIS mirrors first and rotates second
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
frep, rep_offset = repetition_masq2fata(ref.repetition)
# Note: OASIS mirrors first and rotates second offset = rint_cast(ref.offset + rep_offset)
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored) angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
frep, rep_offset = repetition_masq2fata(ref.repetition) placement = fatrec.Placement(
name=target,
flip=mirror_across_x,
angle=angle,
magnification=ref.scale,
properties=annotations_to_properties(ref.annotations),
x=offset[0],
y=offset[1],
repetition=frep,
)
offset = rint_cast(ref.offset + rep_offset) placements.append(placement)
angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
placement = fatrec.Placement(
name=ref.target,
flip=mirror_across_x,
angle=angle,
magnification=ref.scale,
properties=annotations_to_properties(ref.annotations),
x=offset[0],
y=offset[1],
repetition=frep,
)
placements.append(placement)
return placements return placements
def _shapes_to_elements( def _shapes_to_elements(
shapes: list[Shape], shapes: dict[layer_t, list[Shape]],
layer2oas: Callable[[layer_t], tuple[int, int]], layer2oas: Callable[[layer_t], tuple[int, int]],
) -> list[fatrec.Polygon | fatrec.Path | fatrec.Circle]: ) -> list[fatrec.Polygon | fatrec.Path | fatrec.Circle]:
# Add a Polygon record for each shape, and Path elements if necessary # Add a Polygon record for each shape, and Path elements if necessary
elements: list[fatrec.Polygon | fatrec.Path | fatrec.Circle] = [] elements: list[fatrec.Polygon | fatrec.Path | fatrec.Circle] = []
for shape in shapes: for mlayer, sseq in shapes.items():
layer, datatype = layer2oas(shape.layer) layer, datatype = layer2oas(mlayer)
repetition, rep_offset = repetition_masq2fata(shape.repetition) for shape in sseq:
properties = annotations_to_properties(shape.annotations) repetition, rep_offset = repetition_masq2fata(shape.repetition)
if isinstance(shape, Circle): properties = annotations_to_properties(shape.annotations)
offset = rint_cast(shape.offset + rep_offset) if isinstance(shape, Circle):
radius = rint_cast(shape.radius) offset = rint_cast(shape.offset + rep_offset)
circle = fatrec.Circle( radius = rint_cast(shape.radius)
layer=layer, circle = fatrec.Circle(
datatype=datatype,
radius=cast(int, radius),
x=offset[0],
y=offset[1],
properties=properties,
repetition=repetition,
)
elements.append(circle)
elif isinstance(shape, Path):
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
half_width = rint_cast(shape.width / 2)
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
path = fatrec.Path(
layer=layer,
datatype=datatype,
point_list=cast(Sequence[Sequence[int]], deltas),
half_width=cast(int, half_width),
x=xy[0],
y=xy[1],
extension_start=extension_start, # TODO implement multiple cap types?
extension_end=extension_end,
properties=properties,
repetition=repetition,
)
elements.append(path)
else:
for polygon in shape.to_polygons():
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
elements.append(fatrec.Polygon(
layer=layer, layer=layer,
datatype=datatype, datatype=datatype,
x=xy[0], radius=cast(int, radius),
y=xy[1], x=offset[0],
point_list=cast(list[list[int]], points), y=offset[1],
properties=properties, properties=properties,
repetition=repetition, repetition=repetition,
)) )
elements.append(circle)
elif isinstance(shape, Path):
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
half_width = rint_cast(shape.width / 2)
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
path = fatrec.Path(
layer=layer,
datatype=datatype,
point_list=cast(Sequence[Sequence[int]], deltas),
half_width=cast(int, half_width),
x=xy[0],
y=xy[1],
extension_start=extension_start, # TODO implement multiple cap types?
extension_end=extension_end,
properties=properties,
repetition=repetition,
)
elements.append(path)
else:
for polygon in shape.to_polygons():
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
elements.append(fatrec.Polygon(
layer=layer,
datatype=datatype,
x=xy[0],
y=xy[1],
point_list=cast(list[list[int]], points),
properties=properties,
repetition=repetition,
))
return elements return elements
def _labels_to_texts( def _labels_to_texts(
labels: list[Label], labels: dict[layer_t, list[Label]],
layer2oas: Callable[[layer_t], tuple[int, int]], layer2oas: Callable[[layer_t], tuple[int, int]],
) -> list[fatrec.Text]: ) -> list[fatrec.Text]:
texts = [] texts = []
for label in labels: for mlayer, lseq in labels.items():
layer, datatype = layer2oas(label.layer) layer, datatype = layer2oas(mlayer)
repetition, rep_offset = repetition_masq2fata(label.repetition) for label in lseq:
xy = rint_cast(label.offset + rep_offset) repetition, rep_offset = repetition_masq2fata(label.repetition)
properties = annotations_to_properties(label.annotations) xy = rint_cast(label.offset + rep_offset)
texts.append(fatrec.Text( properties = annotations_to_properties(label.annotations)
layer=layer, texts.append(fatrec.Text(
datatype=datatype, layer=layer,
x=xy[0], datatype=datatype,
y=xy[1], x=xy[0],
string=label.string, y=xy[1],
properties=properties, string=label.string,
repetition=repetition, properties=properties,
)) repetition=repetition,
))
return texts return texts

@ -65,22 +65,24 @@ def writefile(
for name, pat in library.items(): for name, pat in library.items():
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red') 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 polygon in shape.to_polygons(): for shape in shapes:
path_spec = poly2path(polygon.vertices + polygon.offset) for polygon in shape.to_polygons():
path_spec = poly2path(polygon.vertices + polygon.offset)
path = svg.path(d=path_spec) path = svg.path(d=path_spec)
if custom_attributes: if custom_attributes:
path['pattern_layer'] = polygon.layer path['pattern_layer'] = layer
svg_group.add(path) svg_group.add(path)
for ref in pat.refs: for target, refs in pat.refs.items():
if ref.target is None: if target is None:
continue continue
transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})' for ref in refs:
use = svg.use(href='#' + mangle_name(ref.target), transform=transform) transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})'
svg_group.add(use) use = svg.use(href='#' + mangle_name(target), transform=transform)
svg_group.add(use)
svg.defs.add(svg_group) svg.defs.add(svg_group)
svg.add(svg.use(href='#' + mangle_name(top))) svg.add(svg.use(href='#' + mangle_name(top)))
@ -133,9 +135,10 @@ def writefile_inverted(
path_spec = poly2path(slab_edge) path_spec = poly2path(slab_edge)
# Draw polygons with reversed vertex order # Draw polygons with reversed vertex order
for shape in pattern.shapes: for _layer, shapes in pattern.shapes.items():
for polygon in shape.to_polygons(): for shape in shapes:
path_spec += poly2path(polygon.vertices[::-1] + polygon.offset) for polygon in shape.to_polygons():
path_spec += poly2path(polygon.vertices[::-1] + polygon.offset)
svg.add(svg.path(d=path_spec, fill='blue', stroke='red')) svg.add(svg.path(d=path_spec, fill='blue', stroke='red'))
svg.save() svg.save()

@ -42,16 +42,17 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
Returns: Returns:
pat pat
""" """
remove_inds = [] for shapes in pat.shapes.values():
for ii, shape in enumerate(pat.shapes): remove_inds = []
if not isinstance(shape, (Polygon, Path)): for ii, shape in enumerate(shapes):
continue if not isinstance(shape, (Polygon, Path)):
try: continue
shape.clean_vertices() try:
except PatternError: shape.clean_vertices()
remove_inds.append(ii) except PatternError:
for ii in sorted(remove_inds, reverse=True): remove_inds.append(ii)
del pat.shapes[ii] for ii in sorted(remove_inds, reverse=True):
del shapes[ii]
return pat return pat

@ -5,15 +5,15 @@ import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
from .repetition import Repetition from .repetition import Repetition
from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t from .utils import rotation_matrix_2d, AutoSlots, annotations_t
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, RepeatableImpl from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded
from .traits import AnnotatableImpl from .traits import AnnotatableImpl
class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl, class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl,
Pivotable, Copyable, metaclass=AutoSlots): 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', ) __slots__ = ( '_string', )
@ -40,13 +40,11 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
string: str, string: str,
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
) -> None: ) -> None:
self.string = string self.string = string
self.offset = numpy.array(offset, dtype=float, copy=True) self.offset = numpy.array(offset, dtype=float, copy=True)
self.layer = layer
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
@ -54,7 +52,6 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
return type(self)( return type(self)(
string=self.string, string=self.string,
offset=self.offset.copy(), offset=self.offset.copy(),
layer=self.layer,
repetition=self.repetition, repetition=self.repetition,
) )

@ -226,11 +226,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
flattened[name] = None flattened[name] = None
pat = self[name].deepcopy() pat = self[name].deepcopy()
for ref in pat.refs: for target in pat.refs:
target = ref.target
if target is None: if target is None:
continue continue
if target not in flattened: if target not in flattened:
flatten_single(target) flatten_single(target)
@ -240,10 +238,11 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
if target_pat.is_empty(): # avoid some extra allocations if target_pat.is_empty(): # avoid some extra allocations
continue continue
p = ref.as_pattern(pattern=flattened[target]) for ref in pat.refs[target]:
if not flatten_ports: p = ref.as_pattern(pattern=target_pat)
p.ports.clear() if not flatten_ports:
pat.append(p) p.ports.clear()
pat.append(p)
pat.refs.clear() pat.refs.clear()
flattened[name] = pat flattened[name] = pat
@ -316,7 +315,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
names = set(self.keys()) names = set(self.keys())
not_toplevel: set[str | None] = set() not_toplevel: set[str | None] = set()
for name in names: 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) toplevel = list(names - not_toplevel)
return toplevel return toplevel
@ -352,9 +351,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
At each pattern in the tree, the following sequence is called: At each pattern in the tree, the following sequence is called:
``` ```
current_pattern = visit_before(current_pattern, **vist_args) current_pattern = visit_before(current_pattern, **vist_args)
for sp in current_pattern.refs] for target in current_pattern.refs:
self.dfs(sp.target, visit_before, visit_after, for ref in pattern.refs[target]:
hierarchy + (sp.target,), updated_transform, memo) self.dfs(target, visit_before, visit_after,
hierarchy + (sp.target,), updated_transform, memo)
current_pattern = visit_after(current_pattern, **visit_args) current_pattern = visit_after(current_pattern, **visit_args)
``` ```
where `visit_args` are where `visit_args` are
@ -398,32 +398,33 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
if visit_before is not None: if visit_before is not None:
pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform) pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
for ref in pattern.refs: for target in pattern.refs:
if transform is not False: if target is None:
sign = numpy.ones(2)
if transform[3]:
sign[1] = -1
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
mirror_x, angle = normalize_mirror(ref.mirrored)
angle += ref.rotation
ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
ref_transform[3] %= 2
else:
ref_transform = False
if ref.target is None:
continue continue
if ref.target in hierarchy: if target in hierarchy:
raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"') raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
self.dfs( for ref in pattern.refs[target]:
pattern=self[ref.target], if transform is not False:
visit_before=visit_before, sign = numpy.ones(2)
visit_after=visit_after, if transform[3]:
hierarchy=hierarchy + (ref.target,), sign[1] = -1
transform=ref_transform, xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
memo=memo, mirror_x, angle = normalize_mirror(ref.mirrored)
) angle += ref.rotation
ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
ref_transform[3] %= 2
else:
ref_transform = False
self.dfs(
pattern=self[target],
visit_before=visit_before,
visit_after=visit_after,
hierarchy=hierarchy + (target,),
transform=ref_transform,
memo=memo,
)
if visit_after is not None: if visit_after is not None:
pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform) pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
@ -508,9 +509,9 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
self self
""" """
for pattern in self.values(): for pattern in self.values():
for ref in pattern.refs: if old_target in pattern.refs:
if ref.target == old_target: pattern.refs[new_target].extend(pattern.refs[old_target])
ref.target = new_target del pattern.refs[old_target]
return self return self
def mkpat(self, name: str) -> tuple[str, 'Pattern']: def mkpat(self, name: str) -> tuple[str, 'Pattern']:
@ -549,6 +550,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
Returns: Returns:
self self
""" """
from .pattern import map_targets
duplicates = set(self.keys()) & set(other.keys()) duplicates = set(self.keys()) & set(other.keys())
if not duplicates: if not duplicates:
@ -572,8 +574,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
# Update references in the newly-added cells # Update references in the newly-added cells
for old_name in temp: for old_name in temp:
new_name = rename_map.get(old_name, old_name) new_name = rename_map.get(old_name, old_name)
for ref in self[new_name].refs: pat = self[new_name]
ref.target = rename_map.get(cast(str, ref.target), ref.target) pat.refs = map_targets(pat.refs, rename_map)
return rename_map return rename_map
@ -644,11 +646,13 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
# Using the label tuple from `.normalized_form()` as a key, check how many of each shape # 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 # are present and store the shape function for each one
for pat in tuple(self.values()): for pat in tuple(self.values()):
for i, shape in enumerate(pat.shapes): for layer, sseq in pat.shapes.items():
if not any(isinstance(shape, t) for t in exclude_types): for shape in sseq:
label, _values, func = shape.normalized_form(norm_value) if not any(isinstance(shape, t) for t in exclude_types):
shape_funcs[label] = func base_label, _values, func = shape.normalized_form(norm_value)
shape_counts[label] += 1 label = (*base_label, layer)
shape_funcs[label] = func
shape_counts[label] += 1
shape_pats = {} shape_pats = {}
for label, count in shape_counts.items(): for label, count in shape_counts.items():
@ -656,7 +660,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
continue continue
shape_func = shape_funcs[label] 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 shape_pats[label] = shape_pat
# ## Second pass ## # ## Second pass ##
@ -665,33 +670,36 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
# are to be replaced. # are to be replaced.
# The `values` are `(offset, scale, rotation)`. # The `values` are `(offset, scale, rotation)`.
shape_table: MutableMapping[tuple, list] = defaultdict(list) shape_table: dict[tuple, list] = defaultdict(list)
for i, shape in enumerate(pat.shapes): for layer, sseq in pat.shapes.items():
if any(isinstance(shape, t) for t in exclude_types): for i, shape in enumerate(sseq):
continue 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: if label not in shape_pats:
continue continue
shape_table[label].append((i, values)) shape_table[label].append((i, values))
# For repeated shapes, create a `Pattern` holding a normalized shape object, # For repeated shapes, create a `Pattern` holding a normalized shape object,
# and add `pat.refs` entries for each occurrence in pat. Also, note down that # and add `pat.refs` entries for each occurrence in pat. Also, note down that
# we should delete the `pat.shapes` entries for which we made `Ref`s. # we should delete the `pat.shapes` entries for which we made `Ref`s.
shapes_to_remove = [] shapes_to_remove = []
for label in shape_table: for label in shape_table:
layer = label[-1]
target = label2name(label) target = label2name(label)
for i, values in shape_table[label]: for ii, values in shape_table[label]:
offset, scale, rotation, mirror_x = values offset, scale, rotation, mirror_x = values
pat.ref(target=target, offset=offset, scale=scale, pat.ref(target=target, offset=offset, scale=scale,
rotation=rotation, mirrored=(mirror_x, False)) 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. # Remove any shapes for which we have created refs.
for i in sorted(shapes_to_remove, reverse=True): for ii in sorted(shapes_to_remove, reverse=True):
del pat.shapes[i] del pat.shapes[layer][ii]
for ll, pp in shape_pats.items(): for ll, pp in shape_pats.items():
self[label2name(ll)] = pp 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') #name_func = lambda _pat, _shape: self.get_name('_rep')
for pat in tuple(self.values()): for pat in tuple(self.values()):
new_shapes = [] for layer in pat.shapes:
for shape in pat.shapes: new_shapes = []
if shape.repetition is None: for shape in pat.shapes[layer]:
new_shapes.append(shape) if shape.repetition is None:
continue new_shapes.append(shape)
continue
name = name_func(pat, shape) name = name_func(pat, shape)
self[name] = Pattern(shapes=[shape]) self[name] = Pattern(shapes={layer: [shape]})
pat.ref(name, repetition=shape.repetition) pat.ref(name, repetition=shape.repetition)
shape.repetition = None shape.repetition = None
pat.shapes = new_shapes pat.shapes[layer] = new_shapes
new_labels = [] for layer in pat.labels:
for label in pat.labels: new_labels = []
if label.repetition is None: for label in pat.labels[layer]:
new_labels.append(label) if label.repetition is None:
continue new_labels.append(label)
name = name_func(pat, label) continue
self[name] = Pattern(labels=[label]) name = name_func(pat, label)
pat.ref(name, repetition=label.repetition) self[name] = Pattern(labels={layer: [label]})
label.repetition = None pat.ref(name, repetition=label.repetition)
pat.labels = new_labels label.repetition = None
pat.labels[layer] = new_labels
return self 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()): while empty := set(name for name, pat in self.items() if pat.is_empty()):
for name in empty: for name in empty:
del self[name] del self[name]
for pat in self.values(): 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 trimmed |= empty
if not repeat: if not repeat:
@ -799,7 +813,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
del self[key] del self[key]
if delete_refs: if delete_refs:
for pat in self.values(): 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 return self
@ -1007,9 +1022,9 @@ class LazyLibrary(ILibrary):
""" """
self.precache() self.precache()
for pattern in self.cache.values(): for pattern in self.cache.values():
for ref in pattern.refs: if old_target in pattern.refs:
if ref.target == old_target: pattern.refs[new_target].extend(pattern.refs[old_target])
ref.target = new_target del pattern.refs[old_target]
return self return self
def precache(self) -> Self: def precache(self) -> Self:

@ -1,10 +1,11 @@
""" """
Base object representing a lithography mask. Base object representing a lithography mask.
""" """
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar
from typing import Callable, Sequence, cast, Mapping, Self, Any
import copy import copy
import logging
from itertools import chain from itertools import chain
from collections import defaultdict
import numpy import numpy
from numpy import inf from numpy import inf
@ -12,14 +13,17 @@ from numpy.typing import NDArray, ArrayLike
# .visualize imports matplotlib and matplotlib.collections # .visualize imports matplotlib and matplotlib.collections
from .ref import Ref 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 .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 .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 from .ports import Port, PortList
logger = logging.getLogger(__name__)
class Pattern(PortList, AnnotatableImpl, Mirrorable): class Pattern(PortList, AnnotatableImpl, Mirrorable):
""" """
2D layout consisting of some set of shapes, labels, and references to other Pattern objects 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', '_offset', '_annotations',
) )
shapes: list[Shape] shapes: defaultdict[layer_t, list[Shape]]
""" List of all shapes in this Pattern. """ Stores of all shapes in this Pattern, indexed by layer.
Elements in this list are assumed to inherit from Shape or provide equivalent functions. 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. """ """ 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`. """ List of all references to other patterns (`Ref`s) in this `Pattern`.
Multiple objects in this list may reference the same Pattern object Multiple objects in this list may reference the same Pattern object
(i.e. multiple instances of the same object). (i.e. multiple instances of the same object).
@ -59,9 +63,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def __init__( def __init__(
self, self,
*, *,
shapes: Sequence[Shape] = (), shapes: Mapping[layer_t, Sequence[Shape]] | None = None,
labels: Sequence[Label] = (), labels: Mapping[layer_t, Sequence[Label]] | None = None,
refs: Sequence[Ref] = (), refs: Mapping[str | None, Sequence[Ref]] | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
ports: Mapping[str, 'Port'] | None = None ports: Mapping[str, 'Port'] | None = None
) -> None: ) -> None:
@ -76,20 +80,18 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
annotations: Initial annotations for the pattern annotations: Initial annotations for the pattern
ports: Any ports in the pattern ports: Any ports in the pattern
""" """
if isinstance(shapes, list): self.shapes = defaultdict(list)
self.shapes = shapes self.labels = defaultdict(list)
else: self.refs = defaultdict(list)
self.shapes = list(shapes) if shapes:
for layer, sseq in shapes.items():
if isinstance(labels, list): self.shapes[layer].extend(sseq)
self.labels = labels if labels:
else: for layer, lseq in labels.items():
self.labels = list(labels) self.labels[layer].extend(lseq)
if refs:
if isinstance(refs, list): for target, rseq in refs.items():
self.refs = refs self.refs[target].extend(rseq)
else:
self.refs = list(refs)
if ports is not None: if ports is not None:
self.ports = dict(copy.deepcopy(ports)) self.ports = dict(copy.deepcopy(ports))
@ -99,32 +101,42 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
def __repr__(self) -> str: 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(): for name, port in self.ports.items():
s += f'\n\t{name}: {port}' s += f'\n\t{name}: {port}'
s += ']>' s += ']>'
return s return s
def __copy__(self) -> 'Pattern': def __copy__(self) -> 'Pattern':
return Pattern( logger.warning('Making a shallow copy of a Pattern... old shapes are re-referenced!')
shapes=copy.deepcopy(self.shapes), new = Pattern(
labels=copy.deepcopy(self.labels),
refs=[copy.copy(sp) for sp in self.refs],
annotations=copy.deepcopy(self.annotations), annotations=copy.deepcopy(self.annotations),
ports=copy.deepcopy(self.ports), 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 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: def append(self, other_pattern: 'Pattern') -> Self:
""" """
Appends all shapes, labels and refs from other_pattern to self's shapes, Appends all shapes, labels and refs from other_pattern to self's shapes,
@ -136,9 +148,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self self
""" """
self.refs += other_pattern.refs for target, rseq in other_pattern.refs.items():
self.shapes += other_pattern.shapes self.refs[target].extend(rseq)
self.labels += other_pattern.labels 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()) annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys())
if annotation_conflicts: if annotation_conflicts:
@ -154,9 +169,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def subset( def subset(
self, self,
shapes: Callable[[Shape], bool] | None = None, shapes: Callable[[layer_t, Shape], bool] | None = None,
labels: Callable[[Label], bool] | None = None, labels: Callable[[layer_t, Label], bool] | None = None,
refs: Callable[[Ref], bool] | None = None, refs: Callable[[str | None, Ref], bool] | None = None,
annotations: Callable[[str, list[int | float | str]], bool] | None = None, annotations: Callable[[str, list[int | float | str]], bool] | None = None,
ports: Callable[[str, Port], bool] | None = None, ports: Callable[[str, Port], bool] | None = None,
default_keep: bool = False 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. Self is _not_ altered, but shapes, labels, and refs are _not_ copied, just referenced.
Args: Args:
shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset. shapes: Given a layer and shape, returns a boolean denoting whether the shape is a
labels: Given a label, returns a boolean denoting whether the label is a member of the subset. member of the subset.
refs: Given a ref, returns a boolean denoting if it 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. 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. 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. 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() pat = Pattern()
if shapes is not None: 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: elif default_keep:
pat.shapes = copy.copy(self.shapes) pat.shapes = copy.copy(self.shapes)
if labels is not None: 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: elif default_keep:
pat.labels = copy.copy(self.labels) pat.labels = copy.copy(self.labels)
if refs is not None: 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: elif default_keep:
pat.refs = copy.copy(self.refs) pat.refs = copy.copy(self.refs)
@ -227,10 +247,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self self
""" """
old_shapes = self.shapes for layer in self.shapes:
self.shapes = list(chain.from_iterable(( self.shapes[layer] = list(chain.from_iterable(
shape.to_polygons(num_vertices, max_arclen) ss.to_polygons(num_vertices, max_arclen)
for shape in old_shapes))) for ss in self.shapes[layer]
))
return self return self
def manhattanize( def manhattanize(
@ -251,9 +272,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
""" """
self.polygonize() self.polygonize()
old_shapes = self.shapes for layer in self.shapes:
self.shapes = list(chain.from_iterable( self.shapes[layer] = list(chain.from_iterable((
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes))) ss.manhattanize(grid_x, grid_y)
for ss in self.shapes[layer]
)))
return self return self
def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]: 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],...]`. is of the form `[[x0, y0], [x1, y1],...]`.
""" """
pat = self.deepcopy().polygonize().flatten(library=library) 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]: def referenced_patterns(self) -> set[str | None]:
""" """
@ -277,7 +304,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
A set of all pattern names referenced by this pattern. 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( def get_bounds(
self, self,
@ -301,23 +328,29 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
min_bounds = numpy.array((+inf, +inf)) min_bounds = numpy.array((+inf, +inf))
max_bounds = numpy.array((-inf, -inf)) max_bounds = numpy.array((-inf, -inf))
for entry in chain(self.shapes, self.labels): for entry in chain_elements(self.shapes, self.labels):
bounds = entry.get_bounds() bounds = cast(Bounded, entry).get_bounds()
if bounds is None: if bounds is None:
continue continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :]) min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :]) max_bounds = numpy.maximum(max_bounds, bounds[1, :])
if self.refs and (library is None): if recurse and self.has_refs():
raise PatternError('Must provide a library to get_bounds() to resolve refs') if library is None:
raise PatternError('Must provide a library to get_bounds() to resolve refs')
if recurse: for target, refs in self.refs.items():
for entry in self.refs: if target is None:
bounds = entry.get_bounds(library=library)
if bounds is None:
continue continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :]) if not refs:
max_bounds = numpy.maximum(max_bounds, bounds[1, :]) continue
target_pat = library[target]
for ref in refs:
bounds = ref.get_bounds(target_pat, library=library)
if bounds is None:
continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
if (max_bounds < min_bounds).any(): if (max_bounds < min_bounds).any():
return None return None
@ -352,7 +385,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self 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) cast(Positionable, entry).translate(offset)
return self return self
@ -366,7 +399,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self self
""" """
for entry in chain(self.shapes, self.refs): for entry in chain_elements(self.shapes, self.refs):
cast(Scalable, entry).scale_by(c) cast(Scalable, entry).scale_by(c)
return self return self
@ -382,7 +415,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self self
""" """
for entry in chain(self.shapes, self.refs): for entry in chain_elements(self.shapes, self.refs):
cast(Positionable, entry).offset *= c cast(Positionable, entry).offset *= c
cast(Scalable, entry).scale_by(c) cast(Scalable, entry).scale_by(c)
@ -390,7 +423,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
if rep: if rep:
rep.scale_by(c) rep.scale_by(c)
for label in self.labels: for label in chain_elements(self.labels):
cast(Positionable, label).offset *= c cast(Positionable, label).offset *= c
rep = cast(Repeatable, label).repetition rep = cast(Repeatable, label).repetition
@ -429,7 +462,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self 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 old_offset = cast(Positionable, entry).offset
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset) cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
return self return self
@ -444,7 +477,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self 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) cast(Rotatable, entry).rotate(rotation)
return self return self
@ -459,7 +492,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self 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 cast(Positionable, entry).offset[across_axis - 1] *= -1
return self return self
@ -475,7 +508,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self 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) cast(Mirrorable, entry).mirror(across_axis)
return self return self
@ -521,68 +554,95 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
True if the pattern is contains no shapes, labels, or refs. True if the pattern is contains no shapes, labels, or refs.
""" """
return (len(self.refs) == 0 return not (self.has_refs() or self.has_shapes() or self.has_labels())
and len(self.shapes) == 0
and len(self.labels) == 0)
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 Convenience function which constructs a `Ref` object and adds it
to this pattern. to this pattern.
Args: Args:
target: Target for the ref
*args: Passed to `Ref()` *args: Passed to `Ref()`
**kwargs: Passed to `Ref()` **kwargs: Passed to `Ref()`
Returns: Returns:
self self
""" """
self.refs.append(Ref(*args, **kwargs)) self.refs[target].append(Ref(*args, **kwargs))
return self 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 Convenience function which constructs a `Polygon` object and adds it
to this pattern. to this pattern.
Args: Args:
layer: Layer for the polygon
*args: Passed to `Polygon()` *args: Passed to `Polygon()`
**kwargs: Passed to `Polygon()` **kwargs: Passed to `Polygon()`
Returns: Returns:
self self
""" """
self.shapes.append(Polygon(*args, **kwargs)) self.shapes[layer].append(Polygon(*args, **kwargs))
return self 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 Convenience function which calls `Polygon.rect` to construct a
rectangle and adds it to this pattern. rectangle and adds it to this pattern.
Args: Args:
layer: Layer for the rectangle
*args: Passed to `Polygon.rect()` *args: Passed to `Polygon.rect()`
**kwargs: Passed to `Polygon.rect()` **kwargs: Passed to `Polygon.rect()`
Returns: Returns:
self self
""" """
self.shapes.append(Polygon.rect(*args, **kwargs)) self.shapes[layer].append(Polygon.rect(*args, **kwargs))
return self 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 Convenience function which constructs a `Label` object
and adds it to this pattern. and adds it to this pattern.
Args: Args:
layer: Layer for the label
*args: Passed to `Label()` *args: Passed to `Label()`
**kwargs: Passed to `Label()` **kwargs: Passed to `Label()`
Returns: Returns:
self self
""" """
self.labels.append(Label(*args, **kwargs)) self.labels[layer].append(Label(*args, **kwargs))
return self return self
def flatten( def flatten(
@ -610,24 +670,26 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
pat = library[name].deepcopy() pat = library[name].deepcopy()
flattened[name] = None flattened[name] = None
for ref in pat.refs: for target, refs in pat.refs.items():
target = ref.target
if target is None: if target is None:
continue continue
if not refs:
continue
if target not in flattened: if target not in flattened:
flatten_single(target) flatten_single(target)
target_pat = flattened[target] target_pat = flattened[target]
if target_pat is None: if target_pat is None:
raise PatternError(f'Circular reference in {name} to {target}') raise PatternError(f'Circular reference in {name} to {target}')
if target_pat.is_empty(): # avoid some extra allocations if target_pat.is_empty(): # avoid some extra allocations
continue continue
p = ref.as_pattern(pattern=flattened[target]) for ref in refs:
if not flatten_ports: p = ref.as_pattern(pattern=target_pat)
p.ports.clear() if not flatten_ports:
pat.append(p) p.ports.clear()
pat.append(p)
pat.refs.clear() pat.refs.clear()
flattened[name] = pat flattened[name] = pat
@ -661,7 +723,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
from matplotlib import pyplot # type: ignore from matplotlib import pyplot # type: ignore
import matplotlib.collections # 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') raise PatternError('Must provide a library when visualizing a pattern with refs')
offset = numpy.array(offset, dtype=float) offset = numpy.array(offset, dtype=float)
@ -675,7 +737,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
axes = figure.gca() axes = figure.gca()
polygons = [] 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()] polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
mpl_poly_collection = matplotlib.collections.PolyCollection( mpl_poly_collection = matplotlib.collections.PolyCollection(
@ -686,16 +748,52 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
axes.add_collection(mpl_poly_collection) axes.add_collection(mpl_poly_collection)
pyplot.axis('equal') pyplot.axis('equal')
for ref in self.refs: for target, refs in self.refs.items():
ref.as_pattern(library=library).visualize( if target is None:
library=library, continue
offset=offset, if not refs:
overdraw=True, continue
line_color=line_color, assert library is not None
fill_color=fill_color, target_pat = library[target]
) for ref in refs:
ref.as_pattern(target_pat).visualize(
library=library,
offset=offset,
overdraw=True,
line_color=line_color,
fill_color=fill_color,
)
if not overdraw: if not overdraw:
pyplot.xlabel('x') pyplot.xlabel('x')
pyplot.ylabel('y') pyplot.ylabel('y')
pyplot.show() 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. offset, rotation, scaling, and associated methods.
""" """
__slots__ = ( __slots__ = (
'_target', '_mirrored', '_mirrored',
# inherited # inherited
'_offset', '_rotation', 'scale', '_repetition', '_annotations', '_offset', '_rotation', 'scale', '_repetition', '_annotations',
) )
_target: str | None
""" The name of the `Pattern` being instanced """
_mirrored: NDArray[numpy.bool_] _mirrored: NDArray[numpy.bool_]
""" Whether to mirror the instance across the x and/or y axes. """ """ Whether to mirror the instance across the x and/or y axes. """
def __init__( def __init__(
self, self,
target: str | None,
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
@ -57,14 +53,12 @@ class Ref(
) -> None: ) -> None:
""" """
Args: Args:
target: Name of the Pattern to reference.
offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc. 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). rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
mirrored: Whether to mirror the referenced pattern across its x and y axes. mirrored: Whether to mirror the referenced pattern across its x and y axes.
scale: Scaling factor applied to the pattern's geometry. scale: Scaling factor applied to the pattern's geometry.
repetition: `Repetition` object, default `None` repetition: `Repetition` object, default `None`
""" """
self.target = target
self.offset = offset self.offset = offset
self.rotation = rotation self.rotation = rotation
self.scale = scale self.scale = scale
@ -76,7 +70,6 @@ class Ref(
def __copy__(self) -> 'Ref': def __copy__(self) -> 'Ref':
new = Ref( new = Ref(
target=self.target,
offset=self.offset.copy(), offset=self.offset.copy(),
rotation=self.rotation, rotation=self.rotation,
scale=self.scale, scale=self.scale,
@ -93,17 +86,6 @@ class Ref(
new.annotations = copy.deepcopy(self.annotations, memo) new.annotations = copy.deepcopy(self.annotations, memo)
return new 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 # Mirrored property
@property @property
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]: def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]:
@ -117,27 +99,16 @@ class Ref(
def as_pattern( def as_pattern(
self, self,
*, pattern: 'Pattern',
pattern: 'Pattern | None' = None,
library: Mapping[str, 'Pattern'] | None = None,
) -> 'Pattern': ) -> 'Pattern':
""" """
Args: Args:
pattern: Pattern object to transform pattern: Pattern object to transform
library: A str->Pattern mapping, used instead of `pattern`. Must contain
`self.target`.
Returns: Returns:
A copy of the referenced Pattern which has been scaled, rotated, etc. A copy of the referenced Pattern which has been scaled, rotated, etc.
according to this `Ref`'s properties. 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() pattern = pattern.deepcopy()
if self.scale != 1: if self.scale != 1:
@ -175,8 +146,8 @@ class Ref(
def get_bounds( def get_bounds(
self, self,
pattern: 'Pattern',
*, *,
pattern: 'Pattern | None' = None,
library: Mapping[str, 'Pattern'] | None = None, library: Mapping[str, 'Pattern'] | None = None,
) -> NDArray[numpy.float64] | None: ) -> NDArray[numpy.float64] | None:
""" """
@ -190,20 +161,29 @@ class Ref(
Returns: Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None` `[[x_min, y_min], [x_max, y_max]]` or `None`
""" """
if pattern is None and library is None: if pattern.is_empty():
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():
# no need to run as_pattern() # no need to run as_pattern()
return None 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: def __repr__(self) -> str:
name = f'"{self.target}"' if self.target is not None else None rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
scale = f' d{self.scale:g}' if self.scale != 1 else '' scale = f' d{self.scale:g}' if self.scale != 1 else ''
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' mirrored = ' m{: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 . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, layer_t, annotations_t from ..utils import is_scalar, annotations_t
class Arc(Shape): class Arc(Shape):
@ -24,7 +24,7 @@ class Arc(Shape):
__slots__ = ( __slots__ = (
'_radii', '_angles', '_width', '_rotation', '_radii', '_angles', '_width', '_rotation',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_radii: NDArray[numpy.float64] _radii: NDArray[numpy.float64]
@ -156,7 +156,6 @@ class Arc(Shape):
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -172,7 +171,6 @@ class Arc(Shape):
self._rotation = rotation self._rotation = rotation
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
self._layer = layer
else: else:
self.radii = radii self.radii = radii
self.angles = angles self.angles = angles
@ -181,7 +179,6 @@ class Arc(Shape):
self.rotation = rotation self.rotation = rotation
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Arc': def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
@ -241,7 +238,7 @@ class Arc(Shape):
ys = numpy.hstack((ys1, ys2)) ys = numpy.hstack((ys1, ys2))
xys = numpy.vstack((xs, ys)).T 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] return [poly]
def get_bounds(self) -> NDArray[numpy.float64]: def get_bounds(self) -> NDArray[numpy.float64]:
@ -352,13 +349,12 @@ class Arc(Shape):
rotation %= 2 * pi rotation %= 2 * pi
width = self.width 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), (self.offset, scale / norm_value, rotation, False),
lambda: Arc( lambda: Arc(
radii=radii * norm_value, radii=radii * norm_value,
angles=angles, angles=angles,
width=width * norm_value, width=width * norm_value,
layer=self.layer,
)) ))
def get_cap_edges(self) -> NDArray[numpy.float64]: def get_cap_edges(self) -> NDArray[numpy.float64]:
@ -415,4 +411,4 @@ class Arc(Shape):
def __repr__(self) -> str: def __repr__(self) -> str:
angles = f'{numpy.rad2deg(self.angles)}' angles = f'{numpy.rad2deg(self.angles)}'
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' rotation = f'{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 . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, layer_t, annotations_t from ..utils import is_scalar, annotations_t
class Circle(Shape): class Circle(Shape):
@ -17,7 +17,7 @@ class Circle(Shape):
__slots__ = ( __slots__ = (
'_radius', '_radius',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_radius: float _radius: float
@ -44,7 +44,6 @@ class Circle(Shape):
radius: float, radius: float,
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -55,13 +54,11 @@ class Circle(Shape):
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
self._layer = layer
else: else:
self.radius = radius self.radius = radius
self.offset = offset self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer
def __deepcopy__(self, memo: dict | None = None) -> 'Circle': def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -90,7 +87,7 @@ class Circle(Shape):
ys = numpy.sin(thetas) * self.radius ys = numpy.sin(thetas) * self.radius
xys = numpy.vstack((xs, ys)).T 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]: def get_bounds(self) -> NDArray[numpy.float64]:
return numpy.vstack((self.offset - self.radius, return numpy.vstack((self.offset - self.radius,
@ -110,9 +107,9 @@ class Circle(Shape):
def normalized_form(self, norm_value) -> normalized_shape_tuple: def normalized_form(self, norm_value) -> normalized_shape_tuple:
rotation = 0.0 rotation = 0.0
magnitude = self.radius / norm_value magnitude = self.radius / norm_value
return ((type(self), self.layer), return ((type(self),),
(self.offset, magnitude, rotation, False), (self.offset, magnitude, rotation, False),
lambda: Circle(radius=norm_value, layer=self.layer)) lambda: Circle(radius=norm_value))
def __repr__(self) -> str: 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 . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition 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): class Ellipse(Shape):
@ -20,7 +20,7 @@ class Ellipse(Shape):
__slots__ = ( __slots__ = (
'_radii', '_rotation', '_radii', '_rotation',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_radii: NDArray[numpy.float64] _radii: NDArray[numpy.float64]
@ -91,7 +91,6 @@ class Ellipse(Shape):
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -104,14 +103,12 @@ class Ellipse(Shape):
self._rotation = rotation self._rotation = rotation
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
self._layer = layer
else: else:
self.radii = radii self.radii = radii
self.offset = offset self.offset = offset
self.rotation = rotation self.rotation = rotation
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse': def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse':
@ -152,7 +149,7 @@ class Ellipse(Shape):
ys = r1 * sin_th ys = r1 * sin_th
xys = numpy.vstack((xs, ys)).T 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] return [poly]
def get_bounds(self) -> NDArray[numpy.float64]: def get_bounds(self) -> NDArray[numpy.float64]:
@ -183,10 +180,10 @@ class Ellipse(Shape):
radii = self.radii[::-1] / self.radius_y radii = self.radii[::-1] / self.radius_y
scale = self.radius_y scale = self.radius_y
angle = (self.rotation + pi / 2) % pi angle = (self.rotation + pi / 2) % pi
return ((type(self), radii, self.layer), return ((type(self), radii),
(self.offset, scale / norm_value, angle, False), (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: 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 ''
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}>' 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 . import Shape, normalized_shape_tuple, Polygon, Circle
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition 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 from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
@ -31,7 +31,7 @@ class Path(Shape):
__slots__ = ( __slots__ = (
'_vertices', '_width', '_cap', '_cap_extensions', '_vertices', '_width', '_cap', '_cap_extensions',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_vertices: NDArray[numpy.float64] _vertices: NDArray[numpy.float64]
_width: float _width: float
@ -154,7 +154,6 @@ class Path(Shape):
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -169,7 +168,6 @@ class Path(Shape):
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._width = width self._width = width
self._cap = cap self._cap = cap
self._cap_extensions = cap_extensions self._cap_extensions = cap_extensions
@ -178,7 +176,6 @@ class Path(Shape):
self.offset = offset self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.width = width self.width = width
self.cap = cap self.cap = cap
self.cap_extensions = cap_extensions self.cap_extensions = cap_extensions
@ -204,7 +201,6 @@ class Path(Shape):
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
) -> 'Path': ) -> 'Path':
""" """
Build a path by specifying the turn angles and travel distances 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: Whether to mirror across the x or y axes. For example,
`mirrored=(True, False)` results in a reflection across the x-axis, `mirrored=(True, False)` results in a reflection across the x-axis,
multiplying the path's y-coordinates by -1. Default `(False, False)` multiplying the path's y-coordinates by -1. Default `(False, False)`
layer: Layer, default `0`
Returns: Returns:
The resulting Path object The resulting Path object
@ -238,8 +233,7 @@ class Path(Shape):
verts.append(verts[-1] + direction * distance) verts.append(verts[-1] + direction * distance)
return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions, return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions,
offset=offset, rotation=rotation, mirrored=mirrored, offset=offset, rotation=rotation, mirrored=mirrored)
layer=layer)
def to_polygons( def to_polygons(
self, self,
@ -254,7 +248,7 @@ class Path(Shape):
if self.width == 0: if self.width == 0:
verts = numpy.vstack((v, v[::-1])) 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 perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
@ -305,12 +299,12 @@ class Path(Shape):
o1.append(v[-1] - perp[-1]) o1.append(v[-1] - perp[-1])
verts = numpy.vstack((o0, o1[::-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: if self.cap == PathCap.Circle:
#for vert in v: # not sure if every vertex, or just ends? #for vert in v: # not sure if every vertex, or just ends?
for vert in [v[0], v[-1]]: 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) polys += circ.to_polygons(num_vertices=num_vertices, max_arclen=max_arclen)
return polys return polys
@ -370,13 +364,12 @@ class Path(Shape):
width0 = self.width / norm_value 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), (offset, scale / norm_value, rotation, False),
lambda: Path( lambda: Path(
reordered_vertices * norm_value, reordered_vertices * norm_value,
width=self.width * norm_value, width=self.width * norm_value,
cap=self.cap, cap=self.cap,
layer=self.layer,
)) ))
def clean_vertices(self) -> 'Path': def clean_vertices(self) -> 'Path':
@ -422,4 +415,4 @@ class Path(Shape):
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0) 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 . import Shape, normalized_shape_tuple
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition 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 from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
@ -22,7 +22,7 @@ class Polygon(Shape):
__slots__ = ( __slots__ = (
'_vertices', '_vertices',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_vertices: NDArray[numpy.float64] _vertices: NDArray[numpy.float64]
@ -82,7 +82,6 @@ class Polygon(Shape):
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -94,13 +93,11 @@ class Polygon(Shape):
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
self._layer = layer
else: else:
self.vertices = vertices self.vertices = vertices
self.offset = offset self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.rotate(rotation) self.rotate(rotation)
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
@ -118,7 +115,6 @@ class Polygon(Shape):
*, *,
rotation: float = 0.0, rotation: float = 0.0,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
) -> 'Polygon': ) -> 'Polygon':
""" """
@ -128,7 +124,6 @@ class Polygon(Shape):
side_length: Length of one side side_length: Length of one side
rotation: Rotation counterclockwise, in radians rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)` offset: Offset, default `(0, 0)`
layer: Layer, default `0`
repetition: `Repetition` object, default `None` repetition: `Repetition` object, default `None`
Returns: Returns:
@ -139,7 +134,7 @@ class Polygon(Shape):
[+1, +1], [+1, +1],
[+1, -1]], dtype=float) [+1, -1]], dtype=float)
vertices = 0.5 * side_length * norm_square 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) poly.rotate(rotation)
return poly return poly
@ -150,7 +145,6 @@ class Polygon(Shape):
*, *,
rotation: float = 0, rotation: float = 0,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
) -> 'Polygon': ) -> 'Polygon':
""" """
@ -161,7 +155,6 @@ class Polygon(Shape):
ly: Length along y (before rotation) ly: Length along y (before rotation)
rotation: Rotation counterclockwise, in radians rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)` offset: Offset, default `(0, 0)`
layer: Layer, default `0`
repetition: `Repetition` object, default `None` repetition: `Repetition` object, default `None`
Returns: Returns:
@ -171,7 +164,7 @@ class Polygon(Shape):
[-lx, +ly], [-lx, +ly],
[+lx, +ly], [+lx, +ly],
[+lx, -ly]], dtype=float) [+lx, -ly]], dtype=float)
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition) poly = Polygon(vertices, offset=offset, repetition=repetition)
poly.rotate(rotation) poly.rotate(rotation)
return poly return poly
@ -186,7 +179,6 @@ class Polygon(Shape):
yctr: float | None = None, yctr: float | None = None,
ymax: float | None = None, ymax: float | None = None,
ly: float | None = None, ly: float | None = None,
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
) -> 'Polygon': ) -> 'Polygon':
""" """
@ -204,7 +196,6 @@ class Polygon(Shape):
yctr: Center y coordinate yctr: Center y coordinate
ymax: Maximum y coordinate ymax: Maximum y coordinate
ly: Length along y direction ly: Length along y direction
layer: Layer, default `0`
repetition: `Repetition` object, default `None` repetition: `Repetition` object, default `None`
Returns: Returns:
@ -270,7 +261,7 @@ class Polygon(Shape):
else: else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!') 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 return poly
@staticmethod @staticmethod
@ -281,7 +272,6 @@ class Polygon(Shape):
regular: bool = True, regular: bool = True,
center: ArrayLike = (0.0, 0.0), center: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
) -> 'Polygon': ) -> 'Polygon':
""" """
@ -300,7 +290,6 @@ class Polygon(Shape):
rotation: Rotation counterclockwise, in radians. rotation: Rotation counterclockwise, in radians.
`0` results in four axis-aligned sides (the long sides of the `0` results in four axis-aligned sides (the long sides of the
irregular octagon). irregular octagon).
layer: Layer, default `0`
repetition: `Repetition` object, default `None` repetition: `Repetition` object, default `None`
Returns: Returns:
@ -327,7 +316,7 @@ class Polygon(Shape):
side_length = 2 * inner_radius / s side_length = 2 * inner_radius / s
vertices = 0.5 * side_length * norm_oct 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) poly.rotate(rotation)
return poly return poly
@ -378,9 +367,9 @@ class Polygon(Shape):
# TODO: normalize mirroring? # 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), (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': def clean_vertices(self) -> 'Polygon':
""" """
@ -414,4 +403,4 @@ class Polygon(Shape):
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0) 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 numpy.typing import NDArray, ArrayLike
from ..traits import ( from ..traits import (
Rotatable, Mirrorable, Copyable, Scalable, Rotatable, Mirrorable, Copyable, Scalable, Bounded,
PositionableImpl, LayerableImpl, PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
PivotableImpl, RepeatableImpl, AnnotatableImpl,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -26,7 +25,7 @@ normalized_shape_tuple = tuple[
DEFAULT_POLY_NUM_VERTICES = 24 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): PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
""" """
Class specifying functions common to all shapes. Class specifying functions common to all shapes.
@ -194,10 +193,7 @@ class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Sc
vertex_lists.append(vlist) vertex_lists.append(vlist)
polygon_contours.append(numpy.vstack(vertex_lists)) polygon_contours.append(numpy.vstack(vertex_lists))
manhattan_polygons = [ manhattan_polygons = [Polygon(vertices=contour) for contour in polygon_contours]
Polygon(vertices=contour, layer=self.layer)
for contour in polygon_contours
]
return manhattan_polygons 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]], vertices = numpy.hstack((grx[snapped_contour[:, None, 0] + offset_i[0]],
gry[snapped_contour[:, None, 1] + offset_i[1]])) gry[snapped_contour[:, None, 1] + offset_i[1]]))
manhattan_polygons.append(Polygon( manhattan_polygons.append(Polygon(vertices=vertices))
vertices=vertices,
layer=self.layer,
))
return manhattan_polygons return manhattan_polygons

@ -9,7 +9,7 @@ from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..traits import RotatableImpl 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 from ..utils import annotations_t
# Loaded on use: # Loaded on use:
@ -25,7 +25,7 @@ class Text(RotatableImpl, Shape):
__slots__ = ( __slots__ = (
'_string', '_height', '_mirrored', 'font_path', '_string', '_height', '_mirrored', 'font_path',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_rotation', '_offset', '_repetition', '_annotations', '_rotation',
) )
_string: str _string: str
@ -73,7 +73,6 @@ class Text(RotatableImpl, Shape):
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: ArrayLike = (False, False), mirrored: ArrayLike = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -82,7 +81,6 @@ class Text(RotatableImpl, Shape):
assert isinstance(offset, numpy.ndarray) assert isinstance(offset, numpy.ndarray)
assert isinstance(mirrored, numpy.ndarray) assert isinstance(mirrored, numpy.ndarray)
self._offset = offset self._offset = offset
self._layer = layer
self._string = string self._string = string
self._height = height self._height = height
self._rotation = rotation self._rotation = rotation
@ -91,7 +89,6 @@ class Text(RotatableImpl, Shape):
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
else: else:
self.offset = offset self.offset = offset
self.layer = layer
self.string = string self.string = string
self.height = height self.height = height
self.rotation = rotation self.rotation = rotation
@ -120,7 +117,7 @@ class Text(RotatableImpl, Shape):
# Move these polygons to the right of the previous letter # Move these polygons to the right of the previous letter
for xys in raw_polys: for xys in raw_polys:
poly = Polygon(xys, layer=self.layer) poly = Polygon(xys)
poly.mirror2d(self.mirrored) poly.mirror2d(self.mirrored)
poly.scale_by(self.height) poly.scale_by(self.height)
poly.offset = self.offset + [total_advance, 0] poly.offset = self.offset + [total_advance, 0]
@ -144,7 +141,7 @@ class Text(RotatableImpl, Shape):
mirror_x, rotation = normalize_mirror(self.mirrored) mirror_x, rotation = normalize_mirror(self.mirrored)
rotation += self.rotation rotation += self.rotation
rotation %= 2 * pi 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), (self.offset, self.height / norm_value, rotation, mirror_x),
lambda: Text( lambda: Text(
string=self.string, string=self.string,
@ -152,7 +149,6 @@ class Text(RotatableImpl, Shape):
font_path=self.font_path, font_path=self.font_path,
rotation=rotation, rotation=rotation,
mirrored=(mirror_x, False), mirrored=(mirror_x, False),
layer=self.layer,
)) ))
def get_bounds(self) -> NDArray[numpy.float64]: def get_bounds(self) -> NDArray[numpy.float64]:
@ -256,6 +252,6 @@ def get_char_as_polygons(
return polygons, advance return polygons, advance
def __repr__(self) -> str: def __repr__(self) -> str:
rotation = f'{self.rotation*180/pi:g}' if self.rotation != 0 else '' rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' mirrored = ' m{:d}{: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 Traits (mixins) and default implementations
""" """
from .positionable import Positionable, PositionableImpl from .positionable import Positionable, PositionableImpl, Bounded
from .layerable import Layerable, LayerableImpl from .layerable import Layerable, LayerableImpl
from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
from .repeatable import Repeatable, RepeatableImpl from .repeatable import Repeatable, RepeatableImpl

@ -60,6 +60,8 @@ class Positionable(metaclass=ABCMeta):
""" """
pass pass
class Bounded(metaclass=ABCMeta):
@abstractmethod @abstractmethod
def get_bounds(self) -> NDArray[numpy.float64] | None: def get_bounds(self) -> NDArray[numpy.float64] | None:
""" """

@ -8,7 +8,6 @@ from numpy.typing import NDArray, ArrayLike
from ..error import MasqueError from ..error import MasqueError
from ..pattern import Pattern from ..pattern import Pattern
from ..ref import Ref
def maxrects_bssf( def maxrects_bssf(
@ -160,8 +159,8 @@ def pack_patterns(
locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects) locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects)
pat = Pattern() 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] rejects = [patterns[ii] for ii in reject_inds]
return pat, rejects return pat, rejects

@ -8,11 +8,11 @@ to write equivalent functions for your own format or alternate storage methods.
""" """
from typing import Sequence, Mapping from typing import Sequence, Mapping
import logging import logging
from itertools import chain
import numpy import numpy
from ..pattern import Pattern from ..pattern import Pattern
from ..label import Label
from ..utils import layer_t from ..utils import layer_t
from ..ports import Port from ..ports import Port
from ..error import PatternError from ..error import PatternError
@ -44,9 +44,7 @@ def ports_to_data(pattern: Pattern, layer: layer_t) -> Pattern:
angle_deg = numpy.inf angle_deg = numpy.inf
else: else:
angle_deg = numpy.rad2deg(port.rotation) angle_deg = numpy.rad2deg(port.rotation)
pattern.labels += [ pattern.label(layer=layer, string=f'{name}:{port.ptype} {angle_deg:g}', offset=port.offset)
Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset)
]
return pattern return pattern
@ -97,7 +95,7 @@ def data_to_ports(
# Load ports for all subpatterns, and use any we find # Load ports for all subpatterns, and use any we find
found_ports = False found_ports = False
for target in set(rr.target for rr in pattern.refs): for target in pattern.refs:
if target is None: if target is None:
continue continue
pp = data_to_ports( pp = data_to_ports(
@ -113,17 +111,20 @@ def data_to_ports(
if not found_ports: if not found_ports:
return pattern return pattern
for ref in pattern.refs: for target, refs in pattern.refs.items():
if ref.target is None: if target is None:
continue continue
aa = library.abstract(ref.target) if not refs:
if not aa.ports:
continue continue
aa.apply_ref_transform(ref) for ref in refs:
aa = library.abstract(target)
if not aa.ports:
break
pattern.check_ports(other_names=aa.ports.keys()) aa.apply_ref_transform(ref)
pattern.ports.update(aa.ports) pattern.check_ports(other_names=aa.ports.keys())
pattern.ports.update(aa.ports)
return pattern return pattern
@ -149,13 +150,13 @@ def data_to_ports_flat(
Returns: Returns:
The updated `pattern`. Port labels are not removed. 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: if not labels:
return pattern return pattern
pstr = cell_name if cell_name is not None else repr(pattern) pstr = cell_name if cell_name is not None else repr(pattern)
if pattern.ports: if pattern.ports:
raise PatternError('Pattern "{pstr}" has pre-existing ports!') raise PatternError(f'Pattern "{pstr}" has pre-existing ports!')
local_ports = {} local_ports = {}
for label in labels: for label in labels:

Loading…
Cancel
Save