Compare commits

..

No commits in common. "8d671ed7099dfadf9117cff0eba0cd850a664507" and "cda895a7d3c1b1254cbf224bb0825c5c8e5f2b4f" have entirely different histories.

52 changed files with 297 additions and 438 deletions

View File

@ -233,5 +233,3 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...)
* Tests tests tests * Tests tests tests
* check renderpather * check renderpather
* pather and renderpather examples * pather and renderpather examples
* context manager for retool
* allow a specific mismatch when connecting ports

View File

@ -99,7 +99,6 @@ def main():
print('\nAdded aref_test') print('\nAdded aref_test')
folder = Path('./layouts/') folder = Path('./layouts/')
folder.mkdir(exist_ok=True)
print(f'...writing files to {folder}...') print(f'...writing files to {folder}...')
gds1 = folder / 'rep.gds.gz' gds1 = folder / 'rep.gds.gz'

View File

@ -1,4 +1,4 @@
from collections.abc import Sequence from typing import Sequence
import numpy import numpy
from numpy import pi from numpy import pi

View File

@ -1,4 +1,4 @@
from collections.abc import Sequence, Mapping from typing import Sequence, Mapping
import numpy import numpy
from numpy import pi from numpy import pi

View File

@ -1,5 +1,4 @@
from typing import Any from typing import Sequence, Callable, Any
from collections.abc import Sequence, Callable
from pprint import pformat from pprint import pformat
import numpy import numpy

View File

@ -1,7 +1,7 @@
""" """
Manual wire routing tutorial: Pather and BasicTool Manual wire routing tutorial: Pather and BasicTool
""" """
from collections.abc import Callable from typing import Callable
from numpy import pi from numpy import pi
from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers
from masque.builder.tools import BasicTool, PathTool from masque.builder.tools import BasicTool, PathTool

View File

@ -2,7 +2,7 @@
Routines for creating normalized 2D lattices and common photonic crystal Routines for creating normalized 2D lattices and common photonic crystal
cavity designs. cavity designs.
""" """
from collection.abc import Sequence from typing import Sequence
import numpy import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
@ -233,8 +233,8 @@ def ln_shift_defect(
# Shift holes # Shift holes
# Expand shifts as necessary # Expand shifts as necessary
tmp_a = numpy.asarray(shifts_a) tmp_a = numpy.array(shifts_a)
tmp_r = numpy.asarray(shifts_r) tmp_r = numpy.array(shifts_r)
n_shifted = max(tmp_a.size, tmp_r.size) n_shifted = max(tmp_a.size, tmp_r.size)
shifts_a = numpy.ones(n_shifted) shifts_a = numpy.ones(n_shifted)

View File

@ -1,7 +1,7 @@
""" """
Manual wire routing tutorial: RenderPather an PathTool 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 import RenderPather, Library, Pattern, Port, layer_t, map_layers
from masque.builder.tools import PathTool from masque.builder.tools import PathTool
from masque.file.gdsii import writefile from masque.file.gdsii import writefile

View File

@ -28,65 +28,25 @@
can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally. can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally.
""" """
from .utils import ( from .utils import layer_t, annotations_t, SupportsBool
layer_t as layer_t, from .error import MasqueError, PatternError, LibraryError, BuildError
annotations_t as annotations_t, from .shapes import Shape, Polygon, Path, Circle, Arc, Ellipse
SupportsBool as SupportsBool, from .label import Label
) from .ref import Ref
from .error import ( from .pattern import Pattern, map_layers, map_targets, chain_elements
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 .library import ( from .library import (
ILibraryView as ILibraryView, ILibraryView, ILibrary,
ILibrary as ILibrary, LibraryView, Library, LazyLibrary,
LibraryView as LibraryView, AbstractView, TreeView, Tree,
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,
) )
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' __author__ = 'Jan Petykiewicz'
__version__ = '3.2' __version__ = '3.1'
version = __version__ # legacy version = __version__ # legacy

View File

@ -97,7 +97,7 @@ class Abstract(PortList):
Returns: Returns:
self self
""" """
pivot = numpy.asarray(pivot, dtype=float) pivot = numpy.array(pivot)
self.translate_ports(-pivot) self.translate_ports(-pivot)
self.rotate_ports(rotation) self.rotate_ports(rotation)
self.rotate_port_offsets(rotation) self.rotate_port_offsets(rotation)

View File

@ -1,10 +1,5 @@
from .builder import Builder as Builder from .builder import Builder
from .pather import Pather as Pather from .pather import Pather
from .renderpather import RenderPather as RenderPather from .renderpather import RenderPather
from .utils import ell as ell from .utils import ell
from .tools import ( from .tools import Tool, RenderStep, BasicTool, PathTool
Tool as Tool,
RenderStep as RenderStep,
BasicTool as BasicTool,
PathTool as PathTool,
)

View File

@ -1,8 +1,7 @@
""" """
Simplified Pattern assembly (`Builder`) Simplified Pattern assembly (`Builder`)
""" """
from typing import Self from typing import Self, Sequence, Mapping
from collections.abc import Sequence, Mapping
import copy import copy
import logging import logging
from functools import wraps from functools import wraps
@ -138,7 +137,7 @@ class Builder(PortList):
@classmethod @classmethod
def interface( def interface(
cls: type['Builder'], cls,
source: PortList | Mapping[str, Port] | str, source: PortList | Mapping[str, Port] | str,
*, *,
library: ILibrary | None = None, library: ILibrary | None = None,
@ -276,7 +275,7 @@ class Builder(PortList):
logger.error('Skipping plug() since device is dead') logger.error('Skipping plug() since device is dead')
return self 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 # We got a Tree; add it into self.library and grab an Abstract for it
other = self.library << other other = self.library << other
@ -348,7 +347,7 @@ class Builder(PortList):
logger.error('Skipping place() since device is dead') logger.error('Skipping place() since device is dead')
return self 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 # We got a Tree; add it into self.library and grab an Abstract for it
other = self.library << other other = self.library << other

View File

@ -1,8 +1,7 @@
""" """
Manual wire/waveguide routing (`Pather`) Manual wire/waveguide routing (`Pather`)
""" """
from typing import Self from typing import Self, Sequence, MutableMapping, Mapping
from collections.abc import Sequence, MutableMapping, Mapping
import copy import copy
import logging import logging
from pprint import pformat from pprint import pformat
@ -175,7 +174,7 @@ class Pather(Builder):
@classmethod @classmethod
def from_builder( def from_builder(
cls: type['Pather'], cls,
builder: Builder, builder: Builder,
*, *,
tools: Tool | MutableMapping[str | None, Tool] | None = None, tools: Tool | MutableMapping[str | None, Tool] | None = None,
@ -195,7 +194,7 @@ class Pather(Builder):
@classmethod @classmethod
def interface( def interface(
cls: type['Pather'], cls,
source: PortList | Mapping[str, Port] | str, source: PortList | Mapping[str, Port] | str,
*, *,
library: ILibrary | None = None, library: ILibrary | None = None,
@ -658,7 +657,7 @@ class Pather(Builder):
if not bound_types: if not bound_types:
raise BuildError('No bound type specified for mpath') 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}') raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
bound_type = tuple(bound_types)[0] 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 # Not a bus, so having a container just adds noise to the layout
port_name = tuple(portspec)[0] port_name = tuple(portspec)[0]
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names, **kwargs) return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names, **kwargs)
else:
bld = Pather.interface(source=ports, library=self.library, tools=self.tools) bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
for port_name, length in extensions.items(): for port_name, length in extensions.items():
bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs) bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs)
name = self.library.get_name(base_name) name = self.library.get_name(base_name)
self.library[name] = bld.pattern 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_'? 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()? # TODO def bus_join()?

View File

@ -1,8 +1,7 @@
""" """
Pather with batched (multi-step) rendering Pather with batched (multi-step) rendering
""" """
from typing import Self from typing import Self, Sequence, Mapping, MutableMapping
from collections.abc import Sequence, Mapping, MutableMapping
import copy import copy
import logging import logging
from collections import defaultdict from collections import defaultdict
@ -128,7 +127,7 @@ class RenderPather(PortList):
@classmethod @classmethod
def interface( def interface(
cls: type['RenderPather'], cls,
source: PortList | Mapping[str, Port] | str, source: PortList | Mapping[str, Port] | str,
*, *,
library: ILibrary | None = None, library: ILibrary | None = None,
@ -248,7 +247,7 @@ class RenderPather(PortList):
other_tgt = self.library[other.name] other_tgt = self.library[other.name]
# get rid of plugged ports # get rid of plugged ports
for kk in map_in: for kk in map_in.keys():
if kk in self.paths: if kk in self.paths:
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None)) 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: if not bound_types:
raise BuildError('No bound type specified for mpath') 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}') raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
bound_type = tuple(bound_types)[0] bound_type = tuple(bound_types)[0]

View File

@ -3,8 +3,7 @@ Tools are objects which dynamically generate simple single-use devices (e.g. wir
# TODO document all tools # TODO document all tools
""" """
from typing import Literal, Any from typing import Sequence, Literal, Callable, Any
from collections.abc import Sequence, Callable
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method? from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
from dataclasses import dataclass from dataclasses import dataclass
@ -223,8 +222,8 @@ class Tool:
self, self,
batch: Sequence[RenderStep], batch: Sequence[RenderStep],
*, *,
port_names: Sequence[str] = ('A', 'B'), # noqa: ARG002 (unused) port_names: Sequence[str] = ('A', 'B'),
**kwargs, # noqa: ARG002 (unused) **kwargs,
) -> ILibrary: ) -> ILibrary:
""" """
Render the provided `batch` of `RenderStep`s into geometry, returning a tree 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, in_ptype: str | None = None,
out_ptype: str | None = None, out_ptype: str | None = None,
**kwargs, # noqa: ARG002 (unused) **kwargs,
) -> tuple[Port, LData]: ) -> tuple[Port, LData]:
# TODO check all the math for L-shaped bends # TODO check all the math for L-shaped bends
if ccw is not None: if ccw is not None:
@ -405,7 +404,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
ipat, iport_theirs, _iport_ours = in_transition ipat, iport_theirs, _iport_ours = in_transition
pat.plug(ipat, {port_names[1]: iport_theirs}) pat.plug(ipat, {port_names[1]: iport_theirs})
if not numpy.isclose(straight_length, 0): if not numpy.isclose(straight_length, 0):
straight_pat = gen_straight(straight_length, **kwargs) straight_pat = gen_straight(straight_length)
if append: if append:
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True) pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
else: else:
@ -455,7 +454,7 @@ class PathTool(Tool, metaclass=ABCMeta):
in_ptype: str | None = None, in_ptype: str | None = None,
out_ptype: str | None = None, out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'), port_names: tuple[str, str] = ('A', 'B'),
**kwargs, # noqa: ARG002 (unused) **kwargs,
) -> Library: ) -> Library:
out_port, dxy = self.planL( out_port, dxy = self.planL(
ccw, ccw,
@ -486,9 +485,9 @@ class PathTool(Tool, metaclass=ABCMeta):
ccw: SupportsBool | None, ccw: SupportsBool | None,
length: float, length: float,
*, *,
in_ptype: str | None = None, # noqa: ARG002 (unused) in_ptype: str | None = None,
out_ptype: str | None = None, out_ptype: str | None = None,
**kwargs, # noqa: ARG002 (unused) **kwargs,
) -> tuple[Port, NDArray[numpy.float64]]: ) -> tuple[Port, NDArray[numpy.float64]]:
# TODO check all the math for L-shaped bends # TODO check all the math for L-shaped bends
@ -522,7 +521,7 @@ class PathTool(Tool, metaclass=ABCMeta):
batch: Sequence[RenderStep], batch: Sequence[RenderStep],
*, *,
port_names: Sequence[str] = ('A', 'B'), port_names: Sequence[str] = ('A', 'B'),
**kwargs, # noqa: ARG002 (unused) **kwargs,
) -> ILibrary: ) -> ILibrary:
path_vertices = [batch[0].start_port.offset] path_vertices = [batch[0].start_port.offset]

View File

@ -1,5 +1,4 @@
from typing import SupportsFloat, cast, TYPE_CHECKING from typing import Mapping, Sequence, SupportsFloat, cast, TYPE_CHECKING
from collections.abc import Mapping, Sequence
from pprint import pformat from pprint import pformat
import numpy import numpy
@ -113,7 +112,7 @@ def ell(
is_horizontal = numpy.isclose(rotations[0] % pi, 0) is_horizontal = numpy.isclose(rotations[0] % pi, 0)
if bound_type in ('ymin', 'ymax') and is_horizontal: 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!') 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!') 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) direction = rotations[0] + pi # direction we want to travel in (+pi relative to port)
@ -202,7 +201,7 @@ def ell(
if extension < 0: if extension < 0:
ext_floor = -numpy.floor(extension) ext_floor = -numpy.floor(extension)
raise BuildError(f'Position is too close by at least {ext_floor}. Total extensions would be\n\t' 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 return result

View File

@ -6,8 +6,7 @@ Notes:
* ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID * ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID
to unique values, so byte-for-byte reproducibility is not achievable for now to unique values, so byte-for-byte reproducibility is not achievable for now
""" """
from typing import Any, cast, TextIO, IO from typing import Any, Callable, Mapping, cast, TextIO, IO
from collections.abc import Mapping, Callable
import io import io
import logging import logging
import pathlib import pathlib
@ -16,7 +15,6 @@ import gzip
import numpy import numpy
import ezdxf import ezdxf
from ezdxf.enums import TextEntityAlignment from ezdxf.enums import TextEntityAlignment
from ezdxf.entities import LWPolyline, Polyline, Text, Insert
from .utils import is_gzipped, tmpfile from .utils import is_gzipped, tmpfile
from .. import Pattern, Ref, PatternError, Label from .. import Pattern, Ref, PatternError, Label
@ -40,7 +38,7 @@ def write(
top_name: str, top_name: str,
stream: TextIO, stream: TextIO,
*, *,
dxf_version: str = 'AC1024', dxf_version='AC1024',
) -> None: ) -> None:
""" """
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes 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 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 name = block.name
pat = Pattern() pat = Pattern()
for element in block: for element in block:
if isinstance(element, LWPolyline | Polyline): eltype = element.dxftype()
if isinstance(element, LWPolyline): if eltype in ('POLYLINE', 'LWPOLYLINE'):
points = numpy.asarray(element.get_points()) if eltype == 'LWPOLYLINE':
elif isinstance(element, Polyline): points = numpy.array(tuple(element.lwpoints))
points = numpy.asarray(element.points())[:, :2] else:
points = numpy.array(tuple(element.points()))
attr = element.dxfattribs() attr = element.dxfattribs()
layer = attr.get('layer', DEFAULT_LAYER) layer = attr.get('layer', DEFAULT_LAYER)
if points.shape[1] == 2: if points.shape[1] == 2:
raise PatternError('Invalid or unimplemented polygon?') raise PatternError('Invalid or unimplemented polygon?')
#shape = Polygon()
if points.shape[1] > 2: elif points.shape[1] > 2:
if (points[0, 2] != points[:, 2]).any(): if (points[0, 2] != points[:, 2]).any():
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)') 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!)') raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
width = points[0, 2] width = points[0, 2]
@ -239,9 +238,9 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
pat.shapes[layer].append(shape) pat.shapes[layer].append(shape)
elif isinstance(element, Text): elif eltype in ('TEXT',):
args = dict( 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), layer=element.dxfattribs().get('layer', DEFAULT_LAYER),
) )
string = element.dxfattribs().get('text', '') string = element.dxfattribs().get('text', '')
@ -252,7 +251,7 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
pat.label(string=string, **args) pat.label(string=string, **args)
# else: # else:
# pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????)) # pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????))
elif isinstance(element, Insert): elif eltype in ('INSERT',):
attr = element.dxfattribs() attr = element.dxfattribs()
xscale = attr.get('xscale', 1) xscale = attr.get('xscale', 1)
yscale = attr.get('yscale', 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)) mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0))
rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle 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( args = dict(
target=attr.get('name', None), target=attr.get('name', None),
@ -337,10 +336,10 @@ def _mrefs_to_drefs(
def _shapes_to_elements( def _shapes_to_elements(
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace, block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
shapes: dict[layer_t, list[Shape]], shapes: dict[layer_t, list[Shape]],
polygonize_paths: bool = False,
) -> None: ) -> None:
# Add `LWPolyline`s for each shape. # Add `LWPolyline`s for each shape.
# Could set do paths with width setting, but need to consider endcaps. # Could set do paths with width setting, but need to consider endcaps.
# TODO: can DXF do paths?
for layer, sseq in shapes.items(): for layer, sseq in shapes.items():
attribs = dict(layer=_mlayer2dxf(layer)) attribs = dict(layer=_mlayer2dxf(layer))
for shape in sseq: for shape in sseq:

View File

@ -19,8 +19,7 @@ Notes:
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility. * 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) * Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
""" """
from typing import IO, cast, Any from typing import Callable, Iterable, Mapping, IO, cast, Any
from collections.abc import Iterable, Mapping, Callable
import io import io
import mmap import mmap
import logging import logging
@ -357,7 +356,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
if isinstance(rep, Grid): if isinstance(rep, Grid):
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) 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 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], [0.0, 0.0],
rep.a_vector * rep.a_count, rep.a_vector * rep.a_count,
b_vector * b_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(): for key, vals in annotations.items():
try: try:
i = int(key) i = int(key)
except ValueError as err: except ValueError:
raise PatternError(f'Annotation key {key} is not convertable to an integer') from err raise PatternError(f'Annotation key {key} is not convertable to an integer')
if not (0 < i < 126): if not (0 < i < 126):
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') 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) path = pathlib.Path(filename)
stream: IO[bytes] stream: IO[bytes]
if is_gzipped(path): if is_gzipped(path):
if use_mmap: if mmap:
logger.info('Asked to mmap a gzipped file, reading into memory instead...') 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 stream = io.BytesIO(gz_stream.read()) # type: ignore
else: else:
gz_stream = gzip.open(path, mode='rb') # noqa: SIM115 gz_stream = gzip.open(path, mode='rb')
stream = io.BufferedReader(gz_stream) # type: ignore stream = io.BufferedReader(gz_stream) # type: ignore
else: # noqa: PLR5501 else:
if use_mmap: if mmap:
base_stream = path.open(mode='rb', buffering=0) # noqa: SIM115 base_stream = open(path, mode='rb', buffering=0)
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
else: else:
stream = path.open(mode='rb') # noqa: SIM115 stream = open(path, mode='rb')
return load_library(stream, full_load=full_load, postprocess=postprocess) return load_library(stream, full_load=full_load, postprocess=postprocess)

View File

@ -14,8 +14,7 @@ Note that OASIS references follow the same convention as `masque`,
Notes: Notes:
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01) * Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
""" """
from typing import Any, IO, cast from typing import Any, Callable, Iterable, IO, Mapping, cast, Sequence
from collections.abc import Sequence, Iterable, Mapping, Callable
import logging import logging
import pathlib import pathlib
import gzip import gzip
@ -298,7 +297,7 @@ def read(
cap_start = path_cap_map[element.get_extension_start()[0]] cap_start = path_cap_map[element.get_extension_start()[0]]
cap_end = path_cap_map[element.get_extension_end()[0]] cap_end = path_cap_map[element.get_extension_end()[0]]
if cap_start != cap_end: 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 cap = cap_start
path_args: dict[str, Any] = {} path_args: dict[str, Any] = {}
@ -453,8 +452,6 @@ def read(
for placement in cell.placements: for placement in cell.placements:
target, ref = _placement_to_ref(placement, lib) target, ref = _placement_to_ref(placement, lib)
if isinstance(target, int):
target = lib.cellnames[target].nstring.string
pat.refs[target].append(ref) pat.refs[target].append(ref)
mlib[cell_name] = pat mlib[cell_name] = pat
@ -695,9 +692,9 @@ def properties_to_annotations(
assert proprec.values is not None assert proprec.values is not None
for value in proprec.values: for value in proprec.values:
if isinstance(value, float | int): if isinstance(value, (float, int)):
values.append(value) values.append(value)
elif isinstance(value, NString | AString): elif isinstance(value, (NString, AString)):
values.append(value.string) values.append(value.string)
elif isinstance(value, PropStringReference): elif isinstance(value, PropStringReference):
values.append(propstrings[value.ref].string) # dereference values.append(propstrings[value.ref].string) # dereference

View File

@ -1,7 +1,7 @@
""" """
SVG file format readers and writers SVG file format readers and writers
""" """
from collections.abc import Mapping from typing import Mapping
import warnings import warnings
import numpy import numpy
@ -50,7 +50,7 @@ def writefile(
bounds = pattern.get_bounds(library=library) bounds = pattern.get_bounds(library=library)
if bounds is None: if bounds is None:
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]]) 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: else:
bounds_min, bounds_max = bounds bounds_min, bounds_max = bounds
@ -117,7 +117,7 @@ def writefile_inverted(
bounds = pattern.get_bounds(library=library) bounds = pattern.get_bounds(library=library)
if bounds is None: if bounds is None:
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]]) 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: else:
bounds_min, bounds_max = bounds bounds_min, bounds_max = bounds
@ -154,9 +154,9 @@ def poly2path(vertices: ArrayLike) -> str:
Returns: Returns:
SVG path-string. SVG path-string.
""" """
verts = numpy.asarray(vertices) verts = numpy.array(vertices, copy=False)
commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1]) # noqa: UP032 commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1])
for vertex in verts[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 ' commands += ' Z '
return commands return commands

View File

@ -1,8 +1,7 @@
""" """
Helper functions for file reading and writing Helper functions for file reading and writing
""" """
from typing import IO from typing import IO, Iterator, Mapping
from collections.abc import Iterator, Mapping
import re import re
import pathlib import pathlib
import logging import logging
@ -117,7 +116,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
for shapes in pat.shapes.values(): for shapes in pat.shapes.values():
remove_inds = [] remove_inds = []
for ii, shape in enumerate(shapes): for ii, shape in enumerate(shapes):
if not isinstance(shape, Polygon | Path): if not isinstance(shape, (Polygon, Path)):
continue continue
try: try:
shape.clean_vertices() shape.clean_vertices()
@ -129,7 +128,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
def is_gzipped(path: pathlib.Path) -> bool: def is_gzipped(path: pathlib.Path) -> bool:
with path.open('rb') as stream: with open(path, 'rb') as stream:
magic_bytes = stream.read(2) magic_bytes = stream.read(2)
return magic_bytes == b'\x1f\x8b' return magic_bytes == b'\x1f\x8b'

View File

@ -49,7 +49,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
) -> None: ) -> None:
self.string = string self.string = string
self.offset = numpy.array(offset, dtype=float) self.offset = numpy.array(offset, dtype=float, copy=True)
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
@ -94,7 +94,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
Returns: Returns:
self self
""" """
pivot = numpy.asarray(pivot, dtype=float) pivot = numpy.array(pivot, dtype=float)
self.translate(-pivot) self.translate(-pivot)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
self.translate(+pivot) self.translate(+pivot)

View File

@ -14,14 +14,17 @@ Classes include:
- `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked - `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked
library. Generated with `ILibraryView.abstract_view()`. library. Generated with `ILibraryView.abstract_view()`.
""" """
from typing import Self, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal from typing import Callable, Self, Type, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable from typing import Iterator, Mapping, MutableMapping, Sequence
import logging import logging
import base64
import struct
import re import re
import copy import copy
from pprint import pformat from pprint import pformat
from collections import defaultdict from collections import defaultdict
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from functools import lru_cache
import numpy import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
@ -173,7 +176,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
tops = tuple(self.keys()) tops = tuple(self.keys())
if skip is None: if skip is None:
skip = {None} skip = set([None])
if isinstance(tops, str): if isinstance(tops, str):
tops = (tops,) tops = (tops,)
@ -210,7 +213,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
if isinstance(tops, str): if isinstance(tops, str):
tops = (tops,) tops = (tops,)
keep = cast(set[str], self.referenced_patterns(tops) - {None}) keep = cast(set[str], self.referenced_patterns(tops) - set((None,)))
keep |= set(tops) keep |= set(tops)
filtered = {kk: vv for kk, vv in self.items() if kk in keep} 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): if isinstance(tops, str):
tops = (tops,) tops = (tops,)
flattened: dict[str, Pattern | None] = {} flattened: dict[str, 'Pattern | None'] = {}
def flatten_single(name: str) -> None: def flatten_single(name: str) -> None:
flattened[name] = None flattened[name] = None
@ -346,11 +349,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
else: else:
sanitized_name = name sanitized_name = name
ii = 0
suffixed_name = sanitized_name 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 == '': while suffixed_name in self or suffixed_name == '':
suffixed_name = sanitized_name + b64suffix(ii) suffixed_name = sanitized_name + b64suffix(ii)
ii += 1 ii += 1
@ -460,7 +460,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
if transform is None or transform is True: if transform is None or transform is True:
transform = numpy.zeros(4) transform = numpy.zeros(4)
elif transform is not False: elif transform is not False:
transform = numpy.asarray(transform, dtype=float) transform = numpy.array(transform, dtype=float, copy=False)
original_pattern = pattern original_pattern = pattern
@ -665,7 +665,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
duplicates = set(self.keys()) & set(other.keys()) duplicates = set(self.keys()) & set(other.keys())
if not duplicates: if not duplicates:
for key in other: for key in other.keys():
self._merge(key, other, key) self._merge(key, other, key)
return {} return {}
@ -735,7 +735,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
def dedup( def dedup(
self, self,
norm_value: int = int(1e6), norm_value: int = int(1e6),
exclude_types: tuple[type] = (Polygon,), exclude_types: tuple[Type] = (Polygon,),
label2name: Callable[[tuple], str] | None = None, label2name: Callable[[tuple], str] | None = None,
threshold: int = 2, threshold: int = 2,
) -> Self: ) -> Self:
@ -773,7 +773,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
exclude_types = () exclude_types = ()
if label2name is None: if label2name is None:
def label2name(label: tuple) -> str: # noqa: ARG001 def label2name(label):
return self.get_name(SINGLE_USE_PREFIX + 'shape') return self.get_name(SINGLE_USE_PREFIX + 'shape')
shape_counts: MutableMapping[tuple, int] = defaultdict(int) shape_counts: MutableMapping[tuple, int] = defaultdict(int)
@ -863,7 +863,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
from .pattern import Pattern from .pattern import Pattern
if name_func is None: 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') return self.get_name(SINGLE_USE_PREFIX + 'rep')
for pat in tuple(self.values()): for pat in tuple(self.values()):
@ -912,7 +912,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
if isinstance(tops, str): if isinstance(tops, str):
tops = (tops,) tops = (tops,)
keep = cast(set[str], self.referenced_patterns(tops) - {None}) keep = cast(set[str], self.referenced_patterns(tops) - set((None,)))
keep |= set(tops) keep |= set(tops)
new = type(self)() new = type(self)()
@ -934,7 +934,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
A set containing the names of all deleted patterns A set containing the names of all deleted patterns
""" """
trimmed = set() 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: for name in empty:
del self[name] del self[name]
@ -1038,7 +1038,10 @@ class Library(ILibrary):
if key in self.mapping: if key in self.mapping:
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!') 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 self.mapping[key] = value
def __delitem__(self, key: str) -> None: def __delitem__(self, key: str) -> None:
@ -1051,7 +1054,7 @@ class Library(ILibrary):
return f'<Library ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>' return f'<Library ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>'
@classmethod @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 Create a new Library and immediately add a pattern
@ -1229,18 +1232,8 @@ class AbstractView(Mapping[str, Abstract]):
return self.library.__len__() return self.library.__len__()
@lru_cache(maxsize=8_000)
def b64suffix(ii: int) -> str: def b64suffix(ii: int) -> str:
""" """Turn an integer into a base64-equivalent suffix."""
Turn an integer into a base64-equivalent suffix. suffix = base64.b64encode(struct.pack('>Q', ii), altchars=b'$?').decode('ASCII')
return '$' + suffix[:-1].lstrip('A')
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)

View File

@ -2,8 +2,7 @@
Object representing a one multi-layer lithographic layout. Object representing a one multi-layer lithographic layout.
A single level of hierarchical references is included. A single level of hierarchical references is included.
""" """
from typing import cast, Self, Any, TypeVar from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
from collections.abc import Sequence, Mapping, MutableMapping, Iterable, Callable
import copy import copy
import logging import logging
import functools import functools
@ -296,7 +295,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
if not annotations_eq(self.annotations, other.annotations): if not annotations_eq(self.annotations, other.annotations):
return False return False
if not ports_eq(self.ports, other.ports): # noqa: SIM103 if not ports_eq(self.ports, other.ports):
return False return False
return True return True
@ -313,10 +312,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self self
""" """
if sort_elements: if sort_elements:
def maybe_sort(xx): # noqa:ANN001,ANN202 def maybe_sort(xx):
return sorted(xx) return sorted(xx)
else: else:
def maybe_sort(xx): # noqa:ANN001,ANN202 def maybe_sort(xx):
return xx return xx
self.refs = defaultdict(list, sorted( self.refs = defaultdict(list, sorted(
@ -472,10 +471,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self.polygonize() self.polygonize()
for layer in self.shapes: 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) ss.manhattanize(grid_x, grid_y)
for ss in self.shapes[layer] for ss in self.shapes[layer]
)) )))
return self return self
def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]: 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(): if (cbounds[1] < cbounds[0]).any():
return None return None
return cbounds else:
return cbounds
def get_bounds_nonempty( def get_bounds_nonempty(
self, self,
@ -616,7 +616,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
`[[x_min, y_min], [x_max, y_max]]` `[[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 assert bounds is not None
return bounds return bounds
@ -690,7 +690,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self self
""" """
pivot = numpy.asarray(pivot, dtype=float) pivot = numpy.array(pivot)
self.translate_elements(-pivot) self.translate_elements(-pivot)
self.rotate_elements(rotation) self.rotate_elements(rotation)
self.rotate_element_centers(rotation) self.rotate_element_centers(rotation)
@ -953,7 +953,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
Returns: Returns:
self self
""" """
flattened: dict[str | None, Pattern | None] = {} flattened: dict[str | None, 'Pattern | None'] = {}
def flatten_single(name: str | None) -> None: def flatten_single(name: str | None) -> None:
if name is None: if name is None:
@ -1015,15 +1015,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
try: try:
from matplotlib import pyplot # type: ignore from matplotlib import pyplot # type: ignore
import matplotlib.collections # type: ignore import matplotlib.collections # type: ignore
except ImportError: except ImportError as err:
logger.exception('Pattern.visualize() depends on matplotlib!\n' logger.error('Pattern.visualize() depends on matplotlib!')
+ 'Make sure to install masque with the [visualize] option to pull in the needed dependencies.') logger.error('Make sure to install masque with the [visualize] option to pull in the needed dependencies.')
raise raise err
if self.has_refs() and library is None: if self.has_refs() and library is None:
raise PatternError('Must provide a library when visualizing a pattern with refs') 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: if not overdraw:
figure = pyplot.figure() figure = pyplot.figure()
@ -1324,7 +1324,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
@classmethod @classmethod
def interface( def interface(
cls: type['Pattern'], cls,
source: PortList | Mapping[str, Port], source: PortList | Mapping[str, Port],
*, *,
in_prefix: str = 'in_', in_prefix: str = 'in_',

View File

@ -1,5 +1,4 @@
from typing import overload, Self, NoReturn, Any from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn, Any
from collections.abc import Iterable, KeysView, ValuesView, Mapping
import warnings import warnings
import traceback import traceback
import logging import logging
@ -93,7 +92,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
def copy(self) -> Self: def copy(self) -> Self:
return self.deepcopy() return self.deepcopy()
def get_bounds(self) -> NDArray[numpy.float64]: def get_bounds(self):
return numpy.vstack((self.offset, self.offset)) return numpy.vstack((self.offset, self.offset))
def set_ptype(self, ptype: str) -> Self: def set_ptype(self, ptype: str) -> Self:
@ -181,7 +180,7 @@ class PortList(metaclass=ABCMeta):
""" """
if isinstance(key, str): if isinstance(key, str):
return self.ports[key] return self.ports[key]
else: # noqa: RET505 else:
return {k: self.ports[k] for k in key} return {k: self.ports[k] for k in key}
def __contains__(self, key: str) -> NoReturn: def __contains__(self, key: str) -> NoReturn:
@ -239,7 +238,7 @@ class PortList(metaclass=ABCMeta):
if duplicates: if duplicates:
raise PortError(f'Unrenamed ports would be overwritten: {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: if None in renamed:
del renamed[None] del renamed[None]
@ -294,14 +293,14 @@ class PortList(metaclass=ABCMeta):
Raises: Raises:
`PortError` if the ports are not properly aligned. `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] a_ports = [self.ports[pp] for pp in a_names]
b_ports = [self.ports[pp] for pp in b_names] b_ports = [self.ports[pp] for pp in b_names]
a_types = [pp.ptype for pp in a_ports] a_types = [pp.ptype for pp in a_ports]
b_types = [pp.ptype for pp in b_ports] b_types = [pp.ptype for pp in b_ports]
type_conflicts = numpy.array([at != bt and 'unk' not in (at, bt) type_conflicts = numpy.array([at != bt and at != 'unk' and bt != 'unk'
for at, bt in zip(a_types, b_types, strict=True)]) for at, bt in zip(a_types, b_types)])
if type_conflicts.any(): if type_conflicts.any():
msg = 'Ports have conflicting types:\n' msg = 'Ports have conflicting types:\n'
@ -502,8 +501,8 @@ class PortList(metaclass=ABCMeta):
o_offsets[:, 1] *= -1 o_offsets[:, 1] *= -1
o_rotations *= -1 o_rotations *= -1
type_conflicts = numpy.array([st != ot and 'unk' not in (st, ot) type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
for st, ot in zip(s_types, o_types, strict=True)]) for st, ot in zip(s_types, o_types)])
if type_conflicts.any(): if type_conflicts.any():
msg = 'Ports have conflicting types:\n' msg = 'Ports have conflicting types:\n'
for nn, (k, v) in enumerate(map_in.items()): for nn, (k, v) in enumerate(map_in.items()):

View File

@ -2,8 +2,7 @@
Ref provides basic support for nesting Pattern objects within each other. Ref provides basic support for nesting Pattern objects within each other.
It carries offset, rotation, mirroring, and scaling data for each individual instance. It carries offset, rotation, mirroring, and scaling data for each individual instance.
""" """
from typing import TYPE_CHECKING, Self, Any from typing import Mapping, TYPE_CHECKING, Self, Any
from collections.abc import Mapping
import copy import copy
import functools import functools

View File

@ -2,7 +2,7 @@
Repetitions provide support for efficiently representing multiple identical Repetitions provide support for efficiently representing multiple identical
instances of an object . instances of an object .
""" """
from typing import Any, Self, TypeVar, cast from typing import Any, Type, Self, TypeVar, cast
import copy import copy
import functools import functools
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
@ -101,7 +101,8 @@ class Grid(Repetition):
if b_vector is None: if b_vector is None:
if b_count > 1: if b_count > 1:
raise PatternError('Repetition has b_count > 1 but no b_vector') 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: if a_count < 1:
raise PatternError(f'Repetition has too-small a_count: {a_count}') raise PatternError(f'Repetition has too-small a_count: {a_count}')
@ -115,7 +116,7 @@ class Grid(Repetition):
@classmethod @classmethod
def aligned( def aligned(
cls: type[GG], cls: Type[GG],
x: float, x: float,
y: float, y: float,
x_count: int, x_count: int,
@ -156,11 +157,12 @@ class Grid(Repetition):
@a_vector.setter @a_vector.setter
def a_vector(self, val: ArrayLike) -> None: 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: if val.size != 2:
raise PatternError('a_vector must be convertible to size-2 ndarray') 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 # b_vector property
@property @property
@ -169,7 +171,8 @@ class Grid(Repetition):
@b_vector.setter @b_vector.setter
def b_vector(self, val: ArrayLike) -> None: 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: if val.size != 2:
raise PatternError('b_vector must be convertible to size-2 ndarray') raise PatternError('b_vector must be convertible to size-2 ndarray')
@ -287,7 +290,7 @@ class Grid(Repetition):
return True return True
if self.b_vector is None or other.b_vector is None: if self.b_vector is None or other.b_vector is None:
return False 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 False
return True return True
@ -332,9 +335,9 @@ class Arbitrary(Repetition):
@displacements.setter @displacements.setter
def displacements(self, val: ArrayLike) -> None: def displacements(self, val: ArrayLike) -> None:
vala = numpy.array(val, dtype=float) vala: NDArray[numpy.float64] = numpy.array(val, dtype=float)
order = numpy.lexsort(vala.T[::-1]) # sortrows vala = numpy.sort(vala.view([('', vala.dtype)] * vala.shape[1]), 0).view(vala.dtype) # sort rows
self._displacements = vala[order] self._displacements = vala
def __init__( def __init__(
self, self,

View File

@ -3,15 +3,11 @@ Shapes for use with the Pattern class, as well as the Shape abstract class from
which they are derived. which they are derived.
""" """
from .shape import ( from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
Shape as Shape,
normalized_shape_tuple as normalized_shape_tuple,
DEFAULT_POLY_NUM_VERTICES as DEFAULT_POLY_NUM_VERTICES,
)
from .polygon import Polygon as Polygon from .polygon import Polygon
from .circle import Circle as Circle from .circle import Circle
from .ellipse import Ellipse as Ellipse from .ellipse import Ellipse
from .arc import Arc as Arc from .arc import Arc
from .text import Text as Text from .text import Text
from .path import Path as Path from .path import Path

View File

@ -286,7 +286,7 @@ class Arc(Shape):
return thetas return thetas
wh = self.width / 2.0 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 thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
else: else:
thetas_inner = get_thetas(inner=True) thetas_inner = get_thetas(inner=True)
@ -308,7 +308,7 @@ class Arc(Shape):
return [poly] return [poly]
def get_bounds_single(self) -> NDArray[numpy.float64]: def get_bounds_single(self) -> NDArray[numpy.float64]:
""" '''
Equation for rotated ellipse is Equation for rotated ellipse is
`x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)` `x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)`
`y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)` `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. 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. If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
""" '''
a_ranges = self._angles_to_parameters() a_ranges = self._angles_to_parameters()
mins = [] mins = []
maxs = [] 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 wh = sgn * self.width / 2
rx = self.radius_x + wh rx = self.radius_x + wh
ry = self.radius_y + wh ry = self.radius_y + wh
@ -424,18 +424,18 @@ class Arc(Shape):
)) ))
def get_cap_edges(self) -> NDArray[numpy.float64]: def get_cap_edges(self) -> NDArray[numpy.float64]:
""" '''
Returns: Returns:
``` ```
[[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which [[[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. [[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
``` ```
""" '''
a_ranges = self._angles_to_parameters() a_ranges = self._angles_to_parameters()
mins = [] mins = []
maxs = [] 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 wh = sgn * self.width / 2
rx = self.radius_x + wh rx = self.radius_x + wh
ry = self.radius_y + wh ry = self.radius_y + wh
@ -454,11 +454,11 @@ class Arc(Shape):
return numpy.array([mins, maxs]) + self.offset return numpy.array([mins, maxs]) + self.offset
def _angles_to_parameters(self) -> NDArray[numpy.float64]: def _angles_to_parameters(self) -> NDArray[numpy.float64]:
""" '''
Returns: Returns:
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]` `[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
""" '''
a = [] a = []
for sgn in (-1, +1): for sgn in (-1, +1):
wh = sgn * self.width / 2 wh = sgn * self.width / 2
@ -472,7 +472,7 @@ class Arc(Shape):
a1 += sign * 2 * pi a1 += sign * 2 * pi
a.append((a0, a1)) a.append((a0, a1))
return numpy.array(a, dtype=float) return numpy.array(a)
def __repr__(self) -> str: def __repr__(self) -> str:
angles = f'{numpy.rad2deg(self.angles)}' angles = f'{numpy.rad2deg(self.angles)}'

View File

@ -119,10 +119,10 @@ class Circle(Shape):
return numpy.vstack((self.offset - self.radius, return numpy.vstack((self.offset - self.radius,
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 return self
def mirror(self, axis: int = 0) -> 'Circle': # noqa: ARG002 (axis unused) def mirror(self, axis: int = 0) -> 'Circle':
self.offset *= -1 self.offset *= -1
return self return self
@ -130,7 +130,7 @@ class Circle(Shape):
self.radius *= c self.radius *= c
return self return self
def normalized_form(self, norm_value: float) -> normalized_shape_tuple: def normalized_form(self, norm_value) -> normalized_shape_tuple:
rotation = 0.0 rotation = 0.0
magnitude = self.radius / norm_value magnitude = self.radius / norm_value
return ((type(self),), return ((type(self),),

View File

@ -1,5 +1,4 @@
from typing import Any, cast from typing import Sequence, Any, cast
from collections.abc import Sequence
import copy import copy
import functools import functools
from enum import Enum 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, A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
and an offset. 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. 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,) custom_caps = (PathCap.SquareCustom,)
if self.cap in custom_caps: if self.cap in custom_caps:
if vals is None: 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) self._cap_extensions = numpy.array(vals, dtype=float)
else: else:
if vals is not None: 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 self._cap_extensions = vals
# vertices property # vertices property
@ -117,7 +117,8 @@ class Path(Shape):
""" """
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]` 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 return self._vertices
@ -429,22 +430,22 @@ class Path(Shape):
return self return self
def remove_duplicate_vertices(self) -> 'Path': def remove_duplicate_vertices(self) -> 'Path':
""" '''
Removes all consecutive duplicate (repeated) vertices. Removes all consecutive duplicate (repeated) vertices.
Returns: Returns:
self self
""" '''
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False) self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False)
return self return self
def remove_colinear_vertices(self) -> 'Path': def remove_colinear_vertices(self) -> 'Path':
""" '''
Removes consecutive co-linear vertices. Removes consecutive co-linear vertices.
Returns: Returns:
self self
""" '''
self.vertices = remove_colinear_vertices(self.vertices, closed_path=False) self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
return self return self

View File

@ -1,5 +1,4 @@
from typing import Any, cast from typing import Sequence, Any, cast
from collections.abc import Sequence
import copy import copy
import functools import functools
@ -20,8 +19,8 @@ class Polygon(Shape):
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
implicitly-closed boundary, and an offset. implicitly-closed boundary, and an offset.
Note that the setter for `Polygon.vertices` may creates a copy of the Note that the setter for `Polygon.vertices` may (but may not) create a copy of the
passed vertex coordinates. passed vertex coordinates. See `numpy.array(..., copy=False)` for details.
A `normalized_form(...)` is available, but can be quite slow with lots of vertices. A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
""" """
@ -40,7 +39,8 @@ class Polygon(Shape):
""" """
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) 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 return self._vertices
@ -252,7 +252,7 @@ class Polygon(Shape):
lx = 2 * (xmax - xctr) lx = 2 * (xmax - xctr)
else: else:
raise PatternError('Two of xmin, xctr, xmax, lx must be None!') raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
else: # noqa: PLR5501 else:
if xctr is not None: if xctr is not None:
pass pass
elif xmax is None: elif xmax is None:
@ -282,7 +282,7 @@ class Polygon(Shape):
ly = 2 * (ymax - yctr) ly = 2 * (ymax - yctr)
else: else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!') raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
else: # noqa: PLR5501 else:
if yctr is not None: if yctr is not None:
pass pass
elif ymax is None: elif ymax is None:
@ -330,7 +330,10 @@ class Polygon(Shape):
Returns: Returns:
A Polygon object containing the requested octagon 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([ norm_oct = numpy.array([
[-1, -s], [-1, -s],
@ -354,8 +357,8 @@ class Polygon(Shape):
def to_polygons( def to_polygons(
self, self,
num_vertices: int | None = None, # unused # noqa: ARG002 num_vertices: int | None = None, # unused
max_arclen: float | None = None, # unused # noqa: ARG002 max_arclen: float | None = None, # unused
) -> list['Polygon']: ) -> list['Polygon']:
return [copy.deepcopy(self)] return [copy.deepcopy(self)]
@ -414,22 +417,22 @@ class Polygon(Shape):
return self return self
def remove_duplicate_vertices(self) -> 'Polygon': def remove_duplicate_vertices(self) -> 'Polygon':
""" '''
Removes all consecutive duplicate (repeated) vertices. Removes all consecutive duplicate (repeated) vertices.
Returns: Returns:
self self
""" '''
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True) self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True)
return self return self
def remove_colinear_vertices(self) -> 'Polygon': def remove_colinear_vertices(self) -> 'Polygon':
""" '''
Removes consecutive co-linear vertices. Removes consecutive co-linear vertices.
Returns: Returns:
self self
""" '''
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True) self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
return self return self

View File

@ -1,5 +1,4 @@
from typing import TYPE_CHECKING, Any from typing import Callable, TYPE_CHECKING, Any
from collections.abc import Callable
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy import numpy
@ -135,7 +134,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
vertex_lists = [] vertex_lists = []
p_verts = polygon.vertices + polygon.offset 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 dv = v_next - v
# Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape # Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape
@ -165,7 +164,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
m = dv[1] / dv[0] 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] ys = m * (xes - v[0]) + v[1]
# (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid # (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid
@ -266,12 +265,11 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
mins, maxs = bounds mins, maxs = bounds
keep_x = numpy.logical_and(grx > mins[0], grx < maxs[0]) keep_x = numpy.logical_and(grx > mins[0], grx < maxs[0])
keep_y = numpy.logical_and(gry > mins[1], gry < maxs[1]) keep_y = numpy.logical_and(gry > mins[1], gry < maxs[1])
# Flood left & rightwards by 2 cells for k in (keep_x, keep_y):
for kk in (keep_x, keep_y): for s in (1, 2):
for ss in (1, 2): k[s:] += k[:-s]
kk[ss:] += kk[:-ss] k[:-s] += k[s:]
kk[:-ss] += kk[ss:] k = k > 0
kk[:] = kk > 0
gx = grx[keep_x] gx = grx[keep_x]
gy = gry[keep_y] gy = gry[keep_y]

View File

@ -132,8 +132,8 @@ class Text(RotatableImpl, Shape):
def to_polygons( def to_polygons(
self, self,
num_vertices: int | None = None, # unused # noqa: ARG002 num_vertices: int | None = None, # unused
max_arclen: float | None = None, # unused # noqa: ARG002 max_arclen: float | None = None, # unused
) -> list[Polygon]: ) -> list[Polygon]:
all_polygons = [] all_polygons = []
total_advance = 0.0 total_advance = 0.0
@ -191,11 +191,6 @@ class Text(RotatableImpl, Shape):
return bounds return bounds
def __repr__(self) -> str:
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
mirrored = ' m{:d}' if self.mirrored else ''
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
def get_char_as_polygons( def get_char_as_polygons(
font_path: str, 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) 'advance' distance (distance from the start of this glyph to the start of the next one)
""" """
if len(char) != 1: 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 = Face(font_path)
face.set_char_size(resolution) face.set_char_size(resolution)
@ -230,8 +225,7 @@ def get_char_as_polygons(
outline = slot.outline outline = slot.outline
start = 0 start = 0
all_verts_list = [] all_verts_list, all_codes = [], []
all_codes = []
for end in outline.contours: for end in outline.contours:
points = outline.points[start:end + 1] points = outline.points[start:end + 1]
points.append(points[0]) points.append(points[0])
@ -284,3 +278,8 @@ def get_char_as_polygons(
polygons = path.to_polygons() polygons = path.to_polygons()
return polygons, advance return polygons, advance
def __repr__(self) -> str:
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
mirrored = ' m{:d}' if self.mirrored else ''
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'

View File

@ -3,32 +3,11 @@ Traits (mixins) and default implementations
Traits and mixins should set `__slots__ = ()` to enable use of `__slots__` in subclasses. Traits and mixins should set `__slots__ = ()` to enable use of `__slots__` in subclasses.
""" """
from .positionable import ( from .positionable import Positionable, PositionableImpl, Bounded
Positionable as Positionable, from .layerable import Layerable, LayerableImpl
PositionableImpl as PositionableImpl, from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
Bounded as Bounded, from .repeatable import Repeatable, RepeatableImpl
) from .scalable import Scalable, ScalableImpl
from .layerable import ( from .mirrorable import Mirrorable
Layerable as Layerable, from .copyable import Copyable
LayerableImpl as LayerableImpl, from .annotatable import Annotatable, AnnotatableImpl
)
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,
)

View File

@ -1,8 +1,9 @@
from typing import Self from typing import Self
from abc import ABCMeta
import copy import copy
class Copyable: class Copyable(metaclass=ABCMeta):
""" """
Trait class which adds .copy() and .deepcopy() Trait class which adds .copy() and .deepcopy()
""" """

View File

@ -63,7 +63,7 @@ class LayerableImpl(Layerable, metaclass=ABCMeta):
return self._layer return self._layer
@layer.setter @layer.setter
def layer(self, val: layer_t) -> None: def layer(self, val: layer_t):
self._layer = val self._layer = val
# #

View File

@ -44,7 +44,7 @@ class Mirrorable(metaclass=ABCMeta):
# """ # """
# __slots__ = () # __slots__ = ()
# #
# _mirrored: NDArray[numpy.bool] # _mirrored: numpy.ndarray # ndarray[bool]
# """ Whether to mirror the instance across the x and/or y axes. """ # """ Whether to mirror the instance across the x and/or y axes. """
# #
# # # #
@ -52,15 +52,15 @@ class Mirrorable(metaclass=ABCMeta):
# # # #
# # Mirrored property # # Mirrored property
# @property # @property
# def mirrored(self) -> NDArray[numpy.bool]: # def mirrored(self) -> numpy.ndarray: # ndarray[bool]
# """ Whether to mirror across the [x, y] axes, respectively """ # """ Whether to mirror across the [x, y] axes, respectively """
# return self._mirrored # return self._mirrored
# #
# @mirrored.setter # @mirrored.setter
# def mirrored(self, val: Sequence[bool]) -> None: # def mirrored(self, val: Sequence[bool]):
# if is_scalar(val): # if is_scalar(val):
# raise MasqueError('Mirrored must be a 2-element list of booleans') # 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 # # Methods

View File

@ -81,11 +81,12 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
@offset.setter @offset.setter
def offset(self, val: ArrayLike) -> None: 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: if val.size != 2:
raise MasqueError('Offset must be convertible to size-2 ndarray') raise MasqueError('Offset must be convertible to size-2 ndarray')
self._offset = val.flatten() self._offset = val.flatten() # type: ignore
# #
# Methods # Methods

View File

@ -34,7 +34,7 @@ class Repeatable(metaclass=ABCMeta):
# @repetition.setter # @repetition.setter
# @abstractmethod # @abstractmethod
# def repetition(self, repetition: 'Repetition | None') -> None: # def repetition(self, repetition: 'Repetition | None'):
# pass # pass
# #
@ -75,7 +75,7 @@ class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
return self._repetition return self._repetition
@repetition.setter @repetition.setter
def repetition(self, repetition: 'Repetition | None') -> None: def repetition(self, repetition: 'Repetition | None'):
from ..repetition import Repetition from ..repetition import Repetition
if repetition is not None and not isinstance(repetition, Repetition): if repetition is not None and not isinstance(repetition, Repetition):
raise MasqueError(f'{repetition} is not a valid Repetition object!') raise MasqueError(f'{repetition} is not a valid Repetition object!')

View File

@ -54,7 +54,7 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
return self._rotation return self._rotation
@rotation.setter @rotation.setter
def rotation(self, val: float) -> None: def rotation(self, val: float):
if not numpy.size(val) == 1: if not numpy.size(val) == 1:
raise MasqueError('Rotation must be a scalar') raise MasqueError('Rotation must be a scalar')
self._rotation = val % (2 * pi) self._rotation = val % (2 * pi)
@ -112,7 +112,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
""" `[x_offset, y_offset]` """ """ `[x_offset, y_offset]` """
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self: 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(Positionable, self).translate(-pivot)
cast(Rotatable, self).rotate(rotation) cast(Rotatable, self).rotate(rotation)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004 self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004

View File

@ -48,7 +48,7 @@ class ScalableImpl(Scalable, metaclass=ABCMeta):
return self._scale return self._scale
@scale.setter @scale.setter
def scale(self, val: float) -> None: def scale(self, val: float):
if not is_scalar(val): if not is_scalar(val):
raise MasqueError('Scale must be a scalar') raise MasqueError('Scale must be a scalar')
if not val > 0: if not val > 0:

View File

@ -1,40 +1,19 @@
""" """
Various helper functions, type definitions, etc. Various helper functions, type definitions, etc.
""" """
from .types import ( from .types import layer_t, annotations_t, SupportsBool
layer_t as layer_t, from .array import is_scalar
annotations_t as annotations_t, from .autoslots import AutoSlots
SupportsBool as SupportsBool, from .deferreddict import DeferredDict
) from .decorators import oneshot
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 .bitwise import ( from .bitwise import get_bit, set_bit
get_bit as get_bit,
set_bit as set_bit,
)
from .vertices import ( from .vertices import (
remove_duplicate_vertices as remove_duplicate_vertices, remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points
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,
) )
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

View File

@ -12,16 +12,16 @@ class AutoSlots(ABCMeta):
classes, they can have empty `__slots__` and their attribute type annotations classes, they can have empty `__slots__` and their attribute type annotations
can be used to generate a full `__slots__` for the concrete class. 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() parents = set()
for base in bases: for base in bases:
parents |= set(base.mro()) parents |= set(base.mro())
slots = tuple(dctn.get('__slots__', ())) slots = tuple(dctn.get('__slots__', tuple()))
for parent in parents: for parent in parents:
if not hasattr(parent, '__annotations__'): if not hasattr(parent, '__annotations__'):
continue continue
slots += tuple(parent.__annotations__.keys()) slots += tuple(getattr(parent, '__annotations__').keys())
dctn['__slots__'] = slots dctn['__slots__'] = slots
return super().__new__(cls, name, bases, dctn) return super().__new__(cls, name, bases, dctn)

View File

@ -12,7 +12,7 @@ def annotation2key(aaa: int | float | str) -> tuple[bool, Any]:
def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool: def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool:
if aa is None: if aa is None:
return bb is not None return bb is not None
elif bb is None: # noqa: RET505 elif bb is None:
return False return False
if len(aa) != len(bb): if len(aa) != len(bb):
@ -29,7 +29,7 @@ def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool:
if len(va) != len(vb): if len(va) != len(vb):
return 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: if aaa != bbb:
return annotation2key(aaa) < annotation2key(bbb) return annotation2key(aaa) < annotation2key(bbb)
return False 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: def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool:
if aa is None: if aa is None:
return bb is None return bb is None
elif bb is None: # noqa: RET505 elif bb is None:
return False return False
if len(aa) != len(bb): if len(aa) != len(bb):
@ -55,7 +55,7 @@ def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool:
if len(va) != len(vb): if len(va) != len(vb):
return False return False
for aaa, bbb in zip(va, vb, strict=True): for aaa, bbb in zip(va, vb):
if aaa != bbb: if aaa != bbb:
return False return False

View File

@ -1,4 +1,4 @@
from collections.abc import Callable from typing import Callable
from functools import wraps from functools import wraps
from ..error import OneShotError from ..error import OneShotError
@ -11,7 +11,7 @@ def oneshot(func: Callable) -> Callable:
expired = False expired = False
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): # noqa: ANN202 def wrapper(*args, **kwargs):
nonlocal expired nonlocal expired
if expired: if expired:
raise OneShotError(func.__name__) raise OneShotError(func.__name__)

View File

@ -1,5 +1,4 @@
from typing import TypeVar, Generic from typing import Callable, TypeVar, Generic
from collections.abc import Callable
from functools import lru_cache from functools import lru_cache

View File

@ -1,7 +1,7 @@
""" """
2D bin-packing 2D bin-packing
""" """
from collections.abc import Sequence, Mapping, Callable from typing import Sequence, Callable, Mapping
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
@ -38,8 +38,8 @@ def maxrects_bssf(
Raises: Raises:
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
""" """
regions = numpy.asarray(containers, dtype=float) regions = numpy.array(containers, copy=False, dtype=float)
rect_sizes = numpy.asarray(rects, dtype=float) rect_sizes = numpy.array(rects, copy=False, dtype=float)
rect_locs = numpy.zeros_like(rect_sizes) rect_locs = numpy.zeros_like(rect_sizes)
rejected_inds = set() rejected_inds = set()
@ -70,7 +70,8 @@ def maxrects_bssf(
if allow_rejects: if allow_rejects:
rejected_inds.add(rect_ind) rejected_inds.add(rect_ind)
continue 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 # Read out location
loc = regions[rr, :2] loc = regions[rr, :2]
@ -139,8 +140,8 @@ def guillotine_bssf_sas(
Raises: Raises:
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
""" """
regions = numpy.asarray(containers, dtype=float) regions = numpy.array(containers, copy=False, dtype=float)
rect_sizes = numpy.asarray(rects, dtype=float) rect_sizes = numpy.array(rects, copy=False, dtype=float)
rect_locs = numpy.zeros_like(rect_sizes) rect_locs = numpy.zeros_like(rect_sizes)
rejected_inds = set() rejected_inds = set()
@ -160,7 +161,8 @@ def guillotine_bssf_sas(
if allow_rejects: if allow_rejects:
rejected_inds.add(rect_ind) rejected_inds.add(rect_ind)
continue 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 # Read out location
loc = regions[rr, :2] loc = regions[rr, :2]
@ -227,7 +229,7 @@ def pack_patterns(
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. 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] 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] 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) locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects)
pat = Pattern() 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) pat.ref(pp, offset=oo + loc)
rejects = [patterns[ii] for ii in reject_inds] rejects = [patterns[ii] for ii in reject_inds]

View File

@ -6,7 +6,7 @@ and retrieving it (`data_to_ports`).
the port locations. This particular approach is just a sensible default; feel free to 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. 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 import logging
from itertools import chain from itertools import chain
@ -150,7 +150,7 @@ def data_to_ports_flat(
Returns: Returns:
The updated `pattern`. Port labels are not removed. 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: if not labels:
return pattern return pattern

View File

@ -1,7 +1,7 @@
""" """
Geometric transforms Geometric transforms
""" """
from collections.abc import Sequence from typing import Sequence
from functools import lru_cache from functools import lru_cache
import numpy import numpy

View File

@ -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) (i.e. the last vertex will be removed if it is the same as the first)
Returns: 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) duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
if not closed_path: if not closed_path:
duplicates[0] = False duplicates[0] = False
@ -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`. closed path. If `False`, the path is assumed to be open. Default `True`.
Returns: 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) vertices = remove_duplicate_vertices(vertices)
@ -73,8 +73,8 @@ def poly_contains_points(
Returns: Returns:
ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...] ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...]
""" """
points = numpy.asarray(points, dtype=float) points = numpy.array(points, copy=False)
vertices = numpy.asarray(vertices, dtype=float) vertices = numpy.array(vertices, copy=False)
if points.size == 0: if points.size == 0:
return numpy.zeros(0, dtype=numpy.int8) return numpy.zeros(0, dtype=numpy.int8)

View File

@ -42,7 +42,7 @@ classifiers = [
requires-python = ">=3.11" requires-python = ">=3.11"
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
"numpy>=1.26", "numpy~=1.21",
"klamath~=1.2", "klamath~=1.2",
] ]
@ -57,36 +57,3 @@ svg = ["svgwrite"]
visualize = ["matplotlib"] visualize = ["matplotlib"]
text = ["matplotlib", "freetype-py"] 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
]