Use ArrayLike and NDArray wherever possible. Some type fixes and some related corner cases

nolock
jan 2 years ago
parent 89f327ba37
commit a4fe3d9e2e

@ -6,14 +6,14 @@ import traceback
import logging import logging
from collections import Counter from collections import Counter
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike from numpy.typing import ArrayLike, NDArray
from ..pattern import Pattern from ..pattern import Pattern
from ..subpattern import SubPattern from ..subpattern import SubPattern
from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable 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 from ..error import DeviceError
@ -48,11 +48,12 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met
ptype: str ptype: str
""" Port types must match to be plugged together if both are non-zero """ """ Port types must match to be plugged together if both are non-zero """
def __init__(self, def __init__(
offset: ArrayLike, self,
rotation: Optional[float], offset: ArrayLike,
ptype: str = 'unk', rotation: Optional[float],
) -> None: ptype: str = 'unk',
) -> None:
self.offset = offset self.offset = offset
self.rotation = rotation self.rotation = rotation
self.ptype = ptype self.ptype = ptype
@ -63,7 +64,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met
return self._rotation return self._rotation
@rotation.setter @rotation.setter
def rotation(self, val: float): def rotation(self, val: float) -> None:
if val is None: if val is None:
self._rotation = None self._rotation = None
else: 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]: def __getitem__(self, key: Union[List[str], Tuple[str], KeysView[str], ValuesView[str]]) -> Dict[str, Port]:
pass 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]]: def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, Dict[str, Port]]:
""" """
For convenience, ports can be read out using square brackets: For convenience, ports can be read out using square brackets:
@ -504,9 +520,9 @@ class Device(Copyable, Mirrorable):
self: D, self: D,
other: O, other: O,
*, *,
offset: vector2 = (0, 0), offset: ArrayLike = (0, 0),
rotation: float = 0, rotation: float = 0,
pivot: vector2 = (0, 0), pivot: ArrayLike = (0, 0),
mirrored: Tuple[bool, bool] = (False, False), mirrored: Tuple[bool, bool] = (False, False),
port_map: Optional[Dict[str, Optional[str]]] = None, port_map: Optional[Dict[str, Optional[str]]] = None,
skip_port_check: bool = False, skip_port_check: bool = False,
@ -585,7 +601,7 @@ class Device(Copyable, Mirrorable):
*, *,
mirrored: Tuple[bool, bool] = (False, False), mirrored: Tuple[bool, bool] = (False, False),
set_rotation: Optional[bool] = None, 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, Given a device `other` and a mapping `map_in` specifying port connections,
find the transform which will correctly align the specified ports. 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] 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. Translate the pattern and all ports.
@ -683,7 +699,7 @@ class Device(Copyable, Mirrorable):
port.translate(offset) port.translate(offset)
return self 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. Translate the pattern and all ports.
@ -753,10 +769,10 @@ class Device(Copyable, Mirrorable):
def rotate_offsets_around( def rotate_offsets_around(
offsets: ArrayLike, offsets: NDArray[numpy.float64],
pivot: ArrayLike, pivot: NDArray[numpy.float64],
angle: float, angle: float,
) -> numpy.ndarray: ) -> NDArray[numpy.float64]:
offsets -= pivot offsets -= pivot
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
offsets += pivot offsets += pivot

@ -1,11 +1,12 @@
from typing import Dict, Tuple, List, Optional, Union, Any, cast, Sequence from typing import Dict, Tuple, List, Optional, Union, Any, cast, Sequence
from pprint import pformat from pprint import pformat
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike
from .devices import Port from .devices import Port
from ..utils import rotation_matrix_2d, vector2 from ..utils import rotation_matrix_2d
from ..error import BuildError from ..error import BuildError
@ -13,9 +14,9 @@ def ell(
ports: Dict[str, Port], ports: Dict[str, Port],
ccw: Optional[bool], ccw: Optional[bool],
bound_type: str, 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, set_rotation: Optional[float] = None,
) -> Dict[str, float]: ) -> Dict[str, float]:
""" """
@ -96,8 +97,11 @@ def ell(
rotations[~has_rotation] = rotations[has_rotation][0] rotations[~has_rotation] = rotations[has_rotation][0]
if not numpy.allclose(rotations[0], rotations): 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' 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: else:
if set_rotation is not None: if set_rotation is not None:
raise BuildError('set_rotation must be specified if no ports have rotations!') raise BuildError('set_rotation must be specified if no ports have rotations!')

@ -30,7 +30,8 @@ import logging
import pathlib import pathlib
import gzip import gzip
import numpy # type: ignore import numpy
from numpy.typing import NDArray
import klamath import klamath
from klamath import records 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) properties = _annotations_to_properties(subpat.annotations, 512)
if isinstance(rep, Grid): 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], [0, 0],
rep.a_vector * rep.a_count, rep.a_vector * rep.a_count,
rep.b_vector * rep.b_count, b_vector * b_count,
] ]
aref = klamath.library.Reference(struct_name=encoded_name, aref = klamath.library.Reference(struct_name=encoded_name,
xy=numpy.round(xy).astype(int), xy=numpy.round(xy).astype(int),

@ -198,8 +198,7 @@ def writefile(
open_func = open open_func = open
with io.BufferedWriter(open_func(path, mode='wb')) as stream: with io.BufferedWriter(open_func(path, mode='wb')) as stream:
results = write(patterns, stream, *args, **kwargs) write(patterns, stream, *args, **kwargs)
return results
def readfile( def readfile(
@ -491,10 +490,14 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
pname = placement.get_name() pname = placement.get_name()
name = pname if isinstance(pname, int) else pname.string name = pname if isinstance(pname, int) else pname.string
annotations = properties_to_annotations(placement.properties, lib.propnames, lib.propstrings) 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, subpat = SubPattern(offset=xy,
pattern=None, pattern=None,
mirrored=(placement.flip, False), mirrored=(placement.flip, False),
rotation=numpy.deg2rad(placement.angle), rotation=rotation,
scale=float(mag), scale=float(mag),
identifier=(name,), identifier=(name,),
repetition=repetition_fata2masq(placement.repetition), repetition=repetition_fata2masq(placement.repetition),

@ -28,11 +28,12 @@ import logging
import pathlib import pathlib
import gzip import gzip
import numpy # type: ignore import numpy
from numpy.typing import ArrayLike, NDArray
# python-gdsii # python-gdsii
import gdsii.library import gdsii.library #type: ignore
import gdsii.structure import gdsii.structure #type: ignore
import gdsii.elements import gdsii.elements #type: ignore
from .utils import clean_pattern_vertices, is_gzipped from .utils import clean_pattern_vertices, is_gzipped
from .. import Pattern, SubPattern, PatternError, Label, Shape from .. import Pattern, SubPattern, PatternError, Label, Shape
@ -182,8 +183,7 @@ def writefile(
open_func = open open_func = open
with io.BufferedWriter(open_func(path, mode='wb')) as stream: with io.BufferedWriter(open_func(path, mode='wb')) as stream:
results = write(patterns, stream, *args, **kwargs) write(patterns, stream, *args, **kwargs)
return results
def readfile( def readfile(
@ -402,10 +402,12 @@ def _subpatterns_to_refs(
new_refs: List[Union[gdsii.elements.SRef, gdsii.elements.ARef]] new_refs: List[Union[gdsii.elements.SRef, gdsii.elements.ARef]]
ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] ref: Union[gdsii.elements.SRef, gdsii.elements.ARef]
if isinstance(rep, Grid): 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], [0, 0],
rep.a_vector * rep.a_count, rep.a_vector * rep.a_count,
rep.b_vector * rep.b_count, b_vector * b_count,
] ]
ref = gdsii.elements.ARef(struct_name=encoded_name, ref = gdsii.elements.ARef(struct_name=encoded_name,
xy=numpy.round(xy).astype(int), xy=numpy.round(xy).astype(int),

@ -4,7 +4,8 @@ SVG file format readers and writers
from typing import Dict, Optional from typing import Dict, Optional
import warnings import warnings
import numpy # type: ignore import numpy
from numpy.typing import ArrayLike
import svgwrite # type: ignore import svgwrite # type: ignore
from .utils import mangle_name from .utils import mangle_name
@ -141,7 +142,7 @@ def writefile_inverted(pattern: Pattern, filename: str):
svg.save() svg.save()
def poly2path(vertices: numpy.ndarray) -> str: def poly2path(vertices: ArrayLike) -> str:
""" """
Create an SVG path string from an Nx2 list of vertices. Create an SVG path string from an Nx2 list of vertices.
@ -151,8 +152,9 @@ def poly2path(vertices: numpy.ndarray) -> str:
Returns: Returns:
SVG path-string. SVG path-string.
""" """
commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1]) verts = numpy.array(vertices, copy=False)
for vertex in vertices[1:]: 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 += 'L{:g},{:g}'.format(vertex[0], vertex[1])
commands += ' Z ' commands += ' Z '
return commands return commands

@ -1,9 +1,11 @@
from typing import Tuple, Dict, Optional, TypeVar from typing import Tuple, Dict, Optional, TypeVar
import copy import copy
import numpy # type: ignore
import numpy
from numpy.typing import ArrayLike, NDArray
from .repetition import Repetition 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 PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl
from .traits import AnnotatableImpl from .traits import AnnotatableImpl
@ -43,7 +45,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
self, self,
string: str, string: str,
*, *,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0, layer: layer_t = 0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
@ -74,7 +76,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
new.set_locked(self.locked) new.set_locked(self.locked)
return new 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. Rotate the label around a point.
@ -91,7 +93,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
self.translate(+pivot) self.translate(+pivot)
return self return self
def get_bounds(self) -> numpy.ndarray: def get_bounds(self) -> NDArray[numpy.float64]:
""" """
Return the bounds of the label. Return the bounds of the label.

@ -9,21 +9,21 @@ import pickle
from itertools import chain from itertools import chain
from collections import defaultdict from collections import defaultdict
import numpy # type: ignore import numpy
from numpy import inf from numpy import inf
from numpy.typing import ArrayLike from numpy.typing import NDArray, ArrayLike
# .visualize imports matplotlib and matplotlib.collections # .visualize imports matplotlib and matplotlib.collections
from .subpattern import SubPattern from .subpattern import SubPattern
from .shapes import Shape, Polygon from .shapes import Shape, Polygon
from .label import Label 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 .error import PatternError, PatternLockedError
from .traits import LockableImpl, AnnotatableImpl, Scalable, Mirrorable from .traits import LockableImpl, AnnotatableImpl, Scalable, Mirrorable
from .traits import Rotatable, Positionable 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') P = TypeVar('P', bound='Pattern')
@ -236,7 +236,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
self: P, self: P,
visit_before: visitor_function_t = None, visit_before: visitor_function_t = None,
visit_after: 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, memo: Optional[Dict] = None,
hierarchy: Tuple[P, ...] = (), hierarchy: Tuple[P, ...] = (),
) -> P: ) -> P:
@ -283,6 +283,8 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
if transform is None or transform is True: if transform is None or transform is True:
transform = numpy.zeros(4) transform = numpy.zeros(4)
elif transform is not False:
transform = numpy.array(transform)
if self in hierarchy: if self in hierarchy:
raise PatternError('.dfs() called on pattern with circular reference') raise PatternError('.dfs() called on pattern with circular reference')
@ -441,7 +443,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
return self 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. 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 List of `(pat.name, pat)` tuples for all referenced Pattern objects
""" """
pats_by_id = self.referenced_patterns_by_id(**kwargs) 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()] pat_list = [(p.name if p is not None else None, p) for p in pats_by_id.values()]
return pat_list return pat_list
@ -539,7 +542,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
ids.update(pat.subpatterns_by_id(include_none=include_none)) ids.update(pat.subpatterns_by_id(include_none=include_none))
return dict(ids) 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 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. 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)) min_bounds = numpy.array((+inf, +inf))
max_bounds = numpy.array((-inf, -inf)) max_bounds = numpy.array((-inf, -inf))
for entry in chain(self.shapes, self.subpatterns, self.labels): for entry in chain(self.shapes, self.subpatterns, self.labels):
bounds = entry.get_bounds() bounds = entry.get_bounds()
if bounds is None: if bounds is None:
@ -655,7 +659,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
return self 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. Translates all shapes, label, and subpatterns by the given offset.
@ -702,7 +706,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
label.offset *= c label.offset *= c
return self 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. Rotate the Pattern around the a location.
@ -945,7 +949,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
def visualize( def visualize(
self, self,
offset: vector2 = (0., 0.), offset: ArrayLike = (0., 0.),
line_color: str = 'k', line_color: str = 'k',
fill_color: str = 'none', fill_color: str = 'none',
overdraw: bool = False, overdraw: bool = False,

@ -7,11 +7,11 @@ from typing import Union, Dict, Optional, Sequence, Any
import copy import copy
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy # type: ignore import numpy
from numpy.typing import ArrayLike from numpy.typing import ArrayLike, NDArray
from .error import PatternError 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 from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable
@ -23,7 +23,7 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
@property @property
@abstractmethod @abstractmethod
def displacements(self) -> numpy.ndarray: def displacements(self) -> NDArray[numpy.float64]:
""" """
An Nx2 ndarray specifying all offsets generated by this repetition An Nx2 ndarray specifying all offsets generated by this repetition
""" """
@ -44,7 +44,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
'_a_count', '_a_count',
'_b_count') '_b_count')
_a_vector: numpy.ndarray _a_vector: NDArray[numpy.float64]
""" Vector `[x, y]` specifying the first lattice vector of the grid. """ Vector `[x, y]` specifying the first lattice vector of the grid.
Specifies center-to-center spacing between adjacent elements. Specifies center-to-center spacing between adjacent elements.
""" """
@ -52,7 +52,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
_a_count: int _a_count: int
""" Number of instances along the direction specified by the `a_vector` """ """ 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. """ Vector `[x, y]` specifying a second lattice vector for the grid.
Specifies center-to-center spacing between adjacent elements. Specifies center-to-center spacing between adjacent elements.
Can be `None` for a 1D array. 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}') raise PatternError(f'Repetition has too-small b_count: {b_count}')
object.__setattr__(self, 'locked', False) object.__setattr__(self, 'locked', False)
self.a_vector = a_vector self.a_vector = a_vector # type: ignore # setter handles type conversion
self.b_vector = b_vector self.b_vector = b_vector # type: ignore # setter handles type conversion
self.a_count = a_count self.a_count = a_count
self.b_count = b_count self.b_count = b_count
self.locked = locked self.locked = locked
@ -122,11 +122,11 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
# a_vector property # a_vector property
@property @property
def a_vector(self) -> numpy.ndarray: def a_vector(self) -> NDArray[numpy.float64]:
return self._a_vector return self._a_vector
@a_vector.setter @a_vector.setter
def a_vector(self, val: vector2): def a_vector(self, val: ArrayLike) -> None:
if not isinstance(val, numpy.ndarray): if not isinstance(val, numpy.ndarray):
val = numpy.array(val, dtype=float) val = numpy.array(val, dtype=float)
@ -136,11 +136,11 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
# b_vector property # b_vector property
@property @property
def b_vector(self) -> numpy.ndarray: def b_vector(self) -> Optional[NDArray[numpy.float64]]:
return self._b_vector return self._b_vector
@b_vector.setter @b_vector.setter
def b_vector(self, val: vector2): def b_vector(self, val: ArrayLike) -> None:
if not isinstance(val, numpy.ndarray): if not isinstance(val, numpy.ndarray):
val = numpy.array(val, dtype=float, copy=True) val = numpy.array(val, dtype=float, copy=True)
@ -154,7 +154,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
return self._a_count return self._a_count
@a_count.setter @a_count.setter
def a_count(self, val: int): def a_count(self, val: int) -> None:
if val != int(val): if val != int(val):
raise PatternError('a_count must be convertable to an int!') raise PatternError('a_count must be convertable to an int!')
self._a_count = int(val) self._a_count = int(val)
@ -165,13 +165,16 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
return self._b_count return self._b_count
@b_count.setter @b_count.setter
def b_count(self, val: int): def b_count(self, val: int) -> None:
if val != int(val): if val != int(val):
raise PatternError('b_count must be convertable to an int!') raise PatternError('b_count must be convertable to an int!')
self._b_count = int(val) self._b_count = int(val)
@property @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') aa, bb = numpy.meshgrid(numpy.arange(self.a_count), numpy.arange(self.b_count), indexing='ij')
return (aa.flatten()[:, None] * self.a_vector[None, :] return (aa.flatten()[:, None] * self.a_vector[None, :]
+ bb.flatten()[:, None] * self.b_vector[None, :]) # noqa + bb.flatten()[:, None] * self.b_vector[None, :]) # noqa
@ -207,7 +210,7 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
self.b_vector[1 - axis] *= -1 self.b_vector[1 - axis] *= -1
return self 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 Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the `Grid` in each dimension. 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` `[[x_min, y_min], [x_max, y_max]]` or `None`
""" """
a_extent = self.a_vector * self.a_count 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) corners = ((0, 0), a_extent, b_extent, a_extent + b_extent)
xy_min = numpy.min(corners, axis=0) xy_min = numpy.min(corners, axis=0)
@ -296,24 +299,26 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots):
`[[x0, y0], [x1, y1], ...]` `[[x0, y0], [x1, y1], ...]`
""" """
_displacements: numpy.ndarray _displacements: NDArray[numpy.float64]
""" List of vectors `[[x0, y0], [x1, y1], ...]` specifying the offsets """ List of vectors `[[x0, y0], [x1, y1], ...]` specifying the offsets
of the instances. of the instances.
""" """
@property @property
def displacements(self) -> numpy.ndarray: def displacements(self) -> Any: # TODO: mypy#3004 NDArray[numpy.float64]:
return self._displacements return self._displacements
@displacements.setter @displacements.setter
def displacements(self, val: ArrayLike): def displacements(self, val: ArrayLike) -> None:
val = numpy.array(val, float) vala: NDArray[numpy.float64] = numpy.array(vala, dtype=float)
val = numpy.sort(val.view([('', val.dtype)] * val.shape[1]), 0).view(val.dtype) # sort rows vala = numpy.sort(vala.view([('', vala.dtype)] * vala.shape[1]), 0).view(vala.dtype) # sort rows
self._displacements = val self._displacements = vala
def __init__(self, def __init__(
displacements: ArrayLike, self,
locked: bool = False,): displacements: ArrayLike,
locked: bool = False,
) -> None:
""" """
Args: Args:
displacements: List of vectors (Nx2 ndarray) specifying displacements. displacements: List of vectors (Nx2 ndarray) specifying displacements.
@ -383,7 +388,7 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots):
self.displacements[1 - axis] *= -1 self.displacements[1 - axis] *= -1
return self 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 Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the `displacements` in each dimension. extent of the `displacements` in each dimension.

@ -1,14 +1,15 @@
from typing import List, Dict, Optional, Sequence from typing import List, Dict, Optional, Sequence, Any
import copy import copy
import math import math
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError from .. import PatternError
from ..repetition import Repetition 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 from ..traits import LockableImpl
@ -24,13 +25,13 @@ class Arc(Shape, metaclass=AutoSlots):
__slots__ = ('_radii', '_angles', '_width', '_rotation', __slots__ = ('_radii', '_angles', '_width', '_rotation',
'poly_num_points', 'poly_max_arclen') 'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray _radii: NDArray[numpy.float64]
""" Two radii for defining an ellipse """ """ Two radii for defining an ellipse """
_rotation: float _rotation: float
""" Rotation (ccw, radians) from the x axis to the first radius """ """ 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 """ """ Start and stop angles (ccw, radians) for choosing an arc from the ellipse, measured from the first radius """
_width: float _width: float
@ -44,14 +45,14 @@ class Arc(Shape, metaclass=AutoSlots):
# radius properties # radius properties
@property @property
def radii(self) -> numpy.ndarray: def radii(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]:
""" """
Return the radii `[rx, ry]` Return the radii `[rx, ry]`
""" """
return self._radii return self._radii
@radii.setter @radii.setter
def radii(self, val: vector2) -> None: def radii(self, val: ArrayLike) -> None:
val = numpy.array(val, dtype=float).flatten() val = numpy.array(val, dtype=float).flatten()
if not val.size == 2: if not val.size == 2:
raise PatternError('Radii must have length 2') raise PatternError('Radii must have length 2')
@ -81,7 +82,7 @@ class Arc(Shape, metaclass=AutoSlots):
# arc start/stop angle properties # arc start/stop angle properties
@property @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]`. Return the start and stop angles `[a_start, a_stop]`.
Angles are measured from x-axis after rotation Angles are measured from x-axis after rotation
@ -92,7 +93,7 @@ class Arc(Shape, metaclass=AutoSlots):
return self._angles return self._angles
@angles.setter @angles.setter
def angles(self, val: vector2) -> None: def angles(self, val: ArrayLike) -> None:
val = numpy.array(val, dtype=float).flatten() val = numpy.array(val, dtype=float).flatten()
if not val.size == 2: if not val.size == 2:
raise PatternError('Angles must have length 2') raise PatternError('Angles must have length 2')
@ -152,13 +153,13 @@ class Arc(Shape, metaclass=AutoSlots):
def __init__( def __init__(
self, self,
radii: vector2, radii: ArrayLike,
angles: vector2, angles: ArrayLike,
width: float, width: float,
*, *,
poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: Optional[float] = None, poly_max_arclen: Optional[float] = None,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
@ -171,6 +172,9 @@ class Arc(Shape, metaclass=AutoSlots):
LockableImpl.unlock(self) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(radii, numpy.ndarray))
assert(isinstance(angles, numpy.ndarray))
assert(isinstance(offset, numpy.ndarray))
self._radii = radii self._radii = radii
self._angles = angles self._angles = angles
self._width = width self._width = width
@ -241,7 +245,7 @@ class Arc(Shape, metaclass=AutoSlots):
wh = self.width / 2.0 wh = self.width / 2.0
if wh == r0 and r0 == r1: 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: else:
thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_points, endpoint=True) 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) 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) poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation)
return [poly] return [poly]
def get_bounds(self) -> numpy.ndarray: def get_bounds(self) -> NDArray[numpy.float64]:
''' '''
Equation for rotated ellipse is Equation for rotated ellipse is
`x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)` `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), (self.offset, scale / norm_value, rotation, False, self.dose),
lambda: Arc(radii=radii * norm_value, angles=angles, width=width * norm_value, layer=self.layer)) 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: Returns:
``` ```
@ -397,7 +401,7 @@ class Arc(Shape, metaclass=AutoSlots):
maxs.append([xp, yp]) maxs.append([xp, yp])
return numpy.array([mins, maxs]) + self.offset return numpy.array([mins, maxs]) + self.offset
def _angles_to_parameters(self) -> numpy.ndarray: def _angles_to_parameters(self) -> NDArray[numpy.float64]:
''' '''
Returns: Returns:
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form

@ -1,13 +1,14 @@
from typing import List, Dict, Optional from typing import List, Dict, Optional
import copy import copy
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError from .. import PatternError
from ..repetition import Repetition 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 from ..traits import LockableImpl
@ -48,7 +49,7 @@ class Circle(Shape, metaclass=AutoSlots):
*, *,
poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: Optional[float] = None, poly_max_arclen: Optional[float] = None,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
@ -59,6 +60,7 @@ class Circle(Shape, metaclass=AutoSlots):
LockableImpl.unlock(self) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(offset, numpy.ndarray))
self._radius = radius self._radius = radius
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
@ -98,7 +100,7 @@ class Circle(Shape, metaclass=AutoSlots):
raise PatternError('Number of points and arclength left ' raise PatternError('Number of points and arclength left '
'unspecified (default was also overridden)') 'unspecified (default was also overridden)')
n = [] n: List[float] = []
if poly_num_points is not None: if poly_num_points is not None:
n += [poly_num_points] n += [poly_num_points]
if poly_max_arclen is not None: 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)] 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, return numpy.vstack((self.offset - self.radius,
self.offset + self.radius)) self.offset + self.radius))

@ -1,14 +1,15 @@
from typing import List, Dict, Sequence, Optional from typing import List, Dict, Sequence, Optional, Any
import copy import copy
import math import math
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike, NDArray
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError from .. import PatternError
from ..repetition import Repetition 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 from ..traits import LockableImpl
@ -20,7 +21,7 @@ class Ellipse(Shape, metaclass=AutoSlots):
__slots__ = ('_radii', '_rotation', __slots__ = ('_radii', '_rotation',
'poly_num_points', 'poly_max_arclen') 'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray _radii: NDArray[numpy.float64]
""" Ellipse radii """ """ Ellipse radii """
_rotation: float _rotation: float
@ -34,14 +35,14 @@ class Ellipse(Shape, metaclass=AutoSlots):
# radius properties # radius properties
@property @property
def radii(self) -> numpy.ndarray: def radii(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]:
""" """
Return the radii `[rx, ry]` Return the radii `[rx, ry]`
""" """
return self._radii return self._radii
@radii.setter @radii.setter
def radii(self, val: vector2) -> None: def radii(self, val: ArrayLike) -> None:
val = numpy.array(val).flatten() val = numpy.array(val).flatten()
if not val.size == 2: if not val.size == 2:
raise PatternError('Radii must have length 2') raise PatternError('Radii must have length 2')
@ -89,11 +90,11 @@ class Ellipse(Shape, metaclass=AutoSlots):
def __init__( def __init__(
self, self,
radii: vector2, radii: ArrayLike,
*, *,
poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: Optional[float] = None, poly_max_arclen: Optional[float] = None,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
@ -106,6 +107,8 @@ class Ellipse(Shape, metaclass=AutoSlots):
LockableImpl.unlock(self) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(radii, numpy.ndarray))
assert(isinstance(offset, numpy.ndarray))
self._radii = radii self._radii = radii
self._offset = offset self._offset = offset
self._rotation = rotation self._rotation = rotation
@ -173,7 +176,7 @@ class Ellipse(Shape, metaclass=AutoSlots):
poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation) poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation)
return [poly] 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) rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii)
return numpy.vstack((self.offset - rot_radii[0], return numpy.vstack((self.offset - rot_radii[0],
self.offset + rot_radii[1])) self.offset + rot_radii[1]))

@ -1,15 +1,15 @@
from typing import List, Tuple, Dict, Optional, Sequence from typing import List, Tuple, Dict, Optional, Sequence, Any
import copy import copy
from enum import Enum from enum import Enum
import numpy # type: ignore import numpy
from numpy import pi, inf 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 Shape, normalized_shape_tuple, Polygon, Circle
from .. import PatternError from .. import PatternError
from ..repetition import Repetition 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 ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
from ..traits import LockableImpl 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. A normalized_form(...) is available, but can be quite slow with lots of vertices.
""" """
__slots__ = ('_vertices', '_width', '_cap', '_cap_extensions') __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions')
_vertices: numpy.ndarray _vertices: NDArray[numpy.float64]
_width: float _width: float
_cap: PathCap _cap: PathCap
_cap_extensions: Optional[numpy.ndarray] _cap_extensions: Optional[NDArray[numpy.float64]]
Cap = PathCap Cap = PathCap
@ -73,7 +73,7 @@ class Path(Shape, metaclass=AutoSlots):
# cap_extensions property # cap_extensions property
@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 Path end-cap extension
@ -83,7 +83,7 @@ class Path(Shape, metaclass=AutoSlots):
return self._cap_extensions return self._cap_extensions
@cap_extensions.setter @cap_extensions.setter
def cap_extensions(self, vals: Optional[numpy.ndarray]) -> None: def cap_extensions(self, vals: Optional[ArrayLike]) -> None:
custom_caps = (PathCap.SquareCustom,) custom_caps = (PathCap.SquareCustom,)
if self.cap in custom_caps: if self.cap in custom_caps:
if vals is None: if vals is None:
@ -96,7 +96,7 @@ class Path(Shape, metaclass=AutoSlots):
# vertices property # vertices property
@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], ...]`) Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
""" """
@ -113,7 +113,7 @@ class Path(Shape, metaclass=AutoSlots):
# xs property # xs property
@property @property
def xs(self) -> numpy.ndarray: def xs(self) -> NDArray[numpy.float64]:
""" """
All vertex x coords as a 1D ndarray All vertex x coords as a 1D ndarray
""" """
@ -128,7 +128,7 @@ class Path(Shape, metaclass=AutoSlots):
# ys property # ys property
@property @property
def ys(self) -> numpy.ndarray: def ys(self) -> NDArray[numpy.float64]:
""" """
All vertex y coords as a 1D ndarray All vertex y coords as a 1D ndarray
""" """
@ -148,7 +148,7 @@ class Path(Shape, metaclass=AutoSlots):
*, *,
cap: PathCap = PathCap.Flush, cap: PathCap = PathCap.Flush,
cap_extensions: Optional[ArrayLike] = None, cap_extensions: Optional[ArrayLike] = None,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
@ -163,6 +163,9 @@ class Path(Shape, metaclass=AutoSlots):
self.identifier = () self.identifier = ()
if raw: 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._vertices = vertices
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
@ -199,11 +202,11 @@ class Path(Shape, metaclass=AutoSlots):
@staticmethod @staticmethod
def travel( def travel(
travel_pairs: Tuple[Tuple[float, float]], travel_pairs: Sequence[Tuple[float, float]],
width: float = 0.0, width: float = 0.0,
cap: PathCap = PathCap.Flush, cap: PathCap = PathCap.Flush,
cap_extensions: Optional[Tuple[float, float]] = None, cap_extensions: Optional[Tuple[float, float]] = None,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
@ -236,7 +239,7 @@ class Path(Shape, metaclass=AutoSlots):
#TODO: needs testing #TODO: needs testing
direction = numpy.array([1, 0]) direction = numpy.array([1, 0])
verts = [[0, 0]] verts = [numpy.zeros(2)]
for angle, distance in travel_pairs: for angle, distance in travel_pairs:
direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
verts.append(verts[-1] + direction * distance) verts.append(verts[-1] + direction * distance)
@ -319,7 +322,7 @@ class Path(Shape, metaclass=AutoSlots):
return polys return polys
def get_bounds(self) -> numpy.ndarray: def get_bounds(self) -> NDArray[numpy.float64]:
if self.cap == PathCap.Circle: if self.cap == PathCap.Circle:
bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
numpy.max(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) self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
return self return self
def _calculate_cap_extensions(self) -> numpy.ndarray: def _calculate_cap_extensions(self) -> NDArray[numpy.float64]:
if self.cap == PathCap.Square: if self.cap == PathCap.Square:
extensions = numpy.full(2, self.width / 2) extensions = numpy.full(2, self.width / 2)
elif self.cap == PathCap.SquareCustom: elif self.cap == PathCap.SquareCustom:
assert(isinstance(self.cap_extensions, numpy.ndarray))
extensions = self.cap_extensions extensions = self.cap_extensions
else: else:
# Flush or Circle # Flush or Circle

@ -1,14 +1,14 @@
from typing import List, Dict, Optional, Sequence from typing import List, Dict, Optional, Sequence, Any
import copy import copy
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple from . import Shape, normalized_shape_tuple
from .. import PatternError from .. import PatternError
from ..repetition import Repetition 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 ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
from ..traits import LockableImpl from ..traits import LockableImpl
@ -22,12 +22,12 @@ class Polygon(Shape, metaclass=AutoSlots):
""" """
__slots__ = ('_vertices',) __slots__ = ('_vertices',)
_vertices: numpy.ndarray _vertices: NDArray[numpy.float64]
""" Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """
# vertices property # vertices property
@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], ...]`) Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
""" """
@ -44,7 +44,7 @@ class Polygon(Shape, metaclass=AutoSlots):
# xs property # xs property
@property @property
def xs(self) -> numpy.ndarray: def xs(self) -> NDArray[numpy.float64]:
""" """
All vertex x coords as a 1D ndarray All vertex x coords as a 1D ndarray
""" """
@ -59,7 +59,7 @@ class Polygon(Shape, metaclass=AutoSlots):
# ys property # ys property
@property @property
def ys(self) -> numpy.ndarray: def ys(self) -> NDArray[numpy.float64]:
""" """
All vertex y coords as a 1D ndarray All vertex y coords as a 1D ndarray
""" """
@ -76,7 +76,7 @@ class Polygon(Shape, metaclass=AutoSlots):
self, self,
vertices: ArrayLike, vertices: ArrayLike,
*, *,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
@ -89,6 +89,8 @@ class Polygon(Shape, metaclass=AutoSlots):
LockableImpl.unlock(self) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(vertices, numpy.ndarray))
assert(isinstance(offset, numpy.ndarray))
self._vertices = vertices self._vertices = vertices
self._offset = offset self._offset = offset
self._repetition = repetition self._repetition = repetition
@ -120,7 +122,7 @@ class Polygon(Shape, metaclass=AutoSlots):
side_length: float, side_length: float,
*, *,
rotation: float = 0.0, rotation: float = 0.0,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
@ -155,7 +157,7 @@ class Polygon(Shape, metaclass=AutoSlots):
ly: float, ly: float,
*, *,
rotation: float = 0, rotation: float = 0,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
@ -291,7 +293,7 @@ class Polygon(Shape, metaclass=AutoSlots):
side_length: Optional[float] = None, side_length: Optional[float] = None,
inner_radius: Optional[float] = None, inner_radius: Optional[float] = None,
regular: bool = True, regular: bool = True,
center: vector2 = (0.0, 0.0), center: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
@ -353,7 +355,7 @@ class Polygon(Shape, metaclass=AutoSlots):
) -> List['Polygon']: ) -> List['Polygon']:
return [copy.deepcopy(self)] 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), return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0),
self.offset + numpy.max(self.vertices, axis=0))) self.offset + numpy.max(self.vertices, axis=0)))

@ -1,8 +1,8 @@
from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy # type: ignore import numpy
from numpy.typing import ArrayLike from numpy.typing import NDArray, ArrayLike
from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl,
Rotatable, Mirrorable, Copyable, Scalable, Rotatable, Mirrorable, Copyable, Scalable,
@ -15,7 +15,7 @@ if TYPE_CHECKING:
# Type definitions # Type definitions
normalized_shape_tuple = Tuple[Tuple, normalized_shape_tuple = Tuple[Tuple,
Tuple[numpy.ndarray, float, float, bool, float], Tuple[NDArray[numpy.float64], float, float, bool, float],
Callable[[], 'Shape']] Callable[[], 'Shape']]
# ## Module-wide defaults # ## Module-wide defaults
@ -117,12 +117,16 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
""" """
from . import Polygon from . import Polygon
grid_x = numpy.unique(grid_x) gx = numpy.unique(grid_x)
grid_y = numpy.unique(grid_y) gy = numpy.unique(grid_y)
polygon_contours = [] polygon_contours = []
for polygon in self.to_polygons(): for polygon in self.to_polygons():
mins, maxs = polygon.get_bounds() bounds = polygon.get_bounds()
if not bounds:
continue
mins, maxs = bounds
vertex_lists = [] vertex_lists = []
p_verts = polygon.vertices + polygon.offset p_verts = polygon.vertices + polygon.offset
@ -130,12 +134,12 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
dv = v_next - v 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 # 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_range = numpy.digitize([v[0], v_next[0]], gx)
gxi_min = numpy.min(gxi_range - 1).clip(0, len(grid_x) - 1) gxi_min = numpy.min(gxi_range - 1).clip(0, len(gx) - 1)
gxi_max = numpy.max(gxi_range).clip(0, len(grid_x)) 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_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]) - grid_x[gxi_max - 1]) / (grid_x[gxi_max] - grid_x[gxi_max - 1]) err_xmax = (max(v[0], v_next[0]) - gx[gxi_max - 1]) / (gx[gxi_max] - gx[gxi_max - 1])
if err_xmin >= 0.5: if err_xmin >= 0.5:
gxi_min += 1 gxi_min += 1
@ -146,32 +150,32 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
# Vertical line, don't calculate slope # Vertical line, don't calculate slope
xi = [gxi_min, gxi_max - 1] xi = [gxi_min, gxi_max - 1]
ys = numpy.array([v[1], v_next[1]]) ys = numpy.array([v[1], v_next[1]])
yi = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) yi = numpy.digitize(ys, gy).clip(1, len(gy) - 1)
err_y = (ys - grid_y[yi]) / (grid_y[yi] - grid_y[yi - 1]) err_y = (ys - gy[yi]) / (gy[yi] - gy[yi - 1])
yi[err_y < 0.5] -= 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) vertex_lists.append(segment)
continue continue
m = dv[1] / dv[0] 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] 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 - 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 # 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) # (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 # now set inds to the index of the nearest y-grid line
inds[err < 0.5] -= 1 inds[err < 0.5] -= 1
return inds return inds
# Find the y indices on all x gridlines # 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) inds = get_grid_inds(xs)
# Find y-intersections for x-midpoints # Find y-intersections for x-midpoints
@ -186,7 +190,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
yinds[1::3] = inds2 yinds[1::3] = inds2
yinds[2::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: if dv[0] < 0:
vlist = vlist[::-1] vlist = vlist[::-1]
@ -249,23 +253,27 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
import skimage.measure # type: ignore import skimage.measure # type: ignore
import float_raster import float_raster
grid_x = numpy.unique(grid_x) grx = numpy.unique(grid_x)
grid_y = numpy.unique(grid_y) gry = numpy.unique(grid_y)
polygon_contours = [] polygon_contours = []
for polygon in self.to_polygons(): for polygon in self.to_polygons():
# Get rid of unused gridlines (anything not within 2 lines of the polygon bounds) # Get rid of unused gridlines (anything not within 2 lines of the polygon bounds)
mins, maxs = polygon.get_bounds() bounds = polygon.get_bounds()
keep_x = numpy.logical_and(grid_x > mins[0], grid_x < maxs[0]) if not bounds:
keep_y = numpy.logical_and(grid_y > mins[1], grid_y < maxs[1]) 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 k in (keep_x, keep_y):
for s in (1, 2): for s in (1, 2):
k[s:] += k[:-s] k[s:] += k[:-s]
k[:-s] += k[s:] k[:-s] += k[s:]
k = k > 0 k = k > 0
gx = grid_x[keep_x] gx = grx[keep_x]
gy = grid_y[keep_y] gy = gry[keep_y]
if len(gx) == 0 or len(gy) == 0: if len(gx) == 0 or len(gy) == 0:
continue continue
@ -286,8 +294,8 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
# /2 deals with supersampling # /2 deals with supersampling
# +.5 deals with the fact that our 0-edge becomes -.5 in the super-sampled contour output # +.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) snapped_contour = numpy.round((contour + .5) / 2).astype(int)
vertices = numpy.hstack((grid_x[snapped_contour[:, None, 0] + offset_i[0]], vertices = numpy.hstack((grx[snapped_contour[:, None, 0] + offset_i[0]],
grid_y[snapped_contour[:, None, 1] + offset_i[1]])) gry[snapped_contour[:, None, 1] + offset_i[1]]))
manhattan_polygons.append(Polygon( manhattan_polygons.append(Polygon(
vertices=vertices, vertices=vertices,

@ -1,14 +1,15 @@
from typing import List, Tuple, Dict, Sequence, Optional from typing import List, Tuple, Dict, Sequence, Optional, Any
import copy import copy
import numpy # type: ignore import numpy
from numpy import pi, inf from numpy import pi, inf
from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple from . import Shape, Polygon, normalized_shape_tuple
from .. import PatternError from .. import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..traits import RotatableImpl 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 ..utils import annotations_t
from ..traits import LockableImpl from ..traits import LockableImpl
@ -26,7 +27,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
_string: str _string: str
_height: float _height: float
_mirrored: numpy.ndarray # ndarray[bool] _mirrored: NDArray[numpy.bool_]
font_path: str font_path: str
# vertices property # vertices property
@ -51,7 +52,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
# Mirrored property # Mirrored property
@property @property
def mirrored(self) -> numpy.ndarray: # ndarray[bool] def mirrored(self) -> Any: #TODO mypy#3004 NDArray[numpy.bool_]:
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
@ -66,9 +67,9 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
height: float, height: float,
font_path: str, font_path: str,
*, *,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Tuple[bool, bool] = (False, False), mirrored: ArrayLike = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0, dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
@ -79,6 +80,8 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
LockableImpl.unlock(self) LockableImpl.unlock(self)
self.identifier = () self.identifier = ()
if raw: if raw:
assert(isinstance(offset, numpy.ndarray))
assert(isinstance(mirrored, numpy.ndarray))
self._offset = offset self._offset = offset
self._layer = layer self._layer = layer
self._dose = dose self._dose = dose
@ -155,7 +158,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
mirrored=(mirror_x, False), mirrored=(mirror_x, False),
layer=self.layer)) 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 # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
# just convert to polygons instead # just convert to polygons instead
bounds = numpy.array([[+inf, +inf], [-inf, -inf]]) bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
@ -201,7 +204,7 @@ def get_char_as_polygons(
outline = slot.outline outline = slot.outline
start = 0 start = 0
all_verts, all_codes = [], [] all_verts_list, all_codes = [], []
for end in outline.contours: for end in outline.contours:
points = outline.points[start:end + 1] points = outline.points[start:end + 1]
points.append(points[0]) points.append(points[0])
@ -238,11 +241,11 @@ def get_char_as_polygons(
codes.extend([Path.CURVE3, Path.CURVE3]) codes.extend([Path.CURVE3, Path.CURVE3])
verts.append(segment[-1]) verts.append(segment[-1])
codes.append(Path.CURVE3) codes.append(Path.CURVE3)
all_verts.extend(verts) all_verts_list.extend(verts)
all_codes.extend(codes) all_codes.extend(codes)
start = end + 1 start = end + 1
all_verts = numpy.array(all_verts) / resolution all_verts = numpy.array(all_verts_list) / resolution
advance = slot.advance.x / resolution advance = slot.advance.x / resolution

@ -7,11 +7,12 @@
from typing import Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any, TypeVar from typing import Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any, TypeVar
import copy import copy
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike
from .error import PatternError 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 .repetition import Repetition
from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl, Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl,
@ -40,7 +41,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
_pattern: Optional['Pattern'] _pattern: Optional['Pattern']
""" The `Pattern` being instanced """ """ 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. """ """ Whether to mirror the instance across the x and/or y axes. """
identifier: Tuple[Any, ...] identifier: Tuple[Any, ...]
@ -50,7 +51,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
self, self,
pattern: Optional['Pattern'], pattern: Optional['Pattern'],
*, *,
offset: vector2 = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Optional[Sequence[bool]] = None, mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0, dose: float = 1.0,
@ -113,7 +114,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
return self._pattern return self._pattern
@pattern.setter @pattern.setter
def pattern(self, val: Optional['Pattern']): def pattern(self, val: Optional['Pattern']) -> None:
from .pattern import Pattern from .pattern import Pattern
if val is not None and not isinstance(val, Pattern): if val is not None and not isinstance(val, Pattern):
raise PatternError(f'Provided pattern {val} is not a Pattern object or None!') 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 # Mirrored property
@property @property
def mirrored(self) -> numpy.ndarray: # ndarray[bool] def mirrored(self) -> Any: #TODO mypy#3004 NDArray[numpy.bool_]:
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
def mirrored(self, val: Sequence[bool]): def mirrored(self, val: ArrayLike) -> None:
if is_scalar(val): if is_scalar(val):
raise PatternError('Mirrored must be a 2-element list of booleans') raise PatternError('Mirrored must be a 2-element list of booleans')
self._mirrored = numpy.array(val, dtype=bool, copy=True) self._mirrored = numpy.array(val, dtype=bool, copy=True)
@ -167,7 +168,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
self.repetition.mirror(axis) self.repetition.mirror(axis)
return self 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 Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the `SubPattern` in each dimension. extent of the `SubPattern` in each dimension.

@ -1,11 +1,12 @@
# TODO top-level comment about how traits should set __slots__ = (), and how to use AutoSlots # 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 from abc import ABCMeta, abstractmethod
import numpy # type: ignore
import numpy
from numpy.typing import NDArray, ArrayLike
from ..error import MasqueError from ..error import MasqueError
from ..utils import vector2
T = TypeVar('T', bound='Positionable') T = TypeVar('T', bound='Positionable')
@ -23,7 +24,7 @@ class Positionable(metaclass=ABCMeta):
''' '''
@property @property
@abstractmethod @abstractmethod
def offset(self) -> numpy.ndarray: def offset(self) -> NDArray[numpy.float64]:
""" """
[x, y] offset [x, y] offset
""" """
@ -31,21 +32,11 @@ class Positionable(metaclass=ABCMeta):
# @offset.setter # @offset.setter
# @abstractmethod # @abstractmethod
# def offset(self, val: vector2): # def offset(self, val: ArrayLike):
# pass # pass
'''
--- Abstract methods
'''
@abstractmethod @abstractmethod
def get_bounds(self) -> numpy.ndarray: def set_offset(self: T, offset: ArrayLike) -> T:
"""
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
"""
pass
@abstractmethod
def set_offset(self: T, offset: vector2) -> T:
""" """
Set the offset Set the offset
@ -58,7 +49,7 @@ class Positionable(metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def translate(self: T, offset: vector2) -> T: def translate(self: T, offset: ArrayLike) -> T:
""" """
Translate the entity by the given offset Translate the entity by the given offset
@ -70,6 +61,13 @@ class Positionable(metaclass=ABCMeta):
""" """
pass 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): class PositionableImpl(Positionable, metaclass=ABCMeta):
""" """
@ -77,7 +75,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
""" """
__slots__ = () __slots__ = ()
_offset: numpy.ndarray _offset: NDArray[numpy.float64]
""" `[x_offset, y_offset]` """ """ `[x_offset, y_offset]` """
''' '''
@ -85,14 +83,14 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
''' '''
# offset property # offset property
@property @property
def offset(self) -> numpy.ndarray: def offset(self) -> Any: #TODO mypy#3003 NDArray[numpy.float64]:
""" """
[x, y] offset [x, y] offset
""" """
return self._offset return self._offset
@offset.setter @offset.setter
def offset(self, val: vector2): def offset(self, val: ArrayLike) -> None:
if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64: if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64:
val = numpy.array(val, dtype=float) val = numpy.array(val, dtype=float)
@ -103,12 +101,12 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
''' '''
---- Methods ---- Methods
''' '''
def set_offset(self: I, offset: vector2) -> I: def set_offset(self: I, offset: ArrayLike) -> I:
self.offset = offset self.offset = offset
return self return self
def translate(self: I, offset: vector2) -> I: def translate(self: I, offset: ArrayLike) -> I:
self._offset += offset self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
return self return self
def _lock(self: I) -> I: def _lock(self: I) -> I:

@ -1,12 +1,13 @@
from typing import TypeVar from typing import TypeVar
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy # type: ignore import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike, NDArray
#from .positionable import Positionable #from .positionable import Positionable
from ..error import MasqueError 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') T = TypeVar('T', bound='Rotatable')
I = TypeVar('I', bound='RotatableImpl') I = TypeVar('I', bound='RotatableImpl')
@ -89,7 +90,7 @@ class Pivotable(metaclass=ABCMeta):
__slots__ = () __slots__ = ()
@abstractmethod @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. Rotate the object around a point.
@ -109,11 +110,11 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
""" """
__slots__ = () __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) pivot = numpy.array(pivot, dtype=float)
self.translate(-pivot) self.translate(-pivot)
self.rotate(rotation) 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) self.translate(+pivot)
return self return self

@ -4,12 +4,11 @@ Various helper functions
from typing import Any, Union, Tuple, Sequence, Dict, List from typing import Any, Union, Tuple, Sequence, Dict, List
from abc import ABCMeta from abc import ABCMeta
import numpy # type: ignore import numpy
from numpy.typing import ArrayLike from numpy.typing import NDArray, ArrayLike
# Type definitions # Type definitions
vector2 = ArrayLike
layer_t = Union[int, Tuple[int, int], str] layer_t = Union[int, Tuple[int, int], str]
annotations_t = Dict[str, List[Union[int, float, 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 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. 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 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. Given a list of vertices, remove any consecutive duplicates.
@ -102,13 +101,14 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) ->
Returns: Returns:
`vertices` with no consecutive duplicates. `vertices` with no consecutive duplicates.
""" """
vertices = numpy.array(vertices)
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
if not closed_path: if not closed_path:
duplicates[0] = False duplicates[0] = False
return vertices[~duplicates] 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. Given a list of vertices, remove any superflous vertices (i.e.
those which lie along the line formed by their neighbors) those which lie along the line formed by their neighbors)

Loading…
Cancel
Save