Compare commits
No commits in common. "8d671ed7099dfadf9117cff0eba0cd850a664507" and "cda895a7d3c1b1254cbf224bb0825c5c8e5f2b4f" have entirely different histories.
8d671ed709
...
cda895a7d3
@ -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
|
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
|
||||||
)
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()?
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
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
|
ii = 0
|
||||||
|
suffixed_name = sanitized_name
|
||||||
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)
|
|
||||||
|
@ -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,6 +594,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
|
|
||||||
if (cbounds[1] < cbounds[0]).any():
|
if (cbounds[1] < cbounds[0]).any():
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
return cbounds
|
return cbounds
|
||||||
|
|
||||||
def get_bounds_nonempty(
|
def get_bounds_nonempty(
|
||||||
@ -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_',
|
||||||
|
@ -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()):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,6 +101,7 @@ 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')
|
||||||
|
else:
|
||||||
b_vector = numpy.array([0.0, 0.0])
|
b_vector = numpy.array([0.0, 0.0])
|
||||||
|
|
||||||
if a_count < 1:
|
if a_count < 1:
|
||||||
@ -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:
|
||||||
|
if not isinstance(val, numpy.ndarray):
|
||||||
val = numpy.array(val, dtype=float)
|
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,
|
||||||
|
@ -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
|
||||||
|
@ -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' a°{numpy.rad2deg(self.angles)}'
|
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||||
|
@ -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),),
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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' 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(
|
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' 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,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,
|
|
||||||
)
|
|
||||||
|
@ -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()
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
if not isinstance(val, numpy.ndarray) or val.dtype != numpy.float64:
|
||||||
val = numpy.array(val, dtype=float)
|
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
|
||||||
|
@ -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!')
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,6 +70,7 @@ def maxrects_bssf(
|
|||||||
if allow_rejects:
|
if allow_rejects:
|
||||||
rejected_inds.add(rect_ind)
|
rejected_inds.add(rect_ind)
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||||
|
|
||||||
# Read out location
|
# Read out location
|
||||||
@ -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,6 +161,7 @@ def guillotine_bssf_sas(
|
|||||||
if allow_rejects:
|
if allow_rejects:
|
||||||
rejected_inds.add(rect_ind)
|
rejected_inds.add(rect_ind)
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||||
|
|
||||||
# Read out location
|
# Read out location
|
||||||
@ -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]
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
]
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user