diff --git a/README.md b/README.md index 47e82b0..f2e95e9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ to output to multiple formats. Requirements: * python >= 3.8 * numpy -* klamath (used for `gdsii` i/o and library management) +* klamath (optional, used for `gdsii` i/o) * matplotlib (optional, used for `visualization` functions and `text`) * ezdxf (optional, used for `dxf` i/o) * fatamorgana (optional, used for `oasis` i/o) @@ -49,9 +49,3 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release - boolean ops * Construct polygons from bitmap using `skimage.find_contours` * Deal with shape repetitions for dxf, svg - -* this approach has an issue: how add devices together? - - need to add the other device by name - - need to know the other device's ports - - - - also: device doesn't know its own name, can't wrap itself into a ref diff --git a/examples/pic2mask.py b/examples/pic2mask.py new file mode 100644 index 0000000..883f145 --- /dev/null +++ b/examples/pic2mask.py @@ -0,0 +1,41 @@ +# pip install pillow scikit-image +# or +# sudo apt install python3-pil python3-skimage + +from PIL import Image +from skimage.measure import find_contours +from matplotlib import pyplot +import numpy + +from masque import Pattern, Library, Polygon +from masque.file.gdsii import writefile + +# +# Read the image into a numpy array +# +im = Image.open('./Desktop/Camera/IMG_20220626_091101.jpg') + +aa = numpy.array(im.convert(mode='L').getdata()).reshape(im.height, im.width) + +threshold = (aa.max() - aa.min()) / 2 + +# +# Find edge contours and plot them +# +contours = find_contours(aa, threshold) + +pyplot.imshow(aa) +for contour in contours: + pyplot.plot(contour[:, 1], contour[:, 0], linewidth=2) +pyplot.show(block=False) + +# +# Create the layout from the contours +# +pat = Pattern() +pat.shapes = [Polygon(vertices=vv) for vv in contours if len(vv) < 1_000] + +lib = {} +lib['my_mask_name'] = pat + +writefile(lib, 'test_contours.gds', meters_per_unit=1e-9) diff --git a/masque/__init__.py b/masque/__init__.py index 9139cf1..f0e777e 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -28,7 +28,7 @@ """ from .error import MasqueError, PatternError, LibraryError, BuildError -from .shapes import Shape +from .shapes import Shape, Polygon, Path, Circle, Arc, Ellipse from .label import Label from .ref import Ref from .pattern import Pattern diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 39c8849..fd6e5f4 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -1,17 +1,12 @@ -from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence -from typing import overload, KeysView, ValuesView, MutableMapping, Mapping +from typing import Dict, Tuple, Union, TypeVar, Optional, Sequence +from typing import MutableMapping, Mapping import copy -import warnings -import traceback import logging -from collections import Counter -from abc import ABCMeta import numpy from numpy import pi -from numpy.typing import ArrayLike, NDArray +from numpy.typing import ArrayLike -from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from ..pattern import Pattern from ..ref import Ref from ..library import MutableLibrary @@ -241,8 +236,6 @@ class Builder(PortList): `PortError` if applying the prefixes results in duplicate port names. """ - from ..pattern import Pattern - if library is None: if hasattr(source, 'library') and isinstance(source, MutableLibrary): library = source.library @@ -276,7 +269,7 @@ class Builder(PortList): ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi) for name, port in mapped_ports.items()} ports_out = {f'{out_prefix}{name}': port.deepcopy() - for name, port in mapped_ports.items()} + for name, port in mapped_ports.items()} duplicates = set(ports_out.keys()) & set(ports_in.keys()) if duplicates: @@ -377,7 +370,7 @@ class Builder(PortList): map_out[vi] = None self.place(other, offset=translation, rotation=rotation, pivot=pivot, - mirrored=mirrored, port_map=map_out, skip_port_check=True) + mirrored=mirrored, port_map=map_out, skip_port_check=True) return self def place( diff --git a/masque/builder/port_utils.py b/masque/builder/port_utils.py index 0f7928b..522caa7 100644 --- a/masque/builder/port_utils.py +++ b/masque/builder/port_utils.py @@ -82,6 +82,7 @@ def pat2dev( ports = {} annotated_cells = set() + def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: if len(hierarchy) > max_depth: if max_depth >= 999_999: diff --git a/masque/builder/utils.py b/masque/builder/utils.py index fff42e6..73c5c76 100644 --- a/masque/builder/utils.py +++ b/masque/builder/utils.py @@ -1,5 +1,5 @@ -from typing import Dict, Tuple, List, Mapping, Sequence, SupportsFloat -from typing import Optional, Union, Any, cast, TYPE_CHECKING +from typing import Dict, Mapping, Sequence, SupportsFloat +from typing import Optional, Union, cast, TYPE_CHECKING from pprint import pformat import numpy @@ -173,7 +173,7 @@ def ell( if bound_type in ('emin', 'min_extension', 'min_past_furthest'): offsets += rot_bound.max() - elif bound_type in('emax', 'max_extension'): + elif bound_type in ('emax', 'max_extension'): offsets += rot_bound.min() - offsets.max() else: if numpy.size(bound) == 2: @@ -194,7 +194,7 @@ def ell( if extension < 0: ext_floor = -numpy.floor(extension) raise BuildError(f'Position is too close by at least {ext_floor}. Total extensions would be\n\t' - + '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets))) + + '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets))) result = dict(zip(ports.keys(), offsets)) return result diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 7f1ad41..f47a2d4 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -1,7 +1,7 @@ """ DXF file format readers and writers """ -from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping +from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Mapping import re import io import base64 @@ -17,7 +17,6 @@ from .. import Pattern, Ref, PatternError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid from ..utils import rotation_matrix_2d, layer_t -from .gdsii import check_valid_names logger = logging.getLogger(__name__) @@ -52,8 +51,11 @@ def write( DXF does not support shape repetition (only block repeptition). Please call library.wrap_repeated_shapes() before writing to file. - If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` - prior to calling this function. + Other functions you may want to call: + - `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names + - `library.dangling_references()` to check for references to missing patterns + - `pattern.polygonize()` for any patterns with shapes other + than `masque.shapes.Polygon` or `masque.shapes.Path` 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 @@ -70,8 +72,6 @@ def write( """ #TODO consider supporting DXF arcs? - check_valid_names(library.keys()) - pattern = library[top_name] # Create library @@ -83,7 +83,7 @@ def write( # Now create a block for each referenced pattern, and add in any shapes for name, pat in library.items(): - assert(pat is not None) + assert pat is not None block = lib.blocks.new(name=name) _shapes_to_elements(block, pat.shapes) @@ -173,8 +173,9 @@ def read( msp = lib.modelspace() npat = _read_block(msp, clean_vertices) - patterns_dict = dict([npat] - + [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space']) + patterns_dict = dict( + [npat] + [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space'] + ) library_info = { 'layers': [ll.dxfattribs() for ll in lib.layers] @@ -323,8 +324,10 @@ def _shapes_to_elements( # Could set do paths with width setting, but need to consider endcaps. for shape in shapes: if shape.repetition is not None: - raise PatternError('Shape repetitions are not supported by DXF.' - ' Please call library.wrap_repeated_shapes() before writing to file.') + raise PatternError( + 'Shape repetitions are not supported by DXF.' + ' Please call library.wrap_repeated_shapes() before writing to file.' + ) attribs = {'layer': _mlayer2dxf(shape.layer)} for polygon in shape.to_polygons(): diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 5eb9896..218e57f 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -18,14 +18,10 @@ 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, Iterable, Optional -from typing import Sequence, BinaryIO, Mapping, cast -import re +from typing import List, Any, Dict, Tuple, Callable, Union, Iterable +from typing import BinaryIO, Mapping import io import mmap -import copy -import base64 -import struct import logging import pathlib import gzip @@ -83,10 +79,13 @@ def write( otherwise `0` GDS does not support shape repetition (only cell repeptition). Please call - library.wrap_repeated_shapes() before writing to file. + `library.wrap_repeated_shapes()` before writing to file. - If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` - prior to calling this function. + Other functions you may want to call: + - `masque.file.gdsii.check_valid_names(library.keys())` to check for invalid names + - `library.dangling_references()` to check for references to missing patterns + - `pattern.polygonize()` for any patterns with shapes other + than `masque.shapes.Polygon` or `masque.shapes.Path` Args: library: A {name: Pattern} mapping of patterns to write. @@ -98,10 +97,6 @@ def write( library_name: Library name written into the GDSII file. Default 'masque-klamath'. """ - check_valid_names(library.keys()) - - # TODO check all hierarchy present - if not isinstance(library, MutableLibrary): if isinstance(library, dict): library = WrapLibrary(library) @@ -433,7 +428,7 @@ def _shapes_to_elements( for shape in shapes: if shape.repetition is not None: raise PatternError('Shape repetitions are not supported by GDS.' - ' Please call library.wrap_repeated_shapes() before writing to file.') + ' Please call library.wrap_repeated_shapes() before writing to file.') layer, data_type = _mlayer2gds(shape.layer) properties = _annotations_to_properties(shape.annotations, 128) @@ -504,56 +499,6 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]: return texts -def disambiguate_pattern_names( - names: Iterable[str], - max_name_length: int = 32, - suffix_length: int = 6, - ) -> List[str]: - """ - Args: - names: List of pattern names 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. - """ - new_names = [] - for name in names: - # Shorten names which already exceed max-length - if len(name) > max_name_length: - shortened_name = name[:max_name_length - suffix_length] - logger.warning(f'Pattern name "{name}" is too long ({len(name)}/{max_name_length} chars),\n' - + f' shortening to "{shortened_name}" before generating suffix') - else: - shortened_name = name - - # Remove invalid characters - sanitized_name = re.compile(r'[^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 new_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}"') - - # 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 "{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 "{name}"') - - new_names.append(suffixed_name) - return new_names - - def load_library( stream: BinaryIO, *, diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 6163d77..1d250ed 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -12,11 +12,7 @@ Note that OASIS references follow the same convention as `masque`, vectors or offsets. """ from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping, Optional, cast -import re import io -import copy -import base64 -import struct import logging import pathlib import gzip @@ -77,8 +73,11 @@ def build( 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. + Other functions you may want to call: + - `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names + - `library.dangling_references()` to check for references to missing patterns + - `pattern.polygonize()` for any patterns with shapes other + than `masque.shapes.Polygon`, `masque.shapes.Path`, or `masque.shapes.Circle` Args: library: A {name: Pattern} mapping of patterns to write. @@ -97,10 +96,6 @@ def build( Returns: `fatamorgana.OasisLayout` """ - check_valid_names(library.keys()) - - # TODO check all hierarchy present - if not isinstance(library, MutableLibrary): if isinstance(library, dict): library = WrapLibrary(library) @@ -130,7 +125,7 @@ def build( for tt in (True, False)] def layer2oas(mlayer: layer_t) -> Tuple[int, int]: - assert(layer_map is not None) + assert layer_map is not None layer_num = layer_map[mlayer] if isinstance(mlayer, str) else mlayer return _mlayer2oas(layer_num) else: @@ -270,7 +265,7 @@ def read( # note XELEMENT has no repetition continue - assert(not isinstance(element.repetition, fatamorgana.ReuseRepetition)) + assert not isinstance(element.repetition, fatamorgana.ReuseRepetition) repetition = repetition_fata2masq(element.repetition) # Switch based on element type: @@ -478,7 +473,7 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) """ Helper function to create a Ref from a placment. Sets ref.target to the placement name. """ - assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition)) + 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 @@ -656,7 +651,7 @@ def repetition_masq2fata( frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore offset = rep.displacements[0, :] else: - assert(rep is None) + assert rep is None frep = None offset = (0, 0) return frep, offset @@ -679,14 +674,14 @@ def properties_to_annotations( ) -> annotations_t: annotations = {} for proprec in properties: - assert(proprec.name is not None) + 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) + assert proprec.values is not None for value in proprec.values: if isinstance(value, (float, int)): values.append(value) diff --git a/masque/file/svg.py b/masque/file/svg.py index caaea0c..1296980 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -1,7 +1,7 @@ """ SVG file format readers and writers """ -from typing import Dict, Optional, Mapping +from typing import Mapping import warnings import numpy diff --git a/masque/file/utils.py b/masque/file/utils.py index c478bf8..7eb9788 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -1,14 +1,11 @@ """ Helper functions for file reading and writing """ -from typing import Set, Tuple, List, Iterable, Mapping import re -import copy import pathlib import logging from .. import Pattern, PatternError -from ..library import Library, WrapROLibrary from ..shapes import Polygon, Path diff --git a/masque/label.py b/masque/label.py index fc9460c..d0b1998 100644 --- a/masque/label.py +++ b/masque/label.py @@ -1,4 +1,4 @@ -from typing import Tuple, Dict, Optional, TypeVar +from typing import Dict, Optional, TypeVar import copy import numpy diff --git a/masque/library.py b/masque/library.py index b20ba72..6e846fa 100644 --- a/masque/library.py +++ b/masque/library.py @@ -3,9 +3,8 @@ Library class for managing unique name->pattern mappings and deferred loading or creation. """ from typing import List, Dict, Callable, TypeVar, Generic, Type, TYPE_CHECKING -from typing import Any, Tuple, Union, Iterator, Mapping, MutableMapping, Set, Optional, Sequence +from typing import Tuple, Union, Iterator, Mapping, MutableMapping, Set, Optional, Sequence import logging -import copy import base64 import struct import re @@ -14,7 +13,7 @@ from collections import defaultdict from abc import ABCMeta, abstractmethod import numpy -from numpy.typing import ArrayLike, NDArray, NDArray +from numpy.typing import ArrayLike from .error import LibraryError, PatternError from .utils import rotation_matrix_2d, normalize_mirror @@ -45,21 +44,51 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta): def __repr__(self) -> str: return '' - def referenced_patterns( + def dangling_references( self, - tops: Union[str, Sequence[str]], - skip: Optional[Set[Optional[str]]] = None, + tops: Union[None, str, Sequence[str]] = None, ) -> Set[Optional[str]]: """ - Get the set of all pattern names referenced by `top`. Recursively traverses into any refs. + Get the set of all pattern names not present in the library but referenced + by `tops`, recursively traversing any refs. + + If `tops` are not given, all patterns in the library are checked. Args: - top: Name of the top pattern(s) to check. + tops: Name(s) of the pattern(s) to check. + Default is all patterns in the library. skip: Memo, set patterns which have already been traversed. Returns: Set of all referenced pattern names """ + if tops is None: + tops = tuple(self.keys()) + + referenced = self.referenced_patterns(tops) + return referenced - set(self.keys()) + + def referenced_patterns( + self, + tops: Union[None, str, Sequence[str]] = None, + skip: Optional[Set[Optional[str]]] = None, + ) -> Set[Optional[str]]: + """ + Get the set of all pattern names referenced by `tops`. Recursively traverses into any refs. + + If `tops` are not given, all patterns in the library are checked. + + Args: + tops: Name(s) of the pattern(s) to check. + Default is all patterns in the library. + skip: Memo, set patterns which have already been traversed. + + Returns: + Set of all referenced pattern names + """ + if tops is None: + tops = tuple(self.keys()) + if skip is None: skip = set([None]) @@ -73,17 +102,17 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta): # Perform recursive lookups, but only once for each name for target in targets - skip: - assert(target is not None) - self.referenced_patterns(target, skip) + assert target is not None + if target in self: + targets |= self.referenced_patterns(target, skip=skip) skip.add(target) return targets - # TODO maybe not for immutable? def subtree( self, tops: Union[str, Sequence[str]], - ) -> Library: + ) -> 'Library': """ Return a new `Library`, containing only the specified patterns and the patterns they reference (recursively). @@ -184,7 +213,7 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta): for top in tops: flatten_single(top) - assert(None not in flattened.values()) + assert None not in flattened.values() return flattened # type: ignore def get_name( @@ -364,7 +393,7 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta): #def __len__(self) -> int: @abstractmethod - def __setitem__(self, key: str, value: VVV) -> None: # TODO + def __setitem__(self, key: str, value: VVV) -> None: pass @abstractmethod @@ -390,7 +419,7 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta): Args: other: The library to insert keys from - use_ours: Decision function for name conflicts, called with cell name. + use_ours: Decision function for name conflicts, called with pattern name. Should return `True` if the value from `self` should be used. use_theirs: Decision function for name conflicts. Same format as `use_ours`. Should return `True` if the value from `other` should be used. @@ -451,13 +480,14 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta): exclude_types = () if label2name is None: - label2name = lambda label: self.get_name('_shape') - + def label2name(label): + return self.get_name('_shape_') + #label2name = lambda label: self.get_name('_shape') shape_counts: MutableMapping[Tuple, int] = defaultdict(int) shape_funcs = {} - ### First pass ### + # ## First pass ## # Using the label tuple from `.normalized_form()` as a key, check how many of each shape # are present and store the shape function for each one for pat in tuple(self.values()): @@ -476,7 +506,7 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta): shape_pat = Pattern(shapes=[shape_func()]) shape_pats[label] = shape_pat - ### Second pass ### + # ## Second pass ## for pat in tuple(self.values()): # Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which # are to be replaced. @@ -534,7 +564,9 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta): from .pattern import Pattern if name_func is None: - name_func = lambda _pat, _shape: self.get_name('_rep') + def name_func(_pat, _shape): + return self.get_name('_rep_') + #name_func = lambda _pat, _shape: self.get_name('_rep') for pat in tuple(self.values()): new_shapes = [] @@ -710,7 +742,7 @@ class LazyLibrary(MutableLibrary): def clear_cache(self: LL) -> LL: """ Clear the cache of this library. - This is usually used before modifying or deleting cells, e.g. when merging + This is usually used before modifying or deleting patterns, e.g. when merging with another library. Returns: diff --git a/masque/pattern.py b/masque/pattern.py index 1e5f238..ff4a6c2 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -2,11 +2,10 @@ Base object representing a lithography mask. """ -from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload, cast -from typing import Mapping, MutableMapping, Iterable, TypeVar, Any +from typing import List, Callable, Dict, Union, Set, Sequence, Optional, cast +from typing import Mapping, TypeVar, Any import copy from itertools import chain -from collections import defaultdict import numpy from numpy import inf @@ -14,9 +13,9 @@ from numpy.typing import NDArray, ArrayLike # .visualize imports matplotlib and matplotlib.collections from .ref import Ref -from .shapes import Shape, Polygon +from .shapes import Shape from .label import Label -from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t +from .utils import rotation_matrix_2d, AutoSlots, annotations_t from .error import PatternError from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable from .ports import Port, PortList @@ -30,7 +29,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): 2D layout consisting of some set of shapes, labels, and references to other Pattern objects (via Ref). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. """ - __slots__ = ('shapes', 'labels', 'refs', 'ports') + __slots__ = ( + 'shapes', 'labels', 'refs', 'ports', + # inherited + '_offset', '_annotations' + ) shapes: List[Shape] """ List of all shapes in this Pattern. @@ -116,7 +119,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): ) return new - def append(self: P, other_pattern: Pattern) -> P: + def append(self: P, other_pattern: 'Pattern') -> P: """ Appends all shapes, labels and refs from other_pattern to self's shapes, labels, and supbatterns. @@ -321,7 +324,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): `[[x_min, y_min], [x_max, y_max]]` """ bounds = self.get_bounds(library) - assert(bounds is not None) + assert bounds is not None return bounds def translate_elements(self: P, offset: ArrayLike) -> P: diff --git a/masque/ports.py b/masque/ports.py index 9b8361a..a3716be 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -1,6 +1,5 @@ -from typing import Dict, Iterable, List, Tuple, Iterator, Optional, Sequence, MutableMapping -from typing import overload, KeysView, ValuesView, ItemsView, TYPE_CHECKING, Union, TypeVar, Any -import copy +from typing import Dict, Iterable, List, Tuple, KeysView, ValuesView, ItemsView +from typing import overload, Union, Optional, TypeVar import warnings import traceback import logging @@ -14,17 +13,11 @@ from numpy.typing import ArrayLike, NDArray from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from .utils import AutoSlots, rotate_offsets_around from .error import PortError -from .library import MutableLibrary -from .builder import Tool - -if TYPE_CHECKING: - from .builder import Builder logger = logging.getLogger(__name__) - P = TypeVar('P', bound='Port') PL = TypeVar('PL', bound='PortList') PL2 = TypeVar('PL2', bound='PortList') @@ -200,8 +193,6 @@ class PortList(metaclass=ABCMeta): Returns: self """ - - new_ports = { names[0]: Port(offset, rotation=rotation, ptype=ptype), names[1]: Port(offset, rotation=rotation + pi, ptype=ptype), @@ -335,7 +326,6 @@ class PortList(metaclass=ABCMeta): type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk' for st, ot in zip(s_types, o_types)]) if type_conflicts.any(): - ports = numpy.where(type_conflicts) msg = 'Ports have conflicting types:\n' for nn, (k, v) in enumerate(map_in.items()): if type_conflicts[nn]: @@ -353,7 +343,7 @@ class PortList(metaclass=ABCMeta): if not numpy.allclose(rotations[:1], rotations): rot_deg = numpy.rad2deg(rotations) - msg = f'Port orientations do not match:\n' + msg = 'Port orientations do not match:\n' for nn, (k, v) in enumerate(map_in.items()): msg += f'{k} | {rot_deg[nn]:g} | {v}\n' raise PortError(msg) @@ -362,7 +352,7 @@ class PortList(metaclass=ABCMeta): rotate_offsets_around(o_offsets, pivot, rotations[0]) translations = s_offsets - o_offsets if not numpy.allclose(translations[:1], translations): - msg = f'Port translations do not match:\n' + msg = 'Port translations do not match:\n' for nn, (k, v) in enumerate(map_in.items()): msg += f'{k} | {translations[nn]} | {v}\n' raise PortError(msg) diff --git a/masque/ref.py b/masque/ref.py index 3553d2c..0277b00 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -4,7 +4,7 @@ """ #TODO more top-level documentation -from typing import Dict, Tuple, Optional, Sequence, Mapping, TYPE_CHECKING, Any, TypeVar +from typing import Dict, Optional, Sequence, Mapping, TYPE_CHECKING, Any, TypeVar import copy import numpy @@ -12,7 +12,7 @@ from numpy import pi from numpy.typing import NDArray, ArrayLike from .error import PatternError -from .utils import is_scalar, AutoSlots, annotations_t +from .utils import is_scalar, annotations_t from .repetition import Repetition from .traits import ( PositionableImpl, RotatableImpl, ScalableImpl, @@ -109,7 +109,7 @@ class Ref( # Mirrored property @property - def mirrored(self) -> Any: #TODO mypy#3004 NDArray[numpy.bool_]: + def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]: return self._mirrored @mirrored.setter @@ -121,8 +121,8 @@ class Ref( def as_pattern( self, *, - pattern: Optional[Pattern] = None, - library: Optional[Mapping[str, Pattern]] = None, + pattern: Optional['Pattern'] = None, + library: Optional[Mapping[str, 'Pattern']] = None, ) -> 'Pattern': """ Args: @@ -138,7 +138,7 @@ class Ref( if library is None: raise PatternError('as_pattern() must be given a pattern or library.') - assert(self.target is not None) + assert self.target is not None pattern = library[self.target] pattern = pattern.deepcopy() @@ -196,6 +196,8 @@ class Ref( raise PatternError('as_pattern() must be given a pattern or library.') if pattern is None and self.target is None: return None + if self.target not in library: + raise PatternError(f'get_bounds() called on dangling reference to "{self.target}"') return self.as_pattern(pattern=pattern, library=library).get_bounds() def __repr__(self) -> str: diff --git a/masque/repetition.py b/masque/repetition.py index 02d3c96..5f0d8b0 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -3,7 +3,7 @@ instances of an object . """ -from typing import Union, Dict, Optional, Sequence, Any, Type +from typing import Union, Dict, Optional, Any, Type import copy from abc import ABCMeta, abstractmethod @@ -40,7 +40,7 @@ class Grid(Repetition, metaclass=AutoSlots): Note that the offsets in either the 2D or 1D grids do not have to be axis-aligned. """ __slots__ = ( - '_a_vector','_b_vector', + '_a_vector', '_b_vector', '_a_count', '_b_count', ) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index c5712e5..d254afc 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -48,7 +48,7 @@ class Arc(Shape, metaclass=AutoSlots): # radius properties @property - def radii(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]: + def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ @@ -85,7 +85,7 @@ class Arc(Shape, metaclass=AutoSlots): # arc start/stop angle properties @property - def angles(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]: + def angles(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]: """ Return the start and stop angles `[a_start, a_stop]`. Angles are measured from x-axis after rotation @@ -171,9 +171,9 @@ class Arc(Shape, metaclass=AutoSlots): raw: bool = False, ) -> None: if raw: - assert(isinstance(radii, numpy.ndarray)) - assert(isinstance(angles, numpy.ndarray)) - assert(isinstance(offset, numpy.ndarray)) + assert isinstance(radii, numpy.ndarray) + assert isinstance(angles, numpy.ndarray) + assert isinstance(offset, numpy.ndarray) self._radii = radii self._angles = angles self._width = width diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index e1b0bf2..017f4bb 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -59,7 +59,7 @@ class Circle(Shape, metaclass=AutoSlots): raw: bool = False, ) -> None: if raw: - assert(isinstance(offset, numpy.ndarray)) + assert isinstance(offset, numpy.ndarray) self._radius = radius self._offset = offset self._repetition = repetition diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 2d21b86..32f79fd 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -38,7 +38,7 @@ class Ellipse(Shape, metaclass=AutoSlots): # radius properties @property - def radii(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]: + def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ @@ -106,8 +106,8 @@ class Ellipse(Shape, metaclass=AutoSlots): raw: bool = False, ) -> None: if raw: - assert(isinstance(radii, numpy.ndarray)) - assert(isinstance(offset, numpy.ndarray)) + assert isinstance(radii, numpy.ndarray) + assert isinstance(offset, numpy.ndarray) self._radii = radii self._offset = offset self._rotation = rotation diff --git a/masque/shapes/path.py b/masque/shapes/path.py index ec76bbd..0b826a6 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -76,7 +76,7 @@ class Path(Shape, metaclass=AutoSlots): # cap_extensions property @property - def cap_extensions(self) -> Optional[Any]: #TODO mypy#3004 NDArray[numpy.float64]]: + def cap_extensions(self) -> Optional[Any]: # TODO mypy#3004 NDArray[numpy.float64]]: """ Path end-cap extension @@ -99,7 +99,7 @@ class Path(Shape, metaclass=AutoSlots): # vertices property @property - def vertices(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]]: + def vertices(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]]: """ Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ @@ -162,9 +162,9 @@ class Path(Shape, metaclass=AutoSlots): self._cap_extensions = None # Since .cap setter might access it if raw: - assert(isinstance(vertices, numpy.ndarray)) - assert(isinstance(offset, numpy.ndarray)) - assert(isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None) + assert isinstance(vertices, numpy.ndarray) + assert isinstance(offset, numpy.ndarray) + assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None self._vertices = vertices self._offset = offset self._repetition = repetition @@ -229,7 +229,7 @@ class Path(Shape, metaclass=AutoSlots): Returns: The resulting Path object """ - #TODO: needs testing + # TODO: needs testing direction = numpy.array([1, 0]) verts = [numpy.zeros(2)] @@ -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: - assert(isinstance(self.cap_extensions, numpy.ndarray)) + assert isinstance(self.cap_extensions, numpy.ndarray) extensions = self.cap_extensions else: # Flush or Circle diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 252cf22..acdd9f5 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -30,7 +30,7 @@ class Polygon(Shape, metaclass=AutoSlots): # vertices property @property - def vertices(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]: + def vertices(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]: """ Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ @@ -88,8 +88,8 @@ class Polygon(Shape, metaclass=AutoSlots): raw: bool = False, ) -> None: if raw: - assert(isinstance(vertices, numpy.ndarray)) - assert(isinstance(offset, numpy.ndarray)) + assert isinstance(vertices, numpy.ndarray) + assert isinstance(offset, numpy.ndarray) self._vertices = vertices self._offset = offset self._repetition = repetition @@ -212,17 +212,17 @@ class Polygon(Shape, metaclass=AutoSlots): """ if lx is None: if xctr is None: - assert(xmin is not None) - assert(xmax is not None) + assert xmin is not None + assert xmax is not None xctr = 0.5 * (xmax + xmin) lx = xmax - xmin elif xmax is None: - assert(xmin is not None) - assert(xctr is not None) + assert xmin is not None + assert xctr is not None lx = 2 * (xctr - xmin) elif xmin is None: - assert(xctr is not None) - assert(xmax is not None) + assert xctr is not None + assert xmax is not None lx = 2 * (xmax - xctr) else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') @@ -230,29 +230,29 @@ class Polygon(Shape, metaclass=AutoSlots): if xctr is not None: pass elif xmax is None: - assert(xmin is not None) - assert(lx is not None) + assert xmin is not None + assert lx is not None xctr = xmin + 0.5 * lx elif xmin is None: - assert(xmax is not None) - assert(lx is not None) + assert xmax is not None + assert lx is not None xctr = xmax - 0.5 * lx else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') if ly is None: if yctr is None: - assert(ymin is not None) - assert(ymax is not None) + assert ymin is not None + assert ymax is not None yctr = 0.5 * (ymax + ymin) ly = ymax - ymin elif ymax is None: - assert(ymin is not None) - assert(yctr is not None) + assert ymin is not None + assert yctr is not None ly = 2 * (yctr - ymin) elif ymin is None: - assert(yctr is not None) - assert(ymax is not None) + assert yctr is not None + assert ymax is not None ly = 2 * (ymax - yctr) else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') @@ -260,12 +260,12 @@ class Polygon(Shape, metaclass=AutoSlots): if yctr is not None: pass elif ymax is None: - assert(ymin is not None) - assert(ly is not None) + assert ymin is not None + assert ly is not None yctr = ymin + 0.5 * ly elif ymin is None: - assert(ly is not None) - assert(ymax is not None) + assert ly is not None + assert ymax is not None yctr = ymax - 0.5 * ly else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') @@ -331,7 +331,6 @@ class Polygon(Shape, metaclass=AutoSlots): poly.rotate(rotation) return poly - def to_polygons( self, poly_num_points: Optional[int] = None, # unused diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 92ecb68..d70e477 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -55,7 +55,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): # Mirrored property @property - def mirrored(self) -> Any: #TODO mypy#3004 NDArray[numpy.bool_]: + def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]: return self._mirrored @mirrored.setter @@ -79,8 +79,8 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): raw: bool = False, ) -> None: if raw: - assert(isinstance(offset, numpy.ndarray)) - assert(isinstance(mirrored, numpy.ndarray)) + assert isinstance(offset, numpy.ndarray) + assert isinstance(mirrored, numpy.ndarray) self._offset = offset self._layer = layer self._string = string diff --git a/masque/traits/lockable.py b/masque/traits/lockable.py deleted file mode 100644 index 0350e29..0000000 --- a/masque/traits/lockable.py +++ /dev/null @@ -1,103 +0,0 @@ -from typing import TypeVar, Dict, Tuple, Any -from abc import ABCMeta, abstractmethod - -#from ..error import PatternLockedError - - -T = TypeVar('T', bound='Lockable') -I = TypeVar('I', bound='LockableImpl') - - -class Lockable(metaclass=ABCMeta): - """ - Abstract class for all lockable entities - """ - __slots__ = () # type: Tuple[str, ...] - - ''' - ---- Methods - ''' - @abstractmethod - def lock(self: T) -> T: - """ - Lock the object, disallowing further changes - - Returns: - self - """ - pass - - @abstractmethod - def unlock(self: T) -> T: - """ - Unlock the object, reallowing changes - - Returns: - self - """ - 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): - """ - Simple implementation of Lockable - """ - __slots__ = () # type: Tuple[str, ...] - - 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 __getstate__(self) -> Dict[str, Any]: - if hasattr(self, '__slots__'): - return {key: getattr(self, key) for key in self.__slots__} - else: - return self.__dict__ - - def __setstate__(self, state: Dict[str, Any]) -> None: - for k, v in state.items(): - object.__setattr__(self, k, v) - - def lock(self: I) -> I: - object.__setattr__(self, 'locked', True) - return self - - 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/positionable.py b/masque/traits/positionable.py index 58c225c..593ae46 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -80,7 +80,7 @@ class Positionable(metaclass=ABCMeta): This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()` """ bounds = self.get_bounds() - assert(bounds is not None) + assert bounds is not None return bounds @@ -98,7 +98,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): ''' # offset property @property - def offset(self) -> Any: #TODO mypy#3003 NDArray[numpy.float64]: + def offset(self) -> Any: # TODO mypy#3003 NDArray[numpy.float64]: """ [x, y] offset """ diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index c8b1479..07fb440 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -3,11 +3,11 @@ from abc import ABCMeta, abstractmethod import numpy from numpy import pi -from numpy.typing import ArrayLike, NDArray +from numpy.typing import ArrayLike from .positionable import Positionable from ..error import MasqueError -from ..utils import is_scalar, rotation_matrix_2d +from ..utils import rotation_matrix_2d _empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass @@ -118,7 +118,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta): pivot = numpy.array(pivot, dtype=float) cast(Positionable, self).translate(-pivot) cast(Rotatable, self).rotate(rotation) - self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) #type: ignore #TODO: mypy#3004 + self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # TODO: mypy#3004 cast(Positionable, self).translate(+pivot) return self diff --git a/masque/utils/pack2d.py b/masque/utils/pack2d.py index 430cfee..150403c 100644 --- a/masque/utils/pack2d.py +++ b/masque/utils/pack2d.py @@ -11,31 +11,6 @@ from ..pattern import Pattern from ..ref import Ref -def pack_patterns( - library: Mapping[str, Pattern], - patterns: Sequence[str], - regions: numpy.ndarray, - spacing: Tuple[float, float], - presort: bool = True, - allow_rejects: bool = True, - packer: Callable = maxrects_bssf, - ) -> Tuple[Pattern, List[str]]: - half_spacing = numpy.array(spacing) / 2 - - bounds = [library[pp].get_bounds() for pp in patterns] - sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds] - offsets = [half_spacing - bb[0] if bb is not None else (0, 0) for bb in bounds] - - locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects) - - pat = Pattern() - pat.refs = [Ref(pp, offset=oo + loc) - for pp, oo, loc in zip(patterns, offsets, locations)] - - rejects = [patterns[ii] for ii in reject_inds] - return pat, rejects - - def maxrects_bssf( rects: ArrayLike, containers: ArrayLike, @@ -165,3 +140,28 @@ def guillotine_bssf_sas(rect_sizes: numpy.ndarray, new_region0, new_region1)) return rect_locs, rejected_inds + + +def pack_patterns( + library: Mapping[str, Pattern], + patterns: Sequence[str], + regions: numpy.ndarray, + spacing: Tuple[float, float], + presort: bool = True, + allow_rejects: bool = True, + packer: Callable = maxrects_bssf, + ) -> Tuple[Pattern, List[str]]: + half_spacing = numpy.array(spacing) / 2 + + bounds = [library[pp].get_bounds() for pp in patterns] + sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds] + offsets = [half_spacing - bb[0] if bb is not None else (0, 0) for bb in bounds] + + locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects) + + pat = Pattern() + pat.refs = [Ref(pp, offset=oo + loc) + for pp, oo, loc in zip(patterns, offsets, locations)] + + rejects = [patterns[ii] for ii in reject_inds] + return pat, rejects diff --git a/masque/utils/types.py b/masque/utils/types.py index b0d7acd..4e74306 100644 --- a/masque/utils/types.py +++ b/masque/utils/types.py @@ -1,7 +1,7 @@ """ Type definitions """ -from typing import Union, Tuple, Sequence, Dict, List +from typing import Union, Tuple, Dict, List layer_t = Union[int, Tuple[int, int], str] diff --git a/masque/utils/vertices.py b/masque/utils/vertices.py index 826b3ec..dd04b36 100644 --- a/masque/utils/vertices.py +++ b/masque/utils/vertices.py @@ -83,7 +83,7 @@ def poly_contains_points( max_bounds = numpy.max(vertices, axis=0)[None, :] trivially_outside = ((points < min_bounds).any(axis=1) - | (points > max_bounds).any(axis=1)) + | (points > max_bounds).any(axis=1)) # noqa: E128 nontrivial = ~trivially_outside if trivially_outside.all(): @@ -101,10 +101,10 @@ def poly_contains_points( dv = numpy.roll(verts, -1, axis=0) - verts is_left = (dv[:, 0] * (ntpts[..., 1] - verts[:, 1]) # >0 if left of dv, <0 if right, 0 if on the line - - dv[:, 1] * (ntpts[..., 0] - verts[:, 0])) + - dv[:, 1] * (ntpts[..., 0] - verts[:, 0])) # noqa: E128 winding_number = ((upward & (is_left > 0)).sum(axis=0) - - (downward & (is_left < 0)).sum(axis=0)) + - (downward & (is_left < 0)).sum(axis=0)) # noqa: E128 nontrivial_inside = winding_number != 0 # filter nontrivial points based on winding number if include_boundary: