From d02ea400a02767ca8e74dfc2e1ec76d051b320df Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 7 Oct 2023 01:45:52 -0700 Subject: [PATCH] Move plug/place/interface to Pattern Since Pattern has ports already, these should live in Pattern and get wrapped elsewhere. Builder becomes a context-holder (holding .library and .dead) and some code duplication goes away. --- masque/builder/builder.py | 314 +++++++----------------------- masque/builder/pather.py | 33 ++-- masque/builder/renderpather.py | 188 ++++++++---------- masque/pattern.py | 343 ++++++++++++++++++++++++++++++++- 4 files changed, 512 insertions(+), 366 deletions(-) diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 273361b..d1f8d21 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -18,39 +18,44 @@ logger = logging.getLogger(__name__) class Builder(PortList): """ - TODO DOCUMENT Builder - A `Device` is a combination of a `Pattern` with a set of named `Port`s - which can be used to "snap" devices together to make complex layouts. + A `Builder` is a helper object used for snapping together multiple + lower-level patterns at their `Port`s. - `Device`s can be as simple as one or two ports (e.g. an electrical pad - or wire), but can also be used to build and represent a large routed - layout (e.g. a logical block with multiple I/O connections or even a - full chip). + The `Builder` mostly just holds context, in the form of a `Library`, + in addition to its underlying pattern. This simplifies some calls + to `plug` and `place`, by making the library implicit. - For convenience, ports can be read out using square brackets: - - `device['A'] == Port((0, 0), 0)` - - `device[['A', 'B']] == {'A': Port((0, 0), 0), 'B': Port((0, 0), pi)}` + `Builder` can also be `set_dead()`, at which point further calls to `plug()` + and `place()` are ignored (intended for debugging). - Examples: Creating a Device + + Examples: Creating a Builder =========================== - - `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing - pattern and defines some ports. + - `Builder(library, ports={'A': port_a, 'C': port_c}, name='mypat')` makes + an empty pattern, adds the given ports, and places it into `library` + under the name `'mypat'`. - - `Device(ports=None)` makes a new empty pattern with - default ports ('A' and 'B', in opposite directions, at (0, 0)). + - `Builder(library)` makes an empty pattern with no ports. The pattern + is not added into `library` and must later be added with e.g. + `library['mypat'] = builder.pattern` - - `my_device.build('my_layout')` makes a new pattern and instantiates - `my_device` in it with offset (0, 0) as a base for further building. + - `Builder(library, pattern=pattern, name='mypat')` uses an existing + pattern (including its ports) and sets `library['mypat'] = pattern`. - - `my_device.as_interface('my_component', port_map=['A', 'B'])` makes a new - (empty) pattern, copies over ports 'A' and 'B' from `my_device`, and - creates additional ports 'in_A' and 'in_B' facing in the opposite - directions. This can be used to build a device which can plug into - `my_device` (using the 'in_*' ports) but which does not itself include - `my_device` as a subcomponent. + - `Builder.interface(other_pat, port_map=['A', 'B'], library=library)` + makes a new (empty) pattern, copies over ports 'A' and 'B' from + `other_pat`, and creates additional ports 'in_A' and 'in_B' facing + in the opposite directions. This can be used to build a device which + can plug into `other_pat` (using the 'in_*' ports) but which does not + itself include `other_pat` as a subcomponent. - Examples: Adding to a Device - ============================ + - `Builder.interface(other_builder, ...)` does the same thing as + `Builder.interface(other_builder.pattern, ...)` but also uses + `other_builder.library` as its library by default. + + + Examples: Adding to a pattern + ============================= - `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})` instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B' of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports @@ -75,10 +80,9 @@ class Builder(PortList): pattern: Pattern """ Layout of this device """ - library: ILibrary | None + library: ILibrary """ - Library from which existing patterns should be referenced, and to which - new ones should be added + Library from which patterns should be referenced """ _dead: bool @@ -94,7 +98,7 @@ class Builder(PortList): def __init__( self, - library: ILibrary | None = None, + library: ILibrary, *, pattern: Pattern | None = None, ports: str | Mapping[str, Port] | None = None, @@ -114,15 +118,11 @@ class Builder(PortList): if self.pattern.ports: raise BuildError('Ports supplied for pattern with pre-existing ports!') if isinstance(ports, str): - if library is None: - raise BuildError('Ports given as a string, but `library` was `None`!') ports = library.abstract(ports).ports self.pattern.ports.update(copy.deepcopy(dict(ports))) if name is not None: - if library is None: - raise BuildError('Name was supplied, but no library was given!') library[name] = self.pattern @classmethod @@ -137,31 +137,15 @@ class Builder(PortList): name: str | None = None, ) -> 'Builder': """ - Begin building a new device based on all or some of the ports in the - source device. Do not include the source device; instead use it - to define ports (the "interface") for the new device. - - The ports specified by `port_map` (default: all ports) are copied to - new device, and additional (input) ports are created facing in the - opposite directions. The specified `in_prefix` and `out_prefix` are - prepended to the port names to differentiate them. - - By default, the flipped ports are given an 'in_' prefix and unflipped - ports keep their original names, enabling intuitive construction of - a device that will "plug into" the current device; the 'in_*' ports - are used for plugging the devices together while the original port - names are used for building the new device. - - Another use-case could be to build the new device using the 'in_' - ports, creating a new device which could be used in place of the - current device. + Wrapper for `Pattern.interface()`, which returns a Builder instead. Args: source: A collection of ports (e.g. Pattern, Builder, or dict) - from which to create the interface. - 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). + from which to create the interface. May be a pattern name if + `library` is provided. + library: Library from which existing patterns should be referenced, + and to which the new one should be added (if named). If not provided, + `source.library` must exist and will be used. in_prefix: Prepended to port names for newly-created ports with reversed directions compared to the current device. out_prefix: Prepended to port names for ports which are directly @@ -185,72 +169,16 @@ class Builder(PortList): if library is None: if hasattr(source, 'library') and isinstance(source.library, ILibrary): library = source.library + else: + raise BuildError('No library was given, and `source.library` does not have one either.') if isinstance(source, str): - if library is None: - raise BuildError('Source given as a string, but `library` was `None`!') - orig_ports = library.abstract(source).ports - elif isinstance(source, PortList): - orig_ports = source.ports - elif isinstance(source, dict): - orig_ports = source - else: - raise BuildError(f'Unable to get ports from {type(source)}: {source}') + source = library.abstract(source).ports - if port_map: - if isinstance(port_map, dict): - missing_inkeys = set(port_map.keys()) - set(orig_ports.keys()) - mapped_ports = {port_map[k]: v for k, v in orig_ports.items() if k in port_map} - else: - port_set = set(port_map) - missing_inkeys = port_set - set(orig_ports.keys()) - mapped_ports = {k: v for k, v in orig_ports.items() if k in port_set} - - if missing_inkeys: - raise PortError(f'`port_map` keys not present in source: {missing_inkeys}') - else: - mapped_ports = orig_ports - - ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi) - for name, port in mapped_ports.items()} - ports_out = {f'{out_prefix}{name}': port.deepcopy() - for name, port in mapped_ports.items()} - - duplicates = set(ports_out.keys()) & set(ports_in.keys()) - if duplicates: - raise PortError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') - - new = Builder(library=library, ports={**ports_in, **ports_out}, name=name) + pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map) + new = Builder(library=library, pattern=pat, name=name) return new -# @overload -# def plug( -# self, -# other: Abstract | str, -# map_in: dict[str, str], -# map_out: dict[str, str | None] | None, -# *, -# mirrored: bool = False, -# 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: bool = False, -# inherit_name: bool = True, -# set_rotation: bool | None = None, -# append: bool = False, -# ) -> Self: -# pass - def plug( self, other: Abstract | str | Pattern, @@ -263,34 +191,18 @@ class Builder(PortList): append: bool = False, ) -> Self: """ - 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`. - - Examples: - ========= - - `my_device.plug(lib, 'subdevice', {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})` - instantiates `lib['subdevice']` into `my_device`, plugging ports 'A' and 'B' - of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports - are removed and any unconnected ports from `subdevice` are added to - `my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'. - - - `my_device.plug(lib, 'wire', {'myport': 'A'})` places port 'A' of `lib['wire']` - at 'myport' of `my_device`. - If `'wire'` has only two ports (e.g. 'A' and 'B'), no `map_out` argument is - provided, and the `inherit_name` argument is not explicitly set to `False`, - the unconnected port of `wire` is automatically renamed to 'myport'. This - allows easy extension of existing ports without changing their names or - having to provide `map_out` each time `plug` is called. + Wrapper around `Pattern.plug` which allows a string for `other`. + The `Builder`'s library is used to dereference the string (or `Abstract`, if + one is passed with `append=True`). Args: - other: An `Abstract` describing the device to be instatiated. + other: An `Abstract`, string, or `Pattern` describing the device to be instatiated. map_in: dict of `{'self_port': 'other_port'}` mappings, specifying port connections between the two devices. map_out: dict of `{'old_name': 'new_name'}` mappings, specifying new names for ports in `other`. - mirrored: Enables mirroring `other` across the x or y axes prior - to connecting any ports. + mirrored: Enables mirroring `other` across the x axis prior to + connecting any ports. inherit_name: If `True`, and `map_in` specifies only a single port, and `map_out` is `None`, and `other` has only two ports total, then automatically renames the output port of `other` to the @@ -303,6 +215,9 @@ class Builder(PortList): port with `rotation=None`), `set_rotation` must be provided to indicate how much `other` should be rotated. Otherwise, `set_rotation` must remain `None`. + append: If `True`, `other` is appended instead of being referenced. + Note that this does not flatten `other`, so its refs will still + be refs (now inside `self`). Returns: self @@ -320,72 +235,21 @@ class Builder(PortList): return self if isinstance(other, str): - if self.library is None: - raise BuildError('No library available, but `other` was a string!') other = self.library.abstract(other) + if append and isinstance(other, Abstract): + other = self.library[other.name] - # If asked to inherit a name, check that all conditions are met - if (inherit_name - and not map_out - and len(map_in) == 1 - and len(other.ports) == 2): - out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values()))) - map_out = {out_port_name: next(iter(map_in.keys()))} - - if map_out is None: - map_out = {} - map_out = copy.deepcopy(map_out) - - self.check_ports(other.ports.keys(), map_in, map_out) - translation, rotation, pivot = self.find_transform( - other, - map_in, + self.pattern.plug( + other=other, + map_in=map_in, + map_out=map_out, mirrored=mirrored, + inherit_name=inherit_name, set_rotation=set_rotation, + append=append, ) - - # get rid of plugged ports - for ki, vi in map_in.items(): - del self.ports[ki] - map_out[vi] = None - - 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) return self -# @overload -# def place( -# self, -# other: Abstract | str, -# *, -# offset: ArrayLike, -# rotation: float, -# pivot: ArrayLike, -# mirrored: bool = False, -# 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: bool = False, -# port_map: dict[str, str | None] | None, -# skip_port_check: bool, -# append: Literal[True], -# ) -> Self: -# pass - def place( self, other: Abstract | str | Pattern, @@ -440,52 +304,20 @@ class Builder(PortList): return self if isinstance(other, str): - if self.library is None: - raise BuildError('No library available, but `other` was a string!') other = self.library.abstract(other) + if append and isinstance(other, Abstract): + other = self.library[other.name] - if port_map is None: - port_map = {} - - if not skip_port_check: - self.check_ports(other.ports.keys(), map_in=None, map_out=port_map) - - ports = {} - for name, port in other.ports.items(): - new_name = port_map.get(name, name) - if new_name is None: - continue - ports[new_name] = port - - for name, port in ports.items(): - p = port.deepcopy() - if mirrored: - p.mirror() - p.rotate_around(pivot, rotation) - p.translate(offset) - self.ports[name] = p - - 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() - if mirrored: - other_copy.mirror() - other_copy.rotate_around(pivot, rotation) - other_copy.translate_elements(offset) - self.pattern.append(other_copy) - else: - assert not isinstance(other, Pattern) - ref = Ref(mirrored=mirrored) - ref.rotate_around(pivot, rotation) - ref.translate(offset) - self.pattern.refs[other.name].append(ref) + self.pattern.place( + other=other, + offset=offset, + rotation=rotation, + pivot=pivot, + mirrored=mirrored, + port_map=port_map, + skip_port_check=skip_port_check, + append=append, + ) return self def translate(self, offset: ArrayLike) -> Self: diff --git a/masque/builder/pather.py b/masque/builder/pather.py index 53b306e..84595ea 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -149,14 +149,19 @@ class Pather(Builder): cls, builder: Builder, *, - library: ILibrary | None = None, tools: Tool | MutableMapping[str | None, Tool] | None = None, ) -> 'Pather': - """TODO from_builder docs""" - library = library if library is not None else builder.library - if library is None: - raise BuildError('No library available for Pather!') - new = Pather(library=library, tools=tools, pattern=builder.pattern) + """ + Construct a `Pather` by adding tools to a `Builder`. + + Args: + builder: Builder to turn into a Pather + tools: Tools for the `Pather` + + Returns: + A new Pather object, using `builder.library` and `builder.pattern`. + """ + new = Pather(library=builder.library, tools=tools, pattern=builder.pattern) return new @classmethod @@ -183,17 +188,11 @@ class Pather(Builder): if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict): tools = source.tools - new = Pather.from_builder( - Builder.interface( - source=source, - library=library, - in_prefix=in_prefix, - out_prefix=out_prefix, - port_map=port_map, - name=name, - ), - tools=tools, - ) + if isinstance(source, str): + source = library.abstract(source).ports + + pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map) + new = Pather(library=library, pattern=pat, name=name, tools=tools) return new def __repr__(self) -> str: diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 6f3a40f..5ef7911 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -9,7 +9,7 @@ from numpy.typing import ArrayLike from ..pattern import Pattern from ..ref import Ref -from ..library import ILibrary +from ..library import ILibrary, Library from ..error import PortError, BuildError from ..ports import PortList, Port from ..abstract import Abstract @@ -28,7 +28,7 @@ class RenderPather(PortList): pattern: Pattern """ Layout of this device """ - library: ILibrary | None + library: ILibrary """ Library from which patterns should be referenced """ _dead: bool @@ -52,7 +52,7 @@ class RenderPather(PortList): def __init__( self, - library: ILibrary | None = None, + library: ILibrary, *, pattern: Pattern | None = None, ports: str | Mapping[str, Port] | None = None, @@ -99,6 +99,7 @@ class RenderPather(PortList): source: PortList | Mapping[str, Port] | str, *, library: ILibrary | None = None, + tools: Tool | MutableMapping[str | None, Tool] | None = None, in_prefix: str = 'in_', out_prefix: str = '', port_map: dict[str, str] | Sequence[str] | None = None, @@ -154,42 +155,17 @@ class RenderPather(PortList): if library is None: if hasattr(source, 'library') and isinstance(source.library, ILibrary): library = source.library + else: + raise BuildError('No library provided (and not present in `source.library`') + + if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict): + tools = source.tools if isinstance(source, str): - if library is None: - raise BuildError('Source given as a string, but `library` was `None`!') - orig_ports = library.abstract(source).ports - elif isinstance(source, PortList): - orig_ports = source.ports - elif isinstance(source, dict): - orig_ports = source - else: - raise BuildError(f'Unable to get ports from {type(source)}: {source}') + source = library.abstract(source).ports - if port_map: - if isinstance(port_map, dict): - missing_inkeys = set(port_map.keys()) - set(orig_ports.keys()) - mapped_ports = {port_map[k]: v for k, v in orig_ports.items() if k in port_map} - else: - port_set = set(port_map) - missing_inkeys = port_set - set(orig_ports.keys()) - mapped_ports = {k: v for k, v in orig_ports.items() if k in port_set} - - if missing_inkeys: - raise PortError(f'`port_map` keys not present in source: {missing_inkeys}') - else: - mapped_ports = orig_ports - - ports_in = {f'{in_prefix}{pname}': port.deepcopy().rotate(pi) - for pname, port in mapped_ports.items()} - ports_out = {f'{out_prefix}{pname}': port.deepcopy() - for pname, port in mapped_ports.items()} - - duplicates = set(ports_out.keys()) & set(ports_in.keys()) - if duplicates: - raise PortError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') - - new = RenderPather(library=library, ports={**ports_in, **ports_out}, name=name) + pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map) + new = RenderPather(library=library, pattern=pat, name=name, tools=tools) return new def plug( @@ -201,44 +177,41 @@ class RenderPather(PortList): mirrored: bool = False, inherit_name: bool = True, set_rotation: bool | None = None, + append: bool = False, ) -> Self: if self._dead: logger.error('Skipping plug() since device is dead') return self + other_tgt: Pattern | Abstract if isinstance(other, str): - if self.library is None: - raise BuildError('No library available, but `other` was a string!') - other = self.library.abstract(other) - - # If asked to inherit a name, check that all conditions are met - if (inherit_name - and not map_out - and len(map_in) == 1 - and len(other.ports) == 2): - out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values()))) - map_out = {out_port_name: next(iter(map_in.keys()))} - - if map_out is None: - map_out = {} - map_out = copy.deepcopy(map_out) - - self.check_ports(other.ports.keys(), map_in, map_out) - translation, rotation, pivot = self.find_transform( - other, - map_in, - mirrored=mirrored, - set_rotation=set_rotation, - ) + other_tgt = self.library.abstract(other) + if append and isinstance(other, Abstract): + other_tgt = self.library[other.name] # get rid of plugged ports - for ki, vi in map_in.items(): - if ki in self.paths: - self.paths[ki].append(RenderStep('P', None, self.ports[ki].copy(), self.ports[ki].copy(), None)) - 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) + for kk in map_in.keys(): + if kk in self.paths: + self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None)) + + plugged = map_in.values() + for name, port in other_tgt.ports.items(): + if name in plugged: + continue + new_name = map_out.get(name, name) if map_out is not None else name + if new_name is not None and new_name in self.paths: + self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None)) + + self.pattern.plug( + other=other_tgt, + map_in=map_in, + map_out=map_out, + mirrored=mirrored, + inherit_name=inherit_name, + set_rotation=set_rotation, + append=append, + ) + return self def place( @@ -251,43 +224,34 @@ class RenderPather(PortList): mirrored: bool = False, port_map: dict[str, str | None] | None = None, skip_port_check: bool = False, + append: bool = False, ) -> Self: if self._dead: logger.error('Skipping place() since device is dead') return self + other_tgt: Pattern | Abstract if isinstance(other, str): - if self.library is None: - raise BuildError('No library available, but `other` was a string!') - other = self.library.abstract(other) + other_tgt = self.library.abstract(other) + if append and isinstance(other, Abstract): + other_tgt = self.library[other.name] - if port_map is None: - port_map = {} - - if not skip_port_check: - self.check_ports(other.ports.keys(), map_in=None, map_out=port_map) - - ports = {} - for name, port in other.ports.items(): - new_name = port_map.get(name, name) - if new_name is None: - continue - ports[new_name] = port - if new_name in self.paths: + for name, port in other_tgt.ports.items(): + new_name = port_map.get(name, name) if port_map is not None else name + if new_name is not None and new_name in self.paths: self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None)) - for name, port in ports.items(): - p = port.deepcopy() - if mirrored: - p.mirror() - p.rotate_around(pivot, rotation) - p.translate(offset) - self.ports[name] = p + self.pattern.place( + other=other_tgt, + offset=offset, + rotation=rotation, + pivot=pivot, + mirrored=mirrored, + port_map=port_map, + skip_port_check=skip_port_check, + append=append, + ) - ref = Ref(mirrored=mirrored) - ref.rotate_around(pivot, rotation) - ref.translate(offset) - self.pattern.refs[other.name].append(ref) return self def retool( @@ -409,22 +373,32 @@ class RenderPather(PortList): def render( self, - lib: ILibrary | None = None, append: bool = True, ) -> Self: - lib = lib if lib is not None else self.library - assert lib is not None + """ + Generate the geometry which has been planned out with `path`/`path_to`/etc. + Args: + append: If `True`, the rendered geometry will be directly appended to + `self.pattern`. Note that it will not be flattened, so if only one + layer of hierarchy is eliminated. + + Returns: + self + """ + lib = self.library tool_port_names = ('A', 'B') - bb = Builder(lib) + pat = Pattern() - def render_batch(lib: ILibrary, portspec: str, batch: list[RenderStep], append: bool) -> None: + def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None: assert batch[0].tool is not None name = lib << batch[0].tool.render(batch, port_names=tool_port_names) - bb.ports[portspec] = batch[0].start_port.copy() - bb.plug(name, {portspec: tool_port_names[0]}, append=append) + pat.ports[portspec] = batch[0].start_port.copy() if append: - del lib[name] + pat.plug(lib[name], {portspec: tool_port_names[0]}, append=append) + del lib[name] # NOTE if the rendered pattern has refs, those are now in `pat` but not flattened + else: + pat.plug(lib.abstract(name), {portspec: tool_port_names[0]}, append=append) for portspec, steps in self.paths.items(): batch: list[RenderStep] = [] @@ -434,7 +408,7 @@ class RenderPather(PortList): # If we can't continue a batch, render it if batch and (not appendable_op or not same_tool): - render_batch(lib, portspec, batch, append) + render_batch(portspec, batch, append) batch = [] # batch is emptied already if we couldn't continue it @@ -442,16 +416,16 @@ class RenderPather(PortList): batch.append(step) # Opcodes which break the batch go below this line - if not appendable_op and portspec in bb.ports: - del bb.ports[portspec] + if not appendable_op and portspec in pat.ports: + del pat.ports[portspec] #If the last batch didn't end yet if batch: - render_batch(lib, portspec, batch, append) + render_batch(portspec, batch, append) self.paths.clear() - bb.ports.clear() - self.pattern.append(bb.pattern) + pat.ports.clear() + self.pattern.append(pat) return self diff --git a/masque/pattern.py b/masque/pattern.py index 4b85a3a..325272d 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -14,10 +14,11 @@ from numpy.typing import NDArray, ArrayLike # .visualize imports matplotlib and matplotlib.collections from .ref import Ref +from .abstract import Abstract from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES from .label import Label from .utils import rotation_matrix_2d, annotations_t, layer_t -from .error import PatternError +from .error import PatternError, PortError from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded from .ports import Port, PortList @@ -860,6 +861,346 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): pyplot.ylabel('y') pyplot.show() +# @overload +# def place( +# self, +# other: Pattern, +# *, +# offset: ArrayLike, +# rotation: float, +# pivot: ArrayLike, +# mirrored: bool, +# port_map: dict[str, str | None] | None, +# skip_port_check: bool, +# append: bool, +# ) -> Self: +# pass +# +# @overload +# def place( +# self, +# other: Abstract, +# *, +# offset: ArrayLike, +# rotation: float, +# pivot: ArrayLike, +# mirrored: bool, +# port_map: dict[str, str | None] | None, +# skip_port_check: bool, +# append: Literal[False], +# ) -> Self: +# pass + + def place( + self, + other: Abstract | Pattern, + *, + offset: ArrayLike = (0, 0), + rotation: float = 0, + pivot: ArrayLike = (0, 0), + mirrored: bool = False, + port_map: dict[str, str | None] | None = None, + skip_port_check: bool = False, + append: bool = False, + ) -> Self: + """ + Instantiate or append the pattern `other` into the current pattern, adding its + ports to those of the current pattern (but not connecting/removing any ports). + + Mirroring is applied before rotation; translation (`offset`) is applied last. + + Examples: + ========= + - `my_pat.place(pad_pat, offset=(10, 10), rotation=pi / 2, port_map={'A': 'gnd'})` + instantiates `pad` at the specified (x, y) offset and with the specified + rotation, adding its ports to those of `my_pat`. Port 'A' of `pad` is + renamed to 'gnd' so that further routing can use this signal or net name + rather than the port name on the original `pad_pat` pattern. + + Args: + other: An `Abstract` or `Pattern` describing the device to be instatiated. + offset: Offset at which to place the instance. Default (0, 0). + rotation: Rotation applied to the instance before placement. Default 0. + pivot: Rotation is applied around this pivot point (default (0, 0)). + Rotation is applied prior to translation (`offset`). + mirrored: Whether theinstance should be mirrored across the x axis. + Mirroring is applied before translation and rotation. + port_map: dict of `{'old_name': 'new_name'}` mappings, specifying + new names for ports in the instantiated pattern. New names can be + `None`, which will delete those ports. + skip_port_check: Can be used to skip the internal call to `check_ports`, + in case it has already been performed elsewhere. + append: If `True`, `other` is appended instead of being referenced. + Note that this does not flatten `other`, so its refs will still + be refs (now inside `self`). + + Returns: + self + + Raises: + `PortError` if any ports specified in `map_in` or `map_out` do not + exist in `self.ports` or `other.ports`. + `PortError` if there are any duplicate names after `map_in` and `map_out` + are applied. + """ + if port_map is None: + port_map = {} + + if not skip_port_check: + self.check_ports(other.ports.keys(), map_in=None, map_out=port_map) + + ports = {} + for name, port in other.ports.items(): + new_name = port_map.get(name, name) + if new_name is None: + continue + ports[new_name] = port + + for name, port in ports.items(): + p = port.deepcopy() + if mirrored: + p.mirror() + p.rotate_around(pivot, rotation) + p.translate(offset) + self.ports[name] = p + + if append: + if isinstance(other, Abstract): + raise PatternError('Must provide a full `Pattern` (not an `Abstract`) when appending!') + other_copy = other.deepcopy() + other_copy.ports.clear() + if mirrored: + other_copy.mirror() + other_copy.rotate_around(pivot, rotation) + other_copy.translate_elements(offset) + self.append(other_copy) + else: + assert not isinstance(other, Pattern) + ref = Ref(mirrored=mirrored) + ref.rotate_around(pivot, rotation) + ref.translate(offset) + self.refs[other.name].append(ref) + return self + +# @overload +# def plug( +# self, +# other: Abstract, +# map_in: dict[str, str], +# map_out: dict[str, str | None] | None, +# *, +# mirrored: bool, +# inherit_name: bool, +# set_rotation: bool | None, +# append: Literal[False], +# ) -> Self: +# pass +# +# @overload +# def plug( +# self, +# other: Pattern, +# map_in: dict[str, str], +# map_out: dict[str, str | None] | None, +# *, +# mirrored: bool, +# inherit_name: bool, +# set_rotation: bool | None, +# append: bool, +# ) -> Self: +# pass + + def plug( + self, + other: Abstract | Pattern, + map_in: dict[str, str], + map_out: dict[str, str | None] | None = None, + *, + mirrored: bool = False, + inherit_name: bool = True, + set_rotation: bool | None = None, + append: bool = False, + ) -> Self: + """ + Instantiate or append a pattern into the current pattern, connecting + the ports specified by `map_in` and renaming the unconnected + ports specified by `map_out`. + + Examples: + ========= + - `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})` + instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B' + of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports + are removed and any unconnected ports from `subdevice` are added to + `my_pat`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'. + + - `my_pat.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport' + of `my_pat`. + If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out` argument is + provided, and the `inherit_name` argument is not explicitly set to `False`, + the unconnected port of `wire` is automatically renamed to 'myport'. This + allows easy extension of existing ports without changing their names or + having to provide `map_out` each time `plug` is called. + + Args: + other: A `Pattern` or `Abstract` describing the subdevice to be instatiated. + map_in: dict of `{'self_port': 'other_port'}` mappings, specifying + port connections between the current pattern and the subdevice. + map_out: dict of `{'old_name': 'new_name'}` mappings, specifying + new names for ports in `other`. + mirrored: Enables mirroring `other` across the x axis prior to connecting + any ports. + inherit_name: If `True`, and `map_in` specifies only a single port, + and `map_out` is `None`, and `other` has only two ports total, + then automatically renames the output port of `other` to the + name of the port from `self` that appears in `map_in`. This + makes it easy to extend a pattern with simple 2-port devices + (e.g. wires) without providing `map_out` each time `plug` is + called. See "Examples" above for more info. Default `True`. + set_rotation: If the necessary rotation cannot be determined from + the ports being connected (i.e. all pairs have at least one + port with `rotation=None`), `set_rotation` must be provided + to indicate how much `other` should be rotated. Otherwise, + `set_rotation` must remain `None`. + append: If `True`, `other` is appended instead of being referenced. + Note that this does not flatten `other`, so its refs will still + be refs (now inside `self`). + + Returns: + self + + Raises: + `PortError` if any ports specified in `map_in` or `map_out` do not + exist in `self.ports` or `other_names`. + `PortError` if there are any duplicate names after `map_in` and `map_out` + are applied. + `PortError` if the specified port mapping is not achieveable (the ports + do not line up) + """ + # If asked to inherit a name, check that all conditions are met + if (inherit_name + and not map_out + and len(map_in) == 1 + and len(other.ports) == 2): + out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values()))) + map_out = {out_port_name: next(iter(map_in.keys()))} + + if map_out is None: + map_out = {} + map_out = copy.deepcopy(map_out) + + self.check_ports(other.ports.keys(), map_in, map_out) + translation, rotation, pivot = self.find_transform( + other, + map_in, + mirrored=mirrored, + set_rotation=set_rotation, + ) + + # get rid of plugged ports + for ki, vi in map_in.items(): + del self.ports[ki] + map_out[vi] = None + + 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, + ) + return self + + @classmethod + def interface( + cls, + source: PortList | Mapping[str, Port], + *, + in_prefix: str = 'in_', + out_prefix: str = '', + port_map: dict[str, str] | Sequence[str] | None = None, + ) -> 'Pattern': + """ + Generate an empty pattern with ports based on all or some of the ports + in the `source`. Do not include the source device istelf; instead use + it to define ports (the "interface") for the new device. + + The ports specified by `port_map` (default: all ports) are copied to + new device, and additional (input) ports are created facing in the + opposite directions. The specified `in_prefix` and `out_prefix` are + prepended to the port names to differentiate them. + + By default, the flipped ports are given an 'in_' prefix and unflipped + ports keep their original names, enabling intuitive construction of + a device that will "plug into" the current device; the 'in_*' ports + are used for plugging the devices together while the original port + names are used for building the new device. + + Another use-case could be to build the new device using the 'in_' + ports, creating a new device which could be used in place of the + current device. + + Args: + source: A collection of ports (e.g. Pattern, Builder, or dict) + from which to create the interface. + in_prefix: Prepended to port names for newly-created ports with + reversed directions compared to the current device. + out_prefix: Prepended to port names for ports which are directly + copied from the current device. + port_map: Specification for ports to copy into the new device: + - If `None`, all ports are copied. + - If a sequence, only the listed ports are copied + - If a mapping, the listed ports (keys) are copied and + renamed (to the values). + + Returns: + The new empty pattern, with 2x as many ports as listed in port_map. + + Raises: + `PortError` if `port_map` contains port names not present in the + current device. + `PortError` if applying the prefixes results in duplicate port + names. + """ + if isinstance(source, PortList): + orig_ports = source.ports + elif isinstance(source, dict): + orig_ports = source + else: + raise PatternError(f'Unable to get ports from {type(source)}: {source}') + + if port_map: + if isinstance(port_map, dict): + missing_inkeys = set(port_map.keys()) - set(orig_ports.keys()) + mapped_ports = {port_map[k]: v for k, v in orig_ports.items() if k in port_map} + else: + port_set = set(port_map) + missing_inkeys = port_set - set(orig_ports.keys()) + mapped_ports = {k: v for k, v in orig_ports.items() if k in port_set} + + if missing_inkeys: + raise PortError(f'`port_map` keys not present in source: {missing_inkeys}') + else: + mapped_ports = orig_ports + + ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi) + for name, port in mapped_ports.items()} + ports_out = {f'{out_prefix}{name}': port.deepcopy() + for name, port in mapped_ports.items()} + + duplicates = set(ports_out.keys()) & set(ports_in.keys()) + if duplicates: + raise PortError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') + + new = Pattern(ports={**ports_in, **ports_out}) + return new + TT = TypeVar('TT')