style and type fixes (per flake8)

could potentially fix some bugs in `Library` class and dxf reader
lethe/HEAD
Jan Petykiewicz 3 years ago
parent f6ad272c2c
commit f364970403

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

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

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

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

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

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

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

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

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

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

@ -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':
"""

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

@ -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
@ -287,7 +287,7 @@ class Arc(Shape, metaclass=AutoSlots):
# Cutoff angles
xpt = (-self.rotation) % (2 * pi) + a0_offset
ypt = (pi/2 - self.rotation) % (2 * pi) + a0_offset
ypt = (pi / 2 - self.rotation) % (2 * pi) + a0_offset
xnt = (xpt - pi) % (2 * pi) + a0_offset
ynt = (ypt - pi) % (2 * pi) + a0_offset
@ -356,9 +356,9 @@ class Arc(Shape, metaclass=AutoSlots):
rotation %= 2 * pi
width = self.width
return (type(self), radii, angles, width/norm_value, self.layer), \
(self.offset, scale/norm_value, rotation, False, self.dose), \
lambda: Arc(radii=radii*norm_value, angles=angles, width=width*norm_value, layer=self.layer)
return ((type(self), radii, angles, width / norm_value, self.layer),
(self.offset, scale / norm_value, rotation, False, self.dose),
lambda: Arc(radii=radii * norm_value, angles=angles, width=width * norm_value, layer=self.layer))
def get_cap_edges(self) -> numpy.ndarray:
'''
@ -373,7 +373,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
@ -388,7 +388,7 @@ class Arc(Shape, metaclass=AutoSlots):
mins.append([xn, yn])
maxs.append([xp, yp])
return numpy.array([mins, maxs]) + self.offset
return numpy.array([mins, maxs]) + self.offset
def _angles_to_parameters(self) -> numpy.ndarray:
'''
@ -398,12 +398,12 @@ class Arc(Shape, metaclass=AutoSlots):
'''
a = []
for sgn in (-1, +1):
wh = sgn * self.width/2
wh = sgn * self.width / 2
rx = self.radius_x + wh
ry = self.radius_y + wh
# create paremeter 'a' for parametrized ellipse
a0, a1 = (numpy.arctan2(rx*numpy.sin(a), ry*numpy.cos(a)) for a in self.angles)
a0, a1 = (numpy.arctan2(rx * numpy.sin(a), ry * numpy.cos(a)) for a in self.angles)
sign = numpy.sign(self.angles[1] - self.angles[0])
if sign != numpy.sign(a1 - a0):
a1 += sign * 2 * pi
@ -424,8 +424,8 @@ class Arc(Shape, metaclass=AutoSlots):
return self
def __repr__(self) -> str:
angles = f'{self.angles*180/pi}'
rotation = f'{self.rotation*180/pi:g}' if self.rotation != 0 else ''
angles = f'{numpy.rad2deg(self.angles)}'
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else ''
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}{locked}>'

@ -75,7 +75,7 @@ class Circle(Shape, metaclass=AutoSlots):
self.poly_max_arclen = poly_max_arclen
self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Circle':
def __deepcopy__(self, memo: Dict = None) -> 'Circle':
memo = {} if memo is None else memo
new = copy.copy(self).unlock()
new._offset = self._offset.copy()
@ -127,9 +127,9 @@ class Circle(Shape, metaclass=AutoSlots):
def normalized_form(self, norm_value) -> normalized_shape_tuple:
rotation = 0.0
magnitude = self.radius / norm_value
return (type(self), self.layer), \
(self.offset, magnitude, rotation, False, self.dose), \
lambda: Circle(radius=norm_value, layer=self.layer)
return ((type(self), self.layer),
(self.offset, magnitude, rotation, False, self.dose),
lambda: Circle(radius=norm_value, layer=self.layer))
def __repr__(self) -> str:
dose = f' d{self.dose:g}' if self.dose != 1 else ''

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict, Sequence, Optional
from typing import List, Dict, Sequence, Optional
import copy
import math
@ -125,7 +125,7 @@ class Ellipse(Shape, metaclass=AutoSlots):
self.poly_max_arclen = poly_max_arclen
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
new = copy.copy(self).unlock()
new._offset = self._offset.copy()
@ -198,9 +198,9 @@ class Ellipse(Shape, metaclass=AutoSlots):
radii = self.radii[::-1] / self.radius_y
scale = self.radius_y
angle = (self.rotation + pi / 2) % pi
return (type(self), radii, self.layer), \
(self.offset, scale/norm_value, angle, False, self.dose), \
lambda: Ellipse(radii=radii*norm_value, layer=self.layer)
return ((type(self), radii, self.layer),
(self.offset, scale / norm_value, angle, False, self.dose),
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
def lock(self) -> 'Ellipse':
self.radii.flags.writeable = False

@ -18,7 +18,7 @@ class PathCap(Enum):
Circle = 1 # Path extends past final vertices with a semicircle of radius width/2
Square = 2 # Path extends past final vertices with a width-by-width/2 rectangle
SquareCustom = 4 # Path extends past final vertices with a rectangle of length
# defined by path.cap_extensions
# # defined by path.cap_extensions
class Path(Shape, metaclass=AutoSlots):
@ -103,7 +103,7 @@ class Path(Shape, metaclass=AutoSlots):
@vertices.setter
def vertices(self, val: numpy.ndarray):
val = numpy.array(val, dtype=float) #TODO document that these might not be copied
val = numpy.array(val, dtype=float) # TODO document that these might not be copied
if len(val.shape) < 2 or val.shape[1] != 2:
raise PatternError('Vertices must be an Nx2 array')
if val.shape[0] < 2:
@ -184,7 +184,7 @@ class Path(Shape, metaclass=AutoSlots):
[self.mirror(a) for a, do in enumerate(mirrored) if do]
self.set_locked(locked)
def __deepcopy__(self, memo: Dict = None) -> 'Path':
def __deepcopy__(self, memo: Dict = None) -> 'Path':
memo = {} if memo is None else memo
new = copy.copy(self).unlock()
new._offset = self._offset.copy()
@ -199,7 +199,7 @@ class Path(Shape, metaclass=AutoSlots):
def travel(travel_pairs: Tuple[Tuple[float, float]],
width: float = 0.0,
cap: PathCap = PathCap.Flush,
cap_extensions = None,
cap_extensions: Optional[Tuple[float, float]] = None,
offset: vector2 = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
@ -275,9 +275,9 @@ class Path(Shape, metaclass=AutoSlots):
intersection_p = v[:-2] + rp * dv[:-1] + perp[:-1]
intersection_n = v[:-2] + rn * dv[:-1] - perp[:-1]
towards_perp = (dv[1:] * perp[:-1]).sum(axis=1) > 0 # path bends towards previous perp?
# straight = (dv[1:] * perp[:-1]).sum(axis=1) == 0 # path is straight
acute = (dv[1:] * dv[:-1]).sum(axis=1) < 0 # angle is acute?
towards_perp = (dv[1:] * perp[:-1]).sum(axis=1) > 0 # path bends towards previous perp?
# straight = (dv[1:] * perp[:-1]).sum(axis=1) == 0 # path is straight
acute = (dv[1:] * dv[:-1]).sum(axis=1) < 0 # angle is acute?
# Build vertices
o0 = [v[0] + perp[0]]
@ -370,10 +370,10 @@ class Path(Shape, metaclass=AutoSlots):
width0 = self.width / norm_value
return (type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), \
(offset, scale/norm_value, rotation, False, self.dose), \
lambda: Path(reordered_vertices*norm_value, width=self.width*norm_value,
cap=self.cap, layer=self.layer)
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer),
(offset, scale / norm_value, rotation, False, self.dose),
lambda: Path(reordered_vertices * norm_value, width=self.width * norm_value,
cap=self.cap, layer=self.layer))
def clean_vertices(self) -> 'Path':
"""
@ -409,7 +409,7 @@ class Path(Shape, metaclass=AutoSlots):
if self.cap == PathCap.Square:
extensions = numpy.full(2, self.width / 2)
elif self.cap == PathCap.SquareCustom:
extensions = self.cap_extensions
extensions = self.cap_extensions
else:
# Flush or Circle
extensions = numpy.zeros(2)

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict, Optional, Sequence
from typing import List, Dict, Optional, Sequence
import copy
import numpy # type: ignore
@ -34,7 +34,7 @@ class Polygon(Shape, metaclass=AutoSlots):
@vertices.setter
def vertices(self, val: numpy.ndarray):
val = numpy.array(val, dtype=float) #TODO document that these might not be copied
val = numpy.array(val, dtype=float) # TODO document that these might not be copied
if len(val.shape) < 2 or val.shape[1] != 2:
raise PatternError('Vertices must be an Nx2 array')
if val.shape[0] < 3:
@ -104,7 +104,7 @@ class Polygon(Shape, metaclass=AutoSlots):
[self.mirror(a) for a, do in enumerate(mirrored) if do]
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
new = copy.copy(self).unlock()
new._offset = self._offset.copy()
@ -269,7 +269,6 @@ class Polygon(Shape, metaclass=AutoSlots):
layer=layer, dose=dose)
return poly
def to_polygons(self,
poly_num_points: int = None, # unused
poly_max_arclen: float = None, # unused
@ -316,9 +315,9 @@ class Polygon(Shape, metaclass=AutoSlots):
# TODO: normalize mirroring?
return (type(self), reordered_vertices.data.tobytes(), self.layer), \
(offset, scale/norm_value, rotation, False, self.dose), \
lambda: Polygon(reordered_vertices*norm_value, layer=self.layer)
return ((type(self), reordered_vertices.data.tobytes(), self.layer),
(offset, scale / norm_value, rotation, False, self.dose),
lambda: Polygon(reordered_vertices * norm_value, layer=self.layer))
def clean_vertices(self) -> 'Polygon':
"""

@ -1,11 +1,8 @@
from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING
from abc import ABCMeta, abstractmethod
import copy
import numpy # type: ignore
from ..error import PatternError, PatternLockedError
from ..utils import rotation_matrix_2d, vector2, layer_t
from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl,
Rotatable, Mirrorable, Copyable, Scalable,
PivotableImpl, LockableImpl, RepeatableImpl,
@ -142,7 +139,6 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
if err_xmax >= 0.5:
gxi_max += 1
if abs(dv[0]) < 1e-20:
# Vertical line, don't calculate slope
xi = [gxi_min, gxi_max - 1]
@ -155,8 +151,9 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
vertex_lists.append(segment)
continue
m = dv[1]/dv[0]
def get_grid_inds(xes):
m = dv[1] / dv[0]
def get_grid_inds(xes: numpy.ndarray) -> numpy.ndarray:
ys = m * (xes - v[0]) + v[1]
# (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid
@ -178,7 +175,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
xs2 = (xs[:-1] + xs[1:]) / 2
inds2 = get_grid_inds(xs2)
xinds = numpy.round(numpy.arange(gxi_min, gxi_max - 0.99, 1/3)).astype(int)
xinds = numpy.round(numpy.arange(gxi_min, gxi_max - 0.99, 1 / 3)).astype(int)
# interleave the results
yinds = xinds.copy()
@ -202,7 +199,6 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
return manhattan_polygons
def manhattanize(self,
grid_x: numpy.ndarray,
grid_y: numpy.ndarray

@ -1,4 +1,4 @@
from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence
from typing import List, Tuple, Dict, Sequence, Optional
import copy
import numpy # type: ignore
@ -26,7 +26,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
_string: str
_height: float
_mirrored: numpy.ndarray #ndarray[bool]
_mirrored: numpy.ndarray # ndarray[bool]
font_path: str
# vertices property
@ -51,7 +51,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
# Mirrored property
@property
def mirrored(self) -> numpy.ndarray: #ndarray[bool]
def mirrored(self) -> numpy.ndarray: # ndarray[bool]
return self._mirrored
@mirrored.setter
@ -100,7 +100,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
self.font_path = font_path
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
new = copy.copy(self).unlock()
new._offset = self._offset.copy()
@ -144,14 +144,14 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
mirror_x, rotation = normalize_mirror(self.mirrored)
rotation += self.rotation
rotation %= 2 * pi
return (type(self), self.string, self.font_path, self.layer), \
(self.offset, self.height / norm_value, rotation, mirror_x, self.dose), \
lambda: Text(string=self.string,
height=self.height * norm_value,
font_path=self.font_path,
rotation=rotation,
mirrored=(mirror_x, False),
layer=self.layer)
return ((type(self), self.string, self.font_path, self.layer),
(self.offset, self.height / norm_value, rotation, mirror_x, self.dose),
lambda: Text(string=self.string,
height=self.height * norm_value,
font_path=self.font_path,
rotation=rotation,
mirrored=(mirror_x, False),
layer=self.layer))
def get_bounds(self) -> numpy.ndarray:
# rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
@ -168,7 +168,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
def get_char_as_polygons(font_path: str,
char: str,
resolution: float = 48*64,
resolution: float = 48 * 64,
) -> Tuple[List[List[List[float]]], float]:
from freetype import Face # type: ignore
from matplotlib.path import Path # type: ignore

@ -4,14 +4,14 @@
"""
#TODO more top-level documentation
from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
from typing import Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
import copy
import numpy # type: ignore
from numpy import pi
from .error import PatternError, PatternLockedError
from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots, annotations_t
from .error import PatternError
from .utils import is_scalar, vector2, AutoSlots, annotations_t
from .repetition import Repetition
from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl,
@ -82,7 +82,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
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,
offset=self.offset.copy(),
rotation=self.rotation,
@ -94,7 +94,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
locked=self.locked)
return new
def __deepcopy__(self, memo: Dict = None) -> 'SubPattern':
def __deepcopy__(self, memo: Dict = None) -> 'SubPattern':
memo = {} if memo is None else memo
new = copy.copy(self).unlock()
new.pattern = copy.deepcopy(self.pattern, memo)

@ -1,7 +1,6 @@
from typing import TypeVar
from types import MappingProxyType
#from types import MappingProxyType
from abc import ABCMeta, abstractmethod
import copy
from ..utils import annotations_t
from ..error import PatternError
@ -44,10 +43,10 @@ class AnnotatableImpl(Annotatable, metaclass=ABCMeta):
'''
@property
def annotations(self) -> annotations_t:
return self._annotations
# # TODO: Find a way to make sure the subclass implements Lockable without dealing with diamond inheritance or this extra hasattr
# if hasattr(self, 'is_locked') and self.is_locked():
# return MappingProxyType(self._annotations)
return self._annotations
@annotations.setter
def annotations(self, annotations: annotations_t):

@ -1,5 +1,5 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
from typing import TypeVar
from abc import ABCMeta
import copy

@ -1,9 +1,7 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from typing import TypeVar
from abc import ABCMeta, abstractmethod
import copy
from ..error import PatternError, PatternLockedError
from ..utils import is_scalar
from ..error import PatternError
T = TypeVar('T', bound='Doseable')
@ -70,7 +68,6 @@ class DoseableImpl(Doseable, metaclass=ABCMeta):
raise PatternError('Dose must be non-negative')
self._dose = val
'''
---- Non-abstract methods
'''

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

@ -1,8 +1,7 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from typing import TypeVar
from abc import ABCMeta, abstractmethod
import copy
from ..error import PatternError, PatternLockedError
from ..error import PatternLockedError
T = TypeVar('T', bound='Lockable')

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

@ -1,12 +1,11 @@
# TODO top-level comment about how traits should set __slots__ = (), and how to use AutoSlots
from typing import List, Tuple, Callable, TypeVar, Optional
from typing import TypeVar
from abc import ABCMeta, abstractmethod
import copy
import numpy # type: ignore
from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2
from ..error import PatternError
from ..utils import vector2
T = TypeVar('T', bound='Positionable')
@ -101,7 +100,6 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
raise PatternError('Offset must be convertible to size-2 ndarray')
self._offset = val.flatten()
'''
---- Methods
'''

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

@ -1,12 +1,11 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from typing import TypeVar
from abc import ABCMeta, abstractmethod
import copy
import numpy # type: ignore
from numpy import pi
from .positionable import Positionable
from ..error import PatternError, PatternLockedError
#from .positionable import Positionable
from ..error import PatternError
from ..utils import is_scalar, rotation_matrix_2d, vector2
T = TypeVar('T', bound='Rotatable')

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

@ -84,7 +84,7 @@ def normalize_mirror(mirrored: Sequence[bool]) -> Tuple[bool, float]:
"""
mirrored_x, mirrored_y = mirrored
mirror_x = (mirrored_x != mirrored_y) #XOR
mirror_x = (mirrored_x != mirrored_y) # XOR
angle = numpy.pi if mirrored_y else 0
return mirror_x, angle
@ -124,8 +124,8 @@ def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True)
# Check for dx0/dy0 == dx1/dy1
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]]
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]]
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40

Loading…
Cancel
Save