Turn Builder into a subset of Pather

This commit is contained in:
Jan Petykiewicz 2023-01-31 22:28:02 -08:00
parent 83c710a85f
commit 4a2c4c5220
3 changed files with 186 additions and 39 deletions

View File

@ -42,7 +42,7 @@ from .library import (
) )
from .ports import Port, PortList from .ports import Port, PortList
from .abstract import Abstract from .abstract import Abstract
from .builder import Builder, Tool, FlatBuilder from .builder import Builder, Tool, FlatBuilder, Pather
from .utils import ports2data from .utils import ports2data

View File

@ -1,4 +1,4 @@
from .builder import Builder from .builder import Builder, Pather
from .flatbuilder import FlatBuilder from .flatbuilder import FlatBuilder
from .utils import ell from .utils import ell
from .tools import Tool from .tools import Tool

View File

@ -22,6 +22,7 @@ logger = logging.getLogger(__name__)
BB = TypeVar('BB', bound='Builder') BB = TypeVar('BB', bound='Builder')
PP = TypeVar('PP', bound='Pather')
class Builder(PortList): class Builder(PortList):
@ -78,23 +79,17 @@ class Builder(PortList):
renamed to 'gnd' so that further routing can use this signal or net name renamed to 'gnd' so that further routing can use this signal or net name
rather than the port name on the original `pad` device. rather than the port name on the original `pad` device.
""" """
__slots__ = ('pattern', 'library', 'tools', '_dead') __slots__ = ('pattern', 'library', '_dead')
pattern: Pattern pattern: Pattern
""" Layout of this device """ """ Layout of this device """
library: MutableLibrary library: Optional[MutableLibrary]
""" """
Library from which existing patterns should be referenced, and to which Library from which existing patterns should be referenced, and to which
new ones should be added new ones should be added
""" """
tools: Dict[Optional[str], Tool]
"""
Tool objects are used to dynamically generate new single-use Devices
(e.g wires or waveguides) to be plugged into this device.
"""
_dead: bool _dead: bool
""" If True, plug()/place() are skipped (for debugging)""" """ If True, plug()/place() are skipped (for debugging)"""
@ -108,11 +103,10 @@ class Builder(PortList):
def __init__( def __init__(
self, self,
library: MutableLibrary, library: Optional[MutableLibrary],
*, *,
pattern: Optional[Pattern] = None, pattern: Optional[Pattern] = None,
ports: Union[None, str, Mapping[str, Port]] = None, ports: Union[None, str, Mapping[str, Port]] = None,
tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None,
) -> None: ) -> None:
""" """
# TODO documentation for Builder() constructor # TODO documentation for Builder() constructor
@ -133,17 +127,12 @@ 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 tools is None:
self.tools = {}
elif isinstance(tools, Tool):
self.tools = {None: tools}
else:
self.tools = dict(tools)
self._dead = False self._dead = False
@classmethod @classmethod
@ -152,7 +141,6 @@ class Builder(PortList):
source: Union[PortList, Mapping[str, Port], str], source: Union[PortList, Mapping[str, Port], str],
*, *,
library: Optional[MutableLibrary] = None, library: Optional[MutableLibrary] = None,
tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None,
in_prefix: str = 'in_', in_prefix: str = 'in_',
out_prefix: str = '', out_prefix: str = '',
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None, port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None,
@ -184,9 +172,6 @@ class Builder(PortList):
library: Library from which existing patterns should be referenced, library: Library from which existing patterns should be referenced,
and to which new ones should be added. If not provided, and to which new ones should be added. If not provided,
the source's library will be used (if available). the source's library will be used (if available).
tools: Tool objects are used to dynamically generate new single-use
patterns (e.g wires or waveguides) while building. If not provided,
the source's tools will be reused (if available).
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
@ -210,13 +195,10 @@ class Builder(PortList):
if library is None: if library is None:
if hasattr(source, 'library') and isinstance(source.library, MutableLibrary): if hasattr(source, 'library') and isinstance(source.library, MutableLibrary):
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:
raise BuildError('Source given as a string, but `library` was `None`!')
orig_ports = library.abstract(source).ports orig_ports = library.abstract(source).ports
elif isinstance(source, PortList): elif isinstance(source, PortList):
orig_ports = source.ports orig_ports = source.ports
@ -248,7 +230,7 @@ class Builder(PortList):
if duplicates: if duplicates:
raise PortError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') raise PortError(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, ports={**ports_in, **ports_out})
return new return new
def plug( def plug(
@ -319,6 +301,8 @@ 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 asked to inherit a name, check that all conditions are met # If asked to inherit a name, check that all conditions are met
@ -403,6 +387,8 @@ 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 port_map is None: if port_map is None:
@ -499,11 +485,172 @@ class Builder(PortList):
s = f'<Builder {self.pattern} >' # TODO maybe show lib and tools? in builder repr? s = f'<Builder {self.pattern} >' # TODO maybe show lib and tools? in builder repr?
return s return s
class Pather(Builder):
"""
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.
`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).
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)}`
Examples: Creating a Device
===========================
- `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing
pattern and defines some ports.
- `Device(ports=None)` makes a new empty pattern with
default ports ('A' and 'B', in opposite directions, at (0, 0)).
- `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.
- `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.
Examples: Adding to a Device
============================
- `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
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(wire, {'myport': 'A'})` places port 'A' of `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.
- `my_device.place(pad, 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_device`. 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` device.
"""
__slots__ = ('tools',)
library: MutableLibrary
"""
Library from which existing patterns should be referenced, and to which
new ones should be added
"""
tools: Dict[Optional[str], Tool]
"""
Tool objects are used to dynamically generate new single-use Devices
(e.g wires or waveguides) to be plugged into this device.
"""
def __init__(
self,
library: MutableLibrary,
*,
pattern: Optional[Pattern] = None,
ports: Union[None, str, Mapping[str, Port]] = None,
tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None,
) -> 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.library = library
if pattern is not None:
self.pattern = pattern
else:
self.pattern = Pattern()
if ports is not None:
if self.pattern.ports:
raise BuildError('Ports supplied for pattern with pre-existing ports!')
if isinstance(ports, str):
ports = library.abstract(ports).ports
self.pattern.ports.update(copy.deepcopy(dict(ports)))
if tools is None:
self.tools = {}
elif isinstance(tools, Tool):
self.tools = {None: tools}
else:
self.tools = dict(tools)
self._dead = False
@classmethod
def from_builder(
cls,
builder: Builder,
*,
library: Optional[MutableLibrary] = None,
tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = 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)
return new
@classmethod
def interface(
cls,
source: Union[PortList, Mapping[str, Port], str],
*,
library: Optional[MutableLibrary] = 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,
) -> 'Pather':
"""
TODO doc pather.interface
"""
if library is None:
if hasattr(source, 'library') and isinstance(source.library, MutableLibrary):
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
new = Pather.from_builder(Builder.interface(
source=source,
library=library,
in_prefix=in_prefix,
out_prefix=out_prefix,
port_map=port_map,
))
return new
def __repr__(self) -> str:
s = f'<Builder {self.pattern} >' # TODO maybe show lib and tools? in builder repr?
return s
def retool( def retool(
self: BB, self: PP,
tool: Tool, tool: Tool,
keys: Union[Optional[str], Sequence[Optional[str]]] = None, keys: Union[Optional[str], Sequence[Optional[str]]] = None,
) -> BB: ) -> PP:
if keys is None or isinstance(keys, str): if keys is None or isinstance(keys, str):
self.tools[keys] = tool self.tools[keys] = tool
else: else:
@ -512,7 +659,7 @@ class Builder(PortList):
return self return self
def path( def path(
self: BB, self: PP,
portspec: str, portspec: str,
ccw: Optional[SupportsBool], ccw: Optional[SupportsBool],
length: float, length: float,
@ -520,7 +667,7 @@ class Builder(PortList):
tool_port_names: Sequence[str] = ('A', 'B'), tool_port_names: Sequence[str] = ('A', 'B'),
base_name: str = '_path', base_name: str = '_path',
**kwargs, **kwargs,
) -> BB: ) -> PP:
if self._dead: if self._dead:
logger.error('Skipping path() since device is dead') logger.error('Skipping path() since device is dead')
return self return self
@ -533,7 +680,7 @@ class Builder(PortList):
return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]}) return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]})
def path_to( def path_to(
self: BB, self: PP,
portspec: str, portspec: str,
ccw: Optional[SupportsBool], ccw: Optional[SupportsBool],
position: float, position: float,
@ -541,7 +688,7 @@ class Builder(PortList):
tool_port_names: Sequence[str] = ('A', 'B'), tool_port_names: Sequence[str] = ('A', 'B'),
base_name: str = '_pathto', base_name: str = '_pathto',
**kwargs, **kwargs,
) -> BB: ) -> PP:
if self._dead: if self._dead:
logger.error('Skipping path_to() since device is dead') logger.error('Skipping path_to() since device is dead')
return self return self
@ -567,7 +714,7 @@ class Builder(PortList):
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs) return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs)
def mpath( def mpath(
self: BB, self: PP,
portspec: Union[str, Sequence[str]], portspec: Union[str, Sequence[str]],
ccw: Optional[SupportsBool], ccw: Optional[SupportsBool],
*, *,
@ -577,7 +724,7 @@ class Builder(PortList):
force_container: bool = False, force_container: bool = False,
base_name: str = '_mpath', base_name: str = '_mpath',
**kwargs, **kwargs,
) -> BB: ) -> PP:
if self._dead: if self._dead:
logger.error('Skipping mpath() since device is dead') logger.error('Skipping mpath() since device is dead')
return self return self
@ -608,7 +755,7 @@ class Builder(PortList):
port_name = tuple(portspec)[0] port_name = tuple(portspec)[0]
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names) return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
else: else:
bld = Builder.interface(source=ports, library=self.library, tools=self.tools) bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
for port_name, length in extensions.items(): for port_name, length in extensions.items():
bld.path(port_name, ccw, length, tool_port_names=tool_port_names) bld.path(port_name, ccw, length, tool_port_names=tool_port_names)
name = self.library.get_name(base_name) name = self.library.get_name(base_name)
@ -617,7 +764,7 @@ class Builder(PortList):
# TODO def path_join() and def bus_join()? # TODO def path_join() and def bus_join()?
def flatten(self: BB) -> BB: def flatten(self: PP) -> PP:
""" """
Flatten the contained pattern, using the contained library to resolve references. Flatten the contained pattern, using the contained library to resolve references.