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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save