Compare commits
No commits in common. "6cf96001932d26ac327320b2f35607b817db5fbd" and "02f0833fb3407c19ddf18da62a8af120f332ed1e" have entirely different histories.
6cf9600193
...
02f0833fb3
20 changed files with 173 additions and 285 deletions
54
MIGRATION.md
54
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(...))
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue