add support for annotations

and other fixes
This commit is contained in:
Jan Petykiewicz 2020-09-10 20:06:58 -07:00
parent 5d83e0e5c0
commit 49a3b4e322
28 changed files with 400 additions and 133 deletions

View File

@ -17,7 +17,8 @@ def main():
pat.shapes.append(shapes.Arc( pat.shapes.append(shapes.Arc(
radii=(rmin, rmin), radii=(rmin, rmin),
width=0.1, width=0.1,
angles=(0*-numpy.pi/4, numpy.pi/4) angles=(0*-numpy.pi/4, numpy.pi/4),
annotations={'1': ['blah']},
)) ))
pat.scale_by(1000) pat.scale_by(1000)
@ -27,7 +28,7 @@ def main():
pat3 = Pattern('sref_test') pat3 = Pattern('sref_test')
pat3.subpatterns = [ pat3.subpatterns = [
SubPattern(pat, offset=(1e5, 3e5)), SubPattern(pat, offset=(1e5, 3e5), annotations={'4': ['Hello I am the base subpattern']}),
SubPattern(pat, offset=(2e5, 3e5), rotation=pi/3), SubPattern(pat, offset=(2e5, 3e5), rotation=pi/3),
SubPattern(pat, offset=(3e5, 3e5), rotation=pi/2), SubPattern(pat, offset=(3e5, 3e5), rotation=pi/2),
SubPattern(pat, offset=(4e5, 3e5), rotation=pi), SubPattern(pat, offset=(4e5, 3e5), rotation=pi),

View File

@ -31,7 +31,7 @@ from .shapes import Shape
from .label import Label from .label import Label
from .subpattern import SubPattern from .subpattern import SubPattern
from .pattern import Pattern from .pattern import Pattern
from .utils import layer_t from .utils import layer_t, annotations_t
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'

View File

@ -10,10 +10,10 @@ import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
import numpy
from numpy import pi
import ezdxf import numpy # type: ignore
from numpy import pi
import ezdxf # type: ignore
from .utils import mangle_name, make_dose_table from .utils import mangle_name, make_dose_table
from .. import Pattern, SubPattern, PatternError, Label, Shape from .. import Pattern, SubPattern, PatternError, Label, Shape
@ -264,12 +264,11 @@ def _read_block(block, clean_vertices):
} }
if 'column_count' in attr: if 'column_count' in attr:
args['a_vector'] = (attr['column_spacing'], 0) args['repetition'] = Grid(
args['b_vector'] = (0, attr['row_spacing']) a_vector=(attr['column_spacing'], 0),
args['a_count'] = attr['column_count'] b_vector=(0, attr['row_spacing']),
args['b_count'] = attr['row_count'] a_count=attr['column_count'],
pat.subpatterns.append(GridRepetition(**args)) b_count=attr['row_count'])
else:
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).')

View File

@ -11,17 +11,18 @@ Note that GDSII references follow the same convention as `masque`,
Scaling, rotation, and mirroring apply to individual instances, not grid Scaling, rotation, and mirroring apply to individual instances, not grid
vectors or offsets. vectors or offsets.
""" """
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional, Sequence from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
from typing import Sequence, Mapping
import re import re
import io import io
import copy import copy
import numpy
import base64 import base64
import struct import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
import numpy # type: ignore
# python-gdsii # python-gdsii
import gdsii.library import gdsii.library
import gdsii.structure import gdsii.structure
@ -32,7 +33,7 @@ from .. import Pattern, SubPattern, PatternError, Label, Shape
from ..shapes import Polygon, Path from ..shapes import Polygon, Path
from ..repetition import Grid from ..repetition import Grid
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror from ..utils import remove_colinear_vertices, normalize_mirror, annotations_t
#TODO absolute positioning #TODO absolute positioning
@ -99,6 +100,7 @@ def build(patterns: Union[Pattern, List[Pattern]],
if disambiguate_func is None: if disambiguate_func is None:
disambiguate_func = disambiguate_pattern_names disambiguate_func = disambiguate_pattern_names
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 = [p.deepunlock() for p in copy.deepcopy(patterns)]
@ -124,6 +126,8 @@ def build(patterns: Union[Pattern, List[Pattern]],
structure = gdsii.structure.Structure(name=pat.name.encode('ASCII')) structure = gdsii.structure.Structure(name=pat.name.encode('ASCII'))
lib.append(structure) lib.append(structure)
structure.properties = _annotations_to_properties(pat.annotations, 512)
structure += _shapes_to_elements(pat.shapes) structure += _shapes_to_elements(pat.shapes)
structure += _labels_to_texts(pat.labels) structure += _labels_to_texts(pat.labels)
structure += _subpatterns_to_refs(pat.subpatterns) structure += _subpatterns_to_refs(pat.subpatterns)
@ -238,6 +242,9 @@ def read(stream: io.BufferedIOBase,
patterns = [] patterns = []
for structure in lib: for structure in lib:
pat = Pattern(name=structure.name.decode('ASCII')) pat = Pattern(name=structure.name.decode('ASCII'))
if pat.annotations:
logger.warning('Dropping Pattern-level annotations; they are not supported by python-gdsii')
# pat.annotations = {str(k): v for k, v in structure.properties}
for element in structure: for element in structure:
# Switch based on element type: # Switch based on element type:
if isinstance(element, gdsii.elements.Boundary): if isinstance(element, gdsii.elements.Boundary):
@ -343,6 +350,7 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef,
rotation=rotation, rotation=rotation,
scale=scale, scale=scale,
mirrored=(mirror_across_x, False), mirrored=(mirror_across_x, False),
annotations=_properties_to_annotations(element.properties),
repetition=repetition) repetition=repetition)
subpat.identifier = (element.struct_name,) subpat.identifier = (element.struct_name,)
return subpat return subpat
@ -359,6 +367,7 @@ def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path:
'width': element.width if element.width is not None else 0.0, 'width': element.width if element.width is not None else 0.0,
'cap': cap, 'cap': cap,
'offset': numpy.zeros(2), 'offset': numpy.zeros(2),
'annotations':_properties_to_annotations(element.properties),
'raw': raw_mode, 'raw': raw_mode,
} }
@ -376,6 +385,7 @@ def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Po
args = {'vertices': element.xy[:-1].astype(float), args = {'vertices': element.xy[:-1].astype(float),
'layer': (element.layer, element.data_type), 'layer': (element.layer, element.data_type),
'offset': numpy.zeros(2), 'offset': numpy.zeros(2),
'annotations':_properties_to_annotations(element.properties),
'raw': raw_mode, 'raw': raw_mode,
} }
return Polygon(**args) return Polygon(**args)
@ -420,11 +430,38 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]
# strans must be non-None for angle and mag to take effect # strans must be non-None for angle and mag to take effect
ref.strans = set_bit(0, 15 - 0, mirror_across_x) ref.strans = set_bit(0, 15 - 0, mirror_across_x)
ref.mag = subpat.scale ref.mag = subpat.scale
ref.properties = _annotations_to_properties(subpat.annotations, 512)
refs += new_refs refs += new_refs
return refs return refs
def _properties_to_annotations(properties: List[Tuple[int, bytes]]) -> annotations_t:
return {str(k): [v.decode()] for k, v in properties}
def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> List[Tuple[int, bytes]]:
cum_len = 0
props = []
for key, vals in annotations.items():
try:
i = int(key)
except:
raise PatternError(f'Annotation key {key} is not convertable to an integer')
if not (0 < i < 126):
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
val_strings = ' '.join(str(val) for val in vals)
b = val_strings.encode()
if len(b) > 126:
raise PatternError(f'Annotation value {b!r} is longer than 126 characters!')
cum_len += numpy.ceil(len(b) / 2) * 2 + 2
if cum_len > max_len:
raise PatternError(f'Sum of annotation data will be longer than {max_len} bytes! Generated bytes were {b!r}')
props.append((i, b))
return props
def _shapes_to_elements(shapes: List[Shape], def _shapes_to_elements(shapes: List[Shape],
polygonize_paths: bool = False polygonize_paths: bool = False
) -> List[Union[gdsii.elements.Boundary, gdsii.elements.Path]]: ) -> List[Union[gdsii.elements.Boundary, gdsii.elements.Path]]:
@ -432,6 +469,7 @@ def _shapes_to_elements(shapes: List[Shape],
# Add a Boundary element for each shape, and Path elements if necessary # Add a Boundary element for each shape, and Path elements if necessary
for shape in shapes: for shape in shapes:
layer, data_type = _mlayer2gds(shape.layer) layer, data_type = _mlayer2gds(shape.layer)
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 = numpy.round(shape.vertices + shape.offset).astype(int)
width = numpy.round(shape.width).astype(int) width = numpy.round(shape.width).astype(int)
@ -441,26 +479,32 @@ def _shapes_to_elements(shapes: List[Shape],
xy=xy) xy=xy)
path.path_type = path_type path.path_type = path_type
path.width = width path.width = width
path.properties = properties
elements.append(path) elements.append(path)
else: else:
for polygon in shape.to_polygons(): for polygon in shape.to_polygons():
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
xy_closed = numpy.vstack((xy_open, xy_open[0, :])) xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
elements.append(gdsii.elements.Boundary(layer=layer, boundary = gdsii.elements.Boundary(layer=layer,
data_type=data_type, data_type=data_type,
xy=xy_closed)) xy=xy_closed)
boundary.properties = properties
elements.append(boundary)
return elements return elements
def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
texts = [] texts = []
for label in labels: for label in labels:
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 = numpy.round([label.offset]).astype(int)
texts.append(gdsii.elements.Text(layer=layer, text = gdsii.elements.Text(layer=layer,
text_type=text_type, text_type=text_type,
xy=xy, xy=xy,
string=label.string.encode('ASCII'))) string=label.string.encode('ASCII'))
text.properties = properties
texts.append(text)
return texts return texts

View File

@ -20,19 +20,19 @@ import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
import numpy
from numpy import pi
import numpy # type: ignore
from numpy import pi
import fatamorgana import fatamorgana
import fatamorgana.records as fatrec import fatamorgana.records as fatrec
from fatamorgana.basic import PathExtensionScheme from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
from .utils import mangle_name, make_dose_table from .utils import mangle_name, make_dose_table
from .. import Pattern, SubPattern, PatternError, Label, Shape from .. import Pattern, SubPattern, PatternError, Label, Shape
from ..shapes import Polygon, Path, Circle from ..shapes import Polygon, Path, Circle
from ..repetition import Grid, Arbitrary, Repetition from ..repetition import Grid, Arbitrary, Repetition
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror from ..utils import remove_colinear_vertices, normalize_mirror, annotations_t
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,9 +52,11 @@ path_cap_map = {
def build(patterns: Union[Pattern, List[Pattern]], def build(patterns: Union[Pattern, List[Pattern]],
units_per_micron: int, units_per_micron: int,
layer_map: Dict[str, Union[int, Tuple[int, int]]] = None, layer_map: Optional[Dict[str, Union[int, Tuple[int, int]]]] = None,
*,
modify_originals: bool = False, modify_originals: bool = False,
disambiguate_func: Callable[[Iterable[Pattern]], None] = None, disambiguate_func: Optional[Callable[[Iterable[Pattern]], None]] = None,
annotations: Optional[annotations_t] = None
) -> fatamorgana.OasisLayout: ) -> fatamorgana.OasisLayout:
""" """
Convert a `Pattern` or list of patterns to an OASIS stream, writing patterns Convert a `Pattern` or list of patterns to an OASIS stream, writing patterns
@ -91,6 +93,7 @@ def build(patterns: Union[Pattern, List[Pattern]],
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`.
annotations: dictionary of key-value pairs which are saved as library-level properties
Returns: Returns:
`fatamorgana.OasisLayout` `fatamorgana.OasisLayout`
@ -104,11 +107,15 @@ def build(patterns: Union[Pattern, List[Pattern]],
if disambiguate_func is None: if disambiguate_func is None:
disambiguate_func = disambiguate_pattern_names disambiguate_func = disambiguate_pattern_names
if annotations is None:
annotations = {}
if not modify_originals: if not modify_originals:
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] patterns = [p.deepunlock() for p in 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)
lib.properties = annotations_to_properties(annotations)
if layer_map: if layer_map:
for name, layer_num in layer_map.items(): for name, layer_num in layer_map.items():
@ -139,9 +146,11 @@ def build(patterns: Union[Pattern, List[Pattern]],
structure = fatamorgana.Cell(name=pat.name) structure = fatamorgana.Cell(name=pat.name)
lib.cells.append(structure) lib.cells.append(structure)
structure.properties += annotations_to_properties(pat.annotations)
structure.geometry += _shapes_to_elements(pat.shapes, layer2oas) structure.geometry += _shapes_to_elements(pat.shapes, layer2oas)
structure.geometry += _labels_to_texts(pat.labels, layer2oas) structure.geometry += _labels_to_texts(pat.labels, layer2oas)
structure.placements += _subpatterns_to_refs(pat.subpatterns) structure.placements += _subpatterns_to_placements(pat.subpatterns)
return lib return lib
@ -226,6 +235,8 @@ def read(stream: io.BufferedIOBase,
Additional library info is returned in a dict, containing: Additional library info is returned in a dict, containing:
'units_per_micrometer': number of database units per micrometer (all values are in database units) 'units_per_micrometer': number of database units per micrometer (all values are in database units)
'layer_map': Mapping from layer names to fatamorgana.LayerName objects
'annotations': Mapping of {key: value} pairs from library's properties
Args: Args:
stream: Stream to read from. stream: Stream to read from.
@ -242,6 +253,7 @@ def read(stream: io.BufferedIOBase,
library_info: Dict[str, Any] = { library_info: Dict[str, Any] = {
'units_per_micrometer': lib.unit, 'units_per_micrometer': lib.unit,
'annotations': properties_to_annotations(lib.properties, lib.propnames, lib.propstrings),
} }
layer_map = {} layer_map = {}
@ -252,7 +264,7 @@ def read(stream: io.BufferedIOBase,
patterns = [] patterns = []
for cell in lib.cells: for cell in lib.cells:
if isinstance(cell.name, int): if isinstance(cell.name, int):
cell_name = lib.cellnames[cell.name].string cell_name = lib.cellnames[cell.name].nstring.string
else: else:
cell_name = cell.name.string cell_name = cell.name.string
@ -263,15 +275,16 @@ def read(stream: io.BufferedIOBase,
# note XELEMENT has no repetition # note XELEMENT has no repetition
continue continue
repetition = repetition_fata2masq(element.repetition) repetition = repetition_fata2masq(element.repetition)
# Switch based on element type: # Switch based on element type:
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)
poly = Polygon(vertices=vertices, poly = Polygon(vertices=vertices,
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
annotations=annotations,
repetition=repetition) repetition=repetition)
if clean_vertices: if clean_vertices:
@ -295,10 +308,13 @@ def read(stream: io.BufferedIOBase,
if cap == Path.Cap.SquareCustom: if cap == Path.Cap.SquareCustom:
path_args['cap_extensions'] = numpy.array((element.get_extension_start()[1], path_args['cap_extensions'] = numpy.array((element.get_extension_start()[1],
element.get_extension_end()[1])) element.get_extension_end()[1]))
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
path = Path(vertices=vertices, path = Path(vertices=vertices,
layer=element.get_layer_tuple(), layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
annotations=annotations,
width=element.get_half_width() * 2, width=element.get_half_width() * 2,
cap=cap, cap=cap,
**path_args) **path_args)
@ -314,10 +330,12 @@ def read(stream: io.BufferedIOBase,
elif isinstance(element, fatrec.Rectangle): elif isinstance(element, fatrec.Rectangle):
width = element.get_width() width = element.get_width()
height = element.get_height() height = element.get_height()
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
rect = Polygon(layer=element.get_layer_tuple(), rect = Polygon(layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
annotations=annotations,
) )
pat.shapes.append(rect) pat.shapes.append(rect)
@ -346,10 +364,12 @@ def read(stream: io.BufferedIOBase,
else: else:
vertices[2, 0] -= b vertices[2, 0] -= b
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
trapz = Polygon(layer=element.get_layer_tuple(), trapz = Polygon(layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
vertices=vertices, vertices=vertices,
annotations=annotations,
) )
pat.shapes.append(trapz) pat.shapes.append(trapz)
@ -399,24 +419,30 @@ def read(stream: io.BufferedIOBase,
vertices = vertices[[0, 2, 3], :] vertices = vertices[[0, 2, 3], :]
vertices[0, 1] += width vertices[0, 1] += width
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
ctrapz = Polygon(layer=element.get_layer_tuple(), ctrapz = Polygon(layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
vertices=vertices, 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)
circle = Circle(layer=element.get_layer_tuple(), circle = Circle(layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
annotations=annotations,
radius=float(element.get_radius())) radius=float(element.get_radius()))
pat.shapes.append(circle) pat.shapes.append(circle)
elif isinstance(element, fatrec.Text): elif isinstance(element, fatrec.Text):
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
label = Label(layer=element.get_layer_tuple(), label = Label(layer=element.get_layer_tuple(),
offset=element.get_xy(), offset=element.get_xy(),
repetition=repetition, repetition=repetition,
annotations=annotations,
string=str(element.get_string())) string=str(element.get_string()))
pat.labels.append(label) pat.labels.append(label)
@ -425,7 +451,7 @@ def read(stream: io.BufferedIOBase,
continue continue
for placement in cell.placements: for placement in cell.placements:
pat.subpatterns.append(_placement_to_subpat(placement)) pat.subpatterns.append(_placement_to_subpat(placement, lib))
patterns.append(pat) patterns.append(pat)
@ -435,7 +461,7 @@ def read(stream: io.BufferedIOBase,
for p in patterns_dict.values(): for p in patterns_dict.values():
for sp in p.subpatterns: for sp in p.subpatterns:
ident = sp.identifier[0] ident = sp.identifier[0]
name = ident if isinstance(ident, str) else lib.cellnames[ident].string name = ident if isinstance(ident, str) else lib.cellnames[ident].nstring.string
sp.pattern = patterns_dict[name] sp.pattern = patterns_dict[name]
del sp.identifier del sp.identifier
@ -459,7 +485,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
return layer, data_type return layer, data_type
def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern:
""" """
Helper function to create a SubPattern from a placment. Sets subpat.pattern to None Helper function to create a SubPattern from a placment. Sets subpat.pattern to None
and sets the instance .identifier to (struct_name,). and sets the instance .identifier to (struct_name,).
@ -468,20 +494,19 @@ def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern:
mag = placement.magnification if placement.magnification is not None else 1 mag = placement.magnification if placement.magnification is not None else 1
pname = placement.get_name() pname = placement.get_name()
name = pname if isinstance(pname, int) else pname.string name = pname if isinstance(pname, int) else pname.string
args: Dict[str, Any] = { annotations = properties_to_annotations(placement.properties, lib.propnames, lib.propstrings)
'pattern': None, subpat = SubPattern(offset=xy,
'mirrored': (placement.flip, False), pattern=None,
'rotation': float(placement.angle * pi/180), mirrored=(placement.flip, False),
'scale': mag, rotation=float(placement.angle * pi/180),
'identifier': (name,), scale=float(mag),
'repetition': repetition_fata2masq(placement.repetition), identifier=(name,),
} repetition=repetition_fata2masq(placement.repetition),
annotations=annotations)
subpat = SubPattern(offset=xy, **args)
return subpat return subpat
def _subpatterns_to_refs(subpatterns: List[SubPattern] def _subpatterns_to_placements(subpatterns: List[SubPattern]
) -> List[fatrec.Placement]: ) -> List[fatrec.Placement]:
refs = [] refs = []
for subpat in subpatterns: for subpat in subpatterns:
@ -493,19 +518,16 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]
frep, rep_offset = repetition_masq2fata(subpat.repetition) frep, rep_offset = repetition_masq2fata(subpat.repetition)
offset = numpy.round(subpat.offset + rep_offset).astype(int) offset = numpy.round(subpat.offset + rep_offset).astype(int)
args: Dict[str, Any] = {
'x': offset[0],
'y': offset[1],
'repetition': frep,
}
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,
flip=mirror_across_x, flip=mirror_across_x,
angle=angle, angle=angle,
magnification=subpat.scale, magnification=subpat.scale,
**args) properties=annotations_to_properties(subpat.annotations),
x=offset[0],
y=offset[1],
repetition=frep)
refs.append(ref) refs.append(ref)
return refs return refs
@ -519,6 +541,7 @@ def _shapes_to_elements(shapes: List[Shape],
for shape in shapes: for shape in shapes:
layer, datatype = layer2oas(shape.layer) layer, datatype = layer2oas(shape.layer)
repetition, rep_offset = repetition_masq2fata(shape.repetition) repetition, rep_offset = repetition_masq2fata(shape.repetition)
properties = annotations_to_properties(shape.annotations)
if isinstance(shape, Circle): if isinstance(shape, Circle):
offset = numpy.round(shape.offset + rep_offset).astype(int) offset = numpy.round(shape.offset + rep_offset).astype(int)
radius = numpy.round(shape.radius).astype(int) radius = numpy.round(shape.radius).astype(int)
@ -527,6 +550,7 @@ def _shapes_to_elements(shapes: List[Shape],
radius=radius, radius=radius,
x=offset[0], x=offset[0],
y=offset[1], y=offset[1],
properties=properties,
repetition=repetition) repetition=repetition)
elements.append(circle) elements.append(circle)
elif isinstance(shape, Path): elif isinstance(shape, Path):
@ -544,6 +568,7 @@ def _shapes_to_elements(shapes: List[Shape],
y=xy[1], y=xy[1],
extension_start=extension_start, #TODO implement multiple cap types? extension_start=extension_start, #TODO implement multiple cap types?
extension_end=extension_end, extension_end=extension_end,
properties=properties,
repetition=repetition, repetition=repetition,
) )
elements.append(path) elements.append(path)
@ -556,6 +581,7 @@ def _shapes_to_elements(shapes: List[Shape],
x=xy[0], x=xy[0],
y=xy[1], y=xy[1],
point_list=points, point_list=points,
properties=properties,
repetition=repetition)) repetition=repetition))
return elements return elements
@ -568,11 +594,13 @@ def _labels_to_texts(labels: List[Label],
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 = numpy.round(label.offset + rep_offset).astype(int)
properties = annotations_to_properties(label.annotations)
texts.append(fatrec.Text(layer=layer, texts.append(fatrec.Text(layer=layer,
datatype=datatype, datatype=datatype,
x=xy[0], x=xy[0],
y=xy[1], y=xy[1],
string=label.string, string=label.string,
properties=properties,
repetition=repetition)) repetition=repetition))
return texts return texts
@ -609,6 +637,7 @@ def disambiguate_pattern_names(patterns,
def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None] def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None]
) -> Optional[Repetition]: ) -> Optional[Repetition]:
mrep: Optional[Repetition]
if isinstance(rep, fatamorgana.GridRepetition): if isinstance(rep, fatamorgana.GridRepetition):
mrep = Grid(a_vector=rep.a_vector, mrep = Grid(a_vector=rep.a_vector,
b_vector=rep.b_vector, b_vector=rep.b_vector,
@ -624,7 +653,12 @@ def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.Arbi
return mrep return mrep
def repetition_masq2fata(rep: Optional[Repetition]): def repetition_masq2fata(rep: Optional[Repetition]
) -> Tuple[Union[fatamorgana.GridRepetition,
fatamorgana.ArbitraryRepetition,
None],
Tuple[int, int]]:
frep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None]
if isinstance(rep, Grid): if isinstance(rep, Grid):
frep = fatamorgana.GridRepetition( frep = fatamorgana.GridRepetition(
a_vector=numpy.round(rep.a_vector).astype(int), a_vector=numpy.round(rep.a_vector).astype(int),
@ -642,3 +676,46 @@ def repetition_masq2fata(rep: Optional[Repetition]):
frep = None frep = None
offset = (0, 0) offset = (0, 0)
return frep, offset return frep, offset
def annotations_to_properties(annotations: annotations_t) -> List[fatrec.Property]:
#TODO determine is_standard based on key?
properties = []
for key, values in annotations.items():
vals = [AString(v) if isinstance(v, str) else v
for v in values]
properties.append(fatrec.Property(key, vals, is_standard=False))
return properties
def properties_to_annotations(properties: List[fatrec.Property],
propnames: Dict[int, NString],
propstrings: Dict[int, AString],
) -> annotations_t:
annotations = {}
for proprec in properties:
assert(proprec.name is not None)
if isinstance(proprec.name, int):
key = propnames[proprec.name].string
else:
key = proprec.name.string
values: List[Union[str, float, int]] = []
assert(proprec.values is not None)
for value in proprec.values:
if isinstance(value, (float, int)):
values.append(value)
elif isinstance(value, (NString, AString)):
values.append(value.string)
elif isinstance(value, PropStringReference):
values.append(propstrings[value.ref].string) # dereference
else:
string = repr(value)
logger.warning(f'Converting property value for key ({key}) to string ({string})')
values.append(string)
annotations[key] = values
return annotations
properties = [fatrec.Property(key, vals, is_standard=False)
for key, vals in annotations.items()]
return properties

View File

@ -2,10 +2,11 @@
SVG file format readers and writers SVG file format readers and writers
""" """
from typing import Dict, Optional from typing import Dict, Optional
import svgwrite
import numpy
import warnings import warnings
import numpy # type: ignore
import svgwrite # type: ignore
from .utils import mangle_name from .utils import mangle_name
from .. import Pattern from .. import Pattern

View File

@ -1,15 +1,16 @@
from typing import List, Tuple, Dict, Optional from typing import List, Tuple, Dict, Optional
import copy import copy
import numpy import numpy # type: ignore
from numpy import pi from numpy import pi
from .repetition import Repetition from .repetition import Repetition
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots, annotations_t
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl
from .traits import AnnotatableImpl
class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, class Label(PositionableImpl, LayerableImpl, LockableImpl, 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)
@ -42,14 +43,17 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl,
offset: vector2 = (0.0, 0.0), offset: vector2 = (0.0, 0.0),
layer: layer_t = 0, layer: layer_t = 0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
locked: bool = False): annotations: Optional[annotations_t] = None,
object.__setattr__(self, 'locked', False) locked: bool = False,
):
LockableImpl.unlock(self)
self.identifier = () self.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.locked = locked self.annotations = annotations if annotations is not None else {}
self.set_locked(locked)
def __copy__(self) -> 'Label': def __copy__(self) -> 'Label':
return Label(string=self.string, return Label(string=self.string,
@ -62,7 +66,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl,
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new._offset = self._offset.copy() new._offset = self._offset.copy()
new.locked = self.locked new.set_locked(self.locked)
return new return new
def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': def rotate_around(self, pivot: vector2, rotation: float) -> 'Label':

View File

@ -9,26 +9,27 @@ import itertools
import pickle import pickle
from collections import defaultdict from collections import defaultdict
import numpy import numpy # type: ignore
from numpy import inf from numpy import inf
# .visualize imports matplotlib and matplotlib.collections # .visualize imports matplotlib and matplotlib.collections
from .subpattern import SubPattern 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, vector2, normalize_mirror from .utils import rotation_matrix_2d, vector2, normalize_mirror, AutoSlots, annotations_t
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .traits import LockableImpl, AnnotatableImpl
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern'] visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern']
class Pattern: class Pattern(LockableImpl, AnnotatableImpl, 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.
""" """
__slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') __slots__ = ('shapes', 'labels', 'subpatterns', 'name')
shapes: List[Shape] shapes: List[Shape]
""" List of all shapes in this Pattern. """ List of all shapes in this Pattern.
@ -47,14 +48,12 @@ class Pattern:
name: str name: str
""" A name for this pattern """ """ A name for this pattern """
locked: bool
""" When the pattern is locked, no changes may be made. """
def __init__(self, def __init__(self,
name: str = '', name: str = '',
shapes: Sequence[Shape] = (), shapes: Sequence[Shape] = (),
labels: Sequence[Label] = (), labels: Sequence[Label] = (),
subpatterns: Sequence[SubPattern] = (), subpatterns: Sequence[SubPattern] = (),
annotations: Optional[annotations_t] = None,
locked: bool = False, locked: bool = False,
): ):
""" """
@ -68,7 +67,7 @@ class Pattern:
name: An identifier for the Pattern name: An identifier for the Pattern
locked: Whether to lock the pattern after construction locked: Whether to lock the pattern after construction
""" """
object.__setattr__(self, 'locked', False) LockableImpl.unlock(self)
if isinstance(shapes, list): if isinstance(shapes, list):
self.shapes = shapes self.shapes = shapes
else: else:
@ -84,8 +83,9 @@ class Pattern:
else: else:
self.subpatterns = list(subpatterns) self.subpatterns = list(subpatterns)
self.annotations = annotations if annotations is not None else {}
self.name = name self.name = name
self.locked = locked self.set_locked(locked)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if self.locked and name != 'locked': if self.locked and name != 'locked':
@ -97,6 +97,7 @@ class Pattern:
shapes=copy.deepcopy(self.shapes), shapes=copy.deepcopy(self.shapes),
labels=copy.deepcopy(self.labels), labels=copy.deepcopy(self.labels),
subpatterns=[copy.copy(sp) for sp in self.subpatterns], subpatterns=[copy.copy(sp) for sp in self.subpatterns],
annotations=copy.deepcopy(self.annotations),
locked=self.locked) locked=self.locked)
def __deepcopy__(self, memo: Dict = None) -> 'Pattern': def __deepcopy__(self, memo: Dict = None) -> 'Pattern':
@ -105,6 +106,7 @@ class Pattern:
shapes=copy.deepcopy(self.shapes, memo), shapes=copy.deepcopy(self.shapes, memo),
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),
locked=self.locked) locked=self.locked)
return new return new
@ -815,7 +817,7 @@ class Pattern:
self.shapes = tuple(self.shapes) self.shapes = tuple(self.shapes)
self.labels = tuple(self.labels) self.labels = tuple(self.labels)
self.subpatterns = tuple(self.subpatterns) self.subpatterns = tuple(self.subpatterns)
object.__setattr__(self, 'locked', True) LockableImpl.lock(self)
return self return self
def unlock(self) -> 'Pattern': def unlock(self) -> 'Pattern':
@ -826,7 +828,7 @@ class Pattern:
self self
""" """
if self.locked: if self.locked:
object.__setattr__(self, 'locked', False) LockableImpl.unlock(self)
self.shapes = list(self.shapes) self.shapes = list(self.shapes)
self.labels = list(self.labels) self.labels = list(self.labels)
self.subpatterns = list(self.subpatterns) self.subpatterns = list(self.subpatterns)

View File

@ -7,7 +7,7 @@ from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING,
import copy import copy
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy import numpy # type: ignore
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .utils import rotation_matrix_2d, vector2, AutoSlots from .utils import rotation_matrix_2d, vector2, AutoSlots

View File

@ -1,13 +1,15 @@
from typing import List, Tuple, Dict, Optional, Sequence from typing import List, Tuple, Dict, Optional, Sequence
import copy import copy
import math import math
import numpy
import numpy # type: ignore
from numpy import pi from numpy import pi
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS 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, vector2, layer_t, AutoSlots from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t
from ..traits import LockableImpl
class Arc(Shape, metaclass=AutoSlots): class Arc(Shape, metaclass=AutoSlots):
@ -160,10 +162,11 @@ class Arc(Shape, metaclass=AutoSlots):
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
locked: bool = False, locked: bool = False,
raw: bool = False, raw: bool = False,
): ):
object.__setattr__(self, 'locked', False) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
self._radii = radii self._radii = radii
@ -172,6 +175,7 @@ class Arc(Shape, metaclass=AutoSlots):
self._offset = offset self._offset = offset
self._rotation = rotation self._rotation = rotation
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer self._layer = layer
self._dose = dose self._dose = dose
else: else:
@ -181,12 +185,13 @@ class Arc(Shape, metaclass=AutoSlots):
self.offset = offset self.offset = offset
self.rotation = rotation self.rotation = rotation
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
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.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
self.locked = locked self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Arc': def __deepcopy__(self, memo: Dict = None) -> 'Arc':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -194,7 +199,8 @@ class Arc(Shape, metaclass=AutoSlots):
new._offset = self._offset.copy() new._offset = self._offset.copy()
new._radii = self._radii.copy() new._radii = self._radii.copy()
new._angles = self._angles.copy() new._angles = self._angles.copy()
new.locked = self.locked new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new return new
def to_polygons(self, def to_polygons(self,

View File

@ -1,12 +1,14 @@
from typing import List, Dict, Optional from typing import List, Dict, Optional
import copy import copy
import numpy
import numpy # type: ignore
from numpy import pi from numpy import pi
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS 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, vector2, layer_t, AutoSlots from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t
from ..traits import LockableImpl
class Circle(Shape, metaclass=AutoSlots): class Circle(Shape, metaclass=AutoSlots):
@ -48,23 +50,36 @@ class Circle(Shape, metaclass=AutoSlots):
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
locked: bool = False): annotations: Optional[annotations_t] = None,
object.__setattr__(self, 'locked', False) locked: bool = False,
raw: bool = False,
):
LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
self.offset = numpy.array(offset, dtype=float) if raw:
self._radius = radius
self._offset = offset
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._dose = dose
else:
self.radius = radius
self.offset = offset
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
self.dose = dose self.dose = dose
self.radius = radius
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.repetition = repetition self.set_locked(locked)
self.locked = locked
def __deepcopy__(self, memo: Dict = None) -> 'Circle': def __deepcopy__(self, memo: Dict = None) -> 'Circle':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new._offset = self._offset.copy() new._offset = self._offset.copy()
new.locked = self.locked new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new return new
def to_polygons(self, def to_polygons(self,

View File

@ -1,13 +1,15 @@
from typing import List, Tuple, Dict, Sequence, Optional from typing import List, Tuple, Dict, Sequence, Optional
import copy import copy
import math import math
import numpy
import numpy # type: ignore
from numpy import pi from numpy import pi
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS 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, vector2, layer_t, AutoSlots from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots, annotations_t
from ..traits import LockableImpl
class Ellipse(Shape, metaclass=AutoSlots): class Ellipse(Shape, metaclass=AutoSlots):
@ -95,16 +97,18 @@ class Ellipse(Shape, metaclass=AutoSlots):
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
locked: bool = False, locked: bool = False,
raw: bool = False, raw: bool = False,
): ):
object.__setattr__(self, 'locked', False) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
self._radii = radii self._radii = radii
self._offset = offset self._offset = offset
self._rotation = rotation self._rotation = rotation
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer self._layer = layer
self._dose = dose self._dose = dose
else: else:
@ -112,19 +116,21 @@ class Ellipse(Shape, metaclass=AutoSlots):
self.offset = offset self.offset = offset
self.rotation = rotation self.rotation = rotation
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
self.dose = dose self.dose = dose
[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.locked = locked self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': def __deepcopy__(self, memo: Dict = None) -> 'Ellipse':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new._offset = self._offset.copy() new._offset = self._offset.copy()
new._radii = self._radii.copy() new._radii = self._radii.copy()
new.locked = self.locked new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new return new
def to_polygons(self, def to_polygons(self,

View File

@ -1,14 +1,16 @@
from typing import List, Tuple, Dict, Optional, Sequence from typing import List, Tuple, Dict, Optional, Sequence
import copy import copy
from enum import Enum from enum import Enum
import numpy
import numpy # type: ignore
from numpy import pi, inf from numpy import pi, inf
from . import Shape, normalized_shape_tuple, Polygon, Circle from . import Shape, normalized_shape_tuple, Polygon, Circle
from .. import PatternError from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
from ..utils import remove_colinear_vertices, remove_duplicate_vertices from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
from ..traits import LockableImpl
class PathCap(Enum): class PathCap(Enum):
@ -149,10 +151,11 @@ class Path(Shape, metaclass=AutoSlots):
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
locked: bool = False, locked: bool = False,
raw: bool = False, raw: bool = False,
): ):
object.__setattr__(self, 'locked', False) 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 = ()
@ -160,6 +163,7 @@ class Path(Shape, metaclass=AutoSlots):
self._vertices = vertices self._vertices = vertices
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer self._layer = layer
self._dose = dose self._dose = dose
self._width = width self._width = width
@ -169,6 +173,7 @@ class Path(Shape, metaclass=AutoSlots):
self.vertices = vertices self.vertices = vertices
self.offset = offset self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
self.dose = dose self.dose = dose
self.width = width self.width = width
@ -176,7 +181,7 @@ 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.locked = locked 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
@ -185,7 +190,8 @@ class Path(Shape, metaclass=AutoSlots):
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.locked = self.locked new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new return new
@staticmethod @staticmethod

View File

@ -1,13 +1,15 @@
from typing import List, Tuple, Dict, Optional, Sequence from typing import List, Tuple, Dict, Optional, Sequence
import copy import copy
import numpy
import numpy # type: ignore
from numpy import pi from numpy import pi
from . import Shape, normalized_shape_tuple from . import Shape, normalized_shape_tuple
from .. import PatternError from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
from ..utils import remove_colinear_vertices, remove_duplicate_vertices 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):
@ -77,33 +79,37 @@ class Polygon(Shape, metaclass=AutoSlots):
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
locked: bool = False, locked: bool = False,
raw: bool = False, raw: bool = False,
): ):
object.__setattr__(self, 'locked', False) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
self._vertices = vertices self._vertices = vertices
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer self._layer = layer
self._dose = dose self._dose = dose
else: else:
self.vertices = vertices self.vertices = vertices
self.offset = offset self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
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.locked = locked self.set_locked(locked)
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new._offset = self._offset.copy() new._offset = self._offset.copy()
new._vertices = self._vertices.copy() new._vertices = self._vertices.copy()
new.locked = self.locked new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new return new
@staticmethod @staticmethod

View File

@ -1,13 +1,15 @@
from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy
import numpy # type: ignore
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl,
Rotatable, Mirrorable, Copyable, Scalable, Rotatable, Mirrorable, Copyable, Scalable,
PivotableImpl, LockableImpl, RepeatableImpl) PivotableImpl, LockableImpl, RepeatableImpl,
AnnotatableImpl)
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Polygon from . import Polygon
@ -27,7 +29,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, metaclass=ABCMeta): PivotableImpl, RepeatableImpl, LockableImpl, AnnotatableImpl, metaclass=ABCMeta):
""" """
Abstract class specifying functions common to all shapes. Abstract class specifying functions common to all shapes.
""" """
@ -39,7 +41,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
def __copy__(self) -> 'Shape': def __copy__(self) -> 'Shape':
cls = self.__class__ cls = self.__class__
new = cls.__new__(cls) new = cls.__new__(cls)
for name in self.__slots__: for name in self.__slots__: # type: str
object.__setattr__(new, name, getattr(self, name)) object.__setattr__(new, name, getattr(self, name))
return new return new

View File

@ -1,6 +1,7 @@
from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence
import copy import copy
import numpy
import numpy # type: ignore
from numpy import pi, inf from numpy import pi, inf
from . import Shape, Polygon, normalized_shape_tuple from . import Shape, Polygon, normalized_shape_tuple
@ -8,6 +9,8 @@ from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..traits import RotatableImpl from ..traits import RotatableImpl
from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots
from ..utils import annotations_t
from ..traits import LockableImpl
# Loaded on use: # Loaded on use:
# from freetype import Face # from freetype import Face
@ -67,10 +70,11 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
locked: bool = False, locked: bool = False,
raw: bool = False, raw: bool = False,
): ):
object.__setattr__(self, 'locked', False) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
self._offset = offset self._offset = offset
@ -81,6 +85,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
self._rotation = rotation self._rotation = rotation
self._mirrored = mirrored self._mirrored = mirrored
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
else: else:
self.offset = offset self.offset = offset
self.layer = layer self.layer = layer
@ -90,15 +95,17 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
self.rotation = rotation self.rotation = rotation
self.mirrored = mirrored self.mirrored = mirrored
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.font_path = font_path self.font_path = font_path
self.locked = locked self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Text': def __deepcopy__(self, memo: Dict = None) -> 'Text':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new._offset = self._offset.copy() new._offset = self._offset.copy()
new._mirrored = copy.deepcopy(self._mirrored, memo) new._mirrored = copy.deepcopy(self._mirrored, memo)
new.locked = self.locked new._annotations = copy.deepcopy(self._annotations)
new.set_locked(self.locked)
return new return new
def to_polygons(self, def to_polygons(self,

View File

@ -7,14 +7,15 @@
from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
import copy import copy
import numpy import numpy # type: ignore
from numpy import pi from numpy import pi
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots, annotations_t
from .repetition import Repetition from .repetition import Repetition
from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl) Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl,
AnnotatableImpl)
if TYPE_CHECKING: if TYPE_CHECKING:
@ -22,7 +23,8 @@ if TYPE_CHECKING:
class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable,
PivotableImpl, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots): PivotableImpl, Copyable, RepeatableImpl, LockableImpl, AnnotatableImpl,
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.
@ -49,8 +51,10 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
dose: float = 1.0, dose: float = 1.0,
scale: float = 1.0, scale: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
locked: bool = False, locked: bool = False,
identifier: Tuple[Any, ...] = ()): identifier: Tuple[Any, ...] = (),
):
""" """
Args: Args:
pattern: Pattern to reference. pattern: Pattern to reference.
@ -74,7 +78,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
mirrored = [False, False] mirrored = [False, False]
self.mirrored = mirrored self.mirrored = mirrored
self.repetition = repetition self.repetition = repetition
self.locked = locked 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(pattern=self.pattern,
@ -84,6 +89,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
scale=self.scale, scale=self.scale,
mirrored=self.mirrored.copy(), mirrored=self.mirrored.copy(),
repetition=copy.deepcopy(self.repetition), repetition=copy.deepcopy(self.repetition),
annotations=copy.deepcopy(self.annotations),
locked=self.locked) locked=self.locked)
return new return new
@ -92,7 +98,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new.pattern = copy.deepcopy(self.pattern, memo) new.pattern = copy.deepcopy(self.pattern, memo)
new.repetition = copy.deepcopy(self.repetition, memo) new.repetition = copy.deepcopy(self.repetition, memo)
new.locked = self.locked new.annotations = copy.deepcopy(self.annotations, memo)
new.set_locked(self.locked)
return new return new
# pattern property # pattern property

View File

@ -7,3 +7,4 @@ 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 .lockable import Lockable, LockableImpl
from .annotatable import Annotatable, AnnotatableImpl

View File

@ -0,0 +1,56 @@
from typing import TypeVar
from types import MappingProxyType
from abc import ABCMeta, abstractmethod
import copy
from ..utils import annotations_t
from ..error import PatternError
T = TypeVar('T', bound='Annotatable')
I = TypeVar('I', bound='AnnotatableImpl')
class Annotatable(metaclass=ABCMeta):
"""
Abstract class for all annotatable entities
Annotations correspond to GDS/OASIS "properties"
"""
__slots__ = ()
'''
---- Properties
'''
@property
@abstractmethod
def annotations(self) -> annotations_t:
"""
Dictionary mapping annotation names to values
"""
pass
class AnnotatableImpl(Annotatable, metaclass=ABCMeta):
"""
Simple implementation of `Annotatable`.
"""
__slots__ = ()
_annotations: annotations_t
""" Dictionary storing annotation name/value pairs """
'''
---- Non-abstract properties
'''
@property
def annotations(self) -> annotations_t:
# 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)
return self._annotations
@annotations.setter
def annotations(self, annotations: annotations_t):
if not isinstance(annotations, dict):
raise PatternError(f'annotations expected dict, got {type(annotations)}')
self._annotations = annotations

View File

@ -1,7 +1,6 @@
from typing import List, Tuple, Callable, TypeVar, Optional from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
from ..utils import is_scalar from ..utils import is_scalar

View File

@ -1,7 +1,6 @@
from typing import List, Tuple, Callable, TypeVar, Optional from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
from ..utils import layer_t from ..utils import layer_t

View File

@ -1,7 +1,6 @@
from typing import List, Tuple, Callable, TypeVar, Optional from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
@ -19,6 +18,7 @@ class Lockable(metaclass=ABCMeta):
''' '''
---- Methods ---- Methods
''' '''
@abstractmethod
def lock(self: T) -> T: def lock(self: T) -> T:
""" """
Lock the object, disallowing further changes Lock the object, disallowing further changes
@ -28,6 +28,7 @@ class Lockable(metaclass=ABCMeta):
""" """
pass pass
@abstractmethod
def unlock(self: T) -> T: def unlock(self: T) -> T:
""" """
Unlock the object, reallowing changes Unlock the object, reallowing changes
@ -37,6 +38,32 @@ class Lockable(metaclass=ABCMeta):
""" """
pass 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): class LockableImpl(Lockable, metaclass=ABCMeta):
""" """
@ -62,3 +89,6 @@ class LockableImpl(Lockable, metaclass=ABCMeta):
def unlock(self: I) -> I: def unlock(self: I) -> I:
object.__setattr__(self, 'locked', False) object.__setattr__(self, 'locked', False)
return self return self
def is_locked(self) -> bool:
return self.locked

View File

@ -1,10 +1,10 @@
from typing import List, Tuple, Callable, TypeVar, Optional from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
T = TypeVar('T', bound='Mirrorable') T = TypeVar('T', bound='Mirrorable')
#I = TypeVar('I', bound='MirrorableImpl') #I = TypeVar('I', bound='MirrorableImpl')

View File

@ -3,7 +3,7 @@
from typing import List, Tuple, Callable, TypeVar, Optional from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy import numpy # type: ignore
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2 from ..utils import is_scalar, rotation_matrix_2d, vector2

View File

@ -1,7 +1,6 @@
from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError

View File

@ -2,7 +2,7 @@ from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy import numpy # type: ignore
from numpy import pi from numpy import pi
from .positionable import Positionable from .positionable import Positionable

View File

@ -1,7 +1,6 @@
from typing import List, Tuple, Callable, TypeVar, Optional from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import copy import copy
import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
from ..utils import is_scalar from ..utils import is_scalar

View File

@ -1,15 +1,16 @@
""" """
Various helper functions Various helper functions
""" """
from typing import Any, Union, Tuple, Sequence, Dict, List
from typing import Any, Union, Tuple, Sequence
from abc import ABCMeta from abc import ABCMeta
import numpy import numpy # type: ignore
# Type definitions # Type definitions
vector2 = Union[numpy.ndarray, Tuple[float, float], Sequence[float]] vector2 = Union[numpy.ndarray, Tuple[float, float], Sequence[float]]
layer_t = Union[int, Tuple[int, int], str] layer_t = Union[int, Tuple[int, int], str]
annotations_t = Dict[str, List[Union[int, float, str]]]
def is_scalar(var: Any) -> bool: def is_scalar(var: Any) -> bool: