334 lines
11 KiB
Python
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)])
|