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

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

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

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

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

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

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

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

@ -5,15 +5,15 @@ import numpy
from numpy.typing import ArrayLike, NDArray
from .repetition import Repetition
from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, RepeatableImpl
from .utils import rotation_matrix_2d, AutoSlots, annotations_t
from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded
from .traits import AnnotatableImpl
class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
Pivotable, Copyable, metaclass=AutoSlots):
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl,
Bounded, Pivotable, Copyable, metaclass=AutoSlots):
"""
A text annotation with a position and layer (but no size; it is not drawn)
A text annotation with a position (but no size; it is not drawn)
"""
__slots__ = ( '_string', )
@ -40,13 +40,11 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
string: str,
*,
offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
) -> None:
self.string = string
self.offset = numpy.array(offset, dtype=float, copy=True)
self.layer = layer
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
@ -54,7 +52,6 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
return type(self)(
string=self.string,
offset=self.offset.copy(),
layer=self.layer,
repetition=self.repetition,
)

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

@ -1,10 +1,11 @@
"""
Base object representing a lithography mask.
"""
from typing import Callable, Sequence, cast, Mapping, Self, Any
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar
import copy
import logging
from itertools import chain
from collections import defaultdict
import numpy
from numpy import inf
@ -12,14 +13,17 @@ from numpy.typing import NDArray, ArrayLike
# .visualize imports matplotlib and matplotlib.collections
from .ref import Ref
from .shapes import Shape, Polygon, DEFAULT_POLY_NUM_VERTICES
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
from .label import Label
from .utils import rotation_matrix_2d, annotations_t
from .utils import rotation_matrix_2d, annotations_t, layer_t
from .error import PatternError
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
from .ports import Port, PortList
logger = logging.getLogger(__name__)
class Pattern(PortList, AnnotatableImpl, Mirrorable):
"""
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
@ -31,15 +35,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
'_offset', '_annotations',
)
shapes: list[Shape]
""" List of all shapes in this Pattern.
shapes: defaultdict[layer_t, list[Shape]]
""" Stores of all shapes in this Pattern, indexed by layer.
Elements in this list are assumed to inherit from Shape or provide equivalent functions.
"""
labels: list[Label]
labels: defaultdict[layer_t, list[Label]]
""" List of all labels in this Pattern. """
refs: list[Ref]
refs: defaultdict[str | None, list[Ref]]
""" List of all references to other patterns (`Ref`s) in this `Pattern`.
Multiple objects in this list may reference the same Pattern object
(i.e. multiple instances of the same object).
@ -59,9 +63,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def __init__(
self,
*,
shapes: Sequence[Shape] = (),
labels: Sequence[Label] = (),
refs: Sequence[Ref] = (),
shapes: Mapping[layer_t, Sequence[Shape]] | None = None,
labels: Mapping[layer_t, Sequence[Label]] | None = None,
refs: Mapping[str | None, Sequence[Ref]] | None = None,
annotations: annotations_t | None = None,
ports: Mapping[str, 'Port'] | None = None
) -> None:
@ -76,20 +80,18 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
annotations: Initial annotations for the pattern
ports: Any ports in the pattern
"""
if isinstance(shapes, list):
self.shapes = shapes
else:
self.shapes = list(shapes)
if isinstance(labels, list):
self.labels = labels
else:
self.labels = list(labels)
if isinstance(refs, list):
self.refs = refs
else:
self.refs = list(refs)
self.shapes = defaultdict(list)
self.labels = defaultdict(list)
self.refs = defaultdict(list)
if shapes:
for layer, sseq in shapes.items():
self.shapes[layer].extend(sseq)
if labels:
for layer, lseq in labels.items():
self.labels[layer].extend(lseq)
if refs:
for target, rseq in refs.items():
self.refs[target].extend(rseq)
if ports is not None:
self.ports = dict(copy.deepcopy(ports))
@ -99,32 +101,42 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self.annotations = annotations if annotations is not None else {}
def __repr__(self) -> str:
s = f'<Pattern: s{len(self.shapes)} r{len(self.refs)} l{len(self.labels)} ['
nshapes = sum(len(seq) for seq in self.shapes.values())
nrefs = sum(len(seq) for seq in self.refs.values())
nlabels = sum(len(seq) for seq in self.labels.values())
s = f'<Pattern: s{nshapes} r{nrefs} l{nlabels} ['
for name, port in self.ports.items():
s += f'\n\t{name}: {port}'
s += ']>'
return s
def __copy__(self) -> 'Pattern':
return Pattern(
shapes=copy.deepcopy(self.shapes),
labels=copy.deepcopy(self.labels),
refs=[copy.copy(sp) for sp in self.refs],
logger.warning('Making a shallow copy of a Pattern... old shapes are re-referenced!')
new = Pattern(
annotations=copy.deepcopy(self.annotations),
ports=copy.deepcopy(self.ports),
)
for target, rseq in self.refs.items():
new.refs[target].extend(rseq)
for layer, sseq in self.shapes.items():
new.shapes[layer].extend(sseq)
for layer, lseq in self.labels.items():
new.labels[layer].extend(lseq)
def __deepcopy__(self, memo: dict | None = None) -> 'Pattern':
memo = {} if memo is None else memo
new = Pattern(
shapes=copy.deepcopy(self.shapes, memo),
labels=copy.deepcopy(self.labels, memo),
refs=copy.deepcopy(self.refs, memo),
annotations=copy.deepcopy(self.annotations, memo),
ports=copy.deepcopy(self.ports),
)
return new
# def __deepcopy__(self, memo: dict | None = None) -> 'Pattern':
# memo = {} if memo is None else memo
# new = Pattern(
# shapes=copy.deepcopy(self.shapes, memo),
# labels=copy.deepcopy(self.labels, memo),
# refs=copy.deepcopy(self.refs, memo),
# annotations=copy.deepcopy(self.annotations, memo),
# ports=copy.deepcopy(self.ports),
# )
# return new
def append(self, other_pattern: 'Pattern') -> Self:
"""
Appends all shapes, labels and refs from other_pattern to self's shapes,
@ -136,9 +148,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
self.refs += other_pattern.refs
self.shapes += other_pattern.shapes
self.labels += other_pattern.labels
for target, rseq in other_pattern.refs.items():
self.refs[target].extend(rseq)
for layer, sseq in other_pattern.shapes.items():
self.shapes[layer].extend(sseq)
for layer, lseq in other_pattern.labels.items():
self.labels[layer].extend(lseq)
annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys())
if annotation_conflicts:
@ -154,9 +169,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def subset(
self,
shapes: Callable[[Shape], bool] | None = None,
labels: Callable[[Label], bool] | None = None,
refs: Callable[[Ref], bool] | None = None,
shapes: Callable[[layer_t, Shape], bool] | None = None,
labels: Callable[[layer_t, Label], bool] | None = None,
refs: Callable[[str | None, Ref], bool] | None = None,
annotations: Callable[[str, list[int | float | str]], bool] | None = None,
ports: Callable[[str, Port], bool] | None = None,
default_keep: bool = False
@ -167,9 +182,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Self is _not_ altered, but shapes, labels, and refs are _not_ copied, just referenced.
Args:
shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset.
labels: Given a label, returns a boolean denoting whether the label is a member of the subset.
refs: Given a ref, returns a boolean denoting if it is a member of the subset.
shapes: Given a layer and shape, returns a boolean denoting whether the shape is a
member of the subset.
labels: Given a layer and label, returns a boolean denoting whether the label is a
member of the subset.
refs: Given a target and ref, returns a boolean denoting if it is a member of the subset.
annotations: Given an annotation, returns a boolean denoting if it is a member of the subset.
ports: Given a port, returns a boolean denoting if it is a member of the subset.
default_keep: If `True`, keeps all elements of a given type if no function is supplied.
@ -182,17 +199,20 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
pat = Pattern()
if shapes is not None:
pat.shapes = [s for s in self.shapes if shapes(s)]
for layer in self.shapes:
pat.shapes[layer] = [ss for ss in self.shapes[layer] if shapes(layer, ss)]
elif default_keep:
pat.shapes = copy.copy(self.shapes)
if labels is not None:
pat.labels = [s for s in self.labels if labels(s)]
for layer in self.labels:
pat.labels[layer] = [ll for ll in self.labels[layer] if labels(layer, ll)]
elif default_keep:
pat.labels = copy.copy(self.labels)
if refs is not None:
pat.refs = [s for s in self.refs if refs(s)]
for target in self.refs:
pat.refs[target] = [rr for rr in self.refs[target] if refs(target, rr)]
elif default_keep:
pat.refs = copy.copy(self.refs)
@ -227,10 +247,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
old_shapes = self.shapes
self.shapes = list(chain.from_iterable((
shape.to_polygons(num_vertices, max_arclen)
for shape in old_shapes)))
for layer in self.shapes:
self.shapes[layer] = list(chain.from_iterable(
ss.to_polygons(num_vertices, max_arclen)
for ss in self.shapes[layer]
))
return self
def manhattanize(
@ -251,9 +272,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
"""
self.polygonize()
old_shapes = self.shapes
self.shapes = list(chain.from_iterable(
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
for layer in self.shapes:
self.shapes[layer] = list(chain.from_iterable((
ss.manhattanize(grid_x, grid_y)
for ss in self.shapes[layer]
)))
return self
def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]:
@ -268,7 +291,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
is of the form `[[x0, y0], [x1, y1],...]`.
"""
pat = self.deepcopy().polygonize().flatten(library=library)
return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now
polys = [
cast(Polygon, shape).vertices + cast(Polygon, shape).offset
for shape in chain_elements(pat.shapes)
]
return polys
def referenced_patterns(self) -> set[str | None]:
"""
@ -277,7 +304,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
A set of all pattern names referenced by this pattern.
"""
return set(sp.target for sp in self.refs)
return set(self.refs.keys())
def get_bounds(
self,
@ -301,23 +328,29 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
min_bounds = numpy.array((+inf, +inf))
max_bounds = numpy.array((-inf, -inf))
for entry in chain(self.shapes, self.labels):
bounds = entry.get_bounds()
for entry in chain_elements(self.shapes, self.labels):
bounds = cast(Bounded, entry).get_bounds()
if bounds is None:
continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
if self.refs and (library is None):
raise PatternError('Must provide a library to get_bounds() to resolve refs')
if recurse and self.has_refs():
if library is None:
raise PatternError('Must provide a library to get_bounds() to resolve refs')
if recurse:
for entry in self.refs:
bounds = entry.get_bounds(library=library)
if bounds is None:
for target, refs in self.refs.items():
if target is None:
continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
if not refs:
continue
target_pat = library[target]
for ref in refs:
bounds = ref.get_bounds(target_pat, library=library)
if bounds is None:
continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
if (max_bounds < min_bounds).any():
return None
@ -352,7 +385,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
cast(Positionable, entry).translate(offset)
return self
@ -366,7 +399,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(self.shapes, self.refs):
for entry in chain_elements(self.shapes, self.refs):
cast(Scalable, entry).scale_by(c)
return self
@ -382,7 +415,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(self.shapes, self.refs):
for entry in chain_elements(self.shapes, self.refs):
cast(Positionable, entry).offset *= c
cast(Scalable, entry).scale_by(c)
@ -390,7 +423,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
if rep:
rep.scale_by(c)
for label in self.labels:
for label in chain_elements(self.labels):
cast(Positionable, label).offset *= c
rep = cast(Repeatable, label).repetition
@ -429,7 +462,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
old_offset = cast(Positionable, entry).offset
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
return self
@ -444,7 +477,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.ports.values()):
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
cast(Rotatable, entry).rotate(rotation)
return self
@ -459,7 +492,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
cast(Positionable, entry).offset[across_axis - 1] *= -1
return self
@ -475,7 +508,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.ports.values()):
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
cast(Mirrorable, entry).mirror(across_axis)
return self
@ -521,68 +554,95 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns:
True if the pattern is contains no shapes, labels, or refs.
"""
return (len(self.refs) == 0
and len(self.shapes) == 0
and len(self.labels) == 0)
return not (self.has_refs() or self.has_shapes() or self.has_labels())
def ref(self, *args: Any, **kwargs: Any) -> Self:
def has_refs(self) -> bool:
return any(True for _ in chain.from_iterable(self.refs.values()))
def has_shapes(self) -> bool:
return any(True for _ in chain.from_iterable(self.shapes.values()))
def has_labels(self) -> bool:
return any(True for _ in chain.from_iterable(self.labels.values()))
def ref(self, target: str | None, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which constructs a `Ref` object and adds it
to this pattern.
Args:
target: Target for the ref
*args: Passed to `Ref()`
**kwargs: Passed to `Ref()`
Returns:
self
"""
self.refs.append(Ref(*args, **kwargs))
self.refs[target].append(Ref(*args, **kwargs))
return self
def polygon(self, *args: Any, **kwargs: Any) -> Self:
def polygon(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which constructs a `Polygon` object and adds it
to this pattern.
Args:
layer: Layer for the polygon
*args: Passed to `Polygon()`
**kwargs: Passed to `Polygon()`
Returns:
self
"""
self.shapes.append(Polygon(*args, **kwargs))
self.shapes[layer].append(Polygon(*args, **kwargs))
return self
def rect(self, *args: Any, **kwargs: Any) -> Self:
def rect(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which calls `Polygon.rect` to construct a
rectangle and adds it to this pattern.
Args:
layer: Layer for the rectangle
*args: Passed to `Polygon.rect()`
**kwargs: Passed to `Polygon.rect()`
Returns:
self
"""
self.shapes.append(Polygon.rect(*args, **kwargs))
self.shapes[layer].append(Polygon.rect(*args, **kwargs))
return self
def label(self, *args: Any, **kwargs: Any) -> Self:
def path(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which constructs a `Path` object and adds it
to this pattern.
Args:
layer: Layer for the path
*args: Passed to `Path()`
**kwargs: Passed to `Path()`
Returns:
self
"""
self.shapes[layer].append(Path(*args, **kwargs))
return self
def label(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which constructs a `Label` object
and adds it to this pattern.
Args:
layer: Layer for the label
*args: Passed to `Label()`
**kwargs: Passed to `Label()`
Returns:
self
"""
self.labels.append(Label(*args, **kwargs))
self.labels[layer].append(Label(*args, **kwargs))
return self
def flatten(
@ -610,24 +670,26 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
pat = library[name].deepcopy()
flattened[name] = None
for ref in pat.refs:
target = ref.target
for target, refs in pat.refs.items():
if target is None:
continue
if not refs:
continue
if target not in flattened:
flatten_single(target)
target_pat = flattened[target]
if target_pat is None:
raise PatternError(f'Circular reference in {name} to {target}')
if target_pat.is_empty(): # avoid some extra allocations
continue
p = ref.as_pattern(pattern=flattened[target])
if not flatten_ports:
p.ports.clear()
pat.append(p)
for ref in refs:
p = ref.as_pattern(pattern=target_pat)
if not flatten_ports:
p.ports.clear()
pat.append(p)
pat.refs.clear()
flattened[name] = pat
@ -661,7 +723,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
from matplotlib import pyplot # type: ignore
import matplotlib.collections # type: ignore
if self.refs and library is None:
if self.has_refs() and library is None:
raise PatternError('Must provide a library when visualizing a pattern with refs')
offset = numpy.array(offset, dtype=float)
@ -675,7 +737,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
axes = figure.gca()
polygons = []
for shape in self.shapes:
for shape in chain.from_iterable(self.shapes.values()):
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
mpl_poly_collection = matplotlib.collections.PolyCollection(
@ -686,16 +748,52 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
axes.add_collection(mpl_poly_collection)
pyplot.axis('equal')
for ref in self.refs:
ref.as_pattern(library=library).visualize(
library=library,
offset=offset,
overdraw=True,
line_color=line_color,
fill_color=fill_color,
)
for target, refs in self.refs.items():
if target is None:
continue
if not refs:
continue
assert library is not None
target_pat = library[target]
for ref in refs:
ref.as_pattern(target_pat).visualize(
library=library,
offset=offset,
overdraw=True,
line_color=line_color,
fill_color=fill_color,
)
if not overdraw:
pyplot.xlabel('x')
pyplot.ylabel('y')
pyplot.show()
TT = TypeVar('TT')
def chain_elements(*args: Mapping[Any, Iterable[TT]]) -> Iterable[TT]:
return chain(*(chain.from_iterable(aa.values()) for aa in args))
def map_layers(
elements: Mapping[layer_t, Sequence[TT]],
layer_map: Mapping[layer_t, layer_t],
) -> defaultdict[layer_t, list[TT]]:
new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list)
for old_layer, seq in elements.items():
new_layer = layer_map.get(old_layer, old_layer)
new_elements[new_layer].extend(seq)
return new_elements
def map_targets(
refs: Mapping[str | None, Sequence[Ref]],
target_map: Mapping[str | None, str | None] | Mapping[str, str | None],
) -> defaultdict[str | None, list[Ref]]:
new_refs: defaultdict[str | None, list[Ref]] = defaultdict(list)
for old_target, seq in refs.items():
new_target = target_map.get(old_target, old_target) # type: ignore # OK to .get() wrong type
new_refs[new_target].extend(seq)
return new_refs

@ -33,20 +33,16 @@ class Ref(
offset, rotation, scaling, and associated methods.
"""
__slots__ = (
'_target', '_mirrored',
'_mirrored',
# inherited
'_offset', '_rotation', 'scale', '_repetition', '_annotations',
)
_target: str | None
""" The name of the `Pattern` being instanced """
_mirrored: NDArray[numpy.bool_]
""" Whether to mirror the instance across the x and/or y axes. """
def __init__(
self,
target: str | None,
*,
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
@ -57,14 +53,12 @@ class Ref(
) -> None:
"""
Args:
target: Name of the Pattern to reference.
offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc.
rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
mirrored: Whether to mirror the referenced pattern across its x and y axes.
scale: Scaling factor applied to the pattern's geometry.
repetition: `Repetition` object, default `None`
"""
self.target = target
self.offset = offset
self.rotation = rotation
self.scale = scale
@ -76,7 +70,6 @@ class Ref(
def __copy__(self) -> 'Ref':
new = Ref(
target=self.target,
offset=self.offset.copy(),
rotation=self.rotation,
scale=self.scale,
@ -93,17 +86,6 @@ class Ref(
new.annotations = copy.deepcopy(self.annotations, memo)
return new
# target property
@property
def target(self) -> str | None:
return self._target
@target.setter
def target(self, val: str | None) -> None:
if val is not None and not isinstance(val, str):
raise PatternError(f'Provided target {val} is not a str or None!')
self._target = val
# Mirrored property
@property
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]:
@ -117,27 +99,16 @@ class Ref(
def as_pattern(
self,
*,
pattern: 'Pattern | None' = None,
library: Mapping[str, 'Pattern'] | None = None,
pattern: 'Pattern',
) -> 'Pattern':
"""
Args:
pattern: Pattern object to transform
library: A str->Pattern mapping, used instead of `pattern`. Must contain
`self.target`.
Returns:
A copy of the referenced Pattern which has been scaled, rotated, etc.
according to this `Ref`'s properties.
"""
if pattern is None:
if library is None:
raise PatternError('as_pattern() must be given a pattern or library.')
assert self.target is not None
pattern = library[self.target]
pattern = pattern.deepcopy()
if self.scale != 1:
@ -175,8 +146,8 @@ class Ref(
def get_bounds(
self,
pattern: 'Pattern',
*,
pattern: 'Pattern | None' = None,
library: Mapping[str, 'Pattern'] | None = None,
) -> NDArray[numpy.float64] | None:
"""
@ -190,20 +161,29 @@ class Ref(
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
if pattern is None and library is None:
raise PatternError('as_pattern() must be given a pattern or library.')
if pattern is None and self.target is None:
return None
if library is not None and self.target not in library:
raise PatternError(f'get_bounds() called on dangling reference to "{self.target}"')
if pattern is not None and pattern.is_empty():
if pattern.is_empty():
# no need to run as_pattern()
return None
return self.as_pattern(pattern=pattern, library=library).get_bounds(library)
return self.as_pattern(pattern=pattern).get_bounds(library) # TODO can just take pattern's bounds and then transform those!
def get_bounds_nonempty(
self,
pattern: 'Pattern',
*,
library: Mapping[str, 'Pattern'] | None = None,
) -> NDArray[numpy.float64]:
"""
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
"""
bounds = self.get_bounds(pattern, library=library)
assert bounds is not None
return bounds
def __repr__(self) -> str:
name = f'"{self.target}"' if self.target is not None else None
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
scale = f' d{self.scale:g}' if self.scale != 1 else ''
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
return f'<Ref {name} at {self.offset}{rotation}{scale}{mirrored}>'
return f'<Ref {self.offset}{rotation}{scale}{mirrored}>'

@ -9,7 +9,7 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, layer_t, annotations_t
from ..utils import is_scalar, annotations_t
class Arc(Shape):
@ -24,7 +24,7 @@ class Arc(Shape):
__slots__ = (
'_radii', '_angles', '_width', '_rotation',
# Inherited
'_offset', '_layer', '_repetition', '_annotations',
'_offset', '_repetition', '_annotations',
)
_radii: NDArray[numpy.float64]
@ -156,7 +156,6 @@ class Arc(Shape):
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -172,7 +171,6 @@ class Arc(Shape):
self._rotation = rotation
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
else:
self.radii = radii
self.angles = angles
@ -181,7 +179,6 @@ class Arc(Shape):
self.rotation = rotation
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
[self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
@ -241,7 +238,7 @@ class Arc(Shape):
ys = numpy.hstack((ys1, ys2))
xys = numpy.vstack((xs, ys)).T
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
return [poly]
def get_bounds(self) -> NDArray[numpy.float64]:
@ -352,13 +349,12 @@ class Arc(Shape):
rotation %= 2 * pi
width = self.width
return ((type(self), radii, angles, width / norm_value, self.layer),
return ((type(self), radii, angles, width / norm_value),
(self.offset, scale / norm_value, rotation, False),
lambda: Arc(
radii=radii * norm_value,
angles=angles,
width=width * norm_value,
layer=self.layer,
))
def get_cap_edges(self) -> NDArray[numpy.float64]:
@ -415,4 +411,4 @@ class Arc(Shape):
def __repr__(self) -> str:
angles = f'{numpy.rad2deg(self.angles)}'
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 ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, layer_t, annotations_t
from ..utils import is_scalar, annotations_t
class Circle(Shape):
@ -17,7 +17,7 @@ class Circle(Shape):
__slots__ = (
'_radius',
# Inherited
'_offset', '_layer', '_repetition', '_annotations',
'_offset', '_repetition', '_annotations',
)
_radius: float
@ -44,7 +44,6 @@ class Circle(Shape):
radius: float,
*,
offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -55,13 +54,11 @@ class Circle(Shape):
self._offset = offset
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
else:
self.radius = radius
self.offset = offset
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
memo = {} if memo is None else memo
@ -90,7 +87,7 @@ class Circle(Shape):
ys = numpy.sin(thetas) * self.radius
xys = numpy.vstack((xs, ys)).T
return [Polygon(xys, offset=self.offset, layer=self.layer)]
return [Polygon(xys, offset=self.offset)]
def get_bounds(self) -> NDArray[numpy.float64]:
return numpy.vstack((self.offset - self.radius,
@ -110,9 +107,9 @@ class Circle(Shape):
def normalized_form(self, norm_value) -> normalized_shape_tuple:
rotation = 0.0
magnitude = self.radius / norm_value
return ((type(self), self.layer),
return ((type(self),),
(self.offset, magnitude, rotation, False),
lambda: Circle(radius=norm_value, layer=self.layer))
lambda: Circle(radius=norm_value))
def __repr__(self) -> str:
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}>'
return f'<Circle o{self.offset} r{self.radius:g}>'

@ -9,7 +9,7 @@ from numpy.typing import ArrayLike, NDArray
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, layer_t, annotations_t
from ..utils import is_scalar, rotation_matrix_2d, annotations_t
class Ellipse(Shape):
@ -20,7 +20,7 @@ class Ellipse(Shape):
__slots__ = (
'_radii', '_rotation',
# Inherited
'_offset', '_layer', '_repetition', '_annotations',
'_offset', '_repetition', '_annotations',
)
_radii: NDArray[numpy.float64]
@ -91,7 +91,6 @@ class Ellipse(Shape):
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -104,14 +103,12 @@ class Ellipse(Shape):
self._rotation = rotation
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
else:
self.radii = radii
self.offset = offset
self.rotation = rotation
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
[self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse':
@ -152,7 +149,7 @@ class Ellipse(Shape):
ys = r1 * sin_th
xys = numpy.vstack((xs, ys)).T
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
return [poly]
def get_bounds(self) -> NDArray[numpy.float64]:
@ -183,10 +180,10 @@ class Ellipse(Shape):
radii = self.radii[::-1] / self.radius_y
scale = self.radius_y
angle = (self.rotation + pi / 2) % pi
return ((type(self), radii, self.layer),
return ((type(self), radii),
(self.offset, scale / norm_value, angle, False),
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
lambda: Ellipse(radii=radii * norm_value))
def __repr__(self) -> str:
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}>'
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
return f'<Ellipse o{self.offset} r{self.radii}{rotation}>'

@ -9,7 +9,7 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple, Polygon, Circle
from ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, layer_t
from ..utils import is_scalar, rotation_matrix_2d
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
@ -31,7 +31,7 @@ class Path(Shape):
__slots__ = (
'_vertices', '_width', '_cap', '_cap_extensions',
# Inherited
'_offset', '_layer', '_repetition', '_annotations',
'_offset', '_repetition', '_annotations',
)
_vertices: NDArray[numpy.float64]
_width: float
@ -154,7 +154,6 @@ class Path(Shape):
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -169,7 +168,6 @@ class Path(Shape):
self._offset = offset
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._width = width
self._cap = cap
self._cap_extensions = cap_extensions
@ -178,7 +176,6 @@ class Path(Shape):
self.offset = offset
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.width = width
self.cap = cap
self.cap_extensions = cap_extensions
@ -204,7 +201,6 @@ class Path(Shape):
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
) -> 'Path':
"""
Build a path by specifying the turn angles and travel distances
@ -224,7 +220,6 @@ class Path(Shape):
mirrored: Whether to mirror across the x or y axes. For example,
`mirrored=(True, False)` results in a reflection across the x-axis,
multiplying the path's y-coordinates by -1. Default `(False, False)`
layer: Layer, default `0`
Returns:
The resulting Path object
@ -238,8 +233,7 @@ class Path(Shape):
verts.append(verts[-1] + direction * distance)
return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions,
offset=offset, rotation=rotation, mirrored=mirrored,
layer=layer)
offset=offset, rotation=rotation, mirrored=mirrored)
def to_polygons(
self,
@ -254,7 +248,7 @@ class Path(Shape):
if self.width == 0:
verts = numpy.vstack((v, v[::-1]))
return [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
return [Polygon(offset=self.offset, vertices=verts)]
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
@ -305,12 +299,12 @@ class Path(Shape):
o1.append(v[-1] - perp[-1])
verts = numpy.vstack((o0, o1[::-1]))
polys = [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
polys = [Polygon(offset=self.offset, vertices=verts)]
if self.cap == PathCap.Circle:
#for vert in v: # not sure if every vertex, or just ends?
for vert in [v[0], v[-1]]:
circ = Circle(offset=vert, radius=self.width / 2, layer=self.layer)
circ = Circle(offset=vert, radius=self.width / 2)
polys += circ.to_polygons(num_vertices=num_vertices, max_arclen=max_arclen)
return polys
@ -370,13 +364,12 @@ class Path(Shape):
width0 = self.width / norm_value
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer),
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap),
(offset, scale / norm_value, rotation, False),
lambda: Path(
reordered_vertices * norm_value,
width=self.width * norm_value,
cap=self.cap,
layer=self.layer,
))
def clean_vertices(self) -> 'Path':
@ -422,4 +415,4 @@ class Path(Shape):
def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0)
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'

@ -8,7 +8,7 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple
from ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, layer_t
from ..utils import is_scalar, rotation_matrix_2d
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
@ -22,7 +22,7 @@ class Polygon(Shape):
__slots__ = (
'_vertices',
# Inherited
'_offset', '_layer', '_repetition', '_annotations',
'_offset', '_repetition', '_annotations',
)
_vertices: NDArray[numpy.float64]
@ -82,7 +82,6 @@ class Polygon(Shape):
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -94,13 +93,11 @@ class Polygon(Shape):
self._offset = offset
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
else:
self.vertices = vertices
self.offset = offset
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.rotate(rotation)
[self.mirror(a) for a, do in enumerate(mirrored) if do]
@ -118,7 +115,6 @@ class Polygon(Shape):
*,
rotation: float = 0.0,
offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None,
) -> 'Polygon':
"""
@ -128,7 +124,6 @@ class Polygon(Shape):
side_length: Length of one side
rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)`
layer: Layer, default `0`
repetition: `Repetition` object, default `None`
Returns:
@ -139,7 +134,7 @@ class Polygon(Shape):
[+1, +1],
[+1, -1]], dtype=float)
vertices = 0.5 * side_length * norm_square
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
poly = Polygon(vertices, offset=offset, repetition=repetition)
poly.rotate(rotation)
return poly
@ -150,7 +145,6 @@ class Polygon(Shape):
*,
rotation: float = 0,
offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
repetition: Repetition | None = None,
) -> 'Polygon':
"""
@ -161,7 +155,6 @@ class Polygon(Shape):
ly: Length along y (before rotation)
rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)`
layer: Layer, default `0`
repetition: `Repetition` object, default `None`
Returns:
@ -171,7 +164,7 @@ class Polygon(Shape):
[-lx, +ly],
[+lx, +ly],
[+lx, -ly]], dtype=float)
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
poly = Polygon(vertices, offset=offset, repetition=repetition)
poly.rotate(rotation)
return poly
@ -186,7 +179,6 @@ class Polygon(Shape):
yctr: float | None = None,
ymax: float | None = None,
ly: float | None = None,
layer: layer_t = 0,
repetition: Repetition | None = None,
) -> 'Polygon':
"""
@ -204,7 +196,6 @@ class Polygon(Shape):
yctr: Center y coordinate
ymax: Maximum y coordinate
ly: Length along y direction
layer: Layer, default `0`
repetition: `Repetition` object, default `None`
Returns:
@ -270,7 +261,7 @@ class Polygon(Shape):
else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), layer=layer, repetition=repetition)
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), repetition=repetition)
return poly
@staticmethod
@ -281,7 +272,6 @@ class Polygon(Shape):
regular: bool = True,
center: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
layer: layer_t = 0,
repetition: Repetition | None = None,
) -> 'Polygon':
"""
@ -300,7 +290,6 @@ class Polygon(Shape):
rotation: Rotation counterclockwise, in radians.
`0` results in four axis-aligned sides (the long sides of the
irregular octagon).
layer: Layer, default `0`
repetition: `Repetition` object, default `None`
Returns:
@ -327,7 +316,7 @@ class Polygon(Shape):
side_length = 2 * inner_radius / s
vertices = 0.5 * side_length * norm_oct
poly = Polygon(vertices, offset=center, layer=layer, repetition=repetition)
poly = Polygon(vertices, offset=center, repetition=repetition)
poly.rotate(rotation)
return poly
@ -378,9 +367,9 @@ class Polygon(Shape):
# TODO: normalize mirroring?
return ((type(self), reordered_vertices.data.tobytes(), self.layer),
return ((type(self), reordered_vertices.data.tobytes()),
(offset, scale / norm_value, rotation, False),
lambda: Polygon(reordered_vertices * norm_value, layer=self.layer))
lambda: Polygon(reordered_vertices * norm_value))
def clean_vertices(self) -> 'Polygon':
"""
@ -414,4 +403,4 @@ class Polygon(Shape):
def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0)
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}>'
return f'<Polygon centroid {centroid} v{len(self.vertices)}>'

@ -5,9 +5,8 @@ import numpy
from numpy.typing import NDArray, ArrayLike
from ..traits import (
Rotatable, Mirrorable, Copyable, Scalable,
PositionableImpl, LayerableImpl,
PivotableImpl, RepeatableImpl, AnnotatableImpl,
Rotatable, Mirrorable, Copyable, Scalable, Bounded,
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
)
if TYPE_CHECKING:
@ -26,7 +25,7 @@ normalized_shape_tuple = tuple[
DEFAULT_POLY_NUM_VERTICES = 24
class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Scalable,
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, Bounded,
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
"""
Class specifying functions common to all shapes.
@ -194,10 +193,7 @@ class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Sc
vertex_lists.append(vlist)
polygon_contours.append(numpy.vstack(vertex_lists))
manhattan_polygons = [
Polygon(vertices=contour, layer=self.layer)
for contour in polygon_contours
]
manhattan_polygons = [Polygon(vertices=contour) for contour in polygon_contours]
return manhattan_polygons
@ -292,9 +288,6 @@ class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Sc
vertices = numpy.hstack((grx[snapped_contour[:, None, 0] + offset_i[0]],
gry[snapped_contour[:, None, 1] + offset_i[1]]))
manhattan_polygons.append(Polygon(
vertices=vertices,
layer=self.layer,
))
manhattan_polygons.append(Polygon(vertices=vertices))
return manhattan_polygons

@ -9,7 +9,7 @@ from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError
from ..repetition import Repetition
from ..traits import RotatableImpl
from ..utils import is_scalar, get_bit, normalize_mirror, layer_t
from ..utils import is_scalar, get_bit, normalize_mirror
from ..utils import annotations_t
# Loaded on use:
@ -25,7 +25,7 @@ class Text(RotatableImpl, Shape):
__slots__ = (
'_string', '_height', '_mirrored', 'font_path',
# Inherited
'_offset', '_layer', '_repetition', '_annotations', '_rotation',
'_offset', '_repetition', '_annotations', '_rotation',
)
_string: str
@ -73,7 +73,6 @@ class Text(RotatableImpl, Shape):
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
mirrored: ArrayLike = (False, False),
layer: layer_t = 0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -82,7 +81,6 @@ class Text(RotatableImpl, Shape):
assert isinstance(offset, numpy.ndarray)
assert isinstance(mirrored, numpy.ndarray)
self._offset = offset
self._layer = layer
self._string = string
self._height = height
self._rotation = rotation
@ -91,7 +89,6 @@ class Text(RotatableImpl, Shape):
self._annotations = annotations if annotations is not None else {}
else:
self.offset = offset
self.layer = layer
self.string = string
self.height = height
self.rotation = rotation
@ -120,7 +117,7 @@ class Text(RotatableImpl, Shape):
# Move these polygons to the right of the previous letter
for xys in raw_polys:
poly = Polygon(xys, layer=self.layer)
poly = Polygon(xys)
poly.mirror2d(self.mirrored)
poly.scale_by(self.height)
poly.offset = self.offset + [total_advance, 0]
@ -144,7 +141,7 @@ class Text(RotatableImpl, Shape):
mirror_x, rotation = normalize_mirror(self.mirrored)
rotation += self.rotation
rotation %= 2 * pi
return ((type(self), self.string, self.font_path, self.layer),
return ((type(self), self.string, self.font_path),
(self.offset, self.height / norm_value, rotation, mirror_x),
lambda: Text(
string=self.string,
@ -152,7 +149,6 @@ class Text(RotatableImpl, Shape):
font_path=self.font_path,
rotation=rotation,
mirrored=(mirror_x, False),
layer=self.layer,
))
def get_bounds(self) -> NDArray[numpy.float64]:
@ -256,6 +252,6 @@ def get_char_as_polygons(
return polygons, advance
def __repr__(self) -> str:
rotation = f'{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 ''
return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}>'
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'

@ -1,7 +1,7 @@
"""
Traits (mixins) and default implementations
"""
from .positionable import Positionable, PositionableImpl
from .positionable import Positionable, PositionableImpl, Bounded
from .layerable import Layerable, LayerableImpl
from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
from .repeatable import Repeatable, RepeatableImpl

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

@ -8,7 +8,6 @@ from numpy.typing import NDArray, ArrayLike
from ..error import MasqueError
from ..pattern import Pattern
from ..ref import Ref
def maxrects_bssf(
@ -160,8 +159,8 @@ def pack_patterns(
locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects)
pat = Pattern()
pat.refs = [Ref(pp, offset=oo + loc)
for pp, oo, loc in zip(patterns, offsets, locations)]
for pp, oo, loc in zip(patterns, offsets, locations):
pat.ref(pp, offset=oo + loc)
rejects = [patterns[ii] for ii in reject_inds]
return pat, rejects

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

Loading…
Cancel
Save