flake8-aided fixes

This commit is contained in:
Jan Petykiewicz 2023-01-23 22:27:26 -08:00 committed by jan
parent db9b39dbc0
commit 6b01b43559
29 changed files with 243 additions and 351 deletions

View File

@ -15,7 +15,7 @@ to output to multiple formats.
Requirements: Requirements:
* python >= 3.8 * python >= 3.8
* numpy * 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`) * matplotlib (optional, used for `visualization` functions and `text`)
* ezdxf (optional, used for `dxf` i/o) * ezdxf (optional, used for `dxf` i/o)
* fatamorgana (optional, used for `oasis` 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 - boolean ops
* Construct polygons from bitmap using `skimage.find_contours` * Construct polygons from bitmap using `skimage.find_contours`
* Deal with shape repetitions for dxf, svg * 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

41
examples/pic2mask.py Normal file
View File

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

View File

@ -28,7 +28,7 @@
""" """
from .error import MasqueError, PatternError, LibraryError, BuildError 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 .label import Label
from .ref import Ref from .ref import Ref
from .pattern import Pattern from .pattern import Pattern

View File

@ -1,17 +1,12 @@
from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence from typing import Dict, Tuple, Union, TypeVar, Optional, Sequence
from typing import overload, KeysView, ValuesView, MutableMapping, Mapping from typing import MutableMapping, Mapping
import copy import copy
import warnings
import traceback
import logging import logging
from collections import Counter
from abc import ABCMeta
import numpy import numpy
from numpy import pi 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 ..pattern import Pattern
from ..ref import Ref from ..ref import Ref
from ..library import MutableLibrary from ..library import MutableLibrary
@ -241,8 +236,6 @@ class Builder(PortList):
`PortError` if applying the prefixes results in duplicate port `PortError` if applying the prefixes results in duplicate port
names. names.
""" """
from ..pattern import Pattern
if library is None: if library is None:
if hasattr(source, 'library') and isinstance(source, MutableLibrary): if hasattr(source, 'library') and isinstance(source, MutableLibrary):
library = source.library library = source.library

View File

@ -82,6 +82,7 @@ def pat2dev(
ports = {} ports = {}
annotated_cells = set() annotated_cells = set()
def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: def find_ports_each(pat, hierarchy, transform, memo) -> Pattern:
if len(hierarchy) > max_depth: if len(hierarchy) > max_depth:
if max_depth >= 999_999: if max_depth >= 999_999:

View File

@ -1,5 +1,5 @@
from typing import Dict, Tuple, List, Mapping, Sequence, SupportsFloat from typing import Dict, Mapping, Sequence, SupportsFloat
from typing import Optional, Union, Any, cast, TYPE_CHECKING from typing import Optional, Union, cast, TYPE_CHECKING
from pprint import pformat from pprint import pformat
import numpy import numpy

View File

@ -1,7 +1,7 @@
""" """
DXF file format readers and writers 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 re
import io import io
import base64 import base64
@ -17,7 +17,6 @@ from .. import Pattern, Ref, PatternError, Label, Shape
from ..shapes import Polygon, Path from ..shapes import Polygon, Path
from ..repetition import Grid from ..repetition import Grid
from ..utils import rotation_matrix_2d, layer_t from ..utils import rotation_matrix_2d, layer_t
from .gdsii import check_valid_names
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,8 +51,11 @@ def write(
DXF does not support shape repetition (only block repeptition). Please call DXF does not support shape repetition (only block 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()` Other functions you may want to call:
prior to calling this function. - `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 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 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? #TODO consider supporting DXF arcs?
check_valid_names(library.keys())
pattern = library[top_name] pattern = library[top_name]
# Create library # Create library
@ -83,7 +83,7 @@ def write(
# Now create a block for each referenced pattern, and add in any shapes # Now create a block for each referenced pattern, and add in any shapes
for name, pat in library.items(): for name, pat in library.items():
assert(pat is not None) assert pat is not None
block = lib.blocks.new(name=name) block = lib.blocks.new(name=name)
_shapes_to_elements(block, pat.shapes) _shapes_to_elements(block, pat.shapes)
@ -173,8 +173,9 @@ def read(
msp = lib.modelspace() msp = lib.modelspace()
npat = _read_block(msp, clean_vertices) npat = _read_block(msp, clean_vertices)
patterns_dict = dict([npat] patterns_dict = dict(
+ [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space']) [npat] + [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space']
)
library_info = { library_info = {
'layers': [ll.dxfattribs() for ll in lib.layers] '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. # Could set do paths with width setting, but need to consider endcaps.
for shape in shapes: for shape in shapes:
if shape.repetition is not None: if shape.repetition is not None:
raise PatternError('Shape repetitions are not supported by DXF.' raise PatternError(
' Please call library.wrap_repeated_shapes() before writing to file.') 'Shape repetitions are not supported by DXF.'
' Please call library.wrap_repeated_shapes() before writing to file.'
)
attribs = {'layer': _mlayer2dxf(shape.layer)} attribs = {'layer': _mlayer2dxf(shape.layer)}
for polygon in shape.to_polygons(): for polygon in shape.to_polygons():

View File

@ -18,14 +18,10 @@ Notes:
* GDS does not support library- or structure-level annotations * GDS does not support library- or structure-level annotations
* Creation/modification/access times are set to 1900-01-01 for reproducibility. * 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 List, Any, Dict, Tuple, Callable, Union, Iterable
from typing import Sequence, BinaryIO, Mapping, cast from typing import BinaryIO, Mapping
import re
import io import io
import mmap import mmap
import copy
import base64
import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
@ -83,10 +79,13 @@ def write(
otherwise `0` otherwise `0`
GDS does not support shape repetition (only cell repeptition). Please call 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()` Other functions you may want to call:
prior to calling this function. - `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: Args:
library: A {name: Pattern} mapping of patterns to write. library: A {name: Pattern} mapping of patterns to write.
@ -98,10 +97,6 @@ def write(
library_name: Library name written into the GDSII file. library_name: Library name written into the GDSII file.
Default 'masque-klamath'. Default 'masque-klamath'.
""" """
check_valid_names(library.keys())
# TODO check all hierarchy present
if not isinstance(library, MutableLibrary): if not isinstance(library, MutableLibrary):
if isinstance(library, dict): if isinstance(library, dict):
library = WrapLibrary(library) library = WrapLibrary(library)
@ -504,56 +499,6 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
return texts 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( def load_library(
stream: BinaryIO, stream: BinaryIO,
*, *,

View File

@ -12,11 +12,7 @@ Note that OASIS references follow the same convention as `masque`,
vectors or offsets. vectors or offsets.
""" """
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping, Optional, cast from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping, Optional, cast
import re
import io import io
import copy
import base64
import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
@ -77,8 +73,11 @@ def build(
If a layer map is provided, layer strings will be converted If a layer map is provided, layer strings will be converted
automatically, and layer names will be written to the file. automatically, and layer names will be written to the file.
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` Other functions you may want to call:
prior to calling this function. - `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: Args:
library: A {name: Pattern} mapping of patterns to write. library: A {name: Pattern} mapping of patterns to write.
@ -97,10 +96,6 @@ def build(
Returns: Returns:
`fatamorgana.OasisLayout` `fatamorgana.OasisLayout`
""" """
check_valid_names(library.keys())
# TODO check all hierarchy present
if not isinstance(library, MutableLibrary): if not isinstance(library, MutableLibrary):
if isinstance(library, dict): if isinstance(library, dict):
library = WrapLibrary(library) library = WrapLibrary(library)
@ -130,7 +125,7 @@ def build(
for tt in (True, False)] for tt in (True, False)]
def layer2oas(mlayer: layer_t) -> Tuple[int, int]: 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 layer_num = layer_map[mlayer] if isinstance(mlayer, str) else mlayer
return _mlayer2oas(layer_num) return _mlayer2oas(layer_num)
else: else:
@ -270,7 +265,7 @@ def read(
# note XELEMENT has no repetition # note XELEMENT has no repetition
continue continue
assert(not isinstance(element.repetition, fatamorgana.ReuseRepetition)) assert not isinstance(element.repetition, fatamorgana.ReuseRepetition)
repetition = repetition_fata2masq(element.repetition) repetition = repetition_fata2masq(element.repetition)
# Switch based on element type: # Switch based on element type:
@ -481,7 +476,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. 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)) xy = numpy.array((placement.x, placement.y))
mag = placement.magnification if placement.magnification is not None else 1 mag = placement.magnification if placement.magnification is not None else 1
@ -659,7 +654,7 @@ def repetition_masq2fata(
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore
offset = rep.displacements[0, :] offset = rep.displacements[0, :]
else: else:
assert(rep is None) assert rep is None
frep = None frep = None
offset = (0, 0) offset = (0, 0)
return frep, offset return frep, offset
@ -682,14 +677,14 @@ def properties_to_annotations(
) -> annotations_t: ) -> annotations_t:
annotations = {} annotations = {}
for proprec in properties: for proprec in properties:
assert(proprec.name is not None) assert proprec.name is not None
if isinstance(proprec.name, int): if isinstance(proprec.name, int):
key = propnames[proprec.name].string key = propnames[proprec.name].string
else: else:
key = proprec.name.string key = proprec.name.string
values: List[Union[str, float, int]] = [] values: List[Union[str, float, int]] = []
assert(proprec.values is not None) assert proprec.values is not None
for value in proprec.values: for value in proprec.values:
if isinstance(value, (float, int)): if isinstance(value, (float, int)):
values.append(value) values.append(value)

View File

@ -1,7 +1,7 @@
""" """
SVG file format readers and writers SVG file format readers and writers
""" """
from typing import Dict, Optional, Mapping from typing import Mapping
import warnings import warnings
import numpy import numpy

View File

@ -1,14 +1,11 @@
""" """
Helper functions for file reading and writing Helper functions for file reading and writing
""" """
from typing import Set, Tuple, List, Iterable, Mapping
import re import re
import copy
import pathlib import pathlib
import logging import logging
from .. import Pattern, PatternError from .. import Pattern, PatternError
from ..library import Library, WrapROLibrary
from ..shapes import Polygon, Path from ..shapes import Polygon, Path

View File

@ -1,4 +1,4 @@
from typing import Tuple, Dict, Optional, TypeVar from typing import Dict, Optional, TypeVar
import copy import copy
import numpy import numpy

View File

@ -3,9 +3,8 @@ Library class for managing unique name->pattern mappings and
deferred loading or creation. deferred loading or creation.
""" """
from typing import List, Dict, Callable, TypeVar, Generic, Type, TYPE_CHECKING 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 logging
import copy
import base64 import base64
import struct import struct
import re import re
@ -14,7 +13,7 @@ from collections import defaultdict
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy import numpy
from numpy.typing import ArrayLike, NDArray, NDArray from numpy.typing import ArrayLike
from .error import LibraryError, PatternError from .error import LibraryError, PatternError
from .utils import rotation_matrix_2d, normalize_mirror from .utils import rotation_matrix_2d, normalize_mirror
@ -45,21 +44,51 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
def __repr__(self) -> str: def __repr__(self) -> str:
return '<Library with keys ' + repr(list(self.keys())) + '>' return '<Library with keys ' + repr(list(self.keys())) + '>'
def referenced_patterns( def dangling_references(
self, self,
tops: Union[str, Sequence[str]], tops: Union[None, str, Sequence[str]] = None,
skip: Optional[Set[Optional[str]]] = None,
) -> Set[Optional[str]]: ) -> 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: 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. skip: Memo, set patterns which have already been traversed.
Returns: Returns:
Set of all referenced pattern names 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: if skip is None:
skip = set([None]) skip = set([None])
@ -73,17 +102,17 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
# Perform recursive lookups, but only once for each name # Perform recursive lookups, but only once for each name
for target in targets - skip: for target in targets - skip:
assert(target is not None) assert target is not None
self.referenced_patterns(target, skip) if target in self:
targets |= self.referenced_patterns(target, skip=skip)
skip.add(target) skip.add(target)
return targets return targets
# TODO maybe not for immutable?
def subtree( def subtree(
self, self,
tops: Union[str, Sequence[str]], tops: Union[str, Sequence[str]],
) -> Library: ) -> 'Library':
""" """
Return a new `Library`, containing only the specified patterns and the patterns they Return a new `Library`, containing only the specified patterns and the patterns they
reference (recursively). reference (recursively).
@ -184,7 +213,7 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
for top in tops: for top in tops:
flatten_single(top) flatten_single(top)
assert(None not in flattened.values()) assert None not in flattened.values()
return flattened # type: ignore return flattened # type: ignore
def get_name( def get_name(
@ -364,7 +393,7 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta):
#def __len__(self) -> int: #def __len__(self) -> int:
@abstractmethod @abstractmethod
def __setitem__(self, key: str, value: VVV) -> None: # TODO def __setitem__(self, key: str, value: VVV) -> None:
pass pass
@abstractmethod @abstractmethod
@ -390,7 +419,7 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta):
Args: Args:
other: The library to insert keys from 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. Should return `True` if the value from `self` should be used.
use_theirs: Decision function for name conflicts. Same format as `use_ours`. use_theirs: Decision function for name conflicts. Same format as `use_ours`.
Should return `True` if the value from `other` should be used. Should return `True` if the value from `other` should be used.
@ -451,13 +480,14 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta):
exclude_types = () exclude_types = ()
if label2name is None: 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_counts: MutableMapping[Tuple, int] = defaultdict(int)
shape_funcs = {} shape_funcs = {}
### First pass ### # ## First pass ##
# Using the label tuple from `.normalized_form()` as a key, check how many of each shape # 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 # are present and store the shape function for each one
for pat in tuple(self.values()): for pat in tuple(self.values()):
@ -476,7 +506,7 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta):
shape_pat = Pattern(shapes=[shape_func()]) shape_pat = Pattern(shapes=[shape_func()])
shape_pats[label] = shape_pat shape_pats[label] = shape_pat
### Second pass ### # ## Second pass ##
for pat in tuple(self.values()): for pat in tuple(self.values()):
# Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which # Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which
# are to be replaced. # are to be replaced.
@ -534,7 +564,9 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta):
from .pattern import Pattern from .pattern import Pattern
if name_func is None: 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()): for pat in tuple(self.values()):
new_shapes = [] new_shapes = []
@ -710,7 +742,7 @@ class LazyLibrary(MutableLibrary):
def clear_cache(self: LL) -> LL: def clear_cache(self: LL) -> LL:
""" """
Clear the cache of this library. 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. with another library.
Returns: Returns:

View File

@ -2,11 +2,10 @@
Base object representing a lithography mask. Base object representing a lithography mask.
""" """
from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload, cast from typing import List, Callable, Dict, Union, Set, Sequence, Optional, cast
from typing import Mapping, MutableMapping, Iterable, TypeVar, Any from typing import Mapping, TypeVar, Any
import copy import copy
from itertools import chain from itertools import chain
from collections import defaultdict
import numpy import numpy
from numpy import inf from numpy import inf
@ -14,9 +13,9 @@ from numpy.typing import NDArray, ArrayLike
# .visualize imports matplotlib and matplotlib.collections # .visualize imports matplotlib and matplotlib.collections
from .ref import Ref from .ref import Ref
from .shapes import Shape, Polygon from .shapes import Shape
from .label import Label 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 .error import PatternError
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable
from .ports import Port, PortList 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 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. (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] shapes: List[Shape]
""" List of all shapes in this Pattern. """ List of all shapes in this Pattern.
@ -116,7 +119,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
) )
return new 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, Appends all shapes, labels and refs from other_pattern to self's shapes,
labels, and supbatterns. labels, and supbatterns.
@ -321,7 +324,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
`[[x_min, y_min], [x_max, y_max]]` `[[x_min, y_min], [x_max, y_max]]`
""" """
bounds = self.get_bounds(library) bounds = self.get_bounds(library)
assert(bounds is not None) assert bounds is not None
return bounds return bounds
def translate_elements(self: P, offset: ArrayLike) -> P: def translate_elements(self: P, offset: ArrayLike) -> P:

View File

@ -1,6 +1,5 @@
from typing import Dict, Iterable, List, Tuple, Iterator, Optional, Sequence, MutableMapping from typing import Dict, Iterable, List, Tuple, KeysView, ValuesView, ItemsView
from typing import overload, KeysView, ValuesView, ItemsView, TYPE_CHECKING, Union, TypeVar, Any from typing import overload, Union, Optional, TypeVar
import copy
import warnings import warnings
import traceback import traceback
import logging import logging
@ -14,17 +13,11 @@ from numpy.typing import ArrayLike, NDArray
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
from .utils import AutoSlots, rotate_offsets_around from .utils import AutoSlots, rotate_offsets_around
from .error import PortError from .error import PortError
from .library import MutableLibrary
from .builder import Tool
if TYPE_CHECKING:
from .builder import Builder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
P = TypeVar('P', bound='Port') P = TypeVar('P', bound='Port')
PL = TypeVar('PL', bound='PortList') PL = TypeVar('PL', bound='PortList')
PL2 = TypeVar('PL2', bound='PortList') PL2 = TypeVar('PL2', bound='PortList')
@ -200,8 +193,6 @@ class PortList(metaclass=ABCMeta):
Returns: Returns:
self self
""" """
new_ports = { new_ports = {
names[0]: Port(offset, rotation=rotation, ptype=ptype), names[0]: Port(offset, rotation=rotation, ptype=ptype),
names[1]: Port(offset, rotation=rotation + pi, 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' type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
for st, ot in zip(s_types, o_types)]) for st, ot in zip(s_types, o_types)])
if type_conflicts.any(): if type_conflicts.any():
ports = numpy.where(type_conflicts)
msg = 'Ports have conflicting types:\n' msg = 'Ports have conflicting types:\n'
for nn, (k, v) in enumerate(map_in.items()): for nn, (k, v) in enumerate(map_in.items()):
if type_conflicts[nn]: if type_conflicts[nn]:
@ -353,7 +343,7 @@ class PortList(metaclass=ABCMeta):
if not numpy.allclose(rotations[:1], rotations): if not numpy.allclose(rotations[:1], rotations):
rot_deg = numpy.rad2deg(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()): for nn, (k, v) in enumerate(map_in.items()):
msg += f'{k} | {rot_deg[nn]:g} | {v}\n' msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
raise PortError(msg) raise PortError(msg)
@ -362,7 +352,7 @@ class PortList(metaclass=ABCMeta):
rotate_offsets_around(o_offsets, pivot, rotations[0]) rotate_offsets_around(o_offsets, pivot, rotations[0])
translations = s_offsets - o_offsets translations = s_offsets - o_offsets
if not numpy.allclose(translations[:1], translations): 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()): for nn, (k, v) in enumerate(map_in.items()):
msg += f'{k} | {translations[nn]} | {v}\n' msg += f'{k} | {translations[nn]} | {v}\n'
raise PortError(msg) raise PortError(msg)

View File

@ -4,7 +4,7 @@
""" """
#TODO more top-level documentation #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 copy
import numpy import numpy
@ -12,7 +12,7 @@ from numpy import pi
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from .error import PatternError from .error import PatternError
from .utils import is_scalar, AutoSlots, annotations_t from .utils import is_scalar, annotations_t
from .repetition import Repetition from .repetition import Repetition
from .traits import ( from .traits import (
PositionableImpl, RotatableImpl, ScalableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
@ -121,8 +121,8 @@ class Ref(
def as_pattern( def as_pattern(
self, self,
*, *,
pattern: Optional[Pattern] = None, pattern: Optional['Pattern'] = None,
library: Optional[Mapping[str, Pattern]] = None, library: Optional[Mapping[str, 'Pattern']] = None,
) -> 'Pattern': ) -> 'Pattern':
""" """
Args: Args:
@ -138,7 +138,7 @@ class Ref(
if library is None: if library is None:
raise PatternError('as_pattern() must be given a pattern or library.') 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 = library[self.target]
pattern = pattern.deepcopy() pattern = pattern.deepcopy()
@ -196,6 +196,8 @@ class Ref(
raise PatternError('as_pattern() must be given a pattern or library.') raise PatternError('as_pattern() must be given a pattern or library.')
if pattern is None and self.target is None: if pattern is None and self.target is None:
return 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() return self.as_pattern(pattern=pattern, library=library).get_bounds()
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@ -3,7 +3,7 @@
instances of an object . instances of an object .
""" """
from typing import Union, Dict, Optional, Sequence, Any, Type from typing import Union, Dict, Optional, Any, Type
import copy import copy
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod

View File

@ -171,9 +171,9 @@ class Arc(Shape, metaclass=AutoSlots):
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
if raw: if raw:
assert(isinstance(radii, numpy.ndarray)) assert isinstance(radii, numpy.ndarray)
assert(isinstance(angles, numpy.ndarray)) assert isinstance(angles, numpy.ndarray)
assert(isinstance(offset, numpy.ndarray)) assert isinstance(offset, numpy.ndarray)
self._radii = radii self._radii = radii
self._angles = angles self._angles = angles
self._width = width self._width = width

View File

@ -59,7 +59,7 @@ class Circle(Shape, metaclass=AutoSlots):
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
if raw: if raw:
assert(isinstance(offset, numpy.ndarray)) assert isinstance(offset, numpy.ndarray)
self._radius = radius self._radius = radius
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition

View File

@ -106,8 +106,8 @@ class Ellipse(Shape, metaclass=AutoSlots):
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
if raw: if raw:
assert(isinstance(radii, numpy.ndarray)) assert isinstance(radii, numpy.ndarray)
assert(isinstance(offset, numpy.ndarray)) assert isinstance(offset, numpy.ndarray)
self._radii = radii self._radii = radii
self._offset = offset self._offset = offset
self._rotation = rotation self._rotation = rotation

View File

@ -162,9 +162,9 @@ class Path(Shape, metaclass=AutoSlots):
self._cap_extensions = None # Since .cap setter might access it self._cap_extensions = None # Since .cap setter might access it
if raw: if raw:
assert(isinstance(vertices, numpy.ndarray)) assert isinstance(vertices, numpy.ndarray)
assert(isinstance(offset, numpy.ndarray)) assert isinstance(offset, numpy.ndarray)
assert(isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None) assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None
self._vertices = vertices self._vertices = vertices
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
@ -409,7 +409,7 @@ class Path(Shape, metaclass=AutoSlots):
if self.cap == PathCap.Square: if self.cap == PathCap.Square:
extensions = numpy.full(2, self.width / 2) extensions = numpy.full(2, self.width / 2)
elif self.cap == PathCap.SquareCustom: elif self.cap == PathCap.SquareCustom:
assert(isinstance(self.cap_extensions, numpy.ndarray)) assert isinstance(self.cap_extensions, numpy.ndarray)
extensions = self.cap_extensions extensions = self.cap_extensions
else: else:
# Flush or Circle # Flush or Circle

View File

@ -88,8 +88,8 @@ class Polygon(Shape, metaclass=AutoSlots):
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
if raw: if raw:
assert(isinstance(vertices, numpy.ndarray)) assert isinstance(vertices, numpy.ndarray)
assert(isinstance(offset, numpy.ndarray)) assert isinstance(offset, numpy.ndarray)
self._vertices = vertices self._vertices = vertices
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
@ -212,17 +212,17 @@ class Polygon(Shape, metaclass=AutoSlots):
""" """
if lx is None: if lx is None:
if xctr is None: if xctr is None:
assert(xmin is not None) assert xmin is not None
assert(xmax is not None) assert xmax is not None
xctr = 0.5 * (xmax + xmin) xctr = 0.5 * (xmax + xmin)
lx = xmax - xmin lx = xmax - xmin
elif xmax is None: elif xmax is None:
assert(xmin is not None) assert xmin is not None
assert(xctr is not None) assert xctr is not None
lx = 2 * (xctr - xmin) lx = 2 * (xctr - xmin)
elif xmin is None: elif xmin is None:
assert(xctr is not None) assert xctr is not None
assert(xmax is not None) assert xmax is not None
lx = 2 * (xmax - xctr) lx = 2 * (xmax - xctr)
else: else:
raise PatternError('Two of xmin, xctr, xmax, lx must be None!') 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: if xctr is not None:
pass pass
elif xmax is None: elif xmax is None:
assert(xmin is not None) assert xmin is not None
assert(lx is not None) assert lx is not None
xctr = xmin + 0.5 * lx xctr = xmin + 0.5 * lx
elif xmin is None: elif xmin is None:
assert(xmax is not None) assert xmax is not None
assert(lx is not None) assert lx is not None
xctr = xmax - 0.5 * lx xctr = xmax - 0.5 * lx
else: else:
raise PatternError('Two of xmin, xctr, xmax, lx must be None!') raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
if ly is None: if ly is None:
if yctr is None: if yctr is None:
assert(ymin is not None) assert ymin is not None
assert(ymax is not None) assert ymax is not None
yctr = 0.5 * (ymax + ymin) yctr = 0.5 * (ymax + ymin)
ly = ymax - ymin ly = ymax - ymin
elif ymax is None: elif ymax is None:
assert(ymin is not None) assert ymin is not None
assert(yctr is not None) assert yctr is not None
ly = 2 * (yctr - ymin) ly = 2 * (yctr - ymin)
elif ymin is None: elif ymin is None:
assert(yctr is not None) assert yctr is not None
assert(ymax is not None) assert ymax is not None
ly = 2 * (ymax - yctr) ly = 2 * (ymax - yctr)
else: else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!') 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: if yctr is not None:
pass pass
elif ymax is None: elif ymax is None:
assert(ymin is not None) assert ymin is not None
assert(ly is not None) assert ly is not None
yctr = ymin + 0.5 * ly yctr = ymin + 0.5 * ly
elif ymin is None: elif ymin is None:
assert(ly is not None) assert ly is not None
assert(ymax is not None) assert ymax is not None
yctr = ymax - 0.5 * ly yctr = ymax - 0.5 * ly
else: else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!') raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
@ -331,7 +331,6 @@ class Polygon(Shape, metaclass=AutoSlots):
poly.rotate(rotation) poly.rotate(rotation)
return poly return poly
def to_polygons( def to_polygons(
self, self,
poly_num_points: Optional[int] = None, # unused poly_num_points: Optional[int] = None, # unused

View File

@ -79,8 +79,8 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
if raw: if raw:
assert(isinstance(offset, numpy.ndarray)) assert isinstance(offset, numpy.ndarray)
assert(isinstance(mirrored, numpy.ndarray)) assert isinstance(mirrored, numpy.ndarray)
self._offset = offset self._offset = offset
self._layer = layer self._layer = layer
self._string = string self._string = string

View File

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

View File

@ -80,7 +80,7 @@ class Positionable(metaclass=ABCMeta):
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()` This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
""" """
bounds = self.get_bounds() bounds = self.get_bounds()
assert(bounds is not None) assert bounds is not None
return bounds return bounds

View File

@ -3,11 +3,11 @@ from abc import ABCMeta, abstractmethod
import numpy import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike
from .positionable import Positionable from .positionable import Positionable
from ..error import MasqueError 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 _empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass

View File

@ -11,31 +11,6 @@ from ..pattern import Pattern
from ..ref import Ref 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( def maxrects_bssf(
rects: ArrayLike, rects: ArrayLike,
containers: ArrayLike, containers: ArrayLike,
@ -165,3 +140,28 @@ def guillotine_bssf_sas(rect_sizes: numpy.ndarray,
new_region0, new_region1)) new_region0, new_region1))
return rect_locs, rejected_inds 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

View File

@ -1,7 +1,7 @@
""" """
Type definitions 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] layer_t = Union[int, Tuple[int, int], str]

View File

@ -83,7 +83,7 @@ def poly_contains_points(
max_bounds = numpy.max(vertices, axis=0)[None, :] max_bounds = numpy.max(vertices, axis=0)[None, :]
trivially_outside = ((points < min_bounds).any(axis=1) 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 nontrivial = ~trivially_outside
if trivially_outside.all(): if trivially_outside.all():
@ -101,10 +101,10 @@ def poly_contains_points(
dv = numpy.roll(verts, -1, axis=0) - verts 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 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) 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 nontrivial_inside = winding_number != 0 # filter nontrivial points based on winding number
if include_boundary: if include_boundary: