From e7f7cae8b900387750cdf50d8e29dd164dbb39fe Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 13 Jul 2026 21:40:10 -0700 Subject: [PATCH] [library / gdsii] more cleanup --- masque/library/capabilities.py | 4 ++++ masque/library/mapping.py | 2 ++ masque/library/overlay.py | 2 ++ masque/test/test_gdsii_lazy.py | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/masque/library/capabilities.py b/masque/library/capabilities.py index bf85262..7014e51 100644 --- a/masque/library/capabilities.py +++ b/masque/library/capabilities.py @@ -47,5 +47,9 @@ 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 d8b5666..9b4c390 100644 --- a/masque/library/mapping.py +++ b/masque/library/mapping.py @@ -70,6 +70,7 @@ 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) @@ -136,6 +137,7 @@ 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 089d953..7e5fa84 100644 --- a/masque/library/overlay.py +++ b/masque/library/overlay.py @@ -79,6 +79,7 @@ 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) @@ -160,6 +161,7 @@ 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 8b4b7c4..964b5c4 100644 --- a/masque/test/test_gdsii_lazy.py +++ b/masque/test/test_gdsii_lazy.py @@ -7,10 +7,11 @@ 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, OverlayLibrary, PortsLibraryView +from ..library import IBorrowing, IMaterializable, LazyLibrary, Library, LibraryView, OverlayLibrary, PortsLibraryView def _make_lazy_port_library() -> Library: @@ -76,6 +77,19 @@ 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() @@ -147,6 +161,23 @@ 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()