masque/masque/test/test_gdsii_lazy_arrow.py

334 lines
11 KiB
Python

from pathlib import Path
import subprocess
import sys
import textwrap
import klamath
import numpy
import pytest
pytest.importorskip('pyarrow')
from .. import PatternError
from ..library import Library
from ..pattern import Pattern
from ..repetition import Grid
from ..file import gdsii, gdsii_lazy_arrow
from ..file.gdsii_perf import write_fixture
if not gdsii_lazy_arrow.is_available():
pytest.skip('klamath_rs_ext shared library is not available', allow_module_level=True)
def _make_small_library() -> Library:
lib = Library()
leaf = Pattern()
leaf.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 5], [0, 5]])
lib['leaf'] = leaf
mid = Pattern()
mid.ref('leaf', offset=(10, 20))
mid.ref('leaf', offset=(40, 0), repetition=Grid(a_vector=(12, 0), a_count=2, b_vector=(0, 9), b_count=2))
lib['mid'] = mid
top = Pattern()
top.ref('mid', offset=(100, 200))
lib['top'] = top
return lib
def _make_complex_ref_library() -> Library:
lib = Library()
leaf = Pattern()
leaf.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]])
lib['leaf'] = leaf
child = Pattern()
child.ref('leaf', offset=(100, 200), rotation=numpy.pi / 2, mirrored=True, scale=1.25)
lib['child'] = child
sibling = Pattern()
sibling.ref(
'leaf',
offset=(-50, 60),
repetition=Grid(a_vector=(20, 0), a_count=3, b_vector=(0, 30), b_count=2),
)
lib['sibling'] = sibling
fanout = Pattern()
fanout.ref('leaf', offset=(0, 0))
fanout.ref('child', offset=(10, 0), mirrored=True, rotation=numpy.pi / 6, scale=1.1)
fanout.ref('leaf', offset=(30, 0), repetition=Grid(a_vector=(5, 0), a_count=2, b_vector=(0, 7), b_count=3))
fanout.ref(
'child',
offset=(40, 0),
mirrored=True,
rotation=numpy.pi / 4,
scale=1.2,
repetition=Grid(a_vector=(9, 0), a_count=2, b_vector=(0, 11), b_count=2),
)
lib['fanout'] = fanout
top = Pattern()
top.ref('child', offset=(500, 600))
top.ref('sibling', offset=(-100, 50), rotation=numpy.pi)
top.ref('fanout', offset=(250, -75))
lib['top'] = top
return lib
def _write_invalid_path_type_fixture(path: Path) -> None:
with path.open('wb') as stream:
header = klamath.library.FileHeader(
name=b'test',
user_units_per_db_unit=1.0,
meters_per_db_unit=1e-9,
)
header.write(stream)
elem = klamath.elements.Path(
layer=(1, 0),
path_type=3,
width=10,
extension=(0, 0),
xy=numpy.array([[0, 0], [10, 0]], dtype=numpy.int32),
properties={},
)
klamath.library.write_struct(stream, name=b'top', elements=[elem])
klamath.records.ENDLIB.write(stream, None)
def _transform_rows_key(values: numpy.ndarray) -> tuple[tuple[object, ...], ...]:
arr = numpy.asarray(values, dtype=float)
arr = numpy.atleast_2d(arr)
rows = [
(
round(float(row[0]), 8),
round(float(row[1]), 8),
round(float(row[2]), 8),
bool(int(round(float(row[3])))),
round(float(row[4]), 8),
)
for row in arr
]
return tuple(sorted(rows))
def _local_refs_key(refs: dict[str, list[numpy.ndarray]]) -> dict[str, tuple[tuple[object, ...], ...]]:
return {
parent: _transform_rows_key(numpy.concatenate(transforms))
for parent, transforms in refs.items()
}
def _global_refs_key(refs: dict[tuple[str, ...], numpy.ndarray]) -> dict[tuple[str, ...], tuple[tuple[object, ...], ...]]:
return {
path: _transform_rows_key(transforms)
for path, transforms in refs.items()
}
def test_gdsii_lazy_arrow_loads_perf_fixture(tmp_path: Path) -> None:
gds_file = tmp_path / 'many_cells_lazy.gds'
manifest = write_fixture(gds_file, preset='many_cells', scale=0.001)
lib, info = gdsii_lazy_arrow.readfile(gds_file)
assert info['name'] == manifest.library_name
assert len(lib) == manifest.cells
assert lib.top() == 'TOP'
assert 'TOP' in lib.child_graph(dangling='ignore')
def test_gdsii_lazy_arrow_local_and_global_refs(tmp_path: Path) -> None:
gds_file = tmp_path / 'refs.gds'
src = _make_small_library()
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='lazy-refs')
lib, _ = gdsii_lazy_arrow.readfile(gds_file)
local = lib.find_refs_local('leaf')
assert set(local) == {'mid'}
assert sum(arr.shape[0] for arr in local['mid']) == 5
global_refs = lib.find_refs_global('leaf')
assert {path for path in global_refs} == {('top', 'mid', 'leaf')}
assert global_refs[('top', 'mid', 'leaf')].shape[0] == 5
def test_gdsii_lazy_arrow_ref_queries_match_eager_reader(tmp_path: Path) -> None:
gds_file = tmp_path / 'complex_refs.gds'
src = _make_complex_ref_library()
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='lazy-complex-refs')
eager, _ = gdsii.readfile(gds_file)
lazy, _ = gdsii_lazy_arrow.readfile(gds_file)
for name in ('leaf', 'child'):
assert _local_refs_key(lazy.find_refs_local(name)) == _local_refs_key(eager.find_refs_local(name))
assert _global_refs_key(lazy.find_refs_global(name)) == _global_refs_key(eager.find_refs_global(name))
def test_gdsii_lazy_arrow_invalid_input_raises_klamath_error(tmp_path: Path) -> None:
gds_file = tmp_path / 'invalid.gds'
gds_file.write_bytes(b'not-a-gds')
script = textwrap.dedent(f"""
from masque.file import gdsii_lazy_arrow
try:
gdsii_lazy_arrow.readfile({str(gds_file)!r})
except Exception as exc:
print(type(exc).__module__)
print(type(exc).__qualname__)
print(exc)
else:
raise SystemExit('expected gdsii_lazy_arrow.readfile() to fail')
""")
result = subprocess.run([sys.executable, '-c', script], capture_output=True, text=True, check=False)
assert result.returncode == 0, result.stderr
assert 'klamath.basic' in result.stdout
assert 'KlamathError' in result.stdout
def test_gdsii_lazy_arrow_invalid_path_type_raises_pattern_error(tmp_path: Path) -> None:
gds_file = tmp_path / 'invalid_path_type.gds'
_write_invalid_path_type_fixture(gds_file)
lib, _ = gdsii_lazy_arrow.readfile(gds_file)
with pytest.raises(PatternError, match='Unrecognized path type: 3'):
lib['top']
def test_gdsii_lazy_arrow_untouched_write_is_copy_through(tmp_path: Path) -> None:
gds_file = tmp_path / 'copy_source.gds'
src = _make_small_library()
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='copy-through')
lib, info = gdsii_lazy_arrow.readfile(gds_file)
out_file = tmp_path / 'copy_out.gds'
gdsii_lazy_arrow.writefile(
lib,
out_file,
meters_per_unit=info['meters_per_unit'],
logical_units_per_unit=info['logical_units_per_unit'],
library_name=info['name'],
)
assert out_file.read_bytes() == gds_file.read_bytes()
def test_gdsii_lazy_arrow_gzipped_copy_through(tmp_path: Path) -> None:
gds_file = tmp_path / 'copy_source.gds.gz'
src = _make_small_library()
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='copy-through-gz')
lib, info = gdsii_lazy_arrow.readfile(gds_file)
out_file = tmp_path / 'copy_out.gds.gz'
gdsii_lazy_arrow.writefile(
lib,
out_file,
meters_per_unit=info['meters_per_unit'],
logical_units_per_unit=info['logical_units_per_unit'],
library_name=info['name'],
)
assert out_file.read_bytes() == gds_file.read_bytes()
def test_gdsii_lazy_overlay_merge_and_write(tmp_path: Path) -> None:
base_a = Library()
leaf_a = Pattern()
leaf_a.polygon((1, 0), vertices=[[0, 0], [8, 0], [8, 8], [0, 8]])
base_a['leaf'] = leaf_a
top_a = Pattern()
top_a.ref('leaf', offset=(0, 0))
base_a['top_a'] = top_a
base_b = Library()
leaf_b = Pattern()
leaf_b.polygon((2, 0), vertices=[[0, 0], [5, 0], [5, 5], [0, 5]])
base_b['leaf'] = leaf_b
top_b = Pattern()
top_b.ref('leaf', offset=(20, 30))
base_b['top_b'] = top_b
gds_a = tmp_path / 'a.gds'
gds_b = tmp_path / 'b.gds'
gdsii.writefile(base_a, gds_a, meters_per_unit=1e-9, library_name='overlay')
gdsii.writefile(base_b, gds_b, meters_per_unit=1e-9, library_name='overlay')
lib_a, _ = gdsii_lazy_arrow.readfile(gds_a)
lib_b, _ = gdsii_lazy_arrow.readfile(gds_b)
overlay = gdsii_lazy_arrow.OverlayLibrary()
overlay.add_source(lib_a)
rename_map = overlay.add_source(lib_b, rename_theirs=lambda lib, name: lib.get_name(name))
renamed_leaf = rename_map['leaf']
assert rename_map == {'leaf': renamed_leaf}
assert renamed_leaf != 'leaf'
assert len(lib_a._cache) == 0
assert len(lib_b._cache) == 0
overlay.move_references('leaf', renamed_leaf)
out_file = tmp_path / 'overlay_out.gds'
gdsii_lazy_arrow.writefile(overlay, out_file)
roundtrip, _ = gdsii.readfile(out_file)
assert set(roundtrip.keys()) == {'leaf', renamed_leaf, 'top_a', 'top_b'}
assert 'top_b' in roundtrip
assert list(roundtrip['top_b'].refs.keys()) == [renamed_leaf]
def test_gdsii_writer_accepts_overlay_library(tmp_path: Path) -> None:
gds_file = tmp_path / 'overlay_source.gds'
src = _make_small_library()
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='overlay-src')
lib, info = gdsii_lazy_arrow.readfile(gds_file)
overlay = gdsii_lazy_arrow.OverlayLibrary()
overlay.add_source(lib)
overlay.rename('leaf', 'leaf_copy', move_references=True)
out_file = tmp_path / 'overlay_via_eager_writer.gds'
gdsii.writefile(
overlay,
out_file,
meters_per_unit=info['meters_per_unit'],
logical_units_per_unit=info['logical_units_per_unit'],
library_name=info['name'],
)
roundtrip, _ = gdsii.readfile(out_file)
assert set(roundtrip.keys()) == {'leaf_copy', 'mid', 'top'}
assert list(roundtrip['mid'].refs.keys()) == ['leaf_copy']
def test_svg_writer_uses_detached_materialized_copy(tmp_path: Path) -> None:
pytest.importorskip('svgwrite')
from ..file import svg
from ..shapes import Path as MPath
gds_file = tmp_path / 'svg_source.gds'
src = _make_small_library()
src['top'].path((3, 0), vertices=[[0, 0], [0, 20]], width=4)
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='svg-src')
lib, _ = gdsii_lazy_arrow.readfile(gds_file)
top_pat = lib['top']
assert list(top_pat.refs.keys()) == ['mid']
assert any(isinstance(shape, MPath) for shape in top_pat.shapes[(3, 0)])
svg_path = tmp_path / 'lazy.svg'
svg.writefile(lib, 'top', str(svg_path))
assert svg_path.exists()
assert list(top_pat.refs.keys()) == ['mid']
assert any(isinstance(shape, MPath) for shape in top_pat.shapes[(3, 0)])