diff --git a/masque/file/gdsii/lazy.py b/masque/file/gdsii/lazy.py index 7ddc76d..1439306 100644 --- a/masque/file/gdsii/lazy.py +++ b/masque/file/gdsii/lazy.py @@ -42,11 +42,12 @@ from ...library import ( from ...utils import apply_transforms if TYPE_CHECKING: - from collections.abc import Iterator, Sequence + from collections.abc import Iterator, Mapping, Sequence from numpy.typing import NDArray from ...pattern import Pattern + from ...ports import Port logger = logging.getLogger(__name__) @@ -284,12 +285,28 @@ class GdsLibrarySource(ILibraryView): layers: Sequence[tuple[int, int] | int], max_depth: int = 0, skip_subcells: bool = True, + ports: Mapping[str, Mapping[str, Port]] | None = None, + replace: bool = False, ) -> PortsLibraryView: return PortsLibraryView( self, layers=layers, max_depth=max_depth, skip_subcells=skip_subcells, + ports=ports, + replace=replace, + ) + + def with_port_overrides( + self, + ports: Mapping[str, Mapping[str, Port]], + *, + replace: bool = False, + ) -> PortsLibraryView: + return PortsLibraryView( + self, + ports=ports, + replace=replace, ) def find_refs_local( diff --git a/masque/file/gdsii/lazy_arrow.py b/masque/file/gdsii/lazy_arrow.py index 5a0da0f..acfd454 100644 --- a/masque/file/gdsii/lazy_arrow.py +++ b/masque/file/gdsii/lazy_arrow.py @@ -28,12 +28,13 @@ from ...library import ( from ...utils import apply_transforms if TYPE_CHECKING: - from collections.abc import Iterator, Sequence + from collections.abc import Iterator, Mapping, Sequence from numpy.typing import NDArray import pyarrow from ...pattern import Pattern + from ...ports import Port logger = logging.getLogger(__name__) @@ -407,12 +408,28 @@ class ArrowLibrary(ILibraryView): layers: Sequence[tuple[int, int] | int], max_depth: int = 0, skip_subcells: bool = True, + ports: Mapping[str, Mapping[str, Port]] | None = None, + replace: bool = False, ) -> PortsLibraryView: return PortsLibraryView( self, layers=layers, max_depth=max_depth, skip_subcells=skip_subcells, + ports=ports, + replace=replace, + ) + + def with_port_overrides( + self, + ports: Mapping[str, Mapping[str, Port]], + *, + replace: bool = False, + ) -> PortsLibraryView: + return PortsLibraryView( + self, + ports=ports, + replace=replace, ) def close(self) -> None: diff --git a/masque/library/overlay.py b/masque/library/overlay.py index 9637236..a325bc6 100644 --- a/masque/library/overlay.py +++ b/masque/library/overlay.py @@ -20,6 +20,8 @@ if TYPE_CHECKING: from numpy.typing import NDArray + from ..ports import Port + @dataclass class _SourceLayer: @@ -47,7 +49,7 @@ def _materialize_detached_pattern(view: ILibraryView, name: str) -> Pattern: class PortsLibraryView(ILibraryView): """ - Read-only view which imports ports into cells on first materialization. + Read-only view which imports or applies ports on first materialization. The wrapped source remains untouched; this view owns a separate processed cache so direct-copy workflows can continue to use the raw source view. @@ -61,14 +63,21 @@ class PortsLibraryView(ILibraryView): self, source: ILibraryView, *, - layers: Sequence[layer_t], + layers: Sequence[layer_t] = (), max_depth: int = 0, skip_subcells: bool = True, + ports: Mapping[str, Mapping[str, Port]] | None = None, + replace: bool = False, ) -> None: self._source = source self._layers = tuple(layers) self._max_depth = max_depth self._skip_subcells = skip_subcells + self._ports = { + name: copy.deepcopy(dict(cell_ports)) + for name, cell_ports in (ports or {}).items() + } + self._replace = replace self._cache: dict[str, Pattern] = {} self._lookups_in_progress: list[str] = [] if hasattr(source, 'library_info'): @@ -103,14 +112,21 @@ class PortsLibraryView(ILibraryView): self._lookups_in_progress.append(name) try: pat = _materialize_detached_pattern(self._source, name) - pat = data_to_ports( - layers=self._layers, - library=self, - pattern=pat, - name=name, - max_depth=self._max_depth, - skip_subcells=self._skip_subcells, - ) + if self._layers: + pat = data_to_ports( + layers=self._layers, + library=self, + pattern=pat, + name=name, + max_depth=self._max_depth, + skip_subcells=self._skip_subcells, + ) + if name in self._ports: + ports = copy.deepcopy(self._ports[name]) + if self._replace: + pat.ports = ports + else: + pat.ports.update(ports) finally: self._lookups_in_progress.pop() diff --git a/masque/test/test_gdsii_lazy.py b/masque/test/test_gdsii_lazy.py index 026580f..b9da929 100644 --- a/masque/test/test_gdsii_lazy.py +++ b/masque/test/test_gdsii_lazy.py @@ -7,6 +7,7 @@ from numpy.testing import assert_allclose from ..file import gdsii from ..file.gdsii import lazy as gdsii_lazy from ..pattern import Pattern +from ..ports import Port from ..library import Library, OverlayLibrary @@ -66,6 +67,80 @@ def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> No assert not raw_top.ports +def test_gdsii_lazy_port_overrides_without_data_stay_lazy(tmp_path: Path) -> None: + gds_file = tmp_path / 'lazy_port_overrides.gds' + src = _make_lazy_port_library() + gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overrides') + + raw, _ = gdsii_lazy.readfile(gds_file) + processed = raw.with_port_overrides({ + 'top': { + 'P': Port((1, 2), rotation=0, ptype='wire'), + }, + }) + + top = processed['top'] + assert set(top.ports) == {'P'} + assert_allclose(top.ports['P'].offset, [1, 2], atol=1e-10) + assert top.ports['P'].rotation == 0 + assert top.ports['P'].ptype == 'wire' + assert not raw._cache + + raw_top = raw['top'] + assert not raw_top.ports + + +def test_gdsii_lazy_port_overrides_apply_after_extraction(tmp_path: Path) -> None: + gds_file = tmp_path / 'lazy_ports_override_extracted.gds' + src = _make_lazy_port_library() + gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-override-extracted') + + raw, _ = gdsii_lazy.readfile(gds_file) + processed = raw.with_ports_from_data( + layers=[(10, 0)], + max_depth=2, + ports={ + 'top': { + 'A': Port((1, 2), rotation=numpy.pi, ptype='manual'), + 'B': Port((3, 4), rotation=None, ptype=None), + }, + }, + ) + + top = processed['top'] + assert set(top.ports) == {'A', 'B'} + assert_allclose(top.ports['A'].offset, [1, 2], atol=1e-10) + assert top.ports['A'].rotation == numpy.pi + assert top.ports['A'].ptype == 'manual' + assert_allclose(top.ports['B'].offset, [3, 4], atol=1e-10) + assert top.ports['B'].rotation is None + assert top.ports['B'].ptype is None + assert not raw._cache + + +def test_gdsii_lazy_port_overrides_replace_extracted_ports(tmp_path: Path) -> None: + gds_file = tmp_path / 'lazy_ports_replace.gds' + src = _make_lazy_port_library() + gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-replace-ports') + + raw, _ = gdsii_lazy.readfile(gds_file) + processed = raw.with_ports_from_data( + layers=[(10, 0)], + max_depth=2, + ports={ + 'top': { + 'B': Port((3, 4), rotation=None, ptype=None), + }, + }, + replace=True, + ) + + top = processed['top'] + assert set(top.ports) == {'B'} + assert_allclose(top.ports['B'].offset, [3, 4], atol=1e-10) + assert not raw._cache + + def test_gdsii_lazy_overlay_add_source_stays_lazy_for_processed_view(tmp_path: Path) -> None: gds_file = tmp_path / 'lazy_overlay.gds' src = _make_lazy_port_library() @@ -84,6 +159,29 @@ def test_gdsii_lazy_overlay_add_source_stays_lazy_for_processed_view(tmp_path: P assert set(abstract.ports) == {'A'} +def test_gdsii_lazy_overlay_add_source_sees_port_overrides(tmp_path: Path) -> None: + gds_file = tmp_path / 'lazy_overlay_override.gds' + src = _make_lazy_port_library() + gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overlay-override') + + raw, _ = gdsii_lazy.readfile(gds_file) + processed = raw.with_port_overrides({ + 'top': { + 'P': Port((1, 2), rotation=0, ptype='wire'), + }, + }) + + overlay = OverlayLibrary() + overlay.add_source(processed) + + assert not raw._cache + assert not processed._cache + + abstract = overlay.abstract('top') + assert set(abstract.ports) == {'P'} + assert_allclose(abstract.ports['P'].offset, [1, 2], atol=1e-10) + + def test_gdsii_lazy_overlay_add_source_can_rename_every_source_cell() -> None: src = _make_lazy_port_library() overlay = OverlayLibrary()