Compare commits
No commits in common. "42d291b1b64a5d664dccc5ba7f9ac93058e2e905" and "7b589e4f447cd034ef95c8f30d95d83759f14b79" have entirely different histories.
42d291b1b6
...
7b589e4f44
7 changed files with 10 additions and 275 deletions
|
|
@ -57,7 +57,6 @@ for custom Tools.
|
||||||
from .pather import (
|
from .pather import (
|
||||||
Pather as Pather,
|
Pather as Pather,
|
||||||
PortPather as PortPather,
|
PortPather as PortPather,
|
||||||
RouteCompletionCallback as RouteCompletionCallback,
|
|
||||||
)
|
)
|
||||||
from .error import (
|
from .error import (
|
||||||
ToolContractError as ToolContractError,
|
ToolContractError as ToolContractError,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ Rendering is not transactional across Pattern and Library mutations. A render
|
||||||
exception is terminal for that Pather: callers may catch it for reporting or
|
exception is terminal for that Pather: callers may catch it for reporting or
|
||||||
cleanup, but must not retry rendering or continue routing with the same object.
|
cleanup, but must not retry rendering or continue routing with the same object.
|
||||||
"""
|
"""
|
||||||
from typing import Self, Any, Literal, Protocol, overload
|
from typing import Self, Any, Literal, overload
|
||||||
from collections.abc import Iterator, Iterable, Mapping, MutableMapping, Sequence
|
from collections.abc import Iterator, Iterable, Mapping, MutableMapping, Sequence
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -56,7 +56,6 @@ from pprint import pformat
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from types import MappingProxyType
|
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
|
@ -93,19 +92,6 @@ RENDER_POLICIES: tuple[RenderPolicy, ...] = ('auto', 'immediate', 'deferred', 'w
|
||||||
RESERVED_TOOL_OPTION_KEYS = frozenset(('kind', 'in_ptype', 'out_ptype', 'ccw'))
|
RESERVED_TOOL_OPTION_KEYS = frozenset(('kind', 'in_ptype', 'out_ptype', 'ccw'))
|
||||||
|
|
||||||
|
|
||||||
class RouteCompletionCallback(Protocol):
|
|
||||||
"""Callback invoked after one routing operation updates live Pather state."""
|
|
||||||
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
pather: 'Pather',
|
|
||||||
endpoints: Mapping[str, Port],
|
|
||||||
/,
|
|
||||||
) -> None:
|
|
||||||
"""Handle copied final endpoints keyed by their original routed names."""
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def _present_route_args(**kwargs: Any) -> dict[str, Any]:
|
def _present_route_args(**kwargs: Any) -> dict[str, Any]:
|
||||||
"""Return explicitly supplied route arguments for planner validation."""
|
"""Return explicitly supplied route arguments for planner validation."""
|
||||||
return {key: value for key, value in kwargs.items() if value is not None}
|
return {key: value for key, value in kwargs.items() if value is not None}
|
||||||
|
|
@ -158,25 +144,10 @@ class Pather(PortList):
|
||||||
- `pather.trace('my_port', ccw=True, length=100)` plans a 100-unit bend
|
- `pather.trace('my_port', ccw=True, length=100)` plans a 100-unit bend
|
||||||
starting at 'my_port'. If the `Pather` is used as a context manager,
|
starting at 'my_port'. If the `Pather` is used as a context manager,
|
||||||
geometry is generated on clean context exit.
|
geometry is generated on clean context exit.
|
||||||
|
|
||||||
Examples: Route completion
|
|
||||||
==========================
|
|
||||||
Route completion callbacks can add labels or other endpoint annotations
|
|
||||||
without wrapping the individual routing methods::
|
|
||||||
|
|
||||||
def label_route_endpoints(pather, endpoints):
|
|
||||||
for name, port in endpoints.items():
|
|
||||||
pather.label('PORT_LABELS', string=name, offset=port.offset)
|
|
||||||
|
|
||||||
pather = Pather(
|
|
||||||
library,
|
|
||||||
tools=my_tool,
|
|
||||||
on_route_complete=label_route_endpoints,
|
|
||||||
)
|
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'pattern', 'library', 'tools', 'planner', '_paths',
|
'pattern', 'library', 'tools', 'planner', '_paths',
|
||||||
'on_route_complete', '_dead', '_logger', '_render_policy', '_render_append', '_context_depth'
|
'_dead', '_logger', '_render_policy', '_render_append', '_context_depth'
|
||||||
)
|
)
|
||||||
|
|
||||||
pattern: Pattern
|
pattern: Pattern
|
||||||
|
|
@ -202,9 +173,6 @@ class Pather(PortList):
|
||||||
than on the planner instance.
|
than on the planner instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
on_route_complete: RouteCompletionCallback | None
|
|
||||||
"""Optional callback run once after each routing operation updates live state."""
|
|
||||||
|
|
||||||
_dead: bool
|
_dead: bool
|
||||||
""" If True, geometry generation is skipped (for debugging) """
|
""" If True, geometry generation is skipped (for debugging) """
|
||||||
|
|
||||||
|
|
@ -270,7 +238,6 @@ class Pather(PortList):
|
||||||
render: RenderPolicy = 'auto',
|
render: RenderPolicy = 'auto',
|
||||||
render_append: bool = True,
|
render_append: bool = True,
|
||||||
planner: RoutingPlanner | None = None,
|
planner: RoutingPlanner | None = None,
|
||||||
on_route_complete: RouteCompletionCallback | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -292,11 +259,6 @@ class Pather(PortList):
|
||||||
whether to append geometry or add a reference.
|
whether to append geometry or add a reference.
|
||||||
planner: Optional stateless route-selection planner. If omitted,
|
planner: Optional stateless route-selection planner. If omitted,
|
||||||
a new `RoutingPlanner` is used.
|
a new `RoutingPlanner` is used.
|
||||||
on_route_complete: Optional callback invoked once per routing
|
|
||||||
operation after port updates, plugs, and renames, but before
|
|
||||||
automatic rendering. It receives this Pather and a read-only,
|
|
||||||
request-ordered mapping from original routed names to copied
|
|
||||||
final Ports. Callback exceptions propagate without rollback.
|
|
||||||
"""
|
"""
|
||||||
if render not in RENDER_POLICIES:
|
if render not in RENDER_POLICIES:
|
||||||
raise BuildError(f'Invalid render policy {render!r}; expected one of {RENDER_POLICIES}')
|
raise BuildError(f'Invalid render policy {render!r}; expected one of {RENDER_POLICIES}')
|
||||||
|
|
@ -309,7 +271,6 @@ class Pather(PortList):
|
||||||
self.library = library
|
self.library = library
|
||||||
self.pattern = pattern if pattern is not None else Pattern()
|
self.pattern = pattern if pattern is not None else Pattern()
|
||||||
self.planner = RoutingPlanner() if planner is None else planner
|
self.planner = RoutingPlanner() if planner is None else planner
|
||||||
self.on_route_complete = on_route_complete
|
|
||||||
self._paths = defaultdict(list)
|
self._paths = defaultdict(list)
|
||||||
|
|
||||||
if ports is not None:
|
if ports is not None:
|
||||||
|
|
@ -539,14 +500,6 @@ class Pather(PortList):
|
||||||
#
|
#
|
||||||
# Routing Logic (Deferred / Incremental)
|
# Routing Logic (Deferred / Incremental)
|
||||||
#
|
#
|
||||||
def _notify_route_complete(self, endpoints: Iterable[tuple[str, Port]]) -> None:
|
|
||||||
"""Invoke the configured route callback with isolated endpoint snapshots."""
|
|
||||||
callback = self.on_route_complete
|
|
||||||
if callback is None:
|
|
||||||
return
|
|
||||||
snapshots = MappingProxyType({name: port.copy() for name, port in endpoints})
|
|
||||||
callback(self, snapshots)
|
|
||||||
|
|
||||||
def _apply_route_result(self, result: PreparedRouteResult) -> None:
|
def _apply_route_result(self, result: PreparedRouteResult) -> None:
|
||||||
"""
|
"""
|
||||||
Apply every action and deferred rename in a prepared route result.
|
Apply every action and deferred rename in a prepared route result.
|
||||||
|
|
@ -568,9 +521,6 @@ class Pather(PortList):
|
||||||
self.plugged({action.portspec: action.plug_into})
|
self.plugged({action.portspec: action.plug_into})
|
||||||
for old_name, new_name in result.renames:
|
for old_name, new_name in result.renames:
|
||||||
self.rename_ports({old_name: new_name})
|
self.rename_ports({old_name: new_name})
|
||||||
self._notify_route_complete(
|
|
||||||
(action.portspec, action.final_port) for action in result.actions
|
|
||||||
)
|
|
||||||
render_immediately = (
|
render_immediately = (
|
||||||
self._render_policy == 'immediate'
|
self._render_policy == 'immediate'
|
||||||
or (self._render_policy == 'auto' and self._context_depth == 0)
|
or (self._render_policy == 'auto' and self._context_depth == 0)
|
||||||
|
|
@ -589,7 +539,7 @@ class Pather(PortList):
|
||||||
*,
|
*,
|
||||||
out_rot: float | None = None,
|
out_rot: float | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
) -> Port:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Move a dead Pather port without generating geometry.
|
Move a dead Pather port without generating geometry.
|
||||||
|
|
||||||
|
|
@ -614,7 +564,6 @@ class Pather(PortList):
|
||||||
self.pattern.ports[portspec] = out_port
|
self.pattern.ports[portspec] = out_port
|
||||||
if plug_into is not None:
|
if plug_into is not None:
|
||||||
self.plugged({portspec: plug_into})
|
self.plugged({portspec: plug_into})
|
||||||
return out_port.copy()
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# High-level Routing Methods
|
# High-level Routing Methods
|
||||||
|
|
@ -690,7 +639,7 @@ class Pather(PortList):
|
||||||
raise
|
raise
|
||||||
if length is not None and len(contexts) == 1:
|
if length is not None and len(contexts) == 1:
|
||||||
context = contexts[0]
|
context = contexts[0]
|
||||||
endpoint = self._apply_dead_fallback(
|
self._apply_dead_fallback(
|
||||||
context.portspec,
|
context.portspec,
|
||||||
length,
|
length,
|
||||||
0,
|
0,
|
||||||
|
|
@ -698,13 +647,11 @@ class Pather(PortList):
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
self._notify_route_complete(((context.portspec, endpoint),))
|
|
||||||
return self
|
return self
|
||||||
if bounds.get('each') is not None:
|
if bounds.get('each') is not None:
|
||||||
each = bounds['each']
|
each = bounds['each']
|
||||||
endpoints: list[tuple[str, Port]] = []
|
|
||||||
for context in contexts:
|
for context in contexts:
|
||||||
endpoint = self._apply_dead_fallback(
|
self._apply_dead_fallback(
|
||||||
context.portspec,
|
context.portspec,
|
||||||
each,
|
each,
|
||||||
0,
|
0,
|
||||||
|
|
@ -712,8 +659,6 @@ class Pather(PortList):
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
endpoints.append((context.portspec, endpoint))
|
|
||||||
self._notify_route_complete(endpoints)
|
|
||||||
return self
|
return self
|
||||||
raise
|
raise
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
|
|
@ -810,7 +755,7 @@ class Pather(PortList):
|
||||||
raise
|
raise
|
||||||
_key, _value, length = resolved
|
_key, _value, length = resolved
|
||||||
context = contexts[0]
|
context = contexts[0]
|
||||||
endpoint = self._apply_dead_fallback(
|
self._apply_dead_fallback(
|
||||||
context.portspec,
|
context.portspec,
|
||||||
length,
|
length,
|
||||||
0,
|
0,
|
||||||
|
|
@ -818,7 +763,6 @@ class Pather(PortList):
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
self._notify_route_complete(((context.portspec, endpoint),))
|
|
||||||
return self
|
return self
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
return self
|
return self
|
||||||
|
|
@ -1018,7 +962,7 @@ class Pather(PortList):
|
||||||
if length is None:
|
if length is None:
|
||||||
raise
|
raise
|
||||||
context = contexts[0]
|
context = contexts[0]
|
||||||
endpoint = self._apply_dead_fallback(
|
self._apply_dead_fallback(
|
||||||
context.portspec,
|
context.portspec,
|
||||||
length,
|
length,
|
||||||
0,
|
0,
|
||||||
|
|
@ -1026,11 +970,10 @@ class Pather(PortList):
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
self._notify_route_complete(((context.portspec, endpoint),))
|
|
||||||
return self
|
return self
|
||||||
fallback_length = length if length is not None else 0
|
fallback_length = length if length is not None else 0
|
||||||
context = contexts[0]
|
context = contexts[0]
|
||||||
endpoint = self._apply_dead_fallback(
|
self._apply_dead_fallback(
|
||||||
context.portspec,
|
context.portspec,
|
||||||
fallback_length,
|
fallback_length,
|
||||||
offset,
|
offset,
|
||||||
|
|
@ -1039,7 +982,6 @@ class Pather(PortList):
|
||||||
out_rot = pi,
|
out_rot = pi,
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
self._notify_route_complete(((context.portspec, endpoint),))
|
|
||||||
return self
|
return self
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
return self
|
return self
|
||||||
|
|
@ -1093,7 +1035,7 @@ class Pather(PortList):
|
||||||
):
|
):
|
||||||
raise
|
raise
|
||||||
context = contexts[0]
|
context = contexts[0]
|
||||||
endpoint = self._apply_dead_fallback(
|
self._apply_dead_fallback(
|
||||||
context.portspec,
|
context.portspec,
|
||||||
length,
|
length,
|
||||||
offset,
|
offset,
|
||||||
|
|
@ -1102,7 +1044,6 @@ class Pather(PortList):
|
||||||
out_rot = 0.0,
|
out_rot = 0.0,
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
self._notify_route_complete(((context.portspec, endpoint),))
|
|
||||||
return self
|
return self
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
return self
|
return self
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,5 @@ class IBorrowing(ABC):
|
||||||
|
|
||||||
The source may use a different name, which is returned alongside it.
|
The source may use a different name, which is returned alongside it.
|
||||||
Port metadata may differ because ports are not layout-file content.
|
Port metadata may differ because ports are not layout-file content.
|
||||||
The result is not recursively resolved: consumers must follow further
|
|
||||||
borrowing views themselves and enforce format-specific constraints such
|
|
||||||
as whether the visible and source names must match. `None` means the
|
|
||||||
source cannot be reused safely or no source provenance is available.
|
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
name: set(child_graph.get(name, set()))
|
name: set(child_graph.get(name, set()))
|
||||||
for name in self._order
|
for name in self._order
|
||||||
}
|
}
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
if key not in self._names:
|
if key not in self._names:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
@ -137,7 +136,6 @@ class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
refs = self._source.find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
refs = self._source.find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
||||||
return {parent: transforms for parent, transforms in refs.items() if parent in self._names}
|
return {parent: transforms for parent, transforms in refs.items() if parent in self._names}
|
||||||
|
|
||||||
|
|
||||||
class Library(ILibrary):
|
class Library(ILibrary):
|
||||||
"""
|
"""
|
||||||
Default implementation for a writeable library.
|
Default implementation for a writeable library.
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
self._replace = replace
|
self._replace = replace
|
||||||
self._cache: dict[str, Pattern] = {}
|
self._cache: dict[str, Pattern] = {}
|
||||||
self._lookups_in_progress: list[str] = []
|
self._lookups_in_progress: list[str] = []
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
return self.materialize(key, persist=True)
|
return self.materialize(key, persist=True)
|
||||||
|
|
||||||
|
|
@ -161,7 +160,6 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
return cast('dict[str, list[NDArray[numpy.float64]]]', finder(name, parent_graph=parent_graph, dangling=dangling))
|
return cast('dict[str, list[NDArray[numpy.float64]]]', finder(name, parent_graph=parent_graph, dangling=dangling))
|
||||||
return super().find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
return super().find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
||||||
|
|
||||||
|
|
||||||
class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
"""
|
"""
|
||||||
Mutable overlay over one or more source libraries.
|
Mutable overlay over one or more source libraries.
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,10 @@ from numpy.testing import assert_allclose
|
||||||
|
|
||||||
from ..file import gdsii
|
from ..file import gdsii
|
||||||
from ..file.gdsii import lazy as gdsii_lazy
|
from ..file.gdsii import lazy as gdsii_lazy
|
||||||
from ..file.gdsii import lazy_write as gdsii_lazy_write
|
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
from ..library import IBorrowing, IMaterializable, LazyLibrary, Library, LibraryView, OverlayLibrary, PortsLibraryView
|
from ..library import IBorrowing, IMaterializable, LazyLibrary, Library, OverlayLibrary, PortsLibraryView
|
||||||
|
|
||||||
|
|
||||||
def _make_lazy_port_library() -> Library:
|
def _make_lazy_port_library() -> Library:
|
||||||
|
|
@ -77,19 +76,6 @@ def test_gdsii_lazy_write_materializes_transiently() -> None:
|
||||||
assert info['name'] == 'transient'
|
assert info['name'] == 'transient'
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_raw_copy_provenance_cycle_falls_back() -> None:
|
|
||||||
class SelfBorrowingView(LibraryView, IBorrowing):
|
|
||||||
def borrowed_sources(self) -> tuple['SelfBorrowingView', ...]:
|
|
||||||
return (self,)
|
|
||||||
|
|
||||||
def source_cell(self, name: str) -> tuple['SelfBorrowingView', str] | None:
|
|
||||||
return self, name
|
|
||||||
|
|
||||||
view = SelfBorrowingView({'top': Pattern()})
|
|
||||||
|
|
||||||
assert gdsii_lazy_write._resolve_raw_struct(view, 'top') is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
|
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'lazy_source.gds'
|
gds_file = tmp_path / 'lazy_source.gds'
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
@ -161,23 +147,6 @@ def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path
|
||||||
assert set(roundtrip) == {'leaf', 'child', 'top'}
|
assert set(roundtrip) == {'leaf', 'child', 'top'}
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_borrowed_sources_require_matching_units(tmp_path: Path) -> None:
|
|
||||||
gds_a = tmp_path / 'units_a.gds'
|
|
||||||
gds_b = tmp_path / 'units_b.gds'
|
|
||||||
source = Library({'top': Pattern()})
|
|
||||||
gdsii.writefile(source, gds_a, meters_per_unit=1e-9, library_name='units-a')
|
|
||||||
gdsii.writefile(source, gds_b, meters_per_unit=2e-9, library_name='units-b')
|
|
||||||
|
|
||||||
lazy_a, _ = gdsii_lazy.readfile(gds_a)
|
|
||||||
lazy_b, _ = gdsii_lazy.readfile(gds_b)
|
|
||||||
overlay = OverlayLibrary()
|
|
||||||
overlay.add_source(lazy_a)
|
|
||||||
overlay.add_source(lazy_b, rename_theirs=lambda lib, name: lib.get_name(name))
|
|
||||||
|
|
||||||
with pytest.raises(LibraryError, match='identical units'):
|
|
||||||
gdsii_lazy.write(overlay, io.BytesIO())
|
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_overlay_subtree_preserves_source_laziness(tmp_path: Path) -> None:
|
def test_gdsii_lazy_overlay_subtree_preserves_source_laziness(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'lazy_overlay_subtree_source.gds'
|
gds_file = tmp_path / 'lazy_overlay_subtree_source.gds'
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
from collections.abc import Mapping, Sequence
|
|
||||||
from typing import Any, Literal
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from masque import Library, Pather, Port
|
|
||||||
from masque.builder import PathTool, RenderStep, RouteCompletionCallback, Tool
|
|
||||||
from masque.error import BuildError
|
|
||||||
|
|
||||||
|
|
||||||
def test_route_completion_can_label_before_automatic_render() -> None:
|
|
||||||
calls = 0
|
|
||||||
|
|
||||||
def label_endpoints(pather: Pather, endpoints: Mapping[str, Port]) -> None:
|
|
||||||
nonlocal calls
|
|
||||||
calls += 1
|
|
||||||
assert not pather.pattern.has_shapes()
|
|
||||||
assert numpy.allclose(pather.ports['A'].offset, endpoints['A'].offset)
|
|
||||||
for name, port in endpoints.items():
|
|
||||||
pather.label('LABELS', string=name, offset=port.offset)
|
|
||||||
|
|
||||||
callback: RouteCompletionCallback = label_endpoints
|
|
||||||
p = Pather(
|
|
||||||
Library(),
|
|
||||||
tools=PathTool(layer='M1', width=1, ptype='wire'),
|
|
||||||
on_route_complete=callback,
|
|
||||||
)
|
|
||||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
||||||
|
|
||||||
p.at('A').straight(5)
|
|
||||||
|
|
||||||
assert calls == 1
|
|
||||||
assert p.pattern.has_shapes()
|
|
||||||
assert len(p.pattern.labels['LABELS']) == 1
|
|
||||||
assert p.pattern.labels['LABELS'][0].string == 'A'
|
|
||||||
assert numpy.allclose(p.pattern.labels['LABELS'][0].offset, p.ports['A'].offset)
|
|
||||||
|
|
||||||
|
|
||||||
def test_bundle_route_completion_is_ordered_read_only_and_isolated() -> None:
|
|
||||||
calls: list[Mapping[str, Port]] = []
|
|
||||||
|
|
||||||
def record(pather: Pather, endpoints: Mapping[str, Port]) -> None:
|
|
||||||
_ = pather
|
|
||||||
calls.append(endpoints)
|
|
||||||
with pytest.raises(TypeError):
|
|
||||||
endpoints['extra'] = Port((0, 0)) # type: ignore[index]
|
|
||||||
endpoints['B'].translate((100, 100))
|
|
||||||
|
|
||||||
p = Pather(
|
|
||||||
Library(),
|
|
||||||
tools=PathTool(layer='M1', width=1, ptype='wire'),
|
|
||||||
render='deferred',
|
|
||||||
on_route_complete=record,
|
|
||||||
)
|
|
||||||
p.ports['B'] = Port((0, 2), rotation=0, ptype='wire')
|
|
||||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
||||||
|
|
||||||
p.trace(['A', 'B'], None, each=5)
|
|
||||||
|
|
||||||
assert len(calls) == 1
|
|
||||||
assert list(calls[0]) == ['A', 'B']
|
|
||||||
assert numpy.allclose(p.ports['A'].offset, (-5, 0))
|
|
||||||
assert numpy.allclose(p.ports['B'].offset, (-5, 2))
|
|
||||||
|
|
||||||
|
|
||||||
def test_trace_into_completion_retains_consumed_source_endpoint() -> None:
|
|
||||||
calls: list[Mapping[str, Port]] = []
|
|
||||||
|
|
||||||
def record(pather: Pather, endpoints: Mapping[str, Port]) -> None:
|
|
||||||
assert set(pather.ports) == {'src'}
|
|
||||||
assert numpy.allclose(pather.ports['src'].offset, (20, 0))
|
|
||||||
calls.append(endpoints)
|
|
||||||
|
|
||||||
p = Pather(
|
|
||||||
Library(),
|
|
||||||
tools=PathTool(layer='M1', width=1, ptype='wire'),
|
|
||||||
render='deferred',
|
|
||||||
on_route_complete=record,
|
|
||||||
)
|
|
||||||
p.ports['src'] = Port((0, 0), rotation=0, ptype='wire')
|
|
||||||
p.ports['dst'] = Port((-10, 0), rotation=numpy.pi, ptype='wire')
|
|
||||||
p.ports['thru'] = Port((20, 0), rotation=0, ptype='wire')
|
|
||||||
|
|
||||||
p.trace_into('src', 'dst', thru='thru')
|
|
||||||
|
|
||||||
assert len(calls) == 1
|
|
||||||
assert list(calls[0]) == ['src']
|
|
||||||
assert numpy.allclose(calls[0]['src'].offset, (-10, 0))
|
|
||||||
|
|
||||||
|
|
||||||
def test_route_completion_exception_propagates_before_render() -> None:
|
|
||||||
def fail(pather: Pather, endpoints: Mapping[str, Port]) -> None:
|
|
||||||
_ = pather, endpoints
|
|
||||||
raise RuntimeError('completion failed')
|
|
||||||
|
|
||||||
p = Pather(
|
|
||||||
Library(),
|
|
||||||
tools=PathTool(layer='M1', width=1, ptype='wire'),
|
|
||||||
on_route_complete=fail,
|
|
||||||
)
|
|
||||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match='completion failed'):
|
|
||||||
p.straight('A', 5)
|
|
||||||
|
|
||||||
assert numpy.allclose(p.ports['A'].offset, (-5, 0))
|
|
||||||
assert p._paths['A']
|
|
||||||
assert not p.pattern.has_shapes()
|
|
||||||
|
|
||||||
|
|
||||||
class NoRouteTool(Tool):
|
|
||||||
def primitive_offers(
|
|
||||||
self,
|
|
||||||
kind: Literal['straight', 'bend', 's', 'u'],
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> tuple[()]:
|
|
||||||
_ = kind, kwargs
|
|
||||||
return ()
|
|
||||||
|
|
||||||
def render(
|
|
||||||
self,
|
|
||||||
batch: Sequence[RenderStep],
|
|
||||||
*,
|
|
||||||
port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> Library:
|
|
||||||
_ = batch, port_names, kwargs
|
|
||||||
return Library()
|
|
||||||
|
|
||||||
|
|
||||||
def test_dead_bundle_fallback_invokes_route_completion_once() -> None:
|
|
||||||
calls: list[Mapping[str, Port]] = []
|
|
||||||
p = Pather(
|
|
||||||
Library(),
|
|
||||||
tools=NoRouteTool(),
|
|
||||||
on_route_complete=lambda _pather, endpoints: calls.append(endpoints),
|
|
||||||
)
|
|
||||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
||||||
p.ports['B'] = Port((0, 2), rotation=0, ptype='wire')
|
|
||||||
p.set_dead()
|
|
||||||
|
|
||||||
p.trace(['B', 'A'], None, each=5)
|
|
||||||
|
|
||||||
assert len(calls) == 1
|
|
||||||
assert list(calls[0]) == ['B', 'A']
|
|
||||||
assert numpy.allclose(calls[0]['B'].offset, (-5, 2))
|
|
||||||
assert numpy.allclose(calls[0]['A'].offset, (-5, 0))
|
|
||||||
assert not p.pattern.has_shapes()
|
|
||||||
|
|
||||||
|
|
||||||
def test_failed_route_does_not_invoke_route_completion() -> None:
|
|
||||||
calls = 0
|
|
||||||
|
|
||||||
def record(pather: Pather, endpoints: Mapping[str, Port]) -> None:
|
|
||||||
nonlocal calls
|
|
||||||
_ = pather, endpoints
|
|
||||||
calls += 1
|
|
||||||
|
|
||||||
p = Pather(Library(), tools=NoRouteTool(), on_route_complete=record)
|
|
||||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
||||||
|
|
||||||
with pytest.raises(BuildError):
|
|
||||||
p.straight('A', 5)
|
|
||||||
|
|
||||||
assert calls == 0
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue