[PortsLibraryView] now accepts overrides

This commit is contained in:
Jan Petykiewicz 2026-07-09 19:47:26 -07:00
commit 7083edc7ca
4 changed files with 160 additions and 12 deletions

View file

@ -42,11 +42,12 @@ from ...library import (
from ...utils import apply_transforms from ...utils import apply_transforms
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterator, Sequence from collections.abc import Iterator, Mapping, Sequence
from numpy.typing import NDArray from numpy.typing import NDArray
from ...pattern import Pattern from ...pattern import Pattern
from ...ports import Port
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -284,12 +285,28 @@ class GdsLibrarySource(ILibraryView):
layers: Sequence[tuple[int, int] | int], layers: Sequence[tuple[int, int] | int],
max_depth: int = 0, max_depth: int = 0,
skip_subcells: bool = True, skip_subcells: bool = True,
ports: Mapping[str, Mapping[str, Port]] | None = None,
replace: bool = False,
) -> PortsLibraryView: ) -> PortsLibraryView:
return PortsLibraryView( return PortsLibraryView(
self, self,
layers=layers, layers=layers,
max_depth=max_depth, max_depth=max_depth,
skip_subcells=skip_subcells, 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( def find_refs_local(

View file

@ -28,12 +28,13 @@ from ...library import (
from ...utils import apply_transforms from ...utils import apply_transforms
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterator, Sequence from collections.abc import Iterator, Mapping, Sequence
from numpy.typing import NDArray from numpy.typing import NDArray
import pyarrow import pyarrow
from ...pattern import Pattern from ...pattern import Pattern
from ...ports import Port
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -407,12 +408,28 @@ class ArrowLibrary(ILibraryView):
layers: Sequence[tuple[int, int] | int], layers: Sequence[tuple[int, int] | int],
max_depth: int = 0, max_depth: int = 0,
skip_subcells: bool = True, skip_subcells: bool = True,
ports: Mapping[str, Mapping[str, Port]] | None = None,
replace: bool = False,
) -> PortsLibraryView: ) -> PortsLibraryView:
return PortsLibraryView( return PortsLibraryView(
self, self,
layers=layers, layers=layers,
max_depth=max_depth, max_depth=max_depth,
skip_subcells=skip_subcells, 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: def close(self) -> None:

View file

@ -20,6 +20,8 @@ if TYPE_CHECKING:
from numpy.typing import NDArray from numpy.typing import NDArray
from ..ports import Port
@dataclass @dataclass
class _SourceLayer: class _SourceLayer:
@ -47,7 +49,7 @@ def _materialize_detached_pattern(view: ILibraryView, name: str) -> Pattern:
class PortsLibraryView(ILibraryView): 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 The wrapped source remains untouched; this view owns a separate processed
cache so direct-copy workflows can continue to use the raw source view. cache so direct-copy workflows can continue to use the raw source view.
@ -61,14 +63,21 @@ class PortsLibraryView(ILibraryView):
self, self,
source: ILibraryView, source: ILibraryView,
*, *,
layers: Sequence[layer_t], layers: Sequence[layer_t] = (),
max_depth: int = 0, max_depth: int = 0,
skip_subcells: bool = True, skip_subcells: bool = True,
ports: Mapping[str, Mapping[str, Port]] | None = None,
replace: bool = False,
) -> None: ) -> None:
self._source = source self._source = source
self._layers = tuple(layers) self._layers = tuple(layers)
self._max_depth = max_depth self._max_depth = max_depth
self._skip_subcells = skip_subcells 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._cache: dict[str, Pattern] = {}
self._lookups_in_progress: list[str] = [] self._lookups_in_progress: list[str] = []
if hasattr(source, 'library_info'): if hasattr(source, 'library_info'):
@ -103,14 +112,21 @@ class PortsLibraryView(ILibraryView):
self._lookups_in_progress.append(name) self._lookups_in_progress.append(name)
try: try:
pat = _materialize_detached_pattern(self._source, name) pat = _materialize_detached_pattern(self._source, name)
pat = data_to_ports( if self._layers:
layers=self._layers, pat = data_to_ports(
library=self, layers=self._layers,
pattern=pat, library=self,
name=name, pattern=pat,
max_depth=self._max_depth, name=name,
skip_subcells=self._skip_subcells, 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: finally:
self._lookups_in_progress.pop() self._lookups_in_progress.pop()

View file

@ -7,6 +7,7 @@ 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 ..pattern import Pattern from ..pattern import Pattern
from ..ports import Port
from ..library import Library, OverlayLibrary 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 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: def test_gdsii_lazy_overlay_add_source_stays_lazy_for_processed_view(tmp_path: Path) -> None:
gds_file = tmp_path / 'lazy_overlay.gds' gds_file = tmp_path / 'lazy_overlay.gds'
src = _make_lazy_port_library() 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'} 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: def test_gdsii_lazy_overlay_add_source_can_rename_every_source_cell() -> None:
src = _make_lazy_port_library() src = _make_lazy_port_library()
overlay = OverlayLibrary() overlay = OverlayLibrary()