[Pather] move render check from __del__ to __exit__ and use render= enum instead of auto_render
This commit is contained in:
parent
84664303f1
commit
9a39a436b2
13 changed files with 277 additions and 126 deletions
|
|
@ -29,7 +29,7 @@ Contents
|
|||
* Define a custom `Tool` that exposes primitive routing offers
|
||||
* Use primitive offers 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 `Pather(render='deferred')` 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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ def main() -> None:
|
|||
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)
|
||||
rpather = Pather(library, tools=M2_tool, render='deferred')
|
||||
|
||||
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'})
|
||||
|
|
@ -157,7 +157,7 @@ def main() -> None:
|
|||
#
|
||||
# Rendering and Saving
|
||||
#
|
||||
# Since we deferred auto-rendering, we must call .render() to generate the geometry.
|
||||
# Since routing is deferred, we must call .render() to generate the geometry.
|
||||
rpather.render()
|
||||
|
||||
library['PortPather_Tutorial'] = rpather.pattern
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ def main() -> None:
|
|||
#
|
||||
# To illustrate deferred routing with `Pather`, 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 `Pather(render='deferred')`, 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 = Pather(tools=M2_ptool, library=library, render='deferred')
|
||||
|
||||
# As in the pather tutorial, we make some pads and labels...
|
||||
rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Unified Pattern assembly and routing (`Pather`).
|
|||
|
||||
`Pather` is the public object that owns layout state. It holds the working
|
||||
Pattern, Library, per-port Tool assignments, pending `RenderStep`s, and routing
|
||||
side effects such as plug consumption, port renames, and auto-render. The
|
||||
side effects such as plug consumption, port renames, and automatic rendering. The
|
||||
planner package is intentionally internal: custom route generators should
|
||||
extend `Tool.primitive_offers()` and `Tool.render()` rather than depending on
|
||||
planner classes or search details.
|
||||
|
|
@ -16,7 +16,7 @@ Routing is split into four ownership phases:
|
|||
`RenderStep.data`, and returns prepared actions,
|
||||
- application: `Pather` appends the prepared steps to its pending queue,
|
||||
replaces live output ports, consumes plug destinations, applies deferred
|
||||
trace-thru renames, and batches auto-render around the whole route,
|
||||
trace-thru renames, and batches immediate rendering around the whole route,
|
||||
- rendering: pending steps are grouped by live port, Tool, and continuity before
|
||||
`Tool.render()` builds geometry for each compatible batch.
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ prepared actions are applied, later commit, plug, rename, render, or insertion
|
|||
failures may leave partial output; that mutation boundary belongs to Pather,
|
||||
not to Tool implementations or the route solver.
|
||||
"""
|
||||
from typing import Self, Any, overload
|
||||
from typing import Self, Any, Literal, overload
|
||||
from collections.abc import Iterator, Iterable, Mapping, MutableMapping, Sequence
|
||||
import copy
|
||||
import logging
|
||||
|
|
@ -36,6 +36,7 @@ from functools import wraps
|
|||
from pprint import pformat
|
||||
from contextlib import contextmanager
|
||||
from itertools import chain
|
||||
from types import TracebackType
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
|
@ -64,6 +65,8 @@ from .logging import PatherLogger
|
|||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
RenderPolicy = Literal['auto', 'immediate', 'deferred', 'warn', 'error', 'ignore']
|
||||
RENDER_POLICIES: tuple[RenderPolicy, ...] = ('auto', 'immediate', 'deferred', 'warn', 'error', 'ignore')
|
||||
|
||||
|
||||
class Pather(PortList):
|
||||
|
|
@ -76,9 +79,9 @@ class Pather(PortList):
|
|||
pattern, and a set of `Tool`s for generating routing segments.
|
||||
|
||||
Routing operations (`trace`, `jog`, `uturn`, etc.) select primitive offers
|
||||
from the active `Tool` and compose them into deferred `RenderStep`s.
|
||||
Set `auto_render=False` to defer geometry generation until an explicit call
|
||||
to `render()`.
|
||||
from the active `Tool` and compose them into `RenderStep`s. By default,
|
||||
geometry is rendered after each route, unless the `Pather` is used as a
|
||||
context manager.
|
||||
|
||||
Examples: Creating a Pather
|
||||
===========================
|
||||
|
|
@ -94,12 +97,12 @@ 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 the `Pather` is used as a context manager,
|
||||
geometry is generated on clean context exit.
|
||||
"""
|
||||
__slots__ = (
|
||||
'pattern', 'library', 'tools', 'planner', '_paths',
|
||||
'_dead', '_logger', '_auto_render', '_auto_render_append'
|
||||
'_dead', '_logger', '_render_policy', '_render_append', '_context_depth'
|
||||
)
|
||||
|
||||
pattern: Pattern
|
||||
|
|
@ -128,8 +131,14 @@ class Pather(PortList):
|
|||
_logger: PatherLogger
|
||||
""" Handles diagnostic logging of operations """
|
||||
|
||||
_auto_render: bool
|
||||
""" If True, routing operations call render() immediately """
|
||||
_render_policy: RenderPolicy
|
||||
""" Routing render behavior """
|
||||
|
||||
_render_append: bool
|
||||
""" If True, automatic render calls append geometry instead of adding references """
|
||||
|
||||
_context_depth: int
|
||||
""" Number of active context-manager entries """
|
||||
|
||||
_paths: defaultdict[str, list[RenderStep]]
|
||||
""" Per-port pending render steps, consumed by `render()` """
|
||||
|
|
@ -168,8 +177,8 @@ class Pather(PortList):
|
|||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
||||
name: str | None = None,
|
||||
debug: bool = False,
|
||||
auto_render: bool = True,
|
||||
auto_render_append: bool = True,
|
||||
render: RenderPolicy = 'auto',
|
||||
render_append: bool = True,
|
||||
planner: RoutingPlanner | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
|
|
@ -181,16 +190,26 @@ class Pather(PortList):
|
|||
tools: Tool(s) to use for routing segments.
|
||||
name: If specified, `library[name]` is set to `self.pattern`.
|
||||
debug: If True, enables detailed logging.
|
||||
auto_render: If True, enables immediate rendering of routing steps.
|
||||
auto_render_append: If `auto_render` is True, determines whether
|
||||
to append geometry or add a reference.
|
||||
render: Routing render policy. `'auto'` renders after each route
|
||||
outside a context manager and defers until clean context exit
|
||||
inside one. Use `'immediate'` to always render after each route,
|
||||
`'deferred'` to keep paths pending until `render()` or clean
|
||||
context exit, `'warn'` to log pending paths on clean context
|
||||
exit, `'error'` to reject pending paths on clean context exit,
|
||||
or `'ignore'` to leave pending paths silent.
|
||||
render_append: If an automatic render is triggered, determines
|
||||
whether to append geometry or add a reference.
|
||||
planner: Optional stateless route-selection planner. If omitted,
|
||||
a new `RoutingPlanner` is used.
|
||||
"""
|
||||
if render not in RENDER_POLICIES:
|
||||
raise BuildError(f'Invalid render policy {render!r}; expected one of {RENDER_POLICIES}')
|
||||
|
||||
self._dead = False
|
||||
self._logger = PatherLogger(debug=debug)
|
||||
self._auto_render = auto_render
|
||||
self._auto_render_append = auto_render_append
|
||||
self._render_policy = render
|
||||
self._render_append = render_append
|
||||
self._context_depth = 0
|
||||
self.library = library
|
||||
self.pattern = pattern if pattern is not None else Pattern()
|
||||
self.planner = RoutingPlanner() if planner is None else planner
|
||||
|
|
@ -213,9 +232,40 @@ class Pather(PortList):
|
|||
if name is not None:
|
||||
library[name] = self.pattern
|
||||
|
||||
def __del__(self) -> None:
|
||||
if any(self._paths.values()):
|
||||
logger.warning(f'Pather {self} had pending render steps', stack_info=True)
|
||||
def __enter__(self) -> Self:
|
||||
self._context_depth += 1
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool:
|
||||
_ = exc_value, traceback
|
||||
self._context_depth -= 1
|
||||
if exc_type is not None or self._context_depth != 0 or not any(self._paths.values()):
|
||||
return False
|
||||
|
||||
if self._render_policy in ('auto', 'deferred'):
|
||||
self.render(append=self._render_append)
|
||||
elif self._render_policy == 'warn':
|
||||
logger.warning(
|
||||
'Pather context exited with %s; call render() or use render="deferred"',
|
||||
self._pending_render_summary(),
|
||||
)
|
||||
elif self._render_policy == 'error':
|
||||
raise BuildError(f'Pather context exited with {self._pending_render_summary()}')
|
||||
return False
|
||||
|
||||
def _pending_render_summary(self) -> str:
|
||||
ports = [(portspec, len(steps)) for portspec, steps in self._paths.items() if steps]
|
||||
port_count = len(ports)
|
||||
step_count = sum(count for _portspec, count in ports)
|
||||
return (
|
||||
f'{step_count} pending render step{"s" if step_count != 1 else ""} '
|
||||
f'on {port_count} port{"s" if port_count != 1 else ""}'
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
|
|
@ -397,12 +447,9 @@ class Pather(PortList):
|
|||
Apply every action and deferred rename in a prepared route result.
|
||||
|
||||
Route actions may contain several primitive render steps and port
|
||||
mutations. Temporarily disabling auto-render prevents each step from
|
||||
becoming its own rendered cell.
|
||||
mutations. Immediate rendering happens once after the whole prepared
|
||||
result has been applied.
|
||||
"""
|
||||
saved_auto_render = self._auto_render
|
||||
self._auto_render = False
|
||||
try:
|
||||
for action in result.actions:
|
||||
if not action.render_steps:
|
||||
raise BuildError('Prepared route action has no render steps')
|
||||
|
|
@ -416,11 +463,12 @@ class Pather(PortList):
|
|||
self.plugged({action.portspec: action.plug_into})
|
||||
for old_name, new_name in result.renames:
|
||||
self.rename_ports({old_name: new_name})
|
||||
self._auto_render = saved_auto_render
|
||||
if saved_auto_render and any(self._paths.values()):
|
||||
self.render(append = self._auto_render_append)
|
||||
finally:
|
||||
self._auto_render = saved_auto_render
|
||||
render_immediately = (
|
||||
self._render_policy == 'immediate'
|
||||
or (self._render_policy == 'auto' and self._context_depth == 0)
|
||||
)
|
||||
if render_immediately and any(self._paths.values()):
|
||||
self.render(append=self._render_append)
|
||||
|
||||
def _apply_dead_fallback(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@ class RenderStep:
|
|||
"""
|
||||
A single deferred routing operation.
|
||||
|
||||
`Pather(auto_render=False)` stores these records while routing and later
|
||||
`Pather(render='deferred')` stores these records while routing and later
|
||||
passes batches of compatible steps to `Tool.render()` when `Pather.render()`
|
||||
is called.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ def test_autotool_straight_offer_supports_requested_output_transition(
|
|||
) -> None:
|
||||
_p, tool, lib = autotool_setup
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["start"] = Port((0, 0), pi, ptype="wire_m1")
|
||||
p.straight("start", 10, out_ptype="wire_m2")
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ def test_pather_straight_topology_allows_transition_offset_cancellation() -> Non
|
|||
.add_transition(lib.abstract("trans_out"), "EXT", "CORE")
|
||||
)
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), pi, ptype="ext_in")
|
||||
p.trace("A", None, length=10, out_ptype="ext_out")
|
||||
|
||||
|
|
@ -550,7 +550,7 @@ def test_autotool_s_offer_bbox_matches_rendered_sbend() -> None:
|
|||
|
||||
assert_offer_bbox_matches_rendered_offer(tool, offer, 4, lib)
|
||||
|
||||
pather = Pather(lib, tools=tool, auto_render=False)
|
||||
pather = Pather(lib, tools=tool, render='deferred')
|
||||
pather.ports["A"] = Port((0, 0), 0, ptype="ext")
|
||||
pather.jog("A", 4)
|
||||
|
||||
|
|
@ -581,7 +581,7 @@ def test_autotool_s_offer_uses_absolute_jog_range_for_both_signs() -> None:
|
|||
assert valid_positive
|
||||
assert valid_negative
|
||||
|
||||
pather = Pather(Library(), tools=tool, auto_render=False)
|
||||
pather = Pather(Library(), tools=tool, render='deferred')
|
||||
pather.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
pather.jog("A", -4)
|
||||
assert_allclose(pather.ports["A"].offset, [-10, 4])
|
||||
|
|
@ -623,7 +623,7 @@ def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None:
|
|||
assert [offer.endpoint_at(-4).y for offer in offers if offer.jog_domain == (-4.0, -4.0)] == [-4]
|
||||
|
||||
for jog, expected_y in ((4, -4), (-4, 4)):
|
||||
pather = Pather(Library(), tools=tool, auto_render=False)
|
||||
pather = Pather(Library(), tools=tool)
|
||||
pather.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
pather.jog("A", jog)
|
||||
assert_allclose(pather.ports["A"].offset, [-10, expected_y])
|
||||
|
|
@ -669,7 +669,7 @@ def test_autotool_uturn_offer_mirror_exposes_both_signs() -> None:
|
|||
|
||||
def test_pather_autotool_uses_prebaked_uturn_offer() -> None:
|
||||
tool, lib = make_uturn_tool()
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
|
||||
p.uturn("A", 4, length=10)
|
||||
|
|
@ -695,7 +695,7 @@ def test_pather_autotool_omitted_uturn_composes_l_offers(
|
|||
multi_bend_tool: tuple[AutoTool, Library],
|
||||
) -> None:
|
||||
tool, lib = multi_bend_tool
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
||||
|
||||
p.uturn("A", 20)
|
||||
|
|
@ -714,7 +714,7 @@ def test_pather_autotool_explicit_uturn_composes_l_offers(
|
|||
multi_bend_tool: tuple[AutoTool, Library],
|
||||
) -> None:
|
||||
tool, lib = multi_bend_tool
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
||||
|
||||
p.uturn("A", 20, length=10)
|
||||
|
|
@ -739,7 +739,7 @@ def test_autotool_offer_bbox_rejects_invalid_parameter(multi_bend_tool: tuple[Au
|
|||
|
||||
def test_pather_autotool_uses_l_offer_domains(multi_bend_tool: tuple[AutoTool, Library]) -> None:
|
||||
tool, lib = multi_bend_tool
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
||||
|
||||
p.trace("A", True, length=15)
|
||||
|
|
@ -760,7 +760,7 @@ def test_autotool_generated_primitives_do_not_capture_route_kwargs() -> None:
|
|||
return make_straight(length, ptype="wire")
|
||||
|
||||
tool = AutoTool().add_straight("wire", make_marked_straight, "A", length_range=(0, 1e8))
|
||||
p = Pather(Library(), tools=tool, auto_render=False)
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
||||
|
||||
p.straight("A", 5, marker="route")
|
||||
|
|
@ -790,7 +790,7 @@ def test_autotool_bend_offer_supports_requested_output_transition(ccw: bool) ->
|
|||
.add_transition(lib.abstract("out_trans"), "EXT", "CORE")
|
||||
)
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
p.trace("A", ccw, length=10, out_ptype="ext")
|
||||
|
||||
|
|
@ -824,7 +824,7 @@ def test_autotool_bend_offer_supports_bend_input_transition(ccw: bool) -> None:
|
|||
.add_transition(lib.abstract("bend_trans"), "MID", "CORE")
|
||||
)
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
p.trace("A", ccw, length=10, out_ptype="mid")
|
||||
|
||||
|
|
@ -857,7 +857,7 @@ def test_pather_accepts_bend_offer_with_zero_lateral_endpoint() -> None:
|
|||
.add_transition(lib.abstract("bend_trans"), "MID", "CORE")
|
||||
)
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
p.trace("A", True, length=10, out_ptype="mid")
|
||||
|
||||
|
|
@ -897,7 +897,7 @@ def test_autotool_bend_offer_supports_bend_and_output_transitions(ccw: bool) ->
|
|||
.add_transition(lib.abstract("out_trans"), "EXT", "MID")
|
||||
)
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
p.trace("A", ccw, length=12, out_ptype="ext")
|
||||
|
||||
|
|
@ -955,7 +955,7 @@ def test_pather_autotool_pure_sbend_with_transition_dx() -> None:
|
|||
assert isinstance(s_data, AutoTool.GeneratedData)
|
||||
assert s_offer.out_ptype == "core"
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="ext")
|
||||
p.jog("A", 4)
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ def test_autotool_uturn() -> None:
|
|||
.add_bend(lib.abstract('bend'), 'in', 'out', clockwise=True)
|
||||
)
|
||||
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool)
|
||||
p.pattern.ports['A'] = Port((0, 0), 0)
|
||||
|
||||
p.at('A').uturn(offset=-2000, length=1000)
|
||||
|
|
@ -82,7 +82,7 @@ def test_autotool_uturn() -> None:
|
|||
|
||||
def test_deferred_render_autotool_double_L(multi_bend_tool: tuple[AutoTool, Library]) -> None:
|
||||
tool, lib = multi_bend_tool
|
||||
rp = Pather(lib, tools=tool, auto_render=False)
|
||||
rp = Pather(lib, tools=tool)
|
||||
rp.ports["A"] = Port((0,0), 0, ptype="wire")
|
||||
|
||||
rp.jog("A", 10, length=20)
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class CountingPathTool(PathTool):
|
|||
def test_pather_jog_failed_two_bend_route_is_atomic() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool)
|
||||
p = Pather(lib, tools=tool, render='immediate')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='S-bend'):
|
||||
|
|
@ -130,7 +130,7 @@ def test_pather_jog_failed_two_bend_route_is_atomic() -> None:
|
|||
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 = Pather(lib, tools=tool, render='immediate')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.jog('A', 1.5, length=5)
|
||||
|
|
@ -139,10 +139,10 @@ def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None
|
|||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert len(p._paths['A']) == 0
|
||||
|
||||
def test_pather_auto_render_batches_multi_step_selected_route_once() -> None:
|
||||
def test_pather_immediate_render_batches_multi_step_selected_route_once() -> None:
|
||||
lib = Library()
|
||||
tool = CountingPathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool, auto_render=True)
|
||||
p = Pather(lib, tools=tool, render='immediate')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.jog('A', 4, length=10)
|
||||
|
|
@ -169,7 +169,7 @@ def test_pather_jog_length_solved_from_single_position_bound() -> None:
|
|||
|
||||
|
||||
def test_pather_positional_bound_requires_port_rotation() -> None:
|
||||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'), auto_render=False)
|
||||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=None, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='Ports must have rotation'):
|
||||
|
|
@ -179,7 +179,7 @@ def test_pather_positional_bound_requires_port_rotation() -> None:
|
|||
def test_pather_jog_omitted_length_uses_minimum_length_route() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.jog('A', 2)
|
||||
|
|
@ -195,7 +195,7 @@ def test_pather_jog_omitted_length_uses_minimum_length_route() -> None:
|
|||
def test_pather_trace_omitted_length_uses_minimum_offer() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.trace('A', None)
|
||||
|
|
@ -211,7 +211,7 @@ def test_pather_trace_omitted_length_uses_minimum_offer() -> None:
|
|||
def test_pather_trace_to_without_bound_uses_single_port_trace_minimum() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.trace_to('A', False)
|
||||
|
|
@ -229,7 +229,7 @@ def test_pather_trace_to_rejects_conflicting_position_bounds() -> None:
|
|||
with pytest.raises(BuildError, match='exactly one positional bound'):
|
||||
p.trace_to('A', None, **kwargs)
|
||||
|
||||
p = Pather(Library(), tools=tool)
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
with pytest.raises(BuildError, match='length cannot be combined'):
|
||||
p.trace_to('A', None, x=-5, length=3)
|
||||
|
|
@ -261,7 +261,7 @@ def test_planner_constrained_bend_requires_jog() -> None:
|
|||
|
||||
def test_pather_trace_each_plans_all_ports_before_mutation() -> None:
|
||||
tool = FirstPortOnlyTraceTool()
|
||||
p = Pather(Library(), tools=tool, auto_render=False)
|
||||
p = Pather(Library(), tools=tool)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((-2, 5), rotation=0, ptype='blocked')
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ def test_pather_trace_each_plans_all_ports_before_mutation() -> None:
|
|||
|
||||
def test_pather_bundle_trace_plans_all_ports_before_mutation_or_render() -> None:
|
||||
tool = FirstPortOnlyTraceTool()
|
||||
p = Pather(Library(), tools=tool, auto_render=True)
|
||||
p = Pather(Library(), tools=tool, render='immediate')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((0, 4), rotation=0, ptype='blocked')
|
||||
|
||||
|
|
@ -343,7 +343,7 @@ def test_pather_route_commit_failure_is_atomic_for_multi_port_trace() -> None:
|
|||
return tree
|
||||
|
||||
tool = CommitFailureTool()
|
||||
p = Pather(Library(), tools=tool, auto_render=True)
|
||||
p = Pather(Library(), tools=tool, render='immediate')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((0, 4), rotation=0, ptype='bad')
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ def test_pather_uturn_does_not_use_direct_planl_fallback() -> None:
|
|||
jog = -1
|
||||
return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length}
|
||||
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='No legal primitive offer for omitted-length U-turn'):
|
||||
|
|
@ -476,7 +476,7 @@ def test_pather_su_topology_rejects_out_ptype_sensitive_planl_jog() -> None:
|
|||
ptype = out_ptype or in_ptype or 'wire'
|
||||
return Port((length, jog), rotation=rotation, ptype=ptype), {'ccw': ccw, 'length': length}
|
||||
|
||||
p = Pather(Library(), tools=OutPtypeSensitiveTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=OutPtypeSensitiveTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
|
|
@ -509,7 +509,7 @@ def test_pather_two_l_planl_only_uturn_is_not_supported() -> None:
|
|||
jog = -1
|
||||
return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length}
|
||||
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
|
|
@ -543,7 +543,7 @@ def test_pather_two_l_planl_only_jog_is_not_supported() -> None:
|
|||
jog = -1
|
||||
return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length}
|
||||
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def test_builder_tool_symbol_exports() -> None:
|
|||
|
||||
|
||||
def test_pather_pending_render_steps_are_private() -> None:
|
||||
p = Pather(Library(), tools=PathTool(layer=(1, 0), width=1), auto_render=False)
|
||||
p = Pather(Library(), tools=PathTool(layer=(1, 0), width=1))
|
||||
|
||||
assert not hasattr(p, 'paths')
|
||||
assert hasattr(p, '_paths')
|
||||
|
|
@ -81,7 +81,7 @@ def test_pather_accepts_and_reuses_planner_instance() -> None:
|
|||
p = Pather(
|
||||
Library(),
|
||||
tools=PathTool(layer=(1, 0), width=1),
|
||||
auto_render=False,
|
||||
render='deferred',
|
||||
planner=planner,
|
||||
)
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
|
@ -148,7 +148,7 @@ def test_pather_dead_ports() -> None:
|
|||
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)
|
||||
|
||||
# Routing extends opposite the port's inward-facing rotation.
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
|
|
@ -164,7 +164,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)
|
||||
|
||||
|
|
@ -177,7 +177,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)
|
||||
|
|
@ -194,13 +194,13 @@ def test_pather_bundle_trace() -> None:
|
|||
def test_portpather_default_spacing_matches_explicit_spacing() -> None:
|
||||
lib_default = Library()
|
||||
tool_default = PathTool(layer='M1', width=1000)
|
||||
p_default = Pather(lib_default, tools=tool_default, auto_render=False)
|
||||
p_default = Pather(lib_default, tools=tool_default)
|
||||
p_default.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_default.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
lib_explicit = Library()
|
||||
tool_explicit = PathTool(layer='M1', width=1000)
|
||||
p_explicit = Pather(lib_explicit, tools=tool_explicit, auto_render=False)
|
||||
p_explicit = Pather(lib_explicit, tools=tool_explicit)
|
||||
p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
|
|
@ -211,11 +211,11 @@ def test_portpather_default_spacing_matches_explicit_spacing() -> None:
|
|||
assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset)
|
||||
|
||||
def test_portpather_default_spacing_reused_and_overridden() -> None:
|
||||
p_default = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_default = Pather(Library(), tools=PathTool(layer='M1', width=1000))
|
||||
p_default.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_default.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
p_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000))
|
||||
p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
|
|
@ -227,11 +227,11 @@ def test_portpather_default_spacing_reused_and_overridden() -> None:
|
|||
assert_allclose(p_default.pattern.ports['A'].offset, p_explicit.pattern.ports['A'].offset)
|
||||
assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset)
|
||||
|
||||
p_override = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_override = Pather(Library(), tools=PathTool(layer='M1', width=1000))
|
||||
p_override.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_override.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
p_override_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_override_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000))
|
||||
p_override_explicit.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_override_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
|
|
@ -244,7 +244,7 @@ def test_portpather_default_spacing_reused_and_overridden() -> None:
|
|||
def test_portpather_default_spacing_not_injected_for_straight_bundle() -> 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)
|
||||
|
||||
|
|
@ -256,7 +256,7 @@ def test_portpather_default_spacing_not_injected_for_straight_bundle() -> None:
|
|||
def test_portpather_default_spacing_vector_revalidated_after_selection_change() -> 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)
|
||||
p.pattern.ports['C'] = Port((0, 4000), rotation=0)
|
||||
|
|
@ -270,7 +270,7 @@ def test_portpather_default_spacing_vector_revalidated_after_selection_change()
|
|||
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)
|
||||
|
|
@ -390,7 +390,7 @@ def test_rename() -> 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()
|
||||
|
||||
|
|
@ -424,7 +424,7 @@ def test_pather_dead_fallback_does_not_hide_fatal_route_errors() -> None:
|
|||
commit_planner=lambda length: {'length': length},
|
||||
),)
|
||||
|
||||
p = Pather(Library(), tools=MismatchedEndpointTool(layer='M1', width=1000, ptype='wire'), auto_render=False)
|
||||
p = Pather(Library(), tools=MismatchedEndpointTool(layer='M1', width=1000, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.set_dead()
|
||||
|
||||
|
|
@ -472,7 +472,7 @@ def test_pather_valid_candidate_does_not_hide_fatal_route_errors() -> None:
|
|||
),
|
||||
)
|
||||
|
||||
p = Pather(Library(), tools=PartlyMismatchedEndpointTool(layer='M1', width=1000, ptype='wire'), auto_render=False)
|
||||
p = Pather(Library(), tools=PartlyMismatchedEndpointTool(layer='M1', width=1000, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='does not match declared offer out_ptype'):
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from masque.error import PortError, PatternError
|
|||
def test_pather_place_treeview_resolves_once() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1000)
|
||||
p = Pather(lib, tools=tool)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
|
||||
tree = {'child': Pattern(ports={'B': Port((1, 0), pi)})}
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ def test_pather_place_treeview_resolves_once() -> None:
|
|||
def test_pather_plug_treeview_resolves_once() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1000)
|
||||
p = Pather(lib, tools=tool)
|
||||
p = Pather(lib, tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
|
||||
tree = {'child': Pattern(ports={'B': Port((0, 0), pi)})}
|
||||
|
|
@ -39,7 +39,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, render='deferred')
|
||||
p.pattern.annotations = {'k': [1]}
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
|
||||
|
|
@ -60,7 +60,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, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
|
||||
p.at('A').straight(5000)
|
||||
|
|
@ -80,7 +80,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, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p.pattern.ports['B'] = Port((0, 0), rotation=0)
|
||||
|
||||
|
|
@ -107,7 +107,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, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
|
||||
p.at('A').straight(5000)
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ def test_pather_treats_notimplemented_offer_query_as_no_offers() -> None:
|
|||
_ = kind, in_ptype, out_ptype, kwargs
|
||||
raise NotImplementedError
|
||||
|
||||
p = Pather(Library(), tools=NoOfferTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=NoOfferTool())
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='No legal primitive offer for trace'):
|
||||
|
|
@ -381,7 +381,7 @@ def test_pather_propagates_offer_query_errors(err_type: type[Exception]) -> None
|
|||
_ = kind, in_ptype, out_ptype, kwargs
|
||||
raise err_type('offer query failed')
|
||||
|
||||
p = Pather(Library(), tools=BrokenTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=BrokenTool())
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(err_type):
|
||||
|
|
@ -413,7 +413,7 @@ def test_pather_selects_lowest_cost_offer() -> None:
|
|||
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, priority_bias=0, **offer_callbacks(low)),
|
||||
)
|
||||
|
||||
p = Pather(Library(), tools=MultiOfferTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=MultiOfferTool(), render='deferred')
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.straight('A', 7)
|
||||
|
|
@ -460,7 +460,7 @@ def test_solver_rejects_rotation_impossible_candidates_before_parameter_solving(
|
|||
),
|
||||
)
|
||||
|
||||
p = Pather(Library(), tools=RotationOfferTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=RotationOfferTool(), render='deferred')
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.straight('A', 7)
|
||||
|
|
@ -503,7 +503,7 @@ def test_pather_commits_only_selected_offer() -> None:
|
|||
|
||||
return (make('expensive', 100), make('cheap', 0))
|
||||
|
||||
p = Pather(Library(), tools=RecordingTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=RecordingTool(), render='deferred')
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.straight('A', 5)
|
||||
|
|
@ -553,7 +553,7 @@ def test_pather_routes_with_offer_only_s_and_u_tool() -> None:
|
|||
),)
|
||||
return ()
|
||||
|
||||
p = Pather(Library(), tools=OfferOnlyTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=OfferOnlyTool(), render='deferred')
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.jog('A', 3)
|
||||
p.uturn('A', 4)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import TYPE_CHECKING, cast
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import numpy
|
||||
|
|
@ -20,10 +21,27 @@ if TYPE_CHECKING:
|
|||
def deferred_render_setup() -> tuple[Pather, PathTool, Library]:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
rp = Pather(lib, tools=tool, auto_render=False)
|
||||
rp = Pather(lib, tools=tool, render='deferred')
|
||||
rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
|
||||
return rp, tool, lib
|
||||
|
||||
|
||||
def add_pending_straight(pather: Pather) -> None:
|
||||
pather.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
|
||||
pather.straight("start", 10)
|
||||
|
||||
|
||||
def route_in_context(pather: Pather) -> None:
|
||||
with pather:
|
||||
add_pending_straight(pather)
|
||||
|
||||
|
||||
def fail_in_context(pather: Pather) -> None:
|
||||
with pather:
|
||||
add_pending_straight(pather)
|
||||
raise RuntimeError('body failed')
|
||||
|
||||
|
||||
def test_deferred_render_stores_pending_paths_until_render(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
||||
rp, tool, lib = deferred_render_setup
|
||||
rp.at("start").straight(10).straight(10)
|
||||
|
|
@ -101,7 +119,7 @@ def test_portpather_translate_only_affects_future_steps(deferred_render_setup: t
|
|||
def test_deferred_render_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 = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool, render='deferred')
|
||||
rp.set_dead()
|
||||
|
||||
rp.straight("in", -10)
|
||||
|
|
@ -113,6 +131,91 @@ def test_deferred_render_dead_ports() -> None:
|
|||
rp.render()
|
||||
assert not rp.pattern.has_shapes()
|
||||
|
||||
|
||||
def test_pather_default_auto_policy_renders_immediately_outside_context() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
p = Pather(lib, tools=tool)
|
||||
p.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
|
||||
|
||||
p.straight("start", 10)
|
||||
|
||||
assert p.pattern.has_shapes()
|
||||
assert not any(p._paths.values())
|
||||
|
||||
|
||||
def test_pather_default_auto_policy_defers_until_clean_context_exit() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
|
||||
with Pather(lib, tools=tool) as p:
|
||||
add_pending_straight(p)
|
||||
assert not p.pattern.has_shapes()
|
||||
assert len(p._paths["start"]) == 1
|
||||
|
||||
assert p.pattern.has_shapes()
|
||||
assert not any(p._paths.values())
|
||||
|
||||
|
||||
def test_pather_context_warn_policy_logs_pending_paths(caplog: pytest.LogCaptureFixture) -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="masque.builder.pather"), Pather(lib, tools=tool, render='warn') as p:
|
||||
add_pending_straight(p)
|
||||
|
||||
records = [
|
||||
record
|
||||
for record in caplog.records
|
||||
if 'Pather context exited with 1 pending render step on 1 port' in record.getMessage()
|
||||
]
|
||||
assert len(records) == 1
|
||||
assert records[0].stack_info is None
|
||||
assert len(p._paths["start"]) == 1
|
||||
assert not p.pattern.has_shapes()
|
||||
|
||||
|
||||
def test_pather_context_error_policy_rejects_pending_paths() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
p = Pather(lib, tools=tool, render='error')
|
||||
|
||||
with pytest.raises(BuildError, match='1 pending render step on 1 port'):
|
||||
route_in_context(p)
|
||||
|
||||
assert len(p._paths["start"]) == 1
|
||||
assert not p.pattern.has_shapes()
|
||||
|
||||
|
||||
def test_pather_context_ignore_policy_leaves_pending_paths_silent(caplog: pytest.LogCaptureFixture) -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="masque.builder.pather"), Pather(lib, tools=tool, render='ignore') as p:
|
||||
add_pending_straight(p)
|
||||
|
||||
assert not caplog.records
|
||||
assert len(p._paths["start"]) == 1
|
||||
assert not p.pattern.has_shapes()
|
||||
|
||||
|
||||
def test_pather_context_policy_does_not_mask_body_exception() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
p = Pather(lib, tools=tool, render='error')
|
||||
|
||||
with pytest.raises(RuntimeError, match='body failed'):
|
||||
fail_in_context(p)
|
||||
|
||||
assert len(p._paths["start"]) == 1
|
||||
assert not p.pattern.has_shapes()
|
||||
|
||||
|
||||
def test_pather_rejects_invalid_render_policy() -> None:
|
||||
with pytest.raises(BuildError, match='Invalid render policy'):
|
||||
Pather(Library(), tools=PathTool(layer=(1, 0), width=2), render='later') # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_deferred_render_rename_port(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
||||
rp, tool, lib = deferred_render_setup
|
||||
rp.at("start").straight(10)
|
||||
|
|
@ -174,7 +277,7 @@ def test_pathtool_s_offer_render_geometry_matches_ports() -> None:
|
|||
def test_deferred_render_uturn_fallback() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1000)
|
||||
rp = Pather(lib, tools=tool, auto_render=False)
|
||||
rp = Pather(lib, tools=tool, render='deferred')
|
||||
rp.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
|
||||
rp.at('A').uturn(offset=10000, length=5000)
|
||||
|
|
@ -213,7 +316,7 @@ def test_pather_render_auto_renames_single_use_tool_children() -> None:
|
|||
return tree
|
||||
|
||||
lib = Library()
|
||||
p = Pather(lib, tools=FullTreeTool(), auto_render=False)
|
||||
p = Pather(lib, tools=FullTreeTool())
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.straight('A', 10)
|
||||
|
|
@ -258,7 +361,7 @@ def test_custom_tool_render_preserves_segment_subtrees() -> None:
|
|||
return batch[0].data
|
||||
|
||||
lib = Library()
|
||||
p = Pather(lib, tools=TraceTreeTool(), auto_render=False)
|
||||
p = Pather(lib, tools=TraceTreeTool())
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.straight('A', 10)
|
||||
|
|
@ -293,7 +396,7 @@ def test_pather_render_rejects_missing_single_use_tool_refs() -> None:
|
|||
|
||||
lib = Library()
|
||||
lib['_seg'] = Pattern(annotations={'stale': [1]})
|
||||
p = Pather(lib, tools=MissingSingleUseTool(), auto_render=False)
|
||||
p = Pather(lib, tools=MissingSingleUseTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.straight('A', 10)
|
||||
|
||||
|
|
@ -329,7 +432,7 @@ def test_pather_render_allows_missing_non_single_use_tool_refs() -> None:
|
|||
|
||||
lib = Library()
|
||||
lib['shared'] = Pattern(annotations={'shared': [1]})
|
||||
p = Pather(lib, tools=SharedRefTool(), auto_render=False)
|
||||
p = Pather(lib, tools=SharedRefTool())
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.straight('A', 10)
|
||||
|
|
@ -362,7 +465,7 @@ def test_pather_render_rejects_output_port_that_misses_planned_endpoint(append:
|
|||
return tree
|
||||
|
||||
lib = Library()
|
||||
p = Pather(lib, tools=WrongEndpointTool(), auto_render=False)
|
||||
p = Pather(lib, tools=WrongEndpointTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.straight('A', 10)
|
||||
|
||||
|
|
@ -396,7 +499,7 @@ def test_pather_render_rejects_output_port_with_wrong_ptype(append: bool) -> Non
|
|||
return tree
|
||||
|
||||
lib = Library()
|
||||
p = Pather(lib, tools=WrongPtypeTool(), auto_render=False)
|
||||
p = Pather(lib, tools=WrongPtypeTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.straight('A', 10)
|
||||
|
||||
|
|
@ -409,7 +512,7 @@ def test_pather_render_rejects_output_port_with_wrong_ptype(append: bool) -> Non
|
|||
def test_deferred_render_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 = Pather(lib, tools=tool, render='deferred')
|
||||
rp.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
|
||||
rp.at('A').straight(5000)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from masque.error import BuildError, PortError
|
|||
def trace_into_setup() -> tuple[Pather, PathTool, Library]:
|
||||
lib = Library()
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
p = Pather(lib, tools=tool, auto_render=True, auto_render_append=False)
|
||||
p = Pather(lib, tools=tool, render='immediate', render_append=False)
|
||||
return p, tool, lib
|
||||
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ def test_path_into_thru(trace_into_setup: tuple[Pather, PathTool, Library]) -> N
|
|||
def test_pather_trace_into_shapes() -> 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((-10000, 0), rotation=pi)
|
||||
|
|
@ -175,7 +175,7 @@ def test_pather_trace_into_refines_output_adapter_route_before_plug() -> None:
|
|||
pat.add_port_pair(names=port_names, ptype=batch[-1].end_port.ptype if batch else 'm1wire')
|
||||
return tree
|
||||
|
||||
p = Pather(Library(), tools=TransitionTool(), auto_render=False)
|
||||
p = Pather(Library(), tools=TransitionTool(), render='deferred')
|
||||
p.pattern.ports['src'] = Port((-65000, -11500), rotation=pi / 2, ptype='m1wire')
|
||||
p.pattern.ports['dst'] = Port((-100000, -100000), rotation=pi, ptype='m2wire')
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ def test_pather_trace_into_refines_output_adapter_route_before_plug() -> 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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue