From 1b0b056bf961bfd2cef1d5e76ceba83d15e48be3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 19 May 2020 00:03:29 -0700 Subject: [PATCH 01/71] break out build() which returns the gdsii.library.Library object --- masque/file/gdsii.py | 46 ++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 98a9a51..94c6928 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -48,15 +48,15 @@ path_cap_map = { } -def write(patterns: Union[Pattern, List[Pattern]], - stream: io.BufferedIOBase, +def build(patterns: Union[Pattern, List[Pattern]], meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-gdsii-write', modify_originals: bool = False, - disambiguate_func: Callable[[Iterable[Pattern]], None] = None): + disambiguate_func: Callable[[Iterable[Pattern]], None] = None, + ) -> gdsii.library.Library: """ - Write a `Pattern` or list of patterns to a GDSII file, by first calling + Convert a `Pattern` or list of patterns to a GDSII stream, by first calling `.polygonize()` to change the shapes into polygons, and then writing patterns as GDSII structures, polygons as boundary elements, and subpatterns as structure references (sref). @@ -74,8 +74,7 @@ def write(patterns: Union[Pattern, List[Pattern]], prior to calling this function. Args: - patterns: A Pattern or list of patterns to write to the stream. - stream: Stream object to write to. + patterns: A Pattern or list of patterns to convert. meters_per_unit: Written into the GDSII file, meters per (database) length unit. All distances are assumed to be an integer multiple of this unit, and are stored as such. logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a @@ -90,6 +89,9 @@ def write(patterns: Union[Pattern, List[Pattern]], to make their names valid and unique. Default is `disambiguate_pattern_names`, which attempts to adhere to the GDSII standard as well as possible. WARNING: No additional error checking is performed on the results. + + Returns: + `gdsii.library.Library` """ if isinstance(patterns, Pattern): patterns = [patterns] @@ -123,25 +125,42 @@ def write(patterns: Union[Pattern, List[Pattern]], structure += _labels_to_texts(pat.labels) structure += _subpatterns_to_refs(pat.subpatterns) + return lib + + +def write(patterns: Union[Pattern, List[Pattern]], + stream: io.BufferedIOBase, + *args, + **kwargs): + """ + Write a `Pattern` or list of patterns to a GDSII file. + See `masque.file.gdsii.build()` for details. + + Args: + patterns: A Pattern or list of patterns to write to file. + stream: Stream to write to. + *args: passed to `oasis.build()` + **kwargs: passed to `oasis.build()` + """ + lib = build(patterns, *args, **kwargs) lib.save(stream) return - def writefile(patterns: Union[List[Pattern], Pattern], filename: Union[str, pathlib.Path], *args, **kwargs, ): """ - Wrapper for `gdsii.write()` that takes a filename or path instead of a stream. + Wrapper for `masque.file.gdsii.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 `gdsii.write` - **kwargs: passed to `gdsii.write` + *args: passed to `masque.file.gdsii.write` + **kwargs: passed to `masque.file.gdsii.write` """ path = pathlib.Path(filename) if path.suffix == '.gz': @@ -243,14 +262,14 @@ def readfile(filename: Union[str, pathlib.Path], **kwargs, ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: """ - Wrapper for `gdsii.read()` that takes a filename or path instead of a stream. + Wrapper for `masque.file.gdsii.read()` that takes a filename or path instead of a stream. Will automatically decompress files with a .gz suffix. Args: filename: Filename to save to. - *args: passed to `gdsii.read` - **kwargs: passed to `gdsii.read` + *args: passed to `masque.file.gdsii.read` + **kwargs: passed to `masque.file.gdsii.read` """ path = pathlib.Path(filename) if path.suffix == '.gz': @@ -591,4 +610,3 @@ def disambiguate_pattern_names(patterns, pat.name = encoded_name used_names.append(suffixed_name) - From 8082743e1736bec7f77c7e555d7751e593f8cd41 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 19 May 2020 00:13:50 -0700 Subject: [PATCH 02/71] move dose2dtype() into masque.file.utils, add dtype2dose(), and add a note that use_dtype_as_dose --- masque/file/gdsii.py | 106 ++++------------------------------------- masque/file/utils.py | 110 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 99 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 94c6928..60755d6 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -27,7 +27,7 @@ import gdsii.library import gdsii.structure import gdsii.elements -from .utils import mangle_name, make_dose_table +from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t from ..shapes import Polygon, Path from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t @@ -173,90 +173,6 @@ def writefile(patterns: Union[List[Pattern], Pattern], return results -def dose2dtype(patterns: List[Pattern], - ) -> Tuple[List[Pattern], List[float]]: - """ - For each shape in each pattern, set shape.layer to the tuple - (base_layer, datatype), where: - layer is chosen to be equal to the original shape.layer if it is an int, - or shape.layer[0] if it is a tuple. `str` layers raise a PatterError. - datatype is chosen arbitrarily, based on calcualted dose for each shape. - Shapes with equal calcualted dose will have the same datatype. - A list of doses is retured, providing a mapping between datatype - (list index) and dose (list entry). - - Note that this function modifies the input Pattern(s). - - Args: - patterns: A `Pattern` or list of patterns to write to file. Modified by this function. - - Returns: - (patterns, dose_list) - patterns: modified input patterns - dose_list: A list of doses, providing a mapping between datatype (int, list index) - and dose (float, list entry). - """ - # Get a dict of id(pattern) -> pattern - patterns_by_id = {id(pattern): pattern for pattern in patterns} - for pattern in patterns: - for i, p in pattern.referenced_patterns_by_id().items(): - patterns_by_id[i] = p - - # Get a table of (id(pat), written_dose) for each pattern and subpattern - sd_table = make_dose_table(patterns) - - # Figure out all the unique doses necessary to write this pattern - # This means going through each row in sd_table and adding the dose values needed to write - # that subpattern at that dose level - dose_vals = set() - for pat_id, pat_dose in sd_table: - pat = patterns_by_id[pat_id] - for shape in pat.shapes: - dose_vals.add(shape.dose * pat_dose) - - if len(dose_vals) > 256: - raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) - - dose_vals_list = list(dose_vals) - - # 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 - 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.shapes = copy.deepcopy(old_pat.shapes) - pat.labels = copy.deepcopy(old_pat.labels) - - encoded_name = mangle_name(pat, pat_dose) - if len(encoded_name) == 0: - raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name)) - pat.name = encoded_name - - for shape in pat.shapes: - data_type = dose_vals_list.index(shape.dose * pat_dose) - if isinstance(shape.layer, int): - shape.layer = (shape.layer, data_type) - elif isinstance(shape.layer, tuple): - shape.layer = (shape.layer[0], data_type) - else: - raise PatternError(f'Invalid layer for gdsii: {shape.layer}') - - new_pats[(pat_id, pat_dose)] = pat - - # Go back through all the dose-specific patterns and fix up their subpattern entries - for (pat_id, pat_dose), pat in new_pats.items(): - for subpat in pat.subpatterns: - dose_mult = subpat.dose * pat_dose - subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)] - - return patterns, dose_vals_list - - def readfile(filename: Union[str, pathlib.Path], *args, **kwargs, @@ -302,6 +218,8 @@ def read(stream: io.BufferedIOBase, use_dtype_as_dose: If `False`, set each polygon's layer to `(gds_layer, gds_datatype)`. If `True`, set the layer to `gds_layer` and the dose to `gds_datatype`. Default `False`. + NOTE: This will be deprecated in the future in favor of + `pattern.apply(masque.file.utils.dtype2dose)`. clean_vertices: If `True`, remove any redundant vertices when loading polygons. The cleaning process removes any polygons with zero area or <3 vertices. Default `True`. @@ -325,14 +243,9 @@ def read(stream: io.BufferedIOBase, # Switch based on element type: if isinstance(element, gdsii.elements.Boundary): args = {'vertices': element.xy[:-1], + 'layer': (element.layer, element.data_type), } - if use_dtype_as_dose: - args['dose'] = element.data_type - args['layer'] = element.layer - else: - args['layer'] = (element.layer, element.data_type) - poly = Polygon(**args) if clean_vertices: @@ -350,6 +263,7 @@ def read(stream: io.BufferedIOBase, raise PatternError('Unrecognized path type: {}'.format(element.path_type)) args = {'vertices': element.xy, + 'layer': (element.layer, element.data_type), 'width': element.width if element.width is not None else 0.0, 'cap': cap, } @@ -361,12 +275,6 @@ def read(stream: io.BufferedIOBase, if element.end_extn is not None: args['cap_extensions'][1] = element.end_extn - if use_dtype_as_dose: - args['dose'] = element.data_type - args['layer'] = element.layer - else: - args['layer'] = (element.layer, element.data_type) - path = Path(**args) if clean_vertices: @@ -389,6 +297,10 @@ def read(stream: io.BufferedIOBase, elif isinstance(element, gdsii.elements.ARef): pat.subpatterns.append(_aref_to_gridrep(element)) + if use_dtype_as_dose: + logger.warning('use_dtype_as_dose will be removed in the future!') + pat = dtype2dose(pat) + patterns.append(pat) # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries diff --git a/masque/file/utils.py b/masque/file/utils.py index 9092ab9..e36765e 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -1,10 +1,11 @@ """ Helper functions for file reading and writing """ -import re from typing import Set, Tuple, List +import re +import copy -from masque.pattern import Pattern +from .. import Pattern, PatternError def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: @@ -45,3 +46,108 @@ def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[ subpat_dose_table = make_dose_table([subpat.pattern], subpat.dose * dose_multiplier) dose_table = dose_table.union(subpat_dose_table) return dose_table + + +def dtype2dose(pattern: Pattern) -> Pattern: + """ + For each shape in the pattern, if the layer is a tuple, set the + layer to the tuple's first element and set the dose to the + tuple's second element. + + Generally intended for use with `Pattern.apply()`. + + Args: + pattern: Pattern to modify + + Returns: + pattern + """ + for shape in pattern.shapes: + if isinstance(shape.layer, tuple): + shape.dose = shape.layer[1] + shape.layer = shape.layer[0] + return pattern + + +def dose2dtype(patterns: List[Pattern], + ) -> Tuple[List[Pattern], List[float]]: + """ + For each shape in each pattern, set shape.layer to the tuple + (base_layer, datatype), where: + layer is chosen to be equal to the original shape.layer if it is an int, + or shape.layer[0] if it is a tuple. `str` layers raise a PatterError. + datatype is chosen arbitrarily, based on calcualted dose for each shape. + Shapes with equal calcualted dose will have the same datatype. + A list of doses is retured, providing a mapping between datatype + (list index) and dose (list entry). + + Note that this function modifies the input Pattern(s). + + Args: + patterns: A `Pattern` or list of patterns to write to file. Modified by this function. + + Returns: + (patterns, dose_list) + patterns: modified input patterns + dose_list: A list of doses, providing a mapping between datatype (int, list index) + and dose (float, list entry). + """ + # Get a dict of id(pattern) -> pattern + patterns_by_id = {id(pattern): pattern for pattern in patterns} + for pattern in patterns: + for i, p in pattern.referenced_patterns_by_id().items(): + patterns_by_id[i] = p + + # Get a table of (id(pat), written_dose) for each pattern and subpattern + sd_table = make_dose_table(patterns) + + # Figure out all the unique doses necessary to write this pattern + # This means going through each row in sd_table and adding the dose values needed to write + # that subpattern at that dose level + dose_vals = set() + for pat_id, pat_dose in sd_table: + pat = patterns_by_id[pat_id] + for shape in pat.shapes: + dose_vals.add(shape.dose * pat_dose) + + if len(dose_vals) > 256: + raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) + + dose_vals_list = list(dose_vals) + + # 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 + 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.shapes = copy.deepcopy(old_pat.shapes) + pat.labels = copy.deepcopy(old_pat.labels) + + encoded_name = mangle_name(pat, pat_dose) + if len(encoded_name) == 0: + raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name)) + pat.name = encoded_name + + for shape in pat.shapes: + data_type = dose_vals_list.index(shape.dose * pat_dose) + if isinstance(shape.layer, int): + shape.layer = (shape.layer, data_type) + elif isinstance(shape.layer, tuple): + shape.layer = (shape.layer[0], data_type) + else: + raise PatternError(f'Invalid layer for gdsii: {shape.layer}') + + new_pats[(pat_id, pat_dose)] = pat + + # Go back through all the dose-specific patterns and fix up their subpattern entries + for (pat_id, pat_dose), pat in new_pats.items(): + for subpat in pat.subpatterns: + dose_mult = subpat.dose * pat_dose + subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)] + + return patterns, dose_vals_list From 1bb4bd6bb7d60e0caee4b16277807e11964854b4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 19 May 2020 00:15:51 -0700 Subject: [PATCH 03/71] add py.typed to enable type checking for downstream --- masque/py.typed | 0 setup.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 masque/py.typed diff --git a/masque/py.typed b/masque/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 739b5f3..f45961e 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,9 @@ setup(name='masque', url='https://mpxd.net/code/jan/masque', packages=find_packages(), package_data={ - 'masque': ['VERSION'] + 'masque': ['VERSION', + 'py.typed', + ] }, install_requires=[ 'numpy', From 6e957d761ac58891d0f36e8b3d290f9cfc5e1790 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 19 May 2020 00:29:30 -0700 Subject: [PATCH 04/71] newline --- masque/file/dxf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 335cd80..737a915 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -24,6 +24,7 @@ from ..utils import remove_colinear_vertices, normalize_mirror logger = logging.getLogger(__name__) + logger.warning('DXF support is experimental and only slightly tested!') From f204d917c9845e945664eb6a9f732d048953c646 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 19 May 2020 01:00:00 -0700 Subject: [PATCH 05/71] Add basic support for OASIS and update setup/docs for OASIS and DXF support --- README.md | 9 +- masque/file/oasis.py | 612 +++++++++++++++++++++++++++++++++++++++++++ setup.py | 4 +- 3 files changed, 621 insertions(+), 4 deletions(-) create mode 100644 masque/file/oasis.py diff --git a/README.md b/README.md index fd16875..38ad690 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,19 @@ E-beam doses, and the ability to output to multiple formats. ## Installation Requirements: -* python >= 3.5 (written and tested with 3.6) +* python >= 3.7 (written and tested with 3.8) * numpy * matplotlib (optional, used for `visualization` functions and `text`) * python-gdsii (optional, used for `gdsii` i/o) +* ezdxf (optional, used for `dxf` i/o) +* fatamorgana (optional, used for `oasis` i/o) * svgwrite (optional, used for `svg` output) * freetype (optional, used for `text`) Install with pip: ```bash -pip3 install 'masque[visualization,gdsii,svg,text]' +pip3 install 'masque[visualization,gdsii,oasis,dxf,svg,text]' ``` Alternatively, install from git @@ -36,4 +38,5 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release * Polygon de-embedding * Construct from bitmap * Boolean operations on polygons (using pyclipper) -* Output to OASIS (using fatamorgana) +* Implement shape/cell properties +* Implement OASIS-style repetitions for shapes diff --git a/masque/file/oasis.py b/masque/file/oasis.py new file mode 100644 index 0000000..1de1e09 --- /dev/null +++ b/masque/file/oasis.py @@ -0,0 +1,612 @@ +""" +OASIS file format readers and writers + +Note that OASIS references follow the same convention as `masque`, + with this order of operations: + 1. Mirroring + 2. Rotation + 3. Scaling + 4. Offset and array expansion (no mirroring/rotation/scaling applied to offsets) + + Scaling, rotation, and mirroring apply to individual instances, not grid + vectors or offsets. +""" +from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional +import re +import io +import copy +import base64 +import struct +import logging +import pathlib +import gzip +import numpy +from numpy import pi + +import fatamorgana +import fatamorgana.records as fatrec +from fatamorgana.basic import PathExtensionScheme + +from .utils import mangle_name, make_dose_table +from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t +from ..shapes import Polygon, Path, Circle +from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t +from ..utils import remove_colinear_vertices, normalize_mirror + + +logger = logging.getLogger(__name__) + + +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, + } + +#TODO implement properties +#TODO implement more shape types? + +def build(patterns: Union[Pattern, List[Pattern]], + units_per_micron: int, + layer_map: Dict[str, Union[int, Tuple[int, int]]] = None, + modify_originals: bool = False, + disambiguate_func: Callable[[Iterable[Pattern]], None] = None, + ) -> fatamorgana.OasisLayout: + """ + Convert a `Pattern` or list of patterns to an OASIS stream, writing patterns + as OASIS cells, subpatterns as Placement records, and other shapes and labels + mapped to equivalent record types (Polygon, Path, Circle, Text). + Other shape types may be converted to polygons if no equivalent + record type exists (or is not implemented here yet). + + For each shape, + layer is chosen to be equal to `shape.layer` if it is an int, + or `shape.layer[0]` if it is a tuple + datatype is chosen to be `shape.layer[1]` if available, + otherwise `0` + If a layer map is provided, layer strings will be converted + automatically, and layer names will be written to the file. + + If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` + prior to calling this function. + + Args: + patterns: A Pattern or list of patterns to convert. + units_per_micron: Written into the OASIS file, number of grid steps per micrometer. + All distances are assumed to be an integer multiple of the grid step, and are stored as such. + layer_map: Dictionary which translates layer names into layer numbers. If this argument is + provided, input shapes and labels are allowed to have layer names instead of numbers. + It is assumed that geometry and text share the same layer names, and each name is + assigned only to a single layer (not a range). + If more fine-grained control is needed, manually pre-processing shapes' layer names + into numbers, omit this argument, and manually generate the required + `fatamorgana.records.LayerName` entries. + Default is an empty dict (no names provided). + modify_originals: If `True`, the original pattern is modified as part of the writing + process. Otherwise, a copy is made and `deepunlock()`-ed. + Default `False`. + disambiguate_func: Function which takes a list of patterns and alters them + to make their names valid and unique. Default is `disambiguate_pattern_names`. + + Returns: + `fatamorgana.OasisLayout` + """ + if isinstance(patterns, Pattern): + patterns = [patterns] + + if layer_map is None: + layer_map = {} + + if disambiguate_func is None: + disambiguate_func = disambiguate_pattern_names + + if not modify_originals: + patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] + + # Create library + lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None) + + if layer_map: + 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)] + + def layer2oas(mlayer: layer_t) -> Tuple[int, int]: + layer_num = layer_map[mlayer] if isinstance(mlayer, str) else mlayer + return _mlayer2oas(layer_num) + else: + layer2oas = _mlayer2oas + + # Get a dict of id(pattern) -> pattern + patterns_by_id = {id(pattern): pattern for pattern in patterns} + for pattern in patterns: + for i, p in pattern.referenced_patterns_by_id().items(): + patterns_by_id[i] = p + + disambiguate_func(patterns_by_id.values()) + + # Now create a structure for each pattern + for pat in patterns_by_id.values(): + structure = fatamorgana.Cell(name=pat.name) + lib.cells.append(structure) + + structure.geometry += _shapes_to_elements(pat.shapes, layer2oas) + structure.geometry += _labels_to_texts(pat.labels, layer2oas) + structure.placements += _subpatterns_to_refs(pat.subpatterns) + + return lib + + +def write(patterns: Union[List[Pattern], Pattern], + stream: io.BufferedIOBase, + *args, + **kwargs): + """ + Write a `Pattern` or list of patterns to a OASIS file. See `oasis.build()` + for details. + + Args: + patterns: A Pattern or list of patterns to write to file. + stream: Stream to write to. + *args: passed to `oasis.build()` + **kwargs: passed to `oasis.build()` + """ + lib = build(patterns, *args, **kwargs) + lib.write(stream) + + +def writefile(patterns: Union[List[Pattern], Pattern], + filename: Union[str, pathlib.Path], + *args, + **kwargs, + ): + """ + Wrapper for `oasis.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 `oasis.write` + **kwargs: passed to `oasis.write` + """ + path = pathlib.Path(filename) + if path.suffix == '.gz': + open_func: Callable = gzip.open + else: + open_func = open + + with io.BufferedWriter(open_func(path, mode='wb')) as stream: + results = write(patterns, stream, *args, **kwargs) + return results + + +def readfile(filename: Union[str, pathlib.Path], + *args, + **kwargs, + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: + """ + Wrapper for `oasis.read()` that takes a filename or path instead of a stream. + + Will automatically decompress files with a .gz suffix. + + Args: + filename: Filename to save to. + *args: passed to `oasis.read` + **kwargs: passed to `oasis.read` + """ + path = pathlib.Path(filename) + if path.suffix == '.gz': + open_func: Callable = gzip.open + else: + open_func = open + + with io.BufferedReader(open_func(path, mode='rb')) as stream: + results = read(stream, *args, **kwargs) + return results + + +def read(stream: io.BufferedIOBase, + clean_vertices: bool = True, + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: + """ + Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are + translated into Pattern objects; Polygons are translated into polygons, and Placements + are translated into SubPattern or GridRepetition objects. + + Additional library info is returned in a dict, containing: + 'units_per_micrometer': number of database units per micrometer (all values are in database units) + + Args: + stream: Stream to read from. + clean_vertices: If `True`, remove any redundant vertices when loading polygons. + The cleaning process removes any polygons with zero area or <3 vertices. + Default `True`. + + Returns: + - Dict of `pattern_name`:`Pattern`s generated from OASIS cells + - Dict of OASIS library info + """ + + lib = fatamorgana.OasisLayout.read(stream) + + library_info: Dict[str, Any] = { + 'units_per_micrometer': lib.unit, + } + + layer_map = {} + for layer_name in lib.layers: + layer_map[str(layer_name.nstring)] = layer_name + library_info['layer_map'] = layer_map + + patterns = [] + for cell in lib.cells: + if isinstance(cell.name, int): + cell_name = lib.cellnames[cell.name].string + else: + cell_name = cell.name.string + + pat = Pattern(name=cell_name) + for element in cell.geometry: + if isinstance(element, fatrec.XElement): + logger.warning('Skipping XElement record') + continue + + if element.repetition is not None: + # note XELEMENT has no repetition + raise PatternError('masque OASIS reader does not implement repetitions for shapes yet') + + # Switch based on element type: + if isinstance(element, fatrec.Polygon): + vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0) + poly = Polygon(vertices=vertices, + layer=element.get_layer_tuple(), + offset=element.get_xy()) + + if clean_vertices: + try: + poly.clean_vertices() + except PatternError: + continue + + pat.shapes.append(poly) + + elif isinstance(element, fatrec.Path): + vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0) + + 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 + cap = cap_start + + path_args: Dict[str, Any] = {} + if cap == Path.Cap.SquareCustom: + path_args['cap_extensions'] = numpy.array((element.get_extension_start()[1], + element.get_extension_end()[1])) + path = Path(vertices=vertices, + layer=element.get_layer_tuple(), + offset=element.get_xy(), + width=element.get_half_width() * 2, + cap=cap, + **path_args) + + if clean_vertices: + try: + path.clean_vertices() + except PatternError as err: + continue + + pat.shapes.append(path) + + elif isinstance(element, fatrec.Rectangle): + width = element.get_width() + height = element.get_height() + rect = Polygon(layer=element.get_layer_tuple(), + offset=element.get_xy(), + vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), + ) + pat.shapes.append(rect) + + elif isinstance(element, fatrec.Trapezoid): + vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (element.get_width(), element.get_height()) + a = element.get_delta_a() + b = element.get_delta_b() + if element.get_is_vertical(): + if a > 0: + vertices[0, 1] += a + else: + vertices[3, 1] += a + + if b > 0: + vertices[2, 1] -= b + else: + vertices[1, 1] -= b + else: + if a > 0: + vertices[1, 0] += a + else: + vertices[0, 0] += a + + if b > 0: + vertices[3, 0] -= b + else: + vertices[2, 0] -= b + + trapz = Polygon(layer=element.get_layer_tuple(), + offset=element.get_xy(), + vertices=vertices, + ) + pat.shapes.append(trapz) + + elif isinstance(element, fatrec.CTrapezoid): + cttype = element.get_ctrapezoid_type() + height = element.get_height() + width = element.get_width() + + vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height) + + if cttype in (0, 4, 7): + vertices[2, 0] -= height + if cttype in (1, 5, 6): + vertices[3, 0] -= height + if cttype in (2, 4, 6): + vertices[1, 0] += height + if cttype in (3, 5, 7): + vertices[0, 0] += height + + if cttype in (8, 12, 15): + vertices[2, 0] -= width + if cttype in (9, 13, 14): + vertices[1, 0] -= width + if cttype in (10, 12, 14): + vertices[3, 0] += width + if cttype in (11, 13, 15): + vertices[0, 0] += width + + if cttype == 16: + vertices = vertices[[0, 1, 3], :] + elif cttype == 17: + vertices = vertices[[0, 1, 2], :] + elif cttype == 18: + vertices = vertices[[0, 2, 3], :] + elif cttype == 19: + vertices = vertices[[1, 2, 3], :] + elif cttype == 20: + vertices = vertices[[0, 1, 3], :] + vertices[1, 0] += height + elif cttype == 21: + vertices = vertices[[0, 1, 2], :] + vertices[0, 0] += height + elif cttype == 22: + vertices = vertices[[0, 1, 3], :] + vertices[3, 1] += width + elif cttype == 23: + vertices = vertices[[0, 2, 3], :] + vertices[0, 1] += width + + ctrapz = Polygon(layer=element.get_layer_tuple(), + offset=element.get_xy(), + vertices=vertices, + ) + pat.shapes.append(ctrapz) + + elif isinstance(element, fatrec.Circle): + circle = Circle(layer=element.get_layer_tuple(), + offset=element.get_xy(), + radius=float(element.get_radius())) + pat.shapes.append(circle) + + elif isinstance(element, fatrec.Text): + label = Label(layer=element.get_layer_tuple(), + offset=element.get_xy(), + string=str(element.get_string())) + pat.labels.append(label) + + else: + logger.warning(f'Skipping record {element} (unimplemented)') + continue + + for placement in cell.placements: + pat.subpatterns += _placement_to_subpats(placement) + + patterns.append(pat) + + # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries + # according to the subpattern.identifier (which is deleted after use). + patterns_dict = dict(((p.name, p) for p in patterns)) + for p in patterns_dict.values(): + for sp in p.subpatterns: + ident = sp.identifier[0] + name = ident if isinstance(ident, str) else lib.cellnames[ident].string + sp.pattern = patterns_dict[name] + del sp.identifier + + return patterns_dict, library_info + + +def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]: + """ Helper to turn a layer tuple-or-int into a layer and datatype""" + if isinstance(mlayer, int): + layer = mlayer + data_type = 0 + elif isinstance(mlayer, tuple): + layer = mlayer[0] + if len(mlayer) > 1: + data_type = mlayer[1] + else: + 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.') + return layer, data_type + + +def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]: + """ + Helper function to create a SubPattern from a placment. Sets subpat.pattern to None + and sets the instance .identifier to (struct_name,). + """ + xy = numpy.array((placement.x, placement.y)) + mag = placement.magnification if placement.magnification is not None else 1 + pname = placement.get_name() + name = pname if isinstance(pname, int) else pname.string + args: Dict[str, Any] = { + 'pattern': None, + 'mirrored': (placement.flip, False), + 'rotation': float(placement.angle * pi/180), + 'scale': mag, + 'identifier': (name,), + } + + subpats: List[subpattern_t] + rep = placement.repetition + if isinstance(rep, fatamorgana.GridRepetition): + subpat = GridRepetition(a_vector=rep.a_vector, + b_vector=rep.b_vector, + a_count=rep.a_count, + b_count=rep.b_count, + offset=xy, + **args) + subpats = [subpat] + elif isinstance(rep, fatamorgana.ArbitraryRepetition): + subpats = [] + for rep_offset in numpy.cumsum(numpy.column_stack((rep.x_displacements, + rep.y_displacements))): + subpats.append(SubPattern(offset=xy + rep_offset, **args)) + elif rep is None: + subpats = [SubPattern(offset=xy, **args)] + return subpats + + +def _subpatterns_to_refs(subpatterns: List[subpattern_t] + ) -> List[fatrec.Placement]: + refs = [] + for subpat in subpatterns: + if subpat.pattern is None: + continue + + # Note: OASIS mirrors first and rotates second + mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) + xy = numpy.round(subpat.offset).astype(int) + args: Dict[str, Any] = { + 'x': xy[0], + 'y': xy[1], + } + + if isinstance(subpat, GridRepetition): + args['repetition'] = fatamorgana.GridRepetition( + a_vector=numpy.round(subpat.a_vector).astype(int), + b_vector=numpy.round(subpat.b_vector).astype(int), + a_count=numpy.round(subpat.a_count).astype(int), + b_count=numpy.round(subpat.b_count).astype(int)) + + angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 + ref = fatrec.Placement( + name=subpat.pattern.name, + flip=mirror_across_x, + angle=angle, + magnification=subpat.scale, + **args) + + refs.append(ref) + return refs + + +def _shapes_to_elements(shapes: List[Shape], + layer2oas: Callable[[layer_t], Tuple[int, int]], + ) -> List[Union[fatrec.Polygon, fatrec.Path, fatrec.Circle]]: + # Add a Polygon record for each shape, and Path elements if necessary + elements: List[Union[fatrec.Polygon, fatrec.Path, fatrec.Circle]] = [] + for shape in shapes: + layer, datatype = layer2oas(shape.layer) + if isinstance(shape, Circle): + offset = numpy.round(shape.offset).astype(int) + radius = numpy.round(shape.radius).astype(int) + circle = fatrec.Circle(layer=layer, + datatype=datatype, + radius=radius, + x=offset[0], + y=offset[1]) + elements.append(circle) + elif isinstance(shape, Path): + xy = numpy.round(shape.offset + shape.vertices[0]).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 + 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, + datatype=datatype, + point_list=deltas, + half_width=half_width, + x=xy[0], + y=xy[1], + extension_start=extension_start, #TODO implement multiple cap types? + extension_end=extension_end, + ) + elements.append(path) + else: + for polygon in shape.to_polygons(): + xy = numpy.round(polygon.offset + polygon.vertices[0]).astype(int) + points = numpy.round(numpy.diff(polygon.vertices, axis=0)).astype(int) + elements.append(fatrec.Polygon(layer=layer, + datatype=datatype, + x=xy[0], + y=xy[1], + point_list=points)) + return elements + + +def _labels_to_texts(labels: List[Label], + layer2oas: Callable[[layer_t], Tuple[int, int]], + ) -> List[fatrec.Text]: + texts = [] + for label in labels: + layer, datatype = layer2oas(label.layer) + xy = numpy.round(label.offset).astype(int) + texts.append(fatrec.Text(layer=layer, + datatype=datatype, + x=xy[0], + y=xy[1], + string=label.string)) + return texts + + +def disambiguate_pattern_names(patterns, + 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) + + i = 0 + suffixed_name = sanitized_name + while suffixed_name in used_names or suffixed_name == '': + suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII') + + suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') + i += 1 + + if sanitized_name == '': + logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name)) + elif suffixed_name != sanitized_name: + if dup_warn_filter is None or dup_warn_filter(pat.name): + logger.warning('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format( + pat.name, sanitized_name, suffixed_name)) + + if len(suffixed_name) == 0: + # Should never happen since zero-length names are replaced + raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name)) + + pat.name = suffixed_name + used_names.append(suffixed_name) diff --git a/setup.py b/setup.py index f45961e..595c8e9 100644 --- a/setup.py +++ b/setup.py @@ -26,9 +26,11 @@ setup(name='masque', 'numpy', ], extras_require={ - 'visualization': ['matplotlib'], 'gdsii': ['python-gdsii'], + 'oasis': ['fatamorgana>=0.7'], + 'dxf': ['ezdxf'], 'svg': ['svgwrite'], + 'visualization': ['matplotlib'], 'text': ['freetype-py', 'matplotlib'], }, classifiers=[ From f2c58c290ff399b4a9ea671e33f667869461dde4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 19 May 2020 01:01:03 -0700 Subject: [PATCH 06/71] add .oas.gz to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3557665..64466d7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,6 @@ dist/ *.gds.gz *.svg *.oas +*.oas.gz *.dxf *.dxf.gz From 778e54c89560ffb0be4f38ee40b1b349960572cd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 19 May 2020 01:01:31 -0700 Subject: [PATCH 07/71] bump version to v1.4 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index 7e32cd5..c068b24 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -1.3 +1.4 From e401f37993e7b0d2f07e80a2463ea91b5707c262 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 23 May 2020 19:37:55 -0700 Subject: [PATCH 08/71] Improve documentation on disambiguate_pattern_names --- masque/file/gdsii.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 60755d6..4837765 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -11,7 +11,7 @@ Note that GDSII references follow the same convention as `masque`, Scaling, rotation, and mirroring apply to individual instances, not grid vectors or offsets. """ -from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional +from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional, Sequence import re import io import copy @@ -482,13 +482,24 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: return texts -def disambiguate_pattern_names(patterns, +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: Optional[Callable[[str,], bool]] = None, ): + """ + Args: + patterns: List of patterns to disambiguate + max_name_length: Names longer than this will be truncated + suffix_length: Names which get truncated are truncated by this many extra characters. This is to + leave room for a suffix if one is necessary. + dup_warn_filter: (optional) Function for suppressing warnings about cell names changing. Receives + the cell name and returns `False` if the warning should be suppressed and `True` if it should + be displayed. Default displays all warnings. + """ used_names = [] for pat in patterns: + # 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('Pattern name "{}" is too long ({}/{} chars),\n'.format(pat.name, len(pat.name), max_name_length) + @@ -496,8 +507,10 @@ def disambiguate_pattern_names(patterns, else: shortened_name = pat.name + # Remove invalid characters sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', shortened_name) + # Add a suffix that makes the name unique i = 0 suffixed_name = sanitized_name while suffixed_name in used_names or suffixed_name == '': @@ -513,6 +526,7 @@ def disambiguate_pattern_names(patterns, logger.warning('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format( pat.name, sanitized_name, suffixed_name)) + # Encode into a byte-string and perform some final checks encoded_name = suffixed_name.encode('ASCII') if len(encoded_name) == 0: # Should never happen since zero-length names are replaced From 09615eaea6506c2208657daa5203035556787bf7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 23 May 2020 19:38:17 -0700 Subject: [PATCH 09/71] use set() to remove any duplicates in patterns --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 4837765..3496d9c 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -498,7 +498,7 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern], be displayed. Default displays all warnings. """ used_names = [] - for pat in patterns: + for pat in set(patterns): # Shorten names which already exceed max-length if len(pat.name) > max_name_length: shortened_name = pat.name[:max_name_length - suffix_length] From 1976c6e684eb096075019c9924b86f5a8d8d0d02 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 23 May 2020 19:38:48 -0700 Subject: [PATCH 10/71] Add `recursive` arg to referenced_patterns_by_id --- masque/pattern.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 4348338..9cd49c2 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -441,28 +441,31 @@ class Pattern: pass def referenced_patterns_by_id(self, - include_none: bool = False + include_none: bool = False, + recursive: bool = True, ) -> Union[Dict[int, Optional['Pattern']], Dict[int, 'Pattern']]: """ Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this - Pattern (operates recursively on all referenced Patterns as well) + Pattern (by default, operates recursively on all referenced Patterns as well). Args: include_none: If `True`, references to `None` will be included. Default `False`. + recursive: If `True`, operates recursively on all referenced patterns. Default `True`. Returns: Dictionary with `{id(pat): pat}` for all referenced Pattern objects """ ids: Dict[int, Optional['Pattern']] = {} for subpat in self.subpatterns: - if id(subpat.pattern) not in ids: - if subpat.pattern is not None: - ids[id(subpat.pattern)] = subpat.pattern - ids.update(subpat.pattern.referenced_patterns_by_id()) - elif include_none: - ids[id(subpat.pattern)] = subpat.pattern + pat = subpat.pattern + if id(pat) in ids: + continue + if include_none or pat is not None: + ids[id(pat)] = pat + if recursive and pat is not None: + ids.update(pat.referenced_patterns_by_id()) return ids def referenced_patterns_by_name(self, **kwargs) -> List[Tuple[Optional[str], Optional['Pattern']]]: From 07ee25e735a372528afbce091864b2e37f854a1d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 23 May 2020 19:39:03 -0700 Subject: [PATCH 11/71] add subpatterns_by_id() --- masque/pattern.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index 9cd49c2..e3f8c8c 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -486,6 +486,32 @@ class Pattern: pat_list = [(p.name if p is not None else None, p) for p in pats_by_id.values()] return pat_list + def subpatterns_by_id(self, + include_none: bool = False, + recursive: bool = True, + ) -> Dict[int, List[subpattern_t]]: + """ + Create a dictionary which maps `{id(referenced_pattern): [subpattern0, ...]}` + for all SubPattern objects referenced by this Pattern (by default, operates + recursively on all referenced Patterns as well). + + Args: + include_none: If `True`, references to `None` will be included. Default `False`. + recursive: If `True`, operates recursively on all referenced patterns. Default `True`. + + Returns: + Dictionary mapping each pattern id to a list of subpattern objects referencing the pattern. + """ + ids: Dict[int, List[subpattern_t]] = defaultdict(list) + for subpat in self.subpatterns: + pat = subpat.pattern + if include_none or pat is not None: + ids[id(pat)].append(subpat) + if recursive and pat is not None: + 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 From 53d2a9ca1a850f1d19a64a466e63eb9dd4c44283 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 23 May 2020 19:39:48 -0700 Subject: [PATCH 12/71] Only swap between tuple/list if actually necessary --- masque/pattern.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index e3f8c8c..7599e06 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -764,10 +764,11 @@ class Pattern: Returns: self """ - self.shapes = tuple(self.shapes) - self.labels = tuple(self.labels) - self.subpatterns = tuple(self.subpatterns) - object.__setattr__(self, 'locked', True) + if not self.locked: + self.shapes = tuple(self.shapes) + self.labels = tuple(self.labels) + self.subpatterns = tuple(self.subpatterns) + object.__setattr__(self, 'locked', True) return self def unlock(self) -> 'Pattern': @@ -777,10 +778,11 @@ class Pattern: Returns: self """ - object.__setattr__(self, 'locked', False) - self.shapes = list(self.shapes) - self.labels = list(self.labels) - self.subpatterns = list(self.subpatterns) + if self.locked: + object.__setattr__(self, 'locked', False) + self.shapes = list(self.shapes) + self.labels = list(self.labels) + self.subpatterns = list(self.subpatterns) return self def deeplock(self) -> 'Pattern': From f3a1db30c58b5a0d1014b51b0e845c3908f4853f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 8 Jul 2020 18:21:42 -0700 Subject: [PATCH 13/71] Fix order of rotation/mirror/offset when calling as_pattern() on repetitions --- masque/repetition.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index 247f1eb..81e8b86 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -332,22 +332,20 @@ class GridRepetition: assert(self.pattern is not None) patterns = [] + pat = self.pattern.deepcopy().deepunlock() + pat.scale_by(self.scale) + [pat.mirror(ax) for ax, do in enumerate(self.mirrored) if do] + pat.rotate_around((0.0, 0.0), self.rotation) + pat.translate_elements(self.offset) + pat.scale_element_doses(self.dose) + + combined = type(pat)(name='__GridRepetition__') for a in range(self.a_count): for b in range(self.b_count): offset = a * self.a_vector + b * self.b_vector - newPat = self.pattern.deepcopy().deepunlock() + newPat = pat.deepcopy() newPat.translate_elements(offset) - patterns.append(newPat) - - combined = patterns[0] - for p in patterns[1:]: - combined.append(p) - - combined.scale_by(self.scale) - [combined.mirror(ax) for ax, do in enumerate(self.mirrored) if do] - combined.rotate_around((0.0, 0.0), self.rotation) - combined.translate_elements(self.offset) - combined.scale_element_doses(self.dose) + combined.append(newPat) return combined From 1ae9225130ccea50fb7a3ae1553450f1385b3e48 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 8 Jul 2020 18:32:19 -0700 Subject: [PATCH 14/71] add rename() method for Pattern --- masque/pattern.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index 7599e06..84c274d 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -111,6 +111,10 @@ class Pattern: locked=self.locked) return new + def rename(self, name: str) -> 'Pattern': + self.name = name + return self + def append(self, other_pattern: 'Pattern') -> 'Pattern': """ Appends all shapes, labels and subpatterns from other_pattern to self's shapes, From 0589fbb1b8f903dbcf73ee25f736b438bf550a06 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 8 Jul 2020 18:42:39 -0700 Subject: [PATCH 15/71] bump version to v1.5 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index c068b24..c239c60 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -1.4 +1.5 From a4b57762086cbb8c93a2f9b0d3c9481a4f3e2cf7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sun, 12 Jul 2020 03:50:19 -0700 Subject: [PATCH 16/71] Don't return early, since we add patterns to memo before they've been checked --- masque/pattern.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 84c274d..f254352 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -913,9 +913,6 @@ class Pattern: A filtered list in which no pattern is referenced by any other pattern. """ def get_children(pat: Pattern, memo: Set) -> Set: - if pat in memo: - return memo - children = set(sp.pattern for sp in pat.subpatterns if sp.pattern is not None) new_children = children - memo memo |= children From 89bd1e6abed71779a440b37df561b5307249ac2e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sun, 12 Jul 2020 03:50:32 -0700 Subject: [PATCH 17/71] only add new_children (marginally faster) --- masque/pattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index f254352..663b060 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -915,7 +915,7 @@ class Pattern: def get_children(pat: Pattern, memo: Set) -> Set: children = set(sp.pattern for sp in pat.subpatterns if sp.pattern is not None) new_children = children - memo - memo |= children + memo |= new_children for child_pat in new_children: memo |= get_children(child_pat, memo) From 0fa073b48820e5b59f8535e7ae4e2cf96b4867ca Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 21 Jul 2020 20:38:38 -0700 Subject: [PATCH 18/71] Make sure linspace gets an integer number of points --- masque/shapes/arc.py | 5 +++-- masque/shapes/circle.py | 3 ++- masque/shapes/ellipse.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 7b1cedc..25a9b2e 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -212,8 +212,9 @@ class Arc(Shape): n += [poly_num_points] if poly_max_arclen is not None: n += [perimeter / poly_max_arclen] - thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], max(n), endpoint=True) - thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], max(n), endpoint=True) + num_points = int(round(max(n))) + thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_points, endpoint=True) + thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], num_points, endpoint=True) sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner)) sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer)) diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index f7221d5..404aeae 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -81,7 +81,8 @@ class Circle(Shape): n += [poly_num_points] if poly_max_arclen is not None: n += [2 * pi * self.radius / poly_max_arclen] - thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) + num_points = int(round(max(n))) + thetas = numpy.linspace(2 * pi, 0, num_points, endpoint=False) xs = numpy.cos(thetas) * self.radius ys = numpy.sin(thetas) * self.radius xys = numpy.vstack((xs, ys)).T diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 9779e69..3288614 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -139,7 +139,8 @@ class Ellipse(Shape): n += [poly_num_points] if poly_max_arclen is not None: n += [perimeter / poly_max_arclen] - thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) + num_points = int(round(max(n))) + thetas = numpy.linspace(2 * pi, 0, num_points, endpoint=False) sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) xs = r0 * cos_th From d4fbdd8d27167cfcb94016dc563c1b0b487f428a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 11 Aug 2020 01:17:23 -0700 Subject: [PATCH 19/71] add fast-path for 0-degree rotations --- masque/shapes/path.py | 3 ++- masque/shapes/polygon.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index db7831a..b618001 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -314,7 +314,8 @@ class Path(Shape): return bounds def rotate(self, theta: float) -> 'Path': - self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T + if theta != 0: + self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T return self def mirror(self, axis: int) -> 'Path': diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 71c9491..97cc66c 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -262,7 +262,8 @@ class Polygon(Shape): self.offset + numpy.max(self.vertices, axis=0))) def rotate(self, theta: float) -> 'Polygon': - self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T + if theta != 0: + self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T return self def mirror(self, axis: int) -> 'Polygon': From bab40474a0c511926b544a141387b5df501a6d15 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 22 Jul 2020 02:45:16 -0700 Subject: [PATCH 20/71] Add repetitions and split up code into traits --- examples/test_rep.py | 102 +++++++ masque/__init__.py | 9 +- masque/file/dxf.py | 24 +- masque/file/gdsii.py | 113 +++----- masque/file/oasis.py | 55 ++-- masque/label.py | 93 +----- masque/pattern.py | 21 +- masque/repetition.py | 515 ++++++++++------------------------ masque/shapes/arc.py | 5 +- masque/shapes/circle.py | 5 +- masque/shapes/ellipse.py | 5 +- masque/shapes/path.py | 4 +- masque/shapes/polygon.py | 5 +- masque/shapes/shape.py | 203 +------------- masque/shapes/text.py | 24 +- masque/subpattern.py | 194 +++---------- masque/traits/__init__.py | 9 + masque/traits/copyable.py | 34 +++ masque/traits/doseable.py | 82 ++++++ masque/traits/layerable.py | 76 +++++ masque/traits/lockable.py | 76 +++++ masque/traits/mirrorable.py | 61 ++++ masque/traits/positionable.py | 135 +++++++++ masque/traits/repeatable.py | 79 ++++++ masque/traits/rotatable.py | 119 ++++++++ masque/traits/scalable.py | 79 ++++++ masque/utils.py | 23 ++ 27 files changed, 1202 insertions(+), 948 deletions(-) create mode 100644 examples/test_rep.py create mode 100644 masque/traits/__init__.py create mode 100644 masque/traits/copyable.py create mode 100644 masque/traits/doseable.py create mode 100644 masque/traits/layerable.py create mode 100644 masque/traits/lockable.py create mode 100644 masque/traits/mirrorable.py create mode 100644 masque/traits/positionable.py create mode 100644 masque/traits/repeatable.py create mode 100644 masque/traits/rotatable.py create mode 100644 masque/traits/scalable.py diff --git a/examples/test_rep.py b/examples/test_rep.py new file mode 100644 index 0000000..80496ad --- /dev/null +++ b/examples/test_rep.py @@ -0,0 +1,102 @@ +import numpy +from numpy import pi + +import masque +import masque.file.gdsii +import masque.file.dxf +import masque.file.oasis +from masque import shapes, Pattern, SubPattern +from masque.repetition import Grid + +from pprint import pprint + + +def main(): + pat = masque.Pattern(name='ellip_grating') + for rmin in numpy.arange(10, 15, 0.5): + pat.shapes.append(shapes.Arc( + radii=(rmin, rmin), + width=0.1, + angles=(0*-numpy.pi/4, numpy.pi/4) + )) + + pat.scale_by(1000) +# pat.visualize() + pat2 = pat.copy() + pat2.name = 'grating2' + + pat3 = Pattern('sref_test') + pat3.subpatterns = [ + SubPattern(pat, offset=(1e5, 3e5)), + SubPattern(pat, offset=(2e5, 3e5), rotation=pi/3), + SubPattern(pat, offset=(3e5, 3e5), rotation=pi/2), + SubPattern(pat, offset=(4e5, 3e5), rotation=pi), + SubPattern(pat, offset=(5e5, 3e5), rotation=3*pi/2), + SubPattern(pat, mirrored=(True, False), offset=(1e5, 4e5)), + SubPattern(pat, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3), + SubPattern(pat, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2), + SubPattern(pat, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi), + SubPattern(pat, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2), + SubPattern(pat, mirrored=(False, True), offset=(1e5, 5e5)), + SubPattern(pat, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3), + SubPattern(pat, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2), + SubPattern(pat, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi), + SubPattern(pat, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2), + SubPattern(pat, mirrored=(True, True), offset=(1e5, 6e5)), + SubPattern(pat, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3), + SubPattern(pat, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2), + SubPattern(pat, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi), + SubPattern(pat, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2), + ] + + pprint(pat3) + pprint(pat3.subpatterns) + pprint(pat.shapes) + + rep = Grid(a_vector=[1e4, 0], + b_vector=[0, 1.5e4], + a_count=3, + b_count=2,) + pat4 = Pattern('aref_test') + pat4.subpatterns = [ + SubPattern(pat, repetition=rep, offset=(1e5, 3e5)), + SubPattern(pat, repetition=rep, offset=(2e5, 3e5), rotation=pi/3), + SubPattern(pat, repetition=rep, offset=(3e5, 3e5), rotation=pi/2), + SubPattern(pat, repetition=rep, offset=(4e5, 3e5), rotation=pi), + SubPattern(pat, repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2), + SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(1e5, 4e5)), + SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3), + SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2), + SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi), + SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2), + SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(1e5, 5e5)), + SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3), + SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2), + SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi), + SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2), + SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(1e5, 6e5)), + SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3), + SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2), + SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi), + SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2), + ] + + folder = 'layouts/' + masque.file.gdsii.writefile((pat, pat2, pat3, pat4), folder + 'rep.gds.gz', 1e-9, 1e-3) + + cells = list(masque.file.gdsii.readfile(folder + 'rep.gds.gz')[0].values()) + masque.file.gdsii.writefile(cells, folder + 'rerep.gds.gz', 1e-9, 1e-3) + + masque.file.dxf.writefile(pat4, folder + 'rep.dxf.gz') + dxf, info = masque.file.dxf.readfile(folder + 'rep.dxf.gz') + masque.file.dxf.writefile(dxf, folder + 'rerep.dxf.gz') + + layer_map = {'base': (0,0), 'mylabel': (1,2)} + masque.file.oasis.writefile((pat, pat2, pat3, pat4), folder + 'rep.oas.gz', 1000, layer_map=layer_map) + oas, info = masque.file.oasis.readfile(folder + 'rep.oas.gz') + masque.file.oasis.writefile(list(oas.values()), folder + 'rerep.oas.gz', 1000, layer_map=layer_map) + print(info) + + +if __name__ == '__main__': + main() diff --git a/masque/__init__.py b/masque/__init__.py index c826d18..7087b00 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -8,12 +8,10 @@ `Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape` objects, a list of `Label` objects, and a list of references to other `Patterns` (using - `SubPattern` and `GridRepetition`). + `SubPattern`). `SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding - offset, rotation, scaling, and other such properties to a Pattern reference. - - `GridRepetition` provides support for nesting regular arrays of `Pattern` objects. + offset, rotation, scaling, repetition, and other such properties to a Pattern reference. Note that the methods for these classes try to avoid copying wherever possible, so unless otherwise noted, assume that arguments are stored by-reference. @@ -31,8 +29,7 @@ import pathlib from .error import PatternError, PatternLockedError from .shapes import Shape from .label import Label -from .subpattern import SubPattern, subpattern_t -from .repetition import GridRepetition +from .subpattern import SubPattern from .pattern import Pattern diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 737a915..4faac8c 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -16,8 +16,9 @@ from numpy import pi import ezdxf from .utils import mangle_name, make_dose_table -from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t +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 @@ -55,7 +56,7 @@ def write(pattern: Pattern, If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` prior to calling this function. - Only `GridRepetition` objects with manhattan basis vectors are preserved as arrays. Since DXF + Only `Grid` repetition objects with manhattan basis vectors are preserved as arrays. Since DXF rotations apply to basis vectors while `masque`'s rotations do not, the basis vectors of an array with rotated instances must be manhattan _after_ having a compensating rotation applied. @@ -276,7 +277,7 @@ def _read_block(block, clean_vertices): def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], - subpatterns: List[subpattern_t]): + subpatterns: List[SubPattern]): for subpat in subpatterns: if subpat.pattern is None: continue @@ -289,9 +290,12 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M 'rotation': rotation, } - if isinstance(subpat, GridRepetition): - a = subpat.a_vector - b = subpat.b_vector if subpat.b_vector is not None else numpy.zeros(2) + rep = subpat.repetition + if rep is None: + block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs) + elif isinstance(rep, Grid): + a = rep.a_vector + b = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) rotated_a = rotation_matrix_2d(-subpat.rotation) @ a rotated_b = rotation_matrix_2d(-subpat.rotation) @ b if rotated_a[1] == 0 and rotated_b[0] == 0: @@ -310,11 +314,11 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M #NOTE: We could still do non-manhattan (but still orthogonal) grids by getting # creative with counter-rotated nested patterns, but probably not worth it. # Instead, just break appart the grid into individual elements: - for aa in numpy.arange(subpat.a_count): - for bb in numpy.arange(subpat.b_count): - block.add_blockref(encoded_name, subpat.offset + aa * a + bb * b, dxfattribs=attribs) + for dd in rep.displacements: + block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs) else: - block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs) + for dd in rep.displacements: + block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs) def _shapes_to_elements(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 3496d9c..4c2198c 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -28,8 +28,9 @@ import gdsii.structure import gdsii.elements from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose -from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t +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 @@ -291,11 +292,9 @@ def read(stream: io.BufferedIOBase, string=element.string.decode('ASCII')) pat.labels.append(label) - elif isinstance(element, gdsii.elements.SRef): - pat.subpatterns.append(_sref_to_subpat(element)) - - elif isinstance(element, gdsii.elements.ARef): - pat.subpatterns.append(_aref_to_gridrep(element)) + elif (isinstance(element, gdsii.elements.SRef) or + isinstance(element, gdsii.elements.ARef)): + pat.subpatterns.append(_ref_to_subpat(element)) if use_dtype_as_dose: logger.warning('use_dtype_as_dose will be removed in the future!') @@ -330,40 +329,11 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]: return layer, data_type -def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: +def _ref_to_subpat(element: Union[gdsii.elements.SRef, + gdsii.elements.ARef] + ) -> SubPattern: """ - Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None - and sets the instance .identifier to (struct_name,). - - BUG: - "Absolute" means not affected by parent elements. - That's not currently supported by masque at all, so need to either tag it and - undo the parent transformations, or implement it in masque. - """ - subpat = SubPattern(pattern=None, offset=element.xy) - subpat.identifier = (element.struct_name,) - if element.strans is not None: - if element.mag is not None: - subpat.scale = element.mag - # Bit 13 means absolute scale - if get_bit(element.strans, 15 - 13): - #subpat.offset *= subpat.scale - raise PatternError('Absolute scale is not implemented yet!') - if element.angle is not None: - subpat.rotation = element.angle * numpy.pi / 180 - # Bit 14 means absolute rotation - if get_bit(element.strans, 15 - 14): - #subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset) - raise PatternError('Absolute rotation is not implemented yet!') - # Bit 0 means mirror x-axis - if get_bit(element.strans, 15 - 0): - subpat.mirrored[0] = 1 - return subpat - - -def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: - """ - Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None + Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None and sets the instance .identifier to (struct_name,). BUG: @@ -375,6 +345,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: offset = numpy.array(element.xy[0]) scale = 1 mirror_across_x = False + repetition = None if element.strans is not None: if element.mag is not None: @@ -383,7 +354,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: if get_bit(element.strans, 15 - 13): raise PatternError('Absolute scale is not implemented yet!') if element.angle is not None: - rotation = element.angle * numpy.pi / 180 + rotation = numpy.deg2rad(element.angle) # Bit 14 means absolute rotation if get_bit(element.strans, 15 - 14): raise PatternError('Absolute rotation is not implemented yet!') @@ -391,25 +362,24 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: if get_bit(element.strans, 15 - 0): mirror_across_x = True - counts = [element.cols, element.rows] - a_vector = (element.xy[1] - offset) / counts[0] - b_vector = (element.xy[2] - offset) / counts[1] + if isinstance(element, gdsii.elements.ARef): + a_count = element.cols + b_count = element.rows + a_vector = (element.xy[1] - offset) / counts[0] + b_vector = (element.xy[2] - offset) / counts[1] + repetition = Grid(a_vector=a_vector, b_vector=b_vector, + a_count=a_count, b_count=b_count) - gridrep = GridRepetition(pattern=None, - a_vector=a_vector, - b_vector=b_vector, - a_count=counts[0], - b_count=counts[1], - offset=offset, - rotation=rotation, - scale=scale, - mirrored=(mirror_across_x, False)) - gridrep.identifier = (element.struct_name,) - - return gridrep + subpat = SubPattern(pattern=None, + offset=offset, + rotation=rotation, + scale=scale, + mirrored=(mirror_across_x, False)) + subpat.identifier = (element.struct_name,) + return subpat -def _subpatterns_to_refs(subpatterns: List[subpattern_t] +def _subpatterns_to_refs(subpatterns: List[SubPattern] ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]: refs = [] for subpat in subpatterns: @@ -420,26 +390,35 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t] # Note: GDS mirrors first and rotates second mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] - if isinstance(subpat, GridRepetition): + + rep = subpat.repetition + if isinstance(rep, Grid): xy = numpy.array(subpat.offset) + [ [0, 0], - subpat.a_vector * subpat.a_count, - subpat.b_vector * subpat.b_count, + 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(subpat.a_count).astype(int), - rows=numpy.round(subpat.b_count).astype(int)) - else: + 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, xy=numpy.round([subpat.offset]).astype(int)) + new_refs = [ref] + else: + new_refs = [gdsii.elements.SRef(struct_name=encoded_name, + xy=numpy.round([subpat.offset + dd]).astype(int)) + for dd in rep.displacements] - ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 - # strans must be non-None for angle and mag to take effect - ref.strans = set_bit(0, 15 - 0, mirror_across_x) - ref.mag = subpat.scale + for ref in new_refs: + ref.angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 + # strans must be non-None for angle and mag to take effect + ref.strans = set_bit(0, 15 - 0, mirror_across_x) + ref.mag = subpat.scale - refs.append(ref) + refs += new_refs return refs diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 1de1e09..775cff7 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -28,8 +28,9 @@ import fatamorgana.records as fatrec from fatamorgana.basic import PathExtensionScheme from .utils import mangle_name, make_dose_table -from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t +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 @@ -221,7 +222,7 @@ def read(stream: io.BufferedIOBase, """ Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are translated into Pattern objects; Polygons are translated into polygons, and Placements - are translated into SubPattern or GridRepetition objects. + are translated into SubPattern objects. Additional library info is returned in a dict, containing: 'units_per_micrometer': number of database units per micrometer (all values are in database units) @@ -417,7 +418,7 @@ def read(stream: io.BufferedIOBase, continue for placement in cell.placements: - pat.subpatterns += _placement_to_subpats(placement) + pat.subpatterns.append(_placement_to_subpat(placement)) patterns.append(pat) @@ -451,7 +452,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]: return layer, data_type -def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]: +def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: """ Helper function to create a SubPattern from a placment. Sets subpat.pattern to None and sets the instance .identifier to (struct_name,). @@ -468,27 +469,24 @@ def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]: 'identifier': (name,), } - subpats: List[subpattern_t] + mrep: Repetition rep = placement.repetition if isinstance(rep, fatamorgana.GridRepetition): - subpat = GridRepetition(a_vector=rep.a_vector, - b_vector=rep.b_vector, - a_count=rep.a_count, - b_count=rep.b_count, - offset=xy, - **args) - subpats = [subpat] + mrep = Grid(a_vector=rep.a_vector, + b_vector=rep.b_vector, + a_count=rep.a_count, + b_count=rep.b_count) elif isinstance(rep, fatamorgana.ArbitraryRepetition): - subpats = [] - for rep_offset in numpy.cumsum(numpy.column_stack((rep.x_displacements, - rep.y_displacements))): - subpats.append(SubPattern(offset=xy + rep_offset, **args)) + mrep = Arbitrary(numpy.cumsum(numpy.column_stack((rep.x_displacements, + rep.y_displacements)))) elif rep is None: - subpats = [SubPattern(offset=xy, **args)] - return subpats + mrep = None + + subpat = SubPattern(offset=xy, repetition=mrep, **args) + return subpat -def _subpatterns_to_refs(subpatterns: List[subpattern_t] +def _subpatterns_to_refs(subpatterns: List[SubPattern] ) -> List[fatrec.Placement]: refs = [] for subpat in subpatterns: @@ -503,14 +501,21 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t] 'y': xy[1], } - if isinstance(subpat, GridRepetition): + rep = subpat.repetition + if isinstance(rep, Grid): args['repetition'] = fatamorgana.GridRepetition( - a_vector=numpy.round(subpat.a_vector).astype(int), - b_vector=numpy.round(subpat.b_vector).astype(int), - a_count=numpy.round(subpat.a_count).astype(int), - b_count=numpy.round(subpat.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)) + elif isinstance(rep, Arbitrary): + diffs = numpy.diff(rep.displacements, axis=0) + args['repetition'] = fatamorgana.ArbitraryRepetition( + numpy.round(diffs).astype(int)) + else: + assert(rep is None) - angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 + angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 ref = fatrec.Placement( name=subpat.pattern.name, flip=mirror_across_x, diff --git a/masque/label.py b/masque/label.py index df7f3cf..9a6d326 100644 --- a/masque/label.py +++ b/masque/label.py @@ -4,20 +4,15 @@ import numpy from numpy import pi from .error import PatternError, PatternLockedError -from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t +from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots +from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl -class Label: +class Label(PositionableImpl, LayerableImpl, LockableImpl, Pivotable, Copyable, metaclass=AutoSlots): """ A text annotation with a position and layer (but no size; it is not drawn) """ - __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked') - - _offset: numpy.ndarray - """ [x_offset, y_offset] """ - - _layer: layer_t - """ Layer (integer >= 0, or 2-Tuple of integers) """ + __slots__ = ( '_string', 'identifier') _string: str """ Label string """ @@ -25,44 +20,9 @@ class Label: identifier: Tuple """ Arbitrary identifier tuple, useful for keeping track of history when flattening """ - locked: bool - """ If `True`, any changes to the label will raise a `PatternLockedError` """ - - def __setattr__(self, name, value): - if self.locked and name != 'locked': - raise PatternLockedError() - object.__setattr__(self, name, value) - - # ---- Properties - # offset property - @property - def offset(self) -> numpy.ndarray: - """ - [x, y] offset - """ - return self._offset - - @offset.setter - def offset(self, val: vector2): - if not isinstance(val, numpy.ndarray): - val = numpy.array(val, dtype=float) - - if val.size != 2: - raise PatternError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten().astype(float) - - # layer property - @property - def layer(self) -> layer_t: - """ - Layer number or name (int, tuple of ints, or string) - """ - return self._layer - - @layer.setter - def layer(self, val: layer_t): - self._layer = val - + ''' + ---- Properties + ''' # string property @property def string(self) -> str: @@ -100,25 +60,6 @@ class Label: new.locked = self.locked return new - def copy(self) -> 'Label': - """ - Returns a deep copy of the label. - """ - return copy.deepcopy(self) - - def translate(self, offset: vector2) -> 'Label': - """ - Translate the label by the given offset - - Args: - offset: [x_offset, y,offset] - - Returns: - self - """ - self.offset += offset - return self - def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': """ Rotate the label around a point. @@ -150,25 +91,13 @@ class Label: return numpy.array([self.offset, self.offset]) def lock(self) -> 'Label': - """ - Lock the Label, causing any modifications to raise an exception. - - Return: - self - """ - self.offset.flags.writeable = False - object.__setattr__(self, 'locked', True) + PositionableImpl._lock(self) + LockableImpl.lock(self) return self def unlock(self) -> 'Label': - """ - Unlock the Label, re-allowing changes. - - Return: - self - """ - object.__setattr__(self, 'locked', False) - self.offset.flags.writeable = True + LockableImpl.unlock(self) + PositionableImpl._unlock(self) return self def __repr__(self) -> str: diff --git a/masque/pattern.py b/masque/pattern.py index 663b060..2e57464 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -13,8 +13,7 @@ import numpy from numpy import inf # .visualize imports matplotlib and matplotlib.collections -from .subpattern import SubPattern, subpattern_t -from .repetition import GridRepetition +from .subpattern import SubPattern from .shapes import Shape, Polygon from .label import Label from .utils import rotation_matrix_2d, vector2, normalize_mirror @@ -27,8 +26,7 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray] class Pattern: """ 2D layout consisting of some set of shapes, labels, and references to other Pattern objects - (via SubPattern and GridRepetition). Shapes are assumed to inherit from - masque.shapes.Shape or provide equivalent functions. + (via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. """ __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') @@ -40,11 +38,10 @@ class Pattern: labels: List[Label] """ List of all labels in this Pattern. """ - subpatterns: List[subpattern_t] - """ List of all objects referencing other patterns in this Pattern. - Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays") + subpatterns: List[SubPattern] + """ List of all references to other patterns (`SubPattern`s) in this `Pattern`. Multiple objects in this list may reference the same Pattern object - (multiple instances of the same object). + (i.e. multiple instances of the same object). """ name: str @@ -57,7 +54,7 @@ class Pattern: name: str = '', shapes: Sequence[Shape] = (), labels: Sequence[Label] = (), - subpatterns: Sequence[subpattern_t] = (), + subpatterns: Sequence[SubPattern] = (), locked: bool = False, ): """ @@ -134,7 +131,7 @@ class Pattern: def subset(self, shapes_func: Callable[[Shape], bool] = None, labels_func: Callable[[Label], bool] = None, - subpatterns_func: Callable[[subpattern_t], bool] = None, + subpatterns_func: Callable[[SubPattern], bool] = None, recursive: bool = False, ) -> 'Pattern': """ @@ -493,7 +490,7 @@ class Pattern: def subpatterns_by_id(self, include_none: bool = False, recursive: bool = True, - ) -> Dict[int, List[subpattern_t]]: + ) -> Dict[int, List[SubPattern]]: """ Create a dictionary which maps `{id(referenced_pattern): [subpattern0, ...]}` for all SubPattern objects referenced by this Pattern (by default, operates @@ -506,7 +503,7 @@ class Pattern: Returns: Dictionary mapping each pattern id to a list of subpattern objects referencing the pattern. """ - ids: Dict[int, List[subpattern_t]] = defaultdict(list) + ids: Dict[int, List[SubPattern]] = defaultdict(list) for subpat in self.subpatterns: pat = subpat.pattern if include_none or pat is not None: diff --git a/masque/repetition.py b/masque/repetition.py index 81e8b86..737fb33 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -1,78 +1,47 @@ """ - Repetitions provides support for efficiently nesting multiple identical - instances of a Pattern in the same parent Pattern. + Repetitions provide support for efficiently representing multiple identical + instances of an object . """ from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any import copy +from abc import ABCMeta, abstractmethod import numpy -from numpy import pi from .error import PatternError, PatternLockedError -from .utils import is_scalar, rotation_matrix_2d, vector2 - -if TYPE_CHECKING: - from . import Pattern +from .utils import rotation_matrix_2d, vector2, AutoSlots +from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable -# TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied - -class GridRepetition: +class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta): """ - GridRepetition provides support for efficiently embedding multiple copies of a `Pattern` - into another `Pattern` at regularly-spaced offsets. - - Note that rotation, scaling, and mirroring are applied to individual instances of the - pattern, not to the grid vectors. - - The order of operations is - 1. A single refernce instance to the target pattern is mirrored - 2. The single instance is rotated. - 3. The instance is scaled by the scaling factor. - 4. The instance is shifted by the provided offset - (no mirroring/scaling/rotation is applied to the offset). - 5. Additional copies of the instance will appear at coordinates specified by - `(offset + aa * a_vector + bb * b_vector)`, with `aa in range(0, a_count)` - and `bb in range(0, b_count)`. All instance locations remain unaffected by - mirroring/scaling/rotation, though each instance's data will be transformed - relative to the instance's location (i.e. relative to the contained pattern's - (0, 0) point). + Interface common to all objects which specify repetitions """ - __slots__ = ('_pattern', - '_offset', - '_rotation', - '_dose', - '_scale', - '_mirrored', - '_a_vector', + __slots__ = () + + @property + @abstractmethod + def displacements(self) -> numpy.ndarray: + """ + An Nx2 ndarray specifying all offsets generated by this repetition + """ + pass + + +class Grid(LockableImpl, Repetition, metaclass=AutoSlots): + """ + `Grid` describes a 2D grid formed by two basis vectors and two 'counts' (sizes). + + The second basis vector and count (`b_vector` and `b_count`) may be omitted, + which makes the grid describe a 1D array. + + Note that the offsets in either the 2D or 1D grids do not have to be axis-aligned. + """ + __slots__ = ('_a_vector', '_b_vector', '_a_count', - '_b_count', - 'identifier', - 'locked') - - _pattern: Optional['Pattern'] - """ The `Pattern` being instanced """ - - _offset: numpy.ndarray - """ (x, y) offset for the base instance """ - - _dose: float - """ Scaling factor applied to the dose """ - - _rotation: float - """ Rotation of the individual instances in the grid (not the grid vectors). - Radians, counterclockwise. - """ - - _scale: float - """ Scaling factor applied to individual instances in the grid (not the grid vectors) """ - - _mirrored: numpy.ndarray # ndarray[bool] - """ Whether to mirror individual instances across the x and y axes - (Applies to individual instances in the grid, not the grid vectors) - """ + '_b_count') _a_vector: numpy.ndarray """ Vector `[x, y]` specifying the first lattice vector of the grid. @@ -91,28 +60,14 @@ class GridRepetition: _b_count: int """ Number of instances along the direction specified by the `b_vector` """ - identifier: Tuple[Any, ...] - """ Arbitrary identifier, used internally by some `masque` functions. """ - - locked: bool - """ If `True`, disallows changes to the GridRepetition """ - def __init__(self, - pattern: Optional['Pattern'], a_vector: numpy.ndarray, a_count: int, b_vector: Optional[numpy.ndarray] = None, b_count: Optional[int] = 1, - offset: vector2 = (0.0, 0.0), - rotation: float = 0.0, - mirrored: Optional[Sequence[bool]] = None, - dose: float = 1.0, - scale: float = 1.0, - locked: bool = False, - identifier: Tuple[Any, ...] = ()): + locked: bool = False,): """ Args: - pattern: Pattern to reference. a_vector: First lattice vector, of the form `[x, y]`. Specifies center-to-center spacing between adjacent instances. a_count: Number of elements in the a_vector direction. @@ -121,14 +76,7 @@ class GridRepetition: Can be omitted when specifying a 1D array. b_count: Number of elements in the `b_vector` direction. Should be omitted if `b_vector` was omitted. - offset: (x, y) offset applied to all instances. - rotation: Rotation (radians, counterclockwise) applied to each instance. - Relative to each instance's (0, 0). - mirrored: Whether to mirror individual instances across the x and y axes. - dose: Scaling factor applied to the dose. - scale: Scaling factor applied to the instances' geometry. - locked: Whether the `GridRepetition` is locked after initialization. - identifier: Arbitrary tuple, used internally by some `masque` functions. + locked: Whether the `Grid` is locked after initialization. Raises: PatternError if `b_*` inputs conflict with each other @@ -144,132 +92,31 @@ class GridRepetition: b_vector = numpy.array([0.0, 0.0]) if a_count < 1: - raise PatternError('Repetition has too-small a_count: ' - '{}'.format(a_count)) + raise PatternError(f'Repetition has too-small a_count: {a_count}') if b_count < 1: - raise PatternError('Repetition has too-small b_count: ' - '{}'.format(b_count)) + raise PatternError(f'Repetition has too-small b_count: {b_count}') object.__setattr__(self, 'locked', False) self.a_vector = a_vector self.b_vector = b_vector self.a_count = a_count self.b_count = b_count - - self.identifier = identifier - self.pattern = pattern - self.offset = offset - self.rotation = rotation - self.dose = dose - self.scale = scale - if mirrored is None: - mirrored = [False, False] - self.mirrored = mirrored self.locked = locked - def __setattr__(self, name, value): - if self.locked and name != 'locked': - raise PatternLockedError() - object.__setattr__(self, name, value) - - def __copy__(self) -> 'GridRepetition': - new = GridRepetition(pattern=self.pattern, - a_vector=self.a_vector.copy(), - b_vector=copy.copy(self.b_vector), - a_count=self.a_count, - b_count=self.b_count, - offset=self.offset.copy(), - rotation=self.rotation, - dose=self.dose, - scale=self.scale, - mirrored=self.mirrored.copy(), - locked=self.locked) + def __copy__(self) -> 'Grid': + new = Grid(a_vector=self.a_vector.copy(), + b_vector=copy.copy(self.b_vector), + a_count=self.a_count, + b_count=self.b_count, + locked=self.locked) return new - def __deepcopy__(self, memo: Dict = None) -> 'GridRepetition': + def __deepcopy__(self, memo: Dict = None) -> 'Grid': memo = {} if memo is None else memo new = copy.copy(self).unlock() - new.pattern = copy.deepcopy(self.pattern, memo) new.locked = self.locked return new - # pattern property - @property - def pattern(self) -> Optional['Pattern']: - return self._pattern - - @pattern.setter - def pattern(self, val: Optional['Pattern']): - from .pattern import Pattern - if val is not None and not isinstance(val, Pattern): - raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) - self._pattern = val - - # offset property - @property - def offset(self) -> numpy.ndarray: - return self._offset - - @offset.setter - def offset(self, val: vector2): - if self.locked: - raise PatternLockedError() - - if not isinstance(val, numpy.ndarray): - val = numpy.array(val, dtype=float) - - if val.size != 2: - raise PatternError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten().astype(float) - - # dose property - @property - def dose(self) -> float: - return self._dose - - @dose.setter - def dose(self, val: float): - if not is_scalar(val): - raise PatternError('Dose must be a scalar') - if not val >= 0: - raise PatternError('Dose must be non-negative') - self._dose = val - - # scale property - @property - def scale(self) -> float: - return self._scale - - @scale.setter - def scale(self, val: float): - if not is_scalar(val): - raise PatternError('Scale must be a scalar') - if not val > 0: - raise PatternError('Scale must be positive') - self._scale = val - - # Rotation property [ccw] - @property - def rotation(self) -> float: - return self._rotation - - @rotation.setter - def rotation(self, val: float): - if not is_scalar(val): - raise PatternError('Rotation must be a scalar') - self._rotation = val % (2 * pi) - - # Mirrored property - @property - def mirrored(self) -> numpy.ndarray: # ndarray[bool] - return self._mirrored - - @mirrored.setter - def mirrored(self, val: Sequence[bool]): - if is_scalar(val): - raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = numpy.array(val, dtype=bool, copy=True) - # a_vector property @property def a_vector(self) -> numpy.ndarray: @@ -320,69 +167,15 @@ class GridRepetition: raise PatternError('b_count must be convertable to an int!') self._b_count = int(val) - def as_pattern(self) -> 'Pattern': + @property + def displacements(self) -> numpy.ndarray: + aa, bb = numpy.meshgrid(numpy.arange(self.a_count), numpy.arange(self.b_count), indexing='ij') + return (aa.flat[:, None] * self.a_vector[None, :] + + bb.flat[:, None] * self.b_vector[None, :]) + + def rotate(self, rotation: float) -> 'Grid': """ - Returns a copy of self.pattern which has been scaled, rotated, repeated, etc. - etc. according to this `GridRepetition`'s properties. - - Returns: - A copy of self.pattern which has been scaled, rotated, repeated, etc. - etc. according to this `GridRepetition`'s properties. - """ - assert(self.pattern is not None) - patterns = [] - - pat = self.pattern.deepcopy().deepunlock() - pat.scale_by(self.scale) - [pat.mirror(ax) for ax, do in enumerate(self.mirrored) if do] - pat.rotate_around((0.0, 0.0), self.rotation) - pat.translate_elements(self.offset) - pat.scale_element_doses(self.dose) - - combined = type(pat)(name='__GridRepetition__') - for a in range(self.a_count): - for b in range(self.b_count): - offset = a * self.a_vector + b * self.b_vector - newPat = pat.deepcopy() - newPat.translate_elements(offset) - combined.append(newPat) - - return combined - - def translate(self, offset: vector2) -> 'GridRepetition': - """ - Translate by the given offset - - Args: - offset: `[x, y]` to translate by - - Returns: - self - """ - self.offset += offset - return self - - def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition': - """ - Rotate the array around a point - - Args: - pivot: Point `[x, y]` to rotate around - rotation: Angle to rotate by (counterclockwise, radians) - - Returns: - self - """ - pivot = numpy.array(pivot, dtype=float) - self.translate(-pivot) - self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) - self.rotate(rotation) - self.translate(+pivot) - return self - - def rotate(self, rotation: float) -> 'GridRepetition': - """ - Rotate around (0, 0) + Rotate lattice vectors (around (0, 0)) Args: rotation: Angle to rotate by (counterclockwise, radians) @@ -390,28 +183,14 @@ class GridRepetition: Returns: self """ - self.rotate_elements(rotation) self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector) if self.b_vector is not None: self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector) return self - def rotate_elements(self, rotation: float) -> 'GridRepetition': + def mirror(self, axis: int) -> 'Grid': """ - Rotate each element around its origin - - Args: - rotation: Angle to rotate by (counterclockwise, radians) - - Returns: - self - """ - self.rotation += rotation - return self - - def mirror(self, axis: int) -> 'GridRepetition': - """ - Mirror the GridRepetition across an axis. + Mirror the Grid across an axis. Args: axis: Axis to mirror across. @@ -420,43 +199,30 @@ class GridRepetition: Returns: self """ - self.mirror_elements(axis) self.a_vector[1-axis] *= -1 if self.b_vector is not None: self.b_vector[1-axis] *= -1 return self - def mirror_elements(self, axis: int) -> 'GridRepetition': - """ - Mirror each element across an axis relative to its origin. - - Args: - axis: Axis to mirror across. - (0: mirror across x-axis, 1: mirror across y-axis) - - Returns: - self - """ - self.mirrored[axis] = not self.mirrored[axis] - self.rotation *= -1 - return self - def get_bounds(self) -> Optional[numpy.ndarray]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the - extent of the `GridRepetition` in each dimension. - Returns `None` if the contained `Pattern` is empty. + extent of the `Grid` in each dimension. Returns: `[[x_min, y_min], [x_max, y_max]]` or `None` """ - if self.pattern is None: - return None - return self.as_pattern().get_bounds() + a_extent = self.a_vector * self.a_count + b_extent = self.b_vector * self.b_count if self.b_count != 0 else 0 - def scale_by(self, c: float) -> 'GridRepetition': + corners = ((0, 0), a_extent, b_extent, a_extent + b_extent) + xy_min = numpy.min(corners, axis=0) + xy_max = numpy.min(corners, axis=0) + return numpy.array((xy_min, xy_max)) + + def scale_by(self, c: float) -> 'Grid': """ - Scale the GridRepetition by a factor + Scale the Grid by a factor Args: c: scaling factor @@ -464,107 +230,116 @@ class GridRepetition: Returns: self """ - self.scale_elements_by(c) self.a_vector *= c if self.b_vector is not None: self.b_vector *= c return self - def scale_elements_by(self, c: float) -> 'GridRepetition': + def lock(self) -> 'Grid': """ - Scale each element by a factor - - Args: - c: scaling factor + Lock the `Grid`, disallowing changes. Returns: self """ - self.scale *= c - return self - - def copy(self) -> 'GridRepetition': - """ - Return a shallow copy of the repetition. - - Returns: - `copy.copy(self)` - """ - return copy.copy(self) - - def deepcopy(self) -> 'GridRepetition': - """ - Return a deep copy of the repetition. - - Returns: - `copy.deepcopy(self)` - """ - return copy.deepcopy(self) - - def lock(self) -> 'GridRepetition': - """ - Lock the `GridRepetition`, disallowing changes. - - Returns: - self - """ - self.offset.flags.writeable = False self.a_vector.flags.writeable = False - self.mirrored.flags.writeable = False if self.b_vector is not None: self.b_vector.flags.writeable = False - object.__setattr__(self, 'locked', True) + LockableImpl.lock(self) return self - def unlock(self) -> 'GridRepetition': + def unlock(self) -> 'Grid': """ - Unlock the `GridRepetition` + Unlock the `Grid` Returns: self """ - self.offset.flags.writeable = True self.a_vector.flags.writeable = True - self.mirrored.flags.writeable = True if self.b_vector is not None: self.b_vector.flags.writeable = True - object.__setattr__(self, 'locked', False) - return self - - def deeplock(self) -> 'GridRepetition': - """ - Recursively lock the `GridRepetition` and its contained pattern - - Returns: - self - """ - assert(self.pattern is not None) - self.lock() - self.pattern.deeplock() - return self - - def deepunlock(self) -> 'GridRepetition': - """ - Recursively unlock the `GridRepetition` and its contained pattern - - This is dangerous unless you have just performed a deepcopy, since - the component parts may be reused elsewhere. - - Returns: - self - """ - assert(self.pattern is not None) - self.unlock() - self.pattern.deepunlock() + LockableImpl.unlock(self) return self def __repr__(self) -> str: - name = self.pattern.name if self.pattern is not None else None - rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else '' - scale = f' d{self.scale:g}' if self.scale != 1 else '' - mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' - dose = f' d{self.dose:g}' if self.dose != 1 else '' locked = ' L' if self.locked else '' bv = f', {self.b_vector}' if self.b_vector is not None else '' - return (f'<GridRepetition "{name}" at {self.offset} {rotation}{scale}{mirrored}{dose}' - f' {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>') + return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>') + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, type(self)): + return False + if self.a_count != other.a_count or self.b_count != other.b_count: + return False + if any(self.a_vector[ii] != other.a_vector[ii] for ii in range(2)): + return False + if self.b_vector is None and other.b_vector is None: + return True + if self.b_vector is None or other.b_vector is None: + return False + if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): + return False + if self.locked != other.locked: + return False + return True + + +class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): + """ + `Arbitrary` is a simple list of (absolute) displacements for instances. + + Attributes: + displacements (numpy.ndarray): absolute displacements of all elements + `[[x0, y0], [x1, y1], ...]` + """ + + _displacements: numpy.ndarray + """ List of vectors `[[x0, y0], [x1, y1], ...]` specifying the offsets + of the instances. + """ + + locked: bool + """ If `True`, disallows changes to the object. """ + + @property + def displacements(self) -> numpy.ndarray: + return self._displacements + + @displacements.setter + def displacements(self, val: Union[Sequence[Sequence[float]], numpy.ndarray]): + val = numpy.array(val, float) + val = numpy.sort(val.view([('', val.dtype)] * val.shape[1]), 0).view(val.dtype) # sort rows + self._displacements = val + + def lock(self) -> 'Arbitrary': + """ + Lock the object, disallowing changes. + + Returns: + self + """ + self._displacements.flags.writeable = False + LockableImpl.lock(self) + return self + + def unlock(self) -> 'Arbitrary': + """ + Unlock the object + + Returns: + self + """ + self._displacements.flags.writeable = True + LockableImpl.unlock(self) + return self + + def __repr__(self) -> str: + locked = ' L' if self.locked else '' + return (f'<Arbitrary {len(self.displacements)}pts {locked}>') + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, type(self)): + return False + if self.locked != other.locked: + return False + return numpy.array_equal(self.displacements, other.displacements) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 25a9b2e..7de8f76 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -6,10 +6,10 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError -from ..utils import is_scalar, vector2, layer_t +from ..utils import is_scalar, vector2, layer_t, AutoSlots -class Arc(Shape): +class Arc(Shape, metaclass=AutoSlots): """ An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its center. It has a position, two radii, a start and stop angle, a rotation, and a width. @@ -20,6 +20,7 @@ class Arc(Shape): """ __slots__ = ('_radii', '_angles', '_width', '_rotation', 'poly_num_points', 'poly_max_arclen') + _radii: numpy.ndarray """ Two radii for defining an ellipse """ diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 404aeae..1090588 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -5,14 +5,15 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError -from ..utils import is_scalar, vector2, layer_t +from ..utils import is_scalar, vector2, layer_t, AutoSlots -class Circle(Shape): +class Circle(Shape, metaclass=AutoSlots): """ A circle, which has a position and radius. """ __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen') + _radius: float """ Circle radius """ diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 3288614..45c1ea1 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -6,16 +6,17 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots -class Ellipse(Shape): +class Ellipse(Shape, metaclass=AutoSlots): """ An ellipse, which has a position, two radii, and a rotation. The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. """ __slots__ = ('_radii', '_rotation', 'poly_num_points', 'poly_max_arclen') + _radii: numpy.ndarray """ Ellipse radii """ diff --git a/masque/shapes/path.py b/masque/shapes/path.py index b618001..d4ffac9 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -6,7 +6,7 @@ from numpy import pi, inf from . import Shape, normalized_shape_tuple, Polygon, Circle from .. import PatternError -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices @@ -18,7 +18,7 @@ class PathCap(Enum): # defined by path.cap_extensions -class Path(Shape): +class Path(Shape, metaclass=AutoSlots): """ A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, and an offset. diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 97cc66c..ca0c301 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -5,11 +5,11 @@ from numpy import pi from . import Shape, normalized_shape_tuple from .. import PatternError -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices -class Polygon(Shape): +class Polygon(Shape, metaclass=AutoSlots): """ A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an implicitly-closed boundary, and an offset. @@ -17,6 +17,7 @@ class Polygon(Shape): A `normalized_form(...)` is available, but can be quite slow with lots of vertices. """ __slots__ = ('_vertices',) + _vertices: numpy.ndarray """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index cfc4a54..759942f 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -5,6 +5,9 @@ import numpy from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t +from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, + Rotatable, Mirrorable, Copyable, Scalable, + PivotableImpl, LockableImpl) if TYPE_CHECKING: from . import Polygon @@ -23,38 +26,20 @@ DEFAULT_POLY_NUM_POINTS = 24 T = TypeVar('T', bound='Shape') -class Shape(metaclass=ABCMeta): +class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, LockableImpl, metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ - __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked') - - _offset: numpy.ndarray - """ `[x_offset, y_offset]` """ - - _layer: layer_t - """ Layer (integer >= 0 or tuple) """ - - _dose: float - """ Dose """ identifier: Tuple """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ - locked: bool - """ If `True`, any changes to the shape will raise a `PatternLockedError` """ - - def __setattr__(self, name, value): - if self.locked and name != 'locked': - raise PatternLockedError() - object.__setattr__(self, name, value) - - def __copy__(self) -> 'Shape': - cls = self.__class__ - new = cls.__new__(cls) - for name in Shape.__slots__ + self.__slots__: - object.__setattr__(new, name, getattr(self, name)) - return new +# def __copy__(self) -> 'Shape': +# cls = self.__class__ +# new = cls.__new__(cls) +# for name in Shape.__slots__ + self.__slots__: +# object.__setattr__(new, name, getattr(self, name)) +# return new ''' --- Abstract methods @@ -79,53 +64,6 @@ class Shape(metaclass=ABCMeta): """ pass - @abstractmethod - def get_bounds(self) -> numpy.ndarray: - """ - Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape. - """ - pass - - @abstractmethod - def rotate(self: T, theta: float) -> T: - """ - Rotate the shape around its origin (0, 0), ignoring its offset. - - Args: - theta: Angle to rotate by (counterclockwise, radians) - - Returns: - self - """ - pass - - @abstractmethod - def mirror(self: T, axis: int) -> T: - """ - Mirror the shape across an axis. - - Args: - axis: Axis to mirror across. - (0: mirror across x axis, 1: mirror across y axis) - - Returns: - self - """ - pass - - @abstractmethod - def scale_by(self: T, c: float) -> T: - """ - Scale the shape's size (eg. radius, for a circle) by a constant factor. - - Args: - c: Factor to scale by - - Returns: - self - """ - pass - @abstractmethod def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple: """ @@ -150,97 +88,9 @@ class Shape(metaclass=ABCMeta): """ pass - ''' - ---- Non-abstract properties - ''' - # offset property - @property - def offset(self) -> numpy.ndarray: - """ - [x, y] offset - """ - return self._offset - - @offset.setter - def offset(self, val: vector2): - if not isinstance(val, numpy.ndarray): - val = numpy.array(val, dtype=float) - - if val.size != 2: - raise PatternError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten() - - # layer property - @property - def layer(self) -> layer_t: - """ - Layer number or name (int, tuple of ints, or string) - """ - return self._layer - - @layer.setter - def layer(self, val: layer_t): - self._layer = val - - # dose property - @property - def dose(self) -> float: - """ - Dose (float >= 0) - """ - return self._dose - - @dose.setter - def dose(self, val: float): - if not is_scalar(val): - raise PatternError('Dose must be a scalar') - if not val >= 0: - raise PatternError('Dose must be non-negative') - self._dose = val - ''' ---- Non-abstract methods ''' - def copy(self: T) -> T: - """ - Returns a deep copy of the shape. - - Returns: - copy.deepcopy(self) - """ - return copy.deepcopy(self) - - def translate(self: T, offset: vector2) -> T: - """ - Translate the shape by the given offset - - Args: - offset: [x_offset, y,offset] - - Returns: - self - """ - self.offset += offset - return self - - def rotate_around(self: T, pivot: vector2, rotation: float) -> T: - """ - Rotate the shape around a point. - - Args: - pivot: Point (x, y) to rotate around - rotation: Angle to rotate by (counterclockwise, radians) - - Returns: - self - """ - pivot = numpy.array(pivot, dtype=float) - self.translate(-pivot) - self.rotate(rotation) - self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) - self.translate(+pivot) - return self - def manhattanize_fast(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray, @@ -442,37 +292,12 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons - def set_layer(self: T, layer: layer_t) -> T: - """ - Chainable method for changing the layer. - - Args: - layer: new value for self.layer - - Returns: - self - """ - self.layer = layer - return self - def lock(self: T) -> T: - """ - Lock the Shape, disallowing further changes - - Returns: - self - """ - self.offset.flags.writeable = False - object.__setattr__(self, 'locked', True) + PositionableImpl._lock(self) + LockableImpl.lock(self) return self def unlock(self: T) -> T: - """ - Unlock the Shape - - Returns: - self - """ - object.__setattr__(self, 'locked', False) - self.offset.flags.writeable = True + LockableImpl.unlock(self) + PositionableImpl._unlock(self) return self diff --git a/masque/shapes/text.py b/masque/shapes/text.py index bb8ed0d..9b00161 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -5,22 +5,23 @@ from numpy import pi, inf from . import Shape, Polygon, normalized_shape_tuple from .. import PatternError -from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t +from ..traits import RotatableImpl +from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots # Loaded on use: # from freetype import Face # from matplotlib.path import Path -class Text(Shape): +class Text(RotatableImpl, Shape, metaclass=AutoSlots): """ Text (to be printed e.g. as a set of polygons). This is distinct from non-printed Label objects. """ - __slots__ = ('_string', '_height', '_rotation', '_mirrored', 'font_path') + __slots__ = ('_string', '_height', '_mirrored', 'font_path') + _string: str _height: float - _rotation: float _mirrored: numpy.ndarray #ndarray[bool] font_path: str @@ -33,17 +34,6 @@ class Text(Shape): def string(self, val: str): self._string = val - # Rotation property - @property - def rotation(self) -> float: - return self._rotation - - @rotation.setter - def rotation(self, val: float): - if not is_scalar(val): - raise PatternError('Rotation must be a scalar') - self._rotation = val % (2 * pi) - # Height property @property def height(self) -> float: @@ -120,10 +110,6 @@ class Text(Shape): return all_polygons - def rotate(self, theta: float) -> 'Text': - self.rotation += theta - return self - def mirror(self, axis: int) -> 'Text': self.mirrored[axis] = not self.mirrored[axis] return self diff --git a/masque/subpattern.py b/masque/subpattern.py index f0af4c0..7243e5e 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -11,51 +11,50 @@ import numpy from numpy import pi from .error import PatternError, PatternLockedError -from .utils import is_scalar, rotation_matrix_2d, vector2 -from .repetition import GridRepetition +from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots +from .repetition import Repetition +from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, + Mirrorable, Pivotable, Copyable, LockableImpl, RepeatableImpl) if TYPE_CHECKING: from . import Pattern -class SubPattern: +class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, + Pivotable, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots): """ SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. """ __slots__ = ('_pattern', - '_offset', - '_rotation', - '_dose', - '_scale', '_mirrored', 'identifier', - 'locked') + ) _pattern: Optional['Pattern'] """ The `Pattern` being instanced """ - _offset: numpy.ndarray - """ (x, y) offset for the instance """ +# _offset: numpy.ndarray +# """ (x, y) offset for the instance """ - _rotation: float - """ rotation for the instance, radians counterclockwise """ +# _rotation: float +# """ rotation for the instance, radians counterclockwise """ - _dose: float - """ dose factor for the instance """ +# _dose: float +# """ dose factor for the instance """ - _scale: float - """ scale factor for the instance """ +# _scale: float +# """ scale factor for the instance """ _mirrored: numpy.ndarray # ndarray[bool] - """ Whether to mirror the instanc across the x and/or y axes. """ + """ Whether to mirror the instance across the x and/or y axes. """ identifier: Tuple[Any, ...] """ Arbitrary identifier, used internally by some `masque` functions. """ - locked: bool - """ If `True`, disallows changes to the GridRepetition """ +# locked: bool +# """ If `True`, disallows changes to the SubPattern""" def __init__(self, pattern: Optional['Pattern'], @@ -64,6 +63,7 @@ class SubPattern: mirrored: Optional[Sequence[bool]] = None, dose: float = 1.0, scale: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False, identifier: Tuple[Any, ...] = ()): """ @@ -74,10 +74,12 @@ class SubPattern: mirrored: Whether to mirror the referenced pattern across its x and y axes. dose: Scaling factor applied to the dose. scale: Scaling factor applied to the pattern's geometry. + repetition: TODO locked: Whether the `SubPattern` is locked after initialization. identifier: Arbitrary tuple, used internally by some `masque` functions. """ - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) +# object.__setattr__(self, 'locked', False) self.identifier = identifier self.pattern = pattern self.offset = offset @@ -87,13 +89,9 @@ class SubPattern: if mirrored is None: mirrored = [False, False] self.mirrored = mirrored + self.repetition = repetition self.locked = locked - def __setattr__(self, name, value): - if self.locked and name != 'locked': - raise PatternLockedError() - object.__setattr__(self, name, value) - def __copy__(self) -> 'SubPattern': new = SubPattern(pattern=self.pattern, offset=self.offset.copy(), @@ -123,57 +121,6 @@ class SubPattern: raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) self._pattern = val - # offset property - @property - def offset(self) -> numpy.ndarray: - return self._offset - - @offset.setter - def offset(self, val: vector2): - if not isinstance(val, numpy.ndarray): - val = numpy.array(val, dtype=float) - - if val.size != 2: - raise PatternError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten().astype(float) - - # dose property - @property - def dose(self) -> float: - return self._dose - - @dose.setter - def dose(self, val: float): - if not is_scalar(val): - raise PatternError('Dose must be a scalar') - if not val >= 0: - raise PatternError('Dose must be non-negative') - self._dose = val - - # scale property - @property - def scale(self) -> float: - return self._scale - - @scale.setter - def scale(self, val: float): - if not is_scalar(val): - raise PatternError('Scale must be a scalar') - if not val > 0: - raise PatternError('Scale must be positive') - self._scale = val - - # Rotation property [ccw] - @property - def rotation(self) -> float: - return self._rotation - - @rotation.setter - def rotation(self, val: float): - if not is_scalar(val): - raise PatternError('Rotation must be a scalar') - self._rotation = val % (2 * pi) - # Mirrored property @property def mirrored(self) -> numpy.ndarray: # ndarray[bool] @@ -198,52 +145,17 @@ class SubPattern: pattern.rotate_around((0.0, 0.0), self.rotation) pattern.translate_elements(self.offset) pattern.scale_element_doses(self.dose) + + if pattern.repetition is not None: + combined = type(pat)(name='__repetition__') + for dd in pattern.repetition.displacements: + temp_pat = pattern.deepcopy() + temp_pat.translate_elements(dd) + combined.append(temp_pat) + pattern = combined + return pattern - def translate(self, offset: vector2) -> 'SubPattern': - """ - Translate by the given offset - - Args: - offset: Offset `[x, y]` to translate by - - Returns: - self - """ - self.offset += offset - return self - - def rotate_around(self, pivot: vector2, rotation: float) -> 'SubPattern': - """ - Rotate around a point - - Args: - pivot: Point `[x, y]` to rotate around - rotation: Angle to rotate by (counterclockwise, radians) - - Returns: - self - """ - pivot = numpy.array(pivot, dtype=float) - self.translate(-pivot) - self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) - self.rotate(rotation) - self.translate(+pivot) - return self - - def rotate(self, rotation: float) -> 'SubPattern': - """ - Rotate the instance around it's origin - - Args: - rotation: Angle to rotate by (counterclockwise, radians) - - Returns: - self - """ - self.rotation += rotation - return self - def mirror(self, axis: int) -> 'SubPattern': """ Mirror the subpattern across an axis. @@ -271,37 +183,6 @@ class SubPattern: return None return self.as_pattern().get_bounds() - def scale_by(self, c: float) -> 'SubPattern': - """ - Scale the subpattern by a factor - - Args: - c: scaling factor - - Returns: - self - """ - self.scale *= c - return self - - def copy(self) -> 'SubPattern': - """ - Return a shallow copy of the subpattern. - - Returns: - `copy.copy(self)` - """ - return copy.copy(self) - - def deepcopy(self) -> 'SubPattern': - """ - Return a deep copy of the subpattern. - - Returns: - `copy.deepcopy(self)` - """ - return copy.deepcopy(self) - def lock(self) -> 'SubPattern': """ Lock the SubPattern, disallowing changes @@ -309,9 +190,9 @@ class SubPattern: Returns: self """ - self.offset.flags.writeable = False self.mirrored.flags.writeable = False - object.__setattr__(self, 'locked', True) + PositionableImpl._lock(self) + LockableImpl.lock(self) return self def unlock(self) -> 'SubPattern': @@ -321,9 +202,9 @@ class SubPattern: Returns: self """ - self.offset.flags.writeable = True + LockableImpl.unlock(self) + PositionableImpl._unlock(self) self.mirrored.flags.writeable = True - object.__setattr__(self, 'locked', False) return self def deeplock(self) -> 'SubPattern': @@ -361,6 +242,3 @@ class SubPattern: dose = f' d{self.dose:g}' if self.dose != 1 else '' locked = ' L' if self.locked else '' return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>' - - -subpattern_t = Union[SubPattern, GridRepetition] diff --git a/masque/traits/__init__.py b/masque/traits/__init__.py new file mode 100644 index 0000000..8af6434 --- /dev/null +++ b/masque/traits/__init__.py @@ -0,0 +1,9 @@ +from .positionable import Positionable, PositionableImpl +from .layerable import Layerable, LayerableImpl +from .doseable import Doseable, DoseableImpl +from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl +from .repeatable import Repeatable, RepeatableImpl +from .scalable import Scalable, ScalableImpl +from .mirrorable import Mirrorable +from .copyable import Copyable +from .lockable import Lockable, LockableImpl diff --git a/masque/traits/copyable.py b/masque/traits/copyable.py new file mode 100644 index 0000000..5a318d7 --- /dev/null +++ b/masque/traits/copyable.py @@ -0,0 +1,34 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy + + +T = TypeVar('T', bound='Copyable') + + +class Copyable(metaclass=ABCMeta): + """ + Abstract class which adds .copy() and .deepcopy() + """ + __slots__ = () + + ''' + ---- Non-abstract methods + ''' + def copy(self: T) -> T: + """ + Return a shallow copy of the object. + + Returns: + `copy.copy(self)` + """ + return copy.copy(self) + + def deepcopy(self: T) -> T: + """ + Return a deep copy of the object. + + Returns: + `copy.deepcopy(self)` + """ + return copy.deepcopy(self) diff --git a/masque/traits/doseable.py b/masque/traits/doseable.py new file mode 100644 index 0000000..ded93fa --- /dev/null +++ b/masque/traits/doseable.py @@ -0,0 +1,82 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy +import numpy + +from ..error import PatternError, PatternLockedError +from ..utils import is_scalar + + +T = TypeVar('T', bound='Doseable') +I = TypeVar('I', bound='DoseableImpl') + + +class Doseable(metaclass=ABCMeta): + """ + Abstract class for all doseable entities + """ + __slots__ = () + + ''' + ---- Properties + ''' + @property + @abstractmethod + def dose(self) -> float: + """ + Dose (float >= 0) + """ + pass + + @dose.setter + @abstractmethod + def dose(self, val: float): + pass + + ''' + ---- Methods + ''' + def set_dose(self: T, dose: float) -> T: + """ + Set the dose + + Args: + dose: new value for dose + + Returns: + self + """ + pass + + +class DoseableImpl(Doseable, metaclass=ABCMeta): + """ + Simple implementation of Doseable + """ + __slots__ = () + + _dose: float + """ Dose """ + + ''' + ---- Non-abstract properties + ''' + @property + def dose(self) -> float: + return self._dose + + @dose.setter + def dose(self, val: float): + if not is_scalar(val): + raise PatternError('Dose must be a scalar') + if not val >= 0: + raise PatternError('Dose must be non-negative') + self._dose = val + + + ''' + ---- Non-abstract methods + ''' + def set_dose(self: I, dose: float) -> I: + self.dose = dose + return self diff --git a/masque/traits/layerable.py b/masque/traits/layerable.py new file mode 100644 index 0000000..4cf0b1f --- /dev/null +++ b/masque/traits/layerable.py @@ -0,0 +1,76 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy +import numpy + +from ..error import PatternError, PatternLockedError +from ..utils import layer_t + + +T = TypeVar('T', bound='Layerable') +I = TypeVar('I', bound='LayerableImpl') + + +class Layerable(metaclass=ABCMeta): + """ + Abstract class for all layerable entities + """ + __slots__ = () + ''' + ---- Properties + ''' + @property + @abstractmethod + def layer(self) -> layer_t: + """ + Layer number or name (int, tuple of ints, or string) + """ + return self._layer + + @layer.setter + @abstractmethod + def layer(self, val: layer_t): + self._layer = val + + ''' + ---- Methods + ''' + def set_layer(self: T, layer: layer_t) -> T: + """ + Set the layer + + Args: + layer: new value for layer + + Returns: + self + """ + pass + + +class LayerableImpl(Layerable, metaclass=ABCMeta): + """ + Simple implementation of Layerable + """ + __slots__ = () + + _layer: layer_t + """ Layer number, pair, or name """ + + ''' + ---- Non-abstract properties + ''' + @property + def layer(self) -> layer_t: + return self._layer + + @layer.setter + def layer(self, val: layer_t): + self._layer = val + + ''' + ---- Non-abstract methods + ''' + def set_layer(self: I, layer: layer_t) -> I: + self.layer = layer + return self diff --git a/masque/traits/lockable.py b/masque/traits/lockable.py new file mode 100644 index 0000000..e0ea24e --- /dev/null +++ b/masque/traits/lockable.py @@ -0,0 +1,76 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy +import numpy + +from ..error import PatternError, PatternLockedError + + +T = TypeVar('T', bound='Lockable') +I = TypeVar('I', bound='LockableImpl') + + +class Lockable(metaclass=ABCMeta): + """ + Abstract class for all lockable entities + """ + __slots__ = () + + ''' + ---- Methods + ''' + def set_dose(self: T, dose: float) -> T: + """ + Set the dose + + Args: + dose: new value for dose + + Returns: + self + """ + pass + + def lock(self: T) -> T: + """ + Lock the object, disallowing further changes + + Returns: + self + """ + pass + + def unlock(self: T) -> T: + """ + Unlock the object, reallowing changes + + Returns: + self + """ + pass + + +class LockableImpl(Lockable, metaclass=ABCMeta): + """ + Simple implementation of Lockable + """ + __slots__ = () + + locked: bool + """ If `True`, disallows changes to the object """ + + ''' + ---- Non-abstract methods + ''' + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) + + def lock(self: I) -> I: + object.__setattr__(self, 'locked', True) + return self + + def unlock(self: I) -> I: + object.__setattr__(self, 'locked', False) + return self diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py new file mode 100644 index 0000000..76226c4 --- /dev/null +++ b/masque/traits/mirrorable.py @@ -0,0 +1,61 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy +import numpy + +from ..error import PatternError, PatternLockedError + +T = TypeVar('T', bound='Mirrorable') +T = TypeVar('T', bound='MirrorableImpl') + + +class Mirrorable(metaclass=ABCMeta): + """ + Abstract class for all mirrorable entities + """ + __slots__ = () + + ''' + ---- Abstract methods + ''' + @abstractmethod + def mirror(self: T, axis: int) -> T: + """ + Mirror the entity across an axis. + + Args: + axis: Axis to mirror across. + + Returns: + self + """ + pass + + +#class MirrorableImpl(Mirrorable, metaclass=ABCMeta): +# """ +# Simple implementation of `Mirrorable` +# """ +# __slots__ = () +# +# _mirrored: numpy.ndarray # ndarray[bool] +# """ Whether to mirror the instance across the x and/or y axes. """ +# +# ''' +# ---- Properties +# ''' +# # Mirrored property +# @property +# def mirrored(self) -> numpy.ndarray: # ndarray[bool] +# """ Whether to mirror across the [x, y] axes, respectively """ +# return self._mirrored +# +# @mirrored.setter +# def mirrored(self, val: Sequence[bool]): +# if is_scalar(val): +# raise PatternError('Mirrored must be a 2-element list of booleans') +# self._mirrored = numpy.array(val, dtype=bool, copy=True) +# +# ''' +# ---- Methods +# ''' diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py new file mode 100644 index 0000000..3669a7e --- /dev/null +++ b/masque/traits/positionable.py @@ -0,0 +1,135 @@ +# TODO top-level comment about how traits should set __slots__ = (), and how to use AutoSlots + +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy +import numpy + +from ..error import PatternError, PatternLockedError +from ..utils import is_scalar, rotation_matrix_2d, vector2 + + +T = TypeVar('T', bound='Positionable') +I = TypeVar('I', bound='PositionableImpl') + + +class Positionable(metaclass=ABCMeta): + """ + Abstract class for all positionable entities + """ + __slots__ = () + + ''' + ---- Abstract properties + ''' + @property + @abstractmethod + def offset(self) -> numpy.ndarray: + """ + [x, y] offset + """ + pass + + @offset.setter + @abstractmethod + def offset(self, val: vector2): + pass + + ''' + --- Abstract methods + ''' + @abstractmethod + def get_bounds(self) -> numpy.ndarray: + """ + Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity. + """ + pass + + @abstractmethod + def set_offset(self: T, offset: vector2) -> T: + """ + Set the offset + + Args: + offset: [x_offset, y,offset] + + Returns: + self + """ + pass + + @abstractmethod + def translate(self: T, offset: vector2) -> T: + """ + Translate the entity by the given offset + + Args: + offset: [x_offset, y,offset] + + Returns: + self + """ + pass + + +class PositionableImpl(Positionable, metaclass=ABCMeta): + """ + Simple implementation of Positionable + """ + __slots__ = () + + _offset: numpy.ndarray + """ `[x_offset, y_offset]` """ + + ''' + ---- Properties + ''' + # offset property + @property + def offset(self) -> numpy.ndarray: + """ + [x, y] offset + """ + return self._offset + + @offset.setter + def offset(self, val: vector2): + if not isinstance(val, numpy.ndarray): + val = numpy.array(val, dtype=float) + + if val.size != 2: + raise PatternError('Offset must be convertible to size-2 ndarray') + self._offset = val.flatten() + + + ''' + ---- Methods + ''' + def set_offset(self: I, offset: vector2) -> I: + self.offset = offset + return self + + + def translate(self: I, offset: vector2) -> I: + self._offset += offset + return self + + def _lock(self: I) -> I: + """ + Lock the entity, disallowing further changes + + Returns: + self + """ + self._offset.flags.writeable = False + return self + + def _unlock(self: I) -> I: + """ + Unlock the entity + + Returns: + self + """ + self._offset.flags.writeable = True + return self diff --git a/masque/traits/repeatable.py b/masque/traits/repeatable.py new file mode 100644 index 0000000..1f2a99b --- /dev/null +++ b/masque/traits/repeatable.py @@ -0,0 +1,79 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy +import numpy + +from ..error import PatternError, PatternLockedError + + +T = TypeVar('T', bound='Repeatable') +I = TypeVar('I', bound='RepeatableImpl') + + +class Repeatable(metaclass=ABCMeta): + """ + Abstract class for all repeatable entities + """ + __slots__ = () + + ''' + ---- Properties + ''' + @property + @abstractmethod + def repetition(self) -> Optional['Repetition']: + """ + Repetition object, or None (single instance only) + """ + pass + + @repetition.setter + @abstractmethod + def repetition(self, repetition: Optional['Repetition']): + pass + + ''' + ---- Methods + ''' + def set_repetition(self: T, repetition: Optional['Repetition']) -> T: + """ + Set the repetition + + Args: + repetition: new value for repetition, or None (single instance) + + Returns: + self + """ + pass + + +class RepeatableImpl(Repeatable, metaclass=ABCMeta): + """ + Simple implementation of `Repeatable` + """ + __slots__ = () + + _repetition: Optional['Repetition'] + """ Repetition object, or None (single instance only) """ + + ''' + ---- Non-abstract properties + ''' + @property + def repetition(self) -> Optional['Repetition']: + return self._repetition + + @repetition.setter + def repetition(self, repetition: Optional['Repetition']): + from ..repetition import Repetition + if repetition is not None and not isinstance(repetition, Repetition): + raise PatternError(f'{repetition} is not a valid Repetition object!') + self._repetition = repetition + + ''' + ---- Non-abstract methods + ''' + def set_repetition(self: I, repetition: 'Repetition') -> I: + self.repetition = repetition + return self diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py new file mode 100644 index 0000000..e01c81d --- /dev/null +++ b/masque/traits/rotatable.py @@ -0,0 +1,119 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy + +import numpy +from numpy import pi + +from .positionable import Positionable +from ..error import PatternError, PatternLockedError +from ..utils import is_scalar, rotation_matrix_2d, vector2 + +T = TypeVar('T', bound='Rotatable') +I = TypeVar('I', bound='RotatableImpl') +P = TypeVar('P', bound='Pivotable') +J = TypeVar('J', bound='PivotableImpl') + + +class Rotatable(metaclass=ABCMeta): + """ + Abstract class for all rotatable entities + """ + __slots__ = () + + ''' + ---- Abstract methods + ''' + @abstractmethod + def rotate(self: T, theta: float) -> T: + """ + Rotate the shape around its origin (0, 0), ignoring its offset. + + Args: + theta: Angle to rotate by (counterclockwise, radians) + + Returns: + self + """ + pass + + +class RotatableImpl(Rotatable, metaclass=ABCMeta): + """ + Simple implementation of `Rotatable` + """ + __slots__ = () + + _rotation: float + """ rotation for the object, radians counterclockwise """ + + ''' + ---- Properties + ''' + @property + def rotation(self) -> float: + """ Rotation, radians counterclockwise """ + return self._rotation + + @rotation.setter + def rotation(self, val: float): + if not is_scalar(val): + raise PatternError('Rotation must be a scalar') + self._rotation = val % (2 * pi) + + ''' + ---- Methods + ''' + def rotate(self: I, rotation: float) -> I: + self.rotation += rotation + return self + + def set_rotation(self: I, rotation: float) -> I: + """ + Set the rotation to a value + + Args: + rotation: radians ccw + + Returns: + self + """ + self.rotation = rotation + return self + + +class Pivotable(metaclass=ABCMeta): + """ + Abstract class for entites which can be rotated around a point. + This requires that they are `Positionable` but not necessarily `Rotatable` themselves. + """ + __slots__ = () + + def rotate_around(self: P, pivot: vector2, rotation: float) -> P: + """ + Rotate the object around a point. + + Args: + pivot: Point (x, y) to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self + """ + pass + + +class PivotableImpl(Pivotable, metaclass=ABCMeta): + """ + Implementation of `Pivotable` for objects which are `Rotatable` + """ + __slots__ = () + + def rotate_around(self: J, pivot: vector2, rotation: float) -> J: + pivot = numpy.array(pivot, dtype=float) + self.translate(-pivot) + self.rotate(rotation) + self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) + self.translate(+pivot) + return self + diff --git a/masque/traits/scalable.py b/masque/traits/scalable.py new file mode 100644 index 0000000..ac349a2 --- /dev/null +++ b/masque/traits/scalable.py @@ -0,0 +1,79 @@ +from typing import List, Tuple, Callable, TypeVar, Optional +from abc import ABCMeta, abstractmethod +import copy +import numpy + +from ..error import PatternError, PatternLockedError +from ..utils import is_scalar + + +T = TypeVar('T', bound='Scalable') +I = TypeVar('I', bound='ScalableImpl') + + +class Scalable(metaclass=ABCMeta): + """ + Abstract class for all scalable entities + """ + __slots__ = () + + ''' + ---- Abstract methods + ''' + @abstractmethod + def scale_by(self: T, c: float) -> T: + """ + Scale the entity by a factor + + Args: + c: scaling factor + + Returns: + self + """ + pass + + +class ScalableImpl(Scalable, metaclass=ABCMeta): + """ + Simple implementation of Scalable + """ + __slots__ = () + + _scale: float + """ scale factor for the entity """ + + ''' + ---- Properties + ''' + @property + def scale(self) -> float: + return self._scale + + @scale.setter + def scale(self, val: float): + if not is_scalar(val): + raise PatternError('Scale must be a scalar') + if not val > 0: + raise PatternError('Scale must be positive') + self._scale = val + + ''' + ---- Methods + ''' + def scale_by(self: I, c: float) -> I: + self.scale *= c + return self + + def set_scale(self: I, scale: float) -> I: + """ + Set the sclae to a value + + Args: + scale: absolute scale factor + + Returns: + self + """ + self.scale = scale + return self diff --git a/masque/utils.py b/masque/utils.py index e71a0cd..979ffde 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -3,6 +3,7 @@ Various helper functions """ from typing import Any, Union, Tuple, Sequence +from abc import ABCMeta import numpy @@ -133,3 +134,25 @@ def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) slopes_equal[[0, -1]] = False return vertices[~slopes_equal] + + +class AutoSlots(ABCMeta): + """ + Metaclass for automatically generating __slots__ based on superclass type annotations. + + Superclasses must set `__slots__ = ()` to make this work properly. + + This is a workaround for the fact that non-empty `__slots__` can't be used + with multiple inheritance. Since we only use multiple inheritance with abstract + classes, they can have empty `__slots__` and their attribute type annotations + can be used to generate a full `__slots__` for the concrete class. + """ + def __new__(cls, name, bases, dctn): + slots = tuple(dctn.get('__slots__', tuple())) + for base in bases: + if not hasattr(base, '__annotations__'): + continue + slots += tuple(getattr(base, '__annotations__').keys()) + dctn['__slots__'] = slots + return super().__new__(cls,name,bases,dctn) + From 794ebb6b37a36e05728311b780acc0d7473a37ad Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 22 Jul 2020 21:48:34 -0700 Subject: [PATCH 21/71] repetition related fixup --- masque/file/dxf.py | 8 ++-- masque/file/gdsii.py | 9 +++-- masque/file/oasis.py | 14 ++++--- masque/repetition.py | 70 +++++++++++++++++++++++++++++++++-- masque/subpattern.py | 22 ++--------- masque/traits/doseable.py | 8 ++-- masque/traits/layerable.py | 10 ++--- masque/traits/lockable.py | 12 ------ masque/traits/mirrorable.py | 2 +- masque/traits/positionable.py | 8 ++-- masque/traits/repeatable.py | 17 ++++++--- 11 files changed, 112 insertions(+), 68 deletions(-) diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 4faac8c..56b0bc5 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -299,14 +299,14 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M rotated_a = rotation_matrix_2d(-subpat.rotation) @ a rotated_b = rotation_matrix_2d(-subpat.rotation) @ b if rotated_a[1] == 0 and rotated_b[0] == 0: - attribs['column_count'] = subpat.a_count - attribs['row_count'] = subpat.b_count + attribs['column_count'] = rep.a_count + attribs['row_count'] = rep.b_count attribs['column_spacing'] = rotated_a[0] attribs['row_spacing'] = rotated_b[1] block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs) elif rotated_a[0] == 0 and rotated_b[1] == 0: - attribs['column_count'] = subpat.b_count - attribs['row_count'] = subpat.a_count + attribs['column_count'] = rep.b_count + attribs['row_count'] = rep.a_count attribs['column_spacing'] = rotated_b[0] attribs['row_spacing'] = rotated_a[1] block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 4c2198c..0be8e3e 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -365,8 +365,8 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, if isinstance(element, gdsii.elements.ARef): a_count = element.cols b_count = element.rows - a_vector = (element.xy[1] - offset) / counts[0] - b_vector = (element.xy[2] - offset) / counts[1] + a_vector = (element.xy[1] - offset) / a_count + b_vector = (element.xy[2] - offset) / b_count repetition = Grid(a_vector=a_vector, b_vector=b_vector, a_count=a_count, b_count=b_count) @@ -389,9 +389,10 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] # Note: GDS mirrors first and rotates second mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) - ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] - rep = subpat.repetition + + new_refs: List[Union[gdsii.elements.SRef, gdsii.elements.ARef]] + ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] if isinstance(rep, Grid): xy = numpy.array(subpat.offset) + [ [0, 0], diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 775cff7..981d1e0 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -469,7 +469,7 @@ def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: 'identifier': (name,), } - mrep: Repetition + mrep: Optional[Repetition] rep = placement.repetition if isinstance(rep, fatamorgana.GridRepetition): mrep = Grid(a_vector=rep.a_vector, @@ -477,8 +477,10 @@ def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: a_count=rep.a_count, b_count=rep.b_count) elif isinstance(rep, fatamorgana.ArbitraryRepetition): - mrep = Arbitrary(numpy.cumsum(numpy.column_stack((rep.x_displacements, - rep.y_displacements)))) + displacements = numpy.cumsum(numpy.column_stack((rep.x_displacements, + rep.y_displacements))) + displacements = numpy.vstack(([0, 0], displacements)) + mrep = Arbitrary(displacements) elif rep is None: mrep = None @@ -510,8 +512,10 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] b_count=numpy.round(rep.b_count).astype(int)) elif isinstance(rep, Arbitrary): diffs = numpy.diff(rep.displacements, axis=0) - args['repetition'] = fatamorgana.ArbitraryRepetition( - numpy.round(diffs).astype(int)) + diff_ints = numpy.round(diffs).astype(int) + args['repetition'] = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) + args['x'] += rep.displacements[0, 0] + args['y'] += rep.displacements[0, 1] else: assert(rep is None) diff --git a/masque/repetition.py b/masque/repetition.py index 737fb33..5707a50 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -217,7 +217,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): corners = ((0, 0), a_extent, b_extent, a_extent + b_extent) xy_min = numpy.min(corners, axis=0) - xy_max = numpy.min(corners, axis=0) + xy_max = numpy.max(corners, axis=0) return numpy.array((xy_min, xy_max)) def scale_by(self, c: float) -> 'Grid': @@ -298,9 +298,6 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): of the instances. """ - locked: bool - """ If `True`, disallows changes to the object. """ - @property def displacements(self) -> numpy.ndarray: return self._displacements @@ -311,6 +308,18 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): val = numpy.sort(val.view([('', val.dtype)] * val.shape[1]), 0).view(val.dtype) # sort rows self._displacements = val + def __init__(self, + displacements: numpy.ndarray, + locked: bool = False,): + """ + Args: + displacements: List of vectors (Nx2 ndarray) specifying displacements. + locked: Whether the object is locked after initialization. + """ + object.__setattr__(self, 'locked', False) + self.displacements = displacements + self.locked = locked + def lock(self) -> 'Arbitrary': """ Lock the object, disallowing changes. @@ -343,3 +352,56 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): if self.locked != other.locked: return False return numpy.array_equal(self.displacements, other.displacements) + + def rotate(self, rotation: float) -> 'Arbitrary': + """ + Rotate dispacements (around (0, 0)) + + Args: + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self + """ + self.displacements = numpy.dot(rotation_matrix_2d(rotation), self.displacements.T).T + return self + + def mirror(self, axis: int) -> 'Arbitrary': + """ + Mirror the displacements across an axis. + + Args: + axis: Axis to mirror across. + (0: mirror across x-axis, 1: mirror across y-axis) + + Returns: + self + """ + self.displacements[1-axis] *= -1 + return self + + def get_bounds(self) -> Optional[numpy.ndarray]: + """ + Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the + extent of the `displacements` in each dimension. + + Returns: + `[[x_min, y_min], [x_max, y_max]]` or `None` + """ + xy_min = numpy.min(self.displacements, axis=0) + xy_max = numpy.max(self.displacements, axis=0) + return numpy.array((xy_min, xy_max)) + + def scale_by(self, c: float) -> 'Arbitrary': + """ + Scale the displacements by a factor + + Args: + c: scaling factor + + Returns: + self + """ + self.displacements *= c + return self + diff --git a/masque/subpattern.py b/masque/subpattern.py index 7243e5e..e898349 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -35,27 +35,12 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi _pattern: Optional['Pattern'] """ The `Pattern` being instanced """ -# _offset: numpy.ndarray -# """ (x, y) offset for the instance """ - -# _rotation: float -# """ rotation for the instance, radians counterclockwise """ - -# _dose: float -# """ dose factor for the instance """ - -# _scale: float -# """ scale factor for the instance """ - _mirrored: numpy.ndarray # ndarray[bool] """ Whether to mirror the instance across the x and/or y axes. """ identifier: Tuple[Any, ...] """ Arbitrary identifier, used internally by some `masque` functions. """ -# locked: bool -# """ If `True`, disallows changes to the SubPattern""" - def __init__(self, pattern: Optional['Pattern'], offset: vector2 = (0.0, 0.0), @@ -79,7 +64,6 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi identifier: Arbitrary tuple, used internally by some `masque` functions. """ LockableImpl.unlock(self) -# object.__setattr__(self, 'locked', False) self.identifier = identifier self.pattern = pattern self.offset = offset @@ -146,9 +130,9 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi pattern.translate_elements(self.offset) pattern.scale_element_doses(self.dose) - if pattern.repetition is not None: - combined = type(pat)(name='__repetition__') - for dd in pattern.repetition.displacements: + if self.repetition is not None: + combined = type(pattern)(name='__repetition__') + for dd in self.repetition.displacements: temp_pat = pattern.deepcopy() temp_pat.translate_elements(dd) combined.append(temp_pat) diff --git a/masque/traits/doseable.py b/masque/traits/doseable.py index ded93fa..4b9daf3 100644 --- a/masque/traits/doseable.py +++ b/masque/traits/doseable.py @@ -28,10 +28,10 @@ class Doseable(metaclass=ABCMeta): """ pass - @dose.setter - @abstractmethod - def dose(self, val: float): - pass +# @dose.setter +# @abstractmethod +# def dose(self, val: float): +# pass ''' ---- Methods diff --git a/masque/traits/layerable.py b/masque/traits/layerable.py index 4cf0b1f..5382450 100644 --- a/masque/traits/layerable.py +++ b/masque/traits/layerable.py @@ -25,12 +25,12 @@ class Layerable(metaclass=ABCMeta): """ Layer number or name (int, tuple of ints, or string) """ - return self._layer + pass - @layer.setter - @abstractmethod - def layer(self, val: layer_t): - self._layer = val +# @layer.setter +# @abstractmethod +# def layer(self, val: layer_t): +# pass ''' ---- Methods diff --git a/masque/traits/lockable.py b/masque/traits/lockable.py index e0ea24e..cc12760 100644 --- a/masque/traits/lockable.py +++ b/masque/traits/lockable.py @@ -19,18 +19,6 @@ class Lockable(metaclass=ABCMeta): ''' ---- Methods ''' - def set_dose(self: T, dose: float) -> T: - """ - Set the dose - - Args: - dose: new value for dose - - Returns: - self - """ - pass - def lock(self: T) -> T: """ Lock the object, disallowing further changes diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index 76226c4..1e10800 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -6,7 +6,7 @@ import numpy from ..error import PatternError, PatternLockedError T = TypeVar('T', bound='Mirrorable') -T = TypeVar('T', bound='MirrorableImpl') +#I = TypeVar('I', bound='MirrorableImpl') class Mirrorable(metaclass=ABCMeta): diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index 3669a7e..4db6880 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -30,10 +30,10 @@ class Positionable(metaclass=ABCMeta): """ pass - @offset.setter - @abstractmethod - def offset(self, val: vector2): - pass +# @offset.setter +# @abstractmethod +# def offset(self, val: vector2): +# pass ''' --- Abstract methods diff --git a/masque/traits/repeatable.py b/masque/traits/repeatable.py index 1f2a99b..3971a94 100644 --- a/masque/traits/repeatable.py +++ b/masque/traits/repeatable.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Callable, TypeVar, Optional +from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from abc import ABCMeta, abstractmethod import copy import numpy @@ -6,6 +6,10 @@ import numpy from ..error import PatternError, PatternLockedError +if TYPE_CHECKING: + from ..repetition import Repetition + + T = TypeVar('T', bound='Repeatable') I = TypeVar('I', bound='RepeatableImpl') @@ -27,14 +31,15 @@ class Repeatable(metaclass=ABCMeta): """ pass - @repetition.setter - @abstractmethod - def repetition(self, repetition: Optional['Repetition']): - pass +# @repetition.setter +# @abstractmethod +# def repetition(self, repetition: Optional['Repetition']): +# pass ''' ---- Methods ''' + @abstractmethod def set_repetition(self: T, repetition: Optional['Repetition']) -> T: """ Set the repetition @@ -74,6 +79,6 @@ class RepeatableImpl(Repeatable, metaclass=ABCMeta): ''' ---- Non-abstract methods ''' - def set_repetition(self: I, repetition: 'Repetition') -> I: + def set_repetition(self: I, repetition: Optional['Repetition']) -> I: self.repetition = repetition return self From ad6fa88e53bd70d37b18b8beb49e57c8a344b99b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 22 Jul 2020 21:49:27 -0700 Subject: [PATCH 22/71] Expect name to still be a string after disambiguation Check that encode('ascii') doesn't make it zero-length, but don't actually return the encoded form. --- masque/file/gdsii.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 0be8e3e..c91c061 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -119,7 +119,7 @@ def build(patterns: Union[Pattern, List[Pattern]], # Now create a structure for each pattern, and add in any Boundary and SREF elements for pat in patterns_by_id.values(): - structure = gdsii.structure.Structure(name=pat.name) + structure = gdsii.structure.Structure(name=pat.name.encode('ASCII')) lib.append(structure) structure += _shapes_to_elements(pat.shapes) @@ -385,7 +385,7 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] for subpat in subpatterns: if subpat.pattern is None: continue - encoded_name = subpat.pattern.name + encoded_name = subpat.pattern.name.encode('ASCII') # Note: GDS mirrors first and rotates second mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) @@ -514,5 +514,5 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern], if len(encoded_name) > max_name_length: raise PatternError('Pattern name "{!r}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, pat.name)) - pat.name = encoded_name + pat.name = suffixed_name used_names.append(suffixed_name) From 629a6a9ba2d8113a0d73de07364f562970b16c34 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 22 Jul 2020 21:50:39 -0700 Subject: [PATCH 23/71] enable per-shape repetitions --- masque/label.py | 11 +++++++--- masque/pattern.py | 46 ++++++++++++++++++++++++++++++++++++++++ masque/shapes/arc.py | 3 +++ masque/shapes/circle.py | 3 +++ masque/shapes/ellipse.py | 3 +++ masque/shapes/path.py | 3 +++ masque/shapes/polygon.py | 3 +++ masque/shapes/shape.py | 5 +++-- masque/shapes/text.py | 3 +++ 9 files changed, 75 insertions(+), 5 deletions(-) diff --git a/masque/label.py b/masque/label.py index 9a6d326..72b7266 100644 --- a/masque/label.py +++ b/masque/label.py @@ -1,14 +1,16 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional import copy import numpy from numpy import pi +from .repetition import Repetition from .error import PatternError, PatternLockedError from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots -from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl +from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl -class Label(PositionableImpl, LayerableImpl, LockableImpl, Pivotable, Copyable, metaclass=AutoSlots): +class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, + Pivotable, Copyable, metaclass=AutoSlots): """ A text annotation with a position and layer (but no size; it is not drawn) """ @@ -39,18 +41,21 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, Pivotable, Copyable, string: str, offset: vector2 = (0.0, 0.0), layer: layer_t = 0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () self.string = string self.offset = numpy.array(offset, dtype=float, copy=True) self.layer = layer + self.repetition = repetition self.locked = locked def __copy__(self) -> 'Label': return Label(string=self.string, offset=self.offset.copy(), layer=self.layer, + repetition=self.repetition, locked=self.locked) def __deepcopy__(self, memo: Dict = None) -> 'Label': diff --git a/masque/pattern.py b/masque/pattern.py index 2e57464..9c45749 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -575,6 +575,52 @@ class Pattern: self.append(p) return self + def wrap_repeated_shapes(self, + name_func: Callable[['Pattern', Union[Shape, Label]], str] = lambda p, s: '_repetition', + recursive: bool = True, + ) -> 'Pattern': + """ + Wraps all shapes and labels with a non-`None` `repetition` attribute + into a `SubPattern`/`Pattern` combination, and applies the `repetition` + to each `SubPattern` instead of its contained shape. + + Args: + name_func: Function f(this_pattern, shape) which generates a name for the + wrapping pattern. Default always returns '_repetition'. + recursive: If `True`, this function is also applied to all referenced patterns + recursively. Default `True`. + + Returns: + self + """ + def do_wrap(pat: Optional[Pattern]) -> Optional[Pattern]: + if pat is None: + return pat + + new_subpatterns = [] + for shape in pat.shapes: + if shape.repetition is None: + continue + new_subpatterns.append(SubPattern(Pattern(name_func(pat, shape), shapes=[shape]))) + shape.repetition = None + + for label in self.labels: + if label.repetition is None: + continue + new_subpatterns.append(SubPattern(Pattern(name_func(pat, shape), labels=[label]))) + label.repetition = None + + pat.subpatterns += new_subpatterns + return pat + + if recursive: + self.apply(do_wrap) + else: + do_wrap(self) + + return self + + def translate_elements(self, offset: vector2) -> 'Pattern': """ Translates all shapes, label, and subpatterns by the given offset. diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 7de8f76..a33d0d7 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -6,6 +6,7 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, vector2, layer_t, AutoSlots @@ -158,6 +159,7 @@ class Arc(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () @@ -171,6 +173,7 @@ class Arc(Shape, metaclass=AutoSlots): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Arc': diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 1090588..2834b2a 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -5,6 +5,7 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, vector2, layer_t, AutoSlots @@ -46,6 +47,7 @@ class Circle(Shape, metaclass=AutoSlots): offset: vector2 = (0.0, 0.0), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () @@ -55,6 +57,7 @@ class Circle(Shape, metaclass=AutoSlots): self.radius = radius self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Circle': diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 45c1ea1..f4cc683 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -6,6 +6,7 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots @@ -93,6 +94,7 @@ class Ellipse(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () @@ -104,6 +106,7 @@ class Ellipse(Shape, metaclass=AutoSlots): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': diff --git a/masque/shapes/path.py b/masque/shapes/path.py index d4ffac9..15b1ead 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -6,6 +6,7 @@ from numpy import pi, inf from . import Shape, normalized_shape_tuple, Polygon, Circle from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices @@ -147,6 +148,7 @@ class Path(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False, ): object.__setattr__(self, 'locked', False) @@ -163,6 +165,7 @@ class Path(Shape, metaclass=AutoSlots): self.cap_extensions = cap_extensions self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Path': diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index ca0c301..cee5780 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -5,6 +5,7 @@ from numpy import pi from . import Shape, normalized_shape_tuple from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices @@ -75,6 +76,7 @@ class Polygon(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False, ): object.__setattr__(self, 'locked', False) @@ -85,6 +87,7 @@ class Polygon(Shape, metaclass=AutoSlots): self.offset = offset self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 759942f..c6a0c63 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -7,7 +7,7 @@ from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, - PivotableImpl, LockableImpl) + PivotableImpl, LockableImpl, RepeatableImpl) if TYPE_CHECKING: from . import Polygon @@ -26,7 +26,8 @@ DEFAULT_POLY_NUM_POINTS = 24 T = TypeVar('T', bound='Shape') -class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, LockableImpl, metaclass=ABCMeta): +class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, + PivotableImpl, RepeatableImpl, LockableImpl, metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 9b00161..d796511 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -5,6 +5,7 @@ from numpy import pi, inf from . import Shape, Polygon, normalized_shape_tuple from .. import PatternError +from ..repetition import Repetition from ..traits import RotatableImpl from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots @@ -65,6 +66,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): mirrored: Tuple[bool, bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False, ): object.__setattr__(self, 'locked', False) @@ -77,6 +79,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): self.rotation = rotation self.font_path = font_path self.mirrored = mirrored + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Text': From 7ce601dc1d5875dcc1a665fa40feb6e21e06fc9f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Mon, 27 Jul 2020 01:32:34 -0700 Subject: [PATCH 24/71] Enable repeated shapes in gdsii and oasis --- masque/file/gdsii.py | 2 + masque/file/oasis.py | 123 +++++++++++++++++++++++++------------------ 2 files changed, 75 insertions(+), 50 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index c91c061..a8a583d 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -103,6 +103,8 @@ def build(patterns: Union[Pattern, List[Pattern]], if not modify_originals: patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] + patterns = [p.wrap_repeated_shapes() for p in patterns] + # Create library lib = gdsii.library.Library(version=600, name=library_name.encode('ASCII'), diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 981d1e0..834949d 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -260,18 +260,19 @@ def read(stream: io.BufferedIOBase, for element in cell.geometry: if isinstance(element, fatrec.XElement): logger.warning('Skipping XElement record') + # note XELEMENT has no repetition continue - if element.repetition is not None: - # note XELEMENT has no repetition - raise PatternError('masque OASIS reader does not implement repetitions for shapes yet') + + repetition = repetition_fata2masq(element.repetition) # Switch based on element type: if isinstance(element, fatrec.Polygon): vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0) poly = Polygon(vertices=vertices, layer=element.get_layer_tuple(), - offset=element.get_xy()) + offset=element.get_xy(), + repetition=repetition) if clean_vertices: try: @@ -297,6 +298,7 @@ def read(stream: io.BufferedIOBase, path = Path(vertices=vertices, layer=element.get_layer_tuple(), offset=element.get_xy(), + repeition=repetition, width=element.get_half_width() * 2, cap=cap, **path_args) @@ -314,6 +316,7 @@ def read(stream: io.BufferedIOBase, height = element.get_height() rect = Polygon(layer=element.get_layer_tuple(), offset=element.get_xy(), + repetition=repetition, vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), ) pat.shapes.append(rect) @@ -345,6 +348,7 @@ def read(stream: io.BufferedIOBase, trapz = Polygon(layer=element.get_layer_tuple(), offset=element.get_xy(), + repetition=repetition, vertices=vertices, ) pat.shapes.append(trapz) @@ -396,20 +400,23 @@ def read(stream: io.BufferedIOBase, vertices[0, 1] += width ctrapz = Polygon(layer=element.get_layer_tuple(), - offset=element.get_xy(), - vertices=vertices, - ) + offset=element.get_xy(), + repetition=repetition, + vertices=vertices, + ) pat.shapes.append(ctrapz) elif isinstance(element, fatrec.Circle): circle = Circle(layer=element.get_layer_tuple(), offset=element.get_xy(), + repetition=repetition, radius=float(element.get_radius())) pat.shapes.append(circle) elif isinstance(element, fatrec.Text): label = Label(layer=element.get_layer_tuple(), offset=element.get_xy(), + repetition=repetition, string=str(element.get_string())) pat.labels.append(label) @@ -467,24 +474,10 @@ def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: 'rotation': float(placement.angle * pi/180), 'scale': mag, 'identifier': (name,), + 'repetition': repetition_fata2masq(placement.repetition), } - mrep: Optional[Repetition] - rep = placement.repetition - if isinstance(rep, fatamorgana.GridRepetition): - mrep = Grid(a_vector=rep.a_vector, - b_vector=rep.b_vector, - a_count=rep.a_count, - b_count=rep.b_count) - elif isinstance(rep, fatamorgana.ArbitraryRepetition): - displacements = numpy.cumsum(numpy.column_stack((rep.x_displacements, - rep.y_displacements))) - displacements = numpy.vstack(([0, 0], displacements)) - mrep = Arbitrary(displacements) - elif rep is None: - mrep = None - - subpat = SubPattern(offset=xy, repetition=mrep, **args) + subpat = SubPattern(offset=xy, **args) return subpat @@ -497,27 +490,14 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] # Note: OASIS mirrors first and rotates second mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) - xy = numpy.round(subpat.offset).astype(int) - args: Dict[str, Any] = { - 'x': xy[0], - 'y': xy[1], - } + frep, rep_offset = repetition_masq2fata(subpat.repetition) - rep = subpat.repetition - if isinstance(rep, Grid): - args['repetition'] = 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)) - elif isinstance(rep, Arbitrary): - diffs = numpy.diff(rep.displacements, axis=0) - diff_ints = numpy.round(diffs).astype(int) - args['repetition'] = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) - args['x'] += rep.displacements[0, 0] - args['y'] += rep.displacements[0, 1] - else: - assert(rep is None) + offset = numpy.round(subpat.offset + rep_offset).astype(int) + args: Dict[str, Any] = { + 'x': offset[0], + 'y': offset[1], + 'repetition': frep, + } angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 ref = fatrec.Placement( @@ -538,17 +518,19 @@ def _shapes_to_elements(shapes: List[Shape], elements: List[Union[fatrec.Polygon, fatrec.Path, fatrec.Circle]] = [] for shape in shapes: layer, datatype = layer2oas(shape.layer) + repetition, rep_offset = repetition_masq2fata(shape.repetition) if isinstance(shape, Circle): - offset = numpy.round(shape.offset).astype(int) + offset = numpy.round(shape.offset + rep_offset).astype(int) radius = numpy.round(shape.radius).astype(int) circle = fatrec.Circle(layer=layer, datatype=datatype, radius=radius, x=offset[0], - y=offset[1]) + y=offset[1], + repetition=repetition) elements.append(circle) elif isinstance(shape, Path): - xy = numpy.round(shape.offset + shape.vertices[0]).astype(int) + 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 @@ -562,17 +544,19 @@ def _shapes_to_elements(shapes: List[Shape], y=xy[1], extension_start=extension_start, #TODO implement multiple cap types? extension_end=extension_end, + repetition=repetition, ) elements.append(path) else: for polygon in shape.to_polygons(): - xy = numpy.round(polygon.offset + polygon.vertices[0]).astype(int) + xy = numpy.round(polygon.offset + polygon.vertices[0] + rep_offset).astype(int) points = numpy.round(numpy.diff(polygon.vertices, axis=0)).astype(int) elements.append(fatrec.Polygon(layer=layer, datatype=datatype, x=xy[0], y=xy[1], - point_list=points)) + point_list=points, + repetition=repetition)) return elements @@ -582,12 +566,14 @@ def _labels_to_texts(labels: List[Label], texts = [] for label in labels: layer, datatype = layer2oas(label.layer) - xy = numpy.round(label.offset).astype(int) + repetition, rep_offset = repetition_masq2fata(shape.repetition) + xy = numpy.round(label.offset + rep_offset).astype(int) texts.append(fatrec.Text(layer=layer, datatype=datatype, x=xy[0], y=xy[1], - string=label.string)) + string=label.string, + repetition=repetition)) return texts @@ -619,3 +605,40 @@ def disambiguate_pattern_names(patterns, pat.name = suffixed_name used_names.append(suffixed_name) + + +def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None] + ) -> Optional[Repetition]: + if isinstance(rep, fatamorgana.GridRepetition): + mrep = Grid(a_vector=rep.a_vector, + b_vector=rep.b_vector, + a_count=rep.a_count, + b_count=rep.b_count) + elif isinstance(rep, fatamorgana.ArbitraryRepetition): + displacements = numpy.cumsum(numpy.column_stack((rep.x_displacements, + rep.y_displacements))) + displacements = numpy.vstack(([0, 0], displacements)) + mrep = Arbitrary(displacements) + elif rep is None: + mrep = None + return mrep + + +def repetition_masq2fata(rep: Optional[Repetition]): + 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)) + offset = (0, 0) + elif isinstance(rep, Arbitrary): + diffs = numpy.diff(rep.displacements, axis=0) + diff_ints = numpy.round(diffs).astype(int) + frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) + offset = rep.displacements[0, :] + else: + assert(rep is None) + frep = None + offset = (0, 0) + return frep, offset From f57ccc073d9981b0d7fd237921480121dd216ace Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 11 Aug 2020 01:18:29 -0700 Subject: [PATCH 25/71] add `raw` arg, which skips setter funcs --- masque/shapes/arc.py | 32 ++++++++++++++++++++++---------- masque/shapes/ellipse.py | 24 +++++++++++++++++------- masque/shapes/path.py | 26 ++++++++++++++++++-------- masque/shapes/polygon.py | 18 +++++++++++++----- masque/shapes/text.py | 27 +++++++++++++++++++-------- 5 files changed, 89 insertions(+), 38 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index a33d0d7..4918ea7 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -160,20 +160,32 @@ class Arc(Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, - locked: bool = False): + locked: bool = False, + raw: bool = False, + ): object.__setattr__(self, 'locked', False) self.identifier = () - self.radii = radii - self.angles = angles - self.width = width - self.offset = offset - self.rotation = rotation - [self.mirror(a) for a, do in enumerate(mirrored) if do] - self.layer = layer - self.dose = dose + if raw: + self._radii = radii + self._angles = angles + self._width = width + self._offset = offset + self._rotation = rotation + self._repetition = repetition + self._layer = layer + self._dose = dose + else: + self.radii = radii + self.angles = angles + self.width = width + self.offset = offset + self.rotation = rotation + self.repetition = repetition + self.layer = layer + self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen - self.repetition = repetition + [self.mirror(a) for a, do in enumerate(mirrored) if do] self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Arc': diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index f4cc683..e3836a1 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -95,18 +95,28 @@ class Ellipse(Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, - locked: bool = False): + locked: bool = False, + raw: bool = False, + ): object.__setattr__(self, 'locked', False) self.identifier = () - self.radii = radii - self.offset = offset - self.rotation = rotation + if raw: + self._radii = radii + self._offset = offset + self._rotation = rotation + self._repetition = repetition + self._layer = layer + self._dose = dose + else: + self.radii = radii + self.offset = offset + self.rotation = rotation + self.repetition = repetition + self.layer = layer + self.dose = dose [self.mirror(a) for a, do in enumerate(mirrored) if do] - self.layer = layer - self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen - self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 15b1ead..3f2f72f 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -150,22 +150,32 @@ class Path(Shape, metaclass=AutoSlots): dose: float = 1.0, repetition: Optional[Repetition] = None, locked: bool = False, + raw: bool = False, ): object.__setattr__(self, 'locked', False) self._cap_extensions = None # Since .cap setter might access it self.identifier = () - self.offset = offset - self.layer = layer - self.dose = dose - self.vertices = vertices - self.width = width - self.cap = cap - if cap_extensions is not None: + if raw: + self._vertices = vertices + self._offset = offset + self._repetition = repetition + self._layer = layer + self._dose = dose + self._width = width + self._cap = cap + self._cap_extensions = cap_extensions + else: + self.vertices = vertices + self.offset = offset + self.repetition = repetition + self.layer = layer + self.dose = dose + self.width = width + self.cap = cap self.cap_extensions = cap_extensions self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] - self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Path': diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index cee5780..08da89e 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -78,16 +78,24 @@ class Polygon(Shape, metaclass=AutoSlots): dose: float = 1.0, repetition: Optional[Repetition] = None, locked: bool = False, + raw: bool = False, ): object.__setattr__(self, 'locked', False) self.identifier = () - self.layer = layer - self.dose = dose - self.vertices = vertices - self.offset = offset + if raw: + self._vertices = vertices + self._offset = offset + self._repetition = repetition + self._layer = layer + self._dose = dose + else: + self.vertices = vertices + self.offset = offset + self.repetition = repetition + self.layer = layer + self.dose = dose self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] - self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': diff --git a/masque/shapes/text.py b/masque/shapes/text.py index d796511..599d377 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -68,18 +68,29 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): dose: float = 1.0, repetition: Optional[Repetition] = None, locked: bool = False, + raw: bool = False, ): object.__setattr__(self, 'locked', False) self.identifier = () - self.offset = offset - self.layer = layer - self.dose = dose - self.string = string - self.height = height - self.rotation = rotation + if raw: + self._offset = offset + self._layer = layer + self._dose = dose + self._string = string + self._height = height + self._rotation = rotation + self._mirrored = mirrored + self._repetition = repetition + else: + self.offset = offset + self.layer = layer + self.dose = dose + self.string = string + self.height = height + self.rotation = rotation + self.mirrored = mirrored + self.repetition = repetition self.font_path = font_path - self.mirrored = mirrored - self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Text': From 99ded5c1138f69e2d5df214e5dd882a827b778fd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 11 Aug 2020 01:18:52 -0700 Subject: [PATCH 26/71] Don't bother checking that dose is a scalar --- masque/traits/doseable.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/masque/traits/doseable.py b/masque/traits/doseable.py index 4b9daf3..419d0bd 100644 --- a/masque/traits/doseable.py +++ b/masque/traits/doseable.py @@ -67,8 +67,6 @@ class DoseableImpl(Doseable, metaclass=ABCMeta): @dose.setter def dose(self, val: float): - if not is_scalar(val): - raise PatternError('Dose must be a scalar') if not val >= 0: raise PatternError('Dose must be non-negative') self._dose = val From b98553a7709c85084fb69ffcadf463d71d0eb3c9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 12 Aug 2020 21:42:57 -0700 Subject: [PATCH 27/71] set repetition on subpattern --- masque/file/gdsii.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index a8a583d..353bb74 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -376,7 +376,8 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, offset=offset, rotation=rotation, scale=scale, - mirrored=(mirror_across_x, False)) + mirrored=(mirror_across_x, False), + repetition=repetition) subpat.identifier = (element.struct_name,) return subpat From d14182998b0ec5c5024e3f8509887a7c3a46b3b0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 12 Aug 2020 21:43:46 -0700 Subject: [PATCH 28/71] various fixes --- masque/repetition.py | 4 ++-- masque/shapes/shape.py | 13 +++++++------ masque/subpattern.py | 21 +++++++++++---------- masque/traits/positionable.py | 3 +-- masque/traits/rotatable.py | 1 + masque/utils.py | 17 +++++++++++------ 6 files changed, 33 insertions(+), 26 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index 5707a50..5c9f105 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -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.flat[:, None] * self.a_vector[None, :] + - bb.flat[:, None] * self.b_vector[None, :]) + return (aa.flatten()[:, None] * self.a_vector[None, :] + + bb.flatten()[:, None] * self.b_vector[None, :]) def rotate(self, rotation: float) -> 'Grid': """ diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index c6a0c63..7a5e3f3 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -31,16 +31,17 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable """ Abstract class specifying functions common to all shapes. """ + __slots__ = () # Children should use AutoSlots identifier: Tuple """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ -# def __copy__(self) -> 'Shape': -# cls = self.__class__ -# new = cls.__new__(cls) -# for name in Shape.__slots__ + self.__slots__: -# object.__setattr__(new, name, getattr(self, name)) -# return new + def __copy__(self) -> 'Shape': + cls = self.__class__ + new = cls.__new__(cls) + for name in self.__slots__: + object.__setattr__(new, name, getattr(self, name)) + return new ''' --- Abstract methods diff --git a/masque/subpattern.py b/masque/subpattern.py index e898349..6964676 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, - Pivotable, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots): + PivotableImpl, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots): """ SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. @@ -83,6 +83,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi dose=self.dose, scale=self.scale, mirrored=self.mirrored.copy(), + repetition=copy.deepcopy(self.repetition), locked=self.locked) return new @@ -90,6 +91,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi memo = {} if memo is None else memo new = copy.copy(self).unlock() new.pattern = copy.deepcopy(self.pattern, memo) + new.repetition = copy.deepcopy(self.repetition, memo) new.locked = self.locked return new @@ -140,18 +142,17 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi return pattern + def rotate(self, rotation: float) -> 'SubPattern': + self.rotation += rotation + if self.repetition is not None: + self.repetition.rotate(rotation) + return self + def mirror(self, axis: int) -> 'SubPattern': - """ - Mirror the subpattern across an axis. - - Args: - axis: Axis to mirror across. - - Returns: - self - """ self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 + if self.repetition is not None: + self.repetiton.mirror(axis) return self def get_bounds(self) -> Optional[numpy.ndarray]: diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index 4db6880..d433a91 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -94,7 +94,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): @offset.setter def offset(self, val: vector2): - if not isinstance(val, numpy.ndarray): + if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64: val = numpy.array(val, dtype=float) if val.size != 2: @@ -109,7 +109,6 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): self.offset = offset return self - def translate(self: I, offset: vector2) -> I: self._offset += offset return self diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index e01c81d..64c2c51 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -109,6 +109,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta): """ __slots__ = () + @abstractmethod def rotate_around(self: J, pivot: vector2, rotation: float) -> J: pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) diff --git a/masque/utils.py b/masque/utils.py index 979ffde..2f2e499 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -148,11 +148,16 @@ class AutoSlots(ABCMeta): can be used to generate a full `__slots__` for the concrete class. """ def __new__(cls, name, bases, dctn): - slots = tuple(dctn.get('__slots__', tuple())) + parents = set() for base in bases: - if not hasattr(base, '__annotations__'): - continue - slots += tuple(getattr(base, '__annotations__').keys()) - dctn['__slots__'] = slots - return super().__new__(cls,name,bases,dctn) + parents |= set(base.mro()) + + slots = tuple(dctn.get('__slots__', tuple())) + for parent in parents: + if not hasattr(parent, '__annotations__'): + continue + slots += tuple(getattr(parent, '__annotations__').keys()) + + dctn['__slots__'] = slots + return super().__new__(cls, name, bases, dctn) From b4a19a3176575b89b6daa107873660f80406dbc0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 15 Aug 2020 17:40:49 -0700 Subject: [PATCH 29/71] fix @abstractmethod on wrong function --- masque/traits/rotatable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index 64c2c51..ef7e748 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -89,6 +89,7 @@ class Pivotable(metaclass=ABCMeta): """ __slots__ = () + @abstractmethod def rotate_around(self: P, pivot: vector2, rotation: float) -> P: """ Rotate the object around a point. @@ -109,7 +110,6 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta): """ __slots__ = () - @abstractmethod def rotate_around(self: J, pivot: vector2, rotation: float) -> J: pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) From cbb5462fcbdc90bba01d111631cf15439caadae8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 15 Aug 2020 17:41:09 -0700 Subject: [PATCH 30/71] spelling fix and wrong import --- masque/subpattern.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/subpattern.py b/masque/subpattern.py index 6964676..41facd1 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -14,7 +14,7 @@ from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots from .repetition import Repetition from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, - Mirrorable, Pivotable, Copyable, LockableImpl, RepeatableImpl) + Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl) if TYPE_CHECKING: @@ -152,7 +152,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 if self.repetition is not None: - self.repetiton.mirror(axis) + self.repetition.mirror(axis) return self def get_bounds(self) -> Optional[numpy.ndarray]: From 352c03c0ae668f04eb34d7caac6c34b16cde5536 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 15 Aug 2020 18:20:04 -0700 Subject: [PATCH 31/71] remove use_dtype_as_dose arg --- masque/file/gdsii.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 353bb74..f255b49 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -202,7 +202,6 @@ def readfile(filename: Union[str, pathlib.Path], def read(stream: io.BufferedIOBase, - use_dtype_as_dose: bool = False, clean_vertices: bool = True, ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: """ @@ -218,11 +217,6 @@ def read(stream: io.BufferedIOBase, Args: stream: Stream to read from. - use_dtype_as_dose: If `False`, set each polygon's layer to `(gds_layer, gds_datatype)`. - If `True`, set the layer to `gds_layer` and the dose to `gds_datatype`. - Default `False`. - NOTE: This will be deprecated in the future in favor of - `pattern.apply(masque.file.utils.dtype2dose)`. clean_vertices: If `True`, remove any redundant vertices when loading polygons. The cleaning process removes any polygons with zero area or <3 vertices. Default `True`. @@ -298,10 +292,6 @@ def read(stream: io.BufferedIOBase, isinstance(element, gdsii.elements.ARef)): pat.subpatterns.append(_ref_to_subpat(element)) - if use_dtype_as_dose: - logger.warning('use_dtype_as_dose will be removed in the future!') - pat = dtype2dose(pat) - patterns.append(pat) # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries From b845b0f7bc170f2ebaba43fb9f93f8e89cc50107 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 15 Aug 2020 18:20:37 -0700 Subject: [PATCH 32/71] move shape conversions to their own functions, and use raw mode --- masque/file/gdsii.py | 73 +++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index f255b49..6e2eda0 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -233,57 +233,33 @@ def read(stream: io.BufferedIOBase, 'logical_units_per_unit': lib.logical_unit, } + raw_mode = True # Whether to construct shapes in raw mode (less error checking) + patterns = [] for structure in lib: pat = Pattern(name=structure.name.decode('ASCII')) for element in structure: # Switch based on element type: if isinstance(element, gdsii.elements.Boundary): - args = {'vertices': element.xy[:-1], - 'layer': (element.layer, element.data_type), - } - - poly = Polygon(**args) - + poly = _boundary_to_polygon(element, raw_mode) if clean_vertices: try: poly.clean_vertices() except PatternError: continue - pat.shapes.append(poly) if isinstance(element, gdsii.elements.Path): - if element.path_type in path_cap_map: - cap = path_cap_map[element.path_type] - else: - raise PatternError('Unrecognized path type: {}'.format(element.path_type)) - - args = {'vertices': element.xy, - 'layer': (element.layer, element.data_type), - 'width': element.width if element.width is not None else 0.0, - 'cap': cap, - } - - if cap == Path.Cap.SquareCustom: - args['cap_extensions'] = numpy.zeros(2) - if element.bgn_extn is not None: - args['cap_extensions'][0] = element.bgn_extn - if element.end_extn is not None: - args['cap_extensions'][1] = element.end_extn - - path = Path(**args) - + path = _gpath_to_mpath(element, raw_mode) if clean_vertices: try: path.clean_vertices() except PatternError as err: continue - pat.shapes.append(path) elif isinstance(element, gdsii.elements.Text): - label = Label(offset=element.xy, + label = Label(offset=element.xy.astype(float), layer=(element.layer, element.text_type), string=element.string.decode('ASCII')) pat.labels.append(label) @@ -333,9 +309,9 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, That's not currently supported by masque at all, so need to either tag it and undo the parent transformations, or implement it in masque. """ - rotation = 0 - offset = numpy.array(element.xy[0]) - scale = 1 + rotation = 0.0 + offset = numpy.array(element.xy[0], dtype=float) + scale = 1.0 mirror_across_x = False repetition = None @@ -372,6 +348,39 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, return subpat +def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path: + if element.path_type in path_cap_map: + cap = path_cap_map[element.path_type] + else: + raise PatternError(f'Unrecognized path type: {element.path_type}') + + args = {'vertices': element.xy.astype(float), + 'layer': (element.layer, element.data_type), + 'width': element.width if element.width is not None else 0.0, + 'cap': cap, + 'offset': numpy.zeros(2), + 'raw': raw_mode, + } + + if cap == Path.Cap.SquareCustom: + args['cap_extensions'] = numpy.zeros(2) + if element.bgn_extn is not None: + args['cap_extensions'][0] = element.bgn_extn + if element.end_extn is not None: + args['cap_extensions'][1] = element.end_extn + + return Path(**args) + + +def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Polygon: + args = {'vertices': element.xy[:-1].astype(float), + 'layer': (element.layer, element.data_type), + 'offset': numpy.zeros(2), + 'raw': raw_mode, + } + return Polygon(**args) + + def _subpatterns_to_refs(subpatterns: List[SubPattern] ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]: refs = [] From 3ec28d472056b264516770d8ecbb47c85cf86502 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 15 Aug 2020 18:22:11 -0700 Subject: [PATCH 33/71] typo fixes --- masque/file/oasis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 834949d..fa93388 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -298,7 +298,7 @@ def read(stream: io.BufferedIOBase, path = Path(vertices=vertices, layer=element.get_layer_tuple(), offset=element.get_xy(), - repeition=repetition, + repetition=repetition, width=element.get_half_width() * 2, cap=cap, **path_args) @@ -566,7 +566,7 @@ def _labels_to_texts(labels: List[Label], texts = [] for label in labels: layer, datatype = layer2oas(label.layer) - repetition, rep_offset = repetition_masq2fata(shape.repetition) + repetition, rep_offset = repetition_masq2fata(label.repetition) xy = numpy.round(label.offset + rep_offset).astype(int) texts.append(fatrec.Text(layer=layer, datatype=datatype, From e7c8708f7f5c967c29349a609f2329da65da3317 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 15 Aug 2020 18:23:04 -0700 Subject: [PATCH 34/71] prefer f-strings --- masque/file/dxf.py | 11 ++++++----- masque/file/gdsii.py | 15 ++++++++------- masque/file/oasis.py | 8 ++++---- masque/file/svg.py | 3 +-- masque/shapes/path.py | 2 +- masque/subpattern.py | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 56b0bc5..07eb1db 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -370,17 +370,18 @@ def disambiguate_pattern_names(patterns, i += 1 if sanitized_name == '': - logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name)) + 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('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format( - pat.name, sanitized_name, 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('Zero-length name after sanitize,\n originally "{}"'.format(pat.name)) + raise PatternError(f'Zero-length name after sanitize,\n originally "{pat.name}"') if len(suffixed_name) > max_name_length: - raise PatternError('Pattern name "{!r}" length > {} after encode,\n originally "{}"'.format(suffixed_name, max_name_length, 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) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 6e2eda0..b7885ac 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -484,8 +484,8 @@ 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('Pattern name "{}" is too long ({}/{} chars),\n'.format(pat.name, len(pat.name), max_name_length) + - ' shortening to "{}" before generating suffix'.format(shortened_name)) + 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 @@ -502,19 +502,20 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern], i += 1 if sanitized_name == '': - logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name)) + 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('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format( - pat.name, sanitized_name, 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') if len(encoded_name) == 0: # Should never happen since zero-length names are replaced - raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name)) + raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"') if len(encoded_name) > max_name_length: - raise PatternError('Pattern name "{!r}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, 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) diff --git a/masque/file/oasis.py b/masque/file/oasis.py index fa93388..9a27a9f 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -593,15 +593,15 @@ def disambiguate_pattern_names(patterns, i += 1 if sanitized_name == '': - logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name)) + 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('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format( - pat.name, sanitized_name, 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('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name)) + raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"') pat.name = suffixed_name used_names.append(suffixed_name) diff --git a/masque/file/svg.py b/masque/file/svg.py index deef59a..fb49b5b 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -79,8 +79,7 @@ def writefile(pattern: Pattern, for subpat in pat.subpatterns: if subpat.pattern is None: continue - transform = 'scale({:g}) rotate({:g}) translate({:g},{:g})'.format( - subpat.scale, subpat.rotation, subpat.offset[0], subpat.offset[1]) + transform = f'scale({subpat.scale:g}) rotate({subpat.rotation:g}) translate({subpat.offset[0]:g},{subpat.offset[1]:g})' use = svg.use(href='#' + mangle_name(subpat.pattern), transform=transform) if custom_attributes: use['pattern_dose'] = subpat.dose diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 3f2f72f..7570c7c 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -322,7 +322,7 @@ class Path(Shape, metaclass=AutoSlots): bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :]) bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :]) else: - raise PatternError('get_bounds() not implemented for endcaps: {}'.format(self.cap)) + raise PatternError(f'get_bounds() not implemented for endcaps: {self.cap}') return bounds diff --git a/masque/subpattern.py b/masque/subpattern.py index 41facd1..ebd6876 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -104,7 +104,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi def pattern(self, val: Optional['Pattern']): from .pattern import Pattern if val is not None and not isinstance(val, Pattern): - raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) + raise PatternError(f'Provided pattern {val} is not a Pattern object or None!') self._pattern = val # Mirrored property From 92a3b9b72e0faffa7ef24b87482578dde43224a0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 15 Aug 2020 18:23:16 -0700 Subject: [PATCH 35/71] documentation fixes/updates --- README.md | 2 +- masque/file/gdsii.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 38ad690..fae263e 100644 --- a/README.md +++ b/README.md @@ -39,4 +39,4 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release * Construct from bitmap * Boolean operations on polygons (using pyclipper) * Implement shape/cell properties -* Implement OASIS-style repetitions for shapes +* Deal with shape repetitions for dxf, svg diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index b7885ac..b87271c 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -142,8 +142,8 @@ def write(patterns: Union[Pattern, List[Pattern]], Args: patterns: A Pattern or list of patterns to write to file. stream: Stream to write to. - *args: passed to `oasis.build()` - **kwargs: passed to `oasis.build()` + *args: passed to `masque.file.gdsii.build()` + **kwargs: passed to `masque.file.gdsii.build()` """ lib = build(patterns, *args, **kwargs) lib.save(stream) From e330c34a0c24a5408766d167e75a3994ccc0a7c3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 9 Sep 2020 19:40:50 -0700 Subject: [PATCH 36/71] import layer_t at top level --- masque/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/masque/__init__.py b/masque/__init__.py index 7087b00..896d2ac 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -31,6 +31,7 @@ from .shapes import Shape from .label import Label from .subpattern import SubPattern from .pattern import Pattern +from .utils import layer_t __author__ = 'Jan Petykiewicz' From ea21353d2e9fc5d272521179af45d7305eedc263 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 9 Sep 2020 19:41:06 -0700 Subject: [PATCH 37/71] fix incorrect variable name --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index b87271c..dcb9649 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -293,7 +293,7 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]: else: data_type = 0 else: - raise PatternError(f'Invalid layer for gdsii: {layer}. Note that gdsii layers cannot be strings.') + raise PatternError(f'Invalid layer for gdsii: {mlayer}. Note that gdsii layers cannot be strings.') return layer, data_type From 5d83e0e5c03c4e6f3ab880cfc10b7d5c05d82962 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Wed, 9 Sep 2020 20:22:32 -0700 Subject: [PATCH 38/71] add package keywords --- setup.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 595c8e9..1d3138b 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ from setuptools import setup, find_packages + with open('README.md', 'r') as f: long_description = f.read() @@ -41,10 +42,42 @@ setup(name='masque', 'Intended Audience :: Manufacturing', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Operating System :: POSIX :: Linux', - 'Operating System :: Microsoft :: Windows', 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', 'Topic :: Scientific/Engineering :: Visualization', ], + keywords=[ + 'layout', + 'design', + 'CAD', + 'EDA', + 'electronics', + 'photonics', + 'IC', + 'mask', + 'pattern', + 'drawing', + 'lithography', + 'litho', + 'geometry', + 'geometric', + 'polygon', + 'curve', + 'ellipse', + 'oas', + 'gds', + 'dxf', + 'svg', + 'OASIS', + 'gdsii', + 'gds2', + 'convert', + 'stream', + 'custom', + 'visualize', + 'vector', + 'freeform', + 'manhattan', + 'angle', + ], ) From 49a3b4e322062e53fc90fadd288aaaf7d06d9fb5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Thu, 10 Sep 2020 20:06:58 -0700 Subject: [PATCH 39/71] add support for annotations and other fixes --- examples/test_rep.py | 5 +- masque/__init__.py | 2 +- masque/file/dxf.py | 19 +++-- masque/file/gdsii.py | 64 ++++++++++++--- masque/file/oasis.py | 141 ++++++++++++++++++++++++++-------- masque/file/svg.py | 5 +- masque/label.py | 18 +++-- masque/pattern.py | 24 +++--- masque/repetition.py | 2 +- masque/shapes/arc.py | 16 ++-- masque/shapes/circle.py | 37 ++++++--- masque/shapes/ellipse.py | 16 ++-- masque/shapes/path.py | 16 ++-- masque/shapes/polygon.py | 16 ++-- masque/shapes/shape.py | 10 ++- masque/shapes/text.py | 15 +++- masque/subpattern.py | 21 +++-- masque/traits/__init__.py | 1 + masque/traits/annotatable.py | 56 ++++++++++++++ masque/traits/doseable.py | 1 - masque/traits/layerable.py | 1 - masque/traits/lockable.py | 32 +++++++- masque/traits/mirrorable.py | 2 +- masque/traits/positionable.py | 2 +- masque/traits/repeatable.py | 1 - masque/traits/rotatable.py | 2 +- masque/traits/scalable.py | 1 - masque/utils.py | 7 +- 28 files changed, 400 insertions(+), 133 deletions(-) create mode 100644 masque/traits/annotatable.py diff --git a/examples/test_rep.py b/examples/test_rep.py index 80496ad..22e4b65 100644 --- a/examples/test_rep.py +++ b/examples/test_rep.py @@ -17,7 +17,8 @@ def main(): pat.shapes.append(shapes.Arc( radii=(rmin, rmin), width=0.1, - angles=(0*-numpy.pi/4, numpy.pi/4) + angles=(0*-numpy.pi/4, numpy.pi/4), + annotations={'1': ['blah']}, )) pat.scale_by(1000) @@ -27,7 +28,7 @@ def main(): pat3 = Pattern('sref_test') pat3.subpatterns = [ - SubPattern(pat, offset=(1e5, 3e5)), + SubPattern(pat, offset=(1e5, 3e5), annotations={'4': ['Hello I am the base subpattern']}), SubPattern(pat, offset=(2e5, 3e5), rotation=pi/3), SubPattern(pat, offset=(3e5, 3e5), rotation=pi/2), SubPattern(pat, offset=(4e5, 3e5), rotation=pi), diff --git a/masque/__init__.py b/masque/__init__.py index 896d2ac..9b8efb1 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -31,7 +31,7 @@ from .shapes import Shape from .label import Label from .subpattern import SubPattern from .pattern import Pattern -from .utils import layer_t +from .utils import layer_t, annotations_t __author__ = 'Jan Petykiewicz' diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 07eb1db..7ae4b6d 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -10,10 +10,10 @@ import struct import logging import pathlib import gzip -import numpy -from numpy import pi -import ezdxf +import numpy # type: ignore +from numpy import pi +import ezdxf # type: ignore from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, PatternError, Label, Shape @@ -264,13 +264,12 @@ def _read_block(block, clean_vertices): } if 'column_count' in attr: - args['a_vector'] = (attr['column_spacing'], 0) - args['b_vector'] = (0, attr['row_spacing']) - args['a_count'] = attr['column_count'] - args['b_count'] = attr['row_count'] - pat.subpatterns.append(GridRepetition(**args)) - else: - pat.subpatterns.append(SubPattern(**args)) + args['repetition'] = Grid( + a_vector=(attr['column_spacing'], 0), + b_vector=(0, attr['row_spacing']), + a_count=attr['column_count'], + b_count=attr['row_count']) + pat.subpatterns.append(SubPattern(**args)) else: logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).') return pat diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index dcb9649..7192ecc 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -11,17 +11,18 @@ Note that GDSII references follow the same convention as `masque`, Scaling, rotation, and mirroring apply to individual instances, not grid vectors or offsets. """ -from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional, Sequence +from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional +from typing import Sequence, Mapping import re import io import copy -import numpy import base64 import struct import logging import pathlib import gzip +import numpy # type: ignore # python-gdsii import gdsii.library import gdsii.structure @@ -32,7 +33,7 @@ from .. import Pattern, SubPattern, PatternError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t -from ..utils import remove_colinear_vertices, normalize_mirror +from ..utils import remove_colinear_vertices, normalize_mirror, annotations_t #TODO absolute positioning @@ -99,6 +100,7 @@ def build(patterns: Union[Pattern, List[Pattern]], if disambiguate_func is None: disambiguate_func = disambiguate_pattern_names + assert(disambiguate_func is not None) # placate mypy if not modify_originals: patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] @@ -124,6 +126,8 @@ def build(patterns: Union[Pattern, List[Pattern]], structure = gdsii.structure.Structure(name=pat.name.encode('ASCII')) lib.append(structure) + structure.properties = _annotations_to_properties(pat.annotations, 512) + structure += _shapes_to_elements(pat.shapes) structure += _labels_to_texts(pat.labels) structure += _subpatterns_to_refs(pat.subpatterns) @@ -238,6 +242,9 @@ def read(stream: io.BufferedIOBase, patterns = [] for structure in lib: pat = Pattern(name=structure.name.decode('ASCII')) + if pat.annotations: + logger.warning('Dropping Pattern-level annotations; they are not supported by python-gdsii') +# pat.annotations = {str(k): v for k, v in structure.properties} for element in structure: # Switch based on element type: if isinstance(element, gdsii.elements.Boundary): @@ -343,6 +350,7 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, rotation=rotation, scale=scale, mirrored=(mirror_across_x, False), + annotations=_properties_to_annotations(element.properties), repetition=repetition) subpat.identifier = (element.struct_name,) return subpat @@ -359,6 +367,7 @@ def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path: 'width': element.width if element.width is not None else 0.0, 'cap': cap, 'offset': numpy.zeros(2), + 'annotations':_properties_to_annotations(element.properties), 'raw': raw_mode, } @@ -376,6 +385,7 @@ def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Po args = {'vertices': element.xy[:-1].astype(float), 'layer': (element.layer, element.data_type), 'offset': numpy.zeros(2), + 'annotations':_properties_to_annotations(element.properties), 'raw': raw_mode, } return Polygon(**args) @@ -420,11 +430,38 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] # strans must be non-None for angle and mag to take effect ref.strans = set_bit(0, 15 - 0, mirror_across_x) ref.mag = subpat.scale + ref.properties = _annotations_to_properties(subpat.annotations, 512) refs += new_refs return refs +def _properties_to_annotations(properties: List[Tuple[int, bytes]]) -> annotations_t: + return {str(k): [v.decode()] for k, v in properties} + + +def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> List[Tuple[int, bytes]]: + cum_len = 0 + props = [] + for key, vals in annotations.items(): + try: + i = int(key) + except: + raise PatternError(f'Annotation key {key} is not convertable to an integer') + if not (0 < i < 126): + raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') + + val_strings = ' '.join(str(val) for val in vals) + b = val_strings.encode() + if len(b) > 126: + raise PatternError(f'Annotation value {b!r} is longer than 126 characters!') + cum_len += numpy.ceil(len(b) / 2) * 2 + 2 + if cum_len > max_len: + raise PatternError(f'Sum of annotation data will be longer than {max_len} bytes! Generated bytes were {b!r}') + props.append((i, b)) + return props + + def _shapes_to_elements(shapes: List[Shape], polygonize_paths: bool = False ) -> List[Union[gdsii.elements.Boundary, gdsii.elements.Path]]: @@ -432,6 +469,7 @@ def _shapes_to_elements(shapes: List[Shape], # Add a Boundary element for each shape, and Path elements if necessary for shape in shapes: layer, data_type = _mlayer2gds(shape.layer) + properties = _annotations_to_properties(shape.annotations, 128) if isinstance(shape, Path) and not polygonize_paths: xy = numpy.round(shape.vertices + shape.offset).astype(int) width = numpy.round(shape.width).astype(int) @@ -441,26 +479,32 @@ def _shapes_to_elements(shapes: List[Shape], xy=xy) path.path_type = path_type path.width = width + path.properties = properties elements.append(path) else: for polygon in shape.to_polygons(): xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) xy_closed = numpy.vstack((xy_open, xy_open[0, :])) - elements.append(gdsii.elements.Boundary(layer=layer, - data_type=data_type, - xy=xy_closed)) + boundary = gdsii.elements.Boundary(layer=layer, + data_type=data_type, + xy=xy_closed) + boundary.properties = properties + elements.append(boundary) return elements def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: texts = [] for label in labels: + properties = _annotations_to_properties(label.annotations, 128) layer, text_type = _mlayer2gds(label.layer) xy = numpy.round([label.offset]).astype(int) - texts.append(gdsii.elements.Text(layer=layer, - text_type=text_type, - xy=xy, - string=label.string.encode('ASCII'))) + text = gdsii.elements.Text(layer=layer, + text_type=text_type, + xy=xy, + string=label.string.encode('ASCII')) + text.properties = properties + texts.append(text) return texts diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 9a27a9f..34208b0 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -20,19 +20,19 @@ import struct import logging import pathlib import gzip -import numpy -from numpy import pi +import numpy # type: ignore +from numpy import pi import fatamorgana import fatamorgana.records as fatrec -from fatamorgana.basic import PathExtensionScheme +from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, PatternError, Label, Shape from ..shapes import Polygon, Path, Circle from ..repetition import Grid, Arbitrary, Repetition from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t -from ..utils import remove_colinear_vertices, normalize_mirror +from ..utils import remove_colinear_vertices, normalize_mirror, annotations_t logger = logging.getLogger(__name__) @@ -52,9 +52,11 @@ path_cap_map = { def build(patterns: Union[Pattern, List[Pattern]], units_per_micron: int, - layer_map: Dict[str, Union[int, Tuple[int, int]]] = None, + layer_map: Optional[Dict[str, Union[int, Tuple[int, int]]]] = None, + *, modify_originals: bool = False, - disambiguate_func: Callable[[Iterable[Pattern]], None] = None, + disambiguate_func: Optional[Callable[[Iterable[Pattern]], None]] = None, + annotations: Optional[annotations_t] = None ) -> fatamorgana.OasisLayout: """ Convert a `Pattern` or list of patterns to an OASIS stream, writing patterns @@ -91,6 +93,7 @@ def build(patterns: Union[Pattern, List[Pattern]], Default `False`. disambiguate_func: Function which takes a list of patterns and alters them to make their names valid and unique. Default is `disambiguate_pattern_names`. + annotations: dictionary of key-value pairs which are saved as library-level properties Returns: `fatamorgana.OasisLayout` @@ -104,11 +107,15 @@ def build(patterns: Union[Pattern, List[Pattern]], if disambiguate_func is None: disambiguate_func = disambiguate_pattern_names + if annotations is None: + annotations = {} + if not modify_originals: patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] # Create library lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None) + lib.properties = annotations_to_properties(annotations) if layer_map: for name, layer_num in layer_map.items(): @@ -139,9 +146,11 @@ def build(patterns: Union[Pattern, List[Pattern]], structure = fatamorgana.Cell(name=pat.name) lib.cells.append(structure) + structure.properties += annotations_to_properties(pat.annotations) + structure.geometry += _shapes_to_elements(pat.shapes, layer2oas) structure.geometry += _labels_to_texts(pat.labels, layer2oas) - structure.placements += _subpatterns_to_refs(pat.subpatterns) + structure.placements += _subpatterns_to_placements(pat.subpatterns) return lib @@ -226,6 +235,8 @@ def read(stream: io.BufferedIOBase, Additional library info is returned in a dict, containing: 'units_per_micrometer': number of database units per micrometer (all values are in database units) + 'layer_map': Mapping from layer names to fatamorgana.LayerName objects + 'annotations': Mapping of {key: value} pairs from library's properties Args: stream: Stream to read from. @@ -242,6 +253,7 @@ def read(stream: io.BufferedIOBase, library_info: Dict[str, Any] = { 'units_per_micrometer': lib.unit, + 'annotations': properties_to_annotations(lib.properties, lib.propnames, lib.propstrings), } layer_map = {} @@ -252,7 +264,7 @@ def read(stream: io.BufferedIOBase, patterns = [] for cell in lib.cells: if isinstance(cell.name, int): - cell_name = lib.cellnames[cell.name].string + cell_name = lib.cellnames[cell.name].nstring.string else: cell_name = cell.name.string @@ -263,15 +275,16 @@ def read(stream: io.BufferedIOBase, # note XELEMENT has no repetition continue - repetition = repetition_fata2masq(element.repetition) # Switch based on element type: if isinstance(element, fatrec.Polygon): vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0) + annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) poly = Polygon(vertices=vertices, layer=element.get_layer_tuple(), offset=element.get_xy(), + annotations=annotations, repetition=repetition) if clean_vertices: @@ -295,10 +308,13 @@ def read(stream: io.BufferedIOBase, if cap == Path.Cap.SquareCustom: path_args['cap_extensions'] = numpy.array((element.get_extension_start()[1], element.get_extension_end()[1])) + + annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) path = Path(vertices=vertices, layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, + annotations=annotations, width=element.get_half_width() * 2, cap=cap, **path_args) @@ -314,10 +330,12 @@ def read(stream: io.BufferedIOBase, elif isinstance(element, fatrec.Rectangle): width = element.get_width() height = element.get_height() + annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) rect = Polygon(layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), + annotations=annotations, ) pat.shapes.append(rect) @@ -346,10 +364,12 @@ def read(stream: io.BufferedIOBase, else: vertices[2, 0] -= b + annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) trapz = Polygon(layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, vertices=vertices, + annotations=annotations, ) pat.shapes.append(trapz) @@ -399,24 +419,30 @@ def read(stream: io.BufferedIOBase, vertices = vertices[[0, 2, 3], :] vertices[0, 1] += width + annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) ctrapz = Polygon(layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, vertices=vertices, + annotations=annotations, ) pat.shapes.append(ctrapz) elif isinstance(element, fatrec.Circle): + annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) circle = Circle(layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, + annotations=annotations, radius=float(element.get_radius())) pat.shapes.append(circle) elif isinstance(element, fatrec.Text): + annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) label = Label(layer=element.get_layer_tuple(), offset=element.get_xy(), repetition=repetition, + annotations=annotations, string=str(element.get_string())) pat.labels.append(label) @@ -425,7 +451,7 @@ def read(stream: io.BufferedIOBase, continue for placement in cell.placements: - pat.subpatterns.append(_placement_to_subpat(placement)) + pat.subpatterns.append(_placement_to_subpat(placement, lib)) patterns.append(pat) @@ -435,7 +461,7 @@ def read(stream: io.BufferedIOBase, for p in patterns_dict.values(): for sp in p.subpatterns: ident = sp.identifier[0] - name = ident if isinstance(ident, str) else lib.cellnames[ident].string + name = ident if isinstance(ident, str) else lib.cellnames[ident].nstring.string sp.pattern = patterns_dict[name] del sp.identifier @@ -459,7 +485,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]: return layer, data_type -def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: +def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern: """ Helper function to create a SubPattern from a placment. Sets subpat.pattern to None and sets the instance .identifier to (struct_name,). @@ -468,21 +494,20 @@ def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: mag = placement.magnification if placement.magnification is not None else 1 pname = placement.get_name() name = pname if isinstance(pname, int) else pname.string - args: Dict[str, Any] = { - 'pattern': None, - 'mirrored': (placement.flip, False), - 'rotation': float(placement.angle * pi/180), - 'scale': mag, - 'identifier': (name,), - 'repetition': repetition_fata2masq(placement.repetition), - } - - subpat = SubPattern(offset=xy, **args) + annotations = properties_to_annotations(placement.properties, lib.propnames, lib.propstrings) + subpat = SubPattern(offset=xy, + pattern=None, + mirrored=(placement.flip, False), + rotation=float(placement.angle * pi/180), + scale=float(mag), + identifier=(name,), + repetition=repetition_fata2masq(placement.repetition), + annotations=annotations) return subpat -def _subpatterns_to_refs(subpatterns: List[SubPattern] - ) -> List[fatrec.Placement]: +def _subpatterns_to_placements(subpatterns: List[SubPattern] + ) -> List[fatrec.Placement]: refs = [] for subpat in subpatterns: if subpat.pattern is None: @@ -493,19 +518,16 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] frep, rep_offset = repetition_masq2fata(subpat.repetition) offset = numpy.round(subpat.offset + rep_offset).astype(int) - args: Dict[str, Any] = { - 'x': offset[0], - 'y': offset[1], - 'repetition': frep, - } - angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 ref = fatrec.Placement( name=subpat.pattern.name, flip=mirror_across_x, angle=angle, magnification=subpat.scale, - **args) + properties=annotations_to_properties(subpat.annotations), + x=offset[0], + y=offset[1], + repetition=frep) refs.append(ref) return refs @@ -519,6 +541,7 @@ def _shapes_to_elements(shapes: List[Shape], for shape in shapes: layer, datatype = layer2oas(shape.layer) repetition, rep_offset = repetition_masq2fata(shape.repetition) + properties = annotations_to_properties(shape.annotations) if isinstance(shape, Circle): offset = numpy.round(shape.offset + rep_offset).astype(int) radius = numpy.round(shape.radius).astype(int) @@ -527,6 +550,7 @@ def _shapes_to_elements(shapes: List[Shape], radius=radius, x=offset[0], y=offset[1], + properties=properties, repetition=repetition) elements.append(circle) elif isinstance(shape, Path): @@ -544,6 +568,7 @@ def _shapes_to_elements(shapes: List[Shape], y=xy[1], extension_start=extension_start, #TODO implement multiple cap types? extension_end=extension_end, + properties=properties, repetition=repetition, ) elements.append(path) @@ -556,6 +581,7 @@ def _shapes_to_elements(shapes: List[Shape], x=xy[0], y=xy[1], point_list=points, + properties=properties, repetition=repetition)) return elements @@ -568,11 +594,13 @@ def _labels_to_texts(labels: List[Label], layer, datatype = layer2oas(label.layer) repetition, rep_offset = repetition_masq2fata(label.repetition) xy = numpy.round(label.offset + rep_offset).astype(int) + properties = annotations_to_properties(label.annotations) texts.append(fatrec.Text(layer=layer, datatype=datatype, x=xy[0], y=xy[1], string=label.string, + properties=properties, repetition=repetition)) return texts @@ -609,6 +637,7 @@ def disambiguate_pattern_names(patterns, def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None] ) -> Optional[Repetition]: + mrep: Optional[Repetition] if isinstance(rep, fatamorgana.GridRepetition): mrep = Grid(a_vector=rep.a_vector, b_vector=rep.b_vector, @@ -624,7 +653,12 @@ def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.Arbi return mrep -def repetition_masq2fata(rep: Optional[Repetition]): +def repetition_masq2fata(rep: Optional[Repetition] + ) -> Tuple[Union[fatamorgana.GridRepetition, + fatamorgana.ArbitraryRepetition, + None], + Tuple[int, int]]: + frep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None] if isinstance(rep, Grid): frep = fatamorgana.GridRepetition( a_vector=numpy.round(rep.a_vector).astype(int), @@ -642,3 +676,46 @@ def repetition_masq2fata(rep: Optional[Repetition]): frep = None offset = (0, 0) return frep, offset + + +def annotations_to_properties(annotations: annotations_t) -> List[fatrec.Property]: + #TODO determine is_standard based on key? + properties = [] + for key, values in annotations.items(): + vals = [AString(v) if isinstance(v, str) else v + for v in values] + properties.append(fatrec.Property(key, vals, is_standard=False)) + return properties + + +def properties_to_annotations(properties: List[fatrec.Property], + propnames: Dict[int, NString], + propstrings: Dict[int, AString], + ) -> annotations_t: + annotations = {} + for proprec in properties: + assert(proprec.name is not None) + if isinstance(proprec.name, int): + key = propnames[proprec.name].string + else: + key = proprec.name.string + values: List[Union[str, float, int]] = [] + + assert(proprec.values is not None) + for value in proprec.values: + if isinstance(value, (float, int)): + values.append(value) + elif isinstance(value, (NString, AString)): + values.append(value.string) + elif isinstance(value, PropStringReference): + values.append(propstrings[value.ref].string) # dereference + else: + string = repr(value) + logger.warning(f'Converting property value for key ({key}) to string ({string})') + values.append(string) + annotations[key] = values + return annotations + + properties = [fatrec.Property(key, vals, is_standard=False) + for key, vals in annotations.items()] + return properties diff --git a/masque/file/svg.py b/masque/file/svg.py index fb49b5b..0ce5750 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -2,10 +2,11 @@ SVG file format readers and writers """ from typing import Dict, Optional -import svgwrite -import numpy import warnings +import numpy # type: ignore +import svgwrite # type: ignore + from .utils import mangle_name from .. import Pattern diff --git a/masque/label.py b/masque/label.py index 72b7266..f157f46 100644 --- a/masque/label.py +++ b/masque/label.py @@ -1,15 +1,16 @@ from typing import List, Tuple, Dict, Optional import copy -import numpy +import numpy # type: ignore from numpy import pi from .repetition import Repetition from .error import PatternError, PatternLockedError -from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots +from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots, annotations_t from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl +from .traits import AnnotatableImpl -class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, +class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, AnnotatableImpl, Pivotable, Copyable, metaclass=AutoSlots): """ A text annotation with a position and layer (but no size; it is not drawn) @@ -42,14 +43,17 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, offset: vector2 = (0.0, 0.0), layer: layer_t = 0, repetition: Optional[Repetition] = None, - locked: bool = False): - object.__setattr__(self, 'locked', False) + annotations: Optional[annotations_t] = None, + locked: bool = False, + ): + LockableImpl.unlock(self) self.identifier = () self.string = string self.offset = numpy.array(offset, dtype=float, copy=True) self.layer = layer self.repetition = repetition - self.locked = locked + self.annotations = annotations if annotations is not None else {} + self.set_locked(locked) def __copy__(self) -> 'Label': return Label(string=self.string, @@ -62,7 +66,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, memo = {} if memo is None else memo new = copy.copy(self).unlock() new._offset = self._offset.copy() - new.locked = self.locked + new.set_locked(self.locked) return new def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': diff --git a/masque/pattern.py b/masque/pattern.py index 9c45749..de7ecd2 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -9,26 +9,27 @@ import itertools import pickle from collections import defaultdict -import numpy +import numpy # type: ignore from numpy import inf # .visualize imports matplotlib and matplotlib.collections from .subpattern import SubPattern from .shapes import Shape, Polygon from .label import Label -from .utils import rotation_matrix_2d, vector2, normalize_mirror +from .utils import rotation_matrix_2d, vector2, normalize_mirror, AutoSlots, annotations_t from .error import PatternError, PatternLockedError +from .traits import LockableImpl, AnnotatableImpl visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern'] -class Pattern: +class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): """ 2D layout consisting of some set of shapes, labels, and references to other Pattern objects (via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. """ - __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') + __slots__ = ('shapes', 'labels', 'subpatterns', 'name') shapes: List[Shape] """ List of all shapes in this Pattern. @@ -47,14 +48,12 @@ class Pattern: name: str """ A name for this pattern """ - locked: bool - """ When the pattern is locked, no changes may be made. """ - def __init__(self, name: str = '', shapes: Sequence[Shape] = (), labels: Sequence[Label] = (), subpatterns: Sequence[SubPattern] = (), + annotations: Optional[annotations_t] = None, locked: bool = False, ): """ @@ -68,7 +67,7 @@ class Pattern: name: An identifier for the Pattern locked: Whether to lock the pattern after construction """ - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) if isinstance(shapes, list): self.shapes = shapes else: @@ -84,8 +83,9 @@ class Pattern: else: self.subpatterns = list(subpatterns) + self.annotations = annotations if annotations is not None else {} self.name = name - self.locked = locked + self.set_locked(locked) def __setattr__(self, name, value): if self.locked and name != 'locked': @@ -97,6 +97,7 @@ class Pattern: shapes=copy.deepcopy(self.shapes), labels=copy.deepcopy(self.labels), subpatterns=[copy.copy(sp) for sp in self.subpatterns], + annotations=copy.deepcopy(self.annotations), locked=self.locked) def __deepcopy__(self, memo: Dict = None) -> 'Pattern': @@ -105,6 +106,7 @@ class Pattern: shapes=copy.deepcopy(self.shapes, memo), labels=copy.deepcopy(self.labels, memo), subpatterns=copy.deepcopy(self.subpatterns, memo), + annotations=copy.deepcopy(self.annotations, memo), locked=self.locked) return new @@ -815,7 +817,7 @@ class Pattern: self.shapes = tuple(self.shapes) self.labels = tuple(self.labels) self.subpatterns = tuple(self.subpatterns) - object.__setattr__(self, 'locked', True) + LockableImpl.lock(self) return self def unlock(self) -> 'Pattern': @@ -826,7 +828,7 @@ class Pattern: self """ if self.locked: - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) self.shapes = list(self.shapes) self.labels = list(self.labels) self.subpatterns = list(self.subpatterns) diff --git a/masque/repetition.py b/masque/repetition.py index 5c9f105..4ed29f4 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -7,7 +7,7 @@ from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, import copy from abc import ABCMeta, abstractmethod -import numpy +import numpy # type: ignore from .error import PatternError, PatternLockedError from .utils import rotation_matrix_2d, vector2, AutoSlots diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 4918ea7..ea9b4c7 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,13 +1,15 @@ from typing import List, Tuple, Dict, Optional, Sequence import copy import math -import numpy + +import numpy # type: ignore from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, vector2, layer_t, AutoSlots +from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t +from ..traits import LockableImpl class Arc(Shape, metaclass=AutoSlots): @@ -160,10 +162,11 @@ class Arc(Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, + annotations: Optional[annotations_t] = None, locked: bool = False, raw: bool = False, ): - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) self.identifier = () if raw: self._radii = radii @@ -172,6 +175,7 @@ class Arc(Shape, metaclass=AutoSlots): self._offset = offset self._rotation = rotation self._repetition = repetition + self._annotations = annotations if annotations is not None else {} self._layer = layer self._dose = dose else: @@ -181,12 +185,13 @@ class Arc(Shape, metaclass=AutoSlots): self.offset = offset self.rotation = rotation self.repetition = repetition + self.annotations = annotations if annotations is not None else {} self.layer = layer self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen [self.mirror(a) for a, do in enumerate(mirrored) if do] - self.locked = locked + self.set_locked(locked) def __deepcopy__(self, memo: Dict = None) -> 'Arc': memo = {} if memo is None else memo @@ -194,7 +199,8 @@ class Arc(Shape, metaclass=AutoSlots): new._offset = self._offset.copy() new._radii = self._radii.copy() new._angles = self._angles.copy() - new.locked = self.locked + new._annotations = copy.deepcopy(self._annotations) + new.set_locked(self.locked) return new def to_polygons(self, diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 2834b2a..447145f 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -1,12 +1,14 @@ from typing import List, Dict, Optional import copy -import numpy + +import numpy # type: ignore from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, vector2, layer_t, AutoSlots +from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t +from ..traits import LockableImpl class Circle(Shape, metaclass=AutoSlots): @@ -48,23 +50,36 @@ class Circle(Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, - locked: bool = False): - object.__setattr__(self, 'locked', False) + annotations: Optional[annotations_t] = None, + locked: bool = False, + raw: bool = False, + ): + LockableImpl.unlock(self) self.identifier = () - self.offset = numpy.array(offset, dtype=float) - self.layer = layer - self.dose = dose - self.radius = radius + if raw: + self._radius = radius + self._offset = offset + self._repetition = repetition + self._annotations = annotations if annotations is not None else {} + self._layer = layer + self._dose = dose + else: + self.radius = radius + self.offset = offset + self.repetition = repetition + self.annotations = annotations if annotations is not None else {} + self.layer = layer + self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen - self.repetition = repetition - self.locked = locked + self.set_locked(locked) def __deepcopy__(self, memo: Dict = None) -> 'Circle': memo = {} if memo is None else memo new = copy.copy(self).unlock() new._offset = self._offset.copy() - new.locked = self.locked + new._annotations = copy.deepcopy(self._annotations) + new.set_locked(self.locked) return new def to_polygons(self, diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index e3836a1..0b73dd1 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,13 +1,15 @@ from typing import List, Tuple, Dict, Sequence, Optional import copy import math -import numpy + +import numpy # type: ignore from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots, annotations_t +from ..traits import LockableImpl class Ellipse(Shape, metaclass=AutoSlots): @@ -95,16 +97,18 @@ class Ellipse(Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, + annotations: Optional[annotations_t] = None, locked: bool = False, raw: bool = False, ): - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) self.identifier = () if raw: self._radii = radii self._offset = offset self._rotation = rotation self._repetition = repetition + self._annotations = annotations if annotations is not None else {} self._layer = layer self._dose = dose else: @@ -112,19 +116,21 @@ class Ellipse(Shape, metaclass=AutoSlots): self.offset = offset self.rotation = rotation self.repetition = repetition + self.annotations = annotations if annotations is not None else {} self.layer = layer self.dose = dose [self.mirror(a) for a, do in enumerate(mirrored) if do] self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen - self.locked = locked + self.set_locked(locked) def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': memo = {} if memo is None else memo new = copy.copy(self).unlock() new._offset = self._offset.copy() new._radii = self._radii.copy() - new.locked = self.locked + new._annotations = copy.deepcopy(self._annotations) + new.set_locked(self.locked) return new def to_polygons(self, diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 7570c7c..4d11d9e 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,14 +1,16 @@ from typing import List, Tuple, Dict, Optional, Sequence import copy from enum import Enum -import numpy + +import numpy # type: ignore from numpy import pi, inf from . import Shape, normalized_shape_tuple, Polygon, Circle from .. import PatternError from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots -from ..utils import remove_colinear_vertices, remove_duplicate_vertices +from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t +from ..traits import LockableImpl class PathCap(Enum): @@ -149,10 +151,11 @@ class Path(Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, + annotations: Optional[annotations_t] = None, locked: bool = False, raw: bool = False, ): - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) self._cap_extensions = None # Since .cap setter might access it self.identifier = () @@ -160,6 +163,7 @@ class Path(Shape, metaclass=AutoSlots): self._vertices = vertices self._offset = offset self._repetition = repetition + self._annotations = annotations if annotations is not None else {} self._layer = layer self._dose = dose self._width = width @@ -169,6 +173,7 @@ class Path(Shape, metaclass=AutoSlots): self.vertices = vertices self.offset = offset self.repetition = repetition + self.annotations = annotations if annotations is not None else {} self.layer = layer self.dose = dose self.width = width @@ -176,7 +181,7 @@ class Path(Shape, metaclass=AutoSlots): self.cap_extensions = cap_extensions self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] - self.locked = locked + self.set_locked(locked) def __deepcopy__(self, memo: Dict = None) -> 'Path': memo = {} if memo is None else memo @@ -185,7 +190,8 @@ class Path(Shape, metaclass=AutoSlots): new._vertices = self._vertices.copy() new._cap = copy.deepcopy(self._cap, memo) new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) - new.locked = self.locked + new._annotations = copy.deepcopy(self._annotations) + new.set_locked(self.locked) return new @staticmethod diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 08da89e..24d609c 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,13 +1,15 @@ from typing import List, Tuple, Dict, Optional, Sequence import copy -import numpy + +import numpy # type: ignore from numpy import pi from . import Shape, normalized_shape_tuple from .. import PatternError from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots -from ..utils import remove_colinear_vertices, remove_duplicate_vertices +from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t +from ..traits import LockableImpl class Polygon(Shape, metaclass=AutoSlots): @@ -77,33 +79,37 @@ class Polygon(Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, + annotations: Optional[annotations_t] = None, locked: bool = False, raw: bool = False, ): - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) self.identifier = () if raw: self._vertices = vertices self._offset = offset self._repetition = repetition + self._annotations = annotations if annotations is not None else {} self._layer = layer self._dose = dose else: self.vertices = vertices self.offset = offset self.repetition = repetition + self.annotations = annotations if annotations is not None else {} self.layer = layer self.dose = dose self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] - self.locked = locked + self.set_locked(locked) def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': memo = {} if memo is None else memo new = copy.copy(self).unlock() new._offset = self._offset.copy() new._vertices = self._vertices.copy() - new.locked = self.locked + new._annotations = copy.deepcopy(self._annotations) + new.set_locked(self.locked) return new @staticmethod diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 7a5e3f3..b9a0cdd 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -1,13 +1,15 @@ from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from abc import ABCMeta, abstractmethod import copy -import numpy + +import numpy # type: ignore from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, - PivotableImpl, LockableImpl, RepeatableImpl) + PivotableImpl, LockableImpl, RepeatableImpl, + AnnotatableImpl) if TYPE_CHECKING: from . import Polygon @@ -27,7 +29,7 @@ T = TypeVar('T', bound='Shape') class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, - PivotableImpl, RepeatableImpl, LockableImpl, metaclass=ABCMeta): + PivotableImpl, RepeatableImpl, LockableImpl, AnnotatableImpl, metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ @@ -39,7 +41,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable def __copy__(self) -> 'Shape': cls = self.__class__ new = cls.__new__(cls) - for name in self.__slots__: + for name in self.__slots__: # type: str object.__setattr__(new, name, getattr(self, name)) return new diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 599d377..c8af2cc 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,6 +1,7 @@ from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence import copy -import numpy + +import numpy # type: ignore from numpy import pi, inf from . import Shape, Polygon, normalized_shape_tuple @@ -8,6 +9,8 @@ from .. import PatternError from ..repetition import Repetition from ..traits import RotatableImpl from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots +from ..utils import annotations_t +from ..traits import LockableImpl # Loaded on use: # from freetype import Face @@ -67,10 +70,11 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, + annotations: Optional[annotations_t] = None, locked: bool = False, raw: bool = False, ): - object.__setattr__(self, 'locked', False) + LockableImpl.unlock(self) self.identifier = () if raw: self._offset = offset @@ -81,6 +85,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): self._rotation = rotation self._mirrored = mirrored self._repetition = repetition + self._annotations = annotations if annotations is not None else {} else: self.offset = offset self.layer = layer @@ -90,15 +95,17 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): self.rotation = rotation self.mirrored = mirrored self.repetition = repetition + self.annotations = annotations if annotations is not None else {} self.font_path = font_path - self.locked = locked + self.set_locked(locked) def __deepcopy__(self, memo: Dict = None) -> 'Text': memo = {} if memo is None else memo new = copy.copy(self).unlock() new._offset = self._offset.copy() new._mirrored = copy.deepcopy(self._mirrored, memo) - new.locked = self.locked + new._annotations = copy.deepcopy(self._annotations) + new.set_locked(self.locked) return new def to_polygons(self, diff --git a/masque/subpattern.py b/masque/subpattern.py index ebd6876..6462cff 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -7,14 +7,15 @@ from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any import copy -import numpy +import numpy # type: ignore from numpy import pi from .error import PatternError, PatternLockedError -from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots +from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots, annotations_t from .repetition import Repetition from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, - Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl) + Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl, + AnnotatableImpl) if TYPE_CHECKING: @@ -22,7 +23,8 @@ if TYPE_CHECKING: class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, - PivotableImpl, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots): + PivotableImpl, Copyable, RepeatableImpl, LockableImpl, AnnotatableImpl, + metaclass=AutoSlots): """ SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. @@ -49,8 +51,10 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi dose: float = 1.0, scale: float = 1.0, repetition: Optional[Repetition] = None, + annotations: Optional[annotations_t] = None, locked: bool = False, - identifier: Tuple[Any, ...] = ()): + identifier: Tuple[Any, ...] = (), + ): """ Args: pattern: Pattern to reference. @@ -74,7 +78,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi mirrored = [False, False] self.mirrored = mirrored self.repetition = repetition - self.locked = locked + self.annotations = annotations if annotations is not None else {} + self.set_locked(locked) def __copy__(self) -> 'SubPattern': new = SubPattern(pattern=self.pattern, @@ -84,6 +89,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi scale=self.scale, mirrored=self.mirrored.copy(), repetition=copy.deepcopy(self.repetition), + annotations=copy.deepcopy(self.annotations), locked=self.locked) return new @@ -92,7 +98,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi new = copy.copy(self).unlock() new.pattern = copy.deepcopy(self.pattern, memo) new.repetition = copy.deepcopy(self.repetition, memo) - new.locked = self.locked + new.annotations = copy.deepcopy(self.annotations, memo) + new.set_locked(self.locked) return new # pattern property diff --git a/masque/traits/__init__.py b/masque/traits/__init__.py index 8af6434..d885264 100644 --- a/masque/traits/__init__.py +++ b/masque/traits/__init__.py @@ -7,3 +7,4 @@ from .scalable import Scalable, ScalableImpl from .mirrorable import Mirrorable from .copyable import Copyable from .lockable import Lockable, LockableImpl +from .annotatable import Annotatable, AnnotatableImpl diff --git a/masque/traits/annotatable.py b/masque/traits/annotatable.py new file mode 100644 index 0000000..0285cfd --- /dev/null +++ b/masque/traits/annotatable.py @@ -0,0 +1,56 @@ +from typing import TypeVar +from types import MappingProxyType +from abc import ABCMeta, abstractmethod +import copy + +from ..utils import annotations_t +from ..error import PatternError + + +T = TypeVar('T', bound='Annotatable') +I = TypeVar('I', bound='AnnotatableImpl') + + +class Annotatable(metaclass=ABCMeta): + """ + Abstract class for all annotatable entities + Annotations correspond to GDS/OASIS "properties" + """ + __slots__ = () + + ''' + ---- Properties + ''' + @property + @abstractmethod + def annotations(self) -> annotations_t: + """ + Dictionary mapping annotation names to values + """ + pass + + +class AnnotatableImpl(Annotatable, metaclass=ABCMeta): + """ + Simple implementation of `Annotatable`. + """ + __slots__ = () + + _annotations: annotations_t + """ Dictionary storing annotation name/value pairs """ + + ''' + ---- Non-abstract properties + ''' + @property + def annotations(self) -> annotations_t: + # TODO: Find a way to make sure the subclass implements Lockable without dealing with diamond inheritance or this extra hasattr + if hasattr(self, 'is_locked') and self.is_locked(): + return MappingProxyType(self._annotations) + return self._annotations + + @annotations.setter + def annotations(self, annotations: annotations_t): + if not isinstance(annotations, dict): + raise PatternError(f'annotations expected dict, got {type(annotations)}') + self._annotations = annotations diff --git a/masque/traits/doseable.py b/masque/traits/doseable.py index 419d0bd..96c535c 100644 --- a/masque/traits/doseable.py +++ b/masque/traits/doseable.py @@ -1,7 +1,6 @@ from typing import List, Tuple, Callable, TypeVar, Optional from abc import ABCMeta, abstractmethod import copy -import numpy from ..error import PatternError, PatternLockedError from ..utils import is_scalar diff --git a/masque/traits/layerable.py b/masque/traits/layerable.py index 5382450..e3d5f7b 100644 --- a/masque/traits/layerable.py +++ b/masque/traits/layerable.py @@ -1,7 +1,6 @@ from typing import List, Tuple, Callable, TypeVar, Optional from abc import ABCMeta, abstractmethod import copy -import numpy from ..error import PatternError, PatternLockedError from ..utils import layer_t diff --git a/masque/traits/lockable.py b/masque/traits/lockable.py index cc12760..fadaaa3 100644 --- a/masque/traits/lockable.py +++ b/masque/traits/lockable.py @@ -1,7 +1,6 @@ from typing import List, Tuple, Callable, TypeVar, Optional from abc import ABCMeta, abstractmethod import copy -import numpy from ..error import PatternError, PatternLockedError @@ -19,6 +18,7 @@ class Lockable(metaclass=ABCMeta): ''' ---- Methods ''' + @abstractmethod def lock(self: T) -> T: """ Lock the object, disallowing further changes @@ -28,6 +28,7 @@ class Lockable(metaclass=ABCMeta): """ pass + @abstractmethod def unlock(self: T) -> T: """ Unlock the object, reallowing changes @@ -37,6 +38,32 @@ class Lockable(metaclass=ABCMeta): """ pass + @abstractmethod + def is_locked(self) -> bool: + """ + Returns: + True if the object is locked + """ + pass + + def set_locked(self: T, locked: bool) -> T: + """ + Locks or unlocks based on the argument. + No action if already in the requested state. + + Args: + locked: State to set. + + Returns: + self + """ + if locked != self.is_locked(): + if locked: + self.lock() + else: + self.unlock() + return self + class LockableImpl(Lockable, metaclass=ABCMeta): """ @@ -62,3 +89,6 @@ class LockableImpl(Lockable, metaclass=ABCMeta): def unlock(self: I) -> I: object.__setattr__(self, 'locked', False) return self + + def is_locked(self) -> bool: + return self.locked diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index 1e10800..a66c074 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -1,10 +1,10 @@ from typing import List, Tuple, Callable, TypeVar, Optional from abc import ABCMeta, abstractmethod import copy -import numpy from ..error import PatternError, PatternLockedError + T = TypeVar('T', bound='Mirrorable') #I = TypeVar('I', bound='MirrorableImpl') diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index d433a91..150daf0 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -3,7 +3,7 @@ from typing import List, Tuple, Callable, TypeVar, Optional from abc import ABCMeta, abstractmethod import copy -import numpy +import numpy # type: ignore from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2 diff --git a/masque/traits/repeatable.py b/masque/traits/repeatable.py index 3971a94..67183ad 100644 --- a/masque/traits/repeatable.py +++ b/masque/traits/repeatable.py @@ -1,7 +1,6 @@ from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from abc import ABCMeta, abstractmethod import copy -import numpy from ..error import PatternError, PatternLockedError diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index ef7e748..c79e89e 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -2,7 +2,7 @@ from typing import List, Tuple, Callable, TypeVar, Optional from abc import ABCMeta, abstractmethod import copy -import numpy +import numpy # type: ignore from numpy import pi from .positionable import Positionable diff --git a/masque/traits/scalable.py b/masque/traits/scalable.py index ac349a2..bebda69 100644 --- a/masque/traits/scalable.py +++ b/masque/traits/scalable.py @@ -1,7 +1,6 @@ from typing import List, Tuple, Callable, TypeVar, Optional from abc import ABCMeta, abstractmethod import copy -import numpy from ..error import PatternError, PatternLockedError from ..utils import is_scalar diff --git a/masque/utils.py b/masque/utils.py index 2f2e499..c33b8c4 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -1,15 +1,16 @@ """ Various helper functions """ - -from typing import Any, Union, Tuple, Sequence +from typing import Any, Union, Tuple, Sequence, Dict, List from abc import ABCMeta -import numpy +import numpy # type: ignore + # Type definitions vector2 = Union[numpy.ndarray, Tuple[float, float], Sequence[float]] layer_t = Union[int, Tuple[int, int], str] +annotations_t = Dict[str, List[Union[int, float, str]]] def is_scalar(var: Any) -> bool: From 2019c4a16bf754ec3931f04cd16a6dcac8af0bc6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Thu, 10 Sep 2020 20:18:34 -0700 Subject: [PATCH 40/71] Update readme --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fae263e..214f7ad 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,19 @@ Alternatively, install from git pip3 install git+https://mpxd.net/code/jan/masque.git@release ``` +## Translation +- `Pattern`: OASIS or GDS "Cell", DXF "Block" +- `SubPattern`: GDS "AREF/SREF", OASIS "Placement" +- `Shape`: OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline" +- `repetition`: OASIS "repetition". GDS "AREF" is a `SubPattern` combined with a `Grid` repetition. +- `Label`: OASIS, GDS, DXF "Text". +- `annotation`: OASIS or GDS "property" + + ## TODO -* Polygon de-embedding -* Construct from bitmap -* Boolean operations on polygons (using pyclipper) -* Implement shape/cell properties +* Better interface for polygon operations (e.g. with `pyclipper`) + - de-embedding + - boolean ops +* Construct polygons from bitmap using `skimage.find_contours` * Deal with shape repetitions for dxf, svg From 2a8e43cbcd8472b8eca33521f2d11b11ef4a0151 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Thu, 10 Sep 2020 20:18:59 -0700 Subject: [PATCH 41/71] bump version to 2.0 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index c239c60..cd5ac03 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -1.5 +2.0 From 0e04633f618b34e8e946f58f8fc90a78520c2973 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Thu, 10 Sep 2020 20:37:19 -0700 Subject: [PATCH 42/71] Force use of keyword args on most constructors --- masque/label.py | 1 + masque/pattern.py | 1 + masque/shapes/arc.py | 1 + masque/shapes/circle.py | 1 + masque/shapes/ellipse.py | 1 + masque/shapes/path.py | 1 + masque/shapes/polygon.py | 1 + masque/shapes/text.py | 1 + masque/subpattern.py | 1 + 9 files changed, 9 insertions(+) diff --git a/masque/label.py b/masque/label.py index f157f46..96c5aa1 100644 --- a/masque/label.py +++ b/masque/label.py @@ -40,6 +40,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot def __init__(self, string: str, + *, offset: vector2 = (0.0, 0.0), layer: layer_t = 0, repetition: Optional[Repetition] = None, diff --git a/masque/pattern.py b/masque/pattern.py index de7ecd2..5a7dd01 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -50,6 +50,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): def __init__(self, name: str = '', + *, shapes: Sequence[Shape] = (), labels: Sequence[Label] = (), subpatterns: Sequence[SubPattern] = (), diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index ea9b4c7..6f75e7e 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -154,6 +154,7 @@ class Arc(Shape, metaclass=AutoSlots): radii: vector2, angles: vector2, width: float, + *, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 447145f..b3e07ae 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -44,6 +44,7 @@ class Circle(Shape, metaclass=AutoSlots): def __init__(self, radius: float, + *, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 0b73dd1..b2ceec6 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -89,6 +89,7 @@ class Ellipse(Shape, metaclass=AutoSlots): def __init__(self, radii: vector2, + *, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 4d11d9e..b25f464 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -143,6 +143,7 @@ class Path(Shape, metaclass=AutoSlots): def __init__(self, vertices: numpy.ndarray, width: float = 0.0, + *, cap: PathCap = PathCap.Flush, cap_extensions: numpy.ndarray = None, offset: vector2 = (0.0, 0.0), diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 24d609c..c11b662 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -73,6 +73,7 @@ class Polygon(Shape, metaclass=AutoSlots): def __init__(self, vertices: numpy.ndarray, + *, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: Sequence[bool] = (False, False), diff --git a/masque/shapes/text.py b/masque/shapes/text.py index c8af2cc..debd994 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -64,6 +64,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): string: str, height: float, font_path: str, + *, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: Tuple[bool, bool] = (False, False), diff --git a/masque/subpattern.py b/masque/subpattern.py index 6462cff..6d0c2a9 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -45,6 +45,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi def __init__(self, pattern: Optional['Pattern'], + *, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: Optional[Sequence[bool]] = None, From f996a1629f070fb2756572ee1a25796eebe839d7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Thu, 10 Sep 2020 20:47:00 -0700 Subject: [PATCH 43/71] limit number of arguments on more functions --- masque/file/dxf.py | 1 + masque/file/gdsii.py | 1 + masque/file/svg.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 7ae4b6d..581bfcd 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -34,6 +34,7 @@ DEFAULT_LAYER = 'DEFAULT' def write(pattern: Pattern, stream: io.TextIOBase, + *, modify_originals: bool = False, dxf_version='AC1024', disambiguate_func: Callable[[Iterable[Pattern]], None] = None): diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 7192ecc..df23293 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -54,6 +54,7 @@ def build(patterns: Union[Pattern, List[Pattern]], meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-gdsii-write', + *, modify_originals: bool = False, disambiguate_func: Callable[[Iterable[Pattern]], None] = None, ) -> gdsii.library.Library: diff --git a/masque/file/svg.py b/masque/file/svg.py index 0ce5750..b719b0b 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -12,8 +12,8 @@ from .. import Pattern def writefile(pattern: Pattern, - filename: str, - custom_attributes: bool=False): + filename: str, + custom_attributes: bool=False): """ 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 From c4dfd06a424a0bd8c460f548a38a919b586eaf65 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Fri, 18 Sep 2020 19:06:44 -0700 Subject: [PATCH 44/71] improve type annotations --- masque/file/gdsii.py | 2 +- masque/file/oasis.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index df23293..1a29a8f 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -100,7 +100,7 @@ def build(patterns: Union[Pattern, List[Pattern]], patterns = [patterns] if disambiguate_func is None: - disambiguate_func = disambiguate_pattern_names + disambiguate_func = disambiguate_pattern_names # type: ignore assert(disambiguate_func is not None) # placate mypy if not modify_originals: diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 34208b0..d1cbc13 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -128,6 +128,7 @@ def build(patterns: Union[Pattern, List[Pattern]], for tt in (True, False)] def layer2oas(mlayer: layer_t) -> Tuple[int, int]: + assert(layer_map is not None) layer_num = layer_map[mlayer] if isinstance(mlayer, str) else mlayer return _mlayer2oas(layer_num) else: @@ -275,6 +276,7 @@ def read(stream: io.BufferedIOBase, # note XELEMENT has no repetition continue + assert(not isinstance(element.repetition, fatamorgana.ReuseRepetition)) repetition = repetition_fata2masq(element.repetition) # Switch based on element type: @@ -490,6 +492,7 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo Helper function to create a SubPattern from a placment. Sets subpat.pattern to None and sets the instance .identifier to (struct_name,). """ + assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition)) xy = numpy.array((placement.x, placement.y)) mag = placement.magnification if placement.magnification is not None else 1 pname = placement.get_name() From 64fbd08cac83011a1b1dd7653f1975f5b74e13d3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Fri, 18 Sep 2020 19:06:56 -0700 Subject: [PATCH 45/71] don't attempt to set structure properties --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 1a29a8f..1aec686 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -127,7 +127,7 @@ def build(patterns: Union[Pattern, List[Pattern]], structure = gdsii.structure.Structure(name=pat.name.encode('ASCII')) lib.append(structure) - structure.properties = _annotations_to_properties(pat.annotations, 512) +# structure.properties = _annotations_to_properties(pat.annotations, 512) structure += _shapes_to_elements(pat.shapes) structure += _labels_to_texts(pat.labels) From 5f72fe318f4c19ae055a9945627842f7c1779029 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Fri, 18 Sep 2020 19:07:14 -0700 Subject: [PATCH 46/71] Loosen requirements from List to Sequence --- masque/file/gdsii.py | 6 +++--- masque/file/oasis.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 1aec686..1b9c1d0 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -50,7 +50,7 @@ path_cap_map = { } -def build(patterns: Union[Pattern, List[Pattern]], +def build(patterns: Union[Pattern, Sequence[Pattern]], meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-gdsii-write', @@ -136,7 +136,7 @@ def build(patterns: Union[Pattern, List[Pattern]], return lib -def write(patterns: Union[Pattern, List[Pattern]], +def write(patterns: Union[Pattern, Sequence[Pattern]], stream: io.BufferedIOBase, *args, **kwargs): @@ -154,7 +154,7 @@ def write(patterns: Union[Pattern, List[Pattern]], lib.save(stream) return -def writefile(patterns: Union[List[Pattern], Pattern], +def writefile(patterns: Union[Sequence[Pattern], Pattern], filename: Union[str, pathlib.Path], *args, **kwargs, diff --git a/masque/file/oasis.py b/masque/file/oasis.py index d1cbc13..6e0284d 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -50,7 +50,7 @@ path_cap_map = { #TODO implement properties #TODO implement more shape types? -def build(patterns: Union[Pattern, List[Pattern]], +def build(patterns: Union[Pattern, Sequence[Pattern]], units_per_micron: int, layer_map: Optional[Dict[str, Union[int, Tuple[int, int]]]] = None, *, @@ -156,7 +156,7 @@ def build(patterns: Union[Pattern, List[Pattern]], return lib -def write(patterns: Union[List[Pattern], Pattern], +def write(patterns: Union[Sequence[Pattern], Pattern], stream: io.BufferedIOBase, *args, **kwargs): @@ -174,7 +174,7 @@ def write(patterns: Union[List[Pattern], Pattern], lib.write(stream) -def writefile(patterns: Union[List[Pattern], Pattern], +def writefile(patterns: Union[Sequence[Pattern], Pattern], filename: Union[str, pathlib.Path], *args, **kwargs, From 3f59168cec875d58a9d0eb664000c55e82de2936 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Fri, 18 Sep 2020 19:46:57 -0700 Subject: [PATCH 47/71] Use chain() instead of adding lists --- masque/pattern.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 5a7dd01..9edfdd2 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -5,8 +5,8 @@ from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload from typing import MutableMapping, Iterable import copy -import itertools import pickle +from itertools import chain from collections import defaultdict import numpy # type: ignore @@ -321,7 +321,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): self """ old_shapes = self.shapes - self.shapes = list(itertools.chain.from_iterable( + self.shapes = list(chain.from_iterable( (shape.to_polygons(poly_num_points, poly_max_arclen) for shape in old_shapes))) for subpat in self.subpatterns: @@ -347,7 +347,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): self.polygonize().flatten() old_shapes = self.shapes - self.shapes = list(itertools.chain.from_iterable( + self.shapes = list(chain.from_iterable( (shape.manhattanize(grid_x, grid_y) for shape in old_shapes))) return self @@ -525,13 +525,12 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: `[[x_min, y_min], [x_max, y_max]]` or `None` """ - entries = self.shapes + self.subpatterns + self.labels - if not entries: + if self.is_empty(): return None min_bounds = numpy.array((+inf, +inf)) max_bounds = numpy.array((-inf, -inf)) - for entry in entries: + for entry in chain(self.shapes, self.subpatterns, self.labels): bounds = entry.get_bounds() if bounds is None: continue @@ -634,7 +633,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: self """ - for entry in self.shapes + self.subpatterns + self.labels: + for entry in chain(self.shapes, self.subpatterns, self.labels): entry.translate(offset) return self @@ -648,7 +647,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: self """ - for entry in self.shapes + self.subpatterns: + for entry in chain(self.shapes, self.subpatterns): entry.scale_by(c) return self @@ -663,7 +662,8 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: self """ - for entry in self.shapes + self.subpatterns: + entry: Scalable + for entry in chain(self.shapes, self.subpatterns): entry.offset *= c entry.scale_by(c) for label in self.labels: @@ -698,7 +698,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: self """ - for entry in self.shapes + self.subpatterns + self.labels: + for entry in chain(self.shapes, self.subpatterns, self.labels): entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset) return self @@ -712,7 +712,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: self """ - for entry in self.shapes + self.subpatterns: + for entry in chain(self.shapes, self.subpatterns): entry.rotate(rotation) return self @@ -727,7 +727,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: self """ - for entry in self.shapes + self.subpatterns + self.labels: + for entry in chain(self.shapes, self.subpatterns, self.labels): entry.offset[axis - 1] *= -1 return self @@ -743,7 +743,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Returns: self """ - for entry in self.shapes + self.subpatterns: + for entry in chain(self.shapes, self.subpatterns): entry.mirror(axis) return self @@ -772,7 +772,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): Return: self """ - for entry in self.shapes + self.subpatterns: + for entry in chain(self.shapes, self.subpatterns): entry.dose *= c return self @@ -843,7 +843,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): self """ self.lock() - for ss in self.shapes + self.labels: + for ss in chain(self.shapes, self.labels): ss.lock() for sp in self.subpatterns: sp.deeplock() @@ -860,7 +860,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): self """ self.unlock() - for ss in self.shapes + self.labels: + for ss in chain(self.shapes, self.labels): ss.unlock() for sp in self.subpatterns: sp.deepunlock() From f51144ae6ac3dd28dfdd79fe44e57830be00011d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Fri, 18 Sep 2020 19:47:31 -0700 Subject: [PATCH 48/71] misc doc/import/typing fixes --- masque/label.py | 2 +- masque/pattern.py | 4 ++-- masque/shapes/shape.py | 2 +- masque/traits/__init__.py | 3 +++ 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/masque/label.py b/masque/label.py index 96c5aa1..c8ca802 100644 --- a/masque/label.py +++ b/masque/label.py @@ -5,7 +5,7 @@ from numpy import pi from .repetition import Repetition from .error import PatternError, PatternLockedError -from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots, annotations_t +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 diff --git a/masque/pattern.py b/masque/pattern.py index 9edfdd2..adbef2b 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -916,8 +916,8 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): overdraw: Whether to create a new figure or draw on a pre-existing one """ # TODO: add text labels to visualize() - from matplotlib import pyplot - import matplotlib.collections + from matplotlib import pyplot # type: ignore + import matplotlib.collections # type: ignore offset = numpy.array(offset, dtype=float) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index b9a0cdd..6c72263 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -5,7 +5,7 @@ import copy import numpy # type: ignore from ..error import PatternError, PatternLockedError -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t +from ..utils import rotation_matrix_2d, vector2, layer_t from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, LockableImpl, RepeatableImpl, diff --git a/masque/traits/__init__.py b/masque/traits/__init__.py index d885264..3f88554 100644 --- a/masque/traits/__init__.py +++ b/masque/traits/__init__.py @@ -1,3 +1,6 @@ +""" +Traits (mixins) and default implementations +""" from .positionable import Positionable, PositionableImpl from .layerable import Layerable, LayerableImpl from .doseable import Doseable, DoseableImpl From a02dfdc9824387712218077cf420b9931c7def6f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 00:44:56 -0700 Subject: [PATCH 49/71] remove dependency list from top-level comment --- masque/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/masque/__init__.py b/masque/__init__.py index 9b8efb1..0a13faa 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -15,13 +15,6 @@ Note that the methods for these classes try to avoid copying wherever possible, so unless otherwise noted, assume that arguments are stored by-reference. - - - Dependencies: - - `numpy` - - `matplotlib` [Pattern.visualize(...)] - - `python-gdsii` [masque.file.gdsii] - - `svgwrite` [masque.file.svg] """ import pathlib From 0e4b6828df671e3659617227cf231ac70011f734 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:24:04 -0700 Subject: [PATCH 50/71] Disable height warning for DXF --- masque/file/dxf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 581bfcd..a5e7bc1 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -237,9 +237,9 @@ def _read_block(block, clean_vertices): } string = element.dxfattribs().get('text', '') 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.') +# 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.') pat.labels.append(Label(string=string, **args)) # else: # pat.shapes.append(Text(string=string, height=height, font_path=????)) From 84f811e9d1570b8b170ecc06d7e9fd9eb58906c1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:32:12 -0700 Subject: [PATCH 51/71] move clean_vertices functionality out into a common function --- masque/file/gdsii.py | 14 +++----------- masque/file/oasis.py | 16 +++------------- masque/file/utils.py | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 1b9c1d0..18fc9ff 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -28,7 +28,7 @@ import gdsii.library import gdsii.structure import gdsii.elements -from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose +from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose, clean_pattern_vertices from .. import Pattern, SubPattern, PatternError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid @@ -250,20 +250,10 @@ def read(stream: io.BufferedIOBase, # Switch based on element type: if isinstance(element, gdsii.elements.Boundary): poly = _boundary_to_polygon(element, raw_mode) - if clean_vertices: - try: - poly.clean_vertices() - except PatternError: - continue pat.shapes.append(poly) if isinstance(element, gdsii.elements.Path): path = _gpath_to_mpath(element, raw_mode) - if clean_vertices: - try: - path.clean_vertices() - except PatternError as err: - continue pat.shapes.append(path) elif isinstance(element, gdsii.elements.Text): @@ -276,6 +266,8 @@ def read(stream: io.BufferedIOBase, isinstance(element, gdsii.elements.ARef)): pat.subpatterns.append(_ref_to_subpat(element)) + if clean_vertices: + clean_pattern_vertices(pat) patterns.append(pat) # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 6e0284d..142b4a1 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -27,7 +27,7 @@ import fatamorgana import fatamorgana.records as fatrec from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference -from .utils import mangle_name, make_dose_table +from .utils import mangle_name, make_dose_table, clean_pattern_vertices from .. import Pattern, SubPattern, PatternError, Label, Shape from ..shapes import Polygon, Path, Circle from ..repetition import Grid, Arbitrary, Repetition @@ -289,12 +289,6 @@ def read(stream: io.BufferedIOBase, annotations=annotations, repetition=repetition) - if clean_vertices: - try: - poly.clean_vertices() - except PatternError: - continue - pat.shapes.append(poly) elif isinstance(element, fatrec.Path): @@ -321,12 +315,6 @@ def read(stream: io.BufferedIOBase, cap=cap, **path_args) - if clean_vertices: - try: - path.clean_vertices() - except PatternError as err: - continue - pat.shapes.append(path) elif isinstance(element, fatrec.Rectangle): @@ -455,6 +443,8 @@ def read(stream: io.BufferedIOBase, for placement in cell.placements: pat.subpatterns.append(_placement_to_subpat(placement, lib)) + if clean_vertices: + clean_pattern_vertices(pat) patterns.append(pat) # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries diff --git a/masque/file/utils.py b/masque/file/utils.py index e36765e..30b2808 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -6,6 +6,7 @@ import re import copy from .. import Pattern, PatternError +from ..shapes import Polygon, Path def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: @@ -25,6 +26,30 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: return sanitized_name +def clean_pattern_vertices(pat: Pattern) -> Pattern: + """ + Given a pattern, remove any redundant vertices in its polygons and paths. + The cleaning process completely removes any polygons with zero area or <3 vertices. + + Args: + pat: Pattern to clean + + Returns: + pat + """ + remove_inds = [] + for ii, shape in enumerate(pat.shapes): + if not isinstance(shape, (Polygon, Path)): + continue + try: + shape.clean_vertices() + except PatternError: + remove_inds.append(ii) + for ii in sorted(remove_inds, reverse=True): + del pat.shapes[ii] + return pat + + 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) From c6684936cf2b3025f481c367e18ef0ca33f1237b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:33:46 -0700 Subject: [PATCH 52/71] Improve docs, error messages, and type annotations --- masque/file/gdsii.py | 23 ++++++++++------------- masque/file/oasis.py | 1 - masque/pattern.py | 2 +- masque/shapes/shape.py | 2 +- masque/shapes/text.py | 4 ++-- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 18fc9ff..3d97fe3 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -10,6 +10,12 @@ Note that GDSII references follow the same convention as `masque`, Scaling, rotation, and mirroring apply to individual instances, not grid vectors or offsets. + +Notes: + * absolute positioning is not supported + * PLEX is not supported + * 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 @@ -35,8 +41,6 @@ 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 -#TODO absolute positioning - logger = logging.getLogger(__name__) @@ -127,8 +131,6 @@ def build(patterns: Union[Pattern, Sequence[Pattern]], structure = gdsii.structure.Structure(name=pat.name.encode('ASCII')) lib.append(structure) -# structure.properties = _annotations_to_properties(pat.annotations, 512) - structure += _shapes_to_elements(pat.shapes) structure += _labels_to_texts(pat.labels) structure += _subpatterns_to_refs(pat.subpatterns) @@ -243,9 +245,6 @@ def read(stream: io.BufferedIOBase, patterns = [] for structure in lib: pat = Pattern(name=structure.name.decode('ASCII')) - if pat.annotations: - logger.warning('Dropping Pattern-level annotations; they are not supported by python-gdsii') -# pat.annotations = {str(k): v for k, v in structure.properties} for element in structure: # Switch based on element type: if isinstance(element, gdsii.elements.Boundary): @@ -304,10 +303,8 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None and sets the instance .identifier to (struct_name,). - BUG: - "Absolute" means not affected by parent elements. - That's not currently supported by masque at all, so need to either tag it and - undo the parent transformations, or implement it in masque. + NOTE: "Absolute" means not affected by parent elements. + That's not currently supported by masque at all (and not planned). """ rotation = 0.0 offset = numpy.array(element.xy[0], dtype=float) @@ -320,12 +317,12 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, scale = element.mag # Bit 13 means absolute scale if get_bit(element.strans, 15 - 13): - raise PatternError('Absolute scale is not implemented yet!') + raise PatternError('Absolute scale is not implemented in masque!') if element.angle is not None: rotation = numpy.deg2rad(element.angle) # Bit 14 means absolute rotation if get_bit(element.strans, 15 - 14): - raise PatternError('Absolute rotation is not implemented yet!') + raise PatternError('Absolute rotation is not implemented in masque!') # Bit 0 means mirror x-axis if get_bit(element.strans, 15 - 0): mirror_across_x = True diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 142b4a1..8d9e074 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -47,7 +47,6 @@ path_cap_map = { PathExtensionScheme.Arbitrary: Path.Cap.SquareCustom, } -#TODO implement properties #TODO implement more shape types? def build(patterns: Union[Pattern, Sequence[Pattern]], diff --git a/masque/pattern.py b/masque/pattern.py index adbef2b..2393a6f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -18,7 +18,7 @@ from .shapes import Shape, Polygon from .label import Label from .utils import rotation_matrix_2d, vector2, normalize_mirror, AutoSlots, annotations_t from .error import PatternError, PatternLockedError -from .traits import LockableImpl, AnnotatableImpl +from .traits import LockableImpl, AnnotatableImpl, Scalable visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern'] diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 6c72263..8ffb1a4 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -246,7 +246,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable List of `Polygon` objects with grid-aligned edges. """ from . import Polygon - import skimage.measure + import skimage.measure # type: ignore import float_raster grid_x = numpy.unique(grid_x) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index debd994..2384404 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -170,8 +170,8 @@ def get_char_as_polygons(font_path: str, char: str, resolution: float = 48*64, ) -> Tuple[List[List[List[float]]], float]: - from freetype import Face - from matplotlib.path import Path + from freetype import Face # type: ignore + from matplotlib.path import Path # type: ignore """ Get a list of polygons representing a single character. From 7cad46fa469edb550ff5bfa33e5f1b11fab550b0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:35:05 -0700 Subject: [PATCH 53/71] add klamath-based gds read/write --- masque/file/klamath.py | 541 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 541 insertions(+) create mode 100644 masque/file/klamath.py diff --git a/masque/file/klamath.py b/masque/file/klamath.py new file mode 100644 index 0000000..4440457 --- /dev/null +++ b/masque/file/klamath.py @@ -0,0 +1,541 @@ +""" +GDSII file format readers and writers using the `klamath` library. + +Note that GDSII references follow the same convention as `masque`, + with this order of operations: + 1. Mirroring + 2. Rotation + 3. Scaling + 4. Offset and array expansion (no mirroring/rotation/scaling applied to offsets) + + Scaling, rotation, and mirroring apply to individual instances, not grid + vectors or offsets. + +Notes: + * absolute positioning is not supported + * PLEX is not supported + * ELFLAGS are not supported + * 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 +import re +import io +import copy +import base64 +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 +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 ..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, + } + + +def write(patterns: Union[Pattern, Sequence[Pattern]], + stream: BinaryIO, + meters_per_unit: float, + logical_units_per_unit: float = 1, + library_name: str = 'masque-klamath', + *, + modify_originals: bool = False, + disambiguate_func: Callable[[Iterable[Pattern]], None] = None, + ) -> None: + """ + Convert a `Pattern` or list of patterns to a GDSII stream, and then mapping data as follows: + Pattern -> GDSII structure + SubPattern -> GDSII SREF or AREF + Path -> GSDII path + Shape (other than path) -> GDSII boundary/ies + Label -> GDSII text + annnotations -> properties, where possible + + For each shape, + layer is chosen to be equal to `shape.layer` if it is an int, + or `shape.layer[0]` if it is a tuple + datatype is chosen to be `shape.layer[1]` if available, + otherwise `0` + + It is often a good idea to run `pattern.subpatternize()` prior to calling this function, + especially if calling `.polygonize()` will result in very many vertices. + + If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` + prior to calling this function. + + Args: + patterns: A Pattern or list of patterns to convert. + meters_per_unit: Written into the GDSII file, meters per (database) length unit. + All distances are assumed to be an integer multiple of this unit, and are stored as such. + logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a + "logical" unit which is different from the "database" unit, for display purposes. + Default `1`. + library_name: Library name written into the GDSII file. + Default 'masque-klamath'. + modify_originals: If `True`, the original pattern is modified as part of the writing + process. Otherwise, a copy is made and `deepunlock()`-ed. + Default `False`. + disambiguate_func: Function which takes a list of patterns and alters them + to make their names valid and unique. Default is `disambiguate_pattern_names`, which + attempts to adhere to the GDSII standard as well as possible. + WARNING: No additional error checking is performed on the results. + """ + if isinstance(patterns, Pattern): + patterns = [patterns] + + if disambiguate_func is None: + disambiguate_func = disambiguate_pattern_names # type: ignore + assert(disambiguate_func is not None) # placate mypy + + if not modify_originals: + patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] + + patterns = [p.wrap_repeated_shapes() for p in patterns] + + # Create library + header = klamath.library.FileHeader(name=library_name.encode('ASCII'), + user_units_per_db_unit=logical_units_per_unit, + meters_per_db_unit=meters_per_unit) + header.write(stream) + + # Get a dict of id(pattern) -> pattern + patterns_by_id = {id(pattern): pattern for pattern in patterns} + for pattern in patterns: + for i, p in pattern.referenced_patterns_by_id().items(): + patterns_by_id[i] = p + + disambiguate_func(patterns_by_id.values()) + + # Now create a structure for each pattern, and add in any Boundary and SREF elements + for pat in patterns_by_id.values(): + elements: List[klamath.elements.Element] = [] + elements += _shapes_to_elements(pat.shapes) + elements += _labels_to_texts(pat.labels) + elements += _subpatterns_to_refs(pat.subpatterns) + + klamath.library.write_struct(stream, name=pat.name.encode('ASCII'), elements=elements) + records.ENDLIB.write(stream, None) + + +def writefile(patterns: Union[Sequence[Pattern], Pattern], + filename: Union[str, pathlib.Path], + *args, + **kwargs, + ) -> None: + """ + Wrapper for `masque.file.gdsii.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` + """ + path = pathlib.Path(filename) + if path.suffix == '.gz': + open_func: Callable = gzip.open + else: + open_func = open + + with io.BufferedWriter(open_func(path, mode='wb')) as stream: + write(patterns, stream, *args, **kwargs) + + +def readfile(filename: Union[str, pathlib.Path], + *args, + **kwargs, + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: + """ + Wrapper for `masque.file.gdsii.read()` that takes a filename or path instead of a stream. + + Will automatically decompress files with a .gz suffix. + + Args: + filename: Filename to save to. + *args: passed to `masque.file.gdsii.read` + **kwargs: passed to `masque.file.gdsii.read` + """ + path = pathlib.Path(filename) + if path.suffix == '.gz': + open_func: Callable = gzip.open + else: + open_func = open + + with io.BufferedReader(open_func(path, mode='rb')) as stream: + results = read(stream)#, *args, **kwargs) + return results + + +def read(stream: BinaryIO, + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: + """ + Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are + translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs + are translated into SubPattern objects. + + Additional library info is returned in a dict, containing: + 'name': name of the library + 'meters_per_unit': number of meters per database unit (all values are in database units) + 'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns) + per database unit + + Args: + stream: Stream to read from. + + Returns: + - Dict of pattern_name:Patterns generated from GDSII structures + - Dict of GDSII library info + """ + raw_mode = True # Whether to construct shapes in raw mode (less error checking) + library_info = _read_header(stream) + + patterns = [] + 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')) + patterns.append(pat) + found_struct = records.BGNSTR.skip_past(stream) + + # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries + # according to the subpattern.identifier (which is deleted after use). + patterns_dict = dict(((p.name, p) for p in patterns)) + for p in patterns_dict.values(): + for sp in p.subpatterns: + sp.pattern = patterns_dict[sp.identifier[0]] + del sp.identifier + + return patterns_dict, library_info + + +def _read_header(stream: BinaryIO) -> Dict[str, Any]: + """ + Read the file header and create the library_info dict. + """ + header = klamath.library.FileHeader.read(stream) + + library_info = {'name': header.name.decode('ASCII'), + 'meters_per_unit': header.meters_per_db_unit, + 'logical_units_per_unit': header.user_units_per_db_unit, + } + return library_info + + +def read_elements(stream: BinaryIO, + name: str, + raw_mode: bool = True, + ) -> Pattern: + """ + Read elements from a GDS structure and build a Pattern from them. + + Args: + stream: Seekable stream, positioned at a record boundary. + Will be read until an ENDSTR record is consumed. + name: Name of the resulting Pattern + raw_mode: If True, bypass per-shape consistency checking + + Returns: + A pattern containing the elements that were read. + """ + pat = Pattern(name) + + elements = klamath.library.read_elements(stream) + for element in elements: + if isinstance(element, klamath.elements.Boundary): + poly = _boundary_to_polygon(element, raw_mode) + pat.shapes.append(poly) + elif isinstance(element, klamath.elements.Path): + path = _gpath_to_mpath(element, raw_mode) + pat.shapes.append(path) + elif isinstance(element, klamath.elements.Text): + label = Label(offset=element.xy.astype(float), + layer=element.layer, + string=element.string.decode('ASCII'), + annotations=_properties_to_annotations(element.properties)) + pat.labels.append(label) + elif isinstance(element, klamath.elements.Reference): + pat.subpatterns.append(_ref_to_subpat(element)) + return pat + + +def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]: + """ Helper to turn a layer tuple-or-int into a layer and datatype""" + if isinstance(mlayer, int): + layer = mlayer + data_type = 0 + elif isinstance(mlayer, tuple): + layer = mlayer[0] + if len(mlayer) > 1: + data_type = mlayer[1] + else: + data_type = 0 + else: + raise PatternError(f'Invalid layer for gdsii: {mlayer}. Note that gdsii layers cannot be strings.') + return layer, data_type + + +def _ref_to_subpat(ref: klamath.library.Reference, + ) -> SubPattern: + """ + Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None + and sets the instance .identifier to (struct_name,). + """ + xy = ref.xy.astype(float) + offset = xy[0] + repetition = None + if ref.colrow is not None: + a_count, b_count = ref.colrow + a_vector = (xy[1] - offset) / a_count + b_vector = (xy[2] - offset) / b_count + repetition = Grid(a_vector=a_vector, b_vector=b_vector, + a_count=a_count, b_count=b_count) + + subpat = SubPattern(pattern=None, + offset=offset, + rotation=numpy.deg2rad(ref.angle_deg), + scale=ref.mag, + mirrored=(ref.invert_y, False), + annotations=_properties_to_annotations(ref.properties), + repetition=repetition) + subpat.identifier = (ref.struct_name.decode('ASCII'),) + return subpat + + +def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path: + if gpath.path_type in path_cap_map: + cap = path_cap_map[gpath.path_type] + else: + raise PatternError(f'Unrecognized path type: {gpath.path_type}') + + mpath = Path(vertices=gpath.xy.astype(float), + layer=gpath.layer, + width=gpath.width, + cap=cap, + offset=numpy.zeros(2), + annotations=_properties_to_annotations(gpath.properties), + raw=raw_mode, + ) + if cap == Path.Cap.SquareCustom: + mpath.cap_extensions = gpath.extension + return mpath + + +def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon: + return Polygon(vertices=boundary.xy[:-1].astype(float), + layer=boundary.layer, + offset=numpy.zeros(2), + annotations=_properties_to_annotations(boundary.properties), + raw=raw_mode, + ) + + +def _subpatterns_to_refs(subpatterns: List[SubPattern] + ) -> List[klamath.library.Reference]: + refs = [] + for subpat in subpatterns: + if subpat.pattern is None: + continue + encoded_name = subpat.pattern.name.encode('ASCII') + + # Note: GDS mirrors first and rotates second + mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) + rep = subpat.repetition + angle_deg = numpy.rad2deg(subpat.rotation + extra_angle) % 360 + properties = _annotations_to_properties(subpat.annotations, 512) + + if isinstance(rep, Grid): + xy = numpy.array(subpat.offset) + [ + [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)), + angle_deg=angle_deg, + invert_y=mirror_across_x, + mag=subpat.scale, + properties=properties) + refs.append(aref) + elif rep is None: + ref = klamath.library.Reference(struct_name=encoded_name, + xy=numpy.round([subpat.offset]).astype(int), + colrow=None, + angle_deg=angle_deg, + invert_y=mirror_across_x, + mag=subpat.scale, + properties=properties) + refs.append(ref) + else: + new_srefs = [klamath.library.Reference(struct_name=encoded_name, + xy=numpy.round([subpat.offset + dd]).astype(int), + colrow=None, + angle_deg=angle_deg, + invert_y=mirror_across_x, + mag=subpat.scale, + properties=properties) + for dd in rep.displacements] + refs += new_srefs + return refs + + +def _properties_to_annotations(properties: Dict[int, bytes]) -> annotations_t: + return {str(k): [v.decode()] for k, v in properties.items()} + + +def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> Dict[int, bytes]: + cum_len = 0 + props = {} + for key, vals in annotations.items(): + try: + i = int(key) + except: + raise PatternError(f'Annotation key {key} is not convertable to an integer') + if not (0 < i < 126): + raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') + + val_strings = ' '.join(str(val) for val in vals) + b = val_strings.encode() + if len(b) > 126: + raise PatternError(f'Annotation value {b!r} is longer than 126 characters!') + cum_len += numpy.ceil(len(b) / 2) * 2 + 2 + if cum_len > max_len: + raise PatternError(f'Sum of annotation data will be longer than {max_len} bytes! Generated bytes were {b!r}') + props[i] = b + return props + + +def _shapes_to_elements(shapes: List[Shape], + polygonize_paths: bool = False + ) -> List[klamath.elements.Element]: + elements: List[klamath.elements.Element] = [] + # Add a Boundary element for each shape, and Path elements if necessary + for shape in shapes: + layer, data_type = _mlayer2gds(shape.layer) + properties = _annotations_to_properties(shape.annotations, 128) + if isinstance(shape, Path) and not polygonize_paths: + xy = numpy.round(shape.vertices + shape.offset).astype(int) + width = numpy.round(shape.width).astype(int) + 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: + extension = tuple(shape.cap_extensions) # type: ignore + else: + extension = (0, 0) + + path = klamath.elements.Path(layer=(layer, data_type), + xy=xy, + path_type=path_type, + width=width, + extension=extension, + properties=properties) + elements.append(path) + else: + for polygon in shape.to_polygons(): + xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) + xy_closed = numpy.vstack((xy_open, xy_open[0, :])) + boundary = klamath.elements.Boundary(layer=(layer, data_type), + xy=xy_closed, + properties=properties) + elements.append(boundary) + return elements + + +def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]: + texts = [] + for label in labels: + properties = _annotations_to_properties(label.annotations, 128) + layer, text_type = _mlayer2gds(label.layer) + xy = numpy.round([label.offset]).astype(int) + text = klamath.elements.Text(layer=(layer, text_type), + xy=xy, + string=label.string.encode('ASCII'), + properties=properties, + presentation=0, #TODO maybe set some of these? + angle_deg=0, + invert_y=False, + width=0, + path_type=0, + mag=1) + texts.append(text) + return texts + + +def disambiguate_pattern_names(patterns: Sequence[Pattern], + max_name_length: int = 32, + suffix_length: int = 6, + dup_warn_filter: Optional[Callable[[str,], bool]] = None, + ): + """ + Args: + patterns: List of patterns to disambiguate + max_name_length: Names longer than this will be truncated + suffix_length: Names which get truncated are truncated by this many extra characters. This is to + leave room for a suffix if one is necessary. + dup_warn_filter: (optional) Function for suppressing warnings about cell names changing. Receives + the cell name and returns `False` if the warning should be suppressed and `True` if it should + be displayed. Default displays all warnings. + """ + used_names = [] + for pat in set(patterns): + # 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') + else: + shortened_name = pat.name + + # Remove invalid characters + sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', shortened_name) + + # Add a suffix that makes the name unique + i = 0 + suffixed_name = sanitized_name + while suffixed_name in used_names or suffixed_name == '': + suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII') + + suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') + i += 1 + + if sanitized_name == '': + 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}"') + + # Encode into a byte-string and perform some final checks + encoded_name = suffixed_name.encode('ASCII') + if len(encoded_name) == 0: + # 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}"') + + pat.name = suffixed_name + used_names.append(suffixed_name) From aa5696d884db3a08ab056c3dbf972651e607e85f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:37:23 -0700 Subject: [PATCH 54/71] Add Library management functionality --- masque/__init__.py | 1 + masque/error.py | 9 ++ masque/file/klamath.py | 45 +++++++ masque/library/__init__.py | 1 + masque/library/library.py | 267 +++++++++++++++++++++++++++++++++++++ masque/library/utils.py | 48 +++++++ 6 files changed, 371 insertions(+) create mode 100644 masque/library/__init__.py create mode 100644 masque/library/library.py create mode 100644 masque/library/utils.py diff --git a/masque/__init__.py b/masque/__init__.py index 0a13faa..87ceb5d 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -25,6 +25,7 @@ from .label import Label from .subpattern import SubPattern from .pattern import Pattern from .utils import layer_t, annotations_t +from .library import Library __author__ = 'Jan Petykiewicz' diff --git a/masque/error.py b/masque/error.py index 4a5c21a..e109c20 100644 --- a/masque/error.py +++ b/masque/error.py @@ -15,3 +15,12 @@ class PatternLockedError(PatternError): """ def __init__(self): PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape') + + +class LibraryError(Exception): + """ + Exception raised by Library classes + """ + pass + + diff --git a/masque/file/klamath.py b/masque/file/klamath.py index 4440457..192ce9f 100644 --- a/masque/file/klamath.py +++ b/masque/file/klamath.py @@ -539,3 +539,48 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern], pat.name = suffixed_name used_names.append(suffixed_name) + + +def load_library(stream: BinaryIO, + tag: str, + is_secondary: Optional[Callable[[str], bool]] = None, + ) -> Tuple[Library, Dict[str, Any]]: + """ + Scan a GDSII file to determine what structures are present, and create + a library from them. This enables deferred reading of structures + on an as-needed basis. + All structures are loaded as secondary + + Args: + stream: Seekable stream. Position 0 should be the start of the file. + The caller should leave the stream open while the library + is still in use, since the library will need to access it + in order to read the structure contents. + tag: Unique identifier that will be used to identify this data source + is_secondary: Function which takes a structure name and returns + True if the structure should only be used as a subcell + and not appear in the main Library interface. + Default always returns False. + + Returns: + Library object, allowing for deferred load of structures. + Additional library info (dict, same format as from `read`). + """ + if is_secondary is None: + is_secondary = lambda k: False + + stream.seek(0) + library_info = _read_header(stream) + structs = klamath.library.scan_structs(stream) + + lib = Library() + for name_bytes, pos in structs.items(): + name = name_bytes.decode('ASCII') + + def mkstruct(pos: int = pos, name: str = name) -> Pattern: + stream.seek(pos) + return read_elements(stream, name, raw_mode=True) + + lib.set_value(name, tag, mkstruct, secondary=is_secondary(name)) + + return lib diff --git a/masque/library/__init__.py b/masque/library/__init__.py new file mode 100644 index 0000000..a72f3b9 --- /dev/null +++ b/masque/library/__init__.py @@ -0,0 +1 @@ +from .library import Library, PatternGenerator diff --git a/masque/library/library.py b/masque/library/library.py new file mode 100644 index 0000000..8e5ec11 --- /dev/null +++ b/masque/library/library.py @@ -0,0 +1,267 @@ +""" +Library class for managing unique name->pattern mappings and + deferred loading or creation. +""" +from typing import Dict, Callable, TypeVar, Generic, TYPE_CHECKING, Any, Tuple, Union +import logging +from pprint import pformat +from dataclasses import dataclass +from functools import lru_cache + +from ..error import LibraryError + +if TYPE_CHECKING: + from ..pattern import Pattern + + +logger = logging.getLogger(__name__) + + +@dataclass +class PatternGenerator: + __slots__ = ('tag', 'gen') + tag: str + """ Unique identifier for the source """ + + gen: Callable[[], 'Pattern'] + """ Function which generates a pattern when called """ + + +L = TypeVar('L', bound='Library') + + +class Library: + """ + This class is usually used to create a device library by mapping names to + functions which generate or load the relevant `Pattern` object as-needed. + + Generated/loaded patterns can have "symbolic" references, where a SubPattern + object `sp` has a `None`-valued `sp.pattern` attribute, in which case the + Library expects `sp.identifier[0]` to contain a string which specifies the + referenced pattern's name. + + Patterns can either be "primary" (default) or "secondary". Both get the + same deferred-load behavior, but "secondary" patterns may have conflicting + names and are not accessible through basic []-indexing. They are only used + to fill symbolic references in cases where there is no "primary" pattern + available, and only if both the referencing and referenced pattern-generators' + `tag` values match (i.e., only if they came from the same source). + + Primary patterns can be turned into secondary patterns with the `demote` + method, `promote` performs the reverse (secondary -> primary) operation. + + The `set_const` and `set_value` methods provide an easy way to transparently + construct PatternGenerator objects and directly set create "secondary" + patterns. + + The cache can be disabled by setting the `enable_cache` attribute to `False`. + """ + primary: Dict[str, PatternGenerator] + secondary: Dict[Tuple[str, str], PatternGenerator] + cache: Dict[Union[str, Tuple[str, str]], 'Pattern'] + enable_cache: bool = True + + def __init__(self) -> None: + self.primary = {} + self.secondary = {} + self.cache = {} + + def __setitem__(self, key: str, value: PatternGenerator) -> None: + self.primary[key] = value + if key in self.cache: + del self.cache[key] + + def __delitem__(self, key: str) -> None: + if isinstance(key, str): + del self.primary[key] + elif isinstance(key, tuple): + del self.secondary[key] + + if key in self.cache: + del self.cache[key] + + def __getitem__(self, key: str) -> 'Pattern': + if self.enable_cache and key in self.cache: + logger.debug(f'found {key} in cache') + return self.cache[key] + + logger.debug(f'loading {key}') + pg = self.primary[key] + pat = pg.gen() + self.resolve_subpatterns(pat, pg.tag) + self.cache[key] = pat + return pat + + def get_primary(self, key: str) -> 'Pattern': + return self[key] + + def get_secondary(self, key: str, tag: str) -> 'Pattern': + logger.debug(f'get_secondary({key}, {tag})') + key2 = (key, tag) + if self.enable_cache and key2 in self.cache: + return self.cache[key2] + + pg = self.secondary[key2] + pat = pg.gen() + self.resolve_subpatterns(pat, pg.tag) + self.cache[key2] = pat + return pat + + def resolve_subpatterns(self, pat: 'Pattern', tag: str) -> 'Pattern': + logger.debug(f'Resolving subpatterns in {pat.name}') + for sp in pat.subpatterns: + if sp.pattern is not None: + continue + + key = sp.identifier[0] + if key in self.primary: + sp.pattern = self[key] + continue + + if (key, tag) in self.secondary: + sp.pattern = self.get_secondary(key, tag) + continue + + raise LibraryError(f'Broken reference to {key} (tag {tag})') + return pat + + def __repr__(self) -> str: + return '<Library with keys ' + repr(list(self.primary.keys())) + '>' + + def set_const(self, key: str, tag: Any, const: 'Pattern', secondary: bool = False) -> None: + """ + Convenience function to avoid having to manually wrap + constant values into callables. + + Args: + key: Lookup key, usually the cell/pattern name + tag: Unique tag for the source, used to disambiguate secondary patterns + const: Pattern object to return + secondary: If True, this pattern is not accessible for normal lookup, and is + only used as a sub-component of other patterns if no non-secondary + equivalent is available. + """ + pg = PatternGenerator(tag=tag, gen=lambda: const) + if secondary: + self.secondary[(key, tag)] = pg + else: + self.primary[key] = pg + + def set_value(self, key: str, tag: str, value: Callable[[], 'Pattern'], secondary: bool = False) -> None: + """ + Convenience function to automatically build a PatternGenerator. + + Args: + key: Lookup key, usually the cell/pattern name + tag: Unique tag for the source, used to disambiguate secondary patterns + value: Callable which takes no arguments and generates the `Pattern` object + secondary: If True, this pattern is not accessible for normal lookup, and is + only used as a sub-component of other patterns if no non-secondary + equivalent is available. + """ + pg = PatternGenerator(tag=tag, gen=value) + if secondary: + self.secondary[(key, tag)] = pg + else: + self.primary[key] = pg + + def precache(self) -> 'Library': + """ + Force all patterns into the cache + + Returns: + self + """ + for key in self.primary: + _ = self.get_primary(key) + for key2 in self.secondary: + _ = self.get_secondary(key2) + return self + + def add(self, other: 'Library') -> 'Library': + """ + Add keys from another library into this one. + + There must be no conflicting keys. + + Args: + other: The library to insert keys from + + Returns: + self + """ + conflicts = [key for key in other.primary + if key in self.primary] + if conflicts: + raise LibraryError('Duplicate keys encountered in library merge: ' + pformat(conflicts)) + + conflicts2 = [key2 for key2 in other.secondary + if key2 in self.secondary] + if conflicts2: + raise LibraryError('Duplicate secondary keys encountered in library merge: ' + pformat(conflicts2)) + + self.primary.update(other.primary) + self.secondary.update(other.secondary) + self.cache.update(other.cache) + return self + + def demote(self, key: str) -> None: + """ + Turn a primary pattern into a secondary one. + It will no longer be accessible through [] indexing and will only be used to + when referenced by other patterns from the same source, and only if no primary + pattern with the same name exists. + + Args: + key: Lookup key, usually the cell/pattern name + """ + pg = self.primary[key] + key2 = (key, pg.tag) + self.secondary[key2] = pg + if key in self.cache: + self.cache[key2] = self.cache[key] + del self[key] + + def promote(self, key: str, tag: str) -> None: + """ + Turn a secondary pattern into a primary one. + It will become accessible through [] indexing and will be used to satisfy any + reference to a pattern with its key, regardless of tag. + + Args: + key: Lookup key, usually the cell/pattern name + tag: Unique tag for identifying the pattern's source, used to disambiguate + secondary patterns + """ + if key in self.primary: + raise LibraryError(f'Promoting ({key}, {tag}), but {key} already exists in primary!') + + key2 = (key, tag) + pg = self.secondary[key2] + self.primary[key] = pg + if key2 in self.cache: + self.cache[key] = self.cache[key2] + del self.secondary[key2] + del self.cache[key2] + + +r""" + # Add a filter for names which aren't added + + - Registration: + - scanned files (tag=filename, gen_fn[stream, {name: pos}]) + - generator functions (tag='fn?', gen_fn[params]) + - merge decision function (based on tag and cell name, can be "neither") ??? neither=keep both, load using same tag! + - Load process: + - file: + - read single cell + - check subpat identifiers, and load stuff recursively based on those. If not present, load from same file?? + - function: + - generate cell + - traverse and check if we should load any subcells from elsewhere. replace if so. + * should fn generate subcells at all, or register those separately and have us control flow? maybe ask us and generate itself if not present? + + - Scan all GDS files, save name -> (file, position). Keep the streams handy. + - Merge all names. This requires subcell merge because we don't know hierarchy. + - possibly include a "neither" option during merge, to deal with subcells. Means: just use parent's file. +""" diff --git a/masque/library/utils.py b/masque/library/utils.py new file mode 100644 index 0000000..020ac3f --- /dev/null +++ b/masque/library/utils.py @@ -0,0 +1,48 @@ +from typing import Callable, TypeVar, Generic +from functools import lru_cache + + +Key = TypeVar('Key') +Value = TypeVar('Value') + + +class DeferredDict(dict, Generic[Key, Value]): + """ + This is a modified `dict` which is used to defer loading/generating + values until they are accessed. + + ``` + bignum = my_slow_function() # slow function call, would like to defer this + numbers = Library() + numbers['big'] = my_slow_function # no slow function call here + assert(bignum == numbers['big']) # first access is slow (function called) + assert(bignum == numbers['big']) # second access is fast (result is cached) + ``` + + The `set_const` method is provided for convenience; + `numbers['a'] = lambda: 10` is equivalent to `numbers.set_const('a', 10)`. + """ + def __init__(self, *args, **kwargs) -> None: + dict.__init__(self) + self.update(*args, **kwargs) + + def __setitem__(self, key: Key, value: Callable[[], Value]) -> None: + cached_fn = lru_cache(maxsize=1)(value) + dict.__setitem__(self, key, cached_fn) + + def __getitem__(self, key: Key) -> Value: + return dict.__getitem__(self, key)() + + def update(self, *args, **kwargs) -> None: + for k, v in dict(*args, **kwargs).items(): + self[k] = v + + def __repr__(self) -> str: + return '<Library with keys ' + repr(set(self.keys())) + '>' + + def set_const(self, key: Key, value: Value) -> None: + """ + Convenience function to avoid having to manually wrap + constant values into callables. + """ + self[key] = lambda: value From b8ef80b9914dd3b81b598bdd73ae1198ef608c60 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:40:52 -0700 Subject: [PATCH 55/71] add klamath dependency --- README.md | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 214f7ad..d524665 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Requirements: * numpy * matplotlib (optional, used for `visualization` functions and `text`) * python-gdsii (optional, used for `gdsii` i/o) +* klamath (optional, used for `gdsii` i/o and library management) * ezdxf (optional, used for `dxf` i/o) * fatamorgana (optional, used for `oasis` i/o) * svgwrite (optional, used for `svg` output) @@ -25,7 +26,7 @@ Requirements: Install with pip: ```bash -pip3 install 'masque[visualization,gdsii,oasis,dxf,svg,text]' +pip3 install 'masque[visualization,gdsii,oasis,dxf,svg,text,klamath]' ``` Alternatively, install from git diff --git a/setup.py b/setup.py index 1d3138b..ff8aad9 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ setup(name='masque', ], extras_require={ 'gdsii': ['python-gdsii'], + 'klamath': ['klamath'], 'oasis': ['fatamorgana>=0.7'], 'dxf': ['ezdxf'], 'svg': ['svgwrite'], From 91dcc4f04f12667074747247a4b8f65fdf685820 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:41:00 -0700 Subject: [PATCH 56/71] doc fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d524665..69e7a7e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ with some vectorized element types (eg. circles, not just polygons), better supp E-beam doses, and the ability to output to multiple formats. - [Source repository](https://mpxd.net/code/jan/masque) -- [PyPi](https://pypi.org/project/masque) +- [PyPI](https://pypi.org/project/masque) ## Installation From 682a99470f01adfbfd68926d0445d19fcad0ec7d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 26 Sep 2020 17:41:08 -0700 Subject: [PATCH 57/71] Bump version to 2.1 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index cd5ac03..879b416 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -2.0 +2.1 From b873a5ddf3701f58cef23aa29e384e0778dc7341 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Mon, 28 Sep 2020 23:49:33 -0700 Subject: [PATCH 58/71] make __getitem__ call get_primary rather than the other way around this makes subclassing easier --- masque/library/library.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/masque/library/library.py b/masque/library/library.py index 8e5ec11..f2151d2 100644 --- a/masque/library/library.py +++ b/masque/library/library.py @@ -2,7 +2,8 @@ Library class for managing unique name->pattern mappings and deferred loading or creation. """ -from typing import Dict, Callable, TypeVar, Generic, TYPE_CHECKING, Any, Tuple, Union +from typing import Dict, Callable, TypeVar, Generic, TYPE_CHECKING +from typing import Any, Tuple, Union, Iterator import logging from pprint import pformat from dataclasses import dataclass @@ -81,6 +82,10 @@ class Library: del self.cache[key] def __getitem__(self, key: str) -> 'Pattern': + return self.get_primary(key) + + + def get_primary(self, key: str) -> 'Pattern': if self.enable_cache and key in self.cache: logger.debug(f'found {key} in cache') return self.cache[key] @@ -92,9 +97,6 @@ class Library: self.cache[key] = pat return pat - def get_primary(self, key: str) -> 'Pattern': - return self[key] - def get_secondary(self, key: str, tag: str) -> 'Pattern': logger.debug(f'get_secondary({key}, {tag})') key2 = (key, tag) @@ -115,7 +117,7 @@ class Library: key = sp.identifier[0] if key in self.primary: - sp.pattern = self[key] + sp.pattern = self.get_primary(key) continue if (key, tag) in self.secondary: From 03a359e446fabf657352d2166b26250cda12b6b6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Mon, 28 Sep 2020 23:49:42 -0700 Subject: [PATCH 59/71] add more dict-like methods --- masque/library/library.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/masque/library/library.py b/masque/library/library.py index f2151d2..7fc5a86 100644 --- a/masque/library/library.py +++ b/masque/library/library.py @@ -84,6 +84,11 @@ class Library: def __getitem__(self, key: str) -> 'Pattern': return self.get_primary(key) + def __iter__(self) -> Iterator[str]: + return self.keys() + + def __contains__(self, key: str) -> bool: + return key in self.primary def get_primary(self, key: str) -> 'Pattern': if self.enable_cache and key in self.cache: @@ -127,6 +132,15 @@ class Library: raise LibraryError(f'Broken reference to {key} (tag {tag})') return pat + def keys(self) -> Iterator[str]: + return self.primary.keys() + + def values(self) -> Iterator['Pattern']: + return (self[key] for key in self.keys()) + + def items(self) -> Iterator[Tuple[str, 'Pattern']]: + return ((key, self[key]) for key in self.keys()) + def __repr__(self) -> str: return '<Library with keys ' + repr(list(self.primary.keys())) + '>' From ce5d386a244e43e99749b7c85c56c0335407b9de Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 29 Sep 2020 00:57:26 -0700 Subject: [PATCH 60/71] Determine if an existing file is gzipped based on magic bytes, not suffix --- masque/file/gdsii.py | 5 +++-- masque/file/klamath.py | 6 +++--- masque/file/oasis.py | 6 +++--- masque/file/utils.py | 8 ++++++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 3d97fe3..68b69a9 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -35,6 +35,7 @@ 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 .. import Pattern, SubPattern, PatternError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid @@ -190,7 +191,7 @@ def readfile(filename: Union[str, pathlib.Path], """ Wrapper for `masque.file.gdsii.read()` that takes a filename or path instead of a stream. - Will automatically decompress files with a .gz suffix. + Will automatically decompress gzipped files. Args: filename: Filename to save to. @@ -198,7 +199,7 @@ def readfile(filename: Union[str, pathlib.Path], **kwargs: passed to `masque.file.gdsii.read` """ path = pathlib.Path(filename) - if path.suffix == '.gz': + if is_gzipped(path): open_func: Callable = gzip.open else: open_func = open diff --git a/masque/file/klamath.py b/masque/file/klamath.py index 192ce9f..ba89638 100644 --- a/masque/file/klamath.py +++ b/masque/file/klamath.py @@ -34,7 +34,7 @@ import numpy # type: ignore import klamath from klamath import records -from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose +from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose, is_gzipped from .. import Pattern, SubPattern, PatternError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid @@ -170,7 +170,7 @@ def readfile(filename: Union[str, pathlib.Path], """ Wrapper for `masque.file.gdsii.read()` that takes a filename or path instead of a stream. - Will automatically decompress files with a .gz suffix. + Will automatically decompress gzipped files. Args: filename: Filename to save to. @@ -178,7 +178,7 @@ def readfile(filename: Union[str, pathlib.Path], **kwargs: passed to `masque.file.gdsii.read` """ path = pathlib.Path(filename) - if path.suffix == '.gz': + if is_gzipped(path): open_func: Callable = gzip.open else: open_func = open diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 8d9e074..5e20121 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -27,7 +27,7 @@ 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 +from .utils import mangle_name, make_dose_table, clean_pattern_vertices, is_gzipped from .. import Pattern, SubPattern, PatternError, Label, Shape from ..shapes import Polygon, Path, Circle from ..repetition import Grid, Arbitrary, Repetition @@ -207,7 +207,7 @@ def readfile(filename: Union[str, pathlib.Path], """ Wrapper for `oasis.read()` that takes a filename or path instead of a stream. - Will automatically decompress files with a .gz suffix. + Will automatically decompress gzipped files. Args: filename: Filename to save to. @@ -215,7 +215,7 @@ def readfile(filename: Union[str, pathlib.Path], **kwargs: passed to `oasis.read` """ path = pathlib.Path(filename) - if path.suffix == '.gz': + if is_gzipped(path): open_func: Callable = gzip.open else: open_func = open diff --git a/masque/file/utils.py b/masque/file/utils.py index 30b2808..6239a1a 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -4,6 +4,8 @@ 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 @@ -176,3 +178,9 @@ def dose2dtype(patterns: List[Pattern], subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)] return patterns, dose_vals_list + + +def is_gzipped(path: pathlib.Path) -> bool: + with open(path, 'rb') as stream: + magic_bytes = stream.read(2) + return magic_bytes == b'\x1f\x8b' From de4726955b4ebd57b5ea9573c2f687132097254b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 29 Sep 2020 01:00:37 -0700 Subject: [PATCH 61/71] add load_libraryfile convenience wrapper --- masque/file/klamath.py | 47 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/masque/file/klamath.py b/masque/file/klamath.py index ba89638..97fe7e0 100644 --- a/masque/file/klamath.py +++ b/masque/file/klamath.py @@ -22,6 +22,7 @@ from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, from typing import Sequence, Mapping, BinaryIO import re import io +import mmap import copy import base64 import struct @@ -546,7 +547,7 @@ def load_library(stream: BinaryIO, is_secondary: Optional[Callable[[str], bool]] = None, ) -> Tuple[Library, Dict[str, Any]]: """ - Scan a GDSII file to determine what structures are present, and create + Scan a GDSII stream to determine what structures are present, and create a library from them. This enables deferred reading of structures on an as-needed basis. All structures are loaded as secondary @@ -584,3 +585,47 @@ def load_library(stream: BinaryIO, lib.set_value(name, tag, mkstruct, secondary=is_secondary(name)) return lib + + +def load_libraryfile(filename: Union[str, pathlib.Path], + tag: str, + is_secondary: Optional[Callable[[str], bool]] = None, + use_mmap: bool = True, + ) -> Tuple[Library, Dict[str, Any]]: + """ + Wrapper for `load_library()` that takes a filename or path instead of a stream. + + Will automatically decompress the file if it is gzipped. + + NOTE that any streams/mmaps opened will remain open until ALL of the + `PatternGenerator` objects in the library are garbage collected. + + Args: + path: filename or path to read from + tag: Unique identifier for library, see `load_library` + is_secondary: Function specifying subcess, see `load_library` + use_mmap: If `True`, will attempt to memory-map the file instead + of buffering. In the case of gzipped files, the file + is decompressed into a python `bytes` object in memory + and reopened as an `io.BytesIO` stream. + + Returns: + Library object, allowing for deferred load of structures. + Additional library info (dict, same format as from `read`). + """ + path = pathlib.Path(filename) + if is_gzipped(path): + if mmap: + logger.info('Asked to mmap a gzipped file, reading into memory instead...') + base_stream = gzip.open(path, mode='rb') + stream = io.BytesIO(base_stream.read()) + else: + base_stream = gzip.open(path, mode='rb') + stream = io.BufferedReader(base_stream) + else: + base_stream = open(path, mode='rb') + if mmap: + stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) + else: + stream = io.BufferedReader(base_stream) + return load_library(stream, tag, is_secondary) From 08cf7ca4b137f036f4cbc4109cffb55683b003ec Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Tue, 29 Sep 2020 01:01:10 -0700 Subject: [PATCH 62/71] avoid calling to_polygons on Polygons (for speed) --- masque/file/klamath.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/masque/file/klamath.py b/masque/file/klamath.py index 97fe7e0..c70f704 100644 --- a/masque/file/klamath.py +++ b/masque/file/klamath.py @@ -454,6 +454,14 @@ def _shapes_to_elements(shapes: List[Shape], extension=extension, 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) else: for polygon in shape.to_polygons(): xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) From c23c391d830dd14961e5d851e1c690eae8753f87 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 10 Oct 2020 19:10:17 -0700 Subject: [PATCH 63/71] disable locking for annotations until I can find a better way to do it --- masque/traits/annotatable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/traits/annotatable.py b/masque/traits/annotatable.py index 0285cfd..e818b3b 100644 --- a/masque/traits/annotatable.py +++ b/masque/traits/annotatable.py @@ -44,9 +44,9 @@ class AnnotatableImpl(Annotatable, metaclass=ABCMeta): ''' @property def annotations(self) -> annotations_t: - # TODO: Find a way to make sure the subclass implements Lockable without dealing with diamond inheritance or this extra hasattr - if hasattr(self, 'is_locked') and self.is_locked(): - return MappingProxyType(self._annotations) +# # 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 From ae71dc9a8f578989b99d1fde8289294c01803666 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 10 Oct 2020 19:10:35 -0700 Subject: [PATCH 64/71] use klamath for examples --- examples/ellip_grating.py | 4 ++-- examples/test_rep.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/ellip_grating.py b/examples/ellip_grating.py index 948df42..0c34cce 100644 --- a/examples/ellip_grating.py +++ b/examples/ellip_grating.py @@ -3,7 +3,7 @@ import numpy import masque -import masque.file.gdsii +import masque.file.klamath from masque import shapes @@ -24,7 +24,7 @@ def main(): pat2 = pat.copy() pat2.name = 'grating2' - masque.file.gdsii.writefile((pat, pat2), 'out.gds.gz', 1e-9, 1e-3) + masque.file.klamath.writefile((pat, pat2), 'out.gds.gz', 1e-9, 1e-3) if __name__ == '__main__': diff --git a/examples/test_rep.py b/examples/test_rep.py index 22e4b65..042e1af 100644 --- a/examples/test_rep.py +++ b/examples/test_rep.py @@ -3,6 +3,7 @@ from numpy import pi import masque import masque.file.gdsii +import masque.file.klamath import masque.file.dxf import masque.file.oasis from masque import shapes, Pattern, SubPattern @@ -83,10 +84,10 @@ def main(): ] folder = 'layouts/' - masque.file.gdsii.writefile((pat, pat2, pat3, pat4), folder + 'rep.gds.gz', 1e-9, 1e-3) + masque.file.klamath.writefile((pat, pat2, pat3, pat4), folder + 'rep.gds.gz', 1e-9, 1e-3) - cells = list(masque.file.gdsii.readfile(folder + 'rep.gds.gz')[0].values()) - masque.file.gdsii.writefile(cells, folder + 'rerep.gds.gz', 1e-9, 1e-3) + cells = list(masque.file.klamath.readfile(folder + 'rep.gds.gz')[0].values()) + masque.file.klamath.writefile(cells, folder + 'rerep.gds.gz', 1e-9, 1e-3) masque.file.dxf.writefile(pat4, folder + 'rep.dxf.gz') dxf, info = masque.file.dxf.readfile(folder + 'rep.dxf.gz') From 4a7e20d6baf9ed5cd17b3f5441c0d091dc9f1f3b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 10 Oct 2020 19:11:22 -0700 Subject: [PATCH 65/71] improve type annotations in dxf writer --- masque/file/dxf.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/masque/file/dxf.py b/masque/file/dxf.py index a5e7bc1..4429466 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -37,7 +37,8 @@ def write(pattern: Pattern, *, modify_originals: bool = False, dxf_version='AC1024', - disambiguate_func: Callable[[Iterable[Pattern]], None] = None): + disambiguate_func: Callable[[Iterable[Pattern]], None] = None, + ) -> None: """ Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes into polygons, and then writing patterns as DXF `Block`s, polygons as `LWPolyline`s, @@ -105,7 +106,7 @@ def writefile(pattern: Pattern, filename: Union[str, pathlib.Path], *args, **kwargs, - ): + ) -> None: """ Wrapper for `dxf.write()` that takes a filename or path instead of a stream. @@ -131,7 +132,7 @@ def writefile(pattern: Pattern, def readfile(filename: Union[str, pathlib.Path], *args, **kwargs, - ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: + ) -> Tuple[Pattern, Dict[str, Any]]: """ Wrapper for `dxf.read()` that takes a filename or path instead of a stream. @@ -155,7 +156,7 @@ def readfile(filename: Union[str, pathlib.Path], def read(stream: io.TextIOBase, clean_vertices: bool = True, - ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: + ) -> Tuple[Pattern, Dict[str, Any]]: """ Read a dxf file and translate it into a dict of `Pattern` objects. DXF `Block`s are translated into `Pattern` objects; `LWPolyline`s are translated into polygons, and `Insert`s @@ -193,7 +194,7 @@ def read(stream: io.TextIOBase, return pat, library_info -def _read_block(block, clean_vertices): +def _read_block(block, clean_vertices: bool) -> Pattern: pat = Pattern(block.name) for element in block: eltype = element.dxftype() @@ -277,7 +278,7 @@ def _read_block(block, clean_vertices): def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], - subpatterns: List[SubPattern]): + subpatterns: List[SubPattern]) -> None: for subpat in subpatterns: if subpat.pattern is None: continue @@ -335,7 +336,7 @@ def _shapes_to_elements(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Mo def _labels_to_texts(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], - labels: List[Label]): + labels: List[Label]) -> None: for label in labels: attribs = {'layer': _mlayer2dxf(label.layer)} xy = label.offset @@ -352,11 +353,11 @@ def _mlayer2dxf(layer: layer_t) -> str: raise PatternError(f'Unknown layer type: {layer} ({type(layer)})') -def disambiguate_pattern_names(patterns, +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 - ): + ) -> None: used_names = [] for pat in patterns: sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name) From 0f35eb5e588e1aa171fa2d2cdcab42641d14cc7d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 10 Oct 2020 19:11:35 -0700 Subject: [PATCH 66/71] fix dxf reader --- masque/file/dxf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 4429466..95814ef 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -200,9 +200,9 @@ def _read_block(block, clean_vertices: bool) -> Pattern: eltype = element.dxftype() if eltype in ('POLYLINE', 'LWPOLYLINE'): if eltype == 'LWPOLYLINE': - points = numpy.array(element.lwpoints) + points = numpy.array(tuple(element.lwpoints())) else: - points = numpy.array(element.points) + points = numpy.array(tuple(element.points())) attr = element.dxfattribs() args = {'layer': attr.get('layer', DEFAULT_LAYER), } From 5bc82b9d4917bc6dccc82a5ad624241139488907 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 10 Oct 2020 19:11:52 -0700 Subject: [PATCH 67/71] __iter__ should actually return an iterator --- masque/library/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/library/library.py b/masque/library/library.py index 7fc5a86..66d9f17 100644 --- a/masque/library/library.py +++ b/masque/library/library.py @@ -85,7 +85,7 @@ class Library: return self.get_primary(key) def __iter__(self) -> Iterator[str]: - return self.keys() + return iter(self.keys()) def __contains__(self, key: str) -> bool: return key in self.primary From 7ed3b26b02e740993bbfaa06da641d19dafc4af3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 10 Oct 2020 19:12:17 -0700 Subject: [PATCH 68/71] skip assignment in dfs() to avoid PatternLockedError on unmodified patterns --- masque/pattern.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 2393a6f..31a331c 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -291,11 +291,14 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): sp_transform = False if subpattern.pattern is not None: - subpattern.pattern = subpattern.pattern.dfs(visit_before=visit_before, - visit_after=visit_after, - transform=sp_transform, - memo=memo, - hierarchy=hierarchy + (self,)) + result = subpattern.pattern.dfs(visit_before=visit_before, + visit_after=visit_after, + transform=sp_transform, + memo=memo, + hierarchy=hierarchy + (self,)) + if result is not subpattern.pattern: + # skip assignment to avoid PatternLockedError unless modified + subpattern.pattern = result if visit_after is not None: pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore From f6ad272c2c06fc3a1dfa1a5f2e7332181fe43185 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Sat, 10 Oct 2020 19:12:56 -0700 Subject: [PATCH 69/71] bump version to v2.2 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index 879b416..8bbe6cf 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -2.1 +2.2 From f3649704037202a5598f6880fcaf132db224e543 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Fri, 16 Oct 2020 19:00:50 -0700 Subject: [PATCH 70/71] style and type fixes (per flake8) could potentially fix some bugs in `Library` class and dxf reader --- .flake8 | 29 ++++++++++++ masque/file/__init__.py | 3 +- masque/file/dxf.py | 60 ++++++++++++------------- masque/file/gdsii.py | 63 +++++++++++++------------- masque/file/klamath.py | 85 +++++++++++++++++------------------ masque/file/oasis.py | 72 +++++++++++++++-------------- masque/file/svg.py | 3 +- masque/file/utils.py | 11 +++-- masque/label.py | 6 +-- masque/library/library.py | 11 +++-- masque/pattern.py | 33 +++++++------- masque/repetition.py | 18 ++++---- masque/shapes/arc.py | 32 ++++++------- masque/shapes/circle.py | 8 ++-- masque/shapes/ellipse.py | 10 ++--- masque/shapes/path.py | 24 +++++----- masque/shapes/polygon.py | 13 +++--- masque/shapes/shape.py | 12 ++--- masque/shapes/text.py | 26 +++++------ masque/subpattern.py | 10 ++--- masque/traits/annotatable.py | 5 +-- masque/traits/copyable.py | 4 +- masque/traits/doseable.py | 7 +-- masque/traits/layerable.py | 4 +- masque/traits/lockable.py | 5 +-- masque/traits/mirrorable.py | 5 +-- masque/traits/positionable.py | 8 ++-- masque/traits/repeatable.py | 5 +-- masque/traits/rotatable.py | 7 ++- masque/traits/scalable.py | 5 +-- masque/utils.py | 6 +-- 31 files changed, 293 insertions(+), 297 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..0042015 --- /dev/null +++ b/.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, diff --git a/masque/file/__init__.py b/masque/file/__init__.py index 8de11a7..8f550c4 100644 --- a/masque/file/__init__.py +++ b/masque/file/__init__.py @@ -1,3 +1,4 @@ """ Functions for reading from and writing to various file formats. -""" \ No newline at end of file +""" + diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 95814ef..906fc2f 100644 --- a/masque/file/dxf.py +++ b/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) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 68b69a9..c0ea11d 100644 --- a/masque/file/gdsii.py +++ b/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) diff --git a/masque/file/klamath.py b/masque/file/klamath.py index c70f704..0f858cf 100644 --- a/masque/file/klamath.py +++ b/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], diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 5e20121..dea703d 100644 --- a/masque/file/oasis.py +++ b/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) diff --git a/masque/file/svg.py b/masque/file/svg.py index b719b0b..a9f7c47 100644 --- a/masque/file/svg.py +++ b/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 diff --git a/masque/file/utils.py b/masque/file/utils.py index 6239a1a..d183b39 100644 --- a/masque/file/utils.py +++ b/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) diff --git a/masque/label.py b/masque/label.py index c8ca802..5027af5 100644 --- a/masque/label.py +++ b/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() diff --git a/masque/library/library.py b/masque/library/library.py index 66d9f17..694360f 100644 --- a/masque/library/library.py +++ b/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': diff --git a/masque/pattern.py b/masque/pattern.py index 31a331c..33c5030 100644 --- a/masque/pattern.py +++ b/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': """ diff --git a/masque/repetition.py b/masque/repetition.py index 4ed29f4..396d351 100644 --- a/masque/repetition.py +++ b/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]: diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 6f75e7e..0416d01 100644 --- a/masque/shapes/arc.py +++ b/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 @@ -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' a°{self.angles*180/pi}' - rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else '' + angles = f' a°{numpy.rad2deg(self.angles)}' + rotation = f' r°{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}>' diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index b3e07ae..d7aaba4 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -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 '' diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index b2ceec6..140f590 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -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 diff --git a/masque/shapes/path.py b/masque/shapes/path.py index b25f464..1cad032 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -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) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index c11b662..59ab82d 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -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': """ diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 8ffb1a4..0603ec7 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -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 diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 2384404..07cc1a7 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -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 diff --git a/masque/subpattern.py b/masque/subpattern.py index 6d0c2a9..8eebc95 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -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) diff --git a/masque/traits/annotatable.py b/masque/traits/annotatable.py index e818b3b..9d49018 100644 --- a/masque/traits/annotatable.py +++ b/masque/traits/annotatable.py @@ -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): diff --git a/masque/traits/copyable.py b/masque/traits/copyable.py index 5a318d7..aa84356 100644 --- a/masque/traits/copyable.py +++ b/masque/traits/copyable.py @@ -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 diff --git a/masque/traits/doseable.py b/masque/traits/doseable.py index 96c535c..217872c 100644 --- a/masque/traits/doseable.py +++ b/masque/traits/doseable.py @@ -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 ''' diff --git a/masque/traits/layerable.py b/masque/traits/layerable.py index e3d5f7b..812511b 100644 --- a/masque/traits/layerable.py +++ b/masque/traits/layerable.py @@ -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 diff --git a/masque/traits/lockable.py b/masque/traits/lockable.py index fadaaa3..5a0f06f 100644 --- a/masque/traits/lockable.py +++ b/masque/traits/lockable.py @@ -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') diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index a66c074..1ec54f6 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -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') diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index 150daf0..71f90ec 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -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 ''' diff --git a/masque/traits/repeatable.py b/masque/traits/repeatable.py index 67183ad..4a7f391 100644 --- a/masque/traits/repeatable.py +++ b/masque/traits/repeatable.py @@ -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: diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index c79e89e..c0641f0 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -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') diff --git a/masque/traits/scalable.py b/masque/traits/scalable.py index bebda69..b31c2f9 100644 --- a/masque/traits/scalable.py +++ b/masque/traits/scalable.py @@ -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 diff --git a/masque/utils.py b/masque/utils.py index c33b8c4..a09a09c 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -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 From eb11f31960ab1b49da3be066e307e48eca5b0b09 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz <anewusername@gmail.com> Date: Mon, 26 Oct 2020 19:45:46 -0700 Subject: [PATCH 71/71] improve type hints for Library --- masque/library/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/library/library.py b/masque/library/library.py index 694360f..91fd6a8 100644 --- a/masque/library/library.py +++ b/masque/library/library.py @@ -180,7 +180,7 @@ class Library: else: self.primary[key] = pg - def precache(self) -> 'Library': + def precache(self: L) -> L: """ Force all patterns into the cache @@ -193,7 +193,7 @@ class Library: _ = self.get_secondary(*key2) return self - def add(self, other: 'Library') -> 'Library': + def add(self: L, other: L) -> L: """ Add keys from another library into this one.