flake8-aided fixes

master
Jan Petykiewicz 1 year ago committed by jan
parent db9b39dbc0
commit 6b01b43559

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

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

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

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

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

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

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

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

@ -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:
@ -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.
"""
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
@ -659,7 +654,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
@ -682,14 +677,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)

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

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

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

@ -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 '<Library with keys ' + repr(list(self.keys())) + '>'
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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save