diff --git a/MIGRATION.md b/MIGRATION.md index 818b133..5ab568c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -18,17 +18,11 @@ The biggest migration point is that the old routing verbs were renamed: | `Pather.path(...)` | `Pather.trace(...)` | | `Pather.path_to(...)` | `Pather.trace_to(...)` | | `Pather.mpath(...)` | `Pather.trace(...)` / `Pather.trace_to(...)` with multiple ports | -| `Pather.pathS(...)` | `Pather.jog(...)` | -| `Pather.pathU(...)` | `Pather.uturn(...)` | | `Pather.path_into(...)` | `Pather.trace_into(...)` | -| `Pather.path_from(src, dst)` | `Pather.at(src).trace_into(dst)` | -| `RenderPather.path(...)` | `Pather(..., auto_render=False).trace(...)` | -| `RenderPather.path_to(...)` | `Pather(..., auto_render=False).trace_to(...)` | -| `RenderPather.mpath(...)` | `Pather(..., auto_render=False).trace(...)` / `Pather(..., auto_render=False).trace_to(...)` | -| `RenderPather.pathS(...)` | `Pather(..., auto_render=False).jog(...)` | -| `RenderPather.pathU(...)` | `Pather(..., auto_render=False).uturn(...)` | -| `RenderPather.path_into(...)` | `Pather(..., auto_render=False).trace_into(...)` | -| `RenderPather.path_from(src, dst)` | `Pather(..., auto_render=False).at(src).trace_into(dst)` | +| `RenderPather.path(...)` | `RenderPather.trace(...)` | +| `RenderPather.path_to(...)` | `RenderPather.trace_to(...)` | +| `RenderPather.mpath(...)` | `RenderPather.trace(...)` / `RenderPather.trace_to(...)` | +| `RenderPather.path_into(...)` | `RenderPather.trace_into(...)` | There are also new convenience wrappers: @@ -49,19 +43,13 @@ that still calls `pather.path(...)` must be renamed. pather.path('VCC', False, 6_000) pather.path_to('VCC', None, x=0) pather.mpath(['GND', 'VCC'], True, xmax=-10_000, spacing=5_000) -pather.pathS('VCC', offset=-2_000, length=8_000) -pather.pathU('VCC', offset=4_000, length=5_000) pather.path_into('src', 'dst') -pather.path_from('src', 'dst') # new pather.cw('VCC', 6_000) pather.straight('VCC', x=0) pather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000) -pather.jog('VCC', offset=-2_000, length=8_000) -pather.uturn('VCC', offset=4_000, length=5_000) pather.trace_into('src', 'dst') -pather.at('src').trace_into('dst') ``` If you prefer the more explicit spelling, `trace(...)` and `trace_to(...)` @@ -85,30 +73,11 @@ Routing can now be written in a fluent style via `.at(...)`, which returns a ``` This is additive, not required for migration. Existing code can stay with the -non-fluent `Pather` methods after renaming the verbs above. - -Old `PortPather` helper names were also cleaned up: - -| Old API | New API | -| --- | --- | -| `save_copy(...)` | `mark(...)` | -| `rename_to(...)` | `rename(...)` | - -Example: - -```python -# old -pp.save_copy('branch') -pp.rename_to('feed') - -# new -pp.mark('branch') -pp.rename('feed') -``` +non-fluent `Pather`/`RenderPather` methods after renaming the verbs above. ## Imports and module layout -`Pather` now provides the remaining builder/routing surface in +`Builder`, `Pather`, and `RenderPather` now live together in `masque/builder/pather.py`. The old module files `masque/builder/builder.py` and `masque/builder/renderpather.py` were removed. @@ -120,17 +89,14 @@ from masque.builder.builder import Builder from masque.builder.renderpather import RenderPather # new -from masque.builder import Pather - -builder = Pather(...) -deferred = Pather(..., auto_render=False) +from masque.builder import Builder, RenderPather ``` Top-level imports from `masque` also continue to work. -`Pather` now defaults to `auto_render=True`, so plain construction replaces the -old `Builder` behavior. Use `Pather(..., auto_render=False)` where you -previously used `RenderPather`. +`Builder` is now a thin compatibility wrapper over the unified `Pather` +implementation with `auto_render=True`. `RenderPather` is the same wrapper with +`auto_render=False`. ## `BasicTool` was replaced diff --git a/README.md b/README.md index 71c37f0..6ebc5ab 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ References are accomplished by listing the target's name, not its `Pattern` obje in order to create a reference, but they also need to access the pattern's ports. * One way to provide this data is through an `Abstract`, generated via `Library.abstract()` or through a `Library.abstract_view()`. - * Another way is use `Pather.place()` or `Pather.plug()`, which automatically creates + * Another way is use `Builder.place()` or `Builder.plug()`, which automatically creates an `Abstract` from its internally-referenced `Library`. @@ -193,8 +193,8 @@ my_pattern.ref(new_name, ...) # instantiate the cell # In practice, you may do lots of my_pattern.ref(lib << make_tree(...), ...) -# With a `Pather` and `place()`/`plug()` the `lib <<` portion can be implicit: -my_builder = Pather(library=lib, ...) +# With a `Builder` and `place()`/`plug()` the `lib <<` portion can be implicit: +my_builder = Builder(library=lib, ...) ... my_builder.place(make_tree(...)) ``` diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index 749915b..ea4471f 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -17,7 +17,7 @@ Contents * Build hierarchical photonic-crystal example devices * Reference other patterns * Add ports to a pattern - * Use `Pather` to snap ports together into a circuit + * Use `Builder` to snap ports together into a circuit * Check for dangling references - [library](library.py) * Continue from `devices.py` using a lazy library @@ -29,7 +29,7 @@ Contents * Use `AutoTool` to generate paths * Use `AutoTool` to automatically transition between path types - [renderpather](renderpather.py) - * Use `Pather(auto_render=False)` and `PathTool` to build a layout similar to the one in [pather](pather.py), + * Use `RenderPather` and `PathTool` to build a layout similar to the one in [pather](pather.py), but using `Path` shapes instead of `Polygon`s. - [port_pather](port_pather.py) * Use `PortPather` and the `.at()` syntax for more concise routing diff --git a/examples/tutorial/devices.py b/examples/tutorial/devices.py index 955e786..d6beb2a 100644 --- a/examples/tutorial/devices.py +++ b/examples/tutorial/devices.py @@ -1,5 +1,5 @@ """ -Tutorial: building hierarchical devices with `Pattern`, `Port`, and `Pather`. +Tutorial: building hierarchical devices with `Pattern`, `Port`, and `Builder`. This file uses photonic-crystal components as the concrete example, so some of the geometry-generation code is domain-specific. The tutorial value is in the @@ -12,7 +12,7 @@ import numpy from numpy import pi from masque import ( - layer_t, Pattern, Ref, Pather, Port, Polygon, + layer_t, Pattern, Ref, Builder, Port, Polygon, Library, ) from masque.utils import ports2data @@ -261,8 +261,8 @@ def main(interactive: bool = True) -> None: # # Build a circuit # - # Create a `Pather`, and register the resulting top cell as "my_circuit". - circ = Pather(library=lib, name='my_circuit') + # Create a `Builder`, and register the resulting top cell as "my_circuit". + circ = Builder(library=lib, name='my_circuit') # Start by placing a waveguide and renaming its ports to match the circuit-level # names we want to use while assembling the design. @@ -278,7 +278,7 @@ def main(interactive: bool = True) -> None: # lib['my_circuit'] = circ_pat # circ_pat.place(lib.abstract('wg10'), ...) # circ_pat.plug(lib.abstract('wg10'), ...) - # but `Pather` removes some repeated `lib.abstract(...)` boilerplate and keeps + # but `Builder` removes some repeated `lib.abstract(...)` boilerplate and keeps # the assembly code focused on port-level intent. # Attach a y-splitter to the signal path. diff --git a/examples/tutorial/library.py b/examples/tutorial/library.py index 1b9a1da..faaa5a1 100644 --- a/examples/tutorial/library.py +++ b/examples/tutorial/library.py @@ -1,5 +1,5 @@ """ -Tutorial: using `LazyLibrary` and `Pather.interface()`. +Tutorial: using `LazyLibrary` and `Builder.interface()`. This example assumes you have already read `devices.py` and generated the `circuit.gds` file it writes. The goal here is not the photonic-crystal geometry @@ -10,7 +10,7 @@ from typing import Any from pprint import pformat -from masque import Pather, LazyLibrary +from masque import Builder, LazyLibrary from masque.file.gdsii import writefile, load_libraryfile import basic_shapes @@ -64,10 +64,10 @@ def main() -> None: # Start a new design by copying the ports from an existing library cell. # This gives `circ2` the same external interface as `tri_l3cav`. - circ2 = Pather(library=lib, ports='tri_l3cav') + circ2 = Builder(library=lib, ports='tri_l3cav') # First way to specify what we are plugging in: request an explicit abstract. - # This works with `Pattern` methods directly as well as with `Pather`. + # This works with `Pattern` methods directly as well as with `Builder`. circ2.plug(lib.abstract('wg10'), {'input': 'right'}) # Second way: use an `AbstractView`, which behaves like a mapping of names @@ -75,7 +75,7 @@ def main() -> None: abstracts = lib.abstract_view() circ2.plug(abstracts['wg10'], {'output': 'left'}) - # Third way: let `Pather` resolve a pattern name through its own library. + # Third way: let `Builder` resolve a pattern name through its own library. # This shorthand is convenient, but it is specific to helpers that already # carry a library reference. circ2.plug('tri_wg10', {'input': 'right'}) @@ -89,10 +89,10 @@ def main() -> None: # Build a second device that is explicitly designed to mate with `circ2`. # - # `Pather.interface()` makes a new pattern whose ports mirror an existing + # `Builder.interface()` makes a new pattern whose ports mirror an existing # design's external interface. That is useful when you want to design an # adapter, continuation, or mating structure. - circ3 = Pather.interface(source=circ2) + circ3 = Builder.interface(source=circ2) # Continue routing outward from those inherited ports. circ3.plug('tri_bend0', {'input': 'right'}) diff --git a/examples/tutorial/pather.py b/examples/tutorial/pather.py index 5cc5a61..386384a 100644 --- a/examples/tutorial/pather.py +++ b/examples/tutorial/pather.py @@ -204,9 +204,9 @@ def prepare_tools() -> tuple[Library, Tool, Tool]: # # Now we can start building up our library (collection of static cells) and pathing tools. # -# If any of the operations below are confusing, you can cross-reference against the deferred -# `Pather` tutorial, which handles some things more explicitly (e.g. via placement) and simplifies -# others (e.g. geometry definition). +# If any of the operations below are confusing, you can cross-reference against the `RenderPather` +# tutorial, which handles some things more explicitly (e.g. via placement) and simplifies others +# (e.g. geometry definition). # def main() -> None: library, M1_tool, M2_tool = prepare_tools() diff --git a/examples/tutorial/port_pather.py b/examples/tutorial/port_pather.py index ab942d7..6d41a39 100644 --- a/examples/tutorial/port_pather.py +++ b/examples/tutorial/port_pather.py @@ -1,7 +1,7 @@ """ PortPather tutorial: Using .at() syntax """ -from masque import Pather, Pattern, Port, R90 +from masque import RenderPather, Pattern, Port, R90 from masque.file.gdsii import writefile from basic_shapes import GDS_OPTS @@ -12,8 +12,8 @@ def main() -> None: # Reuse the same patterns (pads, bends, vias) and tools as in pather.py library, M1_tool, M2_tool = prepare_tools() - # Create a deferred Pather and place some initial pads (same as Pather tutorial) - rpather = Pather(library, tools=M2_tool, auto_render=False) + # Create a RenderPather and place some initial pads (same as Pather tutorial) + rpather = RenderPather(library, tools=M2_tool) rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'}) @@ -156,7 +156,7 @@ def main() -> None: # # Rendering and Saving # - # Since we deferred auto-rendering, we must call .render() to generate the geometry. + # Since we used RenderPather, we must call .render() to generate the geometry. rpather.render() library['PortPather_Tutorial'] = rpather.pattern diff --git a/examples/tutorial/renderpather.py b/examples/tutorial/renderpather.py index 4b43b19..7b75f5d 100644 --- a/examples/tutorial/renderpather.py +++ b/examples/tutorial/renderpather.py @@ -1,7 +1,7 @@ """ -Manual wire routing tutorial: deferred Pather and PathTool +Manual wire routing tutorial: RenderPather an PathTool """ -from masque import Pather, Library +from masque import RenderPather, Library from masque.builder.tools import PathTool from masque.file.gdsii import writefile @@ -11,9 +11,9 @@ from pather import M1_WIDTH, V1_WIDTH, M2_WIDTH, map_layer, make_pad, make_via def main() -> None: # - # To illustrate deferred routing with `Pather`, we use `PathTool` instead + # To illustrate the advantages of using `RenderPather`, we use `PathTool` instead # of `AutoTool`. `PathTool` lacks some sophistication (e.g. no automatic transitions) - # but when used with `Pather(auto_render=False)`, it can consolidate multiple routing steps into + # but when used with `RenderPather`, it can consolidate multiple routing steps into # a single `Path` shape. # # We'll try to nearly replicate the layout from the `Pather` tutorial; see `pather.py` @@ -39,7 +39,7 @@ def main() -> None: # and what port type to present. M1_ptool = PathTool(layer='M1', width=M1_WIDTH, ptype='m1wire') M2_ptool = PathTool(layer='M2', width=M2_WIDTH, ptype='m2wire') - rpather = Pather(tools=M2_ptool, library=library, auto_render=False) + rpather = RenderPather(tools=M2_ptool, library=library) # As in the pather tutorial, we make some pads and labels... rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) @@ -85,7 +85,7 @@ def main() -> None: # Render the path we defined rpather.render() - library['Deferred_Pather_and_PathTool'] = rpather.pattern + library['RenderPather_and_PathTool'] = rpather.pattern # Convert from text-based layers to numeric layers for GDS, and output the file diff --git a/masque/__init__.py b/masque/__init__.py index 8e13bb9..e435fac 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -73,8 +73,10 @@ from .ports import ( ) from .abstract import Abstract as Abstract from .builder import ( + Builder as Builder, Tool as Tool, Pather as Pather, + RenderPather as RenderPather, RenderStep as RenderStep, SimpleTool as SimpleTool, AutoTool as AutoTool, diff --git a/masque/builder/__init__.py b/masque/builder/__init__.py index a8f4cc0..65958c1 100644 --- a/masque/builder/__init__.py +++ b/masque/builder/__init__.py @@ -1,6 +1,8 @@ from .pather import ( Pather as Pather, PortPather as PortPather, + Builder as Builder, + RenderPather as RenderPather, ) from .utils import ell as ell from .tools import ( diff --git a/masque/builder/logging.py b/masque/builder/logging.py index b4a113b..78a566e 100644 --- a/masque/builder/logging.py +++ b/masque/builder/logging.py @@ -1,5 +1,5 @@ """ -Logging and operation decorators for Pather +Logging and operation decorators for Builder/Pather """ from typing import TYPE_CHECKING, Any from collections.abc import Iterator, Sequence, Callable @@ -31,7 +31,7 @@ def _format_log_args(**kwargs) -> str: class PatherLogger: """ - Encapsulates state for Pather diagnostic logging. + Encapsulates state for Pather/Builder diagnostic logging. """ debug: bool indent: int @@ -90,7 +90,7 @@ def logged_op( portspec_getter: Callable[[dict[str, Any]], str | Sequence[str] | None] | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """ - Decorator to wrap Pather methods with logging. + Decorator to wrap Builder methods with logging. """ def decorator(func: Callable[..., Any]) -> Callable[..., Any]: sig = inspect.signature(func) diff --git a/masque/builder/pather.py b/masque/builder/pather.py index a7bbedd..e190203 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -38,9 +38,11 @@ class Pather(PortList): The `Pather` holds context in the form of a `Library`, its underlying pattern, and a set of `Tool`s for generating routing segments. - Routing operations (`trace`, `jog`, `uturn`, etc.) are rendered - incrementally by default. Set `auto_render=False` to defer geometry - generation until an explicit call to `render()`. + Routing operations (`trace`, `jog`, `uturn`, etc.) are by default + deferred: they record the intended path but do not immediately generate + geometry. `render()` must be called to generate the final layout. + Alternatively, setting `auto_render=True` in the constructor will + cause geometry to be generated incrementally after each routing step. Examples: Creating a Pather =========================== @@ -56,8 +58,8 @@ class Pather(PortList): connects port 'A' of the current pattern to port 'C' of `subdevice`. - `pather.trace('my_port', ccw=True, length=100)` plans a 100-unit bend - starting at 'my_port'. Geometry is added immediately by default. - Set `auto_render=False` to defer and call `pather.render()` later. + starting at 'my_port'. If `auto_render=True`, geometry is added + immediately. Otherwise, call `pather.render()` later. """ __slots__ = ( 'pattern', 'library', 'tools', 'paths', @@ -116,7 +118,7 @@ class Pather(PortList): tools: Tool | MutableMapping[str | None, Tool] | None = None, name: str | None = None, debug: bool = False, - auto_render: bool = True, + auto_render: bool = False, auto_render_append: bool = True, ) -> None: """ @@ -1356,3 +1358,53 @@ class PortPather: self.pather.rename_ports({name: None}) self.ports = [pp for pp in self.ports if pp != name] return self + + +class Builder(Pather): + """ + Backward-compatible wrapper for Pather with auto_render=True. + """ + 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, + debug: bool = False, + ) -> None: + super().__init__( + library=library, + pattern=pattern, + ports=ports, + tools=tools, + name=name, + debug=debug, + auto_render=True, + ) + + +class RenderPather(Pather): + """ + Backward-compatible wrapper for Pather with auto_render=False. + """ + 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, + debug: bool = False, + ) -> None: + super().__init__( + library=library, + pattern=pattern, + ports=ports, + tools=tools, + name=name, + debug=debug, + auto_render=False, + ) diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 44318d1..f0772a1 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -25,8 +25,8 @@ from ..error import BuildError @dataclass(frozen=True, slots=True) class RenderStep: """ - Representation of a single saved operation, used by deferred `Pather` - instances and passed to `Tool.render()` when `Pather.render()` is called. + Representation of a single saved operation, used by `RenderPather` and passed + to `Tool.render()` when `RenderPather.render()` is called. """ opcode: Literal['L', 'S', 'U', 'P'] """ What operation is being performed. @@ -128,7 +128,7 @@ class Tool: Create a wire or waveguide that travels exactly `length` distance along the axis of its input port. - Used by `Pather`. + Used by `Pather` and `RenderPather`. The output port must be exactly `length` away along the input port's axis, but may be placed an additional (unspecified) distance away along the perpendicular @@ -174,7 +174,7 @@ class Tool: of its input port, and `jog` distance on the perpendicular axis. `jog` is positive when moving left of the direction of travel (from input to ouput port). - Used by `Pather`. + Used by `Pather` and `RenderPather`. The output port should be rotated to face the input port (i.e. plugging the device into a port will move that port but keep its orientation). @@ -214,7 +214,7 @@ class Tool: Plan a wire or waveguide that travels exactly `length` distance along the axis of its input port. - Used by `Pather` when `auto_render=False`. + Used by `RenderPather`. The output port must be exactly `length` away along the input port's axis, but may be placed an additional (unspecified) distance away along the perpendicular @@ -266,7 +266,7 @@ class Tool: Plan a wire or waveguide that travels exactly `length` distance along the axis of its input port and `jog` distance along the perpendicular axis (i.e. an S-bend). - Used by `Pather` when `auto_render=False`. + Used by `RenderPather`. The output port must have an orientation rotated by pi from the input port. @@ -315,7 +315,7 @@ class Tool: Create a wire or waveguide that travels exactly `jog` distance along the axis perpendicular to its input port (i.e. a U-bend). - Used by `Pather`. Tools may leave this unimplemented if they + Used by `Pather` and `RenderPather`. Tools may leave this unimplemented if they do not support a native U-bend primitive. The output port must have an orientation identical to the input port. @@ -354,7 +354,7 @@ class Tool: Plan a wire or waveguide that travels exactly `jog` distance along the axis perpendicular to its input port (i.e. a U-bend). - Used by `Pather` when `auto_render=False`. This is an optional native-planning hook: tools may + Used by `RenderPather`. This is an optional native-planning hook: tools may implement it when they can represent a U-turn directly, otherwise they may rely on `traceU()` or let `Pather` synthesize the route from simpler primitives. @@ -1238,39 +1238,6 @@ class PathTool(Tool, metaclass=ABCMeta): # self.width = width # self.ptype: str - def _check_out_ptype(self, out_ptype: str | None) -> None: - if out_ptype and out_ptype != self.ptype: - raise BuildError(f'Requested {out_ptype=} does not match path ptype {self.ptype}') - - def _bend_radius(self) -> float: - return self.width / 2 - - def _plan_l_vertices(self, length: float, bend_run: float) -> NDArray[numpy.float64]: - vertices = [(0.0, 0.0), (length, 0.0)] - if not numpy.isclose(bend_run, 0): - vertices.append((length, bend_run)) - return numpy.array(vertices, dtype=float) - - def _plan_s_vertices(self, length: float, jog: float) -> NDArray[numpy.float64]: - if numpy.isclose(jog, 0): - return numpy.array([(0.0, 0.0), (length, 0.0)], dtype=float) - - if length < self.width: - raise BuildError( - f'Asked to draw S-path with total length {length:,g}, shorter than required bend: {self.width:,g}' - ) - - # Match AutoTool's straight-then-s-bend placement so the jog happens - # width/2 before the end while still allowing smaller lateral offsets. - jog_x = length - self._bend_radius() - vertices = [ - (0.0, 0.0), - (jog_x, 0.0), - (jog_x, jog), - (length, jog), - ] - return numpy.array(vertices, dtype=float) - def traceL( self, ccw: SupportsBool | None, @@ -1281,7 +1248,7 @@ class PathTool(Tool, metaclass=ABCMeta): port_names: tuple[str, str] = ('A', 'B'), **kwargs, # noqa: ARG002 (unused) ) -> Library: - out_port, data = self.planL( + out_port, _data = self.planL( ccw, length, in_ptype=in_ptype, @@ -1289,7 +1256,12 @@ class PathTool(Tool, metaclass=ABCMeta): ) tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') - pat.path(layer=self.layer, width=self.width, vertices=self._plan_l_vertices(length, float(out_port.y))) + vertices: list[tuple[float, float]] + if ccw is None: + vertices = [(0.0, 0.0), (length, 0.0)] + else: + vertices = [(0.0, 0.0), (length, 0.0), tuple(out_port.offset)] + pat.path(layer=self.layer, width=self.width, vertices=vertices) if ccw is None: out_rot = pi @@ -1316,10 +1288,11 @@ class PathTool(Tool, metaclass=ABCMeta): ) -> tuple[Port, NDArray[numpy.float64]]: # TODO check all the math for L-shaped bends - self._check_out_ptype(out_ptype) + if out_ptype and out_ptype != self.ptype: + raise BuildError(f'Requested {out_ptype=} does not match path ptype {self.ptype}') if ccw is not None: - bend_dxy = numpy.array([1, -1]) * self._bend_radius() + bend_dxy = numpy.array([1, -1]) * self.width / 2 bend_angle = pi / 2 if bool(ccw): @@ -1340,46 +1313,6 @@ class PathTool(Tool, metaclass=ABCMeta): out_port = Port(data, rotation=bend_angle, ptype=self.ptype) return out_port, data - def traceS( - self, - length: float, - jog: float, - *, - in_ptype: str | None = None, - out_ptype: str | None = None, - port_names: tuple[str, str] = ('A', 'B'), - **kwargs, # noqa: ARG002 (unused) - ) -> Library: - out_port, _data = self.planS( - length, - jog, - in_ptype=in_ptype, - out_ptype=out_ptype, - ) - - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS') - pat.path(layer=self.layer, width=self.width, vertices=self._plan_s_vertices(length, jog)) - pat.ports = { - port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype), - port_names[1]: out_port, - } - return tree - - def planS( - self, - length: float, - jog: float, - *, - in_ptype: str | None = None, # noqa: ARG002 (unused) - out_ptype: str | None = None, - **kwargs, # noqa: ARG002 (unused) - ) -> tuple[Port, NDArray[numpy.float64]]: - self._check_out_ptype(out_ptype) - self._plan_s_vertices(length, jog) - data = numpy.array((length, jog)) - out_port = Port((length, jog), rotation=pi, ptype=self.ptype) - return out_port, data - def render( self, batch: Sequence[RenderStep], @@ -1390,7 +1323,7 @@ class PathTool(Tool, metaclass=ABCMeta): # Transform the batch so the first port is local (at 0,0) but retains its global rotation. # This allows the path to be rendered with its original orientation, simplified by - # translation to the origin. Pather.render will handle the final placement + # translation to the origin. RenderPather.render will handle the final placement # (including rotation alignment) via `pat.plug`. first_port = batch[0].start_port translation = -first_port.offset @@ -1408,18 +1341,19 @@ class PathTool(Tool, metaclass=ABCMeta): # Masque convention: Port rotation points INTO the device. # So the direction of travel for the path is AWAY from the port, i.e., port_rot + pi. assert port_rot is not None - transform = rotation_matrix_2d(port_rot + pi) - delta = step.end_port.offset - step.start_port.offset - local_end = rotation_matrix_2d(-(port_rot + pi)) @ delta + if step.opcode == 'L': - local_vertices = self._plan_l_vertices(float(local_end[0]), float(local_end[1])) - elif step.opcode == 'S': - local_vertices = self._plan_s_vertices(float(local_end[0]), float(local_end[1])) + + length, _ = step.data + dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0) + path_vertices.append(step.start_port.offset + dxy) else: raise BuildError(f'Unrecognized opcode "{step.opcode}"') - for vertex in local_vertices[1:]: - path_vertices.append(step.start_port.offset + transform @ vertex) + # Check if the last vertex added is already at the end port location + if not numpy.allclose(path_vertices[-1], local_batch[-1].end_port.offset): + # If the path ends in a bend, we need to add the final vertex + path_vertices.append(local_batch[-1].end_port.offset) tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') pat.path(layer=self.layer, width=self.width, vertices=path_vertices) diff --git a/masque/pattern.py b/masque/pattern.py index e882795..dfa45c7 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -38,8 +38,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): 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 `builder.Pather` - and `ports.PortsList`. + 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)` @@ -1664,7 +1664,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): current device. Args: - source: A collection of ports (e.g. Pattern, Pather, or dict) + 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. diff --git a/masque/ports.py b/masque/ports.py index e745880..323050f 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -273,12 +273,6 @@ class PortList(metaclass=ABCMeta): else: # noqa: RET505 return {k: self.ports[k] for k in key} - def measure_travel(self, src: str, dst: str) -> tuple[NDArray[numpy.float64], float | None]: - """ - Convenience wrapper for measuring travel between two named ports. - """ - return self[src].measure_travel(self[dst]) - def __contains__(self, key: str) -> NoReturn: raise NotImplementedError('PortsList.__contains__ is left unimplemented. Use `key in container.ports` instead.') diff --git a/masque/test/test_autotool_refactor.py b/masque/test/test_autotool_refactor.py index d5f1c86..d93f935 100644 --- a/masque/test/test_autotool_refactor.py +++ b/masque/test/test_autotool_refactor.py @@ -6,7 +6,7 @@ from masque.builder.tools import AutoTool from masque.pattern import Pattern from masque.ports import Port from masque.library import Library -from masque.builder.pather import Pather +from masque.builder.pather import Pather, RenderPather def make_straight(length, width=2, ptype="wire"): pat = Pattern() @@ -166,7 +166,7 @@ def test_autotool_planS_pure_sbend_with_transition_dx() -> None: def test_renderpather_autotool_double_L(multi_bend_tool) -> None: tool, lib = multi_bend_tool - rp = Pather(lib, tools=tool, auto_render=False) + rp = RenderPather(lib, tools=tool) rp.ports["A"] = Port((0,0), 0, ptype="wire") # This should trigger double-L fallback in planS diff --git a/masque/test/test_builder.py b/masque/test/test_builder.py index 9f73d2b..309dab6 100644 --- a/masque/test/test_builder.py +++ b/masque/test/test_builder.py @@ -3,7 +3,7 @@ import pytest from numpy.testing import assert_equal, assert_allclose from numpy import pi -from ..builder import Pather +from ..builder import Builder from ..builder.utils import ell from ..error import BuildError from ..library import Library @@ -13,7 +13,7 @@ from ..ports import Port def test_builder_init() -> None: lib = Library() - b = Pather(lib, name="mypat") + b = Builder(lib, name="mypat") assert b.pattern is lib["mypat"] assert b.library is lib @@ -24,7 +24,7 @@ def test_builder_place() -> None: child.ports["A"] = Port((0, 0), 0) lib["child"] = child - b = Pather(lib) + b = Builder(lib) b.place("child", offset=(10, 20), port_map={"A": "child_A"}) assert "child_A" in b.ports @@ -40,7 +40,7 @@ def test_builder_plug() -> None: wire.ports["out"] = Port((10, 0), pi) lib["wire"] = wire - b = Pather(lib) + b = Builder(lib) b.ports["start"] = Port((100, 100), 0) # Plug wire's "in" port into builder's "start" port @@ -64,7 +64,7 @@ def test_builder_interface() -> None: source.ports["P1"] = Port((0, 0), 0) lib["source"] = source - b = Pather.interface("source", library=lib, name="iface") + b = Builder.interface("source", library=lib, name="iface") assert "in_P1" in b.ports assert "P1" in b.ports assert b.pattern is lib["iface"] @@ -73,7 +73,7 @@ def test_builder_interface() -> None: def test_builder_set_dead() -> None: lib = Library() lib["sub"] = Pattern() - b = Pather(lib) + b = Builder(lib) b.set_dead() b.place("sub") @@ -84,7 +84,7 @@ def test_builder_dead_ports() -> None: lib = Library() pat = Pattern() pat.ports['A'] = Port((0, 0), 0) - b = Pather(lib, pattern=pat) + b = Builder(lib, pattern=pat) b.set_dead() # Attempt to plug a device where ports don't line up @@ -107,7 +107,7 @@ def test_dead_plug_best_effort() -> None: lib = Library() pat = Pattern() pat.ports['A'] = Port((0, 0), 0) - b = Pather(lib, pattern=pat) + b = Builder(lib, pattern=pat) b.set_dead() # Device with multiple ports, none of which line up correctly diff --git a/masque/test/test_pather_api.py b/masque/test/test_pather_api.py index 5f84101..2e841ad 100644 --- a/masque/test/test_pather_api.py +++ b/masque/test/test_pather_api.py @@ -3,14 +3,14 @@ from typing import Any import pytest import numpy from numpy import pi -from masque import Pather, Library, Pattern, Port +from masque import Pather, RenderPather, Library, Pattern, Port from masque.builder.tools import PathTool, Tool from masque.error import BuildError, PortError, PatternError def test_pather_trace_basic() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) # Port rotation 0 points in +x (INTO device). # To extend it, we move in -x direction. @@ -35,7 +35,7 @@ def test_pather_trace_basic() -> None: def test_pather_trace_to() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) @@ -50,7 +50,7 @@ def test_pather_trace_to() -> None: def test_pather_bundle_trace() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 2000), rotation=0) @@ -74,7 +74,7 @@ def test_pather_bundle_trace() -> None: def test_pather_each_bound() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((-1000, 2000), rotation=0) @@ -198,7 +198,7 @@ def test_rename() -> None: def test_renderpather_uturn_fallback() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - rp = Pather(lib, tools=tool, auto_render=False) + rp = RenderPather(lib, tools=tool) rp.pattern.ports['A'] = Port((0, 0), rotation=0) # PathTool doesn't implement planU, so it should fall back to two planL calls @@ -239,7 +239,7 @@ def test_autotool_uturn() -> None: default_out_ptype='wire' ) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), 0) # CW U-turn (jog < 0) @@ -261,7 +261,7 @@ def test_autotool_uturn() -> None: def test_pather_trace_into() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) # 1. Straight connector p.pattern.ports['A'] = Port((0, 0), rotation=0) @@ -313,7 +313,7 @@ def test_pather_trace_into() -> None: def test_pather_trace_into_dead_updates_ports_without_geometry() -> None: lib = Library() tool = PathTool(layer='M1', width=1000, ptype='wire') - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.pattern.ports['B'] = Port((-10000, 0), rotation=pi, ptype='wire') p.set_dead() @@ -332,7 +332,7 @@ def test_pather_trace_into_dead_updates_ports_without_geometry() -> None: def test_pather_dead_fallback_preserves_out_ptype() -> None: lib = Library() tool = PathTool(layer='M1', width=1000, ptype='wire') - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.set_dead() @@ -407,26 +407,13 @@ def test_pather_jog_failed_fallback_is_atomic() -> None: p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') with pytest.raises(BuildError, match='shorter than required bend'): - p.jog('A', 1.5, length=1.5) + p.jog('A', 1.5, length=5) assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0)) assert p.pattern.ports['A'].rotation == 0 assert len(p.paths['A']) == 0 -def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None: - lib = Library() - tool = PathTool(layer='M1', width=2, ptype='wire') - p = Pather(lib, tools=tool) - p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') - - p.jog('A', 1.5, length=5) - - assert numpy.allclose(p.pattern.ports['A'].offset, (-5, -1.5)) - assert p.pattern.ports['A'].rotation == 0 - assert len(p.paths['A']) == 0 - - def test_pather_jog_length_solved_from_single_position_bound() -> None: lib = Library() tool = PathTool(layer='M1', width=1, ptype='wire') @@ -687,7 +674,7 @@ def test_pather_uturn_failed_fallback_is_atomic() -> None: def test_renderpather_rename_to_none_keeps_pending_geometry_without_port() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - rp = Pather(lib, tools=tool, auto_render=False) + rp = RenderPather(lib, tools=tool) rp.pattern.ports['A'] = Port((0, 0), rotation=0) rp.at('A').straight(5000) @@ -735,7 +722,7 @@ def test_pather_plug_treeview_resolves_once() -> None: def test_pather_failed_plug_does_not_add_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.annotations = {'k': [1]} p.pattern.ports['A'] = Port((0, 0), rotation=0) @@ -757,7 +744,7 @@ def test_pather_failed_plug_does_not_add_break_marker() -> None: def test_pather_place_reused_deleted_name_keeps_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.at('A').straight(5000) @@ -778,7 +765,7 @@ def test_pather_place_reused_deleted_name_keeps_break_marker() -> None: def test_pather_plug_reused_deleted_name_keeps_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 0), rotation=0) @@ -806,7 +793,7 @@ def test_pather_plug_reused_deleted_name_keeps_break_marker() -> None: def test_pather_failed_plugged_does_not_add_break_marker() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool, auto_render=False) + p = Pather(lib, tools=tool) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.at('A').straight(5000) diff --git a/masque/test/test_ports.py b/masque/test/test_ports.py index 6d24879..fa19bab 100644 --- a/masque/test/test_ports.py +++ b/masque/test/test_ports.py @@ -47,29 +47,6 @@ def test_port_measure_travel() -> None: assert rotation == pi -def test_port_list_measure_travel() -> None: - class MyPorts(PortList): - def __init__(self) -> None: - self._ports = { - "A": Port((0, 0), 0), - "B": Port((10, 5), pi), - } - - @property - def ports(self) -> dict[str, Port]: - return self._ports - - @ports.setter - def ports(self, val: dict[str, Port]) -> None: - self._ports = val - - pl = MyPorts() - (travel, jog), rotation = pl.measure_travel("A", "B") - assert travel == 10 - assert jog == 5 - assert rotation == pi - - def test_port_describe_any_rotation() -> None: p = Port((0, 0), None) assert p.describe() == "pos=(0, 0), rot=any" diff --git a/masque/test/test_renderpather.py b/masque/test/test_renderpather.py index b518a1f..0da9588 100644 --- a/masque/test/test_renderpather.py +++ b/masque/test/test_renderpather.py @@ -3,7 +3,7 @@ from typing import cast, TYPE_CHECKING from numpy.testing import assert_allclose from numpy import pi -from ..builder import Pather +from ..builder import RenderPather from ..builder.tools import PathTool from ..library import Library from ..ports import Port @@ -13,15 +13,15 @@ if TYPE_CHECKING: @pytest.fixture -def rpather_setup() -> tuple[Pather, PathTool, Library]: +def rpather_setup() -> tuple[RenderPather, PathTool, Library]: lib = Library() tool = PathTool(layer=(1, 0), width=2, ptype="wire") - rp = Pather(lib, tools=tool, auto_render=False) + rp = RenderPather(lib, tools=tool) rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire") return rp, tool, lib -def test_renderpather_basic(rpather_setup: tuple[Pather, PathTool, Library]) -> None: +def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan two segments rp.at("start").straight(10).straight(10) @@ -46,7 +46,7 @@ def test_renderpather_basic(rpather_setup: tuple[Pather, PathTool, Library]) -> assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10) -def test_renderpather_bend(rpather_setup: tuple[Pather, PathTool, Library]) -> None: +def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan straight then bend rp.at("start").straight(10).cw(10) @@ -65,21 +65,7 @@ def test_renderpather_bend(rpather_setup: tuple[Pather, PathTool, Library]) -> N assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20], [-1, -20]], atol=1e-10) -def test_renderpather_jog_uses_native_pathtool_planS(rpather_setup: tuple[Pather, PathTool, Library]) -> None: - rp, tool, lib = rpather_setup - rp.at("start").jog(4, length=10) - - assert len(rp.paths["start"]) == 1 - assert rp.paths["start"][0].opcode == "S" - - rp.render() - path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0]) - # Native PathTool S-bends place the jog width/2 before the route end. - assert_allclose(path_shape.vertices, [[0, 0], [0, -9], [4, -9], [4, -10]], atol=1e-10) - assert_allclose(rp.ports["start"].offset, [4, -10], atol=1e-10) - - -def test_renderpather_mirror_preserves_planned_bend_geometry(rpather_setup: tuple[Pather, PathTool, Library]) -> None: +def test_renderpather_mirror_preserves_planned_bend_geometry(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup rp.at("start").straight(10).cw(10) @@ -90,7 +76,7 @@ def test_renderpather_mirror_preserves_planned_bend_geometry(rpather_setup: tupl assert_allclose(path_shape.vertices, [[0, 0], [0, 10], [0, 20], [-1, 20]], atol=1e-10) -def test_renderpather_retool(rpather_setup: tuple[Pather, PathTool, Library]) -> None: +def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool1, lib = rpather_setup tool2 = PathTool(layer=(2, 0), width=4, ptype="wire") @@ -104,7 +90,7 @@ def test_renderpather_retool(rpather_setup: tuple[Pather, PathTool, Library]) -> assert len(rp.pattern.shapes[(2, 0)]) == 1 -def test_portpather_translate_only_affects_future_steps(rpather_setup: tuple[Pather, PathTool, Library]) -> None: +def test_portpather_translate_only_affects_future_steps(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup pp = rp.at("start") pp.straight(10) @@ -123,7 +109,7 @@ def test_portpather_translate_only_affects_future_steps(rpather_setup: tuple[Pat def test_renderpather_dead_ports() -> None: lib = Library() tool = PathTool(layer=(1, 0), width=1) - rp = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool, auto_render=False) + rp = RenderPather(lib, ports={"in": Port((0, 0), 0)}, tools=tool) rp.set_dead() # Impossible path @@ -140,7 +126,7 @@ def test_renderpather_dead_ports() -> None: assert not rp.pattern.has_shapes() -def test_renderpather_rename_port(rpather_setup: tuple[Pather, PathTool, Library]) -> None: +def test_renderpather_rename_port(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup rp.at("start").straight(10) # Rename port while path is planned @@ -162,7 +148,7 @@ def test_renderpather_rename_port(rpather_setup: tuple[Pather, PathTool, Library assert_allclose(rp.ports["new_start"].offset, [0, -20], atol=1e-10) -def test_renderpather_drop_keeps_pending_geometry_without_port(rpather_setup: tuple[Pather, PathTool, Library]) -> None: +def test_renderpather_drop_keeps_pending_geometry_without_port(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup rp.at("start").straight(10).drop() @@ -185,15 +171,3 @@ def test_pathtool_traceL_bend_geometry_matches_ports() -> None: assert_allclose(path_shape.vertices, [[0, 0], [10, 0], [10, 1]], atol=1e-10) assert_allclose(pat.ports["B"].offset, [10, 1], atol=1e-10) - - -def test_pathtool_traceS_geometry_matches_ports() -> None: - tool = PathTool(layer=(1, 0), width=2, ptype="wire") - - tree = tool.traceS(10, 4) - pat = tree.top_pattern() - path_shape = cast("Path", pat.shapes[(1, 0)][0]) - - assert_allclose(path_shape.vertices, [[0, 0], [9, 0], [9, 4], [10, 4]], atol=1e-10) - assert_allclose(pat.ports["B"].offset, [10, 4], atol=1e-10) - assert_allclose(pat.ports["B"].rotation, pi, atol=1e-10)