From 1008b6aabd65f3a6578b9055c9df924236f68521 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 7 Apr 2023 23:20:09 -0700 Subject: [PATCH] split pather into its own file --- masque/builder/__init__.py | 3 +- masque/builder/builder.py | 311 ---------------------------------- masque/builder/pather.py | 331 +++++++++++++++++++++++++++++++++++++ 3 files changed, 333 insertions(+), 312 deletions(-) create mode 100644 masque/builder/pather.py diff --git a/masque/builder/__init__.py b/masque/builder/__init__.py index 543bd3d..f96781d 100644 --- a/masque/builder/__init__.py +++ b/masque/builder/__init__.py @@ -1,4 +1,5 @@ -from .builder import Builder, Pather +from .builder import Builder from .flatbuilder import FlatBuilder +from .pather import Pather from .utils import ell from .tools import Tool diff --git a/masque/builder/builder.py b/masque/builder/builder.py index b4998df..1a8c075 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -12,9 +12,6 @@ from ..library import ILibrary from ..error import PortError, BuildError from ..ports import PortList, Port from ..abstract import Abstract -from ..utils import SupportsBool -from .tools import Tool -from .utils import ell logger = logging.getLogger(__name__) @@ -485,311 +482,3 @@ class Builder(PortList): 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: ILibrary - """ - Library from which existing patterns should be referenced, and to which - new ones should be added - """ - - tools: dict[str | None, 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: ILibrary, - *, - pattern: Pattern | None = None, - ports: str | Mapping[str, Port] | None = None, - tools: Tool | MutableMapping[str | None, Tool] | None = None, - name: str | None = 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._dead = False - 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) - - if name is not None: - library[name] = self.pattern - - @classmethod - def mk( - cls, - library: ILibrary, - name: str, - *, - ports: str | Mapping[str, Port] | None = None, - tools: Tool | MutableMapping[str | None, Tool] | None = None, - ) -> tuple[str, 'Pather']: - """ Name-and-make combination """ # TODO document - pather = Pather(library, name=name, ports=ports, tools=tools) - return name, pather - - @classmethod - def from_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) - return new - - @classmethod - def interface( - cls, - 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, - name: str | None = None, - ) -> 'Pather': - """ - TODO doc pather.interface - """ - 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 - - 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, - ) - return new - - def __repr__(self) -> str: - s = f'' # TODO maybe show lib and tools? in builder repr? - return s - - def retool( - self, - tool: Tool, - keys: str | Sequence[str | None] | None = None, - ) -> Self: - if keys is None or isinstance(keys, str): - self.tools[keys] = tool - else: - for key in keys: - self.tools[key] = tool - return self - - def path( - self, - portspec: str, - ccw: SupportsBool | None, - length: float, - *, - tool_port_names: Sequence[str] = ('A', 'B'), - base_name: str = '_path', - **kwargs, - ) -> Self: - if self._dead: - logger.error('Skipping path() since device is dead') - return self - - tool = self.tools.get(portspec, self.tools[None]) - in_ptype = self.pattern[portspec].ptype - pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) - name = self.library.get_name(base_name) - self.library[name] = pat - return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]}) - - def path_to( - self, - portspec: str, - ccw: SupportsBool | None, - position: float, - *, - tool_port_names: Sequence[str] = ('A', 'B'), - base_name: str = '_pathto', - **kwargs, - ) -> Self: - if self._dead: - logger.error('Skipping path_to() since device is dead') - return self - - port = self.pattern[portspec] - x, y = port.offset - if port.rotation is None: - raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()') - - if not numpy.isclose(port.rotation % (pi / 2), 0): - raise BuildError('path_to was asked to route from non-manhattan port') - - is_horizontal = numpy.isclose(port.rotation % pi, 0) - if is_horizontal: - if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x): - raise BuildError(f'path_to routing to behind source port: x={x:g} to {position:g}') - length = numpy.abs(position - x) - else: - if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y): - raise BuildError(f'path_to routing to behind source port: y={y:g} to {position:g}') - length = numpy.abs(position - y) - - return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs) - - def mpath( - self, - portspec: str | Sequence[str], - ccw: SupportsBool | None, - *, - spacing: float | ArrayLike | None = None, - set_rotation: float | None = None, - tool_port_names: Sequence[str] = ('A', 'B'), - force_container: bool = False, - base_name: str = '_mpath', - **kwargs, - ) -> Self: - if self._dead: - logger.error('Skipping mpath() since device is dead') - return self - - bound_types = set() - if 'bound_type' in kwargs: - bound_types.add(kwargs['bound_type']) - bound = kwargs['bound'] - for bt in ('emin', 'emax', 'pmin', 'pmax', 'min_past_furthest'): - if bt in kwargs: - bound_types.add(bt) - bound = kwargs[bt] - - if not bound_types: - raise BuildError('No bound type specified for mpath') - elif len(bound_types) > 1: - raise BuildError(f'Too many bound types specified for mpath: {bound_types}') - bound_type = tuple(bound_types)[0] - - if isinstance(portspec, str): - portspec = [portspec] - ports = self.pattern[tuple(portspec)] - - extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation) - - if len(ports) == 1 and not force_container: - # Not a bus, so having a container just adds noise to the layout - port_name = tuple(portspec)[0] - return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names) - else: - bld = Pather.interface(source=ports, library=self.library, tools=self.tools) - for port_name, length in extensions.items(): - bld.path(port_name, ccw, length, tool_port_names=tool_port_names) - name = self.library.get_name(base_name) - self.library[name] = bld.pattern - return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'? - - # TODO def path_join() and def bus_join()? - - def flatten(self) -> Self: - """ - Flatten the contained pattern, using the contained library to resolve references. - - Returns: - self - """ - self.pattern.flatten(self.library) - return self - diff --git a/masque/builder/pather.py b/masque/builder/pather.py new file mode 100644 index 0000000..1064df2 --- /dev/null +++ b/masque/builder/pather.py @@ -0,0 +1,331 @@ +from typing import Self, Sequence, MutableMapping, Mapping +import copy +import logging + +import numpy +from numpy import pi +from numpy.typing import ArrayLike + +from ..pattern import Pattern +from ..ref import Ref +from ..library import ILibrary +from ..error import PortError, BuildError +from ..ports import PortList, Port +from ..abstract import Abstract +from ..utils import SupportsBool +from .tools import Tool +from .utils import ell +from .builder import Builder + + +logger = logging.getLogger(__name__) + + +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: ILibrary + """ + Library from which existing patterns should be referenced, and to which + new ones should be added + """ + + tools: dict[str | None, 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: ILibrary, + *, + pattern: Pattern | None = None, + ports: str | Mapping[str, Port] | None = None, + tools: Tool | MutableMapping[str | None, Tool] | None = None, + name: str | None = 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._dead = False + 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) + + if name is not None: + library[name] = self.pattern + + @classmethod + def mk( + cls, + library: ILibrary, + name: str, + *, + ports: str | Mapping[str, Port] | None = None, + tools: Tool | MutableMapping[str | None, Tool] | None = None, + ) -> tuple[str, 'Pather']: + """ Name-and-make combination """ # TODO document + pather = Pather(library, name=name, ports=ports, tools=tools) + return name, pather + + @classmethod + def from_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) + return new + + @classmethod + def interface( + cls, + 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, + name: str | None = None, + ) -> 'Pather': + """ + TODO doc pather.interface + """ + 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 + + 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, + ) + return new + + def __repr__(self) -> str: + s = f'' # TODO maybe show lib and tools? in builder repr? + return s + + def retool( + self, + tool: Tool, + keys: str | Sequence[str | None] | None = None, + ) -> Self: + if keys is None or isinstance(keys, str): + self.tools[keys] = tool + else: + for key in keys: + self.tools[key] = tool + return self + + def path( + self, + portspec: str, + ccw: SupportsBool | None, + length: float, + *, + tool_port_names: Sequence[str] = ('A', 'B'), + base_name: str = '_path', + **kwargs, + ) -> Self: + if self._dead: + logger.error('Skipping path() since device is dead') + return self + + tool = self.tools.get(portspec, self.tools[None]) + in_ptype = self.pattern[portspec].ptype + pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) + name = self.library.get_name(base_name) + self.library[name] = pat + return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]}) + + def path_to( + self, + portspec: str, + ccw: SupportsBool | None, + position: float, + *, + tool_port_names: Sequence[str] = ('A', 'B'), + base_name: str = '_pathto', + **kwargs, + ) -> Self: + if self._dead: + logger.error('Skipping path_to() since device is dead') + return self + + port = self.pattern[portspec] + x, y = port.offset + if port.rotation is None: + raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()') + + if not numpy.isclose(port.rotation % (pi / 2), 0): + raise BuildError('path_to was asked to route from non-manhattan port') + + is_horizontal = numpy.isclose(port.rotation % pi, 0) + if is_horizontal: + if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x): + raise BuildError(f'path_to routing to behind source port: x={x:g} to {position:g}') + length = numpy.abs(position - x) + else: + if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y): + raise BuildError(f'path_to routing to behind source port: y={y:g} to {position:g}') + length = numpy.abs(position - y) + + return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs) + + def mpath( + self, + portspec: str | Sequence[str], + ccw: SupportsBool | None, + *, + spacing: float | ArrayLike | None = None, + set_rotation: float | None = None, + tool_port_names: Sequence[str] = ('A', 'B'), + force_container: bool = False, + base_name: str = '_mpath', + **kwargs, + ) -> Self: + if self._dead: + logger.error('Skipping mpath() since device is dead') + return self + + bound_types = set() + if 'bound_type' in kwargs: + bound_types.add(kwargs['bound_type']) + bound = kwargs['bound'] + for bt in ('emin', 'emax', 'pmin', 'pmax', 'min_past_furthest'): + if bt in kwargs: + bound_types.add(bt) + bound = kwargs[bt] + + if not bound_types: + raise BuildError('No bound type specified for mpath') + elif len(bound_types) > 1: + raise BuildError(f'Too many bound types specified for mpath: {bound_types}') + bound_type = tuple(bound_types)[0] + + if isinstance(portspec, str): + portspec = [portspec] + ports = self.pattern[tuple(portspec)] + + extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation) + + if len(ports) == 1 and not force_container: + # Not a bus, so having a container just adds noise to the layout + port_name = tuple(portspec)[0] + return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names) + else: + bld = Pather.interface(source=ports, library=self.library, tools=self.tools) + for port_name, length in extensions.items(): + bld.path(port_name, ccw, length, tool_port_names=tool_port_names) + name = self.library.get_name(base_name) + self.library[name] = bld.pattern + return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'? + + # TODO def path_join() and def bus_join()? + + def flatten(self) -> Self: + """ + Flatten the contained pattern, using the contained library to resolve references. + + Returns: + self + """ + self.pattern.flatten(self.library) + return self +