add support for annotations

and other fixes
lethe/HEAD
Jan Petykiewicz 4 years ago
parent 5d83e0e5c0
commit 49a3b4e322

@ -17,7 +17,8 @@ def main():
pat.shapes.append(shapes.Arc(
radii=(rmin, rmin),
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)
@ -27,7 +28,7 @@ def main():
pat3 = Pattern('sref_test')
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=(3e5, 3e5), rotation=pi/2),
SubPattern(pat, offset=(4e5, 3e5), rotation=pi),

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

@ -10,10 +10,10 @@ import struct
import logging
import pathlib
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 .. import Pattern, SubPattern, PatternError, Label, Shape
@ -264,13 +264,12 @@ def _read_block(block, clean_vertices):
}
if 'column_count' in attr:
args['a_vector'] = (attr['column_spacing'], 0)
args['b_vector'] = (0, attr['row_spacing'])
args['a_count'] = attr['column_count']
args['b_count'] = attr['row_count']
pat.subpatterns.append(GridRepetition(**args))
else:
pat.subpatterns.append(SubPattern(**args))
args['repetition'] = Grid(
a_vector=(attr['column_spacing'], 0),
b_vector=(0, attr['row_spacing']),
a_count=attr['column_count'],
b_count=attr['row_count'])
pat.subpatterns.append(SubPattern(**args))
else:
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
return pat

@ -11,17 +11,18 @@ Note that GDSII references follow the same convention as `masque`,
Scaling, rotation, and mirroring apply to individual instances, not grid
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 io
import copy
import numpy
import base64
import struct
import logging
import pathlib
import gzip
import numpy # type: ignore
# python-gdsii
import gdsii.library
import gdsii.structure
@ -32,7 +33,7 @@ from .. import Pattern, SubPattern, PatternError, Label, Shape
from ..shapes import Polygon, Path
from ..repetition import Grid
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
@ -99,6 +100,7 @@ def build(patterns: Union[Pattern, List[Pattern]],
if disambiguate_func is None:
disambiguate_func = disambiguate_pattern_names
assert(disambiguate_func is not None) # placate mypy
if not modify_originals:
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'))
lib.append(structure)
structure.properties = _annotations_to_properties(pat.annotations, 512)
structure += _shapes_to_elements(pat.shapes)
structure += _labels_to_texts(pat.labels)
structure += _subpatterns_to_refs(pat.subpatterns)
@ -238,6 +242,9 @@ def read(stream: io.BufferedIOBase,
patterns = []
for structure in lib:
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:
# Switch based on element type:
if isinstance(element, gdsii.elements.Boundary):
@ -343,6 +350,7 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef,
rotation=rotation,
scale=scale,
mirrored=(mirror_across_x, False),
annotations=_properties_to_annotations(element.properties),
repetition=repetition)
subpat.identifier = (element.struct_name,)
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,
'cap': cap,
'offset': numpy.zeros(2),
'annotations':_properties_to_annotations(element.properties),
'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),
'layer': (element.layer, element.data_type),
'offset': numpy.zeros(2),
'annotations':_properties_to_annotations(element.properties),
'raw': raw_mode,
}
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
ref.strans = set_bit(0, 15 - 0, mirror_across_x)
ref.mag = subpat.scale
ref.properties = _annotations_to_properties(subpat.annotations, 512)
refs += new_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],
polygonize_paths: bool = False
) -> 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
for shape in shapes:
layer, data_type = _mlayer2gds(shape.layer)
properties = _annotations_to_properties(shape.annotations, 128)
if isinstance(shape, Path) and not polygonize_paths:
xy = numpy.round(shape.vertices + shape.offset).astype(int)
width = numpy.round(shape.width).astype(int)
@ -441,26 +479,32 @@ def _shapes_to_elements(shapes: List[Shape],
xy=xy)
path.path_type = path_type
path.width = width
path.properties = properties
elements.append(path)
else:
for polygon in shape.to_polygons():
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
elements.append(gdsii.elements.Boundary(layer=layer,
data_type=data_type,
xy=xy_closed))
boundary = gdsii.elements.Boundary(layer=layer,
data_type=data_type,
xy=xy_closed)
boundary.properties = properties
elements.append(boundary)
return elements
def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
texts = []
for label in labels:
properties = _annotations_to_properties(label.annotations, 128)
layer, text_type = _mlayer2gds(label.layer)
xy = numpy.round([label.offset]).astype(int)
texts.append(gdsii.elements.Text(layer=layer,
text_type=text_type,
xy=xy,
string=label.string.encode('ASCII')))
text = gdsii.elements.Text(layer=layer,
text_type=text_type,
xy=xy,
string=label.string.encode('ASCII'))
text.properties = properties
texts.append(text)
return texts

@ -20,19 +20,19 @@ import struct
import logging
import pathlib
import gzip
import numpy
from numpy import pi
import numpy # type: ignore
from numpy import pi
import fatamorgana
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 .. import Pattern, SubPattern, PatternError, Label, Shape
from ..shapes import Polygon, Path, Circle
from ..repetition import Grid, Arbitrary, Repetition
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__)
@ -52,9 +52,11 @@ path_cap_map = {
def build(patterns: Union[Pattern, List[Pattern]],
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,
disambiguate_func: Callable[[Iterable[Pattern]], None] = None,
disambiguate_func: Optional[Callable[[Iterable[Pattern]], None]] = None,
annotations: Optional[annotations_t] = None
) -> fatamorgana.OasisLayout:
"""
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`.
disambiguate_func: Function which takes a list of patterns and alters them
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:
`fatamorgana.OasisLayout`
@ -104,11 +107,15 @@ def build(patterns: Union[Pattern, List[Pattern]],
if disambiguate_func is None:
disambiguate_func = disambiguate_pattern_names
if annotations is None:
annotations = {}
if not modify_originals:
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
# Create library
lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None)
lib.properties = annotations_to_properties(annotations)
if layer_map:
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)
lib.cells.append(structure)
structure.properties += annotations_to_properties(pat.annotations)
structure.geometry += _shapes_to_elements(pat.shapes, 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
@ -226,6 +235,8 @@ def read(stream: io.BufferedIOBase,
Additional library info is returned in a dict, containing:
'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:
stream: Stream to read from.
@ -242,6 +253,7 @@ def read(stream: io.BufferedIOBase,
library_info: Dict[str, Any] = {
'units_per_micrometer': lib.unit,
'annotations': properties_to_annotations(lib.properties, lib.propnames, lib.propstrings),
}
layer_map = {}
@ -252,7 +264,7 @@ def read(stream: io.BufferedIOBase,
patterns = []
for cell in lib.cells:
if isinstance(cell.name, int):
cell_name = lib.cellnames[cell.name].string
cell_name = lib.cellnames[cell.name].nstring.string
else:
cell_name = cell.name.string
@ -263,15 +275,16 @@ def read(stream: io.BufferedIOBase,
# note XELEMENT has no repetition
continue
repetition = repetition_fata2masq(element.repetition)
# Switch based on element type:
if isinstance(element, fatrec.Polygon):
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,
layer=element.get_layer_tuple(),
offset=element.get_xy(),
annotations=annotations,
repetition=repetition)
if clean_vertices:
@ -295,10 +308,13 @@ def read(stream: io.BufferedIOBase,
if cap == Path.Cap.SquareCustom:
path_args['cap_extensions'] = numpy.array((element.get_extension_start()[1],
element.get_extension_end()[1]))
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
path = Path(vertices=vertices,
layer=element.get_layer_tuple(),
offset=element.get_xy(),
repetition=repetition,
annotations=annotations,
width=element.get_half_width() * 2,
cap=cap,
**path_args)
@ -314,10 +330,12 @@ def read(stream: io.BufferedIOBase,
elif isinstance(element, fatrec.Rectangle):
width = element.get_width()
height = element.get_height()
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
rect = Polygon(layer=element.get_layer_tuple(),
offset=element.get_xy(),
repetition=repetition,
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
annotations=annotations,
)
pat.shapes.append(rect)
@ -346,10 +364,12 @@ def read(stream: io.BufferedIOBase,
else:
vertices[2, 0] -= b
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
trapz = Polygon(layer=element.get_layer_tuple(),
offset=element.get_xy(),
repetition=repetition,
vertices=vertices,
annotations=annotations,
)
pat.shapes.append(trapz)
@ -399,24 +419,30 @@ def read(stream: io.BufferedIOBase,
vertices = vertices[[0, 2, 3], :]
vertices[0, 1] += width
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
ctrapz = Polygon(layer=element.get_layer_tuple(),
offset=element.get_xy(),
repetition=repetition,
vertices=vertices,
annotations=annotations,
)
pat.shapes.append(ctrapz)
elif isinstance(element, fatrec.Circle):
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
circle = Circle(layer=element.get_layer_tuple(),
offset=element.get_xy(),
repetition=repetition,
annotations=annotations,
radius=float(element.get_radius()))
pat.shapes.append(circle)
elif isinstance(element, fatrec.Text):
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
label = Label(layer=element.get_layer_tuple(),
offset=element.get_xy(),
repetition=repetition,
annotations=annotations,
string=str(element.get_string()))
pat.labels.append(label)
@ -425,7 +451,7 @@ def read(stream: io.BufferedIOBase,
continue
for placement in cell.placements:
pat.subpatterns.append(_placement_to_subpat(placement))
pat.subpatterns.append(_placement_to_subpat(placement, lib))
patterns.append(pat)
@ -435,7 +461,7 @@ def read(stream: io.BufferedIOBase,
for p in patterns_dict.values():
for sp in p.subpatterns:
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]
del sp.identifier
@ -459,7 +485,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
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
and sets the instance .identifier to (struct_name,).
@ -468,21 +494,20 @@ def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern:
mag = placement.magnification if placement.magnification is not None else 1
pname = placement.get_name()
name = pname if isinstance(pname, int) else pname.string
args: Dict[str, Any] = {
'pattern': None,
'mirrored': (placement.flip, False),
'rotation': float(placement.angle * pi/180),
'scale': mag,
'identifier': (name,),
'repetition': repetition_fata2masq(placement.repetition),
}
subpat = SubPattern(offset=xy, **args)
annotations = properties_to_annotations(placement.properties, lib.propnames, lib.propstrings)
subpat = SubPattern(offset=xy,
pattern=None,
mirrored=(placement.flip, False),
rotation=float(placement.angle * pi/180),
scale=float(mag),
identifier=(name,),
repetition=repetition_fata2masq(placement.repetition),
annotations=annotations)
return subpat
def _subpatterns_to_refs(subpatterns: List[SubPattern]
) -> List[fatrec.Placement]:
def _subpatterns_to_placements(subpatterns: List[SubPattern]
) -> List[fatrec.Placement]:
refs = []
for subpat in subpatterns:
if subpat.pattern is None:
@ -493,19 +518,16 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]
frep, rep_offset = repetition_masq2fata(subpat.repetition)
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
ref = fatrec.Placement(
name=subpat.pattern.name,
flip=mirror_across_x,
angle=angle,
magnification=subpat.scale,
**args)
properties=annotations_to_properties(subpat.annotations),
x=offset[0],
y=offset[1],
repetition=frep)
refs.append(ref)
return refs
@ -519,6 +541,7 @@ def _shapes_to_elements(shapes: List[Shape],
for shape in shapes:
layer, datatype = layer2oas(shape.layer)
repetition, rep_offset = repetition_masq2fata(shape.repetition)
properties = annotations_to_properties(shape.annotations)
if isinstance(shape, Circle):
offset = numpy.round(shape.offset + rep_offset).astype(int)
radius = numpy.round(shape.radius).astype(int)
@ -527,6 +550,7 @@ def _shapes_to_elements(shapes: List[Shape],
radius=radius,
x=offset[0],
y=offset[1],
properties=properties,
repetition=repetition)
elements.append(circle)
elif isinstance(shape, Path):
@ -544,6 +568,7 @@ def _shapes_to_elements(shapes: List[Shape],
y=xy[1],
extension_start=extension_start, #TODO implement multiple cap types?
extension_end=extension_end,
properties=properties,
repetition=repetition,
)
elements.append(path)
@ -556,6 +581,7 @@ def _shapes_to_elements(shapes: List[Shape],
x=xy[0],
y=xy[1],
point_list=points,
properties=properties,
repetition=repetition))
return elements
@ -568,11 +594,13 @@ def _labels_to_texts(labels: List[Label],
layer, datatype = layer2oas(label.layer)
repetition, rep_offset = repetition_masq2fata(label.repetition)
xy = numpy.round(label.offset + rep_offset).astype(int)
properties = annotations_to_properties(label.annotations)
texts.append(fatrec.Text(layer=layer,
datatype=datatype,
x=xy[0],
y=xy[1],
string=label.string,
properties=properties,
repetition=repetition))
return texts
@ -609,6 +637,7 @@ def disambiguate_pattern_names(patterns,
def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None]
) -> Optional[Repetition]:
mrep: Optional[Repetition]
if isinstance(rep, fatamorgana.GridRepetition):
mrep = Grid(a_vector=rep.a_vector,
b_vector=rep.b_vector,
@ -624,7 +653,12 @@ def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.Arbi
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):
frep = fatamorgana.GridRepetition(
a_vector=numpy.round(rep.a_vector).astype(int),
@ -642,3 +676,46 @@ def repetition_masq2fata(rep: Optional[Repetition]):
frep = None
offset = (0, 0)
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

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

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

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

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

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

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

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

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

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

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

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

@ -7,14 +7,15 @@
from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
import copy
import numpy
import numpy # type: ignore
from numpy import pi
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 .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl)
Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl,
AnnotatableImpl)
if TYPE_CHECKING:
@ -22,7 +23,8 @@ if TYPE_CHECKING:
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
offset, rotation, scaling, and associated methods.
@ -49,8 +51,10 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
dose: float = 1.0,
scale: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
locked: bool = False,
identifier: Tuple[Any, ...] = ()):
identifier: Tuple[Any, ...] = (),
):
"""
Args:
pattern: Pattern to reference.
@ -74,7 +78,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
mirrored = [False, False]
self.mirrored = mirrored
self.repetition = repetition
self.locked = locked
self.annotations = annotations if annotations is not None else {}
self.set_locked(locked)
def __copy__(self) -> 'SubPattern':
new = SubPattern(pattern=self.pattern,
@ -84,6 +89,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
scale=self.scale,
mirrored=self.mirrored.copy(),
repetition=copy.deepcopy(self.repetition),
annotations=copy.deepcopy(self.annotations),
locked=self.locked)
return new
@ -92,7 +98,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
new = copy.copy(self).unlock()
new.pattern = copy.deepcopy(self.pattern, 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
# pattern property

@ -7,3 +7,4 @@ from .scalable import Scalable, ScalableImpl
from .mirrorable import Mirrorable
from .copyable import Copyable
from .lockable import Lockable, LockableImpl
from .annotatable import Annotatable, AnnotatableImpl

@ -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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save