diff --git a/masque/builder/devices.py b/masque/builder/devices.py index 8d4875b..b3956bc 100644 --- a/masque/builder/devices.py +++ b/masque/builder/devices.py @@ -1,5 +1,5 @@ from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence -from typing import overload, KeysView, ValuesView +from typing import overload, KeysView, ValuesView, MutableMapping import copy import warnings import traceback @@ -46,7 +46,11 @@ class PortsRef(PortList): self.name = name self.ports = copy.deepcopy(ports) - def build(self, library: MutableLibrary) -> 'Builder': + def build( + self, + library: MutableLibrary, + tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None, + ) -> 'Builder': """ Begin building a new device around an instance of the current device (rather than modifying the current device). @@ -56,7 +60,7 @@ class PortsRef(PortList): """ pat = Pattern(ports=self.ports) pat.ref(self.name) - new = Builder(library=library, pattern=pat, tools=self.tools) # TODO should Ref have tools? + new = Builder(library=library, pattern=pat, tools=tools) # TODO should Ref have tools? return new # TODO do we want to store a Ref instead of just a name? then we can translate/rotate/mirror... @@ -148,7 +152,7 @@ class Builder(PortList): library: MutableLibrary, pattern: Optional[Pattern] = None, *, - tools: Union[None, Tool, Dict[Optional[str], Tool]] = None, + tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None, ) -> None: """ If `ports` is `None`, two default ports ('A' and 'B') are created. @@ -173,7 +177,7 @@ class Builder(PortList): elif isinstance(tools, Tool): self.tools = {None: tools} else: - self.tools = tools + self.tools = dict(tools) self._dead = False @@ -546,12 +550,12 @@ class Builder(PortList): port_name = tuple(portspec)[0] return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names) else: - bld = ports.as_interface(self.library, tools=self.tools) + bld = Pattern(ports=ports).as_interface(self.library, tools=self.tools) # TODO: maybe Builder static as_interface-like should optionally take ports instead? Maybe constructor could do it? for port_name, length in extensions.items(): bld.path(port_name, ccw, length, tool_port_names=tool_port_names) name = self.library.get_name(base_name) self.library._set(name, bld.pattern) - return self.plug(PortsRef(name, pat.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'? + return self.plug(PortsRef(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'? # TODO def path_join() and def bus_join()? diff --git a/masque/builder/port_utils.py b/masque/builder/port_utils.py index 1229b0e..3995de3 100644 --- a/masque/builder/port_utils.py +++ b/masque/builder/port_utils.py @@ -13,13 +13,13 @@ import numpy from ..pattern import Pattern from ..label import Label from ..utils import rotation_matrix_2d, layer_t -from .devices import Device, Port +from ..ports import Port logger = logging.getLogger(__name__) -def dev2pat(device: Device, layer: layer_t) -> Pattern: +def dev2pat(pattern: Pattern, layer: layer_t) -> Pattern: """ Place a text label at each port location, specifying the port data in the format 'name:ptype angle_deg' @@ -27,24 +27,24 @@ def dev2pat(device: Device, layer: layer_t) -> Pattern: This can be used to debug port locations or to automatically generate ports when reading in a GDS file. - NOTE that `device` is modified by this function, and `device.pattern` is returned. + NOTE that `pattern` is modified by this function Args: - device: The device which is to have its ports labeled. MODIFIED in-place. + pattern: The pattern which is to have its ports labeled. MODIFIED in-place. layer: The layer on which the labels will be placed. Returns: - `device.pattern` + `pattern` """ - for name, port in device.ports.items(): + for name, port in pattern.ports.items(): if port.rotation is None: angle_deg = numpy.inf else: angle_deg = numpy.rad2deg(port.rotation) - device.pattern.labels += [ + pattern.labels += [ Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset) ] - return device.pattern + return pattern def pat2dev( @@ -53,10 +53,10 @@ def pat2dev( library: Optional[Mapping[str, Pattern]] = None, max_depth: int = 999_999, skip_subcells: bool = True, - ) -> Device: + ) -> Pattern: """ Examine `pattern` for labels specifying port info, and use that info - to build a `Device` object. + to fill out its `ports` attribute. Labels are assumed to be placed at the port locations, and have the format 'name:ptype angle_deg' @@ -68,11 +68,12 @@ def pat2dev( Reduce this to 0 to avoid ever searching subcells. skip_subcells: If port labels are found at a given hierarcy level, do not continue searching at deeper levels. This allows subcells - to contain their own port info (and thus become their own Devices). + to contain their own port info without interfering with supercells' + port data. Default True. Returns: - The constructed Device object. Port labels are not removed from the pattern. + The updated `pattern`. Port labels are not removed. """ ports = {} # Note: could do a list here, if they're not unique annotated_cells = set() @@ -109,5 +110,13 @@ def pat2dev( return pat - pattern.dfs(visit_before=find_ports_each, transform=True) #TODO: don't check Library if there are ports in top level - return Device(pattern, ports) + # TODO TODO TODO + if skip_subcells and ports := find_ports_each(pattern, ...): + # TODO Could do this with just the `pattern` itself + pass + else + # TODO need `name` and `library` here + ports = library.dfs(name, visit_before=find_ports_each, transform=True) #TODO: don't check Library if there are ports in top level + pattern.check_ports(other_ports=ports) + pattern.ports.update(ports) + return pattern diff --git a/masque/builder/tools.py b/masque/builder/tools.py index bd93f0c..1239100 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -4,7 +4,7 @@ Tools are objects which dynamically generate simple single-use devices (e.g. wir from typing import TYPE_CHECKING, Optional, Sequence if TYPE_CHECKING: - from .devices import Device + from ..pattern import Pattern class Tool: @@ -17,6 +17,6 @@ class Tool: out_ptype: Optional[str] = None, port_names: Sequence[str] = ('A', 'B'), **kwargs, - ) -> 'Device': + ) -> 'Pattern': raise NotImplementedError(f'path() not implemented for {type(self)}') diff --git a/masque/builder/utils.py b/masque/builder/utils.py index 2a6507d..fff42e6 100644 --- a/masque/builder/utils.py +++ b/masque/builder/utils.py @@ -1,9 +1,10 @@ -from typing import Dict, Tuple, List, Optional, Union, Any, cast, Sequence, TYPE_CHECKING +from typing import Dict, Tuple, List, Mapping, Sequence, SupportsFloat +from typing import Optional, Union, Any, cast, TYPE_CHECKING from pprint import pformat import numpy from numpy import pi -from numpy.typing import ArrayLike +from numpy.typing import ArrayLike, NDArray from ..utils import rotation_matrix_2d from ..error import BuildError @@ -135,6 +136,7 @@ def ell( # D-----------| `d_to_align[3]` # d_to_align = x_start.max() - x_start # distance to travel to align all + offsets: NDArray[numpy.float64] if bound_type == 'min_past_furthest': # A------------------V `d_to_exit[0]` # B-----V `d_to_exit[1]` @@ -154,6 +156,7 @@ def ell( travel = d_to_align - (ch_offsets.max() - ch_offsets) offsets = travel - travel.min().clip(max=0) + rot_bound: SupportsFloat if bound_type in ('emin', 'min_extension', 'emax', 'max_extension', 'min_past_furthest',): diff --git a/masque/pattern.py b/masque/pattern.py index 51805a0..3d72122 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -13,7 +13,7 @@ from numpy import inf from numpy.typing import NDArray, ArrayLike # .visualize imports matplotlib and matplotlib.collections -from .refs import Ref +from .ref import Ref from .shapes import Shape, Polygon from .label import Label from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t @@ -56,7 +56,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): labels: Sequence[Label] = (), refs: Sequence[Ref] = (), annotations: Optional[annotations_t] = None, - ports: Optional[Mapping[str, Port]] = None + ports: Optional[Mapping[str, 'Port']] = None ) -> None: """ Basic init; arguments get assigned to member variables. @@ -130,8 +130,17 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): self.refs += other_pattern.refs self.shapes += other_pattern.shapes self.labels += other_pattern.labels - self.annotations += other_pattern.annotations - self.ports += other_pattern.ports + + annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys()) + if annotation_conflicts: + raise PatternError(f'Annotation keys overlap: {annotation_conflicts}') + self.annotations.update(other_pattern.annotations) + + port_conflicts = set(self.ports.keys()) & set(other_pattern.ports.keys()) + if port_conflicts: + raise PatternError(f'Port names overlap: {port_conflicts}') + self.ports.update(other_pattern.ports) + return self def subset( @@ -139,8 +148,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): shapes: Optional[Callable[[Shape], bool]] = None, labels: Optional[Callable[[Label], bool]] = None, refs: Optional[Callable[[Ref], bool]] = None, - annotations: Optional[Callable[[annotation_t], bool]] = None, - ports: Optional[Callable[[str], bool]] = None, + annotations: Optional[Callable[[str, List[Union[int, float, str]]], bool]] = None, + ports: Optional[Callable[[str, Port], bool]] = None, default_keep: bool = False ) -> 'Pattern': """ @@ -179,12 +188,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): pat.refs = copy.copy(self.refs) if annotations is not None: - pat.annotations = [s for s in self.annotations if annotations(s)] + pat.annotations = {k: v for k, v in self.annotations.items() if annotations(k, v)} elif default_keep: pat.annotations = copy.copy(self.annotations) if ports is not None: - pat.ports = {k: v for k, v in self.ports.items() if ports(k)} + pat.ports = {k: v for k, v in self.ports.items() if ports(k, v)} elif default_keep: pat.ports = copy.copy(self.ports) diff --git a/masque/ports.py b/masque/ports.py index fd9ffa3..0e0f377 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -1,5 +1,5 @@ -from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence -from typing import overload, KeysView, ValuesView, ItemsView +from typing import Dict, Iterable, List, Tuple, Iterator, Optional, Sequence, MutableMapping +from typing import overload, KeysView, ValuesView, ItemsView, TYPE_CHECKING, Union, TypeVar, Any import copy import warnings import traceback @@ -17,6 +17,9 @@ from .error import DeviceError from .library import MutableLibrary from .builder import Tool +if TYPE_CHECKING: + from .builder import Builder + logger = logging.getLogger(__name__) @@ -117,10 +120,10 @@ class PortList(metaclass=ABCMeta): pass @overload - def __getitem__(self, key: Union[List[str], Tuple[str, ...], KeysView[str], ValuesView[str]]) -> PortList: + def __getitem__(self, key: Union[List[str], Tuple[str, ...], KeysView[str], ValuesView[str]]) -> Dict[str, Port]: pass - def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, PortList]: + def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, Dict[str, Port]]: """ For convenience, ports can be read out using square brackets: - `pattern['A'] == Port((0, 0), 0)` @@ -137,7 +140,7 @@ class PortList(metaclass=ABCMeta): return {k: self.ports[k] for k in key} # TODO add Mapping stuff to PortsList - def keys(self) -> KeysView[Port]: + def keys(self) -> KeysView[str]: return self.ports.keys() def values(self) -> ValuesView[Port]: @@ -250,7 +253,7 @@ class PortList(metaclass=ABCMeta): self, library: MutableLibrary, *, - tools: Optional[Dict[str, Tool]] = None, + tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None, in_prefix: str = 'in_', out_prefix: str = '', port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None, @@ -296,6 +299,8 @@ class PortList(metaclass=ABCMeta): `DeviceError` if applying the prefixes results in duplicate port names. """ + from .pattern import Pattern + if port_map: if isinstance(port_map, dict): missing_inkeys = set(port_map.keys()) - set(self.ports.keys()) @@ -319,7 +324,7 @@ class PortList(metaclass=ABCMeta): if duplicates: raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') - new = Builder(library=library, ports={**ports_in, **ports_out}, tools=tools) + new = Builder(library=library, pattern=Pattern(ports={**ports_in, **ports_out}), tools=tools) return new def find_transform( diff --git a/masque/ref.py b/masque/ref.py index b8f60b0..3553d2c 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -162,13 +162,13 @@ class Ref( return pattern - def rotate(self: S, rotation: float) -> S: + def rotate(self: R, rotation: float) -> R: self.rotation += rotation if self.repetition is not None: self.repetition.rotate(rotation) return self - def mirror(self: S, axis: int) -> S: + def mirror(self: R, axis: int) -> R: self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 if self.repetition is not None: