From 1a823a54f3ce282a7d20292cc07c887682a32570 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 7 Oct 2023 01:54:16 -0700 Subject: [PATCH] Lots of doc updates --- masque/abstract.py | 6 + masque/builder/builder.py | 35 ++-- masque/builder/pather.py | 310 ++++++++++++++++++++++++++++----- masque/builder/renderpather.py | 238 ++++++++++++++++++++++--- masque/pattern.py | 57 +++++- 5 files changed, 559 insertions(+), 87 deletions(-) diff --git a/masque/abstract.py b/masque/abstract.py index 05f8f06..e3ab46e 100644 --- a/masque/abstract.py +++ b/masque/abstract.py @@ -18,6 +18,12 @@ logger = logging.getLogger(__name__) class Abstract(PortList): + """ + An `Abstract` is a container for a name and associated ports. + + When snapping a sub-component to an existing pattern, only the name (not contained + in a `Pattern` object) and port info is needed, and not the geometry itself. + """ __slots__ = ('name', '_ports') name: str diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 834171b..243891d 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -1,3 +1,6 @@ +""" +Simplified Pattern assembly (`Builder`) +""" from typing import Self, Sequence, Mapping, Literal, overload import copy import logging @@ -105,7 +108,15 @@ class Builder(PortList): name: str | None = None, ) -> None: """ - # TODO documentation for Builder() constructor + Args: + library: The library from which referenced patterns will be taken + pattern: The pattern which will be modified by subsequent operations. + If `None` (default), a new pattern is created. + ports: Allows specifying the initial set of ports, if `pattern` does + not already have any ports (or is not provided). May be a string, + in which case it is interpreted as a name in `library`. + Default `None` (no ports). + name: If specified, `library[name]` is set to `self.pattern`. """ self._dead = False self.library = library @@ -263,32 +274,26 @@ class Builder(PortList): append: bool = False, ) -> Self: """ - Instantiate or append the device `other` into the current device, adding its - ports to those of the current device (but not connecting any ports). - - Mirroring is applied before rotation; translation (`offset`) is applied last. - - Examples: - ========= - - `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. + Wrapper around `Pattern.place` 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. 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 and y axes. + 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 device. 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 diff --git a/masque/builder/pather.py b/masque/builder/pather.py index ba1c642..5fbd746 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -1,3 +1,6 @@ +""" +Manual wire/waveguide routing (`Pather`) +""" from typing import Self, Sequence, MutableMapping, Mapping import copy import logging @@ -23,57 +26,87 @@ 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. + An extension of `Builder` which provides functionality for routing and attaching + single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns. - `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). + `Pather` is mostly concerned with calculating how long each wire should be. It calls + out to `Tool.path` functions provided by subclasses of `Tool` to build the actual patterns. + `Tool`s are assigned on a per-port basis and stored in `.tools`; a key of `None` represents + a "default" `Tool` used for all ports which do not have a port-specific `Tool` assigned. - 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 + Examples: Creating a Pather =========================== - - `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing - pattern and defines some ports. + - `Pather(library, tools=my_tool)` makes an empty pattern with no ports. The pattern + is not added into `library` and must later be added with e.g. + `library['mypat'] = pather.pattern`. + The default wire/waveguide generating tool for all ports is set to `my_tool`. - - `Device(ports=None)` makes a new empty pattern with - default ports ('A' and 'B', in opposite directions, at (0, 0)). + - `Pather(library, ports={'in': Port(...), 'out': ...}, name='mypat', tools=my_tool)` + makes an empty pattern, adds the given ports, and places it into `library` + under the name `'mypat'`. The default wire/waveguide generating tool + for all ports is set to `my_tool` - - `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. + - `Pather(..., tools={'in': top_metal_40um, 'out': bottom_metal_1um, None: my_tool})` + assigns specific tools to individual ports, and `my_tool` as a default for ports + which are not specified. - - `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. + - `Pather.interface(other_pat, port_map=['A', 'B'], library=library, tools=my_tool)` + 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 - ============================ - - `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'. + - `Pather.interface(other_pather, ...)` does the same thing as + `Builder.interface(other_builder.pattern, ...)` but also uses + `other_builder.library` as its library by default. - - `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`, + + Examples: Adding to a pattern + ============================= + - `pather.path('my_port', ccw=True, distance)` creates a "wire" for which the output + port is `distance` units away along the axis of `'my_port'` and rotated 90 degrees + counterclockwise (since `ccw=True`) relative to `'my_port'`. The wire is `plug`ged + into the existing `'my_port'`, causing the port to move to the wire's output. + + There is no formal guarantee about how far off-axis the output will be located; + there may be a significant width to the bend that is used to accomplish the 90 degree + turn. However, an error is raised if `distance` is too small to fit the bend. + + - `pather.path('my_port', ccw=None, distance)` creates a straight wire with a length + of `distance` and `plug`s it into `'my_port'`. + + - `pather.path_to('my_port', ccw=False, position)` creates a wire which starts at + `'my_port'` and has its output at the specified `position`, pointing 90 degrees + clockwise relative to the input. Again, the off-axis position or distance to the + output is not specified, so `position` takes the form of a single coordinate. To + ease debugging, position may be specified as `x=position` or `y=position` and an + error will be raised if the wrong coordinate is given. + + - `pather.mpath(['A', 'B', 'C'], ..., spacing=spacing)` is a superset of `path` + and `path_to` which can act on multiple ports simultaneously. Each port's wire is + generated using its own `Tool` (or the default tool if left unspecified). + The output ports are spaced out by `spacing` along the input ports' axis, unless + `ccw=None` is specified (i.e. no bends) in which case they all end at the same + destination coordinate. + + - `pather.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport' + of `pather.pattern`. 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'})` + - `pather.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 + rotation, adding its ports to those of `pather.pattern`. 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. + + - `pather.retool(tool)` or `pather.retool(tool, ['in', 'out', None])` can change + which tool is used for the given ports (or as the default tool). Useful + when placing vias or using multiple waveguide types along a route. """ __slots__ = ('tools',) @@ -85,8 +118,9 @@ class Pather(Builder): 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. + Tool objects are used to dynamically generate new single-use `Pattern`s + (e.g wires or waveguides) to be plugged into this device. A key of `None` + indicates the default `Tool`. """ def __init__( @@ -99,13 +133,19 @@ class Pather(Builder): 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). + Args: + library: The library from which referenced patterns will be taken, + and where new patterns (e.g. generated by the `tools`) will be placed. + pattern: The pattern which will be modified by subsequent operations. + If `None` (default), a new pattern is created. + ports: Allows specifying the initial set of ports, if `pattern` does + not already have any ports (or is not provided). May be a string, + in which case it is interpreted as a name in `library`. + Default `None` (no ports). + tools: A mapping of {port: tool} which specifies what `Tool` should be used + to generate waveguide or wire segments when `path`/`path_to`/`mpath` + are called. Relies on `Tool.path` implementations. + name: If specified, `library[name]` is set to `self.pattern`. """ self._dead = False self.library = library @@ -165,7 +205,36 @@ class Pather(Builder): name: str | None = None, ) -> 'Pather': """ - TODO doc pather.interface + Wrapper for `Pattern.interface()`, which returns a Pather instead. + + Args: + source: A collection of ports (e.g. Pattern, Builder, or dict) + 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. + tools: `Tool`s which will be used by the pather for generating new wires + or waveguides (via `path`/`path_to`/`mpath`). + 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 pather, with an empty pattern and 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 library is None: if hasattr(source, 'library') and isinstance(source.library, ILibrary): @@ -192,6 +261,18 @@ class Pather(Builder): tool: Tool, keys: str | Sequence[str | None] | None = None, ) -> Self: + """ + Update the `Tool` which will be used when generating `Pattern`s for the ports + given by `keys`. + + Args: + tool: The new `Tool` to use for the given ports. + keys: Which ports the tool should apply to. `None` indicates the default tool, + used when there is no matching entry in `self.tools` for the port in question. + + Returns: + self + """ if keys is None or isinstance(keys, str): self.tools[keys] = tool else: @@ -209,6 +290,34 @@ class Pather(Builder): base_name: str = '_path', **kwargs, ) -> Self: + """ + Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim + of traveling exactly `length` distance. + + The wire will travel `length` distance along the port's axis, an an unspecified + (tool-dependent) distance in the perpendicular direction. The output port will + be rotated (or not) based on the `ccw` parameter. + + Args: + portspec: The name of the port into which the wire will be plugged. + ccw: If `None`, the output should be along the same axis as the input. + Otherwise, cast to bool and turn counterclockwise if True + and clockwise otherwise. + length: The total distance from input to output, along the input's axis only. + (There may be a tool-dependent offset along the other axis.) + tool_port_names: The names of the ports on the generated pattern. It is unlikely + that you will need to change these. The first port is the input (to be + connected to `portspec`). + base_name: Name to use for the generated `Pattern`. This will be passed through + `self.library.get_name()` to get a unique name for each new `Pattern`. + + Returns: + self + + Raises: + BuildError if `distance` is too small to fit the bend (if a bend is present). + LibraryError if no valid name could be picked for the pattern. + """ if self._dead: logger.error('Skipping path() since device is dead') return self @@ -232,6 +341,44 @@ class Pather(Builder): base_name: str = '_pathto', **kwargs, ) -> Self: + """ + Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim + of ending exactly at a target position. + + The wire will travel so that the output port will be placed at exactly the target + position along the input port's axis. There can be an unspecified (tool-dependent) + offset in the perpendicular direction. The output port will be rotated (or not) + based on the `ccw` parameter. + + Args: + portspec: The name of the port into which the wire will be plugged. + ccw: If `None`, the output should be along the same axis as the input. + Otherwise, cast to bool and turn counterclockwise if True + and clockwise otherwise. + position: The final port position, along the input's axis only. + (There may be a tool-dependent offset along the other axis.) + Only one of `position`, `x`, and `y` may be specified. + x: The final port position along the x axis. + `portspec` must refer to a horizontal port if `x` is passed, otherwise a + BuildError will be raised. + y: The final port position along the y axis. + `portspec` must refer to a vertical port if `y` is passed, otherwise a + BuildError will be raised. + tool_port_names: The names of the ports on the generated pattern. It is unlikely + that you will need to change these. The first port is the input (to be + connected to `portspec`). + base_name: Name to use for the generated `Pattern`. This will be passed through + `self.library.get_name()` to get a unique name for each new `Pattern`. + + Returns: + self + + Raises: + BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend + is present). + BuildError if `x` or `y` is specified but does not match the axis of `portspec`. + BuildError if more than one of `x`, `y`, and `position` is specified. + """ if self._dead: logger.error('Skipping path_to() since device is dead') return self @@ -285,6 +432,81 @@ class Pather(Builder): base_name: str = '_mpath', **kwargs, ) -> Self: + """ + `mpath` is a superset of `path` and `path_to` which can act on bundles or buses + of "wires or "waveguides". + + The wires will travel so that the output ports will be placed at well-defined + locations along the axis of their input ports, but may have arbitrary (tool- + dependent) offsets in the perpendicular direction. + + If `ccw` is not `None`, the wire bundle will turn 90 degres in either the + clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the + bundle, the center-to-center wire spacings after the turn are set by `spacing`, + which is required when `ccw` is not `None`. The final position of bundle as a + whole can be set in a number of ways: + + =A>---------------------------V turn direction: `ccw=False` + =B>-------------V | + =C>-----------------------V | + =D=>----------------V | + | + + x---x---x---x `spacing` (can be scalar or array) + + <--------------> `emin=` + <------> `bound_type='min_past_furthest', bound=` + <--------------------------------> `emax=` + x `pmin=` + x `pmax=` + + - `emin=`, equivalent to `bound_type='min_extension', bound=` + The total extension value for the furthest-out port (B in the diagram). + - `emax=`, equivalent to `bound_type='max_extension', bound=`: + The total extension value for the closest-in port (C in the diagram). + - `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`: + The coordinate of the innermost bend (D's bend). + The x/y versions throw an error if they do not match the port axis (for debug) + - `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`: + The coordinate of the outermost bend (A's bend). + The x/y versions throw an error if they do not match the port axis (for debug) + - `bound_type='min_past_furthest', bound=`: + The distance between furthest out-port (B) and the innermost bend (D's bend). + + If `ccw=None`, final output positions (along the input axis) of all wires will be + identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is + required. In this case, `emin=` and `emax=` are equivalent to each other, and + `pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other. + + + Args: + portspec: The names of the ports which are to be routed. + ccw: If `None`, the outputs should be along the same axis as the inputs. + Otherwise, cast to bool and turn 90 degrees counterclockwise if `True` + and clockwise otherwise. + spacing: Center-to-center distance between output ports along the input port's axis. + Must be provided if (and only if) `ccw` is not `None`. + set_rotation: If the provided ports have `rotation=None`, this can be used + to set a rotation for them. + tool_port_names: The names of the ports on the generated pattern. It is unlikely + that you will need to change these. The first port is the input (to be + connected to `portspec`). + force_container: If `False` (default), and only a single port is provided, the + generated wire for that port will be referenced directly, rather than being + wrapped in an additonal `Pattern`. + base_name: Name to use for the generated `Pattern`. This will be passed through + `self.library.get_name()` to get a unique name for each new `Pattern`. + + Returns: + self + + Raises: + BuildError if the implied length for any wire is too close to fit the bend + (if a bend is requested). + BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not + match the axis of `portspec`. + BuildError if an incorrect bound type or spacing is specified. + """ if self._dead: logger.error('Skipping mpath() since device is dead') return self diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 8997182..6a09d03 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -24,6 +24,24 @@ logger = logging.getLogger(__name__) class RenderPather(PortList): + """ + `RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath` + functions to plan out wire paths without incrementally generating the layout. Instead, + it waits until `render` is called, at which point it draws all the planned segments + simultaneously. This allows it to e.g. draw each wire using a single `Path` or + `Polygon` shape instead of multiple rectangles. + + `RenderPather` calls out to `Tool.planL` and `Tool.render` to provide tool-specific + dimensions and build the final geometry for each wire. `Tool.planL` provides the + output port data (relative to the input) for each segment. The tool, input and output + ports are placed into a `RenderStep`, and a sequence of `RenderStep`s is stored for + each port. When `render` is called, it bundles `RenderStep`s into batches which use + the same `Tool`, and passes each batch to the relevant tool's `Tool.render` to build + the geometry. + + See `Pather` for routing examples. After routing is complete, `render` must be called + to generate the final geometry. + """ __slots__ = ('pattern', 'library', 'paths', 'tools', '_dead', ) pattern: Pattern @@ -36,6 +54,7 @@ class RenderPather(PortList): """ If True, plug()/place() are skipped (for debugging) """ paths: defaultdict[str, list[RenderStep]] + """ Per-port list of operations, to be used by `render` """ tools: dict[str | None, Tool] """ @@ -61,8 +80,19 @@ class RenderPather(PortList): name: str | None = None, ) -> None: """ - # TODO documentation for Builder() constructor - + Args: + library: The library from which referenced patterns will be taken, + and where new patterns (e.g. generated by the `tools`) will be placed. + pattern: The pattern which will be modified by subsequent operations. + If `None` (default), a new pattern is created. + ports: Allows specifying the initial set of ports, if `pattern` does + not already have any ports (or is not provided). May be a string, + in which case it is interpreted as a name in `library`. + Default `None` (no ports). + tools: A mapping of {port: tool} which specifies what `Tool` should be used + to generate waveguide or wire segments when `path`/`path_to`/`mpath` + are called. Relies on `Tool.planL` and `Tool.render` implementations. + name: If specified, `library[name]` is set to `self.pattern`. """ self._dead = False self.paths = defaultdict(list) @@ -107,32 +137,17 @@ class RenderPather(PortList): name: str | None = None, ) -> 'RenderPather': """ - 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 RenderPather instead. Args: source: A collection of ports (e.g. Pattern, Builder, or dict) - from which to create the interface. - library: Used for buildin functions; if not passed and the source + 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 new ones should be added. If not provided, - 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. + tools: `Tool`s which will be used by the pather for generating new wires + or waveguides (via `path`/`path_to`/`mpath`). 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 @@ -144,7 +159,7 @@ class RenderPather(PortList): renamed (to the values). Returns: - The new builder, with an empty pattern and 2x as many ports as + The new `RenderPather`, with an empty pattern and 2x as many ports as listed in port_map. Raises: @@ -180,6 +195,46 @@ class RenderPather(PortList): set_rotation: bool | None = None, append: bool = False, ) -> Self: + """ + Wrapper for `Pattern.plug` which adds a `RenderStep` with opcode 'P' + for any affected ports. This separates any future `RenderStep`s on the + same port into a new batch, since the plugged device interferes with drawing. + + Args: + 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 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 device 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 self._dead: logger.error('Skipping plug() since device is dead') return self @@ -227,6 +282,39 @@ class RenderPather(PortList): skip_port_check: bool = False, append: bool = False, ) -> Self: + """ + Wrapper for `Pattern.place` which adds a `RenderStep` with opcode 'P' + for any affected ports. This separates any future `RenderStep`s on the + same port into a new batch, since the placed device interferes with drawing. + + Note that mirroring is applied before rotation; translation (`offset`) is applied last. + + 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 self._dead: logger.error('Skipping place() since device is dead') return self @@ -260,6 +348,18 @@ class RenderPather(PortList): tool: Tool, keys: str | Sequence[str | None] | None = None, ) -> Self: + """ + Update the `Tool` which will be used when generating `Pattern`s for the ports + given by `keys`. + + Args: + tool: The new `Tool` to use for the given ports. + keys: Which ports the tool should apply to. `None` indicates the default tool, + used when there is no matching entry in `self.tools` for the port in question. + + Returns: + self + """ if keys is None or isinstance(keys, str): self.tools[keys] = tool else: @@ -274,6 +374,31 @@ class RenderPather(PortList): length: float, **kwargs, ) -> Self: + """ + Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim + of traveling exactly `length` distance. + + The wire will travel `length` distance along the port's axis, an an unspecified + (tool-dependent) distance in the perpendicular direction. The output port will + be rotated (or not) based on the `ccw` parameter. + + `RenderPather.render` must be called after all paths have been fully planned. + + Args: + portspec: The name of the port into which the wire will be plugged. + ccw: If `None`, the output should be along the same axis as the input. + Otherwise, cast to bool and turn counterclockwise if True + and clockwise otherwise. + length: The total distance from input to output, along the input's axis only. + (There may be a tool-dependent offset along the other axis.) + + Returns: + self + + Raises: + BuildError if `distance` is too small to fit the bend (if a bend is present). + LibraryError if no valid name could be picked for the pattern. + """ if self._dead: logger.error('Skipping path() since device is dead') return self @@ -281,7 +406,7 @@ class RenderPather(PortList): port = self.pattern[portspec] in_ptype = port.ptype port_rot = port.rotation - assert port_rot is not None # TODO allow manually setting rotation? + assert port_rot is not None # TODO allow manually setting rotation for RenderPather.path()? tool = self.tools.get(portspec, self.tools[None]) # ask the tool for bend size (fill missing dx or dy), check feasibility, and get out_ptype @@ -308,6 +433,41 @@ class RenderPather(PortList): y: float | None = None, **kwargs, ) -> Self: + """ + Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim + of ending exactly at a target position. + + The wire will travel so that the output port will be placed at exactly the target + position along the input port's axis. There can be an unspecified (tool-dependent) + offset in the perpendicular direction. The output port will be rotated (or not) + based on the `ccw` parameter. + + `RenderPather.render` must be called after all paths have been fully planned. + + Args: + portspec: The name of the port into which the wire will be plugged. + ccw: If `None`, the output should be along the same axis as the input. + Otherwise, cast to bool and turn counterclockwise if True + and clockwise otherwise. + position: The final port position, along the input's axis only. + (There may be a tool-dependent offset along the other axis.) + Only one of `position`, `x`, and `y` may be specified. + x: The final port position along the x axis. + `portspec` must refer to a horizontal port if `x` is passed, otherwise a + BuildError will be raised. + y: The final port position along the y axis. + `portspec` must refer to a vertical port if `y` is passed, otherwise a + BuildError will be raised. + + Returns: + self + + Raises: + BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend + is present). + BuildError if `x` or `y` is specified but does not match the axis of `portspec`. + BuildError if more than one of `x`, `y`, and `position` is specified. + """ if self._dead: logger.error('Skipping path_to() since device is dead') return self @@ -358,6 +518,32 @@ class RenderPather(PortList): set_rotation: float | None = None, **kwargs, ) -> Self: + """ + `mpath` is a superset of `path` and `path_to` which can act on bundles or buses + of "wires or "waveguides". + + See `Pather.mpath` for details. + + Args: + portspec: The names of the ports which are to be routed. + ccw: If `None`, the outputs should be along the same axis as the inputs. + Otherwise, cast to bool and turn 90 degrees counterclockwise if `True` + and clockwise otherwise. + spacing: Center-to-center distance between output ports along the input port's axis. + Must be provided if (and only if) `ccw` is not `None`. + set_rotation: If the provided ports have `rotation=None`, this can be used + to set a rotation for them. + + Returns: + self + + Raises: + BuildError if the implied length for any wire is too close to fit the bend + (if a bend is requested). + BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not + match the axis of `portspec`. + BuildError if an incorrect bound type or spacing is specified. + """ if self._dead: logger.error('Skipping mpath() since device is dead') return self diff --git a/masque/pattern.py b/masque/pattern.py index 4fae275..7efa72e 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -28,8 +28,61 @@ logger = logging.getLogger(__name__) class Pattern(PortList, AnnotatableImpl, Mirrorable): """ - 2D layout consisting of some set of shapes, labels, and references to other Pattern objects - (via Ref). Shapes are assumed to inherit from `masque.shapes.Shape` or provide equivalent functions. + 2D layout consisting of some set of shapes, labels, and references to other + Pattern objects (via Ref). Shapes are assumed to inherit from `masque.shapes.Shape` + or provide equivalent functions. + + `Pattern` also stores a dict of `Port`s, which can be used to "snap" together points. + See `Pattern.plug()` and `Pattern.place()`, as well as the helper classes + `builder.Builder`, `builder.Pather`, `builder.RenderPather`, and `ports.PortsList`. + + For convenience, ports can be read out using square brackets: + - `pattern['A'] == Port((0, 0), 0)` + - `pattern[['A', 'B']] == {'A': Port((0, 0), 0), 'B': Port((0, 0), pi)}` + + + Examples: Making a Pattern + ========================== + - `pat = Pattern()` just creates an empty pattern, with no geometry or ports + + - To immediately set some of the pattern's contents, + ``` + pat = Pattern( + shapes={'layer1': [shape0, ...], 'layer2': [shape,...], ...}, + labels={'layer1': [...], ...}, + refs={'name1': [ref0, ...], 'name2': [ref, ...], ...}, + ports={'in': Port(...), 'out': Port(...)}, + ) + ``` + + - `Pattern.interface(other_pat, port_map=['A', 'B'])` 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 pattern + ============================= + - `pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})` + instantiates `subdevice` into `pat`, plugging ports 'A' and 'B' + of `pat` into ports 'C' and 'B' of `subdevice`. The connected ports + are removed and any unconnected ports from `subdevice` are added to + `pat`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'. + + - `pat.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport' + of `pat`. If `wire` has only two ports (e.g. 'A' and 'B'), since 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. + + - `pat.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 `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` device. """ __slots__ = ( 'shapes', 'labels', 'refs', '_ports',