diff --git a/masque/builder/__init__.py b/masque/builder/__init__.py index cc0b2aa..de1a0d1 100644 --- a/masque/builder/__init__.py +++ b/masque/builder/__init__.py @@ -57,7 +57,6 @@ for custom Tools. from .pather import ( Pather as Pather, PortPather as PortPather, - RouteCompletionCallback as RouteCompletionCallback, ) from .error import ( ToolContractError as ToolContractError, diff --git a/masque/builder/pather.py b/masque/builder/pather.py index b773da8..85acca9 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -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 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 import copy import logging @@ -56,7 +56,6 @@ from pprint import pformat from contextlib import contextmanager from itertools import chain from types import TracebackType -from types import MappingProxyType import numpy 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')) -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]: """Return explicitly supplied route arguments for planner validation.""" 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 starting at 'my_port'. If the `Pather` is used as a context manager, 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__ = ( '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 @@ -202,9 +173,6 @@ class Pather(PortList): than on the planner instance. """ - on_route_complete: RouteCompletionCallback | None - """Optional callback run once after each routing operation updates live state.""" - _dead: bool """ If True, geometry generation is skipped (for debugging) """ @@ -270,7 +238,6 @@ class Pather(PortList): render: RenderPolicy = 'auto', render_append: bool = True, planner: RoutingPlanner | None = None, - on_route_complete: RouteCompletionCallback | None = None, ) -> None: """ Args: @@ -292,11 +259,6 @@ class Pather(PortList): whether to append geometry or add a reference. planner: Optional stateless route-selection planner. If omitted, 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: raise BuildError(f'Invalid render policy {render!r}; expected one of {RENDER_POLICIES}') @@ -309,7 +271,6 @@ class Pather(PortList): self.library = library self.pattern = pattern if pattern is not None else Pattern() self.planner = RoutingPlanner() if planner is None else planner - self.on_route_complete = on_route_complete self._paths = defaultdict(list) if ports is not None: @@ -539,14 +500,6 @@ class Pather(PortList): # # 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: """ 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}) for old_name, new_name in result.renames: self.rename_ports({old_name: new_name}) - self._notify_route_complete( - (action.portspec, action.final_port) for action in result.actions - ) render_immediately = ( self._render_policy == 'immediate' or (self._render_policy == 'auto' and self._context_depth == 0) @@ -589,7 +539,7 @@ class Pather(PortList): *, out_rot: float | None = None, out_ptype: str | None = None, - ) -> Port: + ) -> None: """ Move a dead Pather port without generating geometry. @@ -614,7 +564,6 @@ class Pather(PortList): self.pattern.ports[portspec] = out_port if plug_into is not None: self.plugged({portspec: plug_into}) - return out_port.copy() # # High-level Routing Methods @@ -690,7 +639,7 @@ class Pather(PortList): raise if length is not None and len(contexts) == 1: context = contexts[0] - endpoint = self._apply_dead_fallback( + self._apply_dead_fallback( context.portspec, length, 0, @@ -698,13 +647,11 @@ class Pather(PortList): context.port.ptype, out_ptype = out_ptype, ) - self._notify_route_complete(((context.portspec, endpoint),)) return self if bounds.get('each') is not None: each = bounds['each'] - endpoints: list[tuple[str, Port]] = [] for context in contexts: - endpoint = self._apply_dead_fallback( + self._apply_dead_fallback( context.portspec, each, 0, @@ -712,8 +659,6 @@ class Pather(PortList): context.port.ptype, out_ptype = out_ptype, ) - endpoints.append((context.portspec, endpoint)) - self._notify_route_complete(endpoints) return self raise self._apply_route_result(result) @@ -810,7 +755,7 @@ class Pather(PortList): raise _key, _value, length = resolved context = contexts[0] - endpoint = self._apply_dead_fallback( + self._apply_dead_fallback( context.portspec, length, 0, @@ -818,7 +763,6 @@ class Pather(PortList): context.port.ptype, out_ptype = out_ptype, ) - self._notify_route_complete(((context.portspec, endpoint),)) return self self._apply_route_result(result) return self @@ -1018,7 +962,7 @@ class Pather(PortList): if length is None: raise context = contexts[0] - endpoint = self._apply_dead_fallback( + self._apply_dead_fallback( context.portspec, length, 0, @@ -1026,11 +970,10 @@ class Pather(PortList): context.port.ptype, out_ptype = out_ptype, ) - self._notify_route_complete(((context.portspec, endpoint),)) return self fallback_length = length if length is not None else 0 context = contexts[0] - endpoint = self._apply_dead_fallback( + self._apply_dead_fallback( context.portspec, fallback_length, offset, @@ -1039,7 +982,6 @@ class Pather(PortList): out_rot = pi, out_ptype = out_ptype, ) - self._notify_route_complete(((context.portspec, endpoint),)) return self self._apply_route_result(result) return self @@ -1093,7 +1035,7 @@ class Pather(PortList): ): raise context = contexts[0] - endpoint = self._apply_dead_fallback( + self._apply_dead_fallback( context.portspec, length, offset, @@ -1102,7 +1044,6 @@ class Pather(PortList): out_rot = 0.0, out_ptype = out_ptype, ) - self._notify_route_complete(((context.portspec, endpoint),)) return self self._apply_route_result(result) return self diff --git a/masque/library/capabilities.py b/masque/library/capabilities.py index 7014e51..bf85262 100644 --- a/masque/library/capabilities.py +++ b/masque/library/capabilities.py @@ -47,9 +47,5 @@ class IBorrowing(ABC): The source may use a different name, which is returned alongside it. 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 diff --git a/masque/library/mapping.py b/masque/library/mapping.py index 9b4c390..d8b5666 100644 --- a/masque/library/mapping.py +++ b/masque/library/mapping.py @@ -70,7 +70,6 @@ class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing): name: set(child_graph.get(name, set())) for name in self._order } - def __getitem__(self, key: str) -> Pattern: if key not in self._names: 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) return {parent: transforms for parent, transforms in refs.items() if parent in self._names} - class Library(ILibrary): """ Default implementation for a writeable library. diff --git a/masque/library/overlay.py b/masque/library/overlay.py index 7e5fa84..089d953 100644 --- a/masque/library/overlay.py +++ b/masque/library/overlay.py @@ -79,7 +79,6 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing): self._replace = replace self._cache: dict[str, Pattern] = {} self._lookups_in_progress: list[str] = [] - def __getitem__(self, key: str) -> Pattern: 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 super().find_refs_local(name, parent_graph=parent_graph, dangling=dangling) - class OverlayLibrary(ILibrary, IMaterializable, IBorrowing): """ Mutable overlay over one or more source libraries. diff --git a/masque/test/test_gdsii_lazy.py b/masque/test/test_gdsii_lazy.py index 964b5c4..8b4b7c4 100644 --- a/masque/test/test_gdsii_lazy.py +++ b/masque/test/test_gdsii_lazy.py @@ -7,11 +7,10 @@ from numpy.testing import assert_allclose from ..file import gdsii from ..file.gdsii import lazy as gdsii_lazy -from ..file.gdsii import lazy_write as gdsii_lazy_write from ..error import LibraryError from ..pattern import Pattern 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: @@ -77,19 +76,6 @@ def test_gdsii_lazy_write_materializes_transiently() -> None: 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: gds_file = tmp_path / 'lazy_source.gds' 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'} -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: gds_file = tmp_path / 'lazy_overlay_subtree_source.gds' src = _make_lazy_port_library() diff --git a/masque/test/test_pather_route_completion.py b/masque/test/test_pather_route_completion.py deleted file mode 100644 index 36fbd69..0000000 --- a/masque/test/test_pather_route_completion.py +++ /dev/null @@ -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