From a4fe3d9e2eaf73251c75de6d9326036b6da27ad3 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 23 Feb 2022 15:47:38 -0800 Subject: [PATCH] Use ArrayLike and NDArray wherever possible. Some type fixes and some related corner cases --- masque/builder/devices.py | 50 +++++++++++++++++---------- masque/builder/utils.py | 14 +++++--- masque/file/gdsii.py | 9 +++-- masque/file/oasis.py | 9 +++-- masque/file/python_gdsii.py | 18 +++++----- masque/file/svg.py | 10 +++--- masque/label.py | 12 ++++--- masque/pattern.py | 24 +++++++------ masque/repetition.py | 59 +++++++++++++++++--------------- masque/shapes/arc.py | 36 +++++++++++--------- masque/shapes/circle.py | 12 ++++--- masque/shapes/ellipse.py | 21 +++++++----- masque/shapes/path.py | 38 +++++++++++---------- masque/shapes/polygon.py | 28 ++++++++------- masque/shapes/shape.py | 64 ++++++++++++++++++++--------------- masque/shapes/text.py | 25 ++++++++------ masque/subpattern.py | 17 +++++----- masque/traits/positionable.py | 44 ++++++++++++------------ masque/traits/rotatable.py | 11 +++--- masque/utils.py | 12 +++---- 20 files changed, 290 insertions(+), 223 deletions(-) diff --git a/masque/builder/devices.py b/masque/builder/devices.py index 1dbd313..15ed45a 100644 --- a/masque/builder/devices.py +++ b/masque/builder/devices.py @@ -6,14 +6,14 @@ import traceback import logging from collections import Counter -import numpy # type: ignore +import numpy from numpy import pi -from numpy.typing import ArrayLike +from numpy.typing import ArrayLike, NDArray from ..pattern import Pattern from ..subpattern import SubPattern from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable -from ..utils import AutoSlots, rotation_matrix_2d, vector2 +from ..utils import AutoSlots, rotation_matrix_2d from ..error import DeviceError @@ -48,11 +48,12 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met ptype: str """ Port types must match to be plugged together if both are non-zero """ - def __init__(self, - offset: ArrayLike, - rotation: Optional[float], - ptype: str = 'unk', - ) -> None: + def __init__( + self, + offset: ArrayLike, + rotation: Optional[float], + ptype: str = 'unk', + ) -> None: self.offset = offset self.rotation = rotation self.ptype = ptype @@ -63,7 +64,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met return self._rotation @rotation.setter - def rotation(self, val: float): + def rotation(self, val: float) -> None: if val is None: self._rotation = None else: @@ -207,6 +208,21 @@ class Device(Copyable, Mirrorable): def __getitem__(self, key: Union[List[str], Tuple[str], KeysView[str], ValuesView[str]]) -> Dict[str, Port]: pass +#======= +## from typing import overload +## from _collections_abc import dict_keys, dict_values +## +## @overload +## def __getitem__(self, key: str) -> Port: +## pass +## +## @overload +## def __getitem__(self, key: Union[List[str], Tuple[str], dict_keys[str, str], dict_values[str, str]]) -> Dict[str, Port]: +### def __getitem__(self, key: Iterable[str]) -> Dict[str, Port]: +## pass +# +## def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, Dict[str, Port]]: +# def __getitem__(self, key: Union[str, Iterable[str]]) -> Any: def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, Dict[str, Port]]: """ For convenience, ports can be read out using square brackets: @@ -504,9 +520,9 @@ class Device(Copyable, Mirrorable): self: D, other: O, *, - offset: vector2 = (0, 0), + offset: ArrayLike = (0, 0), rotation: float = 0, - pivot: vector2 = (0, 0), + pivot: ArrayLike = (0, 0), mirrored: Tuple[bool, bool] = (False, False), port_map: Optional[Dict[str, Optional[str]]] = None, skip_port_check: bool = False, @@ -585,7 +601,7 @@ class Device(Copyable, Mirrorable): *, mirrored: Tuple[bool, bool] = (False, False), set_rotation: Optional[bool] = None, - ) -> Tuple[numpy.ndarray, float, numpy.ndarray]: + ) -> Tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]: """ Given a device `other` and a mapping `map_in` specifying port connections, find the transform which will correctly align the specified ports. @@ -668,7 +684,7 @@ class Device(Copyable, Mirrorable): return translations[0], rotations[0], o_offsets[0] - def translate(self: D, offset: vector2) -> D: + def translate(self: D, offset: ArrayLike) -> D: """ Translate the pattern and all ports. @@ -683,7 +699,7 @@ class Device(Copyable, Mirrorable): port.translate(offset) return self - def rotate_around(self: D, pivot: vector2, angle: float) -> D: + def rotate_around(self: D, pivot: ArrayLike, angle: float) -> D: """ Translate the pattern and all ports. @@ -753,10 +769,10 @@ class Device(Copyable, Mirrorable): def rotate_offsets_around( - offsets: ArrayLike, - pivot: ArrayLike, + offsets: NDArray[numpy.float64], + pivot: NDArray[numpy.float64], angle: float, - ) -> numpy.ndarray: + ) -> NDArray[numpy.float64]: offsets -= pivot offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T offsets += pivot diff --git a/masque/builder/utils.py b/masque/builder/utils.py index 872a1c3..7ff8920 100644 --- a/masque/builder/utils.py +++ b/masque/builder/utils.py @@ -1,11 +1,12 @@ from typing import Dict, Tuple, List, Optional, Union, Any, cast, Sequence from pprint import pformat -import numpy # type: ignore +import numpy from numpy import pi +from numpy.typing import ArrayLike from .devices import Port -from ..utils import rotation_matrix_2d, vector2 +from ..utils import rotation_matrix_2d from ..error import BuildError @@ -13,9 +14,9 @@ def ell( ports: Dict[str, Port], ccw: Optional[bool], bound_type: str, - bound: Union[float, vector2], + bound: Union[float, ArrayLike], *, - spacing: Optional[Union[float, numpy.ndarray]] = None, + spacing: Optional[Union[float, ArrayLike]] = None, set_rotation: Optional[float] = None, ) -> Dict[str, float]: """ @@ -96,8 +97,11 @@ def ell( rotations[~has_rotation] = rotations[has_rotation][0] if not numpy.allclose(rotations[0], rotations): + port_rotations = {k: numpy.rad2deg(p.rotation) if p.rotation is not None else None + for k, p in ports.items()} + raise BuildError('Asked to find aggregation for ports that face in different directions:\n' - + pformat({k: numpy.rad2deg(p.rotation) for k, p in ports.items()})) + + pformat(port_rotations)) else: if set_rotation is not None: raise BuildError('set_rotation must be specified if no ports have rotations!') diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index e4583fe..6bea759 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -30,7 +30,8 @@ import logging import pathlib import gzip -import numpy # type: ignore +import numpy +from numpy.typing import NDArray import klamath from klamath import records @@ -369,10 +370,12 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library. properties = _annotations_to_properties(subpat.annotations, 512) if isinstance(rep, Grid): - xy = numpy.array(subpat.offset) + [ + b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) + b_count = rep.b_count if rep.b_count is not None else 1 + xy: NDArray[numpy.float64] = numpy.array(subpat.offset) + [ [0, 0], rep.a_vector * rep.a_count, - rep.b_vector * rep.b_count, + b_vector * b_count, ] aref = klamath.library.Reference(struct_name=encoded_name, xy=numpy.round(xy).astype(int), diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 89b0a11..f9b8d65 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -198,8 +198,7 @@ def writefile( open_func = open with io.BufferedWriter(open_func(path, mode='wb')) as stream: - results = write(patterns, stream, *args, **kwargs) - return results + write(patterns, stream, *args, **kwargs) def readfile( @@ -491,10 +490,14 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo pname = placement.get_name() name = pname if isinstance(pname, int) else pname.string annotations = properties_to_annotations(placement.properties, lib.propnames, lib.propstrings) + if placement.angle is None: + rotation = 0 + else: + rotation = numpy.deg2rad(float(placement.angle)) subpat = SubPattern(offset=xy, pattern=None, mirrored=(placement.flip, False), - rotation=numpy.deg2rad(placement.angle), + rotation=rotation, scale=float(mag), identifier=(name,), repetition=repetition_fata2masq(placement.repetition), diff --git a/masque/file/python_gdsii.py b/masque/file/python_gdsii.py index b160f65..b929a5b 100644 --- a/masque/file/python_gdsii.py +++ b/masque/file/python_gdsii.py @@ -28,11 +28,12 @@ import logging import pathlib import gzip -import numpy # type: ignore +import numpy +from numpy.typing import ArrayLike, NDArray # python-gdsii -import gdsii.library -import gdsii.structure -import gdsii.elements +import gdsii.library #type: ignore +import gdsii.structure #type: ignore +import gdsii.elements #type: ignore from .utils import clean_pattern_vertices, is_gzipped from .. import Pattern, SubPattern, PatternError, Label, Shape @@ -182,8 +183,7 @@ def writefile( open_func = open with io.BufferedWriter(open_func(path, mode='wb')) as stream: - results = write(patterns, stream, *args, **kwargs) - return results + write(patterns, stream, *args, **kwargs) def readfile( @@ -402,10 +402,12 @@ def _subpatterns_to_refs( new_refs: List[Union[gdsii.elements.SRef, gdsii.elements.ARef]] ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] if isinstance(rep, Grid): - xy = numpy.array(subpat.offset) + [ + b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) + b_count = rep.b_count if rep.b_count is not None else 1 + xy: NDArray[numpy.float64] = numpy.array(subpat.offset) + [ [0, 0], rep.a_vector * rep.a_count, - rep.b_vector * rep.b_count, + b_vector * b_count, ] ref = gdsii.elements.ARef(struct_name=encoded_name, xy=numpy.round(xy).astype(int), diff --git a/masque/file/svg.py b/masque/file/svg.py index 0b3ef85..58c9c6a 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -4,7 +4,8 @@ SVG file format readers and writers from typing import Dict, Optional import warnings -import numpy # type: ignore +import numpy +from numpy.typing import ArrayLike import svgwrite # type: ignore from .utils import mangle_name @@ -141,7 +142,7 @@ def writefile_inverted(pattern: Pattern, filename: str): svg.save() -def poly2path(vertices: numpy.ndarray) -> str: +def poly2path(vertices: ArrayLike) -> str: """ Create an SVG path string from an Nx2 list of vertices. @@ -151,8 +152,9 @@ def poly2path(vertices: numpy.ndarray) -> str: Returns: SVG path-string. """ - commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1]) - for vertex in vertices[1:]: + verts = numpy.array(vertices, copy=False) + commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1]) + for vertex in verts[1:]: commands += 'L{:g},{:g}'.format(vertex[0], vertex[1]) commands += ' Z ' return commands diff --git a/masque/label.py b/masque/label.py index 57fe4ca..baa7305 100644 --- a/masque/label.py +++ b/masque/label.py @@ -1,9 +1,11 @@ from typing import Tuple, Dict, Optional, TypeVar import copy -import numpy # type: ignore + +import numpy +from numpy.typing import ArrayLike, NDArray from .repetition import Repetition -from .utils import vector2, rotation_matrix_2d, layer_t, AutoSlots, annotations_t +from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl from .traits import AnnotatableImpl @@ -43,7 +45,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot self, string: str, *, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), layer: layer_t = 0, repetition: Optional[Repetition] = None, annotations: Optional[annotations_t] = None, @@ -74,7 +76,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot new.set_locked(self.locked) return new - def rotate_around(self: L, pivot: vector2, rotation: float) -> L: + def rotate_around(self: L, pivot: ArrayLike, rotation: float) -> L: """ Rotate the label around a point. @@ -91,7 +93,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot self.translate(+pivot) return self - def get_bounds(self) -> numpy.ndarray: + def get_bounds(self) -> NDArray[numpy.float64]: """ Return the bounds of the label. diff --git a/masque/pattern.py b/masque/pattern.py index 3fef604..2ed8251 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -9,21 +9,21 @@ import pickle from itertools import chain from collections import defaultdict -import numpy # type: ignore +import numpy from numpy import inf -from numpy.typing import ArrayLike +from numpy.typing import NDArray, ArrayLike # .visualize imports matplotlib and matplotlib.collections from .subpattern import SubPattern from .shapes import Shape, Polygon from .label import Label -from .utils import rotation_matrix_2d, vector2, normalize_mirror, AutoSlots, annotations_t +from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t from .error import PatternError, PatternLockedError from .traits import LockableImpl, AnnotatableImpl, Scalable, Mirrorable from .traits import Rotatable, Positionable -visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern'] +visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern'] P = TypeVar('P', bound='Pattern') @@ -236,7 +236,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): self: P, visit_before: visitor_function_t = None, visit_after: visitor_function_t = None, - transform: Union[numpy.ndarray, bool, None] = False, + transform: Union[ArrayLike, bool, None] = False, memo: Optional[Dict] = None, hierarchy: Tuple[P, ...] = (), ) -> P: @@ -283,6 +283,8 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): if transform is None or transform is True: transform = numpy.zeros(4) + elif transform is not False: + transform = numpy.array(transform) if self in hierarchy: raise PatternError('.dfs() called on pattern with circular reference') @@ -441,7 +443,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): return self - def as_polygons(self) -> List[numpy.ndarray]: + def as_polygons(self) -> List[NDArray[numpy.float64]]: """ Represents the pattern as a list of polygons. @@ -510,6 +512,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): List of `(pat.name, pat)` tuples for all referenced Pattern objects """ pats_by_id = self.referenced_patterns_by_id(**kwargs) + pat_list: List[Tuple[Optional[str], Optional['Pattern']]] pat_list = [(p.name if p is not None else None, p) for p in pats_by_id.values()] return pat_list @@ -539,7 +542,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): ids.update(pat.subpatterns_by_id(include_none=include_none)) return dict(ids) - def get_bounds(self) -> Union[numpy.ndarray, None]: + def get_bounds(self) -> Union[NDArray[numpy.float64], None]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the Pattern's contents in each dimension. @@ -553,6 +556,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): min_bounds = numpy.array((+inf, +inf)) max_bounds = numpy.array((-inf, -inf)) + for entry in chain(self.shapes, self.subpatterns, self.labels): bounds = entry.get_bounds() if bounds is None: @@ -655,7 +659,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): return self - def translate_elements(self: P, offset: vector2) -> P: + def translate_elements(self: P, offset: ArrayLike) -> P: """ Translates all shapes, label, and subpatterns by the given offset. @@ -702,7 +706,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): label.offset *= c return self - def rotate_around(self: P, pivot: vector2, rotation: float) -> P: + def rotate_around(self: P, pivot: ArrayLike, rotation: float) -> P: """ Rotate the Pattern around the a location. @@ -945,7 +949,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): def visualize( self, - offset: vector2 = (0., 0.), + offset: ArrayLike = (0., 0.), line_color: str = 'k', fill_color: str = 'none', overdraw: bool = False, diff --git a/masque/repetition.py b/masque/repetition.py index 032ac89..7cc949a 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -7,11 +7,11 @@ from typing import Union, Dict, Optional, Sequence, Any import copy from abc import ABCMeta, abstractmethod -import numpy # type: ignore -from numpy.typing import ArrayLike +import numpy +from numpy.typing import ArrayLike, NDArray from .error import PatternError -from .utils import rotation_matrix_2d, vector2, AutoSlots +from .utils import rotation_matrix_2d, AutoSlots from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable @@ -23,7 +23,7 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta): @property @abstractmethod - def displacements(self) -> numpy.ndarray: + def displacements(self) -> NDArray[numpy.float64]: """ An Nx2 ndarray specifying all offsets generated by this repetition """ @@ -44,7 +44,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): '_a_count', '_b_count') - _a_vector: numpy.ndarray + _a_vector: NDArray[numpy.float64] """ Vector `[x, y]` specifying the first lattice vector of the grid. Specifies center-to-center spacing between adjacent elements. """ @@ -52,7 +52,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): _a_count: int """ Number of instances along the direction specified by the `a_vector` """ - _b_vector: Optional[numpy.ndarray] + _b_vector: Optional[NDArray[numpy.float64]] """ Vector `[x, y]` specifying a second lattice vector for the grid. Specifies center-to-center spacing between adjacent elements. Can be `None` for a 1D array. @@ -100,8 +100,8 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): raise PatternError(f'Repetition has too-small b_count: {b_count}') object.__setattr__(self, 'locked', False) - self.a_vector = a_vector - self.b_vector = b_vector + self.a_vector = a_vector # type: ignore # setter handles type conversion + self.b_vector = b_vector # type: ignore # setter handles type conversion self.a_count = a_count self.b_count = b_count self.locked = locked @@ -122,11 +122,11 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): # a_vector property @property - def a_vector(self) -> numpy.ndarray: + def a_vector(self) -> NDArray[numpy.float64]: return self._a_vector @a_vector.setter - def a_vector(self, val: vector2): + def a_vector(self, val: ArrayLike) -> None: if not isinstance(val, numpy.ndarray): val = numpy.array(val, dtype=float) @@ -136,11 +136,11 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): # b_vector property @property - def b_vector(self) -> numpy.ndarray: + def b_vector(self) -> Optional[NDArray[numpy.float64]]: return self._b_vector @b_vector.setter - def b_vector(self, val: vector2): + def b_vector(self, val: ArrayLike) -> None: if not isinstance(val, numpy.ndarray): val = numpy.array(val, dtype=float, copy=True) @@ -154,7 +154,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): return self._a_count @a_count.setter - def a_count(self, val: int): + def a_count(self, val: int) -> None: if val != int(val): raise PatternError('a_count must be convertable to an int!') self._a_count = int(val) @@ -165,13 +165,16 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): return self._b_count @b_count.setter - def b_count(self, val: int): + def b_count(self, val: int) -> None: if val != int(val): raise PatternError('b_count must be convertable to an int!') self._b_count = int(val) @property - def displacements(self) -> numpy.ndarray: + def displacements(self) -> NDArray[numpy.float64]: + if self.b_vector is None: + return numpy.arange(self.a_count)[:, None] * self.a_vector[None, :] + aa, bb = numpy.meshgrid(numpy.arange(self.a_count), numpy.arange(self.b_count), indexing='ij') return (aa.flatten()[:, None] * self.a_vector[None, :] + bb.flatten()[:, None] * self.b_vector[None, :]) # noqa @@ -207,7 +210,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): self.b_vector[1 - axis] *= -1 return self - def get_bounds(self) -> Optional[numpy.ndarray]: + def get_bounds(self) -> Optional[NDArray[numpy.float64]]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the `Grid` in each dimension. @@ -216,7 +219,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots): `[[x_min, y_min], [x_max, y_max]]` or `None` """ a_extent = self.a_vector * self.a_count - b_extent = self.b_vector * self.b_count if self.b_count != 0 else 0 + b_extent = self.b_vector * self.b_count if (self.b_vector is not None) else 0 # type: Union[NDArray[numpy.float64], float] corners = ((0, 0), a_extent, b_extent, a_extent + b_extent) xy_min = numpy.min(corners, axis=0) @@ -296,24 +299,26 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): `[[x0, y0], [x1, y1], ...]` """ - _displacements: numpy.ndarray + _displacements: NDArray[numpy.float64] """ List of vectors `[[x0, y0], [x1, y1], ...]` specifying the offsets of the instances. """ @property - def displacements(self) -> numpy.ndarray: + def displacements(self) -> Any: # TODO: mypy#3004 NDArray[numpy.float64]: return self._displacements @displacements.setter - def displacements(self, val: ArrayLike): - val = numpy.array(val, float) - val = numpy.sort(val.view([('', val.dtype)] * val.shape[1]), 0).view(val.dtype) # sort rows - self._displacements = val + def displacements(self, val: ArrayLike) -> None: + vala: NDArray[numpy.float64] = numpy.array(vala, dtype=float) + vala = numpy.sort(vala.view([('', vala.dtype)] * vala.shape[1]), 0).view(vala.dtype) # sort rows + self._displacements = vala - def __init__(self, - displacements: ArrayLike, - locked: bool = False,): + def __init__( + self, + displacements: ArrayLike, + locked: bool = False, + ) -> None: """ Args: displacements: List of vectors (Nx2 ndarray) specifying displacements. @@ -383,7 +388,7 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): self.displacements[1 - axis] *= -1 return self - def get_bounds(self) -> Optional[numpy.ndarray]: + def get_bounds(self) -> Optional[NDArray[numpy.float64]]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the `displacements` in each dimension. diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 3bea013..fbde98d 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,14 +1,15 @@ -from typing import List, Dict, Optional, Sequence +from typing import List, Dict, Optional, Sequence, Any import copy import math -import numpy # type: ignore +import numpy from numpy import pi +from numpy.typing import NDArray, ArrayLike from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t +from ..utils import is_scalar, layer_t, AutoSlots, annotations_t from ..traits import LockableImpl @@ -24,13 +25,13 @@ class Arc(Shape, metaclass=AutoSlots): __slots__ = ('_radii', '_angles', '_width', '_rotation', 'poly_num_points', 'poly_max_arclen') - _radii: numpy.ndarray + _radii: NDArray[numpy.float64] """ Two radii for defining an ellipse """ _rotation: float """ Rotation (ccw, radians) from the x axis to the first radius """ - _angles: numpy.ndarray + _angles: NDArray[numpy.float64] """ Start and stop angles (ccw, radians) for choosing an arc from the ellipse, measured from the first radius """ _width: float @@ -44,14 +45,14 @@ class Arc(Shape, metaclass=AutoSlots): # radius properties @property - def radii(self) -> numpy.ndarray: + def radii(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ return self._radii @radii.setter - def radii(self, val: vector2) -> None: + def radii(self, val: ArrayLike) -> None: val = numpy.array(val, dtype=float).flatten() if not val.size == 2: raise PatternError('Radii must have length 2') @@ -81,7 +82,7 @@ class Arc(Shape, metaclass=AutoSlots): # arc start/stop angle properties @property - def angles(self) -> numpy.ndarray: + 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 @@ -92,7 +93,7 @@ class Arc(Shape, metaclass=AutoSlots): return self._angles @angles.setter - def angles(self, val: vector2) -> None: + def angles(self, val: ArrayLike) -> None: val = numpy.array(val, dtype=float).flatten() if not val.size == 2: raise PatternError('Angles must have length 2') @@ -152,13 +153,13 @@ class Arc(Shape, metaclass=AutoSlots): def __init__( self, - radii: vector2, - angles: vector2, + radii: ArrayLike, + angles: ArrayLike, width: float, *, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_max_arclen: Optional[float] = None, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, @@ -171,6 +172,9 @@ class Arc(Shape, metaclass=AutoSlots): LockableImpl.unlock(self) self.identifier = () if raw: + assert(isinstance(radii, numpy.ndarray)) + assert(isinstance(angles, numpy.ndarray)) + assert(isinstance(offset, numpy.ndarray)) self._radii = radii self._angles = angles self._width = width @@ -241,7 +245,7 @@ class Arc(Shape, metaclass=AutoSlots): wh = self.width / 2.0 if wh == r0 and r0 == r1: - thetas_inner = [0] # Don't generate multiple vertices if we're at the origin + thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin else: thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_points, endpoint=True) thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], num_points, endpoint=True) @@ -261,7 +265,7 @@ class Arc(Shape, metaclass=AutoSlots): poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation) return [poly] - def get_bounds(self) -> numpy.ndarray: + def get_bounds(self) -> NDArray[numpy.float64]: ''' Equation for rotated ellipse is `x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)` @@ -367,7 +371,7 @@ class Arc(Shape, metaclass=AutoSlots): (self.offset, scale / norm_value, rotation, False, self.dose), lambda: Arc(radii=radii * norm_value, angles=angles, width=width * norm_value, layer=self.layer)) - def get_cap_edges(self) -> numpy.ndarray: + def get_cap_edges(self) -> NDArray[numpy.float64]: ''' Returns: ``` @@ -397,7 +401,7 @@ class Arc(Shape, metaclass=AutoSlots): maxs.append([xp, yp]) return numpy.array([mins, maxs]) + self.offset - def _angles_to_parameters(self) -> numpy.ndarray: + def _angles_to_parameters(self) -> NDArray[numpy.float64]: ''' Returns: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 44c9896..505f009 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -1,13 +1,14 @@ from typing import List, Dict, Optional import copy -import numpy # type: ignore +import numpy from numpy import pi +from numpy.typing import NDArray, ArrayLike from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t +from ..utils import is_scalar, layer_t, AutoSlots, annotations_t from ..traits import LockableImpl @@ -48,7 +49,7 @@ class Circle(Shape, metaclass=AutoSlots): *, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_max_arclen: Optional[float] = None, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, @@ -59,6 +60,7 @@ class Circle(Shape, metaclass=AutoSlots): LockableImpl.unlock(self) self.identifier = () if raw: + assert(isinstance(offset, numpy.ndarray)) self._radius = radius self._offset = offset self._repetition = repetition @@ -98,7 +100,7 @@ class Circle(Shape, metaclass=AutoSlots): raise PatternError('Number of points and arclength left ' 'unspecified (default was also overridden)') - n = [] + n: List[float] = [] if poly_num_points is not None: n += [poly_num_points] if poly_max_arclen is not None: @@ -111,7 +113,7 @@ class Circle(Shape, metaclass=AutoSlots): return [Polygon(xys, offset=self.offset, dose=self.dose, layer=self.layer)] - def get_bounds(self) -> numpy.ndarray: + def get_bounds(self) -> NDArray[numpy.float64]: return numpy.vstack((self.offset - self.radius, self.offset + self.radius)) diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 51b7eef..a018a79 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,14 +1,15 @@ -from typing import List, Dict, Sequence, Optional +from typing import List, Dict, Sequence, Optional, Any import copy import math -import numpy # type: ignore +import numpy from numpy import pi +from numpy.typing import ArrayLike, NDArray from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots, annotations_t +from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots, annotations_t from ..traits import LockableImpl @@ -20,7 +21,7 @@ class Ellipse(Shape, metaclass=AutoSlots): __slots__ = ('_radii', '_rotation', 'poly_num_points', 'poly_max_arclen') - _radii: numpy.ndarray + _radii: NDArray[numpy.float64] """ Ellipse radii """ _rotation: float @@ -34,14 +35,14 @@ class Ellipse(Shape, metaclass=AutoSlots): # radius properties @property - def radii(self) -> numpy.ndarray: + def radii(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ return self._radii @radii.setter - def radii(self, val: vector2) -> None: + def radii(self, val: ArrayLike) -> None: val = numpy.array(val).flatten() if not val.size == 2: raise PatternError('Radii must have length 2') @@ -89,11 +90,11 @@ class Ellipse(Shape, metaclass=AutoSlots): def __init__( self, - radii: vector2, + radii: ArrayLike, *, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_max_arclen: Optional[float] = None, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, @@ -106,6 +107,8 @@ class Ellipse(Shape, metaclass=AutoSlots): LockableImpl.unlock(self) self.identifier = () if raw: + assert(isinstance(radii, numpy.ndarray)) + assert(isinstance(offset, numpy.ndarray)) self._radii = radii self._offset = offset self._rotation = rotation @@ -173,7 +176,7 @@ class Ellipse(Shape, metaclass=AutoSlots): poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation) return [poly] - def get_bounds(self) -> numpy.ndarray: + def get_bounds(self) -> NDArray[numpy.float64]: rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii) return numpy.vstack((self.offset - rot_radii[0], self.offset + rot_radii[1])) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index b73c9cd..3bdffea 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,15 +1,15 @@ -from typing import List, Tuple, Dict, Optional, Sequence +from typing import List, Tuple, Dict, Optional, Sequence, Any import copy from enum import Enum -import numpy # type: ignore +import numpy from numpy import pi, inf -from numpy.typing import ArrayLike +from numpy.typing import NDArray, ArrayLike from . import Shape, normalized_shape_tuple, Polygon, Circle from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots +from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t from ..traits import LockableImpl @@ -30,10 +30,10 @@ class Path(Shape, metaclass=AutoSlots): A normalized_form(...) is available, but can be quite slow with lots of vertices. """ __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions') - _vertices: numpy.ndarray + _vertices: NDArray[numpy.float64] _width: float _cap: PathCap - _cap_extensions: Optional[numpy.ndarray] + _cap_extensions: Optional[NDArray[numpy.float64]] Cap = PathCap @@ -73,7 +73,7 @@ class Path(Shape, metaclass=AutoSlots): # cap_extensions property @property - def cap_extensions(self) -> Optional[numpy.ndarray]: + def cap_extensions(self) -> Optional[Any]: #TODO mypy#3004 NDArray[numpy.float64]]: """ Path end-cap extension @@ -83,7 +83,7 @@ class Path(Shape, metaclass=AutoSlots): return self._cap_extensions @cap_extensions.setter - def cap_extensions(self, vals: Optional[numpy.ndarray]) -> None: + def cap_extensions(self, vals: Optional[ArrayLike]) -> None: custom_caps = (PathCap.SquareCustom,) if self.cap in custom_caps: if vals is None: @@ -96,7 +96,7 @@ class Path(Shape, metaclass=AutoSlots): # vertices property @property - def vertices(self) -> numpy.ndarray: + def vertices(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]]: """ Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ @@ -113,7 +113,7 @@ class Path(Shape, metaclass=AutoSlots): # xs property @property - def xs(self) -> numpy.ndarray: + def xs(self) -> NDArray[numpy.float64]: """ All vertex x coords as a 1D ndarray """ @@ -128,7 +128,7 @@ class Path(Shape, metaclass=AutoSlots): # ys property @property - def ys(self) -> numpy.ndarray: + def ys(self) -> NDArray[numpy.float64]: """ All vertex y coords as a 1D ndarray """ @@ -148,7 +148,7 @@ class Path(Shape, metaclass=AutoSlots): *, cap: PathCap = PathCap.Flush, cap_extensions: Optional[ArrayLike] = None, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, @@ -163,6 +163,9 @@ class Path(Shape, metaclass=AutoSlots): self.identifier = () if raw: + 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 @@ -199,11 +202,11 @@ class Path(Shape, metaclass=AutoSlots): @staticmethod def travel( - travel_pairs: Tuple[Tuple[float, float]], + travel_pairs: Sequence[Tuple[float, float]], width: float = 0.0, cap: PathCap = PathCap.Flush, cap_extensions: Optional[Tuple[float, float]] = None, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), rotation: float = 0, mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, @@ -236,7 +239,7 @@ class Path(Shape, metaclass=AutoSlots): #TODO: needs testing direction = numpy.array([1, 0]) - verts = [[0, 0]] + verts = [numpy.zeros(2)] for angle, distance in travel_pairs: direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T verts.append(verts[-1] + direction * distance) @@ -319,7 +322,7 @@ class Path(Shape, metaclass=AutoSlots): return polys - def get_bounds(self) -> numpy.ndarray: + def get_bounds(self) -> NDArray[numpy.float64]: if self.cap == PathCap.Circle: bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, numpy.max(self.vertices, axis=0) + self.width / 2)) @@ -409,10 +412,11 @@ class Path(Shape, metaclass=AutoSlots): self.vertices = remove_colinear_vertices(self.vertices, closed_path=False) return self - def _calculate_cap_extensions(self) -> numpy.ndarray: + def _calculate_cap_extensions(self) -> NDArray[numpy.float64]: if self.cap == PathCap.Square: extensions = numpy.full(2, self.width / 2) elif self.cap == PathCap.SquareCustom: + 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 fe24ef4..c3a8a6a 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,14 +1,14 @@ -from typing import List, Dict, Optional, Sequence +from typing import List, Dict, Optional, Sequence, Any import copy -import numpy # type: ignore +import numpy from numpy import pi -from numpy.typing import ArrayLike +from numpy.typing import NDArray, ArrayLike from . import Shape, normalized_shape_tuple from .. import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots +from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t from ..traits import LockableImpl @@ -22,12 +22,12 @@ class Polygon(Shape, metaclass=AutoSlots): """ __slots__ = ('_vertices',) - _vertices: numpy.ndarray + _vertices: NDArray[numpy.float64] """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ # vertices property @property - def vertices(self) -> numpy.ndarray: + def vertices(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]: """ Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ @@ -44,7 +44,7 @@ class Polygon(Shape, metaclass=AutoSlots): # xs property @property - def xs(self) -> numpy.ndarray: + def xs(self) -> NDArray[numpy.float64]: """ All vertex x coords as a 1D ndarray """ @@ -59,7 +59,7 @@ class Polygon(Shape, metaclass=AutoSlots): # ys property @property - def ys(self) -> numpy.ndarray: + def ys(self) -> NDArray[numpy.float64]: """ All vertex y coords as a 1D ndarray """ @@ -76,7 +76,7 @@ class Polygon(Shape, metaclass=AutoSlots): self, vertices: ArrayLike, *, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, @@ -89,6 +89,8 @@ class Polygon(Shape, metaclass=AutoSlots): LockableImpl.unlock(self) self.identifier = () if raw: + assert(isinstance(vertices, numpy.ndarray)) + assert(isinstance(offset, numpy.ndarray)) self._vertices = vertices self._offset = offset self._repetition = repetition @@ -120,7 +122,7 @@ class Polygon(Shape, metaclass=AutoSlots): side_length: float, *, rotation: float = 0.0, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, @@ -155,7 +157,7 @@ class Polygon(Shape, metaclass=AutoSlots): ly: float, *, rotation: float = 0, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, @@ -291,7 +293,7 @@ class Polygon(Shape, metaclass=AutoSlots): side_length: Optional[float] = None, inner_radius: Optional[float] = None, regular: bool = True, - center: vector2 = (0.0, 0.0), + center: ArrayLike = (0.0, 0.0), rotation: float = 0.0, layer: layer_t = 0, dose: float = 1.0, @@ -353,7 +355,7 @@ class Polygon(Shape, metaclass=AutoSlots): ) -> List['Polygon']: return [copy.deepcopy(self)] - def get_bounds(self) -> numpy.ndarray: + def get_bounds(self) -> NDArray[numpy.float64]: return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0), self.offset + numpy.max(self.vertices, axis=0))) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index f903e9e..874734b 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -1,8 +1,8 @@ from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from abc import ABCMeta, abstractmethod -import numpy # type: ignore -from numpy.typing import ArrayLike +import numpy +from numpy.typing import NDArray, ArrayLike from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, @@ -15,7 +15,7 @@ if TYPE_CHECKING: # Type definitions normalized_shape_tuple = Tuple[Tuple, - Tuple[numpy.ndarray, float, float, bool, float], + Tuple[NDArray[numpy.float64], float, float, bool, float], Callable[[], 'Shape']] # ## Module-wide defaults @@ -117,12 +117,16 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable """ from . import Polygon - grid_x = numpy.unique(grid_x) - grid_y = numpy.unique(grid_y) + gx = numpy.unique(grid_x) + gy = numpy.unique(grid_y) polygon_contours = [] for polygon in self.to_polygons(): - mins, maxs = polygon.get_bounds() + bounds = polygon.get_bounds() + if not bounds: + continue + + mins, maxs = bounds vertex_lists = [] p_verts = polygon.vertices + polygon.offset @@ -130,12 +134,12 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable dv = v_next - v # Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape - gxi_range = numpy.digitize([v[0], v_next[0]], grid_x) - gxi_min = numpy.min(gxi_range - 1).clip(0, len(grid_x) - 1) - gxi_max = numpy.max(gxi_range).clip(0, len(grid_x)) + gxi_range = numpy.digitize([v[0], v_next[0]], gx) + gxi_min = numpy.min(gxi_range - 1).clip(0, len(gx) - 1) + gxi_max = numpy.max(gxi_range).clip(0, len(gx)) - err_xmin = (min(v[0], v_next[0]) - grid_x[gxi_min]) / (grid_x[gxi_min + 1] - grid_x[gxi_min]) - err_xmax = (max(v[0], v_next[0]) - grid_x[gxi_max - 1]) / (grid_x[gxi_max] - grid_x[gxi_max - 1]) + err_xmin = (min(v[0], v_next[0]) - gx[gxi_min]) / (gx[gxi_min + 1] - gx[gxi_min]) + err_xmax = (max(v[0], v_next[0]) - gx[gxi_max - 1]) / (gx[gxi_max] - gx[gxi_max - 1]) if err_xmin >= 0.5: gxi_min += 1 @@ -146,32 +150,32 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable # Vertical line, don't calculate slope xi = [gxi_min, gxi_max - 1] ys = numpy.array([v[1], v_next[1]]) - yi = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) - err_y = (ys - grid_y[yi]) / (grid_y[yi] - grid_y[yi - 1]) + yi = numpy.digitize(ys, gy).clip(1, len(gy) - 1) + err_y = (ys - gy[yi]) / (gy[yi] - gy[yi - 1]) yi[err_y < 0.5] -= 1 - segment = numpy.column_stack((grid_x[xi], grid_y[yi])) + segment = numpy.column_stack((gx[xi], gy[yi])) vertex_lists.append(segment) continue m = dv[1] / dv[0] - def get_grid_inds(xes: numpy.ndarray) -> numpy.ndarray: + def get_grid_inds(xes: ArrayLike) -> NDArray[numpy.float64]: ys = m * (xes - v[0]) + v[1] # (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid - inds = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) + inds = numpy.digitize(ys, gy).clip(1, len(gy) - 1) # err is what fraction of the cell upwards we have to go to reach our y # (can be negative at bottom edge due to clip above) - err = (ys - grid_y[inds - 1]) / (grid_y[inds] - grid_y[inds - 1]) + err = (ys - gy[inds - 1]) / (gy[inds] - gy[inds - 1]) # now set inds to the index of the nearest y-grid line inds[err < 0.5] -= 1 return inds # Find the y indices on all x gridlines - xs = grid_x[gxi_min:gxi_max] + xs = gx[gxi_min:gxi_max] inds = get_grid_inds(xs) # Find y-intersections for x-midpoints @@ -186,7 +190,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable yinds[1::3] = inds2 yinds[2::3] = inds2 - vlist = numpy.column_stack((grid_x[xinds], grid_y[yinds])) + vlist = numpy.column_stack((gx[xinds], gy[yinds])) if dv[0] < 0: vlist = vlist[::-1] @@ -249,23 +253,27 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable import skimage.measure # type: ignore import float_raster - grid_x = numpy.unique(grid_x) - grid_y = numpy.unique(grid_y) + grx = numpy.unique(grid_x) + gry = numpy.unique(grid_y) polygon_contours = [] for polygon in self.to_polygons(): # Get rid of unused gridlines (anything not within 2 lines of the polygon bounds) - mins, maxs = polygon.get_bounds() - keep_x = numpy.logical_and(grid_x > mins[0], grid_x < maxs[0]) - keep_y = numpy.logical_and(grid_y > mins[1], grid_y < maxs[1]) + bounds = polygon.get_bounds() + if not bounds: + continue + + mins, maxs = bounds + keep_x = numpy.logical_and(grx > mins[0], grx < maxs[0]) + keep_y = numpy.logical_and(gry > mins[1], gry < maxs[1]) for k in (keep_x, keep_y): for s in (1, 2): k[s:] += k[:-s] k[:-s] += k[s:] k = k > 0 - gx = grid_x[keep_x] - gy = grid_y[keep_y] + gx = grx[keep_x] + gy = gry[keep_y] if len(gx) == 0 or len(gy) == 0: continue @@ -286,8 +294,8 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable # /2 deals with supersampling # +.5 deals with the fact that our 0-edge becomes -.5 in the super-sampled contour output snapped_contour = numpy.round((contour + .5) / 2).astype(int) - vertices = numpy.hstack((grid_x[snapped_contour[:, None, 0] + offset_i[0]], - grid_y[snapped_contour[:, None, 1] + offset_i[1]])) + vertices = numpy.hstack((grx[snapped_contour[:, None, 0] + offset_i[0]], + gry[snapped_contour[:, None, 1] + offset_i[1]])) manhattan_polygons.append(Polygon( vertices=vertices, diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 9504ce5..4e94d84 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,14 +1,15 @@ -from typing import List, Tuple, Dict, Sequence, Optional +from typing import List, Tuple, Dict, Sequence, Optional, Any import copy -import numpy # type: ignore +import numpy from numpy import pi, inf +from numpy.typing import NDArray, ArrayLike from . import Shape, Polygon, normalized_shape_tuple from .. import PatternError from ..repetition import Repetition from ..traits import RotatableImpl -from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots +from ..utils import is_scalar, get_bit, normalize_mirror, layer_t, AutoSlots from ..utils import annotations_t from ..traits import LockableImpl @@ -26,7 +27,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): _string: str _height: float - _mirrored: numpy.ndarray # ndarray[bool] + _mirrored: NDArray[numpy.bool_] font_path: str # vertices property @@ -51,7 +52,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): # Mirrored property @property - def mirrored(self) -> numpy.ndarray: # ndarray[bool] + def mirrored(self) -> Any: #TODO mypy#3004 NDArray[numpy.bool_]: return self._mirrored @mirrored.setter @@ -66,9 +67,9 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): height: float, font_path: str, *, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, - mirrored: Tuple[bool, bool] = (False, False), + mirrored: ArrayLike = (False, False), layer: layer_t = 0, dose: float = 1.0, repetition: Optional[Repetition] = None, @@ -79,6 +80,8 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): LockableImpl.unlock(self) self.identifier = () if raw: + assert(isinstance(offset, numpy.ndarray)) + assert(isinstance(mirrored, numpy.ndarray)) self._offset = offset self._layer = layer self._dose = dose @@ -155,7 +158,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): mirrored=(mirror_x, False), layer=self.layer)) - def get_bounds(self) -> numpy.ndarray: + def get_bounds(self) -> NDArray[numpy.float64]: # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so # just convert to polygons instead bounds = numpy.array([[+inf, +inf], [-inf, -inf]]) @@ -201,7 +204,7 @@ def get_char_as_polygons( outline = slot.outline start = 0 - all_verts, all_codes = [], [] + all_verts_list, all_codes = [], [] for end in outline.contours: points = outline.points[start:end + 1] points.append(points[0]) @@ -238,11 +241,11 @@ def get_char_as_polygons( codes.extend([Path.CURVE3, Path.CURVE3]) verts.append(segment[-1]) codes.append(Path.CURVE3) - all_verts.extend(verts) + all_verts_list.extend(verts) all_codes.extend(codes) start = end + 1 - all_verts = numpy.array(all_verts) / resolution + all_verts = numpy.array(all_verts_list) / resolution advance = slot.advance.x / resolution diff --git a/masque/subpattern.py b/masque/subpattern.py index 9bee4af..64e96e3 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -7,11 +7,12 @@ from typing import Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any, TypeVar import copy -import numpy # type: ignore +import numpy from numpy import pi +from numpy.typing import NDArray, ArrayLike from .error import PatternError -from .utils import is_scalar, vector2, AutoSlots, annotations_t +from .utils import is_scalar, AutoSlots, annotations_t from .repetition import Repetition from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl, @@ -40,7 +41,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi _pattern: Optional['Pattern'] """ The `Pattern` being instanced """ - _mirrored: numpy.ndarray # ndarray[bool] + _mirrored: NDArray[numpy.bool_] """ Whether to mirror the instance across the x and/or y axes. """ identifier: Tuple[Any, ...] @@ -50,7 +51,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi self, pattern: Optional['Pattern'], *, - offset: vector2 = (0.0, 0.0), + offset: ArrayLike = (0.0, 0.0), rotation: float = 0.0, mirrored: Optional[Sequence[bool]] = None, dose: float = 1.0, @@ -113,7 +114,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi return self._pattern @pattern.setter - def pattern(self, val: Optional['Pattern']): + def pattern(self, val: Optional['Pattern']) -> None: from .pattern import Pattern if val is not None and not isinstance(val, Pattern): raise PatternError(f'Provided pattern {val} is not a Pattern object or None!') @@ -121,11 +122,11 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi # Mirrored property @property - def mirrored(self) -> numpy.ndarray: # ndarray[bool] + def mirrored(self) -> Any: #TODO mypy#3004 NDArray[numpy.bool_]: return self._mirrored @mirrored.setter - def mirrored(self, val: Sequence[bool]): + def mirrored(self, val: ArrayLike) -> None: if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.array(val, dtype=bool, copy=True) @@ -167,7 +168,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi self.repetition.mirror(axis) return self - def get_bounds(self) -> Optional[numpy.ndarray]: + def get_bounds(self) -> Optional[NDArray[numpy.float64]]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the `SubPattern` in each dimension. diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index 4e59e32..c06cb84 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -1,11 +1,12 @@ # TODO top-level comment about how traits should set __slots__ = (), and how to use AutoSlots -from typing import TypeVar +from typing import TypeVar, Any, Optional from abc import ABCMeta, abstractmethod -import numpy # type: ignore + +import numpy +from numpy.typing import NDArray, ArrayLike from ..error import MasqueError -from ..utils import vector2 T = TypeVar('T', bound='Positionable') @@ -23,7 +24,7 @@ class Positionable(metaclass=ABCMeta): ''' @property @abstractmethod - def offset(self) -> numpy.ndarray: + def offset(self) -> NDArray[numpy.float64]: """ [x, y] offset """ @@ -31,21 +32,11 @@ class Positionable(metaclass=ABCMeta): # @offset.setter # @abstractmethod -# def offset(self, val: vector2): +# def offset(self, val: ArrayLike): # pass - ''' - --- Abstract methods - ''' @abstractmethod - def get_bounds(self) -> numpy.ndarray: - """ - Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity. - """ - pass - - @abstractmethod - def set_offset(self: T, offset: vector2) -> T: + def set_offset(self: T, offset: ArrayLike) -> T: """ Set the offset @@ -58,7 +49,7 @@ class Positionable(metaclass=ABCMeta): pass @abstractmethod - def translate(self: T, offset: vector2) -> T: + def translate(self: T, offset: ArrayLike) -> T: """ Translate the entity by the given offset @@ -70,6 +61,13 @@ class Positionable(metaclass=ABCMeta): """ pass + @abstractmethod + def get_bounds(self) -> Optional[NDArray[numpy.float64]]: + """ + Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity. + """ + pass + class PositionableImpl(Positionable, metaclass=ABCMeta): """ @@ -77,7 +75,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): """ __slots__ = () - _offset: numpy.ndarray + _offset: NDArray[numpy.float64] """ `[x_offset, y_offset]` """ ''' @@ -85,14 +83,14 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): ''' # offset property @property - def offset(self) -> numpy.ndarray: + def offset(self) -> Any: #TODO mypy#3003 NDArray[numpy.float64]: """ [x, y] offset """ return self._offset @offset.setter - def offset(self, val: vector2): + def offset(self, val: ArrayLike) -> None: if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64: val = numpy.array(val, dtype=float) @@ -103,12 +101,12 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): ''' ---- Methods ''' - def set_offset(self: I, offset: vector2) -> I: + def set_offset(self: I, offset: ArrayLike) -> I: self.offset = offset return self - def translate(self: I, offset: vector2) -> I: - self._offset += offset + def translate(self: I, offset: ArrayLike) -> I: + self._offset += offset # type: ignore # NDArray += ArrayLike should be fine?? return self def _lock(self: I) -> I: diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index 1e83697..26be3c4 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -1,12 +1,13 @@ from typing import TypeVar from abc import ABCMeta, abstractmethod -import numpy # type: ignore +import numpy from numpy import pi +from numpy.typing import ArrayLike, NDArray #from .positionable import Positionable from ..error import MasqueError -from ..utils import is_scalar, rotation_matrix_2d, vector2 +from ..utils import is_scalar, rotation_matrix_2d T = TypeVar('T', bound='Rotatable') I = TypeVar('I', bound='RotatableImpl') @@ -89,7 +90,7 @@ class Pivotable(metaclass=ABCMeta): __slots__ = () @abstractmethod - def rotate_around(self: P, pivot: vector2, rotation: float) -> P: + def rotate_around(self: P, pivot: ArrayLike, rotation: float) -> P: """ Rotate the object around a point. @@ -109,11 +110,11 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta): """ __slots__ = () - def rotate_around(self: J, pivot: vector2, rotation: float) -> J: + def rotate_around(self: J, pivot: ArrayLike, rotation: float) -> J: pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) self.rotate(rotation) - self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) + self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) #type: ignore #TODO: mypy#3004 self.translate(+pivot) return self diff --git a/masque/utils.py b/masque/utils.py index 7b4cc2a..5f7d987 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -4,12 +4,11 @@ Various helper functions from typing import Any, Union, Tuple, Sequence, Dict, List from abc import ABCMeta -import numpy # type: ignore -from numpy.typing import ArrayLike +import numpy +from numpy.typing import NDArray, ArrayLike # Type definitions -vector2 = ArrayLike layer_t = Union[int, Tuple[int, int], str] annotations_t = Dict[str, List[Union[int, float, str]]] @@ -57,7 +56,7 @@ def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any: return bit_string -def rotation_matrix_2d(theta: float) -> numpy.ndarray: +def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]: """ 2D rotation matrix for rotating counterclockwise around the origin. @@ -90,7 +89,7 @@ def normalize_mirror(mirrored: Sequence[bool]) -> Tuple[bool, float]: return mirror_x, angle -def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) -> numpy.ndarray: +def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) -> NDArray[numpy.float64]: """ Given a list of vertices, remove any consecutive duplicates. @@ -102,13 +101,14 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) -> Returns: `vertices` with no consecutive duplicates. """ + vertices = numpy.array(vertices) duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) if not closed_path: duplicates[0] = False return vertices[~duplicates] -def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> numpy.ndarray: +def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> NDArray[numpy.float64]: """ Given a list of vertices, remove any superflous vertices (i.e. those which lie along the line formed by their neighbors)