293 lines
9.4 KiB
Python
293 lines
9.4 KiB
Python
from pathlib import Path
|
|
|
|
import numpy
|
|
import pytest
|
|
|
|
pytest.importorskip('pyarrow')
|
|
|
|
from .. import Ref, Label
|
|
from ..library import Library
|
|
from ..pattern import Pattern
|
|
from ..repetition import Grid
|
|
from ..shapes import Path as MPath
|
|
from ..file import gdsii, gdsii_arrow
|
|
from ..file.gdsii_perf import write_fixture
|
|
|
|
|
|
if not gdsii_arrow.is_available():
|
|
pytest.skip('klamath_rs_ext shared library is not available', allow_module_level=True)
|
|
|
|
|
|
def _annotations_key(annotations: dict[str, list[object]] | None) -> tuple[tuple[str, tuple[object, ...]], ...] | None:
|
|
if not annotations:
|
|
return None
|
|
return tuple(sorted((key, tuple(values)) for key, values in annotations.items()))
|
|
|
|
|
|
def _coord_key(values: object) -> tuple[int, ...] | tuple[tuple[int, int], ...]:
|
|
arr = numpy.rint(numpy.asarray(values, dtype=float)).astype(int)
|
|
if arr.ndim == 1:
|
|
return tuple(arr.tolist())
|
|
return tuple(tuple(row.tolist()) for row in arr)
|
|
|
|
|
|
def _shape_key(shape: object, layer: tuple[int, int]) -> list[tuple[object, ...]]:
|
|
if isinstance(shape, MPath):
|
|
cap_extensions = None if shape.cap_extensions is None else _coord_key(shape.cap_extensions)
|
|
return [(
|
|
'path',
|
|
layer,
|
|
_coord_key(shape.vertices),
|
|
_coord_key(shape.offset),
|
|
int(round(float(shape.width))),
|
|
shape.cap.name,
|
|
cap_extensions,
|
|
_annotations_key(shape.annotations),
|
|
)]
|
|
|
|
keys = []
|
|
for poly in shape.to_polygons():
|
|
keys.append((
|
|
'polygon',
|
|
layer,
|
|
_coord_key(poly.vertices),
|
|
_coord_key(poly.offset),
|
|
_annotations_key(poly.annotations),
|
|
))
|
|
return keys
|
|
|
|
|
|
def _ref_key(target: str, ref: object) -> tuple[object, ...]:
|
|
repetition = None
|
|
if ref.repetition is not None:
|
|
repetition = (
|
|
_coord_key(ref.repetition.a_vector),
|
|
int(ref.repetition.a_count),
|
|
_coord_key(ref.repetition.b_vector),
|
|
int(ref.repetition.b_count),
|
|
)
|
|
return (
|
|
target,
|
|
_coord_key(ref.offset),
|
|
round(float(ref.rotation), 8),
|
|
round(float(ref.scale), 8),
|
|
bool(ref.mirrored),
|
|
repetition,
|
|
_annotations_key(ref.annotations),
|
|
)
|
|
|
|
|
|
def _label_key(layer: tuple[int, int], label: object) -> tuple[object, ...]:
|
|
return (
|
|
layer,
|
|
label.string,
|
|
_coord_key(label.offset),
|
|
_annotations_key(label.annotations),
|
|
)
|
|
|
|
|
|
def _pattern_summary(pattern: Pattern) -> dict[str, object]:
|
|
shape_keys: list[tuple[object, ...]] = []
|
|
for layer, shapes in pattern.shapes.items():
|
|
for shape in shapes:
|
|
shape_keys.extend(_shape_key(shape, layer))
|
|
|
|
ref_keys = [
|
|
_ref_key(target, ref)
|
|
for target, refs in pattern.refs.items()
|
|
for ref in refs
|
|
]
|
|
|
|
label_keys = [
|
|
_label_key(layer, label)
|
|
for layer, labels in pattern.labels.items()
|
|
for label in labels
|
|
]
|
|
|
|
return {
|
|
'shapes': sorted(shape_keys),
|
|
'refs': sorted(ref_keys),
|
|
'labels': sorted(label_keys),
|
|
}
|
|
|
|
|
|
def _library_summary(lib: Library) -> dict[str, dict[str, object]]:
|
|
return {name: _pattern_summary(pattern) for name, pattern in lib.items()}
|
|
|
|
|
|
def _make_arrow_test_library() -> Library:
|
|
lib = Library()
|
|
|
|
leaf = Pattern()
|
|
leaf.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]], annotations={'1': ['leaf-poly']})
|
|
leaf.polygon((2, 0), vertices=[[40, 0], [50, 0], [50, 10], [40, 10]])
|
|
leaf.polygon((1, 0), vertices=[[20, 0], [30, 0], [30, 10], [20, 10]])
|
|
leaf.polygon((1, 0), vertices=[[80, 0], [90, 0], [90, 10], [80, 10]])
|
|
leaf.polygon((2, 0), vertices=[[60, 0], [70, 0], [70, 10], [60, 10]], annotations={'18': ['leaf-poly-2']})
|
|
leaf.label((10, 0), string='LEAF', offset=(3, 4), annotations={'10': ['leaf-label']})
|
|
lib['leaf'] = leaf
|
|
|
|
child = Pattern()
|
|
child.path(
|
|
(2, 0),
|
|
vertices=[[0, 0], [15, 5], [30, 5]],
|
|
width=6,
|
|
cap=MPath.Cap.SquareCustom,
|
|
cap_extensions=(2, 4),
|
|
annotations={'2': ['child-path']},
|
|
)
|
|
child.label((11, 0), string='CHILD', offset=(7, 8), annotations={'11': ['child-label']})
|
|
child.ref('leaf', offset=(100, 200), rotation=numpy.pi / 2, mirrored=True, scale=1.25, annotations={'12': ['child-ref']})
|
|
lib['child'] = child
|
|
|
|
sibling = Pattern()
|
|
sibling.polygon((3, 0), vertices=[[0, 0], [5, 0], [5, 6], [0, 6]])
|
|
sibling.label((12, 0), string='SIB', offset=(1, 2), annotations={'13': ['sib-label']})
|
|
sibling.ref(
|
|
'leaf',
|
|
offset=(-50, 60),
|
|
repetition=Grid(a_vector=(20, 0), a_count=3, b_vector=(0, 30), b_count=2),
|
|
annotations={'14': ['sib-ref']},
|
|
)
|
|
lib['sibling'] = sibling
|
|
|
|
top = Pattern()
|
|
top.ref('child', offset=(500, 600), annotations={'15': ['top-child-ref']})
|
|
top.ref('sibling', offset=(-100, 50), rotation=numpy.pi, annotations={'16': ['top-sibling-ref']})
|
|
top.label((13, 0), string='TOP', offset=(0, 0), annotations={'17': ['top-label']})
|
|
lib['top'] = top
|
|
|
|
return lib
|
|
|
|
|
|
def test_gdsii_arrow_matches_gdsii_readfile(tmp_path: Path) -> None:
|
|
lib = _make_arrow_test_library()
|
|
gds_file = tmp_path / 'arrow_roundtrip.gds'
|
|
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
|
|
|
canonical_lib, canonical_info = gdsii.readfile(gds_file)
|
|
arrow_lib, arrow_info = gdsii_arrow.readfile(gds_file)
|
|
|
|
assert canonical_info == arrow_info
|
|
assert _library_summary(canonical_lib) == _library_summary(arrow_lib)
|
|
|
|
|
|
def test_gdsii_arrow_reads_small_perf_fixture(tmp_path: Path) -> None:
|
|
gds_file = tmp_path / 'many_cells_smoke.gds'
|
|
manifest = write_fixture(gds_file, preset='many_cells', scale=0.001)
|
|
|
|
lib, info = gdsii_arrow.readfile(gds_file)
|
|
|
|
assert info['name'] == manifest.library_name
|
|
assert len(lib) == manifest.cells
|
|
assert 'TOP' in lib
|
|
assert sum(len(refs) for refs in lib['TOP'].refs.values()) > 0
|
|
|
|
|
|
def test_gdsii_arrow_boundary_batch_schema(tmp_path: Path) -> None:
|
|
lib = _make_arrow_test_library()
|
|
gds_file = tmp_path / 'arrow_batches.gds'
|
|
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
|
|
|
libarr = gdsii_arrow._read_to_arrow(gds_file)[0]
|
|
cells = libarr['cells'].values
|
|
cell_ids = cells.field('id').to_numpy()
|
|
cell_names = libarr['cell_names'].as_py()
|
|
layer_table = [
|
|
((int(layer) >> 16) & 0xFFFF, int(layer) & 0xFFFF)
|
|
for layer in libarr['layers'].values.to_numpy()
|
|
]
|
|
|
|
leaf_index = next(ii for ii, cell_id in enumerate(cell_ids) if cell_names[cell_id] == 'leaf')
|
|
|
|
boundary_batches = cells.field('boundary_batches')[leaf_index].as_py()
|
|
boundary_props = cells.field('boundary_props')[leaf_index].as_py()
|
|
|
|
assert len(boundary_batches) == 2
|
|
assert len(boundary_props) == 2
|
|
|
|
batch_by_layer = {tuple(layer_table[entry['layer']]): entry for entry in boundary_batches}
|
|
assert batch_by_layer[(1, 0)]['vertex_offsets'] == [0, 4]
|
|
assert len(batch_by_layer[(1, 0)]['vertices']) == 16
|
|
assert batch_by_layer[(2, 0)]['vertex_offsets'] == [0]
|
|
assert len(batch_by_layer[(2, 0)]['vertices']) == 8
|
|
|
|
props_by_layer = {tuple(layer_table[entry['layer']]): entry for entry in boundary_props}
|
|
assert sorted(props_by_layer) == [(1, 0), (2, 0)]
|
|
assert props_by_layer[(1, 0)]['properties'][0]['value'] == 'leaf-poly'
|
|
assert props_by_layer[(2, 0)]['properties'][0]['value'] == 'leaf-poly-2'
|
|
|
|
|
|
def test_raw_ref_grid_label_constructors_match_public() -> None:
|
|
raw_grid = Grid._from_raw(
|
|
a_vector=numpy.array([20, 0]),
|
|
a_count=3,
|
|
b_vector=numpy.array([0, 30]),
|
|
b_count=2,
|
|
)
|
|
public_grid = Grid(a_vector=(20, 0), a_count=3, b_vector=(0, 30), b_count=2)
|
|
assert raw_grid == public_grid
|
|
|
|
raw_ref_empty = Ref._from_raw(
|
|
offset=numpy.array([100, 200]),
|
|
rotation=numpy.pi / 2,
|
|
mirrored=False,
|
|
scale=1.0,
|
|
repetition=None,
|
|
annotations=None,
|
|
)
|
|
public_ref_empty = Ref(
|
|
offset=(100, 200),
|
|
rotation=numpy.pi / 2,
|
|
mirrored=False,
|
|
scale=1.0,
|
|
repetition=None,
|
|
annotations=None,
|
|
)
|
|
assert raw_ref_empty.annotations is None
|
|
assert raw_ref_empty == public_ref_empty
|
|
|
|
raw_ref = Ref._from_raw(
|
|
offset=numpy.array([100, 200]),
|
|
rotation=numpy.pi / 2,
|
|
mirrored=True,
|
|
scale=1.25,
|
|
repetition=raw_grid,
|
|
annotations={'12': ['child-ref']},
|
|
)
|
|
public_ref = Ref(
|
|
offset=(100, 200),
|
|
rotation=numpy.pi / 2,
|
|
mirrored=True,
|
|
scale=1.25,
|
|
repetition=public_grid,
|
|
annotations={'12': ['child-ref']},
|
|
)
|
|
assert raw_ref == public_ref
|
|
assert numpy.array_equal(raw_ref.as_transforms(), public_ref.as_transforms())
|
|
|
|
raw_label_empty = Label._from_raw(
|
|
'LEAF',
|
|
offset=numpy.array([3, 4]),
|
|
annotations=None,
|
|
)
|
|
public_label_empty = Label(
|
|
'LEAF',
|
|
offset=(3, 4),
|
|
annotations=None,
|
|
)
|
|
assert raw_label_empty.annotations is None
|
|
assert raw_label_empty == public_label_empty
|
|
|
|
raw_label = Label._from_raw(
|
|
'LEAF',
|
|
offset=numpy.array([3, 4]),
|
|
annotations={'10': ['leaf-label']},
|
|
)
|
|
public_label = Label(
|
|
'LEAF',
|
|
offset=(3, 4),
|
|
annotations={'10': ['leaf-label']},
|
|
)
|
|
assert raw_label == public_label
|
|
assert numpy.array_equal(raw_label.get_bounds_single(), public_label.get_bounds_single())
|