Compare commits
37 Commits
cda895a7d3
...
8d671ed709
Author | SHA1 | Date | |
---|---|---|---|
8d671ed709 | |||
a816a7db8e | |||
a8a42bba1d | |||
da7118f521 | |||
ef6c5df386 | |||
ad0adec8e8 | |||
8fd6896a71 | |||
1ae3ffb9a2 | |||
810a09f18b | |||
97688ffae1 | |||
445c5690e1 | |||
7e1f617274 | |||
b10803efe9 | |||
5f0a450ffa | |||
aa3636ebc6 | |||
48ffc9709e | |||
5cdafd580f | |||
2cf187fdb8 | |||
99e55f931c | |||
c48b427c77 | |||
62fc64c344 | |||
f304217d76 | |||
ae21a2132e | |||
e159c80b0c | |||
38e9d5c250 | |||
5614eea3b4 | |||
8035daee7e | |||
4c69e773fd | |||
39d9b88fa4 | |||
9d5b1ef5e6 | |||
3d50ff0070 | |||
01fe53dc79 | |||
d5adf57bc6 | |||
4c721feaec | |||
6ec94fb3c3 | |||
b1d78b9acb | |||
dca918e63f |
@ -233,3 +233,5 @@ 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
|
||||
|
@ -99,6 +99,7 @@ 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'
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Sequence, Mapping
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Sequence, Callable, Any
|
||||
from typing import Any
|
||||
from collections.abc import Sequence, Callable
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Manual wire routing tutorial: Pather and BasicTool
|
||||
"""
|
||||
from typing import Callable
|
||||
from collections.abc 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
|
||||
|
@ -2,7 +2,7 @@
|
||||
Routines for creating normalized 2D lattices and common photonic crystal
|
||||
cavity designs.
|
||||
"""
|
||||
from typing import Sequence
|
||||
from collection.abc 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.array(shifts_a)
|
||||
tmp_r = numpy.array(shifts_r)
|
||||
tmp_a = numpy.asarray(shifts_a)
|
||||
tmp_r = numpy.asarray(shifts_r)
|
||||
n_shifted = max(tmp_a.size, tmp_r.size)
|
||||
|
||||
shifts_a = numpy.ones(n_shifted)
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Manual wire routing tutorial: RenderPather an PathTool
|
||||
"""
|
||||
from typing import Callable
|
||||
from collections.abc 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
|
||||
|
@ -28,25 +28,65 @@
|
||||
can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally.
|
||||
"""
|
||||
|
||||
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 .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 .library import (
|
||||
ILibraryView, ILibrary,
|
||||
LibraryView, Library, LazyLibrary,
|
||||
AbstractView, TreeView, Tree,
|
||||
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,
|
||||
)
|
||||
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.1'
|
||||
__version__ = '3.2'
|
||||
version = __version__ # legacy
|
||||
|
@ -97,7 +97,7 @@ class Abstract(PortList):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pivot = numpy.array(pivot)
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
self.translate_ports(-pivot)
|
||||
self.rotate_ports(rotation)
|
||||
self.rotate_port_offsets(rotation)
|
||||
|
@ -1,5 +1,10 @@
|
||||
from .builder import Builder
|
||||
from .pather import Pather
|
||||
from .renderpather import RenderPather
|
||||
from .utils import ell
|
||||
from .tools import Tool, RenderStep, BasicTool, PathTool
|
||||
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,
|
||||
)
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""
|
||||
Simplified Pattern assembly (`Builder`)
|
||||
"""
|
||||
from typing import Self, Sequence, Mapping
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Mapping
|
||||
import copy
|
||||
import logging
|
||||
from functools import wraps
|
||||
@ -137,7 +138,7 @@ class Builder(PortList):
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls,
|
||||
cls: type['Builder'],
|
||||
source: PortList | Mapping[str, Port] | str,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
@ -275,7 +276,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
|
||||
|
||||
@ -347,7 +348,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
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""
|
||||
Manual wire/waveguide routing (`Pather`)
|
||||
"""
|
||||
from typing import Self, Sequence, MutableMapping, Mapping
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, MutableMapping, Mapping
|
||||
import copy
|
||||
import logging
|
||||
from pprint import pformat
|
||||
@ -174,7 +175,7 @@ class Pather(Builder):
|
||||
|
||||
@classmethod
|
||||
def from_builder(
|
||||
cls,
|
||||
cls: type['Pather'],
|
||||
builder: Builder,
|
||||
*,
|
||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
||||
@ -194,7 +195,7 @@ class Pather(Builder):
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls,
|
||||
cls: type['Pather'],
|
||||
source: PortList | Mapping[str, Port] | str,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
@ -657,7 +658,7 @@ class Pather(Builder):
|
||||
|
||||
if not bound_types:
|
||||
raise BuildError('No bound type specified for mpath')
|
||||
elif len(bound_types) > 1:
|
||||
if len(bound_types) > 1:
|
||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||
bound_type = tuple(bound_types)[0]
|
||||
|
||||
@ -671,13 +672,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)
|
||||
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_'?
|
||||
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
||||
|
||||
# TODO def bus_join()?
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""
|
||||
Pather with batched (multi-step) rendering
|
||||
"""
|
||||
from typing import Self, Sequence, Mapping, MutableMapping
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Mapping, MutableMapping
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
@ -127,7 +128,7 @@ class RenderPather(PortList):
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls,
|
||||
cls: type['RenderPather'],
|
||||
source: PortList | Mapping[str, Port] | str,
|
||||
*,
|
||||
library: ILibrary | None = None,
|
||||
@ -247,7 +248,7 @@ class RenderPather(PortList):
|
||||
other_tgt = self.library[other.name]
|
||||
|
||||
# get rid of plugged ports
|
||||
for kk in map_in.keys():
|
||||
for kk in map_in:
|
||||
if kk in self.paths:
|
||||
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
|
||||
|
||||
@ -560,7 +561,7 @@ class RenderPather(PortList):
|
||||
|
||||
if not bound_types:
|
||||
raise BuildError('No bound type specified for mpath')
|
||||
elif len(bound_types) > 1:
|
||||
if len(bound_types) > 1:
|
||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||
bound_type = tuple(bound_types)[0]
|
||||
|
||||
|
@ -3,7 +3,8 @@ Tools are objects which dynamically generate simple single-use devices (e.g. wir
|
||||
|
||||
# TODO document all tools
|
||||
"""
|
||||
from typing import Sequence, Literal, Callable, Any
|
||||
from typing import Literal, 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 dataclasses import dataclass
|
||||
|
||||
@ -222,8 +223,8 @@ class Tool:
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
port_names: Sequence[str] = ('A', 'B'), # noqa: ARG002 (unused)
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
"""
|
||||
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
||||
@ -312,7 +313,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> tuple[Port, LData]:
|
||||
# TODO check all the math for L-shaped bends
|
||||
if ccw is not None:
|
||||
@ -404,7 +405,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)
|
||||
straight_pat = gen_straight(straight_length, **kwargs)
|
||||
if append:
|
||||
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
|
||||
else:
|
||||
@ -454,7 +455,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> Library:
|
||||
out_port, dxy = self.planL(
|
||||
ccw,
|
||||
@ -485,9 +486,9 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> tuple[Port, NDArray[numpy.float64]]:
|
||||
# TODO check all the math for L-shaped bends
|
||||
|
||||
@ -521,7 +522,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
|
||||
path_vertices = [batch[0].start_port.offset]
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Mapping, Sequence, SupportsFloat, cast, TYPE_CHECKING
|
||||
from typing import SupportsFloat, cast, TYPE_CHECKING
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
@ -112,7 +113,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!')
|
||||
elif bound_type in ('xmin', 'xmax') and not is_horizontal:
|
||||
if 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)
|
||||
@ -201,7 +202,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)))
|
||||
+ '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets, strict=True)))
|
||||
|
||||
result = dict(zip(ports.keys(), offsets))
|
||||
result = dict(zip(ports.keys(), offsets, strict=True))
|
||||
return result
|
||||
|
@ -6,7 +6,8 @@ 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, Callable, Mapping, cast, TextIO, IO
|
||||
from typing import Any, cast, TextIO, IO
|
||||
from collections.abc import Mapping, Callable
|
||||
import io
|
||||
import logging
|
||||
import pathlib
|
||||
@ -15,6 +16,7 @@ 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
|
||||
@ -38,7 +40,7 @@ def write(
|
||||
top_name: str,
|
||||
stream: TextIO,
|
||||
*,
|
||||
dxf_version='AC1024',
|
||||
dxf_version: str = 'AC1024',
|
||||
) -> None:
|
||||
"""
|
||||
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
||||
@ -204,26 +206,25 @@ def read(
|
||||
return mlib, library_info
|
||||
|
||||
|
||||
def _read_block(block) -> tuple[str, Pattern]:
|
||||
def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> tuple[str, Pattern]:
|
||||
name = block.name
|
||||
pat = Pattern()
|
||||
for element in block:
|
||||
eltype = element.dxftype()
|
||||
if eltype in ('POLYLINE', 'LWPOLYLINE'):
|
||||
if eltype == 'LWPOLYLINE':
|
||||
points = numpy.array(tuple(element.lwpoints))
|
||||
else:
|
||||
points = numpy.array(tuple(element.points()))
|
||||
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]
|
||||
attr = element.dxfattribs()
|
||||
layer = attr.get('layer', DEFAULT_LAYER)
|
||||
|
||||
if points.shape[1] == 2:
|
||||
raise PatternError('Invalid or unimplemented polygon?')
|
||||
#shape = Polygon()
|
||||
elif points.shape[1] > 2:
|
||||
|
||||
if points.shape[1] > 2:
|
||||
if (points[0, 2] != points[:, 2]).any():
|
||||
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)')
|
||||
elif points.shape[1] == 4 and (points[:, 3] != 0).any():
|
||||
if points.shape[1] == 4 and (points[:, 3] != 0).any():
|
||||
raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
|
||||
|
||||
width = points[0, 2]
|
||||
@ -238,9 +239,9 @@ def _read_block(block) -> tuple[str, Pattern]:
|
||||
|
||||
pat.shapes[layer].append(shape)
|
||||
|
||||
elif eltype in ('TEXT',):
|
||||
elif isinstance(element, Text):
|
||||
args = dict(
|
||||
offset=numpy.array(element.get_pos()[1])[:2],
|
||||
offset=numpy.asarray(element.get_placement()[1])[:2],
|
||||
layer=element.dxfattribs().get('layer', DEFAULT_LAYER),
|
||||
)
|
||||
string = element.dxfattribs().get('text', '')
|
||||
@ -251,7 +252,7 @@ def _read_block(block) -> tuple[str, Pattern]:
|
||||
pat.label(string=string, **args)
|
||||
# else:
|
||||
# pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????))
|
||||
elif eltype in ('INSERT',):
|
||||
elif isinstance(element, Insert):
|
||||
attr = element.dxfattribs()
|
||||
xscale = attr.get('xscale', 1)
|
||||
yscale = attr.get('yscale', 1)
|
||||
@ -261,7 +262,7 @@ def _read_block(block) -> tuple[str, Pattern]:
|
||||
mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0))
|
||||
rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle
|
||||
|
||||
offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2]
|
||||
offset = numpy.asarray(attr.get('insert', (0, 0, 0)))[:2]
|
||||
|
||||
args = dict(
|
||||
target=attr.get('name', None),
|
||||
@ -336,10 +337,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:
|
||||
|
@ -19,7 +19,8 @@ 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 Callable, Iterable, Mapping, IO, cast, Any
|
||||
from typing import IO, cast, Any
|
||||
from collections.abc import Iterable, Mapping, Callable
|
||||
import io
|
||||
import mmap
|
||||
import logging
|
||||
@ -356,7 +357,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.array(ref.offset) + numpy.array([
|
||||
xy = numpy.asarray(ref.offset) + numpy.array([
|
||||
[0.0, 0.0],
|
||||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
@ -408,8 +409,8 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
|
||||
for key, vals in annotations.items():
|
||||
try:
|
||||
i = int(key)
|
||||
except ValueError:
|
||||
raise PatternError(f'Annotation key {key} is not convertable to an integer')
|
||||
except ValueError as err:
|
||||
raise PatternError(f'Annotation key {key} is not convertable to an integer') from err
|
||||
if not (0 < i < 126):
|
||||
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
|
||||
|
||||
@ -596,19 +597,19 @@ def load_libraryfile(
|
||||
path = pathlib.Path(filename)
|
||||
stream: IO[bytes]
|
||||
if is_gzipped(path):
|
||||
if mmap:
|
||||
if use_mmap:
|
||||
logger.info('Asked to mmap a gzipped file, reading into memory instead...')
|
||||
gz_stream = gzip.open(path, mode='rb')
|
||||
gz_stream = gzip.open(path, mode='rb') # noqa: SIM115
|
||||
stream = io.BytesIO(gz_stream.read()) # type: ignore
|
||||
else:
|
||||
gz_stream = gzip.open(path, mode='rb')
|
||||
gz_stream = gzip.open(path, mode='rb') # noqa: SIM115
|
||||
stream = io.BufferedReader(gz_stream) # type: ignore
|
||||
else:
|
||||
if mmap:
|
||||
base_stream = open(path, mode='rb', buffering=0)
|
||||
else: # noqa: PLR5501
|
||||
if use_mmap:
|
||||
base_stream = path.open(mode='rb', buffering=0) # noqa: SIM115
|
||||
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
|
||||
else:
|
||||
stream = open(path, mode='rb')
|
||||
stream = path.open(mode='rb') # noqa: SIM115
|
||||
return load_library(stream, full_load=full_load, postprocess=postprocess)
|
||||
|
||||
|
||||
|
@ -14,7 +14,8 @@ 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, Callable, Iterable, IO, Mapping, cast, Sequence
|
||||
from typing import Any, IO, cast
|
||||
from collections.abc import Sequence, Iterable, Mapping, Callable
|
||||
import logging
|
||||
import pathlib
|
||||
import gzip
|
||||
@ -297,7 +298,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 Exception('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types
|
||||
raise PatternError('masque does not support multiple cap types on a single path.') # TODO handle multiple cap types
|
||||
cap = cap_start
|
||||
|
||||
path_args: dict[str, Any] = {}
|
||||
@ -452,6 +453,8 @@ 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
|
||||
@ -692,9 +695,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
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
SVG file format readers and writers
|
||||
"""
|
||||
from typing import Mapping
|
||||
from collections.abc 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')
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
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')
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
else:
|
||||
bounds_min, bounds_max = bounds
|
||||
|
||||
@ -154,9 +154,9 @@ def poly2path(vertices: ArrayLike) -> str:
|
||||
Returns:
|
||||
SVG path-string.
|
||||
"""
|
||||
verts = numpy.array(vertices, copy=False)
|
||||
commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1])
|
||||
verts = numpy.asarray(vertices)
|
||||
commands = 'M{:g},{:g} '.format(verts[0][0], verts[0][1]) # noqa: UP032
|
||||
for vertex in verts[1:]:
|
||||
commands += 'L{:g},{:g}'.format(vertex[0], vertex[1])
|
||||
commands += 'L{:g},{:g}'.format(vertex[0], vertex[1]) # noqa: UP032
|
||||
commands += ' Z '
|
||||
return commands
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""
|
||||
Helper functions for file reading and writing
|
||||
"""
|
||||
from typing import IO, Iterator, Mapping
|
||||
from typing import IO
|
||||
from collections.abc import Iterator, Mapping
|
||||
import re
|
||||
import pathlib
|
||||
import logging
|
||||
@ -116,7 +117,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()
|
||||
@ -128,7 +129,7 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
|
||||
|
||||
|
||||
def is_gzipped(path: pathlib.Path) -> bool:
|
||||
with open(path, 'rb') as stream:
|
||||
with path.open('rb') as stream:
|
||||
magic_bytes = stream.read(2)
|
||||
return magic_bytes == b'\x1f\x8b'
|
||||
|
||||
|
@ -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, copy=True)
|
||||
self.offset = numpy.array(offset, dtype=float)
|
||||
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.array(pivot, dtype=float)
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
self.translate(-pivot)
|
||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||
self.translate(+pivot)
|
||||
|
@ -14,17 +14,14 @@ 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 Callable, Self, Type, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal
|
||||
from typing import Iterator, Mapping, MutableMapping, Sequence
|
||||
from typing import Self, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal
|
||||
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable
|
||||
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
|
||||
@ -176,7 +173,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
tops = tuple(self.keys())
|
||||
|
||||
if skip is None:
|
||||
skip = set([None])
|
||||
skip = {None}
|
||||
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
@ -213,7 +210,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - set((None,)))
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - {None})
|
||||
keep |= set(tops)
|
||||
|
||||
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
||||
@ -285,7 +282,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
|
||||
@ -349,8 +346,11 @@ 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.array(transform, dtype=float, copy=False)
|
||||
transform = numpy.asarray(transform, dtype=float)
|
||||
|
||||
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.keys():
|
||||
for key in other:
|
||||
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):
|
||||
def label2name(label: tuple) -> str: # noqa: ARG001
|
||||
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, _shape):
|
||||
def name_func(_pat: Pattern, _shape: Shape | Label) -> str:
|
||||
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) - set((None,)))
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - {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 := set(name for name, pat in self.items() if pat.is_empty()):
|
||||
while empty := {name for name, pat in self.items() if pat.is_empty()}:
|
||||
for name in empty:
|
||||
del self[name]
|
||||
|
||||
@ -1038,10 +1038,7 @@ class Library(ILibrary):
|
||||
if key in self.mapping:
|
||||
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!')
|
||||
|
||||
if callable(value):
|
||||
value = value()
|
||||
else:
|
||||
value = value
|
||||
value = value() if callable(value) else value
|
||||
self.mapping[key] = value
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
@ -1054,7 +1051,7 @@ class Library(ILibrary):
|
||||
return f'<Library ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>'
|
||||
|
||||
@classmethod
|
||||
def mktree(cls, name: str) -> tuple[Self, 'Pattern']:
|
||||
def mktree(cls: type[Self], name: str) -> tuple[Self, 'Pattern']:
|
||||
"""
|
||||
Create a new Library and immediately add a pattern
|
||||
|
||||
@ -1232,8 +1229,18 @@ 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."""
|
||||
suffix = base64.b64encode(struct.pack('>Q', ii), altchars=b'$?').decode('ASCII')
|
||||
return '$' + suffix[:-1].lstrip('A')
|
||||
"""
|
||||
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)
|
||||
|
@ -2,7 +2,8 @@
|
||||
Object representing a one multi-layer lithographic layout.
|
||||
A single level of hierarchical references is included.
|
||||
"""
|
||||
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
|
||||
from typing import cast, Self, Any, TypeVar
|
||||
from collections.abc import Sequence, Mapping, MutableMapping, Iterable, Callable
|
||||
import copy
|
||||
import logging
|
||||
import functools
|
||||
@ -295,7 +296,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
if not annotations_eq(self.annotations, other.annotations):
|
||||
return False
|
||||
|
||||
if not ports_eq(self.ports, other.ports):
|
||||
if not ports_eq(self.ports, other.ports): # noqa: SIM103
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -312,10 +313,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
self
|
||||
"""
|
||||
if sort_elements:
|
||||
def maybe_sort(xx):
|
||||
def maybe_sort(xx): # noqa:ANN001,ANN202
|
||||
return sorted(xx)
|
||||
else:
|
||||
def maybe_sort(xx):
|
||||
def maybe_sort(xx): # noqa:ANN001,ANN202
|
||||
return xx
|
||||
|
||||
self.refs = defaultdict(list, sorted(
|
||||
@ -471,10 +472,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]]:
|
||||
@ -594,7 +595,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
|
||||
if (cbounds[1] < cbounds[0]).any():
|
||||
return None
|
||||
else:
|
||||
return cbounds
|
||||
|
||||
def get_bounds_nonempty(
|
||||
@ -616,7 +616,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
`[[x_min, y_min], [x_max, y_max]]`
|
||||
"""
|
||||
bounds = self.get_bounds(library)
|
||||
bounds = self.get_bounds(library, recurse=recurse)
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
@ -690,7 +690,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pivot = numpy.array(pivot)
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
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 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
|
||||
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
|
||||
|
||||
if self.has_refs() and library is None:
|
||||
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
||||
|
||||
offset = numpy.array(offset, dtype=float)
|
||||
offset = numpy.asarray(offset, dtype=float)
|
||||
|
||||
if not overdraw:
|
||||
figure = pyplot.figure()
|
||||
@ -1324,7 +1324,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
|
||||
@classmethod
|
||||
def interface(
|
||||
cls,
|
||||
cls: type['Pattern'],
|
||||
source: PortList | Mapping[str, Port],
|
||||
*,
|
||||
in_prefix: str = 'in_',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn, Any
|
||||
from typing import overload, Self, NoReturn, Any
|
||||
from collections.abc import Iterable, KeysView, ValuesView, Mapping
|
||||
import warnings
|
||||
import traceback
|
||||
import logging
|
||||
@ -92,7 +93,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
def copy(self) -> Self:
|
||||
return self.deepcopy()
|
||||
|
||||
def get_bounds(self):
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
return numpy.vstack((self.offset, self.offset))
|
||||
|
||||
def set_ptype(self, ptype: str) -> Self:
|
||||
@ -180,7 +181,7 @@ class PortList(metaclass=ABCMeta):
|
||||
"""
|
||||
if isinstance(key, str):
|
||||
return self.ports[key]
|
||||
else:
|
||||
else: # noqa: RET505
|
||||
return {k: self.ports[k] for k in key}
|
||||
|
||||
def __contains__(self, key: str) -> NoReturn:
|
||||
@ -238,7 +239,7 @@ class PortList(metaclass=ABCMeta):
|
||||
if duplicates:
|
||||
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
||||
|
||||
renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()}
|
||||
renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()}
|
||||
if None in renamed:
|
||||
del renamed[None]
|
||||
|
||||
@ -293,14 +294,14 @@ class PortList(metaclass=ABCMeta):
|
||||
Raises:
|
||||
`PortError` if the ports are not properly aligned.
|
||||
"""
|
||||
a_names, b_names = list(zip(*connections.items()))
|
||||
a_names, b_names = list(zip(*connections.items(), strict=True))
|
||||
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 at != 'unk' and bt != 'unk'
|
||||
for at, bt in zip(a_types, b_types)])
|
||||
type_conflicts = numpy.array([at != bt and 'unk' not in (at, bt)
|
||||
for at, bt in zip(a_types, b_types, strict=True)])
|
||||
|
||||
if type_conflicts.any():
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
@ -501,8 +502,8 @@ class PortList(metaclass=ABCMeta):
|
||||
o_offsets[:, 1] *= -1
|
||||
o_rotations *= -1
|
||||
|
||||
type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
|
||||
for st, ot in zip(s_types, o_types)])
|
||||
type_conflicts = numpy.array([st != ot and 'unk' not in (st, ot)
|
||||
for st, ot in zip(s_types, o_types, strict=True)])
|
||||
if type_conflicts.any():
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
for nn, (k, v) in enumerate(map_in.items()):
|
||||
|
@ -2,7 +2,8 @@
|
||||
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 Mapping, TYPE_CHECKING, Self, Any
|
||||
from typing import TYPE_CHECKING, Self, Any
|
||||
from collections.abc import Mapping
|
||||
import copy
|
||||
import functools
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
Repetitions provide support for efficiently representing multiple identical
|
||||
instances of an object .
|
||||
"""
|
||||
from typing import Any, Type, Self, TypeVar, cast
|
||||
from typing import Any, Self, TypeVar, cast
|
||||
import copy
|
||||
import functools
|
||||
from abc import ABCMeta, abstractmethod
|
||||
@ -101,7 +101,6 @@ class Grid(Repetition):
|
||||
if b_vector is None:
|
||||
if b_count > 1:
|
||||
raise PatternError('Repetition has b_count > 1 but no b_vector')
|
||||
else:
|
||||
b_vector = numpy.array([0.0, 0.0])
|
||||
|
||||
if a_count < 1:
|
||||
@ -116,7 +115,7 @@ class Grid(Repetition):
|
||||
|
||||
@classmethod
|
||||
def aligned(
|
||||
cls: Type[GG],
|
||||
cls: type[GG],
|
||||
x: float,
|
||||
y: float,
|
||||
x_count: int,
|
||||
@ -157,12 +156,11 @@ class Grid(Repetition):
|
||||
|
||||
@a_vector.setter
|
||||
def a_vector(self, val: ArrayLike) -> None:
|
||||
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().astype(float)
|
||||
self._a_vector = val.flatten()
|
||||
|
||||
# b_vector property
|
||||
@property
|
||||
@ -171,8 +169,7 @@ class Grid(Repetition):
|
||||
|
||||
@b_vector.setter
|
||||
def b_vector(self, val: ArrayLike) -> None:
|
||||
if not isinstance(val, numpy.ndarray):
|
||||
val = numpy.array(val, dtype=float, copy=True)
|
||||
val = numpy.array(val, dtype=float)
|
||||
|
||||
if val.size != 2:
|
||||
raise PatternError('b_vector must be convertible to size-2 ndarray')
|
||||
@ -290,7 +287,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)):
|
||||
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): # noqa: SIM103
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -335,9 +332,9 @@ class Arbitrary(Repetition):
|
||||
|
||||
@displacements.setter
|
||||
def displacements(self, val: ArrayLike) -> None:
|
||||
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
|
||||
vala = numpy.array(val, dtype=float)
|
||||
order = numpy.lexsort(vala.T[::-1]) # sortrows
|
||||
self._displacements = vala[order]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -3,11 +3,15 @@ Shapes for use with the Pattern class, as well as the Shape abstract class from
|
||||
which they are derived.
|
||||
"""
|
||||
|
||||
from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from .shape import (
|
||||
Shape as Shape,
|
||||
normalized_shape_tuple as normalized_shape_tuple,
|
||||
DEFAULT_POLY_NUM_VERTICES as DEFAULT_POLY_NUM_VERTICES,
|
||||
)
|
||||
|
||||
from .polygon import Polygon
|
||||
from .circle import Circle
|
||||
from .ellipse import Ellipse
|
||||
from .arc import Arc
|
||||
from .text import Text
|
||||
from .path import Path
|
||||
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
|
||||
|
@ -286,7 +286,7 @@ class Arc(Shape):
|
||||
return thetas
|
||||
|
||||
wh = self.width / 2.0
|
||||
if wh == r0 or wh == r1:
|
||||
if wh in (r0, 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)):
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
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)):
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
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)
|
||||
return numpy.array(a, dtype=float)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||
|
@ -119,10 +119,10 @@ class Circle(Shape):
|
||||
return numpy.vstack((self.offset - self.radius,
|
||||
self.offset + self.radius))
|
||||
|
||||
def rotate(self, theta: float) -> 'Circle':
|
||||
def rotate(self, theta: float) -> 'Circle': # noqa: ARG002 (theta unused)
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> 'Circle':
|
||||
def mirror(self, axis: int = 0) -> 'Circle': # noqa: ARG002 (axis unused)
|
||||
self.offset *= -1
|
||||
return self
|
||||
|
||||
@ -130,7 +130,7 @@ class Circle(Shape):
|
||||
self.radius *= c
|
||||
return self
|
||||
|
||||
def normalized_form(self, norm_value) -> normalized_shape_tuple:
|
||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||
rotation = 0.0
|
||||
magnitude = self.radius / norm_value
|
||||
return ((type(self),),
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Sequence, Any, cast
|
||||
from typing import Any, cast
|
||||
from collections.abc import Sequence
|
||||
import copy
|
||||
import functools
|
||||
from enum import Enum
|
||||
@ -32,8 +33,7 @@ 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` may (but may not) create a copy of the
|
||||
passed vertex coordinates. See `numpy.array(..., copy=False)` for details.
|
||||
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
|
||||
|
||||
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 Exception('Tried to set cap extensions to None on path with custom cap type')
|
||||
raise PatternError('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 Exception('Tried to set custom cap extensions on path with non-custom cap type')
|
||||
raise PatternError('Tried to set custom cap extensions on path with non-custom cap type')
|
||||
self._cap_extensions = vals
|
||||
|
||||
# vertices property
|
||||
@ -117,8 +117,7 @@ class Path(Shape):
|
||||
"""
|
||||
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
|
||||
|
||||
When setting, note that a copy of the provided vertices may or may not be made,
|
||||
following the rules from `numpy.array(.., copy=False)`.
|
||||
When setting, note that a copy of the provided vertices will be made.
|
||||
"""
|
||||
return self._vertices
|
||||
|
||||
@ -430,22 +429,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
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Sequence, Any, cast
|
||||
from typing import Any, cast
|
||||
from collections.abc import Sequence
|
||||
import copy
|
||||
import functools
|
||||
|
||||
@ -19,8 +20,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 (but may not) create a copy of the
|
||||
passed vertex coordinates. See `numpy.array(..., copy=False)` for details.
|
||||
Note that the setter for `Polygon.vertices` may creates a copy of the
|
||||
passed vertex coordinates.
|
||||
|
||||
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
|
||||
"""
|
||||
@ -39,8 +40,7 @@ class Polygon(Shape):
|
||||
"""
|
||||
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
||||
|
||||
When setting, note that a copy of the provided vertices may or may not be made,
|
||||
following the rules from `numpy.array(.., copy=False)`.
|
||||
When setting, note that a copy of the provided vertices will be made,
|
||||
"""
|
||||
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:
|
||||
else: # noqa: PLR5501
|
||||
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:
|
||||
else: # noqa: PLR5501
|
||||
if yctr is not None:
|
||||
pass
|
||||
elif ymax is None:
|
||||
@ -330,10 +330,7 @@ class Polygon(Shape):
|
||||
Returns:
|
||||
A Polygon object containing the requested octagon
|
||||
"""
|
||||
if regular:
|
||||
s = 1 + numpy.sqrt(2)
|
||||
else:
|
||||
s = 2
|
||||
s = (1 + numpy.sqrt(2)) if regular else 2
|
||||
|
||||
norm_oct = numpy.array([
|
||||
[-1, -s],
|
||||
@ -357,8 +354,8 @@ class Polygon(Shape):
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = None, # unused
|
||||
max_arclen: float | None = None, # unused
|
||||
num_vertices: int | None = None, # unused # noqa: ARG002
|
||||
max_arclen: float | None = None, # unused # noqa: ARG002
|
||||
) -> list['Polygon']:
|
||||
return [copy.deepcopy(self)]
|
||||
|
||||
@ -417,22 +414,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
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Callable, TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from collections.abc import Callable
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
@ -134,7 +135,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)):
|
||||
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True):
|
||||
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
|
||||
@ -164,7 +165,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
|
||||
m = dv[1] / dv[0]
|
||||
|
||||
def get_grid_inds(xes: ArrayLike) -> NDArray[numpy.float64]:
|
||||
def get_grid_inds(xes: ArrayLike, m: float = m, v: NDArray = v) -> 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
|
||||
@ -265,11 +266,12 @@ 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])
|
||||
for k in (keep_x, keep_y):
|
||||
for s in (1, 2):
|
||||
k[s:] += k[:-s]
|
||||
k[:-s] += k[s:]
|
||||
k = k > 0
|
||||
# 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
|
||||
|
||||
gx = grx[keep_x]
|
||||
gy = gry[keep_y]
|
||||
|
@ -132,8 +132,8 @@ class Text(RotatableImpl, Shape):
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = None, # unused
|
||||
max_arclen: float | None = None, # unused
|
||||
num_vertices: int | None = None, # unused # noqa: ARG002
|
||||
max_arclen: float | None = None, # unused # noqa: ARG002
|
||||
) -> list[Polygon]:
|
||||
all_polygons = []
|
||||
total_advance = 0.0
|
||||
@ -191,6 +191,11 @@ 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'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
||||
|
||||
|
||||
def get_char_as_polygons(
|
||||
font_path: str,
|
||||
@ -216,7 +221,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 Exception('get_char_as_polygons called with non-char')
|
||||
raise PatternError('get_char_as_polygons called with non-char')
|
||||
|
||||
face = Face(font_path)
|
||||
face.set_char_size(resolution)
|
||||
@ -225,7 +230,8 @@ 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])
|
||||
@ -278,8 +284,3 @@ 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'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
||||
|
@ -3,11 +3,32 @@ Traits (mixins) and default implementations
|
||||
|
||||
Traits and mixins should set `__slots__ = ()` to enable use of `__slots__` in subclasses.
|
||||
"""
|
||||
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
|
||||
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,
|
||||
)
|
||||
|
@ -1,9 +1,8 @@
|
||||
from typing import Self
|
||||
from abc import ABCMeta
|
||||
import copy
|
||||
|
||||
|
||||
class Copyable(metaclass=ABCMeta):
|
||||
class Copyable:
|
||||
"""
|
||||
Trait class which adds .copy() and .deepcopy()
|
||||
"""
|
||||
|
@ -63,7 +63,7 @@ class LayerableImpl(Layerable, metaclass=ABCMeta):
|
||||
return self._layer
|
||||
|
||||
@layer.setter
|
||||
def layer(self, val: layer_t):
|
||||
def layer(self, val: layer_t) -> None:
|
||||
self._layer = val
|
||||
|
||||
#
|
||||
|
@ -44,7 +44,7 @@ class Mirrorable(metaclass=ABCMeta):
|
||||
# """
|
||||
# __slots__ = ()
|
||||
#
|
||||
# _mirrored: numpy.ndarray # ndarray[bool]
|
||||
# _mirrored: NDArray[numpy.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) -> numpy.ndarray: # ndarray[bool]
|
||||
# def mirrored(self) -> NDArray[numpy.bool]:
|
||||
# """ Whether to mirror across the [x, y] axes, respectively """
|
||||
# return self._mirrored
|
||||
#
|
||||
# @mirrored.setter
|
||||
# def mirrored(self, val: Sequence[bool]):
|
||||
# def mirrored(self, val: Sequence[bool]) -> None:
|
||||
# if is_scalar(val):
|
||||
# raise MasqueError('Mirrored must be a 2-element list of booleans')
|
||||
# self._mirrored = numpy.array(val, dtype=bool, copy=True)
|
||||
# self._mirrored = numpy.array(val, dtype=bool)
|
||||
#
|
||||
# #
|
||||
# # Methods
|
||||
|
@ -81,12 +81,11 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
|
||||
@offset.setter
|
||||
def offset(self, val: ArrayLike) -> None:
|
||||
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() # type: ignore
|
||||
self._offset = val.flatten()
|
||||
|
||||
#
|
||||
# Methods
|
||||
|
@ -34,7 +34,7 @@ class Repeatable(metaclass=ABCMeta):
|
||||
|
||||
# @repetition.setter
|
||||
# @abstractmethod
|
||||
# def repetition(self, repetition: 'Repetition | None'):
|
||||
# def repetition(self, repetition: 'Repetition | None') -> None:
|
||||
# pass
|
||||
|
||||
#
|
||||
@ -75,7 +75,7 @@ class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
|
||||
return self._repetition
|
||||
|
||||
@repetition.setter
|
||||
def repetition(self, repetition: 'Repetition | None'):
|
||||
def repetition(self, repetition: 'Repetition | None') -> 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!')
|
||||
|
@ -54,7 +54,7 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float):
|
||||
def rotation(self, val: float) -> None:
|
||||
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.array(pivot, dtype=float)
|
||||
pivot = numpy.asarray(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
|
||||
|
@ -48,7 +48,7 @@ class ScalableImpl(Scalable, metaclass=ABCMeta):
|
||||
return self._scale
|
||||
|
||||
@scale.setter
|
||||
def scale(self, val: float):
|
||||
def scale(self, val: float) -> None:
|
||||
if not is_scalar(val):
|
||||
raise MasqueError('Scale must be a scalar')
|
||||
if not val > 0:
|
||||
|
@ -1,19 +1,40 @@
|
||||
"""
|
||||
Various helper functions, type definitions, etc.
|
||||
"""
|
||||
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, set_bit
|
||||
from .vertices import (
|
||||
remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points
|
||||
from .types import (
|
||||
layer_t as layer_t,
|
||||
annotations_t as annotations_t,
|
||||
SupportsBool as SupportsBool,
|
||||
)
|
||||
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 .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 . import ports2data
|
||||
from .bitwise import (
|
||||
get_bit as get_bit,
|
||||
set_bit as 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,
|
||||
)
|
||||
|
||||
from . import pack2d
|
||||
from . import ports2data as ports2data
|
||||
|
||||
from . import pack2d as pack2d
|
||||
|
@ -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):
|
||||
def __new__(cls, name, bases, dctn): # noqa: ANN001,ANN204
|
||||
parents = set()
|
||||
for base in bases:
|
||||
parents |= set(base.mro())
|
||||
|
||||
slots = tuple(dctn.get('__slots__', tuple()))
|
||||
slots = tuple(dctn.get('__slots__', ()))
|
||||
for parent in parents:
|
||||
if not hasattr(parent, '__annotations__'):
|
||||
continue
|
||||
slots += tuple(getattr(parent, '__annotations__').keys())
|
||||
slots += tuple(parent.__annotations__.keys())
|
||||
|
||||
dctn['__slots__'] = slots
|
||||
return super().__new__(cls, name, bases, dctn)
|
||||
|
@ -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:
|
||||
elif bb is None: # noqa: RET505
|
||||
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):
|
||||
for aaa, bbb in zip(va, vb, strict=True):
|
||||
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:
|
||||
elif bb is None: # noqa: RET505
|
||||
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):
|
||||
for aaa, bbb in zip(va, vb, strict=True):
|
||||
if aaa != bbb:
|
||||
return False
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Callable
|
||||
from collections.abc 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):
|
||||
def wrapper(*args, **kwargs): # noqa: ANN202
|
||||
nonlocal expired
|
||||
if expired:
|
||||
raise OneShotError(func.__name__)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Callable, TypeVar, Generic
|
||||
from typing import TypeVar, Generic
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
2D bin-packing
|
||||
"""
|
||||
from typing import Sequence, Callable, Mapping
|
||||
from collections.abc import Sequence, Mapping, Callable
|
||||
|
||||
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.array(containers, copy=False, dtype=float)
|
||||
rect_sizes = numpy.array(rects, copy=False, dtype=float)
|
||||
regions = numpy.asarray(containers, dtype=float)
|
||||
rect_sizes = numpy.asarray(rects, dtype=float)
|
||||
rect_locs = numpy.zeros_like(rect_sizes)
|
||||
rejected_inds = set()
|
||||
|
||||
@ -70,7 +70,6 @@ def maxrects_bssf(
|
||||
if allow_rejects:
|
||||
rejected_inds.add(rect_ind)
|
||||
continue
|
||||
else:
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
|
||||
# Read out location
|
||||
@ -140,8 +139,8 @@ def guillotine_bssf_sas(
|
||||
Raises:
|
||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||
"""
|
||||
regions = numpy.array(containers, copy=False, dtype=float)
|
||||
rect_sizes = numpy.array(rects, copy=False, dtype=float)
|
||||
regions = numpy.asarray(containers, dtype=float)
|
||||
rect_sizes = numpy.asarray(rects, dtype=float)
|
||||
rect_locs = numpy.zeros_like(rect_sizes)
|
||||
rejected_inds = set()
|
||||
|
||||
@ -161,7 +160,6 @@ def guillotine_bssf_sas(
|
||||
if allow_rejects:
|
||||
rejected_inds.add(rect_ind)
|
||||
continue
|
||||
else:
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
|
||||
# Read out location
|
||||
@ -229,7 +227,7 @@ def pack_patterns(
|
||||
MasqueError if `allow_rejects` is `True` but some `rects` could not be placed.
|
||||
"""
|
||||
|
||||
half_spacing = numpy.array(spacing, copy=False, dtype=float) / 2
|
||||
half_spacing = numpy.asarray(spacing, 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]
|
||||
@ -238,7 +236,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):
|
||||
for pp, oo, loc in zip(patterns, offsets, locations, strict=True):
|
||||
pat.ref(pp, offset=oo + loc)
|
||||
|
||||
rejects = [patterns[ii] for ii in reject_inds]
|
||||
|
@ -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 typing import Sequence, Mapping
|
||||
from collections.abc 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
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Geometric transforms
|
||||
"""
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
from functools import lru_cache
|
||||
|
||||
import numpy
|
||||
|
@ -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.
|
||||
`vertices` with no consecutive duplicates. This may be a view into the original array.
|
||||
"""
|
||||
vertices = numpy.array(vertices)
|
||||
vertices = numpy.asarray(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.
|
||||
`vertices` with colinear (superflous) vertices removed. May be a view into the original array.
|
||||
"""
|
||||
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.array(points, copy=False)
|
||||
vertices = numpy.array(vertices, copy=False)
|
||||
points = numpy.asarray(points, dtype=float)
|
||||
vertices = numpy.asarray(vertices, dtype=float)
|
||||
|
||||
if points.size == 0:
|
||||
return numpy.zeros(0, dtype=numpy.int8)
|
||||
|
@ -42,7 +42,7 @@ classifiers = [
|
||||
requires-python = ">=3.11"
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"numpy~=1.21",
|
||||
"numpy>=1.26",
|
||||
"klamath~=1.2",
|
||||
]
|
||||
|
||||
@ -57,3 +57,36 @@ 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
|
||||
]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user