diff --git a/MIGRATION.md b/MIGRATION.md index f15ba91..13a8025 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -599,17 +599,23 @@ old Nx4 form. `BuildReport` mapping fields are now defensively copied and read-only. Copy a field to a new `dict` before adding or removing report entries. -`PortsLibraryView`, `OverlayLibrary`, and source-backed outputs returned by -`LibraryBuilder.build()` borrow their sources. They do not close source resources, and -`PortsLibraryView` is not a context manager. Keep each lazy source open and -unchanged until every borrowing view or overlay is finished, then close the -owning source explicitly. An eager `build(output='library')` result is detached -and does not need its sources afterward. +`PortLoadView`, `LayerMappedView`, `OverlayLibrary`, and source-backed outputs +returned by `LibraryBuilder.build()` borrow their sources. They do not close +source resources, and the views are not context managers. Keep each lazy +source open and unchanged until every borrowing view or overlay is finished, +then close the owning source explicitly. An eager `build(output='library')` +result is detached and does not need its sources afterward. Lazy GDS sources no longer provide `with_ports_from_data()` or `with_port_overrides()` convenience methods. Construct the generic view -directly instead: `PortsLibraryView(source, layers=...)` imports port data, and -`PortsLibraryView(source, ports=..., replace=...)` applies explicit overrides. +directly instead: `PortLoadView(source, layers=...)` imports port data, and +`PortLoadView(source, ports=..., replace=...)` applies explicit overrides. + +`LayerMappedView(source, map_layer)` lazily remaps shape and label layers while +leaving the source untouched. Its default `copy_through=False` forces mapped +serialization of every cell. Set `copy_through=True` only when source-aware +writers may copy untouched cells unchanged and intentionally skip their layer +mapping; persistently accessed cells are mapped and no longer copied through. Generic borrowing views no longer expose GDS-specific `raw_struct_bytes()`, `can_copy_raw_struct()`, or forwarded `library_info` attributes. Keep the @@ -697,11 +703,12 @@ through `source_cell()`; format writers decide whether that provenance permits raw copy-through. Raw GDS structure access remains confined to GDS sources and writers. -`LibraryBuilder`, `OverlayLibrary`, and `PortsLibraryView` are new additive -library implementations. `LibraryBuilder` supports declarative `@cell` recipes -and dependency-aware builds; `OverlayLibrary` composes source libraries without -eagerly copying all patterns; `PortsLibraryView` overlays port metadata on a -read-only source. +`LibraryBuilder`, `OverlayLibrary`, `PortLoadView`, and `LayerMappedView` are new +additive library implementations. `LibraryBuilder` supports declarative `@cell` +recipes and dependency-aware builds; `OverlayLibrary` composes source libraries +without eagerly copying all patterns; `PortLoadView` loads port metadata from +labels and/or explicit mappings; `LayerMappedView` remaps shape and label layers +on detached materialization. Flattening with `flatten_ports=True` now rejects repeated refs whose target has ports, because expanding them would create duplicate port names. Resolve the @@ -797,7 +804,7 @@ scikit-image implementation. The `arrow` and `boolean` extras are also new. These are additive, but available now from `masque` and `masque.builder`: - from `masque`: `RectCollection`, `boolean`, `OverlayLibrary`, - `PortsLibraryView`, `IMaterializable`, `IBorrowing`, `LibraryBuilder`, + `PortLoadView`, `LayerMappedView`, `IMaterializable`, `IBorrowing`, `LibraryBuilder`, `BuildReport`, `CellProvenance`, and `cell` - from `masque.builder`: `CostCallable`, `RenderStepKind`, the concrete primitive-offer classes, structured route error/status types, diff --git a/examples/tutorial/library.py b/examples/tutorial/library.py index 2c9de45..03170a0 100644 --- a/examples/tutorial/library.py +++ b/examples/tutorial/library.py @@ -11,7 +11,7 @@ from typing import Any from pprint import pformat -from masque import ILibrary, LibraryBuilder, Pather, Pattern, PortsLibraryView, cell +from masque import ILibrary, LibraryBuilder, Pather, Pattern, PortLoadView, cell from masque.file.gdsii import writefile from masque.file.gdsii.lazy import readfile @@ -53,7 +53,7 @@ def main() -> None: # imported on first materialization, but the raw source remains untouched # until we build the final library. gds_lib, _properties = readfile('circuit.gds') - builder.add_source(PortsLibraryView(gds_lib, layers=[(3, 0)], max_depth=1)) + builder.add_source(PortLoadView(gds_lib, layers=[(3, 0)], max_depth=1)) print('Registered imported cells:\n' + pformat(list(gds_lib.keys()))) diff --git a/masque/__init__.py b/masque/__init__.py index bc0a466..38925c1 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -67,7 +67,8 @@ from .library import ( LibraryView as LibraryView, Library as Library, OverlayLibrary as OverlayLibrary, - PortsLibraryView as PortsLibraryView, + PortLoadView as PortLoadView, + LayerMappedView as LayerMappedView, LibraryBuilder as LibraryBuilder, BuildReport as BuildReport, CellProvenance as CellProvenance, diff --git a/masque/file/gdsii/lazy.py b/masque/file/gdsii/lazy.py index 798eb26..63401d8 100644 --- a/masque/file/gdsii/lazy.py +++ b/masque/file/gdsii/lazy.py @@ -7,7 +7,7 @@ This module provides the non-Arrow half of Masque's lazy GDS architecture: struct order, and child edges without materializing every cell. - cells are materialized on demand through the classic `gdsii` decoder whenever a caller indexes the lazy view -- the source can be wrapped in `PortsLibraryView` or merged through +- the source can be wrapped in `PortLoadView` or merged through `OverlayLibrary` The public surface intentionally parallels `gdsii.lazy_arrow` closely so that diff --git a/masque/library/__init__.py b/masque/library/__init__.py index 5e206a3..a781938 100644 --- a/masque/library/__init__.py +++ b/masque/library/__init__.py @@ -23,7 +23,8 @@ from .mapping import ( ) from .overlay import ( OverlayLibrary as OverlayLibrary, - PortsLibraryView as PortsLibraryView, + PortLoadView as PortLoadView, + LayerMappedView as LayerMappedView, ) from .build import ( LibraryBuilder as LibraryBuilder, diff --git a/masque/library/overlay.py b/masque/library/overlay.py index 7e5fa84..6b6412c 100644 --- a/masque/library/overlay.py +++ b/masque/library/overlay.py @@ -1,4 +1,4 @@ -"""Overlay and ports-importing library views.""" +"""Overlay and lazily processed library views.""" from __future__ import annotations from collections import defaultdict @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Literal, Self, cast import copy from ..error import LibraryError -from ..pattern import Pattern, map_targets +from ..pattern import Pattern, map_layers, map_targets from .base import ILibrary, ILibraryView from .capabilities import IBorrowing, IMaterializable from .utils import INameView, dangling_mode_t, _plan_source_names, _source_rename_map, _validate_dangling_mode @@ -44,39 +44,17 @@ def _materialize_detached_pattern(view: ILibraryView, name: str) -> Pattern: return view[name].deepcopy() -class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing): - """ - 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. - The view borrows its source: callers must keep the source open for the - lifetime of the view and close the source themselves. - - Graph queries and source ordering are delegated to the wrapped source, - while `source_cell()` exposes unchanged layout provenance and `__getitem__` - and `materialize_many()` return port-imported patterns. - """ +class _ProcessedLibraryView(ILibraryView, IMaterializable, IBorrowing): + """Shared detached-materialization behavior for read-only processing views.""" def __init__( self, source: ILibraryView, *, - layers: Sequence[layer_t] = (), - max_depth: int = 0, - skip_subcells: bool = True, - ports: Mapping[str, Mapping[str, Port]] | None = None, - replace: bool = False, + copy_through: bool, ) -> 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._copy_through = copy_through self._cache: dict[str, Pattern] = {} self._lookups_in_progress: list[str] = [] @@ -92,9 +70,11 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing): def __contains__(self, key: object) -> bool: return key in self._source - def materialize(self, name: str, *, persist: bool = True) -> Pattern: - from ..utils.ports2data import data_to_ports # noqa: PLC0415 + def _process_pattern(self, name: str, pattern: Pattern) -> Pattern: + """Apply this view's processing to one detached source pattern.""" + raise NotImplementedError + def materialize(self, name: str, *, persist: bool = True) -> Pattern: if name in self._cache: return self._cache[name] @@ -108,28 +88,14 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing): self._lookups_in_progress.append(name) try: - pat = _materialize_detached_pattern(self._source, name) - 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) + pattern = _materialize_detached_pattern(self._source, name) + pattern = self._process_pattern(name, pattern) finally: self._lookups_in_progress.pop() if persist: - self._cache[name] = pat - return pat + self._cache[name] = pattern + return pattern def source_order(self) -> tuple[str, ...]: return self._source.source_order() @@ -138,7 +104,7 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing): return (self._source,) def source_cell(self, name: str) -> tuple[ILibraryView, str] | None: - if name not in self._source or name in self._cache: + if not self._copy_through or name not in self._source or name in self._cache: return None return self._source, name @@ -162,6 +128,88 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing): return super().find_refs_local(name, parent_graph=parent_graph, dangling=dangling) +class PortLoadView(_ProcessedLibraryView): + """ + Read-only view which loads 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. + The view borrows its source: callers must keep the source open for the + lifetime of the view and close the source themselves. + + Graph queries and source ordering are delegated to the wrapped source, + while `source_cell()` exposes unchanged layout provenance and `__getitem__` + and `materialize_many()` return port-imported patterns. + """ + + def __init__( + self, + source: ILibraryView, + *, + layers: Sequence[layer_t] = (), + max_depth: int = 0, + skip_subcells: bool = True, + ports: Mapping[str, Mapping[str, Port]] | None = None, + replace: bool = False, + ) -> None: + super().__init__(source, copy_through=True) + 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 + + def _process_pattern(self, name: str, pattern: Pattern) -> Pattern: + from ..utils.ports2data import data_to_ports # noqa: PLC0415 + + if self._layers: + pattern = data_to_ports( + layers=self._layers, + library=self, + pattern=pattern, + 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: + pattern.ports = ports + else: + pattern.ports.update(ports) + return pattern + + +class LayerMappedView(_ProcessedLibraryView): + """ + Read-only view which remaps shape and label layers on materialization. + + The wrapped source remains untouched. By default, source-aware writers + must materialize and serialize every mapped cell. With `copy_through=True`, + unmaterialized cells may instead be copied unchanged from their source; + persistent access maps and caches a cell, disabling copy-through for it. + """ + + def __init__( + self, + source: ILibraryView, + map_layer: Callable[[layer_t], layer_t], + *, + copy_through: bool = False, + ) -> None: + super().__init__(source, copy_through=copy_through) + self._map_layer = map_layer + + def _process_pattern(self, name: str, pattern: Pattern) -> Pattern: + _ = name + pattern.shapes = map_layers(pattern.shapes, self._map_layer) + pattern.labels = map_layers(pattern.labels, self._map_layer) + return pattern + + 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..3f12928 100644 --- a/masque/test/test_gdsii_lazy.py +++ b/masque/test/test_gdsii_lazy.py @@ -11,7 +11,10 @@ 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, LayerMappedView, LazyLibrary, Library, LibraryView, + OverlayLibrary, PortLoadView, + ) def _make_lazy_port_library() -> Library: @@ -201,13 +204,13 @@ def test_gdsii_lazy_overlay_subtree_preserves_source_laziness(tmp_path: Path) -> assert set(roundtrip) == {'leaf', 'child', 'top'} -def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> None: +def test_gdsii_lazy_port_load_view_keeps_raw_source_unmodified(tmp_path: Path) -> None: gds_file = tmp_path / 'lazy_ports.gds' src = _make_lazy_port_library() gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-ports') raw, _ = gdsii_lazy.readfile(gds_file) - processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2) + processed = PortLoadView(raw, layers=[(10, 0)], max_depth=2) assert not hasattr(processed, 'library_info') top = processed['top'] @@ -219,14 +222,14 @@ def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> No assert not raw_top.ports -def test_gdsii_lazy_ports_view_detaches_previously_cached_source(tmp_path: Path) -> None: +def test_gdsii_lazy_port_load_view_detaches_previously_cached_source(tmp_path: Path) -> None: gds_file = tmp_path / 'lazy_cached_ports.gds' src = _make_lazy_port_library() gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-cached-ports') raw, _ = gdsii_lazy.readfile(gds_file) raw_top = raw['top'] - processed = PortsLibraryView(raw, ports={ + processed = PortLoadView(raw, ports={ 'top': { 'P': Port((1, 2), rotation=0, ptype='wire'), }, @@ -247,7 +250,7 @@ def test_gdsii_lazy_port_overrides_without_data_stay_lazy(tmp_path: Path) -> Non gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overrides') raw, _ = gdsii_lazy.readfile(gds_file) - processed = PortsLibraryView(raw, ports={ + processed = PortLoadView(raw, ports={ 'top': { 'P': Port((1, 2), rotation=0, ptype='wire'), }, @@ -270,7 +273,7 @@ def test_gdsii_lazy_port_overrides_apply_after_extraction(tmp_path: Path) -> Non gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-override-extracted') raw, _ = gdsii_lazy.readfile(gds_file) - processed = PortsLibraryView( + processed = PortLoadView( raw, layers=[(10, 0)], max_depth=2, @@ -299,7 +302,7 @@ def test_gdsii_lazy_port_overrides_replace_extracted_ports(tmp_path: Path) -> No gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-replace-ports') raw, _ = gdsii_lazy.readfile(gds_file) - processed = PortsLibraryView( + processed = PortLoadView( raw, layers=[(10, 0)], max_depth=2, @@ -323,7 +326,7 @@ def test_gdsii_lazy_overlay_add_source_stays_lazy_for_processed_view(tmp_path: P gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overlay') raw, _ = gdsii_lazy.readfile(gds_file) - processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2) + processed = PortLoadView(raw, layers=[(10, 0)], max_depth=2) overlay = OverlayLibrary() overlay.add_source(processed) @@ -341,7 +344,7 @@ def test_gdsii_lazy_overlay_add_source_sees_port_overrides(tmp_path: Path) -> No gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overlay-override') raw, _ = gdsii_lazy.readfile(gds_file) - processed = PortsLibraryView(raw, ports={ + processed = PortLoadView(raw, ports={ 'top': { 'P': Port((1, 2), rotation=0, ptype='wire'), }, @@ -393,7 +396,7 @@ def test_gdsii_lazy_processed_write_roundtrips_without_explicit_units(tmp_path: gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-roundtrip') raw, _ = gdsii_lazy.readfile(gds_file) - processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2) + processed = PortLoadView(raw, layers=[(10, 0)], max_depth=2) out_file = tmp_path / 'lazy_roundtrip_out.gds' gdsii_lazy.writefile(processed, out_file) @@ -401,6 +404,23 @@ def test_gdsii_lazy_processed_write_roundtrips_without_explicit_units(tmp_path: assert out_file.read_bytes() == gds_file.read_bytes() +def test_gdsii_lazy_layer_mapped_write_materializes_without_source_cache(tmp_path: Path) -> None: + gds_file = tmp_path / 'lazy_layer_source.gds' + src = _make_lazy_port_library() + gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-layers') + + raw, _ = gdsii_lazy.readfile(gds_file) + mapped = LayerMappedView(raw, lambda layer: (20, 0) if layer == (10, 0) else layer) + out_file = tmp_path / 'lazy_layer_mapped.gds' + gdsii_lazy.writefile(mapped, out_file) + + assert not raw._cache + roundtrip, info = gdsii.readfile(out_file) + assert info['name'] == 'classic-layers' + assert set(roundtrip['leaf'].labels) == {(20, 0)} + assert (10, 0) not in roundtrip['leaf'].labels + + def test_gdsii_removed_closure_based_lazy_loader() -> None: assert not hasattr(gdsii, 'load_library') assert not hasattr(gdsii, 'load_libraryfile') diff --git a/masque/test/test_gdsii_lazy_arrow.py b/masque/test/test_gdsii_lazy_arrow.py index bbe8d78..0a02e8a 100644 --- a/masque/test/test_gdsii_lazy_arrow.py +++ b/masque/test/test_gdsii_lazy_arrow.py @@ -10,7 +10,7 @@ import pytest pytest.importorskip('pyarrow') from .. import PatternError -from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary, PortsLibraryView +from ..library import IBorrowing, IMaterializable, LayerMappedView, Library, OverlayLibrary, PortLoadView from ..pattern import Pattern from ..repetition import Grid from ..file import gdsii @@ -240,7 +240,7 @@ def test_gdsii_raw_copy_resolves_generic_borrowing_views(tmp_path: Path, monkeyp gdsii.writefile(_make_small_library(), gds_file, meters_per_unit=1e-9, library_name='provenance') raw, _ = gdsii_lazy_arrow.readfile(gds_file) - ports = PortsLibraryView(raw) + ports = PortLoadView(raw) subtree = ports.subtree('top') overlay = OverlayLibrary() overlay.add_source(subtree) @@ -270,6 +270,55 @@ def test_gdsii_raw_copy_resolves_generic_borrowing_views(tmp_path: Path, monkeyp assert gdsii_lazy_write._resolve_raw_struct(remapped, 'mid') is None +def test_gdsii_layer_mapped_view_controls_raw_copy_through( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + gds_file = tmp_path / 'layer_mapped_source.gds' + gdsii.writefile(_make_small_library(), gds_file, meters_per_unit=1e-9, library_name='layer-mapped') + + raw, _ = gdsii_lazy_arrow.readfile(gds_file) + copied: list[str] = [] + raw_reader = raw.raw_struct_bytes + + def record_raw_read(name: str) -> bytes: + copied.append(name) + return raw_reader(name) + + def map_layer(layer): # noqa: ANN001,ANN202 + return (20, 0) if layer == (1, 0) else layer + + monkeypatch.setattr(raw, 'raw_struct_bytes', record_raw_read) + + mapped = LayerMappedView(raw, map_layer) + mapped_file = tmp_path / 'layer_mapped_all.gds' + gdsii_lazy_arrow.writefile(mapped, mapped_file) + + assert copied == [] + assert not raw._cache + roundtrip, info = gdsii.readfile(mapped_file) + assert info['name'] == 'layer-mapped' + assert set(roundtrip['leaf'].shapes) == {(20, 0)} + + passthrough = LayerMappedView(raw, map_layer, copy_through=True) + copied_file = tmp_path / 'layer_mapped_copied.gds' + gdsii_lazy_arrow.writefile(passthrough, copied_file) + + assert copied == ['leaf', 'mid', 'top'] + assert copied_file.read_bytes() == gds_file.read_bytes() + assert not raw._cache + + copied.clear() + assert set(passthrough['leaf'].shapes) == {(20, 0)} + promoted_file = tmp_path / 'layer_mapped_promoted.gds' + gdsii_lazy_arrow.writefile(passthrough, promoted_file) + + assert copied == ['mid', 'top'] + assert not raw._cache + roundtrip, _ = gdsii.readfile(promoted_file) + assert set(roundtrip['leaf'].shapes) == {(20, 0)} + + def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -279,7 +328,7 @@ def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy( gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='processed-edit') raw, _ = gdsii_lazy_arrow.readfile(gds_file) - processed = PortsLibraryView(raw) + processed = PortLoadView(raw) processed['top'].polygon((7, 0), vertices=[[0, 0], [4, 0], [0, 4]]) copied: list[str] = [] diff --git a/masque/test/test_library.py b/masque/test/test_library.py index e939ae7..fdd49ef 100644 --- a/masque/test/test_library.py +++ b/masque/test/test_library.py @@ -2,7 +2,10 @@ import pytest from collections.abc import Mapping, MutableMapping from typing import cast, TYPE_CHECKING from numpy.testing import assert_allclose -from ..library import IBorrowing, INameView, IMaterializable, Library, LibraryView, LazyLibrary, OverlayLibrary, PortsLibraryView +from ..library import ( + IBorrowing, INameView, IMaterializable, LayerMappedView, Library, LibraryView, + LazyLibrary, OverlayLibrary, PortLoadView, + ) from ..pattern import Pattern from ..error import LibraryError, PatternError from ..ports import Port @@ -540,11 +543,11 @@ def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None: assert set(rename_map.values()) <= set(overlay) -def test_ports_view_detaches_already_materialized_overlay_pattern() -> None: +def test_port_load_view_detaches_already_materialized_overlay_pattern() -> None: overlay = OverlayLibrary() overlay.add_source(Library({"top": Pattern()})) raw = overlay["top"] - processed = PortsLibraryView( + processed = PortLoadView( overlay, ports={"top": {"P": Port((1, 2), 0)}}, ) @@ -557,6 +560,77 @@ def test_ports_view_detaches_already_materialized_overlay_pattern() -> None: assert not hasattr(processed, "close") +def test_layer_mapped_view_detaches_and_maps_shapes_and_labels() -> None: + import masque + + child = Pattern() + source_pattern = Pattern(ports={'P': Port((1, 2), 0, ptype='wire')}) + source_pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]]) + source_pattern.polygon('B', vertices=[[2, 0], [3, 0], [2, 1]]) + source_pattern.label('TEXT', string='port', offset=(1, 2)) + source_pattern.ref('child') + source_pattern.annotations['note'] = ['unchanged'] + source = Library({'child': child, 'top': source_pattern}) + mapped = LayerMappedView( + source, + lambda layer: {'A': 'DRAW', 'B': 'DRAW', 'TEXT': 'LABEL'}.get(layer, layer), + ) + + mapped_top = mapped['top'] + + assert mapped_top is not source_pattern + assert set(mapped_top.shapes) == {'DRAW'} + assert len(mapped_top.shapes['DRAW']) == 2 + assert set(mapped_top.labels) == {'LABEL'} + assert set(mapped_top.ports) == {'P'} + assert set(mapped_top.refs) == {'child'} + assert mapped_top.annotations == {'note': ['unchanged']} + assert set(source_pattern.shapes) == {'A', 'B'} + assert set(source_pattern.labels) == {'TEXT'} + assert mapped.child_graph() == source.child_graph() + assert mapped.source_order() == source.source_order() + assert masque.LayerMappedView is LayerMappedView + assert masque.PortLoadView is PortLoadView + assert not hasattr(masque, 'PortsLibraryView') + assert not hasattr(masque.library, 'PortsLibraryView') + + +def test_layer_mapped_view_materialization_and_copy_through() -> None: + pattern = Pattern() + pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]]) + source = Library({'top': pattern}) + mapped = LayerMappedView(source, lambda _layer: 'B') + passthrough = LayerMappedView(source, lambda _layer: 'B', copy_through=True) + + assert mapped.source_cell('top') is None + assert passthrough.source_cell('top') == (source, 'top') + transient = passthrough.materialize('top', persist=False) + assert set(transient.shapes) == {'B'} + assert passthrough.source_cell('top') == (source, 'top') + + persistent = passthrough['top'] + assert passthrough['top'] is persistent + assert passthrough.source_cell('top') is None + assert set(persistent.shapes) == {'B'} + + +def test_layer_mapped_view_composes_with_ports_and_overlay() -> None: + pattern = Pattern() + pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]]) + source = Library({'top': pattern}) + ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}}) + mapped = LayerMappedView(ports, lambda _layer: 'B') + overlay = OverlayLibrary() + overlay.add_source(mapped) + + top = overlay['top'] + + assert set(top.shapes) == {'B'} + assert set(top.ports) == {'P'} + assert mapped.borrowed_sources() == (ports,) + assert not pattern.ports + + def test_library_subtree() -> None: lib = Library() lib["a"] = Pattern() @@ -655,7 +729,7 @@ def test_library_materialization_and_borrowing_capabilities() -> None: overlay = OverlayLibrary() overlay.add_source(lazy) - ports = PortsLibraryView(overlay) + ports = PortLoadView(overlay) subtree = ports.subtree("top") assert isinstance(overlay, IMaterializable) @@ -676,7 +750,7 @@ def test_library_materialization_and_borrowing_capabilities() -> None: def test_borrowed_source_cell_tracks_persistent_materialization() -> None: source = Library({"top": Pattern()}) source.library_info = {"name": "not-forwarded"} # type: ignore[attr-defined] - processed = PortsLibraryView(source) + processed = PortLoadView(source) assert processed.source_cell("top") == (source, "top") assert not hasattr(processed, "library_info")