diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 9b4931c..21d6400 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -1,4 +1,4 @@ -from typing import Self, Sequence, Mapping +from typing import Self, Sequence, Mapping, Literal, overload, Final, cast import copy import logging @@ -102,12 +102,6 @@ class Builder(PortList): ) -> None: """ # TODO documentation for Builder() constructor - - # TODO MOVE THE BELOW DOCS to PortList - # If `ports` is `None`, two default ports ('A' and 'B') are created. - # Both are placed at (0, 0) and have default `ptype`, but 'A' has rotation 0 - # (attached devices will be placed to the left) and 'B' has rotation - # pi (attached devices will be placed to the right). """ self._dead = False self.library = library @@ -165,8 +159,7 @@ class Builder(PortList): Args: source: A collection of ports (e.g. Pattern, Builder, or dict) from which to create the interface. - library: Used for buildin functions; if not passed and the source - library: Library from which existing patterns should be referenced, + library: Library from which existing patterns should be referenced, TODO and to which new ones should be added. If not provided, the source's library will be used (if available). in_prefix: Prepended to port names for newly-created ports with @@ -230,18 +223,47 @@ class Builder(PortList): new = Builder(library=library, ports={**ports_in, **ports_out}, name=name) return new +# @overload +# def plug( +# self, +# other: Abstract | str, +# map_in: dict[str, str], +# map_out: dict[str, str | None] | None, +# *, +# mirrored: tuple[bool, bool], +# inherit_name: bool, +# set_rotation: bool | None, +# append: bool, +# ) -> Self: +# pass +# +# @overload +# def plug( +# self, +# other: Pattern, +# map_in: dict[str, str], +# map_out: dict[str, str | None] | None = None, +# *, +# mirrored: tuple[bool, bool] = (False, False), +# inherit_name: bool = True, +# set_rotation: bool | None = None, +# append: bool = False, +# ) -> Self: +# pass + def plug( self, - other: Abstract | str, + other: Abstract | str | Pattern, map_in: dict[str, str], map_out: dict[str, str | None] | None = None, *, mirrored: tuple[bool, bool] = (False, False), inherit_name: bool = True, set_rotation: bool | None = None, + append: bool = False, ) -> Self: """ - Instantiate a device `library[name]` into the current device, connecting + Instantiate or append a pattern into the current device, connecting the ports specified by `map_in` and renaming the unconnected ports specified by `map_out`. @@ -327,23 +349,59 @@ class Builder(PortList): del self.ports[ki] map_out[vi] = None - self.place(other, offset=translation, rotation=rotation, pivot=pivot, - mirrored=mirrored, port_map=map_out, skip_port_check=True) + if isinstance(other, Pattern): + assert append + self.place(other, offset=translation, rotation=rotation, pivot=pivot, + mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append) + else: + self.place(other, offset=translation, rotation=rotation, pivot=pivot, + mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append) return self + @overload def place( self, other: Abstract | str, *, + offset: ArrayLike, + rotation: float, + pivot: ArrayLike, + mirrored: tuple[bool, bool], + port_map: dict[str, str | None] | None, + skip_port_check: bool, + append: bool, + ) -> Self: + pass + + @overload + def place( + self, + other: Pattern, + *, + offset: ArrayLike, + rotation: float, + pivot: ArrayLike, + mirrored: tuple[bool, bool], + port_map: dict[str, str | None] | None, + skip_port_check: bool, + append: Literal[True], + ) -> Self: + pass + + def place( + self, + other: Abstract | str | Pattern, + *, offset: ArrayLike = (0, 0), rotation: float = 0, pivot: ArrayLike = (0, 0), mirrored: tuple[bool, bool] = (False, False), port_map: dict[str, str | None] | None = None, skip_port_check: bool = False, + append: bool = False, ) -> Self: """ - Instantiate the device `other` into the current device, adding its + Instantiate or append the device `other` into the current device, adding its ports to those of the current device (but not connecting any ports). Mirroring is applied before rotation; translation (`offset`) is applied last. @@ -375,7 +433,7 @@ class Builder(PortList): Raises: `PortError` if any ports specified in `map_in` or `map_out` do not - exist in `self.ports` or `library[name].ports`. + exist in `self.ports` or `other.ports`. `PortError` if there are any duplicate names after `map_in` and `map_out` are applied. """ @@ -408,10 +466,26 @@ class Builder(PortList): p.translate(offset) self.ports[name] = p - sp = Ref(other.name, mirrored=mirrored) - sp.rotate_around(pivot, rotation) - sp.translate(offset) - self.pattern.refs.append(sp) + if append: + if isinstance(other, Pattern): + other_pat = other + elif isinstance(other, Abstract): + assert self.library is not None + other_pat = self.library[other.name] + else: + other_pat = self.library[name] + other_copy = other_pat.deepcopy() + other_copy.ports.clear() + other_copy.mirror2d(mirrored) + other_copy.rotate_around(pivot, rotation) + other_copy.translate_elements(offset) + self.pattern.append(other_copy) + else: + assert not isinstance(other, Pattern) + ref = Ref(other.name, mirrored=mirrored) + ref.rotate_around(pivot, rotation) + ref.translate(offset) + self.pattern.refs.append(ref) return self def translate(self, offset: ArrayLike) -> Self: diff --git a/masque/builder/flatbuilder.py b/masque/builder/flatbuilder.py index b90769a..9511350 100644 --- a/masque/builder/flatbuilder.py +++ b/masque/builder/flatbuilder.py @@ -53,6 +53,7 @@ class FlatBuilder(PortList): """ # TODO documentation for FlatBuilder() constructor """ + self._dead = False if pattern is not None: self.pattern = pattern else: @@ -70,8 +71,6 @@ class FlatBuilder(PortList): else: self.tools = dict(tools) - self._dead = False - @classmethod def interface( cls, @@ -105,7 +104,6 @@ class FlatBuilder(PortList): Args: source: A collection of ports (e.g. Pattern, Builder, or dict) from which to create the interface. - library: Used for buildin functions; if not passed and the source TODO library: Library from which existing patterns should be referenced, TODO and to which new ones should be added. If not provided, the source's library will be used (if available). @@ -429,7 +427,7 @@ class FlatBuilder(PortList): ccw: SupportsBool | None, length: float, *, - tool_port_names: Sequence[str] = ('A', 'B'), + tool_port_names: tuple[str, str] = ('A', 'B'), base_name: str = '_path', **kwargs, ) -> Self: @@ -448,7 +446,7 @@ class FlatBuilder(PortList): ccw: SupportsBool | None, position: float, *, - tool_port_names: Sequence[str] = ('A', 'B'), + tool_port_names: tuple[str, str] = ('A', 'B'), base_name: str = '_pathto', **kwargs, ) -> Self: @@ -483,7 +481,7 @@ class FlatBuilder(PortList): *, spacing: float | ArrayLike | None = None, set_rotation: float | None = None, - tool_port_names: Sequence[str] = ('A', 'B'), + tool_port_names: tuple[str, str] = ('A', 'B'), force_container: bool = False, base_name: str = '_mpath', **kwargs, diff --git a/masque/builder/pather.py b/masque/builder/pather.py index b17d70f..5ad8dc4 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -218,7 +218,7 @@ class Pather(Builder): ccw: SupportsBool | None, length: float, *, - tool_port_names: Sequence[str] = ('A', 'B'), + tool_port_names: tuple[str, str] = ('A', 'B'), base_name: str = '_path', **kwargs, ) -> Self: @@ -239,7 +239,7 @@ class Pather(Builder): ccw: SupportsBool | None, position: float, *, - tool_port_names: Sequence[str] = ('A', 'B'), + tool_port_names: tuple[str, str] = ('A', 'B'), base_name: str = '_pathto', **kwargs, ) -> Self: @@ -274,7 +274,7 @@ class Pather(Builder): *, spacing: float | ArrayLike | None = None, set_rotation: float | None = None, - tool_port_names: Sequence[str] = ('A', 'B'), + tool_port_names: tuple[str, str] = ('A', 'B'), force_container: bool = False, base_name: str = '_mpath', **kwargs, diff --git a/masque/builder/tools.py b/masque/builder/tools.py index e3cdf22..545ff73 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -1,12 +1,18 @@ """ Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides) """ -from typing import TYPE_CHECKING, Sequence, Literal, Tuple +from typing import TYPE_CHECKING, Sequence, Literal, Callable +from abc import ABCMeta, abstractmethod -from ..utils import SupportsBool +import numpy + +from ..utils import SupportsBool, rotation_matrix_2d from ..ports import Port from ..pattern import Pattern -from ..library import ILibrary +from ..abstract import Abstract +from ..library import ILibrary, Library +from ..error import BuildError +from .builder import Builder render_step_t = ( @@ -22,7 +28,7 @@ class Tool: *, in_ptype: str | None = None, out_ptype: str | None = None, - port_names: Sequence[str] = ('A', 'B'), + port_names: tuple[str, str] = ('A', 'B'), **kwargs, ) -> Pattern: raise NotImplementedError(f'path() not implemented for {type(self)}') @@ -35,7 +41,7 @@ class Tool: in_ptype: str | None = None, out_ptype: str | None = None, **kwargs, - ) -> Tuple[float, str]: + ) -> tuple[float, str]: raise NotImplementedError(f'planL() not implemented for {type(self)}') def planS( @@ -62,3 +68,91 @@ class Tool: assert batch[0][-1] == self raise NotImplementedError(f'render() not implemented for {type(self)}') + +class BasicTool(Tool, metaclass=ABCMeta): + straight: tuple[Callable[[float], Pattern], str, str] + bend: tuple[Abstract, str, str] # Assumed to be clockwise + transitions: dict[str, tuple[Abstract, str, str]] + + def path( + self, + ccw: SupportsBool | None, + length: float, + *, + in_ptype: str | None = None, + out_ptype: str | None = None, + port_names: tuple[str, str] = ('A', 'B'), + **kwargs, + ) -> Pattern: + + # TODO check all the math for L-shaped bends + straight_length = length + bend_run = 0 + if ccw is not None: + bend, bport_in, bport_out = self.bend + brot = bend.ports[bport_in].rotation + assert brot is not None + bend_dxy = numpy.abs( + rotation_matrix_2d(-brot) @ ( + bend.ports[bport_out].offset + - bend.ports[bport_in].offset + ) + ) + + straight_length -= bend_dxy[0] + bend_run += bend_dxy[1] + else: + bend_dxy = numpy.zeros(2) + + in_transition = self.transitions.get('unk' if in_ptype is None else in_ptype, None) + if in_transition is not None: + ipat, iport_theirs, iport_ours = in_transition + irot = ipat.ports[iport_theirs].rotation + assert irot is not None + itrans_dxy = rotation_matrix_2d(-irot) @ ( + ipat.ports[iport_ours].offset + - ipat.ports[iport_theirs].offset + ) + + straight_length -= itrans_dxy[0] + bend_run += itrans_dxy[1] + else: + itrans_dxy = numpy.zeros(2) + + out_transition = self.transitions.get('unk' if out_ptype is None else out_ptype, None) + if out_transition is not None: + opat, oport_theirs, oport_ours = out_transition + orot = opat.ports[oport_ours].rotation + assert orot is not None + otrans_dxy = rotation_matrix_2d(-orot) @ ( + opat.ports[oport_theirs].offset + - opat.ports[oport_ours].offset + ) + if ccw: + otrans_dxy[0] *= -1 + + straight_length -= otrans_dxy[1] + bend_run += otrans_dxy[0] + else: + otrans_dxy = numpy.zeros(2) + + if straight_length < 0: + raise BuildError(f'Asked to draw path with total length {length:g}, shorter than required bends and tapers:\n' + f'bend: {bend_dxy[0]:g} in_taper: {abs(itrans_dxy[0])} out_taper: {otrans_dxy[1]}') + + gen_straight, sport_in, sport_out = self.straight + tree = Library() + bb = Builder(library=tree, name='_path').add_port_pair(names=port_names) + if in_transition: + bb.plug(ipat, {port_names[1]: iport_theirs}) + if not numpy.isclose(straight_length, 0): + straight = tree << {'_straight': gen_straight(straight_length)} + bb.plug(straight, {port_names[1]: sport_in}) + if ccw is not None: + bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw))) + if out_transition: + bb.plug(opat, {port_names[1]: oport_ours}) + + + return bb.pattern +