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.
This commit is contained in:
Jan Petykiewicz 2023-10-07 01:45:52 -07:00
parent 4af9493840
commit f6bfd3b638
4 changed files with 512 additions and 366 deletions

View File

@ -18,39 +18,44 @@ logger = logging.getLogger(__name__)
class Builder(PortList): class Builder(PortList):
""" """
TODO DOCUMENT Builder A `Builder` is a helper object used for snapping together multiple
A `Device` is a combination of a `Pattern` with a set of named `Port`s lower-level patterns at their `Port`s.
which can be used to "snap" devices together to make complex layouts.
`Device`s can be as simple as one or two ports (e.g. an electrical pad The `Builder` mostly just holds context, in the form of a `Library`,
or wire), but can also be used to build and represent a large routed in addition to its underlying pattern. This simplifies some calls
layout (e.g. a logical block with multiple I/O connections or even a to `plug` and `place`, by making the library implicit.
full chip).
For convenience, ports can be read out using square brackets: `Builder` can also be `set_dead()`, at which point further calls to `plug()`
- `device['A'] == Port((0, 0), 0)` and `place()` are ignored (intended for debugging).
- `device[['A', 'B']] == {'A': Port((0, 0), 0), 'B': Port((0, 0), pi)}`
Examples: Creating a Device
Examples: Creating a Builder
=========================== ===========================
- `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing - `Builder(library, ports={'A': port_a, 'C': port_c}, name='mypat')` makes
pattern and defines some ports. 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 - `Builder(library)` makes an empty pattern with no ports. The pattern
default ports ('A' and 'B', in opposite directions, at (0, 0)). 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 - `Builder(library, pattern=pattern, name='mypat')` uses an existing
`my_device` in it with offset (0, 0) as a base for further building. pattern (including its ports) and sets `library['mypat'] = pattern`.
- `my_device.as_interface('my_component', port_map=['A', 'B'])` makes a new - `Builder.interface(other_pat, port_map=['A', 'B'], library=library)`
(empty) pattern, copies over ports 'A' and 'B' from `my_device`, and makes a new (empty) pattern, copies over ports 'A' and 'B' from
creates additional ports 'in_A' and 'in_B' facing in the opposite `other_pat`, and creates additional ports 'in_A' and 'in_B' facing
directions. This can be used to build a device which can plug into in the opposite directions. This can be used to build a device which
`my_device` (using the 'in_*' ports) but which does not itself include can plug into `other_pat` (using the 'in_*' ports) but which does not
`my_device` as a subcomponent. 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'})` - `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B' instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B'
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
@ -75,10 +80,9 @@ class Builder(PortList):
pattern: Pattern pattern: Pattern
""" Layout of this device """ """ Layout of this device """
library: ILibrary | None library: ILibrary
""" """
Library from which existing patterns should be referenced, and to which Library from which patterns should be referenced
new ones should be added
""" """
_dead: bool _dead: bool
@ -94,7 +98,7 @@ class Builder(PortList):
def __init__( def __init__(
self, self,
library: ILibrary | None = None, library: ILibrary,
*, *,
pattern: Pattern | None = None, pattern: Pattern | None = None,
ports: str | Mapping[str, Port] | None = None, ports: str | Mapping[str, Port] | None = None,
@ -114,15 +118,11 @@ class Builder(PortList):
if self.pattern.ports: if self.pattern.ports:
raise BuildError('Ports supplied for pattern with pre-existing ports!') raise BuildError('Ports supplied for pattern with pre-existing ports!')
if isinstance(ports, str): if isinstance(ports, str):
if library is None:
raise BuildError('Ports given as a string, but `library` was `None`!')
ports = library.abstract(ports).ports ports = library.abstract(ports).ports
self.pattern.ports.update(copy.deepcopy(dict(ports))) self.pattern.ports.update(copy.deepcopy(dict(ports)))
if name is not None: if name is not None:
if library is None:
raise BuildError('Name was supplied, but no library was given!')
library[name] = self.pattern library[name] = self.pattern
@classmethod @classmethod
@ -137,31 +137,15 @@ class Builder(PortList):
name: str | None = None, name: str | None = None,
) -> 'Builder': ) -> 'Builder':
""" """
Begin building a new device based on all or some of the ports in the Wrapper for `Pattern.interface()`, which returns a Builder instead.
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.
Args: Args:
source: A collection of ports (e.g. Pattern, Builder, or dict) source: A collection of ports (e.g. Pattern, Builder, or dict)
from which to create the interface. from which to create the interface. May be a pattern name if
library: Library from which existing patterns should be referenced, TODO `library` is provided.
and to which new ones should be added. If not provided, library: Library from which existing patterns should be referenced,
the source's library will be used (if available). 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 in_prefix: Prepended to port names for newly-created ports with
reversed directions compared to the current device. reversed directions compared to the current device.
out_prefix: Prepended to port names for ports which are directly out_prefix: Prepended to port names for ports which are directly
@ -185,72 +169,16 @@ class Builder(PortList):
if library is None: if library is None:
if hasattr(source, 'library') and isinstance(source.library, ILibrary): if hasattr(source, 'library') and isinstance(source.library, ILibrary):
library = source.library library = source.library
else:
raise BuildError('No library was given, and `source.library` does not have one either.')
if isinstance(source, str): if isinstance(source, str):
if library is None: source = library.abstract(source).ports
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}')
if port_map: pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
if isinstance(port_map, dict): new = Builder(library=library, pattern=pat, name=name)
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)
return new 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( def plug(
self, self,
other: Abstract | str | Pattern, other: Abstract | str | Pattern,
@ -263,34 +191,18 @@ class Builder(PortList):
append: bool = False, append: bool = False,
) -> Self: ) -> Self:
""" """
Instantiate or append a pattern into the current device, connecting Wrapper around `Pattern.plug` which allows a string for `other`.
the ports specified by `map_in` and renaming the unconnected The `Builder`'s library is used to dereference the string (or `Abstract`, if
ports specified by `map_out`. one is passed with `append=True`).
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.
Args: 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 map_in: dict of `{'self_port': 'other_port'}` mappings, specifying
port connections between the two devices. port connections between the two devices.
map_out: dict of `{'old_name': 'new_name'}` mappings, specifying map_out: dict of `{'old_name': 'new_name'}` mappings, specifying
new names for ports in `other`. new names for ports in `other`.
mirrored: Enables mirroring `other` across the x or y axes prior mirrored: Enables mirroring `other` across the x axis prior to
to connecting any ports. connecting any ports.
inherit_name: If `True`, and `map_in` specifies only a single port, inherit_name: If `True`, and `map_in` specifies only a single port,
and `map_out` is `None`, and `other` has only two ports total, and `map_out` is `None`, and `other` has only two ports total,
then automatically renames the output port of `other` to the 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 port with `rotation=None`), `set_rotation` must be provided
to indicate how much `other` should be rotated. Otherwise, to indicate how much `other` should be rotated. Otherwise,
`set_rotation` must remain `None`. `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: Returns:
self self
@ -320,72 +235,21 @@ class Builder(PortList):
return self return self
if isinstance(other, str): 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 = 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 self.pattern.plug(
if (inherit_name other=other,
and not map_out map_in=map_in,
and len(map_in) == 1 map_out=map_out,
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, mirrored=mirrored,
inherit_name=inherit_name,
set_rotation=set_rotation, 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 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( def place(
self, self,
other: Abstract | str | Pattern, other: Abstract | str | Pattern,
@ -440,52 +304,20 @@ class Builder(PortList):
return self return self
if isinstance(other, str): 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 = self.library.abstract(other)
if append and isinstance(other, Abstract):
other = self.library[other.name]
if port_map is None: self.pattern.place(
port_map = {} other=other,
offset=offset,
if not skip_port_check: rotation=rotation,
self.check_ports(other.ports.keys(), map_in=None, map_out=port_map) pivot=pivot,
mirrored=mirrored,
ports = {} port_map=port_map,
for name, port in other.ports.items(): skip_port_check=skip_port_check,
new_name = port_map.get(name, name) append=append,
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)
return self return self
def translate(self, offset: ArrayLike) -> Self: def translate(self, offset: ArrayLike) -> Self:

View File

@ -149,14 +149,19 @@ class Pather(Builder):
cls, cls,
builder: Builder, builder: Builder,
*, *,
library: ILibrary | None = None,
tools: Tool | MutableMapping[str | None, Tool] | None = None, tools: Tool | MutableMapping[str | None, Tool] | None = None,
) -> 'Pather': ) -> 'Pather':
"""TODO from_builder docs""" """
library = library if library is not None else builder.library Construct a `Pather` by adding tools to a `Builder`.
if library is None:
raise BuildError('No library available for Pather!') Args:
new = Pather(library=library, tools=tools, pattern=builder.pattern) 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 return new
@classmethod @classmethod
@ -183,17 +188,11 @@ class Pather(Builder):
if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict): if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict):
tools = source.tools tools = source.tools
new = Pather.from_builder( if isinstance(source, str):
Builder.interface( source = library.abstract(source).ports
source=source,
library=library, pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
in_prefix=in_prefix, new = Pather(library=library, pattern=pat, name=name, tools=tools)
out_prefix=out_prefix,
port_map=port_map,
name=name,
),
tools=tools,
)
return new return new
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@ -9,7 +9,7 @@ from numpy.typing import ArrayLike
from ..pattern import Pattern from ..pattern import Pattern
from ..ref import Ref from ..ref import Ref
from ..library import ILibrary from ..library import ILibrary, Library
from ..error import PortError, BuildError from ..error import PortError, BuildError
from ..ports import PortList, Port from ..ports import PortList, Port
from ..abstract import Abstract from ..abstract import Abstract
@ -28,7 +28,7 @@ class RenderPather(PortList):
pattern: Pattern pattern: Pattern
""" Layout of this device """ """ Layout of this device """
library: ILibrary | None library: ILibrary
""" Library from which patterns should be referenced """ """ Library from which patterns should be referenced """
_dead: bool _dead: bool
@ -52,7 +52,7 @@ class RenderPather(PortList):
def __init__( def __init__(
self, self,
library: ILibrary | None = None, library: ILibrary,
*, *,
pattern: Pattern | None = None, pattern: Pattern | None = None,
ports: str | Mapping[str, Port] | None = None, ports: str | Mapping[str, Port] | None = None,
@ -99,6 +99,7 @@ class RenderPather(PortList):
source: PortList | Mapping[str, Port] | str, source: PortList | Mapping[str, Port] | str,
*, *,
library: ILibrary | None = None, library: ILibrary | None = None,
tools: Tool | MutableMapping[str | None, Tool] | None = None,
in_prefix: str = 'in_', in_prefix: str = 'in_',
out_prefix: str = '', out_prefix: str = '',
port_map: dict[str, str] | Sequence[str] | None = None, port_map: dict[str, str] | Sequence[str] | None = None,
@ -154,42 +155,17 @@ class RenderPather(PortList):
if library is None: if library is None:
if hasattr(source, 'library') and isinstance(source.library, ILibrary): if hasattr(source, 'library') and isinstance(source.library, ILibrary):
library = source.library 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 isinstance(source, str):
if library is None: source = library.abstract(source).ports
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}')
if port_map: pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
if isinstance(port_map, dict): new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
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)
return new return new
def plug( def plug(
@ -201,44 +177,41 @@ class RenderPather(PortList):
mirrored: bool = False, mirrored: bool = False,
inherit_name: bool = True, inherit_name: bool = True,
set_rotation: bool | None = None, set_rotation: bool | None = None,
append: bool = False,
) -> Self: ) -> Self:
if self._dead: if self._dead:
logger.error('Skipping plug() since device is dead') logger.error('Skipping plug() since device is dead')
return self return self
other_tgt: Pattern | Abstract
if isinstance(other, str): if isinstance(other, str):
if self.library is None: other_tgt = self.library.abstract(other)
raise BuildError('No library available, but `other` was a string!') if append and isinstance(other, Abstract):
other = self.library.abstract(other) other_tgt = 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,
mirrored=mirrored,
set_rotation=set_rotation,
)
# get rid of plugged ports # get rid of plugged ports
for ki, vi in map_in.items(): for kk in map_in.keys():
if ki in self.paths: if kk in self.paths:
self.paths[ki].append(RenderStep('P', None, self.ports[ki].copy(), self.ports[ki].copy(), None)) self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
del self.ports[ki]
map_out[vi] = None plugged = map_in.values()
self.place(other, offset=translation, rotation=rotation, pivot=pivot, for name, port in other_tgt.ports.items():
mirrored=mirrored, port_map=map_out, skip_port_check=True) 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 return self
def place( def place(
@ -251,43 +224,34 @@ class RenderPather(PortList):
mirrored: bool = False, mirrored: bool = False,
port_map: dict[str, str | None] | None = None, port_map: dict[str, str | None] | None = None,
skip_port_check: bool = False, skip_port_check: bool = False,
append: bool = False,
) -> Self: ) -> Self:
if self._dead: if self._dead:
logger.error('Skipping place() since device is dead') logger.error('Skipping place() since device is dead')
return self return self
other_tgt: Pattern | Abstract
if isinstance(other, str): if isinstance(other, str):
if self.library is None: other_tgt = self.library.abstract(other)
raise BuildError('No library available, but `other` was a string!') if append and isinstance(other, Abstract):
other = self.library.abstract(other) other_tgt = self.library[other.name]
if port_map is None: for name, port in other_tgt.ports.items():
port_map = {} 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:
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:
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None)) self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
for name, port in ports.items(): self.pattern.place(
p = port.deepcopy() other=other_tgt,
if mirrored: offset=offset,
p.mirror() rotation=rotation,
p.rotate_around(pivot, rotation) pivot=pivot,
p.translate(offset) mirrored=mirrored,
self.ports[name] = p 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 return self
def retool( def retool(
@ -409,22 +373,32 @@ class RenderPather(PortList):
def render( def render(
self, self,
lib: ILibrary | None = None,
append: bool = True, append: bool = True,
) -> Self: ) -> 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') 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 assert batch[0].tool is not None
name = lib << batch[0].tool.render(batch, port_names=tool_port_names) name = lib << batch[0].tool.render(batch, port_names=tool_port_names)
bb.ports[portspec] = batch[0].start_port.copy() pat.ports[portspec] = batch[0].start_port.copy()
bb.plug(name, {portspec: tool_port_names[0]}, append=append)
if append: 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(): for portspec, steps in self.paths.items():
batch: list[RenderStep] = [] batch: list[RenderStep] = []
@ -434,7 +408,7 @@ class RenderPather(PortList):
# If we can't continue a batch, render it # If we can't continue a batch, render it
if batch and (not appendable_op or not same_tool): if batch and (not appendable_op or not same_tool):
render_batch(lib, portspec, batch, append) render_batch(portspec, batch, append)
batch = [] batch = []
# batch is emptied already if we couldn't continue it # batch is emptied already if we couldn't continue it
@ -442,16 +416,16 @@ class RenderPather(PortList):
batch.append(step) batch.append(step)
# Opcodes which break the batch go below this line # Opcodes which break the batch go below this line
if not appendable_op and portspec in bb.ports: if not appendable_op and portspec in pat.ports:
del bb.ports[portspec] del pat.ports[portspec]
#If the last batch didn't end yet #If the last batch didn't end yet
if batch: if batch:
render_batch(lib, portspec, batch, append) render_batch(portspec, batch, append)
self.paths.clear() self.paths.clear()
bb.ports.clear() pat.ports.clear()
self.pattern.append(bb.pattern) self.pattern.append(pat)
return self return self

View File

@ -14,10 +14,11 @@ from numpy.typing import NDArray, ArrayLike
# .visualize imports matplotlib and matplotlib.collections # .visualize imports matplotlib and matplotlib.collections
from .ref import Ref from .ref import Ref
from .abstract import Abstract
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
from .label import Label from .label import Label
from .utils import rotation_matrix_2d, annotations_t, layer_t 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 .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
from .ports import Port, PortList from .ports import Port, PortList
@ -860,6 +861,346 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
pyplot.ylabel('y') pyplot.ylabel('y')
pyplot.show() 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') TT = TypeVar('TT')