Browse Source

style and type fixes (per flake8)

could potentially fix some bugs in `Library` class and dxf reader
builder
Jan Petykiewicz 2 years ago
parent
commit
f364970403
  1. 29
      .flake8
  2. 3
      masque/file/__init__.py
  3. 60
      masque/file/dxf.py
  4. 63
      masque/file/gdsii.py
  5. 85
      masque/file/klamath.py
  6. 72
      masque/file/oasis.py
  7. 3
      masque/file/svg.py
  8. 11
      masque/file/utils.py
  9. 6
      masque/label.py
  10. 11
      masque/library/library.py
  11. 33
      masque/pattern.py
  12. 18
      masque/repetition.py
  13. 32
      masque/shapes/arc.py
  14. 8
      masque/shapes/circle.py
  15. 10
      masque/shapes/ellipse.py
  16. 24
      masque/shapes/path.py
  17. 13
      masque/shapes/polygon.py
  18. 12
      masque/shapes/shape.py
  19. 26
      masque/shapes/text.py
  20. 10
      masque/subpattern.py
  21. 5
      masque/traits/annotatable.py
  22. 4
      masque/traits/copyable.py
  23. 7
      masque/traits/doseable.py
  24. 4
      masque/traits/layerable.py
  25. 5
      masque/traits/lockable.py
  26. 5
      masque/traits/mirrorable.py
  27. 8
      masque/traits/positionable.py
  28. 5
      masque/traits/repeatable.py
  29. 7
      masque/traits/rotatable.py
  30. 5
      masque/traits/scalable.py
  31. 6
      masque/utils.py

29
.flake8

@ -0,0 +1,29 @@
[flake8]
ignore =
# E501 line too long
E501,
# W391 newlines at EOF
W391,
# E241 multiple spaces after comma
E241,
# E302 expected 2 newlines
E302,
# W503 line break before binary operator (to be deprecated)
W503,
# E265 block comment should start with '# '
E265,
# E123 closing bracket does not match indentation of opening bracket's line
E123,
# E124 closing bracket does not match visual indentation
E124,
# E221 multiple spaces before operator
E221,
# E201 whitespace after '['
E201,
# E741 ambiguous variable name 'I'
E741,
per-file-ignores =
# F401 import without use
*/__init__.py: F401,

3
masque/file/__init__.py

@ -1,3 +1,4 @@
"""
Functions for reading from and writing to various file formats.
"""
"""

60
masque/file/dxf.py

@ -1,10 +1,9 @@
"""
DXF file format readers and writers
"""
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable
import re
import io
import copy
import base64
import struct
import logging
@ -12,15 +11,12 @@ import pathlib
import gzip
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
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 rotation_matrix_2d, layer_t
logger = logging.getLogger(__name__)
@ -75,6 +71,7 @@ def write(pattern: Pattern,
#TODO consider supporting DXF arcs?
if disambiguate_func is None:
disambiguate_func = disambiguate_pattern_names
assert(disambiguate_func is not None)
if not modify_originals:
pattern = pattern.deepcopy().deepunlock()
@ -125,8 +122,7 @@ def writefile(pattern: Pattern,
open_func = open
with open_func(path, mode='wt') as stream:
results = write(pattern, stream, *args, **kwargs)
return results
write(pattern, stream, *args, **kwargs)
def readfile(filename: Union[str, pathlib.Path],
@ -204,25 +200,26 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
else:
points = numpy.array(tuple(element.points()))
attr = element.dxfattribs()
args = {'layer': attr.get('layer', DEFAULT_LAYER),
}
layer = attr.get('layer', DEFAULT_LAYER)
if points.shape[1] == 2:
shape = Polygon(**args)
raise PatternError('Invalid or unimplemented polygon?')
#shape = Polygon(layer=layer)
elif points.shape[1] > 2:
if (points[0, 2] != points[:, 2]).any():
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)')
elif points.shape[1] == 4 and (points[:, 3] != 0).any():
raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
else:
width = points[0, 2]
if width == 0:
width = attr.get('const_width', 0)
if width == 0 and numpy.array_equal(points[0], points[-1]):
shape = Polygon(**args, vertices=points[:-1, :2])
else:
shape = Path(**args, width=width, vertices=points[:, :2])
width = points[0, 2]
if width == 0:
width = attr.get('const_width', 0)
shape: Union[Path, Polygon]
if width == 0 and numpy.array_equal(points[0], points[-1]):
shape = Polygon(layer=layer, vertices=points[:-1, :2])
else:
shape = Path(layer=layer, width=width, vertices=points[:, :2])
if clean_vertices:
try:
@ -237,7 +234,7 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
'layer': element.dxfattribs().get('layer', DEFAULT_LAYER),
}
string = element.dxfattribs().get('text', '')
height = element.dxfattribs().get('height', 0)
# height = element.dxfattribs().get('height', 0)
# if height != 0:
# logger.warning('Interpreting DXF TEXT as a label despite nonzero height. '
# 'This could be changed in the future by setting a font path in the masque DXF code.')
@ -252,7 +249,7 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
logger.warning('Masque does not support per-axis scaling; using x-scaling only!')
scale = abs(xscale)
mirrored = (yscale < 0, xscale < 0)
rotation = attr.get('rotation', 0) * pi/180
rotation = numpy.deg2rad(attr.get('rotation', 0))
offset = attr.get('insert', (0, 0, 0))[:2]
@ -266,11 +263,10 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
}
if 'column_count' in attr:
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'])
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).')
@ -356,11 +352,11 @@ def _mlayer2dxf(layer: layer_t) -> str:
def disambiguate_pattern_names(patterns: Sequence[Pattern],
max_name_length: int = 32,
suffix_length: int = 6,
dup_warn_filter: Callable[[str,], bool] = None, # If returns False, don't warn about this name
dup_warn_filter: Callable[[str], bool] = None, # If returns False, don't warn about this name
) -> None:
used_names = []
for pat in patterns:
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name)
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', pat.name)
i = 0
suffixed_name = sanitized_name
@ -374,15 +370,15 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
elif suffixed_name != sanitized_name:
if dup_warn_filter is None or dup_warn_filter(pat.name):
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n' +
f' renaming to "{suffixed_name}"')
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
+ f' renaming to "{suffixed_name}"')
if len(suffixed_name) == 0:
# Should never happen since zero-length names are replaced
raise PatternError(f'Zero-length name after sanitize,\n originally "{pat.name}"')
if len(suffixed_name) > max_name_length:
raise PatternError(f'Pattern name "{suffixed_name!r}" length > {max_name_length} after encode,\n' +
f' originally "{pat.name}"')
raise PatternError(f'Pattern name "{suffixed_name!r}" length > {max_name_length} after encode,\n'
+ f' originally "{pat.name}"')
pat.name = suffixed_name
used_names.append(suffixed_name)

63
masque/file/gdsii.py

@ -17,8 +17,8 @@ Notes:
* ELFLAGS are not supported
* GDS does not support library- or structure-level annotations
"""
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
from typing import Sequence, Mapping
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional
from typing import Sequence
import re
import io
import copy
@ -34,25 +34,23 @@ import gdsii.library
import gdsii.structure
import gdsii.elements
from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose, clean_pattern_vertices
from .utils import is_gzipped
from .utils import clean_pattern_vertices, is_gzipped
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, annotations_t
from ..utils import get_bit, set_bit, layer_t, normalize_mirror, annotations_t
logger = logging.getLogger(__name__)
path_cap_map = {
None: Path.Cap.Flush,
0: Path.Cap.Flush,
1: Path.Cap.Circle,
2: Path.Cap.Square,
4: Path.Cap.SquareCustom,
}
None: Path.Cap.Flush,
0: Path.Cap.Flush,
1: Path.Cap.Circle,
2: Path.Cap.Square,
4: Path.Cap.SquareCustom,
}
def build(patterns: Union[Pattern, Sequence[Pattern]],
@ -262,8 +260,7 @@ def read(stream: io.BufferedIOBase,
string=element.string.decode('ASCII'))
pat.labels.append(label)
elif (isinstance(element, gdsii.elements.SRef) or
isinstance(element, gdsii.elements.ARef)):
elif isinstance(element, (gdsii.elements.SRef, gdsii.elements.ARef)):
pat.subpatterns.append(_ref_to_subpat(element))
if clean_vertices:
@ -358,7 +355,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),
'annotations': _properties_to_annotations(element.properties),
'raw': raw_mode,
}
@ -376,7 +373,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),
'annotations': _properties_to_annotations(element.properties),
'raw': raw_mode,
}
return Polygon(**args)
@ -398,14 +395,14 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]
ref: Union[gdsii.elements.SRef, gdsii.elements.ARef]
if isinstance(rep, Grid):
xy = numpy.array(subpat.offset) + [
[0, 0],
rep.a_vector * rep.a_count,
rep.b_vector * rep.b_count,
]
[0, 0],
rep.a_vector * rep.a_count,
rep.b_vector * rep.b_count,
]
ref = gdsii.elements.ARef(struct_name=encoded_name,
xy=numpy.round(xy).astype(int),
cols=numpy.round(rep.a_count).astype(int),
rows=numpy.round(rep.b_count).astype(int))
xy=numpy.round(xy).astype(int),
cols=numpy.round(rep.a_count).astype(int),
rows=numpy.round(rep.b_count).astype(int))
new_refs = [ref]
elif rep is None:
ref = gdsii.elements.SRef(struct_name=encoded_name,
@ -437,7 +434,7 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
for key, vals in annotations.items():
try:
i = int(key)
except:
except ValueError:
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])')
@ -464,7 +461,7 @@ def _shapes_to_elements(shapes: List[Shape],
if isinstance(shape, Path) and not polygonize_paths:
xy = numpy.round(shape.vertices + shape.offset).astype(int)
width = numpy.round(shape.width).astype(int)
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) #reverse lookup
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
path = gdsii.elements.Path(layer=layer,
data_type=data_type,
xy=xy)
@ -502,7 +499,7 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
def disambiguate_pattern_names(patterns: Sequence[Pattern],
max_name_length: int = 32,
suffix_length: int = 6,
dup_warn_filter: Optional[Callable[[str,], bool]] = None,
dup_warn_filter: Optional[Callable[[str], bool]] = None,
):
"""
Args:
@ -519,13 +516,13 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
# Shorten names which already exceed max-length
if len(pat.name) > max_name_length:
shortened_name = pat.name[:max_name_length - suffix_length]
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n' +
f' shortening to "{shortened_name}" before generating suffix')
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n'
+ f' shortening to "{shortened_name}" before generating suffix')
else:
shortened_name = pat.name
# Remove invalid characters
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
# Add a suffix that makes the name unique
i = 0
@ -540,8 +537,8 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
elif suffixed_name != sanitized_name:
if dup_warn_filter is None or dup_warn_filter(pat.name):
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n' +
f' renaming to "{suffixed_name}"')
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
+ f' renaming to "{suffixed_name}"')
# Encode into a byte-string and perform some final checks
encoded_name = suffixed_name.encode('ASCII')
@ -549,8 +546,8 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
# Should never happen since zero-length names are replaced
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
if len(encoded_name) > max_name_length:
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n' +
f' originally "{pat.name}"')
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
+ f' originally "{pat.name}"')
pat.name = suffixed_name
used_names.append(suffixed_name)

85
masque/file/klamath.py

@ -18,8 +18,8 @@ Notes:
* GDS does not support library- or structure-level annotations
* Creation/modification/access times are set to 1900-01-01 for reproducibility.
"""
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
from typing import Sequence, Mapping, BinaryIO
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional
from typing import Sequence, BinaryIO
import re
import io
import mmap
@ -29,29 +29,27 @@ import struct
import logging
import pathlib
import gzip
from itertools import chain
import numpy # type: ignore
import klamath
from klamath import records
from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose, is_gzipped
from .utils import is_gzipped
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, annotations_t
from ..utils import layer_t, normalize_mirror, annotations_t
from ..library import Library
logger = logging.getLogger(__name__)
path_cap_map = {
0: Path.Cap.Flush,
1: Path.Cap.Circle,
2: Path.Cap.Square,
4: Path.Cap.SquareCustom,
}
0: Path.Cap.Flush,
1: Path.Cap.Circle,
2: Path.Cap.Square,
4: Path.Cap.SquareCustom,
}
def write(patterns: Union[Pattern, Sequence[Pattern]],
@ -144,15 +142,15 @@ def writefile(patterns: Union[Sequence[Pattern], Pattern],
**kwargs,
) -> None:
"""
Wrapper for `masque.file.gdsii.write()` that takes a filename or path instead of a stream.
Wrapper for `write()` that takes a filename or path instead of a stream.
Will automatically compress the file if it has a .gz suffix.
Args:
patterns: `Pattern` or list of patterns to save
filename: Filename to save to.
*args: passed to `masque.file.gdsii.write`
**kwargs: passed to `masque.file.gdsii.write`
*args: passed to `write()`
**kwargs: passed to `write()`
"""
path = pathlib.Path(filename)
if path.suffix == '.gz':
@ -169,14 +167,14 @@ def readfile(filename: Union[str, pathlib.Path],
**kwargs,
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
"""
Wrapper for `masque.file.gdsii.read()` that takes a filename or path instead of a stream.
Wrapper for `read()` that takes a filename or path instead of a stream.
Will automatically decompress gzipped files.
Args:
filename: Filename to save to.
*args: passed to `masque.file.gdsii.read`
**kwargs: passed to `masque.file.gdsii.read`
*args: passed to `read()`
**kwargs: passed to `read()`
"""
path = pathlib.Path(filename)
if is_gzipped(path):
@ -185,7 +183,7 @@ def readfile(filename: Union[str, pathlib.Path],
open_func = open
with io.BufferedReader(open_func(path, mode='rb')) as stream:
results = read(stream)#, *args, **kwargs)
results = read(stream, *args, **kwargs)
return results
@ -216,7 +214,7 @@ def read(stream: BinaryIO,
found_struct = records.BGNSTR.skip_past(stream)
while found_struct:
name = records.STRNAME.skip_and_read(stream)
pat = read_elements(stream, name=name.decode('ASCII'))
pat = read_elements(stream, name=name.decode('ASCII'), raw_mode=raw_mode)
patterns.append(pat)
found_struct = records.BGNSTR.skip_past(stream)
@ -368,10 +366,10 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]
if isinstance(rep, Grid):
xy = numpy.array(subpat.offset) + [
[0, 0],
rep.a_vector * rep.a_count,
rep.b_vector * rep.b_count,
]
[0, 0],
rep.a_vector * rep.a_count,
rep.b_vector * rep.b_count,
]
aref = klamath.library.Reference(struct_name=encoded_name,
xy=numpy.round(xy).astype(int),
colrow=(numpy.round(rep.a_count), numpy.round(rep.b_count)),
@ -412,7 +410,7 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
for key, vals in annotations.items():
try:
i = int(key)
except:
except ValueError:
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])')
@ -439,7 +437,7 @@ def _shapes_to_elements(shapes: List[Shape],
if isinstance(shape, Path) and not polygonize_paths:
xy = numpy.round(shape.vertices + shape.offset).astype(int)
width = numpy.round(shape.width).astype(int)
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) #reverse lookup
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
extension: Tuple[int, int]
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None:
@ -455,13 +453,13 @@ def _shapes_to_elements(shapes: List[Shape],
properties=properties)
elements.append(path)
elif isinstance(shape, Polygon):
polygon = shape
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
boundary = klamath.elements.Boundary(layer=(layer, data_type),
xy=xy_closed,
properties=properties)
elements.append(boundary)
polygon = shape
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
boundary = klamath.elements.Boundary(layer=(layer, data_type),
xy=xy_closed,
properties=properties)
elements.append(boundary)
else:
for polygon in shape.to_polygons():
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
@ -483,7 +481,7 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
xy=xy,
string=label.string.encode('ASCII'),
properties=properties,
presentation=0, #TODO maybe set some of these?
presentation=0, # TODO maybe set some of these?
angle_deg=0,
invert_y=False,
width=0,
@ -496,7 +494,7 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
def disambiguate_pattern_names(patterns: Sequence[Pattern],
max_name_length: int = 32,
suffix_length: int = 6,
dup_warn_filter: Optional[Callable[[str,], bool]] = None,
dup_warn_filter: Optional[Callable[[str], bool]] = None,
):
"""
Args:
@ -513,13 +511,13 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
# Shorten names which already exceed max-length
if len(pat.name) > max_name_length:
shortened_name = pat.name[:max_name_length - suffix_length]
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n' +
f' shortening to "{shortened_name}" before generating suffix')
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n'
+ f' shortening to "{shortened_name}" before generating suffix')
else:
shortened_name = pat.name
# Remove invalid characters
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
# Add a suffix that makes the name unique
i = 0
@ -534,8 +532,8 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
elif suffixed_name != sanitized_name:
if dup_warn_filter is None or dup_warn_filter(pat.name):
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n' +
f' renaming to "{suffixed_name}"')
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
+ f' renaming to "{suffixed_name}"')
# Encode into a byte-string and perform some final checks
encoded_name = suffixed_name.encode('ASCII')
@ -543,8 +541,8 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
# Should never happen since zero-length names are replaced
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
if len(encoded_name) > max_name_length:
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n' +
f' originally "{pat.name}"')
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
+ f' originally "{pat.name}"')
pat.name = suffixed_name
used_names.append(suffixed_name)
@ -576,7 +574,8 @@ def load_library(stream: BinaryIO,
Additional library info (dict, same format as from `read`).
"""
if is_secondary is None:
is_secondary = lambda k: False
def is_secondary(k: str):
return False
stream.seek(0)
library_info = _read_header(stream)
@ -592,7 +591,7 @@ def load_library(stream: BinaryIO,
lib.set_value(name, tag, mkstruct, secondary=is_secondary(name))
return lib
return lib, library_info
def load_libraryfile(filename: Union[str, pathlib.Path],

72
masque/file/oasis.py

@ -22,17 +22,15 @@ import pathlib
import gzip
import numpy # type: ignore
from numpy import pi
import fatamorgana
import fatamorgana.records as fatrec
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
from .utils import mangle_name, make_dose_table, clean_pattern_vertices, is_gzipped
from .utils import clean_pattern_vertices, is_gzipped
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, annotations_t
from ..utils import layer_t, normalize_mirror, annotations_t
logger = logging.getLogger(__name__)
@ -42,10 +40,10 @@ logger.warning('OASIS support is experimental and mostly untested!')
path_cap_map = {
PathExtensionScheme.Flush: Path.Cap.Flush,
PathExtensionScheme.HalfWidth: Path.Cap.Square,
PathExtensionScheme.Arbitrary: Path.Cap.SquareCustom,
}
PathExtensionScheme.Flush: Path.Cap.Flush,
PathExtensionScheme.HalfWidth: Path.Cap.Square,
PathExtensionScheme.Arbitrary: Path.Cap.SquareCustom,
}
#TODO implement more shape types?
@ -120,11 +118,11 @@ def build(patterns: Union[Pattern, Sequence[Pattern]],
for name, layer_num in layer_map.items():
layer, data_type = _mlayer2oas(layer_num)
lib.layers += [
fatrec.LayerName(nstring=name,
layer_interval=(layer, layer),
type_interval=(data_type, data_type),
is_textlayer=tt)
for tt in (True, False)]
fatrec.LayerName(nstring=name,
layer_interval=(layer, layer),
type_interval=(data_type, data_type),
is_textlayer=tt)
for tt in (True, False)]
def layer2oas(mlayer: layer_t) -> Tuple[int, int]:
assert(layer_map is not None)
@ -252,9 +250,9 @@ def read(stream: io.BufferedIOBase,
lib = fatamorgana.OasisLayout.read(stream)
library_info: Dict[str, Any] = {
'units_per_micrometer': lib.unit,
'annotations': properties_to_annotations(lib.properties, lib.propnames, lib.propstrings),
}
'units_per_micrometer': lib.unit,
'annotations': properties_to_annotations(lib.properties, lib.propnames, lib.propstrings),
}
layer_map = {}
for layer_name in lib.layers:
@ -296,7 +294,7 @@ def read(stream: io.BufferedIOBase,
cap_start = path_cap_map[element.get_extension_start()[0]]
cap_end = path_cap_map[element.get_extension_end()[0]]
if cap_start != cap_end:
raise Exception('masque does not support multiple cap types on a single path.') #TODO handle multiple cap types
raise Exception('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types
cap = cap_start
path_args: Dict[str, Any] = {}
@ -472,7 +470,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
data_type = 0
else:
raise PatternError(f'Invalid layer for OASIS: {layer}. Note that OASIS layers cannot be '
'strings unless a layer map is provided.')
f'strings unless a layer map is provided.')
return layer, data_type
@ -490,7 +488,7 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
subpat = SubPattern(offset=xy,
pattern=None,
mirrored=(placement.flip, False),
rotation=float(placement.angle * pi/180),
rotation=numpy.deg2rad(placement.angle),
scale=float(mag),
identifier=(name,),
repetition=repetition_fata2masq(placement.repetition),
@ -512,14 +510,14 @@ def _subpatterns_to_placements(subpatterns: List[SubPattern]
offset = numpy.round(subpat.offset + rep_offset).astype(int)
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
ref = fatrec.Placement(
name=subpat.pattern.name,
flip=mirror_across_x,
angle=angle,
magnification=subpat.scale,
properties=annotations_to_properties(subpat.annotations),
x=offset[0],
y=offset[1],
repetition=frep)
name=subpat.pattern.name,
flip=mirror_across_x,
angle=angle,
magnification=subpat.scale,
properties=annotations_to_properties(subpat.annotations),
x=offset[0],
y=offset[1],
repetition=frep)
refs.append(ref)
return refs
@ -549,7 +547,7 @@ def _shapes_to_elements(shapes: List[Shape],
xy = numpy.round(shape.offset + shape.vertices[0] + rep_offset).astype(int)
deltas = numpy.round(numpy.diff(shape.vertices, axis=0)).astype(int)
half_width = numpy.round(shape.width / 2).astype(int)
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) #reverse lookup
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
path = fatrec.Path(layer=layer,
@ -558,7 +556,7 @@ def _shapes_to_elements(shapes: List[Shape],
half_width=half_width,
x=xy[0],
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,
properties=properties,
repetition=repetition,
@ -598,11 +596,11 @@ def _labels_to_texts(labels: List[Label],
def disambiguate_pattern_names(patterns,
dup_warn_filter: Callable[[str,], bool] = None, # If returns False, don't warn about this name
dup_warn_filter: Callable[[str], bool] = None, # If returns False, don't warn about this name
):
used_names = []
for pat in patterns:
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name)
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', pat.name)
i = 0
suffixed_name = sanitized_name
@ -616,8 +614,8 @@ def disambiguate_pattern_names(patterns,
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
elif suffixed_name != sanitized_name:
if dup_warn_filter is None or dup_warn_filter(pat.name):
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n' +
f' renaming to "{suffixed_name}"')
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
+ f' renaming to "{suffixed_name}"')
if len(suffixed_name) == 0:
# Should never happen since zero-length names are replaced
@ -653,10 +651,10 @@ def repetition_masq2fata(rep: Optional[Repetition]
frep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None]
if isinstance(rep, Grid):
frep = fatamorgana.GridRepetition(
a_vector=numpy.round(rep.a_vector).astype(int),
b_vector=numpy.round(rep.b_vector).astype(int),
a_count=numpy.round(rep.a_count).astype(int),
b_count=numpy.round(rep.b_count).astype(int))
a_vector=numpy.round(rep.a_vector).astype(int),
b_vector=numpy.round(rep.b_vector).astype(int),
a_count=numpy.round(rep.a_count).astype(int),
b_count=numpy.round(rep.b_count).astype(int))
offset = (0, 0)
elif isinstance(rep, Arbitrary):
diffs = numpy.diff(rep.displacements, axis=0)

3
masque/file/svg.py

@ -13,7 +13,8 @@ from .. import Pattern
def writefile(pattern: Pattern,
filename: str,
custom_attributes: bool=False):
custom_attributes: bool = False,
) -> None:
"""
Write a Pattern to an SVG file, by first calling .polygonize() on it
to change the shapes into polygons, and then writing patterns as SVG

11
masque/file/utils.py

@ -4,14 +4,13 @@ Helper functions for file reading and writing
from typing import Set, Tuple, List
import re
import copy
import gzip
import pathlib
from .. import Pattern, PatternError
from ..shapes import Polygon, Path
def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
def mangle_name(pattern: Pattern, dose_multiplier: float = 1.0) -> str:
"""
Create a name using `pattern.name`, `id(pattern)`, and the dose multiplier.
@ -22,7 +21,7 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
Returns:
Mangled name.
"""
expression = re.compile('[^A-Za-z0-9_\?\$]')
expression = re.compile(r'[^A-Za-z0-9_\?\$]')
full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern))
sanitized_name = expression.sub('_', full_name)
return sanitized_name
@ -52,7 +51,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
return pat
def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]:
def make_dose_table(patterns: List[Pattern], dose_multiplier: float = 1.0) -> Set[Tuple[int, float]]:
"""
Create a set containing `(id(pat), written_dose)` for each pattern (including subpatterns)
@ -144,14 +143,14 @@ def dose2dtype(patterns: List[Pattern],
# Create a new pattern for each non-1-dose entry in the dose table
# and update the shapes to reflect their new dose
new_pats = {} # (id, dose) -> new_pattern mapping
new_pats = {} # (id, dose) -> new_pattern mapping
for pat_id, pat_dose in sd_table:
if pat_dose == 1:
new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id]
continue
old_pat = patterns_by_id[pat_id]
pat = old_pat.copy() # keep old subpatterns
pat = old_pat.copy() # keep old subpatterns
pat.shapes = copy.deepcopy(old_pat.shapes)
pat.labels = copy.deepcopy(old_pat.labels)

6
masque/label.py

@ -1,10 +1,8 @@
from typing import List, Tuple, Dict, Optional
from typing import Tuple, Dict, Optional
import copy
import numpy # type: ignore
from numpy import pi
from .repetition import Repetition
from .error import PatternError, PatternLockedError
from .utils import vector2, rotation_matrix_2d, layer_t, AutoSlots, annotations_t
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl
from .traits import AnnotatableImpl
@ -63,7 +61,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
repetition=self.repetition,
locked=self.locked)
def __deepcopy__(self, memo: Dict = None) -> 'Label':
def __deepcopy__(self, memo: Dict = None) -> 'Label':
memo = {} if memo is None else memo
new = copy.copy(self).unlock()
new._offset = self._offset.copy()

11
masque/library/library.py

@ -2,12 +2,11 @@
Library class for managing unique name->pattern mappings and
deferred loading or creation.
"""
from typing import Dict, Callable, TypeVar, Generic, TYPE_CHECKING
from typing import Dict, Callable, TypeVar, TYPE_CHECKING
from typing import Any, Tuple, Union, Iterator
import logging
from pprint import pformat
from dataclasses import dataclass
from functools import lru_cache
from ..error import LibraryError
@ -133,13 +132,13 @@ class Library:
return pat
def keys(self) -> Iterator[str]:
return self.primary.keys()
return iter(self.primary.keys())
def values(self) -> Iterator['Pattern']:
return (self[key] for key in self.keys())
return iter(self[key] for key in self.keys())
def items(self) -> Iterator[Tuple[str, 'Pattern']]:
return ((key, self[key]) for key in self.keys())
return iter((key, self[key]) for key in self.keys())
def __repr__(self) -> str:
return '<Library with keys ' + repr(list(self.primary.keys())) + '>'
@ -191,7 +190,7 @@ class Library:
for key in self.primary:
_ = self.get_primary(key)
for key2 in self.secondary:
_ = self.get_secondary(key2)
_ = self.get_secondary(*key2)
return self
def add(self, other: 'Library') -> 'Library':

33
masque/pattern.py

@ -93,7 +93,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
raise PatternLockedError()
object.__setattr__(self, name, value)
def __copy__(self, memo: Dict = None) -> 'Pattern':
def __copy__(self, memo: Dict = None) -> 'Pattern':
return Pattern(name=self.name,
shapes=copy.deepcopy(self.shapes),
labels=copy.deepcopy(self.labels),
@ -101,14 +101,15 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
annotations=copy.deepcopy(self.annotations),
locked=self.locked)
def __deepcopy__(self, memo: Dict = None) -> 'Pattern':
def __deepcopy__(self, memo: Dict = None) -> 'Pattern':
memo = {} if memo is None else memo
new = Pattern(name=self.name,
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)
new = Pattern(
name=self.name,
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
def rename(self, name: str) -> 'Pattern':
@ -281,7 +282,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
if transform is not False:
sign = numpy.ones(2)
if transform[3]:
sign[1] = -1
sign[1] = -1
xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign)
mirror_x, angle = normalize_mirror(subpattern.mirrored)
angle += subpattern.rotation
@ -325,8 +326,8 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
"""
old_shapes = self.shapes
self.shapes = list(chain.from_iterable(
(shape.to_polygons(poly_num_points, poly_max_arclen)
for shape in old_shapes)))
(shape.to_polygons(poly_num_points, poly_max_arclen)
for shape in old_shapes)))
for subpat in self.subpatterns:
if subpat.pattern is not None:
subpat.pattern.polygonize(poly_num_points, poly_max_arclen)
@ -351,7 +352,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
self.polygonize().flatten()
old_shapes = self.shapes
self.shapes = list(chain.from_iterable(
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
return self
def subpatternize(self,
@ -518,7 +519,6 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
ids.update(pat.subpatterns_by_id(include_none=include_none))
return dict(ids)
def get_bounds(self) -> Union[numpy.ndarray, None]:
"""
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
@ -625,7 +625,6 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
return self
def translate_elements(self, offset: vector2) -> 'Pattern':
"""
Translates all shapes, label, and subpatterns by the given offset.
@ -805,9 +804,9 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
Returns:
True if the pattern is contains no shapes, labels, or subpatterns.
"""
return (len(self.subpatterns) == 0 and
len(self.shapes) == 0 and
len(self.labels) == 0)
return (len(self.subpatterns) == 0
and len(self.shapes) == 0
and len(self.labels) == 0)
def lock(self) -> 'Pattern':
"""

18
masque/repetition.py

@ -3,13 +3,13 @@
instances of an object .
"""
from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
from typing import Union, Dict, Optional, Sequence, Any
import copy
from abc import ABCMeta, abstractmethod
import numpy # type: ignore
from .error import PatternError, PatternLockedError
from .error import PatternError
from .utils import rotation_matrix_2d, vector2, AutoSlots
from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable
@ -103,7 +103,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
self.b_count = b_count
self.locked = locked
def __copy__(self) -> 'Grid':
def __copy__(self) -> 'Grid':
new = Grid(a_vector=self.a_vector.copy(),
b_vector=copy.copy(self.b_vector),
a_count=self.a_count,
@ -111,7 +111,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
locked=self.locked)
return new
def __deepcopy__(self, memo: Dict = None) -> 'Grid':
def __deepcopy__(self, memo: Dict = None) -> 'Grid':
memo = {} if memo is None else memo
new = copy.copy(self).unlock()
new.locked = self.locked
@ -170,8 +170,8 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
@property
def displacements(self) -> numpy.ndarray:
aa, bb = numpy.meshgrid(numpy.arange(self.a_count), numpy.arange(self.b_count), indexing='ij')
return (aa.flatten()[:, None] * self.a_vector[None, :] +
bb.flatten()[:, None] * self.b_vector[None, :])
return (aa.flatten()[:, None] * self.a_vector[None, :]
+ bb.flatten()[:, None] * self.b_vector[None, :]) # noqa
def rotate(self, rotation: float) -> 'Grid':
"""
@ -199,9 +199,9 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
Returns:
self
"""
self.a_vector[1-axis] *= -1
self.a_vector[1 - axis] *= -1
if self.b_vector is not None:
self.b_vector[1-axis] *= -1
self.b_vector[1 - axis] *= -1
return self
def get_bounds(self) -> Optional[numpy.ndarray]:
@ -377,7 +377,7 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots):
Returns:
self
"""
self.displacements[1-axis] *= -1
self.displacements[1 - axis] *= -1
return self
def get_bounds(self) -> Optional[numpy.ndarray]:

32
masque/shapes/arc.py

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict, Optional, Sequence
from typing import List, Dict, Optional, Sequence
import copy
import math
@ -81,7 +81,7 @@ class Arc(Shape, metaclass=AutoSlots):
# arc start/stop angle properties
@property
def angles(self) -> numpy.ndarray: #ndarray[float]
def angles(self) -> numpy.ndarray:
"""
Return the start and stop angles `[a_start, a_stop]`.
Angles are measured from x-axis after rotation
@ -194,7 +194,7 @@ class Arc(Shape, metaclass=AutoSlots):
[self.mirror(a) for a, do in enumerate(mirrored) if do]
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
new = copy.copy(self).unlock()
new._offset = self._offset.copy()
@ -214,8 +214,8 @@ class Arc(Shape, metaclass=AutoSlots):
poly_max_arclen = self.poly_max_arclen
if (poly_num_points is None) and (poly_max_arclen is None):
raise PatternError('Max number of points and arclength left unspecified' +
' (default was also overridden)')
raise PatternError('Max number of points and arclength left unspecified'
+ ' (default was also overridden)')
r0, r1 = self.radii
@ -273,7 +273,7 @@ class Arc(Shape, metaclass=AutoSlots):
mins = []
maxs = []
for a, sgn in zip(a_ranges, (-1, +1)):
wh = sgn * self.width/2
wh = sgn * self.width / 2
rx = self.radius_x + wh
ry = self.radius_y + wh