[library / gdsii] cleanup using new library rework
This commit is contained in:
parent
ddb7742493
commit
7b589e4f44
14 changed files with 696 additions and 391 deletions
17
MIGRATION.md
17
MIGRATION.md
|
|
@ -606,6 +606,17 @@ unchanged until every borrowing view or overlay is finished, then close the
|
||||||
owning source explicitly. An eager `build(output='library')` result is detached
|
owning source explicitly. An eager `build(output='library')` result is detached
|
||||||
and does not need its sources afterward.
|
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.
|
||||||
|
|
||||||
|
Generic borrowing views no longer expose GDS-specific `raw_struct_bytes()`,
|
||||||
|
`can_copy_raw_struct()`, or forwarded `library_info` attributes. Keep the
|
||||||
|
owning GDS source or the metadata returned by `readfile()` when direct access is
|
||||||
|
needed. `IBorrowing.source_cell()` now provides format-neutral per-cell
|
||||||
|
provenance; GDS writers use it internally to preserve safe raw copy-through.
|
||||||
|
|
||||||
Read-only `subtree()` results are now borrowed lazy views rather than eager
|
Read-only `subtree()` results are now borrowed lazy views rather than eager
|
||||||
`LibraryView` snapshots. Creating one no longer loads its reachable patterns,
|
`LibraryView` snapshots. Creating one no longer loads its reachable patterns,
|
||||||
and the view preserves source ordering, hierarchy metadata, and lazy GDS
|
and the view preserves source ordering, hierarchy metadata, and lazy GDS
|
||||||
|
|
@ -681,8 +692,10 @@ erases the marker.
|
||||||
libraries and expose them through `borrowed_sources()`. Keep those sources open
|
libraries and expose them through `borrowed_sources()`. Keep those sources open
|
||||||
and unchanged for the lifetime of the borrowing view. Owner libraries such as
|
and unchanged for the lifetime of the borrowing view. Owner libraries such as
|
||||||
`LazyLibrary` and the lazy GDS readers are materializable but do not implement
|
`LazyLibrary` and the lazy GDS readers are materializable but do not implement
|
||||||
`IBorrowing`. GDS raw-structure copy-through remains a separate,
|
`IBorrowing`. Borrowing views may expose unchanged per-cell layout provenance
|
||||||
format-specific capability.
|
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
|
`LibraryBuilder`, `OverlayLibrary`, and `PortsLibraryView` are new additive
|
||||||
library implementations. `LibraryBuilder` supports declarative `@cell` recipes
|
library implementations. `LibraryBuilder` supports declarative `@cell` recipes
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from typing import Any
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
|
|
||||||
from masque import ILibrary, LibraryBuilder, Pather, Pattern, cell
|
from masque import ILibrary, LibraryBuilder, Pather, Pattern, PortsLibraryView, cell
|
||||||
from masque.file.gdsii import writefile
|
from masque.file.gdsii import writefile
|
||||||
from masque.file.gdsii.lazy import readfile
|
from masque.file.gdsii.lazy import readfile
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ def main() -> None:
|
||||||
# imported on first materialization, but the raw source remains untouched
|
# imported on first materialization, but the raw source remains untouched
|
||||||
# until we build the final library.
|
# until we build the final library.
|
||||||
gds_lib, _properties = readfile('circuit.gds')
|
gds_lib, _properties = readfile('circuit.gds')
|
||||||
builder.add_source(gds_lib.with_ports_from_data(layers=[(3, 0)], max_depth=1))
|
builder.add_source(PortsLibraryView(gds_lib, layers=[(3, 0)], max_depth=1))
|
||||||
|
|
||||||
print('Registered imported cells:\n' + pformat(list(gds_lib.keys())))
|
print('Registered imported cells:\n' + pformat(list(gds_lib.keys())))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ from ... import Pattern, Ref, PatternError, LibraryError, Label, Shape
|
||||||
from ...shapes import Polygon, Path, RectCollection
|
from ...shapes import Polygon, Path, RectCollection
|
||||||
from ...repetition import Grid
|
from ...repetition import Grid
|
||||||
from ...utils import layer_t, annotations_t
|
from ...utils import layer_t, annotations_t
|
||||||
from ...library import Library, ILibrary
|
from ...library import Library
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -58,6 +58,32 @@ def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
||||||
return numpy.rint(val).astype(numpy.int32)
|
return numpy.rint(val).astype(numpy.int32)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_header(
|
||||||
|
stream: IO[bytes],
|
||||||
|
meters_per_unit: float,
|
||||||
|
logical_units_per_unit: float,
|
||||||
|
library_name: str,
|
||||||
|
) -> None:
|
||||||
|
header = klamath.library.FileHeader(
|
||||||
|
name=library_name.encode('ASCII'),
|
||||||
|
user_units_per_db_unit=logical_units_per_unit,
|
||||||
|
meters_per_db_unit=meters_per_unit,
|
||||||
|
)
|
||||||
|
header.write(stream)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_pattern_struct(stream: IO[bytes], name: str, pat: Pattern) -> None:
|
||||||
|
elements: list[klamath.elements.Element] = []
|
||||||
|
elements += _shapes_to_elements(pat.shapes)
|
||||||
|
elements += _labels_to_texts(pat.labels)
|
||||||
|
elements += _mrefs_to_grefs(pat.refs)
|
||||||
|
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_footer(stream: IO[bytes]) -> None:
|
||||||
|
records.ENDLIB.write(stream, None)
|
||||||
|
|
||||||
|
|
||||||
def write(
|
def write(
|
||||||
library: Mapping[str, Pattern],
|
library: Mapping[str, Pattern],
|
||||||
stream: IO[bytes],
|
stream: IO[bytes],
|
||||||
|
|
@ -99,29 +125,36 @@ def write(
|
||||||
library_name: Library name written into the GDSII file.
|
library_name: Library name written into the GDSII file.
|
||||||
Default 'masque-klamath'.
|
Default 'masque-klamath'.
|
||||||
"""
|
"""
|
||||||
if not isinstance(library, ILibrary):
|
_write_header(stream, meters_per_unit, logical_units_per_unit, library_name)
|
||||||
if isinstance(library, dict):
|
|
||||||
library = Library(library)
|
|
||||||
else:
|
|
||||||
library = Library(dict(library))
|
|
||||||
|
|
||||||
# Create library
|
|
||||||
header = klamath.library.FileHeader(
|
|
||||||
name=library_name.encode('ASCII'),
|
|
||||||
user_units_per_db_unit=logical_units_per_unit,
|
|
||||||
meters_per_db_unit=meters_per_unit,
|
|
||||||
)
|
|
||||||
header.write(stream)
|
|
||||||
|
|
||||||
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
||||||
for name, pat in library.items():
|
for name, pat in library.items():
|
||||||
elements: list[klamath.elements.Element] = []
|
_write_pattern_struct(stream, name, pat)
|
||||||
elements += _shapes_to_elements(pat.shapes)
|
_write_footer(stream)
|
||||||
elements += _labels_to_texts(pat.labels)
|
|
||||||
elements += _mrefs_to_grefs(pat.refs)
|
|
||||||
|
|
||||||
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
|
||||||
records.ENDLIB.write(stream, None)
|
def _writefile_impl(
|
||||||
|
writer: Callable[..., None],
|
||||||
|
library: Any,
|
||||||
|
filename: str | pathlib.Path,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
path = pathlib.Path(filename)
|
||||||
|
|
||||||
|
with tmpfile(path) as base_stream:
|
||||||
|
streams: tuple[Any, ...] = (base_stream,)
|
||||||
|
if path.suffix == '.gz':
|
||||||
|
stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
||||||
|
streams = (stream,) + streams
|
||||||
|
else:
|
||||||
|
stream = base_stream
|
||||||
|
|
||||||
|
try:
|
||||||
|
writer(library, stream, *args, **kwargs)
|
||||||
|
finally:
|
||||||
|
for ss in streams:
|
||||||
|
ss.close()
|
||||||
|
|
||||||
|
|
||||||
def writefile(
|
def writefile(
|
||||||
|
|
@ -141,21 +174,7 @@ def writefile(
|
||||||
*args: passed to `write()`
|
*args: passed to `write()`
|
||||||
**kwargs: passed to `write()`
|
**kwargs: passed to `write()`
|
||||||
"""
|
"""
|
||||||
path = pathlib.Path(filename)
|
_writefile_impl(write, library, filename, *args, **kwargs)
|
||||||
|
|
||||||
with tmpfile(path) as base_stream:
|
|
||||||
streams: tuple[Any, ...] = (base_stream,)
|
|
||||||
if path.suffix == '.gz':
|
|
||||||
stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
|
||||||
streams = (stream,) + streams
|
|
||||||
else:
|
|
||||||
stream = base_stream
|
|
||||||
|
|
||||||
try:
|
|
||||||
write(library, stream, *args, **kwargs)
|
|
||||||
finally:
|
|
||||||
for ss in streams:
|
|
||||||
ss.close()
|
|
||||||
|
|
||||||
|
|
||||||
def readfile(
|
def readfile(
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import IO, TYPE_CHECKING, Any, cast
|
from typing import IO, TYPE_CHECKING, Any, cast
|
||||||
from collections import defaultdict
|
|
||||||
import gzip
|
import gzip
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -35,19 +34,15 @@ from ...error import LibraryError
|
||||||
from ...library import (
|
from ...library import (
|
||||||
ILibraryView,
|
ILibraryView,
|
||||||
IMaterializable,
|
IMaterializable,
|
||||||
PortsLibraryView,
|
|
||||||
dangling_mode_t,
|
|
||||||
)
|
)
|
||||||
from ...library.utils import _validate_dangling_mode
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterator, Sequence
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
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__)
|
||||||
|
|
@ -205,98 +200,19 @@ class GdsLibrarySource(ILibraryView, IMaterializable):
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
def _raw_children(self, name: str) -> set[str]:
|
def _raw_children(self, name: str) -> set[str]:
|
||||||
|
if name in self._cache:
|
||||||
|
return super()._raw_children(name)
|
||||||
return set(self._cells[name].children)
|
return set(self._cells[name].children)
|
||||||
|
|
||||||
def child_graph(
|
def _raw_ref_transforms(
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
parent: str,
|
||||||
) -> dict[str, set[str]]:
|
target: str,
|
||||||
_validate_dangling_mode(dangling)
|
) -> list[NDArray[numpy.float64]]:
|
||||||
graph: dict[str, set[str]] = {}
|
|
||||||
for name in self._cell_order:
|
|
||||||
if name in self._cache:
|
|
||||||
graph[name] = {child for child, refs in self._cache[name].refs.items() if child is not None and refs}
|
|
||||||
else:
|
|
||||||
graph[name] = self._raw_children(name)
|
|
||||||
|
|
||||||
existing = set(graph)
|
|
||||||
dangling_refs = set().union(*(children - existing for children in graph.values()))
|
|
||||||
if dangling == 'error':
|
|
||||||
if dangling_refs:
|
|
||||||
raise self._dangling_refs_error(cast('set[str]', dangling_refs), 'building child graph')
|
|
||||||
return graph
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return {name: {child for child in children if child in existing} for name, children in graph.items()}
|
|
||||||
|
|
||||||
for child in dangling_refs:
|
|
||||||
graph.setdefault(cast('str', child), set())
|
|
||||||
return graph
|
|
||||||
|
|
||||||
def subtree(
|
|
||||||
self,
|
|
||||||
tops: str | Sequence[str],
|
|
||||||
) -> ILibraryView:
|
|
||||||
return super().subtree(tops)
|
|
||||||
|
|
||||||
def with_ports_from_data(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
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(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
|
||||||
_validate_dangling_mode(dangling)
|
|
||||||
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
|
||||||
if parent_graph is None:
|
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
|
||||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
|
||||||
|
|
||||||
if name not in self:
|
|
||||||
if name not in parent_graph:
|
|
||||||
return instances
|
|
||||||
if dangling == 'error':
|
|
||||||
raise self._dangling_refs_error({name}, f'finding local refs for {name!r}')
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return instances
|
|
||||||
|
|
||||||
for parent in parent_graph.get(name, set()):
|
|
||||||
if parent in self._cache:
|
if parent in self._cache:
|
||||||
for ref in self._cache[parent].refs.get(name, []):
|
return super()._raw_ref_transforms(parent, target)
|
||||||
instances[parent].append(ref.as_transforms())
|
|
||||||
continue
|
|
||||||
pat = self.materialize(parent, persist=False)
|
pat = self.materialize(parent, persist=False)
|
||||||
for ref in pat.refs.get(name, []):
|
return [ref.as_transforms() for ref in pat.refs.get(target, ())]
|
||||||
instances[parent].append(ref.as_transforms())
|
|
||||||
return instances
|
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self._source.close()
|
self._source.close()
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ keeps its current behavior and performance profile.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import IO, TYPE_CHECKING, Any, cast
|
from typing import IO, TYPE_CHECKING, Any
|
||||||
from collections import defaultdict
|
|
||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
import mmap
|
import mmap
|
||||||
|
|
@ -23,19 +22,15 @@ from ...library import (
|
||||||
ILibraryView,
|
ILibraryView,
|
||||||
IMaterializable,
|
IMaterializable,
|
||||||
LibraryView,
|
LibraryView,
|
||||||
PortsLibraryView,
|
|
||||||
dangling_mode_t,
|
|
||||||
)
|
)
|
||||||
from ...library.utils import _validate_dangling_mode
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterator, 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__)
|
||||||
|
|
@ -304,6 +299,8 @@ class ArrowLibrary(ILibraryView, IMaterializable):
|
||||||
return self._materialize_patterns((name,), persist=persist)[name]
|
return self._materialize_patterns((name,), persist=persist)[name]
|
||||||
|
|
||||||
def _raw_children(self, name: str) -> set[str]:
|
def _raw_children(self, name: str) -> set[str]:
|
||||||
|
if name in self._cache:
|
||||||
|
return super()._raw_children(name)
|
||||||
return set(self._payload.cells[name].children)
|
return set(self._payload.cells[name].children)
|
||||||
|
|
||||||
def _collect_raw_transforms(self, cell: _CellScan, target_id: int) -> list[NDArray[numpy.float64]]:
|
def _collect_raw_transforms(self, cell: _CellScan, target_id: int) -> list[NDArray[numpy.float64]]:
|
||||||
|
|
@ -343,67 +340,6 @@ class ArrowLibrary(ILibraryView, IMaterializable):
|
||||||
))
|
))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def child_graph(
|
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, set[str]]:
|
|
||||||
_validate_dangling_mode(dangling)
|
|
||||||
graph: dict[str, set[str]] = {}
|
|
||||||
for name in self._payload.cell_order:
|
|
||||||
if name in self._cache:
|
|
||||||
graph[name] = {child for child, refs in self._cache[name].refs.items() if child is not None and refs}
|
|
||||||
else:
|
|
||||||
graph[name] = self._raw_children(name)
|
|
||||||
|
|
||||||
existing = set(graph)
|
|
||||||
dangling_refs = set().union(*(children - existing for children in graph.values()))
|
|
||||||
if dangling == 'error':
|
|
||||||
if dangling_refs:
|
|
||||||
raise self._dangling_refs_error(cast('set[str]', dangling_refs), 'building child graph')
|
|
||||||
return graph
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return {name: {child for child in children if child in existing} for name, children in graph.items()}
|
|
||||||
|
|
||||||
for child in dangling_refs:
|
|
||||||
graph.setdefault(cast('str', child), set())
|
|
||||||
return graph
|
|
||||||
|
|
||||||
def subtree(
|
|
||||||
self,
|
|
||||||
tops: str | Sequence[str],
|
|
||||||
) -> ILibraryView:
|
|
||||||
return super().subtree(tops)
|
|
||||||
|
|
||||||
def with_ports_from_data(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
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:
|
def close(self) -> None:
|
||||||
data = self._source.data
|
data = self._source.data
|
||||||
if isinstance(data, mmap.mmap):
|
if isinstance(data, mmap.mmap):
|
||||||
|
|
@ -418,39 +354,17 @@ class ArrowLibrary(ILibraryView, IMaterializable):
|
||||||
def __exit__(self, *_args: object) -> None:
|
def __exit__(self, *_args: object) -> None:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def find_refs_local(
|
def _raw_ref_transforms(
|
||||||
self,
|
self,
|
||||||
name: str,
|
parent: str,
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
target: str,
|
||||||
dangling: dangling_mode_t = 'error',
|
) -> list[NDArray[numpy.float64]]:
|
||||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
|
||||||
_validate_dangling_mode(dangling)
|
|
||||||
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
|
||||||
if parent_graph is None:
|
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
|
||||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
|
||||||
|
|
||||||
if name not in self:
|
|
||||||
if name not in parent_graph:
|
|
||||||
return instances
|
|
||||||
if dangling == 'error':
|
|
||||||
raise self._dangling_refs_error({name}, f'finding local refs for {name!r}')
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return instances
|
|
||||||
|
|
||||||
target_id = self._payload.cells.get(name)
|
|
||||||
for parent in parent_graph.get(name, set()):
|
|
||||||
if parent in self._cache:
|
if parent in self._cache:
|
||||||
for ref in self._cache[parent].refs.get(name, []):
|
return super()._raw_ref_transforms(parent, target)
|
||||||
instances[parent].append(ref.as_transforms())
|
target_cell = self._payload.cells.get(target)
|
||||||
continue
|
if target_cell is None or parent not in self._payload.cells:
|
||||||
|
return []
|
||||||
if target_id is None or parent not in self._payload.cells:
|
return self._collect_raw_transforms(self._payload.cells[parent], target_cell.cell_id)
|
||||||
continue
|
|
||||||
rows = self._collect_raw_transforms(self._payload.cells[parent], target_id.cell_id)
|
|
||||||
if rows:
|
|
||||||
instances[parent].extend(rows)
|
|
||||||
return instances
|
|
||||||
|
|
||||||
|
|
||||||
def readfile(
|
def readfile(
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,16 @@ or remapped.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import IO, TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
|
from typing import IO, TYPE_CHECKING, Any, Protocol, runtime_checkable
|
||||||
import gzip
|
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
|
||||||
|
|
||||||
import klamath
|
|
||||||
|
|
||||||
from . import klamath as gdsii
|
from . import klamath as gdsii
|
||||||
from ..utils import tmpfile
|
|
||||||
from ...error import LibraryError
|
from ...error import LibraryError
|
||||||
from ...library import IBorrowing, ILibraryView
|
from ...library import IBorrowing, ILibraryView, IMaterializable
|
||||||
from ...library.overlay import _materialize_detached_pattern
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
import pathlib
|
||||||
|
|
||||||
from ...pattern import Pattern
|
from ...pattern import Pattern
|
||||||
|
|
||||||
|
|
@ -40,13 +35,42 @@ class _GdsInfoSource(Protocol):
|
||||||
class _GdsRawCellSource(Protocol):
|
class _GdsRawCellSource(Protocol):
|
||||||
"""GDS-specific raw-structure copy-through capability."""
|
"""GDS-specific raw-structure copy-through capability."""
|
||||||
|
|
||||||
def source_order(self) -> tuple[str, ...]: ...
|
|
||||||
|
|
||||||
def can_copy_raw_struct(self, name: str) -> bool: ...
|
def can_copy_raw_struct(self, name: str) -> bool: ...
|
||||||
|
|
||||||
def raw_struct_bytes(self, name: str) -> bytes: ...
|
def raw_struct_bytes(self, name: str) -> bytes: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_raw_struct(
|
||||||
|
library: ILibraryView,
|
||||||
|
name: str,
|
||||||
|
) -> tuple[_GdsRawCellSource, str] | None:
|
||||||
|
"""Resolve an unchanged visible cell to a copyable raw GDS structure."""
|
||||||
|
current = library
|
||||||
|
current_name = name
|
||||||
|
seen: set[tuple[int, str]] = set()
|
||||||
|
while True:
|
||||||
|
key = (id(current), current_name)
|
||||||
|
if key in seen:
|
||||||
|
return None
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
if isinstance(current, _GdsRawCellSource):
|
||||||
|
if current.can_copy_raw_struct(current_name):
|
||||||
|
return current, current_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(current, IBorrowing):
|
||||||
|
return None
|
||||||
|
source_cell = current.source_cell(current_name)
|
||||||
|
if source_cell is None:
|
||||||
|
return None
|
||||||
|
source, source_name = source_cell
|
||||||
|
if source_name != current_name:
|
||||||
|
return None
|
||||||
|
current = source
|
||||||
|
current_name = source_name
|
||||||
|
|
||||||
|
|
||||||
def _get_write_info(
|
def _get_write_info(
|
||||||
library: Mapping[str, Pattern] | ILibraryView,
|
library: Mapping[str, Pattern] | ILibraryView,
|
||||||
*,
|
*,
|
||||||
|
|
@ -85,14 +109,6 @@ def _get_write_info(
|
||||||
return meters_per_unit, logical_units_per_unit, library_name
|
return meters_per_unit, logical_units_per_unit, library_name
|
||||||
|
|
||||||
|
|
||||||
def _write_pattern_struct(stream: IO[bytes], name: str, pat: Pattern) -> None:
|
|
||||||
elements: list[klamath.elements.Element] = []
|
|
||||||
elements += gdsii._shapes_to_elements(pat.shapes)
|
|
||||||
elements += gdsii._labels_to_texts(pat.labels)
|
|
||||||
elements += gdsii._mrefs_to_grefs(pat.refs)
|
|
||||||
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
|
||||||
|
|
||||||
|
|
||||||
def write(
|
def write(
|
||||||
library: Mapping[str, Pattern] | ILibraryView,
|
library: Mapping[str, Pattern] | ILibraryView,
|
||||||
stream: IO[bytes],
|
stream: IO[bytes],
|
||||||
|
|
@ -108,23 +124,24 @@ def write(
|
||||||
library_name=library_name,
|
library_name=library_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
header = klamath.library.FileHeader(
|
gdsii._write_header(stream, meters_per_unit, logical_units_per_unit, library_name)
|
||||||
name=library_name.encode('ASCII'),
|
|
||||||
user_units_per_db_unit=logical_units_per_unit,
|
|
||||||
meters_per_db_unit=meters_per_unit,
|
|
||||||
)
|
|
||||||
header.write(stream)
|
|
||||||
|
|
||||||
if isinstance(library, _GdsRawCellSource):
|
names = library.source_order() if isinstance(library, ILibraryView) else tuple(library)
|
||||||
for name in library.source_order():
|
for name in names:
|
||||||
if library.can_copy_raw_struct(name):
|
if isinstance(library, ILibraryView):
|
||||||
stream.write(library.raw_struct_bytes(name))
|
raw_struct = _resolve_raw_struct(library, name)
|
||||||
|
if raw_struct is not None:
|
||||||
|
raw_source, source_name = raw_struct
|
||||||
|
stream.write(raw_source.raw_struct_bytes(source_name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(library, IMaterializable):
|
||||||
|
pat = library.materialize(name, persist=False)
|
||||||
else:
|
else:
|
||||||
_write_pattern_struct(stream, name, _materialize_detached_pattern(cast('ILibraryView', library), name))
|
pat = library[name]
|
||||||
klamath.records.ENDLIB.write(stream, None)
|
gdsii._write_pattern_struct(stream, name, pat)
|
||||||
return
|
|
||||||
|
|
||||||
gdsii.write(cast('Mapping[str, Pattern]', library), stream, meters_per_unit, logical_units_per_unit, library_name)
|
gdsii._write_footer(stream)
|
||||||
|
|
||||||
|
|
||||||
def writefile(
|
def writefile(
|
||||||
|
|
@ -135,24 +152,11 @@ def writefile(
|
||||||
logical_units_per_unit: float | None = None,
|
logical_units_per_unit: float | None = None,
|
||||||
library_name: str | None = None,
|
library_name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
path = pathlib.Path(filename)
|
gdsii._writefile_impl(
|
||||||
|
write,
|
||||||
with tmpfile(path) as base_stream:
|
|
||||||
streams: tuple[Any, ...] = (base_stream,)
|
|
||||||
if path.suffix == '.gz':
|
|
||||||
stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
|
||||||
streams = (stream,) + streams
|
|
||||||
else:
|
|
||||||
stream = base_stream
|
|
||||||
|
|
||||||
try:
|
|
||||||
write(
|
|
||||||
library,
|
library,
|
||||||
stream,
|
filename,
|
||||||
meters_per_unit=meters_per_unit,
|
meters_per_unit=meters_per_unit,
|
||||||
logical_units_per_unit=logical_units_per_unit,
|
logical_units_per_unit=logical_units_per_unit,
|
||||||
library_name=library_name,
|
library_name=library_name,
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
for ss in streams:
|
|
||||||
ss.close()
|
|
||||||
|
|
|
||||||
|
|
@ -378,12 +378,20 @@ class ILibraryView(Mapping[str, 'Pattern'], INameView, metaclass=ABCMeta):
|
||||||
dangling_list = sorted(dangling)
|
dangling_list = sorted(dangling)
|
||||||
return LibraryError(f'Dangling refs found while {context}: ' + pformat(dangling_list))
|
return LibraryError(f'Dangling refs found while {context}: ' + pformat(dangling_list))
|
||||||
|
|
||||||
|
def _raw_children(self, name: str) -> set[str]:
|
||||||
|
"""Return direct, non-empty named references for one pattern."""
|
||||||
|
return {
|
||||||
|
child
|
||||||
|
for child, refs in self[name].refs.items()
|
||||||
|
if child is not None and refs
|
||||||
|
}
|
||||||
|
|
||||||
def _raw_child_graph(self) -> tuple[dict[str, set[str]], set[str]]:
|
def _raw_child_graph(self) -> tuple[dict[str, set[str]], set[str]]:
|
||||||
existing = set(self.keys())
|
existing = set(self.keys())
|
||||||
graph: dict[str, set[str]] = {}
|
graph: dict[str, set[str]] = {}
|
||||||
dangling: set[str] = set()
|
dangling: set[str] = set()
|
||||||
for name, pat in self.items():
|
for name in self:
|
||||||
children = {child for child, refs in pat.refs.items() if child is not None and refs}
|
children = self._raw_children(name)
|
||||||
graph[name] = children
|
graph[name] = children
|
||||||
dangling |= children - existing
|
dangling |= children - existing
|
||||||
return graph, dangling
|
return graph, dangling
|
||||||
|
|
@ -626,11 +634,18 @@ class ILibraryView(Mapping[str, 'Pattern'], INameView, metaclass=ABCMeta):
|
||||||
for parent in parent_graph.get(name, set()):
|
for parent in parent_graph.get(name, set()):
|
||||||
if parent not in self: # parent_graph may be a for a superset of self
|
if parent not in self: # parent_graph may be a for a superset of self
|
||||||
continue
|
continue
|
||||||
for ref in self[parent].refs[name]:
|
instances[parent].extend(self._raw_ref_transforms(parent, name))
|
||||||
instances[parent].append(ref.as_transforms())
|
|
||||||
|
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
|
def _raw_ref_transforms(
|
||||||
|
self,
|
||||||
|
parent: str,
|
||||||
|
target: str,
|
||||||
|
) -> list[NDArray[numpy.float64]]:
|
||||||
|
"""Return local transforms from one parent to one target."""
|
||||||
|
return [ref.as_transforms() for ref in self[parent].refs.get(target, ())]
|
||||||
|
|
||||||
def find_refs_global(
|
def find_refs_global(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,12 @@ class IBorrowing(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
"""Return the source views directly borrowed by this library."""
|
"""Return the source views directly borrowed by this library."""
|
||||||
|
|
||||||
|
def source_cell(self, name: str) -> tuple[ILibraryView, str] | None: # noqa: ARG002
|
||||||
|
"""
|
||||||
|
Return a direct source cell with unchanged layout data, if available.
|
||||||
|
|
||||||
|
The source may use a different name, which is returned alongside it.
|
||||||
|
Port metadata may differ because ports are not layout-file content.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import TYPE_CHECKING, Any, Self, cast
|
from typing import TYPE_CHECKING, Self
|
||||||
|
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
from .base import ILibrary, ILibraryView
|
from .base import ILibrary, ILibraryView
|
||||||
|
|
@ -70,9 +70,6 @@ class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
name: set(child_graph.get(name, set()))
|
name: set(child_graph.get(name, set()))
|
||||||
for name in self._order
|
for name in self._order
|
||||||
}
|
}
|
||||||
if hasattr(source, 'library_info'):
|
|
||||||
self.library_info = cast('dict[str, Any]', source.library_info)
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
if key not in self._names:
|
if key not in self._names:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
@ -90,6 +87,11 @@ class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
return (self._source,)
|
return (self._source,)
|
||||||
|
|
||||||
|
def source_cell(self, name: str) -> tuple[ILibraryView, str] | None:
|
||||||
|
if name not in self._names:
|
||||||
|
return None
|
||||||
|
return self._source, name
|
||||||
|
|
||||||
def source_order(self) -> tuple[str, ...]:
|
def source_order(self) -> tuple[str, ...]:
|
||||||
return self._order
|
return self._order
|
||||||
|
|
||||||
|
|
@ -134,21 +136,6 @@ class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
refs = self._source.find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
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}
|
return {parent: transforms for parent, transforms in refs.items() if parent in self._names}
|
||||||
|
|
||||||
def raw_struct_bytes(self, name: str) -> bytes:
|
|
||||||
if name not in self._names:
|
|
||||||
raise KeyError(name)
|
|
||||||
reader = getattr(self._source, 'raw_struct_bytes', None)
|
|
||||||
if not callable(reader):
|
|
||||||
raise TypeError('raw_struct_bytes')
|
|
||||||
return cast('bytes', reader(name))
|
|
||||||
|
|
||||||
def can_copy_raw_struct(self, name: str) -> bool:
|
|
||||||
if name not in self._names:
|
|
||||||
return False
|
|
||||||
can_copy = getattr(self._source, 'can_copy_raw_struct', None)
|
|
||||||
return bool(callable(can_copy) and can_copy(name))
|
|
||||||
|
|
||||||
|
|
||||||
class Library(ILibrary):
|
class Library(ILibrary):
|
||||||
"""
|
"""
|
||||||
Default implementation for a writeable library.
|
Default implementation for a writeable library.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Literal, Self, cast
|
from typing import TYPE_CHECKING, Literal, Self, cast
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
|
|
@ -53,9 +53,9 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
The view borrows its source: callers must keep the source open for the
|
The view borrows its source: callers must keep the source open for the
|
||||||
lifetime of the view and close the source themselves.
|
lifetime of the view and close the source themselves.
|
||||||
|
|
||||||
Graph queries, source ordering, and copy-through capabilities are delegated
|
Graph queries and source ordering are delegated to the wrapped source,
|
||||||
to the wrapped source whenever possible, while `__getitem__` and
|
while `source_cell()` exposes unchanged layout provenance and `__getitem__`
|
||||||
`materialize_many()` return port-imported patterns.
|
and `materialize_many()` return port-imported patterns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -79,9 +79,6 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
self._replace = replace
|
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'):
|
|
||||||
self.library_info = cast('dict[str, Any]', source.library_info)
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
return self.materialize(key, persist=True)
|
return self.materialize(key, persist=True)
|
||||||
|
|
||||||
|
|
@ -139,6 +136,11 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
return (self._source,)
|
return (self._source,)
|
||||||
|
|
||||||
|
def source_cell(self, name: str) -> tuple[ILibraryView, str] | None:
|
||||||
|
if name not in self._source or name in self._cache:
|
||||||
|
return None
|
||||||
|
return self._source, name
|
||||||
|
|
||||||
def child_graph(
|
def child_graph(
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
|
|
@ -158,21 +160,6 @@ class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
return cast('dict[str, list[NDArray[numpy.float64]]]', finder(name, parent_graph=parent_graph, dangling=dangling))
|
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)
|
return super().find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
||||||
|
|
||||||
def raw_struct_bytes(self, name: str) -> bytes:
|
|
||||||
reader = getattr(self._source, 'raw_struct_bytes', None)
|
|
||||||
if not callable(reader):
|
|
||||||
raise TypeError('raw_struct_bytes')
|
|
||||||
return cast('bytes', reader(name))
|
|
||||||
|
|
||||||
def can_copy_raw_struct(self, name: str) -> bool:
|
|
||||||
if name in self._cache:
|
|
||||||
return False
|
|
||||||
can_copy = getattr(self._source, 'can_copy_raw_struct', None)
|
|
||||||
if not callable(can_copy):
|
|
||||||
return False
|
|
||||||
return bool(can_copy(name))
|
|
||||||
|
|
||||||
|
|
||||||
class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
"""
|
"""
|
||||||
Mutable overlay over one or more source libraries.
|
Mutable overlay over one or more source libraries.
|
||||||
|
|
@ -434,23 +421,12 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
return tuple(layer.library for layer in self._layers)
|
return tuple(layer.library for layer in self._layers)
|
||||||
|
|
||||||
def can_copy_raw_struct(self, name: str) -> bool:
|
def source_cell(self, name: str) -> tuple[ILibraryView, str] | None:
|
||||||
entry = self._entries.get(name)
|
|
||||||
if not isinstance(entry, _SourceEntry) or name != entry.source_name:
|
|
||||||
return False
|
|
||||||
layer = self._layers[entry.layer_index]
|
|
||||||
can_copy = getattr(layer.library, 'can_copy_raw_struct', None)
|
|
||||||
if not callable(can_copy) or not can_copy(entry.source_name):
|
|
||||||
return False
|
|
||||||
children = layer.child_graph.get(entry.source_name, set())
|
|
||||||
return all(self._effective_target(layer, child) == child for child in children)
|
|
||||||
|
|
||||||
def raw_struct_bytes(self, name: str) -> bytes:
|
|
||||||
entry = self._entries.get(name)
|
entry = self._entries.get(name)
|
||||||
if not isinstance(entry, _SourceEntry):
|
if not isinstance(entry, _SourceEntry):
|
||||||
raise TypeError('raw_struct_bytes')
|
return None
|
||||||
layer = self._layers[entry.layer_index]
|
layer = self._layers[entry.layer_index]
|
||||||
reader = getattr(layer.library, 'raw_struct_bytes', None)
|
children = layer.child_graph.get(entry.source_name, set())
|
||||||
if not callable(reader):
|
if any(self._effective_target(layer, child) != child for child in children):
|
||||||
raise TypeError('raw_struct_bytes')
|
return None
|
||||||
return cast('bytes', reader(entry.source_name))
|
return layer.library, entry.source_name
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from ..file.gdsii import lazy as gdsii_lazy
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
|
from ..library import IBorrowing, IMaterializable, LazyLibrary, Library, OverlayLibrary, PortsLibraryView
|
||||||
|
|
||||||
|
|
||||||
def _make_lazy_port_library() -> Library:
|
def _make_lazy_port_library() -> Library:
|
||||||
|
|
@ -39,6 +39,43 @@ def test_gdsii_lazy_write_ignores_non_mapping_library_info() -> None:
|
||||||
gdsii_lazy.write(lib, io.BytesIO())
|
gdsii_lazy.write(lib, io.BytesIO())
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_write_plain_library_matches_eager_writer() -> None:
|
||||||
|
lib = _make_lazy_port_library()
|
||||||
|
eager_stream = io.BytesIO()
|
||||||
|
lazy_stream = io.BytesIO()
|
||||||
|
|
||||||
|
gdsii.write(lib, eager_stream, meters_per_unit=1e-9, library_name='writer-match')
|
||||||
|
gdsii_lazy.write(
|
||||||
|
lib,
|
||||||
|
lazy_stream,
|
||||||
|
meters_per_unit=1e-9,
|
||||||
|
logical_units_per_unit=1,
|
||||||
|
library_name='writer-match',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert lazy_stream.getvalue() == eager_stream.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_write_materializes_transiently() -> None:
|
||||||
|
lib = LazyLibrary()
|
||||||
|
lib['top'] = Pattern()
|
||||||
|
|
||||||
|
stream = io.BytesIO()
|
||||||
|
gdsii_lazy.write(
|
||||||
|
lib,
|
||||||
|
stream,
|
||||||
|
meters_per_unit=1e-9,
|
||||||
|
logical_units_per_unit=1,
|
||||||
|
library_name='transient',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not lib.cache
|
||||||
|
stream.seek(0)
|
||||||
|
roundtrip, info = gdsii.read(stream)
|
||||||
|
assert set(roundtrip) == {'top'}
|
||||||
|
assert info['name'] == 'transient'
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
|
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'lazy_source.gds'
|
gds_file = tmp_path / 'lazy_source.gds'
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
@ -73,6 +110,17 @@ def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_pat
|
||||||
assert set(lib._cache) == {'child'}
|
assert set(lib._cache) == {'child'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_graph_hooks_observe_cached_edits(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'lazy_cached_graph.gds'
|
||||||
|
gdsii.writefile(_make_lazy_port_library(), gds_file, meters_per_unit=1e-9)
|
||||||
|
|
||||||
|
lib, _ = gdsii_lazy.readfile(gds_file)
|
||||||
|
del lib['child'].refs['leaf']
|
||||||
|
|
||||||
|
assert lib.child_graph(dangling='ignore')['child'] == set()
|
||||||
|
assert lib.find_refs_local('leaf') == {}
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path: Path) -> None:
|
def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'lazy_subtree_source.gds'
|
gds_file = tmp_path / 'lazy_subtree_source.gds'
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
@ -87,6 +135,7 @@ def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path
|
||||||
assert isinstance(subtree, IMaterializable)
|
assert isinstance(subtree, IMaterializable)
|
||||||
assert isinstance(subtree, IBorrowing)
|
assert isinstance(subtree, IBorrowing)
|
||||||
assert subtree.source_order() == ('leaf', 'child', 'top')
|
assert subtree.source_order() == ('leaf', 'child', 'top')
|
||||||
|
assert not hasattr(subtree, 'library_info')
|
||||||
assert not raw._cache
|
assert not raw._cache
|
||||||
|
|
||||||
out_file = tmp_path / 'lazy_subtree_out.gds'
|
out_file = tmp_path / 'lazy_subtree_out.gds'
|
||||||
|
|
@ -127,7 +176,8 @@ def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> No
|
||||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-ports')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-ports')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
processed = raw.with_ports_from_data(layers=[(10, 0)], max_depth=2)
|
processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2)
|
||||||
|
assert not hasattr(processed, 'library_info')
|
||||||
|
|
||||||
top = processed['top']
|
top = processed['top']
|
||||||
assert set(top.ports) == {'A'}
|
assert set(top.ports) == {'A'}
|
||||||
|
|
@ -145,7 +195,7 @@ def test_gdsii_lazy_ports_view_detaches_previously_cached_source(tmp_path: Path)
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
raw_top = raw['top']
|
raw_top = raw['top']
|
||||||
processed = raw.with_port_overrides({
|
processed = PortsLibraryView(raw, ports={
|
||||||
'top': {
|
'top': {
|
||||||
'P': Port((1, 2), rotation=0, ptype='wire'),
|
'P': Port((1, 2), rotation=0, ptype='wire'),
|
||||||
},
|
},
|
||||||
|
|
@ -166,7 +216,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')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overrides')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
processed = raw.with_port_overrides({
|
processed = PortsLibraryView(raw, ports={
|
||||||
'top': {
|
'top': {
|
||||||
'P': Port((1, 2), rotation=0, ptype='wire'),
|
'P': Port((1, 2), rotation=0, ptype='wire'),
|
||||||
},
|
},
|
||||||
|
|
@ -189,7 +239,8 @@ 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')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-override-extracted')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
processed = raw.with_ports_from_data(
|
processed = PortsLibraryView(
|
||||||
|
raw,
|
||||||
layers=[(10, 0)],
|
layers=[(10, 0)],
|
||||||
max_depth=2,
|
max_depth=2,
|
||||||
ports={
|
ports={
|
||||||
|
|
@ -217,7 +268,8 @@ 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')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-replace-ports')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
processed = raw.with_ports_from_data(
|
processed = PortsLibraryView(
|
||||||
|
raw,
|
||||||
layers=[(10, 0)],
|
layers=[(10, 0)],
|
||||||
max_depth=2,
|
max_depth=2,
|
||||||
ports={
|
ports={
|
||||||
|
|
@ -240,7 +292,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')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overlay')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
processed = raw.with_ports_from_data(layers=[(10, 0)], max_depth=2)
|
processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2)
|
||||||
|
|
||||||
overlay = OverlayLibrary()
|
overlay = OverlayLibrary()
|
||||||
overlay.add_source(processed)
|
overlay.add_source(processed)
|
||||||
|
|
@ -258,7 +310,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')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-overlay-override')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
processed = raw.with_port_overrides({
|
processed = PortsLibraryView(raw, ports={
|
||||||
'top': {
|
'top': {
|
||||||
'P': Port((1, 2), rotation=0, ptype='wire'),
|
'P': Port((1, 2), rotation=0, ptype='wire'),
|
||||||
},
|
},
|
||||||
|
|
@ -310,7 +362,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')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-roundtrip')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
processed = raw.with_ports_from_data(layers=[(10, 0)], max_depth=2)
|
processed = PortsLibraryView(raw, layers=[(10, 0)], max_depth=2)
|
||||||
|
|
||||||
out_file = tmp_path / 'lazy_roundtrip_out.gds'
|
out_file = tmp_path / 'lazy_roundtrip_out.gds'
|
||||||
gdsii_lazy.writefile(processed, out_file)
|
gdsii_lazy.writefile(processed, out_file)
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ import pytest
|
||||||
pytest.importorskip('pyarrow')
|
pytest.importorskip('pyarrow')
|
||||||
|
|
||||||
from .. import PatternError
|
from .. import PatternError
|
||||||
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
|
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary, PortsLibraryView
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..repetition import Grid
|
from ..repetition import Grid
|
||||||
from ..file import gdsii
|
from ..file import gdsii
|
||||||
from ..file.gdsii import lazy_arrow as gdsii_lazy_arrow
|
from ..file.gdsii import lazy_arrow as gdsii_lazy_arrow
|
||||||
|
from ..file.gdsii import lazy_write as gdsii_lazy_write
|
||||||
from ..file.gdsii.perf import write_fixture
|
from ..file.gdsii.perf import write_fixture
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -160,6 +161,17 @@ def test_gdsii_lazy_arrow_local_and_global_refs(tmp_path: Path) -> None:
|
||||||
assert global_refs[('top', 'mid', 'leaf')].shape[0] == 5
|
assert global_refs[('top', 'mid', 'leaf')].shape[0] == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_arrow_graph_hooks_observe_cached_edits(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'lazy_arrow_cached_graph.gds'
|
||||||
|
gdsii.writefile(_make_small_library(), gds_file, meters_per_unit=1e-9)
|
||||||
|
|
||||||
|
lib, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||||
|
del lib['mid'].refs['leaf']
|
||||||
|
|
||||||
|
assert lib.child_graph(dangling='ignore')['mid'] == set()
|
||||||
|
assert lib.find_refs_local('leaf') == {}
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_arrow_ref_queries_match_eager_reader(tmp_path: Path) -> None:
|
def test_gdsii_lazy_arrow_ref_queries_match_eager_reader(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'complex_refs.gds'
|
gds_file = tmp_path / 'complex_refs.gds'
|
||||||
src = _make_complex_ref_library()
|
src = _make_complex_ref_library()
|
||||||
|
|
@ -223,18 +235,66 @@ def test_gdsii_lazy_arrow_untouched_write_is_copy_through(tmp_path: Path) -> Non
|
||||||
assert out_file.read_bytes() == gds_file.read_bytes()
|
assert out_file.read_bytes() == gds_file.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy(tmp_path: Path) -> None:
|
def test_gdsii_raw_copy_resolves_generic_borrowing_views(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
gds_file = tmp_path / 'provenance_source.gds'
|
||||||
|
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)
|
||||||
|
subtree = ports.subtree('top')
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(subtree)
|
||||||
|
|
||||||
|
copied: list[str] = []
|
||||||
|
raw_reader = raw.raw_struct_bytes
|
||||||
|
|
||||||
|
def record_raw_read(name: str) -> bytes:
|
||||||
|
copied.append(name)
|
||||||
|
return raw_reader(name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(raw, 'raw_struct_bytes', record_raw_read)
|
||||||
|
out_file = tmp_path / 'provenance_out.gds'
|
||||||
|
gdsii_lazy_arrow.writefile(overlay, out_file)
|
||||||
|
|
||||||
|
assert copied == ['leaf', 'mid', 'top']
|
||||||
|
assert out_file.read_bytes() == gds_file.read_bytes()
|
||||||
|
|
||||||
|
renamed = OverlayLibrary()
|
||||||
|
renamed.add_source(raw)
|
||||||
|
renamed.rename('top', 'renamed_top')
|
||||||
|
assert gdsii_lazy_write._resolve_raw_struct(renamed, 'renamed_top') is None
|
||||||
|
|
||||||
|
remapped = OverlayLibrary()
|
||||||
|
remapped.add_source(raw)
|
||||||
|
remapped.rename('leaf', 'renamed_leaf', move_references=True)
|
||||||
|
assert gdsii_lazy_write._resolve_raw_struct(remapped, 'mid') is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
gds_file = tmp_path / 'processed_edit_source.gds'
|
gds_file = tmp_path / 'processed_edit_source.gds'
|
||||||
src = _make_small_library()
|
src = _make_small_library()
|
||||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='processed-edit')
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='processed-edit')
|
||||||
|
|
||||||
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||||
processed = raw.with_port_overrides({})
|
processed = PortsLibraryView(raw)
|
||||||
processed['top'].polygon((7, 0), vertices=[[0, 0], [4, 0], [0, 4]])
|
processed['top'].polygon((7, 0), vertices=[[0, 0], [4, 0], [0, 4]])
|
||||||
|
|
||||||
|
copied: list[str] = []
|
||||||
|
raw_reader = raw.raw_struct_bytes
|
||||||
|
|
||||||
|
def record_raw_read(name: str) -> bytes:
|
||||||
|
copied.append(name)
|
||||||
|
return raw_reader(name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(raw, 'raw_struct_bytes', record_raw_read)
|
||||||
|
|
||||||
out_file = tmp_path / 'processed_edit_out.gds'
|
out_file = tmp_path / 'processed_edit_out.gds'
|
||||||
gdsii_lazy_arrow.writefile(processed, out_file)
|
gdsii_lazy_arrow.writefile(processed, out_file)
|
||||||
|
|
||||||
|
assert 'top' not in copied
|
||||||
roundtrip, _ = gdsii.readfile(out_file)
|
roundtrip, _ = gdsii.readfile(out_file)
|
||||||
assert len(roundtrip['top'].shapes[(7, 0)]) == 1
|
assert len(roundtrip['top'].shapes[(7, 0)]) == 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
from collections.abc import Iterator, Mapping, MutableMapping
|
from collections.abc import Mapping, MutableMapping
|
||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from numpy.testing import assert_allclose
|
from numpy.testing import assert_allclose
|
||||||
from ..library import IBorrowing, INameView, IMaterializable, ILibraryView, Library, LibraryView, LazyLibrary, OverlayLibrary, PortsLibraryView
|
from ..library import IBorrowing, INameView, IMaterializable, Library, LibraryView, LazyLibrary, OverlayLibrary, PortsLibraryView
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..error import LibraryError, PatternError
|
from ..error import LibraryError, PatternError
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
|
|
@ -673,41 +673,43 @@ def test_library_materialization_and_borrowing_capabilities() -> None:
|
||||||
assert not isinstance(plain_view, IMaterializable | IBorrowing)
|
assert not isinstance(plain_view, IMaterializable | IBorrowing)
|
||||||
|
|
||||||
|
|
||||||
class _RawCopyView(ILibraryView):
|
def test_borrowed_source_cell_tracks_persistent_materialization() -> None:
|
||||||
def __init__(self) -> None:
|
source = Library({"top": Pattern()})
|
||||||
self.mapping = {"top": Pattern()}
|
source.library_info = {"name": "not-forwarded"} # type: ignore[attr-defined]
|
||||||
|
processed = PortsLibraryView(source)
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
assert processed.source_cell("top") == (source, "top")
|
||||||
return self.mapping[key]
|
assert not hasattr(processed, "library_info")
|
||||||
|
assert not hasattr(processed, "raw_struct_bytes")
|
||||||
def __iter__(self) -> Iterator[str]:
|
|
||||||
return iter(self.mapping)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.mapping)
|
|
||||||
|
|
||||||
def __contains__(self, key: object) -> bool:
|
|
||||||
return key in self.mapping
|
|
||||||
|
|
||||||
def raw_struct_bytes(self, name: str) -> bytes:
|
|
||||||
return name.encode()
|
|
||||||
|
|
||||||
def can_copy_raw_struct(self, name: str) -> bool:
|
|
||||||
return name in self.mapping
|
|
||||||
|
|
||||||
|
|
||||||
def test_ports_view_raw_copy_eligibility_tracks_persistent_materialization() -> None:
|
|
||||||
processed = PortsLibraryView(_RawCopyView())
|
|
||||||
|
|
||||||
assert processed.can_copy_raw_struct("top")
|
|
||||||
_ = processed.materialize_many(("top",), persist=False)
|
_ = processed.materialize_many(("top",), persist=False)
|
||||||
assert processed.can_copy_raw_struct("top")
|
assert processed.source_cell("top") == (source, "top")
|
||||||
|
|
||||||
subtree = processed.subtree("top")
|
subtree = processed.subtree("top")
|
||||||
assert subtree.can_copy_raw_struct("top")
|
assert subtree.source_cell("top") == (processed, "top")
|
||||||
|
assert not hasattr(subtree, "library_info")
|
||||||
_ = subtree["top"]
|
_ = subtree["top"]
|
||||||
assert not processed.can_copy_raw_struct("top")
|
assert processed.source_cell("top") is None
|
||||||
assert not subtree.can_copy_raw_struct("top")
|
assert subtree.source_cell("top") == (processed, "top")
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_source_cell_tracks_names_references_and_materialization() -> None:
|
||||||
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||||
|
source["parent"].ref("leaf")
|
||||||
|
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(source)
|
||||||
|
assert overlay.source_cell("parent") == (source, "parent")
|
||||||
|
|
||||||
|
overlay.rename("parent", "renamed_parent")
|
||||||
|
assert overlay.source_cell("renamed_parent") == (source, "parent")
|
||||||
|
|
||||||
|
overlay.rename("leaf", "renamed_leaf", move_references=True)
|
||||||
|
assert overlay.source_cell("renamed_parent") is None
|
||||||
|
|
||||||
|
materialized = OverlayLibrary()
|
||||||
|
materialized.add_source(source)
|
||||||
|
_ = materialized["parent"]
|
||||||
|
assert materialized.source_cell("parent") is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("materialize_parent", [False, True])
|
@pytest.mark.parametrize("materialize_parent", [False, True])
|
||||||
|
|
|
||||||
338
masque/test/test_tool_contract.py
Normal file
338
masque/test/test_tool_contract.py
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import pytest
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from masque.builder import (
|
||||||
|
AutoTool,
|
||||||
|
BendOffer,
|
||||||
|
PathTool,
|
||||||
|
PrimitiveKind,
|
||||||
|
PrimitiveOffer,
|
||||||
|
RenderStep,
|
||||||
|
SOffer,
|
||||||
|
StraightOffer,
|
||||||
|
Tool,
|
||||||
|
ToolContractCase,
|
||||||
|
ToolContractError,
|
||||||
|
UOffer,
|
||||||
|
validate_tool_contract,
|
||||||
|
)
|
||||||
|
from masque.error import BuildError
|
||||||
|
from masque.library import ILibrary, Library, SINGLE_USE_PREFIX
|
||||||
|
from masque.pattern import Pattern
|
||||||
|
from masque.ports import Port
|
||||||
|
|
||||||
|
|
||||||
|
def make_straight(length: float, *, ptype: str = 'wire') -> Pattern:
|
||||||
|
return Pattern(ports={
|
||||||
|
'A': Port((0, 0), 0, ptype=ptype),
|
||||||
|
'B': Port((length, 0), pi, ptype=ptype),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyTool(Tool):
|
||||||
|
def primitive_offers(
|
||||||
|
self,
|
||||||
|
kind: PrimitiveKind,
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> tuple[PrimitiveOffer, ...]:
|
||||||
|
_ = kind, in_ptype, out_ptype, kwargs
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def render(
|
||||||
|
self,
|
||||||
|
batch: tuple[RenderStep, ...],
|
||||||
|
*,
|
||||||
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
) -> ILibrary:
|
||||||
|
_ = batch
|
||||||
|
tree, pattern = Library.mktree('empty_tool')
|
||||||
|
pattern.add_port_pair(names=port_names)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('offer', 'kind', 'opcode'),
|
||||||
|
[
|
||||||
|
(StraightOffer('wire', 'wire'), 'straight', 'L'),
|
||||||
|
(BendOffer('wire', 'wire'), 'bend', 'L'),
|
||||||
|
(SOffer('wire', 'wire'), 's', 'S'),
|
||||||
|
(UOffer('wire', 'wire'), 'u', 'U'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_offer_kind_is_canonical_and_opcode_is_derived(
|
||||||
|
offer: StraightOffer | BendOffer | SOffer | UOffer,
|
||||||
|
kind: str,
|
||||||
|
opcode: str,
|
||||||
|
) -> None:
|
||||||
|
assert offer.kind == kind
|
||||||
|
assert offer.opcode == opcode
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_step_stores_kind_and_derives_opcode() -> None:
|
||||||
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||||
|
port = Port((0, 0), 0, ptype='wire')
|
||||||
|
step = RenderStep('straight', tool, port, port, None)
|
||||||
|
plug = RenderStep('plug', None, port, port, None)
|
||||||
|
|
||||||
|
assert step.kind == 'straight'
|
||||||
|
assert step.opcode == 'L'
|
||||||
|
assert step.transformed(numpy.zeros(2), 0, numpy.zeros(2)).kind == 'straight'
|
||||||
|
assert step.mirrored(0).kind == 'straight'
|
||||||
|
assert plug.opcode == 'P'
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match='Unrecognized RenderStep kind'):
|
||||||
|
RenderStep('L', tool, port, port, None) # type: ignore[arg-type]
|
||||||
|
with pytest.raises(BuildError, match='requires tool=None'):
|
||||||
|
RenderStep('plug', tool, port, port, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_standard_offer_endpoint_callback_runs_once_per_solver_evaluation() -> None:
|
||||||
|
endpoint_calls: list[float] = []
|
||||||
|
received_endpoints: list[Port] = []
|
||||||
|
|
||||||
|
def endpoint(length: float) -> Port:
|
||||||
|
endpoint_calls.append(length)
|
||||||
|
return Port((length, 0), pi, ptype='wire')
|
||||||
|
|
||||||
|
def cost(length: float, out_port: Port) -> float:
|
||||||
|
_ = length
|
||||||
|
received_endpoints.append(out_port)
|
||||||
|
return out_port.x
|
||||||
|
|
||||||
|
class OneOfferTool(EmptyTool):
|
||||||
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
if kind != 'straight':
|
||||||
|
return ()
|
||||||
|
return (StraightOffer(
|
||||||
|
in_ptype='wire',
|
||||||
|
out_ptype='wire',
|
||||||
|
cost=cost,
|
||||||
|
endpoint_planner=endpoint,
|
||||||
|
commit_planner=lambda length: length,
|
||||||
|
),)
|
||||||
|
|
||||||
|
from masque.builder import Pather
|
||||||
|
|
||||||
|
pather = Pather(
|
||||||
|
Library(),
|
||||||
|
ports={'A': Port((0, 0), 0, ptype='wire')},
|
||||||
|
tools=OneOfferTool(),
|
||||||
|
render='deferred',
|
||||||
|
)
|
||||||
|
pather.straight('A', 5)
|
||||||
|
|
||||||
|
assert endpoint_calls.count(5) == 1
|
||||||
|
assert len(received_endpoints) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_cost_at_override_remains_authoritative() -> None:
|
||||||
|
calls: list[float] = []
|
||||||
|
|
||||||
|
class CustomCostOffer(StraightOffer):
|
||||||
|
def cost_at(self, parameter: float) -> float:
|
||||||
|
calls.append(parameter)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
offer = CustomCostOffer(
|
||||||
|
in_ptype='wire',
|
||||||
|
out_ptype='wire',
|
||||||
|
endpoint_planner=lambda length: Port((length, 0), pi, ptype='wire'),
|
||||||
|
commit_planner=lambda length: length,
|
||||||
|
)
|
||||||
|
|
||||||
|
class CustomCostTool(EmptyTool):
|
||||||
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
return (offer,) if kind == 'straight' else ()
|
||||||
|
|
||||||
|
from masque.builder import Pather
|
||||||
|
|
||||||
|
pather = Pather(
|
||||||
|
Library(),
|
||||||
|
ports={'A': Port((0, 0), 0, ptype='wire')},
|
||||||
|
tools=CustomCostTool(),
|
||||||
|
render='deferred',
|
||||||
|
)
|
||||||
|
pather.straight('A', 5)
|
||||||
|
assert 5 in calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_solver_rejects_offer_kind_mismatch() -> None:
|
||||||
|
class WrongKindTool(EmptyTool):
|
||||||
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
if kind != 'straight':
|
||||||
|
return ()
|
||||||
|
return (SOffer.generated(
|
||||||
|
'wire',
|
||||||
|
lambda jog: Port((1, jog), pi, ptype='wire'),
|
||||||
|
lambda jog: jog,
|
||||||
|
),)
|
||||||
|
|
||||||
|
from masque.builder import Pather
|
||||||
|
|
||||||
|
pather = Pather(
|
||||||
|
Library(),
|
||||||
|
ports={'A': Port((0, 0), 0, ptype='wire')},
|
||||||
|
tools=WrongKindTool(),
|
||||||
|
render='deferred',
|
||||||
|
)
|
||||||
|
with pytest.raises(ToolContractError, match='returned.*s.*offer'):
|
||||||
|
pather.straight('A', 5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_accepts_pathtool() -> None:
|
||||||
|
tool = PathTool(layer='M1', width=2, ptype='wire')
|
||||||
|
validate_tool_contract(tool, (
|
||||||
|
ToolContractCase('straight', in_ptype='wire', check_bbox=True),
|
||||||
|
ToolContractCase('bend', in_ptype='wire', ccw=False, check_bbox=True),
|
||||||
|
ToolContractCase('bend', in_ptype='wire', ccw=True, check_bbox=True),
|
||||||
|
ToolContractCase('s', in_ptype='wire', check_bbox=True),
|
||||||
|
ToolContractCase('u', in_ptype='wire', require_offers=False),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_accepts_autotool_with_explicit_probe() -> None:
|
||||||
|
tool = AutoTool().add_straight(
|
||||||
|
make_straight,
|
||||||
|
'wire',
|
||||||
|
'A',
|
||||||
|
length_range=(1, 10),
|
||||||
|
)
|
||||||
|
validate_tool_contract(tool, (
|
||||||
|
ToolContractCase('straight', in_ptype='wire', probe_parameters=(7,)),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_empty_offer_policy() -> None:
|
||||||
|
tool = EmptyTool()
|
||||||
|
validate_tool_contract(tool, (ToolContractCase('u', require_offers=False),))
|
||||||
|
|
||||||
|
with pytest.raises(ExceptionGroup) as exc_info:
|
||||||
|
validate_tool_contract(tool, (ToolContractCase('u'),))
|
||||||
|
assert all(isinstance(err, ToolContractError) for err in exc_info.value.exceptions)
|
||||||
|
assert any('no offers' in str(err) for err in exc_info.value.exceptions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_rejects_unmatched_explicit_probe() -> None:
|
||||||
|
tool = AutoTool().add_straight(make_straight, 'wire', 'A', length_range=(1, 5))
|
||||||
|
|
||||||
|
with pytest.raises(ExceptionGroup, match='Tool contract validation') as exc_info:
|
||||||
|
validate_tool_contract(tool, (
|
||||||
|
ToolContractCase('straight', in_ptype='wire', probe_parameters=(10,)),
|
||||||
|
))
|
||||||
|
assert any('outside every discovered offer domain' in str(err) for err in exc_info.value.exceptions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_aggregates_independent_violations() -> None:
|
||||||
|
class BrokenTool(EmptyTool):
|
||||||
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
if kind != 'straight':
|
||||||
|
return ()
|
||||||
|
return (StraightOffer(
|
||||||
|
in_ptype='wire',
|
||||||
|
out_ptype='wire',
|
||||||
|
endpoint_planner=lambda length: Port((length + 1, 0), 0, ptype='wrong'),
|
||||||
|
commit_planner=lambda length: length,
|
||||||
|
),)
|
||||||
|
|
||||||
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
return Library()
|
||||||
|
|
||||||
|
with pytest.raises(ExceptionGroup) as exc_info:
|
||||||
|
validate_tool_contract(BrokenTool(), (
|
||||||
|
ToolContractCase('straight', in_ptype='wire', label='broken straight'),
|
||||||
|
))
|
||||||
|
|
||||||
|
errors = exc_info.value.exceptions
|
||||||
|
assert len(errors) > 1
|
||||||
|
assert all(isinstance(err, ToolContractError) for err in errors)
|
||||||
|
assert all('broken straight' in str(err) for err in errors)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_detects_repeated_discovery_changes() -> None:
|
||||||
|
class ChangingTool(EmptyTool):
|
||||||
|
calls = 0
|
||||||
|
|
||||||
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
if kind != 'straight':
|
||||||
|
return ()
|
||||||
|
self.calls += 1
|
||||||
|
shift = float(self.calls - 1)
|
||||||
|
return (StraightOffer(
|
||||||
|
in_ptype='wire',
|
||||||
|
out_ptype='wire',
|
||||||
|
endpoint_planner=lambda length: Port((length + shift, 0), pi, ptype='wire'),
|
||||||
|
commit_planner=lambda length: length,
|
||||||
|
),)
|
||||||
|
|
||||||
|
with pytest.raises(ExceptionGroup) as exc_info:
|
||||||
|
validate_tool_contract(ChangingTool(), (
|
||||||
|
ToolContractCase('straight', in_ptype='wire'),
|
||||||
|
))
|
||||||
|
assert any('changed after repeated discovery' in str(err) for err in exc_info.value.exceptions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_checks_render_ports_and_single_use_refs() -> None:
|
||||||
|
class BrokenRenderTool(EmptyTool):
|
||||||
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
return (StraightOffer.generated('wire', lambda length: length),) if kind == 'straight' else ()
|
||||||
|
|
||||||
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
tree = Library()
|
||||||
|
pattern = Pattern(ports={port_names[0]: Port((0, 0), 0, ptype='wire')})
|
||||||
|
pattern.ref(SINGLE_USE_PREFIX + 'missing')
|
||||||
|
tree['top'] = pattern
|
||||||
|
return tree
|
||||||
|
|
||||||
|
with pytest.raises(ExceptionGroup) as exc_info:
|
||||||
|
validate_tool_contract(BrokenRenderTool(), (
|
||||||
|
ToolContractCase('straight', in_ptype='wire'),
|
||||||
|
))
|
||||||
|
messages = [str(err) for err in exc_info.value.exceptions]
|
||||||
|
assert any('missing single-use refs' in message for message in messages)
|
||||||
|
assert any('missing ports' in message for message in messages)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_contract_bbox_is_opt_in() -> None:
|
||||||
|
tool = AutoTool().add_straight(make_straight, 'wire', 'A', length_range=(1, 5))
|
||||||
|
validate_tool_contract(tool, (ToolContractCase('straight', in_ptype='wire'),))
|
||||||
|
|
||||||
|
# AutoTool supplies bbox support, so use a minimal custom offer without it.
|
||||||
|
class NoBBoxTool(EmptyTool):
|
||||||
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
return (StraightOffer.generated('wire', lambda length: length),) if kind == 'straight' else ()
|
||||||
|
|
||||||
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||||
|
length = batch[0].data
|
||||||
|
tree = Library()
|
||||||
|
tree['top'] = Pattern(ports={
|
||||||
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
||||||
|
port_names[1]: Port((length, 0), pi, ptype='wire'),
|
||||||
|
})
|
||||||
|
return tree
|
||||||
|
|
||||||
|
validate_tool_contract(NoBBoxTool(), (ToolContractCase('straight', in_ptype='wire'),))
|
||||||
|
with pytest.raises(ExceptionGroup) as exc_info:
|
||||||
|
validate_tool_contract(NoBBoxTool(), (
|
||||||
|
ToolContractCase('straight', in_ptype='wire', check_bbox=True),
|
||||||
|
))
|
||||||
|
assert any('bbox_at()' in str(err) for err in exc_info.value.exceptions)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'kwargs',
|
||||||
|
[
|
||||||
|
{'kind': 'straight', 'ccw': True},
|
||||||
|
{'kind': 'bend'},
|
||||||
|
{'kind': 'straight', 'probe_parameters': (numpy.inf,)},
|
||||||
|
{'kind': 'straight', 'tool_options': {'ccw': True}},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_tool_contract_case_validates_configuration(kwargs: dict[str, Any]) -> None:
|
||||||
|
with pytest.raises(ValueError, match='ccw|requires|finite|reserved'):
|
||||||
|
ToolContractCase(**kwargs) # type: ignore[arg-type]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue