Compare commits

...

10 Commits

Author SHA1 Message Date
jan c2d7290935 Make DeviceLibrary code more similar to Library 1 year ago
jan b6f1af6e09 Remove primary/secondary pattern split
Replacement TBD -- likely some way to rename patterns when merging.
1 year ago
jan bff9940518 disable deepcopy for Library since it doesn't work 1 year ago
jan 9c20960e62 round().astype() -> rint(...) 1 year ago
jan 599723e470 whitespace 1 year ago
jan 4d58516049 Remove object locking/unlocking.
- It was *slow*. Often >50% of runtime for large designs.
- It didn't catch all corner cases. True immutability would require
  language-level support.
- (minor) It doesn't play nice with type checking via mypy.
1 year ago
Jan Petykiewicz fff20b3da9 Avoid generating a container if only a single port is passed 1 year ago
Jan Petykiewicz 4bae737630 allow bounds to be passed as args 1 year ago
Jan Petykiewicz 9891ba9e47 allow passing a single Tool to be used as the default 1 year ago
Jan Petykiewicz df320e80cc Add functionality for building paths (single use wires/waveguides/etc) 1 year ago

@ -24,11 +24,10 @@
metaclass is used to auto-generate slots based on superclass type annotations. metaclass is used to auto-generate slots based on superclass type annotations.
- File I/O submodules are imported by `masque.file` to avoid creating hard dependencies on - File I/O submodules are imported by `masque.file` to avoid creating hard dependencies on
external file-format reader/writers external file-format reader/writers
- Pattern locking/unlocking is quite slow for large hierarchies.
""" """
from .error import PatternError, PatternLockedError from .error import PatternError
from .shapes import Shape from .shapes import Shape
from .label import Label from .label import Label
from .subpattern import SubPattern from .subpattern import SubPattern

@ -1,2 +1,3 @@
from .devices import Port, Device from .devices import Port, Device
from .utils import ell from .utils import ell
from .tools import Tool

@ -15,6 +15,8 @@ from ..subpattern import SubPattern
from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
from ..utils import AutoSlots, rotation_matrix_2d from ..utils import AutoSlots, rotation_matrix_2d
from ..error import DeviceError from ..error import DeviceError
from .tools import Tool
from .utils import ell
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -157,7 +159,7 @@ class Device(Copyable, Mirrorable):
renamed to 'gnd' so that further routing can use this signal or net name renamed to 'gnd' so that further routing can use this signal or net name
rather than the port name on the original `pad` device. rather than the port name on the original `pad` device.
""" """
__slots__ = ('pattern', 'ports', '_dead') __slots__ = ('pattern', 'ports', 'tools', '_dead')
pattern: Pattern pattern: Pattern
""" Layout of this device """ """ Layout of this device """
@ -165,6 +167,12 @@ class Device(Copyable, Mirrorable):
ports: Dict[str, Port] ports: Dict[str, Port]
""" Uniquely-named ports which can be used to snap to other Device instances""" """ Uniquely-named ports which can be used to snap to other Device instances"""
tools: Dict[Optional[str], Tool]
"""
Tool objects are used to dynamically generate new single-use Devices
(e.g wires or waveguides) to be plugged into this device.
"""
_dead: bool _dead: bool
""" If True, plug()/place() are skipped (for debugging)""" """ If True, plug()/place() are skipped (for debugging)"""
@ -173,6 +181,7 @@ class Device(Copyable, Mirrorable):
pattern: Optional[Pattern] = None, pattern: Optional[Pattern] = None,
ports: Optional[Dict[str, Port]] = None, ports: Optional[Dict[str, Port]] = None,
*, *,
tools: Union[None, Tool, Dict[Optional[str], Tool]] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
""" """
@ -198,6 +207,13 @@ class Device(Copyable, Mirrorable):
else: else:
self.ports = copy.deepcopy(ports) self.ports = copy.deepcopy(ports)
if tools is None:
self.tools = {}
elif isinstance(tools, Tool):
self.tools = {None: tools}
else:
self.tools = tools
self._dead = False self._dead = False
@overload @overload
@ -205,7 +221,7 @@ class Device(Copyable, Mirrorable):
pass pass
@overload @overload
def __getitem__(self, key: Union[List[str], Tuple[str], KeysView[str], ValuesView[str]]) -> Dict[str, Port]: def __getitem__(self, key: Union[List[str], Tuple[str, ...], KeysView[str], ValuesView[str]]) -> Dict[str, Port]:
pass pass
def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, Dict[str, Port]]: def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, Dict[str, Port]]:
@ -333,7 +349,7 @@ class Device(Copyable, Mirrorable):
""" """
pat = Pattern(name) pat = Pattern(name)
pat.addsp(self.pattern) pat.addsp(self.pattern)
new = Device(pat, ports=self.ports) new = Device(pat, ports=self.ports, tools=self.tools)
return new return new
def as_interface( def as_interface(
@ -408,7 +424,7 @@ class Device(Copyable, Mirrorable):
if duplicates: if duplicates:
raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}')
new = Device(name=name, ports={**ports_in, **ports_out}) new = Device(name=name, ports={**ports_in, **ports_out}, tools=self.tools)
return new return new
def plug( def plug(
@ -752,6 +768,118 @@ class Device(Copyable, Mirrorable):
s += ']>' s += ']>'
return s return s
def retool(
self: D,
tool: Tool,
keys: Union[Optional[str], Sequence[Optional[str]]] = None,
) -> D:
if keys is None or isinstance(keys, str):
self.tools[keys] = tool
else:
for key in keys:
self.tools[key] = tool
return self
def path(
self: D,
portspec: str,
ccw: Optional[bool],
length: float,
*,
tool_port_names: Sequence[str] = ('A', 'B'),
**kwargs,
) -> D:
if self._dead:
logger.error('Skipping path() since device is dead')
return self
tool = self.tools.get(portspec, self.tools[None])
in_ptype = self.ports[portspec].ptype
dev = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
return self.plug(dev, {portspec: tool_port_names[0]})
def path_to(
self: D,
portspec: str,
ccw: Optional[bool],
position: float,
*,
tool_port_names: Sequence[str] = ('A', 'B'),
**kwargs,
) -> D:
if self._dead:
logger.error('Skipping path_to() since device is dead')
return self
port = self.ports[portspec]
x, y = port.offset
if port.rotation is None:
raise DeviceError(f'Port {portspec} has no rotation and cannot be used for path_to()')
if not numpy.isclose(port.rotation % (pi / 2), 0):
raise DeviceError('path_to was asked to route from non-manhattan port')
is_horizontal = numpy.isclose(port.rotation % pi, 0)
if is_horizontal:
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x):
raise DeviceError(f'path_to routing to behind source port: x={x:g} to {position:g}')
length = numpy.abs(position - x)
else:
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y):
raise DeviceError(f'path_to routing to behind source port: y={y:g} to {position:g}')
length = numpy.abs(position - y)
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, **kwargs)
def busL(
self: D,
portspec: Union[str, Sequence[str]],
ccw: Optional[bool],
*,
spacing: Optional[Union[float, ArrayLike]] = None,
set_rotation: Optional[float] = None,
tool_port_names: Sequence[str] = ('A', 'B'),
container_name: str = '_busL',
force_container: bool = False,
**kwargs,
) -> D:
if self._dead:
logger.error('Skipping busL() since device is dead')
return self
bound_types = set()
if 'bound_type' in kwargs:
bound_types.add(kwargs['bound_type'])
bound = kwargs['bound']
for bt in ('emin', 'emax', 'pmin', 'pmax', 'min_past_furthest'):
if bt in kwargs:
bound_types.add(bt)
bound = kwargs[bt]
if not bound_types:
raise DeviceError('No bound type specified for busL')
elif len(bound_types) > 1:
raise DeviceError(f'Too many bound types specified for busL: {bound_types}')
bound_type = tuple(bound_types)[0]
if isinstance(portspec, str):
portspec = [portspec]
ports = self[tuple(portspec)]
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
if len(ports) == 1 and not force_container:
# Not a bus, so having a container just adds noise to the layout
port_name = tuple(portspec)[0]
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
else:
dev = Device(name='', ports=ports, tools=self.tools).as_interface(container_name)
for name, length in extensions.items():
dev.path(name, ccw, length, tool_port_names=tool_port_names)
return self.plug(dev, {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
# TODO def path_join() and def bus_join()?
def rotate_offsets_around( def rotate_offsets_around(
offsets: NDArray[numpy.float64], offsets: NDArray[numpy.float64],

@ -0,0 +1,22 @@
"""
Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides)
"""
from typing import TYPE_CHECKING, Optional, Sequence
if TYPE_CHECKING:
from .devices import Device
class Tool:
def path(
self,
ccw: Optional[bool],
length: float,
*,
in_ptype: Optional[str] = None,
out_ptype: Optional[str] = None,
port_names: Sequence[str] = ('A', 'B'),
**kwargs,
) -> 'Device':
raise NotImplementedError(f'path() not implemented for {type(self)}')

@ -11,13 +11,6 @@ class PatternError(MasqueError):
""" """
pass pass
class PatternLockedError(PatternError):
"""
Exception raised when trying to modify a locked pattern
"""
def __init__(self):
PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape')
class LibraryError(MasqueError): class LibraryError(MasqueError):
""" """

@ -63,7 +63,7 @@ def write(
patterns: A Pattern or list of patterns to write to the stream. patterns: A Pattern or list of patterns to write to the stream.
stream: Stream object to write to. stream: Stream object to write to.
modify_original: If `True`, the original pattern is modified as part of the writing modify_original: If `True`, the original pattern is modified as part of the writing
process. Otherwise, a copy is made and `deepunlock()`-ed. process. Otherwise, a copy is made.
Default `False`. Default `False`.
disambiguate_func: Function which takes a list of patterns and alters them disambiguate_func: Function which takes a list of patterns and alters them
to make their names valid and unique. Default is `disambiguate_pattern_names`. to make their names valid and unique. Default is `disambiguate_pattern_names`.
@ -75,7 +75,7 @@ def write(
assert(disambiguate_func is not None) assert(disambiguate_func is not None)
if not modify_originals: if not modify_originals:
pattern = pattern.deepcopy().deepunlock() pattern = pattern.deepcopy()
# Get a dict of id(pattern) -> pattern # Get a dict of id(pattern) -> pattern
patterns_by_id = pattern.referenced_patterns_by_id() patterns_by_id = pattern.referenced_patterns_by_id()
@ -267,10 +267,12 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
} }
if 'column_count' in attr: if 'column_count' in attr:
args['repetition'] = Grid(a_vector=(attr['column_spacing'], 0), args['repetition'] = Grid(
b_vector=(0, attr['row_spacing']), a_vector=(attr['column_spacing'], 0),
a_count=attr['column_count'], b_vector=(0, attr['row_spacing']),
b_count=attr['row_count']) a_count=attr['column_count'],
b_count=attr['row_count'],
)
pat.subpatterns.append(SubPattern(**args)) pat.subpatterns.append(SubPattern(**args))
else: else:
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).') logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')

@ -53,6 +53,10 @@ path_cap_map = {
} }
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
return numpy.rint(val, dtype=numpy.int32, casting='unsafe')
def write( def write(
patterns: Union[Pattern, Sequence[Pattern]], patterns: Union[Pattern, Sequence[Pattern]],
stream: BinaryIO, stream: BinaryIO,
@ -94,7 +98,7 @@ def write(
library_name: Library name written into the GDSII file. library_name: Library name written into the GDSII file.
Default 'masque-klamath'. Default 'masque-klamath'.
modify_originals: If `True`, the original pattern is modified as part of the writing modify_originals: If `True`, the original pattern is modified as part of the writing
process. Otherwise, a copy is made and `deepunlock()`-ed. process. Otherwise, a copy is made.
Default `False`. Default `False`.
disambiguate_func: Function which takes a list of patterns and alters them disambiguate_func: Function which takes a list of patterns and alters them
to make their names valid and unique. Default is `disambiguate_pattern_names`, which to make their names valid and unique. Default is `disambiguate_pattern_names`, which
@ -109,14 +113,16 @@ def write(
assert(disambiguate_func is not None) # placate mypy assert(disambiguate_func is not None) # placate mypy
if not modify_originals: if not modify_originals:
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] patterns = copy.deepcopy(patterns)
patterns = [p.wrap_repeated_shapes() for p in patterns] patterns = [p.wrap_repeated_shapes() for p in patterns]
# Create library # Create library
header = klamath.library.FileHeader(name=library_name.encode('ASCII'), header = klamath.library.FileHeader(
user_units_per_db_unit=logical_units_per_unit, name=library_name.encode('ASCII'),
meters_per_db_unit=meters_per_unit) user_units_per_db_unit=logical_units_per_unit,
meters_per_db_unit=meters_per_unit,
)
header.write(stream) header.write(stream)
# Get a dict of id(pattern) -> pattern # Get a dict of id(pattern) -> pattern
@ -241,10 +247,11 @@ def _read_header(stream: BinaryIO) -> Dict[str, Any]:
""" """
header = klamath.library.FileHeader.read(stream) header = klamath.library.FileHeader.read(stream)
library_info = {'name': header.name.decode('ASCII'), library_info = {
'meters_per_unit': header.meters_per_db_unit, 'name': header.name.decode('ASCII'),
'logical_units_per_unit': header.user_units_per_db_unit, 'meters_per_unit': header.meters_per_db_unit,
} 'logical_units_per_unit': header.user_units_per_db_unit,
}
return library_info return library_info
@ -276,10 +283,12 @@ def read_elements(
path = _gpath_to_mpath(element, raw_mode) path = _gpath_to_mpath(element, raw_mode)
pat.shapes.append(path) pat.shapes.append(path)
elif isinstance(element, klamath.elements.Text): elif isinstance(element, klamath.elements.Text):
label = Label(offset=element.xy.astype(float), label = Label(
layer=element.layer, offset=element.xy.astype(float),
string=element.string.decode('ASCII'), layer=element.layer,
annotations=_properties_to_annotations(element.properties)) string=element.string.decode('ASCII'),
annotations=_properties_to_annotations(element.properties),
)
pat.labels.append(label) pat.labels.append(label)
elif isinstance(element, klamath.elements.Reference): elif isinstance(element, klamath.elements.Reference):
pat.subpatterns.append(_ref_to_subpat(element)) pat.subpatterns.append(_ref_to_subpat(element))
@ -314,16 +323,22 @@ def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
a_count, b_count = ref.colrow a_count, b_count = ref.colrow
a_vector = (xy[1] - offset) / a_count a_vector = (xy[1] - offset) / a_count
b_vector = (xy[2] - offset) / b_count b_vector = (xy[2] - offset) / b_count
repetition = Grid(a_vector=a_vector, b_vector=b_vector, repetition = Grid(
a_count=a_count, b_count=b_count) a_vector=a_vector,
b_vector=b_vector,
a_count=a_count,
b_count=b_count,
)
subpat = SubPattern(pattern=None, subpat = SubPattern(
offset=offset, pattern=None,
rotation=numpy.deg2rad(ref.angle_deg), offset=offset,
scale=ref.mag, rotation=numpy.deg2rad(ref.angle_deg),
mirrored=(ref.invert_y, False), scale=ref.mag,
annotations=_properties_to_annotations(ref.properties), mirrored=(ref.invert_y, False),
repetition=repetition) annotations=_properties_to_annotations(ref.properties),
repetition=repetition,
)
subpat.identifier = (ref.struct_name.decode('ASCII'),) subpat.identifier = (ref.struct_name.decode('ASCII'),)
return subpat return subpat
@ -334,26 +349,28 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
else: else:
raise PatternError(f'Unrecognized path type: {gpath.path_type}') raise PatternError(f'Unrecognized path type: {gpath.path_type}')
mpath = Path(vertices=gpath.xy.astype(float), mpath = Path(
layer=gpath.layer, vertices=gpath.xy.astype(float),
width=gpath.width, layer=gpath.layer,
cap=cap, width=gpath.width,
offset=numpy.zeros(2), cap=cap,
annotations=_properties_to_annotations(gpath.properties), offset=numpy.zeros(2),
raw=raw_mode, annotations=_properties_to_annotations(gpath.properties),
) raw=raw_mode,
)
if cap == Path.Cap.SquareCustom: if cap == Path.Cap.SquareCustom:
mpath.cap_extensions = gpath.extension mpath.cap_extensions = gpath.extension
return mpath return mpath
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon: def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon:
return Polygon(vertices=boundary.xy[:-1].astype(float), return Polygon(
layer=boundary.layer, vertices=boundary.xy[:-1].astype(float),
offset=numpy.zeros(2), layer=boundary.layer,
annotations=_properties_to_annotations(boundary.properties), offset=numpy.zeros(2),
raw=raw_mode, annotations=_properties_to_annotations(boundary.properties),
) raw=raw_mode,
)
def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.Reference]: def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.Reference]:
@ -377,31 +394,37 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.
rep.a_vector * rep.a_count, rep.a_vector * rep.a_count,
b_vector * b_count, b_vector * b_count,
] ]
aref = klamath.library.Reference(struct_name=encoded_name, aref = klamath.library.Reference(
xy=numpy.round(xy).astype(int), struct_name=encoded_name,
colrow=(numpy.round(rep.a_count), numpy.round(rep.b_count)), xy=rint_cast(xy),
angle_deg=angle_deg, colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
invert_y=mirror_across_x, angle_deg=angle_deg,
mag=subpat.scale, invert_y=mirror_across_x,
properties=properties) mag=subpat.scale,
properties=properties,
)
refs.append(aref) refs.append(aref)
elif rep is None: elif rep is None:
ref = klamath.library.Reference(struct_name=encoded_name, ref = klamath.library.Reference(
xy=numpy.round([subpat.offset]).astype(int), struct_name=encoded_name,
colrow=None, xy=rint_cast([subpat.offset]),
angle_deg=angle_deg, colrow=None,
invert_y=mirror_across_x, angle_deg=angle_deg,
mag=subpat.scale, invert_y=mirror_across_x,
properties=properties) mag=subpat.scale,
properties=properties,
)
refs.append(ref) refs.append(ref)
else: else:
new_srefs = [klamath.library.Reference(struct_name=encoded_name, new_srefs = [klamath.library.Reference(
xy=numpy.round([subpat.offset + dd]).astype(int), struct_name=encoded_name,
colrow=None, xy=rint_cast([subpat.offset + dd]),
angle_deg=angle_deg, colrow=None,
invert_y=mirror_across_x, angle_deg=angle_deg,
mag=subpat.scale, invert_y=mirror_across_x,
properties=properties) mag=subpat.scale,
properties=properties,
)
for dd in rep.displacements] for dd in rep.displacements]
refs += new_srefs refs += new_srefs
return refs return refs
@ -443,8 +466,8 @@ def _shapes_to_elements(
layer, data_type = _mlayer2gds(shape.layer) 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 = numpy.round(shape.vertices + shape.offset).astype(int) xy = rint_cast(shape.vertices + shape.offset)
width = numpy.round(shape.width).astype(int) 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]
@ -453,30 +476,36 @@ def _shapes_to_elements(
else: else:
extension = (0, 0) extension = (0, 0)
path = klamath.elements.Path(layer=(layer, data_type), path = klamath.elements.Path(
xy=xy, layer=(layer, data_type),
path_type=path_type, xy=xy,
width=width, path_type=path_type,
extension=extension, width=width,
properties=properties) extension=extension,
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) 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]
boundary = klamath.elements.Boundary(layer=(layer, data_type), boundary = klamath.elements.Boundary(
xy=xy_closed, layer=(layer, data_type),
properties=properties) xy=xy_closed,
properties=properties,
)
elements.append(boundary) elements.append(boundary)
else: else:
for polygon in shape.to_polygons(): 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]
boundary = klamath.elements.Boundary(layer=(layer, data_type), boundary = klamath.elements.Boundary(
xy=xy_closed, layer=(layer, data_type),
properties=properties) xy=xy_closed,
properties=properties,
)
elements.append(boundary) elements.append(boundary)
return elements return elements
@ -486,17 +515,19 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
for label in labels: for label in labels:
properties = _annotations_to_properties(label.annotations, 128) properties = _annotations_to_properties(label.annotations, 128)
layer, text_type = _mlayer2gds(label.layer) layer, text_type = _mlayer2gds(label.layer)
xy = numpy.round([label.offset]).astype(int) xy = rint_cast([label.offset])
text = klamath.elements.Text(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

@ -22,6 +22,7 @@ import pathlib
import gzip import gzip
import numpy import numpy
from numpy.typing import ArrayLike, NDArray
import fatamorgana import fatamorgana
import fatamorgana.records as fatrec import fatamorgana.records as fatrec
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
@ -47,6 +48,10 @@ path_cap_map = {
#TODO implement more shape types? #TODO implement more shape types?
def rint_cast(val: ArrayLike) -> NDArray[numpy.int64]:
return numpy.rint(val, dtype=numpy.int64, casting='unsafe')
def build( def build(
patterns: Union[Pattern, Sequence[Pattern]], patterns: Union[Pattern, Sequence[Pattern]],
units_per_micron: int, units_per_micron: int,
@ -87,7 +92,7 @@ def build(
`fatamorgana.records.LayerName` entries. `fatamorgana.records.LayerName` entries.
Default is an empty dict (no names provided). Default is an empty dict (no names provided).
modify_originals: If `True`, the original pattern is modified as part of the writing modify_originals: If `True`, the original pattern is modified as part of the writing
process. Otherwise, a copy is made and `deepunlock()`-ed. process. Otherwise, a copy is made.
Default `False`. Default `False`.
disambiguate_func: Function which takes a list of patterns and alters them disambiguate_func: Function which takes a list of patterns and alters them
to make their names valid and unique. Default is `disambiguate_pattern_names`. to make their names valid and unique. Default is `disambiguate_pattern_names`.
@ -109,7 +114,7 @@ def build(
annotations = {} annotations = {}
if not modify_originals: if not modify_originals:
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] patterns = copy.deepcopy(patterns)
# Create library # Create library
lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None) lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None)
@ -285,11 +290,13 @@ def read(
if isinstance(element, fatrec.Polygon): if isinstance(element, fatrec.Polygon):
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)
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
poly = Polygon(vertices=vertices, poly = Polygon(
layer=element.get_layer_tuple(), vertices=vertices,
offset=element.get_xy(), layer=element.get_layer_tuple(),
annotations=annotations, offset=element.get_xy(),
repetition=repetition) annotations=annotations,
repetition=repetition,
)
pat.shapes.append(poly) pat.shapes.append(poly)
@ -308,14 +315,16 @@ def read(
element.get_extension_end()[1])) element.get_extension_end()[1]))
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
path = Path(vertices=vertices, path = Path(
layer=element.get_layer_tuple(), vertices=vertices,
offset=element.get_xy(), layer=element.get_layer_tuple(),
repetition=repetition, offset=element.get_xy(),
annotations=annotations, repetition=repetition,
width=element.get_half_width() * 2, annotations=annotations,
cap=cap, width=element.get_half_width() * 2,
**path_args) cap=cap,
**path_args,
)
pat.shapes.append(path) pat.shapes.append(path)
@ -323,12 +332,13 @@ def read(
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(layer=element.get_layer_tuple(), rect = Polygon(
offset=element.get_xy(), layer=element.get_layer_tuple(),
repetition=repetition, offset=element.get_xy(),
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), repetition=repetition,
annotations=annotations, vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
) annotations=annotations,
)
pat.shapes.append(rect) pat.shapes.append(rect)
elif isinstance(element, fatrec.Trapezoid): elif isinstance(element, fatrec.Trapezoid):
@ -357,12 +367,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(layer=element.get_layer_tuple(), trapz = Polygon(
offset=element.get_xy(), layer=element.get_layer_tuple(),
repetition=repetition, offset=element.get_xy(),
vertices=vertices, repetition=repetition,
annotations=annotations, vertices=vertices,
) annotations=annotations,
)
pat.shapes.append(trapz) pat.shapes.append(trapz)
elif isinstance(element, fatrec.CTrapezoid): elif isinstance(element, fatrec.CTrapezoid):
@ -412,21 +423,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(layer=element.get_layer_tuple(), ctrapz = Polygon(
offset=element.get_xy(), layer=element.get_layer_tuple(),
repetition=repetition, offset=element.get_xy(),
vertices=vertices, repetition=repetition,
annotations=annotations, vertices=vertices,
) annotations=annotations,
)
pat.shapes.append(ctrapz) 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)
circle = Circle(layer=element.get_layer_tuple(), circle = Circle(
offset=element.get_xy(), layer=element.get_layer_tuple(),
repetition=repetition, offset=element.get_xy(),
annotations=annotations, repetition=repetition,
radius=float(element.get_radius())) annotations=annotations,
radius=float(element.get_radius()),
)
pat.shapes.append(circle) pat.shapes.append(circle)
elif isinstance(element, fatrec.Text): elif isinstance(element, fatrec.Text):
@ -436,11 +450,13 @@ 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(layer=element.get_layer_tuple(), label = Label(
offset=element.get_xy(), layer=element.get_layer_tuple(),
repetition=repetition, offset=element.get_xy(),
annotations=annotations, repetition=repetition,
string=string) annotations=annotations,
string=string,
)
pat.labels.append(label) pat.labels.append(label)
else: else:
@ -499,14 +515,16 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
rotation = 0 rotation = 0
else: else:
rotation = numpy.deg2rad(float(placement.angle)) rotation = numpy.deg2rad(float(placement.angle))
subpat = SubPattern(offset=xy, subpat = SubPattern(
pattern=None, offset=xy,
mirrored=(placement.flip, False), pattern=None,
rotation=rotation, mirrored=(placement.flip, False),
scale=float(mag), rotation=rotation,
identifier=(name,), scale=float(mag),
repetition=repetition_fata2masq(placement.repetition), identifier=(name,),
annotations=annotations) repetition=repetition_fata2masq(placement.repetition),
annotations=annotations,
)
return subpat return subpat
@ -522,7 +540,7 @@ def _subpatterns_to_placements(
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
frep, rep_offset = repetition_masq2fata(subpat.repetition) frep, rep_offset = repetition_masq2fata(subpat.repetition)
offset = numpy.round(subpat.offset + rep_offset).astype(int) offset = rint_cast(subpat.offset + rep_offset)
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
ref = fatrec.Placement( ref = fatrec.Placement(
name=subpat.pattern.name, name=subpat.pattern.name,
@ -532,7 +550,8 @@ def _subpatterns_to_placements(
properties=annotations_to_properties(subpat.annotations), properties=annotations_to_properties(subpat.annotations),
x=offset[0], x=offset[0],
y=offset[1], y=offset[1],
repetition=frep) repetition=frep,
)
refs.append(ref) refs.append(ref)
return refs return refs
@ -549,46 +568,51 @@ def _shapes_to_elements(
repetition, rep_offset = repetition_masq2fata(shape.repetition) repetition, rep_offset = repetition_masq2fata(shape.repetition)
properties = annotations_to_properties(shape.annotations) properties = annotations_to_properties(shape.annotations)
if isinstance(shape, Circle): if isinstance(shape, Circle):
offset = numpy.round(shape.offset + rep_offset).astype(int) offset = rint_cast(shape.offset + rep_offset)
radius = numpy.round(shape.radius).astype(int) radius = rint_cast(shape.radius)
circle = fatrec.Circle(layer=layer, circle = fatrec.Circle(
datatype=datatype, layer=layer,
radius=radius, datatype=datatype,
x=offset[0], radius=radius,
y=offset[1], x=offset[0],
properties=properties, y=offset[1],
repetition=repetition) properties=properties,
repetition=repetition,
)
elements.append(circle) elements.append(circle)
elif isinstance(shape, Path): elif isinstance(shape, Path):
xy = numpy.round(shape.offset + shape.vertices[0] + rep_offset).astype(int) xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
deltas = numpy.round(numpy.diff(shape.vertices, axis=0)).astype(int) deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
half_width = numpy.round(shape.width / 2).astype(int) 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 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_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) extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
path = fatrec.Path(layer=layer, path = fatrec.Path(
datatype=datatype, layer=layer,
point_list=deltas, datatype=datatype,
half_width=half_width, point_list=deltas,
x=xy[0], half_width=half_width,
y=xy[1], x=xy[0],
extension_start=extension_start, # TODO implement multiple cap types? y=xy[1],
extension_end=extension_end, extension_start=extension_start, # TODO implement multiple cap types?
properties=properties, extension_end=extension_end,
repetition=repetition, properties=properties,
) repetition=repetition,
)
elements.append(path) elements.append(path)
else: else:
for polygon in shape.to_polygons(): for polygon in shape.to_polygons():
xy = numpy.round(polygon.offset + polygon.vertices[0] + rep_offset).astype(int) xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
points = numpy.round(numpy.diff(polygon.vertices, axis=0)).astype(int) points = rint_cast(numpy.diff(polygon.vertices, axis=0))
elements.append(fatrec.Polygon(layer=layer, elements.append(fatrec.Polygon(
datatype=datatype, layer=layer,
x=xy[0], datatype=datatype,
y=xy[1], x=xy[0],
point_list=points, y=xy[1],
properties=properties, point_list=points,
repetition=repetition)) properties=properties,
repetition=repetition,
))
return elements return elements
@ -600,15 +624,17 @@ def _labels_to_texts(
for label in labels: for label in labels:
layer, datatype = layer2oas(label.layer) layer, datatype = layer2oas(label.layer)
repetition, rep_offset = repetition_masq2fata(label.repetition) repetition, rep_offset = repetition_masq2fata(label.repetition)
xy = numpy.round(label.offset + rep_offset).astype(int) xy = rint_cast(label.offset + rep_offset)
properties = annotations_to_properties(label.annotations) properties = annotations_to_properties(label.annotations)
texts.append(fatrec.Text(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
@ -648,10 +674,12 @@ def repetition_fata2masq(
) -> Optional[Repetition]: ) -> Optional[Repetition]:
mrep: Optional[Repetition] mrep: Optional[Repetition]
if isinstance(rep, fatamorgana.GridRepetition): if isinstance(rep, fatamorgana.GridRepetition):
mrep = Grid(a_vector=rep.a_vector, mrep = Grid(
b_vector=rep.b_vector, a_vector=rep.a_vector,
a_count=rep.a_count, b_vector=rep.b_vector,
b_count=rep.b_count) a_count=rep.a_count,
b_count=rep.b_count,
)
elif isinstance(rep, fatamorgana.ArbitraryRepetition): elif isinstance(rep, fatamorgana.ArbitraryRepetition):
displacements = numpy.cumsum(numpy.column_stack((rep.x_displacements, displacements = numpy.cumsum(numpy.column_stack((rep.x_displacements,
rep.y_displacements)), axis=0) rep.y_displacements)), axis=0)

@ -95,7 +95,7 @@ def build(
library_name: Library name written into the GDSII file. library_name: Library name written into the GDSII file.
Default 'masque-gdsii-write'. Default 'masque-gdsii-write'.
modify_originals: If `True`, the original pattern is modified as part of the writing modify_originals: If `True`, the original pattern is modified as part of the writing
process. Otherwise, a copy is made and `deepunlock()`-ed. process. Otherwise, a copy is made.
Default `False`. Default `False`.
disambiguate_func: Function which takes a list of patterns and alters them disambiguate_func: Function which takes a list of patterns and alters them
to make their names valid and unique. Default is `disambiguate_pattern_names`, which to make their names valid and unique. Default is `disambiguate_pattern_names`, which
@ -113,15 +113,17 @@ def build(
assert(disambiguate_func is not None) # placate mypy assert(disambiguate_func is not None) # placate mypy
if not modify_originals: if not modify_originals:
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] patterns = copy.deepcopy(patterns)
patterns = [p.wrap_repeated_shapes() for p in patterns] patterns = [p.wrap_repeated_shapes() for p in patterns]
# Create library # Create library
lib = gdsii.library.Library(version=600, lib = gdsii.library.Library(
name=library_name.encode('ASCII'), version=600,
logical_unit=logical_units_per_unit, name=library_name.encode('ASCII'),
physical_unit=meters_per_unit) logical_unit=logical_units_per_unit,
physical_unit=meters_per_unit,
)
# Get a dict of id(pattern) -> pattern # Get a dict of id(pattern) -> pattern
patterns_by_id = {id(pattern): pattern for pattern in patterns} patterns_by_id = {id(pattern): pattern for pattern in patterns}
@ -244,10 +246,11 @@ def read(
lib = gdsii.library.Library.load(stream) lib = gdsii.library.Library.load(stream)
library_info = {'name': lib.name.decode('ASCII'), library_info = {
'meters_per_unit': lib.physical_unit, 'name': lib.name.decode('ASCII'),
'logical_units_per_unit': lib.logical_unit, 'meters_per_unit': lib.physical_unit,
} 'logical_units_per_unit': lib.logical_unit,
}
raw_mode = True # Whether to construct shapes in raw mode (less error checking) raw_mode = True # Whether to construct shapes in raw mode (less error checking)
@ -265,9 +268,11 @@ def read(
pat.shapes.append(path) pat.shapes.append(path)
elif isinstance(element, gdsii.elements.Text): elif isinstance(element, gdsii.elements.Text):
label = Label(offset=element.xy.astype(float), label = Label(
layer=(element.layer, element.text_type), offset=element.xy.astype(float),
string=element.string.decode('ASCII')) layer=(element.layer, element.text_type),
string=element.string.decode('ASCII'),
)
pat.labels.append(label) pat.labels.append(label)
elif isinstance(element, (gdsii.elements.SRef, gdsii.elements.ARef)): elif isinstance(element, (gdsii.elements.SRef, gdsii.elements.ARef)):
@ -341,16 +346,22 @@ def _ref_to_subpat(
b_count = element.rows b_count = element.rows
a_vector = (element.xy[1] - offset) / a_count a_vector = (element.xy[1] - offset) / a_count
b_vector = (element.xy[2] - offset) / b_count b_vector = (element.xy[2] - offset) / b_count
repetition = Grid(a_vector=a_vector, b_vector=b_vector, repetition = Grid(
a_count=a_count, b_count=b_count) a_vector=a_vector,
b_vector=b_vector,
a_count=a_count,
b_count=b_count,
)
subpat = SubPattern(pattern=None, subpat = SubPattern(
offset=offset, pattern=None,
rotation=rotation, offset=offset,
scale=scale, rotation=rotation,
mirrored=(mirror_across_x, False), scale=scale,
annotations=_properties_to_annotations(element.properties), mirrored=(mirror_across_x, False),
repetition=repetition) annotations=_properties_to_annotations(element.properties),
repetition=repetition,
)
subpat.identifier = (element.struct_name,) subpat.identifier = (element.struct_name,)
return subpat return subpat
@ -361,14 +372,15 @@ def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path:
else: else:
raise PatternError(f'Unrecognized path type: {element.path_type}') raise PatternError(f'Unrecognized path type: {element.path_type}')
args = {'vertices': element.xy.astype(float), args = {
'layer': (element.layer, element.data_type), 'vertices': element.xy.astype(float),
'width': element.width if element.width is not None else 0.0, 'layer': (element.layer, element.data_type),
'cap': cap, 'width': element.width if element.width is not None else 0.0,
'offset': numpy.zeros(2), 'cap': cap,
'annotations': _properties_to_annotations(element.properties), 'offset': numpy.zeros(2),
'raw': raw_mode, 'annotations': _properties_to_annotations(element.properties),
} 'raw': raw_mode,
}
if cap == Path.Cap.SquareCustom: if cap == Path.Cap.SquareCustom:
args['cap_extensions'] = numpy.zeros(2) args['cap_extensions'] = numpy.zeros(2)
@ -381,12 +393,13 @@ def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path:
def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Polygon: def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Polygon:
args = {'vertices': element.xy[:-1].astype(float), args = {
'layer': (element.layer, element.data_type), 'vertices': element.xy[:-1].astype(float),
'offset': numpy.zeros(2), 'layer': (element.layer, element.data_type),
'annotations': _properties_to_annotations(element.properties), 'offset': numpy.zeros(2),
'raw': raw_mode, 'annotations': _properties_to_annotations(element.properties),
} 'raw': raw_mode,
}
return Polygon(**args) return Polygon(**args)
@ -483,9 +496,11 @@ def _shapes_to_elements(
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
path = gdsii.elements.Path(layer=layer, path = gdsii.elements.Path(
data_type=data_type, layer=layer,
xy=xy) data_type=data_type,
xy=xy,
)
path.path_type = path_type path.path_type = path_type
path.width = width path.width = width
path.properties = properties path.properties = properties

@ -6,14 +6,13 @@ 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, layer_t, AutoSlots, annotations_t
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, RepeatableImpl, AnnotatableImpl
from .traits import AnnotatableImpl
L = TypeVar('L', bound='Label') L = TypeVar('L', bound='Label')
class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, AnnotatableImpl, class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
Pivotable, Copyable, metaclass=AutoSlots): 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 and layer (but no size; it is not drawn)
@ -49,33 +48,23 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
layer: layer_t = 0, layer: layer_t = 0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
identifier: Tuple = (), identifier: Tuple = (),
) -> None: ) -> None:
LockableImpl.unlock(self)
self.identifier = identifier self.identifier = identifier
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.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 {}
self.set_locked(locked)
def __copy__(self: L) -> L: def __copy__(self: L) -> L:
return type(self)(string=self.string, return type(self)(
offset=self.offset.copy(), string=self.string,
layer=self.layer, offset=self.offset.copy(),
repetition=self.repetition, layer=self.layer,
locked=self.locked, repetition=self.repetition,
identifier=self.identifier) identifier=self.identifier,
)
def __deepcopy__(self: L, memo: Dict = None) -> L:
memo = {} if memo is None else memo
new = copy.copy(self)
LockableImpl.unlock(new)
new._offset = self._offset.copy()
new.set_locked(self.locked)
return new
def rotate_around(self: L, pivot: ArrayLike, rotation: float) -> L: def rotate_around(self: L, pivot: ArrayLike, rotation: float) -> L:
""" """
@ -107,16 +96,5 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
""" """
return numpy.array([self.offset, self.offset]) return numpy.array([self.offset, self.offset])
def lock(self: L) -> L:
PositionableImpl._lock(self)
LockableImpl.lock(self)
return self
def unlock(self: L) -> L:
LockableImpl.unlock(self)
PositionableImpl._unlock(self)
return self
def __repr__(self) -> str: def __repr__(self) -> str:
locked = ' L' if self.locked else '' return f'<Label "{self.string}" l{self.layer} o{self.offset}>'
return f'<Label "{self.string}" l{self.layer} o{self.offset}{locked}>'

@ -26,15 +26,13 @@ class DeviceLibrary:
relevant `Device` object. relevant `Device` object.
This class largely functions the same way as `Library`, but This class largely functions the same way as `Library`, but
operates on `Device`s rather than `Patterns` and thus has no operates on `Device`s rather than `Patterns`.
need for distinctions between primary/secondary devices (as
there is no inter-`Device` hierarchy).
Each device is cached the first time it is used. The cache can Each device is cached the first time it is used. The cache can
be disabled by setting the `enable_cache` attribute to `False`. be disabled by setting the `enable_cache` attribute to `False`.
""" """
generators: Dict[str, Callable[[], Device]] generators: Dict[str, Callable[[], Device]]
cache: Dict[Union[str, Tuple[str, str]], Device] cache: Dict[str, Device]
enable_cache: bool = True enable_cache: bool = True
def __init__(self) -> None: def __init__(self) -> None:
@ -44,11 +42,16 @@ class DeviceLibrary:
def __setitem__(self, key: str, value: Callable[[], Device]) -> None: def __setitem__(self, key: str, value: Callable[[], Device]) -> None:
self.generators[key] = value self.generators[key] = value
if key in self.cache: if key in self.cache:
logger.warning(f'Replaced library item "{key}" & existing cache entry.'
' Previously-generated Device will *not* be updated!')
del self.cache[key] del self.cache[key]
def __delitem__(self, key: str) -> None: def __delitem__(self, key: str) -> None:
del self.generators[key] del self.generators[key]
if key in self.cache: if key in self.cache:
logger.warning(f'Deleting library item "{key}" & existing cache entry.'
' Previously-generated Device may remain in the wild!')
del self.cache[key] del self.cache[key]
def __getitem__(self, key: str) -> Device: def __getitem__(self, key: str) -> Device:
@ -119,10 +122,10 @@ class DeviceLibrary:
raise DeviceLibraryError('Duplicate keys encountered in DeviceLibrary merge: ' raise DeviceLibraryError('Duplicate keys encountered in DeviceLibrary merge: '
+ pformat(conflicts)) + pformat(conflicts))
for name in set(other.generators.keys()) - keep_ours: for key in set(other.keys()) - keep_ours:
self.generators[name] = other.generators[name] self.generators[key] = other.generators[key]
if name in other.cache: if key in other.cache:
self.cache[name] = other.cache[name] self.cache[key] = other.cache[key]
return self return self
def clear_cache(self: D) -> D: def clear_cache(self: D) -> D:

@ -6,7 +6,6 @@ from typing import Dict, Callable, TypeVar, TYPE_CHECKING
from typing import Any, Tuple, Union, Iterator from typing import Any, Tuple, Union, Iterator
import logging import logging
from pprint import pformat from pprint import pformat
from dataclasses import dataclass
import copy import copy
from ..error import LibraryError from ..error import LibraryError
@ -18,16 +17,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class PatternGenerator:
__slots__ = ('tag', 'gen')
tag: str
""" Unique identifier for the source """
gen: Callable[[], 'Pattern']
""" Function which generates a pattern when called """
L = TypeVar('L', bound='Library') L = TypeVar('L', bound='Library')
@ -41,44 +30,25 @@ class Library:
Library expects `sp.identifier[0]` to contain a string which specifies the Library expects `sp.identifier[0]` to contain a string which specifies the
referenced pattern's name. referenced pattern's name.
Patterns can either be "primary" (default) or "secondary". Both get the
same deferred-load behavior, but "secondary" patterns may have conflicting
names and are not accessible through basic []-indexing. They are only used
to fill symbolic references in cases where there is no "primary" pattern
available, and only if both the referencing and referenced pattern-generators'
`tag` values match (i.e., only if they came from the same source).
Primary patterns can be turned into secondary patterns with the `demote`
method, `promote` performs the reverse (secondary -> primary) operation.
The `set_const` and `set_value` methods provide an easy way to transparently
construct PatternGenerator objects and directly set create "secondary"
patterns.
The cache can be disabled by setting the `enable_cache` attribute to `False`. The cache can be disabled by setting the `enable_cache` attribute to `False`.
""" """
primary: Dict[str, PatternGenerator] generators: Dict[str, Callable[[], 'Pattern']]
secondary: Dict[Tuple[str, str], PatternGenerator] cache: Dict[str, 'Pattern']
cache: Dict[Union[str, Tuple[str, str]], 'Pattern']
enable_cache: bool = True enable_cache: bool = True
def __init__(self) -> None: def __init__(self) -> None:
self.primary = {} self.generators = {}
self.secondary = {}
self.cache = {} self.cache = {}
def __setitem__(self, key: str, value: PatternGenerator) -> None: def __setitem__(self, key: str, value: Callable[[], 'Pattern']) -> None:
self.primary[key] = value self.generators[key] = value
if key in self.cache: if key in self.cache:
logger.warning(f'Replaced library item "{key}" & existing cache entry.' logger.warning(f'Replaced library item "{key}" & existing cache entry.'
' Previously-generated Pattern will *not* be updated!') ' Previously-generated Pattern will *not* be updated!')
del self.cache[key] del self.cache[key]
def __delitem__(self, key: str) -> None: def __delitem__(self, key: str) -> None:
if isinstance(key, str): del self.generators[key]
del self.primary[key]
elif isinstance(key, tuple):
del self.secondary[key]
if key in self.cache: if key in self.cache:
logger.warning(f'Deleting library item "{key}" & existing cache entry.' logger.warning(f'Deleting library item "{key}" & existing cache entry.'
@ -86,44 +56,21 @@ class Library:
del self.cache[key] del self.cache[key]
def __getitem__(self, key: str) -> 'Pattern': def __getitem__(self, key: str) -> 'Pattern':
return self.get_primary(key)
def __iter__(self) -> Iterator[str]:
return iter(self.keys())
def __contains__(self, key: str) -> bool:
return key in self.primary
def get_primary(self, key: str) -> 'Pattern':
if self.enable_cache and key in self.cache: if self.enable_cache and key in self.cache:
logger.debug(f'found {key} in cache') logger.debug(f'found {key} in cache')
return self.cache[key] return self.cache[key]
logger.debug(f'loading {key}') logger.debug(f'loading {key}')
pg = self.primary[key] pat = self.generators[key]()
pat = pg.gen() self.resolve_subpatterns(pat)
self.resolve_subpatterns(pat, pg.tag)
self.cache[key] = pat self.cache[key] = pat
return pat return pat
def get_secondary(self, key: str, tag: str) -> 'Pattern': def __iter__(self) -> Iterator[str]:
logger.debug(f'get_secondary({key}, {tag})') return iter(self.keys())
key2 = (key, tag)
if self.enable_cache and key2 in self.cache:
return self.cache[key2]
pg = self.secondary[key2] def __contains__(self, key: str) -> bool:
pat = pg.gen() return key in self.generators
self.resolve_subpatterns(pat, pg.tag)
self.cache[key2] = pat
return pat
def set_secondary(self, key: str, tag: str, value: PatternGenerator) -> None:
self.secondary[(key, tag)] = value
if (key, tag) in self.cache:
logger.warning(f'Replaced library item "{key}" & existing cache entry.'
' Previously-generated Pattern will *not* be updated!')
del self.cache[(key, tag)]
def resolve_subpatterns(self, pat: 'Pattern', tag: str) -> 'Pattern': def resolve_subpatterns(self, pat: 'Pattern', tag: str) -> 'Pattern':
logger.debug(f'Resolving subpatterns in {pat.name}') logger.debug(f'Resolving subpatterns in {pat.name}')
@ -132,19 +79,15 @@ class Library:
continue continue
key = sp.identifier[0] key = sp.identifier[0]
if key in self.primary: if key in self.generators:
sp.pattern = self.get_primary(key) sp.pattern = self[key]
continue
if (key, tag) in self.secondary:
sp.pattern = self.get_secondary(key, tag)
continue continue
raise LibraryError(f'Broken reference to {key} (tag {tag})') raise LibraryError(f'Broken reference to {key} (tag {tag})')
return pat return pat
def keys(self) -> Iterator[str]: def keys(self) -> Iterator[str]:
return iter(self.primary.keys()) return iter(self.generators.keys())
def values(self) -> Iterator['Pattern']: def values(self) -> Iterator['Pattern']:
return iter(self[key] for key in self.keys()) return iter(self[key] for key in self.keys())
@ -153,56 +96,7 @@ class Library:
return iter((key, self[key]) for key in self.keys()) return iter((key, self[key]) for key in self.keys())
def __repr__(self) -> str: def __repr__(self) -> str:
return '<Library with keys ' + repr(list(self.primary.keys())) + '>' return '<Library with keys ' + repr(list(self.generators.keys())) + '>'
def set_const(
self,
key: str,
tag: Any,
const: 'Pattern',
secondary: bool = False,
) -> None:
"""
Convenience function to avoid having to manually wrap
constant values into callables.
Args:
key: Lookup key, usually the cell/pattern name
tag: Unique tag for the source, used to disambiguate secondary patterns
const: Pattern object to return
secondary: If True, this pattern is not accessible for normal lookup, and is
only used as a sub-component of other patterns if no non-secondary
equivalent is available.
"""
pg = PatternGenerator(tag=tag, gen=lambda: const)
if secondary:
self.secondary[(key, tag)] = pg
else:
self.primary[key] = pg
def set_value(
self,
key: str,
tag: str,
value: Callable[[], 'Pattern'],
secondary: bool = False,
) -> None:
"""
Convenience function to automatically build a PatternGenerator.
Args:
key: Lookup key, usually the cell/pattern name
tag: Unique tag for the source, used to disambiguate secondary patterns
value: Callable which takes no arguments and generates the `Pattern` object
secondary: If True, this pattern is not accessible for normal lookup, and is
only used as a sub-component of other patterns if no non-secondary
equivalent is available.
"""
pg = PatternGenerator(tag=tag, gen=value)
if secondary:
self.secondary[(key, tag)] = pg
else:
self.primary[key] = pg
def precache(self: L) -> L: def precache(self: L) -> L:
""" """
@ -211,17 +105,15 @@ class Library:
Returns: Returns:
self self
""" """
for key in self.primary: for key in self.generators:
_ = self.get_primary(key) _ = self[key]
for key2 in self.secondary:
_ = self.get_secondary(*key2)
return self return self
def add( def add(
self: L, self: L,
other: L, other: L,
use_ours: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False, use_ours: Callable[[str], bool] = lambda name: False,
use_theirs: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False, use_theirs: Callable[[str], bool] = lambda name: False,
) -> L: ) -> L:
""" """
Add keys from another library into this one. Add keys from another library into this one.
@ -229,8 +121,6 @@ class Library:
Args: Args:
other: The library to insert keys from other: The library to insert keys from
use_ours: Decision function for name conflicts. use_ours: Decision function for name conflicts.
May be called with cell names and (name, tag) tuples for primary or
secondary cells, respectively.
Should return `True` if the value from `self` should be used. Should return `True` if the value from `self` should be used.
use_theirs: Decision function for name conflicts. Same format as `use_ours`. use_theirs: Decision function for name conflicts. Same format as `use_ours`.
Should return `True` if the value from `other` should be used. Should return `True` if the value from `other` should be used.
@ -238,72 +128,21 @@ class Library:
Returns: Returns:
self self
""" """
duplicates1 = set(self.primary.keys()) & set(other.primary.keys()) duplicates = set(self.keys()) & set(other.keys())
duplicates2 = set(self.secondary.keys()) & set(other.secondary.keys()) keep_ours = set(name for name in duplicates if use_ours(name))
keep_ours1 = set(name for name in duplicates1 if use_ours(name)) keep_theirs = set(name for name in duplicates - keep_ours if use_theirs(name))
keep_ours2 = set(name for name in duplicates2 if use_ours(name)) conflicts = duplicates - keep_ours - keep_theirs
keep_theirs1 = set(name for name in duplicates1 - keep_ours1 if use_theirs(name))
keep_theirs2 = set(name for name in duplicates2 - keep_ours2 if use_theirs(name))
conflicts1 = duplicates1 - keep_ours1 - keep_theirs1
conflicts2 = duplicates2 - keep_ours2 - keep_theirs2
if conflicts1: if conflicts:
raise LibraryError('Unresolved duplicate keys encountered in library merge: ' + pformat(conflicts1)) raise LibraryError('Unresolved duplicate keys encountered in library merge: '
+ pformat(conflicts))
if conflicts2:
raise LibraryError('Unresolved duplicate secondary keys encountered in library merge: ' + pformat(conflicts2))
for key1 in set(other.primary.keys()) - keep_ours1:
self[key1] = other.primary[key1]
if key1 in other.cache:
self.cache[key1] = other.cache[key1]
for key2 in set(other.secondary.keys()) - keep_ours2:
self.set_secondary(*key2, other.secondary[key2])
if key2 in other.cache:
self.cache[key2] = other.cache[key2]
for key in set(other.keys()) - keep_ours:
self[key] = other.generators[key]
if key in other.cache:
self.cache[key] = other.cache[key]
return self return self
def demote(self, key: str) -> None:
"""
Turn a primary pattern into a secondary one.
It will no longer be accessible through [] indexing and will only be used to
when referenced by other patterns from the same source, and only if no primary
pattern with the same name exists.
Args:
key: Lookup key, usually the cell/pattern name
"""
pg = self.primary[key]
key2 = (key, pg.tag)
self.secondary[key2] = pg
if key in self.cache:
self.cache[key2] = self.cache[key]
del self[key]
def promote(self, key: str, tag: str) -> None:
"""
Turn a secondary pattern into a primary one.
It will become accessible through [] indexing and will be used to satisfy any
reference to a pattern with its key, regardless of tag.
Args:
key: Lookup key, usually the cell/pattern name
tag: Unique tag for identifying the pattern's source, used to disambiguate
secondary patterns
"""
if key in self.primary:
raise LibraryError(f'Promoting ({key}, {tag}), but {key} already exists in primary!')
key2 = (key, tag)
pg = self.secondary[key2]
self.primary[key] = pg
if key2 in self.cache:
self.cache[key] = self.cache[key2]
del self.secondary[key2]
del self.cache[key2]
def copy(self, preserve_cache: bool = False) -> 'Library': def copy(self, preserve_cache: bool = False) -> 'Library':
""" """
Create a copy of this `Library`. Create a copy of this `Library`.
@ -315,8 +154,7 @@ class Library:
A copy of self A copy of self
""" """
new = Library() new = Library()
new.primary.update(self.primary) new.generators.update(self.generators)
new.secondary.update(self.secondary)
new.cache.update(self.cache) new.cache.update(self.cache)
return new return new
@ -332,24 +170,6 @@ class Library:
self.cache = {} self.cache = {}
return self return self
def __deepcopy__(self: L, memo: Optional[Dict] = None) -> L:
raise LibraryError('Library cannot be deepcopied -- python copy.deepcopy() does not copy closures!')
r"""
# Add a filter for names which aren't added
- Registration:
- scanned files (tag=filename, gen_fn[stream, {name: pos}])
- generator functions (tag='fn?', gen_fn[params])
- merge decision function (based on tag and cell name, can be "neither") ??? neither=keep both, load using same tag!
- Load process:
- file:
- read single cell
- check subpat identifiers, and load stuff recursively based on those. If not present, load from same file??
- function:
- generate cell
- traverse and check if we should load any subcells from elsewhere. replace if so.
* should fn generate subcells at all, or register those separately and have us control flow? maybe ask us and generate itself if not present?
- Scan all GDS files, save name -> (file, position). Keep the streams handy.
- Merge all names. This requires subcell merge because we don't know hierarchy.
- possibly include a "neither" option during merge, to deal with subcells. Means: just use parent's file.
"""

@ -18,9 +18,8 @@ from .subpattern import SubPattern
from .shapes import Shape, Polygon from .shapes import Shape, Polygon
from .label import Label from .label import Label
from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t
from .error import PatternError, PatternLockedError from .error import PatternError
from .traits import LockableImpl, AnnotatableImpl, Scalable, Mirrorable from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable
from .traits import Rotatable, Positionable
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern'] visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern']
@ -29,7 +28,7 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.
P = TypeVar('P', bound='Pattern') P = TypeVar('P', bound='Pattern')
class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
""" """
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
(via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. (via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
@ -61,7 +60,6 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
labels: Sequence[Label] = (), labels: Sequence[Label] = (),
subpatterns: Sequence[SubPattern] = (), subpatterns: Sequence[SubPattern] = (),
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
) -> None: ) -> None:
""" """
Basic init; arguments get assigned to member variables. Basic init; arguments get assigned to member variables.
@ -72,9 +70,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
labels: Initial labels in the Pattern labels: Initial labels in the Pattern
subpatterns: Initial subpatterns in the Pattern subpatterns: Initial subpatterns in the Pattern
name: An identifier for the Pattern name: An identifier for the Pattern
locked: Whether to lock the pattern after construction
""" """
LockableImpl.unlock(self)
if isinstance(shapes, list): if isinstance(shapes, list):
self.shapes = shapes self.shapes = shapes
else: else:
@ -92,15 +88,15 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.name = name self.name = name
self.set_locked(locked)
def __copy__(self, memo: Dict = None) -> 'Pattern': def __copy__(self, memo: Dict = None) -> 'Pattern':
return Pattern(name=self.name, return Pattern(
shapes=copy.deepcopy(self.shapes), name=self.name,
labels=copy.deepcopy(self.labels), shapes=copy.deepcopy(self.shapes),
subpatterns=[copy.copy(sp) for sp in self.subpatterns], labels=copy.deepcopy(self.labels),
annotations=copy.deepcopy(self.annotations), subpatterns=[copy.copy(sp) for sp in self.subpatterns],
locked=self.locked) annotations=copy.deepcopy(self.annotations),
)
def __deepcopy__(self, memo: Dict = None) -> 'Pattern': def __deepcopy__(self, memo: Dict = None) -> 'Pattern':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -110,7 +106,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
labels=copy.deepcopy(self.labels, memo), labels=copy.deepcopy(self.labels, memo),
subpatterns=copy.deepcopy(self.subpatterns, memo), subpatterns=copy.deepcopy(self.subpatterns, memo),
annotations=copy.deepcopy(self.annotations, memo), annotations=copy.deepcopy(self.annotations, memo),
locked=self.locked) )
return new return new
def rename(self: P, name: str) -> P: def rename(self: P, name: str) -> P:
@ -307,14 +303,13 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
sp_transform = False sp_transform = False
if subpattern.pattern is not None: if subpattern.pattern is not None:
result = subpattern.pattern.dfs(visit_before=visit_before, subpattern.patern = subpattern.pattern.dfs(
visit_after=visit_after, visit_before=visit_before,
transform=sp_transform, visit_after=visit_after,
memo=memo, transform=sp_transform,
hierarchy=hierarchy + (self,)) memo=memo,
if result is not subpattern.pattern: hierarchy=hierarchy + (self,),
# skip assignment to avoid PatternLockedError unless modified )
subpattern.pattern = result
if visit_after is not None: if visit_after is not None:
pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
@ -454,7 +449,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray
is of the form `[[x0, y0], [x1, y1],...]`. is of the form `[[x0, y0], [x1, y1],...]`.
""" """
pat = self.deepcopy().deepunlock().polygonize().flatten() pat = self.deepcopy().polygonize().flatten()
return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now
@overload @overload
@ -872,66 +867,6 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
self.subpatterns.append(SubPattern(*args, **kwargs)) self.subpatterns.append(SubPattern(*args, **kwargs))
return self return self
def lock(self: P) -> P:
"""
Lock the pattern, raising an exception if it is modified.
Also see `deeplock()`.
Returns:
self
"""
if not self.locked:
self.shapes = tuple(self.shapes)
self.labels = tuple(self.labels)
self.subpatterns = tuple(self.subpatterns)
LockableImpl.lock(self)
return self
def unlock(self: P) -> P:
"""
Unlock the pattern
Returns:
self
"""
if self.locked:
LockableImpl.unlock(self)
self.shapes = list(self.shapes)
self.labels = list(self.labels)
self.subpatterns = list(self.subpatterns)
return self
def deeplock(self: P) -> P:
"""
Recursively lock the pattern, all referenced shapes, subpatterns, and labels.
Returns:
self
"""
self.lock()
for ss in chain(self.shapes, self.labels):
ss.lock() # type: ignore # mypy struggles with multiple inheritance :(
for sp in self.subpatterns:
sp.deeplock()
return self
def deepunlock(self: P) -> P:
"""
Recursively unlock the pattern, all referenced shapes, subpatterns, and labels.
This is dangerous unless you have just performed a deepcopy, since anything
you change will be changed everywhere it is referenced!
Return:
self
"""
self.unlock()
for ss in chain(self.shapes, self.labels):
ss.unlock() # type: ignore # mypy struggles with multiple inheritance :(
for sp in self.subpatterns:
sp.deepunlock()
return self
@staticmethod @staticmethod
def load(filename: str) -> 'Pattern': def load(filename: str) -> 'Pattern':
""" """
@ -1046,5 +981,4 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
return toplevel return toplevel
def __repr__(self) -> str: def __repr__(self) -> str:
locked = ' L' if self.locked else '' return (f'<Pattern "{self.name}": sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)}>')
return (f'<Pattern "{self.name}": sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)}{locked}>')

@ -12,7 +12,7 @@ from numpy.typing import ArrayLike, NDArray
from .error import PatternError from .error import PatternError
from .utils import rotation_matrix_2d, AutoSlots from .utils import rotation_matrix_2d, AutoSlots
from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable from .traits import Copyable, Scalable, Rotatable, Mirrorable
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta): class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
@ -30,7 +30,7 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
pass pass
class Grid(LockableImpl, Repetition, metaclass=AutoSlots): class Grid(Repetition, metaclass=AutoSlots):
""" """
`Grid` describes a 2D grid formed by two basis vectors and two 'counts' (sizes). `Grid` describes a 2D grid formed by two basis vectors and two 'counts' (sizes).
@ -67,7 +67,6 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
a_count: int, a_count: int,
b_vector: Optional[ArrayLike] = None, b_vector: Optional[ArrayLike] = None,
b_count: Optional[int] = 1, b_count: Optional[int] = 1,
locked: bool = False,
) -> None: ) -> None:
""" """
Args: Args:
@ -79,7 +78,6 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
Can be omitted when specifying a 1D array. Can be omitted when specifying a 1D array.
b_count: Number of elements in the `b_vector` direction. b_count: Number of elements in the `b_vector` direction.
Should be omitted if `b_vector` was omitted. Should be omitted if `b_vector` was omitted.
locked: Whether the `Grid` is locked after initialization.
Raises: Raises:
PatternError if `b_*` inputs conflict with each other PatternError if `b_*` inputs conflict with each other
@ -99,12 +97,10 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
if b_count < 1: if b_count < 1:
raise PatternError(f'Repetition has too-small b_count: {b_count}') raise PatternError(f'Repetition has too-small b_count: {b_count}')
object.__setattr__(self, 'locked', False)
self.a_vector = a_vector # type: ignore # setter handles type conversion self.a_vector = a_vector # type: ignore # setter handles type conversion
self.b_vector = b_vector # type: ignore # setter handles type conversion self.b_vector = b_vector # type: ignore # setter handles type conversion
self.a_count = a_count self.a_count = a_count
self.b_count = b_count self.b_count = b_count
self.locked = locked
@classmethod @classmethod
def aligned( def aligned(
@ -129,18 +125,12 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
return cls(a_vector=(x, 0), b_vector=(0, y), a_count=x_count, b_count=y_count) return cls(a_vector=(x, 0), b_vector=(0, y), a_count=x_count, b_count=y_count)
def __copy__(self) -> 'Grid': def __copy__(self) -> 'Grid':
new = Grid(a_vector=self.a_vector.copy(), new = Grid(
b_vector=copy.copy(self.b_vector), a_vector=self.a_vector.copy(),
a_count=self.a_count, b_vector=copy.copy(self.b_vector),
b_count=self.b_count, a_count=self.a_count,
locked=self.locked) b_count=self.b_count,
return new )
def __deepcopy__(self, memo: Dict = None) -> 'Grid':
memo = {} if memo is None else memo
new = copy.copy(self)
LocakbleImpl.unlock(new)
new.locked = self.locked
return new return new
# a_vector property # a_vector property
@ -264,36 +254,9 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
self.b_vector *= c self.b_vector *= c
return self return self
def lock(self) -> 'Grid':
"""
Lock the `Grid`, disallowing changes.
Returns:
self
"""
self.a_vector.flags.writeable = False
if self.b_vector is not None:
self.b_vector.flags.writeable = False
LockableImpl.lock(self)
return self
def unlock(self) -> 'Grid':
"""
Unlock the `Grid`
Returns:
self
"""
self.a_vector.flags.writeable = True
if self.b_vector is not None:
self.b_vector.flags.writeable = True
LockableImpl.unlock(self)
return self
def __repr__(self) -> str: def __repr__(self) -> str:
locked = ' L' if self.locked else ''
bv = f', {self.b_vector}' if self.b_vector is not None else '' bv = f', {self.b_vector}' if self.b_vector is not None else ''
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>') return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>')
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)): if not isinstance(other, type(self)):
@ -308,12 +271,10 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
return False return False
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)):
return False return False
if self.locked != other.locked:
return False
return True return True
class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): class Arbitrary(Repetition, metaclass=AutoSlots):
""" """
`Arbitrary` is a simple list of (absolute) displacements for instances. `Arbitrary` is a simple list of (absolute) displacements for instances.
@ -342,48 +303,19 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots):
def __init__( def __init__(
self, self,
displacements: ArrayLike, displacements: ArrayLike,
locked: bool = False,
) -> None: ) -> None:
""" """
Args: Args:
displacements: List of vectors (Nx2 ndarray) specifying displacements. displacements: List of vectors (Nx2 ndarray) specifying displacements.
locked: Whether the object is locked after initialization.
""" """
object.__setattr__(self, 'locked', False)
self.displacements = displacements self.displacements = displacements
self.locked = locked
def lock(self) -> 'Arbitrary':
"""
Lock the object, disallowing changes.
Returns:
self
"""
self._displacements.flags.writeable = False
LockableImpl.lock(self)
return self
def unlock(self) -> 'Arbitrary':
"""
Unlock the object
Returns:
self
"""
self._displacements.flags.writeable = True
LockableImpl.unlock(self)
return self
def __repr__(self) -> str: def __repr__(self) -> str:
locked = ' L' if self.locked else '' return (f'<Arbitrary {len(self.displacements)}pts>')
return (f'<Arbitrary {len(self.displacements)}pts {locked}>')
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)): if not isinstance(other, type(self)):
return False return False
if self.locked != other.locked:
return False
return numpy.array_equal(self.displacements, other.displacements) return numpy.array_equal(self.displacements, other.displacements)
def rotate(self, rotation: float) -> 'Arbitrary': def rotate(self, rotation: float) -> 'Arbitrary':

@ -10,7 +10,6 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, layer_t, AutoSlots, annotations_t from ..utils import is_scalar, layer_t, AutoSlots, annotations_t
from ..traits import LockableImpl
class Arc(Shape, metaclass=AutoSlots): class Arc(Shape, metaclass=AutoSlots):
@ -166,10 +165,8 @@ class Arc(Shape, metaclass=AutoSlots):
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(radii, numpy.ndarray)) assert(isinstance(radii, numpy.ndarray))
@ -197,18 +194,6 @@ class Arc(Shape, metaclass=AutoSlots):
self.poly_num_points = poly_num_points self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen self.poly_max_arclen = poly_max_arclen
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Arc':
memo = {} if memo is None else memo
new = copy.copy(self)
Shape.unlock(new)
new._offset = self._offset.copy()
new._radii = self._radii.copy()
new._angles = self._angles.copy()
new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new
def to_polygons( def to_polygons(
self, self,
@ -429,21 +414,8 @@ class Arc(Shape, metaclass=AutoSlots):
a.append((a0, a1)) a.append((a0, a1))
return numpy.array(a) return numpy.array(a)
def lock(self) -> 'Arc':
self.radii.flags.writeable = False
self.angles.flags.writeable = False
Shape.lock(self)
return self
def unlock(self) -> 'Arc':
Shape.unlock(self)
self.radii.flags.writeable = True
self.angles.flags.writeable = True
return self
def __repr__(self) -> str: def __repr__(self) -> str:
angles = f'{numpy.rad2deg(self.angles)}' angles = f'{numpy.rad2deg(self.angles)}'
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else '' return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}>'
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}{locked}>'

@ -9,7 +9,6 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, layer_t, AutoSlots, annotations_t from ..utils import is_scalar, layer_t, AutoSlots, annotations_t
from ..traits import LockableImpl
class Circle(Shape, metaclass=AutoSlots): class Circle(Shape, metaclass=AutoSlots):
@ -54,10 +53,8 @@ class Circle(Shape, metaclass=AutoSlots):
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(offset, numpy.ndarray)) assert(isinstance(offset, numpy.ndarray))
@ -76,16 +73,6 @@ class Circle(Shape, metaclass=AutoSlots):
self.dose = dose self.dose = dose
self.poly_num_points = poly_num_points self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen self.poly_max_arclen = poly_max_arclen
self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Circle':
memo = {} if memo is None else memo
new = copy.copy(self)
Shape.unlock(new)
new._offset = self._offset.copy()
new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new
def to_polygons( def to_polygons(
self, self,
@ -138,5 +125,4 @@ class Circle(Shape, metaclass=AutoSlots):
def __repr__(self) -> str: def __repr__(self) -> str:
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else '' return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}{dose}>'
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}{dose}{locked}>'

@ -10,7 +10,6 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots, annotations_t from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots, annotations_t
from ..traits import LockableImpl
class Ellipse(Shape, metaclass=AutoSlots): class Ellipse(Shape, metaclass=AutoSlots):
@ -101,10 +100,8 @@ class Ellipse(Shape, metaclass=AutoSlots):
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(radii, numpy.ndarray)) assert(isinstance(radii, numpy.ndarray))
@ -127,17 +124,6 @@ class Ellipse(Shape, metaclass=AutoSlots):
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
self.poly_num_points = poly_num_points self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen self.poly_max_arclen = poly_max_arclen
self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Ellipse':
memo = {} if memo is None else memo
new = copy.copy(self)
Shape.unlock(new)
new._offset = self._offset.copy()
new._radii = self._radii.copy()
new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new
def to_polygons( def to_polygons(
self, self,
@ -209,18 +195,7 @@ class Ellipse(Shape, metaclass=AutoSlots):
(self.offset, scale / norm_value, angle, False, self.dose), (self.offset, scale / norm_value, angle, False, self.dose),
lambda: Ellipse(radii=radii * norm_value, layer=self.layer)) lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
def lock(self) -> 'Ellipse':
self.radii.flags.writeable = False
Shape.lock(self)
return self
def unlock(self) -> 'Ellipse':
Shape.unlock(self)
self.radii.flags.writeable = True
return self
def __repr__(self) -> str: def __repr__(self) -> str:
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else '' rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else '' return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}{dose}>'
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}{dose}{locked}>'

@ -11,7 +11,6 @@ from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
from ..traits import LockableImpl
class PathCap(Enum): class PathCap(Enum):
@ -155,10 +154,8 @@ class Path(Shape, metaclass=AutoSlots):
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
LockableImpl.unlock(self)
self._cap_extensions = None # Since .cap setter might access it self._cap_extensions = None # Since .cap setter might access it
self.identifier = () self.identifier = ()
@ -187,18 +184,15 @@ class Path(Shape, metaclass=AutoSlots):
self.cap_extensions = cap_extensions self.cap_extensions = cap_extensions
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]
self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Path': def __deepcopy__(self, memo: Dict = None) -> 'Path':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
Shape.unlock(new)
new._offset = self._offset.copy() new._offset = self._offset.copy()
new._vertices = self._vertices.copy() new._vertices = self._vertices.copy()
new._cap = copy.deepcopy(self._cap, memo) new._cap = copy.deepcopy(self._cap, memo)
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new return new
@staticmethod @staticmethod
@ -424,22 +418,7 @@ class Path(Shape, metaclass=AutoSlots):
extensions = numpy.zeros(2) extensions = numpy.zeros(2)
return extensions return extensions
def lock(self) -> 'Path':
self.vertices.flags.writeable = False
if self.cap_extensions is not None:
self.cap_extensions.flags.writeable = False
Shape.lock(self)
return self
def unlock(self) -> 'Path':
Shape.unlock(self)
self.vertices.flags.writeable = True
if self.cap_extensions is not None:
self.cap_extensions.flags.writeable = True
return self
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0) centroid = self.offset + self.vertices.mean(axis=0)
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else '' return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}{dose}>'
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}{dose}{locked}>'

@ -10,7 +10,6 @@ from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
from ..traits import LockableImpl
class Polygon(Shape, metaclass=AutoSlots): class Polygon(Shape, metaclass=AutoSlots):
@ -83,10 +82,8 @@ class Polygon(Shape, metaclass=AutoSlots):
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(vertices, numpy.ndarray)) assert(isinstance(vertices, numpy.ndarray))
@ -106,17 +103,6 @@ class Polygon(Shape, metaclass=AutoSlots):
self.dose = dose self.dose = dose
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]
self.set_locked(locked)
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon':
memo = {} if memo is None else memo
new = copy.copy(self)
Shape.unlock(new)
new._offset = self._offset.copy()
new._vertices = self._vertices.copy()
new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new
@staticmethod @staticmethod
def square( def square(
@ -430,18 +416,7 @@ class Polygon(Shape, metaclass=AutoSlots):
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True) self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
return self return self
def lock(self) -> 'Polygon':
self.vertices.flags.writeable = False
Shape.lock(self)
return self
def unlock(self) -> 'Polygon':
Shape.unlock(self)
self.vertices.flags.writeable = True
return self
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0) centroid = self.offset + self.vertices.mean(axis=0)
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else '' return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}{dose}>'
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}{dose}{locked}>'

@ -4,10 +4,11 @@ from abc import ABCMeta, abstractmethod
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, from ..traits import (
Rotatable, Mirrorable, Copyable, Scalable, PositionableImpl, LayerableImpl, DoseableImpl,
PivotableImpl, LockableImpl, RepeatableImpl, Rotatable, Mirrorable, Copyable, Scalable,
AnnotatableImpl) PivotableImpl, RepeatableImpl, AnnotatableImpl,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Polygon from . import Polygon
@ -27,7 +28,7 @@ T = TypeVar('T', bound='Shape')
class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable,
PivotableImpl, RepeatableImpl, LockableImpl, AnnotatableImpl, metaclass=ABCMeta): PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
""" """
Abstract class specifying functions common to all shapes. Abstract class specifying functions common to all shapes.
""" """
@ -36,13 +37,6 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
identifier: Tuple identifier: Tuple
""" An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """
def __copy__(self) -> 'Shape':
cls = self.__class__
new = cls.__new__(cls)
for name in self.__slots__: # type: str
object.__setattr__(new, name, getattr(self, name))
return new
''' '''
--- Abstract methods --- Abstract methods
''' '''
@ -303,13 +297,3 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
dose=self.dose)) dose=self.dose))
return manhattan_polygons return manhattan_polygons
def lock(self: T) -> T:
PositionableImpl._lock(self)
LockableImpl.lock(self)
return self
def unlock(self: T) -> T:
LockableImpl.unlock(self)
PositionableImpl._unlock(self)
return self

@ -11,7 +11,6 @@ from ..repetition import Repetition
from ..traits import RotatableImpl from ..traits import RotatableImpl
from ..utils import is_scalar, get_bit, normalize_mirror, layer_t, AutoSlots from ..utils import is_scalar, get_bit, normalize_mirror, layer_t, AutoSlots
from ..utils import annotations_t from ..utils import annotations_t
from ..traits import LockableImpl
# Loaded on use: # Loaded on use:
# from freetype import Face # from freetype import Face
@ -74,10 +73,8 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(offset, numpy.ndarray)) assert(isinstance(offset, numpy.ndarray))
@ -102,17 +99,6 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
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.font_path = font_path self.font_path = font_path
self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Text':
memo = {} if memo is None else memo
new = copy.copy(self)
Shape.unlock(new)
new._offset = self._offset.copy()
new._mirrored = copy.deepcopy(self._mirrored, memo)
new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new
def to_polygons( def to_polygons(
self, self,
@ -259,19 +245,8 @@ def get_char_as_polygons(
return polygons, advance return polygons, advance
def lock(self) -> 'Text':
self.mirrored.flags.writeable = False
Shape.lock(self)
return self
def unlock(self) -> 'Text':
Shape.unlock(self)
self.mirrored.flags.writeable = True
return self
def __repr__(self) -> str: def __repr__(self) -> str:
rotation = f'{self.rotation*180/pi:g}' if self.rotation != 0 else '' rotation = f'{self.rotation*180/pi:g}' if self.rotation != 0 else ''
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked 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}{dose}{locked}>' return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}{dose}>'

@ -14,9 +14,10 @@ from numpy.typing import NDArray, ArrayLike
from .error import PatternError from .error import PatternError
from .utils import is_scalar, AutoSlots, annotations_t from .utils import is_scalar, AutoSlots, annotations_t
from .repetition import Repetition from .repetition import Repetition
from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, from .traits import (
Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl, PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
AnnotatableImpl) Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl
)
if TYPE_CHECKING: if TYPE_CHECKING:
@ -27,16 +28,17 @@ S = TypeVar('S', bound='SubPattern')
class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable,
PivotableImpl, Copyable, RepeatableImpl, LockableImpl, AnnotatableImpl, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
metaclass=AutoSlots): metaclass=AutoSlots):
""" """
SubPattern provides basic support for nesting Pattern objects within each other, by adding SubPattern provides basic support for nesting Pattern objects within each other, by adding
offset, rotation, scaling, and associated methods. offset, rotation, scaling, and associated methods.
""" """
__slots__ = ('_pattern', __slots__ = (
'_mirrored', '_pattern',
'identifier', '_mirrored',
) 'identifier',
)
_pattern: Optional['Pattern'] _pattern: Optional['Pattern']
""" The `Pattern` being instanced """ """ The `Pattern` being instanced """
@ -58,7 +60,6 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
scale: float = 1.0, scale: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
locked: bool = False,
identifier: Tuple[Any, ...] = (), identifier: Tuple[Any, ...] = (),
) -> None: ) -> None:
""" """
@ -70,10 +71,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
dose: Scaling factor applied to the dose. dose: Scaling factor applied to the dose.
scale: Scaling factor applied to the pattern's geometry. scale: Scaling factor applied to the pattern's geometry.
repetition: TODO repetition: TODO
locked: Whether the `SubPattern` is locked after initialization.
identifier: Arbitrary tuple, used internally by some `masque` functions. identifier: Arbitrary tuple, used internally by some `masque` functions.
""" """
LockableImpl.unlock(self)
self.identifier = identifier self.identifier = identifier
self.pattern = pattern self.pattern = pattern
self.offset = offset self.offset = offset
@ -85,28 +84,18 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
self.mirrored = mirrored self.mirrored = mirrored
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.set_locked(locked)
def __copy__(self) -> 'SubPattern': def __copy__(self) -> 'SubPattern':
new = SubPattern(pattern=self.pattern, new = SubPattern(
offset=self.offset.copy(), pattern=self.pattern,
rotation=self.rotation, offset=self.offset.copy(),
dose=self.dose, rotation=self.rotation,
scale=self.scale, dose=self.dose,
mirrored=self.mirrored.copy(), scale=self.scale,
repetition=copy.deepcopy(self.repetition), mirrored=self.mirrored.copy(),
annotations=copy.deepcopy(self.annotations), repetition=copy.deepcopy(self.repetition),
locked=self.locked) annotations=copy.deepcopy(self.annotations),
return new )
def __deepcopy__(self, memo: Dict = None) -> 'SubPattern':
memo = {} if memo is None else memo
new = copy.copy(self)
LockableImpl.unlock(new)
new.pattern = copy.deepcopy(self.pattern, memo)
new.repetition = copy.deepcopy(self.repetition, memo)
new.annotations = copy.deepcopy(self.annotations, memo)
new.set_locked(self.locked)
return new return new
# pattern property # pattern property
@ -139,7 +128,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
`SubPattern`'s properties. `SubPattern`'s properties.
""" """
assert(self.pattern is not None) assert(self.pattern is not None)
pattern = self.pattern.deepcopy().deepunlock() pattern = self.pattern.deepcopy()
if self.scale != 1: if self.scale != 1:
pattern.scale_by(self.scale) pattern.scale_by(self.scale)
if numpy.any(self.mirrored): if numpy.any(self.mirrored):
@ -187,62 +176,10 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
return None return None
return self.as_pattern().get_bounds() return self.as_pattern().get_bounds()
def lock(self: S) -> S:
"""
Lock the SubPattern, disallowing changes
Returns:
self
"""
self.mirrored.flags.writeable = False
PositionableImpl._lock(self)
LockableImpl.lock(self)
return self
def unlock(self: S) -> S:
"""
Unlock the SubPattern
Returns:
self
"""
LockableImpl.unlock(self)
PositionableImpl._unlock(self)
self.mirrored.flags.writeable = True
return self
def deeplock(self: S) -> S:
"""
Recursively lock the SubPattern and its contained pattern
Returns:
self
"""
assert(self.pattern is not None)
self.lock()
self.pattern.deeplock()
return self
def deepunlock(self: S) -> S:
"""
Recursively unlock the SubPattern and its contained pattern
This is dangerous unless you have just performed a deepcopy, since
the subpattern and its components may be used in more than one once!
Returns:
self
"""
assert(self.pattern is not None)
self.unlock()
self.pattern.deepunlock()
return self
def __repr__(self) -> str: def __repr__(self) -> str:
name = self.pattern.name if self.pattern is not None else None name = self.pattern.name if self.pattern is not None else None
rotation = f' r{self.rotation*180/pi: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 ''
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else '' return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}>'
return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>'

@ -9,5 +9,4 @@ from .repeatable import Repeatable, RepeatableImpl
from .scalable import Scalable, ScalableImpl from .scalable import Scalable, ScalableImpl
from .mirrorable import Mirrorable from .mirrorable import Mirrorable
from .copyable import Copyable from .copyable import Copyable
from .lockable import Lockable, LockableImpl
from .annotatable import Annotatable, AnnotatableImpl from .annotatable import Annotatable, AnnotatableImpl

@ -44,9 +44,6 @@ class AnnotatableImpl(Annotatable, metaclass=ABCMeta):
@property @property
def annotations(self) -> annotations_t: def annotations(self) -> annotations_t:
return self._annotations return self._annotations
# # TODO: Find a way to make sure the subclass implements Lockable without dealing with diamond inheritance or this extra hasattr
# if hasattr(self, 'is_locked') and self.is_locked():
# return MappingProxyType(self._annotations)
@annotations.setter @annotations.setter
def annotations(self, annotations: annotations_t): def annotations(self, annotations: annotations_t):

@ -1,103 +0,0 @@
from typing import TypeVar, Dict, Tuple, Any
from abc import ABCMeta, abstractmethod
from ..error import PatternLockedError
T = TypeVar('T', bound='Lockable')
I = TypeVar('I', bound='LockableImpl')
class Lockable(metaclass=ABCMeta):
"""
Abstract class for all lockable entities
"""
__slots__ = () # type: Tuple[str, ...]
'''
---- Methods
'''
@abstractmethod
def lock(self: T) -> T:
"""
Lock the object, disallowing further changes
Returns:
self
"""
pass
@abstractmethod
def unlock(self: T) -> T:
"""
Unlock the object, reallowing changes
Returns:
self
"""
pass
@abstractmethod
def is_locked(self) -> bool:
"""
Returns:
True if the object is locked
"""
pass
def set_locked(self: T, locked: bool) -> T:
"""
Locks or unlocks based on the argument.
No action if already in the requested state.
Args:
locked: State to set.
Returns:
self
"""
if locked != self.is_locked():
if locked:
self.lock()
else:
self.unlock()
return self
class LockableImpl(Lockable, metaclass=ABCMeta):
"""
Simple implementation of Lockable
"""
__slots__ = () # type: Tuple[str, ...]
locked: bool
""" If `True`, disallows changes to the object """
'''
---- Non-abstract methods
'''
def __setattr__(self, name, value):
if self.locked and name != 'locked':
raise PatternLockedError()
object.__setattr__(self, name, value)
def __getstate__(self) -> Dict[str, Any]:
if hasattr(self, '__slots__'):
return {key: getattr(self, key) for key in self.__slots__}
else:
return self.__dict__
def __setstate__(self, state: Dict[str, Any]) -> None:
for k, v in state.items():
object.__setattr__(self, k, v)
def lock(self: I) -> I:
object.__setattr__(self, 'locked', True)
return self
def unlock(self: I) -> I:
object.__setattr__(self, 'locked', False)
return self
def is_locked(self) -> bool:
return self.locked

@ -120,23 +120,3 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
def translate(self: I, offset: ArrayLike) -> I: def translate(self: I, offset: ArrayLike) -> I:
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine?? self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
return self return self
def _lock(self: I) -> I:
"""
Lock the entity, disallowing further changes
Returns:
self
"""
self._offset.flags.writeable = False
return self
def _unlock(self: I) -> I:
"""
Unlock the entity
Returns:
self
"""
self._offset.flags.writeable = True
return self

Loading…
Cancel
Save