move to dicty layers and targets
This commit is contained in:
parent
dc4f24a028
commit
88d123af71
@ -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' a°{numpy.rad2deg(self.angles)}'
|
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||||
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||||
|
@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
from . 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' r°{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
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…
Reference in New Issue
Block a user