diff --git a/README.md b/README.md index 23743e3..68c0686 100644 --- a/README.md +++ b/README.md @@ -233,5 +233,3 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...) * Tests tests tests * check renderpather * pather and renderpather examples -* context manager for retool -* allow a specific mismatch when connecting ports diff --git a/examples/test_rep.py b/examples/test_rep.py index f82575d..5ed6a6a 100644 --- a/examples/test_rep.py +++ b/examples/test_rep.py @@ -99,7 +99,6 @@ def main(): print('\nAdded aref_test') folder = Path('./layouts/') - folder.mkdir(exist_ok=True) print(f'...writing files to {folder}...') gds1 = folder / 'rep.gds.gz' diff --git a/examples/tutorial/basic_shapes.py b/examples/tutorial/basic_shapes.py index 87baaf0..f8e1eef 100644 --- a/examples/tutorial/basic_shapes.py +++ b/examples/tutorial/basic_shapes.py @@ -1,4 +1,4 @@ -from collections.abc import Sequence +from typing import Sequence import numpy from numpy import pi diff --git a/examples/tutorial/devices.py b/examples/tutorial/devices.py index 6b9cfa2..9556ee3 100644 --- a/examples/tutorial/devices.py +++ b/examples/tutorial/devices.py @@ -1,4 +1,4 @@ -from collections.abc import Sequence, Mapping +from typing import Sequence, Mapping import numpy from numpy import pi diff --git a/examples/tutorial/library.py b/examples/tutorial/library.py index eab8a12..f871f35 100644 --- a/examples/tutorial/library.py +++ b/examples/tutorial/library.py @@ -1,5 +1,4 @@ -from typing import Any -from collections.abc import Sequence, Callable +from typing import Sequence, Callable, Any from pprint import pformat import numpy diff --git a/examples/tutorial/pather.py b/examples/tutorial/pather.py index a7aad68..5aec53c 100644 --- a/examples/tutorial/pather.py +++ b/examples/tutorial/pather.py @@ -1,7 +1,7 @@ """ Manual wire routing tutorial: Pather and BasicTool """ -from collections.abc import Callable +from typing import Callable from numpy import pi from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers from masque.builder.tools import BasicTool, PathTool diff --git a/examples/tutorial/pcgen.py b/examples/tutorial/pcgen.py index 023079c..17243e4 100644 --- a/examples/tutorial/pcgen.py +++ b/examples/tutorial/pcgen.py @@ -2,7 +2,7 @@ Routines for creating normalized 2D lattices and common photonic crystal cavity designs. """ -from collection.abc import Sequence +from typing import Sequence import numpy from numpy.typing import ArrayLike, NDArray @@ -233,8 +233,8 @@ def ln_shift_defect( # Shift holes # Expand shifts as necessary - tmp_a = numpy.asarray(shifts_a) - tmp_r = numpy.asarray(shifts_r) + tmp_a = numpy.array(shifts_a) + tmp_r = numpy.array(shifts_r) n_shifted = max(tmp_a.size, tmp_r.size) shifts_a = numpy.ones(n_shifted) diff --git a/examples/tutorial/renderpather.py b/examples/tutorial/renderpather.py index cb002f3..a7963b8 100644 --- a/examples/tutorial/renderpather.py +++ b/examples/tutorial/renderpather.py @@ -1,7 +1,7 @@ """ Manual wire routing tutorial: RenderPather an PathTool """ -from collections.abc import Callable +from typing import Callable from masque import RenderPather, Library, Pattern, Port, layer_t, map_layers from masque.builder.tools import PathTool from masque.file.gdsii import writefile diff --git a/masque/__init__.py b/masque/__init__.py index 93eabdc..76ad2b9 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -28,65 +28,25 @@ can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally. """ -from .utils import ( - layer_t as layer_t, - annotations_t as annotations_t, - SupportsBool as SupportsBool, - ) -from .error import ( - MasqueError as MasqueError, - PatternError as PatternError, - LibraryError as LibraryError, - BuildError as BuildError, - ) -from .shapes import ( - Shape as Shape, - Polygon as Polygon, - Path as Path, - Circle as Circle, - Arc as Arc, - Ellipse as Ellipse, - ) -from .label import Label as Label -from .ref import Ref as Ref -from .pattern import ( - Pattern as Pattern, - map_layers as map_layers, - map_targets as map_targets, - chain_elements as chain_elements, - ) +from .utils import layer_t, annotations_t, SupportsBool +from .error import MasqueError, PatternError, LibraryError, BuildError +from .shapes import Shape, Polygon, Path, Circle, Arc, Ellipse +from .label import Label +from .ref import Ref +from .pattern import Pattern, map_layers, map_targets, chain_elements from .library import ( - ILibraryView as ILibraryView, - ILibrary as ILibrary, - LibraryView as LibraryView, - Library as Library, - LazyLibrary as LazyLibrary, - AbstractView as AbstractView, - TreeView as TreeView, - Tree as Tree, - ) -from .ports import ( - Port as Port, - PortList as PortList, - ) -from .abstract import Abstract as Abstract -from .builder import ( - Builder as Builder, - Tool as Tool, - Pather as Pather, - RenderPather as RenderPather, - RenderStep as RenderStep, - BasicTool as BasicTool, - PathTool as PathTool, - ) -from .utils import ( - ports2data as ports2data, - oneshot as oneshot, + ILibraryView, ILibrary, + LibraryView, Library, LazyLibrary, + AbstractView, TreeView, Tree, ) +from .ports import Port, PortList +from .abstract import Abstract +from .builder import Builder, Tool, Pather, RenderPather, RenderStep, BasicTool, PathTool +from .utils import ports2data, oneshot __author__ = 'Jan Petykiewicz' -__version__ = '3.2' +__version__ = '3.1' version = __version__ # legacy diff --git a/masque/abstract.py b/masque/abstract.py index 248c8a5..e3ab46e 100644 --- a/masque/abstract.py +++ b/masque/abstract.py @@ -97,7 +97,7 @@ class Abstract(PortList): Returns: self """ - pivot = numpy.asarray(pivot, dtype=float) + pivot = numpy.array(pivot) self.translate_ports(-pivot) self.rotate_ports(rotation) self.rotate_port_offsets(rotation) diff --git a/masque/builder/__init__.py b/masque/builder/__init__.py index 1eea9f1..465db5c 100644 --- a/masque/builder/__init__.py +++ b/masque/builder/__init__.py @@ -1,10 +1,5 @@ -from .builder import Builder as Builder -from .pather import Pather as Pather -from .renderpather import RenderPather as RenderPather -from .utils import ell as ell -from .tools import ( - Tool as Tool, - RenderStep as RenderStep, - BasicTool as BasicTool, - PathTool as PathTool, - ) +from .builder import Builder +from .pather import Pather +from .renderpather import RenderPather +from .utils import ell +from .tools import Tool, RenderStep, BasicTool, PathTool diff --git a/masque/builder/builder.py b/masque/builder/builder.py index b92c9ac..c18e17e 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -1,8 +1,7 @@ """ Simplified Pattern assembly (`Builder`) """ -from typing import Self -from collections.abc import Sequence, Mapping +from typing import Self, Sequence, Mapping import copy import logging from functools import wraps @@ -138,7 +137,7 @@ class Builder(PortList): @classmethod def interface( - cls: type['Builder'], + cls, source: PortList | Mapping[str, Port] | str, *, library: ILibrary | None = None, @@ -276,7 +275,7 @@ class Builder(PortList): logger.error('Skipping plug() since device is dead') return self - if not isinstance(other, str | Abstract | Pattern): + if not isinstance(other, (str, Abstract, Pattern)): # We got a Tree; add it into self.library and grab an Abstract for it other = self.library << other @@ -348,7 +347,7 @@ class Builder(PortList): logger.error('Skipping place() since device is dead') return self - if not isinstance(other, str | Abstract | Pattern): + if not isinstance(other, (str, Abstract, Pattern)): # We got a Tree; add it into self.library and grab an Abstract for it other = self.library << other diff --git a/masque/builder/pather.py b/masque/builder/pather.py index 17d73b3..c795e75 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -1,8 +1,7 @@ """ Manual wire/waveguide routing (`Pather`) """ -from typing import Self -from collections.abc import Sequence, MutableMapping, Mapping +from typing import Self, Sequence, MutableMapping, Mapping import copy import logging from pprint import pformat @@ -175,7 +174,7 @@ class Pather(Builder): @classmethod def from_builder( - cls: type['Pather'], + cls, builder: Builder, *, tools: Tool | MutableMapping[str | None, Tool] | None = None, @@ -195,7 +194,7 @@ class Pather(Builder): @classmethod def interface( - cls: type['Pather'], + cls, source: PortList | Mapping[str, Port] | str, *, library: ILibrary | None = None, @@ -658,7 +657,7 @@ class Pather(Builder): if not bound_types: raise BuildError('No bound type specified for mpath') - if len(bound_types) > 1: + elif len(bound_types) > 1: raise BuildError(f'Too many bound types specified for mpath: {bound_types}') bound_type = tuple(bound_types)[0] @@ -672,13 +671,13 @@ class Pather(Builder): # Not a bus, so having a container just adds noise to the layout port_name = tuple(portspec)[0] return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names, **kwargs) - - bld = Pather.interface(source=ports, library=self.library, tools=self.tools) - for port_name, length in extensions.items(): - bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs) - name = self.library.get_name(base_name) - self.library[name] = bld.pattern - return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'? + else: + bld = Pather.interface(source=ports, library=self.library, tools=self.tools) + for port_name, length in extensions.items(): + bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs) + name = self.library.get_name(base_name) + self.library[name] = bld.pattern + return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'? # TODO def bus_join()? diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 8dae18b..e35a672 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -1,8 +1,7 @@ """ Pather with batched (multi-step) rendering """ -from typing import Self -from collections.abc import Sequence, Mapping, MutableMapping +from typing import Self, Sequence, Mapping, MutableMapping import copy import logging from collections import defaultdict @@ -128,7 +127,7 @@ class RenderPather(PortList): @classmethod def interface( - cls: type['RenderPather'], + cls, source: PortList | Mapping[str, Port] | str, *, library: ILibrary | None = None, @@ -248,7 +247,7 @@ class RenderPather(PortList): other_tgt = self.library[other.name] # get rid of plugged ports - for kk in map_in: + for kk in map_in.keys(): if kk in self.paths: self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None)) @@ -561,7 +560,7 @@ class RenderPather(PortList): if not bound_types: raise BuildError('No bound type specified for mpath') - if len(bound_types) > 1: + elif len(bound_types) > 1: raise BuildError(f'Too many bound types specified for mpath: {bound_types}') bound_type = tuple(bound_types)[0] diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 0e9ec33..1b7a74f 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -3,8 +3,7 @@ Tools are objects which dynamically generate simple single-use devices (e.g. wir # TODO document all tools """ -from typing import Literal, Any -from collections.abc import Sequence, Callable +from typing import Sequence, Literal, Callable, Any from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method? from dataclasses import dataclass @@ -223,8 +222,8 @@ class Tool: self, batch: Sequence[RenderStep], *, - port_names: Sequence[str] = ('A', 'B'), # noqa: ARG002 (unused) - **kwargs, # noqa: ARG002 (unused) + port_names: Sequence[str] = ('A', 'B'), + **kwargs, ) -> ILibrary: """ Render the provided `batch` of `RenderStep`s into geometry, returning a tree @@ -313,7 +312,7 @@ class BasicTool(Tool, metaclass=ABCMeta): *, in_ptype: str | None = None, out_ptype: str | None = None, - **kwargs, # noqa: ARG002 (unused) + **kwargs, ) -> tuple[Port, LData]: # TODO check all the math for L-shaped bends if ccw is not None: @@ -405,7 +404,7 @@ class BasicTool(Tool, metaclass=ABCMeta): ipat, iport_theirs, _iport_ours = in_transition pat.plug(ipat, {port_names[1]: iport_theirs}) if not numpy.isclose(straight_length, 0): - straight_pat = gen_straight(straight_length, **kwargs) + straight_pat = gen_straight(straight_length) if append: pat.plug(straight_pat, {port_names[1]: sport_in}, append=True) else: @@ -455,7 +454,7 @@ class PathTool(Tool, metaclass=ABCMeta): in_ptype: str | None = None, out_ptype: str | None = None, port_names: tuple[str, str] = ('A', 'B'), - **kwargs, # noqa: ARG002 (unused) + **kwargs, ) -> Library: out_port, dxy = self.planL( ccw, @@ -486,9 +485,9 @@ class PathTool(Tool, metaclass=ABCMeta): ccw: SupportsBool | None, length: float, *, - in_ptype: str | None = None, # noqa: ARG002 (unused) + in_ptype: str | None = None, out_ptype: str | None = None, - **kwargs, # noqa: ARG002 (unused) + **kwargs, ) -> tuple[Port, NDArray[numpy.float64]]: # TODO check all the math for L-shaped bends @@ -522,7 +521,7 @@ class PathTool(Tool, metaclass=ABCMeta): batch: Sequence[RenderStep], *, port_names: Sequence[str] = ('A', 'B'), - **kwargs, # noqa: ARG002 (unused) + **kwargs, ) -> ILibrary: path_vertices = [batch[0].start_port.offset] diff --git a/masque/builder/utils.py b/masque/builder/utils.py index c466c71..51d5e8b 100644 --- a/masque/builder/utils.py +++ b/masque/builder/utils.py @@ -1,5 +1,4 @@ -from typing import SupportsFloat, cast, TYPE_CHECKING -from collections.abc import Mapping, Sequence +from typing import Mapping, Sequence, SupportsFloat, cast, TYPE_CHECKING from pprint import pformat import numpy @@ -113,7 +112,7 @@ def ell( is_horizontal = numpy.isclose(rotations[0] % pi, 0) if bound_type in ('ymin', 'ymax') and is_horizontal: raise BuildError(f'Asked for {bound_type} position but ports are pointing along the x-axis!') - if bound_type in ('xmin', 'xmax') and not is_horizontal: + elif bound_type in ('xmin', 'xmax') and not is_horizontal: raise BuildError(f'Asked for {bound_type} position but ports are pointing along the y-axis!') direction = rotations[0] + pi # direction we want to travel in (+pi relative to port) @@ -202,7 +201,7 @@ def ell( if extension < 0: ext_floor = -numpy.floor(extension) raise BuildError(f'Position is too close by at least {ext_floor}. Total extensions would be\n\t' - + '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets, strict=True))) + + '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets))) - result = dict(zip(ports.keys(), offsets, strict=True)) + result = dict(zip(ports.keys(), offsets)) return result diff --git a/masque/file/dxf.py b/masque/file/dxf.py index dc3d6f3..31582b7 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -6,8 +6,7 @@ Notes: * ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID to unique values, so byte-for-byte reproducibility is not achievable for now """ -from typing import Any, cast, TextIO, IO -from collections.abc import Mapping, Callable +from typing import Any, Callable, Mapping, cast, TextIO, IO import io import logging import pathlib @@ -16,7 +15,6 @@ import gzip import numpy import ezdxf from ezdxf.enums import TextEntityAlignment -from ezdxf.entities import LWPolyline, Polyline, Text, Insert from .utils import is_gzipped, tmpfile from .. import Pattern, Ref, PatternError, Label @@ -40,7 +38,7 @@ def write( top_name: str, stream: TextIO, *, - dxf_version: str = 'AC1024', + dxf_version='AC1024', ) -> None: """ Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes @@ -206,25 +204,26 @@ def read( return mlib, library_info -def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> tuple[str, Pattern]: +def _read_block(block) -> tuple[str, Pattern]: name = block.name pat = Pattern() for element in block: - if isinstance(element, LWPolyline | Polyline): - if isinstance(element, LWPolyline): - points = numpy.asarray(element.get_points()) - elif isinstance(element, Polyline): - points = numpy.asarray(element.points())[:, :2] + eltype = element.dxftype() + if eltype in ('POLYLINE', 'LWPOLYLINE'): + if eltype == 'LWPOLYLINE': + points = numpy.array(tuple(element.lwpoints)) + else: + points = numpy.array(tuple(element.points())) attr = element.dxfattribs() layer = attr.get('layer', DEFAULT_LAYER) if points.shape[1] == 2: raise PatternError('Invalid or unimplemented polygon?') - - if points.shape[1] > 2: + #shape = Polygon() + elif points.shape[1] > 2: if (points[0, 2] != points[:, 2]).any(): raise PatternError('PolyLine has non-constant width (not yet representable in masque!)') - if points.shape[1] == 4 and (points[:, 3] != 0).any(): + elif points.shape[1] == 4 and (points[:, 3] != 0).any(): raise PatternError('LWPolyLine has bulge (not yet representable in masque!)') width = points[0, 2] @@ -239,9 +238,9 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> pat.shapes[layer].append(shape) - elif isinstance(element, Text): + elif eltype in ('TEXT',): args = dict( - offset=numpy.asarray(element.get_placement()[1])[:2], + offset=numpy.array(element.get_pos()[1])[:2], layer=element.dxfattribs().get('layer', DEFAULT_LAYER), ) string = element.dxfattribs().get('text', '') @@ -252,7 +251,7 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> pat.label(string=string, **args) # else: # pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????)) - elif isinstance(element, Insert): + elif eltype in ('INSERT',): attr = element.dxfattribs() xscale = attr.get('xscale', 1) yscale = attr.get('yscale', 1) @@ -262,7 +261,7 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0)) rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle - offset = numpy.asarray(attr.get('insert', (0, 0, 0)))[:2] + offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2] args = dict( target=attr.get('name', None), @@ -337,10 +336,10 @@ def _mrefs_to_drefs( def _shapes_to_elements( block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, shapes: dict[layer_t, list[Shape]], + polygonize_paths: bool = False, ) -> None: # Add `LWPolyline`s for each shape. # Could set do paths with width setting, but need to consider endcaps. - # TODO: can DXF do paths? for layer, sseq in shapes.items(): attribs = dict(layer=_mlayer2dxf(layer)) for shape in sseq: diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 71ea94f..4dce32c 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -19,8 +19,7 @@ Notes: * GDS creation/modification/access times are set to 1900-01-01 for reproducibility. * Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01) """ -from typing import IO, cast, Any -from collections.abc import Iterable, Mapping, Callable +from typing import Callable, Iterable, Mapping, IO, cast, Any import io import mmap import logging @@ -357,7 +356,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R if isinstance(rep, Grid): 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 = numpy.asarray(ref.offset) + numpy.array([ + xy = numpy.array(ref.offset) + numpy.array([ [0.0, 0.0], rep.a_vector * rep.a_count, b_vector * b_count, @@ -409,8 +408,8 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) - for key, vals in annotations.items(): try: i = int(key) - except ValueError as err: - raise PatternError(f'Annotation key {key} is not convertable to an integer') from err + except ValueError: + raise PatternError(f'Annotation key {key} is not convertable to an integer') if not (0 < i < 126): raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') @@ -597,19 +596,19 @@ def load_libraryfile( path = pathlib.Path(filename) stream: IO[bytes] if is_gzipped(path): - if use_mmap: + if mmap: logger.info('Asked to mmap a gzipped file, reading into memory instead...') - gz_stream = gzip.open(path, mode='rb') # noqa: SIM115 + gz_stream = gzip.open(path, mode='rb') stream = io.BytesIO(gz_stream.read()) # type: ignore else: - gz_stream = gzip.open(path, mode='rb') # noqa: SIM115 + gz_stream = gzip.open(path, mode='rb') stream = io.BufferedReader(gz_stream) # type: ignore - else: # noqa: PLR5501 - if use_mmap: - base_stream = path.open(mode='rb', buffering=0) # noqa: SIM115 + else: + if mmap: + base_stream = open(path, mode='rb', buffering=0) stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore else: - stream = path.open(mode='rb') # noqa: SIM115 + stream = open(path, mode='rb') return load_library(stream, full_load=full_load, postprocess=postprocess) diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 0e2305a..befa325 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -14,8 +14,7 @@ Note that OASIS references follow the same convention as `masque`, Notes: * Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01) """ -from typing import Any, IO, cast -from collections.abc import Sequence, Iterable, Mapping, Callable +from typing import Any, Callable, Iterable, IO, Mapping, cast, Sequence import logging import pathlib import gzip @@ -298,7 +297,7 @@ def read( cap_start = path_cap_map[element.get_extension_start()[0]] cap_end = path_cap_map[element.get_extension_end()[0]] if cap_start != cap_end: - raise PatternError('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types + raise Exception('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types cap = cap_start path_args: dict[str, Any] = {} @@ -453,8 +452,6 @@ def read( for placement in cell.placements: target, ref = _placement_to_ref(placement, lib) - if isinstance(target, int): - target = lib.cellnames[target].nstring.string pat.refs[target].append(ref) mlib[cell_name] = pat @@ -695,9 +692,9 @@ def properties_to_annotations( assert proprec.values is not None for value in proprec.values: - if isinstance(value, float | int): + if isinstance(value, (float, int)): values.append(value) - elif isinstance(value, NString | AString): + elif isinstance(value, (NString, AString)): values.append(value.string) elif isinstance(value, PropStringReference): values.append(propstrings[value.ref].string) # dereference diff --git a/masque/file/svg.py b/masque/file/svg.py index 148f6d4..baf2252 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -1,7 +1,7 @@ """ SVG file format readers and writers """ -from collections.abc import Mapping +from typing import Mapping import warnings import numpy @@ -50,7 +50,7 @@ def writefile( bounds = pattern.get_bounds(library=library) if bounds is None: bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]]) - warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1) + warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox') else: bounds_min, bounds_max = bounds @@ -117,7 +117,7 @@ def writefile_inverted( bounds = pattern.get_bounds(library=library) if bounds is None: bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]]) - warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1) + warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox') else: bounds_min, bounds_max = bounds @@ -154,9 +154,9 @@ def poly2path(vertices: ArrayLike) -> str: Returns: SVG path-string. """ - verts = numpy.asarray(vertices) - commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1]) # noqa: UP032 + 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]) # noqa: UP032 + commands += 'L{:g},{:g}'.format(vertex[0], vertex[1]) commands += ' Z ' return commands diff --git a/masque/file/utils.py b/masque/file/utils.py index 33f68d4..898bd05 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -1,8 +1,7 @@ """ Helper functions for file reading and writing """ -from typing import IO -from collections.abc import Iterator, Mapping +from typing import IO, Iterator, Mapping import re import pathlib import logging @@ -117,7 +116,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern: for shapes in pat.shapes.values(): remove_inds = [] for ii, shape in enumerate(shapes): - if not isinstance(shape, Polygon | Path): + if not isinstance(shape, (Polygon, Path)): continue try: shape.clean_vertices() @@ -129,7 +128,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern: def is_gzipped(path: pathlib.Path) -> bool: - with path.open('rb') as stream: + with open(path, 'rb') as stream: magic_bytes = stream.read(2) return magic_bytes == b'\x1f\x8b' diff --git a/masque/label.py b/masque/label.py index 711ef35..7eb9068 100644 --- a/masque/label.py +++ b/masque/label.py @@ -49,7 +49,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl annotations: annotations_t | None = None, ) -> None: self.string = string - self.offset = numpy.array(offset, dtype=float) + self.offset = numpy.array(offset, dtype=float, copy=True) self.repetition = repetition self.annotations = annotations if annotations is not None else {} @@ -94,7 +94,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl Returns: self """ - pivot = numpy.asarray(pivot, dtype=float) + pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) self.translate(+pivot) diff --git a/masque/library.py b/masque/library.py index 001ae72..1358198 100644 --- a/masque/library.py +++ b/masque/library.py @@ -14,14 +14,17 @@ Classes include: - `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked library. Generated with `ILibraryView.abstract_view()`. """ -from typing import Self, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal -from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable +from typing import Callable, Self, Type, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal +from typing import Iterator, Mapping, MutableMapping, Sequence import logging +import base64 +import struct import re import copy from pprint import pformat from collections import defaultdict from abc import ABCMeta, abstractmethod +from functools import lru_cache import numpy from numpy.typing import ArrayLike, NDArray @@ -173,7 +176,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): tops = tuple(self.keys()) if skip is None: - skip = {None} + skip = set([None]) if isinstance(tops, str): tops = (tops,) @@ -210,7 +213,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if isinstance(tops, str): tops = (tops,) - keep = cast(set[str], self.referenced_patterns(tops) - {None}) + keep = cast(set[str], self.referenced_patterns(tops) - set((None,))) keep |= set(tops) filtered = {kk: vv for kk, vv in self.items() if kk in keep} @@ -282,7 +285,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if isinstance(tops, str): tops = (tops,) - flattened: dict[str, Pattern | None] = {} + flattened: dict[str, 'Pattern | None'] = {} def flatten_single(name: str) -> None: flattened[name] = None @@ -346,11 +349,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): else: sanitized_name = name + ii = 0 suffixed_name = sanitized_name - if sanitized_name in self: - ii = sum(1 for nn in self.keys() if nn.startswith(sanitized_name)) - else: - ii = 0 while suffixed_name in self or suffixed_name == '': suffixed_name = sanitized_name + b64suffix(ii) ii += 1 @@ -460,7 +460,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if transform is None or transform is True: transform = numpy.zeros(4) elif transform is not False: - transform = numpy.asarray(transform, dtype=float) + transform = numpy.array(transform, dtype=float, copy=False) original_pattern = pattern @@ -665,7 +665,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): duplicates = set(self.keys()) & set(other.keys()) if not duplicates: - for key in other: + for key in other.keys(): self._merge(key, other, key) return {} @@ -735,7 +735,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): def dedup( self, norm_value: int = int(1e6), - exclude_types: tuple[type] = (Polygon,), + exclude_types: tuple[Type] = (Polygon,), label2name: Callable[[tuple], str] | None = None, threshold: int = 2, ) -> Self: @@ -773,7 +773,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): exclude_types = () if label2name is None: - def label2name(label: tuple) -> str: # noqa: ARG001 + def label2name(label): return self.get_name(SINGLE_USE_PREFIX + 'shape') shape_counts: MutableMapping[tuple, int] = defaultdict(int) @@ -863,7 +863,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): from .pattern import Pattern if name_func is None: - def name_func(_pat: Pattern, _shape: Shape | Label) -> str: + def name_func(_pat, _shape): return self.get_name(SINGLE_USE_PREFIX + 'rep') for pat in tuple(self.values()): @@ -912,7 +912,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): if isinstance(tops, str): tops = (tops,) - keep = cast(set[str], self.referenced_patterns(tops) - {None}) + keep = cast(set[str], self.referenced_patterns(tops) - set((None,))) keep |= set(tops) new = type(self)() @@ -934,7 +934,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): A set containing the names of all deleted patterns """ trimmed = set() - while empty := {name for name, pat in self.items() if pat.is_empty()}: + while empty := set(name for name, pat in self.items() if pat.is_empty()): for name in empty: del self[name] @@ -1038,7 +1038,10 @@ class Library(ILibrary): if key in self.mapping: raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!') - value = value() if callable(value) else value + if callable(value): + value = value() + else: + value = value self.mapping[key] = value def __delitem__(self, key: str) -> None: @@ -1051,7 +1054,7 @@ class Library(ILibrary): return f'' @classmethod - def mktree(cls: type[Self], name: str) -> tuple[Self, 'Pattern']: + def mktree(cls, name: str) -> tuple[Self, 'Pattern']: """ Create a new Library and immediately add a pattern @@ -1229,18 +1232,8 @@ class AbstractView(Mapping[str, Abstract]): return self.library.__len__() +@lru_cache(maxsize=8_000) def b64suffix(ii: int) -> str: - """ - Turn an integer into a base64-equivalent suffix. - - This could be done with base64.b64encode, but this way is faster for many small `ii`. - """ - def i2a(nn: int) -> str: - return 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$?'[nn] - - parts = ['$', i2a(ii % 64)] - ii >>= 6 - while ii: - parts.append(i2a(ii % 64)) - ii >>= 6 - return ''.join(parts) + """Turn an integer into a base64-equivalent suffix.""" + suffix = base64.b64encode(struct.pack('>Q', ii), altchars=b'$?').decode('ASCII') + return '$' + suffix[:-1].lstrip('A') diff --git a/masque/pattern.py b/masque/pattern.py index 1816762..614124a 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -2,8 +2,7 @@ Object representing a one multi-layer lithographic layout. A single level of hierarchical references is included. """ -from typing import cast, Self, Any, TypeVar -from collections.abc import Sequence, Mapping, MutableMapping, Iterable, Callable +from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping import copy import logging import functools @@ -296,7 +295,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): if not annotations_eq(self.annotations, other.annotations): return False - if not ports_eq(self.ports, other.ports): # noqa: SIM103 + if not ports_eq(self.ports, other.ports): return False return True @@ -313,10 +312,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): self """ if sort_elements: - def maybe_sort(xx): # noqa:ANN001,ANN202 + def maybe_sort(xx): return sorted(xx) else: - def maybe_sort(xx): # noqa:ANN001,ANN202 + def maybe_sort(xx): return xx self.refs = defaultdict(list, sorted( @@ -472,10 +471,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): self.polygonize() for layer in self.shapes: - self.shapes[layer] = list(chain.from_iterable( + self.shapes[layer] = list(chain.from_iterable(( ss.manhattanize(grid_x, grid_y) for ss in self.shapes[layer] - )) + ))) return self def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]: @@ -595,7 +594,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): if (cbounds[1] < cbounds[0]).any(): return None - return cbounds + else: + return cbounds def get_bounds_nonempty( self, @@ -616,7 +616,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: `[[x_min, y_min], [x_max, y_max]]` """ - bounds = self.get_bounds(library, recurse=recurse) + bounds = self.get_bounds(library) assert bounds is not None return bounds @@ -690,7 +690,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - pivot = numpy.asarray(pivot, dtype=float) + pivot = numpy.array(pivot) self.translate_elements(-pivot) self.rotate_elements(rotation) self.rotate_element_centers(rotation) @@ -953,7 +953,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns: self """ - flattened: dict[str | None, Pattern | None] = {} + flattened: dict[str | None, 'Pattern | None'] = {} def flatten_single(name: str | None) -> None: if name is None: @@ -1015,15 +1015,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): try: from matplotlib import pyplot # type: ignore import matplotlib.collections # type: ignore - except ImportError: - logger.exception('Pattern.visualize() depends on matplotlib!\n' - + 'Make sure to install masque with the [visualize] option to pull in the needed dependencies.') - raise + except ImportError as err: + logger.error('Pattern.visualize() depends on matplotlib!') + logger.error('Make sure to install masque with the [visualize] option to pull in the needed dependencies.') + raise err if self.has_refs() and library is None: raise PatternError('Must provide a library when visualizing a pattern with refs') - offset = numpy.asarray(offset, dtype=float) + offset = numpy.array(offset, dtype=float) if not overdraw: figure = pyplot.figure() @@ -1324,7 +1324,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): @classmethod def interface( - cls: type['Pattern'], + cls, source: PortList | Mapping[str, Port], *, in_prefix: str = 'in_', diff --git a/masque/ports.py b/masque/ports.py index 6f029b1..9ee58f5 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -1,5 +1,4 @@ -from typing import overload, Self, NoReturn, Any -from collections.abc import Iterable, KeysView, ValuesView, Mapping +from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn, Any import warnings import traceback import logging @@ -93,7 +92,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable): def copy(self) -> Self: return self.deepcopy() - def get_bounds(self) -> NDArray[numpy.float64]: + def get_bounds(self): return numpy.vstack((self.offset, self.offset)) def set_ptype(self, ptype: str) -> Self: @@ -181,7 +180,7 @@ class PortList(metaclass=ABCMeta): """ if isinstance(key, str): return self.ports[key] - else: # noqa: RET505 + else: return {k: self.ports[k] for k in key} def __contains__(self, key: str) -> NoReturn: @@ -239,7 +238,7 @@ class PortList(metaclass=ABCMeta): if duplicates: raise PortError(f'Unrenamed ports would be overwritten: {duplicates}') - renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()} + renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()} if None in renamed: del renamed[None] @@ -294,14 +293,14 @@ class PortList(metaclass=ABCMeta): Raises: `PortError` if the ports are not properly aligned. """ - a_names, b_names = list(zip(*connections.items(), strict=True)) + a_names, b_names = list(zip(*connections.items())) a_ports = [self.ports[pp] for pp in a_names] b_ports = [self.ports[pp] for pp in b_names] a_types = [pp.ptype for pp in a_ports] b_types = [pp.ptype for pp in b_ports] - type_conflicts = numpy.array([at != bt and 'unk' not in (at, bt) - for at, bt in zip(a_types, b_types, strict=True)]) + type_conflicts = numpy.array([at != bt and at != 'unk' and bt != 'unk' + for at, bt in zip(a_types, b_types)]) if type_conflicts.any(): msg = 'Ports have conflicting types:\n' @@ -502,8 +501,8 @@ class PortList(metaclass=ABCMeta): o_offsets[:, 1] *= -1 o_rotations *= -1 - type_conflicts = numpy.array([st != ot and 'unk' not in (st, ot) - for st, ot in zip(s_types, o_types, strict=True)]) + type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk' + for st, ot in zip(s_types, o_types)]) if type_conflicts.any(): msg = 'Ports have conflicting types:\n' for nn, (k, v) in enumerate(map_in.items()): diff --git a/masque/ref.py b/masque/ref.py index b84114b..9c9c519 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -2,8 +2,7 @@ Ref provides basic support for nesting Pattern objects within each other. It carries offset, rotation, mirroring, and scaling data for each individual instance. """ -from typing import TYPE_CHECKING, Self, Any -from collections.abc import Mapping +from typing import Mapping, TYPE_CHECKING, Self, Any import copy import functools diff --git a/masque/repetition.py b/masque/repetition.py index a365909..5436c11 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -2,7 +2,7 @@ Repetitions provide support for efficiently representing multiple identical instances of an object . """ -from typing import Any, Self, TypeVar, cast +from typing import Any, Type, Self, TypeVar, cast import copy import functools from abc import ABCMeta, abstractmethod @@ -101,7 +101,8 @@ class Grid(Repetition): if b_vector is None: if b_count > 1: raise PatternError('Repetition has b_count > 1 but no b_vector') - b_vector = numpy.array([0.0, 0.0]) + else: + b_vector = numpy.array([0.0, 0.0]) if a_count < 1: raise PatternError(f'Repetition has too-small a_count: {a_count}') @@ -115,7 +116,7 @@ class Grid(Repetition): @classmethod def aligned( - cls: type[GG], + cls: Type[GG], x: float, y: float, x_count: int, @@ -156,11 +157,12 @@ class Grid(Repetition): @a_vector.setter def a_vector(self, val: ArrayLike) -> None: - val = numpy.array(val, dtype=float) + if not isinstance(val, numpy.ndarray): + val = numpy.array(val, dtype=float) if val.size != 2: raise PatternError('a_vector must be convertible to size-2 ndarray') - self._a_vector = val.flatten() + self._a_vector = val.flatten().astype(float) # b_vector property @property @@ -169,7 +171,8 @@ class Grid(Repetition): @b_vector.setter def b_vector(self, val: ArrayLike) -> None: - val = numpy.array(val, dtype=float) + if not isinstance(val, numpy.ndarray): + val = numpy.array(val, dtype=float, copy=True) if val.size != 2: raise PatternError('b_vector must be convertible to size-2 ndarray') @@ -287,7 +290,7 @@ class Grid(Repetition): return True if self.b_vector is None or other.b_vector is None: return False - if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): # noqa: SIM103 + if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): return False return True @@ -332,9 +335,9 @@ class Arbitrary(Repetition): @displacements.setter def displacements(self, val: ArrayLike) -> None: - vala = numpy.array(val, dtype=float) - order = numpy.lexsort(vala.T[::-1]) # sortrows - self._displacements = vala[order] + vala: NDArray[numpy.float64] = numpy.array(val, dtype=float) + vala = numpy.sort(vala.view([('', vala.dtype)] * vala.shape[1]), 0).view(vala.dtype) # sort rows + self._displacements = vala def __init__( self, diff --git a/masque/shapes/__init__.py b/masque/shapes/__init__.py index 8ad46ef..ab4a83a 100644 --- a/masque/shapes/__init__.py +++ b/masque/shapes/__init__.py @@ -3,15 +3,11 @@ Shapes for use with the Pattern class, as well as the Shape abstract class from which they are derived. """ -from .shape import ( - Shape as Shape, - normalized_shape_tuple as normalized_shape_tuple, - DEFAULT_POLY_NUM_VERTICES as DEFAULT_POLY_NUM_VERTICES, - ) +from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES -from .polygon import Polygon as Polygon -from .circle import Circle as Circle -from .ellipse import Ellipse as Ellipse -from .arc import Arc as Arc -from .text import Text as Text -from .path import Path as Path +from .polygon import Polygon +from .circle import Circle +from .ellipse import Ellipse +from .arc import Arc +from .text import Text +from .path import Path diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index eb565bf..aa171ed 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -286,7 +286,7 @@ class Arc(Shape): return thetas wh = self.width / 2.0 - if wh in (r0, r1): + if wh == r0 or wh == r1: thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin else: thetas_inner = get_thetas(inner=True) @@ -308,7 +308,7 @@ class Arc(Shape): return [poly] def get_bounds_single(self) -> NDArray[numpy.float64]: - """ + ''' Equation for rotated ellipse is `x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)` `y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)` @@ -319,12 +319,12 @@ class Arc(Shape): where -+ is for x, y cases, so that's where the extrema are. If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. - """ + ''' a_ranges = self._angles_to_parameters() mins = [] maxs = [] - for a, sgn in zip(a_ranges, (-1, +1), strict=True): + for a, sgn in zip(a_ranges, (-1, +1)): wh = sgn * self.width / 2 rx = self.radius_x + wh ry = self.radius_y + wh @@ -424,18 +424,18 @@ class Arc(Shape): )) def get_cap_edges(self) -> NDArray[numpy.float64]: - """ + ''' Returns: ``` [[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which [[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse. ``` - """ + ''' a_ranges = self._angles_to_parameters() mins = [] maxs = [] - for a, sgn in zip(a_ranges, (-1, +1), strict=True): + for a, sgn in zip(a_ranges, (-1, +1)): wh = sgn * self.width / 2 rx = self.radius_x + wh ry = self.radius_y + wh @@ -454,11 +454,11 @@ class Arc(Shape): return numpy.array([mins, maxs]) + self.offset def _angles_to_parameters(self) -> NDArray[numpy.float64]: - """ + ''' Returns: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form `[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]` - """ + ''' a = [] for sgn in (-1, +1): wh = sgn * self.width / 2 @@ -472,7 +472,7 @@ class Arc(Shape): a1 += sign * 2 * pi a.append((a0, a1)) - return numpy.array(a, dtype=float) + return numpy.array(a) def __repr__(self) -> str: angles = f' a°{numpy.rad2deg(self.angles)}' diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 5f8ebe0..628f8b8 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -119,10 +119,10 @@ class Circle(Shape): return numpy.vstack((self.offset - self.radius, self.offset + self.radius)) - def rotate(self, theta: float) -> 'Circle': # noqa: ARG002 (theta unused) + def rotate(self, theta: float) -> 'Circle': return self - def mirror(self, axis: int = 0) -> 'Circle': # noqa: ARG002 (axis unused) + def mirror(self, axis: int = 0) -> 'Circle': self.offset *= -1 return self @@ -130,7 +130,7 @@ class Circle(Shape): self.radius *= c return self - def normalized_form(self, norm_value: float) -> normalized_shape_tuple: + def normalized_form(self, norm_value) -> normalized_shape_tuple: rotation = 0.0 magnitude = self.radius / norm_value return ((type(self),), diff --git a/masque/shapes/path.py b/masque/shapes/path.py index aaac0d2..d87286a 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,5 +1,4 @@ -from typing import Any, cast -from collections.abc import Sequence +from typing import Sequence, Any, cast import copy import functools from enum import Enum @@ -33,7 +32,8 @@ class Path(Shape): A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, and an offset. - Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates. + Note that the setter for `Path.vertices` may (but may not) create a copy of the + passed vertex coordinates. See `numpy.array(..., copy=False)` for details. A normalized_form(...) is available, but can be quite slow with lots of vertices. """ @@ -104,11 +104,11 @@ class Path(Shape): custom_caps = (PathCap.SquareCustom,) if self.cap in custom_caps: if vals is None: - raise PatternError('Tried to set cap extensions to None on path with custom cap type') + raise Exception('Tried to set cap extensions to None on path with custom cap type') self._cap_extensions = numpy.array(vals, dtype=float) else: if vals is not None: - raise PatternError('Tried to set custom cap extensions on path with non-custom cap type') + raise Exception('Tried to set custom cap extensions on path with non-custom cap type') self._cap_extensions = vals # vertices property @@ -117,7 +117,8 @@ class Path(Shape): """ Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]` - When setting, note that a copy of the provided vertices will be made. + When setting, note that a copy of the provided vertices may or may not be made, + following the rules from `numpy.array(.., copy=False)`. """ return self._vertices @@ -429,22 +430,22 @@ class Path(Shape): return self def remove_duplicate_vertices(self) -> 'Path': - """ + ''' Removes all consecutive duplicate (repeated) vertices. Returns: self - """ + ''' self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False) return self def remove_colinear_vertices(self) -> 'Path': - """ + ''' Removes consecutive co-linear vertices. Returns: self - """ + ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=False) return self diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 1e0352f..144371b 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,5 +1,4 @@ -from typing import Any, cast -from collections.abc import Sequence +from typing import Sequence, Any, cast import copy import functools @@ -20,8 +19,8 @@ class Polygon(Shape): A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an implicitly-closed boundary, and an offset. - Note that the setter for `Polygon.vertices` may creates a copy of the - passed vertex coordinates. + Note that the setter for `Polygon.vertices` may (but may not) create a copy of the + passed vertex coordinates. See `numpy.array(..., copy=False)` for details. A `normalized_form(...)` is available, but can be quite slow with lots of vertices. """ @@ -40,7 +39,8 @@ class Polygon(Shape): """ Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) - When setting, note that a copy of the provided vertices will be made, + When setting, note that a copy of the provided vertices may or may not be made, + following the rules from `numpy.array(.., copy=False)`. """ return self._vertices @@ -252,7 +252,7 @@ class Polygon(Shape): lx = 2 * (xmax - xctr) else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') - else: # noqa: PLR5501 + else: if xctr is not None: pass elif xmax is None: @@ -282,7 +282,7 @@ class Polygon(Shape): ly = 2 * (ymax - yctr) else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') - else: # noqa: PLR5501 + else: if yctr is not None: pass elif ymax is None: @@ -330,7 +330,10 @@ class Polygon(Shape): Returns: A Polygon object containing the requested octagon """ - s = (1 + numpy.sqrt(2)) if regular else 2 + if regular: + s = 1 + numpy.sqrt(2) + else: + s = 2 norm_oct = numpy.array([ [-1, -s], @@ -354,8 +357,8 @@ class Polygon(Shape): def to_polygons( self, - num_vertices: int | None = None, # unused # noqa: ARG002 - max_arclen: float | None = None, # unused # noqa: ARG002 + num_vertices: int | None = None, # unused + max_arclen: float | None = None, # unused ) -> list['Polygon']: return [copy.deepcopy(self)] @@ -414,22 +417,22 @@ class Polygon(Shape): return self def remove_duplicate_vertices(self) -> 'Polygon': - """ + ''' Removes all consecutive duplicate (repeated) vertices. Returns: self - """ + ''' self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True) return self def remove_colinear_vertices(self) -> 'Polygon': - """ + ''' Removes consecutive co-linear vertices. Returns: self - """ + ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=True) return self diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 0a7c86d..a1c4bcd 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -1,5 +1,4 @@ -from typing import TYPE_CHECKING, Any -from collections.abc import Callable +from typing import Callable, TYPE_CHECKING, Any from abc import ABCMeta, abstractmethod import numpy @@ -135,7 +134,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, vertex_lists = [] p_verts = polygon.vertices + polygon.offset - for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True): + for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0)): 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 @@ -165,7 +164,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, m = dv[1] / dv[0] - def get_grid_inds(xes: ArrayLike, m: float = m, v: NDArray = v) -> NDArray[numpy.float64]: + 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 @@ -266,12 +265,11 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, 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]) - # Flood left & rightwards by 2 cells - for kk in (keep_x, keep_y): - for ss in (1, 2): - kk[ss:] += kk[:-ss] - kk[:-ss] += kk[ss:] - kk[:] = kk > 0 + for k in (keep_x, keep_y): + for s in (1, 2): + k[s:] += k[:-s] + k[:-s] += k[s:] + k = k > 0 gx = grx[keep_x] gy = gry[keep_y] diff --git a/masque/shapes/text.py b/masque/shapes/text.py index e936796..d19d6c5 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -132,8 +132,8 @@ class Text(RotatableImpl, Shape): def to_polygons( self, - num_vertices: int | None = None, # unused # noqa: ARG002 - max_arclen: float | None = None, # unused # noqa: ARG002 + num_vertices: int | None = None, # unused + max_arclen: float | None = None, # unused ) -> list[Polygon]: all_polygons = [] total_advance = 0.0 @@ -191,11 +191,6 @@ class Text(RotatableImpl, Shape): return bounds - def __repr__(self) -> str: - rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' - mirrored = ' m{:d}' if self.mirrored else '' - return f'' - def get_char_as_polygons( font_path: str, @@ -221,7 +216,7 @@ def get_char_as_polygons( 'advance' distance (distance from the start of this glyph to the start of the next one) """ if len(char) != 1: - raise PatternError('get_char_as_polygons called with non-char') + raise Exception('get_char_as_polygons called with non-char') face = Face(font_path) face.set_char_size(resolution) @@ -230,8 +225,7 @@ def get_char_as_polygons( outline = slot.outline start = 0 - all_verts_list = [] - all_codes = [] + all_verts_list, all_codes = [], [] for end in outline.contours: points = outline.points[start:end + 1] points.append(points[0]) @@ -284,3 +278,8 @@ def get_char_as_polygons( polygons = path.to_polygons() return polygons, advance + + def __repr__(self) -> str: + rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' + mirrored = ' m{:d}' if self.mirrored else '' + return f'' diff --git a/masque/traits/__init__.py b/masque/traits/__init__.py index 7c7360c..a3e4361 100644 --- a/masque/traits/__init__.py +++ b/masque/traits/__init__.py @@ -3,32 +3,11 @@ Traits (mixins) and default implementations Traits and mixins should set `__slots__ = ()` to enable use of `__slots__` in subclasses. """ -from .positionable import ( - Positionable as Positionable, - PositionableImpl as PositionableImpl, - Bounded as Bounded, - ) -from .layerable import ( - Layerable as Layerable, - LayerableImpl as LayerableImpl, - ) -from .rotatable import ( - Rotatable as Rotatable, - RotatableImpl as RotatableImpl, - Pivotable as Pivotable, - PivotableImpl as PivotableImpl, - ) -from .repeatable import ( - Repeatable as Repeatable, - RepeatableImpl as RepeatableImpl, - ) -from .scalable import ( - Scalable as Scalable, - ScalableImpl as ScalableImpl, - ) -from .mirrorable import Mirrorable as Mirrorable -from .copyable import Copyable as Copyable -from .annotatable import ( - Annotatable as Annotatable, - AnnotatableImpl as AnnotatableImpl, - ) +from .positionable import Positionable, PositionableImpl, Bounded +from .layerable import Layerable, LayerableImpl +from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl +from .repeatable import Repeatable, RepeatableImpl +from .scalable import Scalable, ScalableImpl +from .mirrorable import Mirrorable +from .copyable import Copyable +from .annotatable import Annotatable, AnnotatableImpl diff --git a/masque/traits/copyable.py b/masque/traits/copyable.py index c652aff..91af84b 100644 --- a/masque/traits/copyable.py +++ b/masque/traits/copyable.py @@ -1,8 +1,9 @@ from typing import Self +from abc import ABCMeta import copy -class Copyable: +class Copyable(metaclass=ABCMeta): """ Trait class which adds .copy() and .deepcopy() """ diff --git a/masque/traits/layerable.py b/masque/traits/layerable.py index 639d97a..2ccd65e 100644 --- a/masque/traits/layerable.py +++ b/masque/traits/layerable.py @@ -63,7 +63,7 @@ class LayerableImpl(Layerable, metaclass=ABCMeta): return self._layer @layer.setter - def layer(self, val: layer_t) -> None: + def layer(self, val: layer_t): self._layer = val # diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index 6d4ec3c..c547780 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -44,7 +44,7 @@ class Mirrorable(metaclass=ABCMeta): # """ # __slots__ = () # -# _mirrored: NDArray[numpy.bool] +# _mirrored: numpy.ndarray # ndarray[bool] # """ Whether to mirror the instance across the x and/or y axes. """ # # # @@ -52,15 +52,15 @@ class Mirrorable(metaclass=ABCMeta): # # # # Mirrored property # @property -# def mirrored(self) -> NDArray[numpy.bool]: +# def mirrored(self) -> numpy.ndarray: # ndarray[bool] # """ Whether to mirror across the [x, y] axes, respectively """ # return self._mirrored # # @mirrored.setter -# def mirrored(self, val: Sequence[bool]) -> None: +# def mirrored(self, val: Sequence[bool]): # if is_scalar(val): # raise MasqueError('Mirrored must be a 2-element list of booleans') -# self._mirrored = numpy.array(val, dtype=bool) +# self._mirrored = numpy.array(val, dtype=bool, copy=True) # # # # # Methods diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index 66e6e7d..2b9c02e 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -81,11 +81,12 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): @offset.setter def offset(self, val: ArrayLike) -> None: - val = numpy.array(val, dtype=float) + if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64: + val = numpy.array(val, dtype=float) if val.size != 2: raise MasqueError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten() + self._offset = val.flatten() # type: ignore # # Methods diff --git a/masque/traits/repeatable.py b/masque/traits/repeatable.py index fbd765f..838e12b 100644 --- a/masque/traits/repeatable.py +++ b/masque/traits/repeatable.py @@ -34,7 +34,7 @@ class Repeatable(metaclass=ABCMeta): # @repetition.setter # @abstractmethod -# def repetition(self, repetition: 'Repetition | None') -> None: +# def repetition(self, repetition: 'Repetition | None'): # pass # @@ -75,7 +75,7 @@ class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta): return self._repetition @repetition.setter - def repetition(self, repetition: 'Repetition | None') -> None: + def repetition(self, repetition: 'Repetition | None'): from ..repetition import Repetition if repetition is not None and not isinstance(repetition, Repetition): raise MasqueError(f'{repetition} is not a valid Repetition object!') diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index f873ce4..850f70a 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -54,7 +54,7 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta): return self._rotation @rotation.setter - def rotation(self, val: float) -> None: + def rotation(self, val: float): if not numpy.size(val) == 1: raise MasqueError('Rotation must be a scalar') self._rotation = val % (2 * pi) @@ -112,7 +112,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta): """ `[x_offset, y_offset]` """ def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self: - pivot = numpy.asarray(pivot, dtype=float) + pivot = numpy.array(pivot, dtype=float) cast(Positionable, self).translate(-pivot) cast(Rotatable, self).rotate(rotation) self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004 diff --git a/masque/traits/scalable.py b/masque/traits/scalable.py index bdbe399..a3d21e2 100644 --- a/masque/traits/scalable.py +++ b/masque/traits/scalable.py @@ -48,7 +48,7 @@ class ScalableImpl(Scalable, metaclass=ABCMeta): return self._scale @scale.setter - def scale(self, val: float) -> None: + def scale(self, val: float): if not is_scalar(val): raise MasqueError('Scale must be a scalar') if not val > 0: diff --git a/masque/utils/__init__.py b/masque/utils/__init__.py index ffa9e85..571c406 100644 --- a/masque/utils/__init__.py +++ b/masque/utils/__init__.py @@ -1,40 +1,19 @@ """ Various helper functions, type definitions, etc. """ -from .types import ( - layer_t as layer_t, - annotations_t as annotations_t, - SupportsBool as SupportsBool, - ) -from .array import is_scalar as is_scalar -from .autoslots import AutoSlots as AutoSlots -from .deferreddict import DeferredDict as DeferredDict -from .decorators import oneshot as oneshot +from .types import layer_t, annotations_t, SupportsBool +from .array import is_scalar +from .autoslots import AutoSlots +from .deferreddict import DeferredDict +from .decorators import oneshot -from .bitwise import ( - get_bit as get_bit, - set_bit as set_bit, - ) +from .bitwise import get_bit, set_bit from .vertices import ( - remove_duplicate_vertices as remove_duplicate_vertices, - remove_colinear_vertices as remove_colinear_vertices, - poly_contains_points as poly_contains_points, - ) -from .transform import ( - rotation_matrix_2d as rotation_matrix_2d, - normalize_mirror as normalize_mirror, - rotate_offsets_around as rotate_offsets_around, - ) -from .comparisons import ( - annotation2key as annotation2key, - annotations_lt as annotations_lt, - annotations_eq as annotations_eq, - layer2key as layer2key, - ports_lt as ports_lt, - ports_eq as ports_eq, - rep2key as rep2key, + remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points ) +from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_around +from .comparisons import annotation2key, annotations_lt, annotations_eq, layer2key, ports_lt, ports_eq, rep2key -from . import ports2data as ports2data +from . import ports2data -from . import pack2d as pack2d +from . import pack2d diff --git a/masque/utils/autoslots.py b/masque/utils/autoslots.py index e82d3db..4b1d001 100644 --- a/masque/utils/autoslots.py +++ b/masque/utils/autoslots.py @@ -12,16 +12,16 @@ class AutoSlots(ABCMeta): classes, they can have empty `__slots__` and their attribute type annotations can be used to generate a full `__slots__` for the concrete class. """ - def __new__(cls, name, bases, dctn): # noqa: ANN001,ANN204 + def __new__(cls, name, bases, dctn): parents = set() for base in bases: parents |= set(base.mro()) - slots = tuple(dctn.get('__slots__', ())) + slots = tuple(dctn.get('__slots__', tuple())) for parent in parents: if not hasattr(parent, '__annotations__'): continue - slots += tuple(parent.__annotations__.keys()) + slots += tuple(getattr(parent, '__annotations__').keys()) dctn['__slots__'] = slots return super().__new__(cls, name, bases, dctn) diff --git a/masque/utils/comparisons.py b/masque/utils/comparisons.py index 63981c9..fe462b5 100644 --- a/masque/utils/comparisons.py +++ b/masque/utils/comparisons.py @@ -12,7 +12,7 @@ def annotation2key(aaa: int | float | str) -> tuple[bool, Any]: def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool: if aa is None: return bb is not None - elif bb is None: # noqa: RET505 + elif bb is None: return False if len(aa) != len(bb): @@ -29,7 +29,7 @@ def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool: if len(va) != len(vb): return len(va) < len(vb) - for aaa, bbb in zip(va, vb, strict=True): + for aaa, bbb in zip(va, vb): if aaa != bbb: return annotation2key(aaa) < annotation2key(bbb) return False @@ -38,7 +38,7 @@ def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool: def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool: if aa is None: return bb is None - elif bb is None: # noqa: RET505 + elif bb is None: return False if len(aa) != len(bb): @@ -55,7 +55,7 @@ def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool: if len(va) != len(vb): return False - for aaa, bbb in zip(va, vb, strict=True): + for aaa, bbb in zip(va, vb): if aaa != bbb: return False diff --git a/masque/utils/decorators.py b/masque/utils/decorators.py index eb81a1d..c212cdd 100644 --- a/masque/utils/decorators.py +++ b/masque/utils/decorators.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from typing import Callable from functools import wraps from ..error import OneShotError @@ -11,7 +11,7 @@ def oneshot(func: Callable) -> Callable: expired = False @wraps(func) - def wrapper(*args, **kwargs): # noqa: ANN202 + def wrapper(*args, **kwargs): nonlocal expired if expired: raise OneShotError(func.__name__) diff --git a/masque/utils/deferreddict.py b/masque/utils/deferreddict.py index aff3bcc..b6471e5 100644 --- a/masque/utils/deferreddict.py +++ b/masque/utils/deferreddict.py @@ -1,5 +1,4 @@ -from typing import TypeVar, Generic -from collections.abc import Callable +from typing import Callable, TypeVar, Generic from functools import lru_cache diff --git a/masque/utils/pack2d.py b/masque/utils/pack2d.py index ce6b006..ed6c4b5 100644 --- a/masque/utils/pack2d.py +++ b/masque/utils/pack2d.py @@ -1,7 +1,7 @@ """ 2D bin-packing """ -from collections.abc import Sequence, Mapping, Callable +from typing import Sequence, Callable, Mapping import numpy from numpy.typing import NDArray, ArrayLike @@ -38,8 +38,8 @@ def maxrects_bssf( Raises: MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. """ - regions = numpy.asarray(containers, dtype=float) - rect_sizes = numpy.asarray(rects, dtype=float) + regions = numpy.array(containers, copy=False, dtype=float) + rect_sizes = numpy.array(rects, copy=False, dtype=float) rect_locs = numpy.zeros_like(rect_sizes) rejected_inds = set() @@ -70,7 +70,8 @@ def maxrects_bssf( if allow_rejects: rejected_inds.add(rect_ind) continue - raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}') + else: + raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}') # Read out location loc = regions[rr, :2] @@ -139,8 +140,8 @@ def guillotine_bssf_sas( Raises: MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. """ - regions = numpy.asarray(containers, dtype=float) - rect_sizes = numpy.asarray(rects, dtype=float) + regions = numpy.array(containers, copy=False, dtype=float) + rect_sizes = numpy.array(rects, copy=False, dtype=float) rect_locs = numpy.zeros_like(rect_sizes) rejected_inds = set() @@ -160,7 +161,8 @@ def guillotine_bssf_sas( if allow_rejects: rejected_inds.add(rect_ind) continue - raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}') + else: + raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}') # Read out location loc = regions[rr, :2] @@ -227,7 +229,7 @@ def pack_patterns( MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. """ - half_spacing = numpy.asarray(spacing, dtype=float) / 2 + half_spacing = numpy.array(spacing, copy=False, dtype=float) / 2 bounds = [library[pp].get_bounds() for pp in patterns] sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds] @@ -236,7 +238,7 @@ def pack_patterns( locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects) pat = Pattern() - for pp, oo, loc in zip(patterns, offsets, locations, strict=True): + for pp, oo, loc in zip(patterns, offsets, locations): pat.ref(pp, offset=oo + loc) rejects = [patterns[ii] for ii in reject_inds] diff --git a/masque/utils/ports2data.py b/masque/utils/ports2data.py index b67fa0a..2843e0d 100644 --- a/masque/utils/ports2data.py +++ b/masque/utils/ports2data.py @@ -6,7 +6,7 @@ and retrieving it (`data_to_ports`). the port locations. This particular approach is just a sensible default; feel free to to write equivalent functions for your own format or alternate storage methods. """ -from collections.abc import Sequence, Mapping +from typing import Sequence, Mapping import logging from itertools import chain @@ -150,7 +150,7 @@ def data_to_ports_flat( Returns: The updated `pattern`. Port labels are not removed. """ - labels = list(chain.from_iterable(pattern.labels[layer] for layer in layers)) + labels = list(chain.from_iterable((pattern.labels[layer] for layer in layers))) if not labels: return pattern diff --git a/masque/utils/transform.py b/masque/utils/transform.py index 3071d99..c50a21c 100644 --- a/masque/utils/transform.py +++ b/masque/utils/transform.py @@ -1,7 +1,7 @@ """ Geometric transforms """ -from collections.abc import Sequence +from typing import Sequence from functools import lru_cache import numpy diff --git a/masque/utils/vertices.py b/masque/utils/vertices.py index 23fb601..0c5f03b 100644 --- a/masque/utils/vertices.py +++ b/masque/utils/vertices.py @@ -15,9 +15,9 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) -> (i.e. the last vertex will be removed if it is the same as the first) Returns: - `vertices` with no consecutive duplicates. This may be a view into the original array. + `vertices` with no consecutive duplicates. """ - vertices = numpy.asarray(vertices) + vertices = numpy.array(vertices) duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) if not closed_path: duplicates[0] = False @@ -35,7 +35,7 @@ def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> N closed path. If `False`, the path is assumed to be open. Default `True`. Returns: - `vertices` with colinear (superflous) vertices removed. May be a view into the original array. + `vertices` with colinear (superflous) vertices removed. """ vertices = remove_duplicate_vertices(vertices) @@ -73,8 +73,8 @@ def poly_contains_points( Returns: ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...] """ - points = numpy.asarray(points, dtype=float) - vertices = numpy.asarray(vertices, dtype=float) + points = numpy.array(points, copy=False) + vertices = numpy.array(vertices, copy=False) if points.size == 0: return numpy.zeros(0, dtype=numpy.int8) diff --git a/pyproject.toml b/pyproject.toml index bd430b8..b335f93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ requires-python = ">=3.11" dynamic = ["version"] dependencies = [ - "numpy>=1.26", + "numpy~=1.21", "klamath~=1.2", ] @@ -57,36 +57,3 @@ svg = ["svgwrite"] visualize = ["matplotlib"] text = ["matplotlib", "freetype-py"] - -[tool.ruff] -exclude = [ - ".git", - "dist", - ] -line-length = 145 -indent-width = 4 -lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -lint.select = [ - "NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG", - "C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT", - "ARG", "PL", "R", "TRY", - "G010", "G101", "G201", "G202", - "Q002", "Q003", "Q004", - ] -lint.ignore = [ - #"ANN001", # No annotation - "ANN002", # *args - "ANN003", # **kwargs - "ANN401", # Any - "ANN101", # self: Self - "SIM108", # single-line if / else assignment - "RET504", # x=y+z; return x - "PIE790", # unnecessary pass - "ISC003", # non-implicit string concatenation - "C408", # dict(x=y) instead of {'x': y} - "PLR09", # Too many xxx - "PLR2004", # magic number - "PLC0414", # import x as x - "TRY003", # Long exception message - ] -